import { ActionFactoryPayloadType, Store, is } from '@zedux/core' import { AnyAtomInstance, AnyAtomTemplate, AtomGetters, AtomParamsType, GraphEdgeInfo, Selectable, } from '../types/index' import { StackItem, Static } from '../utils/index' import { pluginActions } from '../utils/plugin-actions' import { Ecosystem } from './Ecosystem' import { SelectorCache } from './Selectors' import { AtomInstanceBase } from './instances/AtomInstanceBase' /** * A stack of AtomInstances and AtomSelectors that are currently evaluating - * innermost instance/selector (the one that's actually currently evaluating) at * the end of the array. * * This has to live in the module scope so `readInstance` can access it without * any ecosystem context. That's how injectors work. */ export let stack: StackItem[] = [] export const readInstance = () => { const item = stack[stack.length - 1] if (DEV && !is(item?.node, AtomInstanceBase)) { throw new Error('Zedux: Injectors can only be used in atom state factories') } return item.node as AnyAtomInstance } export const setStack = (newStack: StackItem[]) => (stack = newStack) export class EvaluationStack { public atomGetters: AtomGetters constructor(private readonly ecosystem: Ecosystem) { const { _graph, selectors } = ecosystem const get: AtomGetters['get'] = ((atom, params) => { const { id, store } = ecosystem.getInstance(atom, params) // when called outside evaluation, get() is just an alias for // ecosystem.get() if (!stack.length) return store.getState() // if get is called during evaluation, track the required atom instances so // we can add graph edges for them _graph.addEdge(stack[stack.length - 1].node.id, id, 'get', 0) return store.getState() }) as AtomGetters['get'] const getInstance: AtomGetters['getInstance'] = ( atom: A, params?: AtomParamsType, edgeInfo?: GraphEdgeInfo ) => { const instance = ecosystem.getInstance(atom, params as AtomParamsType) // when called outside evaluation, getInstance() is just an alias for // ecosystem.getInstance() if (!stack.length) return instance // if getInstance is called during evaluation, track the required atom // instances so we can add graph edges for them _graph.addEdge( stack[stack.length - 1].node.id, instance.id, edgeInfo?.[1] || 'getInstance', edgeInfo?.[0] ?? Static ) return instance } const select: AtomGetters['select'] = ( selectable: Selectable, ...args: Args ) => { // when called outside evaluation, select() is just an alias for // ecosystem.select() if (!stack.length) { return ecosystem.select(selectable, ...args) } const cache = selectors.getCache(selectable, args) _graph.addEdge(stack[stack.length - 1].node.id, cache.id, 'select', 0) return cache.result as T } this.atomGetters = { ecosystem, get, getInstance, select, } } public finish() { const item = stack.pop() const { _idGenerator, _mods, modBus } = this.ecosystem // if we just popped the last thing off the stack, restore the default // scheduler if (!stack.length) Store._scheduler = undefined if (!item || !_mods.evaluationFinished) return const time = item.start ? _idGenerator.now(true) - item.start : 0 const action: ActionFactoryPayloadType< typeof pluginActions.evaluationFinished > = { node: item.node, time } modBus.dispatch(pluginActions.evaluationFinished(action)) } public read(): StackItem | undefined { return stack[stack.length - 1] } public start(node: AnyAtomInstance | SelectorCache) { const { _idGenerator, _mods, _scheduler } = this.ecosystem const newItem: StackItem = { node, start: _mods.evaluationFinished ? _idGenerator.now(true) : undefined, } stack.push(newItem) // all stores created during evaluation automatically belong to the ecosystem Store._scheduler = _scheduler } }