reactronic
Version:
Reactronic - Transactional Reactive State Management
486 lines (485 loc) • 20.6 kB
JavaScript
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();