@tldraw/state 5.4.0-canary.2cc059276813 → 5.4.0-canary.2fdc29a83f08
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/Computed.js +8 -3
- package/dist-cjs/lib/Computed.js.map +2 -2
- package/dist-cjs/lib/EffectScheduler.js +4 -2
- package/dist-cjs/lib/EffectScheduler.js.map +2 -2
- package/dist-cjs/lib/helpers.js.map +2 -2
- package/dist-cjs/lib/localStorageAtom.js +7 -2
- package/dist-cjs/lib/localStorageAtom.js.map +2 -2
- package/dist-cjs/lib/transactions.js +33 -37
- package/dist-cjs/lib/transactions.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/Computed.mjs +9 -4
- package/dist-esm/lib/Computed.mjs.map +2 -2
- package/dist-esm/lib/EffectScheduler.mjs +4 -2
- package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
- package/dist-esm/lib/helpers.mjs.map +2 -2
- package/dist-esm/lib/localStorageAtom.mjs +7 -2
- package/dist-esm/lib/localStorageAtom.mjs.map +2 -2
- package/dist-esm/lib/transactions.mjs +33 -37
- package/dist-esm/lib/transactions.mjs.map +2 -2
- package/package.json +2 -2
- package/src/lib/Computed.ts +15 -5
- package/src/lib/EffectScheduler.ts +11 -4
- package/src/lib/__tests__/EffectScheduler.test.ts +69 -0
- package/src/lib/__tests__/deferAsyncEffects.test.ts +23 -0
- package/src/lib/__tests__/errors.test.ts +24 -0
- package/src/lib/__tests__/fuzz.tlstate.test.ts +97 -8
- package/src/lib/__tests__/localStorageAtom.test.ts +15 -0
- package/src/lib/__tests__/propagation.test.ts +104 -1
- package/src/lib/__tests__/transactions.test.ts +23 -0
- package/src/lib/helpers.ts +5 -0
- package/src/lib/localStorageAtom.ts +9 -2
- package/src/lib/transactions.ts +55 -61
package/dist-cjs/index.js
CHANGED
package/dist-cjs/lib/Computed.js
CHANGED
|
@@ -73,7 +73,8 @@ class __UNSAFE__Computed {
|
|
|
73
73
|
lastTraversedEpoch = import_constants.GLOBAL_START_EPOCH;
|
|
74
74
|
__debug_ancestor_epochs__ = null;
|
|
75
75
|
/**
|
|
76
|
-
* The epoch when the reactor was last checked.
|
|
76
|
+
* The epoch when the reactor was last checked. Advances on every check, including ones that
|
|
77
|
+
* find no change, unlike `lastChangedEpoch`.
|
|
77
78
|
*/
|
|
78
79
|
lastCheckedEpoch = import_constants.GLOBAL_START_EPOCH;
|
|
79
80
|
parentSet = new import_ArraySet.ArraySet();
|
|
@@ -94,7 +95,11 @@ class __UNSAFE__Computed {
|
|
|
94
95
|
__unsafe__getWithoutCapture(ignoreErrors) {
|
|
95
96
|
const isNew = this.lastChangedEpoch === import_constants.GLOBAL_START_EPOCH;
|
|
96
97
|
const globalEpoch = (0, import_transactions.getGlobalEpoch)();
|
|
97
|
-
if (!isNew && (this.lastCheckedEpoch === globalEpoch ||
|
|
98
|
+
if (!isNew && (this.lastCheckedEpoch === globalEpoch || // Every ancestor change to an actively-listening computed traverses it (`flushChanges`),
|
|
99
|
+
// so if it hasn't been traversed since it was last checked the O(parents) scan can be
|
|
100
|
+
// skipped. Not valid while a transaction is open (changes traverse at commit), and only
|
|
101
|
+
// if it was up to date when attached (see `attach` in helpers.ts).
|
|
102
|
+
this.isActivelyListening && !(0, import_transactions.getIsInTransaction)() && this.lastTraversedEpoch <= this.lastCheckedEpoch || !(0, import_helpers.haveParentsChanged)(this))) {
|
|
98
103
|
this.lastCheckedEpoch = globalEpoch;
|
|
99
104
|
if (this.error) {
|
|
100
105
|
if (!ignoreErrors) {
|
|
@@ -129,7 +134,7 @@ class __UNSAFE__Computed {
|
|
|
129
134
|
return this.state;
|
|
130
135
|
} catch (e) {
|
|
131
136
|
const epoch = (0, import_transactions.getGlobalEpoch)();
|
|
132
|
-
if (this.
|
|
137
|
+
if (this.error === null) {
|
|
133
138
|
this.state = UNINITIALIZED;
|
|
134
139
|
this.lastChangedEpoch = epoch;
|
|
135
140
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/Computed.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { assert } from '@tldraw/utils'\nimport { ArraySet } from './ArraySet'\nimport { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'\nimport { HistoryBuffer } from './HistoryBuffer'\nimport { getGlobalEpoch, getIsReacting, getReactionEpoch } from './transactions'\nimport { Child, ComputeDiff, RESET_VALUE, Signal } from './types'\nimport { logComputedGetterWarning } from './warnings'\n\n/**\n * A special symbol used to indicate that a computed signal has not been initialized yet.\n * This is passed as the `previousValue` parameter to a computed signal function on its first run.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * if (isUninitialized(prevValue)) {\n * console.log('First computation!')\n * }\n * return count.get() * 2\n * })\n * ```\n *\n * @public\n */\nexport const UNINITIALIZED = Symbol.for('com.tldraw.state/UNINITIALIZED')\n/**\n * The type of the first value passed to a computed signal function as the 'prevValue' parameter.\n * This type represents the uninitialized state of a computed signal before its first calculation.\n *\n * @see {@link isUninitialized}\n * @public\n */\nexport type UNINITIALIZED = typeof UNINITIALIZED\n\n/**\n * Call this inside a computed signal function to determine whether it is the first time the function is being called.\n *\n * Mainly useful for incremental signal computation.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * if (isUninitialized(prevValue)) {\n * print('First time!')\n * }\n * return count.get() * 2\n * })\n * ```\n *\n * @param value - The value to check.\n * @public\n */\nexport function isUninitialized(value: any): value is UNINITIALIZED {\n\treturn value === UNINITIALIZED\n}\n\n/**\n * A singleton class used to wrap computed signal values along with their diffs.\n * This class is used internally by the {@link withDiff} function to provide both\n * the computed value and its diff to the signal system.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * const nextValue = count.get() * 2\n * if (isUninitialized(prevValue)) {\n * return nextValue\n * }\n * return withDiff(nextValue, nextValue - prevValue)\n * })\n * ```\n *\n * @public\n */\nexport const WithDiff = singleton(\n\t'WithDiff',\n\t() =>\n\t\tclass WithDiff<Value, Diff> {\n\t\t\tconstructor(\n\t\t\t\tpublic value: Value,\n\t\t\t\tpublic diff: Diff\n\t\t\t) {}\n\t\t}\n)\n\n/**\n * Interface representing a value wrapped with its corresponding diff.\n * Used in incremental computation to provide both the new value and the diff from the previous value.\n *\n * @public\n */\nexport interface WithDiff<Value, Diff> {\n\t/**\n\t * The computed value.\n\t */\n\tvalue: Value\n\t/**\n\t * The diff between the previous and current value.\n\t */\n\tdiff: Diff\n}\n\n/**\n * When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too.\n *\n * You can use this function to wrap the return value of a computed signal function to indicate that the diff should be used instead of calculating a new one with {@link AtomOptions.computeDiff}.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * const nextValue = count.get() * 2\n * if (isUninitialized(prevValue)) {\n * return nextValue\n * }\n * return withDiff(nextValue, nextValue - prevValue)\n * }, { historyLength: 10 })\n * ```\n *\n *\n * @param value - The value.\n * @param diff - The diff.\n * @public\n */\nexport function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff> {\n\treturn new WithDiff(value, diff)\n}\n\n/**\n * Options for configuring computed signals. Used when calling `computed` or using the `@computed` decorator.\n *\n * @example\n * ```ts\n * const greeting = computed('greeting', () => `Hello ${name.get()}!`, {\n * historyLength: 10,\n * isEqual: (a, b) => a === b,\n * computeDiff: (oldVal, newVal) => ({ type: 'change', from: oldVal, to: newVal })\n * })\n * ```\n *\n * @public\n */\nexport interface ComputedOptions<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 supplied via {@link withDiff} or {@link ComputedOptions.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 computed's old and new values. If provided, it will not be used unless you also specify {@link ComputedOptions.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 computed 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 * A computed signal created via the `computed` function or `@computed` decorator.\n * Computed signals derive their values from other signals and automatically update when their dependencies change.\n * They use lazy evaluation, only recalculating when accessed and dependencies have changed.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n * const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`)\n *\n * console.log(fullName.get()) // \"John Doe\"\n * firstName.set('Jane')\n * console.log(fullName.get()) // \"Jane Doe\"\n * ```\n *\n * @public\n */\nexport interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {\n\t/**\n\t * Whether this computed signal is involved in an actively-running effect graph.\n\t * Returns true if there are any reactions or other computed signals depending on this one.\n\t * @public\n\t */\n\treadonly isActivelyListening: boolean\n\n\t/** @internal */\n\treadonly parentSet: ArraySet<Signal<any, any>>\n\t/** @internal */\n\treadonly parents: Signal<any, any>[]\n\t/** @internal */\n\treadonly parentEpochs: number[]\n}\n\n/**\n * @internal\n */\nclass __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff> {\n\treadonly __isComputed = true as const\n\tlastChangedEpoch = GLOBAL_START_EPOCH\n\tlastTraversedEpoch = GLOBAL_START_EPOCH\n\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null = null\n\n\t/**\n\t * The epoch when the reactor was last checked.\n\t */\n\tprivate lastCheckedEpoch = GLOBAL_START_EPOCH\n\n\tparentSet = new ArraySet<Signal<any, any>>()\n\tparents: Signal<any, any>[] = []\n\tparentEpochs: number[] = []\n\n\tchildren = new ArraySet<Child>()\n\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isActivelyListening(): boolean {\n\t\treturn !this.children.isEmpty\n\t}\n\n\thistoryBuffer?: HistoryBuffer<Diff>\n\n\t// The last-computed value of this signal.\n\tprivate state: Value = UNINITIALIZED as unknown as Value\n\t// If the signal throws an error we stash it so we can rethrow it on the next get()\n\tprivate error: null | { thrownValue: any } = null\n\n\tprivate computeDiff?: ComputeDiff<Value, Diff>\n\n\tprivate readonly isEqual: (a: any, b: any) => boolean\n\n\tconstructor(\n\t\t/**\n\t\t * The name of the signal. This is used for debugging and performance profiling purposes. It does not need to be globally unique.\n\t\t */\n\t\tpublic readonly name: string,\n\t\t/**\n\t\t * The function that computes the value of the signal.\n\t\t */\n\t\tprivate readonly derive: (\n\t\t\tpreviousValue: Value | UNINITIALIZED,\n\t\t\tlastComputedEpoch: number\n\t\t) => Value | WithDiff<Value, Diff>,\n\t\toptions?: ComputedOptions<Value, Diff>\n\t) {\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\tthis.isEqual = options?.isEqual ?? equals\n\t}\n\n\t__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {\n\t\tconst isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH\n\n\t\tconst globalEpoch = getGlobalEpoch()\n\n\t\tif (\n\t\t\t!isNew &&\n\t\t\t(this.lastCheckedEpoch === globalEpoch ||\n\t\t\t\t(this.isActivelyListening &&\n\t\t\t\t\tgetIsReacting() &&\n\t\t\t\t\tthis.lastTraversedEpoch < getReactionEpoch()) ||\n\t\t\t\t!haveParentsChanged(this))\n\t\t) {\n\t\t\tthis.lastCheckedEpoch = globalEpoch\n\t\t\tif (this.error) {\n\t\t\t\tif (!ignoreErrors) {\n\t\t\t\t\tthrow this.error.thrownValue\n\t\t\t\t} else {\n\t\t\t\t\treturn this.state // will be UNINITIALIZED\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn this.state\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tstartCapturingParents(this)\n\t\t\tconst result = this.derive(this.state, this.lastCheckedEpoch)\n\t\t\tconst newState = result instanceof WithDiff ? result.value : result\n\t\t\tconst isUninitialized = this.state === UNINITIALIZED\n\t\t\t// `derive` may have advanced the epoch, so re-read it here, but only once \u2014 the whole\n\t\t\t// commit below belongs to a single epoch.\n\t\t\tconst epoch = getGlobalEpoch()\n\t\t\tif (isUninitialized || !this.isEqual(this.state, newState)) {\n\t\t\t\tif (this.historyBuffer && !isUninitialized) {\n\t\t\t\t\t// Only `undefined` means \"no diff supplied\"; `null` can be a legitimate diff.\n\t\t\t\t\tconst diff = result instanceof WithDiff ? result.diff : undefined\n\t\t\t\t\tthis.historyBuffer.pushEntry(\n\t\t\t\t\t\tthis.lastChangedEpoch,\n\t\t\t\t\t\tepoch,\n\t\t\t\t\t\tdiff !== undefined\n\t\t\t\t\t\t\t? diff\n\t\t\t\t\t\t\t: this.computeDiff\n\t\t\t\t\t\t\t\t? this.computeDiff(this.state, newState, this.lastCheckedEpoch, epoch)\n\t\t\t\t\t\t\t\t: RESET_VALUE\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthis.lastChangedEpoch = epoch\n\t\t\t\tthis.state = newState\n\t\t\t}\n\t\t\tthis.error = null\n\t\t\tthis.lastCheckedEpoch = epoch\n\n\t\t\treturn this.state\n\t\t} catch (e) {\n\t\t\t// if a derived value throws an error, we reset the state to UNINITIALIZED\n\t\t\tconst epoch = getGlobalEpoch()\n\t\t\tif (this.state !== UNINITIALIZED) {\n\t\t\t\tthis.state = UNINITIALIZED as unknown as Value\n\t\t\t\tthis.lastChangedEpoch = epoch\n\t\t\t}\n\t\t\tthis.lastCheckedEpoch = epoch\n\t\t\t// we also clear the history buffer if an error was thrown\n\t\t\tif (this.historyBuffer) {\n\t\t\t\tthis.historyBuffer.clear()\n\t\t\t}\n\t\t\tthis.error = { thrownValue: e }\n\t\t\t// we don't wish to propagate errors when derefed via haveParentsChanged()\n\t\t\tif (!ignoreErrors) throw e\n\t\t\treturn this.state\n\t\t} finally {\n\t\t\tstopCapturingParents()\n\t\t}\n\t}\n\n\tget(): Value {\n\t\ttry {\n\t\t\treturn this.__unsafe__getWithoutCapture()\n\t\t} finally {\n\t\t\t// if the deriver throws an error we still need to capture\n\t\t\tmaybeCaptureParent(this)\n\t\t}\n\t}\n\n\tgetDiffSince(epoch: number): RESET_VALUE | Diff[] {\n\t\t// we can ignore any errors thrown during derive\n\t\tthis.__unsafe__getWithoutCapture(true)\n\t\t// and we still need to capture this signal as a parent\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 computed signal implementation class.\n * Used internally by the library to create computed signal instances.\n *\n * @internal\n */\nexport const _Computed = singleton('Computed', () => __UNSAFE__Computed)\n\n/**\n * Type alias for the computed signal implementation class.\n *\n * @internal\n */\nexport type _Computed = InstanceType<typeof __UNSAFE__Computed>\n\n// Set on every decorated wrapper: the symbol its per-instance computed is stored under.\nconst derivationKeyKey = '@@__computedDerivationKey__@@'\n\n/**\n * Builds the method (or getter) body that `@computed` installs: a per-instance, lazily-created\n * computed over the original method. The storage symbol is unique per decoration rather than\n * `Symbol.for(name)` so that a subclass which overrides a `@computed` method and calls\n * `super.method()` reaches the superclass's computed instead of re-entering its own.\n */\nfunction makeComputedWrapper(name: string, compute: () => any, options: ComputedOptions<any, any>) {\n\tconst derivationKey = Symbol('__@tldraw/state__computed__' + name)\n\n\tconst wrapper = function (this: any) {\n\t\tlet d = this[derivationKey] as Computed<any> | undefined\n\n\t\tif (!d) {\n\t\t\td = new _Computed(name, compute.bind(this) as any, options)\n\t\t\tObject.defineProperty(this, derivationKey, {\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: false,\n\t\t\t\twritable: false,\n\t\t\t\tvalue: d,\n\t\t\t})\n\t\t}\n\t\treturn d.get()\n\t} as any\n\twrapper[derivationKeyKey] = derivationKey\n\treturn wrapper\n}\n\nfunction computedDecorator(\n\toptions: ComputedOptions<any, any> = {},\n\targs:\n\t\t| [target: any, key: string, descriptor: PropertyDescriptor]\n\t\t| [originalMethod: () => any, context: ClassMethodDecoratorContext]\n) {\n\tif (args.length === 2) {\n\t\tconst [originalMethod, context] = args\n\t\tassert(context.kind === 'method', '@computed can only be used on methods')\n\t\treturn makeComputedWrapper(String(context.name), originalMethod, options)\n\t} else {\n\t\tconst [_target, key, descriptor] = args\n\t\tif (descriptor.get) {\n\t\t\tlogComputedGetterWarning()\n\t\t\tdescriptor.get = makeComputedWrapper(key, descriptor.get, options)\n\t\t} else {\n\t\t\tdescriptor.value = makeComputedWrapper(key, descriptor.value, options)\n\t\t}\n\t\treturn descriptor\n\t}\n}\n\n/**\n * Retrieves the underlying computed instance for a given property created with the `computed`\n * decorator.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom('count', 0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n *\n * const c = new Counter()\n * const remaining = getComputedInstance(c, 'getRemaining')\n * remaining.get() === 100 // true\n * c.count.set(13)\n * remaining.get() === 87 // true\n * ```\n *\n * @param obj - The object\n * @param propertyName - The property name\n * @public\n */\nexport function getComputedInstance<Obj extends object, Prop extends keyof Obj>(\n\tobj: Obj,\n\tpropertyName: Prop\n): Computed<Obj[Prop] extends () => infer Value ? Value : Obj[Prop]> {\n\t// The nearest decorated wrapper (a method, or a legacy getter) on the prototype chain knows\n\t// which symbol the instance's computed is stored under.\n\tfor (let proto: any = obj; proto; proto = Object.getPrototypeOf(proto)) {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(proto, propertyName)\n\t\tconst wrapper = descriptor?.get ?? descriptor?.value\n\t\tconst key = wrapper?.[derivationKeyKey]\n\t\tif (!key) continue\n\n\t\tlet inst = (obj as any)[key]\n\t\tif (!inst) {\n\t\t\t// calling the wrapper creates the computed (before it first derives, so a throwing derive\n\t\t\t// still leaves the instance behind)\n\t\t\ttry {\n\t\t\t\twrapper.call(obj)\n\t\t\t} catch {\n\t\t\t\t// the caller asked for the instance, not its value\n\t\t\t}\n\t\t\tinst = (obj as any)[key]\n\t\t}\n\t\treturn inst\n\t}\n\treturn undefined as any\n}\n\n/**\n * Creates a computed signal that derives its value from other signals.\n * Computed signals automatically update when their dependencies change and use lazy evaluation\n * for optimal performance.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n * const greeting = computed('greeting', () => `Hello ${name.get()}!`)\n * console.log(greeting.get()) // 'Hello John!'\n * ```\n *\n * `computed` may also be used as a decorator for creating computed getter methods.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom('count', 0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * You may optionally pass in a {@link ComputedOptions} when used as a decorator:\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom('count', 0)\n *\n * @computed({isEqual: (a, b) => a === b})\n * getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * @param name - The name of the signal for debugging purposes\n * @param compute - The function that computes the value of the signal. Receives the previous value and last computed epoch\n * @param options - Optional configuration for the computed signal\n * @returns A new computed signal\n * @public\n */\nexport function computed<Value, Diff = unknown>(\n\tname: string,\n\tcompute: (\n\t\tpreviousValue: Value | typeof UNINITIALIZED,\n\t\tlastComputedEpoch: number\n\t) => Value | WithDiff<Value, Diff>,\n\toptions?: ComputedOptions<Value, Diff>\n): Computed<Value, Diff>\n/**\n * TC39 decorator for creating computed methods in classes.\n *\n * @example\n * ```ts\n * class MyClass {\n * value = atom('value', 10)\n *\n * @computed\n * doubled() {\n * return this.value.get() * 2\n * }\n * }\n * ```\n *\n * @param compute - The method to be decorated\n * @param context - The decorator context provided by TypeScript\n * @returns The decorated method\n * @public\n */\nexport function computed<This extends object, Value>(\n\tcompute: () => Value,\n\tcontext: ClassMethodDecoratorContext<This, () => Value>\n): () => Value\n/**\n * Legacy TypeScript decorator for creating computed methods in classes.\n *\n * @example\n * ```ts\n * class MyClass {\n * value = atom('value', 10)\n *\n * @computed\n * doubled() {\n * return this.value.get() * 2\n * }\n * }\n * ```\n *\n * @param target - The class prototype\n * @param key - The property key\n * @param descriptor - The property descriptor\n * @returns The modified property descriptor\n * @public\n */\nexport function computed(\n\ttarget: any,\n\tkey: string,\n\tdescriptor: PropertyDescriptor\n): PropertyDescriptor\n/**\n * Decorator factory for creating computed methods with options.\n *\n * @example\n * ```ts\n * class MyClass {\n * items = atom('items', [1, 2, 3])\n *\n * @computed({ historyLength: 10 })\n * sum() {\n * return this.items.get().reduce((a, b) => a + b, 0)\n * }\n * }\n * ```\n *\n * @param options - Configuration options for the computed signal\n * @returns A decorator function that can be applied to methods\n * @public\n */\nexport function computed<Value, Diff = unknown>(\n\toptions?: ComputedOptions<Value, Diff>\n): ((target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor) &\n\t(<This>(\n\t\tcompute: () => Value,\n\t\tcontext: ClassMethodDecoratorContext<This, () => Value>\n\t) => () => Value)\n\n/**\n * Implementation function that handles all computed signal creation and decoration scenarios.\n * This function is overloaded to support multiple usage patterns:\n * - Creating computed signals directly\n * - Using as a TC39 decorator\n * - Using as a legacy decorator\n * - Using as a decorator factory with options\n *\n * @returns Either a computed signal instance or a decorator function depending on usage\n * @public\n */\nexport function computed() {\n\tif (arguments.length <= 1) {\n\t\t// decorator factory: `@computed(options)` or `@computed()`\n\t\tconst options = arguments[0]\n\t\treturn (...args: any) => computedDecorator(options, args)\n\t} else if (typeof arguments[0] === 'string') {\n\t\treturn new _Computed(arguments[0], arguments[1], arguments[2])\n\t} else {\n\t\treturn computedDecorator(undefined, arguments as any)\n\t}\n}\n\nimport { isComputed as _isComputed } from './isComputed'\n\n/**\n * Returns true if the given value is a computed signal.\n * @public\n */\nexport function isComputed(value: any): value is Computed<any> {\n\treturn _isComputed(value)\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAAuB;AACvB,sBAAyB;AACzB,qBAAgF;AAChF,uBAAmC;AACnC,qBAAmE;AACnE,2BAA8B;AAC9B,
|
|
4
|
+
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { assert } from '@tldraw/utils'\nimport { ArraySet } from './ArraySet'\nimport { maybeCaptureParent, startCapturingParents, stopCapturingParents } from './capture'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from './helpers'\nimport { HistoryBuffer } from './HistoryBuffer'\nimport { getGlobalEpoch, getIsInTransaction } from './transactions'\nimport { Child, ComputeDiff, RESET_VALUE, Signal } from './types'\nimport { logComputedGetterWarning } from './warnings'\n\n/**\n * A special symbol used to indicate that a computed signal has not been initialized yet.\n * This is passed as the `previousValue` parameter to a computed signal function on its first run.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * if (isUninitialized(prevValue)) {\n * console.log('First computation!')\n * }\n * return count.get() * 2\n * })\n * ```\n *\n * @public\n */\nexport const UNINITIALIZED = Symbol.for('com.tldraw.state/UNINITIALIZED')\n/**\n * The type of the first value passed to a computed signal function as the 'prevValue' parameter.\n * This type represents the uninitialized state of a computed signal before its first calculation.\n *\n * @see {@link isUninitialized}\n * @public\n */\nexport type UNINITIALIZED = typeof UNINITIALIZED\n\n/**\n * Call this inside a computed signal function to determine whether it is the first time the function is being called.\n *\n * Mainly useful for incremental signal computation.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * if (isUninitialized(prevValue)) {\n * print('First time!')\n * }\n * return count.get() * 2\n * })\n * ```\n *\n * @param value - The value to check.\n * @public\n */\nexport function isUninitialized(value: any): value is UNINITIALIZED {\n\treturn value === UNINITIALIZED\n}\n\n/**\n * A singleton class used to wrap computed signal values along with their diffs.\n * This class is used internally by the {@link withDiff} function to provide both\n * the computed value and its diff to the signal system.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * const nextValue = count.get() * 2\n * if (isUninitialized(prevValue)) {\n * return nextValue\n * }\n * return withDiff(nextValue, nextValue - prevValue)\n * })\n * ```\n *\n * @public\n */\nexport const WithDiff = singleton(\n\t'WithDiff',\n\t() =>\n\t\tclass WithDiff<Value, Diff> {\n\t\t\tconstructor(\n\t\t\t\tpublic value: Value,\n\t\t\t\tpublic diff: Diff\n\t\t\t) {}\n\t\t}\n)\n\n/**\n * Interface representing a value wrapped with its corresponding diff.\n * Used in incremental computation to provide both the new value and the diff from the previous value.\n *\n * @public\n */\nexport interface WithDiff<Value, Diff> {\n\t/**\n\t * The computed value.\n\t */\n\tvalue: Value\n\t/**\n\t * The diff between the previous and current value.\n\t */\n\tdiff: Diff\n}\n\n/**\n * When writing incrementally-computed signals it is convenient (and usually more performant) to incrementally compute the diff too.\n *\n * You can use this function to wrap the return value of a computed signal function to indicate that the diff should be used instead of calculating a new one with {@link AtomOptions.computeDiff}.\n *\n * @example\n * ```ts\n * const count = atom('count', 0)\n * const double = computed('double', (prevValue) => {\n * const nextValue = count.get() * 2\n * if (isUninitialized(prevValue)) {\n * return nextValue\n * }\n * return withDiff(nextValue, nextValue - prevValue)\n * }, { historyLength: 10 })\n * ```\n *\n *\n * @param value - The value.\n * @param diff - The diff.\n * @public\n */\nexport function withDiff<Value, Diff>(value: Value, diff: Diff): WithDiff<Value, Diff> {\n\treturn new WithDiff(value, diff)\n}\n\n/**\n * Options for configuring computed signals. Used when calling `computed` or using the `@computed` decorator.\n *\n * @example\n * ```ts\n * const greeting = computed('greeting', () => `Hello ${name.get()}!`, {\n * historyLength: 10,\n * isEqual: (a, b) => a === b,\n * computeDiff: (oldVal, newVal) => ({ type: 'change', from: oldVal, to: newVal })\n * })\n * ```\n *\n * @public\n */\nexport interface ComputedOptions<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 supplied via {@link withDiff} or {@link ComputedOptions.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 computed's old and new values. If provided, it will not be used unless you also specify {@link ComputedOptions.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 computed 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 * A computed signal created via the `computed` function or `@computed` decorator.\n * Computed signals derive their values from other signals and automatically update when their dependencies change.\n * They use lazy evaluation, only recalculating when accessed and dependencies have changed.\n *\n * @example\n * ```ts\n * const firstName = atom('firstName', 'John')\n * const lastName = atom('lastName', 'Doe')\n * const fullName = computed('fullName', () => `${firstName.get()} ${lastName.get()}`)\n *\n * console.log(fullName.get()) // \"John Doe\"\n * firstName.set('Jane')\n * console.log(fullName.get()) // \"Jane Doe\"\n * ```\n *\n * @public\n */\nexport interface Computed<Value, Diff = unknown> extends Signal<Value, Diff> {\n\t/**\n\t * Whether this computed signal is involved in an actively-running effect graph.\n\t * Returns true if there are any reactions or other computed signals depending on this one.\n\t * @public\n\t */\n\treadonly isActivelyListening: boolean\n\n\t/** @internal */\n\treadonly parentSet: ArraySet<Signal<any, any>>\n\t/** @internal */\n\treadonly parents: Signal<any, any>[]\n\t/** @internal */\n\treadonly parentEpochs: number[]\n}\n\n/**\n * @internal\n */\nclass __UNSAFE__Computed<Value, Diff = unknown> implements Computed<Value, Diff> {\n\treadonly __isComputed = true as const\n\tlastChangedEpoch = GLOBAL_START_EPOCH\n\tlastTraversedEpoch = GLOBAL_START_EPOCH\n\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null = null\n\n\t/**\n\t * The epoch when the reactor was last checked. Advances on every check, including ones that\n\t * find no change, unlike `lastChangedEpoch`.\n\t */\n\tprivate lastCheckedEpoch = GLOBAL_START_EPOCH\n\n\tparentSet = new ArraySet<Signal<any, any>>()\n\tparents: Signal<any, any>[] = []\n\tparentEpochs: number[] = []\n\n\tchildren = new ArraySet<Child>()\n\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isActivelyListening(): boolean {\n\t\treturn !this.children.isEmpty\n\t}\n\n\thistoryBuffer?: HistoryBuffer<Diff>\n\n\t// The last-computed value of this signal.\n\tprivate state: Value = UNINITIALIZED as unknown as Value\n\t// If the signal throws an error we stash it so we can rethrow it on the next get()\n\tprivate error: null | { thrownValue: any } = null\n\n\tprivate computeDiff?: ComputeDiff<Value, Diff>\n\n\tprivate readonly isEqual: (a: any, b: any) => boolean\n\n\tconstructor(\n\t\t/**\n\t\t * The name of the signal. This is used for debugging and performance profiling purposes. It does not need to be globally unique.\n\t\t */\n\t\tpublic readonly name: string,\n\t\t/**\n\t\t * The function that computes the value of the signal.\n\t\t */\n\t\tprivate readonly derive: (\n\t\t\tpreviousValue: Value | UNINITIALIZED,\n\t\t\tlastComputedEpoch: number\n\t\t) => Value | WithDiff<Value, Diff>,\n\t\toptions?: ComputedOptions<Value, Diff>\n\t) {\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\tthis.isEqual = options?.isEqual ?? equals\n\t}\n\n\t__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value {\n\t\tconst isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH\n\n\t\tconst globalEpoch = getGlobalEpoch()\n\n\t\tif (\n\t\t\t!isNew &&\n\t\t\t(this.lastCheckedEpoch === globalEpoch ||\n\t\t\t\t// Every ancestor change to an actively-listening computed traverses it (`flushChanges`),\n\t\t\t\t// so if it hasn't been traversed since it was last checked the O(parents) scan can be\n\t\t\t\t// skipped. Not valid while a transaction is open (changes traverse at commit), and only\n\t\t\t\t// if it was up to date when attached (see `attach` in helpers.ts).\n\t\t\t\t(this.isActivelyListening &&\n\t\t\t\t\t!getIsInTransaction() &&\n\t\t\t\t\tthis.lastTraversedEpoch <= this.lastCheckedEpoch) ||\n\t\t\t\t!haveParentsChanged(this))\n\t\t) {\n\t\t\tthis.lastCheckedEpoch = globalEpoch\n\t\t\tif (this.error) {\n\t\t\t\tif (!ignoreErrors) {\n\t\t\t\t\tthrow this.error.thrownValue\n\t\t\t\t} else {\n\t\t\t\t\treturn this.state // will be UNINITIALIZED\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn this.state\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tstartCapturingParents(this)\n\t\t\tconst result = this.derive(this.state, this.lastCheckedEpoch)\n\t\t\tconst newState = result instanceof WithDiff ? result.value : result\n\t\t\tconst isUninitialized = this.state === UNINITIALIZED\n\t\t\t// `derive` may have advanced the epoch, so re-read it here, but only once \u2014 the whole\n\t\t\t// commit below belongs to a single epoch.\n\t\t\tconst epoch = getGlobalEpoch()\n\t\t\tif (isUninitialized || !this.isEqual(this.state, newState)) {\n\t\t\t\tif (this.historyBuffer && !isUninitialized) {\n\t\t\t\t\t// Only `undefined` means \"no diff supplied\"; `null` can be a legitimate diff.\n\t\t\t\t\tconst diff = result instanceof WithDiff ? result.diff : undefined\n\t\t\t\t\tthis.historyBuffer.pushEntry(\n\t\t\t\t\t\tthis.lastChangedEpoch,\n\t\t\t\t\t\tepoch,\n\t\t\t\t\t\tdiff !== undefined\n\t\t\t\t\t\t\t? diff\n\t\t\t\t\t\t\t: this.computeDiff\n\t\t\t\t\t\t\t\t? this.computeDiff(this.state, newState, this.lastCheckedEpoch, epoch)\n\t\t\t\t\t\t\t\t: RESET_VALUE\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthis.lastChangedEpoch = epoch\n\t\t\t\tthis.state = newState\n\t\t\t}\n\t\t\tthis.error = null\n\t\t\tthis.lastCheckedEpoch = epoch\n\n\t\t\treturn this.state\n\t\t} catch (e) {\n\t\t\t// if a derived value throws an error, we reset the state to UNINITIALIZED\n\t\t\tconst epoch = getGlobalEpoch()\n\t\t\t// Entering the error state (from a value, or from never having computed) is a change;\n\t\t\t// throwing again while already in it is not. Checking `error` rather than `state` matters\n\t\t\t// for a first run that throws: `state` is already UNINITIALIZED then, and leaving\n\t\t\t// `lastChangedEpoch` at GLOBAL_START_EPOCH would keep `isNew` true and re-run `derive` on\n\t\t\t// every read instead of rethrowing the cached error.\n\t\t\tif (this.error === null) {\n\t\t\t\tthis.state = UNINITIALIZED as unknown as Value\n\t\t\t\tthis.lastChangedEpoch = epoch\n\t\t\t}\n\t\t\tthis.lastCheckedEpoch = epoch\n\t\t\t// we also clear the history buffer if an error was thrown\n\t\t\tif (this.historyBuffer) {\n\t\t\t\tthis.historyBuffer.clear()\n\t\t\t}\n\t\t\tthis.error = { thrownValue: e }\n\t\t\t// we don't wish to propagate errors when derefed via haveParentsChanged()\n\t\t\tif (!ignoreErrors) throw e\n\t\t\treturn this.state\n\t\t} finally {\n\t\t\tstopCapturingParents()\n\t\t}\n\t}\n\n\tget(): Value {\n\t\ttry {\n\t\t\treturn this.__unsafe__getWithoutCapture()\n\t\t} finally {\n\t\t\t// if the deriver throws an error we still need to capture\n\t\t\tmaybeCaptureParent(this)\n\t\t}\n\t}\n\n\tgetDiffSince(epoch: number): RESET_VALUE | Diff[] {\n\t\t// we can ignore any errors thrown during derive\n\t\tthis.__unsafe__getWithoutCapture(true)\n\t\t// and we still need to capture this signal as a parent\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 computed signal implementation class.\n * Used internally by the library to create computed signal instances.\n *\n * @internal\n */\nexport const _Computed = singleton('Computed', () => __UNSAFE__Computed)\n\n/**\n * Type alias for the computed signal implementation class.\n *\n * @internal\n */\nexport type _Computed = InstanceType<typeof __UNSAFE__Computed>\n\n// Set on every decorated wrapper: the symbol its per-instance computed is stored under.\nconst derivationKeyKey = '@@__computedDerivationKey__@@'\n\n/**\n * Builds the method (or getter) body that `@computed` installs: a per-instance, lazily-created\n * computed over the original method. The storage symbol is unique per decoration rather than\n * `Symbol.for(name)` so that a subclass which overrides a `@computed` method and calls\n * `super.method()` reaches the superclass's computed instead of re-entering its own.\n */\nfunction makeComputedWrapper(name: string, compute: () => any, options: ComputedOptions<any, any>) {\n\tconst derivationKey = Symbol('__@tldraw/state__computed__' + name)\n\n\tconst wrapper = function (this: any) {\n\t\tlet d = this[derivationKey] as Computed<any> | undefined\n\n\t\tif (!d) {\n\t\t\td = new _Computed(name, compute.bind(this) as any, options)\n\t\t\tObject.defineProperty(this, derivationKey, {\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: false,\n\t\t\t\twritable: false,\n\t\t\t\tvalue: d,\n\t\t\t})\n\t\t}\n\t\treturn d.get()\n\t} as any\n\twrapper[derivationKeyKey] = derivationKey\n\treturn wrapper\n}\n\nfunction computedDecorator(\n\toptions: ComputedOptions<any, any> = {},\n\targs:\n\t\t| [target: any, key: string, descriptor: PropertyDescriptor]\n\t\t| [originalMethod: () => any, context: ClassMethodDecoratorContext]\n) {\n\tif (args.length === 2) {\n\t\tconst [originalMethod, context] = args\n\t\tassert(context.kind === 'method', '@computed can only be used on methods')\n\t\treturn makeComputedWrapper(String(context.name), originalMethod, options)\n\t} else {\n\t\tconst [_target, key, descriptor] = args\n\t\tif (descriptor.get) {\n\t\t\tlogComputedGetterWarning()\n\t\t\tdescriptor.get = makeComputedWrapper(key, descriptor.get, options)\n\t\t} else {\n\t\t\tdescriptor.value = makeComputedWrapper(key, descriptor.value, options)\n\t\t}\n\t\treturn descriptor\n\t}\n}\n\n/**\n * Retrieves the underlying computed instance for a given property created with the `computed`\n * decorator.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom('count', 0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n *\n * const c = new Counter()\n * const remaining = getComputedInstance(c, 'getRemaining')\n * remaining.get() === 100 // true\n * c.count.set(13)\n * remaining.get() === 87 // true\n * ```\n *\n * @param obj - The object\n * @param propertyName - The property name\n * @public\n */\nexport function getComputedInstance<Obj extends object, Prop extends keyof Obj>(\n\tobj: Obj,\n\tpropertyName: Prop\n): Computed<Obj[Prop] extends () => infer Value ? Value : Obj[Prop]> {\n\t// The nearest decorated wrapper (a method, or a legacy getter) on the prototype chain knows\n\t// which symbol the instance's computed is stored under.\n\tfor (let proto: any = obj; proto; proto = Object.getPrototypeOf(proto)) {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(proto, propertyName)\n\t\tconst wrapper = descriptor?.get ?? descriptor?.value\n\t\tconst key = wrapper?.[derivationKeyKey]\n\t\tif (!key) continue\n\n\t\tlet inst = (obj as any)[key]\n\t\tif (!inst) {\n\t\t\t// calling the wrapper creates the computed (before it first derives, so a throwing derive\n\t\t\t// still leaves the instance behind)\n\t\t\ttry {\n\t\t\t\twrapper.call(obj)\n\t\t\t} catch {\n\t\t\t\t// the caller asked for the instance, not its value\n\t\t\t}\n\t\t\tinst = (obj as any)[key]\n\t\t}\n\t\treturn inst\n\t}\n\treturn undefined as any\n}\n\n/**\n * Creates a computed signal that derives its value from other signals.\n * Computed signals automatically update when their dependencies change and use lazy evaluation\n * for optimal performance.\n *\n * @example\n * ```ts\n * const name = atom('name', 'John')\n * const greeting = computed('greeting', () => `Hello ${name.get()}!`)\n * console.log(greeting.get()) // 'Hello John!'\n * ```\n *\n * `computed` may also be used as a decorator for creating computed getter methods.\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom('count', 0)\n *\n * @computed getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * You may optionally pass in a {@link ComputedOptions} when used as a decorator:\n *\n * @example\n * ```ts\n * class Counter {\n * max = 100\n * count = atom('count', 0)\n *\n * @computed({isEqual: (a, b) => a === b})\n * getRemaining() {\n * return this.max - this.count.get()\n * }\n * }\n * ```\n *\n * @param name - The name of the signal for debugging purposes\n * @param compute - The function that computes the value of the signal. Receives the previous value and last computed epoch\n * @param options - Optional configuration for the computed signal\n * @returns A new computed signal\n * @public\n */\nexport function computed<Value, Diff = unknown>(\n\tname: string,\n\tcompute: (\n\t\tpreviousValue: Value | typeof UNINITIALIZED,\n\t\tlastComputedEpoch: number\n\t) => Value | WithDiff<Value, Diff>,\n\toptions?: ComputedOptions<Value, Diff>\n): Computed<Value, Diff>\n/**\n * TC39 decorator for creating computed methods in classes.\n *\n * @example\n * ```ts\n * class MyClass {\n * value = atom('value', 10)\n *\n * @computed\n * doubled() {\n * return this.value.get() * 2\n * }\n * }\n * ```\n *\n * @param compute - The method to be decorated\n * @param context - The decorator context provided by TypeScript\n * @returns The decorated method\n * @public\n */\nexport function computed<This extends object, Value>(\n\tcompute: () => Value,\n\tcontext: ClassMethodDecoratorContext<This, () => Value>\n): () => Value\n/**\n * Legacy TypeScript decorator for creating computed methods in classes.\n *\n * @example\n * ```ts\n * class MyClass {\n * value = atom('value', 10)\n *\n * @computed\n * doubled() {\n * return this.value.get() * 2\n * }\n * }\n * ```\n *\n * @param target - The class prototype\n * @param key - The property key\n * @param descriptor - The property descriptor\n * @returns The modified property descriptor\n * @public\n */\nexport function computed(\n\ttarget: any,\n\tkey: string,\n\tdescriptor: PropertyDescriptor\n): PropertyDescriptor\n/**\n * Decorator factory for creating computed methods with options.\n *\n * @example\n * ```ts\n * class MyClass {\n * items = atom('items', [1, 2, 3])\n *\n * @computed({ historyLength: 10 })\n * sum() {\n * return this.items.get().reduce((a, b) => a + b, 0)\n * }\n * }\n * ```\n *\n * @param options - Configuration options for the computed signal\n * @returns A decorator function that can be applied to methods\n * @public\n */\nexport function computed<Value, Diff = unknown>(\n\toptions?: ComputedOptions<Value, Diff>\n): ((target: any, key: string, descriptor: PropertyDescriptor) => PropertyDescriptor) &\n\t(<This>(\n\t\tcompute: () => Value,\n\t\tcontext: ClassMethodDecoratorContext<This, () => Value>\n\t) => () => Value)\n\n/**\n * Implementation function that handles all computed signal creation and decoration scenarios.\n * This function is overloaded to support multiple usage patterns:\n * - Creating computed signals directly\n * - Using as a TC39 decorator\n * - Using as a legacy decorator\n * - Using as a decorator factory with options\n *\n * @returns Either a computed signal instance or a decorator function depending on usage\n * @public\n */\nexport function computed() {\n\tif (arguments.length <= 1) {\n\t\t// decorator factory: `@computed(options)` or `@computed()`\n\t\tconst options = arguments[0]\n\t\treturn (...args: any) => computedDecorator(options, args)\n\t} else if (typeof arguments[0] === 'string') {\n\t\treturn new _Computed(arguments[0], arguments[1], arguments[2])\n\t} else {\n\t\treturn computedDecorator(undefined, arguments as any)\n\t}\n}\n\nimport { isComputed as _isComputed } from './isComputed'\n\n/**\n * Returns true if the given value is a computed signal.\n * @public\n */\nexport function isComputed(value: any): value is Computed<any> {\n\treturn _isComputed(value)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["isUninitialized", "_isComputed"]
|
|
7
7
|
}
|
|
@@ -75,7 +75,7 @@ class __EffectScheduler__ {
|
|
|
75
75
|
maybeScheduleEffect() {
|
|
76
76
|
if (!this._isActivelyListening) return;
|
|
77
77
|
if (this.lastReactedEpoch === (0, import_transactions.getGlobalEpoch)()) return;
|
|
78
|
-
if (this.parents.length && !(0, import_helpers.haveParentsChanged)(this)) {
|
|
78
|
+
if ((this.lastReactedEpoch !== import_constants.GLOBAL_START_EPOCH || this.parents.length > 0) && !(0, import_helpers.haveParentsChanged)(this)) {
|
|
79
79
|
this.lastReactedEpoch = (0, import_transactions.getGlobalEpoch)();
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
@@ -105,7 +105,9 @@ class __EffectScheduler__ {
|
|
|
105
105
|
attach() {
|
|
106
106
|
this._isActivelyListening = true;
|
|
107
107
|
for (let i = 0, n = this.parents.length; i < n; i++) {
|
|
108
|
-
|
|
108
|
+
const parent = this.parents[i];
|
|
109
|
+
parent.__unsafe__getWithoutCapture(true);
|
|
110
|
+
(0, import_helpers.attach)(parent, this);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
/**
|
|
@@ -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// bail out if we have parents and they have not changed since last time\n\t\tif (this.parents.length && !haveParentsChanged(this)) {\n\t\t\tthis.lastReactedEpoch = getGlobalEpoch()\n\t\t\treturn\n\t\t}\n\t\t// if we don't have parents it's probably the first time this is running.\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\tattach(this.parents[i], 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,EAwCpE,YACiB,MACC,WACjB,SACC;AAHe;AACC;AAGjB,SAAK,kBAAkB,SAAS;AAAA,EACjC;AAAA,EALiB;AAAA,EACC;AAAA,EAzCT,sBAAsB;AAAA;AAAA,EAEvB,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM/B,IAAI,sBAAsB;AACzB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAEA,qBAAqB;AAAA;AAAA,EAGb,mBAAmB;AAAA;AAAA,EAGnB,iBAAiB;AAAA;AAAA,EAEzB,4BAAkE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlE,IAAI,gBAAgB;AACnB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGS,YAAY,IAAI,yBAA2B;AAAA;AAAA,EAE3C,eAAyB,CAAC;AAAA;AAAA,EAE1B,UAA8B,CAAC;AAAA;AAAA,EAEvB;AAAA;AAAA,EAUjB,sBAAsB;AAErB,QAAI,CAAC,KAAK,qBAAsB;AAEhC,QAAI,KAAK,yBAAqB,oCAAe,EAAG;
|
|
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": "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",
|
|
6
6
|
"names": ["options"]
|
|
7
7
|
}
|
|
@@ -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 * @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\tconst shallowEquals =\n\t\ta === b || Object.is(a, b) || Boolean(a && b && typeof a.equals === 'function' && a.equals(b))\n\treturn shallowEquals\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;
|
|
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\tconst shallowEquals =\n\t\ta === b || Object.is(a, b) || Boolean(a && b && typeof a.equals === 'function' && a.equals(b))\n\treturn shallowEquals\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,QAAM,gBACL,MAAM,KAAK,OAAO,GAAG,GAAG,CAAC,KAAK,QAAQ,KAAK,KAAK,OAAO,EAAE,WAAW,cAAc,EAAE,OAAO,CAAC,CAAC;AAC9F,SAAO;AACR;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
|
}
|
|
@@ -50,10 +50,15 @@ function localStorageAtom(name, initialValue, options) {
|
|
|
50
50
|
} catch {
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
-
window
|
|
53
|
+
const canListen = typeof window !== "undefined";
|
|
54
|
+
if (canListen) {
|
|
55
|
+
window.addEventListener("storage", handleStorageEvent);
|
|
56
|
+
}
|
|
54
57
|
const cleanup = () => {
|
|
55
58
|
reactCleanup();
|
|
56
|
-
|
|
59
|
+
if (canListen) {
|
|
60
|
+
window.removeEventListener("storage", handleStorageEvent);
|
|
61
|
+
}
|
|
57
62
|
};
|
|
58
63
|
return [outAtom, cleanup];
|
|
59
64
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/localStorageAtom.ts"],
|
|
4
|
-
"sourcesContent": ["import { deleteFromLocalStorage, getFromLocalStorage, setInLocalStorage } from '@tldraw/utils'\nimport { Atom, atom, AtomOptions } from './Atom'\nimport { react } from './EffectScheduler'\n\n/**\n * Creates a new {@link Atom} that persists its value to localStorage.\n *\n * The atom is automatically synced with localStorage - changes to the atom are saved to localStorage,\n * and the initial value is read from localStorage if it exists. Returns both the atom and a cleanup\n * function that should be called to stop syncing when the atom is no longer needed. If you need to delete\n * the atom, you should do it manually after all cleanup functions have been called.\n *\n * @example\n * ```ts\n * const [theme, cleanup] = localStorageAtom('theme', 'light')\n *\n * theme.get() // 'light' or value from localStorage if it exists\n *\n * theme.set('dark') // updates atom and saves to localStorage\n *\n * // When done:\n * cleanup() // stops syncing to localStorage\n * ```\n *\n * @param name - The localStorage key and atom name. This is used for both localStorage persistence\n * and debugging/profiling purposes.\n * @param initialValue - The initial value of the atom, used if no value exists in localStorage.\n * @param options - Optional atom configuration. See {@link AtomOptions}.\n * @returns A tuple containing the atom and a cleanup function to stop localStorage syncing.\n * @public\n */\nexport function localStorageAtom<Value, Diff = unknown>(\n\tname: string,\n\tinitialValue: Value,\n\toptions?: AtomOptions<Value, Diff>\n): [Atom<Value, Diff>, () => void] {\n\t// Try to restore the initial value from localStorage\n\tlet _initialValue = initialValue\n\n\ttry {\n\t\tconst value = getFromLocalStorage(name)\n\t\tif (value) {\n\t\t\t_initialValue = JSON.parse(value) as Value\n\t\t}\n\t} catch {\n\t\t// If parsing fails, the stored value is corrupted - delete it and use the provided initial value\n\t\tdeleteFromLocalStorage(name)\n\t}\n\n\t// Create the atom with the restored or initial value\n\tconst outAtom = atom(name, _initialValue, options)\n\n\t// Set up automatic syncing: whenever the atom changes, save it to localStorage\n\tconst reactCleanup = react(`save ${name} to localStorage`, () => {\n\t\tsetInLocalStorage(name, JSON.stringify(outAtom.get()))\n\t})\n\n\t// Set up cross-tab sync: listen for storage events from other tabs\n\tconst handleStorageEvent = (event: StorageEvent) => {\n\t\t// Only handle events for this specific key\n\t\tif (event.key !== name) return\n\n\t\t// If the value was deleted in another tab\n\t\tif (event.newValue === null) {\n\t\t\toutAtom.set(initialValue)\n\t\t\treturn\n\t\t}\n\n\t\t// If the value was changed in another tab, update the atom\n\t\ttry {\n\t\t\tconst newValue = JSON.parse(event.newValue) as Value\n\t\t\toutAtom.set(newValue)\n\t\t} catch {\n\t\t\t// If parsing fails, the stored value is corrupted; preserve the existing value\n\t\t}\n\t}\n\n\twindow.addEventListener('storage', handleStorageEvent)\n\n\t// Combined cleanup function\n\tconst cleanup = () => {\n\t\treactCleanup()\n\t\twindow.removeEventListener('storage', handleStorageEvent)\n\t}\n\n\treturn [outAtom, cleanup]\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA+E;AAC/E,kBAAwC;AACxC,6BAAsB;AA6Bf,SAAS,iBACf,MACA,cACA,SACkC;AAElC,MAAI,gBAAgB;AAEpB,MAAI;AACH,UAAM,YAAQ,kCAAoB,IAAI;AACtC,QAAI,OAAO;AACV,sBAAgB,KAAK,MAAM,KAAK;AAAA,IACjC;AAAA,EACD,QAAQ;AAEP,6CAAuB,IAAI;AAAA,EAC5B;AAGA,QAAM,cAAU,kBAAK,MAAM,eAAe,OAAO;AAGjD,QAAM,mBAAe,8BAAM,QAAQ,IAAI,oBAAoB,MAAM;AAChE,wCAAkB,MAAM,KAAK,UAAU,QAAQ,IAAI,CAAC,CAAC;AAAA,EACtD,CAAC;AAGD,QAAM,qBAAqB,CAAC,UAAwB;AAEnD,QAAI,MAAM,QAAQ,KAAM;AAGxB,QAAI,MAAM,aAAa,MAAM;AAC5B,cAAQ,IAAI,YAAY;AACxB;AAAA,IACD;AAGA,QAAI;AACH,YAAM,WAAW,KAAK,MAAM,MAAM,QAAQ;AAC1C,cAAQ,IAAI,QAAQ;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACD;
|
|
4
|
+
"sourcesContent": ["import { deleteFromLocalStorage, getFromLocalStorage, setInLocalStorage } from '@tldraw/utils'\nimport { Atom, atom, AtomOptions } from './Atom'\nimport { react } from './EffectScheduler'\n\n/**\n * Creates a new {@link Atom} that persists its value to localStorage.\n *\n * The atom is automatically synced with localStorage - changes to the atom are saved to localStorage,\n * and the initial value is read from localStorage if it exists. Returns both the atom and a cleanup\n * function that should be called to stop syncing when the atom is no longer needed. If you need to delete\n * the atom, you should do it manually after all cleanup functions have been called.\n *\n * @example\n * ```ts\n * const [theme, cleanup] = localStorageAtom('theme', 'light')\n *\n * theme.get() // 'light' or value from localStorage if it exists\n *\n * theme.set('dark') // updates atom and saves to localStorage\n *\n * // When done:\n * cleanup() // stops syncing to localStorage\n * ```\n *\n * @param name - The localStorage key and atom name. This is used for both localStorage persistence\n * and debugging/profiling purposes.\n * @param initialValue - The initial value of the atom, used if no value exists in localStorage.\n * @param options - Optional atom configuration. See {@link AtomOptions}.\n * @returns A tuple containing the atom and a cleanup function to stop localStorage syncing.\n * @public\n */\nexport function localStorageAtom<Value, Diff = unknown>(\n\tname: string,\n\tinitialValue: Value,\n\toptions?: AtomOptions<Value, Diff>\n): [Atom<Value, Diff>, () => void] {\n\t// Try to restore the initial value from localStorage\n\tlet _initialValue = initialValue\n\n\ttry {\n\t\tconst value = getFromLocalStorage(name)\n\t\tif (value) {\n\t\t\t_initialValue = JSON.parse(value) as Value\n\t\t}\n\t} catch {\n\t\t// If parsing fails, the stored value is corrupted - delete it and use the provided initial value\n\t\tdeleteFromLocalStorage(name)\n\t}\n\n\t// Create the atom with the restored or initial value\n\tconst outAtom = atom(name, _initialValue, options)\n\n\t// Set up automatic syncing: whenever the atom changes, save it to localStorage\n\tconst reactCleanup = react(`save ${name} to localStorage`, () => {\n\t\tsetInLocalStorage(name, JSON.stringify(outAtom.get()))\n\t})\n\n\t// Set up cross-tab sync: listen for storage events from other tabs\n\tconst handleStorageEvent = (event: StorageEvent) => {\n\t\t// Only handle events for this specific key\n\t\tif (event.key !== name) return\n\n\t\t// If the value was deleted in another tab\n\t\tif (event.newValue === null) {\n\t\t\toutAtom.set(initialValue)\n\t\t\treturn\n\t\t}\n\n\t\t// If the value was changed in another tab, update the atom\n\t\ttry {\n\t\t\tconst newValue = JSON.parse(event.newValue) as Value\n\t\t\toutAtom.set(newValue)\n\t\t} catch {\n\t\t\t// If parsing fails, the stored value is corrupted; preserve the existing value\n\t\t}\n\t}\n\n\t// The storage helpers above tolerate environments without localStorage (Node, SSR); do the\n\t// same here rather than throwing on `window`.\n\tconst canListen = typeof window !== 'undefined'\n\tif (canListen) {\n\t\twindow.addEventListener('storage', handleStorageEvent)\n\t}\n\n\t// Combined cleanup function\n\tconst cleanup = () => {\n\t\treactCleanup()\n\t\tif (canListen) {\n\t\t\twindow.removeEventListener('storage', handleStorageEvent)\n\t\t}\n\t}\n\n\treturn [outAtom, cleanup]\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA+E;AAC/E,kBAAwC;AACxC,6BAAsB;AA6Bf,SAAS,iBACf,MACA,cACA,SACkC;AAElC,MAAI,gBAAgB;AAEpB,MAAI;AACH,UAAM,YAAQ,kCAAoB,IAAI;AACtC,QAAI,OAAO;AACV,sBAAgB,KAAK,MAAM,KAAK;AAAA,IACjC;AAAA,EACD,QAAQ;AAEP,6CAAuB,IAAI;AAAA,EAC5B;AAGA,QAAM,cAAU,kBAAK,MAAM,eAAe,OAAO;AAGjD,QAAM,mBAAe,8BAAM,QAAQ,IAAI,oBAAoB,MAAM;AAChE,wCAAkB,MAAM,KAAK,UAAU,QAAQ,IAAI,CAAC,CAAC;AAAA,EACtD,CAAC;AAGD,QAAM,qBAAqB,CAAC,UAAwB;AAEnD,QAAI,MAAM,QAAQ,KAAM;AAGxB,QAAI,MAAM,aAAa,MAAM;AAC5B,cAAQ,IAAI,YAAY;AACxB;AAAA,IACD;AAGA,QAAI;AACH,YAAM,WAAW,KAAK,MAAM,MAAM,QAAQ;AAC1C,cAAQ,IAAI,QAAQ;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACD;AAIA,QAAM,YAAY,OAAO,WAAW;AACpC,MAAI,WAAW;AACd,WAAO,iBAAiB,WAAW,kBAAkB;AAAA,EACtD;AAGA,QAAM,UAAU,MAAM;AACrB,iBAAa;AACb,QAAI,WAAW;AACd,aAAO,oBAAoB,WAAW,kBAAkB;AAAA,IACzD;AAAA,EACD;AAEA,SAAO,CAAC,SAAS,OAAO;AACzB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -22,8 +22,7 @@ __export(transactions_exports, {
|
|
|
22
22
|
atomDidChange: () => atomDidChange,
|
|
23
23
|
deferAsyncEffects: () => deferAsyncEffects,
|
|
24
24
|
getGlobalEpoch: () => getGlobalEpoch,
|
|
25
|
-
|
|
26
|
-
getReactionEpoch: () => getReactionEpoch,
|
|
25
|
+
getIsInTransaction: () => getIsInTransaction,
|
|
27
26
|
transact: () => transact,
|
|
28
27
|
transaction: () => transaction
|
|
29
28
|
});
|
|
@@ -44,27 +43,25 @@ class Transaction {
|
|
|
44
43
|
return this.parent === null;
|
|
45
44
|
}
|
|
46
45
|
commit() {
|
|
47
|
-
if (
|
|
48
|
-
for (const atom of this.initialAtomValues.keys()) {
|
|
49
|
-
traverseAtomForCleanup(atom);
|
|
50
|
-
}
|
|
51
|
-
} else if (this.isRoot) {
|
|
46
|
+
if (this.isRoot) {
|
|
52
47
|
flushChanges(this.initialAtomValues.keys());
|
|
53
|
-
|
|
54
|
-
const parentValues = this.parent.initialAtomValues;
|
|
55
|
-
if (parentValues.size === 0) {
|
|
56
|
-
this.parent.initialAtomValues = this.initialAtomValues;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
this.initialAtomValues.forEach((value, atom) => {
|
|
60
|
-
if (!parentValues.has(atom)) {
|
|
61
|
-
parentValues.set(atom, value);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
48
|
+
return;
|
|
64
49
|
}
|
|
50
|
+
const parentValues = this.parent.initialAtomValues;
|
|
51
|
+
if (parentValues.size === 0) {
|
|
52
|
+
this.parent.initialAtomValues = this.initialAtomValues;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.initialAtomValues.forEach((value, atom) => {
|
|
56
|
+
if (!parentValues.has(atom)) {
|
|
57
|
+
parentValues.set(atom, value);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
65
60
|
}
|
|
66
61
|
/**
|
|
67
|
-
* Abort the transaction.
|
|
62
|
+
* Abort the transaction. Restores every atom changed in this transaction to its initial
|
|
63
|
+
* value, then commits. Must be called while this is still `inst.currentTransaction`, so that
|
|
64
|
+
* the restoring sets are recorded against it instead of each flushing effects on their own.
|
|
68
65
|
*
|
|
69
66
|
* @public
|
|
70
67
|
*/
|
|
@@ -83,17 +80,13 @@ const inst = (0, import_helpers.singleton)("transactions", () => ({
|
|
|
83
80
|
// Whether any transaction is reacting.
|
|
84
81
|
globalIsReacting: false,
|
|
85
82
|
currentTransaction: null,
|
|
86
|
-
cleanupReactors: null
|
|
87
|
-
reactionEpoch: import_constants.GLOBAL_START_EPOCH + 1
|
|
83
|
+
cleanupReactors: null
|
|
88
84
|
}));
|
|
89
|
-
function getReactionEpoch() {
|
|
90
|
-
return inst.reactionEpoch;
|
|
91
|
-
}
|
|
92
85
|
function getGlobalEpoch() {
|
|
93
86
|
return inst.globalEpoch;
|
|
94
87
|
}
|
|
95
|
-
function
|
|
96
|
-
return inst.
|
|
88
|
+
function getIsInTransaction() {
|
|
89
|
+
return inst.currentTransaction !== null;
|
|
97
90
|
}
|
|
98
91
|
let traverseReactors;
|
|
99
92
|
function traverseChild(child) {
|
|
@@ -110,13 +103,15 @@ function traverseChild(child) {
|
|
|
110
103
|
}
|
|
111
104
|
function flushChanges(atoms) {
|
|
112
105
|
if (inst.globalIsReacting) {
|
|
113
|
-
|
|
106
|
+
for (const atom of atoms) {
|
|
107
|
+
traverseAtomForCleanup(atom);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
114
110
|
}
|
|
115
111
|
const outerTxn = inst.currentTransaction;
|
|
116
112
|
try {
|
|
117
113
|
inst.currentTransaction = null;
|
|
118
114
|
inst.globalIsReacting = true;
|
|
119
|
-
inst.reactionEpoch = inst.globalEpoch;
|
|
120
115
|
const reactors = /* @__PURE__ */ new Set();
|
|
121
116
|
traverseReactors = reactors;
|
|
122
117
|
for (const atom of atoms) {
|
|
@@ -148,8 +143,6 @@ function atomDidChange(atom, previousValue) {
|
|
|
148
143
|
if (!inst.currentTransaction.initialAtomValues.has(atom)) {
|
|
149
144
|
inst.currentTransaction.initialAtomValues.set(atom, previousValue);
|
|
150
145
|
}
|
|
151
|
-
} else if (inst.globalIsReacting) {
|
|
152
|
-
traverseAtomForCleanup(atom);
|
|
153
146
|
} else {
|
|
154
147
|
flushChanges([atom]);
|
|
155
148
|
}
|
|
@@ -217,13 +210,16 @@ async function deferAsyncEffects(fn) {
|
|
|
217
210
|
return result;
|
|
218
211
|
}
|
|
219
212
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
213
|
+
try {
|
|
214
|
+
if (typeof error !== "undefined") {
|
|
215
|
+
txn.abort();
|
|
216
|
+
throw error;
|
|
217
|
+
} else {
|
|
218
|
+
txn.commit();
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
inst.currentTransaction = null;
|
|
227
223
|
}
|
|
228
224
|
}
|
|
229
225
|
//# sourceMappingURL=transactions.js.map
|
|
@@ -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 (inst.globalIsReacting) {\n\t\t\t// if we're committing during a reaction we actually need to\n\t\t\t// use the 'cleanup' reactors set to ensure we re-run effects if necessary\n\t\t\tfor (const atom of this.initialAtomValues.keys()) {\n\t\t\t\ttraverseAtomForCleanup(atom)\n\t\t\t}\n\t\t} else if (this.isRoot) {\n\t\t\t// For root transactions, flush changed atoms\n\t\t\tflushChanges(this.initialAtomValues.keys())\n\t\t} else {\n\t\t\t// For transactions with parents, add the transaction's initial values to the parent's.\n\t\t\t// A parent that has recorded nothing yet adopts the map outright: this transaction is\n\t\t\t// finished with it, and the common nested case is a single inner transaction doing all\n\t\t\t// the writes.\n\t\t\tconst parentValues = this.parent!.initialAtomValues\n\t\t\tif (parentValues.size === 0) {\n\t\t\t\tthis.parent!.initialAtomValues = this.initialAtomValues\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.initialAtomValues.forEach((value, atom) => {\n\t\t\t\tif (!parentValues.has(atom)) {\n\t\t\t\t\tparentValues.set(atom, value)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n\t/**\n\t * Abort the transaction.\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\treactionEpoch: GLOBAL_START_EPOCH + 1,\n}))\n\n/**\n * Gets the current reaction epoch, which is used to track when reactions are running.\n * The reaction epoch is updated at the start of each reaction cycle.\n *\n * @returns The current reaction epoch number\n * @public\n */\nexport function getReactionEpoch() {\n\treturn inst.reactionEpoch\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 * Checks whether any reactions are currently executing.\n * When true, the system is in the middle of processing effects and side effects.\n *\n * @returns True if reactions are currently running, false otherwise\n * @public\n */\nexport function getIsReacting() {\n\treturn inst.globalIsReacting\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.\n *\n * @param atoms - The atoms to flush changes for.\n */\nfunction flushChanges(atoms: Iterable<_Atom>) {\n\tif (inst.globalIsReacting) {\n\t\tthrow new Error('flushChanges cannot be called during a reaction')\n\t}\n\n\tconst outerTxn = inst.currentTransaction\n\ttry {\n\t\t// clear the transaction stack\n\t\tinst.currentTransaction = null\n\t\tinst.globalIsReacting = true\n\t\tinst.reactionEpoch = inst.globalEpoch\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.\n\t\tif (!inst.currentTransaction.initialAtomValues.has(atom)) {\n\t\t\tinst.currentTransaction.initialAtomValues.set(atom, previousValue)\n\t\t}\n\t} else if (inst.globalIsReacting) {\n\t\t// If the atom changed during the reaction phase of flushChanges\n\t\t// (and there are no transactions started inside the reaction phase)\n\t\t// then we are past the point where a transaction can be aborted\n\t\t// so we don't need to note down the previousValue.\n\t\ttraverseAtomForCleanup(atom)\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\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\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\tinst.currentTransaction = null\n\n\tif (typeof error !== 'undefined') {\n\t\t// If the rollback was triggered, abort the transaction.\n\t\ttxn.abort()\n\t\tthrow error\n\t} else {\n\t\ttxn.commit()\n\t\treturn result\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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\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;AAAA,EAGxC,IAAI,SAAS;AACZ,WAAO,KAAK,WAAW;AAAA,EACxB;AAAA,EAEA,SAAS;AACR,QAAI,KAAK,QAAQ;AAEhB,mBAAa,KAAK,kBAAkB,KAAK,CAAC;AAC1C;AAAA,IACD;AAQA,UAAM,eAAe,KAAK,OAAQ;AAClC,QAAI,aAAa,SAAS,GAAG;AAC5B,WAAK,OAAQ,oBAAoB,KAAK;AACtC;AAAA,IACD;AACA,SAAK,kBAAkB,QAAQ,CAAC,OAAO,SAAS;AAC/C,UAAI,CAAC,aAAa,IAAI,IAAI,GAAG;AAC5B,qBAAa,IAAI,MAAM,KAAK;AAAA,MAC7B;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ;AACP,SAAK;AAEL,SAAK,kBAAkB,QAAQ,CAAC,OAAO,SAAS;AAC/C,WAAK,IAAI,KAAK;AACd,WAAK,eAAe,MAAM;AAAA,IAC3B,CAAC;AAED,SAAK,OAAO;AAAA,EACb;AACD;AAEA,MAAM,WAAO,0BAAU,gBAAgB,OAAO;AAAA;AAAA,EAE7C,aAAa,sCAAqB;AAAA;AAAA,EAElC,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EAEpB,iBAAiB;AAClB,EAAE;AASK,SAAS,iBAAiB;AAChC,SAAO,KAAK;AACb;AAQO,SAAS,qBAAqB;AACpC,SAAO,KAAK,uBAAuB;AACpC;AAKA,IAAI;AAEJ,SAAS,cAAc,OAAc;AACpC,MAAI,MAAM,uBAAuB,KAAK,aAAa;AAClD;AAAA,EACD;AAEA,QAAM,qBAAqB,KAAK;AAEhC,MAAI,yBAAyB,OAAO;AACnC,qBAAiB,IAAI,KAA2B;AAAA,EACjD,OAAO;AACN;AAAC,IAAC,MAA6B,SAAS,MAAM,aAAa;AAAA,EAC5D;AACD;AASA,SAAS,aAAa,OAAwB;AAC7C,MAAI,KAAK,kBAAkB;AAC1B,eAAW,QAAQ,OAAO;AACzB,6BAAuB,IAAI;AAAA,IAC5B;AACA;AAAA,EACD;AAEA,QAAM,WAAW,KAAK;AACtB,MAAI;AAGH,SAAK,qBAAqB;AAC1B,SAAK,mBAAmB;AAExB,UAAM,WAAW,oBAAI,IAAa;AAClC,uBAAmB;AACnB,eAAW,QAAQ,OAAO;AACzB,WAAK,SAAS,MAAM,aAAa;AAAA,IAClC;AAGA,eAAW,KAAK,UAAU;AACzB,QAAE,oBAAoB;AAAA,IACvB;AAEA,QAAI,cAAc;AAClB,WAAO,KAAK,iBAAiB,MAAM;AAClC,UAAI,gBAAgB,KAAM;AACzB,cAAM,IAAI,MAAM,sCAAsC;AAAA,MACvD;AACA,YAAMA,YAAW,KAAK;AACtB,WAAK,kBAAkB;AACvB,iBAAW,KAAKA,WAAU;AACzB,UAAE,oBAAoB;AAAA,MACvB;AAAA,IACD;AAAA,EACD,UAAE;AACD,SAAK,kBAAkB;AACvB,SAAK,mBAAmB;AACxB,SAAK,qBAAqB;AAC1B,uBAAmB;AAAA,EACpB;AACD;AAGO,SAAS,cAAc,MAAa,eAAoB;AAC9D,MAAI,KAAK,oBAAoB;AAM5B,QAAI,CAAC,KAAK,mBAAmB,kBAAkB,IAAI,IAAI,GAAG;AACzD,WAAK,mBAAmB,kBAAkB,IAAI,MAAM,aAAa;AAAA,IAClE;AAAA,EACD,OAAO;AAEN,iBAAa,CAAC,IAAI,CAAC;AAAA,EACpB;AACD;AAEA,SAAS,uBAAuB,MAAa;AAC5C,qBAAmB,KAAK,oBAAoB,oBAAI,IAAI;AACpD,OAAK,SAAS,MAAM,aAAa;AAClC;AAGO,SAAS,qBAAqB;AACpC,OAAK;AACN;AA6EO,SAAS,YAAe,IAAiC;AAC/D,QAAM,MAAM,IAAI,YAAY,KAAK,oBAAoB,IAAI;AAEzD,OAAK,qBAAqB;AAE1B,MAAI;AACH,QAAI,SAAS;AACb,QAAI,WAAW;AAEf,QAAI;AACH,eAAS,GAAG,MAAO,WAAW,IAAK;AAAA,IACpC,SAAS,GAAG;AACX,UAAI,MAAM;AACV,YAAM;AAAA,IACP;AAEA,QAAI,KAAK,uBAAuB,KAAK;AACpC,YAAM,IAAI,MAAM,gCAAgC;AAAA,IACjD;AAEA,QAAI,UAAU;AACb,UAAI,MAAM;AAAA,IACX,OAAO;AACN,UAAI,OAAO;AAAA,IACZ;AAEA,WAAO;AAAA,EACR,UAAE;AACD,SAAK,qBAAqB,IAAI;AAAA,EAC/B;AACD;AA2BO,SAAS,SAAY,IAAgB;AAC3C,MAAI,KAAK,oBAAoB;AAC5B,WAAO,GAAG;AAAA,EACX;AACA,SAAO,YAAY,EAAE;AACtB;AA0BA,eAAsB,kBAAqB,IAAsB;AAIhE,MAAI,KAAK,oBAAoB,QAAQ;AACpC,UAAM,IAAI,MAAM,8DAA8D;AAAA,EAC/E;AAKA,SAAO,KAAK,kBAAkB;AAC7B,UAAM,IAAI,QAAQ,CAAC,MAAM,eAAe,MAAM,EAAE,IAAI,CAAC,CAAC;AAAA,EACvD;AAEA,QAAM,MAAM,KAAK,sBAAsB,IAAI,YAAY,MAAM,KAAK;AAGlE,MAAI,IAAI,OAAQ,OAAM,IAAI,MAAM,8DAA8D;AAE9F,OAAK,qBAAqB;AAC1B,MAAI;AAEJ,MAAI,SAAS;AAGb,MAAI,QAAQ;AACZ,MAAI;AAEH,aAAS,MAAM,GAAG;AAAA,EACnB,SAAS,GAAG;AAEX,YAAQ,KAAK;AAAA,EACd;AAEA,MAAI,EAAE,IAAI,oBAAoB,GAAG;AAEhC,QAAI,OAAO,UAAU,aAAa;AAEjC,YAAM;AAAA,IACP,OAAO;AACN,aAAO;AAAA,IACR;AAAA,EACD;AAKA,MAAI;AACH,QAAI,OAAO,UAAU,aAAa;AAEjC,UAAI,MAAM;AACV,YAAM;AAAA,IACP,OAAO;AACN,UAAI,OAAO;AACX,aAAO;AAAA,IACR;AAAA,EACD,UAAE;AACD,SAAK,qBAAqB;AAAA,EAC3B;AACD;",
|
|
6
6
|
"names": ["reactors"]
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { maybeCaptureParent, startCapturingParents, stopCapturingParents } from
|
|
|
4
4
|
import { GLOBAL_START_EPOCH } from "./constants.mjs";
|
|
5
5
|
import { EMPTY_ARRAY, equals, haveParentsChanged, singleton } from "./helpers.mjs";
|
|
6
6
|
import { HistoryBuffer } from "./HistoryBuffer.mjs";
|
|
7
|
-
import { getGlobalEpoch,
|
|
7
|
+
import { getGlobalEpoch, getIsInTransaction } from "./transactions.mjs";
|
|
8
8
|
import { RESET_VALUE } from "./types.mjs";
|
|
9
9
|
import { logComputedGetterWarning } from "./warnings.mjs";
|
|
10
10
|
const UNINITIALIZED = /* @__PURE__ */ Symbol.for("com.tldraw.state/UNINITIALIZED");
|
|
@@ -42,7 +42,8 @@ class __UNSAFE__Computed {
|
|
|
42
42
|
lastTraversedEpoch = GLOBAL_START_EPOCH;
|
|
43
43
|
__debug_ancestor_epochs__ = null;
|
|
44
44
|
/**
|
|
45
|
-
* The epoch when the reactor was last checked.
|
|
45
|
+
* The epoch when the reactor was last checked. Advances on every check, including ones that
|
|
46
|
+
* find no change, unlike `lastChangedEpoch`.
|
|
46
47
|
*/
|
|
47
48
|
lastCheckedEpoch = GLOBAL_START_EPOCH;
|
|
48
49
|
parentSet = new ArraySet();
|
|
@@ -63,7 +64,11 @@ class __UNSAFE__Computed {
|
|
|
63
64
|
__unsafe__getWithoutCapture(ignoreErrors) {
|
|
64
65
|
const isNew = this.lastChangedEpoch === GLOBAL_START_EPOCH;
|
|
65
66
|
const globalEpoch = getGlobalEpoch();
|
|
66
|
-
if (!isNew && (this.lastCheckedEpoch === globalEpoch ||
|
|
67
|
+
if (!isNew && (this.lastCheckedEpoch === globalEpoch || // Every ancestor change to an actively-listening computed traverses it (`flushChanges`),
|
|
68
|
+
// so if it hasn't been traversed since it was last checked the O(parents) scan can be
|
|
69
|
+
// skipped. Not valid while a transaction is open (changes traverse at commit), and only
|
|
70
|
+
// if it was up to date when attached (see `attach` in helpers.ts).
|
|
71
|
+
this.isActivelyListening && !getIsInTransaction() && this.lastTraversedEpoch <= this.lastCheckedEpoch || !haveParentsChanged(this))) {
|
|
67
72
|
this.lastCheckedEpoch = globalEpoch;
|
|
68
73
|
if (this.error) {
|
|
69
74
|
if (!ignoreErrors) {
|
|
@@ -98,7 +103,7 @@ class __UNSAFE__Computed {
|
|
|
98
103
|
return this.state;
|
|
99
104
|
} catch (e) {
|
|
100
105
|
const epoch = getGlobalEpoch();
|
|
101
|
-
if (this.
|
|
106
|
+
if (this.error === null) {
|
|
102
107
|
this.state = UNINITIALIZED;
|
|
103
108
|
this.lastChangedEpoch = epoch;
|
|
104
109
|
}
|