reactronic
Version:
Reactronic - Transactional Reactive State Management
624 lines (623 loc) • 26 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 { MergeList } from "../util/MergeList.js";
import { emitLetters, getCallerInfo, proceedSyncOrAsync } from "../util/Utils.js";
import { Priority, Mode, Isolation, Reentrance } from "../Enums.js";
import { ObservableObject } from "../core/Mvcc.js";
import { Transaction } from "../core/Transaction.js";
import { ReactiveSystem, options, observable, reactive, runAtomically, runNonReactively, manageReactiveOperation, disposeObservableObject } from "../System.js";
export class ReactiveTreeNode {
static get current() {
return ReactiveTreeNodeImpl.nodeSlot.instance;
}
static get isFirstScriptRun() {
return ReactiveTreeNode.current.stamp === 1;
}
static 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 = (getModeUsingBasisChain(declaration) & Mode.rootNode) !== Mode.rootNode ? gNodeSlot === null || gNodeSlot === void 0 ? void 0 : gNodeSlot.instance : undefined;
if (owner) {
let existing = owner.driver.declareChild(owner, driver, declaration, declaration.basis);
const children = owner.children;
existing !== null && existing !== void 0 ? existing : (existing = children.tryMergeAsExisting(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 new Error(`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 (observablesAreEqual(declaration.triggers, exTriggers))
declaration.triggers = exTriggers;
result.declaration = declaration;
}
else {
result = new ReactiveTreeNodeImpl(effectiveKey || generateKey(owner), driver, declaration, owner);
result.slot = children.mergeAsAdded(result);
}
}
else {
result = new ReactiveTreeNodeImpl(effectiveKey || "", driver, declaration, owner);
result.slot = MergeList.createItem(result);
triggerScriptRunViaSlot(result.slot);
}
return result;
}
static withBasis(declaration, basis) {
if (declaration)
declaration.basis = basis;
else
declaration = basis !== null && basis !== void 0 ? basis : {};
return declaration;
}
static triggerScriptRun(node, triggers) {
const impl = node;
const declaration = impl.declaration;
if (!observablesAreEqual(triggers, declaration.triggers)) {
declaration.triggers = triggers;
triggerScriptRunViaSlot(impl.slot);
}
}
static triggerFinalization(node) {
const impl = node;
triggerFinalization(impl.slot, true, true);
}
static runNestedNodeScriptsThenDo(action) {
runNestedNodeScriptsThenDoImpl(ReactiveTreeNodeImpl.nodeSlot, undefined, action);
}
static markAsMounted(node, yes) {
const n = node;
if (n.stamp < 0)
throw new Error("deactivated node cannot be mounted or unmounted");
if (n.stamp >= Number.MAX_SAFE_INTEGER)
throw new Error("node must be activated before mounting");
n.stamp = yes ? 0 : Number.MAX_SAFE_INTEGER - 1;
}
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 ReactiveTreeNodeImpl.logging;
}
static setDefaultLoggingOptions(logging) {
ReactiveTreeNodeImpl.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) {
ReactiveTreeNodeImpl.setTreeVariableValue(this, value);
}
get value() {
return ReactiveTreeNodeImpl.useTreeVariableValue(this);
}
get valueOrUndefined() {
return ReactiveTreeNodeImpl.tryUseTreeVariableValue(this);
}
}
export function generateKey(owner) {
const n = owner.numerator++;
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 ReactiveTreeNodeContextImpl extends ObservableObject {
constructor(variable, value) {
super();
this.next = undefined;
this.variable = variable;
this.value = value;
}
}
__decorate([
observable(false),
__metadata("design:type", Object)
], ReactiveTreeNodeContextImpl.prototype, "next", void 0);
__decorate([
observable(false),
__metadata("design:type", ReactiveTreeVariable)
], ReactiveTreeNodeContextImpl.prototype, "variable", void 0);
class ReactiveTreeNodeImpl 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 MergeList(getNodeKey, true);
this.slot = undefined;
this.stamp = Number.MAX_SAFE_INTEGER;
this.context = undefined;
this.numerator = 0;
this.priority = Priority.realtime;
this.childrenShuffling = false;
ReactiveTreeNodeImpl.grandNodeCount++;
if (this.has(Mode.autonomous))
ReactiveTreeNodeImpl.disposableNodeCount++;
}
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 (getModeUsingBasisChain(this.declaration) & mode) === mode;
}
script(_triggers) {
runScriptNow(this.slot);
}
configureReactivity(options) {
if (this.stamp < Number.MAX_SAFE_INTEGER - 1 || !this.has(Mode.autonomous))
throw new Error("reactronic can be configured only for elements with autonomous mode and only during preparation");
return manageReactiveOperation(this.script).configure(options);
}
static get nodeSlot() {
if (!gNodeSlot)
throw new Error("current element is undefined");
return gNodeSlot;
}
static tryUseTreeVariableValue(variable) {
var _a, _b;
let node = ReactiveTreeNodeImpl.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 = ReactiveTreeNodeImpl.tryUseTreeVariableValue(variable)) !== null && _a !== void 0 ? _a : variable.defaultValue;
if (!result)
throw new Error("unknown node variable");
return result;
}
static setTreeVariableValue(variable, value) {
const node = ReactiveTreeNodeImpl.nodeSlot.instance;
const owner = node.owner;
const hostCtx = runNonReactively(() => { 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;
runAtomically({ isolation: Isolation.joinAsNestedTransaction }, () => {
const ctx = node.context;
if (ctx) {
ctx.variable = variable;
ctx.value = value;
}
else
node.context = new ReactiveTreeNodeContextImpl(variable, value);
});
}
else if (hostCtx)
node.outer = owner;
else
node.outer = owner.outer;
}
}
ReactiveTreeNodeImpl.logging = undefined;
ReactiveTreeNodeImpl.grandNodeCount = 0;
ReactiveTreeNodeImpl.disposableNodeCount = 0;
__decorate([
reactive,
options({
reentrance: Reentrance.cancelAndWaitPrevious,
allowObsoleteToFinish: true,
observableArgs: true,
noSideEffects: false,
}),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], ReactiveTreeNodeImpl.prototype, "script", null);
function getNodeKey(node) {
return node.stamp >= 0 ? node.key : undefined;
}
function runNestedNodeScriptsThenDoImpl(nodeSlot, error, action) {
runInsideContextOfNode(nodeSlot, () => {
var _a;
const owner = nodeSlot.instance;
const children = owner.children;
if (children.isMergeInProgress) {
let promised = undefined;
try {
children.endMerge(error);
for (const child of children.removedItems(true))
triggerFinalization(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)
triggerScriptRunViaSlot(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.manualMount)) {
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, ReactiveTreeNodeImpl.shortFrameDuration / 3)) {
let outerPriority = ReactiveTreeNodeImpl.currentScriptPriority;
ReactiveTreeNodeImpl.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) {
triggerScriptRunViaSlot(child);
if (Transaction.isFrameOver(1, frameDuration)) {
ReactiveTreeNodeImpl.currentScriptPriority = outerPriority;
yield Transaction.requestNextFrame(0);
outerPriority = ReactiveTreeNodeImpl.currentScriptPriority;
ReactiveTreeNodeImpl.currentScriptPriority = priority;
frameDuration = Math.min(4 * frameDuration, Math.min(frameDurationLimit, ReactiveTreeNode.frameDuration));
}
if (Transaction.isCanceled && Transaction.isFrameOver(1, ReactiveTreeNode.shortFrameDuration / 3))
break;
}
}
finally {
ReactiveTreeNodeImpl.currentScriptPriority = outerPriority;
}
}
});
}
function triggerScriptRunViaSlot(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);
manageReactiveOperation(node.script).configure({
order: node.level,
});
});
}
runNonReactively(node.script, node.declaration.triggers);
}
else if (node.owner !== node)
runScriptNow(nodeSlot);
else
runAtomically(() => runScriptNow(nodeSlot));
}
}
function mountOrRemountIfNecessary(node) {
const driver = node.driver;
if (node.stamp === Number.MAX_SAFE_INTEGER) {
runNonReactively(() => {
node.stamp = Number.MAX_SAFE_INTEGER - 1;
driver.runPreparation(node);
if (!node.has(Mode.manualMount)) {
node.stamp = 0;
if (node.host !== node)
driver.runMount(node);
}
});
}
else if (node.isMoved && !node.has(Mode.manualMount) && node.host !== node)
runNonReactively(() => 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.beginMerge();
const driver = node.driver;
result = driver.runScript(node);
result = proceedSyncOrAsync(result, v => { runNestedNodeScriptsThenDoImpl(nodeSlot, undefined, NOP); return v; }, e => { console.log(e); runNestedNodeScriptsThenDoImpl(nodeSlot, e !== null && e !== void 0 ? e : new Error("unknown error"), NOP); });
}
catch (e) {
runNestedNodeScriptsThenDoImpl(nodeSlot, e, NOP);
console.log(`Reactive node script failed: ${node.key}`);
console.log(`${e}`);
}
}
});
}
}
function triggerFinalization(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 = runNonReactively(() => 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)
runAtomically({ isolation: Isolation.disjoinForInternalDisposal, hint: `runDisposalLoop(initiator=${nodeSlot.instance.key})` }, () => {
void runDisposalLoop().then(NOP, error => console.log(error));
});
}
for (const child of node.children.items())
triggerFinalization(child, childrenAreLeaders, false);
ReactiveTreeNodeImpl.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();
disposeObservableObject(slot.instance);
slot = slot.aux;
ReactiveTreeNodeImpl.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 observablesAreEqual(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;