solid-tag-runtime 0.0.14 → 0.0.15
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 +18 -0
- package/README.md +9 -3
- package/docs/api/html.md +2 -0
- package/docs/getting-started.md +3 -3
- package/docs/html-runtime.md +4 -0
- package/docs/lifecycle-events.md +2 -0
- package/docs/solid-runtime-setup.md +1 -1
- package/package.json +1 -1
- package/src/html.js +76 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -1458,3 +1458,21 @@ 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`.
|
package/README.md
CHANGED
|
@@ -16,6 +16,12 @@ 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.15 observer hardening
|
|
20
|
+
|
|
21
|
+
`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.
|
|
22
|
+
|
|
23
|
+
This prevents lifecycle/devtools subscribers that render diagnostics into an observed document from creating observer → event → DOM mutation feedback loops.
|
|
24
|
+
|
|
19
25
|
## Install
|
|
20
26
|
|
|
21
27
|
```bash
|
|
@@ -211,9 +217,9 @@ When loading from an import map, map every used package subpath to the same rele
|
|
|
211
217
|
```json
|
|
212
218
|
{
|
|
213
219
|
"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.
|
|
220
|
+
"solid-tag-runtime": "https://esm.sh/solid-tag-runtime@0.0.15",
|
|
221
|
+
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.15/html",
|
|
222
|
+
"solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.15/solid"
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
```
|
package/docs/api/html.md
CHANGED
|
@@ -35,6 +35,8 @@ await html.flush(options?);
|
|
|
35
35
|
html.disconnect();
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
`observe()` filters child-list mutation records and schedules discovery only for mutations that can affect runtime declarations, `<solid-render>` retry work, or mounted declarative-render cleanup. `flush()` bypasses that filter and forces a scan.
|
|
39
|
+
|
|
38
40
|
## Explicit ownership
|
|
39
41
|
|
|
40
42
|
```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.15",
|
|
108
|
+
"solid-tag-runtime/html": "https://esm.sh/solid-tag-runtime@0.0.15/html",
|
|
109
|
+
"solid-tag-runtime/solid": "https://esm.sh/solid-tag-runtime@0.0.15/solid"
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
```
|
package/docs/html-runtime.md
CHANGED
|
@@ -29,6 +29,10 @@ await html.observe({ registerExisting: false });
|
|
|
29
29
|
await html.flush();
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
The live observer is declaration-driven rather than mutation-driven. It inspects each `MutationRecord` before scheduling discovery and ignores ordinary UI child-list changes that cannot add a matching runtime declaration or require mounted-render cleanup. This means a document-scoped controller can coexist with application/devtools DOM updates without repeatedly rescanning the document.
|
|
33
|
+
|
|
34
|
+
`flush()` is intentionally different: it remains an explicit forced scan and can be used as a deterministic synchronization point even when no relevant observer mutation was seen.
|
|
35
|
+
|
|
32
36
|
## Script declarations
|
|
33
37
|
|
|
34
38
|
```html
|
package/docs/lifecycle-events.md
CHANGED
|
@@ -74,4 +74,6 @@ 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 actually scheduled. Unrelated child-list mutations are filtered before discovery, so a lifecycle subscriber may render diagnostics/status UI inside an observed document without causing a self-sustaining observer/event loop. Explicit `html.flush()` still performs and reports a forced scan.
|
|
78
|
+
|
|
77
79
|
Listener exceptions are isolated from runtime/HTML operations and are reported to the console rather than aborting the underlying action.
|
|
@@ -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` and `0.0.15`, 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
|
|
package/package.json
CHANGED
package/src/html.js
CHANGED
|
@@ -1219,7 +1219,8 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
}
|
|
1221
1221
|
|
|
1222
|
-
const observer = new MutationObserverImpl(
|
|
1222
|
+
const observer = new MutationObserverImpl(records => {
|
|
1223
|
+
if (!observerMutationsRequireScan(controller, records, callOptions)) return;
|
|
1223
1224
|
void enqueueObserverScan(controller, callOptions).catch(() => {});
|
|
1224
1225
|
});
|
|
1225
1226
|
|
|
@@ -1247,6 +1248,80 @@ async function connectObserver(controller, root, callOptions = {}, mode = {}) {
|
|
|
1247
1248
|
return result;
|
|
1248
1249
|
}
|
|
1249
1250
|
|
|
1251
|
+
function observerMutationsRequireScan(controller, records, callOptions = {}) {
|
|
1252
|
+
const mutations = Array.from(records ?? []);
|
|
1253
|
+
|
|
1254
|
+
// MutationObserver never invokes its callback with an empty batch, but custom
|
|
1255
|
+
// test/host implementations sometimes do. Preserve the historical conservative
|
|
1256
|
+
// behavior for those implementations instead of silently dropping discovery.
|
|
1257
|
+
if (mutations.length === 0) return true;
|
|
1258
|
+
|
|
1259
|
+
const selector = callOptions.selector ?? controller.selector;
|
|
1260
|
+
|
|
1261
|
+
for (const mutation of mutations) {
|
|
1262
|
+
if (!mutation || mutation.type !== "childList") return true;
|
|
1263
|
+
|
|
1264
|
+
const addedNodes = mutation.addedNodes;
|
|
1265
|
+
const removedNodes = mutation.removedNodes;
|
|
1266
|
+
if (addedNodes == null || removedNodes == null) return true;
|
|
1267
|
+
|
|
1268
|
+
for (const node of Array.from(addedNodes)) {
|
|
1269
|
+
if (nodeOrDescendantMatches(node, selector)) return true;
|
|
1270
|
+
|
|
1271
|
+
// <solid-render> is not part of the script selector, but observer batch
|
|
1272
|
+
// completion is also a retry boundary for newly connected render instances.
|
|
1273
|
+
if (nodeOrDescendantMatches(node, "solid-render")) return true;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
for (const node of Array.from(removedNodes)) {
|
|
1277
|
+
if (removedNodeContainsMountedDeclaration(controller, node)) return true;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function nodeOrDescendantMatches(node, selector) {
|
|
1285
|
+
if (!node) return false;
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
if (typeof node.matches === "function" && node.matches(selector)) return true;
|
|
1289
|
+
if (typeof node.querySelector === "function" && node.querySelector(selector)) return true;
|
|
1290
|
+
if (typeof node.querySelectorAll === "function" && node.querySelectorAll(selector)?.length > 0) return true;
|
|
1291
|
+
} catch {
|
|
1292
|
+
// A custom selector/DOM implementation that cannot be classified safely
|
|
1293
|
+
// should keep the old conservative behavior rather than miss discovery.
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Standard DOM nodes that did not match above cannot affect discovery.
|
|
1298
|
+
if (typeof node.nodeType === "number") return false;
|
|
1299
|
+
|
|
1300
|
+
// Unknown host DOM objects cannot be classified safely.
|
|
1301
|
+
return typeof node.getAttribute === "function";
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function removedNodeContainsMountedDeclaration(controller, node) {
|
|
1305
|
+
if (!node) return false;
|
|
1306
|
+
|
|
1307
|
+
for (const record of controller.bindings.values()) {
|
|
1308
|
+
if (!record.mount) continue;
|
|
1309
|
+
const anchor = record.mount.mode === "in-place"
|
|
1310
|
+
? record.range?.startMarker
|
|
1311
|
+
: record.element;
|
|
1312
|
+
if (!anchor) continue;
|
|
1313
|
+
if (node === anchor) return true;
|
|
1314
|
+
|
|
1315
|
+
try {
|
|
1316
|
+
if (typeof node.contains === "function" && node.contains(anchor)) return true;
|
|
1317
|
+
} catch {
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1250
1325
|
function stopObserver(controller, options = {}) {
|
|
1251
1326
|
if (!controller.connected && !controller.observer) return;
|
|
1252
1327
|
const root = controller.observerRoot;
|