@adaas/are-html 0.0.20 → 0.0.22

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 (109) hide show
  1. package/.conf/tsconfig.base.json +1 -0
  2. package/.conf/tsconfig.browser.json +1 -0
  3. package/.conf/tsconfig.node.json +1 -0
  4. package/dist/browser/index.d.mts +206 -7
  5. package/dist/browser/index.mjs +527 -65
  6. package/dist/browser/index.mjs.map +1 -1
  7. package/dist/node/directives/AreDirectiveFor.directive.d.mts +44 -1
  8. package/dist/node/directives/AreDirectiveFor.directive.d.ts +44 -1
  9. package/dist/node/directives/AreDirectiveFor.directive.js +102 -6
  10. package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
  11. package/dist/node/directives/AreDirectiveFor.directive.mjs +102 -6
  12. package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
  13. package/dist/node/directives/AreDirectiveShow.directive.d.mts +32 -0
  14. package/dist/node/directives/AreDirectiveShow.directive.d.ts +32 -0
  15. package/dist/node/directives/AreDirectiveShow.directive.js +81 -0
  16. package/dist/node/directives/AreDirectiveShow.directive.js.map +1 -0
  17. package/dist/node/directives/AreDirectiveShow.directive.mjs +71 -0
  18. package/dist/node/directives/AreDirectiveShow.directive.mjs.map +1 -0
  19. package/dist/node/engine/AreHTML.engine.d.mts +2 -1
  20. package/dist/node/engine/AreHTML.engine.d.ts +2 -1
  21. package/dist/node/engine/AreHTML.engine.js +8 -2
  22. package/dist/node/engine/AreHTML.engine.js.map +1 -1
  23. package/dist/node/engine/AreHTML.engine.mjs +8 -2
  24. package/dist/node/engine/AreHTML.engine.mjs.map +1 -1
  25. package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
  26. package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
  27. package/dist/node/engine/AreHTML.interpreter.js +29 -0
  28. package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
  29. package/dist/node/engine/AreHTML.interpreter.mjs +29 -0
  30. package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
  31. package/dist/node/engine/AreHTML.lifecycle.d.mts +8 -1
  32. package/dist/node/engine/AreHTML.lifecycle.d.ts +8 -1
  33. package/dist/node/engine/AreHTML.lifecycle.js +46 -3
  34. package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
  35. package/dist/node/engine/AreHTML.lifecycle.mjs +46 -3
  36. package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
  37. package/dist/node/helpers/AreScheduler.helper.d.mts +39 -0
  38. package/dist/node/helpers/AreScheduler.helper.d.ts +39 -0
  39. package/dist/node/helpers/AreScheduler.helper.js +40 -0
  40. package/dist/node/helpers/AreScheduler.helper.js.map +1 -0
  41. package/dist/node/helpers/AreScheduler.helper.mjs +40 -0
  42. package/dist/node/helpers/AreScheduler.helper.mjs.map +1 -0
  43. package/dist/node/index.d.mts +4 -1
  44. package/dist/node/index.d.ts +4 -1
  45. package/dist/node/index.js +21 -0
  46. package/dist/node/index.mjs +3 -0
  47. package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
  48. package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
  49. package/dist/node/instructions/AreHTML.instructions.constants.js +2 -1
  50. package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
  51. package/dist/node/instructions/AreHTML.instructions.constants.mjs +2 -1
  52. package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
  53. package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
  54. package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
  55. package/dist/node/instructions/HideElement.instruction.d.mts +13 -0
  56. package/dist/node/instructions/HideElement.instruction.d.ts +13 -0
  57. package/dist/node/instructions/HideElement.instruction.js +31 -0
  58. package/dist/node/instructions/HideElement.instruction.js.map +1 -0
  59. package/dist/node/instructions/HideElement.instruction.mjs +24 -0
  60. package/dist/node/instructions/HideElement.instruction.mjs.map +1 -0
  61. package/dist/node/lib/AreRoot/AreRoot.component.d.mts +57 -3
  62. package/dist/node/lib/AreRoot/AreRoot.component.d.ts +57 -3
  63. package/dist/node/lib/AreRoot/AreRoot.component.js +138 -49
  64. package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
  65. package/dist/node/lib/AreRoot/AreRoot.component.mjs +140 -51
  66. package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
  67. package/dist/node/lib/AreRoot/AreRootCache.context.d.mts +58 -0
  68. package/dist/node/lib/AreRoot/AreRootCache.context.d.ts +58 -0
  69. package/dist/node/lib/AreRoot/AreRootCache.context.js +106 -0
  70. package/dist/node/lib/AreRoot/AreRootCache.context.js.map +1 -0
  71. package/dist/node/lib/AreRoot/AreRootCache.context.mjs +99 -0
  72. package/dist/node/lib/AreRoot/AreRootCache.context.mjs.map +1 -0
  73. package/examples/dashboard/dist/index.html +1 -1
  74. package/examples/dashboard/dist/{mq19zxz4-mnlgmd.js → mqh9ryml-xat335.js} +1922 -1316
  75. package/examples/dashboard/src/concept.ts +3 -2
  76. package/examples/for-perf/concept.ts +45 -0
  77. package/examples/for-perf/containers/UI.container.ts +161 -0
  78. package/examples/for-perf/dist/index.html +270 -0
  79. package/examples/for-perf/dist/mqh9ryde-m243t8.js +15223 -0
  80. package/examples/for-perf/dist/mqh9ryfo-6a8d0o.js +15223 -0
  81. package/examples/for-perf/dist/mqh9ryfq-4pf5cv.js +15223 -0
  82. package/examples/for-perf/public/index.html +270 -0
  83. package/examples/for-perf/src/components/PerfApp.component.ts +37 -0
  84. package/examples/for-perf/src/components/PerfControls.component.ts +34 -0
  85. package/examples/for-perf/src/components/PerfGrid.component.ts +225 -0
  86. package/examples/for-perf/src/components/PerfHeader.component.ts +34 -0
  87. package/examples/for-perf/src/components/PerfStats.component.ts +43 -0
  88. package/examples/for-perf/src/concept.ts +94 -0
  89. package/examples/jumpstart/dist/index.html +1 -1
  90. package/examples/jumpstart/dist/{mq1a0fv0-ccgtz6.js → mq7mgf58-vbf07e.js} +895 -521
  91. package/examples/signal-routing/dist/index.html +1 -1
  92. package/examples/signal-routing/dist/{mq1bzrik-4lec86.js → mqh9ryc9-dkcbkx.js} +2024 -1300
  93. package/examples/signal-routing/src/components/SettingsPage.component.ts +39 -0
  94. package/examples/signal-routing/src/concept.ts +2 -0
  95. package/jest.config.ts +1 -0
  96. package/package.json +10 -9
  97. package/src/directives/AreDirectiveFor.directive.ts +185 -12
  98. package/src/directives/AreDirectiveShow.directive.ts +127 -0
  99. package/src/engine/AreHTML.engine.ts +11 -1
  100. package/src/engine/AreHTML.interpreter.ts +50 -0
  101. package/src/engine/AreHTML.lifecycle.ts +83 -6
  102. package/src/helpers/AreScheduler.helper.ts +61 -0
  103. package/src/index.ts +3 -0
  104. package/src/instructions/AreHTML.instructions.constants.ts +1 -0
  105. package/src/instructions/AreHTML.instructions.types.ts +9 -0
  106. package/src/instructions/HideElement.instruction.ts +29 -0
  107. package/src/lib/AreRoot/AreRoot.component.ts +205 -72
  108. package/src/lib/AreRoot/AreRootCache.context.ts +133 -0
  109. package/tsconfig.json +1 -0
@@ -10,6 +10,7 @@ import { AreDirectiveFeatures } from "@adaas/are-html/directive/AreDirective.con
10
10
  import { AreHTMLEngineContext } from "./AreHTML.context";
11
11
  import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
12
12
  import { A_Frame } from "@adaas/a-frame/core";
13
+ import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
13
14
 
14
15
 
15
16
  @A_Frame.Define({
@@ -18,6 +19,14 @@ import { A_Frame } from "@adaas/a-frame/core";
18
19
  })
19
20
  export class AreHTMLLifecycle extends AreLifecycle {
20
21
 
22
+ /**
23
+ * Per-chunk time budget (ms) for the time-sliced initial mount walk. While
24
+ * mounting a large subtree we keep applying nodes until this much wall-clock
25
+ * time has elapsed, then yield to the browser so it can paint and process
26
+ * input before the next chunk. ~16ms targets a single animation frame.
27
+ */
28
+ private static readonly MOUNT_BUDGET_MS = 16;
29
+
21
30
  @AreLifecycle.Init(AreComponentNode)
22
31
  initComponent(
23
32
  @A_Inject(A_Caller) node: AreHTMLNode,
@@ -92,7 +101,7 @@ export class AreHTMLLifecycle extends AreLifecycle {
92
101
 
93
102
  @A_Inject(A_Logger) logger?: A_Logger,
94
103
  ...args: any[]
95
- ) {
104
+ ): void | Promise<void> {
96
105
 
97
106
  logger?.debug(`[Mount] Component Trigger for <${node.aseid.entity}> with aseid :{${node.aseid.toString()}}`);
98
107
 
@@ -103,18 +112,86 @@ export class AreHTMLLifecycle extends AreLifecycle {
103
112
  if (scene.isInactive) return;
104
113
 
105
114
  /**
106
- * 1. We should simply run and render node itself.
115
+ * 1. Render the root of this mount itself.
107
116
  */
108
117
  node.interpret();
118
+
109
119
  /**
110
- * 2. Then go through all children of the node and mount the.
120
+ * 2. Walk the descendant subtree iteratively with an explicit enter/exit
121
+ * stack so we can TIME-SLICE the work. The previous implementation
122
+ * recursed via `child.mount()`, which fires onBeforeMount → onMount
123
+ * (interpret + recurse) → onAfterMount per node and runs the whole tree
124
+ * in one synchronous, un-yielding block. For large initial trees that
125
+ * froze the main thread on first page load.
126
+ *
127
+ * We replicate the exact per-node hook ordering:
128
+ * - enter → onBeforeMount, then (if active) interpret + queue children
129
+ * - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
130
+ * post-order, matching the recursive `node.mount()` contract)
131
+ *
132
+ * Small trees complete entirely within a single time budget and the
133
+ * handler returns `void` synchronously — a true fast-path with NO
134
+ * behavioural change for typical UIs. Only genuinely large trees exceed
135
+ * the budget, at which point we yield a macrotask (letting the browser
136
+ * paint / stay responsive) and resume the remaining work, returning a
137
+ * Promise that resolves when the whole subtree is mounted.
111
138
  */
112
- for (let i = 0; i < node.children.length; i++) {
113
- const child = node.children[i];
114
- child.mount();
139
+ interface MountFrame { node: AreHTMLNode; entered: boolean; }
140
+
141
+ const stack: MountFrame[] = [];
142
+ for (let i = node.children.length - 1; i >= 0; i--) {
143
+ stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
115
144
  }
145
+
146
+ const step = (): void => {
147
+ const frame = stack[stack.length - 1];
148
+ const current = frame.node;
149
+
150
+ if (frame.entered) {
151
+ // Post-order exit: the whole subtree below `current` is mounted.
152
+ stack.pop();
153
+ current.call(AreNodeFeatures.onAfterMount, current.scope);
154
+ return;
155
+ }
156
+
157
+ frame.entered = true;
158
+
159
+ // onBeforeMount always fires (even for inactive nodes), matching the
160
+ // recursive AreNode.mount() semantics.
161
+ current.call(AreNodeFeatures.onBeforeMount, current.scope);
162
+
163
+ if (!current.scene.isInactive) {
164
+ current.interpret();
165
+ // Push children in reverse so they pop in document order.
166
+ for (let i = current.children.length - 1; i >= 0; i--) {
167
+ stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
168
+ }
169
+ }
170
+ };
171
+
172
+ const drive = (): void | Promise<void> => {
173
+ const start = AreSchedulerHelper.now();
174
+ while (stack.length > 0) {
175
+ step();
176
+ if (stack.length > 0 && AreSchedulerHelper.now() - start >= AreHTMLLifecycle.MOUNT_BUDGET_MS) {
177
+ // Budget exhausted with work remaining — yield, then resume.
178
+ return new Promise<void>((resolve, reject) => {
179
+ AreSchedulerHelper.scheduleMacrotask(() => {
180
+ try {
181
+ resolve(drive());
182
+ } catch (error) {
183
+ reject(error);
184
+ }
185
+ });
186
+ });
187
+ }
188
+ }
189
+ };
190
+
191
+ return drive();
116
192
  }
117
193
 
194
+
118
195
  @A_Feature.Extend({
119
196
  name: AreAttributeFeatures.Update,
120
197
  scope: [AreDirectiveAttribute],
@@ -0,0 +1,61 @@
1
+ /**
2
+ * AreSchedulerHelper
3
+ *
4
+ * Cooperative time-slicing primitives shared by the chunked (async) render
5
+ * paths — the initial whole-page mount walk and the `$for` directive. Both need
6
+ * the SAME two capabilities:
7
+ * 1. a high-resolution clock to measure how long the current chunk has run, and
8
+ * 2. a zero-delay macrotask scheduler to yield to the browser between chunks
9
+ * so it can paint and process input before resuming work.
10
+ *
11
+ * Keeping these in one helper avoids duplicating the `MessageChannel` plumbing
12
+ * across directives/lifecycle and gives a single place to tune the strategy.
13
+ */
14
+ export class AreSchedulerHelper {
15
+
16
+ /**
17
+ * Lazily-created `MessageChannel` used to post zero-delay macrotasks.
18
+ * Created on first use so non-DOM environments (tests / SSR) that never
19
+ * schedule a chunk pay nothing.
20
+ */
21
+ private static _channel?: MessageChannel;
22
+
23
+ /** FIFO queue of callbacks waiting for their posted macrotask to fire. */
24
+ private static readonly _queue: Array<() => void> = [];
25
+
26
+ /**
27
+ * High-resolution wall-clock time in milliseconds. Uses `performance.now()`
28
+ * when available (monotonic, sub-millisecond), falling back to `Date.now()`.
29
+ */
30
+ static now(): number {
31
+ return (typeof performance !== 'undefined' && typeof performance.now === 'function')
32
+ ? performance.now()
33
+ : Date.now();
34
+ }
35
+
36
+ /**
37
+ * Schedule `fn` to run on the next macrotask.
38
+ *
39
+ * `MessageChannel` yields a true macrotask without the ~4ms clamp that nested
40
+ * `setTimeout(0)` calls incur, so the browser can paint between chunks with
41
+ * minimal scheduling overhead. Falls back to `setTimeout` in non-DOM
42
+ * environments (e.g. tests / SSR).
43
+ */
44
+ static scheduleMacrotask(fn: () => void): void {
45
+ if (typeof MessageChannel === 'undefined') {
46
+ setTimeout(fn, 0);
47
+ return;
48
+ }
49
+
50
+ if (!this._channel) {
51
+ this._channel = new MessageChannel();
52
+ this._channel.port1.onmessage = () => {
53
+ const next = this._queue.shift();
54
+ if (next) next();
55
+ };
56
+ }
57
+
58
+ this._queue.push(fn);
59
+ this._channel.port2.postMessage(null);
60
+ }
61
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from './attributes/AreStatic.attribute';
12
12
  export * from './directives/AreComponent.directive';
13
13
  export * from './directives/AreDirectiveFor.directive';
14
14
  export * from './directives/AreDirectiveIf.directive';
15
+ export * from './directives/AreDirectiveShow.directive';
15
16
 
16
17
  // ─────────────────────────────────────────────────────────────────────────────
17
18
  // ── Instructions ─────────────────────────────────────────────────────────────
@@ -22,6 +23,7 @@ export * from './instructions/AddInterpolation.instruction';
22
23
  export * from './instructions/AddListener.instruction';
23
24
  export * from './instructions/AddStyle.instruction';
24
25
  export * from './instructions/AddText.instruction';
26
+ export * from './instructions/HideElement.instruction';
25
27
  export * from './instructions/AreHTML.instructions.constants';
26
28
  export * from './instructions/AreHTML.instructions.types';
27
29
 
@@ -67,6 +69,7 @@ export * from './lib/AreDirective/AreDirective.types';
67
69
  // ── Lib / AreRoot ────────────────────────────────────────────────────────────
68
70
  // ─────────────────────────────────────────────────────────────────────────────
69
71
  export * from './lib/AreRoot/AreRoot.component';
72
+ export * from './lib/AreRoot/AreRootCache.context';
70
73
 
71
74
  // ─────────────────────────────────────────────────────────────────────────────
72
75
  // ── Lib / AreStyle ───────────────────────────────────────────────────────────
@@ -8,4 +8,5 @@ export const AreHTMLInstructions = {
8
8
  AddListener: '_AreHTML_AddListener',
9
9
  AddInterpolation: '_AreHTML_AddInterpolation',
10
10
  AddComment: '_AreHTML_AddComment',
11
+ HideElement: '_AreHTML_HideElement',
11
12
  } as const
@@ -34,6 +34,15 @@ export type AreHtmlAddStyleInstructionPayload = {
34
34
  styles: string;
35
35
  }
36
36
 
37
+ export type AreHtmlHideInstructionPayload = {
38
+ /**
39
+ * Optional explicit display value to restore when the element becomes
40
+ * visible again. When omitted, the interpreter caches and restores the
41
+ * element's own prior inline `display` value (Vue `v-show` semantics).
42
+ */
43
+ display?: string;
44
+ }
45
+
37
46
  export type AreHtmlAddListenerInstructionPayload = {
38
47
  /** DOM event name (e.g. "click", "input", "submit") */
39
48
  name: string;
@@ -0,0 +1,29 @@
1
+ import { A_Frame } from "@adaas/a-frame/core"
2
+ import { AreDeclaration, AreMutation, AreInstructionSerialized } from "@adaas/are";
3
+ import { AreHtmlHideInstructionPayload } from "./AreHTML.instructions.types";
4
+ import { AreHTMLInstructions } from "./AreHTML.instructions.constants";
5
+
6
+
7
+ @A_Frame.Define({
8
+ namespace: 'a-are-html',
9
+ description: 'Toggles the visibility of an existing element by setting its inline display to "none" on apply and restoring the previous inline display on revert. Used by the $show directive to hide/show an element without unmounting it, preserving its subtree, listeners and scene state.'
10
+ })
11
+ export class HideElementInstruction extends AreMutation<AreHtmlHideInstructionPayload> {
12
+
13
+ /**
14
+ * Caches the element's inline `display` value captured at apply time so it
15
+ * can be restored verbatim on revert (mirrors Vue `v-show`).
16
+ */
17
+ cache?: string;
18
+
19
+ constructor(
20
+ parent: AreDeclaration,
21
+ props: AreHtmlHideInstructionPayload | AreInstructionSerialized<AreHtmlHideInstructionPayload>
22
+ ) {
23
+ if ('aseid' in props) {
24
+ super(props as AreInstructionSerialized<AreHtmlHideInstructionPayload>);
25
+ } else {
26
+ super(AreHTMLInstructions.HideElement, parent, props);
27
+ }
28
+ }
29
+ }
@@ -1,9 +1,10 @@
1
- import { A_Caller, A_Context, A_FormatterHelper, A_Inject, } from "@adaas/a-concept";
1
+ import { A_Caller, A_Context, A_FormatterHelper, A_Inject, A_TYPES__Ctor } from "@adaas/a-concept";
2
2
  import { A_Frame } from "@adaas/a-frame/core";
3
3
  import { A_Logger } from "@adaas/a-utils/a-logger";
4
- import { A_SignalVector } from "@adaas/a-utils/a-signal";
4
+ import { A_Signal, A_SignalState, A_SignalVector } from "@adaas/a-utils/a-signal";
5
5
  import { Are, AreNode, AreSignals, AreSignalsMeta, AreSignalsContext } from "@adaas/are";
6
6
  import { AreRoute } from "@adaas/are-html/signals/AreRoute.signal";
7
+ import { AreRootCache, AreRootCacheEntry } from "./AreRootCache.context";
7
8
 
8
9
 
9
10
  @A_Frame.Define({
@@ -17,6 +18,7 @@ export class AreRoot extends Are {
17
18
  @A_Inject(A_Caller) root: AreNode,
18
19
  @A_Inject(A_Logger) logger: A_Logger,
19
20
  @A_Inject(AreSignalsContext) signalsContext?: AreSignalsContext,
21
+ @A_Inject(A_SignalState) signalState?: A_SignalState,
20
22
  ) {
21
23
 
22
24
  const rootId = root.id;
@@ -36,39 +38,19 @@ export class AreRoot extends Are {
36
38
  return;
37
39
  }
38
40
 
39
- const currentRoute = AreRoute.default();
40
-
41
- let componentName: string | undefined;
42
-
43
- if (currentRoute) {
44
- const initialVector = new A_SignalVector([currentRoute]);
45
-
46
- // 1. Lookup via AreSignalsContext (per root-id conditions)
47
- let renderTarget = signalsContext?.findComponentByVector(rootId, initialVector);
48
-
49
- // 2. Fall back to global AreSignalsMeta, pool-filtered.
50
- // IMPORTANT: pass the pool *into* the lookup so it can skip over
51
- // out-of-pool matches (e.g. a meta-outlet component whose condition
52
- // also matches the same vector) and find the highest-priority match
53
- // that this outlet can actually render. Filtering only after the
54
- // fact would mask valid in-pool matches and surface the outlet's
55
- // default instead.
56
- if (!renderTarget) {
57
- const signalsMeta = A_Context.meta<AreSignalsMeta>(AreSignals);
58
- const pool = signalsContext?.getComponentById(rootId);
59
- const metaTarget = signalsMeta?.findComponentByVector(
60
- initialVector,
61
- pool?.length ? pool : undefined,
62
- );
63
- if (metaTarget && (!pool?.length || pool.includes(metaTarget))) {
64
- renderTarget = metaTarget;
65
- }
66
- }
41
+ // Select from the ACCUMULATED signal state (every signal dispatched so
42
+ // far), not just the current URL route. Outlets keyed on domain signals
43
+ // (e.g. a primary-display selector) must reflect the live vector the
44
+ // moment they mount — even when they mount AFTER the routing signal was
45
+ // dispatched (a nested outlet inside a just-rendered parent). Using the
46
+ // same vector + lookup as onSignal keeps initial render and subsequent
47
+ // updates consistent.
48
+ const initialVector = this.buildInitialVector(signalState);
49
+ const renderTarget = this.matchComponent(rootId, initialVector, signalsContext);
67
50
 
68
- if (renderTarget?.name) {
69
- componentName = A_FormatterHelper.toKebabCase(renderTarget.name);
70
- }
71
- }
51
+ let componentName: string | undefined = renderTarget?.name
52
+ ? A_FormatterHelper.toKebabCase(renderTarget.name)
53
+ : undefined;
72
54
 
73
55
  // 3. Fall back to body content (the nodes already placed inside the
74
56
  // <are-root> tag act as the default). No setContent() call needed —
@@ -106,6 +88,7 @@ export class AreRoot extends Are {
106
88
  @A_Inject(A_SignalVector) vector: A_SignalVector,
107
89
  @A_Inject(A_Logger) logger: A_Logger,
108
90
  @A_Inject(AreSignalsContext) signalsContext?: AreSignalsContext,
91
+ @A_Inject(AreRootCache) cache?: AreRootCache,
109
92
  ) {
110
93
  const rootId = root.id;
111
94
 
@@ -114,27 +97,10 @@ export class AreRoot extends Are {
114
97
  return;
115
98
  }
116
99
 
117
- // 1. Try root-specific lookup via AreSignalsContext (keyed by the are-root's id attribute)
118
- let renderTarget = signalsContext?.findComponentByVector(rootId, vector);
119
-
120
- // 2. Fall back to global AreSignalsMeta lookup, restricted to this
121
- // outlet's pool. Passing the pool *into* the lookup is critical:
122
- // without it, the first globally matching component wins and may
123
- // belong to a different outlet (e.g. AisRequirementsPanel for the
124
- // meta-outlet matching AisEditorCursorScope) — the pool check then
125
- // rejects it and the outlet falls back to default, hiding a valid
126
- // in-pool match (e.g. AisDiagramTab matching AisSetPrimaryDisplay).
127
- if (!renderTarget) {
128
- const signalsMeta = A_Context.meta<AreSignalsMeta>(AreSignals);
129
- const pool = signalsContext?.getComponentById(rootId);
130
- const metaTarget = signalsMeta?.findComponentByVector(
131
- vector,
132
- pool?.length ? pool : undefined,
133
- );
134
- if (metaTarget && (!pool?.length || pool.includes(metaTarget))) {
135
- renderTarget = metaTarget;
136
- }
137
- }
100
+ // Resolve the target component for the incoming vector using the SAME
101
+ // lookup the initial template render uses (root-id conditions first,
102
+ // then the global pool-filtered meta map).
103
+ const renderTarget = this.matchComponent(rootId, vector, signalsContext);
138
104
 
139
105
  const def = signalsContext?.getDefault(rootId);
140
106
  const componentName = renderTarget?.name
@@ -145,12 +111,8 @@ export class AreRoot extends Are {
145
111
 
146
112
  // No matching condition for this signal vector and no default — clear the outlet.
147
113
  if (!componentName) {
148
- for (let i = 0; i < root.children.length; i++) {
149
- const child = root.children[i];
150
- signalsContext?.unsubscribe(child);
151
- child.unmount();
152
- child.destroy();
153
- root.removeChild(child);
114
+ for (const child of [...root.children]) {
115
+ this.stashChild(root, child, signalsContext, cache);
154
116
  }
155
117
  root.setContent('');
156
118
  return;
@@ -166,20 +128,25 @@ export class AreRoot extends Are {
166
128
  return;
167
129
  }
168
130
 
131
+ // Stash the currently displayed children so routing back to them can be
132
+ // re-injected instantly from the cache (they are unmounted + detached but
133
+ // NOT destroyed). Falls back to full teardown when no cache is available.
134
+ for (const child of [...root.children]) {
135
+ this.stashChild(root, child, signalsContext, cache);
136
+ }
137
+
169
138
  root.setContent(`<${componentName}></${componentName}>`);
170
139
 
171
- // Unsubscribe old children BEFORE destroying them.
172
- // Without this, AreSignals.handleSignalVector keeps iterating stale
173
- // (scope-less) nodes on every subsequent signal and throws an error.
174
- for (let i = 0; i < root.children.length; i++) {
175
- const child = root.children[i];
176
- signalsContext?.unsubscribe(child);
177
- child.unmount();
178
- child.destroy();
179
- root.removeChild(child);
140
+ // Fast path: a previously rendered subtree for this component is cached —
141
+ // re-attach it and re-mount from the preserved scene plan, skipping the
142
+ // expensive tokenize/init/load/transform/compile pipeline.
143
+ const cached = cache?.take(root.id, componentName);
144
+ if (cached) {
145
+ this.restoreChild(root, cached, signalsContext);
146
+ return;
180
147
  }
181
148
 
182
-
149
+ // Slow path: build the component subtree from scratch.
183
150
  root.tokenize();
184
151
 
185
152
  for (let i = 0; i < root.children.length; i++) {
@@ -193,7 +160,173 @@ export class AreRoot extends Are {
193
160
  child.transform();
194
161
 
195
162
  child.compile();
196
- child.mount();
163
+ // The HTML engine time-slices large initial mounts; await so a heavy
164
+ // routed component renders in yielding chunks instead of freezing the
165
+ // main thread on first entry. Small subtrees resolve synchronously.
166
+ await child.mount();
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Resolves the component a vector should render for the given root, mirroring
172
+ * the priority used everywhere in the routing system:
173
+ * 1. Root-specific conditions registered on AreSignalsContext.
174
+ * 2. The global AreSignalsMeta map, restricted to this outlet's pool.
175
+ *
176
+ * Passing the pool *into* the meta lookup is critical: without it, the first
177
+ * globally matching component wins and may belong to a different outlet
178
+ * (e.g. AisRequirementsPanel for the meta-outlet matching
179
+ * AisEditorCursorScope) — the pool check would then reject it and the outlet
180
+ * would fall back to its default, hiding a valid in-pool match (e.g.
181
+ * AisDiagramTab matching AisSetPrimaryDisplay).
182
+ *
183
+ * Returns `undefined` when nothing matches — callers decide whether to use a
184
+ * configured default, body content, or clear the outlet.
185
+ */
186
+ protected matchComponent(
187
+ rootId: string,
188
+ vector: A_SignalVector | undefined,
189
+ signalsContext?: AreSignalsContext,
190
+ ): A_TYPES__Ctor<Are> | undefined {
191
+ if (!vector) return undefined;
192
+
193
+ // 1. Root-specific conditions.
194
+ let renderTarget = signalsContext?.findComponentByVector(rootId, vector);
195
+
196
+ // 2. Global pool-filtered meta map.
197
+ if (!renderTarget) {
198
+ const signalsMeta = A_Context.meta<AreSignalsMeta>(AreSignals);
199
+ const pool = signalsContext?.getComponentById(rootId);
200
+ const metaTarget = signalsMeta?.findComponentByVector(
201
+ vector,
202
+ pool?.length ? pool : undefined,
203
+ rootId,
204
+ );
205
+ if (metaTarget && (!pool?.length || pool.includes(metaTarget))) {
206
+ renderTarget = metaTarget;
207
+ }
208
+ }
209
+
210
+ return renderTarget as A_TYPES__Ctor<Are> | undefined;
211
+ }
212
+
213
+ /**
214
+ * Builds the vector used for the INITIAL render. It is seeded from the
215
+ * accumulated signal state (every signal dispatched on the bus so far) so a
216
+ * freshly-mounted outlet reflects the live application state immediately,
217
+ * not just on the next signal tick. The current URL route is appended when
218
+ * no AreRoute is already present in the state, so route-driven outlets still
219
+ * resolve on the very first paint (before AreRouteWatcher has dispatched).
220
+ */
221
+ protected buildInitialVector(signalState?: A_SignalState): A_SignalVector {
222
+ const signals: A_Signal[] = [];
223
+
224
+ if (signalState) {
225
+ for (const signal of signalState.toVector()) {
226
+ if (signal) signals.push(signal);
227
+ }
228
+ }
229
+
230
+ if (!signals.some(signal => signal instanceof AreRoute)) {
231
+ try {
232
+ const currentRoute = AreRoute.default();
233
+ if (currentRoute) signals.push(currentRoute);
234
+ } catch {
235
+ // Non-browser environment (no document) — route is simply absent.
236
+ }
237
+ }
238
+
239
+ return new A_SignalVector(signals);
240
+ }
241
+
242
+ /**
243
+ * Detach a displayed child subtree from the outlet and stash it in the cache
244
+ * for fast re-injection later. The subtree is unmounted (its scene plan is
245
+ * preserved) and deregistered from the root scope, but NOT destroyed. The
246
+ * nodes that were subscribed to the signal bus are unsubscribed while cached
247
+ * so the detached DOM never reacts to signals, and recorded so they can be
248
+ * re-subscribed verbatim on restore.
249
+ *
250
+ * When no cache is available, or the LRU evicts an entry, the affected
251
+ * subtree is fully destroyed.
252
+ */
253
+ protected stashChild(
254
+ root: AreNode,
255
+ child: AreNode,
256
+ signalsContext: AreSignalsContext | undefined,
257
+ cache: AreRootCache | undefined,
258
+ ): void {
259
+ const tag = child.type;
260
+
261
+ child.unmount();
262
+
263
+ // Collect exactly the nodes that are currently subscribed within this
264
+ // subtree, then unsubscribe them. Without this, AreSignals keeps
265
+ // delivering vectors to a detached subtree that would update reverted
266
+ // DOM (unmount does not deactivate the scene).
267
+ const subscribers = signalsContext
268
+ ? this.collectSubscribers(child, signalsContext)
269
+ : [];
270
+ for (const node of subscribers) {
271
+ signalsContext?.unsubscribe(node);
272
+ }
273
+
274
+ // Deregister from the root scope (the "deregister node from parent").
275
+ root.removeChild(child);
276
+
277
+ if (!cache) {
278
+ void child.destroy();
279
+ return;
280
+ }
281
+
282
+ const evicted = cache.put(root.id, tag, { node: child, subscribers });
283
+ for (const entry of evicted) {
284
+ // Evicted entries are already unmounted + unsubscribed + detached.
285
+ void entry.node.destroy();
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Re-attach a cached subtree to the outlet and re-mount it from its preserved
291
+ * scene plan, re-subscribing exactly the nodes that were subscribed before it
292
+ * was cached.
293
+ */
294
+ protected restoreChild(
295
+ root: AreNode,
296
+ entry: AreRootCacheEntry,
297
+ signalsContext: AreSignalsContext | undefined,
298
+ ): void {
299
+ const child = entry.node;
300
+
301
+ root.addChild(child);
302
+
303
+ for (const node of entry.subscribers) {
304
+ signalsContext?.subscribe(node);
305
+ }
306
+
307
+ child.mount();
308
+ }
309
+
310
+ /**
311
+ * Walk a subtree and collect the nodes currently registered as signal
312
+ * subscribers. Mirrors the subscription performed at init time in
313
+ * AreHTMLLifecycle (component nodes and root nodes) without depending on the
314
+ * concrete node classes — it simply intersects the subtree with the live
315
+ * subscriber registry.
316
+ */
317
+ protected collectSubscribers(
318
+ node: AreNode,
319
+ signalsContext: AreSignalsContext,
320
+ ): AreNode[] {
321
+ const result: AreNode[] = [];
322
+ const queue: AreNode[] = [node];
323
+ while (queue.length > 0) {
324
+ const current = queue.shift()!;
325
+ if (signalsContext.subscribers.has(current)) {
326
+ result.push(current);
327
+ }
328
+ queue.push(...current.children);
197
329
  }
330
+ return result;
198
331
  }
199
332
  }