solid-tag-runtime 0.0.14 → 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 +91 -0
- package/README.md +34 -3
- package/bench/observer.mjs +103 -0
- package/docs/api/html.md +9 -1
- package/docs/getting-started.md +3 -3
- package/docs/html-runtime.md +37 -3
- package/docs/lifecycle-events.md +7 -0
- 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 +396 -54
package/ARCHITECTURE.md
CHANGED
|
@@ -1458,3 +1458,94 @@ Decision: fix wrapperless Solid 2 delegated events with the smallest ownership-p
|
|
|
1458
1458
|
|
|
1459
1459
|
The delegation host is infrastructure only. It is not a shared application owner, is not a parent of declarative roots, is not disposed with an individual range, and does not change selector rendering or `<solid-render>` container ownership. Low-level `createRuntime()` behavior is unchanged.
|
|
1460
1460
|
|
|
1461
|
+
### 0.0.15 — declaration-driven observer scheduling
|
|
1462
|
+
|
|
1463
|
+
Decision: harden `solid-tag-runtime/html` observation by filtering `MutationObserver` child-list records before `enqueueObserverScan()` is scheduled. The observer no longer rescans its root for arbitrary UI DOM churn.
|
|
1464
|
+
|
|
1465
|
+
A mutation requires observer work when at least one of these is true:
|
|
1466
|
+
|
|
1467
|
+
1. an added node is, or contains, an element matching the controller's runtime declaration selector;
|
|
1468
|
+
2. an added node is, or contains, `<solid-render>`, preserving observer-batch retry behavior for newly connected renderer instances;
|
|
1469
|
+
3. a removed subtree contains the active anchor for a mounted declarative render, so detached render ownership can still be disposed;
|
|
1470
|
+
4. the host MutationObserver/DOM implementation cannot be classified safely, in which case the runtime conservatively preserves the previous scan behavior.
|
|
1471
|
+
|
|
1472
|
+
Explicit `html.flush()` remains a forced scan and does not apply mutation filtering. Empty mutation batches from custom/test `MutationObserver` implementations also retain conservative scan behavior for compatibility.
|
|
1473
|
+
|
|
1474
|
+
Reason: HTML lifecycle subscribers, devtools, status panels, and normal application rendering may mutate DOM inside a document-scoped observer. Previously every child-list mutation queued a full discovery scan, `observer-batch` could cause subscriber UI updates, and those updates could schedule another scan indefinitely. Observer scheduling must be driven by possible runtime-declaration effects rather than arbitrary DOM activity.
|
|
1475
|
+
|
|
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
|
+
|
|
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,37 @@ 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
|
+
|
|
44
|
+
## 0.0.15 observer hardening
|
|
45
|
+
|
|
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.
|
|
47
|
+
|
|
48
|
+
This prevents lifecycle/devtools subscribers that render diagnostics into an observed document from creating observer → event → DOM mutation feedback loops.
|
|
49
|
+
|
|
19
50
|
## Install
|
|
20
51
|
|
|
21
52
|
```bash
|
|
@@ -211,9 +242,9 @@ When loading from an import map, map every used package subpath to the same rele
|
|
|
211
242
|
```json
|
|
212
243
|
{
|
|
213
244
|
"imports": {
|
|
214
|
-
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.
|
|
215
|
-
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.
|
|
216
|
-
"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"
|
|
217
248
|
}
|
|
218
249
|
}
|
|
219
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,11 +30,19 @@ 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
|
|
|
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.
|
|
45
|
+
|
|
38
46
|
## Explicit ownership
|
|
39
47
|
|
|
40
48
|
```ts
|
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,15 +20,49 @@ 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
|
+
```
|
|
33
|
+
|
|
34
|
+
If raw DOM insertion is known and occasional, keep observation disabled and synchronize explicitly:
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
```ts
|
|
37
|
+
root.insertAdjacentHTML("beforeend", markup);
|
|
29
38
|
await html.flush();
|
|
30
39
|
```
|
|
31
40
|
|
|
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.
|
|
63
|
+
|
|
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.
|
|
65
|
+
|
|
32
66
|
## Script declarations
|
|
33
67
|
|
|
34
68
|
```html
|
package/docs/lifecycle-events.md
CHANGED
|
@@ -74,4 +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 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
|
+
|
|
77
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`, the tested fallback line is Solid `2.0.0-rc.13`.
|
|
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
|
|
|
@@ -1219,8 +1315,17 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
1219
1315
|
}
|
|
1220
1316
|
}
|
|
1221
1317
|
|
|
1222
|
-
const observer = new MutationObserverImpl(
|
|
1223
|
-
|
|
1318
|
+
const observer = new MutationObserverImpl(records => {
|
|
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(() => {});
|
|
1224
1329
|
});
|
|
1225
1330
|
|
|
1226
1331
|
controller.observer = observer;
|
|
@@ -1247,6 +1352,159 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
1247
1352
|
return result;
|
|
1248
1353
|
}
|
|
1249
1354
|
|
|
1355
|
+
function collectObserverMutationBatch(controller, records, callOptions = {}) {
|
|
1356
|
+
const mutations = Array.from(records ?? []);
|
|
1357
|
+
|
|
1358
|
+
// MutationObserver never invokes its callback with an empty batch, but custom
|
|
1359
|
+
// test/host implementations sometimes do. Preserve the historical conservative
|
|
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
|
+
}
|
|
1369
|
+
|
|
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();
|
|
1376
|
+
|
|
1377
|
+
for (const mutation of mutations) {
|
|
1378
|
+
if (!mutation || mutation.type !== "childList") {
|
|
1379
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const addedNodes = mutation.addedNodes;
|
|
1383
|
+
const removedNodes = mutation.removedNodes;
|
|
1384
|
+
if (addedNodes == null || removedNodes == null) {
|
|
1385
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
for (const node of Array.from(addedNodes)) {
|
|
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
|
+
}
|
|
1403
|
+
|
|
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
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
for (const node of Array.from(removedNodes)) {
|
|
1417
|
+
const result = collectRemovedMountedRecords(controller, node, removedRecords);
|
|
1418
|
+
if (!result) {
|
|
1419
|
+
return { declarations, solidRenders, removedRecords: [...removedRecords], requiresFallbackScan: true };
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
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
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function collectNodeMatches(node, selector) {
|
|
1437
|
+
const elements = [];
|
|
1438
|
+
const seen = new Set();
|
|
1439
|
+
if (!node) return { elements, uncertain: false };
|
|
1440
|
+
|
|
1441
|
+
let inspected = false;
|
|
1442
|
+
try {
|
|
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
|
+
}
|
|
1459
|
+
} catch {
|
|
1460
|
+
return { elements, uncertain: true };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
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
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return {
|
|
1476
|
+
elements,
|
|
1477
|
+
uncertain: typeof node.getAttribute === "function" || typeof node.contains === "function",
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function collectRemovedMountedRecords(controller, node, records) {
|
|
1482
|
+
if (!node) return true;
|
|
1483
|
+
|
|
1484
|
+
for (const record of controller.bindings.values()) {
|
|
1485
|
+
if (!record.mount) continue;
|
|
1486
|
+
const anchor = record.mount.mode === "in-place"
|
|
1487
|
+
? record.range?.startMarker
|
|
1488
|
+
: record.element;
|
|
1489
|
+
if (!anchor) continue;
|
|
1490
|
+
if (node === anchor) {
|
|
1491
|
+
records.add(record);
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
try {
|
|
1496
|
+
if (typeof node.contains === "function" && node.contains(anchor)) {
|
|
1497
|
+
records.add(record);
|
|
1498
|
+
}
|
|
1499
|
+
} catch {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (typeof node.nodeType === "number" || typeof node.contains === "function") return true;
|
|
1505
|
+
return typeof node.getAttribute !== "function";
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1250
1508
|
function stopObserver(controller, options = {}) {
|
|
1251
1509
|
if (!controller.connected && !controller.observer) return;
|
|
1252
1510
|
const root = controller.observerRoot;
|
|
@@ -1255,6 +1513,10 @@ function stopObserver(controller, options = {}) {
|
|
|
1255
1513
|
controller.observer = undefined;
|
|
1256
1514
|
controller.observerRoot = undefined;
|
|
1257
1515
|
if (!options.preserveOptions) controller.observerOptions = undefined;
|
|
1516
|
+
if (!options.preserveBootstrap && controller.bootstrapState) {
|
|
1517
|
+
controller.bootstrapState.active = false;
|
|
1518
|
+
controller.bootstrapState = undefined;
|
|
1519
|
+
}
|
|
1258
1520
|
emitHTML(controller, {
|
|
1259
1521
|
type: "observer-disconnected",
|
|
1260
1522
|
root,
|
|
@@ -1270,54 +1532,56 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1270
1532
|
const selector = callOptions.selector ?? controller.selector;
|
|
1271
1533
|
const origin = callOptions.origin ?? "observer";
|
|
1272
1534
|
const discovered = Array.from(root.querySelectorAll(selector));
|
|
1273
|
-
const
|
|
1274
|
-
let matched = 0;
|
|
1275
|
-
let ignored = 0;
|
|
1276
|
-
|
|
1277
|
-
for (const element of discovered) {
|
|
1278
|
-
if (controller.ignoredExisting.has(element)) {
|
|
1279
|
-
ignored += 1;
|
|
1280
|
-
emitHTML(controller, {
|
|
1281
|
-
type: "element-discovered",
|
|
1282
|
-
element,
|
|
1283
|
-
moduleId: nonEmpty(element?.getAttribute?.("module")),
|
|
1284
|
-
origin,
|
|
1285
|
-
accepted: false,
|
|
1286
|
-
});
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1535
|
+
const classified = classifyObserverElements(controller, discovered, callOptions, origin);
|
|
1289
1536
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1537
|
+
let result;
|
|
1538
|
+
try {
|
|
1539
|
+
result = await processElements(controller, classified.elements, {
|
|
1540
|
+
...callOptions,
|
|
1541
|
+
root,
|
|
1295
1542
|
origin,
|
|
1296
|
-
|
|
1543
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
1544
|
+
executeRenders: callOptions.executeRenders ?? controller.executeRenders,
|
|
1297
1545
|
});
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
}
|
|
1303
|
-
matched += 1;
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
emitHTMLError(controller, "observer", error, { origin });
|
|
1548
|
+
throw error;
|
|
1549
|
+
}
|
|
1304
1550
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
ignored += 1;
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1551
|
+
await cleanupDetachedRenderedDeclarations(controller, root);
|
|
1552
|
+
await retrySolidRenderElements(
|
|
1553
|
+
controller,
|
|
1554
|
+
collectConnectedSolidRenderElements(controller, root),
|
|
1555
|
+
{ reason: "observer-batch-complete", forceMissingError: true },
|
|
1556
|
+
);
|
|
1314
1557
|
|
|
1315
|
-
|
|
1316
|
-
|
|
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);
|
|
1317
1581
|
|
|
1318
1582
|
let result;
|
|
1319
1583
|
try {
|
|
1320
|
-
result = await processElements(controller, elements, {
|
|
1584
|
+
result = await processElements(controller, classified.elements, {
|
|
1321
1585
|
...callOptions,
|
|
1322
1586
|
root,
|
|
1323
1587
|
origin,
|
|
@@ -1329,26 +1593,106 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1329
1593
|
throw error;
|
|
1330
1594
|
}
|
|
1331
1595
|
|
|
1332
|
-
await
|
|
1596
|
+
await cleanupRemovedRenderedDeclarations(controller, root, batch.removedRecords);
|
|
1333
1597
|
await retrySolidRenderElements(
|
|
1334
1598
|
controller,
|
|
1335
|
-
|
|
1599
|
+
batch.solidRenders,
|
|
1336
1600
|
{ reason: "observer-batch-complete", forceMissingError: true },
|
|
1337
1601
|
);
|
|
1338
1602
|
|
|
1339
|
-
|
|
1340
|
-
type: "observer-batch",
|
|
1603
|
+
emitObserverBatch(controller, {
|
|
1341
1604
|
root,
|
|
1342
1605
|
origin,
|
|
1343
1606
|
discovered: discovered.length,
|
|
1344
|
-
matched,
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
modules: result.modules.map(module => module.id),
|
|
1607
|
+
matched: classified.matched,
|
|
1608
|
+
ignored: classified.ignored,
|
|
1609
|
+
result,
|
|
1348
1610
|
});
|
|
1349
1611
|
return result;
|
|
1350
1612
|
});
|
|
1351
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) {
|
|
1352
1696
|
controller.queue = task.then(
|
|
1353
1697
|
() => undefined,
|
|
1354
1698
|
error => {
|
|
@@ -1360,8 +1704,6 @@ async function enqueueObserverScan(controller, callOptions = {}) {
|
|
|
1360
1704
|
}
|
|
1361
1705
|
},
|
|
1362
1706
|
);
|
|
1363
|
-
|
|
1364
|
-
return task;
|
|
1365
1707
|
}
|
|
1366
1708
|
|
|
1367
1709
|
async function processElements(controller, elements, options = {}) {
|