solid-tag-runtime 0.0.15 → 0.0.17
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/ARCHITECTURE.md +73 -0
- package/README.md +28 -3
- package/bench/observer.mjs +103 -0
- package/docs/api/html.md +8 -2
- package/docs/getting-started.md +3 -3
- package/docs/html-runtime.md +35 -5
- package/docs/lifecycle-events.md +6 -1
- package/docs/solid-runtime-setup.md +1 -1
- package/examples/observer-strategies.js +19 -0
- package/html.d.ts +6 -0
- package/package.json +5 -3
- package/src/html.js +350 -83
package/ARCHITECTURE.md
CHANGED
|
@@ -1476,3 +1476,76 @@ Reason: HTML lifecycle subscribers, devtools, status panels, and normal applicat
|
|
|
1476
1476
|
Invariant: unrelated DOM mutations must not enqueue runtime discovery or emit `observer-batch`. Observing `document` is therefore robust against diagnostic/application UI churn, while matching declarations and mounted-render cleanup remain live.
|
|
1477
1477
|
|
|
1478
1478
|
Regression coverage: package tests verify unrelated child-list additions enqueue no observer batch, matching runtime scripts still register normally, an `observer-batch` subscriber may render unrelated DOM without recursively triggering additional scans, and relevant declaration removals still dispose mounted render ownership. The browser playground test suite carries the same feedback-loop regression against the native `MutationObserver`.
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
### 0.0.16 — incremental mutation-driven observation
|
|
1482
|
+
|
|
1483
|
+
Decision: keep the 0.0.15 declaration-driven relevance filter, but stop turning every relevant `MutationObserver` callback back into `enqueueObserverScan()`. Normal observer callbacks now derive a bounded work set directly from `MutationRecord[]`.
|
|
1484
|
+
|
|
1485
|
+
Pipeline:
|
|
1486
|
+
|
|
1487
|
+
```text
|
|
1488
|
+
MutationRecord[]
|
|
1489
|
+
↓
|
|
1490
|
+
collectObserverMutationBatch()
|
|
1491
|
+
├── exact added declaration elements
|
|
1492
|
+
├── exact added <solid-render> retry candidates
|
|
1493
|
+
├── tracked mounted records affected by removal
|
|
1494
|
+
└── unsafe/unknown host mutation → conservative full scan
|
|
1495
|
+
↓
|
|
1496
|
+
enqueueObserverBatch()
|
|
1497
|
+
↓
|
|
1498
|
+
process exact candidates once
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
Rules:
|
|
1502
|
+
|
|
1503
|
+
1. An added element is checked directly and only its added subtree is queried for matching declarations.
|
|
1504
|
+
2. Candidate identity is deduplicated across mutation records before registration.
|
|
1505
|
+
3. Elements already owned by the same controller are dropped before observer work is queued, so `append()`/`addModule()` do not rediscover their own DOM mutations.
|
|
1506
|
+
4. Relevant removals carry the exact mounted ownership records forward and dispose only records that are actually detached after the mutation batch settles.
|
|
1507
|
+
5. `register()`, initial `observe({ registerExisting:true })`, explicit `flush()`, and root-change registration retain full-root scan semantics.
|
|
1508
|
+
6. Unknown/custom DOM hosts that cannot be classified safely fall back to the previous full-root scan path.
|
|
1509
|
+
7. Define-before-entry/render ordering is preserved because all exact declaration candidates in one observer callback are still claimed and defined as one batch before entries/renders execute.
|
|
1510
|
+
|
|
1511
|
+
Invariant: live observation is mutation-driven and incremental. Relevant mutation cost should scale with the changed subtree or tracked removal set, not with total root size. Full-root discovery is reserved for explicit scan operations and conservative fallback.
|
|
1512
|
+
|
|
1513
|
+
Regression coverage adds direct-addition and nested-subtree cases that assert zero root selector calls, duplicate-record deduplication, and explicit `append()` under active observation producing no observer batch. `bench/observer.mjs` provides a synthetic scaling check comparing incremental addition with forced `flush()`.
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
### 0.0.17 — explicit observation lifecycle strategies
|
|
1517
|
+
|
|
1518
|
+
Decision: observation is a compatibility mechanism for DOM changes the runtime does not already know about, not the primary module lifecycle mechanism. Keep `observe()` continuous by default for compatibility, but add an opt-in bootstrap lifecycle for applications that only need automatic discovery during initial page/framework startup.
|
|
1519
|
+
|
|
1520
|
+
Public modes:
|
|
1521
|
+
|
|
1522
|
+
```text
|
|
1523
|
+
manual / no observer
|
|
1524
|
+
html.register() + explicit APIs + optional html.flush()
|
|
1525
|
+
|
|
1526
|
+
bootstrap / temporary observer
|
|
1527
|
+
html.observe({ mode: "bootstrap", idleMs })
|
|
1528
|
+
|
|
1529
|
+
continuous / long-lived observer
|
|
1530
|
+
html.observe()
|
|
1531
|
+
html.observe({ mode: "continuous" })
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
Bootstrap semantics:
|
|
1535
|
+
|
|
1536
|
+
1. connect the normal incremental observer;
|
|
1537
|
+
2. optionally register existing declarations using the existing `registerExisting` behavior;
|
|
1538
|
+
3. remain connected through DOM readiness;
|
|
1539
|
+
4. only relevant observer mutations increment bootstrap activity; unrelated UI mutations do not extend observation;
|
|
1540
|
+
5. after `idleMs` with no new relevant activity, wait for the controller queue to drain;
|
|
1541
|
+
6. if activity occurred during the wait, repeat the quiet-period check;
|
|
1542
|
+
7. otherwise disconnect with lifecycle reason `bootstrap-idle`;
|
|
1543
|
+
8. a manual `disconnect()` cancels bootstrap settling;
|
|
1544
|
+
9. root rebinding preserves bootstrap state and resets relevant activity rather than creating a second observer;
|
|
1545
|
+
10. after bootstrap completes, a later `observe()` may start a fresh continuous session.
|
|
1546
|
+
|
|
1547
|
+
Default `idleMs` is 50 ms. Invalid modes and negative/non-finite idle values are rejected.
|
|
1548
|
+
|
|
1549
|
+
Invariant: use continuous observation only when runtime declarations may enter the DOM through code outside the runtime's explicit APIs. Controlled dynamic module loading should remain observer-free after initial registration.
|
|
1550
|
+
|
|
1551
|
+
Regression coverage verifies continuous compatibility, bootstrap initial registration, late startup additions, unrelated mutation filtering, automatic disconnect, option validation, and re-observation after bootstrap.
|
package/README.md
CHANGED
|
@@ -16,6 +16,31 @@ host Solid runtime
|
|
|
16
16
|
|
|
17
17
|
The core package is module-first and DOM-independent. Browser discovery, ownership, declarative rendering, and `<solid-render>` live in `solid-tag-runtime/html`. Optional Solid setup/provider helpers live in `solid-tag-runtime/solid`.
|
|
18
18
|
|
|
19
|
+
## 0.0.17 observation strategies
|
|
20
|
+
|
|
21
|
+
`0.0.17` adds explicit observation lifecycle strategies while keeping existing behavior backward compatible. `observe()` is still continuous by default. Applications that only need DOM discovery during startup can use `observe({ mode: "bootstrap" })`; it observes through DOM readiness and disconnects after a configurable quiet period of relevant runtime mutations.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Lowest ongoing overhead: initial scan, then explicit runtime APIs.
|
|
25
|
+
await html.register();
|
|
26
|
+
|
|
27
|
+
// Mostly declarative startup: temporary observer.
|
|
28
|
+
await html.observe({ mode: "bootstrap", idleMs: 50 });
|
|
29
|
+
|
|
30
|
+
// Unknown external DOM mutations throughout application lifetime.
|
|
31
|
+
await html.observe({ mode: "continuous" });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Only relevant declaration/removal activity extends bootstrap lifetime. Ordinary application UI mutations do not.
|
|
35
|
+
|
|
36
|
+
## 0.0.16 incremental observer processing
|
|
37
|
+
|
|
38
|
+
`0.0.16` keeps the `0.0.15` declaration-driven filter but removes the normal relevant-mutation root rescan. `MutationRecord[]` are converted into exact declaration, `<solid-render>`, and mounted-removal candidates; added subtrees are queried locally and duplicate candidates are coalesced before one observer batch.
|
|
39
|
+
|
|
40
|
+
Explicit APIs such as `append()` and `addModule()` claim ownership before DOM insertion, so an active observer now ignores that already-known work entirely. Full-root discovery remains available for initial registration, `flush()`, root-change registration, and conservative fallback when a host DOM cannot be classified safely.
|
|
41
|
+
|
|
42
|
+
The key performance invariant is that relevant observer work scales with the changed subtree rather than total root size.
|
|
43
|
+
|
|
19
44
|
## 0.0.15 observer hardening
|
|
20
45
|
|
|
21
46
|
`0.0.15` filters `MutationObserver` records before scheduling HTML discovery. Ordinary UI DOM changes no longer enqueue a full runtime scan or emit an `observer-batch`; matching runtime declarations, `<solid-render>` retry boundaries, and removals that can detach mounted declarative renders still trigger observer work. Explicit `html.flush()` remains a deterministic forced scan.
|
|
@@ -217,9 +242,9 @@ When loading from an import map, map every used package subpath to the same rele
|
|
|
217
242
|
```json
|
|
218
243
|
{
|
|
219
244
|
"imports": {
|
|
220
|
-
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.
|
|
221
|
-
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.
|
|
222
|
-
"solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.
|
|
245
|
+
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.17",
|
|
246
|
+
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.17/html",
|
|
247
|
+
"solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.17/solid"
|
|
223
248
|
}
|
|
224
249
|
}
|
|
225
250
|
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { createRuntime } from "../src/runtime.js";
|
|
3
|
+
import { createHTMLRuntime } from "../src/html.js";
|
|
4
|
+
|
|
5
|
+
class BenchMutationObserver {
|
|
6
|
+
static instances = [];
|
|
7
|
+
constructor(callback) { this.callback = callback; BenchMutationObserver.instances.push(this); }
|
|
8
|
+
observe(target) { this.target = target; this.connected = true; }
|
|
9
|
+
disconnect() { this.connected = false; }
|
|
10
|
+
trigger(records) { if (this.connected) this.callback(records, this); }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function script(id) {
|
|
14
|
+
const attrs = { type: "solid-js", module: id };
|
|
15
|
+
return {
|
|
16
|
+
nodeType: 1,
|
|
17
|
+
textContent: "export const value = 1;",
|
|
18
|
+
parentNode: null,
|
|
19
|
+
getAttribute(name) { return attrs[name] ?? null; },
|
|
20
|
+
hasAttribute(name) { return Object.prototype.hasOwnProperty.call(attrs, name); },
|
|
21
|
+
setAttribute(name, value) { attrs[name] = String(value); },
|
|
22
|
+
matches(selector) { return String(selector).includes('script[type="solid-js"]'); },
|
|
23
|
+
querySelectorAll() { return []; },
|
|
24
|
+
contains(target) { return target === this; },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeRoot(size) {
|
|
29
|
+
const filler = Array.from({ length: size }, () => ({ nodeType: 1 }));
|
|
30
|
+
const scripts = [];
|
|
31
|
+
let visited = 0;
|
|
32
|
+
return {
|
|
33
|
+
nodes: filler,
|
|
34
|
+
scripts,
|
|
35
|
+
querySelectorAll(selector) {
|
|
36
|
+
visited += this.nodes.length + this.scripts.length;
|
|
37
|
+
if (selector === "solid-render") return [];
|
|
38
|
+
return [...this.scripts];
|
|
39
|
+
},
|
|
40
|
+
contains(node) { return this.nodes.includes(node) || this.scripts.includes(node); },
|
|
41
|
+
resetVisited() { visited = 0; },
|
|
42
|
+
getVisited() { return visited; },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeRuntime() {
|
|
47
|
+
return createRuntime({
|
|
48
|
+
compiler: {
|
|
49
|
+
fingerprint: "observer-bench-v1",
|
|
50
|
+
analyze() { return { imports: [] }; },
|
|
51
|
+
transform(source) { return { code: source, diagnostics: [] }; },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function waitTask() { await new Promise(resolve => setTimeout(resolve, 0)); }
|
|
57
|
+
|
|
58
|
+
async function measureIncremental(size) {
|
|
59
|
+
BenchMutationObserver.instances.length = 0;
|
|
60
|
+
const runtime = makeRuntime();
|
|
61
|
+
const root = makeRoot(size);
|
|
62
|
+
const html = createHTMLRuntime(runtime, { root, MutationObserver: BenchMutationObserver });
|
|
63
|
+
await html.observe({ registerExisting: false });
|
|
64
|
+
root.resetVisited();
|
|
65
|
+
const element = script(`/incremental-${size}.js`);
|
|
66
|
+
element.parentNode = root;
|
|
67
|
+
root.scripts.push(element);
|
|
68
|
+
const t0 = performance.now();
|
|
69
|
+
BenchMutationObserver.instances[0].trigger([{ type: "childList", addedNodes: [element], removedNodes: [] }]);
|
|
70
|
+
await waitTask();
|
|
71
|
+
const ms = performance.now() - t0;
|
|
72
|
+
const visited = root.getVisited();
|
|
73
|
+
html.disconnect();
|
|
74
|
+
runtime.dispose();
|
|
75
|
+
return { ms, visited };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function measureForcedScan(size) {
|
|
79
|
+
BenchMutationObserver.instances.length = 0;
|
|
80
|
+
const runtime = makeRuntime();
|
|
81
|
+
const root = makeRoot(size);
|
|
82
|
+
const html = createHTMLRuntime(runtime, { root, MutationObserver: BenchMutationObserver });
|
|
83
|
+
await html.observe({ registerExisting: false });
|
|
84
|
+
const element = script(`/scan-${size}.js`);
|
|
85
|
+
element.parentNode = root;
|
|
86
|
+
root.scripts.push(element);
|
|
87
|
+
root.resetVisited();
|
|
88
|
+
const t0 = performance.now();
|
|
89
|
+
await html.flush();
|
|
90
|
+
const ms = performance.now() - t0;
|
|
91
|
+
const visited = root.getVisited();
|
|
92
|
+
html.disconnect();
|
|
93
|
+
runtime.dispose();
|
|
94
|
+
return { ms, visited };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log("observer benchmark (synthetic root; lower visited count is the main invariant)");
|
|
98
|
+
console.log("nodes\tincremental ms\tincremental visited\tflush ms\tflush visited");
|
|
99
|
+
for (const size of [1_000, 10_000, 50_000]) {
|
|
100
|
+
const incremental = await measureIncremental(size);
|
|
101
|
+
const scan = await measureForcedScan(size);
|
|
102
|
+
console.log(`${size}\t${incremental.ms.toFixed(3)}\t${incremental.visited}\t${scan.ms.toFixed(3)}\t${scan.visited}`);
|
|
103
|
+
}
|
package/docs/api/html.md
CHANGED
|
@@ -30,12 +30,18 @@ Common options:
|
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
32
|
await html.register(options?);
|
|
33
|
-
await html.observe(
|
|
33
|
+
await html.observe({
|
|
34
|
+
mode: "continuous" | "bootstrap",
|
|
35
|
+
idleMs: 50,
|
|
36
|
+
...options,
|
|
37
|
+
});
|
|
34
38
|
await html.flush(options?);
|
|
35
39
|
html.disconnect();
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
`observe()`
|
|
42
|
+
`observe()` is continuous by default. `mode: "bootstrap"` temporarily observes startup DOM activity and resolves after DOM readiness plus `idleMs` with no relevant runtime mutations, at which point the observer is disconnected. `mode: "continuous"` retains long-lived observation.
|
|
43
|
+
|
|
44
|
+
`observe()` processes relevant child-list mutation records incrementally. Added declarations are discovered from the added node/subtree itself, removals consult tracked mounted ownership, and explicit API-owned elements are skipped. A full-root scan is reserved for initial registration, `flush()`, root-change registration, or conservative fallback. `flush()` always forces a full scan.
|
|
39
45
|
|
|
40
46
|
## Explicit ownership
|
|
41
47
|
|
package/docs/getting-started.md
CHANGED
|
@@ -104,9 +104,9 @@ Map every used package subpath explicitly:
|
|
|
104
104
|
```json
|
|
105
105
|
{
|
|
106
106
|
"imports": {
|
|
107
|
-
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.
|
|
108
|
-
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.
|
|
109
|
-
"solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.
|
|
107
|
+
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.17",
|
|
108
|
+
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.17/html",
|
|
109
|
+
"solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.17/solid"
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
```
|
package/docs/html-runtime.md
CHANGED
|
@@ -20,18 +20,48 @@ const html = createHTMLRuntime(runtime, {
|
|
|
20
20
|
await html.register();
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Choose an observation strategy
|
|
24
|
+
|
|
25
|
+
For controlled applications, prefer one initial scan plus explicit runtime APIs:
|
|
24
26
|
|
|
25
27
|
```ts
|
|
26
|
-
await html.
|
|
28
|
+
await html.register();
|
|
29
|
+
|
|
30
|
+
await html.addModule(...);
|
|
31
|
+
await html.append(script);
|
|
32
|
+
```
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
If raw DOM insertion is known and occasional, keep observation disabled and synchronize explicitly:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
root.insertAdjacentHTML("beforeend", markup);
|
|
29
38
|
await html.flush();
|
|
30
39
|
```
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
For mostly declarative startup, use a temporary bootstrap observer:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
await html.observe({
|
|
45
|
+
mode: "bootstrap",
|
|
46
|
+
idleMs: 50,
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Bootstrap mode observes through DOM readiness, waits until there have been no **relevant runtime mutations** for `idleMs`, drains queued registration work, and disconnects. Ordinary UI mutations do not reset the quiet period.
|
|
51
|
+
|
|
52
|
+
For pages where declarations can arrive through uncontrolled DOM code throughout the application lifetime, use continuous observation:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
await html.observe({ mode: "continuous" });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`observe()` with no `mode` remains equivalent to continuous mode for backward compatibility.
|
|
59
|
+
|
|
60
|
+
The live observer is declaration-driven and incremental. It inspects each `MutationRecord`, ignores ordinary UI child-list changes, and derives exact runtime declaration/removal candidates from relevant added or removed subtrees. Normal relevant mutations do not rescan the configured root.
|
|
61
|
+
|
|
62
|
+
Added subtrees are queried only within the subtree that changed, duplicate candidates are coalesced, and elements already claimed through explicit APIs are skipped before observer work is queued. This means observer cost scales primarily with changed runtime-relevant DOM rather than total page size.
|
|
33
63
|
|
|
34
|
-
`flush()` is intentionally different: it remains an explicit forced scan and can be used as a deterministic synchronization point
|
|
64
|
+
`flush()` is intentionally different: it remains an explicit forced full-root scan and can be used as a deterministic synchronization point after known raw DOM insertion.
|
|
35
65
|
|
|
36
66
|
## Script declarations
|
|
37
67
|
|
package/docs/lifecycle-events.md
CHANGED
|
@@ -74,6 +74,11 @@ html.subscribe("element-registered", event => {
|
|
|
74
74
|
|
|
75
75
|
HTML events cover ownership, observation, roots, entry execution, render mounting/disposal, warnings, and errors.
|
|
76
76
|
|
|
77
|
-
`observer-batch` is emitted only when observer work is
|
|
77
|
+
`observer-batch` is emitted only when semantic observer work is scheduled. Unrelated child-list mutations are filtered before discovery, and relevant additions are processed from their exact changed subtree instead of rescanning the whole root. A lifecycle subscriber may therefore render diagnostics/status UI inside an observed document without causing a feedback loop. Explicit `html.flush()` still performs and reports a forced full-root scan.
|
|
78
78
|
|
|
79
79
|
Listener exceptions are isolated from runtime/HTML operations and are reported to the console rather than aborting the underlying action.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Bootstrap observation lifecycle
|
|
83
|
+
|
|
84
|
+
`observe({ mode: "bootstrap" })` emits the normal `observer-connected` event and later an `observer-disconnected` event with reason `bootstrap-idle` when its relevant-mutation quiet period completes. Unrelated UI DOM mutations neither emit `observer-batch` nor extend bootstrap lifetime.
|
|
@@ -126,7 +126,7 @@ When provider fallback is needed and there is no versioned Solid anchor, this re
|
|
|
126
126
|
import { TESTED_SOLID_VERSION } from "solid-tag-runtime/solid";
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
For `0.0.14`
|
|
129
|
+
For `0.0.14` through `0.0.17`, the tested fallback line is Solid `2.0.0-rc.13`.
|
|
130
130
|
|
|
131
131
|
If an existing Solid mapping is explicitly versioned, that version becomes the family anchor for generated siblings. An explicitly unversioned existing mapping remains unversioned; the loader does not pretend it is pinned.
|
|
132
132
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHTMLRuntime } from "solid-tag-runtime/html";
|
|
2
|
+
|
|
3
|
+
export async function manual(runtime, root) {
|
|
4
|
+
const html = createHTMLRuntime(runtime, { root });
|
|
5
|
+
await html.register();
|
|
6
|
+
return html;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function bootstrap(runtime, root) {
|
|
10
|
+
const html = createHTMLRuntime(runtime, { root });
|
|
11
|
+
await html.observe({ mode: "bootstrap", idleMs: 50 });
|
|
12
|
+
return html; // disconnected after startup settles
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function continuous(runtime, root) {
|
|
16
|
+
const html = createHTMLRuntime(runtime, { root });
|
|
17
|
+
await html.observe({ mode: "continuous" });
|
|
18
|
+
return html;
|
|
19
|
+
}
|
package/html.d.ts
CHANGED
|
@@ -223,7 +223,13 @@ export type HTMLRuntimeEventType = HTMLRuntimeEvent["type"];
|
|
|
223
223
|
export type HTMLRuntimeEventOfType<T extends HTMLRuntimeEventType> =
|
|
224
224
|
Extract<HTMLRuntimeEvent, { type: T }>;
|
|
225
225
|
|
|
226
|
+
export type HTMLObservationMode = "continuous" | "bootstrap";
|
|
227
|
+
|
|
226
228
|
export interface ObserveHTMLOptions extends RegisterHTMLOptions {
|
|
229
|
+
/** Observation lifecycle. Default: "continuous". */
|
|
230
|
+
mode?: HTMLObservationMode;
|
|
231
|
+
/** Bootstrap-mode quiet period after DOM readiness and the last relevant mutation. Default: 50 ms. */
|
|
232
|
+
idleMs?: number;
|
|
227
233
|
/** Register scripts already present when observation starts. Default: true. */
|
|
228
234
|
registerExisting?: boolean;
|
|
229
235
|
/** Observe descendant additions as well as direct children. Default: true. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solid-tag-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
4
4
|
"description": "Runtime module system for JSX modules compiled with solid-tag and executed through @solidjs/html",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"README.md",
|
|
42
42
|
"ARCHITECTURE.md",
|
|
43
43
|
"docs",
|
|
44
|
-
"examples"
|
|
44
|
+
"examples",
|
|
45
|
+
"bench"
|
|
45
46
|
],
|
|
46
47
|
"sideEffects": false,
|
|
47
48
|
"dependencies": {
|
|
@@ -49,7 +50,8 @@
|
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
52
|
"test": "node ./test/run.mjs",
|
|
52
|
-
"pack:check": "npm pack --dry-run"
|
|
53
|
+
"pack:check": "npm pack --dry-run",
|
|
54
|
+
"bench:observer": "node ./bench/observer.mjs"
|
|
53
55
|
},
|
|
54
56
|
"keywords": [
|
|
55
57
|
"solid",
|
package/src/html.js
CHANGED
|
@@ -83,6 +83,10 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
83
83
|
queue: Promise.resolve(),
|
|
84
84
|
lastError: undefined,
|
|
85
85
|
connected: false,
|
|
86
|
+
defaultObservationMode: normalizeObservationMode(options.mode),
|
|
87
|
+
defaultBootstrapIdleMs: options.idleMs == null ? 50 : normalizeBootstrapIdleMs(options.idleMs),
|
|
88
|
+
observationMode: "continuous",
|
|
89
|
+
bootstrapState: undefined,
|
|
86
90
|
registrationDepth: 0,
|
|
87
91
|
registrationPasses: 0,
|
|
88
92
|
solidRenderRetryScheduled: false,
|
|
@@ -202,8 +206,19 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
202
206
|
async function observe(callOptions = {}) {
|
|
203
207
|
if (controller.connected) return api;
|
|
204
208
|
|
|
209
|
+
const observationMode = normalizeObservationMode(callOptions.mode ?? controller.defaultObservationMode);
|
|
205
210
|
const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime observe");
|
|
206
|
-
controller.
|
|
211
|
+
controller.observationMode = observationMode;
|
|
212
|
+
controller.observerOptions = { ...callOptions, mode: observationMode };
|
|
213
|
+
controller.bootstrapState = observationMode === "bootstrap"
|
|
214
|
+
? {
|
|
215
|
+
active: true,
|
|
216
|
+
activityVersion: 0,
|
|
217
|
+
idleMs: callOptions.idleMs == null
|
|
218
|
+
? controller.defaultBootstrapIdleMs
|
|
219
|
+
: normalizeBootstrapIdleMs(callOptions.idleMs),
|
|
220
|
+
}
|
|
221
|
+
: undefined;
|
|
207
222
|
const pendingBeforeUpgrade = collectConnectedSolidRenderElements(controller, root);
|
|
208
223
|
controller.registrationDepth += 1;
|
|
209
224
|
|
|
@@ -225,6 +240,10 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
225
240
|
controller.registrationDepth = Math.max(0, controller.registrationDepth - 1);
|
|
226
241
|
}
|
|
227
242
|
|
|
243
|
+
if (observationMode === "bootstrap") {
|
|
244
|
+
await finishBootstrapObservation(controller, root);
|
|
245
|
+
}
|
|
246
|
+
|
|
228
247
|
return api;
|
|
229
248
|
}
|
|
230
249
|
|
|
@@ -279,7 +298,10 @@ export function createHTMLRuntime(runtime, options = {}) {
|
|
|
279
298
|
const wasConnected = controller.connected;
|
|
280
299
|
const previousObserverOptions = controller.observerOptions ?? {};
|
|
281
300
|
|
|
282
|
-
if (wasConnected)
|
|
301
|
+
if (wasConnected) {
|
|
302
|
+
markBootstrapObserverActivity(controller);
|
|
303
|
+
stopObserver(controller, { preserveOptions: true, preserveBootstrap: true, reason: "root-change" });
|
|
304
|
+
}
|
|
283
305
|
|
|
284
306
|
controller.root = nextRoot;
|
|
285
307
|
if (Object.prototype.hasOwnProperty.call(callOptions, "appendTo")) {
|
|
@@ -1196,6 +1218,80 @@ async function disposeController(controller, options = {}) {
|
|
|
1196
1218
|
controller.events.clear();
|
|
1197
1219
|
}
|
|
1198
1220
|
|
|
1221
|
+
function normalizeObservationMode(mode) {
|
|
1222
|
+
if (mode == null || mode === "continuous") return "continuous";
|
|
1223
|
+
if (mode === "bootstrap") return "bootstrap";
|
|
1224
|
+
throw new TypeError(`Unknown HTML observation mode ${JSON.stringify(mode)}. Expected "continuous" or "bootstrap".`);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function normalizeBootstrapIdleMs(value) {
|
|
1228
|
+
if (value == null) return 50;
|
|
1229
|
+
const idleMs = Number(value);
|
|
1230
|
+
if (!Number.isFinite(idleMs) || idleMs < 0) {
|
|
1231
|
+
throw new TypeError("observe({ idleMs }) must be a finite non-negative number.");
|
|
1232
|
+
}
|
|
1233
|
+
return idleMs;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function markBootstrapObserverActivity(controller) {
|
|
1237
|
+
const state = controller.bootstrapState;
|
|
1238
|
+
if (state?.active) state.activityVersion += 1;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async function finishBootstrapObservation(controller, root) {
|
|
1242
|
+
const state = controller.bootstrapState;
|
|
1243
|
+
if (!state?.active) return;
|
|
1244
|
+
|
|
1245
|
+
await waitForBootstrapDOMReady(root);
|
|
1246
|
+
|
|
1247
|
+
while (state.active && !controller.disposed) {
|
|
1248
|
+
const version = state.activityVersion;
|
|
1249
|
+
await waitForBootstrapIdle(state.idleMs);
|
|
1250
|
+
await controller.queue;
|
|
1251
|
+
|
|
1252
|
+
if (controller.lastError) {
|
|
1253
|
+
const error = controller.lastError;
|
|
1254
|
+
controller.lastError = undefined;
|
|
1255
|
+
stopObserver(controller, { reason: "bootstrap-error" });
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (!state.active || controller.disposed) return;
|
|
1260
|
+
if (state.activityVersion !== version) continue;
|
|
1261
|
+
|
|
1262
|
+
// setRoot() may briefly disconnect/reconnect while preserving bootstrap
|
|
1263
|
+
// state. Wait another turn rather than treating that transition as settled.
|
|
1264
|
+
if (!controller.connected) {
|
|
1265
|
+
await waitForBootstrapIdle(0);
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
stopObserver(controller, { reason: "bootstrap-idle" });
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function waitForBootstrapIdle(idleMs) {
|
|
1275
|
+
return new Promise(resolve => {
|
|
1276
|
+
const schedule = globalThis.setTimeout ?? ((callback) => Promise.resolve().then(callback));
|
|
1277
|
+
schedule(resolve, idleMs);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function waitForBootstrapDOMReady(root) {
|
|
1282
|
+
const document = root?.nodeType === 9
|
|
1283
|
+
? root
|
|
1284
|
+
: root?.ownerDocument ?? (root === globalThis.document ? root : undefined);
|
|
1285
|
+
|
|
1286
|
+
if (!document || document.readyState !== "loading" || typeof document.addEventListener !== "function") {
|
|
1287
|
+
return Promise.resolve();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
return new Promise(resolve => {
|
|
1291
|
+
document.addEventListener("DOMContentLoaded", resolve, { once: true });
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1199
1295
|
async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
1200
1296
|
const MutationObserverImpl = callOptions.MutationObserver ?? controller.MutationObserver ?? globalThis.MutationObserver;
|
|
1201
1297
|
|
|
@@ -1220,8 +1316,16 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
1220
1316
|
}
|
|
1221
1317
|
|
|
1222
1318
|
const observer = new MutationObserverImpl(records => {
|
|
1223
|
-
|
|
1224
|
-
|
|
1319
|
+
const batch = collectObserverMutationBatch(controller, records, callOptions);
|
|
1320
|
+
if (!batch) return;
|
|
1321
|
+
markBootstrapObserverActivity(controller);
|
|
1322
|
+
|
|
1323
|
+
if (batch.requiresFallbackScan) {
|
|
1324
|
+
void enqueueObserverScan(controller, callOptions).catch(() => {});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
void enqueueObserverBatch(controller, batch, callOptions).catch(() => {});
|
|
1225
1329
|
});
|
|
1226
1330
|
|
|
1227
1331
|
controller.observer = observer;
|
|
@@ -1248,61 +1352,134 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
1248
1352
|
return result;
|
|
1249
1353
|
}
|
|
1250
1354
|
|
|
1251
|
-
function
|
|
1355
|
+
function collectObserverMutationBatch(controller, records, callOptions = {}) {
|
|
1252
1356
|
const mutations = Array.from(records ?? []);
|
|
1253
1357
|
|
|
1254
1358
|
// MutationObserver never invokes its callback with an empty batch, but custom
|
|
1255
1359
|
// test/host implementations sometimes do. Preserve the historical conservative
|
|
1256
|
-
// behavior for those implementations
|
|
1257
|
-
if (mutations.length === 0)
|
|
1360
|
+
// behavior for those implementations by falling back to a full scan.
|
|
1361
|
+
if (mutations.length === 0) {
|
|
1362
|
+
return {
|
|
1363
|
+
declarations: [],
|
|
1364
|
+
solidRenders: [],
|
|
1365
|
+
removedRecords: [],
|
|
1366
|
+
requiresFallbackScan: true,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1258
1369
|
|
|
1259
1370
|
const selector = callOptions.selector ?? controller.selector;
|
|
1371
|
+
const declarations = [];
|
|
1372
|
+
const declarationSet = new Set();
|
|
1373
|
+
const solidRenders = [];
|
|
1374
|
+
const solidRenderSet = new Set();
|
|
1375
|
+
const removedRecords = new Set();
|
|
1260
1376
|
|
|
1261
1377
|
for (const mutation of mutations) {
|
|
1262
|
-
if (!mutation || mutation.type !== "childList")
|
|
1378
|
+
if (!mutation || mutation.type !== "childList") {
|
|
1379
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1380
|
+
}
|
|
1263
1381
|
|
|
1264
1382
|
const addedNodes = mutation.addedNodes;
|
|
1265
1383
|
const removedNodes = mutation.removedNodes;
|
|
1266
|
-
if (addedNodes == null || removedNodes == null)
|
|
1384
|
+
if (addedNodes == null || removedNodes == null) {
|
|
1385
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1386
|
+
}
|
|
1267
1387
|
|
|
1268
1388
|
for (const node of Array.from(addedNodes)) {
|
|
1269
|
-
|
|
1389
|
+
const declarationMatches = collectNodeMatches(node, selector);
|
|
1390
|
+
if (declarationMatches.uncertain) {
|
|
1391
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1392
|
+
}
|
|
1393
|
+
for (const element of declarationMatches.elements) {
|
|
1394
|
+
// Explicit APIs claim before inserting into the DOM. If this controller
|
|
1395
|
+
// already owns the element, the observer has no discovery work to do.
|
|
1396
|
+
if (elementOwners.get(element)?.owner === controller) continue;
|
|
1397
|
+
if (controller.ignoredExisting.has(element)) continue;
|
|
1398
|
+
if (!declarationSet.has(element)) {
|
|
1399
|
+
declarationSet.add(element);
|
|
1400
|
+
declarations.push(element);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1270
1403
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1404
|
+
const renderMatches = collectNodeMatches(node, "solid-render");
|
|
1405
|
+
if (renderMatches.uncertain) {
|
|
1406
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1407
|
+
}
|
|
1408
|
+
for (const element of renderMatches.elements) {
|
|
1409
|
+
if (!solidRenderSet.has(element)) {
|
|
1410
|
+
solidRenderSet.add(element);
|
|
1411
|
+
solidRenders.push(element);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1274
1414
|
}
|
|
1275
1415
|
|
|
1276
1416
|
for (const node of Array.from(removedNodes)) {
|
|
1277
|
-
|
|
1417
|
+
const result = collectRemovedMountedRecords(controller, node, removedRecords);
|
|
1418
|
+
if (!result) {
|
|
1419
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1420
|
+
}
|
|
1278
1421
|
}
|
|
1279
1422
|
}
|
|
1280
1423
|
|
|
1281
|
-
|
|
1424
|
+
if (declarations.length === 0 && solidRenders.length === 0 && removedRecords.size === 0) {
|
|
1425
|
+
return undefined;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
return {
|
|
1429
|
+
declarations,
|
|
1430
|
+
solidRenders,
|
|
1431
|
+
removedRecords: [...removedRecords],
|
|
1432
|
+
requiresFallbackScan: false,
|
|
1433
|
+
};
|
|
1282
1434
|
}
|
|
1283
1435
|
|
|
1284
|
-
function
|
|
1285
|
-
|
|
1436
|
+
function collectNodeMatches(node, selector) {
|
|
1437
|
+
const elements = [];
|
|
1438
|
+
const seen = new Set();
|
|
1439
|
+
if (!node) return { elements, uncertain: false };
|
|
1286
1440
|
|
|
1441
|
+
let inspected = false;
|
|
1287
1442
|
try {
|
|
1288
|
-
if (typeof node.matches === "function"
|
|
1289
|
-
|
|
1290
|
-
|
|
1443
|
+
if (typeof node.matches === "function") {
|
|
1444
|
+
inspected = true;
|
|
1445
|
+
if (node.matches(selector)) {
|
|
1446
|
+
seen.add(node);
|
|
1447
|
+
elements.push(node);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (typeof node.querySelectorAll === "function") {
|
|
1452
|
+
inspected = true;
|
|
1453
|
+
for (const element of Array.from(node.querySelectorAll(selector) ?? [])) {
|
|
1454
|
+
if (seen.has(element)) continue;
|
|
1455
|
+
seen.add(element);
|
|
1456
|
+
elements.push(element);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1291
1459
|
} catch {
|
|
1292
|
-
|
|
1293
|
-
// should keep the old conservative behavior rather than miss discovery.
|
|
1294
|
-
return true;
|
|
1460
|
+
return { elements, uncertain: true };
|
|
1295
1461
|
}
|
|
1296
1462
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1463
|
+
if (inspected) return { elements, uncertain: false };
|
|
1464
|
+
|
|
1465
|
+
// Text/comment nodes cannot contain declarations. Element/document/fragment
|
|
1466
|
+
// nodes in a standards DOM should expose matches/querySelectorAll as
|
|
1467
|
+
// appropriate; a host object that does not is conservatively scanned.
|
|
1468
|
+
if (typeof node.nodeType === "number") {
|
|
1469
|
+
if (node.nodeType === 3 || node.nodeType === 4 || node.nodeType === 8) {
|
|
1470
|
+
return { elements, uncertain: false };
|
|
1471
|
+
}
|
|
1472
|
+
return { elements, uncertain: true };
|
|
1473
|
+
}
|
|
1299
1474
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1475
|
+
return {
|
|
1476
|
+
elements,
|
|
1477
|
+
uncertain: typeof node.getAttribute === "function" || typeof node.contains === "function",
|
|
1478
|
+
};
|
|
1302
1479
|
}
|
|
1303
1480
|
|
|
1304
|
-
function
|
|
1305
|
-
if (!node) return
|
|
1481
|
+
function collectRemovedMountedRecords(controller, node, records) {
|
|
1482
|
+
if (!node) return true;
|
|
1306
1483
|
|
|
1307
1484
|
for (const record of controller.bindings.values()) {
|
|
1308
1485
|
if (!record.mount) continue;
|
|
@@ -1310,16 +1487,22 @@ function removedNodeContainsMountedDeclaration(controller, node) {
|
|
|
1310
1487
|
? record.range?.startMarker
|
|
1311
1488
|
: record.element;
|
|
1312
1489
|
if (!anchor) continue;
|
|
1313
|
-
if (node === anchor)
|
|
1490
|
+
if (node === anchor) {
|
|
1491
|
+
records.add(record);
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1314
1494
|
|
|
1315
1495
|
try {
|
|
1316
|
-
if (typeof node.contains === "function" && node.contains(anchor))
|
|
1496
|
+
if (typeof node.contains === "function" && node.contains(anchor)) {
|
|
1497
|
+
records.add(record);
|
|
1498
|
+
}
|
|
1317
1499
|
} catch {
|
|
1318
|
-
return
|
|
1500
|
+
return false;
|
|
1319
1501
|
}
|
|
1320
1502
|
}
|
|
1321
1503
|
|
|
1322
|
-
return
|
|
1504
|
+
if (typeof node.nodeType === "number" || typeof node.contains === "function") return true;
|
|
1505
|
+
return typeof node.getAttribute !== "function";
|
|
1323
1506
|
}
|
|
1324
1507
|
|
|
1325
1508
|
function stopObserver(controller, options = {}) {
|
|
@@ -1330,6 +1513,10 @@ function stopObserver(controller, options = {}) {
|
|
|
1330
1513
|
controller.observer = undefined;
|
|
1331
1514
|
controller.observerRoot = undefined;
|
|
1332
1515
|
if (!options.preserveOptions) controller.observerOptions = undefined;
|
|
1516
|
+
if (!options.preserveBootstrap && controller.bootstrapState) {
|
|
1517
|
+
controller.bootstrapState.active = false;
|
|
1518
|
+
controller.bootstrapState = undefined;
|
|
1519
|
+
}
|
|
1333
1520
|
emitHTML(controller, {
|
|
1334
1521
|
type: "observer-disconnected",
|
|
1335
1522
|
root,
|
|
@@ -1345,54 +1532,56 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1345
1532
|
const selector = callOptions.selector ?? controller.selector;
|
|
1346
1533
|
const origin = callOptions.origin ?? "observer";
|
|
1347
1534
|
const discovered = Array.from(root.querySelectorAll(selector));
|
|
1348
|
-
const
|
|
1349
|
-
let matched = 0;
|
|
1350
|
-
let ignored = 0;
|
|
1351
|
-
|
|
1352
|
-
for (const element of discovered) {
|
|
1353
|
-
if (controller.ignoredExisting.has(element)) {
|
|
1354
|
-
ignored += 1;
|
|
1355
|
-
emitHTML(controller, {
|
|
1356
|
-
type: "element-discovered",
|
|
1357
|
-
element,
|
|
1358
|
-
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
1359
|
-
origin,
|
|
1360
|
-
accepted: false,
|
|
1361
|
-
});
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1535
|
+
const classified = classifyObserverElements(controller, discovered, callOptions, origin);
|
|
1364
1536
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1537
|
+
let result;
|
|
1538
|
+
try {
|
|
1539
|
+
result = await processElements(controller, classified.elements, {
|
|
1540
|
+
...callOptions,
|
|
1541
|
+
root,
|
|
1370
1542
|
origin,
|
|
1371
|
-
|
|
1543
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
1544
|
+
executeRenders: callOptions.executeRenders ?? controller.executeRenders,
|
|
1372
1545
|
});
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
matched += 1;
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
emitHTMLError(controller, "observer", error, { origin });
|
|
1548
|
+
throw error;
|
|
1549
|
+
}
|
|
1379
1550
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
ignored += 1;
|
|
1387
|
-
continue;
|
|
1388
|
-
}
|
|
1551
|
+
await cleanupDetachedRenderedDeclarations(controller, root);
|
|
1552
|
+
await retrySolidRenderElements(
|
|
1553
|
+
controller,
|
|
1554
|
+
collectConnectedSolidRenderElements(controller, root),
|
|
1555
|
+
{ reason: "observer-batch-complete", forceMissingError: true },
|
|
1556
|
+
);
|
|
1389
1557
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1558
|
+
emitObserverBatch(controller, {
|
|
1559
|
+
root,
|
|
1560
|
+
origin,
|
|
1561
|
+
discovered: discovered.length,
|
|
1562
|
+
matched: classified.matched,
|
|
1563
|
+
ignored: classified.ignored,
|
|
1564
|
+
result,
|
|
1565
|
+
});
|
|
1566
|
+
return result;
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
trackObserverTask(controller, task, callOptions);
|
|
1570
|
+
return task;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
async function enqueueObserverBatch(controller, batch, callOptions = {}) {
|
|
1574
|
+
const task = controller.queue.then(async () => {
|
|
1575
|
+
if (!controller.connected) return emptyRegistrationResult();
|
|
1576
|
+
|
|
1577
|
+
const root = controller.observerRoot ?? getRoot(mergeOptions(controller, callOptions), "HTML runtime observer");
|
|
1578
|
+
const origin = callOptions.origin ?? "observer";
|
|
1579
|
+
const discovered = batch.declarations;
|
|
1580
|
+
const classified = classifyObserverElements(controller, discovered, callOptions, origin);
|
|
1392
1581
|
|
|
1393
1582
|
let result;
|
|
1394
1583
|
try {
|
|
1395
|
-
result = await processElements(controller, elements, {
|
|
1584
|
+
result = await processElements(controller, classified.elements, {
|
|
1396
1585
|
...callOptions,
|
|
1397
1586
|
root,
|
|
1398
1587
|
origin,
|
|
@@ -1404,26 +1593,106 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1404
1593
|
throw error;
|
|
1405
1594
|
}
|
|
1406
1595
|
|
|
1407
|
-
await
|
|
1596
|
+
await cleanupRemovedRenderedDeclarations(controller, root, batch.removedRecords);
|
|
1408
1597
|
await retrySolidRenderElements(
|
|
1409
1598
|
controller,
|
|
1410
|
-
|
|
1599
|
+
batch.solidRenders,
|
|
1411
1600
|
{ reason: "observer-batch-complete", forceMissingError: true },
|
|
1412
1601
|
);
|
|
1413
1602
|
|
|
1414
|
-
|
|
1415
|
-
type: "observer-batch",
|
|
1603
|
+
emitObserverBatch(controller, {
|
|
1416
1604
|
root,
|
|
1417
1605
|
origin,
|
|
1418
1606
|
discovered: discovered.length,
|
|
1419
|
-
matched,
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
modules: result.modules.map(module => module.id),
|
|
1607
|
+
matched: classified.matched,
|
|
1608
|
+
ignored: classified.ignored,
|
|
1609
|
+
result,
|
|
1423
1610
|
});
|
|
1424
1611
|
return result;
|
|
1425
1612
|
});
|
|
1426
1613
|
|
|
1614
|
+
trackObserverTask(controller, task, callOptions);
|
|
1615
|
+
return task;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function classifyObserverElements(controller, discovered, callOptions, origin) {
|
|
1619
|
+
const elements = [];
|
|
1620
|
+
let matched = 0;
|
|
1621
|
+
let ignored = 0;
|
|
1622
|
+
|
|
1623
|
+
for (const element of discovered) {
|
|
1624
|
+
if (controller.ignoredExisting.has(element)) {
|
|
1625
|
+
ignored += 1;
|
|
1626
|
+
emitHTML(controller, {
|
|
1627
|
+
type: "element-discovered",
|
|
1628
|
+
element,
|
|
1629
|
+
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
1630
|
+
origin,
|
|
1631
|
+
accepted: false,
|
|
1632
|
+
});
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const accepted = acceptsDiscoveredElement(controller, element, callOptions);
|
|
1637
|
+
emitHTML(controller, {
|
|
1638
|
+
type: "element-discovered",
|
|
1639
|
+
element,
|
|
1640
|
+
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
1641
|
+
origin,
|
|
1642
|
+
accepted,
|
|
1643
|
+
});
|
|
1644
|
+
if (!accepted) {
|
|
1645
|
+
ignored += 1;
|
|
1646
|
+
maybeWarnUnresolvedRenderScope(controller, element, callOptions, origin);
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
matched += 1;
|
|
1650
|
+
|
|
1651
|
+
const ownership = elementOwners.get(element);
|
|
1652
|
+
if (!ownership) {
|
|
1653
|
+
elements.push(element);
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
if (ownership.owner === controller) {
|
|
1657
|
+
ignored += 1;
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
throw ownershipError(controller, element, ownership);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
return { elements, matched, ignored };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
async function cleanupRemovedRenderedDeclarations(controller, root, records) {
|
|
1668
|
+
for (const record of records ?? []) {
|
|
1669
|
+
if (!record?.mount) continue;
|
|
1670
|
+
const anchor = record.mount.mode === "in-place"
|
|
1671
|
+
? record.range?.startMarker
|
|
1672
|
+
: record.element;
|
|
1673
|
+
if (!anchor || rootContainsElement(root, anchor)) continue;
|
|
1674
|
+
|
|
1675
|
+
await disposeRecordMount(controller, record, {
|
|
1676
|
+
reason: "declaration-detached",
|
|
1677
|
+
removeAnchors: false,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function emitObserverBatch(controller, { root, origin, discovered, matched, ignored, result }) {
|
|
1683
|
+
emitHTML(controller, {
|
|
1684
|
+
type: "observer-batch",
|
|
1685
|
+
root,
|
|
1686
|
+
origin,
|
|
1687
|
+
discovered,
|
|
1688
|
+
matched,
|
|
1689
|
+
claimed: result.modules.length,
|
|
1690
|
+
ignored,
|
|
1691
|
+
modules: result.modules.map(module => module.id),
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function trackObserverTask(controller, task, callOptions) {
|
|
1427
1696
|
controller.queue = task.then(
|
|
1428
1697
|
() => undefined,
|
|
1429
1698
|
error => {
|
|
@@ -1435,8 +1704,6 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1435
1704
|
}
|
|
1436
1705
|
},
|
|
1437
1706
|
);
|
|
1438
|
-
|
|
1439
|
-
return task;
|
|
1440
1707
|
}
|
|
1441
1708
|
|
|
1442
1709
|
async function processElements(controller, elements, options = {}) {
|