UNPKG

reactronic

Version:

Reactronic - Transactional Reactive State Management

659 lines (658 loc) 26.9 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 { 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;