UNPKG

@dr.pogodin/react-global-state

Version:
292 lines (268 loc) 12.3 kB
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } import { get, set, toPath } from 'lodash-es'; import { cloneDeepForLog, isDebugMode } from "./utils.js"; const ERR_NO_SSR_WATCH = 'GlobalState must not be watched at server side'; var _asyncDataAbortCallbacks = /*#__PURE__*/new WeakMap(); var _dependencies = /*#__PURE__*/new WeakMap(); var _initialState = /*#__PURE__*/new WeakMap(); var _watchers = /*#__PURE__*/new WeakMap(); var _nextNotifierId = /*#__PURE__*/new WeakMap(); var _currentState = /*#__PURE__*/new WeakMap(); export default class GlobalState { /** * Creates a new global state object. * @param initialState Intial global state content. * @param ssrContext Server-side rendering context. */ constructor(initialState, ssrContext) { _classPrivateFieldInitSpec(this, _asyncDataAbortCallbacks, new Map()); _classPrivateFieldInitSpec(this, _dependencies, new Map()); _classPrivateFieldInitSpec(this, _initialState, void 0); // TODO: It is tempting to replace watchers here by // Emitter from @dr.pogodin/js-utils, but we need to clone // current watchers for emitting later, and this is not something // Emitter supports right now. _classPrivateFieldInitSpec(this, _watchers, []); _classPrivateFieldInitSpec(this, _nextNotifierId, void 0); _classPrivateFieldInitSpec(this, _currentState, void 0); _classPrivateFieldSet(_currentState, this, initialState); _classPrivateFieldSet(_initialState, this, initialState); if (ssrContext) { /* eslint-disable no-param-reassign */ ssrContext.dirty = false; ssrContext.pending = []; ssrContext.state = _classPrivateFieldGet(_currentState, this); /* eslint-enable no-param-reassign */ this.ssrContext = ssrContext; } if (process.env.NODE_ENV !== 'production' && isDebugMode()) { /* eslint-disable no-console */ let msg = 'New ReactGlobalState created'; if (ssrContext) msg += ' (SSR mode)'; console.groupCollapsed(msg); console.log('Initial state:', cloneDeepForLog(initialState)); console.groupEnd(); /* eslint-enable no-console */ } } /** * Returns the number of currently registered async data abort callbacks, * just for the sake of testing the library. */ get numAsyncDataAbortCallbacks() { return _classPrivateFieldGet(_asyncDataAbortCallbacks, this).size; } /** * If `aborted` is "true" and there is an abort callback registered for * the specified operation, it triggers the callback. Then, in any case, * it drops the callback. */ asyncDataLoadDone(opid, aborted) { var _classPrivateFieldGet2; if (aborted) (_classPrivateFieldGet2 = _classPrivateFieldGet(_asyncDataAbortCallbacks, this).get(opid)) === null || _classPrivateFieldGet2 === void 0 || _classPrivateFieldGet2(); _classPrivateFieldGet(_asyncDataAbortCallbacks, this).delete(opid); } /** * Drops the record of dependencies, if any, for the given path. */ dropDependencies(path) { _classPrivateFieldGet(_dependencies, this).delete(path); } /** * Checks if given `deps` are different from previously recorded ones for * the given `path`. If they are, `deps` are recorded as the new deps for * the `path`, and also the array is frozen, to prevent it from being * modified. * * TODO: This may not work as expected if path string is not normalized, * and the for the same path different alternative ways to spell it down * are used. We should normalize given path here, I guess, or on a higher * level in the logic? */ hasChangedDependencies(path, deps) { const prevDeps = _classPrivateFieldGet(_dependencies, this).get(path); let changed = (prevDeps === null || prevDeps === void 0 ? void 0 : prevDeps.length) !== deps.length; for (let i = 0; !changed && i < deps.length; ++i) { changed = prevDeps[i] !== deps[i]; } _classPrivateFieldGet(_dependencies, this).set(path, Object.freeze(deps)); return changed; } /** * Gets entire state, the same way as .get(null, opts) would do. * @param opts.initialState * @param opts.initialValue */ getEntireState(opts) { let state = opts !== null && opts !== void 0 && opts.initialState ? _classPrivateFieldGet(_initialState, this) : _classPrivateFieldGet(_currentState, this); if (state !== undefined || (opts === null || opts === void 0 ? void 0 : opts.initialValue) === undefined) return state; const iv = opts.initialValue; state = typeof iv === 'function' ? iv() : iv; if (_classPrivateFieldGet(_currentState, this) === undefined) this.setEntireState(state); return state; } /** * Notifies all connected state watchers that a state update has happened. */ notifyStateUpdate(path, value) { if (process.env.NODE_ENV !== 'production' && isDebugMode()) { /* eslint-disable no-console */ const p = typeof path === 'string' ? `"${path}"` : 'none (entire state update)'; console.groupCollapsed(`ReactGlobalState update. Path: ${p}`); console.log('New value:', cloneDeepForLog(value, path !== null && path !== void 0 ? path : '')); console.log('New state:', cloneDeepForLog(_classPrivateFieldGet(_currentState, this))); console.groupEnd(); /* eslint-enable no-console */ } if (this.ssrContext) { this.ssrContext.dirty = true; this.ssrContext.state = _classPrivateFieldGet(_currentState, this); } else if (!_classPrivateFieldGet(_nextNotifierId, this)) { _classPrivateFieldSet(_nextNotifierId, this, setTimeout(() => { _classPrivateFieldSet(_nextNotifierId, this, undefined); const watchers = [..._classPrivateFieldGet(_watchers, this)]; for (const watcher of watchers) watcher(); })); } } /** * Registers an abort callback for an async data retrieval operation with * the given operation ID. Throws if already registered. */ setAsyncDataAbortCallback(opid, cb) { _classPrivateFieldGet(_asyncDataAbortCallbacks, this).set(opid, cb); } /** * Sets entire state, the same way as .set(null, value) would do. * @param value */ setEntireState(value) { if (_classPrivateFieldGet(_currentState, this) !== value) { _classPrivateFieldSet(_currentState, this, value); this.notifyStateUpdate(null, value); } return value; } /** * Gets current or initial value at the specified "path" of the global state. * @param path Dot-delimitered state path. * @param options Additional options. * @param options.initialState If "true" the value will be read * from the initial state instead of the current one. * @param options.initialValue If the value read from the "path" is * "undefined", this "initialValue" will be returned instead. In such case * "initialValue" will also be written to the "path" of the current global * state (no matter "initialState" flag), if "undefined" is stored there. * @return Retrieved value. */ // .get() without arguments just falls back to .getEntireState(). // This variant attempts to automatically resolve and check the type of value // at the given path, as precise as the actual state and path types permit. // If the automatic path resolution is not possible, the ValueT fallsback // to `never` (or to `undefined` in some cases), effectively forbidding // to use this .get() variant. // This variant is not callable by default (without generic arguments), // otherwise it allows to set the correct ValueT directly. get(path, opts) { if (typeof path !== 'string') { const res = this.getEntireState(opts); return res; } const state = opts !== null && opts !== void 0 && opts.initialState ? _classPrivateFieldGet(_initialState, this) : _classPrivateFieldGet(_currentState, this); let res = get(state, path); if (res !== undefined || (opts === null || opts === void 0 ? void 0 : opts.initialValue) === undefined) return res; const iv = opts.initialValue; res = typeof iv === 'function' ? iv() : iv; // TODO: Revise. // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression if (!opts.initialState || this.get(path) === undefined) { this.set(path, res); } return res; } /** * Writes the `value` to given global state `path`. * @param path Dot-delimitered state path. If not given, entire * global state content is replaced by the `value`. * @param value The value. * @return Given `value` itself. */ // This variant attempts automatic value type resolution & checking. // This variant is disabled by default, otherwise allows to give // expected value type explicitly. set(path, value) { if (typeof path !== 'string') return this.setEntireState(value); // TODO: Revise. // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression if (value !== this.get(path)) { const root = { state: _classPrivateFieldGet(_currentState, this) }; let segIdx = 0; // TODO: It is not 100% correct, as `pos` can be an array, or any other // value as we travel through the state tree. To simplify the typing for // now, I guess, we can go with this record type, though. let pos = root; const pathSegments = toPath(`state.${path}`); for (; segIdx < pathSegments.length - 1; segIdx += 1) { const seg = pathSegments[segIdx]; // TODO: Revise: Typing is not quite correct here, but it works fine in the runtime. const next = pos[seg]; if (Array.isArray(next)) pos[seg] = [...next];else if (typeof next === 'object') pos[seg] = { ...next };else { // We arrived to a state sub-segment, where the remaining part of // the update path does not exist yet. We rely on lodash's set() // function to create the remaining path, and set the value. set(pos, pathSegments.slice(segIdx), value); break; } pos = pos[seg]; } if (segIdx === pathSegments.length - 1) { pos[pathSegments[segIdx]] = value; } _classPrivateFieldSet(_currentState, this, root.state); this.notifyStateUpdate(path, value); } return value; } /** * Unsubscribes `callback` from watching state updates; no operation if * `callback` is not subscribed to the state updates. * @param callback * @throws if {@link SsrContext} is attached to the state instance: the state * watching functionality is intended for client-side (non-SSR) only. */ unWatch(callback) { if (this.ssrContext) throw new Error(ERR_NO_SSR_WATCH); const watchers = _classPrivateFieldGet(_watchers, this); const pos = watchers.indexOf(callback); if (pos >= 0) { watchers[pos] = watchers[watchers.length - 1]; watchers.pop(); } } /** * Subscribes `callback` to watch state updates; no operation if * `callback` is already subscribed to this state instance. * @param callback It will be called without any arguments every * time the state content changes (note, howhever, separate state updates can * be applied to the state at once, and watching callbacks will be called once * after such bulk update). * @throws if {@link SsrContext} is attached to the state instance: the state * watching functionality is intended for client-side (non-SSR) only. */ watch(callback) { if (this.ssrContext) throw new Error(ERR_NO_SSR_WATCH); const watchers = _classPrivateFieldGet(_watchers, this); if (!watchers.includes(callback)) { watchers.push(callback); } } } //# sourceMappingURL=GlobalState.js.map