@repobit/dex-store-elements 1.7.2 → 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.
Files changed (227) hide show
  1. package/CHANGELOG.md +5 -905
  2. package/README.md +9 -7
  3. package/dist/src/actions/action.adapter.d.ts +36 -0
  4. package/dist/src/actions/action.adapter.js +183 -0
  5. package/dist/src/actions/action.adapter.js.map +1 -0
  6. package/dist/src/actions/action.attributes.d.ts +15 -0
  7. package/dist/src/actions/action.attributes.js +137 -0
  8. package/dist/src/actions/action.attributes.js.map +1 -0
  9. package/dist/src/actions/action.registry.d.ts +4 -0
  10. package/dist/src/actions/action.registry.js +119 -0
  11. package/dist/src/actions/action.registry.js.map +1 -0
  12. package/dist/src/actions/index.d.ts +3 -2
  13. package/dist/src/actions/index.js +3 -80
  14. package/dist/src/actions/index.js.map +1 -1
  15. package/dist/src/compute/compute.expand.d.ts +70 -0
  16. package/dist/src/compute/compute.expand.js +319 -0
  17. package/dist/src/compute/compute.expand.js.map +1 -0
  18. package/dist/src/compute/compute.scan.d.ts +54 -0
  19. package/dist/src/compute/compute.scan.js +160 -0
  20. package/dist/src/compute/compute.scan.js.map +1 -0
  21. package/dist/src/compute/option-resolver.d.ts +31 -0
  22. package/dist/src/compute/option-resolver.js +147 -0
  23. package/dist/src/compute/option-resolver.js.map +1 -0
  24. package/dist/src/contexts/context.compute.d.ts +12 -0
  25. package/dist/src/contexts/context.compute.js +4 -0
  26. package/dist/src/contexts/context.compute.js.map +1 -0
  27. package/dist/src/contexts/context.scope.d.ts +139 -0
  28. package/dist/src/contexts/context.scope.js +48 -0
  29. package/dist/src/contexts/context.scope.js.map +1 -0
  30. package/dist/src/controllers/controller.action-resolve.d.ts +110 -0
  31. package/dist/src/controllers/controller.action-resolve.js +203 -0
  32. package/dist/src/controllers/controller.action-resolve.js.map +1 -0
  33. package/dist/src/controllers/controller.action-topology.d.ts +59 -0
  34. package/dist/src/controllers/controller.action-topology.js +130 -0
  35. package/dist/src/controllers/controller.action-topology.js.map +1 -0
  36. package/dist/src/controllers/controller.compute.d.ts +72 -0
  37. package/dist/src/controllers/controller.compute.js +110 -0
  38. package/dist/src/controllers/controller.compute.js.map +1 -0
  39. package/dist/src/controllers/controller.resolve.d.ts +78 -0
  40. package/dist/src/controllers/controller.resolve.js +205 -0
  41. package/dist/src/controllers/controller.resolve.js.map +1 -0
  42. package/dist/src/core/action-topology.d.ts +62 -0
  43. package/dist/src/core/action-topology.js +67 -0
  44. package/dist/src/core/action-topology.js.map +1 -0
  45. package/dist/src/core/actions.d.ts +64 -0
  46. package/dist/src/core/actions.js +47 -0
  47. package/dist/src/core/actions.js.map +1 -0
  48. package/dist/src/core/node.element.d.ts +179 -0
  49. package/dist/src/core/node.element.js +573 -0
  50. package/dist/src/core/node.element.js.map +1 -0
  51. package/dist/src/core/node.scope.d.ts +374 -0
  52. package/dist/src/core/node.scope.js +558 -0
  53. package/dist/src/core/node.scope.js.map +1 -0
  54. package/dist/src/core/option-update.d.ts +11 -0
  55. package/dist/src/core/option-update.js +59 -0
  56. package/dist/src/core/option-update.js.map +1 -0
  57. package/dist/src/dsl/compilers/array/compiler.js +13 -14
  58. package/dist/src/dsl/compilers/array/compiler.js.map +1 -1
  59. package/dist/src/dsl/compilers/boolean/compiler.d.ts +1 -1
  60. package/dist/src/dsl/compilers/boolean/compiler.js +14 -27
  61. package/dist/src/dsl/compilers/boolean/compiler.js.map +1 -1
  62. package/dist/src/dsl/compilers/enum/compiler.js +7 -10
  63. package/dist/src/dsl/compilers/enum/compiler.js.map +1 -1
  64. package/dist/src/dsl/compilers/index.d.ts +1 -1
  65. package/dist/src/dsl/compilers/index.js +5 -5
  66. package/dist/src/dsl/compilers/index.js.map +1 -1
  67. package/dist/src/dsl/utilty.d.ts +0 -4
  68. package/dist/src/dsl/utilty.js +3 -30
  69. package/dist/src/dsl/utilty.js.map +1 -1
  70. package/dist/src/eta/batch.d.ts +123 -0
  71. package/dist/src/eta/batch.js +162 -0
  72. package/dist/src/eta/batch.js.map +1 -0
  73. package/dist/src/eta/batcher.d.ts +184 -0
  74. package/dist/src/eta/batcher.js +335 -0
  75. package/dist/src/eta/batcher.js.map +1 -0
  76. package/dist/src/eta/cache.d.ts +36 -0
  77. package/dist/src/eta/cache.js +56 -0
  78. package/dist/src/eta/cache.js.map +1 -0
  79. package/dist/src/eta/controller.d.ts +608 -0
  80. package/dist/src/eta/controller.js +1036 -0
  81. package/dist/src/eta/controller.js.map +1 -0
  82. package/dist/src/eta/factory.d.ts +74 -0
  83. package/dist/src/eta/factory.js +121 -0
  84. package/dist/src/eta/factory.js.map +1 -0
  85. package/dist/src/eta/idle.d.ts +39 -0
  86. package/dist/src/eta/idle.js +52 -0
  87. package/dist/src/eta/idle.js.map +1 -0
  88. package/dist/src/eta/index.d.ts +52 -0
  89. package/dist/src/eta/index.js +13 -0
  90. package/dist/src/eta/index.js.map +1 -0
  91. package/dist/src/eta/observer.d.ts +263 -0
  92. package/dist/src/eta/observer.js +263 -0
  93. package/dist/src/eta/observer.js.map +1 -0
  94. package/dist/src/eta/operations.d.ts +41 -0
  95. package/dist/src/eta/operations.js +59 -0
  96. package/dist/src/eta/operations.js.map +1 -0
  97. package/dist/src/eta/source-registry.d.ts +198 -0
  98. package/dist/src/eta/source-registry.js +267 -0
  99. package/dist/src/eta/source-registry.js.map +1 -0
  100. package/dist/src/eta/template-index.d.ts +285 -0
  101. package/dist/src/eta/template-index.js +642 -0
  102. package/dist/src/eta/template-index.js.map +1 -0
  103. package/dist/src/eta/types.d.ts +529 -0
  104. package/dist/src/eta/types.js +2 -0
  105. package/dist/src/eta/types.js.map +1 -0
  106. package/dist/src/index.d.ts +21 -9
  107. package/dist/src/index.js +22 -11
  108. package/dist/src/index.js.map +1 -1
  109. package/dist/src/nodes/node.context.d.ts +5 -3
  110. package/dist/src/nodes/node.context.js +48 -3
  111. package/dist/src/nodes/node.context.js.map +1 -1
  112. package/dist/src/nodes/node.option.d.ts +11 -31
  113. package/dist/src/nodes/node.option.js +158 -222
  114. package/dist/src/nodes/node.option.js.map +1 -1
  115. package/dist/src/nodes/node.product.d.ts +7 -11
  116. package/dist/src/nodes/node.product.js +85 -64
  117. package/dist/src/nodes/node.product.js.map +1 -1
  118. package/dist/src/renders/attributes/buyLink.d.ts +2 -2
  119. package/dist/src/renders/attributes/buyLink.js +14 -11
  120. package/dist/src/renders/attributes/buyLink.js.map +1 -1
  121. package/dist/src/renders/attributes/devices.d.ts +2 -2
  122. package/dist/src/renders/attributes/devices.js +9 -19
  123. package/dist/src/renders/attributes/devices.js.map +1 -1
  124. package/dist/src/renders/attributes/discount.d.ts +2 -2
  125. package/dist/src/renders/attributes/discount.js +56 -44
  126. package/dist/src/renders/attributes/discount.js.map +1 -1
  127. package/dist/src/renders/attributes/hide.d.ts +2 -2
  128. package/dist/src/renders/attributes/hide.js +49 -14
  129. package/dist/src/renders/attributes/hide.js.map +1 -1
  130. package/dist/src/renders/attributes/index.d.ts +3 -3
  131. package/dist/src/renders/attributes/index.js +2 -4
  132. package/dist/src/renders/attributes/index.js.map +1 -1
  133. package/dist/src/renders/attributes/price.d.ts +2 -2
  134. package/dist/src/renders/attributes/price.js +30 -44
  135. package/dist/src/renders/attributes/price.js.map +1 -1
  136. package/dist/src/renders/attributes/subscription.d.ts +2 -2
  137. package/dist/src/renders/attributes/subscription.js +14 -20
  138. package/dist/src/renders/attributes/subscription.js.map +1 -1
  139. package/dist/src/renders/attributes/utilty.d.ts +2 -0
  140. package/dist/src/renders/attributes/utilty.js +17 -4
  141. package/dist/src/renders/attributes/utilty.js.map +1 -1
  142. package/dist/src/renders/context.d.ts +122 -10
  143. package/dist/src/renders/context.js +140 -80
  144. package/dist/src/renders/context.js.map +1 -1
  145. package/dist/src/renders/format.js +2 -2
  146. package/dist/src/renders/format.js.map +1 -1
  147. package/dist/src/renders/index.d.ts +6 -2
  148. package/dist/src/renders/index.js +67 -29
  149. package/dist/src/renders/index.js.map +1 -1
  150. package/dist/src/renders/observe.d.ts +1 -4
  151. package/dist/src/renders/observe.js +10 -24
  152. package/dist/src/renders/observe.js.map +1 -1
  153. package/dist/src/renders/render.wrapper.d.ts +37 -0
  154. package/dist/src/renders/render.wrapper.js +147 -0
  155. package/dist/src/renders/render.wrapper.js.map +1 -0
  156. package/dist/src/templating/eta.js +1 -4
  157. package/dist/src/templating/eta.js.map +1 -1
  158. package/package.json +4 -5
  159. package/dist/src/actions/action.button.d.ts +0 -2
  160. package/dist/src/actions/action.button.js +0 -8
  161. package/dist/src/actions/action.button.js.map +0 -1
  162. package/dist/src/actions/action.input.d.ts +0 -2
  163. package/dist/src/actions/action.input.js +0 -43
  164. package/dist/src/actions/action.input.js.map +0 -1
  165. package/dist/src/actions/action.select.d.ts +0 -2
  166. package/dist/src/actions/action.select.js +0 -14
  167. package/dist/src/actions/action.select.js.map +0 -1
  168. package/dist/src/actions/utilty.d.ts +0 -16
  169. package/dist/src/actions/utilty.js +0 -149
  170. package/dist/src/actions/utilty.js.map +0 -1
  171. package/dist/src/contexts/context.datalayer.d.ts +0 -9
  172. package/dist/src/contexts/context.datalayer.js +0 -3
  173. package/dist/src/contexts/context.datalayer.js.map +0 -1
  174. package/dist/src/contexts/context.derived.d.ts +0 -12
  175. package/dist/src/contexts/context.derived.js +0 -3
  176. package/dist/src/contexts/context.derived.js.map +0 -1
  177. package/dist/src/contexts/context.option.d.ts +0 -5
  178. package/dist/src/contexts/context.option.js +0 -3
  179. package/dist/src/contexts/context.option.js.map +0 -1
  180. package/dist/src/contexts/context.product.d.ts +0 -5
  181. package/dist/src/contexts/context.product.js +0 -3
  182. package/dist/src/contexts/context.product.js.map +0 -1
  183. package/dist/src/contexts/context.state.d.ts +0 -28
  184. package/dist/src/contexts/context.state.js +0 -3
  185. package/dist/src/contexts/context.state.js.map +0 -1
  186. package/dist/src/contexts/context.store.d.ts +0 -5
  187. package/dist/src/contexts/context.store.js +0 -3
  188. package/dist/src/contexts/context.store.js.map +0 -1
  189. package/dist/src/controllers/collect-controller.d.ts +0 -39
  190. package/dist/src/controllers/collect-controller.js +0 -161
  191. package/dist/src/controllers/collect-controller.js.map +0 -1
  192. package/dist/src/controllers/compute-controller.d.ts +0 -55
  193. package/dist/src/controllers/compute-controller.js +0 -350
  194. package/dist/src/controllers/compute-controller.js.map +0 -1
  195. package/dist/src/controllers/eta-renderer/eta-dom-renderer.d.ts +0 -25
  196. package/dist/src/controllers/eta-renderer/eta-dom-renderer.js +0 -192
  197. package/dist/src/controllers/eta-renderer/eta-dom-renderer.js.map +0 -1
  198. package/dist/src/controllers/eta-renderer/eta-mutation-observer.d.ts +0 -28
  199. package/dist/src/controllers/eta-renderer/eta-mutation-observer.js +0 -206
  200. package/dist/src/controllers/eta-renderer/eta-mutation-observer.js.map +0 -1
  201. package/dist/src/controllers/eta-renderer/eta-renderer.d.ts +0 -21
  202. package/dist/src/controllers/eta-renderer/eta-renderer.js +0 -77
  203. package/dist/src/controllers/eta-renderer/eta-renderer.js.map +0 -1
  204. package/dist/src/controllers/eta-renderer/eta-template-cache.d.ts +0 -24
  205. package/dist/src/controllers/eta-renderer/eta-template-cache.js +0 -61
  206. package/dist/src/controllers/eta-renderer/eta-template-cache.js.map +0 -1
  207. package/dist/src/controllers/eta-renderer/eta-types.d.ts +0 -11
  208. package/dist/src/controllers/eta-renderer/eta-types.js +0 -1
  209. package/dist/src/controllers/eta-renderer/eta-types.js.map +0 -1
  210. package/dist/src/controllers/eta-renderer/index.d.ts +0 -2
  211. package/dist/src/controllers/eta-renderer/index.js +0 -2
  212. package/dist/src/controllers/eta-renderer/index.js.map +0 -1
  213. package/dist/src/controllers/event-pipeline-controller.d.ts +0 -28
  214. package/dist/src/controllers/event-pipeline-controller.js +0 -72
  215. package/dist/src/controllers/event-pipeline-controller.js.map +0 -1
  216. package/dist/src/events/events.d.ts +0 -75
  217. package/dist/src/events/events.js +0 -78
  218. package/dist/src/events/events.js.map +0 -1
  219. package/dist/src/nodes/node.root.d.ts +0 -16
  220. package/dist/src/nodes/node.root.js +0 -49
  221. package/dist/src/nodes/node.root.js.map +0 -1
  222. package/dist/src/nodes/node.state.d.ts +0 -88
  223. package/dist/src/nodes/node.state.js +0 -330
  224. package/dist/src/nodes/node.state.js.map +0 -1
  225. package/dist/src/renders/utility.d.ts +0 -32
  226. package/dist/src/renders/utility.js +0 -191
  227. 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