@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,773 @@
1
+ # Router Architecture Audit
2
+
3
+ Companion: see `definitions.md` for the unified `page` / `dock` / `view` API that supersedes the split `ui.element` + `router.register` pattern referenced throughout this document.
4
+
5
+ ---
6
+
7
+ ## 1. Critical Analysis
8
+
9
+ ### 1.1 Hard Refresh Failure
10
+
11
+ **Bug.**
12
+ `setup()` in `intercept.js` fires its initial route match via a single microtask:
13
+
14
+ ```js
15
+ // intercept.js
16
+ Promise.resolve().then(async () => {
17
+ const url = window.navigation.currentEntry?.url || window.location.href;
18
+ const found = await match(url); // routes[] is empty on cold boot
19
+ ...
20
+ });
21
+ ```
22
+
23
+ This races against three parallel processes:
24
+
25
+ 1. **Route registration** — ES module graph has not yet evaluated any `page(...)` calls.
26
+ 2. **Container availability** — Custom Elements have not fired `connectedCallback`.
27
+ 3. **Orchestrator attachment** — listener may not yet exist.
28
+
29
+ The microtask fires *before* `DOMContentLoaded`, before Custom Element `connectedCallback`, and before downstream modules finish. Result: `match()` returns `null` or emits `found` to an orchestrator that calls `getContainer()` on a node that has not mounted.
30
+
31
+ **Edge cases.**
32
+
33
+ - `<script type="module">` is deferred by spec — route registration happens in a subsequent microtask *after* `setup()` already fired.
34
+ - `<script type="module" async>` — execution order is non-deterministic.
35
+ - Service Worker serving stale HTML while JS bundles update — container tags may mismatch registered names.
36
+
37
+ ---
38
+
39
+ ### 1.2 Global Initialization Timing
40
+
41
+ **Bug.**
42
+ The auto-bootstrap block in `router/index.js` runs at module evaluation time:
43
+
44
+ ```js
45
+ if (typeof window !== 'undefined') {
46
+ registerNavigator(navigate);
47
+ setup();
48
+ setupTabSync(router);
49
+ }
50
+ ```
51
+
52
+ - `setup()` attaches the Navigation API listener *before* any route is registered.
53
+ - The initial match fires in the same microtask queue tick as module evaluation.
54
+ - No `window.router` global exists — users must import the object, but the pattern implies global availability.
55
+
56
+ **Edge cases.**
57
+
58
+ - Two entry points both importing `router/index.js` — `setup()` is idempotent, but route registration order is undefined.
59
+ - Dynamic `import()` after page load — `setup()` fires, emits initial match, but `DOMContentLoaded` already passed.
60
+
61
+ ---
62
+
63
+ ### 1.3 Flat Container Registry
64
+
65
+ **Bug.**
66
+ `container.js` stores `Map<string, WeakRef<HTMLElement>>` — a flat key-value store. It has no concept of:
67
+
68
+ - Parent/child relationships
69
+ - Depth or nesting level
70
+ - Sibling adjacency
71
+ - Path from root to a given container
72
+
73
+ The `route.meta.parent` field in `match.js` chains *route patterns*, not *DOM containers*. A route declaring `container: 'sidebar'` carries no information about whether `sidebar` lives inside `main` which lives inside `body`.
74
+
75
+ **Consequence.**
76
+ Cross-branch navigation — from a view in container A to a view in container B — requires understanding tree shape. Without a graph the router cannot:
77
+
78
+ 1. Determine which containers to unmount (everything below the divergence point)
79
+ 2. Determine which containers to mount (everything on the new branch)
80
+ 3. Identify the Lowest Common Ancestor (LCA) to minimise DOM churn
81
+
82
+ ---
83
+
84
+ ### 1.4 Cross-Branch Reconciliation Is Absent
85
+
86
+ **Bug.**
87
+ Resolution logic in `intercept.js` is binary:
88
+
89
+ ```js
90
+ if (routeMatch?.route?.meta?.container) {
91
+ const name = routeMatch.route.meta.container;
92
+ if (!getContainer(name)) {
93
+ throw new Error(...); // hard failure — no recovery
94
+ }
95
+ }
96
+ ```
97
+
98
+ On failure it throws. It makes no attempt to:
99
+
100
+ 1. Walk up to a common ancestor that *is* in the DOM
101
+ 2. Sequentially mount intermediate containers downward
102
+ 3. Await their `connectedCallback` before proceeding
103
+
104
+ **Example.**
105
+
106
+ ```
107
+ Target: /settings/profile → container: 'settings-panel'
108
+ Chain: body → main → sidebar → settings-panel
109
+ Current: /dashboard → only 'main' is mounted
110
+ ```
111
+
112
+ Navigation fails because `settings-panel` doesn't exist — it would only exist after `sidebar` is mounted inside `main`.
113
+
114
+ ---
115
+
116
+ ### 1.5 Route-to-Container Mapping Is One-Dimensional
117
+
118
+ **Bug.**
119
+ Each route declares a single `container` string with no way to express the mount chain:
120
+
121
+ ```js
122
+ router.register('/settings/profile', 'page-profile', { container: 'settings-panel' });
123
+ ```
124
+
125
+ There is no mechanism to say "to reach `settings-panel`, first ensure `sidebar` is in `main`." The `meta.parent` field chains *route patterns*, not *DOM ancestry*. These are orthogonal trees.
126
+
127
+ **Mismatch.**
128
+ Route hierarchy ≠ container hierarchy. A deeply nested route (`/a/b/c`) might render in a top-level container. A shallow route (`/settings`) might need three nested containers. The current architecture conflates them.
129
+
130
+ **Resolution.**
131
+ The `page(path, { via: [...chain] })` definition in the companion `definitions.md` solves this directly — the ordered array is the container chain root-to-leaf, with the last entry as the render target.
132
+
133
+ ---
134
+
135
+ ### 1.6 Auto-Bootstrap Has No Window Bridge
136
+
137
+ **Bug.**
138
+ The router initialises at module evaluation but never attaches to `window`. Any code that runs before the consumer's `import` statement cannot access the router instance. The documentation suggests `window.navigate(...)` but no bridge creates it.
139
+
140
+ ---
141
+
142
+ ### 1.7 No Cycle Detection in Route Chain
143
+
144
+ **Bug.**
145
+ `match.js` builds the parent chain without cycle detection:
146
+
147
+ ```js
148
+ while (currentRoute) {
149
+ const parentPattern = currentRoute.meta?.parent;
150
+ if (parentPattern) {
151
+ const parentRoute = routes.find(r => r.patternStr === parentPattern);
152
+ currentRoute = parentRoute;
153
+ } else {
154
+ currentRoute = null;
155
+ }
156
+ }
157
+ ```
158
+
159
+ Route A declaring parent B and route B declaring parent A loops forever.
160
+
161
+ ---
162
+
163
+ ## 2. Proposed Fixes
164
+
165
+ ### 2.1 Deferred Boot Gate
166
+
167
+ **Problem.** Initial match fires before routes exist or containers mount.
168
+
169
+ **Fix.** Replace the microtask emit with a gated sequence that waits for explicit readiness signals.
170
+
171
+ ```js
172
+ // boot.js
173
+ const gates = [];
174
+ let booted = false;
175
+
176
+ export function gate(promise) {
177
+ if (booted) return;
178
+ gates.push(promise);
179
+ }
180
+
181
+ export async function boot(emit) {
182
+ if (document.readyState === 'loading') {
183
+ await new Promise(r => document.addEventListener('DOMContentLoaded', r, { once: true }));
184
+ }
185
+ await Promise.all(gates);
186
+ gates.length = 0;
187
+ booted = true;
188
+ emit();
189
+ }
190
+
191
+ export function ready() {
192
+ return booted;
193
+ }
194
+ ```
195
+
196
+ **Integration in `intercept.js`:**
197
+
198
+ ```js
199
+ import { boot, ready } from './boot.js';
200
+
201
+ export function setup() {
202
+ if (ready()) return;
203
+ // attach Navigation API listener ...
204
+
205
+ boot(async () => {
206
+ const url = window.navigation.currentEntry?.url || location.href;
207
+ const found = await match(url);
208
+ if (found) emit('found', { ...found, direction: 'load' });
209
+ else emit('notfound', { url });
210
+ });
211
+ }
212
+ ```
213
+
214
+ **Route files gate on their element's CE registration:**
215
+
216
+ ```js
217
+ // called internally by page() in defs/page.js
218
+ import { gate } from '../router/boot.js';
219
+ gate(customElements.whenDefined(tag));
220
+ ```
221
+
222
+ **Why native.**
223
+ `document.readyState` — synchronous, zero-cost check.
224
+ `DOMContentLoaded` — fires once, no polling.
225
+ `Promise.all` — microtask, no timers.
226
+ `customElements.whenDefined` — native CE API, no polling.
227
+
228
+ ---
229
+
230
+ ### 2.2 Window Bridge
231
+
232
+ **Fix.** Attach a non-configurable global during auto-bootstrap.
233
+
234
+ ```js
235
+ // router/index.js
236
+ if (typeof window !== 'undefined') {
237
+ Object.defineProperty(window, 'router', {
238
+ value: router,
239
+ writable: false,
240
+ enumerable: false,
241
+ configurable: false
242
+ });
243
+ registerNavigator(navigate);
244
+ setup();
245
+ setupTabSync(router);
246
+ }
247
+ ```
248
+
249
+ `writable: false` prevents accidental overwrite. `enumerable: false` keeps it out of `for...in`. `configurable: false` makes it permanent. Import-style usage still works identically.
250
+
251
+ ---
252
+
253
+ ### 2.3 Hierarchical Container Graph
254
+
255
+ **Fix.** Replace the flat `Map<string, WeakRef>` with a tree that stores parent-child relationships and depth.
256
+
257
+ ```js
258
+ // graph.js
259
+ class Node {
260
+ constructor(name, ref, parent = null) {
261
+ this.name = name;
262
+ this.ref = ref; // WeakRef<HTMLElement>
263
+ this.parent = parent; // Node | null
264
+ this.children = new Set(); // Set<Node>
265
+ this.depth = parent ? parent.depth + 1 : 0;
266
+ }
267
+
268
+ alive() {
269
+ return this.ref.deref() !== undefined;
270
+ }
271
+ }
272
+
273
+ const nodes = new Map();
274
+ const root = new Node('body', null, null);
275
+ nodes.set('body', root);
276
+
277
+ export function add(name, el, parent = 'body') {
278
+ const ref = new WeakRef(el);
279
+ const parentNode = nodes.get(parent) ?? root;
280
+ const node = new Node(name, ref, parentNode);
281
+ parentNode.children.add(node);
282
+ nodes.set(name, node);
283
+ return node;
284
+ }
285
+
286
+ export function remove(name) {
287
+ const node = nodes.get(name);
288
+ if (!node) return;
289
+ node.parent?.children.delete(node);
290
+ for (const child of node.children) {
291
+ child.parent = node.parent;
292
+ child.depth = child.parent ? child.parent.depth + 1 : 0;
293
+ node.parent?.children.add(child);
294
+ }
295
+ nodes.delete(name);
296
+ }
297
+
298
+ export function get(name) { return nodes.get(name) ?? null; }
299
+ export function element(name) { return nodes.get(name)?.ref.deref() ?? null; }
300
+ export function clear() { nodes.clear(); root.children.clear(); nodes.set('body', root); }
301
+ ```
302
+
303
+ Containers declare their parent when defined via `dock(name, { parent })` in `definitions.md`.
304
+
305
+ ---
306
+
307
+ ### 2.4 Lowest Common Ancestor
308
+
309
+ **Problem.** Navigate from container A to container B — which containers unmount and which mount?
310
+
311
+ ```js
312
+ // lca.js
313
+ import { get } from './graph.js';
314
+
315
+ export function lca(a, b) {
316
+ let na = get(a);
317
+ let nb = get(b);
318
+ if (!na || !nb) return null;
319
+
320
+ while (na.depth > nb.depth) na = na.parent;
321
+ while (nb.depth > na.depth) nb = nb.parent;
322
+ while (na !== nb) { na = na.parent; nb = nb.parent; }
323
+
324
+ return na;
325
+ }
326
+
327
+ export function path(from, to) {
328
+ const ancestor = lca(from, to);
329
+ if (!ancestor) return null;
330
+
331
+ const segments = [];
332
+ let current = get(to);
333
+ while (current && current !== ancestor) {
334
+ segments.unshift(current);
335
+ current = current.parent;
336
+ }
337
+ segments.unshift(ancestor);
338
+ return segments;
339
+ }
340
+ ```
341
+
342
+ **Complexity.** O(d) where d = maximum tree depth. For typical UIs (depth 3–6) this is effectively O(1). No allocations on the hot path.
343
+
344
+ ---
345
+
346
+ ### 2.5 Cascade Mount Sequence
347
+
348
+ **Problem.** Target container not in the DOM. Must render containers root-to-leaf sequentially.
349
+
350
+ ```js
351
+ // cascade.js
352
+ import { get, element as resolve } from './graph.js';
353
+ import { path } from './lca.js';
354
+
355
+ export async function ensure(target, current) {
356
+ const segments = path(current ?? 'body', target);
357
+ if (!segments) throw new Error(`No path: '${current}' → '${target}'`);
358
+
359
+ // Find deepest mounted node on the path
360
+ let mounted = null;
361
+ for (const node of segments) {
362
+ const el = node.ref?.deref();
363
+ if (el?.isConnected) mounted = node;
364
+ else break;
365
+ }
366
+ if (!mounted) mounted = get('body');
367
+
368
+ // Mount sequentially from first unmounted downward
369
+ const start = segments.indexOf(mounted) + 1;
370
+ for (let i = start; i < segments.length; i++) {
371
+ const node = segments[i];
372
+ const parentEl = node.parent.ref?.deref();
373
+ if (!parentEl?.isConnected) throw new Error(`Parent '${node.parent.name}' disconnected`);
374
+
375
+ const tag = node.name;
376
+ if (tag.includes('-') && !customElements.get(tag)) {
377
+ await customElements.whenDefined(tag);
378
+ }
379
+
380
+ const el = document.createElement(tag);
381
+ if (typeof parentEl.swap === 'function') {
382
+ await parentEl.swap(el, { direction: 'push' });
383
+ } else {
384
+ parentEl.replaceChildren(el);
385
+ }
386
+
387
+ // Yield for connectedCallback
388
+ await new Promise(r => requestAnimationFrame(r));
389
+
390
+ if (!resolve(node.name)) throw new Error(`Container '${node.name}' failed to register`);
391
+ }
392
+
393
+ return resolve(target);
394
+ }
395
+ ```
396
+
397
+ **Why native.**
398
+ `customElements.whenDefined` — awaits CE registration, no polling.
399
+ `document.createElement` — zero-cost node factory.
400
+ `requestAnimationFrame` — yields to layout, guarantees `connectedCallback` fires.
401
+
402
+ ---
403
+
404
+ ### 2.6 Route Container Chain
405
+
406
+ **Fix.** Routes declare an ordered array of containers root-to-leaf. The last entry is the render target.
407
+
408
+ Using the new `page()` definition from `definations.md`:
409
+
410
+ ```js
411
+ page('/settings/profile', {
412
+ tag: 'page-profile',
413
+ via: ['main', 'sidebar', 'settings-panel'],
414
+ template: {
415
+ html: './template.html',
416
+ css: './style.css'
417
+ }
418
+ }, import.meta.url)
419
+ ```
420
+
421
+ During navigation:
422
+
423
+ ```js
424
+ // intercept.js handler
425
+ const chain = meta.via ?? (meta.container ? [meta.container] : []);
426
+ const target = chain.at(-1);
427
+
428
+ for (let i = 0; i < chain.length; i++) {
429
+ if (!resolve(chain[i])) {
430
+ await ensure(chain[i], chain[i - 1] ?? 'body');
431
+ }
432
+ }
433
+
434
+ const container = resolve(target);
435
+ ```
436
+
437
+ **Backward compatibility.** A single `container` string still works — treated as `via: [container]`.
438
+
439
+ ---
440
+
441
+ ### 2.7 Cycle Detection in Route Chain
442
+
443
+ ```js
444
+ // match.js
445
+ const visited = new Set();
446
+ while (currentRoute) {
447
+ if (visited.has(currentRoute.patternStr)) break;
448
+ visited.add(currentRoute.patternStr);
449
+ const parentPattern = currentRoute.meta?.parent;
450
+ currentRoute = parentPattern
451
+ ? routes.find(r => r.patternStr === parentPattern)
452
+ : null;
453
+ }
454
+ ```
455
+
456
+ ---
457
+
458
+ ## 3. Data Structures & Algorithms
459
+
460
+ ### 3.1 Container Tree Node
461
+
462
+ ```ts
463
+ Node {
464
+ name: string // 'main' | 'sidebar' | 'settings-panel'
465
+ ref: WeakRef<Element> // GC-safe DOM reference
466
+ parent: Node | null // pointer up the tree
467
+ children: Set<Node> // immediate children
468
+ depth: number // distance from root (body = 0)
469
+ }
470
+ ```
471
+
472
+ | Operation | Complexity | Method |
473
+ |-----------|------------------|----------------------|
474
+ | Add | O(1) | `add(name, el, parent)` |
475
+ | Remove | O(c) c=children | `remove(name)` |
476
+ | Lookup | O(1) | `get(name)` via Map |
477
+ | LCA | O(d) d=max depth | `lca(a, b)` |
478
+ | Path | O(d) | `path(from, to)` |
479
+ | Cascade | O(d) × await | `ensure(target)` |
480
+
481
+ ---
482
+
483
+ ### 3.2 Radix Trie Route Matcher
484
+
485
+ Linear scan through sorted routes is O(n) per navigation. A Radix Trie achieves O(k) where k = URL segment count.
486
+
487
+ ```js
488
+ // trie.js
489
+ class Segment {
490
+ constructor() {
491
+ this.static = new Map(); // literal → Segment
492
+ this.param = null; // Segment (for :named)
493
+ this.wild = null; // Segment (for *)
494
+ this.route = null; // matched route entry
495
+ this.key = null; // param name
496
+ }
497
+ }
498
+
499
+ const trie = new Segment();
500
+
501
+ export function insert(pattern, route) {
502
+ const parts = pattern.split('/').filter(Boolean);
503
+ let node = trie;
504
+
505
+ for (const part of parts) {
506
+ if (part.startsWith(':')) {
507
+ if (!node.param) { node.param = new Segment(); node.param.key = part.slice(1); }
508
+ node = node.param;
509
+ } else if (part === '*') {
510
+ if (!node.wild) node.wild = new Segment();
511
+ node = node.wild;
512
+ } else {
513
+ if (!node.static.has(part)) node.static.set(part, new Segment());
514
+ node = node.static.get(part);
515
+ }
516
+ }
517
+
518
+ node.route = route;
519
+ }
520
+
521
+ export function find(pathname) {
522
+ const parts = pathname.split('/').filter(Boolean);
523
+ const params = {};
524
+ const result = walk(trie, parts, 0, params);
525
+ return result ? { route: result, params } : null;
526
+ }
527
+
528
+ function walk(node, parts, i, params) {
529
+ if (i === parts.length) return node.route;
530
+
531
+ const seg = parts[i];
532
+
533
+ if (node.static.has(seg)) {
534
+ const result = walk(node.static.get(seg), parts, i + 1, params);
535
+ if (result) return result;
536
+ }
537
+
538
+ if (node.param) {
539
+ params[node.param.key] = seg;
540
+ const result = walk(node.param, parts, i + 1, params);
541
+ if (result) return result;
542
+ delete params[node.param.key];
543
+ }
544
+
545
+ if (node.wild) {
546
+ params['*'] = parts.slice(i).join('/');
547
+ return node.wild.route;
548
+ }
549
+
550
+ return null;
551
+ }
552
+ ```
553
+
554
+ | Approach | Time (n routes, k segments) | Memory |
555
+ |------------------------|-----------------------------|----------------------|
556
+ | Linear scan (current) | O(n) | O(n) route objects |
557
+ | Sorted + binary search | O(k · log n) | O(n) |
558
+ | Radix trie | O(k) | O(total segments) |
559
+
560
+ URLPattern remains available as a fallback for patterns the trie cannot express (regex groups, modifiers).
561
+
562
+ ---
563
+
564
+ ### 3.3 LCA Pseudocode
565
+
566
+ ```
567
+ FUNCTION lca(a, b):
568
+ na ← graph.get(a)
569
+ nb ← graph.get(b)
570
+ IF na is null OR nb is null → RETURN null
571
+
572
+ WHILE na.depth > nb.depth → na ← na.parent
573
+ WHILE nb.depth > na.depth → nb ← nb.parent
574
+ WHILE na ≠ nb → na ← na.parent; nb ← nb.parent
575
+
576
+ RETURN na
577
+ ```
578
+
579
+ **Proof.** After depth equalisation both pointers share depth. Since the tree is finite and rooted, simultaneous upward traversal must converge at the LCA.
580
+
581
+ **Worst case.** O(d). For real UI trees d rarely exceeds 5.
582
+
583
+ ---
584
+
585
+ ### 3.4 Cascade Sequence Pseudocode
586
+
587
+ ```
588
+ FUNCTION ensure(target, source):
589
+ segments ← path(source ?? 'body', target)
590
+
591
+ mounted ← null
592
+ FOR node IN segments:
593
+ IF node.ref.deref() AND node.ref.deref().isConnected:
594
+ mounted ← node
595
+ ELSE:
596
+ BREAK
597
+
598
+ IF mounted is null → mounted ← root
599
+
600
+ start ← indexOf(mounted) + 1
601
+ FOR i FROM start TO segments.length - 1:
602
+ node ← segments[i]
603
+ parentEl ← node.parent.ref.deref()
604
+ ASSERT parentEl.isConnected
605
+
606
+ IF node.name contains '-':
607
+ AWAIT customElements.whenDefined(node.name)
608
+
609
+ el ← document.createElement(node.name)
610
+
611
+ IF parentEl.swap exists → AWAIT parentEl.swap(el)
612
+ ELSE → parentEl.replaceChildren(el)
613
+
614
+ AWAIT animationFrame // yield for connectedCallback
615
+
616
+ ASSERT graph.element(node.name) is defined
617
+
618
+ RETURN graph.element(target)
619
+ ```
620
+
621
+ ---
622
+
623
+ ## 4. Edge Cases & Bugs
624
+
625
+ ### 4.1 MutationObserver Starvation
626
+
627
+ **Bug.** The `MutationObserver` in `container.js` is initialised inside `requestIdleCallback`. Under sustained 60 fps animation `requestIdleCallback` may never fire — the observer never attaches and standard-element containers are never discovered.
628
+
629
+ **Fix.**
630
+
631
+ ```js
632
+ function attach() { /* create and observe */ }
633
+
634
+ if (typeof requestIdleCallback !== 'undefined') {
635
+ requestIdleCallback(attach, { timeout: 100 });
636
+ } else {
637
+ requestAnimationFrame(attach);
638
+ }
639
+ ```
640
+
641
+ The `timeout: 100` option forces the callback even under load, with `requestAnimationFrame` as the fallback for environments that lack `requestIdleCallback`.
642
+
643
+ ---
644
+
645
+ ### 4.2 WeakRef Timing Hole
646
+
647
+ **Bug.** Between `disconnectedCallback` and `FinalizationRegistry` callback, `getContainer()` returns `undefined` even for an intentionally unmounted element. The consumer cannot distinguish "GC'd unexpectedly" from "unmounted normally."
648
+
649
+ **Fix.** Track explicit unregistrations:
650
+
651
+ ```js
652
+ const gone = new Set();
653
+
654
+ export function unregister(name) {
655
+ gone.add(name);
656
+ setTimeout(() => gone.delete(name), 0); // clear after one macrotask (rapid remount)
657
+ nodes.delete(name);
658
+ }
659
+
660
+ export function element(name) {
661
+ if (gone.has(name)) return undefined;
662
+ return nodes.get(name)?.ref.deref() ?? null;
663
+ }
664
+ ```
665
+
666
+ ---
667
+
668
+ ### 4.3 Concurrent Navigation Race
669
+
670
+ **Bug.** Two rapid navigations can both pass the container check, then both call `startViewTransition`. The browser aborts the first transition when the second starts — visual jank.
671
+
672
+ **Fix.**
673
+
674
+ ```js
675
+ // In the swap method of a container element
676
+ async swap(el, options = {}) {
677
+ if (this._tx) this._tx.skipTransition();
678
+
679
+ const go = () => this.replaceChildren(el);
680
+
681
+ if (typeof this.startViewTransition === 'function') {
682
+ this._tx = this.startViewTransition({ callback: go });
683
+ await this._tx.finished;
684
+ this._tx = null;
685
+ } else {
686
+ go();
687
+ }
688
+ }
689
+ ```
690
+
691
+ ---
692
+
693
+ ### 4.4 Tab Sync Redirect Amplification
694
+
695
+ **Bug.** `isSyncing` is reset in `.finally()` before a guard-triggered redirect completes. The redirect URL differs from `sent`, causing a re-broadcast storm across tabs.
696
+
697
+ **Fix.** Delay the unsync by one macrotask to cover the redirect chain:
698
+
699
+ ```js
700
+ channel.onmessage = ({ data }) => {
701
+ const { type, url } = data ?? {};
702
+ if (type !== 'sync-navigate') return;
703
+ if (sent === url) return;
704
+ sent = url;
705
+ isSyncing = true;
706
+
707
+ const finish = () => setTimeout(() => { isSyncing = false; }, 0);
708
+ const result = router.navigate(url);
709
+ result?.finished?.then(finish, finish) ?? finish();
710
+ };
711
+ ```
712
+
713
+ ---
714
+
715
+ ## 5. Integration Roadmap
716
+
717
+ | Phase | Change | Risk | Files | Status |
718
+ |-------|--------------------------------|--------|---------------------------------|--------|
719
+ | 1 | `boot.js` deferred gate | Low | `boot.js`, `intercept.js` | Done |
720
+ | 2 | `window.router` bridge | Low | `index.js` | Done |
721
+ | 3 | `graph.js` tree structure | Medium | `graph.js`, `container.js` | Done |
722
+ | 4 | `lca.js` algorithm | Low | `lca.js` | Done |
723
+ | 5 | `cascade.js` mount sequence | Medium | `cascade.js`, `intercept.js` | Done |
724
+ | 6 | `defs/` definitions layer | Low | `defs/page.js`, `defs/dock.js`, `defs/view.js` | Done |
725
+ | 7 | `trie.js` route matcher | Low | `trie.js`, `match.js` | Done |
726
+ | 8 | Bug fixes (§ 4.1 – 4.4) | Low | Various | Done |
727
+
728
+ All phases implemented. The `page` / `dock` / `view` / `part` definition layer is now the primary API. The legacy `ui.element({ url })` + `ui.container()` paths still function but are no longer recommended.
729
+
730
+ ---
731
+
732
+ ## 6. Performance Budget
733
+
734
+ | Operation | Target | Mechanism |
735
+ |------------------------|---------------------|----------------------------------|
736
+ | Route match | < 0.1 ms (warm) | Trie walk / URLPattern.exec |
737
+ | LCA computation | < 0.01 ms | Pointer arithmetic |
738
+ | Cascade (per level) | < 16 ms (one frame) | `createElement` + `rAF` |
739
+ | Container lookup | < 0.01 ms | `Map.get` + `WeakRef.deref` |
740
+ | Boot gate | < 50 ms after DCL | `Promise.all` |
741
+
742
+ All hot-path operations target zero GC pressure. `WeakRef.deref` is non-allocating. `Map.get` is O(1) amortised. The trie avoids intermediate allocations during traversal.
743
+
744
+ ---
745
+
746
+ ## 7. Naming Convention
747
+
748
+ All new identifiers follow the one-word rule. Hyphens in custom element tag names are a browser requirement, not a convention violation.
749
+
750
+ | Concept | Name | File |
751
+ |------------------------|-----------|-------------|
752
+ | Container tree node | `Node` | `graph.js` |
753
+ | Tree module | `graph` | `graph.js` |
754
+ | LCA function | `lca` | `lca.js` |
755
+ | Root-to-target path | `path` | `lca.js` |
756
+ | Boot prerequisite | `gate` | `boot.js` |
757
+ | Boot trigger | `boot` | `boot.js` |
758
+ | Cascade mount | `ensure` | `cascade.js`|
759
+ | Trie node | `Segment` | `trie.js` |
760
+ | Trie insert | `insert` | `trie.js` |
761
+ | Trie lookup | `find` | `trie.js` |
762
+ | Node alive check | `alive` | `graph.js` |
763
+ | DOM element resolve | `element` | `graph.js` |
764
+ | Route + element + slot | `page` | `defs/page.js` |
765
+ | Container element | `dock` | `defs/dock.js` |
766
+ | Plain component | `view` | `defs/view.js` |
767
+ | Container swap method | `swap` | element class |
768
+
769
+ Files: `graph.js`, `lca.js`, `cascade.js`, `boot.js`, `trie.js`, `defs/page.js`, `defs/dock.js`, `defs/view.js` — all single-word, lowercase.
770
+
771
+ ---
772
+
773
+ *End of audit.*