@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.
Files changed (227) hide show
  1. package/CHANGELOG.md +5 -913
  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,263 @@
1
+ /**
2
+ * Options accepted by `EtaMutationObserver`.
3
+ */
4
+ export interface EtaMutationObserverOptions {
5
+ /**
6
+ * Optional callback fired after a batch of mutations has been
7
+ * forwarded. The controller uses this to schedule a fresh
8
+ * render whenever the DOM has changed.
9
+ *
10
+ * The observer only raises `onChange` from
11
+ * `flushPendingRecords({ notify: true })` and from the
12
+ * `notify`-aware parts of `disconnect()` — the live
13
+ * `MutationObserver` callback path does NOT raise `onChange`
14
+ * because the consumer (typically the batcher) decides when
15
+ * to render from its own scheduling logic.
16
+ */
17
+ readonly onChange?: () => void;
18
+ /**
19
+ * Optional callback fired with the raw `MutationRecord[]` batch
20
+ * produced by the underlying `MutationObserver`.
21
+ *
22
+ * The observer's single responsibility is to translate browser
23
+ * MutationObserver records and forward them to this callback.
24
+ * The consumer (typically the `EtaMutationBatcher`) is
25
+ * responsible for folding the records into the index.
26
+ */
27
+ readonly onRecords?: (records: readonly MutationRecord[]) => void;
28
+ /**
29
+ * Optional predicate evaluated for each attribute record.
30
+ * Returning `false` drops the record entirely. Used to ignore
31
+ * attributes the consumer does not care about (e.g. `class` /
32
+ * `style` flips on Lit hosts).
33
+ *
34
+ * **Has no effect when `onRecords` is provided.** The observer
35
+ * forwards the raw record array to `onRecords` without
36
+ * filtering — the consumer owns the filtering decision. This
37
+ * avoids double-filtering the same record.
38
+ */
39
+ readonly isAttributeInteresting?: (name: string) => boolean;
40
+ /**
41
+ * Optional list of attribute names the underlying
42
+ * `MutationObserver` should watch. Passed through as
43
+ * `MutationObserverInit.attributeFilter` so the browser itself
44
+ * only emits records for the listed attributes — a much cheaper
45
+ * filter than letting every attribute flip fire a callback.
46
+ *
47
+ * `undefined` means "watch every attribute". Tests that do not
48
+ * care leave this unset.
49
+ */
50
+ readonly attributeNames?: readonly string[];
51
+ }
52
+ /**
53
+ * Translates browser `MutationObserver` records and forwards them
54
+ * as a raw `MutationRecord[]` batch to the optional `onRecords`
55
+ * callback.
56
+ *
57
+ * The observer is intentionally thin:
58
+ *
59
+ * - It knows about the DOM MutationRecord shape.
60
+ * - It does not know about Eta, scope, the index data structure, or
61
+ * the factory.
62
+ *
63
+ * This lets us unit-test it without a real Eta instance, and lets
64
+ * the controller swap in a different consumer (for example, a
65
+ * batcher) without touching the observer.
66
+ *
67
+ * **Routing.** When `options.onRecords` is provided, the observer
68
+ * forwards the raw `MutationRecord[]` batch to that callback and
69
+ * stops. The batcher is the canonical consumer of this path.
70
+ * `onChange` is NOT raised from the live observer callback — the
71
+ * consumer decides when to render from its own scheduling logic.
72
+ *
73
+ * **Dirty signal.** Whether `onChange` fires depends on the
74
+ * `notify` option of `flushPendingRecords` /
75
+ * `disconnect({ flush, notify })`. The observer itself does not
76
+ * know whether forwarded records were dirty — that decision is
77
+ * the consumer's.
78
+ *
79
+ * Self-induced mutations (e.g. a `TextOperation.execute` writing a
80
+ * new `nodeValue`) are dropped during a `suspendSync(callback)` block.
81
+ * The controller wraps the render loop in `suspendSync(...)` so the
82
+ * records produced by the operations themselves do not get fed
83
+ * back to the consumer as fresh mutations. Before entering that
84
+ * block the controller also calls `flushPendingRecords({ notify:
85
+ * false })` so any external records already queued by the browser
86
+ * are forwarded first; otherwise the `takeRecords()` drain inside
87
+ * `suspendSync`'s `finally` would discard those external records
88
+ * together with the self-induced ones.
89
+ *
90
+ * **Attribute namespace scope.** Attribute observation currently
91
+ * treats attributes as `Element + string` (qualified name). SVG
92
+ * and other namespaced attributes are intentionally out of scope:
93
+ * the observer does not thread `MutationRecord.attributeNamespace`
94
+ * and the index/factory do not call `getAttributeNS`. Future
95
+ * versions that need namespace awareness should propagate
96
+ * `record.attributeNamespace` through whatever attribute branch
97
+ * the consumer uses and add a `getAttributeNS`-based path in the
98
+ * factory. For non-namespaced HTML, which is the current target
99
+ * surface, the qualified-name model is sufficient.
100
+ */
101
+ export declare class EtaMutationObserver {
102
+ private readonly observer;
103
+ private readonly onChange?;
104
+ private readonly onRecords?;
105
+ private readonly isAttributeInteresting?;
106
+ private readonly attributeNames?;
107
+ private suspended;
108
+ constructor(options?: EtaMutationObserverOptions);
109
+ /**
110
+ * Starts observing the given target.
111
+ *
112
+ * `subtree: true` ensures the observer sees mutations in the
113
+ * entire scoped boundary, not just the direct children of
114
+ * `target`.
115
+ *
116
+ * If `attributeNames` was supplied to the constructor it is
117
+ * forwarded to the browser as
118
+ * `MutationObserverInit.attributeFilter`. The browser will then
119
+ * only emit attribute records for the listed attributes, which
120
+ * keeps noisy Lit updates (`class` / `style` / reactive
121
+ * properties) from triggering downstream work.
122
+ */
123
+ observe(target: Node): void;
124
+ /**
125
+ * Stops observing and releases the underlying MutationObserver.
126
+ *
127
+ * Two modes:
128
+ *
129
+ * - **`flush: true`** — drains any queued records through
130
+ * `onRecords` BEFORE disconnecting, so any consumer (the
131
+ * batcher) can absorb them. Use this from a controller's
132
+ * `hostDisconnected()` so an external mutation that landed
133
+ * just before disconnect is not silently dropped.
134
+ * - **`flush: false` (default, current behaviour)** — drains the
135
+ * queue via `takeRecords()` only. Records are discarded
136
+ * without ever being forwarded.
137
+ *
138
+ * In both modes the underlying `MutationObserver.disconnect()`
139
+ * is the last call so delivery is stopped after the drain.
140
+ *
141
+ * `notify` only takes effect when `flush: true`; it controls
142
+ * whether a successful flush invokes `onChange`. Callers that
143
+ * are about to teardown (for example `hostDisconnected()`)
144
+ * should pass `notify: false` to avoid scheduling a render
145
+ * during teardown.
146
+ */
147
+ disconnect(options?: {
148
+ flush?: boolean;
149
+ notify?: boolean;
150
+ }): void;
151
+ /**
152
+ * Drains any records currently queued by the underlying
153
+ * `MutationObserver` and forwards them to `onRecords`.
154
+ *
155
+ * The entire record batch is forwarded as-is — no per-record
156
+ * mapping is performed here. The consumer (the batcher) is
157
+ * responsible for folding records into the index.
158
+ *
159
+ * Use `flushPendingRecords()` when the controller is about to
160
+ * read the index and must not miss legitimate external
161
+ * mutations that landed between the last render and the next
162
+ * one. Without this flush, a render that starts inside
163
+ * `suspendSync(callback)` would call `takeRecords()` in its
164
+ * `finally` block and discard those external records together
165
+ * with the self-induced render records.
166
+ *
167
+ * Returns:
168
+ *
169
+ * - `true` when at least one record was forwarded, `false` when
170
+ * the queue was empty. The caller cannot tell dirty from
171
+ * non-dirty at this layer — the consumer decides dirty on
172
+ * its own tick.
173
+ *
174
+ * **`notify` semantics.** Pass `notify: false` when the flush
175
+ * happens immediately before a render that is already running,
176
+ * to avoid re-entering `task.run()` from inside an in-flight
177
+ * render pass. Pass `notify: true` (the default) when the flush
178
+ * happens outside a render pass and the caller wants the
179
+ * observer's `onChange` scheduling to kick in.
180
+ *
181
+ * `flushPendingRecords()` is synchronous — the records are
182
+ * drained via `takeRecords()` and processed in the same call
183
+ * stack.
184
+ */
185
+ flushPendingRecords(options?: {
186
+ notify?: boolean;
187
+ }): boolean;
188
+ /**
189
+ * Drops any records currently queued by the underlying
190
+ * `MutationObserver` without forwarding them.
191
+ *
192
+ * Most callers should prefer `suspendSync(callback)` so the drain
193
+ * happens immediately after the work that produced the records,
194
+ * or `flushPendingRecords()` when the queued records might
195
+ * contain legitimate external mutations that need to reach the
196
+ * consumer. `takeRecords()` is exposed for tests and advanced
197
+ * consumers that need to inspect the drained records directly.
198
+ */
199
+ takeRecords(): MutationRecord[];
200
+ /**
201
+ * Runs `callback` while suspending record forwarding.
202
+ *
203
+ * While suspended, any records delivered by the underlying
204
+ * `MutationObserver` are dropped on the floor (no consumer
205
+ * notification, no `onChange` callback). This is the right
206
+ * mode for render passes that write to the DOM — the writes
207
+ * should not be fed back as fresh mutations.
208
+ *
209
+ * After `callback` returns, the observer drains whatever the
210
+ * underlying `MutationObserver` has queued via `takeRecords()` so
211
+ * subsequent ticks start from a clean slate.
212
+ *
213
+ * The flag is restored in a `finally` block so a thrown callback
214
+ * does not leave the observer stuck in the suspended state.
215
+ *
216
+ * **Sync-only.** Operation execution and the typical render pass
217
+ * are synchronous, so this method runs `callback` to completion
218
+ * before restoring forwarding. Calling `suspendSync()` with an
219
+ * async function will NOT keep records suppressed past the
220
+ * synchronous portion of the callback — once the callback returns
221
+ * its first `await` boundary (or the callback's promise settles)
222
+ * the suspended flag has already been cleared. If async
223
+ * suppression is ever needed, a separate `suspendAsync` method
224
+ * should be added; it is intentionally not provided today.
225
+ */
226
+ suspendSync(callback: () => void): void;
227
+ /**
228
+ * Handles a batch of records produced by the browser.
229
+ *
230
+ * Records are processed in the order returned by the browser.
231
+ *
232
+ * The entire record batch is forwarded to `onRecords` as-is and
233
+ * the method returns immediately. The consumer (the batcher) is
234
+ * responsible for folding records into the index; the observer
235
+ * no longer maps records into `TemplateIndexMutation` values
236
+ * inline. `onChange` is also NOT raised from this path — the
237
+ * consumer triggers renders from its own scheduling so that
238
+ * "dirty" is decided after compaction, not per record.
239
+ *
240
+ * While `suspended` is true the callback drops every record
241
+ * without forwarding it; this is the path used during a render
242
+ * pass to discard self-induced mutations.
243
+ *
244
+ * **Suspended guard is defensive.** During normal DOM
245
+ * scheduling MutationObserver callbacks do not fire in the
246
+ * middle of a synchronous callback — the browser queues
247
+ * records and delivers them via a microtask once the current
248
+ * call stack unwinds. Self-induced records from a render pass
249
+ * are drained via `takeRecords()` after `suspendSync()`
250
+ * returns, so the `suspended` flag is normally `false` by the
251
+ * time the callback fires. The guard exists to prevent
252
+ * `handleRecords()` from running if the observer is somehow
253
+ * invoked manually while suspended — for example by tests that
254
+ * drive the callback directly or by future scheduling that
255
+ * re-enters this method synchronously from inside a render
256
+ * pass.
257
+ */
258
+ private readonly handleRecords;
259
+ }
260
+ /**
261
+ * Convenience helper used by the default controller.
262
+ */
263
+ export declare function createEtaMutationObserver(options?: EtaMutationObserverOptions): EtaMutationObserver;
@@ -0,0 +1,263 @@
1
+ // src/eta/observer.ts
2
+ /**
3
+ * Translates browser `MutationObserver` records and forwards them
4
+ * as a raw `MutationRecord[]` batch to the optional `onRecords`
5
+ * callback.
6
+ *
7
+ * The observer is intentionally thin:
8
+ *
9
+ * - It knows about the DOM MutationRecord shape.
10
+ * - It does not know about Eta, scope, the index data structure, or
11
+ * the factory.
12
+ *
13
+ * This lets us unit-test it without a real Eta instance, and lets
14
+ * the controller swap in a different consumer (for example, a
15
+ * batcher) without touching the observer.
16
+ *
17
+ * **Routing.** When `options.onRecords` is provided, the observer
18
+ * forwards the raw `MutationRecord[]` batch to that callback and
19
+ * stops. The batcher is the canonical consumer of this path.
20
+ * `onChange` is NOT raised from the live observer callback — the
21
+ * consumer decides when to render from its own scheduling logic.
22
+ *
23
+ * **Dirty signal.** Whether `onChange` fires depends on the
24
+ * `notify` option of `flushPendingRecords` /
25
+ * `disconnect({ flush, notify })`. The observer itself does not
26
+ * know whether forwarded records were dirty — that decision is
27
+ * the consumer's.
28
+ *
29
+ * Self-induced mutations (e.g. a `TextOperation.execute` writing a
30
+ * new `nodeValue`) are dropped during a `suspendSync(callback)` block.
31
+ * The controller wraps the render loop in `suspendSync(...)` so the
32
+ * records produced by the operations themselves do not get fed
33
+ * back to the consumer as fresh mutations. Before entering that
34
+ * block the controller also calls `flushPendingRecords({ notify:
35
+ * false })` so any external records already queued by the browser
36
+ * are forwarded first; otherwise the `takeRecords()` drain inside
37
+ * `suspendSync`'s `finally` would discard those external records
38
+ * together with the self-induced ones.
39
+ *
40
+ * **Attribute namespace scope.** Attribute observation currently
41
+ * treats attributes as `Element + string` (qualified name). SVG
42
+ * and other namespaced attributes are intentionally out of scope:
43
+ * the observer does not thread `MutationRecord.attributeNamespace`
44
+ * and the index/factory do not call `getAttributeNS`. Future
45
+ * versions that need namespace awareness should propagate
46
+ * `record.attributeNamespace` through whatever attribute branch
47
+ * the consumer uses and add a `getAttributeNS`-based path in the
48
+ * factory. For non-namespaced HTML, which is the current target
49
+ * surface, the qualified-name model is sufficient.
50
+ */
51
+ export class EtaMutationObserver {
52
+ constructor(options) {
53
+ this.suspended = false;
54
+ /**
55
+ * Handles a batch of records produced by the browser.
56
+ *
57
+ * Records are processed in the order returned by the browser.
58
+ *
59
+ * The entire record batch is forwarded to `onRecords` as-is and
60
+ * the method returns immediately. The consumer (the batcher) is
61
+ * responsible for folding records into the index; the observer
62
+ * no longer maps records into `TemplateIndexMutation` values
63
+ * inline. `onChange` is also NOT raised from this path — the
64
+ * consumer triggers renders from its own scheduling so that
65
+ * "dirty" is decided after compaction, not per record.
66
+ *
67
+ * While `suspended` is true the callback drops every record
68
+ * without forwarding it; this is the path used during a render
69
+ * pass to discard self-induced mutations.
70
+ *
71
+ * **Suspended guard is defensive.** During normal DOM
72
+ * scheduling MutationObserver callbacks do not fire in the
73
+ * middle of a synchronous callback — the browser queues
74
+ * records and delivers them via a microtask once the current
75
+ * call stack unwinds. Self-induced records from a render pass
76
+ * are drained via `takeRecords()` after `suspendSync()`
77
+ * returns, so the `suspended` flag is normally `false` by the
78
+ * time the callback fires. The guard exists to prevent
79
+ * `handleRecords()` from running if the observer is somehow
80
+ * invoked manually while suspended — for example by tests that
81
+ * drive the callback directly or by future scheduling that
82
+ * re-enters this method synchronously from inside a render
83
+ * pass.
84
+ */
85
+ this.handleRecords = (records) => {
86
+ if (this.suspended) {
87
+ return;
88
+ }
89
+ this.onRecords?.(records);
90
+ };
91
+ this.onChange = options?.onChange;
92
+ this.onRecords = options?.onRecords;
93
+ this.isAttributeInteresting =
94
+ options?.isAttributeInteresting;
95
+ this.attributeNames = options?.attributeNames;
96
+ this.observer = new MutationObserver(this.handleRecords);
97
+ }
98
+ /**
99
+ * Starts observing the given target.
100
+ *
101
+ * `subtree: true` ensures the observer sees mutations in the
102
+ * entire scoped boundary, not just the direct children of
103
+ * `target`.
104
+ *
105
+ * If `attributeNames` was supplied to the constructor it is
106
+ * forwarded to the browser as
107
+ * `MutationObserverInit.attributeFilter`. The browser will then
108
+ * only emit attribute records for the listed attributes, which
109
+ * keeps noisy Lit updates (`class` / `style` / reactive
110
+ * properties) from triggering downstream work.
111
+ */
112
+ observe(target) {
113
+ const init = {
114
+ childList: true,
115
+ subtree: true,
116
+ attributes: true,
117
+ characterData: true
118
+ };
119
+ if (this.attributeNames &&
120
+ this.attributeNames.length > 0) {
121
+ init.attributeFilter = [
122
+ ...this.attributeNames
123
+ ];
124
+ }
125
+ this.observer.observe(target, init);
126
+ }
127
+ /**
128
+ * Stops observing and releases the underlying MutationObserver.
129
+ *
130
+ * Two modes:
131
+ *
132
+ * - **`flush: true`** — drains any queued records through
133
+ * `onRecords` BEFORE disconnecting, so any consumer (the
134
+ * batcher) can absorb them. Use this from a controller's
135
+ * `hostDisconnected()` so an external mutation that landed
136
+ * just before disconnect is not silently dropped.
137
+ * - **`flush: false` (default, current behaviour)** — drains the
138
+ * queue via `takeRecords()` only. Records are discarded
139
+ * without ever being forwarded.
140
+ *
141
+ * In both modes the underlying `MutationObserver.disconnect()`
142
+ * is the last call so delivery is stopped after the drain.
143
+ *
144
+ * `notify` only takes effect when `flush: true`; it controls
145
+ * whether a successful flush invokes `onChange`. Callers that
146
+ * are about to teardown (for example `hostDisconnected()`)
147
+ * should pass `notify: false` to avoid scheduling a render
148
+ * during teardown.
149
+ */
150
+ disconnect(options = {}) {
151
+ if (options.flush === true) {
152
+ this.flushPendingRecords({
153
+ notify: options.notify
154
+ });
155
+ }
156
+ else {
157
+ this.observer.takeRecords();
158
+ }
159
+ this.observer.disconnect();
160
+ }
161
+ /**
162
+ * Drains any records currently queued by the underlying
163
+ * `MutationObserver` and forwards them to `onRecords`.
164
+ *
165
+ * The entire record batch is forwarded as-is — no per-record
166
+ * mapping is performed here. The consumer (the batcher) is
167
+ * responsible for folding records into the index.
168
+ *
169
+ * Use `flushPendingRecords()` when the controller is about to
170
+ * read the index and must not miss legitimate external
171
+ * mutations that landed between the last render and the next
172
+ * one. Without this flush, a render that starts inside
173
+ * `suspendSync(callback)` would call `takeRecords()` in its
174
+ * `finally` block and discard those external records together
175
+ * with the self-induced render records.
176
+ *
177
+ * Returns:
178
+ *
179
+ * - `true` when at least one record was forwarded, `false` when
180
+ * the queue was empty. The caller cannot tell dirty from
181
+ * non-dirty at this layer — the consumer decides dirty on
182
+ * its own tick.
183
+ *
184
+ * **`notify` semantics.** Pass `notify: false` when the flush
185
+ * happens immediately before a render that is already running,
186
+ * to avoid re-entering `task.run()` from inside an in-flight
187
+ * render pass. Pass `notify: true` (the default) when the flush
188
+ * happens outside a render pass and the caller wants the
189
+ * observer's `onChange` scheduling to kick in.
190
+ *
191
+ * `flushPendingRecords()` is synchronous — the records are
192
+ * drained via `takeRecords()` and processed in the same call
193
+ * stack.
194
+ */
195
+ flushPendingRecords(options = {}) {
196
+ const records = this.observer.takeRecords();
197
+ if (records.length === 0) {
198
+ return false;
199
+ }
200
+ this.onRecords?.(records);
201
+ if (options.notify !== false) {
202
+ this.onChange?.();
203
+ }
204
+ return true;
205
+ }
206
+ /**
207
+ * Drops any records currently queued by the underlying
208
+ * `MutationObserver` without forwarding them.
209
+ *
210
+ * Most callers should prefer `suspendSync(callback)` so the drain
211
+ * happens immediately after the work that produced the records,
212
+ * or `flushPendingRecords()` when the queued records might
213
+ * contain legitimate external mutations that need to reach the
214
+ * consumer. `takeRecords()` is exposed for tests and advanced
215
+ * consumers that need to inspect the drained records directly.
216
+ */
217
+ takeRecords() {
218
+ return this.observer.takeRecords();
219
+ }
220
+ /**
221
+ * Runs `callback` while suspending record forwarding.
222
+ *
223
+ * While suspended, any records delivered by the underlying
224
+ * `MutationObserver` are dropped on the floor (no consumer
225
+ * notification, no `onChange` callback). This is the right
226
+ * mode for render passes that write to the DOM — the writes
227
+ * should not be fed back as fresh mutations.
228
+ *
229
+ * After `callback` returns, the observer drains whatever the
230
+ * underlying `MutationObserver` has queued via `takeRecords()` so
231
+ * subsequent ticks start from a clean slate.
232
+ *
233
+ * The flag is restored in a `finally` block so a thrown callback
234
+ * does not leave the observer stuck in the suspended state.
235
+ *
236
+ * **Sync-only.** Operation execution and the typical render pass
237
+ * are synchronous, so this method runs `callback` to completion
238
+ * before restoring forwarding. Calling `suspendSync()` with an
239
+ * async function will NOT keep records suppressed past the
240
+ * synchronous portion of the callback — once the callback returns
241
+ * its first `await` boundary (or the callback's promise settles)
242
+ * the suspended flag has already been cleared. If async
243
+ * suppression is ever needed, a separate `suspendAsync` method
244
+ * should be added; it is intentionally not provided today.
245
+ */
246
+ suspendSync(callback) {
247
+ this.suspended = true;
248
+ try {
249
+ callback();
250
+ }
251
+ finally {
252
+ this.suspended = false;
253
+ this.observer.takeRecords();
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * Convenience helper used by the default controller.
259
+ */
260
+ export function createEtaMutationObserver(options) {
261
+ return new EtaMutationObserver(options);
262
+ }
263
+ //# sourceMappingURL=observer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.js","sourceRoot":"","sources":["../../../src/eta/observer.ts"],"names":[],"mappings":"AAAA,sBAAsB;AA6DtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,MAAM,OAAO,mBAAmB;IAa9B,YAAY,OAAoC;QAFxC,cAAS,GAAG,KAAK,CAAC;QAgM1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WA8BG;QACc,kBAAa,GAAG,CAC/B,OAAyB,EACnB,EAAE;YACR,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YAED,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC,CAAC;QApOA,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,CAAC;QACpC,IAAI,CAAC,sBAAsB;YACzB,OAAO,EAAE,sBAAsB,CAAC;QAClC,IAAI,CAAC,cAAc,GAAG,OAAO,EAAE,cAAc,CAAC;QAE9C,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,CAClC,IAAI,CAAC,aAAa,CACnB,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,MAAY;QAClB,MAAM,IAAI,GAAyB;YACjC,SAAS,EAAM,IAAI;YACnB,OAAO,EAAQ,IAAI;YACnB,UAAU,EAAK,IAAI;YACnB,aAAa,EAAE,IAAI;SACpB,CAAC;QAEF,IACE,IAAI,CAAC,cAAc;YACnB,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAC9B,CAAC;YACD,IAAI,CAAC,eAAe,GAAG;gBACrB,GAAG,IAAI,CAAC,cAAc;aACvB,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACtC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,UAAU,CACR,UAGI,EAAE;QAEN,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YAC3B,IAAI,CAAC,mBAAmB,CAAC;gBACvB,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC9B,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,mBAAmB,CACjB,UAAgC,EAAE;QAElC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QAE5C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;QAE1B,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpB,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;OAUG;IACH,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,WAAW,CAAC,QAAoB;QAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QAEtB,IAAI,CAAC;YACH,QAAQ,EAAE,CAAC;QACb,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC9B,CAAC;IACH,CAAC;CA0CF;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAoC;IAEpC,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC","sourcesContent":["// src/eta/observer.ts\n\n/**\n * Options accepted by `EtaMutationObserver`.\n */\nexport interface EtaMutationObserverOptions {\n /**\n * Optional callback fired after a batch of mutations has been\n * forwarded. The controller uses this to schedule a fresh\n * render whenever the DOM has changed.\n *\n * The observer only raises `onChange` from\n * `flushPendingRecords({ notify: true })` and from the\n * `notify`-aware parts of `disconnect()` — the live\n * `MutationObserver` callback path does NOT raise `onChange`\n * because the consumer (typically the batcher) decides when\n * to render from its own scheduling logic.\n */\n readonly onChange?: () => void;\n\n /**\n * Optional callback fired with the raw `MutationRecord[]` batch\n * produced by the underlying `MutationObserver`.\n *\n * The observer's single responsibility is to translate browser\n * MutationObserver records and forward them to this callback.\n * The consumer (typically the `EtaMutationBatcher`) is\n * responsible for folding the records into the index.\n */\n readonly onRecords?: (\n records: readonly MutationRecord[]\n ) => void;\n\n /**\n * Optional predicate evaluated for each attribute record.\n * Returning `false` drops the record entirely. Used to ignore\n * attributes the consumer does not care about (e.g. `class` /\n * `style` flips on Lit hosts).\n *\n * **Has no effect when `onRecords` is provided.** The observer\n * forwards the raw record array to `onRecords` without\n * filtering — the consumer owns the filtering decision. This\n * avoids double-filtering the same record.\n */\n readonly isAttributeInteresting?: (\n name: string\n ) => boolean;\n\n /**\n * Optional list of attribute names the underlying\n * `MutationObserver` should watch. Passed through as\n * `MutationObserverInit.attributeFilter` so the browser itself\n * only emits records for the listed attributes — a much cheaper\n * filter than letting every attribute flip fire a callback.\n *\n * `undefined` means \"watch every attribute\". Tests that do not\n * care leave this unset.\n */\n readonly attributeNames?: readonly string[];\n}\n\n/**\n * Translates browser `MutationObserver` records and forwards them\n * as a raw `MutationRecord[]` batch to the optional `onRecords`\n * callback.\n *\n * The observer is intentionally thin:\n *\n * - It knows about the DOM MutationRecord shape.\n * - It does not know about Eta, scope, the index data structure, or\n * the factory.\n *\n * This lets us unit-test it without a real Eta instance, and lets\n * the controller swap in a different consumer (for example, a\n * batcher) without touching the observer.\n *\n * **Routing.** When `options.onRecords` is provided, the observer\n * forwards the raw `MutationRecord[]` batch to that callback and\n * stops. The batcher is the canonical consumer of this path.\n * `onChange` is NOT raised from the live observer callback — the\n * consumer decides when to render from its own scheduling logic.\n *\n * **Dirty signal.** Whether `onChange` fires depends on the\n * `notify` option of `flushPendingRecords` /\n * `disconnect({ flush, notify })`. The observer itself does not\n * know whether forwarded records were dirty — that decision is\n * the consumer's.\n *\n * Self-induced mutations (e.g. a `TextOperation.execute` writing a\n * new `nodeValue`) are dropped during a `suspendSync(callback)` block.\n * The controller wraps the render loop in `suspendSync(...)` so the\n * records produced by the operations themselves do not get fed\n * back to the consumer as fresh mutations. Before entering that\n * block the controller also calls `flushPendingRecords({ notify:\n * false })` so any external records already queued by the browser\n * are forwarded first; otherwise the `takeRecords()` drain inside\n * `suspendSync`'s `finally` would discard those external records\n * together with the self-induced ones.\n *\n * **Attribute namespace scope.** Attribute observation currently\n * treats attributes as `Element + string` (qualified name). SVG\n * and other namespaced attributes are intentionally out of scope:\n * the observer does not thread `MutationRecord.attributeNamespace`\n * and the index/factory do not call `getAttributeNS`. Future\n * versions that need namespace awareness should propagate\n * `record.attributeNamespace` through whatever attribute branch\n * the consumer uses and add a `getAttributeNS`-based path in the\n * factory. For non-namespaced HTML, which is the current target\n * surface, the qualified-name model is sufficient.\n */\nexport class EtaMutationObserver {\n private readonly observer: MutationObserver;\n\n private readonly onChange? : () => void;\n private readonly onRecords? : (\n records: readonly MutationRecord[]\n ) => void;\n private readonly isAttributeInteresting?: (\n name: string\n ) => boolean;\n private readonly attributeNames?: readonly string[];\n private suspended = false;\n\n constructor(options?: EtaMutationObserverOptions) {\n this.onChange = options?.onChange;\n this.onRecords = options?.onRecords;\n this.isAttributeInteresting =\n options?.isAttributeInteresting;\n this.attributeNames = options?.attributeNames;\n\n this.observer = new MutationObserver(\n this.handleRecords\n );\n }\n\n /**\n * Starts observing the given target.\n *\n * `subtree: true` ensures the observer sees mutations in the\n * entire scoped boundary, not just the direct children of\n * `target`.\n *\n * If `attributeNames` was supplied to the constructor it is\n * forwarded to the browser as\n * `MutationObserverInit.attributeFilter`. The browser will then\n * only emit attribute records for the listed attributes, which\n * keeps noisy Lit updates (`class` / `style` / reactive\n * properties) from triggering downstream work.\n */\n observe(target: Node): void {\n const init: MutationObserverInit = {\n childList : true,\n subtree : true,\n attributes : true,\n characterData: true\n };\n\n if (\n this.attributeNames &&\n this.attributeNames.length > 0\n ) {\n init.attributeFilter = [\n ...this.attributeNames\n ];\n }\n\n this.observer.observe(target, init);\n }\n\n /**\n * Stops observing and releases the underlying MutationObserver.\n *\n * Two modes:\n *\n * - **`flush: true`** — drains any queued records through\n * `onRecords` BEFORE disconnecting, so any consumer (the\n * batcher) can absorb them. Use this from a controller's\n * `hostDisconnected()` so an external mutation that landed\n * just before disconnect is not silently dropped.\n * - **`flush: false` (default, current behaviour)** — drains the\n * queue via `takeRecords()` only. Records are discarded\n * without ever being forwarded.\n *\n * In both modes the underlying `MutationObserver.disconnect()`\n * is the last call so delivery is stopped after the drain.\n *\n * `notify` only takes effect when `flush: true`; it controls\n * whether a successful flush invokes `onChange`. Callers that\n * are about to teardown (for example `hostDisconnected()`)\n * should pass `notify: false` to avoid scheduling a render\n * during teardown.\n */\n disconnect(\n options: {\n flush? : boolean;\n notify?: boolean;\n } = {}\n ): void {\n if (options.flush === true) {\n this.flushPendingRecords({\n notify: options.notify\n });\n } else {\n this.observer.takeRecords();\n }\n\n this.observer.disconnect();\n }\n\n /**\n * Drains any records currently queued by the underlying\n * `MutationObserver` and forwards them to `onRecords`.\n *\n * The entire record batch is forwarded as-is — no per-record\n * mapping is performed here. The consumer (the batcher) is\n * responsible for folding records into the index.\n *\n * Use `flushPendingRecords()` when the controller is about to\n * read the index and must not miss legitimate external\n * mutations that landed between the last render and the next\n * one. Without this flush, a render that starts inside\n * `suspendSync(callback)` would call `takeRecords()` in its\n * `finally` block and discard those external records together\n * with the self-induced render records.\n *\n * Returns:\n *\n * - `true` when at least one record was forwarded, `false` when\n * the queue was empty. The caller cannot tell dirty from\n * non-dirty at this layer — the consumer decides dirty on\n * its own tick.\n *\n * **`notify` semantics.** Pass `notify: false` when the flush\n * happens immediately before a render that is already running,\n * to avoid re-entering `task.run()` from inside an in-flight\n * render pass. Pass `notify: true` (the default) when the flush\n * happens outside a render pass and the caller wants the\n * observer's `onChange` scheduling to kick in.\n *\n * `flushPendingRecords()` is synchronous — the records are\n * drained via `takeRecords()` and processed in the same call\n * stack.\n */\n flushPendingRecords(\n options: { notify?: boolean } = {}\n ): boolean {\n const records = this.observer.takeRecords();\n\n if (records.length === 0) {\n return false;\n }\n\n this.onRecords?.(records);\n\n if (options.notify !== false) {\n this.onChange?.();\n }\n\n return true;\n }\n\n /**\n * Drops any records currently queued by the underlying\n * `MutationObserver` without forwarding them.\n *\n * Most callers should prefer `suspendSync(callback)` so the drain\n * happens immediately after the work that produced the records,\n * or `flushPendingRecords()` when the queued records might\n * contain legitimate external mutations that need to reach the\n * consumer. `takeRecords()` is exposed for tests and advanced\n * consumers that need to inspect the drained records directly.\n */\n takeRecords(): MutationRecord[] {\n return this.observer.takeRecords();\n }\n\n /**\n * Runs `callback` while suspending record forwarding.\n *\n * While suspended, any records delivered by the underlying\n * `MutationObserver` are dropped on the floor (no consumer\n * notification, no `onChange` callback). This is the right\n * mode for render passes that write to the DOM — the writes\n * should not be fed back as fresh mutations.\n *\n * After `callback` returns, the observer drains whatever the\n * underlying `MutationObserver` has queued via `takeRecords()` so\n * subsequent ticks start from a clean slate.\n *\n * The flag is restored in a `finally` block so a thrown callback\n * does not leave the observer stuck in the suspended state.\n *\n * **Sync-only.** Operation execution and the typical render pass\n * are synchronous, so this method runs `callback` to completion\n * before restoring forwarding. Calling `suspendSync()` with an\n * async function will NOT keep records suppressed past the\n * synchronous portion of the callback — once the callback returns\n * its first `await` boundary (or the callback's promise settles)\n * the suspended flag has already been cleared. If async\n * suppression is ever needed, a separate `suspendAsync` method\n * should be added; it is intentionally not provided today.\n */\n suspendSync(callback: () => void): void {\n this.suspended = true;\n\n try {\n callback();\n } finally {\n this.suspended = false;\n this.observer.takeRecords();\n }\n }\n\n /**\n * Handles a batch of records produced by the browser.\n *\n * Records are processed in the order returned by the browser.\n *\n * The entire record batch is forwarded to `onRecords` as-is and\n * the method returns immediately. The consumer (the batcher) is\n * responsible for folding records into the index; the observer\n * no longer maps records into `TemplateIndexMutation` values\n * inline. `onChange` is also NOT raised from this path — the\n * consumer triggers renders from its own scheduling so that\n * \"dirty\" is decided after compaction, not per record.\n *\n * While `suspended` is true the callback drops every record\n * without forwarding it; this is the path used during a render\n * pass to discard self-induced mutations.\n *\n * **Suspended guard is defensive.** During normal DOM\n * scheduling MutationObserver callbacks do not fire in the\n * middle of a synchronous callback — the browser queues\n * records and delivers them via a microtask once the current\n * call stack unwinds. Self-induced records from a render pass\n * are drained via `takeRecords()` after `suspendSync()`\n * returns, so the `suspended` flag is normally `false` by the\n * time the callback fires. The guard exists to prevent\n * `handleRecords()` from running if the observer is somehow\n * invoked manually while suspended — for example by tests that\n * drive the callback directly or by future scheduling that\n * re-enters this method synchronously from inside a render\n * pass.\n */\n private readonly handleRecords = (\n records: MutationRecord[]\n ): void => {\n if (this.suspended) {\n return;\n }\n\n this.onRecords?.(records);\n };\n}\n\n/**\n * Convenience helper used by the default controller.\n */\nexport function createEtaMutationObserver(\n options?: EtaMutationObserverOptions\n): EtaMutationObserver {\n return new EtaMutationObserver(options);\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { EtaCompiledTemplate, TemplateOperation } from './types.js';
2
+ /**
3
+ * Renders a text node from a compiled Eta template.
4
+ *
5
+ * The text node's `nodeValue` is overwritten with the rendered
6
+ * string on every `execute` call. This matches how Eta is used
7
+ * elsewhere in the codebase: templates produce text output and the
8
+ * runtime writes that text into the DOM.
9
+ *
10
+ * `dispose` is a no-op for text operations; the GC reclaims the
11
+ * closure once the index drops the reference.
12
+ */
13
+ export declare class TextOperation<TContext = object> implements TemplateOperation<TContext> {
14
+ readonly owner: Text;
15
+ private readonly template;
16
+ readonly kind: "text";
17
+ constructor(owner: Text, template: EtaCompiledTemplate);
18
+ execute(context: TContext): void;
19
+ dispose(): void;
20
+ }
21
+ /**
22
+ * Renders a single attribute on an element from a compiled Eta
23
+ * template.
24
+ *
25
+ * Every rendered value — including the empty string — is written
26
+ * via `setAttribute`. The runtime does not remove attributes as a
27
+ * side-effect of rendering; an empty template result is a valid
28
+ * attribute value and removing the attribute would change the
29
+ * element's semantics (e.g. `<input>` vs `<input value="">`).
30
+ *
31
+ * `dispose` is a no-op for attribute operations.
32
+ */
33
+ export declare class AttributeOperation<TContext = object> implements TemplateOperation<TContext> {
34
+ readonly owner: Element;
35
+ private readonly qualifiedName;
36
+ private readonly template;
37
+ readonly kind: "attribute";
38
+ constructor(owner: Element, qualifiedName: string, template: EtaCompiledTemplate);
39
+ execute(context: TContext): void;
40
+ dispose(): void;
41
+ }
@@ -0,0 +1,59 @@
1
+ // src/eta/operations.ts
2
+ /**
3
+ * Renders a text node from a compiled Eta template.
4
+ *
5
+ * The text node's `nodeValue` is overwritten with the rendered
6
+ * string on every `execute` call. This matches how Eta is used
7
+ * elsewhere in the codebase: templates produce text output and the
8
+ * runtime writes that text into the DOM.
9
+ *
10
+ * `dispose` is a no-op for text operations; the GC reclaims the
11
+ * closure once the index drops the reference.
12
+ */
13
+ export class TextOperation {
14
+ constructor(owner, template) {
15
+ this.owner = owner;
16
+ this.template = template;
17
+ this.kind = 'text';
18
+ }
19
+ execute(context) {
20
+ const rendered = this.template(context);
21
+ if (this.owner.nodeValue !== rendered) {
22
+ this.owner.nodeValue = rendered;
23
+ }
24
+ }
25
+ dispose() {
26
+ // TextOperation holds no listeners or observers.
27
+ }
28
+ }
29
+ /**
30
+ * Renders a single attribute on an element from a compiled Eta
31
+ * template.
32
+ *
33
+ * Every rendered value — including the empty string — is written
34
+ * via `setAttribute`. The runtime does not remove attributes as a
35
+ * side-effect of rendering; an empty template result is a valid
36
+ * attribute value and removing the attribute would change the
37
+ * element's semantics (e.g. `<input>` vs `<input value="">`).
38
+ *
39
+ * `dispose` is a no-op for attribute operations.
40
+ */
41
+ export class AttributeOperation {
42
+ constructor(owner, qualifiedName, template) {
43
+ this.owner = owner;
44
+ this.qualifiedName = qualifiedName;
45
+ this.template = template;
46
+ this.kind = 'attribute';
47
+ }
48
+ execute(context) {
49
+ const rendered = this.template(context);
50
+ const current = this.owner.getAttribute(this.qualifiedName);
51
+ if (current !== rendered) {
52
+ this.owner.setAttribute(this.qualifiedName, rendered);
53
+ }
54
+ }
55
+ dispose() {
56
+ // AttributeOperation holds no listeners or observers.
57
+ }
58
+ }
59
+ //# sourceMappingURL=operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../../src/eta/operations.ts"],"names":[],"mappings":"AAAA,wBAAwB;AAOxB;;;;;;;;;;GAUG;AACH,MAAM,OAAO,aAAa;IAKxB,YACW,KAAW,EACH,QAA6B;QADrC,UAAK,GAAL,KAAK,CAAM;QACH,aAAQ,GAAR,QAAQ,CAAqB;QAJvC,SAAI,GAAG,MAAe,CAAC;IAK7B,CAAC;IAEJ,OAAO,CAAC,OAAiB;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAC5B,OAA4B,CAC7B,CAAC;QAEF,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,QAAQ,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO;QACL,iDAAiD;IACnD,CAAC;CACF;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,kBAAkB;IAM7B,YACW,KAAc,EACN,aAAqB,EACrB,QAA6B;QAFrC,UAAK,GAAL,KAAK,CAAS;QACN,kBAAa,GAAb,aAAa,CAAQ;QACrB,aAAQ,GAAR,QAAQ,CAAqB;QALvC,SAAI,GAAG,WAAoB,CAAC;IAMlC,CAAC;IAEJ,OAAO,CAAC,OAAiB;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAC5B,OAA4B,CAC7B,CAAC;QAEF,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CACrC,IAAI,CAAC,aAAa,CACnB,CAAC;QAEF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,YAAY,CACrB,IAAI,CAAC,aAAa,EAClB,QAAQ,CACT,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,sDAAsD;IACxD,CAAC;CACF","sourcesContent":["// src/eta/operations.ts\n\nimport type {\n EtaCompiledTemplate,\n TemplateOperation\n} from './types.js';\n\n/**\n * Renders a text node from a compiled Eta template.\n *\n * The text node's `nodeValue` is overwritten with the rendered\n * string on every `execute` call. This matches how Eta is used\n * elsewhere in the codebase: templates produce text output and the\n * runtime writes that text into the DOM.\n *\n * `dispose` is a no-op for text operations; the GC reclaims the\n * closure once the index drops the reference.\n */\nexport class TextOperation<TContext = object>\nimplements TemplateOperation<TContext>\n{\n readonly kind = 'text' as const;\n\n constructor(\n readonly owner: Text,\n private readonly template: EtaCompiledTemplate\n ) {}\n\n execute(context: TContext): void {\n const rendered = this.template(\n context as unknown as object\n );\n\n if (this.owner.nodeValue !== rendered) {\n this.owner.nodeValue = rendered;\n }\n }\n\n dispose(): void {\n // TextOperation holds no listeners or observers.\n }\n}\n\n/**\n * Renders a single attribute on an element from a compiled Eta\n * template.\n *\n * Every rendered value — including the empty string — is written\n * via `setAttribute`. The runtime does not remove attributes as a\n * side-effect of rendering; an empty template result is a valid\n * attribute value and removing the attribute would change the\n * element's semantics (e.g. `<input>` vs `<input value=\"\">`).\n *\n * `dispose` is a no-op for attribute operations.\n */\nexport class AttributeOperation<\n TContext = object\n> implements TemplateOperation<TContext>\n{\n readonly kind = 'attribute' as const;\n\n constructor(\n readonly owner: Element,\n private readonly qualifiedName: string,\n private readonly template: EtaCompiledTemplate\n ) {}\n\n execute(context: TContext): void {\n const rendered = this.template(\n context as unknown as object\n );\n\n const current = this.owner.getAttribute(\n this.qualifiedName\n );\n\n if (current !== rendered) {\n this.owner.setAttribute(\n this.qualifiedName,\n rendered\n );\n }\n }\n\n dispose(): void {\n // AttributeOperation holds no listeners or observers.\n }\n}\n"]}