UNPKG

@zedux/stores

Version:

The legacy composable store model of Zedux

303 lines (302 loc) 11.4 kB
import { createStore, zeduxTypes, is, Store, } from '@zedux/core'; import { AtomInstance as NewAtomInstance, ZeduxPlugin, zi, } from '@zedux/atoms'; import { AtomApi } from './AtomApi.js'; import { Invalidate, prefix, PromiseChange, getErrorPromiseState, getInitialPromiseState, getSuccessPromiseState, } from './atoms-port.js'; const StoreState = 1; const RawState = 2; const getStateType = (val) => { if (is(val, Store)) return StoreState; return RawState; }; const getStateStore = (factoryResult) => { const stateType = getStateType(factoryResult); const stateStore = stateType === StoreState ? factoryResult : createStore(); // define how we populate our store (doesn't apply to user-supplied stores) if (stateType === RawState) { stateStore.setState(typeof factoryResult === 'function' ? () => factoryResult : factoryResult); } return [stateType, stateStore]; }; export class AtomInstance extends NewAtomInstance { constructor( /** * @see NewAtomInstance.e */ e, /** * @see NewAtomInstance.t */ t, /** * @see NewAtomInstance.id */ id, /** * @see NewAtomInstance.p */ p) { super(e, t, id, p); this.e = e; this.t = t; this.id = id; this.p = p; /** * An alias for `.store.dispatch()` */ this.dispatch = (action) => this.store.dispatch(action); /** * An alias for `.store.setState()` */ this.setState = (settable, meta) => this.store.setState(settable, meta); /** * An alias for `.store.setStateDeep()` */ this.setStateDeep = (settable, meta) => this.store.setStateDeep(settable, meta); this._createdAt = e._idGenerator.now(); } /** * @see NewAtomInstance.destroy */ destroy(force) { var _a, _b; if (!zi.b(this, force)) return; // Clean up effect injectors first, then everything else const nonEffectInjectors = []; (_a = this._injectors) === null || _a === void 0 ? void 0 : _a.forEach(injector => { var _a; if (injector.type !== '@@zedux/effect') { nonEffectInjectors.push(injector); return; } (_a = injector.cleanup) === null || _a === void 0 ? void 0 : _a.call(injector); }); nonEffectInjectors.forEach(injector => { var _a; (_a = injector.cleanup) === null || _a === void 0 ? void 0 : _a.call(injector); }); (_b = this._subscription) === null || _b === void 0 ? void 0 : _b.unsubscribe(); zi.e(this); } /** * An alias for `instance.store.getState()`. Returns the current state of this * atom instance's store. * * @deprecated - use `.get()` instead @see AtomInstance.get */ getState() { return this.store.getState(); } /** * @see NewAtomInstance.get * * An alias for `instance.store.getState()`. */ get() { return this.store.getState(); } /** * Force this atom instance to reevaluate. */ invalidate() { this.r({ t: Invalidate }, false); // run the scheduler synchronously after invalidation this.e._scheduler.flush(); } /** * `.mutate()` is not supported in legacy, store-based atoms. Upgrade to the * new `atom()` factory. */ mutate() { throw new Error('`.mutate()` is not supported in legacy, store-based atoms. Upgrade to the new `atom()` factory'); } set(settable, events) { return this.setState(settable, events && Object.keys(events)[0]); } /** * @see NewAtomInstance.j */ j() { const { n, s } = zi.g(); this._nextInjectors = []; this._isEvaluating = true; // all stores created during evaluation automatically belong to the // ecosystem. This is brittle. It's the only piece of Zedux that isn't // cross-window compatible. The store package would ideally have its own // scheduler. Unfortunately, we're probably never focusing on that since the // real ideal is to move off stores completely in favor of signals. Store._scheduler = this.e._scheduler; zi.s(this); try { const newFactoryResult = this._eval(); if (this.l === 'Initializing') { ; [this._stateType, this.store] = getStateStore(newFactoryResult); this._subscription = this.store.subscribe((newState, oldState, action) => { // buffer updates (with cache size of 1) if this instance is currently // evaluating if (this._isEvaluating) { this._bufferedUpdate = { newState, oldState, action }; return; } this._handleStateChange(newState, oldState, action); }); } else { const newStateType = getStateType(newFactoryResult); if (true /* DEV */ && newStateType !== this._stateType) { throw new Error(`Zedux: atom factory for atom "${this.t.key}" returned a different type than the previous evaluation. This can happen if the atom returned a store initially but then returned a non-store value on a later evaluation or vice versa`); } if (true /* DEV */ && newStateType === StoreState && newFactoryResult !== this.store) { throw new Error(`Zedux: atom factory for atom "${this.t.key}" returned a different store. Did you mean to use \`injectStore()\`, or \`injectMemo()\`?`); } // there is no way to cause an evaluation loop when the StateType is Value if (newStateType === RawState) { this.store.setState(typeof newFactoryResult === 'function' ? () => newFactoryResult : newFactoryResult); } } } catch (err) { this._nextInjectors.forEach(injector => { var _a; (_a = injector.cleanup) === null || _a === void 0 ? void 0 : _a.call(injector); }); zi.d(n, s); throw err; } finally { this._isEvaluating = false; // if we just popped the last thing off the stack, restore the default // scheduler if (!n) Store._scheduler = undefined; // even if evaluation errored, we need to update dependents if the store's // state changed if (this._bufferedUpdate) { this._handleStateChange(this._bufferedUpdate.newState, this._bufferedUpdate.oldState, this._bufferedUpdate.action); this._bufferedUpdate = undefined; } this.w = []; } this._injectors = this._nextInjectors; if (this.l !== 'Initializing') { // let this.i flush updates after status is set to Active zi.f(n, s); } } /** * @see NewAtomInstance.r */ r(reason, shouldSetTimeout) { // TODO: Any calls in this case probably indicate a memory leak on the // user's part. Notify them. TODO: Can we pause evaluations while // status is Stale (and should we just always evaluate once when // waking up a stale atom)? if (this.l !== 'Destroyed' && this.w.push(reason) === 1) { // refCount just hit 1; we haven't scheduled a job for this node yet this.e._scheduler.schedule(this, shouldSetTimeout); } } get _infusedSetter() { if (this._set) return this._set; const setState = (settable, meta) => this.setState(settable, meta); return (this._set = Object.assign(setState, this.exports)); } /** * A standard atom's value can be one of: * * - A raw value * - A Zedux store * - A function that returns a raw value * - A function that returns a Zedux store * - A function that returns an AtomApi */ _eval() { var _a; const { _value } = this.t; if (typeof _value !== 'function') { return _value; } try { const val = _value(...this.p); if (!is(val, AtomApi)) return val; const api = (this.api = val); // Exports can only be set on initial evaluation if (this.l === 'Initializing' && api.exports) { this.exports = api.exports; } // if api.value is a promise, we ignore api.promise if (typeof ((_a = api.value) === null || _a === void 0 ? void 0 : _a.then) === 'function') { return this._setPromise(api.value, true); } else if (api.promise) { this._setPromise(api.promise); } return api.value; } catch (err) { console.error(`Zedux: Error while evaluating atom "${this.t.key}" with params:`, this.p, err); throw err; } } _handleStateChange(newState, oldState, action) { zi.u({ p: oldState, r: this.w, s: this }, false); if (this.e._mods.stateChanged) { this.e.modBus.dispatch(ZeduxPlugin.actions.stateChanged({ action, node: this, newState, oldState, reasons: this.w, })); } // run the scheduler synchronously after any atom instance state update if (action.meta !== zeduxTypes.batch) { this.e._scheduler.flush(); } } _setPromise(promise, isStateUpdater) { var _a; const currentState = (_a = this.store) === null || _a === void 0 ? void 0 : _a.getState(); if (promise === this.promise) return currentState; this.promise = promise; // since we're the first to chain off the returned promise, we don't need to // track the chained promise - it will run first, before React suspense's // `.then` on the thrown promise, for example promise .then(data => { if (this.promise !== promise) return; this._promiseStatus = 'success'; if (!isStateUpdater) return; this.store.setState(getSuccessPromiseState(data)); }) .catch(error => { if (this.promise !== promise) return; this._promiseStatus = 'error'; this._promiseError = error; if (!isStateUpdater) return; this.store.setState(getErrorPromiseState(error)); }); const state = getInitialPromiseState(currentState === null || currentState === void 0 ? void 0 : currentState.data); this._promiseStatus = state.status; zi.u({ s: this, t: PromiseChange }, true, true); return state; } } AtomInstance.$$typeof = Symbol.for(`${prefix}/AtomInstance`);