nrgy
Version:
The library for reactive programming using efficient computing and MVC/MVVM patterns
1,022 lines (972 loc) • 26.5 kB
JavaScript
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// 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;
}
exports.defaultEquals = defaultEquals; exports.isSignal = isSignal; exports.getSignalName = getSignalName; exports.destroySignal = destroySignal; exports.isSignalDestroyed = isSignalDestroyed; exports.isSignalSubscribed = isSignalSubscribed; exports.signal = signal; exports.isAtom = isAtom; exports.getAtomNode = getAtomNode; exports.getAtomName = getAtomName; exports.createAtomFromFunction = createAtomFromFunction; exports.AtomUpdateError = AtomUpdateError; exports.RUNTIME = RUNTIME; exports.compute = compute; exports.combineAtoms = combineAtoms; exports.ScopeDestructionError = ScopeDestructionError; exports.BaseScope = BaseScope; exports.effect = effect; exports.syncEffect = syncEffect; exports.atom = atom; exports.declareStateUpdates = declareStateUpdates; exports.createStore = createStore; exports.createStoreUpdates = createStoreUpdates; exports.declareStore = declareStore;