@tldraw/state 5.5.0-next.4d6473b0bda1 → 5.5.0-next.68ff40bd4640
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/Atom.js +4 -5
- package/dist-cjs/lib/Atom.js.map +2 -2
- package/dist-cjs/lib/EffectScheduler.js +29 -1
- package/dist-cjs/lib/EffectScheduler.js.map +2 -2
- package/dist-cjs/lib/capture.js +36 -38
- package/dist-cjs/lib/capture.js.map +2 -2
- package/dist-cjs/lib/helpers.js +1 -2
- package/dist-cjs/lib/helpers.js.map +2 -2
- package/dist-cjs/lib/transactions.js +1 -5
- package/dist-cjs/lib/transactions.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/Atom.mjs +4 -5
- package/dist-esm/lib/Atom.mjs.map +2 -2
- package/dist-esm/lib/EffectScheduler.mjs +29 -1
- package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
- package/dist-esm/lib/capture.mjs +36 -38
- package/dist-esm/lib/capture.mjs.map +2 -2
- package/dist-esm/lib/helpers.mjs +1 -2
- package/dist-esm/lib/helpers.mjs.map +2 -2
- package/dist-esm/lib/transactions.mjs +1 -5
- package/dist-esm/lib/transactions.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/Atom.ts +5 -9
- package/src/lib/EffectScheduler.ts +39 -1
- package/src/lib/__tests__/propagation.test.ts +96 -1
- package/src/lib/capture.ts +44 -45
- package/src/lib/helpers.ts +2 -2
- package/src/lib/transactions.ts +1 -6
package/dist-cjs/index.js
CHANGED
package/dist-cjs/lib/Atom.js
CHANGED
|
@@ -33,12 +33,11 @@ class __Atom__ {
|
|
|
33
33
|
constructor(name, current, options) {
|
|
34
34
|
this.name = name;
|
|
35
35
|
this.current = current;
|
|
36
|
-
this.isEqual = options?.isEqual ??
|
|
37
|
-
if (
|
|
38
|
-
if (options.historyLength) {
|
|
36
|
+
this.isEqual = options?.isEqual ?? import_helpers.equals;
|
|
37
|
+
if (options?.historyLength) {
|
|
39
38
|
this.historyBuffer = new import_HistoryBuffer.HistoryBuffer(options.historyLength);
|
|
40
39
|
}
|
|
41
|
-
this.computeDiff = options
|
|
40
|
+
this.computeDiff = options?.computeDiff;
|
|
42
41
|
}
|
|
43
42
|
name;
|
|
44
43
|
current;
|
|
@@ -91,7 +90,7 @@ class __Atom__ {
|
|
|
91
90
|
* ```
|
|
92
91
|
*/
|
|
93
92
|
set(value, diff) {
|
|
94
|
-
if (this.isEqual
|
|
93
|
+
if (this.isEqual(this.current, value)) {
|
|
95
94
|
return this.current;
|
|
96
95
|
}
|
|
97
96
|
let historyDiff;
|
package/dist-cjs/lib/Atom.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/Atom.ts"],
|
|
4
|
-
"sourcesContent": ["import { ArraySet } from './ArraySet'\nimport { maybeCaptureParent } from './capture'\nimport { EMPTY_ARRAY, equals, singleton } from './helpers'\nimport { HistoryBuffer } from './HistoryBuffer'\nimport { advanceGlobalEpoch, atomDidChange, getGlobalEpoch } from './transactions'\nimport { Child, ComputeDiff, RESET_VALUE, Signal } from './types'\n\n/**\n * The options to configure an atom, passed into the {@link atom} function.\n * @public\n */\nexport interface AtomOptions<Value, Diff> {\n\t/**\n\t * The maximum number of diffs to keep in the history buffer.\n\t *\n\t * If you don't need diffs, leave this as `undefined` and no history buffer will be created. Diffs passed to {@link Atom.set} or produced by {@link AtomOptions.computeDiff} are only recorded when this is set.\n\t *\n\t * If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).\n\t *\n\t * Otherwise, set this to a higher number based on your usage pattern and memory constraints.\n\t *\n\t */\n\thistoryLength?: number\n\t/**\n\t * A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}.\n\t */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\t/**\n\t * If provided, this will be used to compare the old and new values of the atom to determine if the value has changed.\n\t * By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.\n\t * @param a - The old value\n\t * @param b - The new value\n\t * @returns True if the values are equal, false otherwise.\n\t */\n\tisEqual?(a: any, b: any): boolean\n}\n\n/**\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * Atoms are created using the {@link atom} function.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * print(name.get()) // 'John'\n * ```\n *\n * @public\n */\nexport interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set.\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}.\n\t */\n\tset(value: Value, diff?: Diff): Value\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value.\n\t */\n\tupdate(updater: (value: Value) => Value): Value\n}\n\n/**\n * Internal implementation of the Atom interface. This class should not be used directly - use the {@link atom} function instead.\n *\n * @internal\n */\nclass __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {\n\tconstructor(\n\t\tpublic readonly name: string,\n\t\tprivate current: Value,\n\t\toptions?: AtomOptions<Value, Diff>\n\t) {\n\t\tthis.isEqual = options?.isEqual ??
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AACzB,qBAAmC;AACnC,qBAA+C;AAC/C,2BAA8B;AAC9B,0BAAkE;AAClE,mBAAwD;AAmExD,MAAM,SAA6D;AAAA,EAClE,YACiB,MACR,SACR,SACC;AAHe;AACR;AAGR,SAAK,UAAU,SAAS,WAAW;
|
|
4
|
+
"sourcesContent": ["import { ArraySet } from './ArraySet'\nimport { maybeCaptureParent } from './capture'\nimport { EMPTY_ARRAY, equals, singleton } from './helpers'\nimport { HistoryBuffer } from './HistoryBuffer'\nimport { advanceGlobalEpoch, atomDidChange, getGlobalEpoch } from './transactions'\nimport { Child, ComputeDiff, RESET_VALUE, Signal } from './types'\n\n/**\n * The options to configure an atom, passed into the {@link atom} function.\n * @public\n */\nexport interface AtomOptions<Value, Diff> {\n\t/**\n\t * The maximum number of diffs to keep in the history buffer.\n\t *\n\t * If you don't need diffs, leave this as `undefined` and no history buffer will be created. Diffs passed to {@link Atom.set} or produced by {@link AtomOptions.computeDiff} are only recorded when this is set.\n\t *\n\t * If you expect the value to be part of an active effect subscription all the time, and to not change multiple times inside of a single transaction, you can set this to a relatively low number (e.g. 10).\n\t *\n\t * Otherwise, set this to a higher number based on your usage pattern and memory constraints.\n\t *\n\t */\n\thistoryLength?: number\n\t/**\n\t * A method used to compute a diff between the atom's old and new values. If provided, it will not be used unless you also specify {@link AtomOptions.historyLength}.\n\t */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\t/**\n\t * If provided, this will be used to compare the old and new values of the atom to determine if the value has changed.\n\t * By default, values are compared using first using strict equality (`===`), then `Object.is`, and finally any `.equals` method present in the object's prototype chain.\n\t * @param a - The old value\n\t * @param b - The new value\n\t * @returns True if the values are equal, false otherwise.\n\t */\n\tisEqual?(a: any, b: any): boolean\n}\n\n/**\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * Atoms are created using the {@link atom} function.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * print(name.get()) // 'John'\n * ```\n *\n * @public\n */\nexport interface Atom<Value, Diff = unknown> extends Signal<Value, Diff> {\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set.\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}.\n\t */\n\tset(value: Value, diff?: Diff): Value\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value.\n\t */\n\tupdate(updater: (value: Value) => Value): Value\n}\n\n/**\n * Internal implementation of the Atom interface. This class should not be used directly - use the {@link atom} function instead.\n *\n * @internal\n */\nclass __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {\n\tconstructor(\n\t\tpublic readonly name: string,\n\t\tprivate current: Value,\n\t\toptions?: AtomOptions<Value, Diff>\n\t) {\n\t\tthis.isEqual = options?.isEqual ?? equals\n\t\tif (options?.historyLength) {\n\t\t\tthis.historyBuffer = new HistoryBuffer(options.historyLength)\n\t\t}\n\t\tthis.computeDiff = options?.computeDiff\n\t}\n\n\t/** @internal */\n\treadonly isEqual: (a: any, b: any) => boolean\n\n\t/** @internal */\n\tcomputeDiff?: ComputeDiff<Value, Diff>\n\n\t/** @internal */\n\tlastChangedEpoch = getGlobalEpoch()\n\n\t/** @internal */\n\tchildren = new ArraySet<Child>()\n\n\t/** @internal */\n\thistoryBuffer?: HistoryBuffer<Diff>\n\n\t/**\n\t * Gets the current value without capturing it as a dependency in the current reactive context.\n\t * This is unsafe because it breaks the reactivity chain - use with caution.\n\t *\n\t * @param _ignoreErrors - Unused parameter for API compatibility\n\t * @returns The current value\n\t * @internal\n\t */\n\t__unsafe__getWithoutCapture(_ignoreErrors?: boolean): Value {\n\t\treturn this.current\n\t}\n\n\t/**\n\t * Gets the current value of this atom. When called within a computed signal or reaction,\n\t * this atom will be automatically captured as a dependency.\n\t *\n\t * @returns The current value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 5)\n\t * console.log(count.get()) // 5\n\t * ```\n\t */\n\tget() {\n\t\tmaybeCaptureParent(this)\n\t\treturn this.current\n\t}\n\n\t/**\n\t * Sets the value of this atom to the given value. If the value is the same as the current value, this is a no-op.\n\t *\n\t * @param value - The new value to set\n\t * @param diff - The diff to use for the update. If not provided, the diff will be computed using {@link AtomOptions.computeDiff}\n\t * @returns The new value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 0)\n\t * count.set(5) // count.get() is now 5\n\t * ```\n\t */\n\tset(value: Value, diff?: Diff): Value {\n\t\t// If the value has not changed, do nothing.\n\t\tif (this.isEqual(this.current, value)) {\n\t\t\treturn this.current\n\t\t}\n\n\t\t// `computeDiff` is user code: run it before ticking the epoch, so that any signal it reads is\n\t\t// checked against the epoch this write has not yet happened in. Reading a dependent computed\n\t\t// after the tick would stamp it as checked at the new epoch while the atom was still being\n\t\t// written, and it would then never see the change. Only `undefined` means \"no diff\n\t\t// supplied\"; `null` can be a legitimate diff.\n\t\tlet historyDiff: Diff | RESET_VALUE | undefined\n\t\tif (this.historyBuffer) {\n\t\t\thistoryDiff =\n\t\t\t\tdiff !== undefined\n\t\t\t\t\t? diff\n\t\t\t\t\t: this.computeDiff\n\t\t\t\t\t\t? this.computeDiff(this.current, value, this.lastChangedEpoch, getGlobalEpoch() + 1)\n\t\t\t\t\t\t: RESET_VALUE\n\t\t}\n\n\t\t// Tick forward the global epoch. This write belongs to that one epoch, so read it once and\n\t\t// use it everywhere below \u2014 otherwise a `computeDiff` that touches other atoms could leave\n\t\t// the history entry and `lastChangedEpoch` disagreeing.\n\t\tadvanceGlobalEpoch()\n\t\tconst epoch = getGlobalEpoch()\n\n\t\t// Add the diff to the history buffer.\n\t\tif (this.historyBuffer) {\n\t\t\tthis.historyBuffer.pushEntry(this.lastChangedEpoch, epoch, historyDiff)\n\t\t}\n\n\t\t// Update the atom's record of the epoch when last changed.\n\t\tthis.lastChangedEpoch = epoch\n\n\t\tconst oldValue = this.current\n\t\tthis.current = value\n\n\t\t// Notify all children that this atom has changed.\n\t\tatomDidChange(this as any, oldValue)\n\n\t\treturn value\n\t}\n\n\t/**\n\t * Updates the value of this atom using the given updater function. If the returned value is the same as the current value, this is a no-op.\n\t *\n\t * @param updater - A function that takes the current value and returns the new value\n\t * @returns The new value\n\t * @example\n\t * ```ts\n\t * const count = atom('count', 5)\n\t * count.update(n => n + 1) // count.get() is now 6\n\t * ```\n\t */\n\tupdate(updater: (value: Value) => Value): Value {\n\t\treturn this.set(updater(this.current))\n\t}\n\n\t/**\n\t * Gets all the diffs that have occurred since the given epoch. When called within a computed\n\t * signal or reaction, this atom will be automatically captured as a dependency.\n\t *\n\t * @param epoch - The epoch to get changes since\n\t * @returns An array of diffs, or RESET_VALUE if history is insufficient\n\t * @internal\n\t */\n\tgetDiffSince(epoch: number): RESET_VALUE | Diff[] {\n\t\tmaybeCaptureParent(this)\n\n\t\tif (epoch >= this.lastChangedEpoch) {\n\t\t\treturn EMPTY_ARRAY\n\t\t}\n\n\t\treturn this.historyBuffer?.getChangesSince(epoch) ?? RESET_VALUE\n\t}\n}\n\n/**\n * Singleton reference to the Atom constructor. Used internally to create atom instances.\n * @internal\n */\nexport const _Atom = singleton('Atom', () => __Atom__)\n\n/**\n * Type alias for instances of the internal Atom class.\n * @internal\n */\nexport type _Atom = InstanceType<typeof _Atom>\n\n/**\n * Creates a new {@link Atom}.\n *\n * An Atom is a signal that can be updated directly by calling {@link Atom.set} or {@link Atom.update}.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n *\n * name.get() // 'John'\n *\n * name.set('Jane')\n *\n * name.get() // 'Jane'\n * ```\n *\n * @public\n */\nexport function atom<Value, Diff = unknown>(\n\t/**\n\t * A name for the signal. This is used for debugging and profiling purposes, it does not need to be unique.\n\t */\n\tname: string,\n\t/**\n\t * The initial value of the signal.\n\t */\n\tinitialValue: Value,\n\t/**\n\t * The options to configure the atom. See {@link AtomOptions}.\n\t */\n\toptions?: AtomOptions<Value, Diff>\n): Atom<Value, Diff> {\n\treturn new _Atom(name, initialValue, options)\n}\n\n/**\n * Returns true if the given value is an {@link Atom}.\n *\n * @param value - The value to check\n * @returns True if the value is an Atom, false otherwise\n * @example\n * ```ts\n * const myAtom = atom('test', 42)\n * const notAtom = 'hello'\n *\n * console.log(isAtom(myAtom)) // true\n * console.log(isAtom(notAtom)) // false\n * ```\n * @public\n */\nexport function isAtom(value: unknown): value is Atom<unknown> {\n\treturn value instanceof _Atom\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AACzB,qBAAmC;AACnC,qBAA+C;AAC/C,2BAA8B;AAC9B,0BAAkE;AAClE,mBAAwD;AAmExD,MAAM,SAA6D;AAAA,EAClE,YACiB,MACR,SACR,SACC;AAHe;AACR;AAGR,SAAK,UAAU,SAAS,WAAW;AACnC,QAAI,SAAS,eAAe;AAC3B,WAAK,gBAAgB,IAAI,mCAAc,QAAQ,aAAa;AAAA,IAC7D;AACA,SAAK,cAAc,SAAS;AAAA,EAC7B;AAAA,EATiB;AAAA,EACR;AAAA;AAAA,EAWA;AAAA;AAAA,EAGT;AAAA;AAAA,EAGA,uBAAmB,oCAAe;AAAA;AAAA,EAGlC,WAAW,IAAI,yBAAgB;AAAA;AAAA,EAG/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,4BAA4B,eAAgC;AAC3D,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM;AACL,2CAAmB,IAAI;AACvB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,IAAI,OAAc,MAAoB;AAErC,QAAI,KAAK,QAAQ,KAAK,SAAS,KAAK,GAAG;AACtC,aAAO,KAAK;AAAA,IACb;AAOA,QAAI;AACJ,QAAI,KAAK,eAAe;AACvB,oBACC,SAAS,SACN,OACA,KAAK,cACJ,KAAK,YAAY,KAAK,SAAS,OAAO,KAAK,sBAAkB,oCAAe,IAAI,CAAC,IACjF;AAAA,IACN;AAKA,gDAAmB;AACnB,UAAM,YAAQ,oCAAe;AAG7B,QAAI,KAAK,eAAe;AACvB,WAAK,cAAc,UAAU,KAAK,kBAAkB,OAAO,WAAW;AAAA,IACvE;AAGA,SAAK,mBAAmB;AAExB,UAAM,WAAW,KAAK;AACtB,SAAK,UAAU;AAGf,2CAAc,MAAa,QAAQ;AAEnC,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,OAAO,SAAyC;AAC/C,WAAO,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAC;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,aAAa,OAAqC;AACjD,2CAAmB,IAAI;AAEvB,QAAI,SAAS,KAAK,kBAAkB;AACnC,aAAO;AAAA,IACR;AAEA,WAAO,KAAK,eAAe,gBAAgB,KAAK,KAAK;AAAA,EACtD;AACD;AAMO,MAAM,YAAQ,0BAAU,QAAQ,MAAM,QAAQ;AA0B9C,SAAS,KAIf,MAIA,cAIA,SACoB;AACpB,SAAO,IAAI,MAAM,MAAM,cAAc,OAAO;AAC7C;AAiBO,SAAS,OAAO,OAAwC;AAC9D,SAAO,iBAAiB;AACzB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -54,6 +54,10 @@ class __EffectScheduler__ {
|
|
|
54
54
|
/** @internal */
|
|
55
55
|
_scheduleCount = 0;
|
|
56
56
|
/** @internal */
|
|
57
|
+
_executeDepth = 0;
|
|
58
|
+
/** @internal */
|
|
59
|
+
_wasScheduledWhileExecuting = false;
|
|
60
|
+
/** @internal */
|
|
57
61
|
__debug_ancestor_epochs__ = null;
|
|
58
62
|
/**
|
|
59
63
|
* The number of times this effect has been scheduled.
|
|
@@ -87,13 +91,17 @@ class __EffectScheduler__ {
|
|
|
87
91
|
if (this._scheduleEffect) {
|
|
88
92
|
this._scheduleEffect(this.maybeExecute);
|
|
89
93
|
} else {
|
|
90
|
-
this.
|
|
94
|
+
this.maybeExecute();
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
/** @internal */
|
|
94
98
|
// eslint-disable-next-line tldraw/prefer-class-methods
|
|
95
99
|
maybeExecute = () => {
|
|
96
100
|
if (!this._isActivelyListening) return;
|
|
101
|
+
if (this._executeDepth > 0) {
|
|
102
|
+
this._wasScheduledWhileExecuting = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
97
105
|
this.execute();
|
|
98
106
|
};
|
|
99
107
|
/**
|
|
@@ -127,6 +135,25 @@ class __EffectScheduler__ {
|
|
|
127
135
|
* @public
|
|
128
136
|
*/
|
|
129
137
|
execute() {
|
|
138
|
+
if (this._executeDepth > 0) return this.executeOnce();
|
|
139
|
+
this._wasScheduledWhileExecuting = false;
|
|
140
|
+
let result = this.executeOnce();
|
|
141
|
+
for (let depth = 0; this._wasScheduledWhileExecuting; depth++) {
|
|
142
|
+
this._wasScheduledWhileExecuting = false;
|
|
143
|
+
if (depth >= 1e3) {
|
|
144
|
+
throw new Error("Reaction update depth limit exceeded");
|
|
145
|
+
}
|
|
146
|
+
if (!this._isActivelyListening) break;
|
|
147
|
+
if (!(0, import_helpers.haveParentsChanged)(this)) {
|
|
148
|
+
this.lastReactedEpoch = (0, import_transactions.getGlobalEpoch)();
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
result = this.executeOnce();
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
executeOnce() {
|
|
156
|
+
this._executeDepth++;
|
|
130
157
|
try {
|
|
131
158
|
(0, import_capture.startCapturingParents)(this);
|
|
132
159
|
const currentEpoch = (0, import_transactions.getGlobalEpoch)();
|
|
@@ -135,6 +162,7 @@ class __EffectScheduler__ {
|
|
|
135
162
|
return result;
|
|
136
163
|
} finally {
|
|
137
164
|
(0, import_capture.stopCapturingParents)();
|
|
165
|
+
this._executeDepth--;
|
|
138
166
|
}
|
|
139
167
|
}
|
|
140
168
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/EffectScheduler.ts"],
|
|
4
|
-
"sourcesContent": ["import { ArraySet } from './ArraySet'\nimport { startCapturingParents, stopCapturingParents } from './capture'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { attach, detach, haveParentsChanged, singleton } from './helpers'\nimport { getGlobalEpoch } from './transactions'\nimport { Signal } from './types'\n\n/** @public */\nexport interface EffectSchedulerOptions {\n\t/**\n\t * scheduleEffect is a function that will be called when the effect is scheduled.\n\t *\n\t * It can be used to defer running effects until a later time, for example to batch them together with requestAnimationFrame.\n\t *\n\t *\n\t * @example\n\t * ```ts\n\t * let isRafScheduled = false\n\t * const scheduledEffects: Array<() => void> = []\n\t * const scheduleEffect = (runEffect: () => void) => {\n\t * \tscheduledEffects.push(runEffect)\n\t * \tif (!isRafScheduled) {\n\t * \t\tisRafScheduled = true\n\t * \t\trequestAnimationFrame(() => {\n\t * \t\t\tisRafScheduled = false\n\t * \t\t\tscheduledEffects.forEach((runEffect) => runEffect())\n\t * \t\t\tscheduledEffects.length = 0\n\t * \t\t})\n\t * \t}\n\t * }\n\t * const stop = react('set page title', () => {\n\t * \tdocument.title = doc.title\n\t * }, { scheduleEffect })\n\t * ```\n\t */\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tscheduleEffect?: (execute: () => void) => void\n}\n\nclass __EffectScheduler__<Result> implements EffectScheduler<Result> {\n\treadonly __isEffectScheduler = true as const\n\t/** @internal */\n\tprivate _isActivelyListening = false\n\t/**\n\t * Whether this scheduler is attached and actively listening to its parents.\n\t * @public\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isActivelyListening() {\n\t\treturn this._isActivelyListening\n\t}\n\t/** @internal */\n\tlastTraversedEpoch = GLOBAL_START_EPOCH\n\n\t/** @internal */\n\tprivate lastReactedEpoch = GLOBAL_START_EPOCH\n\n\t/** @internal */\n\tprivate _scheduleCount = 0\n\t/** @internal */\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null = null\n\n\t/**\n\t * The number of times this effect has been scheduled.\n\t * @public\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget scheduleCount() {\n\t\treturn this._scheduleCount\n\t}\n\n\t/** @internal */\n\treadonly parentSet = new ArraySet<Signal<any, any>>()\n\t/** @internal */\n\treadonly parentEpochs: number[] = []\n\t/** @internal */\n\treadonly parents: Signal<any, any>[] = []\n\t/** @internal */\n\tprivate readonly _scheduleEffect?: (execute: () => void) => void\n\tconstructor(\n\t\tpublic readonly name: string,\n\t\tprivate readonly runEffect: (lastReactedEpoch: number) => Result,\n\t\toptions?: EffectSchedulerOptions\n\t) {\n\t\tthis._scheduleEffect = options?.scheduleEffect\n\t}\n\n\t/** @internal */\n\tmaybeScheduleEffect() {\n\t\t// bail out if we have been cancelled by another effect\n\t\tif (!this._isActivelyListening) return\n\t\t// bail out if no atoms have changed since the last time we ran this effect\n\t\tif (this.lastReactedEpoch === getGlobalEpoch()) return\n\n\t\t// An effect that has run before (or captured parents before throwing) only needs to run\n\t\t// again if one of those parents changed; that includes an effect that captured no parents at\n\t\t// all. An effect that has never run always runs.\n\t\tif (\n\t\t\t(this.lastReactedEpoch !== GLOBAL_START_EPOCH || this.parents.length > 0) &&\n\t\t\t!haveParentsChanged(this)\n\t\t) {\n\t\t\tthis.lastReactedEpoch = getGlobalEpoch()\n\t\t\treturn\n\t\t}\n\t\tthis.scheduleEffect()\n\t}\n\n\t/** @internal */\n\tscheduleEffect() {\n\t\tthis._scheduleCount++\n\t\tif (this._scheduleEffect) {\n\t\t\t// if the effect should be deferred (e.g. until a react render), do so\n\t\t\tthis._scheduleEffect(this.maybeExecute)\n\t\t} else {\n\t\t\t// otherwise execute right now!\n\t\t\tthis.execute()\n\t\t}\n\t}\n\n\t/** @internal */\n\t// eslint-disable-next-line tldraw/prefer-class-methods\n\treadonly maybeExecute = () => {\n\t\t// bail out if we have been detached before this runs\n\t\tif (!this._isActivelyListening) return\n\t\tthis.execute()\n\t}\n\n\t/**\n\t * Makes this scheduler become 'actively listening' to its parents.\n\t * If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.\n\t * If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.\n\t * @public\n\t */\n\tattach() {\n\t\tthis._isActivelyListening = true\n\t\tfor (let i = 0, n = this.parents.length; i < n; i++) {\n\t\t\tconst parent = this.parents[i]\n\t\t\t// a computed parent may have gone stale while nothing listened; see `attach` in helpers.ts\n\t\t\tparent.__unsafe__getWithoutCapture(true)\n\t\t\tattach(parent, this)\n\t\t}\n\t}\n\n\t/**\n\t * Makes this scheduler stop 'actively listening' to its parents.\n\t * It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.\n\t * @public\n\t */\n\tdetach() {\n\t\tthis._isActivelyListening = false\n\t\tfor (let i = 0, n = this.parents.length; i < n; i++) {\n\t\t\tdetach(this.parents[i], this)\n\t\t}\n\t}\n\n\t/**\n\t * Executes the effect immediately and returns the result.\n\t * @returns The result of the effect.\n\t * @public\n\t */\n\texecute(): Result {\n\t\ttry {\n\t\t\tstartCapturingParents(this)\n\t\t\t// Important! We have to make a note of the current epoch before running the effect.\n\t\t\t// We allow atoms to be updated during effects, which increments the global epoch,\n\t\t\t// so if we were to wait until after the effect runs, the this.lastReactedEpoch value might get ahead of itself.\n\t\t\tconst currentEpoch = getGlobalEpoch()\n\t\t\tconst result = this.runEffect(this.lastReactedEpoch)\n\t\t\tthis.lastReactedEpoch = currentEpoch\n\t\t\treturn result\n\t\t} finally {\n\t\t\tstopCapturingParents()\n\t\t}\n\t}\n}\n\n/**\n * An EffectScheduler is responsible for executing side effects in response to changes in state.\n *\n * You probably don't need to use this directly unless you're integrating this library with a framework of some kind.\n *\n * Instead, use the {@link react} and {@link reactor} functions.\n *\n * @example\n * ```ts\n * const render = new EffectScheduler('render', drawToCanvas)\n *\n * render.attach()\n * render.execute()\n * ```\n *\n * @public\n */\nexport const EffectScheduler = singleton(\n\t'EffectScheduler',\n\t(): {\n\t\tnew <Result>(\n\t\t\tname: string,\n\t\t\trunEffect: (lastReactedEpoch: number) => Result,\n\t\t\toptions?: EffectSchedulerOptions\n\t\t): EffectScheduler<Result>\n\t} => __EffectScheduler__\n)\n/** @public */\nexport interface EffectScheduler<Result> {\n\t/**\n\t * Whether this scheduler is attached and actively listening to its parents.\n\t * @public\n\t */\n\treadonly isActivelyListening: boolean\n\n\t/** @internal */\n\treadonly lastTraversedEpoch: number\n\n\t/** @public */\n\treadonly name: string\n\n\t/** @internal */\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null\n\n\t/**\n\t * The number of times this effect has been scheduled.\n\t * @public\n\t */\n\treadonly scheduleCount: number\n\n\t/** @internal */\n\treadonly parentSet: ArraySet<Signal<any, any>>\n\n\t/** @internal */\n\treadonly parentEpochs: number[]\n\n\t/** @internal */\n\treadonly parents: Signal<any, any>[]\n\n\t/** @internal */\n\tmaybeScheduleEffect(): void\n\n\t/** @internal */\n\tscheduleEffect(): void\n\n\t/** @internal */\n\tmaybeExecute(): void\n\n\t/**\n\t * Makes this scheduler become 'actively listening' to its parents.\n\t * If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.\n\t * If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.\n\t * @public\n\t */\n\tattach(): void\n\n\t/**\n\t * Makes this scheduler stop 'actively listening' to its parents.\n\t * It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.\n\t * @public\n\t */\n\tdetach(): void\n\n\t/**\n\t * Executes the effect immediately and returns the result.\n\t * @returns The result of the effect.\n\t * @public\n\t */\n\texecute(): Result\n}\n\n/**\n * Starts a new effect scheduler, scheduling the effect immediately.\n *\n * Returns a function that can be called to stop the scheduler.\n *\n * @example\n * ```ts\n * const color = atom('color', 'red')\n * const stop = react('set style', () => {\n * divElem.style.color = color.get()\n * })\n * color.set('blue')\n * // divElem.style.color === 'blue'\n * stop()\n * color.set('green')\n * // divElem.style.color === 'blue'\n * ```\n *\n *\n * Also useful in React applications for running effects outside of the render cycle.\n *\n * @example\n * ```ts\n * useEffect(() => react('set style', () => {\n * divRef.current.style.color = color.get()\n * }), [])\n * ```\n *\n * @public\n */\nexport function react(\n\tname: string,\n\tfn: (lastReactedEpoch: number) => any,\n\toptions?: EffectSchedulerOptions\n) {\n\tconst scheduler = new EffectScheduler(name, fn, options)\n\tscheduler.attach()\n\tscheduler.scheduleEffect()\n\treturn () => {\n\t\tscheduler.detach()\n\t}\n}\n\n/**\n * The reactor is a user-friendly interface for starting and stopping an `EffectScheduler`.\n *\n * Calling `.start()` will attach the scheduler and execute the effect immediately the first time it is called.\n *\n * If the reactor is stopped, calling `.start()` will re-attach the scheduler but will only execute the effect if any of its parents have changed since it was stopped.\n *\n * You can create a reactor with {@link reactor}.\n * @public\n */\nexport interface Reactor<T = unknown> {\n\t/**\n\t * The underlying effect scheduler.\n\t * @public\n\t */\n\tscheduler: EffectScheduler<T>\n\t/**\n\t * Start the scheduler. The first time this is called the effect will be scheduled immediately.\n\t *\n\t * If the reactor is stopped, calling this will start the scheduler again but will only execute the effect if any of its parents have changed since it was stopped.\n\t *\n\t * If you need to force re-execution of the effect, pass `{ force: true }`.\n\t * @public\n\t */\n\tstart(options?: { force?: boolean }): void\n\t/**\n\t * Stop the scheduler.\n\t * @public\n\t */\n\tstop(): void\n}\n\n/**\n * Creates a {@link Reactor}, which is a thin wrapper around an `EffectScheduler`.\n *\n * @public\n */\nexport function reactor<Result>(\n\tname: string,\n\tfn: (lastReactedEpoch: number) => Result,\n\toptions?: EffectSchedulerOptions\n): Reactor<Result> {\n\tconst scheduler = new EffectScheduler<Result>(name, fn, options)\n\treturn {\n\t\tscheduler,\n\t\tstart: (options?: { force?: boolean }) => {\n\t\t\tconst force = options?.force ?? false\n\t\t\tscheduler.attach()\n\t\t\tif (force) {\n\t\t\t\tscheduler.scheduleEffect()\n\t\t\t} else {\n\t\t\t\tscheduler.maybeScheduleEffect()\n\t\t\t}\n\t\t},\n\t\tstop: () => {\n\t\t\tscheduler.detach()\n\t\t},\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAyB;AACzB,qBAA4D;AAC5D,uBAAmC;AACnC,qBAA8D;AAC9D,0BAA+B;AAmC/B,MAAM,oBAA+D;AAAA,
|
|
4
|
+
"sourcesContent": ["import { ArraySet } from './ArraySet'\nimport { startCapturingParents, stopCapturingParents } from './capture'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { attach, detach, haveParentsChanged, singleton } from './helpers'\nimport { getGlobalEpoch } from './transactions'\nimport { Signal } from './types'\n\n/** @public */\nexport interface EffectSchedulerOptions {\n\t/**\n\t * scheduleEffect is a function that will be called when the effect is scheduled.\n\t *\n\t * It can be used to defer running effects until a later time, for example to batch them together with requestAnimationFrame.\n\t *\n\t *\n\t * @example\n\t * ```ts\n\t * let isRafScheduled = false\n\t * const scheduledEffects: Array<() => void> = []\n\t * const scheduleEffect = (runEffect: () => void) => {\n\t * \tscheduledEffects.push(runEffect)\n\t * \tif (!isRafScheduled) {\n\t * \t\tisRafScheduled = true\n\t * \t\trequestAnimationFrame(() => {\n\t * \t\t\tisRafScheduled = false\n\t * \t\t\tscheduledEffects.forEach((runEffect) => runEffect())\n\t * \t\t\tscheduledEffects.length = 0\n\t * \t\t})\n\t * \t}\n\t * }\n\t * const stop = react('set page title', () => {\n\t * \tdocument.title = doc.title\n\t * }, { scheduleEffect })\n\t * ```\n\t */\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tscheduleEffect?: (execute: () => void) => void\n}\n\nclass __EffectScheduler__<Result> implements EffectScheduler<Result> {\n\treadonly __isEffectScheduler = true as const\n\t/** @internal */\n\tprivate _isActivelyListening = false\n\t/**\n\t * Whether this scheduler is attached and actively listening to its parents.\n\t * @public\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isActivelyListening() {\n\t\treturn this._isActivelyListening\n\t}\n\t/** @internal */\n\tlastTraversedEpoch = GLOBAL_START_EPOCH\n\n\t/** @internal */\n\tprivate lastReactedEpoch = GLOBAL_START_EPOCH\n\n\t/** @internal */\n\tprivate _scheduleCount = 0\n\t/** @internal */\n\tprivate _executeDepth = 0\n\t/** @internal */\n\tprivate _wasScheduledWhileExecuting = false\n\t/** @internal */\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null = null\n\n\t/**\n\t * The number of times this effect has been scheduled.\n\t * @public\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget scheduleCount() {\n\t\treturn this._scheduleCount\n\t}\n\n\t/** @internal */\n\treadonly parentSet = new ArraySet<Signal<any, any>>()\n\t/** @internal */\n\treadonly parentEpochs: number[] = []\n\t/** @internal */\n\treadonly parents: Signal<any, any>[] = []\n\t/** @internal */\n\tprivate readonly _scheduleEffect?: (execute: () => void) => void\n\tconstructor(\n\t\tpublic readonly name: string,\n\t\tprivate readonly runEffect: (lastReactedEpoch: number) => Result,\n\t\toptions?: EffectSchedulerOptions\n\t) {\n\t\tthis._scheduleEffect = options?.scheduleEffect\n\t}\n\n\t/** @internal */\n\tmaybeScheduleEffect() {\n\t\t// bail out if we have been cancelled by another effect\n\t\tif (!this._isActivelyListening) return\n\t\t// bail out if no atoms have changed since the last time we ran this effect\n\t\tif (this.lastReactedEpoch === getGlobalEpoch()) return\n\n\t\t// An effect that has run before (or captured parents before throwing) only needs to run\n\t\t// again if one of those parents changed; that includes an effect that captured no parents at\n\t\t// all. An effect that has never run always runs.\n\t\tif (\n\t\t\t(this.lastReactedEpoch !== GLOBAL_START_EPOCH || this.parents.length > 0) &&\n\t\t\t!haveParentsChanged(this)\n\t\t) {\n\t\t\tthis.lastReactedEpoch = getGlobalEpoch()\n\t\t\treturn\n\t\t}\n\t\tthis.scheduleEffect()\n\t}\n\n\t/** @internal */\n\tscheduleEffect() {\n\t\tthis._scheduleCount++\n\t\tif (this._scheduleEffect) {\n\t\t\t// if the effect should be deferred (e.g. until a react render), do so\n\t\t\tthis._scheduleEffect(this.maybeExecute)\n\t\t} else {\n\t\t\t// otherwise execute right now!\n\t\t\tthis.maybeExecute()\n\t\t}\n\t}\n\n\t/** @internal */\n\t// eslint-disable-next-line tldraw/prefer-class-methods\n\treadonly maybeExecute = () => {\n\t\t// bail out if we have been detached before this runs\n\t\tif (!this._isActivelyListening) return\n\t\t// A set inside the running effect flushed synchronously back to this scheduler (only possible\n\t\t// outside the reaction phase, e.g. the first run of `react()`). Running now would open a second\n\t\t// capture frame inside the open one and corrupt `parents`, so re-check after the run instead.\n\t\tif (this._executeDepth > 0) {\n\t\t\tthis._wasScheduledWhileExecuting = true\n\t\t\treturn\n\t\t}\n\t\tthis.execute()\n\t}\n\n\t/**\n\t * Makes this scheduler become 'actively listening' to its parents.\n\t * If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.\n\t * If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.\n\t * @public\n\t */\n\tattach() {\n\t\tthis._isActivelyListening = true\n\t\tfor (let i = 0, n = this.parents.length; i < n; i++) {\n\t\t\tconst parent = this.parents[i]\n\t\t\t// a computed parent may have gone stale while nothing listened; see `attach` in helpers.ts\n\t\t\tparent.__unsafe__getWithoutCapture(true)\n\t\t\tattach(parent, this)\n\t\t}\n\t}\n\n\t/**\n\t * Makes this scheduler stop 'actively listening' to its parents.\n\t * It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.\n\t * @public\n\t */\n\tdetach() {\n\t\tthis._isActivelyListening = false\n\t\tfor (let i = 0, n = this.parents.length; i < n; i++) {\n\t\t\tdetach(this.parents[i], this)\n\t\t}\n\t}\n\n\t/**\n\t * Executes the effect immediately and returns the result.\n\t * @returns The result of the effect.\n\t * @public\n\t */\n\texecute(): Result {\n\t\t// A direct re-entrant `execute()` from inside the effect is unsupported (both runs share one\n\t\t// `parents`); it is left to run so the outer run can at least finish normally.\n\t\tif (this._executeDepth > 0) return this.executeOnce()\n\t\t// a run that threw may have left this set\n\t\tthis._wasScheduledWhileExecuting = false\n\t\tlet result = this.executeOnce()\n\t\t// If a set inside the run reached this scheduler (see `maybeExecute`), settle it now the way\n\t\t// the reaction phase's cleanup pass would: run again while the parents keep changing.\n\t\tfor (let depth = 0; this._wasScheduledWhileExecuting; depth++) {\n\t\t\tthis._wasScheduledWhileExecuting = false\n\t\t\tif (depth >= 1000) {\n\t\t\t\tthrow new Error('Reaction update depth limit exceeded')\n\t\t\t}\n\t\t\tif (!this._isActivelyListening) break\n\t\t\tif (!haveParentsChanged(this)) {\n\t\t\t\tthis.lastReactedEpoch = getGlobalEpoch()\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tresult = this.executeOnce()\n\t\t}\n\t\treturn result\n\t}\n\n\tprivate executeOnce(): Result {\n\t\t// A counter rather than a flag: a nested `execute()` must not mark the outer run as finished.\n\t\tthis._executeDepth++\n\t\ttry {\n\t\t\tstartCapturingParents(this)\n\t\t\t// Important! We have to make a note of the current epoch before running the effect.\n\t\t\t// We allow atoms to be updated during effects, which increments the global epoch,\n\t\t\t// so if we were to wait until after the effect runs, the this.lastReactedEpoch value might get ahead of itself.\n\t\t\tconst currentEpoch = getGlobalEpoch()\n\t\t\tconst result = this.runEffect(this.lastReactedEpoch)\n\t\t\tthis.lastReactedEpoch = currentEpoch\n\t\t\treturn result\n\t\t} finally {\n\t\t\tstopCapturingParents()\n\t\t\tthis._executeDepth--\n\t\t}\n\t}\n}\n\n/**\n * An EffectScheduler is responsible for executing side effects in response to changes in state.\n *\n * You probably don't need to use this directly unless you're integrating this library with a framework of some kind.\n *\n * Instead, use the {@link react} and {@link reactor} functions.\n *\n * @example\n * ```ts\n * const render = new EffectScheduler('render', drawToCanvas)\n *\n * render.attach()\n * render.execute()\n * ```\n *\n * @public\n */\nexport const EffectScheduler = singleton(\n\t'EffectScheduler',\n\t(): {\n\t\tnew <Result>(\n\t\t\tname: string,\n\t\t\trunEffect: (lastReactedEpoch: number) => Result,\n\t\t\toptions?: EffectSchedulerOptions\n\t\t): EffectScheduler<Result>\n\t} => __EffectScheduler__\n)\n/** @public */\nexport interface EffectScheduler<Result> {\n\t/**\n\t * Whether this scheduler is attached and actively listening to its parents.\n\t * @public\n\t */\n\treadonly isActivelyListening: boolean\n\n\t/** @internal */\n\treadonly lastTraversedEpoch: number\n\n\t/** @public */\n\treadonly name: string\n\n\t/** @internal */\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null\n\n\t/**\n\t * The number of times this effect has been scheduled.\n\t * @public\n\t */\n\treadonly scheduleCount: number\n\n\t/** @internal */\n\treadonly parentSet: ArraySet<Signal<any, any>>\n\n\t/** @internal */\n\treadonly parentEpochs: number[]\n\n\t/** @internal */\n\treadonly parents: Signal<any, any>[]\n\n\t/** @internal */\n\tmaybeScheduleEffect(): void\n\n\t/** @internal */\n\tscheduleEffect(): void\n\n\t/** @internal */\n\tmaybeExecute(): void\n\n\t/**\n\t * Makes this scheduler become 'actively listening' to its parents.\n\t * If it has been executed before it will immediately become eligible to receive 'maybeScheduleEffect' calls.\n\t * If it has not executed before it will need to be manually executed once to become eligible for scheduling, i.e. by calling `EffectScheduler.execute`.\n\t * @public\n\t */\n\tattach(): void\n\n\t/**\n\t * Makes this scheduler stop 'actively listening' to its parents.\n\t * It will no longer be eligible to receive 'maybeScheduleEffect' calls until `EffectScheduler.attach` is called again.\n\t * @public\n\t */\n\tdetach(): void\n\n\t/**\n\t * Executes the effect immediately and returns the result.\n\t * @returns The result of the effect.\n\t * @public\n\t */\n\texecute(): Result\n}\n\n/**\n * Starts a new effect scheduler, scheduling the effect immediately.\n *\n * Returns a function that can be called to stop the scheduler.\n *\n * @example\n * ```ts\n * const color = atom('color', 'red')\n * const stop = react('set style', () => {\n * divElem.style.color = color.get()\n * })\n * color.set('blue')\n * // divElem.style.color === 'blue'\n * stop()\n * color.set('green')\n * // divElem.style.color === 'blue'\n * ```\n *\n *\n * Also useful in React applications for running effects outside of the render cycle.\n *\n * @example\n * ```ts\n * useEffect(() => react('set style', () => {\n * divRef.current.style.color = color.get()\n * }), [])\n * ```\n *\n * @public\n */\nexport function react(\n\tname: string,\n\tfn: (lastReactedEpoch: number) => any,\n\toptions?: EffectSchedulerOptions\n) {\n\tconst scheduler = new EffectScheduler(name, fn, options)\n\tscheduler.attach()\n\tscheduler.scheduleEffect()\n\treturn () => {\n\t\tscheduler.detach()\n\t}\n}\n\n/**\n * The reactor is a user-friendly interface for starting and stopping an `EffectScheduler`.\n *\n * Calling `.start()` will attach the scheduler and execute the effect immediately the first time it is called.\n *\n * If the reactor is stopped, calling `.start()` will re-attach the scheduler but will only execute the effect if any of its parents have changed since it was stopped.\n *\n * You can create a reactor with {@link reactor}.\n * @public\n */\nexport interface Reactor<T = unknown> {\n\t/**\n\t * The underlying effect scheduler.\n\t * @public\n\t */\n\tscheduler: EffectScheduler<T>\n\t/**\n\t * Start the scheduler. The first time this is called the effect will be scheduled immediately.\n\t *\n\t * If the reactor is stopped, calling this will start the scheduler again but will only execute the effect if any of its parents have changed since it was stopped.\n\t *\n\t * If you need to force re-execution of the effect, pass `{ force: true }`.\n\t * @public\n\t */\n\tstart(options?: { force?: boolean }): void\n\t/**\n\t * Stop the scheduler.\n\t * @public\n\t */\n\tstop(): void\n}\n\n/**\n * Creates a {@link Reactor}, which is a thin wrapper around an `EffectScheduler`.\n *\n * @public\n */\nexport function reactor<Result>(\n\tname: string,\n\tfn: (lastReactedEpoch: number) => Result,\n\toptions?: EffectSchedulerOptions\n): Reactor<Result> {\n\tconst scheduler = new EffectScheduler<Result>(name, fn, options)\n\treturn {\n\t\tscheduler,\n\t\tstart: (options?: { force?: boolean }) => {\n\t\t\tconst force = options?.force ?? false\n\t\t\tscheduler.attach()\n\t\t\tif (force) {\n\t\t\t\tscheduler.scheduleEffect()\n\t\t\t} else {\n\t\t\t\tscheduler.maybeScheduleEffect()\n\t\t\t}\n\t\t},\n\t\tstop: () => {\n\t\t\tscheduler.detach()\n\t\t},\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["options"]
|
|
7
7
|
}
|
package/dist-cjs/lib/capture.js
CHANGED
|
@@ -62,50 +62,52 @@ function startCapturingParents(child) {
|
|
|
62
62
|
function stopCapturingParents() {
|
|
63
63
|
const frame = inst.stack;
|
|
64
64
|
inst.stack = frame.below;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
const { child, offset, maybeRemoved } = frame;
|
|
66
|
+
if (offset < child.parents.length) {
|
|
67
|
+
for (let i = offset; i < child.parents.length; i++) {
|
|
68
|
+
const maybeRemovedParent = child.parents[i];
|
|
69
|
+
if (!child.parentSet.has(maybeRemovedParent)) {
|
|
70
|
+
(0, import_helpers.detach)(maybeRemovedParent, child);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
child.parents.length = offset;
|
|
74
|
+
child.parentEpochs.length = offset;
|
|
74
75
|
}
|
|
75
|
-
if (
|
|
76
|
-
for (let i = 0; i <
|
|
77
|
-
const maybeRemovedParent =
|
|
78
|
-
if (!
|
|
79
|
-
(0, import_helpers.detach)(maybeRemovedParent,
|
|
76
|
+
if (maybeRemoved) {
|
|
77
|
+
for (let i = 0; i < maybeRemoved.length; i++) {
|
|
78
|
+
const maybeRemovedParent = maybeRemoved[i];
|
|
79
|
+
if (!child.parentSet.has(maybeRemovedParent)) {
|
|
80
|
+
(0, import_helpers.detach)(maybeRemovedParent, child);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
|
-
if (
|
|
84
|
-
captureAncestorEpochs(
|
|
84
|
+
if (child.__debug_ancestor_epochs__) {
|
|
85
|
+
captureAncestorEpochs(child, child.__debug_ancestor_epochs__);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
function maybeCaptureParent(p) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
const stack = inst.stack;
|
|
90
|
+
if (!stack) return;
|
|
91
|
+
const { child } = stack;
|
|
92
|
+
if (!child.parentSet.add(p)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (child.isActivelyListening) {
|
|
96
|
+
(0, import_helpers.attach)(p, child);
|
|
97
|
+
}
|
|
98
|
+
if (stack.offset < child.parents.length) {
|
|
99
|
+
const maybeRemovedParent = child.parents[stack.offset];
|
|
100
|
+
if (maybeRemovedParent !== p) {
|
|
101
|
+
if (!stack.maybeRemoved) {
|
|
102
|
+
stack.maybeRemoved = [maybeRemovedParent];
|
|
103
|
+
} else {
|
|
104
|
+
stack.maybeRemoved.push(maybeRemovedParent);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
inst.stack.child.parents[inst.stack.offset] = p;
|
|
106
|
-
inst.stack.child.parentEpochs[inst.stack.offset] = p.lastChangedEpoch;
|
|
107
|
-
inst.stack.offset++;
|
|
108
107
|
}
|
|
108
|
+
child.parents[stack.offset] = p;
|
|
109
|
+
child.parentEpochs[stack.offset] = p.lastChangedEpoch;
|
|
110
|
+
stack.offset++;
|
|
109
111
|
}
|
|
110
112
|
function whyAmIRunning() {
|
|
111
113
|
const child = inst.stack?.child;
|
|
@@ -117,13 +119,11 @@ function whyAmIRunning() {
|
|
|
117
119
|
function captureAncestorEpochs(child, ancestorEpochs) {
|
|
118
120
|
for (let i = 0; i < child.parents.length; i++) {
|
|
119
121
|
const parent = child.parents[i];
|
|
120
|
-
|
|
121
|
-
ancestorEpochs.set(parent, epoch);
|
|
122
|
+
ancestorEpochs.set(parent, child.parentEpochs[i]);
|
|
122
123
|
if ((0, import_isComputed.isComputed)(parent)) {
|
|
123
124
|
captureAncestorEpochs(parent, ancestorEpochs);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
|
-
return ancestorEpochs;
|
|
127
127
|
}
|
|
128
128
|
function collectChangedAncestors(child, ancestorEpochs) {
|
|
129
129
|
const changeTree = {};
|
|
@@ -132,9 +132,7 @@ function collectChangedAncestors(child, ancestorEpochs) {
|
|
|
132
132
|
if (!ancestorEpochs.has(parent)) {
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
const currentEpoch = parent.lastChangedEpoch;
|
|
137
|
-
if (currentEpoch !== prevEpoch) {
|
|
135
|
+
if (parent.lastChangedEpoch !== ancestorEpochs.get(parent)) {
|
|
138
136
|
if ((0, import_isComputed.isComputed)(parent)) {
|
|
139
137
|
changeTree[parent.name] = collectChangedAncestors(parent, ancestorEpochs);
|
|
140
138
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/capture.ts"],
|
|
4
|
-
"sourcesContent": ["import { attach, detach, singleton } from './helpers'\nimport { isComputed } from './isComputed'\nimport type { Child, Signal } from './types'\n\nclass CaptureStackFrame {\n\toffset = 0\n\n\tmaybeRemoved?: Signal<any>[]\n\n\tconstructor(\n\t\tpublic readonly below: CaptureStackFrame | null,\n\t\tpublic readonly child: Child\n\t) {}\n}\n\nconst inst = singleton('capture', () => ({ stack: null as null | CaptureStackFrame }))\n\n/**\n * Executes the given function without capturing any parents in the current capture context.\n *\n * This is mainly useful if you want to run an effect only when certain signals change while also\n * dereferencing other signals which should not cause the effect to rerun on their own.\n *\n * @example\n * ```ts\n * const name = atom('name', 'Sam')\n * const time = atom('time', Date.now())\n *\n * setInterval(() => {\n * time.set(Date.now())\n * })\n *\n * react('log name changes', () => {\n * \t print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))\n * })\n *\n * ```\n *\n * @public\n */\nexport function unsafe__withoutCapture<T>(fn: () => T): T {\n\tconst oldStack = inst.stack\n\tinst.stack = null\n\ttry {\n\t\treturn fn()\n\t} finally {\n\t\tinst.stack = oldStack\n\t}\n}\n\n/**\n * Begins capturing parent signal dependencies for the given child signal.\n *\n * This function initiates a capture session where any signal accessed via `.get()`\n * will be automatically registered as a dependency of the child signal. It sets up\n * the capture stack frame and clears the existing parent set to prepare for fresh\n * dependency tracking.\n *\n * @param child - The child signal (computed or effect) that will capture dependencies\n *\n * @example\n * ```ts\n * const effect = createEffect('myEffect', () => { /* ... *\\/ })\n * startCapturingParents(effect)\n * // Now any signal.get() calls will be captured as dependencies\n * ```\n *\n * @internal\n */\nexport function startCapturingParents(child: Child) {\n\tinst.stack = new CaptureStackFrame(inst.stack, child)\n\tif (child.__debug_ancestor_epochs__) {\n\t\tconst previousAncestorEpochs = child.__debug_ancestor_epochs__\n\t\tchild.__debug_ancestor_epochs__ = null\n\t\tfor (const p of child.parents) {\n\t\t\tp.__unsafe__getWithoutCapture(true)\n\t\t}\n\t\tlogChangedAncestors(child, previousAncestorEpochs)\n\t}\n\tchild.parentSet.clear()\n}\n\n/**\n * Completes the parent dependency capture session and finalizes the dependency graph.\n *\n * This function cleans up the capture session by removing dependencies that are no\n * longer needed, detaching signals that should no longer be parents, and updating\n * the dependency arrays to reflect the current set of captured parents. It must be\n * called after `startCapturingParents` to complete the capture cycle.\n *\n * @example\n * ```ts\n * startCapturingParents(effect)\n * // ... signal.get() calls happen here ...\n * stopCapturingParents() // Finalizes the dependency graph\n * ```\n *\n * @internal\n */\nexport function stopCapturingParents() {\n\tconst frame = inst.stack!\n\tinst.stack = frame.below\n\n\tif (
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAA0C;AAC1C,wBAA2B;AAG3B,MAAM,kBAAkB;AAAA,EAKvB,YACiB,OACA,OACf;AAFe;AACA;AAAA,EACd;AAAA,EAFc;AAAA,EACA;AAAA,EANjB,SAAS;AAAA,EAET;AAMD;AAEA,MAAM,WAAO,0BAAU,WAAW,OAAO,EAAE,OAAO,KAAiC,EAAE;AAyB9E,SAAS,uBAA0B,IAAgB;AACzD,QAAM,WAAW,KAAK;AACtB,OAAK,QAAQ;AACb,MAAI;AACH,WAAO,GAAG;AAAA,EACX,UAAE;AACD,SAAK,QAAQ;AAAA,EACd;AACD;AAqBO,SAAS,sBAAsB,OAAc;AACnD,OAAK,QAAQ,IAAI,kBAAkB,KAAK,OAAO,KAAK;AACpD,MAAI,MAAM,2BAA2B;AACpC,UAAM,yBAAyB,MAAM;AACrC,UAAM,4BAA4B;AAClC,eAAW,KAAK,MAAM,SAAS;AAC9B,QAAE,4BAA4B,IAAI;AAAA,IACnC;AACA,wBAAoB,OAAO,sBAAsB;AAAA,EAClD;AACA,QAAM,UAAU,MAAM;AACvB;AAmBO,SAAS,uBAAuB;AACtC,QAAM,QAAQ,KAAK;AACnB,OAAK,QAAQ,MAAM;
|
|
4
|
+
"sourcesContent": ["import { attach, detach, singleton } from './helpers'\nimport { isComputed } from './isComputed'\nimport type { Child, Signal } from './types'\n\nclass CaptureStackFrame {\n\toffset = 0\n\n\tmaybeRemoved?: Signal<any>[]\n\n\tconstructor(\n\t\tpublic readonly below: CaptureStackFrame | null,\n\t\tpublic readonly child: Child\n\t) {}\n}\n\nconst inst = singleton('capture', () => ({ stack: null as null | CaptureStackFrame }))\n\n/**\n * Executes the given function without capturing any parents in the current capture context.\n *\n * This is mainly useful if you want to run an effect only when certain signals change while also\n * dereferencing other signals which should not cause the effect to rerun on their own.\n *\n * @example\n * ```ts\n * const name = atom('name', 'Sam')\n * const time = atom('time', Date.now())\n *\n * setInterval(() => {\n * time.set(Date.now())\n * })\n *\n * react('log name changes', () => {\n * \t print(name.get(), 'was changed at', unsafe__withoutCapture(() => time.get()))\n * })\n *\n * ```\n *\n * @public\n */\nexport function unsafe__withoutCapture<T>(fn: () => T): T {\n\tconst oldStack = inst.stack\n\tinst.stack = null\n\ttry {\n\t\treturn fn()\n\t} finally {\n\t\tinst.stack = oldStack\n\t}\n}\n\n/**\n * Begins capturing parent signal dependencies for the given child signal.\n *\n * This function initiates a capture session where any signal accessed via `.get()`\n * will be automatically registered as a dependency of the child signal. It sets up\n * the capture stack frame and clears the existing parent set to prepare for fresh\n * dependency tracking.\n *\n * @param child - The child signal (computed or effect) that will capture dependencies\n *\n * @example\n * ```ts\n * const effect = createEffect('myEffect', () => { /* ... *\\/ })\n * startCapturingParents(effect)\n * // Now any signal.get() calls will be captured as dependencies\n * ```\n *\n * @internal\n */\nexport function startCapturingParents(child: Child) {\n\tinst.stack = new CaptureStackFrame(inst.stack, child)\n\tif (child.__debug_ancestor_epochs__) {\n\t\tconst previousAncestorEpochs = child.__debug_ancestor_epochs__\n\t\tchild.__debug_ancestor_epochs__ = null\n\t\tfor (const p of child.parents) {\n\t\t\tp.__unsafe__getWithoutCapture(true)\n\t\t}\n\t\tlogChangedAncestors(child, previousAncestorEpochs)\n\t}\n\tchild.parentSet.clear()\n}\n\n/**\n * Completes the parent dependency capture session and finalizes the dependency graph.\n *\n * This function cleans up the capture session by removing dependencies that are no\n * longer needed, detaching signals that should no longer be parents, and updating\n * the dependency arrays to reflect the current set of captured parents. It must be\n * called after `startCapturingParents` to complete the capture cycle.\n *\n * @example\n * ```ts\n * startCapturingParents(effect)\n * // ... signal.get() calls happen here ...\n * stopCapturingParents() // Finalizes the dependency graph\n * ```\n *\n * @internal\n */\nexport function stopCapturingParents() {\n\tconst frame = inst.stack!\n\tinst.stack = frame.below\n\tconst { child, offset, maybeRemoved } = frame\n\n\tif (offset < child.parents.length) {\n\t\tfor (let i = offset; i < child.parents.length; i++) {\n\t\t\tconst maybeRemovedParent = child.parents[i]\n\t\t\tif (!child.parentSet.has(maybeRemovedParent)) {\n\t\t\t\tdetach(maybeRemovedParent, child)\n\t\t\t}\n\t\t}\n\n\t\tchild.parents.length = offset\n\t\tchild.parentEpochs.length = offset\n\t}\n\n\tif (maybeRemoved) {\n\t\tfor (let i = 0; i < maybeRemoved.length; i++) {\n\t\t\tconst maybeRemovedParent = maybeRemoved[i]\n\t\t\tif (!child.parentSet.has(maybeRemovedParent)) {\n\t\t\t\tdetach(maybeRemovedParent, child)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (child.__debug_ancestor_epochs__) {\n\t\tcaptureAncestorEpochs(child, child.__debug_ancestor_epochs__)\n\t}\n}\n\n/**\n * Conditionally captures a signal as a parent dependency during an active capture session.\n *\n * This function is called whenever a signal's `.get()` method is invoked during a\n * capture session. It checks if the signal should be added as a dependency and manages\n * the parent-child relationship in the reactive graph. The function handles deduplication,\n * attachment/detachment, and tracks changes in dependency order.\n *\n * Note: This must be called after the parent signal is up to date.\n *\n * @param p - The signal that might be captured as a parent dependency\n *\n * @example\n * ```ts\n * // This is called internally when you do:\n * const value = someAtom.get() // maybeCaptureParent(someAtom) is called\n * ```\n *\n * @internal\n */\nexport function maybeCaptureParent(p: Signal<any, any>) {\n\tconst stack = inst.stack\n\tif (!stack) return\n\tconst { child } = stack\n\n\t// `add` returns false when the parent was already captured this run. In array mode both\n\t// `has` and `add` scan with indexOf, so going straight to `add` halves the scans per\n\t// captured parent.\n\tif (!child.parentSet.add(p)) {\n\t\treturn\n\t}\n\n\tif (child.isActivelyListening) {\n\t\tattach(p, child)\n\t}\n\n\t// Parents are recorded in deref order. If the slot at this offset held a different parent\n\t// last run, that parent may have moved later in the order or been dropped; only\n\t// stopCapturingParents can tell, so remember it for then.\n\tif (stack.offset < child.parents.length) {\n\t\tconst maybeRemovedParent = child.parents[stack.offset]\n\t\tif (maybeRemovedParent !== p) {\n\t\t\tif (!stack.maybeRemoved) {\n\t\t\t\tstack.maybeRemoved = [maybeRemovedParent]\n\t\t\t} else {\n\t\t\t\tstack.maybeRemoved.push(maybeRemovedParent)\n\t\t\t}\n\t\t}\n\t}\n\n\tchild.parents[stack.offset] = p\n\tchild.parentEpochs[stack.offset] = p.lastChangedEpoch\n\tstack.offset++\n}\n\n/**\n * A debugging tool that tells you why a computed signal or effect is running.\n * Call in the body of a computed signal or effect function. Nothing is logged for the run that\n * calls it; from the next run on, each run logs the ancestors that changed.\n *\n * @example\n * ```ts\n * const name = atom('name', 'Bob')\n * react('greeting', () => {\n * \twhyAmIRunning()\n *\tprint('Hello', name.get())\n * })\n *\n * name.set('Alice')\n *\n * // Effect(greeting) is executing because:\n * // \u21B3 Atom(name) changed\n * ```\n *\n * @public\n */\nexport function whyAmIRunning() {\n\tconst child = inst.stack?.child\n\tif (!child) {\n\t\tthrow new Error('whyAmIRunning() called outside of a reactive context')\n\t}\n\tchild.__debug_ancestor_epochs__ = new Map()\n}\n\nfunction captureAncestorEpochs(child: Child, ancestorEpochs: Map<Signal<any>, number>) {\n\tfor (let i = 0; i < child.parents.length; i++) {\n\t\tconst parent = child.parents[i]\n\t\tancestorEpochs.set(parent, child.parentEpochs[i])\n\t\tif (isComputed(parent)) {\n\t\t\tcaptureAncestorEpochs(parent as any, ancestorEpochs)\n\t\t}\n\t}\n}\n\ntype ChangeTree = { [signalName: string]: ChangeTree } | null\nfunction collectChangedAncestors(\n\tchild: Child,\n\tancestorEpochs: Map<Signal<any>, number>\n): NonNullable<ChangeTree> {\n\tconst changeTree: ChangeTree = {}\n\tfor (let i = 0; i < child.parents.length; i++) {\n\t\tconst parent = child.parents[i]\n\t\tif (!ancestorEpochs.has(parent)) {\n\t\t\tcontinue\n\t\t}\n\t\tif (parent.lastChangedEpoch !== ancestorEpochs.get(parent)) {\n\t\t\tif (isComputed(parent)) {\n\t\t\t\tchangeTree[parent.name] = collectChangedAncestors(parent as any, ancestorEpochs)\n\t\t\t} else {\n\t\t\t\tchangeTree[parent.name] = null\n\t\t\t}\n\t\t}\n\t}\n\treturn changeTree\n}\n\nfunction logChangedAncestors(child: Child, ancestorEpochs: Map<Signal<any>, number>) {\n\tconst changeTree = collectChangedAncestors(child, ancestorEpochs)\n\tif (Object.keys(changeTree).length === 0) {\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.log(`Effect(${child.name}) was executed manually.`)\n\t\treturn\n\t}\n\n\tlet str = isComputed(child)\n\t\t? `Computed(${child.name}) is recomputing because:`\n\t\t: `Effect(${child.name}) is executing because:`\n\n\tfunction logParent(tree: NonNullable<ChangeTree>, indent: number) {\n\t\tconst indentStr = '\\n' + ' '.repeat(indent) + '\u21B3 '\n\t\tfor (const [name, val] of Object.entries(tree)) {\n\t\t\tif (val) {\n\t\t\t\tstr += `${indentStr}Computed(${name}) changed`\n\t\t\t\tlogParent(val, indent + 2)\n\t\t\t} else {\n\t\t\t\tstr += `${indentStr}Atom(${name}) changed`\n\t\t\t}\n\t\t}\n\t}\n\n\tlogParent(changeTree, 1)\n\n\t// eslint-disable-next-line no-console\n\tconsole.log(str)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/lib/helpers.js
CHANGED
|
@@ -60,8 +60,7 @@ function attach(parent, child) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
function equals(a, b) {
|
|
63
|
-
|
|
64
|
-
return shallowEquals;
|
|
63
|
+
return a === b || Object.is(a, b) || Boolean(a && b && typeof a.equals === "function" && a.equals(b));
|
|
65
64
|
}
|
|
66
65
|
function singleton(key, init) {
|
|
67
66
|
const symbol = /* @__PURE__ */ Symbol.for(`com.tldraw.state/${key}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/helpers.ts"],
|
|
4
|
-
"sourcesContent": ["import { Child, Signal } from './types'\n\nfunction isChild(x: any): x is Child {\n\treturn x && typeof x === 'object' && 'parents' in x\n}\n\n/**\n * Whether any of the child's parents changed since the child last recorded their epochs. O(parents);\n * returns at the first changed parent, so parents after it are not brought up to date.\n *\n * @internal\n */\nexport function haveParentsChanged(child: Child): boolean {\n\tfor (let i = 0, n = child.parents.length; i < n; i++) {\n\t\tconst parent = child.parents[i]\n\t\t// Bring the parent up to date first: a computed parent's `lastChangedEpoch` only moves when\n\t\t// it is re-derived.\n\t\tparent.__unsafe__getWithoutCapture(true)\n\n\t\tif (parent.lastChangedEpoch !== child.parentEpochs[i]) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n/**\n * Removes `child` from `parent.children`. A computed parent that loses its last child stops\n * listening itself, recursively, so a signal's `children` set is empty whenever nothing downstream\n * is actively listening.\n *\n * @internal\n */\nexport function detach(parent: Signal<any>, child: Child) {\n\tif (!parent.children.remove(child)) {\n\t\treturn\n\t}\n\n\tif (parent.children.isEmpty && isChild(parent)) {\n\t\tfor (let i = 0, n = parent.parents.length; i < n; i++) {\n\t\t\tdetach(parent.parents[i], parent)\n\t\t}\n\t}\n}\n\n/**\n * Adds `child` to `parent.children`. A computed parent that gains its first child starts\n * listening itself, recursively, so that changes to any ancestor are traversed down to the child.\n *\n * `parent` must be up to date when it is attached: `Computed` assumes an actively-listening\n * computed has been traversed for every ancestor change since it was last checked, which only\n * holds from the moment it started listening. Capture attaches a parent right after reading it;\n * `EffectScheduler.attach` refreshes its parents first.\n *\n * @internal\n */\nexport function attach(parent: Signal<any>, child: Child) {\n\tif (!parent.children.add(child)) {\n\t\treturn\n\t}\n\n\tif (isChild(parent)) {\n\t\tfor (let i = 0, n = parent.parents.length; i < n; i++) {\n\t\t\tattach(parent.parents[i], parent)\n\t\t}\n\t}\n}\n\n/**\n * The default equality used for change detection: `===`, then `Object.is` (so `NaN` equals\n * `NaN`; `0` and `-0` are already equal by `===`), then the old value's own `.equals(b)` method\n * if it has one. Only the old value's `equals` is consulted.\n *\n * @internal\n */\nexport function equals(a: any, b: any): boolean {\n\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,SAAS,QAAQ,GAAoB;AACpC,SAAO,KAAK,OAAO,MAAM,YAAY,aAAa;AACnD;AAQO,SAAS,mBAAmB,OAAuB;AACzD,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,QAAQ,IAAI,GAAG,KAAK;AACrD,UAAM,SAAS,MAAM,QAAQ,CAAC;AAG9B,WAAO,4BAA4B,IAAI;AAEvC,QAAI,OAAO,qBAAqB,MAAM,aAAa,CAAC,GAAG;AACtD,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AASO,SAAS,OAAO,QAAqB,OAAc;AACzD,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,GAAG;AACnC;AAAA,EACD;AAEA,MAAI,OAAO,SAAS,WAAW,QAAQ,MAAM,GAAG;AAC/C,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,QAAQ,IAAI,GAAG,KAAK;AACtD,aAAO,OAAO,QAAQ,CAAC,GAAG,MAAM;AAAA,IACjC;AAAA,EACD;AACD;AAaO,SAAS,OAAO,QAAqB,OAAc;AACzD,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,GAAG;AAChC;AAAA,EACD;AAEA,MAAI,QAAQ,MAAM,GAAG;AACpB,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,QAAQ,IAAI,GAAG,KAAK;AACtD,aAAO,OAAO,QAAQ,CAAC,GAAG,MAAM;AAAA,IACjC;AAAA,EACD;AACD;AASO,SAAS,OAAO,GAAQ,GAAiB;AAC/C,
|
|
4
|
+
"sourcesContent": ["import { Child, Signal } from './types'\n\nfunction isChild(x: any): x is Child {\n\treturn x && typeof x === 'object' && 'parents' in x\n}\n\n/**\n * Whether any of the child's parents changed since the child last recorded their epochs. O(parents);\n * returns at the first changed parent, so parents after it are not brought up to date.\n *\n * @internal\n */\nexport function haveParentsChanged(child: Child): boolean {\n\tfor (let i = 0, n = child.parents.length; i < n; i++) {\n\t\tconst parent = child.parents[i]\n\t\t// Bring the parent up to date first: a computed parent's `lastChangedEpoch` only moves when\n\t\t// it is re-derived.\n\t\tparent.__unsafe__getWithoutCapture(true)\n\n\t\tif (parent.lastChangedEpoch !== child.parentEpochs[i]) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n/**\n * Removes `child` from `parent.children`. A computed parent that loses its last child stops\n * listening itself, recursively, so a signal's `children` set is empty whenever nothing downstream\n * is actively listening.\n *\n * @internal\n */\nexport function detach(parent: Signal<any>, child: Child) {\n\tif (!parent.children.remove(child)) {\n\t\treturn\n\t}\n\n\tif (parent.children.isEmpty && isChild(parent)) {\n\t\tfor (let i = 0, n = parent.parents.length; i < n; i++) {\n\t\t\tdetach(parent.parents[i], parent)\n\t\t}\n\t}\n}\n\n/**\n * Adds `child` to `parent.children`. A computed parent that gains its first child starts\n * listening itself, recursively, so that changes to any ancestor are traversed down to the child.\n *\n * `parent` must be up to date when it is attached: `Computed` assumes an actively-listening\n * computed has been traversed for every ancestor change since it was last checked, which only\n * holds from the moment it started listening. Capture attaches a parent right after reading it;\n * `EffectScheduler.attach` refreshes its parents first.\n *\n * @internal\n */\nexport function attach(parent: Signal<any>, child: Child) {\n\tif (!parent.children.add(child)) {\n\t\treturn\n\t}\n\n\tif (isChild(parent)) {\n\t\tfor (let i = 0, n = parent.parents.length; i < n; i++) {\n\t\t\tattach(parent.parents[i], parent)\n\t\t}\n\t}\n}\n\n/**\n * The default equality used for change detection: `===`, then `Object.is` (so `NaN` equals\n * `NaN`; `0` and `-0` are already equal by `===`), then the old value's own `.equals(b)` method\n * if it has one. Only the old value's `equals` is consulted.\n *\n * @internal\n */\nexport function equals(a: any, b: any): boolean {\n\treturn (\n\t\ta === b || Object.is(a, b) || Boolean(a && b && typeof a.equals === 'function' && a.equals(b))\n\t)\n}\n\n/**\n * Creates or retrieves a singleton instance using a global symbol registry.\n * This ensures that the same instance is shared across all code that uses\n * the same key, even across different module boundaries.\n *\n * The singleton is stored on `globalThis` using a symbol created with\n * `Symbol.for()`, which ensures global uniqueness across realms.\n *\n * @param key - A unique string identifier for the singleton\n * @param init - A function that creates the initial value if it doesn't exist\n * @returns The singleton instance\n * @example\n * ```ts\n * // Create a singleton logger\n * const logger = singleton('logger', () => new Logger())\n *\n * // Elsewhere in the codebase, get the same logger instance\n * const sameLogger = singleton('logger', () => new Logger())\n * // logger === sameLogger\n * ```\n * @internal\n */\nexport function singleton<T>(key: string, init: () => T): T {\n\tconst symbol = Symbol.for(`com.tldraw.state/${key}`)\n\tconst global = globalThis as any\n\tglobal[symbol] ??= init()\n\treturn global[symbol]\n}\n\n/**\n * @public\n */\nexport const EMPTY_ARRAY: [] = singleton('empty_array', () => Object.freeze([]) as any)\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,SAAS,QAAQ,GAAoB;AACpC,SAAO,KAAK,OAAO,MAAM,YAAY,aAAa;AACnD;AAQO,SAAS,mBAAmB,OAAuB;AACzD,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,QAAQ,IAAI,GAAG,KAAK;AACrD,UAAM,SAAS,MAAM,QAAQ,CAAC;AAG9B,WAAO,4BAA4B,IAAI;AAEvC,QAAI,OAAO,qBAAqB,MAAM,aAAa,CAAC,GAAG;AACtD,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AASO,SAAS,OAAO,QAAqB,OAAc;AACzD,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,GAAG;AACnC;AAAA,EACD;AAEA,MAAI,OAAO,SAAS,WAAW,QAAQ,MAAM,GAAG;AAC/C,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,QAAQ,IAAI,GAAG,KAAK;AACtD,aAAO,OAAO,QAAQ,CAAC,GAAG,MAAM;AAAA,IACjC;AAAA,EACD;AACD;AAaO,SAAS,OAAO,QAAqB,OAAc;AACzD,MAAI,CAAC,OAAO,SAAS,IAAI,KAAK,GAAG;AAChC;AAAA,EACD;AAEA,MAAI,QAAQ,MAAM,GAAG;AACpB,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,QAAQ,IAAI,GAAG,KAAK;AACtD,aAAO,OAAO,QAAQ,CAAC,GAAG,MAAM;AAAA,IACjC;AAAA,EACD;AACD;AASO,SAAS,OAAO,GAAQ,GAAiB;AAC/C,SACC,MAAM,KAAK,OAAO,GAAG,GAAG,CAAC,KAAK,QAAQ,KAAK,KAAK,OAAO,EAAE,WAAW,cAAc,EAAE,OAAO,CAAC,CAAC;AAE/F;AAwBO,SAAS,UAAa,KAAa,MAAkB;AAC3D,QAAM,SAAS,uBAAO,IAAI,oBAAoB,GAAG,EAAE;AACnD,QAAM,SAAS;AACf,SAAO,MAAM,MAAM,KAAK;AACxB,SAAO,OAAO,MAAM;AACrB;AAKO,MAAM,cAAkB,UAAU,eAAe,MAAM,OAAO,OAAO,CAAC,CAAC,CAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -38,12 +38,8 @@ class Transaction {
|
|
|
38
38
|
isSync;
|
|
39
39
|
asyncProcessCount = 0;
|
|
40
40
|
initialAtomValues = /* @__PURE__ */ new Map();
|
|
41
|
-
// eslint-disable-next-line tldraw/no-setter-getter
|
|
42
|
-
get isRoot() {
|
|
43
|
-
return this.parent === null;
|
|
44
|
-
}
|
|
45
41
|
commit() {
|
|
46
|
-
if (this.
|
|
42
|
+
if (this.parent === null) {
|
|
47
43
|
flushChanges(this.initialAtomValues.keys());
|
|
48
44
|
return;
|
|
49
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/transactions.ts"],
|
|
4
|
-
"sourcesContent": ["import { _Atom } from './Atom'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { singleton } from './helpers'\nimport { Child, Signal } from './types'\n\ninterface Reactor {\n\tmaybeScheduleEffect(): void\n\tlastTraversedEpoch: number\n}\n\nclass Transaction {\n\tasyncProcessCount = 0\n\tconstructor(\n\t\tpublic readonly parent: Transaction | null,\n\t\tpublic readonly isSync: boolean\n\t) {}\n\n\tinitialAtomValues = new Map<_Atom, any>()\n\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isRoot() {\n\t\treturn this.parent === null\n\t}\n\n\tcommit() {\n\t\tif (this.isRoot) {\n\t\t\t// For root transactions, flush changed atoms\n\t\t\tflushChanges(this.initialAtomValues.keys())\n\t\t\treturn\n\t\t}\n\t\t// For transactions with parents, add the transaction's initial values to the parent's, so\n\t\t// an outer rollback can still undo them (T7). This must come before any \"are we reacting?\" special case: a nested\n\t\t// transaction committed by an effect during the reaction phase used to skip this fold and\n\t\t// its changes survived the outer rollback.\n\t\t// A parent that has recorded nothing yet adopts the map outright: this transaction is\n\t\t// finished with it, and the common nested case is a single inner transaction doing all\n\t\t// the writes.\n\t\tconst parentValues = this.parent!.initialAtomValues\n\t\tif (parentValues.size === 0) {\n\t\t\tthis.parent!.initialAtomValues = this.initialAtomValues\n\t\t\treturn\n\t\t}\n\t\tthis.initialAtomValues.forEach((value, atom) => {\n\t\t\tif (!parentValues.has(atom)) {\n\t\t\t\tparentValues.set(atom, value)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Abort the transaction. Restores every atom changed in this transaction to its initial\n\t * value, then commits. Must be called while this is still `inst.currentTransaction`, so that\n\t * the restoring sets are recorded against it instead of each flushing effects on their own.\n\t *\n\t * @public\n\t */\n\tabort() {\n\t\tinst.globalEpoch++\n\n\t\tthis.initialAtomValues.forEach((value, atom) => {\n\t\t\tatom.set(value)\n\t\t\tatom.historyBuffer?.clear()\n\t\t})\n\n\t\tthis.commit()\n\t}\n}\n\nconst inst = singleton('transactions', () => ({\n\t// The current epoch (global to all atoms).\n\tglobalEpoch: GLOBAL_START_EPOCH + 1,\n\t// Whether any transaction is reacting.\n\tglobalIsReacting: false,\n\tcurrentTransaction: null as Transaction | null,\n\n\tcleanupReactors: null as null | Set<Reactor>,\n}))\n\n/**\n * Gets the current global epoch, which is incremented every time any atom changes.\n * This is used to track changes across the entire reactive system.\n *\n * @returns The current global epoch number\n * @public\n */\nexport function getGlobalEpoch() {\n\treturn inst.globalEpoch\n}\n\n/**\n * Whether a transaction (sync or async) is currently open. While one is, atom changes are\n * recorded but their children are not traversed until it commits.\n *\n * @internal\n */\nexport function getIsInTransaction() {\n\treturn inst.currentTransaction !== null\n}\n\n// The set `traverseChild` collects reactors into. Module-level rather than a closure so that a\n// flush over thousands of atoms doesn't allocate a visitor per atom; traversal never runs user\n// code, so nothing can re-enter and swap it mid-walk.\nlet traverseReactors: Set<Reactor>\n\nfunction traverseChild(child: Child) {\n\tif (child.lastTraversedEpoch === inst.globalEpoch) {\n\t\treturn\n\t}\n\n\tchild.lastTraversedEpoch = inst.globalEpoch\n\n\tif ('__isEffectScheduler' in child) {\n\t\ttraverseReactors.add(child as unknown as Reactor)\n\t} else {\n\t\t;(child as any as Signal<any>).children.visit(traverseChild)\n\t}\n}\n\n/**\n * Collect all of the reactors that need to run for an atom and run them. During the reaction\n * phase the affected effects are instead queued for the cleanup pass, so a change made by an\n * effect never interrupts the pass that is running (P4).\n *\n * @param atoms - The atoms to flush changes for.\n */\nfunction flushChanges(atoms: Iterable<_Atom>) {\n\tif (inst.globalIsReacting) {\n\t\tfor (const atom of atoms) {\n\t\t\ttraverseAtomForCleanup(atom)\n\t\t}\n\t\treturn\n\t}\n\n\tconst outerTxn = inst.currentTransaction\n\ttry {\n\t\t// clear the transaction stack. Transactions started by effects must be roots: the committing\n\t\t// transaction (if any) has already handed its changes to this flush.\n\t\tinst.currentTransaction = null\n\t\tinst.globalIsReacting = true\n\n\t\tconst reactors = new Set<Reactor>()\n\t\ttraverseReactors = reactors\n\t\tfor (const atom of atoms) {\n\t\t\tatom.children.visit(traverseChild)\n\t\t}\n\n\t\t// Run each reactor.\n\t\tfor (const r of reactors) {\n\t\t\tr.maybeScheduleEffect()\n\t\t}\n\n\t\tlet updateDepth = 0\n\t\twhile (inst.cleanupReactors?.size) {\n\t\t\tif (updateDepth++ > 1000) {\n\t\t\t\tthrow new Error('Reaction update depth limit exceeded')\n\t\t\t}\n\t\t\tconst reactors = inst.cleanupReactors\n\t\t\tinst.cleanupReactors = null\n\t\t\tfor (const r of reactors) {\n\t\t\t\tr.maybeScheduleEffect()\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tinst.cleanupReactors = null\n\t\tinst.globalIsReacting = false\n\t\tinst.currentTransaction = outerTxn\n\t\ttraverseReactors = undefined! // free memory\n\t}\n}\n\n/** @internal */\nexport function atomDidChange(atom: _Atom, previousValue: any) {\n\tif (inst.currentTransaction) {\n\t\t// If we are in a transaction, then all we have to do is preserve\n\t\t// the value of the atom at the start of the transaction in case\n\t\t// we need to roll back. Children are not traversed until the\n\t\t// transaction commits, which is why `Computed` must not trust its\n\t\t// traversal-based cache shortcut while a transaction is open.\n\t\tif (!inst.currentTransaction.initialAtomValues.has(atom)) {\n\t\t\tinst.currentTransaction.initialAtomValues.set(atom, previousValue)\n\t\t}\n\t} else {\n\t\t// If there is no transaction, flush the changes immediately.\n\t\tflushChanges([atom])\n\t}\n}\n\nfunction traverseAtomForCleanup(atom: _Atom) {\n\ttraverseReactors = inst.cleanupReactors ??= new Set()\n\tatom.children.visit(traverseChild)\n}\n\n/** @internal */\nexport function advanceGlobalEpoch() {\n\tinst.globalEpoch++\n}\n\n/**\n * Batches state updates, deferring side effects until after the transaction completes.\n * Unlike {@link transact}, this function always creates a new transaction, allowing for nested transactions.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n *\n * react('greet', () => {\n * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction(() => {\n * firstName.set('Jane')\n * lastName.set('Smith')\n * })\n *\n * // Logs \"Hello, Jane Smith!\"\n * ```\n *\n * If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began. An aborted transaction still flushes effects: effects whose parents went through a change-and-restore round trip are checked again and, if a parent's value differs from what they last saw (an atom they read directly always will), run once more with the restored values.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n *\n * react('greet', () => {\n * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction(() => {\n * firstName.set('Jane')\n * throw new Error('oops')\n * })\n *\n * // firstName.get() === 'John'\n * // Logs \"Hello, John Doe!\" again: effects whose parents were changed and restored still run,\n * // and observe the restored values.\n * ```\n *\n * A `rollback` callback is passed into the function.\n * Calling this will prevent the transaction from committing and will revert any signals that were updated during the transaction to their state before the transaction began.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n *\n * react('greet', () => {\n * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction((rollback) => {\n * firstName.set('Jane')\n * lastName.set('Smith')\n * rollback()\n * })\n *\n * // firstName.get() === 'John'\n * // lastName.get() === 'Doe'\n * // Logs \"Hello, John Doe!\" again, as above.\n * ```\n *\n * @param fn - The function to run in a transaction, called with a function to roll back the change.\n * @returns The return value of the function\n * @public\n */\nexport function transaction<T>(fn: (rollback: () => void) => T) {\n\tconst txn = new Transaction(inst.currentTransaction, true)\n\n\tinst.currentTransaction = txn\n\n\ttry {\n\t\tlet result = undefined as T | undefined\n\t\tlet rollback = false\n\n\t\ttry {\n\t\t\tresult = fn(() => (rollback = true))\n\t\t} catch (e) {\n\t\t\ttxn.abort()\n\t\t\tthrow e\n\t\t}\n\n\t\tif (inst.currentTransaction !== txn) {\n\t\t\tthrow new Error('Transaction boundaries overlap')\n\t\t}\n\n\t\tif (rollback) {\n\t\t\ttxn.abort()\n\t\t} else {\n\t\t\ttxn.commit()\n\t\t}\n\n\t\treturn result\n\t} finally {\n\t\tinst.currentTransaction = txn.parent\n\t}\n}\n\n/**\n * Like {@link transaction}, but does not create a new transaction if there is already one in progress.\n * This is the preferred way to batch state updates when you don't need the rollback functionality.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const doubled = atom('doubled', 0)\n *\n * react('update doubled', () => {\n * console.log(`Count: ${count.get()}, Doubled: ${doubled.get()}`)\n * })\n *\n * // This batches both updates into a single reaction\n * transact(() => {\n * count.set(5)\n * doubled.set(count.get() * 2)\n * })\n * // Logs: \"Count: 5, Doubled: 10\"\n * ```\n *\n * @param fn - The function to run in a transaction\n * @returns The return value of the function\n * @public\n */\nexport function transact<T>(fn: () => T): T {\n\tif (inst.currentTransaction) {\n\t\treturn fn()\n\t}\n\treturn transaction(fn)\n}\n\n/**\n * Defers the execution of asynchronous effects until they can be properly handled.\n * This function creates an asynchronous transaction context that batches state updates\n * across async operations while preventing conflicts with synchronous transactions.\n *\n * @example\n * ```ts\n * const data = atom('data', null)\n * const loading = atom('loading', false)\n *\n * await deferAsyncEffects(async () => {\n * loading.set(true)\n * const result = await fetch('/api/data')\n * const json = await result.json()\n * data.set(json)\n * loading.set(false)\n * })\n * ```\n *\n * @param fn - The async function to execute within the deferred context\n * @returns A promise that resolves to the return value of the function\n * @throws Will throw if called during a synchronous transaction\n * @internal\n */\nexport async function deferAsyncEffects<T>(fn: () => Promise<T>) {\n\t// Can't kick off async transactions during a sync transaction because\n\t// the async transaction won't finish until after the sync transaction\n\t// is done.\n\tif (inst.currentTransaction?.isSync) {\n\t\tthrow new Error('deferAsyncEffects cannot be called during a sync transaction')\n\t}\n\n\t// Can't kick off async transactions during a reaction phase at the moment,\n\t// because the transaction stack is cleared after the reaction phase.\n\t// So wait until the path ahead is clear\n\twhile (inst.globalIsReacting) {\n\t\tawait new Promise((r) => queueMicrotask(() => r(null)))\n\t}\n\n\tconst txn = inst.currentTransaction ?? new Transaction(null, false)\n\n\t// don't think this can happen, but just in case\n\tif (txn.isSync) throw new Error('deferAsyncEffects cannot be called during a sync transaction')\n\n\tinst.currentTransaction = txn\n\ttxn.asyncProcessCount++\n\n\tlet result = undefined as T | undefined\n\n\t// `undefined` means \"did not throw\"; a thrown `undefined` is stored as `null` so it still counts.\n\tlet error = undefined as any\n\ttry {\n\t\t// Run the function.\n\t\tresult = await fn()\n\t} catch (e) {\n\t\t// Abort the transaction if the function throws.\n\t\terror = e ?? null\n\t}\n\n\tif (--txn.asyncProcessCount > 0) {\n\t\t// Other processes still share this transaction; it settles when the last one finishes.\n\t\tif (typeof error !== 'undefined') {\n\t\t\t// If the rollback was triggered, abort the transaction.\n\t\t\tthrow error\n\t\t} else {\n\t\t\treturn result\n\t\t}\n\t}\n\n\t// Settle while this is still the current transaction (see `Transaction.abort`): clearing it\n\t// first made every restoring set inside `abort()` flush effects on its own, so effects\n\t// observed half-restored state.\n\ttry {\n\t\tif (typeof error !== 'undefined') {\n\t\t\t// If the rollback was triggered, abort the transaction.\n\t\t\ttxn.abort()\n\t\t\tthrow error\n\t\t} else {\n\t\t\ttxn.commit()\n\t\t\treturn result\n\t\t}\n\t} finally {\n\t\tinst.currentTransaction = null\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,uBAAmC;AACnC,qBAA0B;AAQ1B,MAAM,YAAY;AAAA,EAEjB,YACiB,QACA,QACf;AAFe;AACA;AAAA,EACd;AAAA,EAFc;AAAA,EACA;AAAA,EAHjB,oBAAoB;AAAA,EAMpB,oBAAoB,oBAAI,IAAgB;AAAA
|
|
4
|
+
"sourcesContent": ["import { _Atom } from './Atom'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { singleton } from './helpers'\nimport { Child, Signal } from './types'\n\ninterface Reactor {\n\tmaybeScheduleEffect(): void\n\tlastTraversedEpoch: number\n}\n\nclass Transaction {\n\tasyncProcessCount = 0\n\tconstructor(\n\t\tpublic readonly parent: Transaction | null,\n\t\tpublic readonly isSync: boolean\n\t) {}\n\n\tinitialAtomValues = new Map<_Atom, any>()\n\n\tcommit() {\n\t\tif (this.parent === null) {\n\t\t\t// For root transactions, flush changed atoms\n\t\t\tflushChanges(this.initialAtomValues.keys())\n\t\t\treturn\n\t\t}\n\t\t// For transactions with parents, add the transaction's initial values to the parent's, so\n\t\t// an outer rollback can still undo them (T7). This must come before any \"are we reacting?\" special case: a nested\n\t\t// transaction committed by an effect during the reaction phase used to skip this fold and\n\t\t// its changes survived the outer rollback.\n\t\t// A parent that has recorded nothing yet adopts the map outright: this transaction is\n\t\t// finished with it, and the common nested case is a single inner transaction doing all\n\t\t// the writes.\n\t\tconst parentValues = this.parent!.initialAtomValues\n\t\tif (parentValues.size === 0) {\n\t\t\tthis.parent!.initialAtomValues = this.initialAtomValues\n\t\t\treturn\n\t\t}\n\t\tthis.initialAtomValues.forEach((value, atom) => {\n\t\t\tif (!parentValues.has(atom)) {\n\t\t\t\tparentValues.set(atom, value)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Abort the transaction. Restores every atom changed in this transaction to its initial\n\t * value, then commits. Must be called while this is still `inst.currentTransaction`, so that\n\t * the restoring sets are recorded against it instead of each flushing effects on their own.\n\t *\n\t * @public\n\t */\n\tabort() {\n\t\tinst.globalEpoch++\n\n\t\tthis.initialAtomValues.forEach((value, atom) => {\n\t\t\tatom.set(value)\n\t\t\tatom.historyBuffer?.clear()\n\t\t})\n\n\t\tthis.commit()\n\t}\n}\n\nconst inst = singleton('transactions', () => ({\n\t// The current epoch (global to all atoms).\n\tglobalEpoch: GLOBAL_START_EPOCH + 1,\n\t// Whether any transaction is reacting.\n\tglobalIsReacting: false,\n\tcurrentTransaction: null as Transaction | null,\n\n\tcleanupReactors: null as null | Set<Reactor>,\n}))\n\n/**\n * Gets the current global epoch, which is incremented every time any atom changes.\n * This is used to track changes across the entire reactive system.\n *\n * @returns The current global epoch number\n * @public\n */\nexport function getGlobalEpoch() {\n\treturn inst.globalEpoch\n}\n\n/**\n * Whether a transaction (sync or async) is currently open. While one is, atom changes are\n * recorded but their children are not traversed until it commits.\n *\n * @internal\n */\nexport function getIsInTransaction() {\n\treturn inst.currentTransaction !== null\n}\n\n// The set `traverseChild` collects reactors into. Module-level rather than a closure so that a\n// flush over thousands of atoms doesn't allocate a visitor per atom; traversal never runs user\n// code, so nothing can re-enter and swap it mid-walk.\nlet traverseReactors: Set<Reactor>\n\nfunction traverseChild(child: Child) {\n\tif (child.lastTraversedEpoch === inst.globalEpoch) {\n\t\treturn\n\t}\n\n\tchild.lastTraversedEpoch = inst.globalEpoch\n\n\tif ('__isEffectScheduler' in child) {\n\t\ttraverseReactors.add(child as unknown as Reactor)\n\t} else {\n\t\t;(child as any as Signal<any>).children.visit(traverseChild)\n\t}\n}\n\n/**\n * Collect all of the reactors that need to run for an atom and run them. During the reaction\n * phase the affected effects are instead queued for the cleanup pass, so a change made by an\n * effect never interrupts the pass that is running (P4).\n *\n * @param atoms - The atoms to flush changes for.\n */\nfunction flushChanges(atoms: Iterable<_Atom>) {\n\tif (inst.globalIsReacting) {\n\t\tfor (const atom of atoms) {\n\t\t\ttraverseAtomForCleanup(atom)\n\t\t}\n\t\treturn\n\t}\n\n\tconst outerTxn = inst.currentTransaction\n\ttry {\n\t\t// clear the transaction stack. Transactions started by effects must be roots: the committing\n\t\t// transaction (if any) has already handed its changes to this flush.\n\t\tinst.currentTransaction = null\n\t\tinst.globalIsReacting = true\n\n\t\tconst reactors = new Set<Reactor>()\n\t\ttraverseReactors = reactors\n\t\tfor (const atom of atoms) {\n\t\t\tatom.children.visit(traverseChild)\n\t\t}\n\n\t\t// Run each reactor.\n\t\tfor (const r of reactors) {\n\t\t\tr.maybeScheduleEffect()\n\t\t}\n\n\t\tlet updateDepth = 0\n\t\twhile (inst.cleanupReactors?.size) {\n\t\t\tif (updateDepth++ > 1000) {\n\t\t\t\tthrow new Error('Reaction update depth limit exceeded')\n\t\t\t}\n\t\t\tconst reactors = inst.cleanupReactors\n\t\t\tinst.cleanupReactors = null\n\t\t\tfor (const r of reactors) {\n\t\t\t\tr.maybeScheduleEffect()\n\t\t\t}\n\t\t}\n\t} finally {\n\t\tinst.cleanupReactors = null\n\t\tinst.globalIsReacting = false\n\t\tinst.currentTransaction = outerTxn\n\t\ttraverseReactors = undefined! // free memory\n\t}\n}\n\n/** @internal */\nexport function atomDidChange(atom: _Atom, previousValue: any) {\n\tif (inst.currentTransaction) {\n\t\t// If we are in a transaction, then all we have to do is preserve\n\t\t// the value of the atom at the start of the transaction in case\n\t\t// we need to roll back. Children are not traversed until the\n\t\t// transaction commits, which is why `Computed` must not trust its\n\t\t// traversal-based cache shortcut while a transaction is open.\n\t\tif (!inst.currentTransaction.initialAtomValues.has(atom)) {\n\t\t\tinst.currentTransaction.initialAtomValues.set(atom, previousValue)\n\t\t}\n\t} else {\n\t\t// If there is no transaction, flush the changes immediately.\n\t\tflushChanges([atom])\n\t}\n}\n\nfunction traverseAtomForCleanup(atom: _Atom) {\n\ttraverseReactors = inst.cleanupReactors ??= new Set()\n\tatom.children.visit(traverseChild)\n}\n\n/** @internal */\nexport function advanceGlobalEpoch() {\n\tinst.globalEpoch++\n}\n\n/**\n * Batches state updates, deferring side effects until after the transaction completes.\n * Unlike {@link transact}, this function always creates a new transaction, allowing for nested transactions.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n *\n * react('greet', () => {\n * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction(() => {\n * firstName.set('Jane')\n * lastName.set('Smith')\n * })\n *\n * // Logs \"Hello, Jane Smith!\"\n * ```\n *\n * If the function throws, the transaction is aborted and any signals that were updated during the transaction revert to their state before the transaction began. An aborted transaction still flushes effects: effects whose parents went through a change-and-restore round trip are checked again and, if a parent's value differs from what they last saw (an atom they read directly always will), run once more with the restored values.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n *\n * react('greet', () => {\n * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction(() => {\n * firstName.set('Jane')\n * throw new Error('oops')\n * })\n *\n * // firstName.get() === 'John'\n * // Logs \"Hello, John Doe!\" again: effects whose parents were changed and restored still run,\n * // and observe the restored values.\n * ```\n *\n * A `rollback` callback is passed into the function.\n * Calling this will prevent the transaction from committing and will revert any signals that were updated during the transaction to their state before the transaction began.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n *\n * react('greet', () => {\n * console.log(`Hello, ${firstName.get()} ${lastName.get()}!`)\n * })\n *\n * // Logs \"Hello, John Doe!\"\n *\n * transaction((rollback) => {\n * firstName.set('Jane')\n * lastName.set('Smith')\n * rollback()\n * })\n *\n * // firstName.get() === 'John'\n * // lastName.get() === 'Doe'\n * // Logs \"Hello, John Doe!\" again, as above.\n * ```\n *\n * @param fn - The function to run in a transaction, called with a function to roll back the change.\n * @returns The return value of the function\n * @public\n */\nexport function transaction<T>(fn: (rollback: () => void) => T) {\n\tconst txn = new Transaction(inst.currentTransaction, true)\n\n\tinst.currentTransaction = txn\n\n\ttry {\n\t\tlet result = undefined as T | undefined\n\t\tlet rollback = false\n\n\t\ttry {\n\t\t\tresult = fn(() => (rollback = true))\n\t\t} catch (e) {\n\t\t\ttxn.abort()\n\t\t\tthrow e\n\t\t}\n\n\t\tif (inst.currentTransaction !== txn) {\n\t\t\tthrow new Error('Transaction boundaries overlap')\n\t\t}\n\n\t\tif (rollback) {\n\t\t\ttxn.abort()\n\t\t} else {\n\t\t\ttxn.commit()\n\t\t}\n\n\t\treturn result\n\t} finally {\n\t\tinst.currentTransaction = txn.parent\n\t}\n}\n\n/**\n * Like {@link transaction}, but does not create a new transaction if there is already one in progress.\n * This is the preferred way to batch state updates when you don't need the rollback functionality.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const doubled = atom('doubled', 0)\n *\n * react('update doubled', () => {\n * console.log(`Count: ${count.get()}, Doubled: ${doubled.get()}`)\n * })\n *\n * // This batches both updates into a single reaction\n * transact(() => {\n * count.set(5)\n * doubled.set(count.get() * 2)\n * })\n * // Logs: \"Count: 5, Doubled: 10\"\n * ```\n *\n * @param fn - The function to run in a transaction\n * @returns The return value of the function\n * @public\n */\nexport function transact<T>(fn: () => T): T {\n\tif (inst.currentTransaction) {\n\t\treturn fn()\n\t}\n\treturn transaction(fn)\n}\n\n/**\n * Defers the execution of asynchronous effects until they can be properly handled.\n * This function creates an asynchronous transaction context that batches state updates\n * across async operations while preventing conflicts with synchronous transactions.\n *\n * @example\n * ```ts\n * const data = atom('data', null)\n * const loading = atom('loading', false)\n *\n * await deferAsyncEffects(async () => {\n * loading.set(true)\n * const result = await fetch('/api/data')\n * const json = await result.json()\n * data.set(json)\n * loading.set(false)\n * })\n * ```\n *\n * @param fn - The async function to execute within the deferred context\n * @returns A promise that resolves to the return value of the function\n * @throws Will throw if called during a synchronous transaction\n * @internal\n */\nexport async function deferAsyncEffects<T>(fn: () => Promise<T>) {\n\t// Can't kick off async transactions during a sync transaction because\n\t// the async transaction won't finish until after the sync transaction\n\t// is done.\n\tif (inst.currentTransaction?.isSync) {\n\t\tthrow new Error('deferAsyncEffects cannot be called during a sync transaction')\n\t}\n\n\t// Can't kick off async transactions during a reaction phase at the moment,\n\t// because the transaction stack is cleared after the reaction phase.\n\t// So wait until the path ahead is clear\n\twhile (inst.globalIsReacting) {\n\t\tawait new Promise((r) => queueMicrotask(() => r(null)))\n\t}\n\n\tconst txn = inst.currentTransaction ?? new Transaction(null, false)\n\n\t// don't think this can happen, but just in case\n\tif (txn.isSync) throw new Error('deferAsyncEffects cannot be called during a sync transaction')\n\n\tinst.currentTransaction = txn\n\ttxn.asyncProcessCount++\n\n\tlet result = undefined as T | undefined\n\n\t// `undefined` means \"did not throw\"; a thrown `undefined` is stored as `null` so it still counts.\n\tlet error = undefined as any\n\ttry {\n\t\t// Run the function.\n\t\tresult = await fn()\n\t} catch (e) {\n\t\t// Abort the transaction if the function throws.\n\t\terror = e ?? null\n\t}\n\n\tif (--txn.asyncProcessCount > 0) {\n\t\t// Other processes still share this transaction; it settles when the last one finishes.\n\t\tif (typeof error !== 'undefined') {\n\t\t\t// If the rollback was triggered, abort the transaction.\n\t\t\tthrow error\n\t\t} else {\n\t\t\treturn result\n\t\t}\n\t}\n\n\t// Settle while this is still the current transaction (see `Transaction.abort`): clearing it\n\t// first made every restoring set inside `abort()` flush effects on its own, so effects\n\t// observed half-restored state.\n\ttry {\n\t\tif (typeof error !== 'undefined') {\n\t\t\t// If the rollback was triggered, abort the transaction.\n\t\t\ttxn.abort()\n\t\t\tthrow error\n\t\t} else {\n\t\t\ttxn.commit()\n\t\t\treturn result\n\t\t}\n\t} finally {\n\t\tinst.currentTransaction = null\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["reactors"]
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
package/dist-esm/lib/Atom.mjs
CHANGED
|
@@ -8,12 +8,11 @@ class __Atom__ {
|
|
|
8
8
|
constructor(name, current, options) {
|
|
9
9
|
this.name = name;
|
|
10
10
|
this.current = current;
|
|
11
|
-
this.isEqual = options?.isEqual ??
|
|
12
|
-
if (
|
|
13
|
-
if (options.historyLength) {
|
|
11
|
+
this.isEqual = options?.isEqual ?? equals;
|
|
12
|
+
if (options?.historyLength) {
|
|
14
13
|
this.historyBuffer = new HistoryBuffer(options.historyLength);
|
|
15
14
|
}
|
|
16
|
-
this.computeDiff = options
|
|
15
|
+
this.computeDiff = options?.computeDiff;
|
|
17
16
|
}
|
|
18
17
|
name;
|
|
19
18
|
current;
|
|
@@ -66,7 +65,7 @@ class __Atom__ {
|
|
|
66
65
|
* ```
|
|
67
66
|
*/
|
|
68
67
|
set(value, diff) {
|
|
69
|
-
if (this.isEqual
|
|
68
|
+
if (this.isEqual(this.current, value)) {
|
|
70
69
|
return this.current;
|
|
71
70
|
}
|
|
72
71
|
let historyDiff;
|