@zedux/atoms
Version:
A Molecular State Engine for React
375 lines (374 loc) • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AtomInstance = void 0;
const core_1 = require("@zedux/core");
const plugin_actions_1 = require("../../utils/plugin-actions");
const promiseUtils_1 = require("../../utils/promiseUtils");
const AtomApi_1 = require("../AtomApi");
const AtomInstanceBase_1 = require("./AtomInstanceBase");
const StoreState = 1;
const RawState = 2;
const getStateType = (val) => {
if ((0, core_1.is)(val, core_1.Store))
return StoreState;
return RawState;
};
const getStateStore = (factoryResult) => {
const stateType = getStateType(factoryResult);
const stateStore = stateType === StoreState
? factoryResult
: (0, core_1.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];
};
class AtomInstance extends AtomInstanceBase_1.AtomInstanceBase {
constructor(ecosystem, template, id, params) {
super();
this.ecosystem = ecosystem;
this.template = template;
this.id = id;
this.params = params;
this.status = 'Initializing';
this.nextReasons = [];
/**
* 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._scheduleEvaluation = (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.status === 'Destroyed')
return;
this.nextReasons.push(reason);
if (this.nextReasons.length > 1)
return; // job already scheduled
this.ecosystem._scheduler.schedule({
id: this.id,
task: this.evaluationTask,
type: 2, // EvaluateGraphNode (2)
}, shouldSetTimeout);
};
this.evaluationTask = () => this._evaluationTask();
this._createdAt = ecosystem._idGenerator.now();
// lol
this.exports = this.exports;
this.promise = this.promise;
this.store = this.store;
this._promiseStatus = this._promiseStatus;
}
/**
* Detach this atom instance from the ecosystem and clean up all graph edges
* and other subscriptions/effects created by this atom instance.
*
* Destruction will bail out if this atom instance still has dependents. Pass
* `true` to force-destroy the atom instance anyway.
*/
destroy(force) {
var _a, _b, _c, _d;
if (this.status === 'Destroyed')
return;
// If we're not force-destroying, don't destroy if there are dependents
if (!force && ((_a = this.ecosystem._graph.nodes[this.id]) === null || _a === void 0 ? void 0 : _a.refCount)) {
return;
}
(_b = this._cancelDestruction) === null || _b === void 0 ? void 0 : _b.call(this);
this._cancelDestruction = undefined;
this._setStatus('Destroyed');
if (this.nextReasons.length) {
this.ecosystem._scheduler.unschedule(this.evaluationTask);
}
// Clean up effect injectors first, then everything else
const nonEffectInjectors = [];
(_c = this._injectors) === null || _c === void 0 ? void 0 : _c.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);
});
this.ecosystem._graph.removeDependencies(this.id);
(_d = this._subscription) === null || _d === void 0 ? void 0 : _d.unsubscribe();
this.ecosystem._destroyAtomInstance(this.id);
}
/**
* An alias for `instance.store.getState()`. Returns the current state of this
* atom instance's store.
*/
getState() {
return this.store.getState();
}
/**
* Force this atom instance to reevaluate.
*/
invalidate(operation = 'invalidate', sourceType = 'External') {
this._scheduleEvaluation({
operation,
sourceType,
type: 'cache invalidated',
}, false);
// run the scheduler synchronously after invalidation
this.ecosystem._scheduler.flush();
}
get _infusedSetter() {
if (this._set)
return this._set;
const setState = (settable, meta) => this.setState(settable, meta);
return (this._set = Object.assign(setState, this.exports));
}
_init() {
const factoryResult = this._doEvaluate();
[this._stateType, this.store] = getStateStore(factoryResult);
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);
});
this._setStatus('Active');
this.ecosystem._graph.flushUpdates();
// hydrate if possible
const hydration = this.ecosystem._consumeHydration(this);
if (this.template.manualHydration || typeof hydration === 'undefined') {
return;
}
this.store.setState(hydration);
}
/**
* When a standard atom instance's refCount hits 0 and a ttl is set, we set a
* timeout to destroy this atom instance.
*/
_scheduleDestruction() {
// the atom is already scheduled for destruction or destroyed
if (this.status !== 'Active')
return;
this._setStatus('Stale');
const ttl = this._getTtl();
if (ttl == null || ttl === -1)
return;
if (ttl === 0)
return this.destroy();
if (typeof ttl === 'number') {
// ttl is > 0; schedule destruction
const timeoutId = setTimeout(() => {
this._cancelDestruction = undefined;
this.destroy();
}, ttl);
// TODO: dispatch an action over stateStore for these mutations
this._cancelDestruction = () => {
this._setStatus('Active');
this._cancelDestruction = undefined;
clearTimeout(timeoutId);
};
return;
}
if (typeof ttl.then === 'function') {
let isCanceled = false;
Promise.allSettled([ttl]).then(() => {
this._cancelDestruction = undefined;
if (!isCanceled)
this.destroy();
});
this._cancelDestruction = () => {
this._setStatus('Active');
this._cancelDestruction = undefined;
isCanceled = true;
};
return;
}
// ttl is an observable; destroy as soon as it emits
const subscription = ttl.subscribe(() => {
this._cancelDestruction = undefined;
this.destroy();
});
this._cancelDestruction = () => {
this._setStatus('Active');
this._cancelDestruction = undefined;
subscription.unsubscribe();
};
}
_doEvaluate() {
const { _evaluationStack, _graph } = this.ecosystem;
this._nextInjectors = [];
let newFactoryResult;
_evaluationStack.start(this);
this._isEvaluating = true;
_graph.bufferUpdates(this.id);
try {
newFactoryResult = this._evaluate();
}
catch (err) {
this._nextInjectors.forEach(injector => {
var _a;
(_a = injector.cleanup) === null || _a === void 0 ? void 0 : _a.call(injector);
});
this._nextInjectors = undefined;
_graph.destroyBuffer();
throw err;
}
finally {
_evaluationStack.finish();
this._isEvaluating = false;
// 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.prevReasons = this.nextReasons;
this.nextReasons = [];
}
this._injectors = this._nextInjectors;
this._nextInjectors = undefined;
if (this.status !== 'Initializing') {
// let this._init flush updates after status is set to Active
_graph.flushUpdates();
}
return newFactoryResult;
}
/**
* 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
*/
_evaluate() {
var _a;
const { _value } = this.template;
if (typeof _value !== 'function') {
return _value;
}
try {
const val = _value(...this.params);
if (!(0, core_1.is)(val, AtomApi_1.AtomApi))
return val;
const api = (this.api = val);
// Exports can only be set on initial evaluation
if (this.status === '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.template.key}" with params:`, this.params, err);
throw err;
}
}
_evaluationTask() {
const newFactoryResult = this._doEvaluate();
const newStateType = getStateType(newFactoryResult);
if (true /* DEV */ && newStateType !== this._stateType) {
throw new Error(`Zedux: atom factory for atom "${this.template.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.template.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);
}
}
_getTtl() {
var _a, _b, _c;
if (((_a = this.api) === null || _a === void 0 ? void 0 : _a.ttl) == null) {
return (_b = this.template.ttl) !== null && _b !== void 0 ? _b : (_c = this.ecosystem.atomDefaults) === null || _c === void 0 ? void 0 : _c.ttl;
}
// this atom instance set its own ttl
const { ttl } = this.api;
return typeof ttl === 'function' ? ttl() : ttl;
}
_handleStateChange(newState, oldState, action) {
this.ecosystem._graph.scheduleDependents(this.id, this.nextReasons, newState, oldState, false);
if (this.ecosystem._mods.stateChanged) {
this.ecosystem.modBus.dispatch(plugin_actions_1.pluginActions.stateChanged({
action,
instance: this,
newState,
oldState,
reasons: this.nextReasons,
}));
}
// run the scheduler synchronously after any atom instance state update
if (action.meta !== core_1.zeduxTypes.batch) {
this.ecosystem._scheduler.flush();
}
}
_setStatus(newStatus) {
const oldStatus = this.status;
this.status = newStatus;
if (this.ecosystem._mods.statusChanged) {
this.ecosystem.modBus.dispatch(plugin_actions_1.pluginActions.statusChanged({
newStatus,
node: this,
oldStatus,
}));
}
}
_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((0, promiseUtils_1.getSuccessPromiseState)(data));
})
.catch(error => {
if (this.promise !== promise)
return;
this._promiseStatus = 'error';
this._promiseError = error;
if (!isStateUpdater)
return;
this.store.setState((0, promiseUtils_1.getErrorPromiseState)(error));
});
const state = (0, promiseUtils_1.getInitialPromiseState)(currentState === null || currentState === void 0 ? void 0 : currentState.data);
this._promiseStatus = state.status;
this.ecosystem._graph.scheduleDependents(this.id, this.nextReasons, undefined, undefined, true, 'promise changed', 'Updated', true);
return state;
}
}
exports.AtomInstance = AtomInstance;