reactronic
Version:
Reactronic - Transactional Reactive State Management
659 lines (658 loc) • 26.9 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
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 { misuse } from "../util/Dbg.js";
import { Uri } from "../util/Uri.js";
import { ReconciliationList } from "../util/ReconciliationList.js";
import { emitLetters, flags, getCallerInfo, proceedSyncOrAsync } from "../util/Utils.js";
import { Priority, Mode, Isolation, Reentrance } from "../Enums.js";
import { SxObject } from "../core/Mvcc.js";
import { Transaction } from "../core/Transaction.js";
import { ReactiveSystem, options, signal, reaction, runTransactional, runNonReactive, manageReaction, disposeSignallingObject } from "../System.js";
export function declare(driver, scriptOrDeclaration, scriptAsync, key, mode, preparation, preparationAsync, finalization, triggers, basis) {
let result;
let declaration;
if (scriptOrDeclaration instanceof Function) {
declaration = {
script: scriptOrDeclaration, scriptAsync, key, mode,
preparation, preparationAsync, finalization, triggers, basis,
};
}
else
declaration = scriptOrDeclaration !== null && scriptOrDeclaration !== void 0 ? scriptOrDeclaration : {};
let effectiveKey = declaration.key;
const owner = gNodeSlot === null || gNodeSlot === void 0 ? void 0 : gNodeSlot.instance;
if (owner) {
let existing = owner.driver.declareChild(owner, driver, declaration, declaration.basis);
const children = owner.children;
existing !== null && existing !== void 0 ? existing : (existing = children.tryReuse(effectiveKey = effectiveKey || generateKey(owner), undefined, "nested elements can be declared inside 'script' only"));
if (existing) {
result = existing.instance;
if (result.driver !== driver && driver !== undefined)
throw misuse(`changing element driver is not yet supported: "${result.driver.name}" -> "${driver === null || driver === void 0 ? void 0 : driver.name}"`);
const exTriggers = result.declaration.triggers;
if (signalsAreEqual(declaration.triggers, exTriggers))
declaration.triggers = exTriggers;
result.declaration = declaration;
}
else {
result = new ReactiveTreeNode$(effectiveKey || generateKey(owner), driver, declaration, owner);
result.slot = children.add(result);
}
}
else {
result = new ReactiveTreeNode$(effectiveKey || generateKey(owner), driver, declaration, owner);
result.slot = ReconciliationList.createItem(result);
}
return result;
}
export function derivative(declaration, basis) {
if (declaration)
declaration.basis = basis;
else
declaration = basis !== null && basis !== void 0 ? basis : {};
return declaration;
}
export function launch(node, triggers) {
ReactiveTreeNode.launchScript(node, triggers);
return node;
}
export class ReactiveTreeNode {
static get current() {
return ReactiveTreeNode$.nodeSlot.instance;
}
static get isFirstScriptRun() {
return ReactiveTreeNode.current.stamp === 1;
}
static launchScript(node, triggers) {
const impl = node;
const declaration = impl.declaration;
if (node.stamp >= Number.MAX_SAFE_INTEGER || !signalsAreEqual(triggers, declaration.triggers)) {
declaration.triggers = triggers;
launchScriptViaSlot(impl.slot);
}
}
static launchFinalization(node) {
const impl = node;
launchFinalizationViaSlot(impl.slot, true, true);
}
static launchNestedNodesThenDo(action) {
launchNestedNodesThenDoImpl(ReactiveTreeNode$.nodeSlot, undefined, action);
}
static markAsMounted(node, yes) {
const n = node;
if (n.stamp < 0)
throw misuse("deactivated node cannot be mounted or unmounted");
if (n.stamp >= Number.MAX_SAFE_INTEGER)
throw misuse("node must be activated before mounting");
n.stamp = yes ? 0 : Number.MAX_SAFE_INTEGER - 1;
}
lookupTreeNodeByUri(uri) {
var _a;
const t = Uri.parse(uri);
if (t.authority !== this.key)
throw misuse(`authority '${t.authority}' doesn't match root node key '${this.key}'`);
const segments = t.path.split("/");
let result = this;
for (let i = 1; i < segments.length && result !== undefined; i++)
result = (_a = result.children.lookup(segments[i])) === null || _a === void 0 ? void 0 : _a.instance;
return result;
}
static findMatchingHost(node, match) {
let p = node.host;
while (p !== p.host && !match(p))
p = p.host;
return p;
}
static findMatchingPrevSibling(node, match) {
let p = node.slot.prev;
while (p && !match(p.instance))
p = p.prev;
return p === null || p === void 0 ? void 0 : p.instance;
}
static forEachChildRecursively(node, action) {
action(node);
for (const child of node.children.items())
ReactiveTreeNode.forEachChildRecursively(child.instance, action);
}
static getDefaultLoggingOptions() {
return ReactiveTreeNode$.logging;
}
static setDefaultLoggingOptions(logging) {
ReactiveTreeNode$.logging = logging;
}
}
ReactiveTreeNode.shortFrameDuration = 16;
ReactiveTreeNode.longFrameDuration = 300;
ReactiveTreeNode.frameDuration = ReactiveTreeNode.longFrameDuration;
ReactiveTreeNode.currentScriptPriority = Priority.realtime;
export class BaseDriver {
constructor(name, isPartition, initialize) {
this.name = name;
this.isPartition = isPartition;
this.initialize = initialize;
}
runPreparation(node) {
var _a;
(_a = this.initialize) === null || _a === void 0 ? void 0 : _a.call(this, node.element);
return invokePreparationUsingBasisChain(node.element, node.declaration);
}
runFinalization(node, isLeader) {
invokeFinalizationUsingBasisChain(node.element, node.declaration);
return isLeader;
}
runMount(node) {
}
runScript(node) {
return invokeScriptUsingBasisChain(node.element, node.declaration);
}
declareChild(ownerNode, childDriver, childDeclaration, childBasis) {
return undefined;
}
provideHost(node) {
return node;
}
}
export class ReactiveTreeVariable {
constructor(defaultValue) {
this.defaultValue = defaultValue;
}
set value(value) {
ReactiveTreeNode$.setTreeVariableValue(this, value);
}
get value() {
return ReactiveTreeNode$.useTreeVariableValue(this);
}
get valueOrUndefined() {
return ReactiveTreeNode$.tryUseTreeVariableValue(this);
}
}
export function generateKey(owner) {
const n = owner !== undefined ? owner.numerator++ : 0;
const lettered = emitLetters(n);
let result;
if (ReactiveSystem.isLogging)
result = `·${getCallerInfo(lettered)}`;
else
result = `·${lettered}`;
return result;
}
export function getModeUsingBasisChain(declaration) {
var _a;
return (_a = declaration === null || declaration === void 0 ? void 0 : declaration.mode) !== null && _a !== void 0 ? _a : ((declaration === null || declaration === void 0 ? void 0 : declaration.basis) ? getModeUsingBasisChain(declaration === null || declaration === void 0 ? void 0 : declaration.basis) : Mode.default);
}
function invokeScriptUsingBasisChain(element, declaration) {
let result = undefined;
const basis = declaration.basis;
const script = declaration.script;
const scriptAsync = declaration.scriptAsync;
if (script && scriptAsync)
throw misuse("'script' and 'scriptAsync' cannot be defined together");
if (script)
result = script(element, basis ? () => invokeScriptUsingBasisChain(element, basis) : NOP);
else if (scriptAsync)
result = scriptAsync(element, basis ? () => invokeScriptUsingBasisChain(element, basis) : NOP_ASYNC);
else if (basis)
result = invokeScriptUsingBasisChain(element, basis);
return result;
}
function invokePreparationUsingBasisChain(element, declaration) {
let result = undefined;
const basis = declaration.basis;
const preparation = declaration.preparation;
const preparationAsync = declaration.preparationAsync;
if (preparation && preparationAsync)
throw misuse("'preparation' and 'preparationAsync' cannot be defined together");
if (preparation)
result = preparation(element, basis ? () => invokePreparationUsingBasisChain(element, basis) : NOP);
else if (preparationAsync)
result = preparationAsync(element, basis ? () => invokePreparationUsingBasisChain(element, basis) : NOP_ASYNC);
else if (basis)
result = invokePreparationUsingBasisChain(element, basis);
return result;
}
function invokeFinalizationUsingBasisChain(element, declaration) {
const basis = declaration.basis;
const finalization = declaration.finalization;
if (finalization)
finalization(element, basis ? () => invokeFinalizationUsingBasisChain(element, basis) : NOP);
else if (basis)
invokeFinalizationUsingBasisChain(element, basis);
}
class ReactiveTreeNodeContext$ extends SxObject {
constructor(variable, value) {
super();
this.next = undefined;
this.variable = variable;
this.value = value;
}
}
__decorate([
signal(false),
__metadata("design:type", Object)
], ReactiveTreeNodeContext$.prototype, "next", void 0);
__decorate([
signal(false),
__metadata("design:type", ReactiveTreeVariable)
], ReactiveTreeNodeContext$.prototype, "variable", void 0);
class ReactiveTreeNode$ extends ReactiveTreeNode {
constructor(key, driver, declaration, owner) {
super();
const thisAsUnknown = this;
this.key = key;
this.driver = driver;
this.declaration = declaration;
if (owner) {
const node = owner;
this.level = node.level + 1;
this.owner = owner;
this.outer = node.context ? owner : node.outer;
}
else {
this.level = 1;
this.owner = owner = thisAsUnknown;
this.outer = thisAsUnknown;
}
this.element = driver.create(this);
this.host = thisAsUnknown;
this.children = new ReconciliationList(getNodeKey, true);
this.slot = undefined;
this.stamp = Number.MAX_SAFE_INTEGER;
this.context = undefined;
this.numerator = 0;
this.priority = Priority.realtime;
this.childrenShuffling = false;
ReactiveTreeNode$.grandNodeCount++;
if (this.has(Mode.autonomous))
ReactiveTreeNode$.disposableNodeCount++;
}
getUri(relativeTo) {
const path = [];
const authority = gatherAuthorityAndPath(this, path);
const result = Uri.from({
scheme: "node",
authority,
path: "/" + path.join("/"),
});
return result.toString();
}
get strictOrder() {
return this.children.isStrict;
}
set strictOrder(value) {
this.children.isStrict = value;
}
get isMoved() {
return this.owner.children.isMoved(this.slot);
}
has(mode) {
return flags(getModeUsingBasisChain(this.declaration), mode);
}
script(_triggers) {
runScriptNow(this.slot);
}
configureReactivity(options) {
if (this.stamp < Number.MAX_SAFE_INTEGER - 1 || !this.has(Mode.autonomous))
throw misuse("reactronic can be configured only for elements with autonomous mode and only during preparation");
return manageReaction(this.script).configure(options);
}
static get nodeSlot() {
if (!gNodeSlot)
throw misuse("current element is undefined");
return gNodeSlot;
}
static tryUseTreeVariableValue(variable) {
var _a, _b;
let node = ReactiveTreeNode$.nodeSlot.instance;
while (((_a = node.context) === null || _a === void 0 ? void 0 : _a.variable) !== variable && node.owner !== node)
node = node.outer.slot.instance;
return (_b = node.context) === null || _b === void 0 ? void 0 : _b.value;
}
static useTreeVariableValue(variable) {
var _a;
const result = (_a = ReactiveTreeNode$.tryUseTreeVariableValue(variable)) !== null && _a !== void 0 ? _a : variable.defaultValue;
if (!result)
throw misuse("unknown node variable");
return result;
}
static setTreeVariableValue(variable, value) {
const node = ReactiveTreeNode$.nodeSlot.instance;
const owner = node.owner;
const hostCtx = runNonReactive(() => { var _a; return (_a = owner.context) === null || _a === void 0 ? void 0 : _a.value; });
if (value && value !== hostCtx) {
if (hostCtx)
node.outer = owner;
else
node.outer = owner.outer;
runTransactional({ isolation: Isolation.joinAsNestedTransaction }, () => {
const ctx = node.context;
if (ctx) {
ctx.variable = variable;
ctx.value = value;
}
else
node.context = new ReactiveTreeNodeContext$(variable, value);
});
}
else if (hostCtx)
node.outer = owner;
else
node.outer = owner.outer;
}
}
ReactiveTreeNode$.logging = undefined;
ReactiveTreeNode$.grandNodeCount = 0;
ReactiveTreeNode$.disposableNodeCount = 0;
__decorate([
reaction,
options({
reentrance: Reentrance.cancelAndWaitPrevious,
allowObsoleteToFinish: true,
signalArgs: true,
noSideEffects: false,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], ReactiveTreeNode$.prototype, "script", null);
function gatherAuthorityAndPath(node, path, relativeTo) {
let authority;
if (node.owner !== node && node.owner !== relativeTo) {
authority = gatherAuthorityAndPath(node.owner, path);
path.push(node.key);
}
else
authority = node.key;
return authority;
}
function getNodeKey(node) {
return node.stamp >= 0 ? node.key : undefined;
}
function launchNestedNodesThenDoImpl(nodeSlot, error, action) {
runInsideContextOfNode(nodeSlot, () => {
var _a;
const owner = nodeSlot.instance;
const children = owner.children;
if (children.isReconciliationInProgress) {
let promised = undefined;
try {
children.endReconciliation(error);
for (const child of children.itemsRemoved(true))
launchFinalizationViaSlot(child, true, true);
if (!error) {
const sequential = children.isStrict;
let p1 = undefined;
let p2 = undefined;
let mounting = false;
let partition = owner;
for (const child of children.items()) {
if (Transaction.isCanceled)
break;
const childNode = child.instance;
const isPart = childNode.driver.isPartition;
const host = isPart ? owner : partition;
mounting = markToMountIfNecessary(mounting, host, child, children, sequential);
const p = (_a = childNode.priority) !== null && _a !== void 0 ? _a : Priority.realtime;
if (p === Priority.realtime)
launchScriptViaSlot(child);
else if (p === Priority.normal)
p1 = push(child, p1);
else
p2 = push(child, p2);
if (isPart)
partition = childNode;
}
if (!Transaction.isCanceled && (p1 !== undefined || p2 !== undefined))
promised = startIncrementalNestedScriptsRun(nodeSlot, children, p1, p2).then(() => action(error), e => action(e));
}
}
finally {
if (!promised)
action(error);
}
}
});
}
function markToMountIfNecessary(mounting, host, nodeSlot, children, sequential) {
const node = nodeSlot.instance;
if (node.element.native && !node.has(Mode.external)) {
if (mounting || node.host !== host) {
children.markAsMoved(nodeSlot);
mounting = false;
}
}
else if (sequential && children.isMoved(nodeSlot))
mounting = true;
node.host = host;
return mounting;
}
function startIncrementalNestedScriptsRun(ownerSlot, allChildren, priority1, priority2) {
return __awaiter(this, void 0, void 0, function* () {
const stamp = ownerSlot.instance.stamp;
if (priority1)
yield runNestedScriptsIncrementally(ownerSlot, stamp, allChildren, priority1, Priority.normal);
if (priority2)
yield runNestedScriptsIncrementally(ownerSlot, stamp, allChildren, priority2, Priority.background);
});
}
function runNestedScriptsIncrementally(owner, stamp, allChildren, items, priority) {
return __awaiter(this, void 0, void 0, function* () {
yield Transaction.requestNextFrame();
const node = owner.instance;
if (!Transaction.isCanceled || !Transaction.isFrameOver(1, ReactiveTreeNode$.shortFrameDuration / 3)) {
let outerPriority = ReactiveTreeNode$.currentScriptPriority;
ReactiveTreeNode$.currentScriptPriority = priority;
try {
if (node.childrenShuffling)
shuffle(items);
const frameDurationLimit = priority === Priority.background ? ReactiveTreeNode.shortFrameDuration : Infinity;
let frameDuration = Math.min(frameDurationLimit, Math.max(ReactiveTreeNode.frameDuration / 4, ReactiveTreeNode.shortFrameDuration));
for (const child of items) {
launchScriptViaSlot(child);
if (Transaction.isFrameOver(1, frameDuration)) {
ReactiveTreeNode$.currentScriptPriority = outerPriority;
yield Transaction.requestNextFrame(0);
outerPriority = ReactiveTreeNode$.currentScriptPriority;
ReactiveTreeNode$.currentScriptPriority = priority;
frameDuration = Math.min(4 * frameDuration, Math.min(frameDurationLimit, ReactiveTreeNode.frameDuration));
}
if (Transaction.isCanceled && Transaction.isFrameOver(1, ReactiveTreeNode.shortFrameDuration / 3))
break;
}
}
finally {
ReactiveTreeNode$.currentScriptPriority = outerPriority;
}
}
});
}
function launchScriptViaSlot(nodeSlot) {
const node = nodeSlot.instance;
if (node.stamp >= 0) {
if (node.has(Mode.autonomous)) {
if (node.stamp === Number.MAX_SAFE_INTEGER) {
Transaction.outside(() => {
if (ReactiveSystem.isLogging)
ReactiveSystem.setLoggingHint(node.element, node.key);
manageReaction(node.script).configure({
order: node.level,
});
});
}
runNonReactive(node.script, node.declaration.triggers);
}
else if (node.owner !== node)
runScriptNow(nodeSlot);
else
runTransactional(() => runScriptNow(nodeSlot));
}
}
function mountOrRemountIfNecessary(node) {
const driver = node.driver;
if (node.stamp === Number.MAX_SAFE_INTEGER) {
runNonReactive(() => {
node.stamp = Number.MAX_SAFE_INTEGER - 1;
driver.runPreparation(node);
if (!node.has(Mode.external)) {
node.stamp = 0;
if (node.host !== node)
driver.runMount(node);
}
});
}
else if (node.isMoved && !node.has(Mode.external) && node.host !== node)
runNonReactive(() => driver.runMount(node));
}
function runScriptNow(nodeSlot) {
const node = nodeSlot.instance;
if (node.stamp >= 0) {
let result = undefined;
runInsideContextOfNode(nodeSlot, () => {
mountOrRemountIfNecessary(node);
if (node.stamp < Number.MAX_SAFE_INTEGER - 1) {
try {
node.stamp++;
node.numerator = 0;
node.children.beginReconciliation();
const driver = node.driver;
result = driver.runScript(node);
result = proceedSyncOrAsync(result, v => { launchNestedNodesThenDoImpl(nodeSlot, undefined, NOP); return v; }, e => { console.log(e); launchNestedNodesThenDoImpl(nodeSlot, e !== null && e !== void 0 ? e : new Error("unknown error"), NOP); });
}
catch (e) {
launchNestedNodesThenDoImpl(nodeSlot, e, NOP);
console.log(`Reactive node script failed: ${node.key}`);
console.log(`${e}`);
}
}
});
}
}
function launchFinalizationViaSlot(nodeSlot, isLeader, individual) {
const node = nodeSlot.instance;
if (node.stamp >= 0) {
const driver = node.driver;
if (individual && node.key !== node.declaration.key && !driver.isPartition)
console.log(`WARNING: it is recommended to assign explicit key for conditional element in order to avoid unexpected side effects: ${node.key}`);
node.stamp = ~node.stamp;
const childrenAreLeaders = runNonReactive(() => driver.runFinalization(node, isLeader));
if (node.has(Mode.autonomous)) {
nodeSlot.aux = undefined;
const last = gLastToDispose;
if (last)
gLastToDispose = last.aux = nodeSlot;
else
gFirstToDispose = gLastToDispose = nodeSlot;
if (gFirstToDispose === nodeSlot)
runTransactional({ isolation: Isolation.disjoinForInternalDisposal, hint: `runDisposalLoop(initiator=${nodeSlot.instance.key})` }, () => {
void runDisposalLoop().then(NOP, error => console.log(error));
});
}
for (const child of node.children.items())
launchFinalizationViaSlot(child, childrenAreLeaders, false);
ReactiveTreeNode$.grandNodeCount--;
}
}
function runDisposalLoop() {
return __awaiter(this, void 0, void 0, function* () {
yield Transaction.requestNextFrame();
let slot = gFirstToDispose;
while (slot !== undefined) {
if (Transaction.isFrameOver(500, 5))
yield Transaction.requestNextFrame();
disposeSignallingObject(slot.instance);
slot = slot.aux;
ReactiveTreeNode$.disposableNodeCount--;
}
gFirstToDispose = gLastToDispose = undefined;
});
}
function wrapToRunInside(func) {
let wrappedToRunInside;
const outer = gNodeSlot;
if (outer)
wrappedToRunInside = (...args) => {
return runInsideContextOfNode(outer, func, ...args);
};
else
wrappedToRunInside = func;
return wrappedToRunInside;
}
function runInsideContextOfNode(nodeSlot, func, ...args) {
const outer = gNodeSlot;
try {
gNodeSlot = nodeSlot;
return func(...args);
}
finally {
gNodeSlot = outer;
}
}
export function signalsAreEqual(a1, a2) {
let result = a1 === a2;
if (!result) {
if (Array.isArray(a1)) {
result = Array.isArray(a2) &&
a1.length === a2.length &&
a1.every((t, i) => t === a2[i]);
}
else if (a1 === Object(a1) && a2 === Object(a2)) {
for (const p in a1) {
result = a1[p] === a2[p];
if (!result)
break;
}
}
}
return result;
}
function push(item, array) {
if (array == undefined)
array = new Array();
array.push(item);
return array;
}
function shuffle(array) {
const n = array.length - 1;
let i = n;
while (i >= 0) {
const j = Math.floor(Math.random() * n);
const t = array[i];
array[i] = array[j];
array[j] = t;
i--;
}
return array;
}
const ORIGINAL_PROMISE_THEN = Promise.prototype.then;
function reactronicDomHookedThen(resolve, reject) {
resolve = resolve ? wrapToRunInside(resolve) : defaultResolve;
reject = reject ? wrapToRunInside(reject) : defaultReject;
return ORIGINAL_PROMISE_THEN.call(this, resolve, reject);
}
function defaultResolve(value) {
return value;
}
function defaultReject(error) {
throw error;
}
Promise.prototype.then = reactronicDomHookedThen;
const NOP = (...args) => { };
const NOP_ASYNC = (...args) => __awaiter(void 0, void 0, void 0, function* () { });
let gNodeSlot = undefined;
let gFirstToDispose = undefined;
let gLastToDispose = undefined;