UNPKG

@zedux/atoms

Version:

A Molecular State Engine for React

336 lines (335 loc) 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Graph = void 0; const index_1 = require("../utils/index"); const plugin_actions_1 = require("../utils/plugin-actions"); /** * When a node's refCount hits 0, schedule destruction of that node. */ const scheduleNodeDestruction = ({ ecosystem, nodes }, nodeId) => { const node = nodes[nodeId]; if (node && !node.refCount) { if (node.isSelector) { ecosystem.selectors._destroySelector(nodeId); } else { ecosystem._instances[nodeId]._scheduleDestruction(); } } }; /** * When a node's refCount hits 0, we schedule destruction of that node. If * that destruction is still pending and the refCount goes back up to 1, * cancel the scheduled destruction. */ const unscheduleNodeDestruction = ({ ecosystem, nodes }, nodeId) => { var _a, _b; const dependency = nodes[nodeId]; if (!dependency.isSelector) { (_b = (_a = ecosystem._instances[nodeId])._cancelDestruction) === null || _b === void 0 ? void 0 : _b.call(_a); } }; class Graph { constructor(ecosystem) { this.ecosystem = ecosystem; this.nodes = {}; this.updateStack = []; } /** * Draw a new edge between two nodes in the graph. This is how dependencies * are created between atoms, selectors, and external nodes like React * components. */ addEdge(dependentKey, dependencyKey, operation, flags, callback) { const { ecosystem, updateStack } = this; const newEdge = { callback, createdAt: ecosystem._idGenerator.now(), flags, operation, }; if (!updateStack.length) { return this.finishAddingEdge(dependentKey, dependencyKey, newEdge); } // We're buffering updates! Buffer this one const { dependencies, key } = updateStack[updateStack.length - 1]; if (true /* DEV */ && key !== dependentKey) { throw new Error("Zedux: Tried updating dependencies of a graph node that isn't currently evaluating. This means that either you tried updating the graph manually or there is a bug in Zedux. If it's a bug, please report it!"); } const dependency = dependencies.get(dependencyKey); // Don't overwrite a higher-prio edge with a lower one. Also ignore same-prio if (!dependency || dependency.flags > flags) { dependencies.set(dependencyKey, newEdge); return newEdge; } // if this edge was ignored, return the existing buffered edge return dependency; } // Should only be used internally addNode(nodeId, isSelector) { if (this.nodes[nodeId]) return; // already added this.nodes[nodeId] = { dependencies: new Map(), dependents: new Map(), isSelector, refCount: 0, weight: 1, // this node doesn't have dependencies yet; its weight is 1 }; } /** * Prevent new graph edges from being added immediately. Instead, buffer them * so we can prevent duplicates or unnecessary edges. Call `.flushUpdates()` * to finish buffering. * * This is used during atom and AtomSelector evaluation to make the graph as * efficient as possible. */ bufferUpdates(key) { this.updateStack.push({ key, dependencies: new Map() }); } /** * If an atom instance or AtomSelector errors during evaluation, we need to * destroy any instances or AtomSelectors created during that evaluation that * now have no dependents. */ destroyBuffer() { const { dependencies, key } = this.updateStack[this.updateStack.length - 1]; const edges = this.nodes[key].dependencies; for (const dependencyKey of dependencies.keys()) { // the edge wasn't created during the evaluation that errored; keep it if (edges.get(dependencyKey)) continue; scheduleNodeDestruction(this, dependencyKey); } this.updateStack.pop(); } /** * Stop buffering updates for the node passed to `.bufferUpdates()` and add * the buffered edges to the graph. */ flushUpdates() { const { nodes, updateStack } = this; const { dependencies, key } = updateStack[updateStack.length - 1]; const edges = nodes[key].dependencies; // remove any edges that were not recreated while buffering. Don't remove // anything but implicit-internal edges (those are the only kind we // auto-create during evaluation - other types may have been added manually // by the user and we don't want to touch them here) for (const dependencyKey of edges.keys()) { // this cast should be fine const existingEdge = nodes[dependencyKey].dependents.get(key); if (existingEdge.flags & (index_1.Explicit | index_1.External)) continue; const edgeToAdd = dependencies.get(dependencyKey); // if edge still exists, keep it if (!edgeToAdd || edgeToAdd.flags !== existingEdge.flags) { this.removeEdge(key, dependencyKey); } } // add new edges that we tracked while buffering for (const [dependencyKey, edgeToAdd] of dependencies.entries()) { const existingEdge = edges.get(dependencyKey); // if edge already exists, keep it if (!existingEdge) { this.finishAddingEdge(key, dependencyKey, edgeToAdd); } } updateStack.pop(); } removeDependencies(dependentKey) { const node = this.nodes[dependentKey]; if (!node) return; // node already destroyed const edges = node.dependencies; if (!edges) return; // TODO: delete for (const dependencyKey of edges.keys()) { this.removeEdge(dependentKey, dependencyKey); } } /** * Should only be used internally. Remove the graph edge between two nodes. * The dependent may not exist as a node in the graph if it's external, e.g. a * React component * * For some reason in React 18+, React destroys parents before children. This * means a parent EcosystemProvider may have already unmounted and wiped the * whole graph; this edge may already be destroyed. */ removeEdge(dependentKey, dependencyKey) { const dependency = this.nodes[dependencyKey]; const dependent = this.nodes[dependentKey]; // won't exist if external // erase graph edge between dependent and dependency if (dependent) { dependent.dependencies.delete(dependencyKey); } // hmm could maybe happen when a dependency was force-destroyed if a child // tries to destroy its edge before recreating it (I don't think we ever do // that though) if (!dependency) return; const dependentEdge = dependency.dependents.get(dependentKey); // happens in React 18+ (see this method's jsdoc above) if (!dependentEdge) { // useAtomSelector can delete edges that were never drawn 'cause React // StrictMode double-renders can create an entirely new SelectorCache // before the old one received dependents. Delete the old one in this case if (dependency.isSelector) scheduleNodeDestruction(this, dependencyKey); return; } dependency.dependents.delete(dependentKey); dependency.refCount--; // static dependencies don't change a node's weight if (!(dependentEdge.flags & index_1.Static)) { this.recalculateNodeWeight(dependentKey, -dependency.weight); } const { _instances, _mods, _scheduler, modBus, selectors } = this.ecosystem; if (dependentEdge.task) { _scheduler.unschedule(dependentEdge.task); } if (_mods.edgeRemoved) { modBus.dispatch(plugin_actions_1.pluginActions.edgeRemoved({ dependency: _instances[dependencyKey] || selectors._items[dependencyKey], dependent: _instances[dependentKey] || selectors._items[dependentKey] || dependentKey, edge: dependentEdge, })); } scheduleNodeDestruction(this, dependencyKey); } // Should only be used internally removeNode(nodeId) { const node = this.nodes[nodeId]; if (!node) return; // already removed // We don't need to remove this dependent from its dependencies here - the // atom instance/AtomSelector will have removed all its deps before calling // this function as part of its destruction process // if an atom instance is force-destroyed, it could still have dependents. // Inform them of the destruction this.scheduleDependents(nodeId, [], undefined, undefined, true, 'node destroyed', 'Destroyed', true); // Remove this dependency from all its dependents and recalculate all // weights recursively for (const [dependentKey, dependentEdge] of node.dependents.entries()) { if (!(dependentEdge.flags & index_1.Static)) { this.recalculateNodeWeight(dependentKey, -node.weight); } const dependentNode = this.nodes[dependentKey]; if (dependentNode) dependentNode.dependencies.delete(nodeId); // we _probably_ don't need to send edgeRemoved mod events to plugins for // these - it's better that they receive the duplicate edgeCreated event // when the dependency is recreated by its dependent(s) so they can infer // that the edge was "moved" } delete this.nodes[nodeId]; } /** * Schedules a job to update all dependents of a node. This is called e.g. * when an atom instance or AtomSelector updates, when an atom instance is * force-destroyed, or when an atom instance's promise changes. */ scheduleDependents(nodeId, reasons, newState, oldState, shouldSetTimeout, type = 'state changed', signal = 'Updated', scheduleStaticDeps = false) { const { _instances, _scheduler, selectors } = this.ecosystem; const instance = _instances[nodeId]; const cache = selectors._items[nodeId]; const node = this.nodes[nodeId]; for (const [dependentKey, dependentEdge] of node.dependents.entries()) { // if edge.task exists, this edge has already been scheduled if (dependentEdge.task) { if (signal !== 'Destroyed') continue; // destruction jobs supersede update jobs; cancel the existing job so we // can create a new one for the destruction _scheduler.unschedule(dependentEdge.task); } // Static deps don't update on state change. Dynamic deps don't update on // promise change. Both types update on instance force-destruction const isStatic = dependentEdge.flags & index_1.Static; if (isStatic && !scheduleStaticDeps) continue; const reason = { newState, oldState, operation: dependentEdge.operation, reasons, sourceId: nodeId, sourceType: node.isSelector ? 'AtomSelector' : 'Atom', type, }; // let internal dependents (other atoms and AtomSelectors) schedule their // own jobs if (!(dependentEdge.flags & index_1.External)) { if (this.nodes[dependentKey].isSelector) { selectors._scheduleEvaluation(dependentKey, reason, shouldSetTimeout); continue; } _instances[dependentKey]._scheduleEvaluation(reason, shouldSetTimeout); continue; } // schedule external dependents const task = () => { var _a; dependentEdge.task = undefined; (_a = dependentEdge.callback) === null || _a === void 0 ? void 0 : _a.call(dependentEdge, signal, instance ? instance.store.getState() : cache.result, // don't use the snapshotted newState above reason); }; _scheduler.schedule({ flags: dependentEdge.flags, task, type: 3, // UpdateExternalDependent (3) }, shouldSetTimeout); // mutate the edge; give it the scheduled task so it can be cleaned up dependentEdge.task = task; } } /** * Actually add an edge to the graph. When we buffer graph updates, we're * really just deferring the calling of this method. */ finishAddingEdge(dependentKey, dependencyKey, newEdge) { const dependency = this.nodes[dependencyKey]; if (!dependency) return; // happened once for some reason // draw graph edge between dependent and dependency if (!(newEdge.flags & index_1.External)) { this.nodes[dependentKey].dependencies.set(dependencyKey, true); } dependency.dependents.set(dependentKey, newEdge); dependency.refCount++; unscheduleNodeDestruction(this, dependencyKey); // static dependencies don't change a node's weight if (!(newEdge.flags & index_1.Static)) { this.recalculateNodeWeight(dependentKey, dependency.weight); } const { _instances, _mods, modBus, selectors } = this.ecosystem; if (_mods.edgeCreated) { modBus.dispatch(plugin_actions_1.pluginActions.edgeCreated({ dependency: _instances[dependencyKey] || selectors._items[dependencyKey], dependent: _instances[dependentKey] || selectors._items[dependentKey] || dependentKey, edge: newEdge, })); } return newEdge; } /** * When a non-static edge is added or removed, every node below that edge (the * dependent, its dependents, etc) in the graph needs to have its weight * recalculated. */ recalculateNodeWeight(nodeId, weightDiff) { const node = this.nodes[nodeId]; if (!node) return; // happens when node is external node.weight += weightDiff; for (const dependentKey of node.dependents.keys()) { this.recalculateNodeWeight(dependentKey, weightDiff); } } } exports.Graph = Graph;