UNPKG

@zedux/core

Version:

A high-level, declarative, composable form of Redux

409 lines (386 loc) 17.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Store = exports.createStore = void 0; const create_1 = require("../hierarchy/create"); const merge_1 = require("../hierarchy/merge"); const traverse_1 = require("../hierarchy/traverse"); const general_1 = require("../utils/general"); const defaultHierarchyConfig = __importStar(require("../utils/hierarchyConfig")); const detailedTypeof_1 = require("./detailedTypeof"); const isPlainObject_1 = require("./isPlainObject"); const meta_1 = require("./meta"); const zeduxTypes_1 = require("./zeduxTypes"); // When an action is dispatched to a parent store and delegated to a child // store, the child store needs to wait until the update propagates everywhere // and the parent store finishes its dispatch before notifying its subscribers. // A proper scheduler will allow all child stores of the currently-dispatching // parent store to wait to notify their subscribers until all stores in the // hierarchy are done dispatching. const defaultScheduler = { scheduleNow: (job) => job.task(), }; const primeAction = { type: zeduxTypes_1.zeduxTypes.prime }; /** Creates a new Zedux store. */ const createStore = (initialHierarchy, initialState) => { return new Store(initialHierarchy, initialState); }; exports.createStore = createStore; class Store { constructor(initialHierarchy, initialState) { /** * We can optimize some things if this store is never composed. Disable optimizations as soon as this store is used as a parent or child store. */ this._isSolo = true; this._subscribers = []; /** Dispatches an action to the store. The action will be sent through this store's reducer hierarchy (if any) and passed on to any child stores after being wrapped in `inherit` meta nodes The resulting state will be returned synchronously from this call. This is a bound function property. Every store recreates this small function. But it's always bound and can be passed around easily. */ this.dispatch = (action) => { if (this._isSolo) { return this._dispatch(action); } this._scheduler.scheduleNow({ task: () => this._dispatch(action), type: 0, // UpdateStore (0) }); return this._state; }; /** Applies a full hydration to the store. Accepts either the new state or a function that accepts the current state and returns the new state. Dispatches the special `hydrate` action to the store's reducers. Effects subscribers can inspect and record this action to implement time travel. The `hydrate` action's `payload` property will be set to the new state. The action's `meta` property will be set to the passed meta, if any. Throws an error if called from the reducer layer. Returns the new state. Unlike setStateDeep, setState is a bound function property. Every store recreates this small function. But it's always bound and can be passed around easily. */ this.setState = (settable, meta) => { if (this._isSolo) { return this._setState(settable, meta); } this._scheduler.scheduleNow({ task: () => this._setState(settable, meta), type: 0, // UpdateStore (0) }); return this._state; }; this._state = initialState; this._scheduler = Store._scheduler || defaultScheduler; if (initialHierarchy) { this.use(initialHierarchy); } } actionStream() { return { [Symbol.observable]() { return this; }, '@@observable'() { return this; }, subscribe: (subscriber) => { return this.subscribe({ effects: ({ action, error }) => { var _a, _b; if (error && typeof subscriber !== 'function') { (_a = subscriber.error) === null || _a === void 0 ? void 0 : _a.call(subscriber, error); } else if (action) { typeof subscriber === 'function' ? subscriber(action) : (_b = subscriber.next) === null || _b === void 0 ? void 0 : _b.call(subscriber, action); } }, }); }, }; } /** Returns the current state of the store. Do not mutate the returned value. */ getState() { if (true /* DEV */ && this._isDispatching) { throw new Error('Zedux: store.getState() cannot be called in a reducer'); } return this._state; } /** Applies a partial state update to the store. Accepts either a deep partial state object or a function that accepts the current state and returns a deep partial state object. This method only recursively traverses normal JS objects. If your store deeply nests any other data structure, including arrays or maps, you'll have to deeply merge them yourself using `store.setState()`. Dispatches the special `merge` action to the store's reducers. This action's `payload` property will be set to the resolved partial state update. Effects subscribers can record this action to implement time travel. IMPORTANT: Deep setting cannot remove properties from the state tree. Use `store.setState()` for that. Throws an error if called from the reducer layer. Returns the new state. Unlike setState, setStateDeep is not bound. You must call it with context - e.g. by using dot-notation: `store.setStateDeep(...)` */ setStateDeep(settable, meta) { if (this._isSolo) { return this._setState(settable, meta, true); } this._scheduler.scheduleNow({ task: () => this._setState(settable, meta, true), type: 0, // UpdateStore (0) }); return this._state; } /** Registers a subscriber with the store. The subscriber will be notified every time the store's state changes. Returns a subscription object. Calling `subscription.unsubscribe()` unregisters the subscriber. */ subscribe(subscriber) { const subscriberObj = typeof subscriber === 'function' ? { next: subscriber } : subscriber; if (true /* DEV */) { if (subscriberObj.next && typeof subscriberObj.next !== 'function') { throw new TypeError(`Zedux: store.subscribe() expects either a function or an object with a "next" property whose value is a function. Received: ${(0, detailedTypeof_1.detailedTypeof)(subscriberObj.next)}`); } if (subscriberObj.error && typeof subscriberObj.error !== 'function') { throw new TypeError(`Zedux: store.subscribe() - subscriber.error must be a function. Received: ${(0, detailedTypeof_1.detailedTypeof)(subscriberObj.error)}`); } if (subscriberObj.effects && typeof subscriberObj.effects !== 'function') { throw new TypeError(`Zedux: store.subscribe() - subscriber.effects must be a function. Received: ${(0, detailedTypeof_1.detailedTypeof)(subscriberObj.effects)}`); } } this._subscribers.push(subscriberObj); const subscription = { unsubscribe: () => { // Zedux stores rarely have more than 2-3 subscribers (most // "subscriptions" are handled by the atom graph in the @zedux/atoms // package). Array filter is the fastest way to immutably remove items // from such small lists (compared to Array toSpliced, Array cloning and // splicing, Array spreading slices, Map clone and delete, and Object // clone and delete. Benchmark: https://jsbench.me/4ylihvvpo6) this._subscribers = this._subscribers.filter(obj => obj !== subscriberObj); }, }; return subscription; } /** Merges a hierarchy descriptor into the existing hierarchy descriptor. Intelligently diffs the two hierarchies and only creates/recreates the necessary reducers. Dispatches the special `prime` action to the store. */ use(newHierarchy) { const newTree = (0, create_1.hierarchyDescriptorToHierarchy)(newHierarchy, (childStorePath, childStore) => this._registerChildStore(childStorePath, childStore)); const tree = (this._tree = (0, merge_1.mergeHierarchies)(this._tree, newTree, this.constructor.hierarchyConfig)); if ((this._rootReducer = tree.reducer)) { this._dispatchAction(primeAction, primeAction, this._state); } return this; // for chaining } /** * Only for internal use. */ _register(effects) { const parents = this._parents || (this._parents = []); parents.push(effects); this._isSolo = false; return () => { const index = parents.indexOf(effects); if (index > -1) parents.splice(index, 1); }; } [Symbol.observable]() { return this; } '@@observable'() { return this; } _dispatch(action) { if (true /* DEV */ && typeof action === 'function') { throw new TypeError('Zedux: store.dispatch() - Thunks are not currently supported. Only normal action objects can be passed to store.dispatch(). For zero-config stores, you can pass a function to store.setState()'); } if (true /* DEV */ && !(0, isPlainObject_1.isPlainObject)(action)) { throw new TypeError(`Zedux: store.dispatch() - Action must be a plain object. Received ${(0, detailedTypeof_1.detailedTypeof)(action)}`); } const delegateResult = (0, traverse_1.delegate)(this._tree, action); if (delegateResult !== false) { // No need to inform subscribers - this store's effects subscriber // on the child store will have already done that by this point return this._state; } return this._routeAction(action); } _dispatchAction(action, unwrappedAction, rootState) { if (true /* DEV */ && this._isDispatching) { throw new Error('Zedux: dispatch(), setState(), and setStateDeep() cannot be called in a reducer'); } this._isDispatching = true; let error; let newState = rootState; try { if (this._rootReducer) { newState = this._rootReducer(rootState, unwrappedAction); } } catch (err) { error = err; throw err; } finally { this._isDispatching = false; this._notify(newState, action, error); } return newState; } _dispatchHydration(state, actionType, meta) { const newState = actionType === zeduxTypes_1.zeduxTypes.hydrate ? state : (0, merge_1.mergeStateTrees)(this._state, state, this.constructor.hierarchyConfig)[0]; // short-circuit if there's no change and no metadata that needs to reach // this (or a parent/child) store's effects subscribers if (newState === this._state && !meta) { return this._state; } const action = { meta, payload: newState, type: actionType, }; // Propagate the change to child stores return this._dispatchAction(action, action, newState); } _dispatchStateSetter(getState, meta, deep) { let newState; try { newState = getState(this._state); } catch (error) { if (true /* DEV */) { throw new Error(`Zedux: encountered an error while running a state setter passed to store.setState${deep ? 'Deep' : ''}()`, { cause: error }); } throw error; } return this._dispatchHydration(newState, deep ? zeduxTypes_1.zeduxTypes.merge : zeduxTypes_1.zeduxTypes.hydrate, meta); } _doNotify(effect) { // the subscribers array is replaced when anyone unsubscribes const { _subscribers } = this; const hasChanged = effect.newState !== effect.oldState; for (const subscriber of _subscribers) { if (effect.error && subscriber.error) subscriber.error(effect.error); if (hasChanged && subscriber.next) { subscriber.next(effect.newState, effect.oldState, effect.action); } if (subscriber.effects) subscriber.effects(effect); } } _notify(newState, action, error) { var _a; const effect = { action, error, newState, oldState: this._state, store: this, }; // Update the stored state this._state = newState; if (this._isSolo) { return this._doNotify(effect); } // defer informing if a parent store is currently dispatching this._scheduler.scheduleNow({ task: () => this._doNotify(effect), type: 1, // InformSubscribers (1) }); (_a = this._parents) === null || _a === void 0 ? void 0 : _a.forEach(parent => parent(effect)); } _registerChildStore(childStorePath, childStore) { this._isSolo = false; const effectsSubscriber = ({ action, error, newState, oldState, }) => { // If this store's reducer layer dispatched this action to this substore // in the first place, ignore the propagation; this store is already going // to notify its own subscribers of it. if (this._isDispatching) return; const newOwnState = newState === oldState ? this._state : (0, traverse_1.propagateChange)(this._state, childStorePath, newState, this.constructor.hierarchyConfig); // Tell the subscribers what child store this action came from. This store // (the parent) can use this info to determine how to recreate this state // update. const wrappedAction = { metaType: zeduxTypes_1.zeduxTypes.delegate, metaData: childStorePath, payload: action, }; this._notify(newOwnState, wrappedAction, error); }; return childStore._register(effectsSubscriber); } _routeAction(action) { const unwrappedAction = (0, meta_1.removeAllMeta)(action); if (true /* DEV */ && typeof unwrappedAction.type !== 'string') { throw new TypeError(`Zedux: store.dispatch() - Action must have a string "type" property. Received ${(0, detailedTypeof_1.detailedTypeof)(unwrappedAction.type)}`); } if (unwrappedAction.type === zeduxTypes_1.zeduxTypes.hydrate || unwrappedAction.type === zeduxTypes_1.zeduxTypes.merge) { return this._dispatchHydration(unwrappedAction.payload, unwrappedAction.type, unwrappedAction.meta); } return this._dispatchAction(action, unwrappedAction, this._state); } _setState(settable, meta, deep = false) { if (typeof settable === 'function') { return this._dispatchStateSetter(settable, meta, deep); } return this._dispatchHydration(settable, deep ? zeduxTypes_1.zeduxTypes.merge : zeduxTypes_1.zeduxTypes.hydrate, meta); } } exports.Store = Store; /** Used by the store's branch reducers in the generated reducer hierarchy to interact with the hierarchical data type returned by the store's reducers. This "hierarchical data type" is a plain object by default. But these hierarchy config options can teach Zedux how to use an Immutable `Map` or any recursive, map-like data structure. */ Store.hierarchyConfig = defaultHierarchyConfig; Store.$$typeof = general_1.STORE_IDENTIFIER;