@zedux/stores
Version:
The legacy composable store model of Zedux
303 lines (302 loc) • 11.4 kB
JavaScript
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`);