@llui/dom 0.0.17 → 0.0.19
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/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +5 -0
- package/dist/binding.js.map +1 -1
- package/dist/devtools.d.ts +109 -0
- package/dist/devtools.d.ts.map +1 -1
- package/dist/devtools.js +401 -1
- package/dist/devtools.js.map +1 -1
- package/dist/hmr.d.ts.map +1 -1
- package/dist/hmr.js +2 -0
- package/dist/hmr.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +18 -2
- package/dist/mount.js.map +1 -1
- package/dist/primitives/branch.d.ts.map +1 -1
- package/dist/primitives/branch.js +10 -1
- package/dist/primitives/branch.js.map +1 -1
- package/dist/primitives/child.d.ts.map +1 -1
- package/dist/primitives/child.js +25 -5
- package/dist/primitives/child.js.map +1 -1
- package/dist/primitives/context.d.ts +30 -6
- package/dist/primitives/context.d.ts.map +1 -1
- package/dist/primitives/context.js +30 -6
- package/dist/primitives/context.js.map +1 -1
- package/dist/primitives/each.d.ts.map +1 -1
- package/dist/primitives/each.js +79 -6
- package/dist/primitives/each.js.map +1 -1
- package/dist/primitives/foreign.d.ts.map +1 -1
- package/dist/primitives/foreign.js +1 -0
- package/dist/primitives/foreign.js.map +1 -1
- package/dist/primitives/lazy.d.ts.map +1 -1
- package/dist/primitives/lazy.js +1 -0
- package/dist/primitives/lazy.js.map +1 -1
- package/dist/primitives/portal.d.ts.map +1 -1
- package/dist/primitives/portal.js +1 -0
- package/dist/primitives/portal.js.map +1 -1
- package/dist/primitives/show.d.ts.map +1 -1
- package/dist/primitives/show.js +4 -0
- package/dist/primitives/show.js.map +1 -1
- package/dist/primitives/virtual-each.d.ts.map +1 -1
- package/dist/primitives/virtual-each.js +3 -0
- package/dist/primitives/virtual-each.js.map +1 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js.map +1 -1
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +68 -1
- package/dist/scope.js.map +1 -1
- package/dist/ssr.d.ts.map +1 -1
- package/dist/ssr.js +5 -1
- package/dist/ssr.js.map +1 -1
- package/dist/tracking/coverage.d.ts +27 -0
- package/dist/tracking/coverage.d.ts.map +1 -0
- package/dist/tracking/coverage.js +39 -0
- package/dist/tracking/coverage.js.map +1 -0
- package/dist/tracking/disposer-log.d.ts +22 -0
- package/dist/tracking/disposer-log.d.ts.map +1 -0
- package/dist/tracking/disposer-log.js +3 -0
- package/dist/tracking/disposer-log.js.map +1 -0
- package/dist/tracking/each-diff.d.ts +62 -0
- package/dist/tracking/each-diff.d.ts.map +1 -0
- package/dist/tracking/each-diff.js +26 -0
- package/dist/tracking/each-diff.js.map +1 -0
- package/dist/tracking/effect-timeline.d.ts +73 -0
- package/dist/tracking/effect-timeline.d.ts.map +1 -0
- package/dist/tracking/effect-timeline.js +89 -0
- package/dist/tracking/effect-timeline.js.map +1 -0
- package/dist/types.d.ts +18 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-loop.d.ts.map +1 -1
- package/dist/update-loop.js +78 -0
- package/dist/update-loop.js.map +1 -1
- package/package.json +1 -1
package/dist/scope.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
let nextId = 1;
|
|
2
|
+
/**
|
|
3
|
+
* Walk up the scope chain to find the owning ComponentInstance. The
|
|
4
|
+
* instance is stamped onto the rootScope by `installDevTools`, so this
|
|
5
|
+
* returns null in production (no devtools) or for scopes that haven't
|
|
6
|
+
* yet been parented to a tracked root (e.g., during initial creation).
|
|
7
|
+
*/
|
|
8
|
+
function findInstance(scope) {
|
|
9
|
+
let s = scope;
|
|
10
|
+
while (s) {
|
|
11
|
+
if (s.instance)
|
|
12
|
+
return s.instance;
|
|
13
|
+
s = s.parent;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
2
17
|
// Shared empty arrays — avoid allocating per scope when unused
|
|
3
18
|
const EMPTY_SCOPES = [];
|
|
4
19
|
const EMPTY_DISPOSERS = [];
|
|
@@ -8,6 +23,14 @@ const EMPTY_UPDATERS = [];
|
|
|
8
23
|
// Capped to avoid memory leaks in apps that create/destroy thousands of rows.
|
|
9
24
|
const SCOPE_POOL = [];
|
|
10
25
|
const SCOPE_POOL_MAX = 2048;
|
|
26
|
+
// Dev-mode flag. Flipped to true by installDevTools() once any component
|
|
27
|
+
// instance has a disposer log. In production this stays false forever,
|
|
28
|
+
// and disposeScope skips findInstance entirely — zero cost.
|
|
29
|
+
let anyDisposerLogInstalled = false;
|
|
30
|
+
/** @internal — called by devtools.ts::installDevTools */
|
|
31
|
+
export function _markDisposerLogInstalled() {
|
|
32
|
+
anyDisposerLogInstalled = true;
|
|
33
|
+
}
|
|
11
34
|
/** @internal Drain the scope pool — for testing only */
|
|
12
35
|
export function _drainScopePool() {
|
|
13
36
|
SCOPE_POOL.length = 0;
|
|
@@ -18,7 +41,13 @@ export function createScope(parent) {
|
|
|
18
41
|
scope = SCOPE_POOL.pop();
|
|
19
42
|
scope.id = nextId++;
|
|
20
43
|
scope.parent = parent;
|
|
21
|
-
// Arrays already reset to empties by dispose
|
|
44
|
+
// Arrays already reset to empties by dispose. Reset dev-only hints
|
|
45
|
+
// so recycled scopes don't carry stale tagging/back-refs. Cheap
|
|
46
|
+
// (two undefined writes) and keeps production-identical behavior
|
|
47
|
+
// when these fields are never set.
|
|
48
|
+
scope.disposalCause = undefined;
|
|
49
|
+
scope.instance = undefined;
|
|
50
|
+
scope._kind = undefined;
|
|
22
51
|
}
|
|
23
52
|
else {
|
|
24
53
|
scope = {
|
|
@@ -50,6 +79,20 @@ export function createScope(parent) {
|
|
|
50
79
|
*/
|
|
51
80
|
export function disposeScope(scope, skipParentRemoval = false) {
|
|
52
81
|
if (scope.disposers.length === 0 && scope.children.length === 0 && scope.bindings.length === 0) {
|
|
82
|
+
// Dev-only: still emit a DisposerEvent for empty scopes — the log
|
|
83
|
+
// is meant to capture every scope the app destroys, not only ones
|
|
84
|
+
// that had attached work. Outer flag check keeps production (no
|
|
85
|
+
// devtools ever installed) at true zero cost — no parent-chain walk.
|
|
86
|
+
if (anyDisposerLogInstalled) {
|
|
87
|
+
const inst = findInstance(scope);
|
|
88
|
+
if (inst?._disposerLog !== undefined) {
|
|
89
|
+
inst._disposerLog.push({
|
|
90
|
+
scopeId: String(scope.id),
|
|
91
|
+
cause: scope.disposalCause ?? 'component-unmount',
|
|
92
|
+
timestamp: Date.now(),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
53
96
|
if (!skipParentRemoval)
|
|
54
97
|
removeFromParent(scope);
|
|
55
98
|
scope.parent = null;
|
|
@@ -67,6 +110,19 @@ export function disposeScope(scope, skipParentRemoval = false) {
|
|
|
67
110
|
for (const disposer of scope.disposers) {
|
|
68
111
|
disposer();
|
|
69
112
|
}
|
|
113
|
+
// Dev-only: emit disposer events into the owning instance's log.
|
|
114
|
+
// Outer flag check keeps production (no devtools ever installed) at
|
|
115
|
+
// true zero cost — skips the O(depth) parent-chain walk entirely.
|
|
116
|
+
if (anyDisposerLogInstalled) {
|
|
117
|
+
const inst = findInstance(scope);
|
|
118
|
+
if (inst?._disposerLog !== undefined) {
|
|
119
|
+
inst._disposerLog.push({
|
|
120
|
+
scopeId: String(scope.id),
|
|
121
|
+
cause: scope.disposalCause ?? 'component-unmount',
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
70
126
|
// Mark bindings as dead and break closure/DOM retention
|
|
71
127
|
for (const binding of scope.bindings) {
|
|
72
128
|
binding.dead = true;
|
|
@@ -118,6 +174,17 @@ export function disposeScopesBulk(scopes) {
|
|
|
118
174
|
const disposers = scope.disposers;
|
|
119
175
|
for (let d = 0; d < disposers.length; d++)
|
|
120
176
|
disposers[d]();
|
|
177
|
+
// Dev-only: emit disposer events — same guard as disposeScope.
|
|
178
|
+
if (anyDisposerLogInstalled) {
|
|
179
|
+
const inst = findInstance(scope);
|
|
180
|
+
if (inst?._disposerLog !== undefined) {
|
|
181
|
+
inst._disposerLog.push({
|
|
182
|
+
scopeId: String(scope.id),
|
|
183
|
+
cause: scope.disposalCause ?? 'component-unmount',
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
121
188
|
// Mark bindings dead
|
|
122
189
|
const bindings = scope.bindings;
|
|
123
190
|
for (let b = 0; b < bindings.length; b++) {
|
package/dist/scope.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Scope, Binding } from './types.js'\n\nlet nextId = 1\n\n// Shared empty arrays — avoid allocating per scope when unused\nconst EMPTY_SCOPES: Scope[] = []\nconst EMPTY_DISPOSERS: Array<() => void> = []\nconst EMPTY_BINDINGS: Binding[] = []\nconst EMPTY_UPDATERS: Array<() => void> = []\n\n// Scope pool — reuse disposed scope objects to reduce GC pressure.\n// Capped to avoid memory leaks in apps that create/destroy thousands of rows.\nconst SCOPE_POOL: Scope[] = []\nconst SCOPE_POOL_MAX = 2048\n\n/** @internal Drain the scope pool — for testing only */\nexport function _drainScopePool(): void {\n SCOPE_POOL.length = 0\n}\n\nexport function createScope(parent: Scope | null): Scope {\n let scope: Scope\n if (SCOPE_POOL.length > 0) {\n scope = SCOPE_POOL.pop()!\n scope.id = nextId++\n scope.parent = parent\n // Arrays already reset to empties by dispose\n } else {\n scope = {\n id: nextId++,\n parent,\n children: EMPTY_SCOPES,\n disposers: EMPTY_DISPOSERS,\n bindings: EMPTY_BINDINGS,\n itemUpdaters: EMPTY_UPDATERS,\n }\n }\n\n if (parent) {\n if (parent.children === EMPTY_SCOPES) parent.children = []\n parent.children.push(scope)\n }\n\n return scope\n}\n\n/**\n * Dispose a scope and all its children. By default, detaches the scope\n * from its parent's `children` array via `indexOf + splice` — O(N) per\n * call, which becomes O(N²) when disposing many sibling scopes in bulk\n * (e.g. `each` clearing 1000 rows).\n *\n * Pass `skipParentRemoval = true` when the caller will batch-remove\n * children afterwards (see `removeOrphanedFromParent`). The scope's\n * `parent` pointer is still set to `null` so the caller can identify\n * orphaned entries.\n */\nexport function disposeScope(scope: Scope, skipParentRemoval = false): void {\n if (scope.disposers.length === 0 && scope.children.length === 0 && scope.bindings.length === 0) {\n if (!skipParentRemoval) removeFromParent(scope)\n scope.parent = null\n // Don't pool empty scopes from the early-return path — they may be\n // disposed idempotently (twice), which would create pool duplicates\n return\n }\n\n // When skipParentRemoval is true, children don't mutate during disposal —\n // iterate directly without allocating a copy. Otherwise, clone to avoid\n // mutation during iteration.\n const children = skipParentRemoval ? scope.children : scope.children.slice()\n for (const child of children) {\n disposeScope(child, skipParentRemoval)\n }\n\n for (const disposer of scope.disposers) {\n disposer()\n }\n\n // Mark bindings as dead and break closure/DOM retention\n for (const binding of scope.bindings) {\n binding.dead = true\n binding.accessor = null!\n binding.node = null!\n binding.lastValue = undefined\n }\n\n // Reset to shared empties — don't just truncate, so pooled scopes\n // don't hold allocated-but-empty arrays\n scope.disposers = EMPTY_DISPOSERS\n scope.bindings = EMPTY_BINDINGS\n scope.children = EMPTY_SCOPES\n scope.itemUpdaters = EMPTY_UPDATERS\n\n if (!skipParentRemoval) removeFromParent(scope)\n scope.parent = null\n\n // Return to pool for reuse\n if (SCOPE_POOL.length < SCOPE_POOL_MAX) SCOPE_POOL.push(scope)\n}\n\n/**\n * Batch-remove children with `parent === null` from `parent.children`.\n * Called after a bulk `disposeScope(child, true)` pass to collapse the\n * individual O(N) splice operations into one O(N) scan.\n */\nexport function removeOrphanedChildren(parent: Scope): void {\n const children = parent.children\n let w = 0\n for (let r = 0; r < children.length; r++) {\n if (children[r]!.parent !== null) children[w++] = children[r]!\n }\n children.length = w\n}\n\n/**\n * Bulk dispose an array of sibling scopes — avoids per-scope function call\n * overhead. Used by each() clear path where 1000+ scopes are disposed at once.\n * Caller must call removeOrphanedChildren(parent) afterwards.\n */\nexport function disposeScopesBulk(scopes: Scope[]): void {\n for (let i = 0; i < scopes.length; i++) {\n const scope = scopes[i]!\n // Recursively dispose children\n const children = scope.children\n if (children.length > 0) {\n disposeScopesBulk(children)\n }\n // Run disposers\n const disposers = scope.disposers\n for (let d = 0; d < disposers.length; d++) disposers[d]!()\n // Mark bindings dead\n const bindings = scope.bindings\n for (let b = 0; b < bindings.length; b++) {\n const binding = bindings[b]!\n binding.dead = true\n binding.accessor = null!\n binding.node = null!\n binding.lastValue = undefined\n }\n // Reset to shared empties + detach from parent + return to pool\n scope.disposers = EMPTY_DISPOSERS\n scope.bindings = EMPTY_BINDINGS\n scope.children = EMPTY_SCOPES\n scope.itemUpdaters = EMPTY_UPDATERS\n scope.parent = null\n if (SCOPE_POOL.length < SCOPE_POOL_MAX) SCOPE_POOL.push(scope)\n }\n}\n\nexport function addBinding(scope: Scope, binding: Binding): void {\n binding.ownerScope = scope\n if (scope.bindings === EMPTY_BINDINGS) scope.bindings = []\n scope.bindings.push(binding)\n}\n\nexport function addItemUpdater(scope: Scope, updater: () => void): void {\n if (scope.itemUpdaters === EMPTY_UPDATERS) scope.itemUpdaters = []\n scope.itemUpdaters.push(updater)\n}\n\n/**\n * Register a per-item updater that compares the new value against the last\n * value before applying. Shared by `text()`, `elSplit()`, and `elTemplate()`\n * so the equality-check logic lives in one place.\n *\n * @param apply - DOM write: receives the new value when it differs\n * @returns the initial value (caller should apply it to the DOM)\n */\nexport function addCheckedItemUpdater<V>(scope: Scope, get: () => V, apply: (value: V) => void): V {\n let last: V = get()\n addItemUpdater(scope, () => {\n const v = get()\n if (v === last || (v !== v && last !== last)) return\n last = v\n apply(v)\n })\n return last\n}\n\nexport function addDisposer(scope: Scope, disposer: () => void): void {\n if (scope.disposers === EMPTY_DISPOSERS) scope.disposers = []\n scope.disposers.push(disposer)\n}\n\nfunction removeFromParent(scope: Scope): void {\n if (scope.parent) {\n const idx = scope.parent.children.indexOf(scope)\n if (idx !== -1) {\n scope.parent.children.splice(idx, 1)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Scope, Binding } from './types.js'\nimport type { ComponentInstance } from './update-loop.js'\n\nlet nextId = 1\n\n/**\n * Walk up the scope chain to find the owning ComponentInstance. The\n * instance is stamped onto the rootScope by `installDevTools`, so this\n * returns null in production (no devtools) or for scopes that haven't\n * yet been parented to a tracked root (e.g., during initial creation).\n */\nfunction findInstance(scope: Scope): ComponentInstance | null {\n let s: Scope | null = scope\n while (s) {\n if (s.instance) return s.instance\n s = s.parent\n }\n return null\n}\n\n// Shared empty arrays — avoid allocating per scope when unused\nconst EMPTY_SCOPES: Scope[] = []\nconst EMPTY_DISPOSERS: Array<() => void> = []\nconst EMPTY_BINDINGS: Binding[] = []\nconst EMPTY_UPDATERS: Array<() => void> = []\n\n// Scope pool — reuse disposed scope objects to reduce GC pressure.\n// Capped to avoid memory leaks in apps that create/destroy thousands of rows.\nconst SCOPE_POOL: Scope[] = []\nconst SCOPE_POOL_MAX = 2048\n\n// Dev-mode flag. Flipped to true by installDevTools() once any component\n// instance has a disposer log. In production this stays false forever,\n// and disposeScope skips findInstance entirely — zero cost.\nlet anyDisposerLogInstalled = false\n\n/** @internal — called by devtools.ts::installDevTools */\nexport function _markDisposerLogInstalled(): void {\n anyDisposerLogInstalled = true\n}\n\n/** @internal Drain the scope pool — for testing only */\nexport function _drainScopePool(): void {\n SCOPE_POOL.length = 0\n}\n\nexport function createScope(parent: Scope | null): Scope {\n let scope: Scope\n if (SCOPE_POOL.length > 0) {\n scope = SCOPE_POOL.pop()!\n scope.id = nextId++\n scope.parent = parent\n // Arrays already reset to empties by dispose. Reset dev-only hints\n // so recycled scopes don't carry stale tagging/back-refs. Cheap\n // (two undefined writes) and keeps production-identical behavior\n // when these fields are never set.\n scope.disposalCause = undefined\n scope.instance = undefined\n scope._kind = undefined\n } else {\n scope = {\n id: nextId++,\n parent,\n children: EMPTY_SCOPES,\n disposers: EMPTY_DISPOSERS,\n bindings: EMPTY_BINDINGS,\n itemUpdaters: EMPTY_UPDATERS,\n }\n }\n\n if (parent) {\n if (parent.children === EMPTY_SCOPES) parent.children = []\n parent.children.push(scope)\n }\n\n return scope\n}\n\n/**\n * Dispose a scope and all its children. By default, detaches the scope\n * from its parent's `children` array via `indexOf + splice` — O(N) per\n * call, which becomes O(N²) when disposing many sibling scopes in bulk\n * (e.g. `each` clearing 1000 rows).\n *\n * Pass `skipParentRemoval = true` when the caller will batch-remove\n * children afterwards (see `removeOrphanedFromParent`). The scope's\n * `parent` pointer is still set to `null` so the caller can identify\n * orphaned entries.\n */\nexport function disposeScope(scope: Scope, skipParentRemoval = false): void {\n if (scope.disposers.length === 0 && scope.children.length === 0 && scope.bindings.length === 0) {\n // Dev-only: still emit a DisposerEvent for empty scopes — the log\n // is meant to capture every scope the app destroys, not only ones\n // that had attached work. Outer flag check keeps production (no\n // devtools ever installed) at true zero cost — no parent-chain walk.\n if (anyDisposerLogInstalled) {\n const inst = findInstance(scope)\n if (inst?._disposerLog !== undefined) {\n inst._disposerLog.push({\n scopeId: String(scope.id),\n cause: scope.disposalCause ?? 'component-unmount',\n timestamp: Date.now(),\n })\n }\n }\n if (!skipParentRemoval) removeFromParent(scope)\n scope.parent = null\n // Don't pool empty scopes from the early-return path — they may be\n // disposed idempotently (twice), which would create pool duplicates\n return\n }\n\n // When skipParentRemoval is true, children don't mutate during disposal —\n // iterate directly without allocating a copy. Otherwise, clone to avoid\n // mutation during iteration.\n const children = skipParentRemoval ? scope.children : scope.children.slice()\n for (const child of children) {\n disposeScope(child, skipParentRemoval)\n }\n\n for (const disposer of scope.disposers) {\n disposer()\n }\n\n // Dev-only: emit disposer events into the owning instance's log.\n // Outer flag check keeps production (no devtools ever installed) at\n // true zero cost — skips the O(depth) parent-chain walk entirely.\n if (anyDisposerLogInstalled) {\n const inst = findInstance(scope)\n if (inst?._disposerLog !== undefined) {\n inst._disposerLog.push({\n scopeId: String(scope.id),\n cause: scope.disposalCause ?? 'component-unmount',\n timestamp: Date.now(),\n })\n }\n }\n\n // Mark bindings as dead and break closure/DOM retention\n for (const binding of scope.bindings) {\n binding.dead = true\n binding.accessor = null!\n binding.node = null!\n binding.lastValue = undefined\n }\n\n // Reset to shared empties — don't just truncate, so pooled scopes\n // don't hold allocated-but-empty arrays\n scope.disposers = EMPTY_DISPOSERS\n scope.bindings = EMPTY_BINDINGS\n scope.children = EMPTY_SCOPES\n scope.itemUpdaters = EMPTY_UPDATERS\n\n if (!skipParentRemoval) removeFromParent(scope)\n scope.parent = null\n\n // Return to pool for reuse\n if (SCOPE_POOL.length < SCOPE_POOL_MAX) SCOPE_POOL.push(scope)\n}\n\n/**\n * Batch-remove children with `parent === null` from `parent.children`.\n * Called after a bulk `disposeScope(child, true)` pass to collapse the\n * individual O(N) splice operations into one O(N) scan.\n */\nexport function removeOrphanedChildren(parent: Scope): void {\n const children = parent.children\n let w = 0\n for (let r = 0; r < children.length; r++) {\n if (children[r]!.parent !== null) children[w++] = children[r]!\n }\n children.length = w\n}\n\n/**\n * Bulk dispose an array of sibling scopes — avoids per-scope function call\n * overhead. Used by each() clear path where 1000+ scopes are disposed at once.\n * Caller must call removeOrphanedChildren(parent) afterwards.\n */\nexport function disposeScopesBulk(scopes: Scope[]): void {\n for (let i = 0; i < scopes.length; i++) {\n const scope = scopes[i]!\n // Recursively dispose children\n const children = scope.children\n if (children.length > 0) {\n disposeScopesBulk(children)\n }\n // Run disposers\n const disposers = scope.disposers\n for (let d = 0; d < disposers.length; d++) disposers[d]!()\n // Dev-only: emit disposer events — same guard as disposeScope.\n if (anyDisposerLogInstalled) {\n const inst = findInstance(scope)\n if (inst?._disposerLog !== undefined) {\n inst._disposerLog.push({\n scopeId: String(scope.id),\n cause: scope.disposalCause ?? 'component-unmount',\n timestamp: Date.now(),\n })\n }\n }\n // Mark bindings dead\n const bindings = scope.bindings\n for (let b = 0; b < bindings.length; b++) {\n const binding = bindings[b]!\n binding.dead = true\n binding.accessor = null!\n binding.node = null!\n binding.lastValue = undefined\n }\n // Reset to shared empties + detach from parent + return to pool\n scope.disposers = EMPTY_DISPOSERS\n scope.bindings = EMPTY_BINDINGS\n scope.children = EMPTY_SCOPES\n scope.itemUpdaters = EMPTY_UPDATERS\n scope.parent = null\n if (SCOPE_POOL.length < SCOPE_POOL_MAX) SCOPE_POOL.push(scope)\n }\n}\n\nexport function addBinding(scope: Scope, binding: Binding): void {\n binding.ownerScope = scope\n if (scope.bindings === EMPTY_BINDINGS) scope.bindings = []\n scope.bindings.push(binding)\n}\n\nexport function addItemUpdater(scope: Scope, updater: () => void): void {\n if (scope.itemUpdaters === EMPTY_UPDATERS) scope.itemUpdaters = []\n scope.itemUpdaters.push(updater)\n}\n\n/**\n * Register a per-item updater that compares the new value against the last\n * value before applying. Shared by `text()`, `elSplit()`, and `elTemplate()`\n * so the equality-check logic lives in one place.\n *\n * @param apply - DOM write: receives the new value when it differs\n * @returns the initial value (caller should apply it to the DOM)\n */\nexport function addCheckedItemUpdater<V>(scope: Scope, get: () => V, apply: (value: V) => void): V {\n let last: V = get()\n addItemUpdater(scope, () => {\n const v = get()\n if (v === last || (v !== v && last !== last)) return\n last = v\n apply(v)\n })\n return last\n}\n\nexport function addDisposer(scope: Scope, disposer: () => void): void {\n if (scope.disposers === EMPTY_DISPOSERS) scope.disposers = []\n scope.disposers.push(disposer)\n}\n\nfunction removeFromParent(scope: Scope): void {\n if (scope.parent) {\n const idx = scope.parent.children.indexOf(scope)\n if (idx !== -1) {\n scope.parent.children.splice(idx, 1)\n }\n }\n}\n"]}
|
package/dist/ssr.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAKlF;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACjC,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC1B,YAAY,CAAC,EAAE,CAAC,EAChB,WAAW,CAAC,EAAE,KAAK,GAClB;IAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAKlF;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EACjC,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC1B,YAAY,CAAC,EAAE,CAAC,EAChB,WAAW,CAAC,EAAE,KAAK,GAClB;IAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAAE,CAiBrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAezE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,MAAM,CAG5F"}
|
package/dist/ssr.js
CHANGED
|
@@ -21,7 +21,11 @@ export function renderNodes(def, initialState, parentScope) {
|
|
|
21
21
|
inst.state = initialState;
|
|
22
22
|
}
|
|
23
23
|
setFlatBindings(inst.allBindings);
|
|
24
|
-
setRenderContext({
|
|
24
|
+
setRenderContext({
|
|
25
|
+
...inst,
|
|
26
|
+
send: inst.send,
|
|
27
|
+
instance: inst,
|
|
28
|
+
});
|
|
25
29
|
const nodes = def.view(createView(inst.send));
|
|
26
30
|
clearRenderContext();
|
|
27
31
|
setFlatBindings(null);
|
package/dist/ssr.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.js","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAA0B,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE9C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,WAAW,CACzB,GAA0B,EAC1B,YAAgB,EAChB,WAAmB;IAEnB,MAAM,IAAI,GAAG,uBAAuB,CAAC,GAAG,EAAE,SAAS,EAAE,WAAW,IAAI,IAAI,CAAC,CAAA;IACzE,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,KAAK,GAAG,YAAY,CAAA;IAC3B,CAAC;IAED,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IACjC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"ssr.js","sourceRoot":"","sources":["../src/ssr.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentDef, Scope, Binding } from './types.js'\nimport { createComponentInstance, type ComponentInstance } from './update-loop.js'\nimport { setRenderContext, clearRenderContext } from './render-context.js'\nimport { setFlatBindings } from './binding.js'\nimport { createView } from './view-helpers.js'\n\n/**\n * Render a component to DOM nodes for SSR, returning both the produced\n * nodes and the component instance (so callers can compose trees before\n * serializing — e.g. `@llui/vike` stitches layout + page nodes at the\n * `pageSlot()` marker position before one final serialization pass).\n *\n * Accepts an optional `parentScope` so the rendered instance's rootScope\n * becomes a child of an existing scope tree — used by persistent layouts\n * so contexts provided by an outer layout are reachable from an inner\n * page via `useContext`.\n *\n * Call `initSsrDom()` once before using this on the server.\n */\nexport function renderNodes<S, M, E>(\n def: ComponentDef<S, M, E>,\n initialState?: S,\n parentScope?: Scope,\n): { nodes: Node[]; inst: ComponentInstance<S, M, E> } {\n const inst = createComponentInstance(def, undefined, parentScope ?? null)\n if (initialState !== undefined) {\n inst.state = initialState\n }\n\n setFlatBindings(inst.allBindings)\n setRenderContext({\n ...inst,\n send: inst.send as (msg: unknown) => void,\n instance: inst as ComponentInstance,\n })\n const nodes = def.view(createView<S, M>(inst.send))\n clearRenderContext()\n setFlatBindings(null)\n\n return { nodes, inst }\n}\n\n/**\n * Serialize an array of DOM nodes to an HTML string, adding\n * `data-llui-hydrate` markers on elements that own reactive bindings.\n *\n * Accepts a flat binding list so compositions of multiple instances\n * (layout + page, for persistent-layout SSR) produce correct markers\n * across the whole tree. Pass the union of every composed instance's\n * `allBindings`.\n */\nexport function serializeNodes(nodes: Node[], bindings: Binding[]): string {\n const hydrateElements = new Set<Node>()\n for (const binding of bindings) {\n const node = binding.node\n if (node.nodeType === 1) {\n hydrateElements.add(node)\n } else if (node.parentNode && node.parentNode.nodeType === 1) {\n hydrateElements.add(node.parentNode)\n }\n }\n let html = ''\n for (const node of nodes) {\n html += nodeToStringWithMarkers(node, hydrateElements)\n }\n return html\n}\n\n/**\n * Render a component to an HTML string for SSR.\n * Evaluates view() against the initial state (or provided data),\n * serializes the DOM to HTML, and adds data-llui-hydrate markers\n * on nodes with reactive bindings.\n *\n * Call initSsrDom() once before using this on the server.\n *\n * For persistent layouts, use `renderNodes` + `serializeNodes` directly\n * so layout and page nodes can be composed before serialization.\n */\nexport function renderToString<S, M, E>(def: ComponentDef<S, M, E>, initialState?: S): string {\n const { nodes, inst } = renderNodes(def, initialState)\n return serializeNodes(nodes, inst.allBindings)\n}\n\nfunction nodeToStringWithMarkers(node: Node, bindingNodes: Set<Node>): string {\n if (node.nodeType === 3) {\n return escapeHtml(node.textContent ?? '')\n }\n if (node.nodeType === 8) {\n return `<!--${node.textContent ?? ''}-->`\n }\n if (node.nodeType === 1) {\n const el = node as Element\n return elementToString(el, bindingNodes)\n }\n return ''\n}\n\nfunction elementToString(el: Element, bindingNodes: Set<Node>): string {\n const tag = el.tagName.toLowerCase()\n let attrs = ''\n\n for (let i = 0; i < el.attributes.length; i++) {\n const attr = el.attributes[i]!\n // Skip event handler attributes\n if (attr.name.startsWith('on')) continue\n attrs += ` ${attr.name}=\"${escapeAttr(attr.value)}\"`\n }\n\n // Add hydrate marker if this element or any of its text children have bindings\n if (bindingNodes.has(el)) {\n attrs += ' data-llui-hydrate'\n }\n\n // Void elements\n if (isVoidElement(tag)) {\n return `<${tag}${attrs} />`\n }\n\n let children = ''\n for (let i = 0; i < el.childNodes.length; i++) {\n children += nodeToStringWithMarkers(el.childNodes[i]!, bindingNodes)\n }\n\n return `<${tag}${attrs}>${children}</${tag}>`\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\nfunction escapeAttr(s: string): string {\n return s.replace(/&/g, '&').replace(/\"/g, '"')\n}\n\nconst VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\nfunction isVoidElement(tag: string): boolean {\n return VOID_ELEMENTS.has(tag)\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-variant Msg coverage tracker — dev-only.
|
|
3
|
+
*
|
|
4
|
+
* Records each dispatched message's discriminant (or `<non-discriminant>`
|
|
5
|
+
* for objects missing a `type` field) along with the message index it
|
|
6
|
+
* fired at. Consumed by the `llui_coverage` MCP tool to surface untested
|
|
7
|
+
* Msg variants: any variant declared in the compiled `__msgSchema` that
|
|
8
|
+
* never fired in the current session shows up in `neverFired`.
|
|
9
|
+
*
|
|
10
|
+
* Zero cost in production: `installDevTools` is the only caller, and it
|
|
11
|
+
* never runs in prod builds. Hot path is one optional-chain read per
|
|
12
|
+
* dispatched message (`ci._coverage?.record(...)`).
|
|
13
|
+
*/
|
|
14
|
+
export interface CoverageSnapshot {
|
|
15
|
+
fired: Record<string, {
|
|
16
|
+
count: number;
|
|
17
|
+
lastIndex: number;
|
|
18
|
+
}>;
|
|
19
|
+
neverFired: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface CoverageTracker {
|
|
22
|
+
record(variant: string, messageIndex: number): void;
|
|
23
|
+
snapshot(knownVariants?: string[]): CoverageSnapshot;
|
|
24
|
+
clear(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare function createCoverageTracker(): CoverageTracker;
|
|
27
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/tracking/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAA;IACpD,KAAK,IAAI,IAAI,CAAA;CACd;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAsBvD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-variant Msg coverage tracker — dev-only.
|
|
3
|
+
*
|
|
4
|
+
* Records each dispatched message's discriminant (or `<non-discriminant>`
|
|
5
|
+
* for objects missing a `type` field) along with the message index it
|
|
6
|
+
* fired at. Consumed by the `llui_coverage` MCP tool to surface untested
|
|
7
|
+
* Msg variants: any variant declared in the compiled `__msgSchema` that
|
|
8
|
+
* never fired in the current session shows up in `neverFired`.
|
|
9
|
+
*
|
|
10
|
+
* Zero cost in production: `installDevTools` is the only caller, and it
|
|
11
|
+
* never runs in prod builds. Hot path is one optional-chain read per
|
|
12
|
+
* dispatched message (`ci._coverage?.record(...)`).
|
|
13
|
+
*/
|
|
14
|
+
export function createCoverageTracker() {
|
|
15
|
+
const fired = new Map();
|
|
16
|
+
return {
|
|
17
|
+
record(variant, messageIndex) {
|
|
18
|
+
const existing = fired.get(variant);
|
|
19
|
+
if (existing) {
|
|
20
|
+
existing.count++;
|
|
21
|
+
existing.lastIndex = messageIndex;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
fired.set(variant, { count: 1, lastIndex: messageIndex });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
snapshot(knownVariants) {
|
|
28
|
+
const firedObj = {};
|
|
29
|
+
for (const [k, v] of fired)
|
|
30
|
+
firedObj[k] = { ...v };
|
|
31
|
+
const neverFired = knownVariants ? knownVariants.filter((v) => !fired.has(v)) : [];
|
|
32
|
+
return { fired: firedObj, neverFired };
|
|
33
|
+
},
|
|
34
|
+
clear() {
|
|
35
|
+
fired.clear();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage.js","sourceRoot":"","sources":["../../src/tracking/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,MAAM,UAAU,qBAAqB;IACnC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgD,CAAA;IACrE,OAAO;QACL,MAAM,CAAC,OAAO,EAAE,YAAY;YAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACnC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,KAAK,EAAE,CAAA;gBAChB,QAAQ,CAAC,SAAS,GAAG,YAAY,CAAA;YACnC,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAA;YAC3D,CAAC;QACH,CAAC;QACD,QAAQ,CAAC,aAAa;YACpB,MAAM,QAAQ,GAAyD,EAAE,CAAA;YACzE,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,KAAK;gBAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAA;YAClD,MAAM,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;YAClF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAA;QACxC,CAAC;QACD,KAAK;YACH,KAAK,CAAC,KAAK,EAAE,CAAA;QACf,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Per-variant Msg coverage tracker — dev-only.\n *\n * Records each dispatched message's discriminant (or `<non-discriminant>`\n * for objects missing a `type` field) along with the message index it\n * fired at. Consumed by the `llui_coverage` MCP tool to surface untested\n * Msg variants: any variant declared in the compiled `__msgSchema` that\n * never fired in the current session shows up in `neverFired`.\n *\n * Zero cost in production: `installDevTools` is the only caller, and it\n * never runs in prod builds. Hot path is one optional-chain read per\n * dispatched message (`ci._coverage?.record(...)`).\n */\n\nexport interface CoverageSnapshot {\n fired: Record<string, { count: number; lastIndex: number }>\n neverFired: string[]\n}\n\nexport interface CoverageTracker {\n record(variant: string, messageIndex: number): void\n snapshot(knownVariants?: string[]): CoverageSnapshot\n clear(): void\n}\n\nexport function createCoverageTracker(): CoverageTracker {\n const fired = new Map<string, { count: number; lastIndex: number }>()\n return {\n record(variant, messageIndex) {\n const existing = fired.get(variant)\n if (existing) {\n existing.count++\n existing.lastIndex = messageIndex\n } else {\n fired.set(variant, { count: 1, lastIndex: messageIndex })\n }\n },\n snapshot(knownVariants) {\n const firedObj: Record<string, { count: number; lastIndex: number }> = {}\n for (const [k, v] of fired) firedObj[k] = { ...v }\n const neverFired = knownVariants ? knownVariants.filter((v) => !fired.has(v)) : []\n return { fired: firedObj, neverFired }\n },\n clear() {\n fired.clear()\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRingBuffer, type RingBuffer } from './each-diff.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dev-only disposer log entry, emitted once per `disposeScope` call
|
|
4
|
+
* when the owning component instance has an `_disposerLog` ring buffer
|
|
5
|
+
* installed by `installDevTools`.
|
|
6
|
+
*
|
|
7
|
+
* `cause` is set by the structural primitive (each / branch / child)
|
|
8
|
+
* immediately before calling `disposeScope`. When no cause was
|
|
9
|
+
* explicitly set, `disposeScope` falls back to `'component-unmount'`.
|
|
10
|
+
* `'app-unmount'` is reserved for the top-level `mountApp` teardown.
|
|
11
|
+
*
|
|
12
|
+
* Used by the `llui_disposer_log` MCP tool to diagnose leaks on
|
|
13
|
+
* structural transitions (e.g., branch swap that fails to release a
|
|
14
|
+
* subscription registered in the old arm).
|
|
15
|
+
*/
|
|
16
|
+
export interface DisposerEvent {
|
|
17
|
+
scopeId: string;
|
|
18
|
+
cause: 'branch-swap' | 'each-remove' | 'show-hide' | 'child-unmount' | 'app-unmount' | 'component-unmount';
|
|
19
|
+
timestamp: number;
|
|
20
|
+
}
|
|
21
|
+
export { createRingBuffer, type RingBuffer };
|
|
22
|
+
//# sourceMappingURL=disposer-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposer-log.d.ts","sourceRoot":"","sources":["../../src/tracking/disposer-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAElE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EACD,aAAa,GACb,aAAa,GACb,WAAW,GACX,eAAe,GACf,aAAa,GACb,mBAAmB,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disposer-log.js","sourceRoot":"","sources":["../../src/tracking/disposer-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,gBAAgB,CAAA;AA4BlE,OAAO,EAAE,gBAAgB,EAAmB,CAAA","sourcesContent":["import { createRingBuffer, type RingBuffer } from './each-diff.js'\n\n/**\n * Dev-only disposer log entry, emitted once per `disposeScope` call\n * when the owning component instance has an `_disposerLog` ring buffer\n * installed by `installDevTools`.\n *\n * `cause` is set by the structural primitive (each / branch / child)\n * immediately before calling `disposeScope`. When no cause was\n * explicitly set, `disposeScope` falls back to `'component-unmount'`.\n * `'app-unmount'` is reserved for the top-level `mountApp` teardown.\n *\n * Used by the `llui_disposer_log` MCP tool to diagnose leaks on\n * structural transitions (e.g., branch swap that fails to release a\n * subscription registered in the old arm).\n */\nexport interface DisposerEvent {\n scopeId: string\n cause:\n | 'branch-swap'\n | 'each-remove'\n | 'show-hide'\n | 'child-unmount'\n | 'app-unmount'\n | 'component-unmount'\n timestamp: number\n}\n\nexport { createRingBuffer, type RingBuffer }\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-each-block reconciliation diff, recorded once per update that
|
|
3
|
+
* mutates an each() block's key set. Dev-only — populated when
|
|
4
|
+
* `installDevTools` has initialized an `_eachDiffLog` on the instance.
|
|
5
|
+
*
|
|
6
|
+
* `updateIndex` correlates with the message-history index recorded by
|
|
7
|
+
* `devtools.ts` so tools can join diffs back to the message that caused
|
|
8
|
+
* them. `eachSiteId` identifies the each() call site stably across
|
|
9
|
+
* updates (currently derived from the block's index in the instance's
|
|
10
|
+
* `structuralBlocks` array at creation time).
|
|
11
|
+
*/
|
|
12
|
+
export interface EachDiff {
|
|
13
|
+
/**
|
|
14
|
+
* Message-history index at the time the diff was emitted. When messages are
|
|
15
|
+
* batched (multiple send() calls coalescing into one microtask), this is
|
|
16
|
+
* the index of the LAST message in the batch — not necessarily the one that
|
|
17
|
+
* caused the structural change. For per-message correlation, use
|
|
18
|
+
* getMessageHistory with this index as an upper bound.
|
|
19
|
+
*/
|
|
20
|
+
updateIndex: number;
|
|
21
|
+
/**
|
|
22
|
+
* Stable-ish identifier for the each() call site. Currently derived from the
|
|
23
|
+
* position of the block in `ComponentInstance.structuralBlocks` at the moment
|
|
24
|
+
* of registration, formatted as `each#${N}`.
|
|
25
|
+
*
|
|
26
|
+
* Caveats for consumers:
|
|
27
|
+
* - The counter includes ALL structural blocks (branches, shows, portals,
|
|
28
|
+
* eaches), not just eaches. So `each#3` means "the 4th structural block",
|
|
29
|
+
* not "the 4th each".
|
|
30
|
+
* - Blocks registered inside a `branch` arm that switches away are spliced
|
|
31
|
+
* out; a subsequent each registration can reuse the same N.
|
|
32
|
+
* - Across HMR reloads the ID may drift if the view's structural-block
|
|
33
|
+
* order changed.
|
|
34
|
+
*
|
|
35
|
+
* For precise correlation across updates, pair with `updateIndex` and the
|
|
36
|
+
* enclosing component's state at that index (retrievable via
|
|
37
|
+
* getMessageHistory).
|
|
38
|
+
*/
|
|
39
|
+
eachSiteId: string;
|
|
40
|
+
added: string[];
|
|
41
|
+
removed: string[];
|
|
42
|
+
moved: Array<{
|
|
43
|
+
key: string;
|
|
44
|
+
from: number;
|
|
45
|
+
to: number;
|
|
46
|
+
}>;
|
|
47
|
+
reused: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface RingBuffer<T> {
|
|
50
|
+
push(entry: T): void;
|
|
51
|
+
toArray(): T[];
|
|
52
|
+
clear(): void;
|
|
53
|
+
size(): number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Minimal ring buffer: unbounded `push` trimmed to `maxSize` via shift.
|
|
57
|
+
* Kept tiny on purpose — any fancier implementation would pay interest
|
|
58
|
+
* for an allocation-cost saving that only matters under unrealistic
|
|
59
|
+
* dev-only churn. If we ever need it, replace with a circular array.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createRingBuffer<T>(maxSize: number): RingBuffer<T>;
|
|
62
|
+
//# sourceMappingURL=each-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"each-diff.d.ts","sourceRoot":"","sources":["../../src/tracking/each-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvD,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACpB,OAAO,IAAI,CAAC,EAAE,CAAA;IACd,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAiBlE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ring buffer: unbounded `push` trimmed to `maxSize` via shift.
|
|
3
|
+
* Kept tiny on purpose — any fancier implementation would pay interest
|
|
4
|
+
* for an allocation-cost saving that only matters under unrealistic
|
|
5
|
+
* dev-only churn. If we ever need it, replace with a circular array.
|
|
6
|
+
*/
|
|
7
|
+
export function createRingBuffer(maxSize) {
|
|
8
|
+
const buf = [];
|
|
9
|
+
return {
|
|
10
|
+
push(entry) {
|
|
11
|
+
if (buf.length >= maxSize)
|
|
12
|
+
buf.shift();
|
|
13
|
+
buf.push(entry);
|
|
14
|
+
},
|
|
15
|
+
toArray() {
|
|
16
|
+
return buf.slice();
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
buf.length = 0;
|
|
20
|
+
},
|
|
21
|
+
size() {
|
|
22
|
+
return buf.length;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=each-diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"each-diff.js","sourceRoot":"","sources":["../../src/tracking/each-diff.ts"],"names":[],"mappings":"AAoDA;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAI,OAAe;IACjD,MAAM,GAAG,GAAQ,EAAE,CAAA;IACnB,OAAO;QACL,IAAI,CAAC,KAAK;YACR,IAAI,GAAG,CAAC,MAAM,IAAI,OAAO;gBAAE,GAAG,CAAC,KAAK,EAAE,CAAA;YACtC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC;QACD,OAAO;YACL,OAAO,GAAG,CAAC,KAAK,EAAE,CAAA;QACpB,CAAC;QACD,KAAK;YACH,GAAG,CAAC,MAAM,GAAG,CAAC,CAAA;QAChB,CAAC;QACD,IAAI;YACF,OAAO,GAAG,CAAC,MAAM,CAAA;QACnB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Per-each-block reconciliation diff, recorded once per update that\n * mutates an each() block's key set. Dev-only — populated when\n * `installDevTools` has initialized an `_eachDiffLog` on the instance.\n *\n * `updateIndex` correlates with the message-history index recorded by\n * `devtools.ts` so tools can join diffs back to the message that caused\n * them. `eachSiteId` identifies the each() call site stably across\n * updates (currently derived from the block's index in the instance's\n * `structuralBlocks` array at creation time).\n */\nexport interface EachDiff {\n /**\n * Message-history index at the time the diff was emitted. When messages are\n * batched (multiple send() calls coalescing into one microtask), this is\n * the index of the LAST message in the batch — not necessarily the one that\n * caused the structural change. For per-message correlation, use\n * getMessageHistory with this index as an upper bound.\n */\n updateIndex: number\n /**\n * Stable-ish identifier for the each() call site. Currently derived from the\n * position of the block in `ComponentInstance.structuralBlocks` at the moment\n * of registration, formatted as `each#${N}`.\n *\n * Caveats for consumers:\n * - The counter includes ALL structural blocks (branches, shows, portals,\n * eaches), not just eaches. So `each#3` means \"the 4th structural block\",\n * not \"the 4th each\".\n * - Blocks registered inside a `branch` arm that switches away are spliced\n * out; a subsequent each registration can reuse the same N.\n * - Across HMR reloads the ID may drift if the view's structural-block\n * order changed.\n *\n * For precise correlation across updates, pair with `updateIndex` and the\n * enclosing component's state at that index (retrievable via\n * getMessageHistory).\n */\n eachSiteId: string\n added: string[]\n removed: string[]\n moved: Array<{ key: string; from: number; to: number }>\n reused: string[]\n}\n\nexport interface RingBuffer<T> {\n push(entry: T): void\n toArray(): T[]\n clear(): void\n size(): number\n}\n\n/**\n * Minimal ring buffer: unbounded `push` trimmed to `maxSize` via shift.\n * Kept tiny on purpose — any fancier implementation would pay interest\n * for an allocation-cost saving that only matters under unrealistic\n * dev-only churn. If we ever need it, replace with a circular array.\n */\nexport function createRingBuffer<T>(maxSize: number): RingBuffer<T> {\n const buf: T[] = []\n return {\n push(entry) {\n if (buf.length >= maxSize) buf.shift()\n buf.push(entry)\n },\n toArray() {\n return buf.slice()\n },\n clear() {\n buf.length = 0\n },\n size() {\n return buf.length\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect timeline + pending-effects list + mock registry — the three
|
|
3
|
+
* trackers that back the `llui_pending_effects`, `llui_effect_timeline`,
|
|
4
|
+
* `llui_mock_effect`, and `llui_resolve_effect` MCP tools.
|
|
5
|
+
*
|
|
6
|
+
* Dev-only — populated on `ComponentInstance` when `installDevTools`
|
|
7
|
+
* runs. Zero cost in production (the `dispatchEffectDev` wrapper in
|
|
8
|
+
* `update-loop.ts` short-circuits when `_effectTimeline` is undefined).
|
|
9
|
+
*
|
|
10
|
+
* The mock registry stores match-to-response pairs; the actual
|
|
11
|
+
* response delivery (i.e., converting a mocked response back into a
|
|
12
|
+
* Msg via the effect's `onSuccess` callback) is the responsibility of
|
|
13
|
+
* the MCP `llui_resolve_effect` tool — not this module.
|
|
14
|
+
*/
|
|
15
|
+
import { createRingBuffer, type RingBuffer } from './each-diff.js';
|
|
16
|
+
export interface EffectTimelineEntry {
|
|
17
|
+
effectId: string;
|
|
18
|
+
type: string;
|
|
19
|
+
phase: 'dispatched' | 'in-flight' | 'resolved' | 'resolved-mocked' | 'cancelled';
|
|
20
|
+
timestamp: number;
|
|
21
|
+
/** Populated on `resolved` / `resolved-mocked` / `cancelled` entries; undefined on open phases. */
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PendingEffect {
|
|
25
|
+
id: string;
|
|
26
|
+
type: string;
|
|
27
|
+
dispatchedAt: number;
|
|
28
|
+
status: 'queued' | 'in-flight';
|
|
29
|
+
payload: unknown;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Match predicate for the mock registry. All provided fields must
|
|
33
|
+
* match for the mock to fire:
|
|
34
|
+
* - `type`: exact-match against the effect's `type` discriminant.
|
|
35
|
+
* - `payloadPath`: dotted path into the effect object (e.g. `'url'` or
|
|
36
|
+
* `'body.key'`). When present without `payloadEquals`, presence of
|
|
37
|
+
* the path is sufficient.
|
|
38
|
+
* - `payloadEquals`: strict (`===`) equality check at `payloadPath`.
|
|
39
|
+
*
|
|
40
|
+
* An empty match (no fields) matches every effect — callers should
|
|
41
|
+
* set at least `type` to avoid accidental catch-all.
|
|
42
|
+
*/
|
|
43
|
+
export interface EffectMatch {
|
|
44
|
+
type?: string;
|
|
45
|
+
payloadPath?: string;
|
|
46
|
+
payloadEquals?: unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface EffectMock {
|
|
49
|
+
mockId: string;
|
|
50
|
+
match: EffectMatch;
|
|
51
|
+
response: unknown;
|
|
52
|
+
/** When false, the mock is removed after the first match (one-shot). */
|
|
53
|
+
persist: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface MockRegistry {
|
|
56
|
+
add(match: EffectMatch, response: unknown, persist: boolean): string;
|
|
57
|
+
match(effect: unknown): {
|
|
58
|
+
response: unknown;
|
|
59
|
+
mockId: string;
|
|
60
|
+
} | null;
|
|
61
|
+
clear(): void;
|
|
62
|
+
list(): EffectMock[];
|
|
63
|
+
}
|
|
64
|
+
export declare function createMockRegistry(): MockRegistry;
|
|
65
|
+
export interface PendingEffectsList {
|
|
66
|
+
push(p: PendingEffect): void;
|
|
67
|
+
findById(id: string): PendingEffect | undefined;
|
|
68
|
+
remove(id: string): void;
|
|
69
|
+
list(): PendingEffect[];
|
|
70
|
+
}
|
|
71
|
+
export declare function createPendingEffectsList(): PendingEffectsList;
|
|
72
|
+
export { createRingBuffer, type RingBuffer };
|
|
73
|
+
//# sourceMappingURL=effect-timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect-timeline.d.ts","sourceRoot":"","sources":["../../src/tracking/effect-timeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAElE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,YAAY,GAAG,WAAW,GAAG,UAAU,GAAG,iBAAiB,GAAG,WAAW,CAAA;IAChF,SAAS,EAAE,MAAM,CAAA;IACjB,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,QAAQ,GAAG,WAAW,CAAA;IAC9B,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAAA;IACpE,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACpE,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,UAAU,EAAE,CAAA;CACrB;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAqDjD;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAC/C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,IAAI,aAAa,EAAE,CAAA;CACxB;AAED,wBAAgB,wBAAwB,IAAI,kBAAkB,CAa7D;AAED,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect timeline + pending-effects list + mock registry — the three
|
|
3
|
+
* trackers that back the `llui_pending_effects`, `llui_effect_timeline`,
|
|
4
|
+
* `llui_mock_effect`, and `llui_resolve_effect` MCP tools.
|
|
5
|
+
*
|
|
6
|
+
* Dev-only — populated on `ComponentInstance` when `installDevTools`
|
|
7
|
+
* runs. Zero cost in production (the `dispatchEffectDev` wrapper in
|
|
8
|
+
* `update-loop.ts` short-circuits when `_effectTimeline` is undefined).
|
|
9
|
+
*
|
|
10
|
+
* The mock registry stores match-to-response pairs; the actual
|
|
11
|
+
* response delivery (i.e., converting a mocked response back into a
|
|
12
|
+
* Msg via the effect's `onSuccess` callback) is the responsibility of
|
|
13
|
+
* the MCP `llui_resolve_effect` tool — not this module.
|
|
14
|
+
*/
|
|
15
|
+
import { createRingBuffer } from './each-diff.js';
|
|
16
|
+
export function createMockRegistry() {
|
|
17
|
+
const mocks = [];
|
|
18
|
+
let nextId = 1;
|
|
19
|
+
function resolvePath(obj, path) {
|
|
20
|
+
const parts = path.split('.');
|
|
21
|
+
let v = obj;
|
|
22
|
+
for (const p of parts) {
|
|
23
|
+
if (v == null || typeof v !== 'object')
|
|
24
|
+
return undefined;
|
|
25
|
+
v = v[p];
|
|
26
|
+
}
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
function matches(m, effect) {
|
|
30
|
+
if (effect == null || typeof effect !== 'object')
|
|
31
|
+
return false;
|
|
32
|
+
const eff = effect;
|
|
33
|
+
if (m.match.type !== undefined && eff.type !== m.match.type)
|
|
34
|
+
return false;
|
|
35
|
+
if (m.match.payloadPath !== undefined) {
|
|
36
|
+
const v = resolvePath(eff, m.match.payloadPath);
|
|
37
|
+
if (m.match.payloadEquals !== undefined && v !== m.match.payloadEquals)
|
|
38
|
+
return false;
|
|
39
|
+
// Presence-only check: path must resolve to something other than undefined.
|
|
40
|
+
if (m.match.payloadEquals === undefined && v === undefined)
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
add(match, response, persist) {
|
|
47
|
+
const mockId = `mock-${nextId++}`;
|
|
48
|
+
mocks.push({ mockId, match, response, persist });
|
|
49
|
+
return mockId;
|
|
50
|
+
},
|
|
51
|
+
match(effect) {
|
|
52
|
+
for (let i = 0; i < mocks.length; i++) {
|
|
53
|
+
const m = mocks[i];
|
|
54
|
+
if (matches(m, effect)) {
|
|
55
|
+
const response = m.response;
|
|
56
|
+
const mockId = m.mockId;
|
|
57
|
+
if (!m.persist)
|
|
58
|
+
mocks.splice(i, 1);
|
|
59
|
+
return { response, mockId };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
},
|
|
64
|
+
clear() {
|
|
65
|
+
mocks.length = 0;
|
|
66
|
+
nextId = 1;
|
|
67
|
+
},
|
|
68
|
+
list() {
|
|
69
|
+
return mocks.slice();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function createPendingEffectsList() {
|
|
74
|
+
const items = [];
|
|
75
|
+
return {
|
|
76
|
+
push(p) {
|
|
77
|
+
items.push(p);
|
|
78
|
+
},
|
|
79
|
+
findById: (id) => items.find((p) => p.id === id),
|
|
80
|
+
remove(id) {
|
|
81
|
+
const i = items.findIndex((p) => p.id === id);
|
|
82
|
+
if (i >= 0)
|
|
83
|
+
items.splice(i, 1);
|
|
84
|
+
},
|
|
85
|
+
list: () => items.slice(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export { createRingBuffer };
|
|
89
|
+
//# sourceMappingURL=effect-timeline.js.map
|