UNPKG

reactronic

Version:

Reactronic - Transactional Reactive State Management

486 lines (485 loc) 20.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { UNDEF, pause } from "../util/Utils.js"; import { Log, misuse, error, fatal } from "../util/Dbg.js"; import { Isolation } from "../Enums.js"; import { Meta } from "./Data.js"; import { Changeset, Dump, EMPTY_OBJECT_VERSION, UNDEFINED_REVISION } from "./Changeset.js"; export class Transaction { static get current() { return TransactionImpl.curr; } whenFinished(includingParent) { return __awaiter(this, void 0, void 0, function* () { }); } static create(options, parent) { return new TransactionImpl(options, parent); } static run(options, func, ...args) { return TransactionImpl.run(options, func, ...args); } static isolate(func, ...args) { return TransactionImpl.isolate(func, ...args); } static outside(func, ...args) { return TransactionImpl.outside(func, ...args); } static isFrameOver(everyN = 1, timeLimit = 10) { return TransactionImpl.isFrameOver(everyN, timeLimit); } static requestNextFrame(sleepTime = 0) { return TransactionImpl.requestNextFrame(sleepTime); } static get isCanceled() { return TransactionImpl.curr.isCanceled; } } export class TransactionImpl extends Transaction { constructor(options, parent) { super(); this.margin = TransactionImpl.gCurr !== undefined ? TransactionImpl.gCurr.margin + 1 : -1; this.parent = parent; this.changeset = new Changeset(options, parent === null || parent === void 0 ? void 0 : parent.changeset); this.pending = 0; this.sealed = false; this.canceled = undefined; this.after = undefined; this.promise = undefined; this.resolve = UNDEF; this.reject = UNDEF; } static get curr() { return TransactionImpl.gCurr; } get id() { return this.changeset.id; } get hint() { return this.changeset.hint; } get options() { return this.changeset.options; } get timestamp() { return this.changeset.timestamp; } get error() { return this.canceled; } run(func, ...args) { this.guard(); return this.runImpl(undefined, func, ...args); } inspect(func, ...args) { const outer = TransactionImpl.isInspectionMode; try { TransactionImpl.isInspectionMode = true; if (Log.isOn && Log.opt.transaction) Log.write(" ", " ", `T${this.id}[${this.hint}] is being inspected by T${TransactionImpl.gCurr.id}[${TransactionImpl.gCurr.hint}]`); return this.runImpl(undefined, func, ...args); } finally { TransactionImpl.isInspectionMode = outer; } } apply() { if (this.pending > 0) throw misuse("cannot apply transaction having active operations running"); if (this.canceled) throw misuse(`cannot apply transaction that is already canceled: ${this.canceled}`); this.seal(); } seal() { if (!this.sealed) this.run(TransactionImpl.seal, this, undefined, undefined); return this; } wrapAsPending(func, secondary) { this.guard(); const self = this; const inspection = TransactionImpl.isInspectionMode; if (!inspection) self.run(TransactionImpl.preparePendingFunction, self, secondary); else self.inspect(TransactionImpl.preparePendingFunction, self, secondary); const wrappedAsPendingForTransaction = (...args) => { if (!inspection) return self.runImpl(undefined, TransactionImpl.runPendingFunction, self, secondary, func, ...args); else return self.inspect(TransactionImpl.runPendingFunction, self, secondary, func, ...args); }; return wrappedAsPendingForTransaction; } static preparePendingFunction(t, secondary) { if (!secondary) t.pending++; } static runPendingFunction(t, secondary, func, ...args) { t.pending--; const result = func(...args); return result; } cancel(error, restartAfter) { this.runImpl(undefined, TransactionImpl.seal, this, error, restartAfter === null ? TransactionImpl.none : restartAfter); return this; } get isCanceled() { return this.canceled !== undefined; } get isFinished() { return this.sealed && this.pending === 0; } whenFinished(includingParent) { return __awaiter(this, void 0, void 0, function* () { if (includingParent && this.parent) yield this.parent.whenFinished(includingParent); else if (!this.isFinished) yield this.acquirePromise(); }); } static run(options, func, ...args) { const t = TransactionImpl.acquire(options); const isRoot = t !== TransactionImpl.gCurr; t.guard(); let result = t.runImpl(options === null || options === void 0 ? void 0 : options.logging, func, ...args); if (isRoot) { if (result instanceof Promise) { result = TransactionImpl.outside(() => { return t.wrapToRetry(t.wrapToWaitUntilFinish(result), func, ...args); }); } t.seal(); } return result; } static isolate(func, ...args) { return TransactionImpl.run({ isolation: Isolation.disjoinFromOuterTransaction }, func, ...args); } static outside(func, ...args) { const outer = TransactionImpl.gCurr; try { TransactionImpl.gCurr = TransactionImpl.none; return func(...args); } finally { TransactionImpl.gCurr = outer; } } static isFrameOver(everyN = 1, timeLimit = 10) { TransactionImpl.frameOverCounter++; let result = TransactionImpl.frameOverCounter % everyN === 0; if (result) { const ms = performance.now() - TransactionImpl.frameStartTime; result = ms > timeLimit; } return result; } static requestNextFrame(sleepTime = 0) { return pause(sleepTime); } static acquire(options) { var _a; const outer = TransactionImpl.gCurr; const isolation = (_a = options === null || options === void 0 ? void 0 : options.isolation) !== null && _a !== void 0 ? _a : Isolation.joinToCurrentTransaction; if (outer.isFinished || outer.options.isolation === Isolation.disjoinFromOuterAndInnerTransactions) return new TransactionImpl(options); else if (isolation === Isolation.joinAsNestedTransaction) return new TransactionImpl(options, outer); else if (isolation !== Isolation.joinToCurrentTransaction) return new TransactionImpl(options); else return outer; } guard() { if (this.sealed && TransactionImpl.gCurr !== this) throw misuse("cannot run transaction that is already sealed"); } wrapToRetry(p, func, ...args) { return __awaiter(this, void 0, void 0, function* () { try { const result = yield p; if (this.canceled) throw this.canceled; return result; } catch (error) { if (this.after !== TransactionImpl.none) { if (this.after) { yield this.after.whenFinished(); const options = { hint: `${this.hint} - restart after T${this.after.id}`, isolation: this.options.isolation === Isolation.joinToCurrentTransaction ? Isolation.disjoinFromOuterTransaction : this.options.isolation, logging: this.changeset.options.logging, token: this.changeset.options.token, }; return TransactionImpl.run(options, func, ...args); } else throw error; } else return undefined; } }); } wrapToWaitUntilFinish(p) { return __awaiter(this, void 0, void 0, function* () { const result = yield p; yield this.whenFinished(); return result; }); } runImpl(logging, func, ...args) { let result; const outer = TransactionImpl.gCurr; const p = this.parent; try { if (outer === TransactionImpl.none) { TransactionImpl.frameStartTime = performance.now(); TransactionImpl.frameOverCounter = 0; } TransactionImpl.gCurr = this; this.pending++; const acquired = this.changeset.acquire(outer.changeset); if (acquired && p) p.run(() => p.pending++); result = func(...args); if (this.sealed && this.pending === 1) { if (!this.canceled) this.checkForConflicts(); else if (!this.after) throw this.canceled; } } catch (e) { if (!TransactionImpl.isInspectionMode) this.cancel(e); throw e; } finally { this.pending--; if (this.sealed && this.pending === 0) { const obsolete = this.applyOrDiscard(); if (p) p.runImpl(undefined, () => p.pending--); TransactionImpl.gCurr = outer; TransactionImpl.outside(Changeset.enqueueReactionsToRun, obsolete); } else TransactionImpl.gCurr = outer; } return result; } static seal(t, error, after) { if (!t.canceled && error) { t.canceled = error; t.after = after; if (Log.isOn && Log.opt.transaction) { Log.write("║", " [!]", `${error.message}`, undefined, " *** CANCEL ***"); if (after && after !== TransactionImpl.none) Log.write("║", " [!]", `T${t.id}[${t.hint}] will be restarted${t !== after ? ` after T${after.id}[${after.hint}]` : ""}`); } Changeset.discardAllListeners(t.changeset); } t.sealed = true; } checkForConflicts() { const conflicts = this.changeset.rebase(); if (conflicts) this.tryResolveConflicts(conflicts); } tryResolveConflicts(conflicts) { throw error(`T${this.id}[${this.hint}] conflicts with: ${Dump.conflicts(conflicts)}`, undefined); } applyOrDiscard() { let obsolete; try { if (Log.isOn && Log.opt.change) Log.write("╠═", "", "", undefined, "changes"); this.changeset.seal(); obsolete = this.applyOrDiscardChangeset(); this.changeset.triggerGarbageCollection(); if (this.promise) { if (this.canceled && !this.after) this.reject(this.canceled); else this.resolve(); } if (Log.isOn) Object.freeze(this); } catch (e) { fatal(e); throw e; } return obsolete; } applyOrDiscardChangeset() { const error = this.canceled; const changeset = this.changeset; changeset.items.forEach((ov, h) => { changeset.sealObjectVersion(h, ov); if (!error) { this.applyObjectChanges(h, ov); if (Changeset.garbageCollectionSummaryInterval < Number.MAX_SAFE_INTEGER) { Changeset.totalObjectSnapshotCount++; if (ov.former.objectVersion === EMPTY_OBJECT_VERSION) Changeset.totalObjectHandleCount++; } } }); if (Log.isOn) { if (Log.opt.change && !error && !changeset.parent) { changeset.items.forEach((ov, h) => { const fields = []; ov.changes.forEach((o, fk) => fields.push(fk.toString())); const s = fields.join(", "); Log.write("║", "√", `${Dump.snapshot2(h, ov.changeset)} (${s}) is ${ov.former.objectVersion === EMPTY_OBJECT_VERSION ? "constructed" : `applied over #${h.id}t${ov.former.objectVersion.changeset.id}s${ov.former.objectVersion.changeset.timestamp}`}`); }); } if (Log.opt.transaction) Log.write(changeset.timestamp < UNDEFINED_REVISION ? "╚══" : "═══", `s${this.timestamp}`, `${this.hint} - ${error ? "CANCEL" : "APPLY"}(${this.changeset.items.size})${error ? ` - ${error}` : ""}`); } let obsolete = changeset.obsolete; if (changeset.parent) { if (changeset.obsolete.length > 0) { for (const o of changeset.obsolete) changeset.parent.obsolete.push(o); obsolete = []; } } else if (!error) Changeset.propagateAllChangesToListeners(changeset); return obsolete; } applyObjectChanges(h, ov) { const parent = this.parent; if (parent) TransactionImpl.migrateObjectChangesToAnotherTransaction(h, ov, parent); else h.applied = ov; } static migrateObjectChangesToAnotherTransaction(h, ov, tParent) { const csParent = tParent.changeset; const ovParent = csParent.getEditableObjectVersion(h, Meta.Undefined, undefined); if (ov.former.objectVersion.changeset === EMPTY_OBJECT_VERSION.changeset) { for (const fk in ov.data) { TransactionImpl.migrateFieldVersionToAnotherTransaction(h, fk, ov, ovParent, tParent); } } else { ov.changes.forEach((o, fk) => { TransactionImpl.migrateFieldVersionToAnotherTransaction(h, fk, ov, ovParent, tParent); }); } } static migrateFieldVersionToAnotherTransaction(h, fk, ov, ovParent, tParent) { const csParent = tParent.changeset; const cf = ov.data[fk]; const cfParent = ovParent.data[fk]; if (cf.isComputed) { const migrated = TransactionImpl.migrateContentFootprint(cf, tParent); if (ovParent.former.objectVersion.data[fk] !== cfParent) { let listeners = cfParent.listeners; if (listeners) { const migratedListeners = migrated.listeners = new Set(); listeners.forEach(o => { const conformingSignals = o.signals; const sub = conformingSignals.get(cfParent); conformingSignals.delete(cfParent); conformingSignals.set(migrated, sub); migratedListeners.add(o); }); cfParent.listeners = undefined; } listeners = cf.listeners; if (listeners) { let migratedListeners = migrated.listeners; if (migratedListeners === undefined) migratedListeners = migrated.listeners = new Set(); listeners.forEach(o => { const conformingSignals = o.signals; const sub = conformingSignals.get(cf); conformingSignals.delete(cf); conformingSignals.set(migrated, sub); migratedListeners.add(o); }); cf.listeners = undefined; } const signals = cf.signals; const migratedSignals = migrated.signals; if (signals) { signals.forEach((s, o) => { const conformingListeners = o.listeners; conformingListeners.delete(cf); conformingListeners.add(migrated); migratedSignals.set(o, s); }); signals.clear(); } ovParent.data[fk] = migrated; } else { const listeners = cf.listeners; if (listeners) { const migratedReactions = migrated.listeners = new Set(); listeners.forEach(o => { const conformingSignals = o.signals; const sub = conformingSignals.get(cf); conformingSignals.delete(cf); conformingSignals.set(migrated, sub); migratedReactions.add(o); }); cf.listeners = undefined; } const signals = cf.signals; const migratedSignals = migrated.signals; if (signals) { signals.forEach((s, o) => { const conformingListeners = o.listeners; conformingListeners.delete(cf); conformingListeners.add(migrated); migratedSignals.set(o, s); }); signals.clear(); } ovParent.data[fk] = migrated; } csParent.bumpBy(ovParent.former.objectVersion.changeset.timestamp); Changeset.markEdited(undefined, migrated, true, ovParent, fk, h); } else { const parentContent = cfParent === null || cfParent === void 0 ? void 0 : cfParent.content; if (ovParent.former.objectVersion.data[fk] !== cfParent) { cfParent.content = cf.content; const listeners = cf.listeners; if (listeners) { if (cfParent.listeners === undefined) cfParent.listeners = new Set(); listeners.forEach(o => { const conformingSignals = o.signals; const sub = conformingSignals.get(cf); conformingSignals.delete(cf); conformingSignals.set(cfParent, sub); cfParent.listeners.add(o); }); } } else ovParent.data[fk] = cf; Changeset.markEdited(parentContent, cf.content, true, ovParent, fk, h); } } acquirePromise() { if (!this.promise) { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } return this.promise; } static getCurrentChangeset() { return TransactionImpl.gCurr.changeset; } static getEditableChangeset() { if (TransactionImpl.isInspectionMode) throw misuse("cannot make changes during transaction inspection"); return TransactionImpl.gCurr.changeset; } static _init() { Changeset.current = TransactionImpl.getCurrentChangeset; Changeset.edit = TransactionImpl.getEditableChangeset; TransactionImpl.none.sealed = true; TransactionImpl.none.changeset.seal(); Changeset._init(); } } TransactionImpl.none = new TransactionImpl({ hint: "<none>" }); TransactionImpl.gCurr = TransactionImpl.none; TransactionImpl.isInspectionMode = false; TransactionImpl.frameStartTime = 0; TransactionImpl.frameOverCounter = 0; TransactionImpl.migrateContentFootprint = function (fv, target) { throw misuse("this implementation of migrateContentFootprint should never be called"); }; TransactionImpl._init();