UNPKG

@tldraw/state

Version:

tldraw infinite canvas SDK (state).

211 lines (210 loc) • 5.38 kB
import { EffectScheduler } from "./EffectScheduler.mjs"; import { GLOBAL_START_EPOCH } from "./constants.mjs"; import { singleton } from "./helpers.mjs"; class Transaction { constructor(parent, isSync) { this.parent = parent; this.isSync = isSync; } asyncProcessCount = 0; initialAtomValues = /* @__PURE__ */ new Map(); /** * Get whether this transaction is a root (no parents). * * @public */ // eslint-disable-next-line no-restricted-syntax get isRoot() { return this.parent === null; } /** * Commit the transaction's changes. * * @public */ commit() { if (inst.globalIsReacting) { for (const atom of this.initialAtomValues.keys()) { traverseAtomForCleanup(atom); } } else if (this.isRoot) { flushChanges(this.initialAtomValues.keys()); } else { this.initialAtomValues.forEach((value, atom) => { if (!this.parent.initialAtomValues.has(atom)) { this.parent.initialAtomValues.set(atom, value); } }); } } /** * Abort the transaction. * * @public */ abort() { inst.globalEpoch++; this.initialAtomValues.forEach((value, atom) => { atom.set(value); atom.historyBuffer?.clear(); }); this.commit(); } } const inst = singleton("transactions", () => ({ // The current epoch (global to all atoms). globalEpoch: GLOBAL_START_EPOCH + 1, // Whether any transaction is reacting. globalIsReacting: false, currentTransaction: null, cleanupReactors: null, reactionEpoch: GLOBAL_START_EPOCH + 1 })); function getReactionEpoch() { return inst.reactionEpoch; } function getGlobalEpoch() { return inst.globalEpoch; } function getIsReacting() { return inst.globalIsReacting; } function traverse(reactors, child) { if (child.lastTraversedEpoch === inst.globalEpoch) { return; } child.lastTraversedEpoch = inst.globalEpoch; if (child instanceof EffectScheduler) { reactors.add(child); } else { ; child.children.visit((c) => traverse(reactors, c)); } } function flushChanges(atoms) { if (inst.globalIsReacting) { throw new Error("flushChanges cannot be called during a reaction"); } const outerTxn = inst.currentTransaction; try { inst.currentTransaction = null; inst.globalIsReacting = true; inst.reactionEpoch = inst.globalEpoch; const reactors = /* @__PURE__ */ new Set(); for (const atom of atoms) { atom.children.visit((child) => traverse(reactors, child)); } for (const r of reactors) { r.maybeScheduleEffect(); } let updateDepth = 0; while (inst.cleanupReactors?.size) { if (updateDepth++ > 1e3) { throw new Error("Reaction update depth limit exceeded"); } const reactors2 = inst.cleanupReactors; inst.cleanupReactors = null; for (const r of reactors2) { r.maybeScheduleEffect(); } } } finally { inst.cleanupReactors = null; inst.globalIsReacting = false; inst.currentTransaction = outerTxn; } } function atomDidChange(atom, previousValue) { if (inst.currentTransaction) { if (!inst.currentTransaction.initialAtomValues.has(atom)) { inst.currentTransaction.initialAtomValues.set(atom, previousValue); } } else if (inst.globalIsReacting) { traverseAtomForCleanup(atom); } else { flushChanges([atom]); } } function traverseAtomForCleanup(atom) { const rs = inst.cleanupReactors ??= /* @__PURE__ */ new Set(); atom.children.visit((child) => traverse(rs, child)); } function advanceGlobalEpoch() { inst.globalEpoch++; } function transaction(fn) { const txn = new Transaction(inst.currentTransaction, true); inst.currentTransaction = txn; try { let result = void 0; let rollback = false; try { result = fn(() => rollback = true); } catch (e) { txn.abort(); throw e; } if (inst.currentTransaction !== txn) { throw new Error("Transaction boundaries overlap"); } if (rollback) { txn.abort(); } else { txn.commit(); } return result; } finally { inst.currentTransaction = txn.parent; } } function transact(fn) { if (inst.currentTransaction) { return fn(); } return transaction(fn); } async function deferAsyncEffects(fn) { if (inst.currentTransaction?.isSync) { throw new Error("deferAsyncEffects cannot be called during a sync transaction"); } while (inst.globalIsReacting) { await new Promise((r) => queueMicrotask(() => r(null))); } const txn = inst.currentTransaction ?? new Transaction(null, false); if (txn.isSync) throw new Error("deferAsyncEffects cannot be called during a sync transaction"); inst.currentTransaction = txn; txn.asyncProcessCount++; let result = void 0; let error = void 0; try { result = await fn(); } catch (e) { error = e ?? null; } if (--txn.asyncProcessCount > 0) { if (typeof error !== "undefined") { throw error; } else { return result; } } inst.currentTransaction = null; if (typeof error !== "undefined") { txn.abort(); throw error; } else { txn.commit(); return result; } } export { advanceGlobalEpoch, atomDidChange, deferAsyncEffects, getGlobalEpoch, getIsReacting, getReactionEpoch, transact, transaction }; //# sourceMappingURL=transactions.mjs.map