@repobit/dex-store-elements 1.7.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -913
- package/README.md +9 -7
- package/dist/src/actions/action.adapter.d.ts +36 -0
- package/dist/src/actions/action.adapter.js +183 -0
- package/dist/src/actions/action.adapter.js.map +1 -0
- package/dist/src/actions/action.attributes.d.ts +15 -0
- package/dist/src/actions/action.attributes.js +137 -0
- package/dist/src/actions/action.attributes.js.map +1 -0
- package/dist/src/actions/action.registry.d.ts +4 -0
- package/dist/src/actions/action.registry.js +119 -0
- package/dist/src/actions/action.registry.js.map +1 -0
- package/dist/src/actions/index.d.ts +3 -2
- package/dist/src/actions/index.js +3 -80
- package/dist/src/actions/index.js.map +1 -1
- package/dist/src/compute/compute.expand.d.ts +70 -0
- package/dist/src/compute/compute.expand.js +319 -0
- package/dist/src/compute/compute.expand.js.map +1 -0
- package/dist/src/compute/compute.scan.d.ts +54 -0
- package/dist/src/compute/compute.scan.js +160 -0
- package/dist/src/compute/compute.scan.js.map +1 -0
- package/dist/src/compute/option-resolver.d.ts +31 -0
- package/dist/src/compute/option-resolver.js +147 -0
- package/dist/src/compute/option-resolver.js.map +1 -0
- package/dist/src/contexts/context.compute.d.ts +12 -0
- package/dist/src/contexts/context.compute.js +4 -0
- package/dist/src/contexts/context.compute.js.map +1 -0
- package/dist/src/contexts/context.scope.d.ts +139 -0
- package/dist/src/contexts/context.scope.js +48 -0
- package/dist/src/contexts/context.scope.js.map +1 -0
- package/dist/src/controllers/controller.action-resolve.d.ts +110 -0
- package/dist/src/controllers/controller.action-resolve.js +203 -0
- package/dist/src/controllers/controller.action-resolve.js.map +1 -0
- package/dist/src/controllers/controller.action-topology.d.ts +59 -0
- package/dist/src/controllers/controller.action-topology.js +130 -0
- package/dist/src/controllers/controller.action-topology.js.map +1 -0
- package/dist/src/controllers/controller.compute.d.ts +72 -0
- package/dist/src/controllers/controller.compute.js +110 -0
- package/dist/src/controllers/controller.compute.js.map +1 -0
- package/dist/src/controllers/controller.resolve.d.ts +78 -0
- package/dist/src/controllers/controller.resolve.js +205 -0
- package/dist/src/controllers/controller.resolve.js.map +1 -0
- package/dist/src/core/action-topology.d.ts +62 -0
- package/dist/src/core/action-topology.js +67 -0
- package/dist/src/core/action-topology.js.map +1 -0
- package/dist/src/core/actions.d.ts +64 -0
- package/dist/src/core/actions.js +47 -0
- package/dist/src/core/actions.js.map +1 -0
- package/dist/src/core/node.element.d.ts +179 -0
- package/dist/src/core/node.element.js +573 -0
- package/dist/src/core/node.element.js.map +1 -0
- package/dist/src/core/node.scope.d.ts +374 -0
- package/dist/src/core/node.scope.js +558 -0
- package/dist/src/core/node.scope.js.map +1 -0
- package/dist/src/core/option-update.d.ts +11 -0
- package/dist/src/core/option-update.js +59 -0
- package/dist/src/core/option-update.js.map +1 -0
- package/dist/src/dsl/compilers/array/compiler.js +13 -14
- package/dist/src/dsl/compilers/array/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/boolean/compiler.d.ts +1 -1
- package/dist/src/dsl/compilers/boolean/compiler.js +14 -27
- package/dist/src/dsl/compilers/boolean/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/enum/compiler.js +7 -10
- package/dist/src/dsl/compilers/enum/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/index.d.ts +1 -1
- package/dist/src/dsl/compilers/index.js +5 -5
- package/dist/src/dsl/compilers/index.js.map +1 -1
- package/dist/src/dsl/utilty.d.ts +0 -4
- package/dist/src/dsl/utilty.js +3 -30
- package/dist/src/dsl/utilty.js.map +1 -1
- package/dist/src/eta/batch.d.ts +123 -0
- package/dist/src/eta/batch.js +162 -0
- package/dist/src/eta/batch.js.map +1 -0
- package/dist/src/eta/batcher.d.ts +184 -0
- package/dist/src/eta/batcher.js +335 -0
- package/dist/src/eta/batcher.js.map +1 -0
- package/dist/src/eta/cache.d.ts +36 -0
- package/dist/src/eta/cache.js +56 -0
- package/dist/src/eta/cache.js.map +1 -0
- package/dist/src/eta/controller.d.ts +608 -0
- package/dist/src/eta/controller.js +1036 -0
- package/dist/src/eta/controller.js.map +1 -0
- package/dist/src/eta/factory.d.ts +74 -0
- package/dist/src/eta/factory.js +121 -0
- package/dist/src/eta/factory.js.map +1 -0
- package/dist/src/eta/idle.d.ts +39 -0
- package/dist/src/eta/idle.js +52 -0
- package/dist/src/eta/idle.js.map +1 -0
- package/dist/src/eta/index.d.ts +52 -0
- package/dist/src/eta/index.js +13 -0
- package/dist/src/eta/index.js.map +1 -0
- package/dist/src/eta/observer.d.ts +263 -0
- package/dist/src/eta/observer.js +263 -0
- package/dist/src/eta/observer.js.map +1 -0
- package/dist/src/eta/operations.d.ts +41 -0
- package/dist/src/eta/operations.js +59 -0
- package/dist/src/eta/operations.js.map +1 -0
- package/dist/src/eta/source-registry.d.ts +198 -0
- package/dist/src/eta/source-registry.js +267 -0
- package/dist/src/eta/source-registry.js.map +1 -0
- package/dist/src/eta/template-index.d.ts +285 -0
- package/dist/src/eta/template-index.js +642 -0
- package/dist/src/eta/template-index.js.map +1 -0
- package/dist/src/eta/types.d.ts +529 -0
- package/dist/src/eta/types.js +2 -0
- package/dist/src/eta/types.js.map +1 -0
- package/dist/src/index.d.ts +21 -9
- package/dist/src/index.js +22 -11
- package/dist/src/index.js.map +1 -1
- package/dist/src/nodes/node.context.d.ts +5 -3
- package/dist/src/nodes/node.context.js +48 -3
- package/dist/src/nodes/node.context.js.map +1 -1
- package/dist/src/nodes/node.option.d.ts +11 -31
- package/dist/src/nodes/node.option.js +158 -222
- package/dist/src/nodes/node.option.js.map +1 -1
- package/dist/src/nodes/node.product.d.ts +7 -11
- package/dist/src/nodes/node.product.js +85 -64
- package/dist/src/nodes/node.product.js.map +1 -1
- package/dist/src/renders/attributes/buyLink.d.ts +2 -2
- package/dist/src/renders/attributes/buyLink.js +14 -11
- package/dist/src/renders/attributes/buyLink.js.map +1 -1
- package/dist/src/renders/attributes/devices.d.ts +2 -2
- package/dist/src/renders/attributes/devices.js +9 -19
- package/dist/src/renders/attributes/devices.js.map +1 -1
- package/dist/src/renders/attributes/discount.d.ts +2 -2
- package/dist/src/renders/attributes/discount.js +56 -44
- package/dist/src/renders/attributes/discount.js.map +1 -1
- package/dist/src/renders/attributes/hide.d.ts +2 -2
- package/dist/src/renders/attributes/hide.js +49 -14
- package/dist/src/renders/attributes/hide.js.map +1 -1
- package/dist/src/renders/attributes/index.d.ts +3 -3
- package/dist/src/renders/attributes/index.js +2 -4
- package/dist/src/renders/attributes/index.js.map +1 -1
- package/dist/src/renders/attributes/price.d.ts +2 -2
- package/dist/src/renders/attributes/price.js +30 -44
- package/dist/src/renders/attributes/price.js.map +1 -1
- package/dist/src/renders/attributes/subscription.d.ts +2 -2
- package/dist/src/renders/attributes/subscription.js +14 -20
- package/dist/src/renders/attributes/subscription.js.map +1 -1
- package/dist/src/renders/attributes/utilty.d.ts +2 -0
- package/dist/src/renders/attributes/utilty.js +17 -4
- package/dist/src/renders/attributes/utilty.js.map +1 -1
- package/dist/src/renders/context.d.ts +122 -10
- package/dist/src/renders/context.js +140 -80
- package/dist/src/renders/context.js.map +1 -1
- package/dist/src/renders/format.js +2 -2
- package/dist/src/renders/format.js.map +1 -1
- package/dist/src/renders/index.d.ts +6 -2
- package/dist/src/renders/index.js +67 -29
- package/dist/src/renders/index.js.map +1 -1
- package/dist/src/renders/observe.d.ts +1 -4
- package/dist/src/renders/observe.js +10 -24
- package/dist/src/renders/observe.js.map +1 -1
- package/dist/src/renders/render.wrapper.d.ts +37 -0
- package/dist/src/renders/render.wrapper.js +147 -0
- package/dist/src/renders/render.wrapper.js.map +1 -0
- package/dist/src/templating/eta.js +1 -4
- package/dist/src/templating/eta.js.map +1 -1
- package/package.json +4 -5
- package/dist/src/actions/action.button.d.ts +0 -2
- package/dist/src/actions/action.button.js +0 -8
- package/dist/src/actions/action.button.js.map +0 -1
- package/dist/src/actions/action.input.d.ts +0 -2
- package/dist/src/actions/action.input.js +0 -43
- package/dist/src/actions/action.input.js.map +0 -1
- package/dist/src/actions/action.select.d.ts +0 -2
- package/dist/src/actions/action.select.js +0 -14
- package/dist/src/actions/action.select.js.map +0 -1
- package/dist/src/actions/utilty.d.ts +0 -16
- package/dist/src/actions/utilty.js +0 -149
- package/dist/src/actions/utilty.js.map +0 -1
- package/dist/src/contexts/context.datalayer.d.ts +0 -9
- package/dist/src/contexts/context.datalayer.js +0 -3
- package/dist/src/contexts/context.datalayer.js.map +0 -1
- package/dist/src/contexts/context.derived.d.ts +0 -12
- package/dist/src/contexts/context.derived.js +0 -3
- package/dist/src/contexts/context.derived.js.map +0 -1
- package/dist/src/contexts/context.option.d.ts +0 -5
- package/dist/src/contexts/context.option.js +0 -3
- package/dist/src/contexts/context.option.js.map +0 -1
- package/dist/src/contexts/context.product.d.ts +0 -5
- package/dist/src/contexts/context.product.js +0 -3
- package/dist/src/contexts/context.product.js.map +0 -1
- package/dist/src/contexts/context.state.d.ts +0 -28
- package/dist/src/contexts/context.state.js +0 -3
- package/dist/src/contexts/context.state.js.map +0 -1
- package/dist/src/contexts/context.store.d.ts +0 -5
- package/dist/src/contexts/context.store.js +0 -3
- package/dist/src/contexts/context.store.js.map +0 -1
- package/dist/src/controllers/collect-controller.d.ts +0 -39
- package/dist/src/controllers/collect-controller.js +0 -161
- package/dist/src/controllers/collect-controller.js.map +0 -1
- package/dist/src/controllers/compute-controller.d.ts +0 -55
- package/dist/src/controllers/compute-controller.js +0 -350
- package/dist/src/controllers/compute-controller.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.d.ts +0 -25
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.js +0 -192
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.d.ts +0 -28
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.js +0 -206
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-renderer.d.ts +0 -21
- package/dist/src/controllers/eta-renderer/eta-renderer.js +0 -77
- package/dist/src/controllers/eta-renderer/eta-renderer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-template-cache.d.ts +0 -24
- package/dist/src/controllers/eta-renderer/eta-template-cache.js +0 -61
- package/dist/src/controllers/eta-renderer/eta-template-cache.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-types.d.ts +0 -11
- package/dist/src/controllers/eta-renderer/eta-types.js +0 -1
- package/dist/src/controllers/eta-renderer/eta-types.js.map +0 -1
- package/dist/src/controllers/eta-renderer/index.d.ts +0 -2
- package/dist/src/controllers/eta-renderer/index.js +0 -2
- package/dist/src/controllers/eta-renderer/index.js.map +0 -1
- package/dist/src/controllers/event-pipeline-controller.d.ts +0 -28
- package/dist/src/controllers/event-pipeline-controller.js +0 -72
- package/dist/src/controllers/event-pipeline-controller.js.map +0 -1
- package/dist/src/events/events.d.ts +0 -75
- package/dist/src/events/events.js +0 -78
- package/dist/src/events/events.js.map +0 -1
- package/dist/src/nodes/node.root.d.ts +0 -16
- package/dist/src/nodes/node.root.js +0 -49
- package/dist/src/nodes/node.root.js.map +0 -1
- package/dist/src/nodes/node.state.d.ts +0 -88
- package/dist/src/nodes/node.state.js +0 -330
- package/dist/src/nodes/node.state.js.map +0 -1
- package/dist/src/renders/utility.d.ts +0 -32
- package/dist/src/renders/utility.js +0 -191
- package/dist/src/renders/utility.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../../../src/eta/batch.ts"],"names":[],"mappings":"AAAA,mBAAmB;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,MAAM,OAAO,gBAAgB;IAA7B;QACE;;;;;WAKG;QACM,UAAK,GAAG,IAAI,GAAG,EAAQ,CAAC;QAEjC;;;;WAIG;QACM,YAAO,GAAG,IAAI,GAAG,EAAQ,CAAC;QAEnC;;;;;WAKG;QACM,SAAI,GAAG,IAAI,GAAG,EAAQ,CAAC;QAEhC;;;;;WAKG;QACM,eAAU,GAAG,IAAI,GAAG,EAAwB,CAAC;IAiCxD,CAAC;IA/BC;;;;;;;OAOG;IACH,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAClB,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED;;;;;;;OAOG;IACH,IAAI,KAAK;QACP,OAAO,CACL,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;YACrB,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,CAC3B,CAAC;IACJ,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAuB,EACvB,MAAsB;IAEtB,IAAI,MAAM,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACrC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACvC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO;IACT,CAAC;IAED,IACE,MAAM,CAAC,IAAI,KAAK,eAAe;QAC/B,MAAM,CAAC,MAAM,YAAY,IAAI,EAC7B,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9B,OAAO;IACT,CAAC;IAED,IACE,MAAM,CAAC,IAAI,KAAK,YAAY;QAC5B,MAAM,CAAC,MAAM,YAAY,OAAO;QAChC,MAAM,CAAC,aAAa,EACpB,CAAC;QACD,IAAI,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;YAC1B,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAClC,CAAC;AACH,CAAC","sourcesContent":["// src/eta/batch.ts\n\n/**\n * Accumulator for raw `MutationRecord`s produced by the\n * `EtaMutationObserver` between scheduled flushes.\n *\n * Records fall into four buckets:\n *\n * - `added` — nodes introduced into the DOM via `childList` records.\n * - `removed` — nodes detached via `childList` records.\n * - `text` — `Text` nodes whose data changed via `characterData`\n * records.\n * - `attributes` — `(Element, attributeName)` pairs that flipped via\n * `attributes` records.\n *\n * The buckets are `Set` / `Map` collections rather than arrays so\n * that repeated records for the same target naturally collapse\n * before flush (e.g. five `attributes` flips on the same `title`\n * only yield a single `(element, 'title')` entry by the time the\n * batch is applied).\n *\n * **Compaction rules.** A flush through `EtaMutationBatcher` applies\n * the following reductions to the bucket contents before forwarding\n * the compacted set to the `EtaTemplateIndex`:\n *\n * 1. `add` then `remove` of the same node within one batch is a\n * no-op (the node ends up detached, the index never saw it).\n * 2. `remove` then `add` of the same node is forwarded as both a\n * `remove` and an `add` (the move is real).\n * 3. An ancestor `add` subsumes descendant `add`s — the index walks\n * the subtree on `add`, so descendant adds are redundant.\n * 4. An ancestor `remove` subsumes descendant `remove`s for the\n * same reason.\n * 5. A `text` change inside an added subtree is dropped; the index\n * reads the final text when it walks the new subtree.\n * 6. A `text` / `attribute` change inside a removed subtree is\n * dropped; the node is leaving ownership.\n * 7. Repeated text changes on the same `Text` node collapse to one\n * entry (the `Set` storage already handles this).\n * 8. Repeated attribute changes on the same `(Element, name)` pair\n * collapse to one entry (the inner `Set<string>` storage already\n * handles this).\n *\n * The batch itself does not enforce rules 1–6 — those live in the\n * batcher's compaction pass. Rules 7–8 are enforced automatically\n * by the bucket storage.\n *\n * **Immutability of contents.** Bucket contents are mutable in\n * place for performance reasons (`Set.add` / `Map.set` are O(1)).\n * Callers MUST NOT cache references to bucket internals across a\n * flush — `clear()` resets the bucket to empty. The controller\n * treats the batch as opaque between flushes.\n */\nexport class EtaMutationBatch {\n /**\n * Nodes added since the last flush.\n *\n * Stored as a `Set<Node>` so multiple adds of the same node\n * collapse automatically (rule 7-equivalent for adds).\n */\n readonly added = new Set<Node>();\n\n /**\n * Nodes removed since the last flush.\n *\n * Stored as a `Set<Node>` for the same dedup reasons as `added`.\n */\n readonly removed = new Set<Node>();\n\n /**\n * `Text` nodes whose character data changed since the last flush.\n *\n * `Set<Text>` collapses repeated changes on the same text node\n * (rule 7).\n */\n readonly text = new Set<Text>();\n\n /**\n * `Element` → set of changed attribute names since the last flush.\n *\n * The inner `Set<string>` collapses repeated changes to the same\n * attribute name (rule 8).\n */\n readonly attributes = new Map<Element, Set<string>>();\n\n /**\n * Reset every bucket to empty.\n *\n * Called by `EtaMutationBatcher.flush()` after the compacted\n * mutations have been applied to the index. The bucket instances\n * are preserved (`Set` / `Map` references stay valid) so callers\n * can safely hold long-lived references to them.\n */\n clear(): void {\n this.added.clear();\n this.removed.clear();\n this.text.clear();\n this.attributes.clear();\n }\n\n /**\n * `true` when every bucket is empty — i.e. there is nothing to\n * forward to the index on the next flush.\n *\n * Used by `EtaMutationBatcher.flush()` as a fast-path for \"no\n * work to do\" so the batcher can return `false` without touching\n * the index.\n */\n get empty(): boolean {\n return (\n this.added.size === 0 &&\n this.removed.size === 0 &&\n this.text.size === 0 &&\n this.attributes.size === 0\n );\n }\n}\n\n/**\n * Fold a single `MutationRecord` into the appropriate bucket of\n * `batch`.\n *\n * The mapping mirrors `EtaMutationObserver.handleRecord` but\n * accumulates records rather than forwarding them straight to the\n * index. The batcher's compaction pass later walks the accumulated\n * buckets and decides what to actually apply.\n *\n * Mapping:\n *\n * - `childList` → `addedNodes` / `removedNodes` are added to the\n * matching bucket.\n * - `characterData` → the `Text` target is added to `text` (when\n * the target really is a `Text` — the DOM guard is defensive in\n * case some exotic browser delivers a different node type).\n * - `attributes` → `(element, attributeName)` is added to\n * `attributes` (when the target is an `Element` and the\n * attribute name is present — both are required for the entry\n * to be useful to the index).\n *\n * Unknown record types are silently dropped. The DOM only emits\n * the three types above, so this branch is defensive only.\n */\nexport function enqueueRecord(\n batch: EtaMutationBatch,\n record: MutationRecord\n): void {\n if (record.type === 'childList') {\n for (const node of record.addedNodes) {\n batch.added.add(node);\n }\n for (const node of record.removedNodes) {\n batch.removed.add(node);\n }\n return;\n }\n\n if (\n record.type === 'characterData' &&\n record.target instanceof Text\n ) {\n batch.text.add(record.target);\n return;\n }\n\n if (\n record.type === 'attributes' &&\n record.target instanceof Element &&\n record.attributeName\n ) {\n let attrs = batch.attributes.get(record.target);\n if (!attrs) {\n attrs = new Set<string>();\n batch.attributes.set(record.target, attrs);\n }\n attrs.add(record.attributeName);\n }\n}\n"]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { EtaMutationBatchSchedule, EtaMutationBatchScheduleOption, EtaTemplateIndexLike } from './types.js';
|
|
2
|
+
export type { EtaMutationBatchSchedule };
|
|
3
|
+
/**
|
|
4
|
+
* Options accepted by `EtaMutationBatcher`.
|
|
5
|
+
*/
|
|
6
|
+
export interface EtaMutationBatcherOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Schedule mode. Defaults to `'animation-frame'` when omitted.
|
|
9
|
+
*
|
|
10
|
+
* See `EtaMutationBatchSchedule` for the full set of modes.
|
|
11
|
+
*/
|
|
12
|
+
schedule?: EtaMutationBatchScheduleOption;
|
|
13
|
+
/**
|
|
14
|
+
* Called after each flush with the dirty flag from the index.
|
|
15
|
+
*
|
|
16
|
+
* The controller wires this to `task.run()` so a dirty flush
|
|
17
|
+
* triggers a fresh render. When the flush was a no-op (empty
|
|
18
|
+
* batch or every compaction dropped), `dirty` is `false` and the
|
|
19
|
+
* controller should skip the render.
|
|
20
|
+
*
|
|
21
|
+
* `onFlush` runs synchronously inside the flush callback. The
|
|
22
|
+
* controller MUST NOT block here — it should only kick the Task
|
|
23
|
+
* run, not wait for it.
|
|
24
|
+
*/
|
|
25
|
+
onFlush: (dirty: boolean) => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `EtaMutationBatcher` is the bridge between the
|
|
29
|
+
* `EtaMutationObserver` and the `EtaTemplateIndex`.
|
|
30
|
+
*
|
|
31
|
+
* Responsibilities:
|
|
32
|
+
*
|
|
33
|
+
* - Accumulate raw `MutationRecord`s into an `EtaMutationBatch`
|
|
34
|
+
* via `enqueueRecord`.
|
|
35
|
+
* - Schedule a flush on the configured tick
|
|
36
|
+
* (`microtask` / `animation-frame` / `idle` / `manual`).
|
|
37
|
+
* - Compact the accumulated batch when flushing (apply rules 1–8).
|
|
38
|
+
* - Forward the compacted mutations to the index through its
|
|
39
|
+
* existing `apply()` entry point.
|
|
40
|
+
* - Notify the host via `onFlush(dirty)` so a dirty flush can kick
|
|
41
|
+
* a render task.
|
|
42
|
+
*
|
|
43
|
+
* **Why one flush per burst.** Without batching, every mutation
|
|
44
|
+
* record fires its own index update and its own render — DOM
|
|
45
|
+
* decoration flows that add / decorate / measure a block in one
|
|
46
|
+
* frame end up running the render pass three or four times. The
|
|
47
|
+
* batcher collapses that into one flush per frame (or per idle
|
|
48
|
+
* window / per explicit call).
|
|
49
|
+
*
|
|
50
|
+
* **Why compaction matters.** Many DOM edits land as
|
|
51
|
+
* `add subtree` followed by several descendant `add`s and
|
|
52
|
+
* `attribute` flips. Forwarding each one to the index would walk
|
|
53
|
+
* the subtree once per record. Compaction collapses the subtree
|
|
54
|
+
* walk to a single `add(subtree)` call.
|
|
55
|
+
*/
|
|
56
|
+
export declare class EtaMutationBatcher {
|
|
57
|
+
private readonly batch;
|
|
58
|
+
private readonly index;
|
|
59
|
+
private readonly options;
|
|
60
|
+
/**
|
|
61
|
+
* `true` while a flush is queued but has not yet run. Used to
|
|
62
|
+
* coalesce multiple `enqueue()` calls into a single flush.
|
|
63
|
+
*
|
|
64
|
+
* Reset to `false` immediately before the flush callback runs
|
|
65
|
+
* (so a flush that re-enters `enqueue()` schedules a fresh
|
|
66
|
+
* flush, but a flush that runs `flush()` directly does not).
|
|
67
|
+
*/
|
|
68
|
+
private scheduled;
|
|
69
|
+
/**
|
|
70
|
+
* Pending `requestAnimationFrame` handle, if any.
|
|
71
|
+
*
|
|
72
|
+
* Stored so `flush()` / `cancelSchedule()` can cancel a pending
|
|
73
|
+
* frame flush.
|
|
74
|
+
*/
|
|
75
|
+
private frameId;
|
|
76
|
+
/**
|
|
77
|
+
* Pending `requestIdleCallback` handle, if any.
|
|
78
|
+
*
|
|
79
|
+
* Stored so `flush()` / `cancelSchedule()` can cancel a pending
|
|
80
|
+
* idle flush.
|
|
81
|
+
*/
|
|
82
|
+
private idleId;
|
|
83
|
+
/**
|
|
84
|
+
* Token for the most recently scheduled microtask flush, if any.
|
|
85
|
+
*
|
|
86
|
+
* The standard `queueMicrotask` API does not return a handle that
|
|
87
|
+
* can be cancelled, so we capture a fresh object identity every
|
|
88
|
+
* time we schedule a microtask flush. The queued callback checks
|
|
89
|
+
* the captured token against `this.microtaskToken` before
|
|
90
|
+
* running — if they no longer match (because a subsequent
|
|
91
|
+
* `schedule()` produced a new token, or because `cancelSchedule`
|
|
92
|
+
* invalidated the field), the queued callback is a no-op.
|
|
93
|
+
*
|
|
94
|
+
* The existing `cancelSchedule()` already clears `this.scheduled`
|
|
95
|
+
* so a fresh `enqueue()` after a manual flush can schedule a
|
|
96
|
+
* new flush; the token approach is purely additive — it turns a
|
|
97
|
+
* previously-harmless "empty batch re-flush" into an explicit
|
|
98
|
+
* skip so observers can distinguish "the batcher ran" from
|
|
99
|
+
* "the batcher was cancelled".
|
|
100
|
+
*/
|
|
101
|
+
private microtaskToken;
|
|
102
|
+
constructor(index: EtaTemplateIndexLike, options: EtaMutationBatcherOptions);
|
|
103
|
+
/**
|
|
104
|
+
* Fold `records` into the accumulated batch and schedule a flush.
|
|
105
|
+
*
|
|
106
|
+
* Calling `enqueue()` while a flush is already scheduled is a
|
|
107
|
+
* no-op for scheduling — the pending flush will pick up the new
|
|
108
|
+
* records when it runs.
|
|
109
|
+
*
|
|
110
|
+
* When `schedule` is `'manual'`, the records are accumulated but
|
|
111
|
+
* no flush is scheduled; the host is expected to call
|
|
112
|
+
* `flush()` / `flushAndNotify()` itself.
|
|
113
|
+
*
|
|
114
|
+
* **No rule-1 vs rule-2 special case.** Earlier versions of this
|
|
115
|
+
* method tracked the FIRST op seen for each node and dropped
|
|
116
|
+
* nodes whose first op was `'add'` from both buckets (the
|
|
117
|
+
* "rule 1" no-op path). Real-world DOM mutation patterns
|
|
118
|
+
* (`replaceWith`, `replaceChildren`, drain-then-remove, etc.)
|
|
119
|
+
* all use distinct nodes for the removed and added sides — the
|
|
120
|
+
* same-node case only arises in custom code that explicitly
|
|
121
|
+
* moves a node within one flush. For that case, applying both
|
|
122
|
+
* the `remove` and the `add` produces the same end state as
|
|
123
|
+
* dropping both (the factory rebuilds the operation from the
|
|
124
|
+
* source registry on re-add), so the simpler implementation is
|
|
125
|
+
* observationally equivalent. The earlier tracking also kept a
|
|
126
|
+
* strong `Map` alive during the animation-frame window, which
|
|
127
|
+
* is a leak hazard for large mutation bursts; this simpler
|
|
128
|
+
* shape avoids that.
|
|
129
|
+
*/
|
|
130
|
+
enqueue(records: readonly MutationRecord[]): void;
|
|
131
|
+
/**
|
|
132
|
+
* Apply the compacted batch to the index synchronously.
|
|
133
|
+
*
|
|
134
|
+
* Cancels any pending scheduled flush first so the manual flush
|
|
135
|
+
* does not run again on the next tick. When the batch is empty
|
|
136
|
+
* this is a no-op and returns `false`.
|
|
137
|
+
*
|
|
138
|
+
* Returns `true` when at least one compacted mutation made the
|
|
139
|
+
* index dirty, `false` otherwise.
|
|
140
|
+
*
|
|
141
|
+
* `flush()` does NOT call `onFlush`. Use `flushAndNotify()` when
|
|
142
|
+
* the host needs to be told about the flush.
|
|
143
|
+
*/
|
|
144
|
+
flush(): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* `flush()` then notify the host with the dirty flag.
|
|
147
|
+
*
|
|
148
|
+
* Always invokes `onFlush` — `onFlush(false)` on empty batches so
|
|
149
|
+
* the host can stay in sync with the flush cadence if it cares
|
|
150
|
+
* to. Most controllers ignore `dirty === false` because no
|
|
151
|
+
* render is needed.
|
|
152
|
+
*/
|
|
153
|
+
flushAndNotify(): void;
|
|
154
|
+
/**
|
|
155
|
+
* Schedule a flush on the configured tick.
|
|
156
|
+
*
|
|
157
|
+
* Coalescing: subsequent `enqueue()` calls while a flush is
|
|
158
|
+
* already scheduled do not schedule a second flush.
|
|
159
|
+
*
|
|
160
|
+
* When `schedule` is `'manual'`, scheduling is a no-op — the
|
|
161
|
+
* host owns the flush lifecycle.
|
|
162
|
+
*
|
|
163
|
+
* The `scheduled` flag is cleared BEFORE the flush callback runs
|
|
164
|
+
* so a flush that re-enters through `enqueue()` (for example,
|
|
165
|
+
* because an `onFlush` handler mutates the DOM again) can
|
|
166
|
+
* schedule a fresh flush.
|
|
167
|
+
*/
|
|
168
|
+
private schedule;
|
|
169
|
+
private currentSchedule;
|
|
170
|
+
/**
|
|
171
|
+
* Cancel any pending scheduled flush.
|
|
172
|
+
*
|
|
173
|
+
* Called by `flush()` so a manual flush is not followed by the
|
|
174
|
+
* scheduled flush. Safe to call when nothing is scheduled — the
|
|
175
|
+
* `undefined` checks short-circuit.
|
|
176
|
+
*
|
|
177
|
+
* **`microtask` schedule.** `queueMicrotask` does not expose a
|
|
178
|
+
* cancellation handle; we instead invalidate `this.microtaskToken`
|
|
179
|
+
* so the queued callback's identity-check fails and the callback
|
|
180
|
+
* becomes a no-op. `this.scheduled` is also reset so a fresh
|
|
181
|
+
* `enqueue()` after a manual flush can schedule a new flush.
|
|
182
|
+
*/
|
|
183
|
+
private cancelSchedule;
|
|
184
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/eta/batcher.ts
|
|
2
|
+
import { EtaMutationBatch, enqueueRecord } from './batch.js';
|
|
3
|
+
import { cancelIdle, requestIdle } from './idle.js';
|
|
4
|
+
/**
|
|
5
|
+
* Internal helper: keep only the topmost roots in `nodes`.
|
|
6
|
+
*
|
|
7
|
+
* A node is dropped when another node in the same iterable strictly
|
|
8
|
+
* contains it. This collapses "added the subtree root AND added
|
|
9
|
+
* several descendants" into "added the subtree root" — the index
|
|
10
|
+
* walks descendants when it sees an `add`, so the descendant adds
|
|
11
|
+
* are redundant.
|
|
12
|
+
*
|
|
13
|
+
* Self-containment (`other === node`) is treated as "not contained"
|
|
14
|
+
* so the node survives its own filter pass.
|
|
15
|
+
*
|
|
16
|
+
* Used for both the `added` and `removed` buckets.
|
|
17
|
+
*/
|
|
18
|
+
function compactRoots(nodes) {
|
|
19
|
+
const arr = [...nodes];
|
|
20
|
+
return arr.filter((node) => !arr.some((other) => other !== node && other.contains(node)));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Internal helper: `true` when `node` is strictly contained by any
|
|
24
|
+
* root in `roots`.
|
|
25
|
+
*
|
|
26
|
+
* "Strictly" means `root !== node` AND `root.contains(node)`. The
|
|
27
|
+
* strict check matters for the `removed` pass — when a node is both
|
|
28
|
+
* in `removed` and a descendant of another `removed` root, the
|
|
29
|
+
* descendant is dropped by `compactRoots` first; this helper is
|
|
30
|
+
* used later to skip descendants of a removed root when walking the
|
|
31
|
+
* `added` bucket (rule 1 — descendant of a removed root has no
|
|
32
|
+
* observable add) and the `text` / `attributes` buckets
|
|
33
|
+
* (compaction rule 6 — descendants of a removed root have changes
|
|
34
|
+
* that are irrelevant because the ancestor removal wins).
|
|
35
|
+
*
|
|
36
|
+
* Note: the root element itself is NOT considered "inside" any
|
|
37
|
+
* root by this helper. Attribute mutations on the removed root
|
|
38
|
+
* itself therefore reach the index, where `_ownsNode` rejects
|
|
39
|
+
* them by walking `parentNode` upward — see the test
|
|
40
|
+
* `Compaction order rules through the controller pipeline >
|
|
41
|
+
* skips attribute changes inside a removed subtree` for the
|
|
42
|
+
* observable contract.
|
|
43
|
+
*/
|
|
44
|
+
function isInsideAny(node, roots) {
|
|
45
|
+
for (const root of roots) {
|
|
46
|
+
if (root !== node && root.contains(node)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `EtaMutationBatcher` is the bridge between the
|
|
54
|
+
* `EtaMutationObserver` and the `EtaTemplateIndex`.
|
|
55
|
+
*
|
|
56
|
+
* Responsibilities:
|
|
57
|
+
*
|
|
58
|
+
* - Accumulate raw `MutationRecord`s into an `EtaMutationBatch`
|
|
59
|
+
* via `enqueueRecord`.
|
|
60
|
+
* - Schedule a flush on the configured tick
|
|
61
|
+
* (`microtask` / `animation-frame` / `idle` / `manual`).
|
|
62
|
+
* - Compact the accumulated batch when flushing (apply rules 1–8).
|
|
63
|
+
* - Forward the compacted mutations to the index through its
|
|
64
|
+
* existing `apply()` entry point.
|
|
65
|
+
* - Notify the host via `onFlush(dirty)` so a dirty flush can kick
|
|
66
|
+
* a render task.
|
|
67
|
+
*
|
|
68
|
+
* **Why one flush per burst.** Without batching, every mutation
|
|
69
|
+
* record fires its own index update and its own render — DOM
|
|
70
|
+
* decoration flows that add / decorate / measure a block in one
|
|
71
|
+
* frame end up running the render pass three or four times. The
|
|
72
|
+
* batcher collapses that into one flush per frame (or per idle
|
|
73
|
+
* window / per explicit call).
|
|
74
|
+
*
|
|
75
|
+
* **Why compaction matters.** Many DOM edits land as
|
|
76
|
+
* `add subtree` followed by several descendant `add`s and
|
|
77
|
+
* `attribute` flips. Forwarding each one to the index would walk
|
|
78
|
+
* the subtree once per record. Compaction collapses the subtree
|
|
79
|
+
* walk to a single `add(subtree)` call.
|
|
80
|
+
*/
|
|
81
|
+
export class EtaMutationBatcher {
|
|
82
|
+
constructor(index, options) {
|
|
83
|
+
this.batch = new EtaMutationBatch();
|
|
84
|
+
/**
|
|
85
|
+
* `true` while a flush is queued but has not yet run. Used to
|
|
86
|
+
* coalesce multiple `enqueue()` calls into a single flush.
|
|
87
|
+
*
|
|
88
|
+
* Reset to `false` immediately before the flush callback runs
|
|
89
|
+
* (so a flush that re-enters `enqueue()` schedules a fresh
|
|
90
|
+
* flush, but a flush that runs `flush()` directly does not).
|
|
91
|
+
*/
|
|
92
|
+
this.scheduled = false;
|
|
93
|
+
this.index = index;
|
|
94
|
+
this.options = options;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fold `records` into the accumulated batch and schedule a flush.
|
|
98
|
+
*
|
|
99
|
+
* Calling `enqueue()` while a flush is already scheduled is a
|
|
100
|
+
* no-op for scheduling — the pending flush will pick up the new
|
|
101
|
+
* records when it runs.
|
|
102
|
+
*
|
|
103
|
+
* When `schedule` is `'manual'`, the records are accumulated but
|
|
104
|
+
* no flush is scheduled; the host is expected to call
|
|
105
|
+
* `flush()` / `flushAndNotify()` itself.
|
|
106
|
+
*
|
|
107
|
+
* **No rule-1 vs rule-2 special case.** Earlier versions of this
|
|
108
|
+
* method tracked the FIRST op seen for each node and dropped
|
|
109
|
+
* nodes whose first op was `'add'` from both buckets (the
|
|
110
|
+
* "rule 1" no-op path). Real-world DOM mutation patterns
|
|
111
|
+
* (`replaceWith`, `replaceChildren`, drain-then-remove, etc.)
|
|
112
|
+
* all use distinct nodes for the removed and added sides — the
|
|
113
|
+
* same-node case only arises in custom code that explicitly
|
|
114
|
+
* moves a node within one flush. For that case, applying both
|
|
115
|
+
* the `remove` and the `add` produces the same end state as
|
|
116
|
+
* dropping both (the factory rebuilds the operation from the
|
|
117
|
+
* source registry on re-add), so the simpler implementation is
|
|
118
|
+
* observationally equivalent. The earlier tracking also kept a
|
|
119
|
+
* strong `Map` alive during the animation-frame window, which
|
|
120
|
+
* is a leak hazard for large mutation bursts; this simpler
|
|
121
|
+
* shape avoids that.
|
|
122
|
+
*/
|
|
123
|
+
enqueue(records) {
|
|
124
|
+
for (const record of records) {
|
|
125
|
+
enqueueRecord(this.batch, record);
|
|
126
|
+
}
|
|
127
|
+
this.schedule();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Apply the compacted batch to the index synchronously.
|
|
131
|
+
*
|
|
132
|
+
* Cancels any pending scheduled flush first so the manual flush
|
|
133
|
+
* does not run again on the next tick. When the batch is empty
|
|
134
|
+
* this is a no-op and returns `false`.
|
|
135
|
+
*
|
|
136
|
+
* Returns `true` when at least one compacted mutation made the
|
|
137
|
+
* index dirty, `false` otherwise.
|
|
138
|
+
*
|
|
139
|
+
* `flush()` does NOT call `onFlush`. Use `flushAndNotify()` when
|
|
140
|
+
* the host needs to be told about the flush.
|
|
141
|
+
*/
|
|
142
|
+
flush() {
|
|
143
|
+
this.cancelSchedule();
|
|
144
|
+
if (this.batch.empty) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const dirty = applyMutationBatch(this.index, this.batch);
|
|
148
|
+
this.batch.clear();
|
|
149
|
+
return dirty;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `flush()` then notify the host with the dirty flag.
|
|
153
|
+
*
|
|
154
|
+
* Always invokes `onFlush` — `onFlush(false)` on empty batches so
|
|
155
|
+
* the host can stay in sync with the flush cadence if it cares
|
|
156
|
+
* to. Most controllers ignore `dirty === false` because no
|
|
157
|
+
* render is needed.
|
|
158
|
+
*/
|
|
159
|
+
flushAndNotify() {
|
|
160
|
+
const dirty = this.flush();
|
|
161
|
+
this.options.onFlush(dirty);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Schedule a flush on the configured tick.
|
|
165
|
+
*
|
|
166
|
+
* Coalescing: subsequent `enqueue()` calls while a flush is
|
|
167
|
+
* already scheduled do not schedule a second flush.
|
|
168
|
+
*
|
|
169
|
+
* When `schedule` is `'manual'`, scheduling is a no-op — the
|
|
170
|
+
* host owns the flush lifecycle.
|
|
171
|
+
*
|
|
172
|
+
* The `scheduled` flag is cleared BEFORE the flush callback runs
|
|
173
|
+
* so a flush that re-enters through `enqueue()` (for example,
|
|
174
|
+
* because an `onFlush` handler mutates the DOM again) can
|
|
175
|
+
* schedule a fresh flush.
|
|
176
|
+
*/
|
|
177
|
+
schedule() {
|
|
178
|
+
if (this.scheduled) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const scheduleMode = this.currentSchedule();
|
|
182
|
+
if (scheduleMode === 'manual') {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.scheduled = true;
|
|
186
|
+
if (scheduleMode === 'microtask') {
|
|
187
|
+
// Capture a fresh token identity so a subsequent `schedule()`
|
|
188
|
+
// (or `cancelSchedule()` invalidating the field) invalidates
|
|
189
|
+
// the in-flight queued callback. The callback compares
|
|
190
|
+
// against `this.microtaskToken` and becomes a no-op when the
|
|
191
|
+
// tokens diverge — `queueMicrotask` itself cannot be
|
|
192
|
+
// cancelled, so identity-comparison is the standard escape
|
|
193
|
+
// hatch for that API.
|
|
194
|
+
const token = {};
|
|
195
|
+
this.microtaskToken = token;
|
|
196
|
+
queueMicrotask(() => {
|
|
197
|
+
if (this.microtaskToken !== token) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.microtaskToken = undefined;
|
|
201
|
+
this.scheduled = false;
|
|
202
|
+
this.flushAndNotify();
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (scheduleMode === 'idle') {
|
|
207
|
+
this.idleId = requestIdle(() => {
|
|
208
|
+
this.idleId = undefined;
|
|
209
|
+
this.scheduled = false;
|
|
210
|
+
this.flushAndNotify();
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Default: 'animation-frame'.
|
|
215
|
+
this.frameId = requestAnimationFrame(() => {
|
|
216
|
+
this.frameId = undefined;
|
|
217
|
+
this.scheduled = false;
|
|
218
|
+
this.flushAndNotify();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
currentSchedule() {
|
|
222
|
+
const schedule = this.options.schedule;
|
|
223
|
+
if (typeof schedule === 'function') {
|
|
224
|
+
return schedule() ?? 'animation-frame';
|
|
225
|
+
}
|
|
226
|
+
return schedule ?? 'animation-frame';
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Cancel any pending scheduled flush.
|
|
230
|
+
*
|
|
231
|
+
* Called by `flush()` so a manual flush is not followed by the
|
|
232
|
+
* scheduled flush. Safe to call when nothing is scheduled — the
|
|
233
|
+
* `undefined` checks short-circuit.
|
|
234
|
+
*
|
|
235
|
+
* **`microtask` schedule.** `queueMicrotask` does not expose a
|
|
236
|
+
* cancellation handle; we instead invalidate `this.microtaskToken`
|
|
237
|
+
* so the queued callback's identity-check fails and the callback
|
|
238
|
+
* becomes a no-op. `this.scheduled` is also reset so a fresh
|
|
239
|
+
* `enqueue()` after a manual flush can schedule a new flush.
|
|
240
|
+
*/
|
|
241
|
+
cancelSchedule() {
|
|
242
|
+
if (this.frameId !== undefined) {
|
|
243
|
+
cancelAnimationFrame(this.frameId);
|
|
244
|
+
this.frameId = undefined;
|
|
245
|
+
}
|
|
246
|
+
if (this.idleId !== undefined) {
|
|
247
|
+
cancelIdle(this.idleId);
|
|
248
|
+
this.idleId = undefined;
|
|
249
|
+
}
|
|
250
|
+
this.microtaskToken = undefined;
|
|
251
|
+
this.scheduled = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Apply a compacted `EtaMutationBatch` to the index.
|
|
256
|
+
*
|
|
257
|
+
* The compaction rules are documented on `EtaMutationBatch`. The
|
|
258
|
+
* order of application matters:
|
|
259
|
+
*
|
|
260
|
+
* 1. **Compact `removed` and apply.** Removes must run first so
|
|
261
|
+
* later `add` / `text` / `attribute` checks correctly identify
|
|
262
|
+
* descendants of removed roots and skip them.
|
|
263
|
+
* 2. **Compact `added` and apply** (skipping nodes inside a
|
|
264
|
+
* removed root). Adds come next so descendant text / attribute
|
|
265
|
+
* changes inside added subtrees are dropped in step 3/4.
|
|
266
|
+
* 3. **Walk `text`** (skipping nodes inside any removed or added
|
|
267
|
+
* root). The text bucket is walked last because text changes
|
|
268
|
+
* inside add / remove roots are irrelevant.
|
|
269
|
+
* 4. **Walk `attributes`** (skipping elements inside any removed or
|
|
270
|
+
* added root). Same rationale as step 3.
|
|
271
|
+
*
|
|
272
|
+
* Each compacted mutation is forwarded to `index.apply(...)`, which
|
|
273
|
+
* returns `true` only when the index actually changed. The dirty
|
|
274
|
+
* flag is OR-accumulated across all four passes.
|
|
275
|
+
*
|
|
276
|
+
* Returns `true` when at least one compacted mutation made the
|
|
277
|
+
* index dirty, `false` when every pass was a no-op.
|
|
278
|
+
*/
|
|
279
|
+
function applyMutationBatch(index, batch) {
|
|
280
|
+
const removedRoots = compactRoots(batch.removed);
|
|
281
|
+
const addedRoots = compactRoots(batch.added);
|
|
282
|
+
let dirty = false;
|
|
283
|
+
// 1. Removes first.
|
|
284
|
+
for (const node of removedRoots) {
|
|
285
|
+
if (index.apply({
|
|
286
|
+
type: 'remove',
|
|
287
|
+
node
|
|
288
|
+
})) {
|
|
289
|
+
dirty = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// 2. Adds second (skip descendants of removed roots).
|
|
293
|
+
for (const node of addedRoots) {
|
|
294
|
+
if (isInsideAny(node, removedRoots)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (index.apply({
|
|
298
|
+
type: 'add',
|
|
299
|
+
node
|
|
300
|
+
})) {
|
|
301
|
+
dirty = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// 3. Text changes (skip descendants of add/remove roots).
|
|
305
|
+
for (const text of batch.text) {
|
|
306
|
+
if (isInsideAny(text, removedRoots) ||
|
|
307
|
+
isInsideAny(text, addedRoots)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (index.apply({
|
|
311
|
+
type: 'text',
|
|
312
|
+
node: text
|
|
313
|
+
})) {
|
|
314
|
+
dirty = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// 4. Attribute changes (skip descendants of add/remove roots).
|
|
318
|
+
for (const [element, attrs] of batch.attributes) {
|
|
319
|
+
if (isInsideAny(element, removedRoots) ||
|
|
320
|
+
isInsideAny(element, addedRoots)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
for (const attr of attrs) {
|
|
324
|
+
if (index.apply({
|
|
325
|
+
type: 'attribute',
|
|
326
|
+
element,
|
|
327
|
+
attribute: attr
|
|
328
|
+
})) {
|
|
329
|
+
dirty = true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return dirty;
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=batcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batcher.js","sourceRoot":"","sources":["../../../src/eta/batcher.ts"],"names":[],"mappings":"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","sourcesContent":["// src/eta/batcher.ts\n\nimport { EtaMutationBatch, enqueueRecord } from './batch.js';\nimport { cancelIdle, requestIdle } from './idle.js';\nimport type {\n EtaMutationBatchSchedule,\n EtaMutationBatchScheduleOption,\n EtaTemplateIndexLike\n} from './types.js';\n\n// Re-export so direct importers of the batcher module still see\n// the schedule type without reaching into `./types.js`.\nexport type { EtaMutationBatchSchedule };\n\n/**\n * Options accepted by `EtaMutationBatcher`.\n */\nexport interface EtaMutationBatcherOptions {\n /**\n * Schedule mode. Defaults to `'animation-frame'` when omitted.\n *\n * See `EtaMutationBatchSchedule` for the full set of modes.\n */\n schedule?: EtaMutationBatchScheduleOption;\n\n /**\n * Called after each flush with the dirty flag from the index.\n *\n * The controller wires this to `task.run()` so a dirty flush\n * triggers a fresh render. When the flush was a no-op (empty\n * batch or every compaction dropped), `dirty` is `false` and the\n * controller should skip the render.\n *\n * `onFlush` runs synchronously inside the flush callback. The\n * controller MUST NOT block here — it should only kick the Task\n * run, not wait for it.\n */\n onFlush: (dirty: boolean) => void;\n}\n\n/**\n * Internal helper: keep only the topmost roots in `nodes`.\n *\n * A node is dropped when another node in the same iterable strictly\n * contains it. This collapses \"added the subtree root AND added\n * several descendants\" into \"added the subtree root\" — the index\n * walks descendants when it sees an `add`, so the descendant adds\n * are redundant.\n *\n * Self-containment (`other === node`) is treated as \"not contained\"\n * so the node survives its own filter pass.\n *\n * Used for both the `added` and `removed` buckets.\n */\nfunction compactRoots<T extends Node>(\n nodes: Iterable<T>\n): T[] {\n const arr = [...nodes];\n return arr.filter(\n (node) =>\n !arr.some(\n (other) =>\n other !== node && other.contains(node)\n )\n );\n}\n\n/**\n * Internal helper: `true` when `node` is strictly contained by any\n * root in `roots`.\n *\n * \"Strictly\" means `root !== node` AND `root.contains(node)`. The\n * strict check matters for the `removed` pass — when a node is both\n * in `removed` and a descendant of another `removed` root, the\n * descendant is dropped by `compactRoots` first; this helper is\n * used later to skip descendants of a removed root when walking the\n * `added` bucket (rule 1 — descendant of a removed root has no\n * observable add) and the `text` / `attributes` buckets\n * (compaction rule 6 — descendants of a removed root have changes\n * that are irrelevant because the ancestor removal wins).\n *\n * Note: the root element itself is NOT considered \"inside\" any\n * root by this helper. Attribute mutations on the removed root\n * itself therefore reach the index, where `_ownsNode` rejects\n * them by walking `parentNode` upward — see the test\n * `Compaction order rules through the controller pipeline >\n * skips attribute changes inside a removed subtree` for the\n * observable contract.\n */\nfunction isInsideAny(\n node: Node,\n roots: Iterable<Node>\n): boolean {\n for (const root of roots) {\n if (root !== node && root.contains(node)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * `EtaMutationBatcher` is the bridge between the\n * `EtaMutationObserver` and the `EtaTemplateIndex`.\n *\n * Responsibilities:\n *\n * - Accumulate raw `MutationRecord`s into an `EtaMutationBatch`\n * via `enqueueRecord`.\n * - Schedule a flush on the configured tick\n * (`microtask` / `animation-frame` / `idle` / `manual`).\n * - Compact the accumulated batch when flushing (apply rules 1–8).\n * - Forward the compacted mutations to the index through its\n * existing `apply()` entry point.\n * - Notify the host via `onFlush(dirty)` so a dirty flush can kick\n * a render task.\n *\n * **Why one flush per burst.** Without batching, every mutation\n * record fires its own index update and its own render — DOM\n * decoration flows that add / decorate / measure a block in one\n * frame end up running the render pass three or four times. The\n * batcher collapses that into one flush per frame (or per idle\n * window / per explicit call).\n *\n * **Why compaction matters.** Many DOM edits land as\n * `add subtree` followed by several descendant `add`s and\n * `attribute` flips. Forwarding each one to the index would walk\n * the subtree once per record. Compaction collapses the subtree\n * walk to a single `add(subtree)` call.\n */\nexport class EtaMutationBatcher {\n private readonly batch = new EtaMutationBatch();\n private readonly index : EtaTemplateIndexLike;\n private readonly options: EtaMutationBatcherOptions;\n\n /**\n * `true` while a flush is queued but has not yet run. Used to\n * coalesce multiple `enqueue()` calls into a single flush.\n *\n * Reset to `false` immediately before the flush callback runs\n * (so a flush that re-enters `enqueue()` schedules a fresh\n * flush, but a flush that runs `flush()` directly does not).\n */\n private scheduled = false;\n\n /**\n * Pending `requestAnimationFrame` handle, if any.\n *\n * Stored so `flush()` / `cancelSchedule()` can cancel a pending\n * frame flush.\n */\n private frameId: number | undefined;\n\n /**\n * Pending `requestIdleCallback` handle, if any.\n *\n * Stored so `flush()` / `cancelSchedule()` can cancel a pending\n * idle flush.\n */\n private idleId: number | undefined;\n\n /**\n * Token for the most recently scheduled microtask flush, if any.\n *\n * The standard `queueMicrotask` API does not return a handle that\n * can be cancelled, so we capture a fresh object identity every\n * time we schedule a microtask flush. The queued callback checks\n * the captured token against `this.microtaskToken` before\n * running — if they no longer match (because a subsequent\n * `schedule()` produced a new token, or because `cancelSchedule`\n * invalidated the field), the queued callback is a no-op.\n *\n * The existing `cancelSchedule()` already clears `this.scheduled`\n * so a fresh `enqueue()` after a manual flush can schedule a\n * new flush; the token approach is purely additive — it turns a\n * previously-harmless \"empty batch re-flush\" into an explicit\n * skip so observers can distinguish \"the batcher ran\" from\n * \"the batcher was cancelled\".\n */\n private microtaskToken: object | undefined;\n\n constructor(\n index: EtaTemplateIndexLike,\n options: EtaMutationBatcherOptions\n ) {\n this.index = index;\n this.options = options;\n }\n\n /**\n * Fold `records` into the accumulated batch and schedule a flush.\n *\n * Calling `enqueue()` while a flush is already scheduled is a\n * no-op for scheduling — the pending flush will pick up the new\n * records when it runs.\n *\n * When `schedule` is `'manual'`, the records are accumulated but\n * no flush is scheduled; the host is expected to call\n * `flush()` / `flushAndNotify()` itself.\n *\n * **No rule-1 vs rule-2 special case.** Earlier versions of this\n * method tracked the FIRST op seen for each node and dropped\n * nodes whose first op was `'add'` from both buckets (the\n * \"rule 1\" no-op path). Real-world DOM mutation patterns\n * (`replaceWith`, `replaceChildren`, drain-then-remove, etc.)\n * all use distinct nodes for the removed and added sides — the\n * same-node case only arises in custom code that explicitly\n * moves a node within one flush. For that case, applying both\n * the `remove` and the `add` produces the same end state as\n * dropping both (the factory rebuilds the operation from the\n * source registry on re-add), so the simpler implementation is\n * observationally equivalent. The earlier tracking also kept a\n * strong `Map` alive during the animation-frame window, which\n * is a leak hazard for large mutation bursts; this simpler\n * shape avoids that.\n */\n enqueue(records: readonly MutationRecord[]): void {\n for (const record of records) {\n enqueueRecord(this.batch, record);\n }\n\n this.schedule();\n }\n\n /**\n * Apply the compacted batch to the index synchronously.\n *\n * Cancels any pending scheduled flush first so the manual flush\n * does not run again on the next tick. When the batch is empty\n * this is a no-op and returns `false`.\n *\n * Returns `true` when at least one compacted mutation made the\n * index dirty, `false` otherwise.\n *\n * `flush()` does NOT call `onFlush`. Use `flushAndNotify()` when\n * the host needs to be told about the flush.\n */\n flush(): boolean {\n this.cancelSchedule();\n\n if (this.batch.empty) {\n return false;\n }\n\n const dirty = applyMutationBatch(\n this.index,\n this.batch\n );\n\n this.batch.clear();\n\n return dirty;\n }\n\n /**\n * `flush()` then notify the host with the dirty flag.\n *\n * Always invokes `onFlush` — `onFlush(false)` on empty batches so\n * the host can stay in sync with the flush cadence if it cares\n * to. Most controllers ignore `dirty === false` because no\n * render is needed.\n */\n flushAndNotify(): void {\n const dirty = this.flush();\n this.options.onFlush(dirty);\n }\n\n /**\n * Schedule a flush on the configured tick.\n *\n * Coalescing: subsequent `enqueue()` calls while a flush is\n * already scheduled do not schedule a second flush.\n *\n * When `schedule` is `'manual'`, scheduling is a no-op — the\n * host owns the flush lifecycle.\n *\n * The `scheduled` flag is cleared BEFORE the flush callback runs\n * so a flush that re-enters through `enqueue()` (for example,\n * because an `onFlush` handler mutates the DOM again) can\n * schedule a fresh flush.\n */\n private schedule(): void {\n if (this.scheduled) {\n return;\n }\n\n const scheduleMode = this.currentSchedule();\n\n if (scheduleMode === 'manual') {\n return;\n }\n\n this.scheduled = true;\n\n if (scheduleMode === 'microtask') {\n // Capture a fresh token identity so a subsequent `schedule()`\n // (or `cancelSchedule()` invalidating the field) invalidates\n // the in-flight queued callback. The callback compares\n // against `this.microtaskToken` and becomes a no-op when the\n // tokens diverge — `queueMicrotask` itself cannot be\n // cancelled, so identity-comparison is the standard escape\n // hatch for that API.\n const token = {};\n\n this.microtaskToken = token;\n\n queueMicrotask(() => {\n if (this.microtaskToken !== token) {\n return;\n }\n\n this.microtaskToken = undefined;\n this.scheduled = false;\n this.flushAndNotify();\n });\n return;\n }\n\n if (scheduleMode === 'idle') {\n this.idleId = requestIdle(() => {\n this.idleId = undefined;\n this.scheduled = false;\n this.flushAndNotify();\n });\n return;\n }\n\n // Default: 'animation-frame'.\n this.frameId = requestAnimationFrame(() => {\n this.frameId = undefined;\n this.scheduled = false;\n this.flushAndNotify();\n });\n }\n\n private currentSchedule(): EtaMutationBatchSchedule {\n const schedule = this.options.schedule;\n\n if (typeof schedule === 'function') {\n return schedule() ?? 'animation-frame';\n }\n\n return schedule ?? 'animation-frame';\n }\n\n /**\n * Cancel any pending scheduled flush.\n *\n * Called by `flush()` so a manual flush is not followed by the\n * scheduled flush. Safe to call when nothing is scheduled — the\n * `undefined` checks short-circuit.\n *\n * **`microtask` schedule.** `queueMicrotask` does not expose a\n * cancellation handle; we instead invalidate `this.microtaskToken`\n * so the queued callback's identity-check fails and the callback\n * becomes a no-op. `this.scheduled` is also reset so a fresh\n * `enqueue()` after a manual flush can schedule a new flush.\n */\n private cancelSchedule(): void {\n if (this.frameId !== undefined) {\n cancelAnimationFrame(this.frameId);\n this.frameId = undefined;\n }\n\n if (this.idleId !== undefined) {\n cancelIdle(this.idleId);\n this.idleId = undefined;\n }\n\n this.microtaskToken = undefined;\n this.scheduled = false;\n }\n}\n\n/**\n * Apply a compacted `EtaMutationBatch` to the index.\n *\n * The compaction rules are documented on `EtaMutationBatch`. The\n * order of application matters:\n *\n * 1. **Compact `removed` and apply.** Removes must run first so\n * later `add` / `text` / `attribute` checks correctly identify\n * descendants of removed roots and skip them.\n * 2. **Compact `added` and apply** (skipping nodes inside a\n * removed root). Adds come next so descendant text / attribute\n * changes inside added subtrees are dropped in step 3/4.\n * 3. **Walk `text`** (skipping nodes inside any removed or added\n * root). The text bucket is walked last because text changes\n * inside add / remove roots are irrelevant.\n * 4. **Walk `attributes`** (skipping elements inside any removed or\n * added root). Same rationale as step 3.\n *\n * Each compacted mutation is forwarded to `index.apply(...)`, which\n * returns `true` only when the index actually changed. The dirty\n * flag is OR-accumulated across all four passes.\n *\n * Returns `true` when at least one compacted mutation made the\n * index dirty, `false` when every pass was a no-op.\n */\nfunction applyMutationBatch(\n index: EtaTemplateIndexLike,\n batch: EtaMutationBatch\n): boolean {\n const removedRoots = compactRoots(batch.removed);\n const addedRoots = compactRoots(batch.added);\n\n let dirty = false;\n\n // 1. Removes first.\n for (const node of removedRoots) {\n if (\n index.apply({\n type: 'remove',\n node\n })\n ) {\n dirty = true;\n }\n }\n\n // 2. Adds second (skip descendants of removed roots).\n for (const node of addedRoots) {\n if (isInsideAny(node, removedRoots)) {\n continue;\n }\n\n if (\n index.apply({\n type: 'add',\n node\n })\n ) {\n dirty = true;\n }\n }\n\n // 3. Text changes (skip descendants of add/remove roots).\n for (const text of batch.text) {\n if (\n isInsideAny(text, removedRoots) ||\n isInsideAny(text, addedRoots)\n ) {\n continue;\n }\n\n if (\n index.apply({\n type: 'text',\n node: text\n })\n ) {\n dirty = true;\n }\n }\n\n // 4. Attribute changes (skip descendants of add/remove roots).\n for (const [element, attrs] of batch.attributes) {\n if (\n isInsideAny(element, removedRoots) ||\n isInsideAny(element, addedRoots)\n ) {\n continue;\n }\n\n for (const attr of attrs) {\n if (\n index.apply({\n type : 'attribute',\n element,\n attribute: attr\n })\n ) {\n dirty = true;\n }\n }\n }\n\n return dirty;\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Eta } from 'eta/core';
|
|
2
|
+
import type { EtaCompiledTemplate, EtaTemplateCache } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default cache backed by a single Eta instance.
|
|
5
|
+
*
|
|
6
|
+
* The Eta class compiles templates lazily and stores the resulting function
|
|
7
|
+
* strings. Wrapping it in a small cache gives us:
|
|
8
|
+
*
|
|
9
|
+
* - a stable `source -> compiled` map that survives across renders
|
|
10
|
+
* - a single, well-defined place that knows about Eta tags / options
|
|
11
|
+
* - a `clear` hook so tests and HMR can drop the cache when templates change
|
|
12
|
+
*
|
|
13
|
+
* Compilation happens on first access for a given source. The cache is keyed
|
|
14
|
+
* by the raw source string, so two equivalent template strings share a single
|
|
15
|
+
* compiled entry.
|
|
16
|
+
*/
|
|
17
|
+
export declare class DefaultEtaTemplateCache implements EtaTemplateCache {
|
|
18
|
+
private readonly eta;
|
|
19
|
+
private readonly entries;
|
|
20
|
+
constructor(options?: Partial<ConstructorParameters<typeof Eta>[0]>);
|
|
21
|
+
get(source: string): EtaCompiledTemplate;
|
|
22
|
+
clear(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Number of compiled templates currently held in the cache.
|
|
25
|
+
*
|
|
26
|
+
* Exposed for diagnostics and tests; not part of the public contract.
|
|
27
|
+
*/
|
|
28
|
+
get size(): number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Convenience constructor used by the default factory.
|
|
32
|
+
*
|
|
33
|
+
* Tests can swap in a custom cache via the factory constructor; the rest of
|
|
34
|
+
* the subsystem talks only to the `EtaTemplateCache` interface.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createEtaTemplateCache(options?: Partial<ConstructorParameters<typeof Eta>[0]>): EtaTemplateCache;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/eta/cache.ts
|
|
2
|
+
import { Eta } from 'eta/core';
|
|
3
|
+
/**
|
|
4
|
+
* Default cache backed by a single Eta instance.
|
|
5
|
+
*
|
|
6
|
+
* The Eta class compiles templates lazily and stores the resulting function
|
|
7
|
+
* strings. Wrapping it in a small cache gives us:
|
|
8
|
+
*
|
|
9
|
+
* - a stable `source -> compiled` map that survives across renders
|
|
10
|
+
* - a single, well-defined place that knows about Eta tags / options
|
|
11
|
+
* - a `clear` hook so tests and HMR can drop the cache when templates change
|
|
12
|
+
*
|
|
13
|
+
* Compilation happens on first access for a given source. The cache is keyed
|
|
14
|
+
* by the raw source string, so two equivalent template strings share a single
|
|
15
|
+
* compiled entry.
|
|
16
|
+
*/
|
|
17
|
+
export class DefaultEtaTemplateCache {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.entries = new Map();
|
|
20
|
+
this.eta = new Eta({
|
|
21
|
+
tags: ['{{', '}}'],
|
|
22
|
+
...options
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
get(source) {
|
|
26
|
+
let compiled = this.entries.get(source);
|
|
27
|
+
if (compiled === undefined) {
|
|
28
|
+
// Eta's compile() is bound to the instance. We bind once so callers
|
|
29
|
+
// can invoke the cached function without an explicit `this`.
|
|
30
|
+
compiled = this.eta.compile(source).bind(this.eta);
|
|
31
|
+
this.entries.set(source, compiled);
|
|
32
|
+
}
|
|
33
|
+
return compiled;
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.entries.clear();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Number of compiled templates currently held in the cache.
|
|
40
|
+
*
|
|
41
|
+
* Exposed for diagnostics and tests; not part of the public contract.
|
|
42
|
+
*/
|
|
43
|
+
get size() {
|
|
44
|
+
return this.entries.size;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Convenience constructor used by the default factory.
|
|
49
|
+
*
|
|
50
|
+
* Tests can swap in a custom cache via the factory constructor; the rest of
|
|
51
|
+
* the subsystem talks only to the `EtaTemplateCache` interface.
|
|
52
|
+
*/
|
|
53
|
+
export function createEtaTemplateCache(options) {
|
|
54
|
+
return new DefaultEtaTemplateCache(options);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=cache.js.map
|