UNPKG

reactronic

Version:

Reactronic - Transactional Reactive State Management

624 lines (623 loc) 26 kB
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;