UNPKG

reactronic

Version:

Reactronic - Transactional Reactive State Management

654 lines (653 loc) 26.5 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 __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 { LinkedList, LinkedItem, Mark, LinkedSubList } from "../util/LinkedList.js"; import { emitLetters, flags, flagsAny, getCallerInfo, proceedSyncOrAsync } from "../util/Utils.js"; import { Priority, Mode, Isolation, Reentrance } from "../Enums.js"; import { RxObject } from "../core/Mvcc.js"; import { Transaction } from "../core/Transaction.js"; import { ReactiveSystem, options, signal, reaction, runTransactional, runNonReactive, manageReaction, disposeRxObject } from "../System.js"; export function declare(driver, bodyOrDeclaration, bodyTask, key, mode, unmounted, preparation, preparationTask, mounting, finalization, signalArgs, basis) { let result; let declaration; if (bodyOrDeclaration instanceof Function) { declaration = { body: bodyOrDeclaration, bodyTask, key, mode, preparation, preparationTask, finalization, signalArgs, basis, }; } else declaration = bodyOrDeclaration !== null && bodyOrDeclaration !== void 0 ? bodyOrDeclaration : {}; let effectiveKey = declaration.key; const owner = gCurrentNode; if (owner) { let existing = owner.driver.declareChild(owner, driver, declaration, declaration.basis); const renovation = owner.children.renovation; existing !== null && existing !== void 0 ? existing : (existing = renovation.tryReaffirm(effectiveKey = effectiveKey || generateKey(owner), undefined, "nested elements can be declared inside 'body' only")); if (existing) { result = existing; 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 exSignalArgs = result.declaration.signalArgs; if (signalsAreEqual(declaration.signalArgs, exSignalArgs)) declaration.signalArgs = exSignalArgs; result.declaration = declaration; } else { result = new ReactiveTreeNode$(effectiveKey || generateKey(owner), driver, declaration, owner); renovation.thisIsAdded(result); } } else { result = new ReactiveTreeNode$(effectiveKey || generateKey(owner), driver, declaration, owner); ReactiveTreeNode.rebuildBody(result, signalArgs); } return result; } export function setBasis(declaration, basis) { if (declaration) declaration.basis = basis; else declaration = basis !== null && basis !== void 0 ? basis : {}; return declaration; } export function launch(func, ...args) { return Transaction.isolate(runInsideContextOfNode, undefined, func, ...args); } export class ReactiveTreeNode extends LinkedItem { static get current() { return ReactiveTreeNode$.node; } static get isFirstBodyBuild() { return ReactiveTreeNode.current.stamp === 1; } static rebuildBody(node, signalArgs) { const impl = node; const declaration = impl.declaration; if (node.stamp >= Number.MAX_SAFE_INTEGER || !signalsAreEqual(signalArgs, declaration.signalArgs)) { declaration.signalArgs = signalArgs; rebuildBodyImpl(impl); } } static launchFinalization(node) { const impl = node; launchFinalizationImpl(impl, true, true); } static launchNestedNodesThenDo(action) { launchNestedNodesThenDoImpl(ReactiveTreeNode$.node, 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) { 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 = result.children.lookup(segments[i]); 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.prev; while (p && !match(p)) p = p.prev; return p; } static forEachChildRecursively(node, action) { action(node); for (const child of node.children.items()) ReactiveTreeNode.forEachChildRecursively(child, action); } static getDefaultLoggingOptions() { return ReactiveTreeNode$.logging; } static setDefaultLoggingOptions(logging) { ReactiveTreeNode$.logging = logging; } } ReactiveTreeNode.shortFrameDuration = 16; ReactiveTreeNode.longFrameDuration = 300; ReactiveTreeNode.frameDuration = ReactiveTreeNode.longFrameDuration; ReactiveTreeNode.currentBodyPriority = 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) { invokeMountingUsingBasisChain(node.element, node.declaration); } rebuildBody(node) { return invokeBodyUsingBasisChain(node.element, node.declaration); } declareChild(ownerNode, childDriver, childDeclaration, childBasis) { return undefined; } provideHost(node) { return node; } } export class ReactiveTreeVariable { constructor(hint, defaultValue) { this.hint = hint; 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 invokeBodyUsingBasisChain(element, declaration) { let result = undefined; const basis = declaration.basis; const body = declaration.body; const bodyTask = declaration.bodyTask; if (body && bodyTask) throw misuse("'body' and 'bodyTask' cannot be defined together"); if (body) result = body.call(element, element, basis ? () => invokeBodyUsingBasisChain(element, basis) : NOP); else if (bodyTask) result = bodyTask.call(element, element, basis ? () => invokeBodyUsingBasisChain(element, basis) : NOP_ASYNC); else if (basis) result = invokeBodyUsingBasisChain(element, basis); return result; } function invokePreparationUsingBasisChain(element, declaration) { let result = undefined; const basis = declaration.basis; const preparation = declaration.preparation; const preparationTask = declaration.preparationTask; if (preparation && preparationTask) throw misuse("'preparation' and 'preparationTask' cannot be defined together"); if (preparation) result = preparation.call(element, element, basis ? () => invokePreparationUsingBasisChain(element, basis) : NOP); else if (preparationTask) result = preparationTask.call(element, element, basis ? () => invokePreparationUsingBasisChain(element, basis) : NOP_ASYNC); else if (basis) result = invokePreparationUsingBasisChain(element, basis); return result; } function invokeMountingUsingBasisChain(element, declaration) { const basis = declaration.basis; const mounting = declaration.mounting; if (mounting) mounting.call(element, element, basis ? () => invokeMountingUsingBasisChain(element, basis) : NOP); else if (basis) invokeMountingUsingBasisChain(element, basis); } function invokeFinalizationUsingBasisChain(element, declaration) { const basis = declaration.basis; const finalization = declaration.finalization; if (finalization) finalization.call(element, element, basis ? () => invokeFinalizationUsingBasisChain(element, basis) : NOP); else if (basis) invokeFinalizationUsingBasisChain(element, basis); } class ReactiveTreeNodeContext$ extends RxObject { constructor(variable, value) { super(); this.next = undefined; this.variable = variable; this.value = value; } } __decorate([ signal(false) ], ReactiveTreeNodeContext$.prototype, "next", void 0); __decorate([ signal(false) ], 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 LinkedList(getNodeKey, true); 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.primitive)) 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.isStrictOrder; } set strictOrder(value) { this.children.isStrictOrder = value; } get isMoved() { return this.mark === Mark.moved; } has(mode) { return flags(getModeUsingBasisChain(this.declaration), mode); } hasAny(mode) { return flagsAny(getModeUsingBasisChain(this.declaration), mode); } body(_signalArgs) { rebuildBodyNow(this); } configureReactivity(options) { if (this.stamp < Number.MAX_SAFE_INTEGER - 1 || this.has(Mode.primitive)) throw misuse("reactivity can be configured only for elements with non-primitive mode and only during preparation"); return manageReaction(this.body).configure(options); } static get node() { if (!gCurrentNode) throw misuse("current node is undefined"); return gCurrentNode; } static tryUseTreeVariableValue(variable) { var _a; let node = ReactiveTreeNode.current; while (((_a = node.context) === null || _a === void 0 ? void 0 : _a.variable) !== variable && node.owner !== node) node = node.outer; const ctx = node.context; return (ctx === null || ctx === void 0 ? void 0 : ctx.variable) === variable ? ctx.value : undefined; } 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$.node; 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, }) ], ReactiveTreeNode$.prototype, "body", 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(node, error, action) { runInsideContextOfNode(node, () => { var _a; const owner = node; const children = owner.children; if (children.isRenovationInProgress) { let promised = undefined; try { const renovation = children.endRenovation(error); for (const child of renovation.lostItems()) launchFinalizationImpl(child, true, true); if (!error) { const sequential = children.isStrictOrder; let p1 = undefined; let p2 = undefined; let mounting = false; let partition = owner; for (const c of children.items()) { if (Transaction.isCanceled) break; const child = c; const isPartition = child.driver.isPartition; const host = isPartition ? owner : partition; mounting = markToMountIfNecessary(mounting, host, child, children, sequential); const p = (_a = child.priority) !== null && _a !== void 0 ? _a : Priority.realtime; if (p === Priority.realtime) rebuildBodyImpl(child); else if (p === Priority.normal) p1 = push(child, p1); else p2 = push(child, p2); if (isPartition) partition = child; } if (!Transaction.isCanceled && (p1 !== undefined || p2 !== undefined)) promised = startIncrementalNestedScriptsRun(node, children, p1, p2).then(() => action(error), e => action(e)); } } finally { if (!promised) action(error); } } }); } function markToMountIfNecessary(mounting, host, node, children, sequential) { if (node.declaration.unmounted) host = node; if (!node.hasAny(Mode.fragment | Mode.external)) { if (mounting || node.host !== host) { LinkedItem.setStatus$(node, Mark.moved, node.rank); mounting = false; } } else if (sequential && node.mark === Mark.moved) mounting = true; node.host = host; return mounting; } function startIncrementalNestedScriptsRun(ownerNode, allChildren, priority1, priority2) { return __awaiter(this, void 0, void 0, function* () { const stamp = ownerNode.stamp; if (priority1) yield runNestedScriptsIncrementally(ownerNode, stamp, allChildren, priority1, Priority.normal); if (priority2) yield runNestedScriptsIncrementally(ownerNode, 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; if (!Transaction.isCanceled || !Transaction.isFrameOver(1, ReactiveTreeNode$.shortFrameDuration / 3)) { let outerPriority = ReactiveTreeNode$.currentBodyPriority; ReactiveTreeNode$.currentBodyPriority = 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) { rebuildBodyImpl(child); if (Transaction.isFrameOver(1, frameDuration)) { ReactiveTreeNode$.currentBodyPriority = outerPriority; yield Transaction.requestNextFrame(0); outerPriority = ReactiveTreeNode$.currentBodyPriority; ReactiveTreeNode$.currentBodyPriority = priority; frameDuration = Math.min(4 * frameDuration, Math.min(frameDurationLimit, ReactiveTreeNode.frameDuration)); } if (Transaction.isCanceled && Transaction.isFrameOver(1, ReactiveTreeNode.shortFrameDuration / 3)) break; } } finally { ReactiveTreeNode$.currentBodyPriority = outerPriority; } } }); } function rebuildBodyImpl(node) { if (node.stamp >= 0) { if (!node.has(Mode.primitive)) { if (node.stamp === Number.MAX_SAFE_INTEGER) { Transaction.outside(() => { if (ReactiveSystem.isLogging) ReactiveSystem.setLoggingHint(node, node.key); manageReaction(node.body).configure({ order: node.level, }); }); } runNonReactive(node.body, node.declaration.signalArgs); } else if (node.owner !== node) rebuildBodyNow(node); else runTransactional(() => rebuildBodyNow(node)); } } 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 rebuildBodyNow(node) { if (node.stamp >= 0) { let result = undefined; runInsideContextOfNode(node, () => { mountOrRemountIfNecessary(node); if (node.stamp < Number.MAX_SAFE_INTEGER - 1) { try { node.stamp++; node.numerator = 0; node.children.beginRenovation(); const driver = node.driver; result = driver.rebuildBody(node); result = proceedSyncOrAsync(result, v => { launchNestedNodesThenDoImpl(node, undefined, NOP); return v; }, e => { console.log(e); launchNestedNodesThenDoImpl(node, e !== null && e !== void 0 ? e : new Error("unknown error"), NOP); }); } catch (e) { launchNestedNodesThenDoImpl(node, e, NOP); console.log(`Reactive node body failed: ${node.key}`); console.log(`${e}`); } } }); } } function launchFinalizationImpl(node, isLeader, individual) { 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.primitive)) { LinkedItem.link$(gDisposeList, node, undefined); if (gDisposeList.first === node) runTransactional({ isolation: Isolation.disjoinForInternalDisposal, hint: `runDisposalLoop(initiator=${node.key})` }, () => { void runDisposalLoop().then(NOP, error => console.log(error)); }); } for (const child of node.children.items()) launchFinalizationImpl(child, childrenAreLeaders, false); ReactiveTreeNode$.grandNodeCount--; } } function runDisposalLoop() { return __awaiter(this, void 0, void 0, function* () { yield Transaction.requestNextFrame(); let node = gDisposeList.first; while (node !== undefined) { if (Transaction.isFrameOver(500, 5)) yield Transaction.requestNextFrame(); disposeRxObject(node); LinkedItem.link$(undefined, node, undefined); node = gDisposeList.first; ReactiveTreeNode$.disposableNodeCount--; } }); } function wrapToRunInside(func) { let wrappedToRunInside; const outer = gCurrentNode; if (outer) wrappedToRunInside = (...args) => { return runInsideContextOfNode(outer, func, ...args); }; else wrappedToRunInside = func; return wrappedToRunInside; } function runInsideContextOfNode(node, func, ...args) { const outer = gCurrentNode; try { gCurrentNode = node; return func(...args); } finally { gCurrentNode = 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 gCurrentNode = undefined; const gDisposeList = new LinkedSubList();