@tldraw/state 5.3.2 → 5.4.0-canary.02cd0bd3b597
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/DOCS.md +64 -63
- package/README.md +35 -36
- package/dist-cjs/index.d.ts +26 -27
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ArraySet.js +47 -144
- package/dist-cjs/lib/ArraySet.js.map +2 -2
- package/dist-cjs/lib/Atom.js +12 -26
- package/dist-cjs/lib/Atom.js.map +2 -2
- package/dist-cjs/lib/Computed.js +36 -64
- package/dist-cjs/lib/Computed.js.map +2 -2
- package/dist-cjs/lib/EffectScheduler.js +1 -1
- package/dist-cjs/lib/EffectScheduler.js.map +2 -2
- package/dist-cjs/lib/HistoryBuffer.js +8 -8
- package/dist-cjs/lib/HistoryBuffer.js.map +2 -2
- package/dist-cjs/lib/capture.js +1 -3
- package/dist-cjs/lib/capture.js.map +2 -2
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/helpers.js +3 -11
- 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 +11 -19
- package/dist-cjs/lib/transactions.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/warnings.js +2 -4
- package/dist-cjs/lib/warnings.js.map +2 -2
- package/dist-esm/index.d.mts +26 -27
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ArraySet.mjs +47 -144
- package/dist-esm/lib/ArraySet.mjs.map +2 -2
- package/dist-esm/lib/Atom.mjs +12 -26
- package/dist-esm/lib/Atom.mjs.map +2 -2
- package/dist-esm/lib/Computed.mjs +36 -64
- package/dist-esm/lib/Computed.mjs.map +2 -2
- package/dist-esm/lib/EffectScheduler.mjs +1 -1
- package/dist-esm/lib/EffectScheduler.mjs.map +2 -2
- package/dist-esm/lib/HistoryBuffer.mjs +8 -8
- package/dist-esm/lib/HistoryBuffer.mjs.map +2 -2
- package/dist-esm/lib/capture.mjs +1 -3
- package/dist-esm/lib/capture.mjs.map +2 -2
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/helpers.mjs +3 -11
- 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 +11 -19
- package/dist-esm/lib/transactions.mjs.map +2 -2
- package/dist-esm/lib/types.mjs.map +1 -1
- package/dist-esm/lib/warnings.mjs +2 -4
- package/dist-esm/lib/warnings.mjs.map +2 -2
- package/package.json +2 -2
- package/src/lib/ArraySet.ts +68 -176
- package/src/lib/Atom.ts +27 -31
- package/src/lib/Computed.ts +68 -96
- package/src/lib/EffectScheduler.ts +9 -8
- package/src/lib/HistoryBuffer.ts +12 -10
- package/src/lib/__tests__/ArraySet.test.ts +39 -13
- package/src/lib/__tests__/EffectScheduler.test.ts +18 -0
- package/src/lib/__tests__/HistoryBuffer.test.ts +6 -3
- package/src/lib/__tests__/computed.test.ts +75 -0
- package/src/lib/__tests__/errors.test.ts +24 -0
- package/src/lib/__tests__/helpers.test.ts +7 -11
- package/src/lib/__tests__/history.test.ts +32 -2
- package/src/lib/__tests__/localStorageAtom.test.ts +15 -0
- package/src/lib/capture.ts +13 -13
- package/src/lib/constants.ts +3 -22
- package/src/lib/helpers.ts +15 -140
- package/src/lib/localStorageAtom.ts +9 -2
- package/src/lib/transactions.ts +23 -47
- package/src/lib/types.ts +7 -7
- package/src/lib/warnings.ts +2 -10
|
@@ -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/**\n\t * Get whether this transaction is a root (no parents).\n\t *\n\t * @public\n\t */\n\t// eslint-disable-next-line tldraw/no-setter-getter\n\tget isRoot() {\n\t\treturn this.parent === null\n\t}\n\n\t/**\n\t * Commit the transaction's changes.\n\t *\n\t * @public\n\t */\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\tthis.initialAtomValues.forEach((value, atom) => {\n\t\t\t\tif (!this.parent!.initialAtomValues.has(atom)) {\n\t\t\t\t\tthis.parent!.initialAtomValues.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\t// Reset each of the transaction's atoms to its initial value.\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\t// Commit the changes.\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// Reusable state for traverse to avoid closure allocation\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\nfunction traverse(reactors: Set<Reactor>, child: Child) {\n\ttraverseReactors = reactors\n\ttraverseChild(child)\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\t// Collect all of the visited reactors.\n\t\tconst reactors = new Set<Reactor>()\n\n\t\tfor (const atom of atoms) {\n\t\t\tatom.children.visit((child) => traverse(reactors, child))\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/**\n * Handle a change to an atom.\n *\n * @param atom The atom that changed.\n * @param previousValue The atom's previous value.\n *\n * @internal\n */\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\tconst rs = (inst.cleanupReactors ??= new Set())\n\tatom.children.visit((child) => traverse(rs, child))\n}\n\n/**\n * Advances the global epoch counter by one.\n * This is used internally to track when changes occur across the reactive system.\n *\n * @internal\n */\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.\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 * // Does not log\n * // firstName.get() === 'John'\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 * // Does not log\n * // firstName.get() === 'John'\n * // lastName.get() === 'Doe'\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\t// Set the current transaction to the transaction\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\t// Run the function.\n\t\t\tresult = fn(() => (rollback = true))\n\t\t} catch (e) {\n\t\t\t// Abort the transaction if the function throws.\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\t// If the rollback was triggered, abort the transaction.\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\t// Set the current transaction to the transaction's parent.\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": "AACA,SAAS,0BAA0B;AACnC,SAAS,iBAAiB;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
|
|
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": "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",
|
|
6
6
|
"names": ["reactors"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/types.ts"],
|
|
4
|
-
"sourcesContent": ["import { ArraySet } from './ArraySet'\n\n/**\n * A unique symbol used to indicate that a signal's value should be reset or that\n * there is insufficient history to compute diffs between epochs.\n *\n * This value is returned by {@link Signal.getDiffSince} when the requested epoch\n * is too far in the past and the diff sequence cannot be reconstructed.\n *\n * @example\n * ```ts\n * import { atom,
|
|
4
|
+
"sourcesContent": ["import { ArraySet } from './ArraySet'\n\n/**\n * A unique symbol used to indicate that a signal's value should be reset or that\n * there is insufficient history to compute diffs between epochs.\n *\n * This value is returned by {@link Signal.getDiffSince} when the requested epoch\n * is too far in the past and the diff sequence cannot be reconstructed.\n *\n * @example\n * ```ts\n * import { atom, RESET_VALUE } from '@tldraw/state'\n *\n * const count = atom('count', 0, { historyLength: 3, computeDiff: (prev, next) => next - prev })\n * const oldEpoch = count.lastChangedEpoch\n *\n * // Make more changes than the history length can hold\n * count.set(1)\n * count.set(2)\n * count.set(3)\n * count.set(4)\n *\n * const diffs = count.getDiffSince(oldEpoch)\n * if (diffs === RESET_VALUE) {\n * console.log('Too many changes, need to reset state')\n * }\n * ```\n *\n * @public\n */\nexport const RESET_VALUE: unique symbol = Symbol.for('com.tldraw.state/RESET_VALUE')\n\n/**\n * Type representing the the unique symbol RESET_VALUE symbol, used in type annotations\n * to indicate when a signal value should be reset or when diff computation\n * cannot proceed due to insufficient history.\n *\n * @public\n */\nexport type RESET_VALUE = typeof RESET_VALUE\n\n/**\n * A reactive value container that can change over time and track diffs between sequential values.\n *\n * Signals are the foundation of the \\@tldraw/state reactive system. They automatically manage\n * dependencies and trigger updates when their values change. Any computed signal or effect\n * that reads from this signal will be automatically recomputed when the signal's value changes.\n *\n * There are two types of signal:\n * - **Atomic signals** - Created using `atom()`. These are mutable containers that can be\n * directly updated using `set()` or `update()` methods.\n * - **Computed signals** - Created using `computed()`. These derive their values from other\n * signals and are automatically recomputed when dependencies change.\n *\n * @example\n * ```ts\n * import { atom, computed } from '@tldraw/state'\n *\n * // Create an atomic signal\n * const count = atom('count', 0)\n *\n * // Create a computed signal that derives from the atom\n * const doubled = computed('doubled', () => count.get() * 2)\n *\n * console.log(doubled.get()) // 0\n * count.set(5)\n * console.log(doubled.get()) // 10\n * ```\n *\n * @public\n */\nexport interface Signal<Value, Diff = unknown> {\n\t/**\n\t * A human-readable identifier for this signal, used primarily for debugging and performance profiling.\n\t *\n\t * The name is displayed in debug output from {@link whyAmIRunning} and other diagnostic tools.\n\t * It does not need to be globally unique within your application.\n\t */\n\tname: string\n\t/**\n\t * Gets the current value of the signal and establishes a dependency relationship.\n\t *\n\t * When called from within a computed signal or effect, this signal will be automatically\n\t * tracked as a dependency. If this signal's value changes, any dependent computations\n\t * or effects will be marked for re-execution.\n\t *\n\t * @returns The current value stored in the signal\n\t */\n\tget(): Value\n\n\t/**\n\t * The global epoch number when this signal's value last changed.\n\t *\n\t * Note that this represents when the value actually changed, not when it was last computed.\n\t * A computed signal may recalculate and produce the same value without changing its epoch.\n\t * This is used internally for dependency tracking and history management.\n\t */\n\tlastChangedEpoch: number\n\t/**\n\t * Gets the sequence of diffs that occurred between a specific epoch and the current state.\n\t *\n\t * This method enables incremental synchronization by providing a list of changes that\n\t * have occurred since a specific point in time. If the requested epoch is too far in\n\t * the past or the signal doesn't have enough history, it returns the unique symbol RESET_VALUE\n\t * to indicate that a full state reset is required.\n\t *\n\t * @param epoch - The epoch timestamp to get diffs since\n\t * @returns An array of diff objects representing changes since the epoch, or the unique symbol RESET_VALUE if insufficient history is available\n\t */\n\tgetDiffSince(epoch: number): RESET_VALUE | Diff[]\n\t/**\n\t * Gets the current value of the signal without establishing a dependency relationship.\n\t *\n\t * This method bypasses the automatic dependency tracking system, making it useful for\n\t * performance-critical code paths where the overhead of dependency capture would be\n\t * problematic. Use with caution as it breaks the reactive guarantees of the system.\n\t *\n\t * **Warning**: This method should only be used when you're certain that you don't need\n\t * the calling context to react to changes in this signal.\n\t *\n\t * @param ignoreErrors - Whether to suppress errors during value retrieval (optional)\n\t * @returns The current value without establishing dependencies\n\t */\n\t__unsafe__getWithoutCapture(ignoreErrors?: boolean): Value\n\t/** @internal */\n\tchildren: ArraySet<Child>\n}\n\n/**\n * Internal interface representing a child node in the signal dependency graph.\n *\n * This interface is used internally by the reactive system to manage dependencies\n * between signals, computed values, and effects. Each child tracks its parent\n * signals and maintains state needed for efficient dependency graph traversal\n * and change propagation.\n *\n * @internal\n */\nexport interface Child {\n\t/**\n\t * The epoch when this child was last traversed during dependency graph updates.\n\t * Used to prevent redundant traversals during change propagation.\n\t */\n\tlastTraversedEpoch: number\n\n\t/**\n\t * Set of parent signals that this child depends on.\n\t * Used for efficient lookup and cleanup operations.\n\t */\n\treadonly parentSet: ArraySet<Signal<any, any>>\n\n\t/**\n\t * Array of parent signals that this child depends on.\n\t * Maintained in parallel with parentSet for ordered access.\n\t */\n\treadonly parents: Signal<any, any>[]\n\n\t/**\n\t * Array of epochs corresponding to each parent signal.\n\t * Used to detect which parents have changed since last computation.\n\t */\n\treadonly parentEpochs: number[]\n\n\t/**\n\t * Human-readable name for this child, used in debugging output.\n\t */\n\treadonly name: string\n\n\t/**\n\t * Whether this child is currently subscribed to change notifications.\n\t * Used to optimize resource usage by unsubscribing inactive dependencies.\n\t */\n\tisActivelyListening: boolean\n\n\t/**\n\t * Debug information tracking ancestor epochs in the dependency graph.\n\t * Only populated in debug builds for diagnostic purposes.\n\t */\n\t__debug_ancestor_epochs__: Map<Signal<any, any>, number> | null\n}\n\n/**\n * A function type that computes the difference between two values of a signal.\n *\n * This function is used to generate incremental diffs that can be applied to\n * reconstruct state changes over time, so that downstream computeds and effects can\n * update incrementally instead of recomputing from scratch.\n *\n * The function should analyze the previous and current values and return a\n * diff object that represents the change. If the diff cannot be computed\n * (e.g., the values are too different or incompatible), it should return\n * the unique symbol RESET_VALUE to indicate that a full state reset is required.\n *\n * @param previousValue - The previous value of the signal\n * @param currentValue - The current value of the signal\n * @param lastComputedEpoch - For an atom, the epoch when the previous value was set. For a computed, the epoch at which it was last checked (the same value its compute function receives), so that `other.getDiffSince(lastComputedEpoch)` yields exactly the changes not yet accounted for.\n * @param currentEpoch - The epoch when the current value was set\n * @returns A diff object representing the change, or the unique symbol RESET_VALUE if no diff can be computed\n *\n * @example\n * ```ts\n * import { atom, RESET_VALUE } from '@tldraw/state'\n *\n * // Simple numeric diff\n * const numberDiff: ComputeDiff<number, number> = (prev, curr) => curr - prev\n *\n * // Array diff with reset fallback\n * const arrayDiff: ComputeDiff<string[], { added: string[], removed: string[] }> = (prev, curr) => {\n * if (prev.length > 1000 || curr.length > 1000) {\n * return RESET_VALUE // Too complex, force reset\n * }\n * return {\n * added: curr.filter(item => !prev.includes(item)),\n * removed: prev.filter(item => !curr.includes(item))\n * }\n * }\n *\n * const count = atom('count', 0, { computeDiff: numberDiff })\n * ```\n *\n * @public\n */\nexport type ComputeDiff<Value, Diff> = (\n\tpreviousValue: Value,\n\tcurrentValue: Value,\n\tlastComputedEpoch: number,\n\tcurrentEpoch: number\n) => Diff | RESET_VALUE\n"],
|
|
5
5
|
"mappings": "AA8BO,MAAM,cAA6B,uBAAO,IAAI,8BAA8B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { warnOnce } from "@tldraw/utils";
|
|
2
2
|
function logComputedGetterWarning() {
|
|
3
|
-
|
|
4
|
-
didWarnComputedGetter = true;
|
|
5
|
-
console.warn(
|
|
3
|
+
warnOnce(
|
|
6
4
|
`Using \`@computed\` as a decorator for getters is deprecated and will be removed in the near future. Please refactor to use \`@computed\` as a decorator for methods.
|
|
7
5
|
|
|
8
6
|
// Before
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/warnings.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { warnOnce } from '@tldraw/utils'\n\n/**\n * Logs a deprecation warning for the deprecated `@computed` getter decorator syntax.\n * This function is called internally when the library detects usage of `@computed`\n * on a getter method instead of the recommended method syntax.\n *\n * The warning is only shown once per application session to avoid spam in the console.\n * It provides clear guidance on how to migrate from the deprecated getter syntax\n * to the current method-based approach.\n *\n * @example\n * ```ts\n * // Deprecated pattern that triggers this warning:\n * class MyClass {\n * @computed\n * get value() {\n * return this.someAtom.get()\n * }\n * }\n *\n * // Recommended pattern:\n * class MyClass {\n * @computed\n * getValue() {\n * return this.someAtom.get()\n * }\n * }\n * ```\n *\n * @internal\n */\nexport function logComputedGetterWarning() {\n\twarnOnce(\n\t\t`Using \\`@computed\\` as a decorator for getters is deprecated and will be removed in the near future. Please refactor to use \\`@computed\\` as a decorator for methods.\n\n// Before\n@computed\nget foo() {\n\treturn 'foo'\n}\n\n// After\n@computed\ngetFoo() {\n\treturn 'foo'\n}\n`\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gBAAgB;AAgClB,SAAS,2BAA2B;AAC1C;AAAA,IACC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcD;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/state",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (state).",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.4.0-canary.02cd0bd3b597",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"vitest": "^4.1.7"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@tldraw/utils": "5.
|
|
58
|
+
"@tldraw/utils": "5.4.0-canary.02cd0bd3b597"
|
|
59
59
|
},
|
|
60
60
|
"typedoc": {
|
|
61
61
|
"readmeFile": "none",
|
package/src/lib/ArraySet.ts
CHANGED
|
@@ -1,281 +1,173 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
* @
|
|
5
|
-
* @example
|
|
6
|
-
* ```ts
|
|
7
|
-
* import { ARRAY_SIZE_THRESHOLD } from '@tldraw/state'
|
|
8
|
-
*
|
|
9
|
-
* console.log(ARRAY_SIZE_THRESHOLD) // 8
|
|
10
|
-
* ```
|
|
2
|
+
* The number of items an ArraySet holds in array mode before switching to a Set.
|
|
3
|
+
* Exported only for tests.
|
|
4
|
+
* @internal
|
|
11
5
|
*/
|
|
12
6
|
export const ARRAY_SIZE_THRESHOLD = 8
|
|
13
7
|
|
|
14
8
|
/**
|
|
15
9
|
* An ArraySet operates as an array until it reaches a certain size, after which a Set is used
|
|
16
10
|
* instead. In either case, the same methods are used to get, set, remove, and visit the items.
|
|
11
|
+
*
|
|
12
|
+
* `set` and `array` are never both non-null. `set` being null means array mode, but the array
|
|
13
|
+
* itself is only allocated on the first `add` (most signals never get a child, and an empty
|
|
14
|
+
* ArraySet is created for every atom and effect, and two for every computed), so array-mode code
|
|
15
|
+
* must handle `array === null`; `arraySize` is 0 in that state. Once promoted to a set, an
|
|
16
|
+
* ArraySet never goes back.
|
|
17
17
|
* @internal
|
|
18
18
|
*/
|
|
19
19
|
export class ArraySet<T> {
|
|
20
|
-
private
|
|
20
|
+
private set: Set<T> | null = null
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Slots [0, arraySize) hold the items; slots beyond are undefined. `add`/`has` scan the whole
|
|
23
|
+
// array with indexOf, which is why `clear` and `remove` must blank vacated slots.
|
|
24
|
+
private array: (T | undefined)[] | null = null
|
|
23
25
|
|
|
24
|
-
private
|
|
26
|
+
private arraySize = 0
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Get whether this ArraySet has any elements.
|
|
28
|
-
*
|
|
29
|
-
* @returns True if this ArraySet has any elements, false otherwise.
|
|
30
30
|
*/
|
|
31
31
|
// eslint-disable-next-line tldraw/no-setter-getter
|
|
32
32
|
get isEmpty() {
|
|
33
|
-
if (this.array) {
|
|
34
|
-
return this.arraySize === 0
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
if (this.set) {
|
|
38
34
|
return this.set.size === 0
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
return this.arraySize === 0
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
41
|
* Add an element to the ArraySet if it is not already present.
|
|
46
42
|
*
|
|
47
|
-
* @param elem - The element to add to the set
|
|
48
43
|
* @returns `true` if the element was added, `false` if it was already present
|
|
49
|
-
* @example
|
|
50
|
-
* ```ts
|
|
51
|
-
* const arraySet = new ArraySet<string>()
|
|
52
|
-
*
|
|
53
|
-
* console.log(arraySet.add('hello')) // true
|
|
54
|
-
* console.log(arraySet.add('hello')) // false (already exists)
|
|
55
|
-
* ```
|
|
56
44
|
*/
|
|
57
45
|
add(elem: T) {
|
|
58
|
-
if (this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Return false if the element is already in the array.
|
|
62
|
-
if (idx !== -1) {
|
|
46
|
+
if (this.set) {
|
|
47
|
+
if (this.set.has(elem)) {
|
|
63
48
|
return false
|
|
64
49
|
}
|
|
65
50
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Insert the element into the array's next available slot.
|
|
70
|
-
this.array[this.arraySize] = elem
|
|
71
|
-
this.arraySize++
|
|
72
|
-
|
|
73
|
-
return true
|
|
74
|
-
} else {
|
|
75
|
-
// If the array is full, convert it to a set and remove the array.
|
|
76
|
-
this.set = new Set(this.array as any)
|
|
77
|
-
this.array = null
|
|
78
|
-
this.set.add(elem)
|
|
51
|
+
this.set.add(elem)
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
79
54
|
|
|
80
|
-
|
|
81
|
-
|
|
55
|
+
if (!this.array) {
|
|
56
|
+
this.array = Array(ARRAY_SIZE_THRESHOLD)
|
|
57
|
+
} else if (this.array.indexOf(elem) !== -1) {
|
|
58
|
+
return false
|
|
82
59
|
}
|
|
83
60
|
|
|
84
|
-
if (this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return false
|
|
88
|
-
}
|
|
61
|
+
if (this.arraySize < ARRAY_SIZE_THRESHOLD) {
|
|
62
|
+
this.array[this.arraySize] = elem
|
|
63
|
+
this.arraySize++
|
|
89
64
|
|
|
90
|
-
this.set.add(elem)
|
|
91
65
|
return true
|
|
92
66
|
}
|
|
93
67
|
|
|
94
|
-
|
|
68
|
+
// The array is full: promote to a set.
|
|
69
|
+
this.set = new Set(this.array as T[])
|
|
70
|
+
this.set.add(elem)
|
|
71
|
+
this.array = null
|
|
72
|
+
this.arraySize = 0
|
|
73
|
+
|
|
74
|
+
return true
|
|
95
75
|
}
|
|
96
76
|
|
|
97
77
|
/**
|
|
98
78
|
* Remove an element from the ArraySet if it is present.
|
|
99
79
|
*
|
|
100
|
-
* @param elem - The element to remove from the set
|
|
101
80
|
* @returns `true` if the element was removed, `false` if it was not present
|
|
102
|
-
* @example
|
|
103
|
-
* ```ts
|
|
104
|
-
* const arraySet = new ArraySet<string>()
|
|
105
|
-
* arraySet.add('hello')
|
|
106
|
-
*
|
|
107
|
-
* console.log(arraySet.remove('hello')) // true
|
|
108
|
-
* console.log(arraySet.remove('hello')) // false (not present)
|
|
109
|
-
* ```
|
|
110
81
|
*/
|
|
111
82
|
remove(elem: T) {
|
|
112
|
-
if (this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// If the item is not in the array, return false.
|
|
116
|
-
if (idx === -1) {
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
83
|
+
if (this.set) {
|
|
84
|
+
return this.set.delete(elem)
|
|
85
|
+
}
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
|
|
87
|
+
if (!this.array) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
122
90
|
|
|
123
|
-
|
|
124
|
-
// If the item is not the last item in the array, move the last item into the
|
|
125
|
-
// removed item's slot.
|
|
126
|
-
this.array[idx] = this.array[this.arraySize]
|
|
127
|
-
this.array[this.arraySize] = undefined
|
|
128
|
-
}
|
|
91
|
+
const idx = this.array.indexOf(elem)
|
|
129
92
|
|
|
130
|
-
|
|
93
|
+
if (idx === -1) {
|
|
94
|
+
return false
|
|
131
95
|
}
|
|
132
96
|
|
|
133
|
-
|
|
134
|
-
// If the item is not in the set, return false.
|
|
135
|
-
if (!this.set.has(elem)) {
|
|
136
|
-
return false
|
|
137
|
-
}
|
|
97
|
+
this.arraySize--
|
|
138
98
|
|
|
139
|
-
|
|
99
|
+
// Move the last item into the vacated slot so the items stay dense.
|
|
100
|
+
this.array[idx] = this.array[this.arraySize]
|
|
101
|
+
this.array[this.arraySize] = undefined
|
|
140
102
|
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
throw new Error('no set or array')
|
|
103
|
+
return true
|
|
145
104
|
}
|
|
146
105
|
|
|
147
106
|
/**
|
|
148
107
|
* Execute a callback function for each element in the ArraySet.
|
|
149
|
-
*
|
|
150
|
-
* @param visitor - A function to call for each element in the set
|
|
151
|
-
* @example
|
|
152
|
-
* ```ts
|
|
153
|
-
* const arraySet = new ArraySet<string>()
|
|
154
|
-
* arraySet.add('hello')
|
|
155
|
-
* arraySet.add('world')
|
|
156
|
-
*
|
|
157
|
-
* arraySet.visit((item) => {
|
|
158
|
-
* console.log(item) // 'hello', 'world'
|
|
159
|
-
* })
|
|
160
|
-
* ```
|
|
161
108
|
*/
|
|
162
109
|
visit(visitor: (item: T) => void) {
|
|
163
|
-
if (this.
|
|
164
|
-
|
|
165
|
-
const elem = this.array[i]
|
|
166
|
-
|
|
167
|
-
if (typeof elem !== 'undefined') {
|
|
168
|
-
visitor(elem)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
110
|
+
if (this.set) {
|
|
111
|
+
this.set.forEach(visitor)
|
|
171
112
|
|
|
172
113
|
return
|
|
173
114
|
}
|
|
174
115
|
|
|
175
|
-
if (this.
|
|
176
|
-
this.set.forEach(visitor)
|
|
177
|
-
|
|
116
|
+
if (!this.array) {
|
|
178
117
|
return
|
|
179
118
|
}
|
|
180
119
|
|
|
181
|
-
|
|
120
|
+
for (let i = 0; i < this.arraySize; i++) {
|
|
121
|
+
visitor(this.array[i]!)
|
|
122
|
+
}
|
|
182
123
|
}
|
|
183
124
|
|
|
184
125
|
/**
|
|
185
126
|
* Make the ArraySet iterable, allowing it to be used in for...of loops and with spread syntax.
|
|
186
|
-
*
|
|
187
|
-
* @returns An iterator that yields each element in the set
|
|
188
|
-
* @example
|
|
189
|
-
* ```ts
|
|
190
|
-
* const arraySet = new ArraySet<number>()
|
|
191
|
-
* arraySet.add(1)
|
|
192
|
-
* arraySet.add(2)
|
|
193
|
-
*
|
|
194
|
-
* for (const item of arraySet) {
|
|
195
|
-
* console.log(item) // 1, 2
|
|
196
|
-
* }
|
|
197
|
-
*
|
|
198
|
-
* const items = [...arraySet] // [1, 2]
|
|
199
|
-
* ```
|
|
200
127
|
*/
|
|
201
128
|
*[Symbol.iterator]() {
|
|
202
|
-
if (this.
|
|
129
|
+
if (this.set) {
|
|
130
|
+
yield* this.set
|
|
131
|
+
} else if (this.array) {
|
|
203
132
|
for (let i = 0; i < this.arraySize; i++) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (typeof elem !== 'undefined') {
|
|
207
|
-
yield elem
|
|
208
|
-
}
|
|
133
|
+
yield this.array[i]!
|
|
209
134
|
}
|
|
210
|
-
} else if (this.set) {
|
|
211
|
-
yield* this.set
|
|
212
|
-
} else {
|
|
213
|
-
throw new Error('no set or array')
|
|
214
135
|
}
|
|
215
136
|
}
|
|
216
137
|
|
|
217
138
|
/**
|
|
218
139
|
* Check whether an element is present in the ArraySet.
|
|
219
|
-
*
|
|
220
|
-
* @param elem - The element to check for
|
|
221
|
-
* @returns `true` if the element is present, `false` otherwise
|
|
222
|
-
* @example
|
|
223
|
-
* ```ts
|
|
224
|
-
* const arraySet = new ArraySet<string>()
|
|
225
|
-
* arraySet.add('hello')
|
|
226
|
-
*
|
|
227
|
-
* console.log(arraySet.has('hello')) // true
|
|
228
|
-
* console.log(arraySet.has('world')) // false
|
|
229
|
-
* ```
|
|
230
140
|
*/
|
|
231
141
|
has(elem: T) {
|
|
232
|
-
if (this.
|
|
233
|
-
return this.
|
|
234
|
-
} else {
|
|
235
|
-
return this.set!.has(elem)
|
|
142
|
+
if (this.set) {
|
|
143
|
+
return this.set.has(elem)
|
|
236
144
|
}
|
|
145
|
+
|
|
146
|
+
return this.array ? this.array.indexOf(elem) !== -1 : false
|
|
237
147
|
}
|
|
238
148
|
|
|
239
149
|
/**
|
|
240
150
|
* Remove all elements from the ArraySet.
|
|
241
|
-
*
|
|
242
|
-
* @example
|
|
243
|
-
* ```ts
|
|
244
|
-
* const arraySet = new ArraySet<string>()
|
|
245
|
-
* arraySet.add('hello')
|
|
246
|
-
* arraySet.add('world')
|
|
247
|
-
*
|
|
248
|
-
* arraySet.clear()
|
|
249
|
-
* console.log(arraySet.size()) // 0
|
|
250
|
-
* ```
|
|
251
151
|
*/
|
|
252
152
|
clear() {
|
|
253
153
|
if (this.set) {
|
|
254
154
|
this.set.clear()
|
|
255
|
-
} else {
|
|
155
|
+
} else if (this.array) {
|
|
156
|
+
// Blank the used slots in place rather than allocating a new array: this runs on every
|
|
157
|
+
// computed derive and effect run.
|
|
158
|
+
this.array.fill(undefined, 0, this.arraySize)
|
|
256
159
|
this.arraySize = 0
|
|
257
|
-
this.array = []
|
|
258
160
|
}
|
|
259
161
|
}
|
|
260
162
|
|
|
261
163
|
/**
|
|
262
164
|
* Get the number of elements in the ArraySet.
|
|
263
|
-
*
|
|
264
|
-
* @returns The number of elements in the set
|
|
265
|
-
* @example
|
|
266
|
-
* ```ts
|
|
267
|
-
* const arraySet = new ArraySet<string>()
|
|
268
|
-
* console.log(arraySet.size()) // 0
|
|
269
|
-
*
|
|
270
|
-
* arraySet.add('hello')
|
|
271
|
-
* console.log(arraySet.size()) // 1
|
|
272
|
-
* ```
|
|
273
165
|
*/
|
|
274
166
|
size() {
|
|
275
167
|
if (this.set) {
|
|
276
168
|
return this.set.size
|
|
277
|
-
} else {
|
|
278
|
-
return this.arraySize
|
|
279
169
|
}
|
|
170
|
+
|
|
171
|
+
return this.arraySize
|
|
280
172
|
}
|
|
281
173
|
}
|
package/src/lib/Atom.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface AtomOptions<Value, Diff> {
|
|
|
13
13
|
/**
|
|
14
14
|
* The maximum number of diffs to keep in the history buffer.
|
|
15
15
|
*
|
|
16
|
-
* If you don't need
|
|
16
|
+
* If you don't need diffs, leave this as `undefined` and no history buffer will be created. Diffs passed to {@link Atom.set} or produced by {@link AtomOptions.computeDiff} are only recorded when this is set.
|
|
17
17
|
*
|
|
18
18
|
* 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).
|
|
19
19
|
*
|
|
@@ -87,34 +87,19 @@ class __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {
|
|
|
87
87
|
this.computeDiff = options.computeDiff
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
* Custom equality function for comparing values, or null to use default equality.
|
|
92
|
-
* @internal
|
|
93
|
-
*/
|
|
90
|
+
/** @internal */
|
|
94
91
|
readonly isEqual: null | ((a: any, b: any) => boolean)
|
|
95
92
|
|
|
96
|
-
/**
|
|
97
|
-
* Optional function to compute diffs between old and new values.
|
|
98
|
-
* @internal
|
|
99
|
-
*/
|
|
93
|
+
/** @internal */
|
|
100
94
|
computeDiff?: ComputeDiff<Value, Diff>
|
|
101
95
|
|
|
102
|
-
/**
|
|
103
|
-
* The global epoch when this atom was last changed.
|
|
104
|
-
* @internal
|
|
105
|
-
*/
|
|
96
|
+
/** @internal */
|
|
106
97
|
lastChangedEpoch = getGlobalEpoch()
|
|
107
98
|
|
|
108
|
-
/**
|
|
109
|
-
* Set of child signals that depend on this atom.
|
|
110
|
-
* @internal
|
|
111
|
-
*/
|
|
99
|
+
/** @internal */
|
|
112
100
|
children = new ArraySet<Child>()
|
|
113
101
|
|
|
114
|
-
/**
|
|
115
|
-
* Optional history buffer for tracking changes over time.
|
|
116
|
-
* @internal
|
|
117
|
-
*/
|
|
102
|
+
/** @internal */
|
|
118
103
|
historyBuffer?: HistoryBuffer<Diff>
|
|
119
104
|
|
|
120
105
|
/**
|
|
@@ -163,22 +148,34 @@ class __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {
|
|
|
163
148
|
return this.current
|
|
164
149
|
}
|
|
165
150
|
|
|
166
|
-
//
|
|
151
|
+
// `computeDiff` is user code: run it before ticking the epoch, so that any signal it reads is
|
|
152
|
+
// checked against the epoch this write has not yet happened in. Reading a dependent computed
|
|
153
|
+
// after the tick would stamp it as checked at the new epoch while the atom was still being
|
|
154
|
+
// written, and it would then never see the change. Only `undefined` means "no diff
|
|
155
|
+
// supplied"; `null` can be a legitimate diff.
|
|
156
|
+
let historyDiff: Diff | RESET_VALUE | undefined
|
|
157
|
+
if (this.historyBuffer) {
|
|
158
|
+
historyDiff =
|
|
159
|
+
diff !== undefined
|
|
160
|
+
? diff
|
|
161
|
+
: this.computeDiff
|
|
162
|
+
? this.computeDiff(this.current, value, this.lastChangedEpoch, getGlobalEpoch() + 1)
|
|
163
|
+
: RESET_VALUE
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Tick forward the global epoch. This write belongs to that one epoch, so read it once and
|
|
167
|
+
// use it everywhere below — otherwise a `computeDiff` that touches other atoms could leave
|
|
168
|
+
// the history entry and `lastChangedEpoch` disagreeing.
|
|
167
169
|
advanceGlobalEpoch()
|
|
170
|
+
const epoch = getGlobalEpoch()
|
|
168
171
|
|
|
169
172
|
// Add the diff to the history buffer.
|
|
170
173
|
if (this.historyBuffer) {
|
|
171
|
-
this.historyBuffer.pushEntry(
|
|
172
|
-
this.lastChangedEpoch,
|
|
173
|
-
getGlobalEpoch(),
|
|
174
|
-
diff ??
|
|
175
|
-
this.computeDiff?.(this.current, value, this.lastChangedEpoch, getGlobalEpoch()) ??
|
|
176
|
-
RESET_VALUE
|
|
177
|
-
)
|
|
174
|
+
this.historyBuffer.pushEntry(this.lastChangedEpoch, epoch, historyDiff)
|
|
178
175
|
}
|
|
179
176
|
|
|
180
177
|
// Update the atom's record of the epoch when last changed.
|
|
181
|
-
this.lastChangedEpoch =
|
|
178
|
+
this.lastChangedEpoch = epoch
|
|
182
179
|
|
|
183
180
|
const oldValue = this.current
|
|
184
181
|
this.current = value
|
|
@@ -215,7 +212,6 @@ class __Atom__<Value, Diff = unknown> implements Atom<Value, Diff> {
|
|
|
215
212
|
getDiffSince(epoch: number): RESET_VALUE | Diff[] {
|
|
216
213
|
maybeCaptureParent(this)
|
|
217
214
|
|
|
218
|
-
// If no changes have occurred since the given epoch, return an empty array.
|
|
219
215
|
if (epoch >= this.lastChangedEpoch) {
|
|
220
216
|
return EMPTY_ARRAY
|
|
221
217
|
}
|