@repobit/dex-store-elements 1.7.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -913
- package/README.md +9 -7
- package/dist/src/actions/action.adapter.d.ts +36 -0
- package/dist/src/actions/action.adapter.js +183 -0
- package/dist/src/actions/action.adapter.js.map +1 -0
- package/dist/src/actions/action.attributes.d.ts +15 -0
- package/dist/src/actions/action.attributes.js +137 -0
- package/dist/src/actions/action.attributes.js.map +1 -0
- package/dist/src/actions/action.registry.d.ts +4 -0
- package/dist/src/actions/action.registry.js +119 -0
- package/dist/src/actions/action.registry.js.map +1 -0
- package/dist/src/actions/index.d.ts +3 -2
- package/dist/src/actions/index.js +3 -80
- package/dist/src/actions/index.js.map +1 -1
- package/dist/src/compute/compute.expand.d.ts +70 -0
- package/dist/src/compute/compute.expand.js +319 -0
- package/dist/src/compute/compute.expand.js.map +1 -0
- package/dist/src/compute/compute.scan.d.ts +54 -0
- package/dist/src/compute/compute.scan.js +160 -0
- package/dist/src/compute/compute.scan.js.map +1 -0
- package/dist/src/compute/option-resolver.d.ts +31 -0
- package/dist/src/compute/option-resolver.js +147 -0
- package/dist/src/compute/option-resolver.js.map +1 -0
- package/dist/src/contexts/context.compute.d.ts +12 -0
- package/dist/src/contexts/context.compute.js +4 -0
- package/dist/src/contexts/context.compute.js.map +1 -0
- package/dist/src/contexts/context.scope.d.ts +139 -0
- package/dist/src/contexts/context.scope.js +48 -0
- package/dist/src/contexts/context.scope.js.map +1 -0
- package/dist/src/controllers/controller.action-resolve.d.ts +110 -0
- package/dist/src/controllers/controller.action-resolve.js +203 -0
- package/dist/src/controllers/controller.action-resolve.js.map +1 -0
- package/dist/src/controllers/controller.action-topology.d.ts +59 -0
- package/dist/src/controllers/controller.action-topology.js +130 -0
- package/dist/src/controllers/controller.action-topology.js.map +1 -0
- package/dist/src/controllers/controller.compute.d.ts +72 -0
- package/dist/src/controllers/controller.compute.js +110 -0
- package/dist/src/controllers/controller.compute.js.map +1 -0
- package/dist/src/controllers/controller.resolve.d.ts +78 -0
- package/dist/src/controllers/controller.resolve.js +205 -0
- package/dist/src/controllers/controller.resolve.js.map +1 -0
- package/dist/src/core/action-topology.d.ts +62 -0
- package/dist/src/core/action-topology.js +67 -0
- package/dist/src/core/action-topology.js.map +1 -0
- package/dist/src/core/actions.d.ts +64 -0
- package/dist/src/core/actions.js +47 -0
- package/dist/src/core/actions.js.map +1 -0
- package/dist/src/core/node.element.d.ts +179 -0
- package/dist/src/core/node.element.js +573 -0
- package/dist/src/core/node.element.js.map +1 -0
- package/dist/src/core/node.scope.d.ts +374 -0
- package/dist/src/core/node.scope.js +558 -0
- package/dist/src/core/node.scope.js.map +1 -0
- package/dist/src/core/option-update.d.ts +11 -0
- package/dist/src/core/option-update.js +59 -0
- package/dist/src/core/option-update.js.map +1 -0
- package/dist/src/dsl/compilers/array/compiler.js +13 -14
- package/dist/src/dsl/compilers/array/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/boolean/compiler.d.ts +1 -1
- package/dist/src/dsl/compilers/boolean/compiler.js +14 -27
- package/dist/src/dsl/compilers/boolean/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/enum/compiler.js +7 -10
- package/dist/src/dsl/compilers/enum/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/index.d.ts +1 -1
- package/dist/src/dsl/compilers/index.js +5 -5
- package/dist/src/dsl/compilers/index.js.map +1 -1
- package/dist/src/dsl/utilty.d.ts +0 -4
- package/dist/src/dsl/utilty.js +3 -30
- package/dist/src/dsl/utilty.js.map +1 -1
- package/dist/src/eta/batch.d.ts +123 -0
- package/dist/src/eta/batch.js +162 -0
- package/dist/src/eta/batch.js.map +1 -0
- package/dist/src/eta/batcher.d.ts +184 -0
- package/dist/src/eta/batcher.js +335 -0
- package/dist/src/eta/batcher.js.map +1 -0
- package/dist/src/eta/cache.d.ts +36 -0
- package/dist/src/eta/cache.js +56 -0
- package/dist/src/eta/cache.js.map +1 -0
- package/dist/src/eta/controller.d.ts +608 -0
- package/dist/src/eta/controller.js +1036 -0
- package/dist/src/eta/controller.js.map +1 -0
- package/dist/src/eta/factory.d.ts +74 -0
- package/dist/src/eta/factory.js +121 -0
- package/dist/src/eta/factory.js.map +1 -0
- package/dist/src/eta/idle.d.ts +39 -0
- package/dist/src/eta/idle.js +52 -0
- package/dist/src/eta/idle.js.map +1 -0
- package/dist/src/eta/index.d.ts +52 -0
- package/dist/src/eta/index.js +13 -0
- package/dist/src/eta/index.js.map +1 -0
- package/dist/src/eta/observer.d.ts +263 -0
- package/dist/src/eta/observer.js +263 -0
- package/dist/src/eta/observer.js.map +1 -0
- package/dist/src/eta/operations.d.ts +41 -0
- package/dist/src/eta/operations.js +59 -0
- package/dist/src/eta/operations.js.map +1 -0
- package/dist/src/eta/source-registry.d.ts +198 -0
- package/dist/src/eta/source-registry.js +267 -0
- package/dist/src/eta/source-registry.js.map +1 -0
- package/dist/src/eta/template-index.d.ts +285 -0
- package/dist/src/eta/template-index.js +642 -0
- package/dist/src/eta/template-index.js.map +1 -0
- package/dist/src/eta/types.d.ts +529 -0
- package/dist/src/eta/types.js +2 -0
- package/dist/src/eta/types.js.map +1 -0
- package/dist/src/index.d.ts +21 -9
- package/dist/src/index.js +22 -11
- package/dist/src/index.js.map +1 -1
- package/dist/src/nodes/node.context.d.ts +5 -3
- package/dist/src/nodes/node.context.js +48 -3
- package/dist/src/nodes/node.context.js.map +1 -1
- package/dist/src/nodes/node.option.d.ts +11 -31
- package/dist/src/nodes/node.option.js +158 -222
- package/dist/src/nodes/node.option.js.map +1 -1
- package/dist/src/nodes/node.product.d.ts +7 -11
- package/dist/src/nodes/node.product.js +85 -64
- package/dist/src/nodes/node.product.js.map +1 -1
- package/dist/src/renders/attributes/buyLink.d.ts +2 -2
- package/dist/src/renders/attributes/buyLink.js +14 -11
- package/dist/src/renders/attributes/buyLink.js.map +1 -1
- package/dist/src/renders/attributes/devices.d.ts +2 -2
- package/dist/src/renders/attributes/devices.js +9 -19
- package/dist/src/renders/attributes/devices.js.map +1 -1
- package/dist/src/renders/attributes/discount.d.ts +2 -2
- package/dist/src/renders/attributes/discount.js +56 -44
- package/dist/src/renders/attributes/discount.js.map +1 -1
- package/dist/src/renders/attributes/hide.d.ts +2 -2
- package/dist/src/renders/attributes/hide.js +49 -14
- package/dist/src/renders/attributes/hide.js.map +1 -1
- package/dist/src/renders/attributes/index.d.ts +3 -3
- package/dist/src/renders/attributes/index.js +2 -4
- package/dist/src/renders/attributes/index.js.map +1 -1
- package/dist/src/renders/attributes/price.d.ts +2 -2
- package/dist/src/renders/attributes/price.js +30 -44
- package/dist/src/renders/attributes/price.js.map +1 -1
- package/dist/src/renders/attributes/subscription.d.ts +2 -2
- package/dist/src/renders/attributes/subscription.js +14 -20
- package/dist/src/renders/attributes/subscription.js.map +1 -1
- package/dist/src/renders/attributes/utilty.d.ts +2 -0
- package/dist/src/renders/attributes/utilty.js +17 -4
- package/dist/src/renders/attributes/utilty.js.map +1 -1
- package/dist/src/renders/context.d.ts +122 -10
- package/dist/src/renders/context.js +140 -80
- package/dist/src/renders/context.js.map +1 -1
- package/dist/src/renders/format.js +2 -2
- package/dist/src/renders/format.js.map +1 -1
- package/dist/src/renders/index.d.ts +6 -2
- package/dist/src/renders/index.js +67 -29
- package/dist/src/renders/index.js.map +1 -1
- package/dist/src/renders/observe.d.ts +1 -4
- package/dist/src/renders/observe.js +10 -24
- package/dist/src/renders/observe.js.map +1 -1
- package/dist/src/renders/render.wrapper.d.ts +37 -0
- package/dist/src/renders/render.wrapper.js +147 -0
- package/dist/src/renders/render.wrapper.js.map +1 -0
- package/dist/src/templating/eta.js +1 -4
- package/dist/src/templating/eta.js.map +1 -1
- package/package.json +4 -5
- package/dist/src/actions/action.button.d.ts +0 -2
- package/dist/src/actions/action.button.js +0 -8
- package/dist/src/actions/action.button.js.map +0 -1
- package/dist/src/actions/action.input.d.ts +0 -2
- package/dist/src/actions/action.input.js +0 -43
- package/dist/src/actions/action.input.js.map +0 -1
- package/dist/src/actions/action.select.d.ts +0 -2
- package/dist/src/actions/action.select.js +0 -14
- package/dist/src/actions/action.select.js.map +0 -1
- package/dist/src/actions/utilty.d.ts +0 -16
- package/dist/src/actions/utilty.js +0 -149
- package/dist/src/actions/utilty.js.map +0 -1
- package/dist/src/contexts/context.datalayer.d.ts +0 -9
- package/dist/src/contexts/context.datalayer.js +0 -3
- package/dist/src/contexts/context.datalayer.js.map +0 -1
- package/dist/src/contexts/context.derived.d.ts +0 -12
- package/dist/src/contexts/context.derived.js +0 -3
- package/dist/src/contexts/context.derived.js.map +0 -1
- package/dist/src/contexts/context.option.d.ts +0 -5
- package/dist/src/contexts/context.option.js +0 -3
- package/dist/src/contexts/context.option.js.map +0 -1
- package/dist/src/contexts/context.product.d.ts +0 -5
- package/dist/src/contexts/context.product.js +0 -3
- package/dist/src/contexts/context.product.js.map +0 -1
- package/dist/src/contexts/context.state.d.ts +0 -28
- package/dist/src/contexts/context.state.js +0 -3
- package/dist/src/contexts/context.state.js.map +0 -1
- package/dist/src/contexts/context.store.d.ts +0 -5
- package/dist/src/contexts/context.store.js +0 -3
- package/dist/src/contexts/context.store.js.map +0 -1
- package/dist/src/controllers/collect-controller.d.ts +0 -39
- package/dist/src/controllers/collect-controller.js +0 -161
- package/dist/src/controllers/collect-controller.js.map +0 -1
- package/dist/src/controllers/compute-controller.d.ts +0 -55
- package/dist/src/controllers/compute-controller.js +0 -350
- package/dist/src/controllers/compute-controller.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.d.ts +0 -25
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.js +0 -192
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.d.ts +0 -28
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.js +0 -206
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-renderer.d.ts +0 -21
- package/dist/src/controllers/eta-renderer/eta-renderer.js +0 -77
- package/dist/src/controllers/eta-renderer/eta-renderer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-template-cache.d.ts +0 -24
- package/dist/src/controllers/eta-renderer/eta-template-cache.js +0 -61
- package/dist/src/controllers/eta-renderer/eta-template-cache.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-types.d.ts +0 -11
- package/dist/src/controllers/eta-renderer/eta-types.js +0 -1
- package/dist/src/controllers/eta-renderer/eta-types.js.map +0 -1
- package/dist/src/controllers/eta-renderer/index.d.ts +0 -2
- package/dist/src/controllers/eta-renderer/index.js +0 -2
- package/dist/src/controllers/eta-renderer/index.js.map +0 -1
- package/dist/src/controllers/event-pipeline-controller.d.ts +0 -28
- package/dist/src/controllers/event-pipeline-controller.js +0 -72
- package/dist/src/controllers/event-pipeline-controller.js.map +0 -1
- package/dist/src/events/events.d.ts +0 -75
- package/dist/src/events/events.js +0 -78
- package/dist/src/events/events.js.map +0 -1
- package/dist/src/nodes/node.root.d.ts +0 -16
- package/dist/src/nodes/node.root.js +0 -49
- package/dist/src/nodes/node.root.js.map +0 -1
- package/dist/src/nodes/node.state.d.ts +0 -88
- package/dist/src/nodes/node.state.js +0 -330
- package/dist/src/nodes/node.state.js.map +0 -1
- package/dist/src/renders/utility.d.ts +0 -32
- package/dist/src/renders/utility.js +0 -191
- package/dist/src/renders/utility.js.map +0 -1
|
@@ -0,0 +1,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"]}
|