@adukiorg/anza 0.2.0 → 0.2.3

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.
Files changed (83) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/README.md +97 -133
  3. package/bin/anza/anza-linux-arm64 +0 -0
  4. package/bin/anza/anza-linux-x64 +0 -0
  5. package/bin/anza/anza-macos-arm64 +0 -0
  6. package/bin/anza/anza-macos-x64 +0 -0
  7. package/bin/anza/anza-windows-x64.exe +0 -0
  8. package/bin/anza/find.js +35 -0
  9. package/bin/anza/index.js +34 -0
  10. package/bin/anza/launch.js +19 -0
  11. package/bin/common/index.js +7 -0
  12. package/bin/common/logs.js +62 -0
  13. package/bin/create/copy.js +18 -0
  14. package/bin/create/index.js +45 -0
  15. package/bin/create/run.js +210 -0
  16. package/bin/create/write.js +19 -0
  17. package/importmap.json +4 -0
  18. package/package.json +16 -10
  19. package/src/core/offline/{usage.md → notes/usage.md} +11 -1
  20. package/src/core/router/boot.js +82 -0
  21. package/src/core/router/cascade.js +76 -0
  22. package/src/core/router/container.js +63 -72
  23. package/src/core/router/graph.js +144 -0
  24. package/src/core/router/index.js +12 -2
  25. package/src/core/router/intercept.js +26 -7
  26. package/src/core/router/lca.js +58 -0
  27. package/src/core/router/match.js +49 -36
  28. package/src/core/router/notes/audit-old.md +887 -0
  29. package/src/core/router/notes/audti.md +773 -0
  30. package/src/core/router/notes/tasks.md +473 -0
  31. package/src/core/router/{usage.md → notes/usage.md} +57 -35
  32. package/src/core/router/sync/tab.js +6 -4
  33. package/src/core/router/transitions.js +35 -8
  34. package/src/core/router/trie.js +130 -0
  35. package/src/core/security/{usage.md → notes/usage.md} +1 -2
  36. package/src/core/storage/{usage.md → notes/usage.md} +6 -6
  37. package/src/core/theme/index.js +78 -0
  38. package/src/core/ui/define/index.js +2 -1
  39. package/src/core/ui/define/orchestrator.js +10 -4
  40. package/src/core/ui/defs/dock.js +134 -0
  41. package/src/core/ui/defs/index.js +20 -0
  42. package/src/core/ui/defs/page.js +89 -0
  43. package/src/core/ui/defs/part.js +28 -0
  44. package/src/core/ui/defs/spec.js +96 -0
  45. package/src/core/ui/defs/view.js +23 -0
  46. package/src/core/ui/index.js +16 -3
  47. package/src/core/ui/notes/definations.md +979 -0
  48. package/src/tokens/index.css +1 -0
  49. package/src/tokens/semantic/contrast.css +18 -0
  50. package/src/tokens/semantic/transitions.css +32 -0
  51. package/types/core/platform/index.d.ts +39 -10
  52. package/types/core/router/index.d.ts +9 -0
  53. package/types/core/theme/index.d.ts +18 -0
  54. package/types/core/ui/index.d.ts +11 -0
  55. package/types/index.d.ts +1 -0
  56. package/bin/anza.js +0 -63
  57. package/bin/create.js +0 -150
  58. package/src/core/api/plan.md +0 -209
  59. package/src/core/events/missing.md +0 -103
  60. package/src/core/events/plan.md +0 -177
  61. package/src/core/offline/missing.md +0 -89
  62. package/src/core/offline/plan.md +0 -143
  63. package/src/core/platform/missing.md +0 -119
  64. package/src/core/platform/platform.d.ts +0 -88
  65. package/src/core/router/missing.md +0 -716
  66. package/src/core/router/outlet.js +0 -139
  67. package/src/core/router/plan.md +0 -370
  68. package/src/core/security/missing.md +0 -97
  69. package/src/core/state/missing.md +0 -165
  70. package/src/core/storage/missing.md +0 -165
  71. package/src/core/storage/plan.md +0 -69
  72. package/src/core/ui/implementation.md +0 -170
  73. package/src/core/ui/plan.md +0 -510
  74. package/src/core/ui/ui.types.md +0 -890
  75. /package/src/core/animations/{usage.md → notes/usage.md} +0 -0
  76. /package/src/core/api/{usage.md → notes/usage.md} +0 -0
  77. /package/src/core/events/{usage.md → notes/usage.md} +0 -0
  78. /package/src/core/platform/{usage.md → notes/usage.md} +0 -0
  79. /package/src/core/state/{usage.md → notes/usage.md} +0 -0
  80. /package/src/core/ui/{usage.md → notes/usage.md} +0 -0
  81. /package/src/core/ui/{watch.md → notes/watch.md} +0 -0
  82. /package/src/core/workers/{plan.md → notes/plan.md} +0 -0
  83. /package/src/core/workers/{usage.md → notes/usage.md} +0 -0
@@ -0,0 +1,887 @@
1
+ # Router Architecture Audit (Original Draft)
2
+
3
+ > **Superseded by `myaudit.md`.** This file is the original machine-generated draft. The canonical audit is `myaudit.md`, which incorporates all findings from this document plus the unified `page`/`dock`/`view` definitions from `../ui/definations.md`. Refer to `myaudit.md` for the authoritative analysis and implementation plan.
4
+
5
+ Critical analysis of the Anza client-side router architecture, proposed fixes using native browser APIs, and high-performance data structures for container graph traversal and route matching.
6
+
7
+ ---
8
+
9
+ ## 1. Critical Analysis
10
+
11
+ ### 1.1. Hard Refresh Failures
12
+
13
+ **The Bug:**
14
+ On hard refresh (Ctrl+R / direct URL entry), `setup()` in `intercept.js` fires its initial route match via `Promise.resolve().then(...)` — a single microtask delay. This races against:
15
+
16
+ 1. Routes not yet registered (ES module graph hasn't evaluated `ui.element(...)` calls)
17
+ 2. Container elements not yet in the DOM (Custom Elements haven't connected)
18
+ 3. The orchestrator listener not yet attached (if `define/index.js` loads after `router/index.js`)
19
+
20
+ The microtask fires *before* `DOMContentLoaded`, *before* Custom Element `connectedCallback`, and often *before* downstream modules finish executing. The result: `match()` returns `null` (no routes registered) or emits `'found'` to an orchestrator that calls `getContainer()` on a node that hasn't mounted.
21
+
22
+ **Evidence in code:**
23
+
24
+ ```javascript
25
+ // intercept.js line 248
26
+ Promise.resolve().then(async () => {
27
+ const url = window.navigation.currentEntry?.url || window.location.href;
28
+ const routeMatch = await match(url); // routes[] is empty on cold boot
29
+ ...
30
+ });
31
+ ```
32
+
33
+ ```javascript
34
+ // orchestrator.js line 28
35
+ const containerEl = router.getContainer(spec.container);
36
+ if (!containerEl) {
37
+ console.warn(...); // Silent failure — page never renders
38
+ return;
39
+ }
40
+ ```
41
+
42
+ **Edge cases:**
43
+
44
+ - Script loaded via `<script type="module">` (deferred by spec) — route registration happens in a subsequent microtask after `setup()` already fired.
45
+ - `<script type="module" async>` — execution order is completely non-deterministic.
46
+ - Service Worker serving stale cached HTML while JS bundles update — container tags may mismatch.
47
+
48
+ ---
49
+
50
+ ### 1.2. Global Initialization Timing
51
+
52
+ **The Bug:**
53
+ The auto-bootstrap block in `router/index.js` runs at module evaluation time:
54
+
55
+ ```javascript
56
+ if (typeof window !== 'undefined') {
57
+ registerNavigator(navigate);
58
+ setup();
59
+ setupTabSync(router);
60
+ }
61
+ ```
62
+
63
+ This means:
64
+
65
+ - `setup()` attaches the Navigation API listener *before* any route is registered.
66
+ - The initial match fires in the same microtask queue tick as module evaluation.
67
+ - There is no `window.router` global — users must import the object, but the docs propose a global instance. Currently there is no bridge.
68
+
69
+ **The Conflict:**
70
+ You want the router available globally *before* app code loads. But ES modules are deferred and non-blocking. A `<script type="module">` that imports the router cannot guarantee its evaluation precedes other modules that register routes unless you enforce a strict import chain (which defeats parallelism).
71
+
72
+ **Edge cases:**
73
+
74
+ - Two entry points both importing `router/index.js` — `setup()` is idempotent, but route registration order is undefined.
75
+ - Dynamic `import()` of the router after page load — `setup()` fires, emits initial match, but DOMContentLoaded already passed.
76
+
77
+ ---
78
+
79
+ ### 1.3. Container Graph Is Flat
80
+
81
+ **The Bug:**
82
+ `container.js` stores `Map<string, WeakRef<HTMLElement>>` — a flat key-value registry. There is no concept of:
83
+
84
+ - Parent-child relationships between containers
85
+ - Depth or nesting level
86
+ - Sibling adjacency
87
+ - Path from root to a given container
88
+
89
+ The `route.meta.parent` field in `match.js` establishes a *route chain* (parent pattern strings), but this chain has zero connection to the *container hierarchy*. A route declares `container: 'sidebar'` and that's the only topological information. If `sidebar` lives inside `main` which lives inside `body`, the router doesn't know.
90
+
91
+ **Consequence:**
92
+ Cross-branch navigation (from a view mounted in container A to a view mounted in container B, where A and B share a common ancestor C) requires understanding the tree shape. Without a graph, the router cannot:
93
+
94
+ 1. Determine which containers to unmount (everything below divergence point)
95
+ 2. Determine which containers to mount (everything on the new branch)
96
+ 3. Identify the Lowest Common Ancestor (LCA) to minimize DOM churn
97
+
98
+ ---
99
+
100
+ ### 1.4. Cross-Branch Reconciliation Is Absent
101
+
102
+ **The Bug:**
103
+ The current resolution logic in `intercept.js` is binary:
104
+
105
+ ```javascript
106
+ if (routeMatch?.route?.meta?.container) {
107
+ const containerName = routeMatch.route.meta.container;
108
+ if (!getContainer(containerName)) {
109
+ throw new Error(...); // Hard failure, no recovery
110
+ }
111
+ }
112
+ ```
113
+
114
+ If the target container is not in the DOM, the router *throws*. It makes no attempt to:
115
+
116
+ 1. Walk up to a common ancestor that IS in the DOM
117
+ 2. Sequentially render intermediate containers downward
118
+ 3. Await their registration before proceeding
119
+
120
+ **Example failure:**
121
+
122
+ ```text
123
+ URL: /settings/profile
124
+ Container: 'settings-panel'
125
+ 'settings-panel' lives inside 'app-sidebar'
126
+ 'app-sidebar' lives inside 'main'
127
+
128
+ Current page: /dashboard (rendered in 'main')
129
+ ```
130
+
131
+ Navigation to `/settings/profile` fails because `settings-panel` doesn't exist yet — it would only exist after `app-sidebar` is mounted, which itself requires `main` to render the sidebar layout.
132
+
133
+ ---
134
+
135
+ ### 1.5. Route-to-Container Mapping Is One-Dimensional
136
+
137
+ **The Bug:**
138
+ Each route declares a single `container` string:
139
+
140
+ ```javascript
141
+ router.register('/settings/profile', 'page-profile', { container: 'settings-panel' });
142
+ ```
143
+
144
+ But there's no mechanism to express:
145
+ - "To reach `settings-panel`, first ensure `app-sidebar` is mounted in `main`"
146
+ - "The chain of containers from root to target is: `body` → `main` → `app-sidebar` → `settings-panel`"
147
+
148
+ The `meta.parent` field in `match.js` chains *routes* by pattern string — it does NOT chain *containers*. The `chain` array built during matching represents the route ancestry, not the DOM ancestry.
149
+
150
+ **Mismatch:**
151
+ Route hierarchy ≠ Container hierarchy. A deeply nested route (`/a/b/c/d`) might render in a top-level container (`main`). Conversely, a shallow route (`/settings`) might need three nested containers. The current architecture conflates these two orthogonal trees.
152
+
153
+ ---
154
+
155
+ ## 2. Proposed Fixes
156
+
157
+ ### 2.1. Deferred Initial Match (Hard Refresh Survival)
158
+
159
+ **Problem:** The initial match fires before routes exist or containers mount.
160
+
161
+ **Fix:** Replace the microtask-based initial emit with a gated boot sequence that waits for readiness signals.
162
+
163
+ ```javascript
164
+ // boot.js — new module
165
+ const gates = [];
166
+ let booted = false;
167
+
168
+ export function gate(promise) {
169
+ if (booted) return;
170
+ gates.push(promise);
171
+ }
172
+
173
+ export async function boot(emitter) {
174
+ // Wait for DOM to be interactive
175
+ if (document.readyState === 'loading') {
176
+ await new Promise(r => document.addEventListener('DOMContentLoaded', r, { once: true }));
177
+ }
178
+
179
+ // Wait for all registered gates (route files, container definitions)
180
+ await Promise.all(gates);
181
+ gates.length = 0;
182
+ booted = true;
183
+
184
+ // Now safe to emit initial match
185
+ emitter();
186
+ }
187
+
188
+ export function ready() {
189
+ return booted;
190
+ }
191
+ ```
192
+
193
+ **Integration in `intercept.js`:**
194
+
195
+ ```javascript
196
+ import { boot, ready } from './boot.js';
197
+
198
+ export function setup() {
199
+ if (ready()) return;
200
+ // ...attach listeners...
201
+
202
+ boot(async () => {
203
+ const url = window.navigation.currentEntry?.url || location.href;
204
+ const found = await match(url);
205
+ if (found) emit('found', { ...found, direction: 'load' });
206
+ else emit('notfound', { url });
207
+ });
208
+ }
209
+ ```
210
+
211
+ **Route registration gates itself:**
212
+
213
+ ```javascript
214
+ // In element.js, after router.register():
215
+ import { gate } from '../../router/boot.js';
216
+ gate(customElements.whenDefined(tag));
217
+ ```
218
+
219
+ **Why native:**
220
+ - `document.readyState` — synchronous, zero-cost check
221
+ - `DOMContentLoaded` — standard event, fires once DOM is parsed
222
+ - `Promise.all` — microtask scheduling, no timers
223
+ - `customElements.whenDefined` — native Custom Elements API
224
+
225
+ ---
226
+
227
+ ### 2.2. Global Router Attachment
228
+
229
+ **Fix:** Freeze a non-configurable property on `window` during auto-bootstrap.
230
+
231
+ ```javascript
232
+ // router/index.js — at auto-bootstrap
233
+ if (typeof window !== 'undefined') {
234
+ Object.defineProperty(window, 'router', {
235
+ value: router,
236
+ writable: false,
237
+ enumerable: false,
238
+ configurable: false
239
+ });
240
+
241
+ registerNavigator(navigate);
242
+ setup();
243
+ setupTabSync(router);
244
+ }
245
+ ```
246
+
247
+ **Why this is safe:**
248
+ - `Object.defineProperty` with `writable: false` prevents accidental overwrite.
249
+ - `enumerable: false` keeps it out of `for...in` loops and `Object.keys(window)`.
250
+ - `configurable: false` makes it permanent — no library can `delete window.router`.
251
+ - Code that imports `{ router }` still works identically.
252
+ - Code that references `window.router` works without import.
253
+
254
+ ---
255
+
256
+ ### 2.3. Hierarchical Container Graph
257
+
258
+ **Fix:** Replace the flat `Map<string, WeakRef>` with a tree structure that stores parent-child relationships and depth.
259
+
260
+ ```javascript
261
+ // graph.js — new module
262
+
263
+ class Node {
264
+ constructor(name, ref, parent = null) {
265
+ this.name = name;
266
+ this.ref = ref; // WeakRef<HTMLElement>
267
+ this.parent = parent; // Node | null
268
+ this.children = new Set(); // Set<Node>
269
+ this.depth = parent ? parent.depth + 1 : 0;
270
+ }
271
+
272
+ alive() {
273
+ return this.ref.deref() !== undefined;
274
+ }
275
+ }
276
+
277
+ const nodes = new Map(); // string -> Node
278
+ const root = new Node('body', null, null); // virtual root
279
+ nodes.set('body', root);
280
+
281
+ export function add(name, element, parent = 'body') {
282
+ const ref = new WeakRef(element);
283
+ const parentNode = nodes.get(parent) || root;
284
+ const node = new Node(name, ref, parentNode);
285
+ parentNode.children.add(node);
286
+ nodes.set(name, node);
287
+ return node;
288
+ }
289
+
290
+ export function remove(name) {
291
+ const node = nodes.get(name);
292
+ if (!node) return;
293
+ if (node.parent) node.parent.children.delete(node);
294
+ // Reparent orphans to grandparent
295
+ for (const child of node.children) {
296
+ child.parent = node.parent;
297
+ child.depth = child.parent ? child.parent.depth + 1 : 0;
298
+ if (node.parent) node.parent.children.add(child);
299
+ }
300
+ nodes.delete(name);
301
+ }
302
+
303
+ export function get(name) {
304
+ return nodes.get(name) || null;
305
+ }
306
+
307
+ export function element(name) {
308
+ return nodes.get(name)?.ref.deref() || null;
309
+ }
310
+
311
+ export function clear() {
312
+ nodes.clear();
313
+ root.children.clear();
314
+ nodes.set('body', root);
315
+ }
316
+ ```
317
+
318
+ **Registration changes in `container.js`:**
319
+
320
+ ```javascript
321
+ import { add, remove } from './graph.js';
322
+
323
+ export function registerContainer(name, element, parent = 'body') {
324
+ // ...existing singleton check...
325
+ add(name, element, parent);
326
+ }
327
+
328
+ export function unregisterContainer(name, element) {
329
+ // ...existing safety check...
330
+ remove(name);
331
+ }
332
+ ```
333
+
334
+ **Container spec gains a `parent` field:**
335
+
336
+ ```javascript
337
+ ui.container('settings-panel', {
338
+ parent: 'sidebar', // <-- declares hierarchy
339
+ template: '<slot></slot>',
340
+ style: ':host { display: block; }'
341
+ });
342
+ ```
343
+
344
+ ---
345
+
346
+ ### 2.4. Lowest Common Ancestor Algorithm
347
+
348
+ **Problem:** Navigate from container A to container B — which containers must unmount and which must mount?
349
+
350
+ **Algorithm:** Walk both nodes up to equal depth, then walk both up simultaneously until they meet.
351
+
352
+ ```javascript
353
+ // lca.js
354
+
355
+ import { get } from './graph.js';
356
+
357
+ export function lca(a, b) {
358
+ let nodeA = get(a);
359
+ let nodeB = get(b);
360
+ if (!nodeA || !nodeB) return null;
361
+
362
+ // Equalize depth
363
+ while (nodeA.depth > nodeB.depth) nodeA = nodeA.parent;
364
+ while (nodeB.depth > nodeA.depth) nodeB = nodeB.parent;
365
+
366
+ // Walk up together
367
+ while (nodeA !== nodeB) {
368
+ nodeA = nodeA.parent;
369
+ nodeB = nodeB.parent;
370
+ }
371
+
372
+ return nodeA; // The common ancestor node
373
+ }
374
+
375
+ export function path(from, to) {
376
+ const ancestor = lca(from, to);
377
+ if (!ancestor) return null;
378
+
379
+ // Build path: [ancestor, ..., target]
380
+ const segments = [];
381
+ let current = get(to);
382
+ while (current && current !== ancestor) {
383
+ segments.unshift(current);
384
+ current = current.parent;
385
+ }
386
+ segments.unshift(ancestor);
387
+ return segments;
388
+ }
389
+ ```
390
+
391
+ **Complexity:** O(d) where d is the maximum depth of the tree. For typical UIs (depth 3-6), this is effectively O(1).
392
+
393
+ **Memory:** One `Node` object per registered container. Each node holds a name string, a WeakRef, a parent pointer, a Set of children, and an integer depth. For 20 containers: ~2KB.
394
+
395
+ ---
396
+
397
+ ### 2.5. Cascade Mount Sequence
398
+
399
+ **Problem:** Target container is not in the DOM. Must sequentially render containers from the nearest mounted ancestor down to the target.
400
+
401
+ ```javascript
402
+ // cascade.js
403
+
404
+ import { get, element as resolve } from './graph.js';
405
+ import { lca, path } from './lca.js';
406
+
407
+ export async function ensure(target, current) {
408
+ // Find the lowest mounted ancestor on the path to target
409
+ const segments = path(current || 'body', target);
410
+ if (!segments) throw new Error(`No path from '${current}' to '${target}'`);
411
+
412
+ let mounted = null;
413
+
414
+ // Walk down the path, find first unmounted node
415
+ for (const node of segments) {
416
+ const el = node.ref?.deref();
417
+ if (el && el.isConnected) {
418
+ mounted = node;
419
+ } else {
420
+ break;
421
+ }
422
+ }
423
+
424
+ if (!mounted) {
425
+ mounted = get('body'); // Fallback to root
426
+ }
427
+
428
+ // Now render each unmounted container sequentially from mounted downward
429
+ const start = segments.indexOf(mounted) + 1;
430
+ for (let i = start; i < segments.length; i++) {
431
+ const node = segments[i];
432
+ const parent = node.parent;
433
+ const parentEl = parent.ref?.deref();
434
+ if (!parentEl || !parentEl.isConnected) {
435
+ throw new Error(`Parent '${parent.name}' disconnected during cascade`);
436
+ }
437
+
438
+ // Create the container element
439
+ const tag = node.name;
440
+ if (tag.includes('-') && !customElements.get(tag)) {
441
+ await customElements.whenDefined(tag);
442
+ }
443
+
444
+ const el = document.createElement(tag);
445
+
446
+ // Delegate to parent's swap interface or append directly
447
+ if (typeof parentEl.swapView === 'function') {
448
+ await parentEl.swapView(el, { direction: 'push' });
449
+ } else {
450
+ parentEl.replaceChildren(el);
451
+ }
452
+
453
+ // Wait for connectedCallback to fire and register the container
454
+ await new Promise(r => requestAnimationFrame(r));
455
+
456
+ // Verify registration
457
+ if (!resolve(node.name)) {
458
+ throw new Error(`Container '${node.name}' failed to register after mount`);
459
+ }
460
+ }
461
+
462
+ return resolve(target);
463
+ }
464
+ ```
465
+
466
+ **Why native:**
467
+ - `customElements.whenDefined` — awaits CE registration without polling
468
+ - `document.createElement` — zero-cost DOM node factory
469
+ - `requestAnimationFrame` — yields to layout, guarantees `connectedCallback` fires
470
+ - No timers, no polling, no `setTimeout`
471
+
472
+ ---
473
+
474
+ ### 2.6. Route-to-Graph Binding
475
+
476
+ **Fix:** Routes declare a `chain` of containers from root to target, not just a leaf container.
477
+
478
+ ```javascript
479
+ router.register('/settings/profile', 'page-profile', {
480
+ containers: ['main', 'sidebar', 'settings-panel']
481
+ // Ordered root-to-leaf. The last element is the rendering target.
482
+ });
483
+ ```
484
+
485
+ **During navigation:**
486
+
487
+ ```javascript
488
+ // In intercept.js handler():
489
+ const target = meta.containers?.at(-1) || meta.container;
490
+ const chain = meta.containers || [target];
491
+
492
+ // Verify or cascade-mount the full container chain
493
+ for (let i = 0; i < chain.length; i++) {
494
+ const name = chain[i];
495
+ if (!resolve(name)) {
496
+ await ensure(name, chain[i - 1] || 'body');
497
+ }
498
+ }
499
+
500
+ // Now safe to render into the target
501
+ const containerEl = resolve(target);
502
+ ```
503
+
504
+ **Backward compatibility:** The single `container` field still works — it's sugar for `containers: [container]`. When only one container is specified and it's already mounted, behavior is identical to current code.
505
+
506
+ ---
507
+
508
+ ## 3. Data Structures & Algorithms
509
+
510
+ ### 3.1. Container Tree Node
511
+
512
+ ```typescript
513
+ Node {
514
+ name: string // 'main', 'sidebar', 'settings-panel'
515
+ ref: WeakRef<Element> // Garbage-collectable DOM reference
516
+ parent: Node | null // Pointer up the tree
517
+ children: Set<Node> // Immediate children
518
+ depth: uint // Distance from root (body = 0)
519
+ }
520
+ ```
521
+
522
+ **Operations:**
523
+
524
+ | Operation | Complexity | Method |
525
+ |---------- |------------|--------|
526
+ | Add | O(1) | `add(name, el, parent)` |
527
+ | Remove | O(c) where c = children count | `remove(name)` |
528
+ | Lookup | O(1) | `get(name)` via Map |
529
+ | LCA | O(d) where d = max depth | `lca(a, b)` |
530
+ | Path | O(d) | `path(from, to)` |
531
+ | Cascade | O(d) * await | `ensure(target)` |
532
+
533
+ ### 3.2. Trie-Based Route Matcher
534
+
535
+ The current linear scan through sorted routes is O(n) per navigation. For applications with 100+ routes, a Radix Trie achieves O(k) matching where k = number of URL segments.
536
+
537
+ ```javascript
538
+ // trie.js
539
+
540
+ class Segment {
541
+ constructor() {
542
+ this.static = new Map(); // literal -> Segment
543
+ this.param = null; // Segment (for :named)
544
+ this.wild = null; // Segment (for *)
545
+ this.route = null; // matched route entry
546
+ this.key = null; // param name (e.g. 'member')
547
+ }
548
+ }
549
+
550
+ const trie = new Segment();
551
+
552
+ export function insert(pattern, route) {
553
+ const parts = pattern.split('/').filter(Boolean);
554
+ let node = trie;
555
+
556
+ for (const part of parts) {
557
+ if (part.startsWith(':')) {
558
+ if (!node.param) {
559
+ node.param = new Segment();
560
+ node.param.key = part.slice(1);
561
+ }
562
+ node = node.param;
563
+ } else if (part === '*') {
564
+ if (!node.wild) node.wild = new Segment();
565
+ node = node.wild;
566
+ } else {
567
+ if (!node.static.has(part)) {
568
+ node.static.set(part, new Segment());
569
+ }
570
+ node = node.static.get(part);
571
+ }
572
+ }
573
+
574
+ node.route = route;
575
+ }
576
+
577
+ export function find(pathname) {
578
+ const parts = pathname.split('/').filter(Boolean);
579
+ const params = {};
580
+ const result = walk(trie, parts, 0, params);
581
+ return result ? { route: result, params } : null;
582
+ }
583
+
584
+ function walk(node, parts, index, params) {
585
+ if (index === parts.length) {
586
+ return node.route;
587
+ }
588
+
589
+ const segment = parts[index];
590
+
591
+ // Priority 1: static match
592
+ if (node.static.has(segment)) {
593
+ const result = walk(node.static.get(segment), parts, index + 1, params);
594
+ if (result) return result;
595
+ }
596
+
597
+ // Priority 2: param match
598
+ if (node.param) {
599
+ params[node.param.key] = segment;
600
+ const result = walk(node.param, parts, index + 1, params);
601
+ if (result) {
602
+ return result;
603
+ }
604
+ delete params[node.param.key];
605
+ }
606
+
607
+ // Priority 3: wildcard match
608
+ if (node.wild) {
609
+ params['*'] = parts.slice(index).join('/');
610
+ return node.wild.route;
611
+ }
612
+
613
+ return null;
614
+ }
615
+ ```
616
+
617
+ **Comparison:**
618
+
619
+ | Approach | Time (n routes, k segments) | Memory |
620
+ |----------|----------------------------|--------|
621
+ | Linear scan (current) | O(n) | O(n) route objects |
622
+ | Sorted + binary search | O(k * log n) | O(n) |
623
+ | Radix trie | O(k) | O(total segments) |
624
+
625
+ For hot-path navigation, the trie eliminates URLPattern compilation overhead on the fast path. URLPattern remains available as a fallback for complex patterns (regex groups, modifiers) that the trie cannot express.
626
+
627
+ ### 3.3. LCA Algorithm (Detailed)
628
+
629
+ ```
630
+ FUNCTION lca(nameA, nameB):
631
+ nodeA ← graph.get(nameA)
632
+ nodeB ← graph.get(nameB)
633
+
634
+ IF nodeA is null OR nodeB is null:
635
+ RETURN null
636
+
637
+ // Phase 1: Equalize depths
638
+ WHILE nodeA.depth > nodeB.depth:
639
+ nodeA ← nodeA.parent
640
+
641
+ WHILE nodeB.depth > nodeA.depth:
642
+ nodeB ← nodeB.parent
643
+
644
+ // Phase 2: Walk up in tandem
645
+ WHILE nodeA ≠ nodeB:
646
+ nodeA ← nodeA.parent
647
+ nodeB ← nodeB.parent
648
+
649
+ RETURN nodeA
650
+ ```
651
+
652
+ **Correctness proof:** After depth equalization, both pointers are at the same depth. Since the tree is finite and connected to a single root, walking up in tandem must converge at the LCA.
653
+
654
+ **Worst case:** O(d) where d = tree depth. For UI container trees, d rarely exceeds 5.
655
+
656
+ ### 3.4. Cascade Render Sequence
657
+
658
+ ```
659
+ FUNCTION ensure(target, source):
660
+ segments ← path(source || 'body', target)
661
+
662
+ // Find deepest mounted node on the path
663
+ mounted ← null
664
+ FOR node IN segments:
665
+ IF node.ref.deref() AND node.ref.deref().isConnected:
666
+ mounted ← node
667
+ ELSE:
668
+ BREAK
669
+
670
+ IF mounted is null:
671
+ mounted ← root
672
+
673
+ // Render from mounted+1 down to target
674
+ start ← indexOf(mounted) + 1
675
+ FOR i FROM start TO segments.length - 1:
676
+ node ← segments[i]
677
+ parentEl ← node.parent.ref.deref()
678
+
679
+ ASSERT parentEl.isConnected
680
+
681
+ tag ← node.name
682
+ IF tag contains '-':
683
+ AWAIT customElements.whenDefined(tag)
684
+
685
+ el ← document.createElement(tag)
686
+
687
+ IF parentEl has swapView:
688
+ AWAIT parentEl.swapView(el)
689
+ ELSE:
690
+ parentEl.replaceChildren(el)
691
+
692
+ AWAIT animationFrame // Yield for connectedCallback
693
+
694
+ ASSERT graph.element(node.name) is defined
695
+
696
+ RETURN graph.element(target)
697
+ ```
698
+
699
+ ---
700
+
701
+ ## 4. Additional Edge Cases & Bugs
702
+
703
+ ### 4.1. MutationObserver Starvation
704
+
705
+ **Bug:** The `MutationObserver` in `container.js` is initialized inside `requestIdleCallback`. Under heavy load (60fps animation), `requestIdleCallback` may never fire — the observer never attaches and standard-element containers never get discovered.
706
+
707
+ **Fix:** Use `requestAnimationFrame` as the fallback when `requestIdleCallback` doesn't fire within 100ms:
708
+
709
+ ```javascript
710
+ function ensureObserver() {
711
+ if (observer || typeof window === 'undefined') return;
712
+
713
+ const attach = () => { /* ...create and observe... */ };
714
+
715
+ if (typeof requestIdleCallback !== 'undefined') {
716
+ const id = requestIdleCallback(attach, { timeout: 100 });
717
+ } else {
718
+ requestAnimationFrame(attach);
719
+ }
720
+ }
721
+ ```
722
+
723
+ ### 4.2. WeakRef Timing Hole
724
+
725
+ **Bug:** Between `disconnectedCallback` firing and `FinalizationRegistry` callback executing, a `getContainer()` call can return `undefined` even though the element was just removed intentionally (not GC'd). The consumer cannot distinguish "GC'd unexpectedly" from "unmounted normally."
726
+
727
+ **Fix:** `unregisterContainer()` should *immediately* delete the Map entry (it currently does). But `getContainer()` should NOT fall through to `querySelector` for non-selector names after an explicit unregister. Track explicit unregisters in a `Set<string>`:
728
+
729
+ ```javascript
730
+ const unregistered = new Set();
731
+
732
+ export function unregisterContainer(name, element) {
733
+ // ...existing logic...
734
+ unregistered.add(name);
735
+ // Clear after one macrotask to handle rapid remount
736
+ setTimeout(() => unregistered.delete(name), 0);
737
+ }
738
+
739
+ export function getContainer(name) {
740
+ if (unregistered.has(name)) return undefined;
741
+ // ...existing logic...
742
+ }
743
+ ```
744
+
745
+ ### 4.3. Route Chain Infinite Loop
746
+
747
+ **Bug:** In `match.js`, the chain builder walks `meta.parent` without cycle detection:
748
+
749
+ ```javascript
750
+ while (currentRoute) {
751
+ const parentPattern = currentRoute.meta?.parent;
752
+ if (parentPattern) {
753
+ const parentRoute = routes.find(r => r.patternStr === parentPattern);
754
+ currentRoute = parentRoute;
755
+ } else {
756
+ currentRoute = null;
757
+ }
758
+ }
759
+ ```
760
+
761
+ If route A declares parent B and route B declares parent A, this loops forever.
762
+
763
+ **Fix:** Track visited patterns:
764
+
765
+ ```javascript
766
+ const visited = new Set();
767
+ while (currentRoute) {
768
+ if (visited.has(currentRoute.patternStr)) break; // Cycle detected
769
+ visited.add(currentRoute.patternStr);
770
+ // ...rest...
771
+ }
772
+ ```
773
+
774
+ ### 4.4. Concurrent Navigation Race
775
+
776
+ **Bug:** Two rapid navigations can both pass the container check, then both attempt `swapView` on the same container. The first `swapView` starts a View Transition. The second `swapView` calls `startViewTransition` while the first is still animating — this *skips* the first transition (browser spec: "if a view transition is running, starting a new one aborts the previous"). The result is visual jank.
777
+
778
+ **Fix:** Containers should track in-flight transitions and queue or abort:
779
+
780
+ ```javascript
781
+ async swapView(element, options = {}) {
782
+ // Abort previous transition if still running
783
+ if (this._transition) {
784
+ this._transition.skipTransition();
785
+ }
786
+
787
+ const doSwap = () => this.replaceChildren(element);
788
+
789
+ if (typeof this.startViewTransition === 'function') {
790
+ this._transition = this.startViewTransition({ callback: doSwap });
791
+ await this._transition.finished;
792
+ this._transition = null;
793
+ } else {
794
+ doSwap();
795
+ }
796
+ }
797
+ ```
798
+
799
+ ### 4.5. Tab Sync Amplification Storm
800
+
801
+ **Bug:** With N tabs open and sync enabled, a navigation in tab 1 broadcasts to N-1 tabs. Each of those tabs navigates, which triggers `navigatesuccess`, which could broadcast again. The `sent` variable prevents echo, but only for exact URL match. If a guard redirects the URL (e.g., `/admin` → `/login`), the redirect URL differs from `sent`, causing a re-broadcast.
802
+
803
+ **Fix:** Add a `syncing` flag guard on outbound broadcasts:
804
+
805
+ ```javascript
806
+ navHandler = () => {
807
+ if (isSyncing) return; // Already guarded
808
+ // ...existing code...
809
+ };
810
+ ```
811
+
812
+ This is already implemented. But the *redirect* case slips through because `isSyncing` is reset in `.finally()` before the redirect navigation completes. The fix: keep `isSyncing = true` for the full redirect chain:
813
+
814
+ ```javascript
815
+ channel.onmessage = (event) => {
816
+ const { type, url } = event.data || {};
817
+ if (type === 'sync-navigate') {
818
+ if (sent === url) return;
819
+ sent = url;
820
+ isSyncing = true;
821
+
822
+ const finish = () => {
823
+ // Delay unsync by one macrotask to cover redirects
824
+ setTimeout(() => { isSyncing = false; }, 0);
825
+ };
826
+
827
+ const result = router.navigate(url);
828
+ result?.finished?.then(finish, finish) ?? finish();
829
+ }
830
+ };
831
+ ```
832
+
833
+ ---
834
+
835
+ ## 5. Integration Roadmap
836
+
837
+ | Phase | Change | Risk | Files |
838
+ |-------|--------|------|-------|
839
+ | 1 | Add `boot.js` deferred gate | Low — additive, no breaking changes | `boot.js`, `intercept.js`, `element.js` |
840
+ | 2 | Attach `window.router` | Low — additive | `index.js` |
841
+ | 3 | Add `graph.js` tree structure | Medium — new module, container API gains `parent` param | `graph.js`, `container.js` |
842
+ | 4 | Add `lca.js` algorithm | Low — additive utility | `lca.js` |
843
+ | 5 | Add `cascade.js` mount sequence | Medium — changes intercept behavior on missing containers | `cascade.js`, `intercept.js` |
844
+ | 6 | Route `containers` array field | Low — backward-compatible extension | `intercept.js`, `match.js` |
845
+ | 7 | Optional trie matcher | Low — can run alongside URLPattern | `trie.js`, `match.js` |
846
+ | 8 | Fix edge-case bugs (4.1–4.5) | Low — targeted patches | Various |
847
+
848
+ ---
849
+
850
+ ## 6. Performance Budget
851
+
852
+ | Operation | Target | Native API |
853
+ |-----------|--------|-----------|
854
+ | Route match | < 0.1ms (warm) | URLPattern.exec / Trie walk |
855
+ | LCA computation | < 0.01ms | Pointer arithmetic |
856
+ | Cascade mount (per level) | < 16ms (one frame) | createElement + rAF |
857
+ | Container lookup | < 0.01ms | Map.get + WeakRef.deref |
858
+ | Initial boot gate | < 50ms after DOMContentLoaded | Promise.all |
859
+
860
+ All operations target zero GC pressure on the hot path. WeakRef derefs are non-allocating. Map lookups are O(1) amortized. The trie avoids creating intermediate objects during traversal.
861
+
862
+ ---
863
+
864
+ ## 7. Naming Convention Adherence
865
+
866
+ All proposed code follows the one-word naming rule:
867
+
868
+ | Concept | Name | Rationale |
869
+ |---------|------|-----------|
870
+ | Container tree node | `Node` | One word, scoped to `graph.js` |
871
+ | Tree module | `graph` | One word, describes the structure |
872
+ | LCA function | `lca` | Acronym, universally understood |
873
+ | Path function | `path` | One word, returns root-to-target |
874
+ | Boot gate | `gate` | One word, registers a prerequisite |
875
+ | Boot trigger | `boot` | One word, fires the initial match |
876
+ | Cascade function | `ensure` | One word, guarantees container exists |
877
+ | Trie segment | `Segment` | One word, represents one URL part |
878
+ | Trie insert | `insert` | One word |
879
+ | Trie lookup | `find` | One word |
880
+ | Node alive check | `alive` | One word, boolean method |
881
+ | Node element resolve | `element` | One word, returns DOM node |
882
+
883
+ Files: `graph.js`, `lca.js`, `cascade.js`, `boot.js`, `trie.js` — all single-word, lowercase.
884
+
885
+ ---
886
+
887
+ *End of audit.*