@zedux/atoms
Version:
A Molecular State Engine for React
303 lines (302 loc) • 11.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Selectors = exports.SelectorCache = void 0;
const core_1 = require("@zedux/core");
const index_1 = require("../utils/index");
const plugin_actions_1 = require("../utils/plugin-actions");
const defaultResultsComparator = (a, b) => a === b;
class SelectorCache {
constructor(id, selectorRef, args) {
this.id = id;
this.selectorRef = selectorRef;
this.args = args;
this.nextReasons = [];
}
}
exports.SelectorCache = SelectorCache;
SelectorCache.$$typeof = Symbol.for(`${index_1.prefix}/SelectorCache`);
/**
* Since AtomSelectors are meant to feel lightweight, they don't have to be
* instances of a class - they'll often be standalone or even inline
* functions. This class handles all the logic that AtomSelectors would handle
* themselves if they were classes - creation, cache management, and
* destruction.
*/
class Selectors {
constructor(ecosystem) {
this.ecosystem = ecosystem;
/**
* Map selectorKey + params id strings to the SelectorCache for the selector
*/
this._items = {};
/**
* Map selectors (or selector config objects) to a base selectorKey that can
* be used to predictably create selectorKey+params ids to look up the cache
* in `this._items`
*/
this._refBaseKeys = new WeakMap();
}
addDependent(cacheItem, { callback, operation = 'addDependent', } = {}) {
const { _graph, _idGenerator } = this.ecosystem;
const id = _idGenerator.generateNodeId();
_graph.addEdge(id, cacheItem.id, operation, index_1.Explicit | index_1.External, callback);
return () => _graph.removeEdge(id, cacheItem.id);
}
/**
* Get an object mapping all ids in this selectorCache to their current
* values.
*
* Pass a selector to only return caches of that selector.
*
* Pass a partial SelectorCache id string to only return caches whose id
* contains the passed key (case-insensitive).
*
* IMPORTANT: Don't use this for SSR. SelectorCaches are not designed to be
* shared across environments. Selectors should be simple derivations that
* will be predictably recreated from rehydrated atom instances.
*
* In other words, `ecosystem.dehydrate()` is all you need for SSR. Don't
* worry about selectors. This method is solely an inspection/debugging util.
*/
dehydrate(selectableOrName) {
const hash = this.findAll(selectableOrName);
// We just created the object. Just mutate it.
Object.keys(hash).forEach(id => {
hash[id] = hash[id].result;
});
return hash;
}
/**
* Destroys the cache for the given selector + args combo (if it exists).
*
* Destruction bails out by default if the selector's ref count is > 0. Pass
* `true` as the 3rd param to force destruction.
*/
destroyCache(selectable, args, force) {
const id = (0, core_1.is)(selectable, SelectorCache)
? selectable.id
: this.getCacheId(selectable, args);
const cache = (0, core_1.is)(selectable, SelectorCache)
? selectable
: this._items[id];
if (!cache || cache.isDestroyed)
return;
const node = this.ecosystem._graph.nodes[id];
if (force || !node.refCount) {
this._destroySelector(id);
}
}
find(selectable, args) {
if ((0, core_1.is)(selectable, SelectorCache)) {
return selectable;
}
if (typeof selectable === 'string') {
return Object.values(this.findAll(selectable))[0];
}
const id = this.getCacheId(selectable, args, true);
return id && this._items[id];
}
/**
* Get an object of all currently-cached AtomSelectors.
*
* Pass a selector reference or string to filter by caches whose id
* weakly matches the passed selector name.
*/
findAll(selectableOrName = '') {
const hash = {};
const filterKey = typeof selectableOrName === 'string'
? selectableOrName.toLowerCase()
: (0, core_1.is)(selectableOrName, SelectorCache)
? selectableOrName.id
: this.getBaseKey(selectableOrName, true) || this._getIdealCacheId(selectableOrName);
Object.values(this._items)
.sort((a, b) => a.id.localeCompare(b.id))
.forEach(item => {
if (!filterKey || item.id.toLowerCase().includes(filterKey)) {
hash[item.id] = item;
}
});
return hash;
}
/**
* Get the cached args and result for the given AtomSelector (or
* AtomSelectorConfig). Runs the selector, sets up the graph, and caches the
* initial value if this selector hasn't been cached before.
*/
getCache(selectable, args = []) {
if ((0, core_1.is)(selectable, SelectorCache)) {
return selectable;
}
const selectorOrConfig = selectable;
const id = this.getCacheId(selectorOrConfig, args);
let cache = this._items[id];
if (cache)
return cache;
// create the cache; it doesn't exist yet
cache = new SelectorCache(id, selectorOrConfig, args);
this._items[id] = cache;
this.ecosystem._graph.addNode(id, true);
this.runSelector(id, args, true);
return cache;
}
/**
* Get the fully qualified id for the given selector+params combo
*/
getCacheId(selectorOrConfig, args, weak) {
const { complexParams, _idGenerator } = this.ecosystem;
const paramsHash = (args === null || args === void 0 ? void 0 : args.length)
? _idGenerator.hashParams(args, complexParams)
: '';
const baseKey = this.getBaseKey(selectorOrConfig, weak);
return paramsHash ? `${baseKey}-${paramsHash}` : baseKey;
}
/**
* Should only be used internally. Removes the selector from the cache and
* the graph
*/
_destroySelector(id) {
const cache = this._items[id];
if (!cache)
return; // shouldn't happen
const { _graph, _scheduler, _mods, modBus } = this.ecosystem;
if (cache.nextReasons.length && cache.task) {
_scheduler.unschedule(cache.task);
}
_graph.removeDependencies(id);
_graph.removeNode(id);
delete this._items[id];
cache.isDestroyed = true;
// don't delete the ref from this._refBaseKeys; this selector cache isn't
// necessarily the only one using it (if the selector takes params). Just
// let the WeakMap clean itself up.
if (_mods.statusChanged) {
modBus.dispatch(plugin_actions_1.pluginActions.statusChanged({
newStatus: 'Destroyed',
node: cache,
oldStatus: 'Active',
}));
}
}
/**
* Get the string key we would ideally use as the id of the given
* AtomSelector function or AtomSelectorConfig object - doesn't necessarily
* mean we end up caching using this key.
*/
_getIdealCacheId(selectorOrConfig) {
var _a;
const idealKey = selectorOrConfig.name ||
((_a = selectorOrConfig.selector) === null || _a === void 0 ? void 0 : _a.name);
// 'selector' is too generic (it's the key in AtomSelectorConfig objects)
return (idealKey !== 'selector' && idealKey) || undefined;
}
/**
* Should only be used internally
*/
_scheduleEvaluation(id, reason, shouldSetTimeout) {
const cache = this._items[id];
cache.nextReasons.push(reason);
if (cache.nextReasons.length > 1)
return; // job already scheduled
const task = () => {
cache.task = undefined;
this.runSelector(id, cache.args);
};
cache.task = task;
this.ecosystem._scheduler.schedule({
id: id,
task,
type: 2, // EvaluateGraphNode (2)
}, shouldSetTimeout);
}
/**
* Should only be used internally
*/
_swapRefs(oldCache, newRef, args = []) {
const baseKey = this._refBaseKeys.get(oldCache.selectorRef);
if (!baseKey)
return;
this._refBaseKeys.set(newRef, baseKey);
this._refBaseKeys.delete(oldCache.selectorRef);
oldCache.selectorRef = newRef;
this.runSelector(oldCache.id, args, false, true);
}
/**
* Destroy all cached selectors. Should probably only be used internally.
* Prefer `ecosystem.reset()`.
*/
_wipe() {
Object.keys(this._items).forEach(id => {
this._destroySelector(id);
});
this._refBaseKeys = new WeakMap();
}
/**
* Get a base key that can be used to generate consistent ids for the given
* selector
*/
getBaseKey(selectorOrConfig, weak) {
const existingId = this._refBaseKeys.get(selectorOrConfig);
if (existingId || weak)
return existingId;
const selectorName = this._getIdealCacheId(selectorOrConfig) || 'unnamed';
const key = this.ecosystem._idGenerator.generateId(`@-${selectorName}`);
this._refBaseKeys.set(selectorOrConfig, key);
return key;
}
/**
* Run an AtomSelector and, depending on the selector's resultsComparator,
* update its cached result. Updates the graph efficiently (using
* `.bufferUpdates()`)
*/
runSelector(id, args, isInitializing, skipNotifyingDependents) {
const { _evaluationStack, _graph, _mods, modBus } = this.ecosystem;
_graph.bufferUpdates(id);
const cache = this._items[id];
const selector = typeof cache.selectorRef === 'function'
? cache.selectorRef
: cache.selectorRef.selector;
const resultsComparator = (typeof cache.selectorRef !== 'function' &&
cache.selectorRef.resultsComparator) ||
defaultResultsComparator;
_evaluationStack.start(cache);
try {
const result = selector(_evaluationStack.atomGetters, ...args);
if (!isInitializing && !resultsComparator(result, cache.result)) {
if (!skipNotifyingDependents) {
_graph.scheduleDependents(id, cache.nextReasons, result, cache.result);
}
if (_mods.stateChanged) {
modBus.dispatch(plugin_actions_1.pluginActions.stateChanged({
cache: cache,
newState: result,
oldState: cache.result,
reasons: cache.nextReasons,
}));
}
cache.result = result;
}
else if (isInitializing) {
cache.result = result;
if (_mods.statusChanged) {
modBus.dispatch(plugin_actions_1.pluginActions.statusChanged({
newStatus: 'Active',
node: cache,
oldStatus: 'Initializing',
}));
}
}
}
catch (err) {
_graph.destroyBuffer();
console.error(`Zedux encountered an error while running selector with id "${id}":`, err);
throw err;
}
finally {
_evaluationStack.finish();
cache.prevReasons = cache.nextReasons;
cache.nextReasons = [];
}
_graph.flushUpdates();
}
}
exports.Selectors = Selectors;