@tldraw/state 4.3.0-next.2d181ae353a2 → 4.3.0-next.40e4536afc8e

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 CHANGED
@@ -61,7 +61,7 @@ if (actualApiVersion !== currentApiVersion) {
61
61
  }
62
62
  (0, import_utils.registerTldrawLibraryVersion)(
63
63
  "@tldraw/state",
64
- "4.3.0-next.2d181ae353a2",
64
+ "4.3.0-next.40e4536afc8e",
65
65
  "cjs"
66
66
  );
67
67
  //# sourceMappingURL=index.js.map
@@ -99,18 +99,23 @@ function getGlobalEpoch() {
99
99
  function getIsReacting() {
100
100
  return inst.globalIsReacting;
101
101
  }
102
- function traverse(reactors, child) {
102
+ let traverseReactors;
103
+ function traverseChild(child) {
103
104
  if (child.lastTraversedEpoch === inst.globalEpoch) {
104
105
  return;
105
106
  }
106
107
  child.lastTraversedEpoch = inst.globalEpoch;
107
108
  if (child instanceof import_EffectScheduler.EffectScheduler) {
108
- reactors.add(child);
109
+ traverseReactors.add(child);
109
110
  } else {
110
111
  ;
111
- child.children.visit((c) => traverse(reactors, c));
112
+ child.children.visit(traverseChild);
112
113
  }
113
114
  }
115
+ function traverse(reactors, child) {
116
+ traverseReactors = reactors;
117
+ traverseChild(child);
118
+ }
114
119
  function flushChanges(atoms) {
115
120
  if (inst.globalIsReacting) {
116
121
  throw new Error("flushChanges cannot be called during a reaction");
@@ -142,6 +147,7 @@ function flushChanges(atoms) {
142
147
  inst.cleanupReactors = null;
143
148
  inst.globalIsReacting = false;
144
149
  inst.currentTransaction = outerTxn;
150
+ traverseReactors = void 0;
145
151
  }
146
152
  }
147
153
  function atomDidChange(atom, previousValue) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/transactions.ts"],
4
- "sourcesContent": ["import { _Atom } from './Atom'\nimport { EffectScheduler } from './EffectScheduler'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { singleton } from './helpers'\nimport { Child, Signal } from './types'\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 no-restricted-syntax\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<EffectScheduler<unknown>>,\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\nfunction traverse(reactors: Set<EffectScheduler<unknown>>, child: Child) {\n\tif (child.lastTraversedEpoch === inst.globalEpoch) {\n\t\treturn\n\t}\n\n\tchild.lastTraversedEpoch = inst.globalEpoch\n\n\tif (child instanceof EffectScheduler) {\n\t\treactors.add(child)\n\t} else {\n\t\t;(child as any as Signal<any>).children.visit((c) => traverse(reactors, c))\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\t// Collect all of the visited reactors.\n\t\tconst reactors = new Set<EffectScheduler<unknown>>()\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}\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": "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",
4
+ "sourcesContent": ["import { _Atom } from './Atom'\nimport { EffectScheduler } from './EffectScheduler'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { singleton } from './helpers'\nimport { Child, Signal } from './types'\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 no-restricted-syntax\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<EffectScheduler<unknown>>,\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<EffectScheduler<unknown>>\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 (child instanceof EffectScheduler) {\n\t\ttraverseReactors.add(child)\n\t} else {\n\t\t;(child as any as Signal<any>).children.visit(traverseChild)\n\t}\n}\n\nfunction traverse(reactors: Set<EffectScheduler<unknown>>, 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<EffectScheduler<unknown>>()\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": "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",
6
6
  "names": ["reactors"]
7
7
  }
@@ -25,7 +25,7 @@ if (actualApiVersion !== currentApiVersion) {
25
25
  }
26
26
  registerTldrawLibraryVersion(
27
27
  "@tldraw/state",
28
- "4.3.0-next.2d181ae353a2",
28
+ "4.3.0-next.40e4536afc8e",
29
29
  "esm"
30
30
  );
31
31
  export {
@@ -69,18 +69,23 @@ function getGlobalEpoch() {
69
69
  function getIsReacting() {
70
70
  return inst.globalIsReacting;
71
71
  }
72
- function traverse(reactors, child) {
72
+ let traverseReactors;
73
+ function traverseChild(child) {
73
74
  if (child.lastTraversedEpoch === inst.globalEpoch) {
74
75
  return;
75
76
  }
76
77
  child.lastTraversedEpoch = inst.globalEpoch;
77
78
  if (child instanceof EffectScheduler) {
78
- reactors.add(child);
79
+ traverseReactors.add(child);
79
80
  } else {
80
81
  ;
81
- child.children.visit((c) => traverse(reactors, c));
82
+ child.children.visit(traverseChild);
82
83
  }
83
84
  }
85
+ function traverse(reactors, child) {
86
+ traverseReactors = reactors;
87
+ traverseChild(child);
88
+ }
84
89
  function flushChanges(atoms) {
85
90
  if (inst.globalIsReacting) {
86
91
  throw new Error("flushChanges cannot be called during a reaction");
@@ -112,6 +117,7 @@ function flushChanges(atoms) {
112
117
  inst.cleanupReactors = null;
113
118
  inst.globalIsReacting = false;
114
119
  inst.currentTransaction = outerTxn;
120
+ traverseReactors = void 0;
115
121
  }
116
122
  }
117
123
  function atomDidChange(atom, previousValue) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/transactions.ts"],
4
- "sourcesContent": ["import { _Atom } from './Atom'\nimport { EffectScheduler } from './EffectScheduler'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { singleton } from './helpers'\nimport { Child, Signal } from './types'\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 no-restricted-syntax\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<EffectScheduler<unknown>>,\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\nfunction traverse(reactors: Set<EffectScheduler<unknown>>, child: Child) {\n\tif (child.lastTraversedEpoch === inst.globalEpoch) {\n\t\treturn\n\t}\n\n\tchild.lastTraversedEpoch = inst.globalEpoch\n\n\tif (child instanceof EffectScheduler) {\n\t\treactors.add(child)\n\t} else {\n\t\t;(child as any as Signal<any>).children.visit((c) => traverse(reactors, c))\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\t// Collect all of the visited reactors.\n\t\tconst reactors = new Set<EffectScheduler<unknown>>()\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}\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": "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",
4
+ "sourcesContent": ["import { _Atom } from './Atom'\nimport { EffectScheduler } from './EffectScheduler'\nimport { GLOBAL_START_EPOCH } from './constants'\nimport { singleton } from './helpers'\nimport { Child, Signal } from './types'\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 no-restricted-syntax\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<EffectScheduler<unknown>>,\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<EffectScheduler<unknown>>\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 (child instanceof EffectScheduler) {\n\t\ttraverseReactors.add(child)\n\t} else {\n\t\t;(child as any as Signal<any>).children.visit(traverseChild)\n\t}\n}\n\nfunction traverse(reactors: Set<EffectScheduler<unknown>>, 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<EffectScheduler<unknown>>()\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,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,iBAAiB;AAG1B,MAAM,YAAY;AAAA,EAEjB,YACiB,QACA,QACf;AAFe;AACA;AAAA,EACd;AAAA,EAJH,oBAAoB;AAAA,EAMpB,oBAAoB,oBAAI,IAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQxC,IAAI,SAAS;AACZ,WAAO,KAAK,WAAW;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS;AACR,QAAI,KAAK,kBAAkB;AAG1B,iBAAW,QAAQ,KAAK,kBAAkB,KAAK,GAAG;AACjD,+BAAuB,IAAI;AAAA,MAC5B;AAAA,IACD,WAAW,KAAK,QAAQ;AAEvB,mBAAa,KAAK,kBAAkB,KAAK,CAAC;AAAA,IAC3C,OAAO;AAEN,WAAK,kBAAkB,QAAQ,CAAC,OAAO,SAAS;AAC/C,YAAI,CAAC,KAAK,OAAQ,kBAAkB,IAAI,IAAI,GAAG;AAC9C,eAAK,OAAQ,kBAAkB,IAAI,MAAM,KAAK;AAAA,QAC/C;AAAA,MACD,CAAC;AAAA,IACF;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ;AACP,SAAK;AAGL,SAAK,kBAAkB,QAAQ,CAAC,OAAO,SAAS;AAC/C,WAAK,IAAI,KAAK;AACd,WAAK,eAAe,MAAM;AAAA,IAC3B,CAAC;AAGD,SAAK,OAAO;AAAA,EACb;AACD;AAEA,MAAM,OAAO,UAAU,gBAAgB,OAAO;AAAA;AAAA,EAE7C,aAAa,qBAAqB;AAAA;AAAA,EAElC,kBAAkB;AAAA,EAClB,oBAAoB;AAAA,EAEpB,iBAAiB;AAAA,EACjB,eAAe,qBAAqB;AACrC,EAAE;AASK,SAAS,mBAAmB;AAClC,SAAO,KAAK;AACb;AASO,SAAS,iBAAiB;AAChC,SAAO,KAAK;AACb;AASO,SAAS,gBAAgB;AAC/B,SAAO,KAAK;AACb;AAGA,IAAI;AAEJ,SAAS,cAAc,OAAc;AACpC,MAAI,MAAM,uBAAuB,KAAK,aAAa;AAClD;AAAA,EACD;AAEA,QAAM,qBAAqB,KAAK;AAEhC,MAAI,iBAAiB,iBAAiB;AACrC,qBAAiB,IAAI,KAAK;AAAA,EAC3B,OAAO;AACN;AAAC,IAAC,MAA6B,SAAS,MAAM,aAAa;AAAA,EAC5D;AACD;AAEA,SAAS,SAAS,UAAyC,OAAc;AACxE,qBAAmB;AACnB,gBAAc,KAAK;AACpB;AAOA,SAAS,aAAa,OAAwB;AAC7C,MAAI,KAAK,kBAAkB;AAC1B,UAAM,IAAI,MAAM,iDAAiD;AAAA,EAClE;AAEA,QAAM,WAAW,KAAK;AACtB,MAAI;AAEH,SAAK,qBAAqB;AAC1B,SAAK,mBAAmB;AACxB,SAAK,gBAAgB,KAAK;AAG1B,UAAM,WAAW,oBAAI,IAA8B;AAEnD,eAAW,QAAQ,OAAO;AACzB,WAAK,SAAS,MAAM,CAAC,UAAU,SAAS,UAAU,KAAK,CAAC;AAAA,IACzD;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;AAUO,SAAS,cAAc,MAAa,eAAoB;AAC9D,MAAI,KAAK,oBAAoB;AAI5B,QAAI,CAAC,KAAK,mBAAmB,kBAAkB,IAAI,IAAI,GAAG;AACzD,WAAK,mBAAmB,kBAAkB,IAAI,MAAM,aAAa;AAAA,IAClE;AAAA,EACD,WAAW,KAAK,kBAAkB;AAKjC,2BAAuB,IAAI;AAAA,EAC5B,OAAO;AAEN,iBAAa,CAAC,IAAI,CAAC;AAAA,EACpB;AACD;AAEA,SAAS,uBAAuB,MAAa;AAC5C,QAAM,KAAM,KAAK,oBAAoB,oBAAI,IAAI;AAC7C,OAAK,SAAS,MAAM,CAAC,UAAU,SAAS,IAAI,KAAK,CAAC;AACnD;AAQO,SAAS,qBAAqB;AACpC,OAAK;AACN;AA4EO,SAAS,YAAe,IAAiC;AAC/D,QAAM,MAAM,IAAI,YAAY,KAAK,oBAAoB,IAAI;AAGzD,OAAK,qBAAqB;AAE1B,MAAI;AACH,QAAI,SAAS;AACb,QAAI,WAAW;AAEf,QAAI;AAEH,eAAS,GAAG,MAAO,WAAW,IAAK;AAAA,IACpC,SAAS,GAAG;AAEX,UAAI,MAAM;AACV,YAAM;AAAA,IACP;AAEA,QAAI,KAAK,uBAAuB,KAAK;AACpC,YAAM,IAAI,MAAM,gCAAgC;AAAA,IACjD;AAEA,QAAI,UAAU;AAEb,UAAI,MAAM;AAAA,IACX,OAAO;AACN,UAAI,OAAO;AAAA,IACZ;AAEA,WAAO;AAAA,EACR,UAAE;AAED,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;AAEb,MAAI,QAAQ;AACZ,MAAI;AAEH,aAAS,MAAM,GAAG;AAAA,EACnB,SAAS,GAAG;AAEX,YAAQ,KAAK;AAAA,EACd;AAEA,MAAI,EAAE,IAAI,oBAAoB,GAAG;AAChC,QAAI,OAAO,UAAU,aAAa;AAEjC,YAAM;AAAA,IACP,OAAO;AACN,aAAO;AAAA,IACR;AAAA,EACD;AAEA,OAAK,qBAAqB;AAE1B,MAAI,OAAO,UAAU,aAAa;AAEjC,QAAI,MAAM;AACV,UAAM;AAAA,EACP,OAAO;AACN,QAAI,OAAO;AACX,WAAO;AAAA,EACR;AACD;",
6
6
  "names": ["reactors"]
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": "4.3.0-next.2d181ae353a2",
4
+ "version": "4.3.0-next.40e4536afc8e",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,7 +49,7 @@
49
49
  "vitest": "^3.2.4"
50
50
  },
51
51
  "dependencies": {
52
- "@tldraw/utils": "4.3.0-next.2d181ae353a2"
52
+ "@tldraw/utils": "4.3.0-next.40e4536afc8e"
53
53
  },
54
54
  "typedoc": {
55
55
  "readmeFile": "none",
@@ -111,7 +111,10 @@ export function getIsReacting() {
111
111
  return inst.globalIsReacting
112
112
  }
113
113
 
114
- function traverse(reactors: Set<EffectScheduler<unknown>>, child: Child) {
114
+ // Reusable state for traverse to avoid closure allocation
115
+ let traverseReactors: Set<EffectScheduler<unknown>>
116
+
117
+ function traverseChild(child: Child) {
115
118
  if (child.lastTraversedEpoch === inst.globalEpoch) {
116
119
  return
117
120
  }
@@ -119,12 +122,17 @@ function traverse(reactors: Set<EffectScheduler<unknown>>, child: Child) {
119
122
  child.lastTraversedEpoch = inst.globalEpoch
120
123
 
121
124
  if (child instanceof EffectScheduler) {
122
- reactors.add(child)
125
+ traverseReactors.add(child)
123
126
  } else {
124
- ;(child as any as Signal<any>).children.visit((c) => traverse(reactors, c))
127
+ ;(child as any as Signal<any>).children.visit(traverseChild)
125
128
  }
126
129
  }
127
130
 
131
+ function traverse(reactors: Set<EffectScheduler<unknown>>, child: Child) {
132
+ traverseReactors = reactors
133
+ traverseChild(child)
134
+ }
135
+
128
136
  /**
129
137
  * Collect all of the reactors that need to run for an atom and run them.
130
138
  *
@@ -169,6 +177,7 @@ function flushChanges(atoms: Iterable<_Atom>) {
169
177
  inst.cleanupReactors = null
170
178
  inst.globalIsReacting = false
171
179
  inst.currentTransaction = outerTxn
180
+ traverseReactors = undefined! // free memory
172
181
  }
173
182
  }
174
183