-
Notifications
You must be signed in to change notification settings - Fork 182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Serialization hooks #126
Serialization hooks #126
Changes from 17 commits
403c209
a3fd0c1
60822fd
afd4040
53387fd
739cbad
a73b30c
1e61b1f
944f165
ab1ca4d
1cfd11d
ddcca26
95db373
4802bbd
0d32f77
00f2378
9cf69c8
b26774f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
{ | ||
"plugins": ["transform-async-to-generator"], | ||
"presets": ["es2015"] | ||
"presets": ["es2015", "stage-1"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,6 +161,84 @@ No changes are required to your actions, react-chrome-redux automatically adds t | |
|
||
`react-chrome-redux` supports `onMessageExternal` which is fired when a message is sent from another extension, app, or website. By default, if `externally_connectable` is not declared in your extension's manifest, all extensions or apps will be able to send messages to your extension, but no websites will be able to. You can follow [this](https://developer.chrome.com/extensions/manifest/externally_connectable) to address your needs appropriately. | ||
|
||
## Custom Serialization | ||
|
||
You may wish to implement custom serialization and deserialization logic for communication between the background store and your proxy store(s). Chrome's message passing (which is used to implement this library) automatically serializes messages when they are sent and deserializes them when they are received. In the case that you have non-JSON-ifiable information in your Redux state, like a circular reference or a `Date` object, you will lose information between the background store and the proxy store(s). To manage this, both `wrapStore` and `Store` accept `serializer` and `deserializer` options. These should be functions that take a single parameter, the payload of a message, and return a serialized and deserialized form, respectively. The `serializer` function will be called every time a message is sent, and the `deserializer` function will be called every time a message is received. Note that, in addition to state updates, action creators being passed from your content script(s) to your background page will be serialized and deserialized as well. | ||
|
||
### Example | ||
For example, consider the following `state` in your background page: | ||
|
||
```js | ||
{todos: [ | ||
{ | ||
id: 1, | ||
text: 'Write a Chrome extension', | ||
created: new Date(2018, 0, 1) | ||
} | ||
]} | ||
``` | ||
|
||
With no custom serialization, the `state` in your proxy store will look like this: | ||
|
||
```js | ||
{todos: [ | ||
{ | ||
id: 1, | ||
text: 'Write a Chrome extension', | ||
created: {} | ||
} | ||
]} | ||
``` | ||
|
||
As you can see, Chrome's message passing has caused your date to disappear. You can pass a custom `serializer` and `deserializer` to both `wrapStore` and `Store` to make sure your dates get preserved: | ||
|
||
```js | ||
// background.js | ||
|
||
import {wrapStore} from 'react-chrome-redux'; | ||
|
||
const store; // a normal Redux store | ||
|
||
wrapStore(store, { | ||
portName: 'MY_APP', | ||
serializer: payload => JSON.stringify(payload, dateReplacer), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that these were added as options rather than additional parameters to wrap store 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the names too ( |
||
deserializer: payload => JSON.parse(payload, dateReviver) | ||
}); | ||
``` | ||
|
||
```js | ||
// content.js | ||
|
||
import {Store} from 'react-chrome-redux'; | ||
|
||
const store = new Store({ | ||
portName: 'MY_APP', | ||
serializer: payload => JSON.stringify(payload, dateReplacer), | ||
deserializer: payload => JSON.parse(payload, dateReviver) | ||
}); | ||
``` | ||
|
||
In this example, `dateReplacer` and `dateReviver` are a custom JSON [replacer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) and [reviver](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) function, respectively. They are defined as such: | ||
|
||
```js | ||
function dateReplacer (key, value) { | ||
// Put a custom flag on dates instead of relying on JSON's native | ||
// stringification, which would force us to use a regex on the other end | ||
return this[key] instanceof Date ? {"_RECOVER_DATE": this[key].getTime()} : value | ||
}; | ||
|
||
function dateReviver (key, value) { | ||
// Look for the custom flag and revive the date | ||
return value && value["_RECOVER_DATE"] ? new Date(value["_RECOVER_DATE"]) : value | ||
}; | ||
|
||
const stringified = JSON.stringify(state, dateReplacer) | ||
//"{"todos":[{"id":1,"text":"Write a Chrome extension","created":{"_RECOVER_DATE":1514793600000}}]}" | ||
|
||
JSON.parse(stringified, dateReviver) | ||
// {todos: [{ id: 1, text: 'Write a Chrome extension', created: new Date(2018, 0, 1) }]} | ||
``` | ||
|
||
## Docs | ||
|
||
* [Introduction](https://github.com/tshaddix/react-chrome-redux/wiki/Introduction) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,8 @@ export class Store<S = any, A extends redux.Action = redux.Action> { | |
portName: string, | ||
state?: any, | ||
extensionId?: string, | ||
serializer?: Function, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thank you for adding the TS defs |
||
deserializer?: Function | ||
}); | ||
|
||
/** | ||
|
@@ -72,6 +74,8 @@ export function wrapStore<S>( | |
configuration: { | ||
portName: string, | ||
dispatchResponder?(dispatchResult: any, send: (response: any) => void): void, | ||
serializer?: Function, | ||
deserializer?: Function | ||
}, | ||
): void; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
export const noop = (payload) => payload; | ||
|
||
const transformPayload = (message, transformer = noop) => ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be good to mention here why a new object is required rather than mutating the message. It took me a second to realize that manipulations here would manipulate the original action object which could cause unintentional side-effects. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add a comment. |
||
...message, | ||
// If the message has a payload, transform it. Otherwise, | ||
// just return a copy of the message. | ||
...(message.payload ? {payload: transformer(message.payload)} : {}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice - love the cascading spread |
||
}); | ||
|
||
const deserializeListener = (listener, deserializer = noop, shouldDeserialize) => { | ||
// If a shouldDeserialize function is passed, return a function that uses it | ||
// to check if any given message payload should be deserialized | ||
if (shouldDeserialize) { | ||
return (message, ...args) => { | ||
if (shouldDeserialize(message, ...args)) { | ||
return listener(transformPayload(message, deserializer), ...args); | ||
} else { | ||
return listener(message, ...args); | ||
} | ||
}; | ||
} | ||
// Otherwise, return a function that tries to deserialize on every message | ||
return (message, ...args) => listener(transformPayload(message, deserializer), ...args); | ||
}; | ||
|
||
/** | ||
* A function returned from withDeserializer that, when called, wraps addListenerFn with the | ||
* deserializer passed to withDeserializer. | ||
* @name AddListenerDeserializer | ||
* @function | ||
* @param {Function} addListenerFn The add listener function to wrap. | ||
* @returns {DeserializedAddListener} | ||
*/ | ||
|
||
/** | ||
* A wrapped add listener function that registers the given listener. | ||
* @name DeserializedAddListener | ||
* @function | ||
* @param {Function} listener The listener function to register. It should expect the (optionally) | ||
* deserialized message as its first argument. | ||
* @param {Function} [shouldDeserialize] A function that takes the arguments passed to the listener | ||
* and returns whether the message payload should be deserialized. Not all messages (notably, messages | ||
* this listener doesn't care about) should be attempted to be deserialized. | ||
*/ | ||
|
||
/** | ||
* Given a deserializer, returns an AddListenerDeserializer function that that takes an add listener | ||
* function and returns a DeserializedAddListener that automatically deserializes message payloads. | ||
* Each message listener is expected to take the message as its first argument. | ||
* @param {Function} deserializer A function that deserializes a message payload. | ||
* @returns {AddListenerDeserializer} | ||
* Example Usage: | ||
* const withJsonDeserializer = withDeserializer(payload => JSON.parse(payload)); | ||
* const deserializedChromeListener = withJsonDeserializer(chrome.runtime.onMessage.addListener); | ||
* const shouldDeserialize = (message) => message.type === 'DESERIALIZE_ME'; | ||
* deserializedChromeListener(message => console.log("Payload:", message.payload), shouldDeserialize); | ||
* chrome.runtime.sendMessage("{'type:'DESERIALIZE_ME','payload':{'prop':4}}"); | ||
* //Payload: { prop: 4 }; | ||
* chrome.runtime.sendMessage("{'payload':{'prop':4}}"); | ||
* //Payload: "{'prop':4}"; | ||
*/ | ||
export const withDeserializer = (deserializer = noop) => | ||
(addListenerFn) => | ||
(listener, shouldDeserialize) => | ||
addListenerFn(deserializeListener(listener, deserializer, shouldDeserialize)); | ||
|
||
/** | ||
* Given a serializer, returns a function that takes a message sending | ||
* function as its sole argument and returns a wrapped message sender that | ||
* automaticaly serializes message payloads. The message sender | ||
* is expected to take the message as its first argument, unless messageArgIndex | ||
* is nonzero, in which case it is expected in the position specified by messageArgIndex. | ||
* @param {Function} serializer A function that serializes a message payload | ||
* Example Usage: | ||
* const withJsonSerializer = withSerializer(payload => JSON.stringify(payload)) | ||
* const serializedChromeSender = withJsonSerializer(chrome.runtime.sendMessage) | ||
* chrome.runtime.addListener(message => console.log("Payload:", message.payload)) | ||
* serializedChromeSender({ payload: { prop: 4 }}) | ||
* //Payload: "{'prop':4}" | ||
*/ | ||
export const withSerializer = (serializer = noop) => | ||
(sendMessageFn, messageArgIndex = 0) => { | ||
return (...args) => { | ||
if (args.length <= messageArgIndex) { | ||
throw new Error(`Message in request could not be serialized. ` + | ||
`Expected message in position ${messageArgIndex} but only received ${args.length} args.`); | ||
} | ||
args[messageArgIndex] = transformPayload(args[messageArgIndex], serializer); | ||
return sendMessageFn(...args); | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this true? I swore it turned it into a string last time I checked... am I crazy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vanilla
JSON.parse(JSON.stringify(new Date()))
will give you a string, but Chrome's default serialization/deserialization for messaging gives you an empty object for some reason or another.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it - must have been confusing the two. Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow this is really good to know. I could see us having a
serializers
dir with defaults for things like dates (not necessary for this PR ofc).