reactronic
Version:
Reactronic - Transactional Reactive State Management
762 lines (761 loc) • 37.5 kB
JavaScript
import { Log, misuse } from "../util/Dbg.js";
import { Kind, Reentrance, Isolation } from "../Options.js";
import { ObjectHandle, FieldVersion, 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.triggering, EMPTY_OBJECT_VERSION, "<boot>");
export class OperationImpl {
configure(options) { return OperationImpl.configureImpl(this, options); }
get options() { return this.peek(undefined).launch.options; }
get nonreactive() { return this.peek(undefined).launch.content; }
get args() { return this.use().launch.args; }
get result() { return this.reuseOrRelaunch(true, undefined).content; }
get error() { return this.use().launch.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()" }, OperationImpl.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 launch = ror.launch;
const opts = launch.options;
if (!ror.isReusable && !ror.objectVersion.disposed
&& (!weak || launch.cause === BOOT_CAUSE || !launch.successor ||
launch.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.launch.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.launch.operation.ownerHandle, ror.changeset, this.fieldKey)} result is reused from T${ror.launch.transaction.id}[${ror.launch.transaction.hint}]`);
const t = ror.launch;
Changeset.markUsed(t, ror.objectVersion, this.fieldKey, this.ownerHandle, t.options.kind, weak);
return t;
}
static getControllerOf(method) {
const ctl = Meta.get(method, Meta.Controller);
if (!ctl)
throw misuse(`given method is not decorated as reactronic one: ${method.name}`);
return ctl;
}
static configureImpl(self, options) {
let launch;
if (self)
launch = self.edit().launch;
else
launch = Launch.current;
if (!launch)
throw misuse("reactronic decorator is only applicable to methods");
launch.options = new OptionsImpl(launch.options.getter, launch.options.setter, launch.options, options, false);
if (Log.isOn && Log.opt.write)
Log.write("║", " =", `${launch.hint()}.options are changed`);
return launch.options;
}
static proceedWithinGivenLaunch(launch, func, ...args) {
let result = undefined;
const outer = Launch.current;
try {
Launch.current = launch;
result = func(...args);
}
catch (e) {
if (launch)
launch.error = e;
throw e;
}
finally {
Launch.current = outer;
}
return result;
}
static why() {
var _a, _b;
return (_b = (_a = Launch.current) === null || _a === void 0 ? void 0 : _a.why()) !== null && _b !== void 0 ? _b : BOOT_CAUSE;
}
static briefWhy() {
var _a, _b;
return (_b = (_a = Launch.current) === null || _a === void 0 ? void 0 : _a.briefWhy()) !== null && _b !== void 0 ? _b : BOOT_CAUSE;
}
static dependencies() {
const l = Launch.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 launch = this.acquireFromObjectVersion(ov, args);
const applied = this.ownerHandle.applied.data[this.fieldKey];
const isReusable = launch.options.kind !== Kind.atomic && launch.cause !== BOOT_CAUSE &&
(ctx === launch.changeset || ctx.timestamp < launch.obsoleteSince || applied.obsoleteDueTo === undefined) &&
(!launch.options.triggeringArgs || args === undefined ||
launch.args.length === args.length && launch.args.every((t, i) => t === args[i])) || ov.disposed;
return { launch, isReusable, changeset: ctx, objectVersion: ov };
}
use() {
const ror = this.peek(undefined);
Changeset.markUsed(ror.launch, ror.objectVersion, this.fieldKey, this.ownerHandle, ror.launch.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 launch = this.acquireFromObjectVersion(ov, undefined);
if (launch.changeset !== ov.changeset) {
const relaunch = new Launch(Transaction.current, this, ov.changeset, launch, false);
ov.data[fk] = relaunch.reenterOver(launch);
ctx.bumpBy(ov.former.objectVersion.changeset.timestamp);
Changeset.markEdited(launch, relaunch, true, ov, fk, h);
launch = relaunch;
}
return { launch, isReusable: true, changeset: ctx, objectVersion: ov };
}
acquireFromObjectVersion(ov, args) {
const fk = this.fieldKey;
let launch = ov.data[fk];
if (launch.operation !== this) {
if (ov.changeset !== EMPTY_OBJECT_VERSION.changeset) {
const hint = Log.isOn ? `${Dump.obj(this.ownerHandle, fk)}/init` : "MethodController/init";
const isolation = Isolation.joinToCurrentTransaction;
launch = Transaction.run({ hint, isolation, token: this }, () => {
const h = this.ownerHandle;
let r = Changeset.current().getObjectVersion(h, fk);
let relaunch = r.data[fk];
if (relaunch.operation !== this) {
r = Changeset.edit().getEditableObjectVersion(h, fk, Meta.Handle, this);
const t = new Launch(Transaction.current, this, r.changeset, relaunch, false);
if (args)
t.args = args;
t.cause = BOOT_CAUSE;
r.data[fk] = t;
Changeset.markEdited(relaunch, t, true, r, fk, h);
relaunch = t;
}
return relaunch;
});
}
else {
const initialLaunch = new Launch(Transaction.current, this, ov.changeset, launch, false);
if (args)
initialLaunch.args = args;
initialLaunch.cause = BOOT_CAUSE;
ov.data[fk] = initialLaunch;
launch = initialLaunch;
if (Log.isOn && Log.opt.write)
Log.write("║", " ++", `${Dump.obj(this.ownerHandle, fk)} is initialized (revision ${ov.revision})`);
}
}
return launch;
}
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.launch.transaction.isCanceled) {
ror = this.edit();
if (Log.isOn && Log.opt.operation)
Log.write("║", " o", `${ror.launch.why()}`);
ror.launch.proceed(this.ownerHandle.proxy, argsx);
}
else {
ror = this.peek(argsx);
if (ror.launch.options.kind === Kind.atomic || !ror.isReusable) {
ror = this.edit();
if (Log.isOn && Log.opt.operation)
Log.write("║", " o", `${ror.launch.why()}`);
ror.launch.proceed(this.ownerHandle.proxy, argsx);
}
}
return ror.launch.result;
}, args);
ror.launch.result = result;
return ror;
}
static markObsolete(self) {
const ror = self.peek(undefined);
const ctx = ror.changeset;
const obsolete = ror.launch.transaction.isFinished ? ctx.obsolete : ror.launch.transaction.changeset.obsolete;
ror.launch.markObsoleteDueTo(ror.launch, self.fieldKey, EMPTY_OBJECT_VERSION.changeset, EMPTY_HANDLE, BOOT_CAUSE, ctx.timestamp, obsolete);
}
}
class Launch extends FieldVersion {
constructor(transaction, operation, changeset, former, clone) {
super(undefined, 0);
this.margin = Launch.current ? Launch.current.margin + 1 : 1;
this.transaction = transaction;
this.operation = operation;
this.changeset = changeset;
this.triggers = new Map();
if (former instanceof Launch) {
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 isLaunch() { return true; }
hint() { return `${Dump.snapshot2(this.operation.ownerHandle, this.changeset, this.operation.fieldKey, this)}`; }
get order() { return this.options.order; }
get ["#this#"]() {
return `Operation: ${this.why()}`;
}
clone(t, cs) {
return new Launch(t, this.operation, cs, this, true);
}
why() {
let cause;
if (this.cause)
cause = ` ◀◀ ${this.cause}`;
else if (this.operation.options.kind === Kind.atomic)
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 = OperationImpl.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)
OperationImpl.proceedWithinGivenLaunch(this, Launch.proceed, this, proxy);
else
this.result = Promise.reject(this.error);
}
markObsoleteDueTo(trigger, fk, changeset, h, outer, since, collector) {
var _a, _b, _c;
if (this.triggers !== undefined) {
const skip = !trigger.isLaunch &&
changeset.id === this.lastEditorChangesetId;
if (!skip) {
const why = `${Dump.snapshot2(h, changeset, fk, trigger)} ◀◀ ${outer}`;
const isReactive = this.options.kind === Kind.reactive;
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.unsubscribeFromAllTriggers();
if (isReactive)
collector.push(this);
else
(_b = this.reactions) === null || _b === void 0 ? void 0 : _b.forEach(s => s.markObsoleteDueTo(this, this.operation.fieldKey, this.changeset, this.operation.ownerHandle, why, since, collector));
const tran = this.transaction;
if (tran.changeset === changeset) {
}
else if (!tran.isFinished && this !== trigger && !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, trigger)}`);
}
}
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 launch = this.operation.reuseOrRelaunch(false, undefined);
if (launch.result instanceof Promise)
launch.result.catch(error => {
if (launch.options.kind === Kind.reactive)
misuse(`reactive function ${launch.hint()} failed and will not run anymore: ${error}`, error);
});
}
catch (e) {
if (!nothrow)
throw e;
else if (this.options.kind === Kind.reactive)
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.atomic ||
!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(launch, proxy) {
launch.enter();
try {
if (launch.options.getter === undefined)
console.log("(!)");
launch.result = launch.options.getter.call(proxy, ...launch.args);
}
finally {
launch.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.operation.ownerHandle, this.operation.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
};
OperationImpl.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
};
OperationImpl.proceedWithinGivenLaunch(undefined, Transaction.run, options, IndicatorImpl.leave, mon, this.transaction);
};
this.transaction.whenFinished().then(leave, leave);
});
}
addToDeferredReactiveFunctions() {
Launch.deferredReactiveOperations.push(this);
if (Launch.deferredReactiveOperations.length === 1)
setTimeout(Launch.processDeferredReactiveFunctions, 0);
}
static processDeferredReactiveFunctions() {
const deferred = Launch.deferredReactiveOperations;
Launch.deferredReactiveOperations = [];
for (const x of deferred)
x.relaunchIfNotUpToDate(true, true);
}
static markUsed(trigger, ov, fk, h, kind, weak) {
if (kind !== Kind.atomic) {
const launch = Launch.current;
if (launch && launch.options.kind !== Kind.atomic &&
launch.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 (!launch.subscribeTo(trigger, ov, fk, h, t))
launch.markObsoleteDueTo(trigger, 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 Launch && ourValue.obsoleteDueTo === undefined) {
isResolved = true;
resolvedValue = ourValue;
}
else if (theirValue instanceof Launch && (theirValue.obsoleteDueTo === undefined || theirValue.cause === BOOT_CAUSE)) {
isResolved = true;
resolvedValue = theirValue;
}
}
return { isResolved, resolvedValue };
}
static propagateAllChangesThroughSubscriptions(changeset) {
var _a;
const since = changeset.timestamp;
const obsolete = changeset.obsolete;
changeset.items.forEach((ov, h) => {
Launch.propagateFieldChangeThroughSubscriptions(false, since, ov, Meta.Revision, h, obsolete);
if (!ov.disposed)
ov.changes.forEach((o, fk) => Launch.propagateFieldChangeThroughSubscriptions(false, since, ov, fk, h, obsolete));
else
for (const fk in ov.former.objectVersion.data)
Launch.propagateFieldChangeThroughSubscriptions(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 revokeAllSubscriptions(changeset) {
changeset.items.forEach((ov, h) => {
Launch.propagateFieldChangeThroughSubscriptions(true, changeset.timestamp, ov, Meta.Revision, h, undefined);
ov.changes.forEach((o, fk) => Launch.propagateFieldChangeThroughSubscriptions(true, changeset.timestamp, ov, fk, h, undefined));
});
}
static propagateFieldChangeThroughSubscriptions(unsubscribe, 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 FieldVersion) {
const why = `T${ov.changeset.id}[${ov.changeset.hint}]`;
if (former instanceof Launch) {
if ((former.obsoleteSince === MAX_REVISION || former.obsoleteSince <= 0)) {
former.obsoleteDueTo = why;
former.obsoleteSince = timestamp;
former.unsubscribeFromAllTriggers();
}
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.reactions) === 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 Launch) {
if (curr.changeset === ov.changeset && curr.triggers !== undefined) {
if (Mvcc.repetitiveUsageWarningThreshold < Number.MAX_SAFE_INTEGER) {
curr.triggers.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 (unsubscribe)
curr.unsubscribeFromAllTriggers();
}
}
else if (curr instanceof FieldVersion && curr.reactions) {
}
}
static enqueueReactionsToRun(reactions) {
const queue = Launch.queuedReactions;
const isKickOff = queue.length === 0;
for (const r of reactions)
queue.push(r);
if (isKickOff)
OperationImpl.proceedWithinGivenLaunch(undefined, Launch.processQueuedReactions);
}
static migrateFieldVersion(fv, target) {
let result;
if (fv instanceof Launch)
result = new Launch(target, fv.operation, target.changeset, fv, true);
else
result = new FieldVersion(fv.content, fv.lastEditorChangesetId);
return result;
}
static processQueuedReactions() {
const queue = Launch.queuedReactions;
let i = 0;
while (i < queue.length) {
const reactive = queue[i];
reactive.relaunchIfNotUpToDate(false, true);
i++;
}
Launch.queuedReactions = [];
}
unsubscribeFromAllTriggers() {
var _a;
(_a = this.triggers) === null || _a === void 0 ? void 0 : _a.forEach((info, value) => {
var _a;
value.reactions.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 unsubscribed from ${info.memberHint}`);
});
this.triggers = undefined;
}
subscribeTo(trigger, ov, fk, h, timestamp) {
var _a, _b, _c;
const parent = this.transaction.changeset.parent;
const ok = Launch.canSubscribeTo(trigger, ov, parent, fk, h, timestamp);
if (ok) {
let times = 0;
if (Mvcc.repetitiveUsageWarningThreshold < Number.MAX_SAFE_INTEGER) {
const existing = this.triggers.get(trigger);
times = existing ? existing.usageCount + 1 : 1;
}
if (this.triggers !== undefined) {
if (!trigger.reactions)
trigger.reactions = new Set();
const subscription = { memberHint: Dump.snapshot2(h, ov.changeset, fk), usageCount: times };
trigger.reactions.add(this);
this.triggers.set(trigger, subscription);
if (Log.isOn && (Log.opt.read || ((_a = this.options.logging) === null || _a === void 0 ? void 0 : _a.read)))
Log.write("║", " ∞", `${this.hint()} is subscribed to ${Dump.snapshot2(h, ov.changeset, fk, trigger)}${subscription.usageCount > 1 ? ` (${subscription.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 subscribed to ${Dump.snapshot2(h, ov.changeset, fk, trigger)}`);
}
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 subscribed to already obsolete ${Dump.snapshot2(h, ov.changeset, fk, trigger)}`);
}
return ok;
}
static canSubscribeTo(trigger, ov, parent, fk, h, timestamp) {
const parentSnapshot = parent ? parent.lookupObjectVersion(h, fk, false) : h.applied;
const parentTrigger = parentSnapshot.data[fk];
let result = trigger === parentTrigger || (!ov.changeset.sealed && ov.former.objectVersion.data[fk] === parentTrigger);
if (result && timestamp !== -1)
result = !(trigger instanceof Launch && timestamp >= trigger.obsoleteSince);
return result;
}
static createOperation(h, fk, options) {
const rx = new OperationImpl(h, fk);
const operation = (...args) => {
return rx.reuseOrRelaunch(false, args).result;
};
Meta.set(operation, Meta.Controller, rx);
return operation;
}
static rememberOperationOptions(proto, fk, getter, setter, enumerable, configurable, options, implicit) {
const initial = Meta.acquire(proto, Meta.Initial);
let launch = initial[fk];
const rx = launch ? launch.operation : new OperationImpl(EMPTY_HANDLE, fk);
const opts = launch ? launch.options : OptionsImpl.INITIAL;
initial[fk] = launch = new Launch(Transaction.current, rx, EMPTY_OBJECT_VERSION.changeset, new OptionsImpl(getter, setter, opts, options, implicit), false);
if (launch.options.kind === Kind.reactive && launch.options.throttling < Number.MAX_SAFE_INTEGER) {
const reactive = Meta.acquire(proto, Meta.Reactive);
reactive[fk] = launch;
}
else if (launch.options.kind === Kind.reactive && launch.options.throttling >= Number.MAX_SAFE_INTEGER) {
const reactive = Meta.getFrom(proto, Meta.Reactive);
delete reactive[fk];
}
return launch.options;
}
static init() {
Object.freeze(BOOT_ARGS);
Log.getMergedLoggingOptions = getMergedLoggingOptions;
Dump.valueHint = valueHint;
Changeset.markUsed = Launch.markUsed;
Changeset.markEdited = Launch.markEdited;
Changeset.tryResolveConflict = Launch.tryResolveConflict;
Changeset.propagateAllChangesThroughSubscriptions = Launch.propagateAllChangesThroughSubscriptions;
Changeset.revokeAllSubscriptions = Launch.revokeAllSubscriptions;
Changeset.enqueueReactionsToRun = Launch.enqueueReactionsToRun;
TransactionImpl.migrateFieldVersion = Launch.migrateFieldVersion;
Mvcc.createOperation = Launch.createOperation;
Mvcc.rememberOperationOptions = Launch.rememberOperationOptions;
Promise.prototype.then = reactronicHookedThen;
try {
Object.defineProperty(globalThis, "rWhy", {
get: OperationImpl.why, configurable: false, enumerable: false,
});
Object.defineProperty(globalThis, "rBriefWhy", {
get: OperationImpl.briefWhy, configurable: false, enumerable: false,
});
}
catch (e) {
}
try {
Object.defineProperty(global, "rWhy", {
get: OperationImpl.why, configurable: false, enumerable: false,
});
Object.defineProperty(global, "rBriefWhy", {
get: OperationImpl.briefWhy, configurable: false, enumerable: false,
});
}
catch (e) {
}
}
}
Launch.current = undefined;
Launch.queuedReactions = [];
Launch.deferredReactiveOperations = [];
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 Launch)
result = `#${value.operation.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 (Launch.current)
res = Log.merge({ margin2: Launch.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 launch = Launch.current;
if (launch) {
resolve = launch.wrap(resolve);
reject = launch.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;
}
Launch.init();