@tldraw/state
Version:
tldraw infinite canvas SDK (state).
211 lines (210 loc) • 5.38 kB
JavaScript
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