UNPKG

@genshi/core

Version:

A simple, composable and effective JavaScript state management library

567 lines (566 loc) 16.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateWrapper = (obj, member, setter, getter) => ({ set _(value) { __privateSet(obj, member, value, setter); }, get _() { return __privateGet(obj, member, getter); } }); var __privateMethod = (obj, member, method) => { __accessCheck(obj, member, "access private method"); return method; }; var _id, _displayName, _type, _storeId, _handler, _source, _id2, _name, _config, _counter, _state, _previousStates, _subscribers, _dispatchers, _exists, exists_fn, _history, _actionMiddlewares, _effectMiddlewares; var Dispatcher = /* @__PURE__ */ ((Dispatcher2) => { Dispatcher2["ACTION"] = "action"; Dispatcher2["EFFECT"] = "effect"; return Dispatcher2; })(Dispatcher || {}); class BaseDispatcher { constructor({ storeId, displayName, type, handler }) { __privateAdd(this, _id, void 0); __privateAdd(this, _displayName, void 0); __privateAdd(this, _type, void 0); __privateAdd(this, _storeId, void 0); __privateAdd(this, _handler, void 0); __privateAdd(this, _source, null); __publicField(this, "payload", null); __publicField(this, "parent", null); __privateSet(this, _id, `store-0001`); __privateSet(this, _displayName, displayName); __privateSet(this, _type, type); __privateSet(this, _storeId, storeId); __privateSet(this, _handler, this.finalizeHandler(handler)); } /** * Simple wrapper for the handler function to remove the parent dispatcher reference. */ finalizeHandler(handler) { const finalHandler = (...args) => { this.parent = null; return handler(...args); }; return finalHandler; } get id() { return __privateGet(this, _id); } set id(_value) { throw new Error("Cannot set 'id' for dispatcher after creation"); } get displayName() { return __privateGet(this, _displayName); } set displayName(_value) { throw new Error("Cannot set 'displayName' for dispatcher after creation"); } get type() { return __privateGet(this, _type); } set type(_value) { throw new Error("Cannot set 'type' for dispatcher after creation"); } get storeId() { return __privateGet(this, _storeId); } set storeId(_value) { throw new Error("Cannot set 'storeId' for dispatcher after creation"); } get handler() { return __privateGet(this, _handler); } set handler(_value) { throw new Error("Cannot set 'handler' for dispatcher after creation"); } source(source) { if (source || source === null) { __privateSet(this, _source, source); } return { ...__privateGet(this, _source) }; } } _id = new WeakMap(); _displayName = new WeakMap(); _type = new WeakMap(); _storeId = new WeakMap(); _handler = new WeakMap(); _source = new WeakMap(); class Action extends BaseDispatcher { constructor(storeId, name, handler) { super({ storeId, displayName: name, type: Dispatcher.ACTION, handler }); } } class Effect extends BaseDispatcher { constructor(storeId, name, handler) { super({ storeId, displayName: name, type: Dispatcher.EFFECT, handler }); } } const _ConfigManager = class _ConfigManager { constructor() { __privateAdd(this, _id2, void 0); __privateAdd(this, _name, ""); __privateAdd(this, _config, {}); __privateSet(this, _id2, `store-${(__privateWrapper(_ConfigManager, _counter)._++).toString().padStart(4, "0")}`); } setConfig(config) { if (config.name) { __privateSet(this, _name, config.name); } __privateSet(this, _config, config); } get id() { return __privateGet(this, _id2); } set id(_value) { throw new Error("Cannot set 'id' for store after creation"); } get name() { return __privateGet(this, _name); } set name(_value) { throw new Error("Cannot set 'name' for store after creation"); } get tag() { return __privateGet(this, _name) || __privateGet(this, _id2); } set tag(_value) { throw new Error("Cannot set 'tag' for store after creation"); } get config() { return __privateGet(this, _config); } set config(_value) { throw new Error("Cannot set 'config' for store after creation"); } }; _id2 = new WeakMap(); _name = new WeakMap(); _config = new WeakMap(); _counter = new WeakMap(); __privateAdd(_ConfigManager, _counter, 0); let ConfigManager = _ConfigManager; class StateManager extends ConfigManager { constructor(state) { super(); /** * Only mutate this directly within `setState` method. Otherwise */ __privateAdd(this, _state, void 0); __privateAdd(this, _previousStates, void 0); __privateAdd(this, _subscribers, []); __privateSet(this, _state, state); __privateSet(this, _previousStates, [state]); } /** * We have this property separately to make it easier to access the * state in the classes that inherit `StateManager`. */ get state() { return this.getState(); } /** * Never allow setting state directly via this property. It should only be * mutated via the `setState` method to ensure any optimisations or features * we might add while updating state will be centrally managed. */ set state(_value) { throw new Error( "You cannot set the state directly. Use the `setState` method instead." ); } /** * We have this method separately to make it easier to access the * previous state in the classes that inherit from `StateManager`. * * It is by design that the property only returns the immediately previous state. * If you need to access more than one previous state, use the `getPreviousStates` method. */ get previousState() { return __privateGet(this, _previousStates)[0]; } /** * Never allow setting previous state directly. It is populated internally as * a side effect of setting the state. */ set previousState(_value) { throw new Error( "You cannot set the previous state directly. It is managed internally." ); } /** * Set the new state. We do not provide any validation here as it is * the responsibility of the consumer. */ setState(state) { __privateGet(this, _previousStates).unshift(Object.seal(__privateGet(this, _state))); __privateSet(this, _state, state); __privateGet(this, _subscribers).forEach((callback) => callback(state)); } /** * The `getState` method is used to get the current state. */ getState() { return __privateGet(this, _state); } /** * The `getPreviousStates` returns all the previous states in the lifetime of the store. */ getPreviousStates() { return __privateGet(this, _previousStates).slice(); } /** * Allows you to subscribe to the state changes. It returns an object with a `remove` method * that you can call to unsubscribe. */ subscribe(callback) { const index = __privateGet(this, _subscribers).push(callback) - 1; return { remove: () => { __privateGet(this, _subscribers).splice(index, 1); } }; } } _state = new WeakMap(); _previousStates = new WeakMap(); _subscribers = new WeakMap(); class HandlerManager extends StateManager { constructor() { super(...arguments); /** * The `#exists` method is used to check if a dispatcher with the same name * already exists in the store. */ __privateAdd(this, _exists); __privateAdd(this, _dispatchers, /* @__PURE__ */ new Set()); } /** * The `getHandler` method is used to get the handler for the dispatcher. * It checks if the dispatcher is registered with the store and if it is * allowed to be fired from the store. * * @todo (samrith-s) Evaluate moving this to where the dispatcher is actually fired * rather than when the handler is fetched. */ getHandler(dispatcher) { const type = dispatcher.type; const name = dispatcher.displayName; if (!__privateGet(this, _dispatchers).has(`${type}-${name}`)) { throw new TypeError( `Dispatcher ${type} ${name} is not registered with the store '${this.tag}'` ); } return dispatcher.handler; } /** * Register a dispatcher with the store. This method is used to register * both actions and effects with the store. * * It performs an intrinsic non-blocking check to see if the dispatcher is already registered. */ registerDispatcher(dispatcher) { __privateMethod(this, _exists, exists_fn).call(this, dispatcher); __privateGet(this, _dispatchers).add(`${dispatcher.type}-${dispatcher.displayName}`); return dispatcher; } } _dispatchers = new WeakMap(); _exists = new WeakSet(); exists_fn = function(dispatcher) { const type = dispatcher.type; const name = dispatcher.displayName; if (__privateGet(this, _dispatchers).has(`${type}-${name}`)) { console.warn( `The ${type} dispatcher with name '${name}' already exists in store '${this.tag}'. Setting it again will overwrite it.` ); } }; class HistoryManager extends HandlerManager { constructor() { super(...arguments); __privateAdd(this, _history, /* @__PURE__ */ new Map()); } /** * The `trace` method is used to create a trace of the dispatch. It is the * primary method used to start a trace of the dispatch. */ trace(history) { const id = `trace-${__privateGet(this, _history).size.toString().padStart(4, "0")}`; __privateGet(this, _history).set(id, { id, global: false, timestamp: /* @__PURE__ */ new Date(), ...history }); return id; } /** * The `traceEnd` method is used to end a trace of the dispatch. The only way to * end a trace is by calling this method with a valid trace id. */ traceEnd(id) { const trace = __privateGet(this, _history).get(id); if (!trace) { console.warn(`A trace with id '${id}' does not exist.`); return; } const updatedTrace = { ...trace, previousState: this.previousState, currentState: this.state }; __privateGet(this, _history).set(id, updatedTrace); } /** * The `history` method is used to get the history of the store. It returns a new instance * of the history array, so that the original history array is not mutated. */ history() { return [...__privateGet(this, _history).values()].reverse(); } /** * Convenience method to print the history in a table format. Useful for * debugging. */ printHistory() { console.table( this.history().map((history) => { var _a, _b; return { type: history.type, dispatcher: history.name, global: history.global, payload: history.payload, previous_state: history.previousState, current_state: history.currentState, ...history.source ? { source_type: (_a = history.source) == null ? void 0 : _a.type, source_name: (_b = history.source) == null ? void 0 : _b.name } : {} }; }) ); } } _history = new WeakMap(); class MiddlewareManager extends HistoryManager { constructor() { super(...arguments); __privateAdd(this, _actionMiddlewares, []); __privateAdd(this, _effectMiddlewares, []); } /** * A simple method to collect the middlewares from the store configuration, * and make them separately available for the respective apply methods. */ collectMiddlewares(config) { var _a, _b; __privateSet(this, _actionMiddlewares, ((_a = config == null ? void 0 : config.middlewares) == null ? void 0 : _a.action) ?? []); __privateSet(this, _effectMiddlewares, ((_b = config == null ? void 0 : config.middlewares) == null ? void 0 : _b.effect) ?? []); } /** * Applies all the action middlewares, in the order they were specified, * before the actual action handler. */ applyActionMiddleware({ handler, payload }) { if (__privateGet(this, _actionMiddlewares).length === 0) { return handler({ state: this.state, payload }); } return __privateGet(this, _actionMiddlewares).reduce( (acc, middleware) => middleware({ state: acc, payload, handler }), this.state ); } /** * Applies all the effect middlewares, in the order they were specified, * before the actual effect handler. */ applyEffectMiddleware({ handler, payload, dispatch }) { if (__privateGet(this, _effectMiddlewares).length === 0) { return handler({ state: this.state, payload, dispatch }); } else { __privateGet(this, _effectMiddlewares).forEach( (middleware) => middleware({ state: this.state, payload, dispatch, handler }) ); } } } _actionMiddlewares = new WeakMap(); _effectMiddlewares = new WeakMap(); class DispatchManager extends MiddlewareManager { constructor() { super(...arguments); /** * The `dispatch` method is used to dispatch an action or an effect. */ __publicField(this, "dispatch", (dispatcher, ...args) => { var _a, _b; const payload = args[0]; const type = dispatcher.type; const name = dispatcher.displayName; const storeId = dispatcher.storeId; const isGlobal = !dispatcher.parent; if (this.id !== storeId) { const prefix = type === Dispatcher.ACTION ? "Action" : "Effect"; throw new RangeError( `${prefix} '${name}' cannot be fired from store '${this.tag}'.` ); } const handler = this.getHandler(dispatcher); const trace = this.trace({ name, type, payload, ...!isGlobal ? { global: false, source: { name: (_a = dispatcher.parent) == null ? void 0 : _a.displayName, type: (_b = dispatcher.parent) == null ? void 0 : _b.type } } : { global: true } }); switch (type) { case Dispatcher.ACTION: { this.setState( this.applyActionMiddleware({ handler, payload }) ); this.traceEnd(trace); break; } case Dispatcher.EFFECT: { this.traceEnd(trace); this.applyEffectMiddleware({ handler, payload, dispatch: (...argv) => { const d = argv[0]; d.parent = dispatcher; this.dispatch(...argv); } }); break; } } }); } } class Store extends DispatchManager { constructor(state, config) { super(state); this.setConfig(config || {}); this.collectMiddlewares(this.config); } /** * Register an action with the store. In Genshi, an Action is used to * update the state. * * ```ts * const store = new Store({ count: 0 }); * * const increment = store.action('increment', ({ state }) => ({ * ...state, * count: state.count + 1 * })); * * store.dispatch(increment); * ``` */ action(name, handler) { return this.registerDispatcher( new Action(this.id, name, handler) ); } /** * Register an effect with the store. In Genshi, an Effects is used to * perform side effects like API calls, logging, etc. * * ```ts * const store = new Store({ count: 0 }); * * const tick = store.effect('tick', ({ state }) => { * setInterval(() => { * console.log(state.count); * }, 1000) * }); * * store.dispatch(tick); * ``` */ effect(name, handler) { return this.registerDispatcher( new Effect(this.id, name, handler) ); } } export { Store };