UNPKG

nrgy

Version:

The library for reactive programming using efficient computing and MVC/MVVM patterns

1,022 lines (997 loc) 25.9 kB
// src/core/common/defaultEquals.ts var defaultEquals = Object.is; // src/core/common/symbols.ts var ATOM_SYMBOL = Symbol.for("ngry.atom"); var SIGNAL_SYMBOL = Symbol.for("ngry.signal"); // src/core/signals/common.ts function isSignal(value) { return typeof value === "function" && SIGNAL_SYMBOL in value; } function getSignalNode(value) { return value[SIGNAL_SYMBOL]; } function getSignalName(value) { return getSignalNode(value).name; } function destroySignal(value) { getSignalNode(value).destroy(); } function isSignalDestroyed(value) { return getSignalNode(value).isDestroyed; } function isSignalSubscribed(value) { return getSignalNode(value).isSubscribed(); } // src/core/internals/createWeakRef.ts var LeakyRef = class { constructor(target) { this.target = target; } get [Symbol.toStringTag]() { return "LeakyRef"; } deref() { return this.target; } }; var WeakRefImpl = "WeakRef" in globalThis && globalThis["WeakRef"] ? globalThis.WeakRef : LeakyRef; function createWeakRef(value) { return new WeakRefImpl(value); } // src/core/signals/signal.ts var SignalImpl = class { constructor(options) { this.ref = createWeakRef(this); this.consumerEffects = /* @__PURE__ */ new Set(); this.isDestroyed = false; if (options) { this.name = options.name; this.sync = options.sync; this.onEvent = options.onEvent; this.onSubscribe = options.onSubscribe; this.onUnsubscribe = options.onUnsubscribe; this.onDestroy = options.onDestroy; } } /** * Emits a value */ emit(value) { var _a; if (this.isDestroyed) { return; } (_a = this.onEvent) == null ? void 0 : _a.call(this, value); this.producerChanged(value); } destroy() { var _a; if (this.isDestroyed) { return; } this.isDestroyed = true; this.producerDestroyed(); this.consumerEffects.clear(); (_a = this.onDestroy) == null ? void 0 : _a.call(this); this.onEvent = void 0; this.onSubscribe = void 0; this.onUnsubscribe = void 0; this.onDestroy = void 0; } isSubscribed() { return this.consumerEffects.size > 0; } subscribe(effectRef) { var _a; this.consumerEffects.add(effectRef); (_a = this.onSubscribe) == null ? void 0 : _a.call(this); } unsubscribe(effectRef) { var _a; if (this.consumerEffects.delete(effectRef)) { (_a = this.onUnsubscribe) == null ? void 0 : _a.call(this, this.consumerEffects.size === 0); } } /** * Notify all consumers of this producer that its value is changed. */ producerChanged(value) { var _a; if (this.consumerEffects.size === 0) { return; } let hasDeletion = false; for (const effectRef of this.consumerEffects) { const effect2 = effectRef.deref(); if (!effect2 || effect2.isDestroyed) { hasDeletion = true; this.consumerEffects.delete(effectRef); continue; } effect2.notify(value); } if (hasDeletion) { (_a = this.onUnsubscribe) == null ? void 0 : _a.call(this, this.consumerEffects.size === 0); } } /** * Notify all consumers of this producer that it is destroyed */ producerDestroyed() { for (const effectRef of this.consumerEffects) { const effect2 = effectRef.deref(); if (effect2 && !effect2.isDestroyed) { effect2.notifyDestroy(); } } } }; var signal = (options) => { const node = new SignalImpl(options); const result = (value) => node.emit(value); result[SIGNAL_SYMBOL] = node; return result; }; // src/core/internals/nextSafeInteger.ts function nextSafeInteger(value) { return value < Number.MAX_SAFE_INTEGER ? value + 1 : Number.MIN_SAFE_INTEGER; } // src/core/atoms/atom.ts var ATOM_ID = 0; function generateAtomId() { return ATOM_ID = nextSafeInteger(ATOM_ID); } function isAtom(value) { return typeof value === "function" && ATOM_SYMBOL in value && value[ATOM_SYMBOL] !== void 0; } function getAtomNode(value) { return value[ATOM_SYMBOL]; } function getAtomName(value) { return getAtomNode(value).name; } function createAtomFromFunction(node, fn, extraApi = {}) { fn[ATOM_SYMBOL] = node; return Object.assign(fn, extraApi); } var AtomUpdateError = class extends Error { constructor(name) { super( "Atom cannot be updated in tracked context" + (name ? ` (${name})` : "") ); this.name = "AtomUpdateError"; } }; // src/core/internals/queue.ts function createQueue() { let head; let tail; return { isEmpty() { return !head; }, get() { const entry = head; if (entry) { const next = head = entry.next; if (!next) tail = void 0; return entry.item; } return void 0; }, add(item) { const entry = { item }; if (tail) { tail.next = entry; } else { head = entry; } tail = entry; } }; } // src/core/internals/reportError.ts var nrgyReportError = "reportError" in globalThis && typeof globalThis.reportError === "function" ? globalThis.reportError : () => void 0; // src/core/internals/schedulers.ts function createMicrotaskScheduler(options) { var _a; const onError = (_a = options == null ? void 0 : options.onError) != null ? _a : nrgyReportError; const queue = createQueue(); let isActive = false; let isPaused = false; const execute = () => { if (isActive) return; isActive = true; let action; while (!isPaused && (action = queue.get())) { try { action(); } catch (error) { onError(error); } } isActive = false; }; return { isEmpty: () => queue.isEmpty(), schedule: (entry) => { const prevEmpty = queue.isEmpty(); queue.add(entry); if (prevEmpty && !isActive) { queueMicrotask(execute); } }, execute, pause: () => { isPaused = true; }, resume: () => { isPaused = false; if (!queue.isEmpty()) { queueMicrotask(execute); } } }; } function createSyncTaskScheduler(options) { var _a; const onError = (_a = options == null ? void 0 : options.onError) != null ? _a : nrgyReportError; const queue = createQueue(); let isActive = false; let isPaused = false; const execute = () => { if (isActive) return; isActive = true; let action; while (!isPaused && (action = queue.get())) { try { action(); } catch (error) { onError(error); } } isActive = false; }; return { isEmpty: () => queue.isEmpty(), schedule: (entry) => { queue.add(entry); if (!isActive) execute(); }, execute, pause: () => { isPaused = true; }, resume: () => { isPaused = false; if (!queue.isEmpty()) { execute(); } } }; } // src/core/internals/runtime.ts var Runtime = class { constructor() { this.asyncScheduler = createMicrotaskScheduler(); this.syncScheduler = createSyncTaskScheduler(); this.currentEffect = void 0; /** @readonly */ this.batchLock = 0; /** * Marks the current computation context as tracked */ this.tracked = false; /** * @readonly * The current global clock of changed atoms. */ this.clock = 0; } /** * Updates the global clock of changed atoms */ updateAtomClock() { this.clock = nextSafeInteger(this.clock); } /** * Run a function in a tracked context */ runAsTracked(effect2, fn) { const prevEffect = this.currentEffect; this.currentEffect = effect2; const prevTracked = this.tracked; this.tracked = true; try { return fn(); } finally { this.currentEffect = prevEffect; this.tracked = prevTracked; } } /** * Run a function in an untracked context */ runAsUntracked(fn) { const prevEffect = this.currentEffect; this.currentEffect = void 0; const prevTracked = this.tracked; this.tracked = false; try { return fn(); } finally { this.currentEffect = prevEffect; this.tracked = prevTracked; } } batch(fn) { try { this.batchLock++; if (this.batchLock === 1) { this.syncScheduler.pause(); this.asyncScheduler.pause(); } return fn(); } finally { this.batchLock--; if (this.batchLock === 0) { this.syncScheduler.resume(); this.asyncScheduler.resume(); } } } }; var RUNTIME = new Runtime(); // src/core/atoms/compute.ts function compute(computation, options) { const node = new ComputedImpl(computation, options); return createAtomFromFunction(node, node.get.bind(node)); } var UNSET = Symbol("UNSET"); var COMPUTING = Symbol("COMPUTING"); var ERRORED = Symbol("ERRORED"); var ComputedImpl = class { constructor(computation, options) { this.computation = computation; this.id = generateAtomId(); this.clock = void 0; this.version = 0; /** * Current value of the computation. * * This can also be one of the special values `UNSET`, `COMPUTING`, or `ERRORED`. */ this.value = UNSET; /** * If `value` is `ERRORED`, the error caught from the last computation attempt which will * be re-thrown. */ this.error = void 0; var _a; this.name = options == null ? void 0 : options.name; this.equal = (_a = options == null ? void 0 : options.equal) != null ? _a : defaultEquals; } destroy() { this.value = UNSET; this.lastEffectRef = void 0; } get() { this.accessValue(); if (this.value === ERRORED) { throw this.error; } return this.value; } accessValue() { if (this.value === COMPUTING) { throw new Error("Detected cycle in computations"); } const activeEffect = RUNTIME.currentEffect; const isStale = this.clock !== RUNTIME.clock || this.value === UNSET || activeEffect && this.lastEffectRef !== activeEffect.ref; if (isStale) { this.lastEffectRef = activeEffect == null ? void 0 : activeEffect.ref; this.recomputeValue(); } } recomputeValue() { const oldValue = this.value; this.value = COMPUTING; let newValue; try { newValue = this.computation(); } catch (err) { newValue = ERRORED; this.error = err; } this.clock = RUNTIME.clock; if (oldValue === UNSET || oldValue === ERRORED || newValue === ERRORED || !this.equal(oldValue, newValue)) { this.value = newValue; this.version = nextSafeInteger(this.version); } else { this.value = oldValue; } } }; // src/core/utils/combineAtoms.ts function combineAtoms(sources, options) { return compute( () => sources.map((source) => source()), options ); } // src/core/scope/scopeDestructionError.ts var ScopeDestructionError = class extends Error { constructor(errors) { super(); this.name = "ScopeDestructionError"; this.errors = errors; } }; // src/core/internals/isPromise.ts function isPromise(value) { return typeof (value == null ? void 0 : value.then) === "function"; } // src/core/internals/list.ts function removeFromList(head, predicate) { const root = { next: head }; let node = root; while (node.next) { if (node.next && predicate(node.next)) { node.next = node.next.next; } else { node = node.next; } } return root.next; } // src/core/scope/baseScope.ts var BaseScope = class { /** * Registers a callback or unsubscribable resource which will be called when `destroy()` is called */ onDestroy(teardown) { this.subscriptions = { teardown, next: this.subscriptions }; } /** * Registers an unsubscribable resource which will be called when `destroy()` is called */ add(resource) { this.subscriptions = { teardown: resource, next: this.subscriptions }; return resource; } /** * Destroys the scope */ destroy() { if (!this.subscriptions) { return; } let errors; let list = this.subscriptions; this.subscriptions = void 0; while (list) { const { teardown } = list; try { if ("unsubscribe" in teardown) { teardown.unsubscribe(); } else if ("destroy" in teardown) { teardown.destroy(); } else { teardown(); } } catch (error) { if (!errors) { errors = []; } errors.push(error); } list = list.next; } if (errors) { throw new ScopeDestructionError(errors); } } }; // src/core/effects/effectId.ts var EFFECT_ID = 0; function generateEffectId() { return EFFECT_ID = nextSafeInteger(EFFECT_ID); } // src/core/effects/atomEffect.ts var AtomEffect = class { constructor(scheduler, source, action) { this.id = generateEffectId(); this.ref = createWeakRef(this); /** Monotonically increasing version of the effect */ this.clock = 0; /** Whether the effect needs to be re-run */ this.dirty = false; /** Whether the effect has been destroyed */ this.isDestroyed = false; /** * Signals a result of the action function */ this.onResult = signal(); /** * Signals an error which occurred in the execution of the action function */ this.onError = signal({ sync: true }); /** * Signals that the effect has been destroyed */ this.onDestroy = signal({ sync: true }); this.scheduler = scheduler; this.action = action; this.source = source; } /** * Destroys the effect */ destroy() { var _a; if (this.isDestroyed) { return; } this.isDestroyed = true; this.scheduler = void 0; this.source = void 0; this.action = void 0; this.referredAtomIds = void 0; (_a = this.actionScope) == null ? void 0 : _a.destroy(); this.actionScope = void 0; this.actionContext = void 0; this.onDestroy(); destroySignal(this.onResult); destroySignal(this.onError); destroySignal(this.onDestroy); } hasReferredAtom(atomId) { for (let node = this.referredAtomIds; node; node = node.next) { if (node.atomId === atomId) { return true; } } return false; } addReferredAtom(atomId) { if (!this.hasReferredAtom(atomId)) { this.referredAtomIds = { atomId, next: this.referredAtomIds }; } } removeReferredAtom(atomId) { if (!this.referredAtomIds) { return; } this.referredAtomIds = removeFromList( this.referredAtomIds, (node) => node.atomId === atomId ); } /** * Notify the effect that an atom has been accessed */ notifyAccess(atomId) { if (this.isDestroyed) { return; } this.addReferredAtom(atomId); } /** * Schedule the effect to be re-run */ notify() { if (this.isDestroyed) { return; } this.clock = nextSafeInteger(this.clock); const needsSchedule = !this.dirty; this.dirty = true; if (needsSchedule && this.scheduler) { this.scheduler.schedule(() => this.run()); } } /** * Notify the effect that it must be destroyed */ notifyDestroy(atomId) { if (this.isDestroyed) { return; } this.removeReferredAtom(atomId); if (!this.referredAtomIds) { this.destroy(); } } /** * Execute the reactive expression in the context of this `AtomEffect`. * * Should be called by the user scheduling algorithm when the provided * `scheduler` TaskScheduler is called. */ run() { var _a; if (!this.dirty) { return; } this.dirty = false; if (this.isDestroyed || !this.action || !this.source) { return; } let result; try { const value = RUNTIME.runAsTracked(this, this.source); const node = getAtomNode(this.source); if (this.lastValueVersion === node.version) { return; } this.lastValueVersion = node.version; (_a = this.actionScope) == null ? void 0 : _a.destroy(); result = this.action(value, this.getContext()); } catch (error) { this.onError(error); } if (isPromise(result)) { result.then((value) => this.onResult(value)).catch((error) => this.onError(error)); } else { this.onResult(result); } } getContext() { if (!this.actionContext) { this.actionContext = { cleanup: (callback) => { if (!this.actionScope) { this.actionScope = new BaseScope(); } this.actionScope.onDestroy(callback); } }; } return this.actionContext; } }; // src/core/effects/signalEffect.ts var SignalEffect = class { constructor(scheduler, action) { this.ref = createWeakRef(this); this.isDestroyed = false; /** * Signals a result of the action function */ this.onResult = signal(); /** * Signals an error which occurred in the execution of the action function */ this.onError = signal({ sync: true }); /** * Signals that the effect has been destroyed */ this.onDestroy = signal({ sync: true }); this.scheduler = scheduler; this.action = action; } /** * Destroys the effect */ destroy() { var _a; this.isDestroyed = true; this.scheduler = void 0; this.action = void 0; (_a = this.actionScope) == null ? void 0 : _a.destroy(); this.actionScope = void 0; this.actionContext = void 0; this.onDestroy(); destroySignal(this.onResult); destroySignal(this.onError); destroySignal(this.onDestroy); } /** * Schedule the effect to be run */ notify(value) { if (!this.isDestroyed && this.scheduler) { this.scheduler.schedule(() => this.run(value)); } } /** * Notify the effect that it must be destroyed */ notifyDestroy() { if (this.isDestroyed) { return; } this.destroy(); } /** * Execute the subscriber's action in the context of this effect. * * Should be called by the user scheduling algorithm when the provided * `scheduler` TaskScheduler is called. */ run(value) { var _a; if (this.isDestroyed || !this.action) { return; } try { (_a = this.actionScope) == null ? void 0 : _a.destroy(); const result = this.action(value, this.getContext()); if (isPromise(result)) { result.then((result2) => { this.onResult(result2); }).catch((error) => { this.onError(error); }); } else { this.onResult(result); } } catch (error) { this.onError(error); } } getContext() { if (!this.actionContext) { this.actionContext = { cleanup: (callback) => { if (!this.actionScope) { this.actionScope = new BaseScope(); } this.actionScope.onDestroy(callback); } }; } return this.actionContext; } }; // src/core/effects/effect.ts var effect = (source, action, options) => { let scheduler = (options == null ? void 0 : options.sync) ? RUNTIME.syncScheduler : RUNTIME.asyncScheduler; if (isSignal(source)) { const node = getSignalNode(source); if (node.sync) { scheduler = RUNTIME.syncScheduler; } const signalEffect = new SignalEffect(scheduler, action); const nodeRef = node.ref; node.subscribe(signalEffect.ref); return { onResult: signalEffect.onResult, onError: signalEffect.onError, onDestroy: signalEffect.onDestroy, destroy: () => { var _a; (_a = nodeRef.deref()) == null ? void 0 : _a.unsubscribe(signalEffect.ref); signalEffect.destroy(); } }; } if (isAtom(source)) { const atomEffect = new AtomEffect(scheduler, source, action); atomEffect.notify(); return { onResult: atomEffect.onResult, onError: atomEffect.onError, onDestroy: atomEffect.onDestroy, destroy: () => atomEffect.destroy() }; } if (Array.isArray(source)) { const list = combineAtoms(source); return effect(list, action, options); } throw new Error("Unexpected the first argument"); }; var syncEffect = (source, action, options) => { return effect(source, action, { sync: true, ...options }); }; // src/core/atoms/writableAtom.ts var WritableAtomImpl = class { constructor(value, options) { this.value = value; this.id = generateAtomId(); this.version = 0; /** * Signals that the effect has been destroyed */ this.onDestroyed = signal({ sync: true }); this.consumerEffects = /* @__PURE__ */ new Map(); this.isDestroyed = false; var _a; this.name = options == null ? void 0 : options.name; this.equal = (_a = options == null ? void 0 : options.equal) != null ? _a : defaultEquals; if (options == null ? void 0 : options.onDestroy) { syncEffect(this.onDestroyed, options.onDestroy); } } get() { if (!this.isDestroyed) { this.producerAccessed(); } return this.value; } /** * Directly update the value of the atom to a new value, which may or may not be * equal to the previous. * * In the event that `newValue` is semantically equal to the current value, `set` is * a no-op. * * Returns `true` if the value was changed. */ set(newValue) { if (this.isDestroyed) { return false; } this.producerBeforeChange(); if (this.equal(this.value, newValue)) { return false; } this.value = newValue; this.producerChanged(); return true; } /** * Derive a new value for the atom from its current value using the `updater` function. * * This is equivalent to calling `set` on the result of running `updater` on the current * value. * * Returns `true` if the value was changed. */ update(updater) { return this.set(updater(this.value)); } /** * Calls `mutator` on the current value and assumes that it has been mutated. */ mutate(mutator) { if (this.isDestroyed) { return; } this.producerBeforeChange(); mutator(this.value); this.producerChanged(); } asReadonly() { if (this.readonlyAtom === void 0) { this.readonlyAtom = createAtomFromFunction(this, () => this.get()); } return this.readonlyAtom; } destroy() { if (this.isDestroyed) { return; } this.producerDestroyed(); this.consumerEffects.clear(); this.onDestroyed(); destroySignal(this.onDestroyed); this.isDestroyed = true; } /** * Checks if the atom can be updated in the current context */ producerBeforeChange() { if (RUNTIME.tracked) { throw new AtomUpdateError(this.name); } } /** * Notify all consumers of this producer that its value is changed. */ producerChanged() { RUNTIME.updateAtomClock(); this.version = nextSafeInteger(this.version); for (const [effectRef, atEffectClock] of this.consumerEffects) { const effect2 = effectRef.deref(); if (!effect2 || effect2.isDestroyed || !effect2.dirty && effect2.clock !== atEffectClock) { this.consumerEffects.delete(effectRef); continue; } effect2.notify(); } } /** * Notify all consumers of this producer that it is destroyed */ producerDestroyed() { for (const [effectRef] of this.consumerEffects) { const effect2 = effectRef.deref(); if (effect2 && !effect2.isDestroyed) { effect2.notifyDestroy(this.id); } } } /** * Mark that this producer node has been accessed in the current reactive context. */ producerAccessed() { if (!RUNTIME.tracked) { return; } const effect2 = RUNTIME.currentEffect; if (effect2 && !effect2.isDestroyed) { this.consumerEffects.set(effect2.ref, effect2.clock); effect2.notifyAccess(this.id); } } }; var atom = (initialValue, options) => { const node = new WritableAtomImpl(initialValue, options); const result = createAtomFromFunction( node, node.get.bind(node), { onDestroyed: node.onDestroyed, set: node.set.bind(node), update: node.update.bind(node), mutate: node.mutate.bind(node), asReadonly: node.asReadonly.bind(node), destroy: node.destroy.bind(node) } ); return result; }; // src/core/store/store.ts function declareStateUpdates(stateExample, updates) { if (updates) { return updates; } return (updates2) => updates2; } function createStore(initialState, options) { const store = atom(initialState, options); store.updates = createStoreUpdates( store.update, options.updates ); return store; } function createStoreUpdates(atomUpdate, stateUpdates) { const updates = {}; Object.entries(stateUpdates).forEach(([key, mutationFactory]) => { updates[key] = (...args) => { const mutation = mutationFactory(...args); atomUpdate(mutation); }; }); return updates; } // src/core/store/declareStore.ts function declareStore(storeOptions) { const { initialState, updates, options } = storeOptions; function factory(customInitialState, customOptions) { const state = customInitialState === void 0 ? initialState : typeof customInitialState === "function" ? customInitialState(initialState) : customInitialState; const store = createStore(state, { ...options, ...customOptions, updates }); return store; } Object.assign(factory, { initialState, updates }); return factory; } export { defaultEquals, isSignal, getSignalName, destroySignal, isSignalDestroyed, isSignalSubscribed, signal, isAtom, getAtomNode, getAtomName, createAtomFromFunction, AtomUpdateError, RUNTIME, compute, combineAtoms, ScopeDestructionError, BaseScope, effect, syncEffect, atom, declareStateUpdates, createStore, createStoreUpdates, declareStore };