From 2f1438cecf0ba3c55e94287a68186ad7647c649e Mon Sep 17 00:00:00 2001 From: Jon Evans Date: Sun, 26 Mar 2017 18:22:45 +0100 Subject: [PATCH] Adding in synapse record system, adjusting docs, and demo page --- README.md | 85 +++----------- demo/components/StockItem/index.js | 21 ++++ demo/components/StocksTable/StocksTable.js | 8 +- demo/components/TraderCTA/TraderCTA.js | 1 + .../TraderCTA/TraderCTAContainer.js | 4 +- demo/components/TraderCTA/index.js | 2 +- demo/records/TraderRecord.js | 7 +- demo/reducers/index.js | 2 +- demo/reducers/stocksReducer.js | 20 +++- demo/reducers/traderReducer.js | 10 +- docs/API.md | 103 +++++++++++++++++ docs/Provider.md | 36 ++++++ docs/Synapse.md | 30 +++++ docs/The Problem and Solution.md | 12 -- package.json | 4 +- src/components/provider.js | 17 ++- src/generateSynapseRecord.js | 108 ++++++++++++++++++ src/index.js | 2 + src/observer.js | 76 ++++++++++-- 19 files changed, 443 insertions(+), 105 deletions(-) create mode 100644 demo/components/StockItem/index.js create mode 100644 docs/API.md delete mode 100644 docs/The Problem and Solution.md create mode 100644 src/generateSynapseRecord.js diff --git a/README.md b/README.md index fbe6b98..f5dfedc 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,40 @@ # `redux-synapse` + +[![npm](https://img.shields.io/npm/dt/redux-synapse.svg)]() [![npm](https://img.shields.io/npm/v/redux-synapse.svg)]() [![npm](https://img.shields.io/npm/l/redux-synapse.svg)]() + + `redux-synapse` is a library that is heavily inspired by `react-redux` and acts as an alternative for the binding of react components to the store in a more explicit manner. The primary difference is the nature in which each component must declare explicity what updates should affect the component via its higher order component; a `synapse`. With `synapse`'s it is possible to achieve a higher level of performance, than you would with alternative libraries. A `synapse` is declared to listen to specific messages and act upon them. This is an early release of something that I intend to grow over time and build upon to make more efficient. ## Installation +Install `redux-synapse` using `npm` withte following command. + ``` npm install --save redux-synapse ``` +#### [Available on npm](https://www.npmjs.com/package/redux-synapse) -## What it looks like - -### Provider -Much like `react-redux` we have a top level, `Provider` component, that the store should be passed too. This component is necessary for setting up our internal dictionary with subscriber lists. +## Read Docs -```js -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Provider } from 'redux-synapse'; +* `Synapse` HOC [here](/docs/Synapse.md) +* `Provider` [here](/docs/Provider.md) +* `API` [here](/docs/API.md) -ReactDOM.render( - - - , - document.getElementById('app') -); -``` - -### A Standard Component -When setting up the component via `synapse`, you pass in the standard, `mapState*` functions, as well as an array of keys, that this component should update itself on. In this example we only have one level of state as we have a single reducer, however you can see that we have added two key-paths: -* `time` -* `options-enabled` - -`time` is at the top level, however `options-enabled`, says you are interested in the `options` object, and the `enabled` property. -```js +## Why `redux-synapse` exists -import { synapse } from 'redux-synapse'; +### The Problem to Solve +`react` is a fantastic tool, however with larger trees you end up with an inefficient number of rerenders unless you are very strict with your `shouldComponentUpdates`, especially over frequently updated state in a standard flux model, or your own store implementation via `context`. As we know, `react-redux` utilises the `connect` higher order component to theoretically make your tree flat, so that updates are dished out directly from the store, and additional rerenders are only done if the state that we are interested in, handled via a `mapStateToProps` function, changes. This is fantastic as rerenders are expensive, and cutting them out can really solve a large number of performance problems. However in applications that are updating state frequently, such as a video based applcation, or a stock market tracker, you are going to lose performance before the rerenders even happens. -//...Component Declaration +In `react` performance would go down just based on the fact that all those components are rerendering so frequently. +In `react-redux` although we shortcut the rerenders, we are still going to visit our `mapStateToProps` of most of our components, and in essence create a new object every single time to be returned and then evaluated upon. This is fine for smaller applications but in an application with 10's or 100's of components this is going to lead to performance problems. -const mapStateToProps = (state) => { - return { - time: state.time, - }; -} +### The Solution -const mapDispatchToProps = (state, dispatch) => { - return { - setTime: (time) => { - dispatch({ - type: SET_TIME, - time, - }); - }, - }; -}; +This is where `redux-synapse` comes in. Using a similar syntactical solution to `react-redux`, a user can define what paths they are interested in on the state updates, and behind the scenes they are added as subscribers to those keys. If no paths are specified then it will just `subscribe` to the store like it would in `react-redux`, otherwise our `observer` behind the scenes will subscribe to updates to the store via our `reducers` and then using the paths that are specified as being updated in the reducer, will alert all necessary higher order components and trigger them to begin their own rerender cycle as opposed to visiting all components to then determine which ones should or shouldn't be updated. -export default synapse(mapStateToProps, mapDispatchToProps, ['time', 'options-enabled'])(StandardComponent); -``` -### A Standard Reducer -When making changes to state, simply call `prepareNotification` with an array of the affected state keys. This will ensure that on the state being returned the interested components are updated appropriately. - -```js -import { prepareNotification } from 'redux-synapse'; - -const defaultState = { - time: 0, - src: 'none', - options: { - enabled: true, - } -}; - -export default video = (state = defaultState, action) => { - switch(action.type) { - case SET_TIME: - state.time = action.time; - prepareNotification(['time']); - return state; - default: - return state; - } -}; -``` diff --git a/demo/components/StockItem/index.js b/demo/components/StockItem/index.js new file mode 100644 index 0000000..04bd463 --- /dev/null +++ b/demo/components/StockItem/index.js @@ -0,0 +1,21 @@ +import React, { PropTypes } from 'react'; + +export default function StockItem({ name, currentValue, previousValue }) { + return ( +
+
{name}
+
{currentValue}
+
{previousValue}
+
+ + +
+
+ ); +} + +StockItem.propTypes = { + name: PropTypes.string.isRequired, + currentValue: PropTypes.number.isRequired, + previousValue: PropTypes.number.isRequired, +}; diff --git a/demo/components/StocksTable/StocksTable.js b/demo/components/StocksTable/StocksTable.js index ad35088..8ad48ec 100644 --- a/demo/components/StocksTable/StocksTable.js +++ b/demo/components/StocksTable/StocksTable.js @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import StockItem from '../StockItem'; export default class StocksTable extends React.Component { static propTypes = { @@ -13,7 +14,12 @@ export default class StocksTable extends React.Component { renderStocks = () => { return this.props.stocks.map((p, i) => { return ( -

{p.name}

+ ); }); } diff --git a/demo/components/TraderCTA/TraderCTA.js b/demo/components/TraderCTA/TraderCTA.js index 60cb657..3ec6c33 100644 --- a/demo/components/TraderCTA/TraderCTA.js +++ b/demo/components/TraderCTA/TraderCTA.js @@ -27,6 +27,7 @@ export default class TraderCTA extends React.Component { render() { return (
+

Trader Name: {this.props.name}

diff --git a/demo/components/TraderCTA/TraderCTAContainer.js b/demo/components/TraderCTA/TraderCTAContainer.js index 9a2fe76..621fd97 100644 --- a/demo/components/TraderCTA/TraderCTAContainer.js +++ b/demo/components/TraderCTA/TraderCTAContainer.js @@ -1,7 +1,7 @@ export const mapStateToProps = (state) => { return { - name: state.trader.name, - accountValue: state.trader.accountValue, + name: state.trader.getIn(['details', 'name']), + accountValue: state.trader.get('accountValue'), }; }; diff --git a/demo/components/TraderCTA/index.js b/demo/components/TraderCTA/index.js index 27e1c89..19a97b9 100644 --- a/demo/components/TraderCTA/index.js +++ b/demo/components/TraderCTA/index.js @@ -2,4 +2,4 @@ import { synapse } from 'redux-synapse'; import TraderCTA from './TraderCTA'; import { mapStateToProps, mapDispatchToProps } from './TraderCTAContainer'; -export default synapse(mapStateToProps, mapDispatchToProps, ['trader'])(TraderCTA); +export default synapse(mapStateToProps, mapDispatchToProps, ['trader-details-name'])(TraderCTA); diff --git a/demo/records/TraderRecord.js b/demo/records/TraderRecord.js index 7c105b2..477a6b5 100644 --- a/demo/records/TraderRecord.js +++ b/demo/records/TraderRecord.js @@ -1,6 +1,9 @@ -import { Record } from 'immutable'; +import { Map, Record } from 'immutable'; export default Record({ - name: 'NONE_SET', accountValue: 0, + details: Map({ + name: 'NONE_SET', + age: 0, + }), }); diff --git a/demo/reducers/index.js b/demo/reducers/index.js index 6987918..9e12ae4 100644 --- a/demo/reducers/index.js +++ b/demo/reducers/index.js @@ -1,5 +1,5 @@ import trader from './traderReducer'; -import stocks from './stockSReducer'; +import stocks from './stocksReducer'; export default { trader, diff --git a/demo/reducers/stocksReducer.js b/demo/reducers/stocksReducer.js index 777aa80..73c4453 100644 --- a/demo/reducers/stocksReducer.js +++ b/demo/reducers/stocksReducer.js @@ -5,7 +5,7 @@ import { List } from 'immutable'; const defaultState = new StocksRecord({ allStocks: List([ new StockRecord({ - name: 'FTSE', + name: 'FTSE 100', currentValue: 1000, previousValue: 750, valueDifference: 250, @@ -13,6 +13,24 @@ const defaultState = new StocksRecord({ 0, ]), }), + new StockRecord({ + name: 'FTSE 250', + currentValue: 5000, + previousValue: 2500, + valueDifference: 2500, + bidHistory: List([ + 0, + ]), + }), + new StockRecord({ + name: 'FTSE 350', + currentValue: 120, + previousValue: 200, + valueDifference: -80, + bidHistory: List([ + 0, + ]), + }), ]), }); diff --git a/demo/reducers/traderReducer.js b/demo/reducers/traderReducer.js index 9737b98..b0de533 100644 --- a/demo/reducers/traderReducer.js +++ b/demo/reducers/traderReducer.js @@ -1,16 +1,16 @@ import TraderRecord from '../records/TraderRecord'; -import { prepareNotification } from 'redux-synapse'; +import { generateSynapseRecord } from 'redux-synapse'; -const trader = (state = new TraderRecord(), action) => { +const STATE_KEY = 'trader'; +const defaultState = generateSynapseRecord(new TraderRecord(), STATE_KEY); +const trader = (state = defaultState, action) => { let newState; switch (action.type) { case 'SET_TRADER_VALUE': newState = state.set('accountValue', action.accountValue); - prepareNotification(['trader']); return newState; case 'SET_TRADER_NAME': - newState = state.set('name', action.name); - prepareNotification(['trader']); + newState = state.setIn(['details', 'name'], action.name); return newState; default: return state; diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..8f4c5bf --- /dev/null +++ b/docs/API.md @@ -0,0 +1,103 @@ +# API +This outlines the API that is available to use, alongside the Primary components such as the `Provider` and `Synapse`. + +## `generateSynapseRecord(defaultState: any, stateKey: String, getters: Object)` + +### Parameters +- `defaultState` : The default state for a reducer. It accepts, `Immutable.Record|Maps|List`s, or plain old javascript objects. However it doesn't support `Immutable.Record` implementations with custom getters. +- `stateKey` : The associated state key for which this record will be attached to. If you build a reducer and it is added to the store under the key of `"trader"`, then `"trader"` would be the value of this parameter. +- `getters` : Experimental feature for attaching `getters` onto the created `SynapseRecord` + +### Outline +The `generateSynapseRecord` is a utility aimed at breaking the requirement for the `prepareNotification` paradigm that would be used inside your reducers with `redux-synapse`. + +It's purpose is to remove the need for the `prepareNotification` API that is used within reducers. It does this by wrapping the provided `defaultState` in a version of an `Immutable.Record`. This hooks into all `set` and `setIn` calls that are used to manually prepare the notifications for the underlying synapse engine. + +It does however require the `stateKey` that this reducer is associated with to be provided. + +> The `generateSynapseRecord` API may not support custom implementations of the `Immutable.Record` class. + +### Example +The below example outlines a reducer for the `trader` state key, and how the`generateSynapseRecord` API works alongside an `immutable` record. + +```js +import { Record } from 'immutable' +import { generateSynapseRecord } from 'redux-synapse'; + +// Immutable Record of trader state +const TraderRecord = Record({ + name: 'NONE_SET', + accountValue: 0, +}); + +// The expected property name of the reducer on the redux state +const STATE_KEY = 'trader'; +const defaultState = generateSynapseRecord(new TraderRecord(), STATE_KEY); + +// Our Reducer +const trader = (state = defaultState, action) => { + let newState; + switch (action.type) { + case 'SET_TRADER_VALUE': + newState = state.set('accountValue', action.accountValue); + return newState; + case 'SET_TRADER_NAME': + newState = state.set('name', action.name); + return newState; + default: + return state; + } +}; + +export default trader; +``` + +## `prepareNotification(keys: Array)` + +### Parameters +- `keys` : An array of keys that are used to determine which component subscriptions should be updated. + +### Outline +The `prepareNotification` API should be called with an array of the top level keys that have been affected. For example if you have an object in your `redux` state with the key `video`, then you would change those properties and then call `prepareNotification` with the `video` key. This ensures that all relevant subscribers are updated, and only them. A `notify` operation is initiated at the end of the redux `reducer` cycle. + +### Example + + +```js +import { prepareNotification } from 'redux-synapse'; +import { Map } from 'immutable'; + +// Our default state +const defaultState = Map({ + time: 0, + src: 'none', + options: Map({ + enabled: true, + }), +}); + + +// Our video reducer +export default video = (state = defaultState, action) => { + switch(action.type) { + case SET_TIME: + state = state.set('time', action.time); + // We are updating the `time` property on the `video` state key. As + // such we prepare a notification for the components that are subscribed to + // changes to the `video` state key + prepareNotification(['video-time']); + return state; + case SET_OPTIONS_ENABLED: + const options = state.options; + state = state.set('options', options.set('enabled', action.enabled)); + // We have updated the `options` object, on the `video` state key. As such + // we prepare a notification for anyone that is subscribed to changes + // on the `video` state key or the `options` object. + prepareNotification(['video-options-enabled']); + default: + return state; + } +}; +``` +> `redux-synapse` supports a `super-explicit` mode so that in the example of nested objects (`video-options`) it would require +> an explicit subscription to the nested object, and it wouldn't update subscribers on the `video` key. diff --git a/docs/Provider.md b/docs/Provider.md index 1188a89..7b1fd9f 100644 --- a/docs/Provider.md +++ b/docs/Provider.md @@ -7,6 +7,42 @@ The `Provider` works much like the `react-redux` provider as a top level compone |---|---|---|---| |`store`|`Object`|The store that is created by `redux`|`Yes`| |`children`|`node`|The `React` children|`Yes`| +|`delimiter`|`String`|What string to use for the internal dictionary when delimiting keys. Defaults to `'-'` if not provided.|`No`| +|`reverseTravesal`|`Boolean`|Whether to notify each key in a provided path. Defaults to `true` if not provided.|`No`| ## Behind the scenes The `Provider` is responsible for building the internal observer dictionary that is used to determine subscribers across state keys, and paths, from the store state tree. + +## Example Usage +```js +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'redux-synapse'; +import { createStore } from 'redux'; + +const store = creatStore(...); + +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +### `delimiter` Use case +You may find that you have some dynamic keys, or even keys that utilise the `'-'` in their naming. As such we allow users to change the internal delimiter to something else. Single characters are recommended. + +### `reverseTraversal` Use case +Take the following key path: +`video-options-playbackspeed` + +With `reverseTraversal` enabled (_by default it is_) the following keys and their subscriptions would be notified: +- video +- options +- playbackspeed + +With it disabled it would only notify the following: +- playbackspeed + +This allows for a much more explicit approach to defining keys for updates. It also becomes useful in large state trees with various levels, so you can effectively partition updates to entire sections of the react tree. diff --git a/docs/Synapse.md b/docs/Synapse.md index 129bcbd..ff60279 100644 --- a/docs/Synapse.md +++ b/docs/Synapse.md @@ -13,3 +13,33 @@ The `synapse` is the higher order component used for wrapping the components pro ## Behind the scenes The `Provider` is responsible for building the internal observer dictionary that is used to determine subscribers across state keys, and paths, from the store state tree. + +## Example +When setting up the component via `synapse`, you pass in the standard, `mapState*` functions, as well as an array of keys, that this component should update itself on. + +```js + +import { synapse } from 'redux-synapse'; + +//...Component Declaration + +const mapStateToProps = (state) => { + return { + time: state.time, + }; +} + +const mapDispatchToProps = (state, dispatch) => { + return { + setTime: (time) => { + dispatch({ + type: SET_TIME, + time, + }); + }, + }; +}; + +// We are subscribing to changes in the `video.options` key of the redux state. +export default synapse(mapStateToProps, mapDispatchToProps, ['video-options'])(StandardComponent); +``` diff --git a/docs/The Problem and Solution.md b/docs/The Problem and Solution.md deleted file mode 100644 index 66a8e00..0000000 --- a/docs/The Problem and Solution.md +++ /dev/null @@ -1,12 +0,0 @@ -## The Problem - -`react` is a fantastic tool, however with larger trees you end up with an inefficient number of rerenders unless you are very strict with your `shouldComponentUpdates`, especially over frequently updated state in a standard flux model, or your own store implementation via `context`. As we know, `react-redux` utilises the `connect` higher order component to theoretically make your tree flat, so that updates are dished out directly from the store, and additional rerenders are only done if the state that we are interested in, handled via a `mapStateToProps` function, changes. This is fantastic as rerenders are expensive, and cutting them out can really solve a large number of performance problems. However in applications that are updating state frequently, such as a video based applcation, or a stock market tracker, you are going to lose performance before the rerenders even happens. - -In `react` performance would go down just based on the fact that all those components are rerendering so frequently. -In `react-redux` although we shortcut the rerenders, we are still going to visit our `mapStateToProps` of most of our components, and in essence create a new object every single time to be returned and then evaluated upon. This is fine for smaller applications but in an application with 10's or 100's of components this is going to lead to performance problems. - -## The Solution - -This is where `redux-synapse` comes in. Using a similar syntactical solution to `react-redux`, a user can define what paths they are interested in on the state updates, and behind the scenes they are added as subscribers to those keys. If no paths are specified then it will just `subscribe` to the store like it would in `react-redux`, otherwise our `observer` behind the scenes will subscribe to updates to the store via our `reducers` and then using the paths that are specified as being updated in the reducer, will alert all necessary higher order components and trigger them to begin their own rerender cycle as opposed to visiting all components to then determine which ones should or shouldn't be updated. - - diff --git a/package.json b/package.json index b792676..163c6b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-synapse", - "version": "0.3.0", + "version": "0.4.0-beta3", "main": "lib/index.js", "files": [ "src", @@ -46,7 +46,7 @@ "gulp": "3.9.1", "hoist-non-react-statics": "1.2.0", "immutable": "3.8.1", - "jest-cli": "0.10.2", + "jest-cli": "19.0.2", "node-sass": "^4.5.1", "prettyjson": "^1.1.3", "react": "15.4.1", diff --git a/src/components/provider.js b/src/components/provider.js index 250809b..b100a14 100644 --- a/src/components/provider.js +++ b/src/components/provider.js @@ -1,10 +1,21 @@ import React, { Component, PropTypes } from 'react'; -import { createObserverDictionary, releaseNotifications } from '../observer'; +import { + createObserverDictionary, + releaseNotifications, + setDelimiter, + setReverseNotficationTraversal +} from '../observer'; export default class Provider extends Component { static propTypes = { store: PropTypes.object.isRequired, children: PropTypes.node.isRequired, + delimiter: PropTypes.string, + reverseTraversal: PropTypes.bool, + }; + + static defaultPropTypes = { + reverseTraversal: true, }; static childContextTypes = { @@ -23,6 +34,10 @@ export default class Provider extends Component { } componentDidMount() { + if (this.props.delimiter) { + setDelimiter(this.props.delimiter); + } + setReverseNotficationTraversal(this.props.reverseTraversal); createObserverDictionary(this.store.getState()); this.unsubscribe = this.store.subscribe(() => { releaseNotifications(); diff --git a/src/generateSynapseRecord.js b/src/generateSynapseRecord.js new file mode 100644 index 0000000..8b98ccb --- /dev/null +++ b/src/generateSynapseRecord.js @@ -0,0 +1,108 @@ +import { Iterable, fromJS, Record } from 'immutable'; +import { prepareNotification, getDelimiter } from './observer'; + +/** + * Temporary State Key used for maintaining the Synapse context + * after set operations. + */ +let temporaryStateKey; + +/** + * This function takes in a default state shape that would be used by a reducer + * as well as the associated string name of the reducer. It then returns a SynapseRecord + * based off this data + * + * @param {Any} The default state that will be used to generate a record + * @param {String} The state key associated with the reducer. This will be used for correctly + * notifying changes. + * @returns { SynapseRecord } - A synapse record constructed around the state provided + */ +export default function generateSynapseRecord(defaultState: any, stateKey: String, getters: Object) { + let tempValues = defaultState.toJS ? defaultState.toJS() : defaultState; + + /** + * A SynapseRecord overrides the base Immutable Record set functionality, and provides a similar Map setIn API + * for use. The SynpaseRecord prepares notifications for the Observer system based on desired updates. + * + */ + class SynapseRecord extends Record(tempValues) { + constructor(defaults) { + super(defaults); + if (getters) { + console.warn('Providing a list of getters is currently an experimental feature'); + const keys = Object.keys(getters); + for (let i = 0; i < keys.length; i++) { + Object.defineProperty(this.prototype, keys[i], { + get: () => { + return getters[keys[i]]; + }, + }); + } + } + this.__stateKey__ = stateKey; + } + + /** + * Called following any immutable operations, so that any SynapseRecord specifics + * can be attached. + * + * @param {SynapseRecord} record - The record which will have context reattached + * @returns {SynapseRecord} - Returns the updated SynapseRecord + */ + reattachSynapseContext(record, stateKey = this.__stateKey__) { + record.__stateKey__ = stateKey; + return record; + } + + /** + * Provides a method to deep update objects on the record. Works on any properties + * that are a Map on the SynapseRecord + * + * @param {Array} keys - The key path to change. The final key will be the destination + * key that is updated. + * @param {Any} The value to set at the destination key + * @returns {SynapseRecord} - Returns the updated SynapseRecord from the setIn operation + */ + setIn(keys, value) { + let desiredObject = this.get(keys[0]); + if (!Iterable.isKeyed(desiredObject)) { + throw new Error(`Attempted to deep update Keyed Iterable, when it wasn\'t. + Key: ${keys[0]}`); + } + let objKeys = [...keys]; + objKeys = objKeys.splice(-1, 1); + desiredObject = desiredObject.setIn(objKeys, value); + const newSet = super.set(keys[0], desiredObject); + prepareNotification([`${this.__stateKey__}${getDelimiter()}${keys.join(getDelimiter())}`]); + return this.reattachSynapseContext(newSet); + } + + /** + * Used for updating any top level property on the SynapseRecord + * + * @param {String} key - The key which represents the associated property to update on the SynapseRecord + * @param {Any} value - The value to set on the desired property + * @returns {SynapseRecord} - Returns the updated SynapseRecord from the setIn operation + */ + set(key, value) { + const newSet = super.set(key, value); + prepareNotification([`${temporaryStateKey || this.__stateKey__}${getDelimiter()}${key}`]); + return this.reattachSynapseContext(newSet, temporaryStateKey); + } + + /** + * Used for batch updating keys. Calls the underlying withMutations on the Immutable record, whilst + * maintaining the Synapse context + * + * @param {Function} fn - The function to be used for withMutations + */ + withMutations(fn) { + temporaryStateKey = this.__stateKey__; + const resultRecord = this.reattachSynapseContext(super.withMutations(fn), temporaryStateKey); + temporaryStateKey = undefined; + return resultRecord; + } + } + + return new SynapseRecord(defaultState); +} diff --git a/src/index.js b/src/index.js index 3127018..176ed7a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ import Provider from './components/provider'; import synapse from './components/synapse'; +import generateSynapseRecord from './generateSynapseRecord'; import { prepareNotification } from './observer'; export default { Provider, prepareNotification, synapse, + generateSynapseRecord, }; diff --git a/src/observer.js b/src/observer.js index 5e085cf..a059ebd 100644 --- a/src/observer.js +++ b/src/observer.js @@ -1,9 +1,10 @@ -const DEFAULT_DELIMITER = '-'; +let DEFAULT_DELIMITER = '-'; let _reverseNotificationMode = true; let _observerDictionary = {}; let _attachQueue = []; let _prepareQueue = []; let hitSubs = []; +const indexableKey = new RegExp(/{(.*?)}/); const hitSubscriber = (s) => { if (hitSubs.indexOf(s) === -1) { @@ -33,7 +34,7 @@ function notify(keys) { if (!tempKey) { tempKey = split[j]; }else { - tempKey += `-${split[j]}`; + tempKey += `${DEFAULT_DELIMITER}${split[j]}`; } const entry = _observerDictionary[tempKey]; if (entry) { @@ -61,9 +62,21 @@ function attach(keys, observer) { if (!tempKey) { tempKey = split[j]; }else { - tempKey += `-${split[j]}`; + // TODO: Dynamic associative keys + if (indexableKey.test(split[j])) { + throw new Error('Dynamic entries in keypaths are currently not supported. Please do not use the `{i}` format.'); + // const index = split[j].substring(1,split[j].length - 1); + }else { + tempKey += `${DEFAULT_DELIMITER}${split[j]}`; + } + } + if(_observerDictionary[tempKey]) { + _observerDictionary[tempKey].subscribers.push(observer); + }else { + if (process.NODE_ENV !== 'production') { + console.warn('Invalid Paths provided to Synapse during attach process', keys); + } } - _observerDictionary[tempKey].subscribers.push(observer); } } } @@ -80,9 +93,15 @@ function detach(keys, observer) { if (!tempKey) { tempKey = split[j]; }else { - tempKey += `-${split[j]}`; + tempKey += `${DEFAULT_DELIMITER}${split[j]}`; + } + if(_observerDictionary[tempKey]) { + _observerDictionary[tempKey].subscribers.splice(observer, 1); + }else { + if (process.NODE_ENV !== 'production') { + console.warn('Invalid Paths provided to Synapse during detatch process', keys); + } } - _observerDictionary[tempKey].subscribers.splice(observer, 1); } } } @@ -91,7 +110,7 @@ function detach(keys, observer) { function recurseObserverDictionary(state, prefix = null) { for (const propName in state) { if (state.hasOwnProperty(propName)) { - const value = state[propName]; + let value = state[propName]; let key = propName; if (Array.isArray(prefix)) { let prefixConcat = ''; @@ -104,11 +123,15 @@ function recurseObserverDictionary(state, prefix = null) { subscribers: [], }; // TODO: Add in support for Immutable data structures - if (typeof value === 'object' && !value.toJS) { + if (value && typeof value === 'object' && Object.keys(value).length && !Array.isArray(value)) { + let innerPrefix; if (prefix) { - prefix.push(propName); + innerPrefix = [...prefix, propName]; + } + const passedDownPrefix = innerPrefix || [propName]; + if (value.toJS) { + value = value.toJS(); } - const passedDownPrefix = prefix || [propName]; recurseObserverDictionary(value, passedDownPrefix); } } @@ -118,6 +141,7 @@ function recurseObserverDictionary(state, prefix = null) { function createObserverDictionary(state) { _observerDictionary = {}; recurseObserverDictionary(state); + console.log(_observerDictionary); for (let j = 0; j < _attachQueue.length; j++) { const item = _attachQueue[j]; attach(item.keys, item.observer); @@ -141,16 +165,48 @@ function releaseNotifications() { } } +/** + * Experimental feature. This is used when you want to set how notifications work. + * By default if provided with the path [keyfloor-keybasement-keyunderground] it will + * reoslve and noficy subscribers at each level. However when set to false, it will only + * resolve the final key in the path and only notify it's subscribers. + * + * @param {boolean} The value to set the reverseNotificationMode too. Defaults to true + */ function setReverseNotficationTraversal(value = true) { _reverseNotificationMode = value; } + +/** + * This allows the delimiter to be changed that is used within the dictionary on all + * key paths. + * + * @param {String} newDelimiter - The delimiter to use in dictonary creation + */ +function setDelimiter(newDelimiter) { + if (newDelimiter) { + DEFAULT_DELIMITER = newDelimiter; + } +} + +/** + * Function to get and return the configured delimiter. + * + * @returns {String} - The default delimiter + */ +function getDelimiter() { + return DEFAULT_DELIMITER; +} + export default { attach, createObserverDictionary, detach, prepareNotification, releaseNotifications, + setDelimiter, + getDelimiter, get observerDictionary() { return _observerDictionary; },