reactronic
Version:
Reactronic - Transactional Reactive State Management
762 lines (761 loc) • 38.7 kB
JavaScript
import { Log, misuse } from "../util/Dbg.js";
import { Kind, Reentrance, Isolation } from "../Enums.js";
import { ObjectHandle, ContentFootprint, Meta } from "./Data.js";
import { Changeset, Dump, EMPTY_OBJECT_VERSION, MAX_REVISION } from "./Changeset.js";
import { Transaction, TransactionImpl } from "./Transaction.js";
import { IndicatorImpl } from "./Indicator.js";
import { Mvcc, OptionsImpl } from "./Mvcc.js";
import { JournalImpl } from "./Journal.js";
const BOOT_ARGS = [];
const BOOT_CAUSE = "<boot>";
const EMPTY_HANDLE = new ObjectHandle(undefined, undefined, Mvcc.sx, EMPTY_OBJECT_VERSION, "<boot>");
export class ReactionImpl {
configure(options) { return ReactionImpl.configureImpl(this, options); }
get options() { return this.peek(undefined).footprint.options; }
get nonreactive() { return this.peek(undefined).footprint.content; }
get args() { return this.use().footprint.args; }
get result() { return this.reuseOrRelaunch(true, undefined).content; }
get error() { return this.use().footprint.error; }
get stamp() { return this.use().objectVersion.changeset.timestamp; }
get isReusable() { return this.use().isReusable; }
markObsolete() { Transaction.run({ hint: Log.isOn ? `markObsolete(${Dump.obj(this.ownerHandle, this.fieldKey)})` : "markObsolete()" }, ReactionImpl.markObsolete, this); }
pullLastResult(args) { return this.reuseOrRelaunch(true, args).content; }
constructor(h, fk) {
this.ownerHandle = h;
this.fieldKey = fk;
}
reuseOrRelaunch(weak, args) {
let ror = this.peek(args);
const ctx = ror.changeset;
const footprint = ror.footprint;
const opts = footprint.options;
if (!ror.isReusable && !ror.objectVersion.disposed
&& (!weak || footprint.cause === BOOT_CAUSE || !footprint.successor ||
footprint.successor.transaction.isFinished)) {
const isolation = !weak ? opts.isolation : Isolation.disjoinFromOuterTransaction;
const token = opts.noSideEffects ? this : undefined;
const ror2 = this.relaunch(ror, isolation, opts, token, args);
const ctx2 = ror2.footprint.changeset;
if (!weak || ctx === ctx2 || (ctx2.sealed && ctx.timestamp >= ctx2.timestamp))
ror = ror2;
}
else if (Log.isOn && Log.opt.operation && (opts.logging === undefined ||
opts.logging.operation === undefined || opts.logging.operation === true))
Log.write(Transaction.current.isFinished ? "" : "║", " (=)", `${Dump.snapshot2(ror.footprint.descriptor.ownerHandle, ror.changeset, this.fieldKey)} result is reused from T${ror.footprint.transaction.id}[${ror.footprint.transaction.hint}]`);
const t = ror.footprint;
Changeset.markUsed(t, ror.objectVersion, this.fieldKey, this.ownerHandle, t.options.kind, weak);
return t;
}
static manageReaction(method) {
const ctl = Meta.get(method, Meta.Descriptor);
if (!ctl)
throw misuse(`given method is not decorated as reactronic one: ${method.name}`);
return ctl;
}
static configureImpl(self, options) {
let footprint;
if (self)
footprint = self.edit().footprint;
else
footprint = OperationFootprintImpl.current;
if (!footprint)
throw misuse("reactronic decorator is only applicable to methods");
footprint.options = new OptionsImpl(footprint.options.getter, footprint.options.setter, footprint.options, options, false);
if (Log.isOn && Log.opt.write)
Log.write("║", " =", `${footprint.hint()}.options are changed`);
return footprint.options;
}
static proceedWithinGivenLaunch(footprint, func, ...args) {
let result = undefined;
const outer = OperationFootprintImpl.current;
try {
OperationFootprintImpl.current = footprint;
result = func(...args);
}
catch (e) {
if (footprint)
footprint.error = e;
throw e;
}
finally {
OperationFootprintImpl.current = outer;
}
return result;
}
static why() {
var _a, _b;
return (_b = (_a = OperationFootprintImpl.current) === null || _a === void 0 ? void 0 : _a.why()) !== null && _b !== void 0 ? _b : BOOT_CAUSE;
}
static briefWhy() {
var _a, _b;
return (_b = (_a = OperationFootprintImpl.current) === null || _a === void 0 ? void 0 : _a.briefWhy()) !== null && _b !== void 0 ? _b : BOOT_CAUSE;
}
static dependencies() {
const l = OperationFootprintImpl.current;
return l ? l.dependencies() : ["RxSystem.dependencies should be called from inside of reactive method"];
}
peek(args) {
const ctx = Changeset.current();
const ov = ctx.lookupObjectVersion(this.ownerHandle, this.fieldKey, false);
const footprint = this.acquireFromObjectVersion(ov, args);
const applied = this.ownerHandle.applied.data[this.fieldKey];
const isReusable = footprint.options.kind !== Kind.transaction && footprint.cause !== BOOT_CAUSE &&
(ctx === footprint.changeset || ctx.timestamp < footprint.obsoleteSince || applied.obsoleteDueTo === undefined) &&
(!footprint.options.signalArgs || args === undefined ||
footprint.args.length === args.length && footprint.args.every((t, i) => t === args[i])) || ov.disposed;
return { footprint, isReusable, changeset: ctx, objectVersion: ov };
}
use() {
const ror = this.peek(undefined);
Changeset.markUsed(ror.footprint, ror.objectVersion, this.fieldKey, this.ownerHandle, ror.footprint.options.kind, true);
return ror;
}
edit() {
const h = this.ownerHandle;
const fk = this.fieldKey;
const ctx = Changeset.edit();
const ov = ctx.getEditableObjectVersion(h, fk, Meta.Handle, this);
let footprint = this.acquireFromObjectVersion(ov, undefined);
if (footprint.changeset !== ov.changeset) {
const newFootprint = new OperationFootprintImpl(Transaction.current, this, ov.changeset, footprint, false);
ov.data[fk] = newFootprint.reenterOver(footprint);
ctx.bumpBy(ov.former.objectVersion.changeset.timestamp);
Changeset.markEdited(footprint, newFootprint, true, ov, fk, h);
footprint = newFootprint;
}
return { footprint, isReusable: true, changeset: ctx, objectVersion: ov };
}
acquireFromObjectVersion(ov, args) {
const fk = this.fieldKey;
let footprint = ov.data[fk];
if (footprint.descriptor !== this) {
if (ov.changeset !== EMPTY_OBJECT_VERSION.changeset) {
const hint = Log.isOn ? `${Dump.obj(this.ownerHandle, fk)}/init` : "OperationDescriptor/init";
const isolation = Isolation.joinToCurrentTransaction;
footprint = Transaction.run({ hint, isolation, token: this }, () => {
const h = this.ownerHandle;
let r = Changeset.current().getObjectVersion(h, fk);
let newFootprint = r.data[fk];
if (newFootprint.descriptor !== this) {
r = Changeset.edit().getEditableObjectVersion(h, fk, Meta.Handle, this);
const t = new OperationFootprintImpl(Transaction.current, this, r.changeset, newFootprint, false);
if (args)
t.args = args;
t.cause = BOOT_CAUSE;
r.data[fk] = t;
Changeset.markEdited(newFootprint, t, true, r, fk, h);
newFootprint = t;
}
return newFootprint;
});
}
else {
const initialFootprint = new OperationFootprintImpl(Transaction.current, this, ov.changeset, footprint, false);
if (args)
initialFootprint.args = args;
initialFootprint.cause = BOOT_CAUSE;
ov.data[fk] = initialFootprint;
footprint = initialFootprint;
if (Log.isOn && Log.opt.write)
Log.write("║", " ++", `${Dump.obj(this.ownerHandle, fk)} is initialized (revision ${ov.revision})`);
}
}
return footprint;
}
relaunch(existing, isolation, options, token, args) {
const hint = Log.isOn ? `${Dump.obj(this.ownerHandle, this.fieldKey)}${args && args.length > 0 && (typeof args[0] === "number" || typeof args[0] === "string") ? ` - ${args[0]}` : ""}` : `${Dump.obj(this.ownerHandle, this.fieldKey)}`;
let ror = existing;
const opts = { hint, isolation, journal: options.journal, logging: options.logging, token };
const result = Transaction.run(opts, (argsx) => {
if (!ror.footprint.transaction.isCanceled) {
ror = this.edit();
if (Log.isOn && Log.opt.operation)
Log.write("║", " o", `${ror.footprint.why()}`);
ror.footprint.proceed(this.ownerHandle.proxy, argsx);
}
else {
ror = this.peek(argsx);
if (ror.footprint.options.kind === Kind.transaction || !ror.isReusable) {
ror = this.edit();
if (Log.isOn && Log.opt.operation)
Log.write("║", " o", `${ror.footprint.why()}`);
ror.footprint.proceed(this.ownerHandle.proxy, argsx);
}
}
return ror.footprint.result;
}, args);
ror.footprint.result = result;
return ror;
}
static markObsolete(self) {
const ror = self.peek(undefined);
const ctx = ror.changeset;
const obsolete = ror.footprint.transaction.isFinished ? ctx.obsolete : ror.footprint.transaction.changeset.obsolete;
ror.footprint.markObsoleteDueTo(ror.footprint, self.fieldKey, EMPTY_OBJECT_VERSION.changeset, EMPTY_HANDLE, BOOT_CAUSE, ctx.timestamp, obsolete);
}
}
class OperationFootprintImpl extends ContentFootprint {
constructor(transaction, descriptor, changeset, former, clone) {
super(undefined, 0);
this.margin = OperationFootprintImpl.current ? OperationFootprintImpl.current.margin + 1 : 1;
this.transaction = transaction;
this.descriptor = descriptor;
this.changeset = changeset;
this.signals = new Map();
if (former instanceof OperationFootprintImpl) {
this.options = former.options;
this.cause = former.obsoleteDueTo;
this.args = former.args;
if (clone) {
this.lastEditorChangesetId = former.lastEditorChangesetId;
this.result = former.result;
this.error = former.error;
this.started = former.started;
this.obsoleteSince = former.obsoleteSince;
this.obsoleteDueTo = former.obsoleteDueTo;
this.successor = former.successor;
}
else {
this.lastEditorChangesetId = changeset.id;
this.result = undefined;
this.error = undefined;
this.started = 0;
this.obsoleteSince = 0;
this.obsoleteDueTo = undefined;
this.successor = undefined;
}
}
else {
this.lastEditorChangesetId = changeset.id;
this.options = former;
this.cause = undefined;
this.args = BOOT_ARGS;
this.result = undefined;
this.error = undefined;
this.started = 0;
this.obsoleteSince = 0;
this.obsoleteDueTo = undefined;
this.successor = undefined;
}
}
get isComputed() { return true; }
hint() { return `${Dump.snapshot2(this.descriptor.ownerHandle, this.changeset, this.descriptor.fieldKey, this)}`; }
get order() { return this.options.order; }
get ["#this#"]() {
return `Operation: ${this.why()}`;
}
clone(t, cs) {
return new OperationFootprintImpl(t, this.descriptor, cs, this, true);
}
why() {
let cause;
if (this.cause)
cause = ` ◀◀ ${this.cause}`;
else if (this.descriptor.options.kind === Kind.transaction)
cause = " ◀◀ operation";
else
cause = ` ◀◀ T${this.changeset.id}[${this.changeset.hint}]`;
return `${this.hint()}${cause}`;
}
briefWhy() {
return this.why();
}
dependencies() {
throw misuse("not implemented yet");
}
wrap(func) {
const wrappedForOperation = (...args) => {
if (Log.isOn && Log.opt.step && this.result)
Log.writeAs({ margin2: this.margin }, "║", "‾\\", `${this.hint()} - step in `, 0, " │");
const started = Date.now();
const result = ReactionImpl.proceedWithinGivenLaunch(this, func, ...args);
const ms = Date.now() - started;
if (Log.isOn && Log.opt.step && this.result)
Log.writeAs({ margin2: this.margin }, "║", "_/", `${this.hint()} - step out `, 0, this.started > 0 ? " │" : "");
if (ms > Mvcc.mainThreadBlockingWarningThreshold)
Log.write("", "[!]", this.why(), ms, " *** main thread is too busy ***");
return result;
};
return wrappedForOperation;
}
proceed(proxy, args) {
if (args)
this.args = args;
this.obsoleteSince = MAX_REVISION;
if (!this.error)
ReactionImpl.proceedWithinGivenLaunch(this, OperationFootprintImpl.proceed, this, proxy);
else
this.result = Promise.reject(this.error);
}
markObsoleteDueTo(footprint, fk, changeset, h, outer, since, collector) {
var _a, _b, _c;
if (this.signals !== undefined) {
const skip = !footprint.isComputed &&
changeset.id === this.lastEditorChangesetId;
if (!skip) {
const why = `${Dump.snapshot2(h, changeset, fk, footprint)} ◀◀ ${outer}`;
const isReactive = this.options.kind === Kind.reaction;
this.obsoleteDueTo = why;
this.obsoleteSince = since;
if (Log.isOn && (Log.opt.obsolete || ((_a = this.options.logging) === null || _a === void 0 ? void 0 : _a.obsolete)))
Log.write(Log.opt.transaction && !Changeset.current().sealed ? "║" : " ", isReactive ? "█" : "▒", isReactive && changeset === EMPTY_OBJECT_VERSION.changeset
? `${this.hint()} is reactive and will run automatically (order ${this.options.order})`
: `${this.hint()} is obsolete due to ${Dump.snapshot2(h, changeset, fk)} since s${since}${isReactive ? ` and will run automatically (order ${this.options.order})` : ""}`);
this.stopListeningAllSignals();
if (isReactive)
collector.push(this);
else
(_b = this.listeners) === null || _b === void 0 ? void 0 : _b.forEach(s => s.markObsoleteDueTo(this, this.descriptor.fieldKey, this.changeset, this.descriptor.ownerHandle, why, since, collector));
const tran = this.transaction;
if (tran.changeset === changeset) {
}
else if (!tran.isFinished && this !== footprint && !this.options.allowObsoleteToFinish)
tran.cancel(new Error(`T${tran.id}[${tran.hint}] is canceled due to obsolete ${Dump.snapshot2(h, changeset, fk)} changed by T${changeset.id}[${changeset.hint}]`), null);
}
else if (Log.isOn && (Log.opt.obsolete || ((_c = this.options.logging) === null || _c === void 0 ? void 0 : _c.obsolete)))
Log.write(" ", "x", `${this.hint()} is not obsolete due to its own change to ${Dump.snapshot2(h, changeset, fk, footprint)}`);
}
}
relaunchIfNotUpToDate(now, nothrow) {
const t = this.options.throttling;
const interval = Date.now() + this.started;
const hold = t ? t - interval : 0;
if (now || hold < 0) {
if (this.isNotUpToDate()) {
try {
const footprint = this.descriptor.reuseOrRelaunch(false, undefined);
if (footprint.result instanceof Promise)
footprint.result.catch(error => {
if (footprint.options.kind === Kind.reaction)
misuse(`reactive function ${footprint.hint()} failed and will not run anymore: ${error}`, error);
});
}
catch (e) {
if (!nothrow)
throw e;
else if (this.options.kind === Kind.reaction)
misuse(`reactive ${this.hint()} failed and will not run anymore: ${e}`, e);
}
}
}
else if (t < Number.MAX_SAFE_INTEGER) {
if (hold > 0)
setTimeout(() => this.relaunchIfNotUpToDate(true, true), hold);
else
this.addToDeferredReactiveFunctions();
}
}
isNotUpToDate() {
return !this.error && (this.options.kind === Kind.transaction ||
!this.successor || this.successor.transaction.isCanceled);
}
reenterOver(head) {
let error = undefined;
const opponent = head.successor;
if (opponent && !opponent.transaction.isFinished) {
if (Log.isOn && Log.opt.obsolete)
Log.write("║", " [!]", `${this.hint()} is trying to re-enter over ${opponent.hint()}`);
switch (head.options.reentrance) {
case Reentrance.preventWithError:
if (!opponent.transaction.isCanceled)
throw misuse(`${head.hint()} (${head.why()}) is not reentrant over ${opponent.hint()} (${opponent.why()})`);
error = new Error(`T${this.transaction.id}[${this.transaction.hint}] is on hold/PreventWithError due to canceled T${opponent.transaction.id}[${opponent.transaction.hint}]`);
this.transaction.cancel(error, opponent.transaction);
break;
case Reentrance.waitAndRestart:
error = new Error(`T${this.transaction.id}[${this.transaction.hint}] is on hold/WaitAndRestart due to active T${opponent.transaction.id}[${opponent.transaction.hint}]`);
this.transaction.cancel(error, opponent.transaction);
break;
case Reentrance.cancelAndWaitPrevious:
error = new Error(`T${this.transaction.id}[${this.transaction.hint}] is on hold/CancelAndWaitPrevious due to active T${opponent.transaction.id}[${opponent.transaction.hint}]`);
this.transaction.cancel(error, opponent.transaction);
opponent.transaction.cancel(new Error(`T${opponent.transaction.id}[${opponent.transaction.hint}] is canceled due to re-entering T${this.transaction.id}[${this.transaction.hint}]`), null);
break;
case Reentrance.cancelPrevious:
opponent.transaction.cancel(new Error(`T${opponent.transaction.id}[${opponent.transaction.hint}] is canceled due to re-entering T${this.transaction.id}[${this.transaction.hint}]`), null);
break;
case Reentrance.runSideBySide:
break;
}
}
if (!error)
head.successor = this;
else
this.error = error;
return this;
}
static proceed(footprint, proxy) {
footprint.enter();
try {
if (footprint.options.getter === undefined)
console.log("(!)");
footprint.result = footprint.options.getter.call(proxy, ...footprint.args);
}
finally {
footprint.leaveOrAsync();
}
}
enter() {
if (this.options.indicator)
this.indicatorEnter(this.options.indicator);
if (Log.isOn && Log.opt.operation)
Log.write("║", "‾\\", `${this.hint()} - enter`, undefined, ` [ ${Dump.obj(this.descriptor.ownerHandle, this.descriptor.fieldKey)} ]`);
this.started = Date.now();
}
leaveOrAsync() {
if (this.result instanceof Promise) {
this.result = this.result.then(value => {
this.content = value;
this.leave(false, " ⚐", "- finished ", " OK ──┘");
return value;
}, error => {
this.error = error;
this.leave(false, " ⚐", "- finished ", "ERR ──┘");
throw error;
});
if (Log.isOn) {
if (Log.opt.operation)
Log.write("║", "_/", `${this.hint()} - leave... `, 0, "ASYNC ──┐");
else if (Log.opt.transaction)
Log.write("║", " ", `${this.why()} ...`, 0, "ASYNC");
}
}
else {
this.content = this.result;
this.leave(true, "_/", "- leave");
}
}
leave(main, op, message, highlight = undefined) {
const ms = Date.now() - this.started;
this.started = -this.started;
if (Log.isOn && Log.opt.operation)
Log.write("║", `${op}`, `${this.hint()} ${message}`, ms, highlight);
if (ms > (main ? Mvcc.mainThreadBlockingWarningThreshold : Mvcc.asyncActionDurationWarningThreshold))
Log.write("", "[!]", this.why(), ms, main ? " *** main thread is too busy ***" : " *** async is too long ***");
this.cause = undefined;
if (this.options.indicator)
this.indicatorLeave(this.options.indicator);
}
indicatorEnter(mon) {
const options = {
hint: "Indicator.enter",
isolation: Isolation.disjoinFromOuterAndInnerTransactions,
logging: Log.isOn && Log.opt.indicator ? undefined : Log.global
};
ReactionImpl.proceedWithinGivenLaunch(undefined, Transaction.run, options, IndicatorImpl.enter, mon, this.transaction);
}
indicatorLeave(mon) {
Transaction.outside(() => {
const leave = () => {
const options = {
hint: "Indicator.leave",
isolation: Isolation.disjoinFromOuterAndInnerTransactions,
logging: Log.isOn && Log.opt.indicator ? undefined : Log.DefaultLevel
};
ReactionImpl.proceedWithinGivenLaunch(undefined, Transaction.run, options, IndicatorImpl.leave, mon, this.transaction);
};
this.transaction.whenFinished().then(leave, leave);
});
}
addToDeferredReactiveFunctions() {
OperationFootprintImpl.deferredReactions.push(this);
if (OperationFootprintImpl.deferredReactions.length === 1)
setTimeout(OperationFootprintImpl.processDeferredReactions, 0);
}
static processDeferredReactions() {
const deferred = OperationFootprintImpl.deferredReactions;
OperationFootprintImpl.deferredReactions = [];
for (const x of deferred)
x.relaunchIfNotUpToDate(true, true);
}
static markUsed(footprint, ov, fk, h, kind, weak) {
if (kind !== Kind.transaction) {
const listener = OperationFootprintImpl.current;
if (listener && listener.options.kind !== Kind.transaction &&
listener.transaction === Transaction.current && fk !== Meta.Handle) {
const ctx = Changeset.current();
if (ctx !== ov.changeset)
ctx.bumpBy(ov.changeset.timestamp);
const t = weak ? -1 : ctx.timestamp;
if (!listener.listenTo(footprint, ov, fk, h, t))
listener.markObsoleteDueTo(footprint, fk, h.applied.changeset, h, BOOT_CAUSE, ctx.timestamp, ctx.obsolete);
}
}
}
static markEdited(oldValue, newValue, edited, ov, fk, h) {
edited ? ov.changes.add(fk) : ov.changes.delete(fk);
if (Log.isOn && Log.opt.write)
edited ? Log.write("║", " =", `${Dump.snapshot2(h, ov.changeset, fk)} is changed: ${valueHint(oldValue)} ▸▸ ${valueHint(newValue)}`) : Log.write("║", " =", `${Dump.snapshot2(h, ov.changeset, fk)} is changed: ${valueHint(oldValue)} ▸▸ ${valueHint(newValue)}`, undefined, " (same as previous)");
}
static tryResolveConflict(theirValue, ourFormerValue, ourValue) {
let isResolved = theirValue === ourFormerValue;
let resolvedValue = ourValue;
if (!isResolved) {
if (ourValue instanceof OperationFootprintImpl && ourValue.obsoleteDueTo === undefined) {
isResolved = true;
resolvedValue = ourValue;
}
else if (theirValue instanceof OperationFootprintImpl && (theirValue.obsoleteDueTo === undefined || theirValue.cause === BOOT_CAUSE)) {
isResolved = true;
resolvedValue = theirValue;
}
}
return { isResolved, resolvedValue };
}
static propagateAllChangesToListeners(changeset) {
var _a;
const since = changeset.timestamp;
const obsolete = changeset.obsolete;
changeset.items.forEach((ov, h) => {
OperationFootprintImpl.propagateFieldChangeToListeners(false, since, ov, Meta.Revision, h, obsolete);
if (!ov.disposed)
ov.changes.forEach((o, fk) => OperationFootprintImpl.propagateFieldChangeToListeners(false, since, ov, fk, h, obsolete));
else
for (const fk in ov.former.objectVersion.data)
OperationFootprintImpl.propagateFieldChangeToListeners(true, since, ov, fk, h, obsolete);
});
obsolete.sort(compareReactionsByOrder);
(_a = changeset.options.journal) === null || _a === void 0 ? void 0 : _a.edited(JournalImpl.buildPatch(changeset.hint, changeset.items));
}
static discardAllListeners(changeset) {
changeset.items.forEach((ov, h) => {
OperationFootprintImpl.propagateFieldChangeToListeners(true, changeset.timestamp, ov, Meta.Revision, h, undefined);
ov.changes.forEach((o, fk) => OperationFootprintImpl.propagateFieldChangeToListeners(true, changeset.timestamp, ov, fk, h, undefined));
});
}
static propagateFieldChangeToListeners(stopListening, timestamp, ov, fk, h, collector) {
var _a;
const curr = ov.data[fk];
if (collector !== undefined) {
const former = ov.former.objectVersion.data[fk];
if (former !== undefined && former instanceof ContentFootprint) {
const why = `T${ov.changeset.id}[${ov.changeset.hint}]`;
if (former instanceof OperationFootprintImpl) {
if ((former.obsoleteSince === MAX_REVISION || former.obsoleteSince <= 0)) {
former.obsoleteDueTo = why;
former.obsoleteSince = timestamp;
former.stopListeningAllSignals();
}
const formerSuccessor = former.successor;
if (formerSuccessor !== curr) {
if (formerSuccessor && !formerSuccessor.transaction.isFinished)
formerSuccessor.transaction.cancel(new Error(`T${formerSuccessor.transaction.id}[${formerSuccessor.transaction.hint}] is canceled by T${ov.changeset.id}[${ov.changeset.hint}] and will not run anymore`), null);
}
else
former.successor = undefined;
}
(_a = former.listeners) === null || _a === void 0 ? void 0 : _a.forEach(s => {
const t = s.transaction;
const o = t.isFinished ? collector : t.changeset.obsolete;
return s.markObsoleteDueTo(former, fk, ov.changeset, h, why, timestamp, o);
});
}
}
if (curr instanceof OperationFootprintImpl) {
if (curr.changeset === ov.changeset && curr.signals !== undefined) {
if (Mvcc.repetitiveUsageWarningThreshold < Number.MAX_SAFE_INTEGER) {
curr.signals.forEach((info, v) => {
if (info.usageCount > Mvcc.repetitiveUsageWarningThreshold)
Log.write("", "[!]", `${curr.hint()} uses ${info.memberHint} ${info.usageCount} times (consider remembering it in a local variable)`, 0, " *** WARNING ***");
});
}
if (stopListening)
curr.stopListeningAllSignals();
}
}
else if (curr instanceof ContentFootprint && curr.listeners) {
}
}
static enqueueReactionsToRun(reactions) {
const queue = OperationFootprintImpl.queuedReactions;
const isKickOff = queue.length === 0;
for (const r of reactions)
queue.push(r);
if (isKickOff)
ReactionImpl.proceedWithinGivenLaunch(undefined, OperationFootprintImpl.processQueuedReactions);
}
static migrateContentFootprint(cf, target) {
let result;
if (cf instanceof OperationFootprintImpl)
result = new OperationFootprintImpl(target, cf.descriptor, target.changeset, cf, true);
else
result = new ContentFootprint(cf.content, cf.lastEditorChangesetId);
return result;
}
static processQueuedReactions() {
const queue = OperationFootprintImpl.queuedReactions;
let i = 0;
while (i < queue.length) {
const reactive = queue[i];
reactive.relaunchIfNotUpToDate(false, true);
i++;
}
OperationFootprintImpl.queuedReactions = [];
}
stopListeningAllSignals() {
var _a;
(_a = this.signals) === null || _a === void 0 ? void 0 : _a.forEach((info, value) => {
var _a;
value.listeners.delete(this);
if (Log.isOn && (Log.opt.read || ((_a = this.options.logging) === null || _a === void 0 ? void 0 : _a.read)))
Log.write(Log.opt.transaction && !Changeset.current().sealed ? "║" : " ", "-", `${this.hint()} is no longer listening to ${info.memberHint}`);
});
this.signals = undefined;
}
listenTo(footprint, ov, fk, h, timestamp) {
var _a, _b, _c;
const parent = this.transaction.changeset.parent;
const ok = OperationFootprintImpl.canListenTo(footprint, ov, parent, fk, h, timestamp);
if (ok) {
let times = 0;
if (Mvcc.repetitiveUsageWarningThreshold < Number.MAX_SAFE_INTEGER) {
const existing = this.signals.get(footprint);
times = existing ? existing.usageCount + 1 : 1;
}
if (this.signals !== undefined) {
if (!footprint.listeners)
footprint.listeners = new Set();
const listening = { memberHint: Dump.snapshot2(h, ov.changeset, fk), usageCount: times };
footprint.listeners.add(this);
this.signals.set(footprint, listening);
if (Log.isOn && (Log.opt.read || ((_a = this.options.logging) === null || _a === void 0 ? void 0 : _a.read)))
Log.write("║", " ∞", `${this.hint()} now listens to ${Dump.snapshot2(h, ov.changeset, fk, footprint)}${listening.usageCount > 1 ? ` (${listening.usageCount} times)` : ""}`);
}
else if (Log.isOn && (Log.opt.read || ((_b = this.options.logging) === null || _b === void 0 ? void 0 : _b.read)))
Log.write("║", " x", `${this.hint()} is obsolete and is not listening to ${Dump.snapshot2(h, ov.changeset, fk, footprint)}`);
}
else {
if (Log.isOn && (Log.opt.read || ((_c = this.options.logging) === null || _c === void 0 ? void 0 : _c.read)))
Log.write("║", " x", `${this.hint()} is not listening to already obsolete ${Dump.snapshot2(h, ov.changeset, fk, footprint)}`);
}
return ok;
}
static canListenTo(footprint, ov, parent, fk, h, timestamp) {
const parentSnapshot = parent ? parent.lookupObjectVersion(h, fk, false) : h.applied;
const parentFootprint = parentSnapshot.data[fk];
let result = footprint === parentFootprint || (!ov.changeset.sealed && ov.former.objectVersion.data[fk] === parentFootprint);
if (result && timestamp !== -1)
result = !(footprint instanceof OperationFootprintImpl && timestamp >= footprint.obsoleteSince);
return result;
}
static createOperationDescriptor(h, fk, options) {
const ctl = new ReactionImpl(h, fk);
const operation = (...args) => {
return ctl.reuseOrRelaunch(false, args).result;
};
Meta.set(operation, Meta.Descriptor, ctl);
return operation;
}
static rememberOperationOptions(proto, fk, getter, setter, enumerable, configurable, options, implicit) {
const initial = Meta.acquire(proto, Meta.Initial);
let footprint = initial[fk];
const ctl = footprint ? footprint.descriptor : new ReactionImpl(EMPTY_HANDLE, fk);
const opts = footprint ? footprint.options : OptionsImpl.INITIAL;
initial[fk] = footprint = new OperationFootprintImpl(Transaction.current, ctl, EMPTY_OBJECT_VERSION.changeset, new OptionsImpl(getter, setter, opts, options, implicit), false);
if (footprint.options.kind === Kind.reaction && footprint.options.throttling < Number.MAX_SAFE_INTEGER) {
const reactive = Meta.acquire(proto, Meta.Reactive);
reactive[fk] = footprint;
}
else if (footprint.options.kind === Kind.reaction && footprint.options.throttling >= Number.MAX_SAFE_INTEGER) {
const reactive = Meta.getFrom(proto, Meta.Reactive);
delete reactive[fk];
}
return footprint.options;
}
static init() {
Object.freeze(BOOT_ARGS);
Log.getMergedLoggingOptions = getMergedLoggingOptions;
Dump.valueHint = valueHint;
Changeset.markUsed = OperationFootprintImpl.markUsed;
Changeset.markEdited = OperationFootprintImpl.markEdited;
Changeset.tryResolveConflict = OperationFootprintImpl.tryResolveConflict;
Changeset.propagateAllChangesToListeners = OperationFootprintImpl.propagateAllChangesToListeners;
Changeset.discardAllListeners = OperationFootprintImpl.discardAllListeners;
Changeset.enqueueReactionsToRun = OperationFootprintImpl.enqueueReactionsToRun;
TransactionImpl.migrateContentFootprint = OperationFootprintImpl.migrateContentFootprint;
Mvcc.createOperationDescriptor = OperationFootprintImpl.createOperationDescriptor;
Mvcc.rememberOperationOptions = OperationFootprintImpl.rememberOperationOptions;
Promise.prototype.then = reactronicHookedThen;
try {
Object.defineProperty(globalThis, "rWhy", {
get: ReactionImpl.why, configurable: false, enumerable: false,
});
Object.defineProperty(globalThis, "rBriefWhy", {
get: ReactionImpl.briefWhy, configurable: false, enumerable: false,
});
}
catch (e) {
}
try {
Object.defineProperty(global, "rWhy", {
get: ReactionImpl.why, configurable: false, enumerable: false,
});
Object.defineProperty(global, "rBriefWhy", {
get: ReactionImpl.briefWhy, configurable: false, enumerable: false,
});
}
catch (e) {
}
}
}
OperationFootprintImpl.current = undefined;
OperationFootprintImpl.queuedReactions = [];
OperationFootprintImpl.deferredReactions = [];
function valueHint(value) {
let result = "";
if (Array.isArray(value))
result = `Array(${value.length})`;
else if (value instanceof Set)
result = `Set(${value.size})`;
else if (value instanceof Map)
result = `Map(${value.size})`;
else if (value instanceof OperationFootprintImpl)
result = `#${value.descriptor.ownerHandle.id}t${value.changeset.id}s${value.changeset.timestamp}${value.lastEditorChangesetId !== undefined && value.lastEditorChangesetId !== 0 ? `t${value.lastEditorChangesetId}` : ""}`;
else if (value === Meta.Undefined)
result = "undefined";
else if (typeof (value) === "string")
result = `"${value.toString().slice(0, 20)}${value.length > 20 ? "..." : ""}"`;
else if (value !== undefined && value !== null)
result = value.toString().slice(0, 40);
else
result = "undefined";
return result;
}
function getMergedLoggingOptions(local) {
const t = Transaction.current;
let res = Log.merge(t.options.logging, t.id > 1 ? 31 + t.id % 6 : 37, t.id > 1 ? `T${t.id}` : `-${Changeset.idGen.toString().replace(/[0-9]/g, "-")}`, Log.global);
res = Log.merge({ margin1: t.margin }, undefined, undefined, res);
if (OperationFootprintImpl.current)
res = Log.merge({ margin2: OperationFootprintImpl.current.margin }, undefined, undefined, res);
if (local)
res = Log.merge(local, undefined, undefined, res);
return res;
}
const ORIGINAL_PROMISE_THEN = Promise.prototype.then;
function reactronicHookedThen(resolve, reject) {
const tran = Transaction.current;
if (!tran.isFinished) {
if (!resolve)
resolve = resolveReturn;
if (!reject)
reject = rejectRethrow;
const footprint = OperationFootprintImpl.current;
if (footprint) {
resolve = footprint.wrap(resolve);
reject = footprint.wrap(reject);
}
resolve = tran.wrapAsPending(resolve, false);
reject = tran.wrapAsPending(reject, true);
}
return ORIGINAL_PROMISE_THEN.call(this, resolve, reject);
}
function compareReactionsByOrder(r1, r2) {
return r1.order - r2.order;
}
export function resolveReturn(value) {
return value;
}
export function rejectRethrow(error) {
throw error;
}
OperationFootprintImpl.init();