@adaas/are-html 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.d.mts +161 -5
- package/dist/browser/index.mjs +357 -55
- package/dist/browser/index.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.mts +7 -0
- package/dist/node/directives/AreDirectiveFor.directive.d.ts +7 -0
- package/dist/node/directives/AreDirectiveFor.directive.js +17 -2
- package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.mjs +17 -2
- package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveShow.directive.d.mts +32 -0
- package/dist/node/directives/AreDirectiveShow.directive.d.ts +32 -0
- package/dist/node/directives/AreDirectiveShow.directive.js +81 -0
- package/dist/node/directives/AreDirectiveShow.directive.js.map +1 -0
- package/dist/node/directives/AreDirectiveShow.directive.mjs +71 -0
- package/dist/node/directives/AreDirectiveShow.directive.mjs.map +1 -0
- package/dist/node/engine/AreHTML.engine.d.mts +2 -1
- package/dist/node/engine/AreHTML.engine.d.ts +2 -1
- package/dist/node/engine/AreHTML.engine.js +8 -2
- package/dist/node/engine/AreHTML.engine.js.map +1 -1
- package/dist/node/engine/AreHTML.engine.mjs +8 -2
- package/dist/node/engine/AreHTML.engine.mjs.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
- package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
- package/dist/node/engine/AreHTML.interpreter.js +29 -0
- package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.mjs +29 -0
- package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
- package/dist/node/index.d.mts +4 -1
- package/dist/node/index.d.ts +4 -1
- package/dist/node/index.js +21 -0
- package/dist/node/index.mjs +3 -0
- package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.js +2 -1
- package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.constants.mjs +2 -1
- package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
- package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
- package/dist/node/instructions/HideElement.instruction.d.mts +13 -0
- package/dist/node/instructions/HideElement.instruction.d.ts +13 -0
- package/dist/node/instructions/HideElement.instruction.js +31 -0
- package/dist/node/instructions/HideElement.instruction.js.map +1 -0
- package/dist/node/instructions/HideElement.instruction.mjs +24 -0
- package/dist/node/instructions/HideElement.instruction.mjs.map +1 -0
- package/dist/node/lib/AreRoot/AreRoot.component.d.mts +57 -3
- package/dist/node/lib/AreRoot/AreRoot.component.d.ts +57 -3
- package/dist/node/lib/AreRoot/AreRoot.component.js +137 -48
- package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs +139 -50
- package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
- package/dist/node/lib/AreRoot/AreRootCache.context.d.mts +58 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.d.ts +58 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.js +106 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.js.map +1 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.mjs +99 -0
- package/dist/node/lib/AreRoot/AreRootCache.context.mjs.map +1 -0
- package/examples/jumpstart/dist/index.html +1 -1
- package/examples/jumpstart/dist/{mq1a0fv0-ccgtz6.js → mq7hqrxy-4kus50.js} +629 -433
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mq1bzrik-4lec86.js → mq7k53th-qiwy4x.js} +903 -486
- package/examples/signal-routing/src/components/SettingsPage.component.ts +39 -0
- package/examples/signal-routing/src/concept.ts +2 -0
- package/package.json +3 -3
- package/src/directives/AreDirectiveFor.directive.ts +44 -2
- package/src/directives/AreDirectiveShow.directive.ts +127 -0
- package/src/engine/AreHTML.engine.ts +11 -1
- package/src/engine/AreHTML.interpreter.ts +50 -0
- package/src/index.ts +3 -0
- package/src/instructions/AreHTML.instructions.constants.ts +1 -0
- package/src/instructions/AreHTML.instructions.types.ts +9 -0
- package/src/instructions/HideElement.instruction.ts +29 -0
- package/src/lib/AreRoot/AreRoot.component.ts +201 -71
- package/src/lib/AreRoot/AreRootCache.context.ts +133 -0
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 (
|
|
149
|
-
|
|
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
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
signalsContext
|
|
177
|
-
|
|
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++) {
|
|
@@ -196,4 +163,167 @@ export class AreRoot extends Are {
|
|
|
196
163
|
child.mount();
|
|
197
164
|
}
|
|
198
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolves the component a vector should render for the given root, mirroring
|
|
169
|
+
* the priority used everywhere in the routing system:
|
|
170
|
+
* 1. Root-specific conditions registered on AreSignalsContext.
|
|
171
|
+
* 2. The global AreSignalsMeta map, restricted to this outlet's pool.
|
|
172
|
+
*
|
|
173
|
+
* Passing the pool *into* the meta lookup is critical: without it, the first
|
|
174
|
+
* globally matching component wins and may belong to a different outlet
|
|
175
|
+
* (e.g. AisRequirementsPanel for the meta-outlet matching
|
|
176
|
+
* AisEditorCursorScope) — the pool check would then reject it and the outlet
|
|
177
|
+
* would fall back to its default, hiding a valid in-pool match (e.g.
|
|
178
|
+
* AisDiagramTab matching AisSetPrimaryDisplay).
|
|
179
|
+
*
|
|
180
|
+
* Returns `undefined` when nothing matches — callers decide whether to use a
|
|
181
|
+
* configured default, body content, or clear the outlet.
|
|
182
|
+
*/
|
|
183
|
+
protected matchComponent(
|
|
184
|
+
rootId: string,
|
|
185
|
+
vector: A_SignalVector | undefined,
|
|
186
|
+
signalsContext?: AreSignalsContext,
|
|
187
|
+
): A_TYPES__Ctor<Are> | undefined {
|
|
188
|
+
if (!vector) return undefined;
|
|
189
|
+
|
|
190
|
+
// 1. Root-specific conditions.
|
|
191
|
+
let renderTarget = signalsContext?.findComponentByVector(rootId, vector);
|
|
192
|
+
|
|
193
|
+
// 2. Global pool-filtered meta map.
|
|
194
|
+
if (!renderTarget) {
|
|
195
|
+
const signalsMeta = A_Context.meta<AreSignalsMeta>(AreSignals);
|
|
196
|
+
const pool = signalsContext?.getComponentById(rootId);
|
|
197
|
+
const metaTarget = signalsMeta?.findComponentByVector(
|
|
198
|
+
vector,
|
|
199
|
+
pool?.length ? pool : undefined,
|
|
200
|
+
rootId,
|
|
201
|
+
);
|
|
202
|
+
if (metaTarget && (!pool?.length || pool.includes(metaTarget))) {
|
|
203
|
+
renderTarget = metaTarget;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return renderTarget as A_TYPES__Ctor<Are> | undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Builds the vector used for the INITIAL render. It is seeded from the
|
|
212
|
+
* accumulated signal state (every signal dispatched on the bus so far) so a
|
|
213
|
+
* freshly-mounted outlet reflects the live application state immediately,
|
|
214
|
+
* not just on the next signal tick. The current URL route is appended when
|
|
215
|
+
* no AreRoute is already present in the state, so route-driven outlets still
|
|
216
|
+
* resolve on the very first paint (before AreRouteWatcher has dispatched).
|
|
217
|
+
*/
|
|
218
|
+
protected buildInitialVector(signalState?: A_SignalState): A_SignalVector {
|
|
219
|
+
const signals: A_Signal[] = [];
|
|
220
|
+
|
|
221
|
+
if (signalState) {
|
|
222
|
+
for (const signal of signalState.toVector()) {
|
|
223
|
+
if (signal) signals.push(signal);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!signals.some(signal => signal instanceof AreRoute)) {
|
|
228
|
+
try {
|
|
229
|
+
const currentRoute = AreRoute.default();
|
|
230
|
+
if (currentRoute) signals.push(currentRoute);
|
|
231
|
+
} catch {
|
|
232
|
+
// Non-browser environment (no document) — route is simply absent.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return new A_SignalVector(signals);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Detach a displayed child subtree from the outlet and stash it in the cache
|
|
241
|
+
* for fast re-injection later. The subtree is unmounted (its scene plan is
|
|
242
|
+
* preserved) and deregistered from the root scope, but NOT destroyed. The
|
|
243
|
+
* nodes that were subscribed to the signal bus are unsubscribed while cached
|
|
244
|
+
* so the detached DOM never reacts to signals, and recorded so they can be
|
|
245
|
+
* re-subscribed verbatim on restore.
|
|
246
|
+
*
|
|
247
|
+
* When no cache is available, or the LRU evicts an entry, the affected
|
|
248
|
+
* subtree is fully destroyed.
|
|
249
|
+
*/
|
|
250
|
+
protected stashChild(
|
|
251
|
+
root: AreNode,
|
|
252
|
+
child: AreNode,
|
|
253
|
+
signalsContext: AreSignalsContext | undefined,
|
|
254
|
+
cache: AreRootCache | undefined,
|
|
255
|
+
): void {
|
|
256
|
+
const tag = child.type;
|
|
257
|
+
|
|
258
|
+
child.unmount();
|
|
259
|
+
|
|
260
|
+
// Collect exactly the nodes that are currently subscribed within this
|
|
261
|
+
// subtree, then unsubscribe them. Without this, AreSignals keeps
|
|
262
|
+
// delivering vectors to a detached subtree that would update reverted
|
|
263
|
+
// DOM (unmount does not deactivate the scene).
|
|
264
|
+
const subscribers = signalsContext
|
|
265
|
+
? this.collectSubscribers(child, signalsContext)
|
|
266
|
+
: [];
|
|
267
|
+
for (const node of subscribers) {
|
|
268
|
+
signalsContext?.unsubscribe(node);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Deregister from the root scope (the "deregister node from parent").
|
|
272
|
+
root.removeChild(child);
|
|
273
|
+
|
|
274
|
+
if (!cache) {
|
|
275
|
+
void child.destroy();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const evicted = cache.put(root.id, tag, { node: child, subscribers });
|
|
280
|
+
for (const entry of evicted) {
|
|
281
|
+
// Evicted entries are already unmounted + unsubscribed + detached.
|
|
282
|
+
void entry.node.destroy();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Re-attach a cached subtree to the outlet and re-mount it from its preserved
|
|
288
|
+
* scene plan, re-subscribing exactly the nodes that were subscribed before it
|
|
289
|
+
* was cached.
|
|
290
|
+
*/
|
|
291
|
+
protected restoreChild(
|
|
292
|
+
root: AreNode,
|
|
293
|
+
entry: AreRootCacheEntry,
|
|
294
|
+
signalsContext: AreSignalsContext | undefined,
|
|
295
|
+
): void {
|
|
296
|
+
const child = entry.node;
|
|
297
|
+
|
|
298
|
+
root.addChild(child);
|
|
299
|
+
|
|
300
|
+
for (const node of entry.subscribers) {
|
|
301
|
+
signalsContext?.subscribe(node);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
child.mount();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Walk a subtree and collect the nodes currently registered as signal
|
|
309
|
+
* subscribers. Mirrors the subscription performed at init time in
|
|
310
|
+
* AreHTMLLifecycle (component nodes and root nodes) without depending on the
|
|
311
|
+
* concrete node classes — it simply intersects the subtree with the live
|
|
312
|
+
* subscriber registry.
|
|
313
|
+
*/
|
|
314
|
+
protected collectSubscribers(
|
|
315
|
+
node: AreNode,
|
|
316
|
+
signalsContext: AreSignalsContext,
|
|
317
|
+
): AreNode[] {
|
|
318
|
+
const result: AreNode[] = [];
|
|
319
|
+
const queue: AreNode[] = [node];
|
|
320
|
+
while (queue.length > 0) {
|
|
321
|
+
const current = queue.shift()!;
|
|
322
|
+
if (signalsContext.subscribers.has(current)) {
|
|
323
|
+
result.push(current);
|
|
324
|
+
}
|
|
325
|
+
queue.push(...current.children);
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
199
329
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { A_Fragment } from "@adaas/a-concept";
|
|
2
|
+
import { A_Frame } from "@adaas/a-frame/core";
|
|
3
|
+
import { AreNode } from "@adaas/are";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single cached, detached component subtree for an are-root outlet.
|
|
8
|
+
*
|
|
9
|
+
* `node` is fully compiled and its scene plan is intact (it was `unmount()`ed,
|
|
10
|
+
* not destroyed), so it can be re-mounted instantly without re-tokenizing,
|
|
11
|
+
* re-loading, transforming or compiling. `subscribers` records the exact set of
|
|
12
|
+
* nodes inside the subtree that were subscribed to the signal bus at the moment
|
|
13
|
+
* of stashing — they are unsubscribed while cached (so the detached DOM never
|
|
14
|
+
* reacts to signals) and re-subscribed verbatim on restore.
|
|
15
|
+
*/
|
|
16
|
+
export type AreRootCacheEntry = {
|
|
17
|
+
node: AreNode;
|
|
18
|
+
subscribers: AreNode[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@A_Frame.Define({
|
|
23
|
+
namespace: 'a-are-html',
|
|
24
|
+
description: 'AreRootCache is a fragment that keeps a small per-root LRU of previously rendered are-root subtrees. When an are-root swaps the component it displays, the outgoing subtree is stashed here (unmounted + detached, but not destroyed) so that routing back to it can re-inject the preserved scene instantly instead of rebuilding from scratch.'
|
|
25
|
+
})
|
|
26
|
+
export class AreRootCache extends A_Fragment {
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* rootId -> (component tag -> cache entry). The inner Map preserves
|
|
30
|
+
* insertion order which is used as the LRU recency order: the first key is
|
|
31
|
+
* the least-recently-used entry, the last key the most-recently-used.
|
|
32
|
+
*/
|
|
33
|
+
protected _cache: Map<string, Map<string, AreRootCacheEntry>> = new Map();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Maximum number of cached subtrees kept per root. Older entries beyond this
|
|
37
|
+
* limit are evicted (and returned to the caller so it can destroy them).
|
|
38
|
+
*/
|
|
39
|
+
protected _limit: number;
|
|
40
|
+
|
|
41
|
+
constructor(limit: number = 10) {
|
|
42
|
+
super({ name: 'AreRootCache' });
|
|
43
|
+
this._limit = Math.max(0, Math.floor(limit));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maximum number of cached subtrees kept per root.
|
|
48
|
+
*/
|
|
49
|
+
get limit(): number {
|
|
50
|
+
return this._limit;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected bucket(rootId: string): Map<string, AreRootCacheEntry> {
|
|
54
|
+
let bucket = this._cache.get(rootId);
|
|
55
|
+
if (!bucket) {
|
|
56
|
+
bucket = new Map();
|
|
57
|
+
this._cache.set(rootId, bucket);
|
|
58
|
+
}
|
|
59
|
+
return bucket;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a subtree for the given component tag is currently cached.
|
|
64
|
+
*/
|
|
65
|
+
has(rootId: string, tag: string): boolean {
|
|
66
|
+
return this.bucket(rootId).has(tag);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Retrieve AND remove a cached subtree so it can become live again. Returns
|
|
71
|
+
* `undefined` on a cache miss.
|
|
72
|
+
*/
|
|
73
|
+
take(rootId: string, tag: string): AreRootCacheEntry | undefined {
|
|
74
|
+
const bucket = this.bucket(rootId);
|
|
75
|
+
const entry = bucket.get(tag);
|
|
76
|
+
if (entry) {
|
|
77
|
+
bucket.delete(tag);
|
|
78
|
+
}
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Stash a detached subtree under the given component tag. Returns any entries
|
|
84
|
+
* that were evicted to honour the LRU limit (or replaced for the same tag) so
|
|
85
|
+
* the caller can `destroy()` them.
|
|
86
|
+
*/
|
|
87
|
+
put(rootId: string, tag: string, entry: AreRootCacheEntry): AreRootCacheEntry[] {
|
|
88
|
+
const bucket = this.bucket(rootId);
|
|
89
|
+
const evicted: AreRootCacheEntry[] = [];
|
|
90
|
+
|
|
91
|
+
// Replace any stale entry for the same tag (should not normally happen,
|
|
92
|
+
// since a displayed tag is never simultaneously cached) and surface it
|
|
93
|
+
// for destruction so it does not leak.
|
|
94
|
+
const existing = bucket.get(tag);
|
|
95
|
+
if (existing) {
|
|
96
|
+
bucket.delete(tag);
|
|
97
|
+
if (existing.node !== entry.node) {
|
|
98
|
+
evicted.push(existing);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A limit of 0 disables caching: the freshly added entry is evicted
|
|
103
|
+
// immediately so the caller tears it down.
|
|
104
|
+
bucket.set(tag, entry);
|
|
105
|
+
|
|
106
|
+
while (bucket.size > this._limit) {
|
|
107
|
+
const oldestKey = bucket.keys().next().value as string | undefined;
|
|
108
|
+
if (oldestKey === undefined) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
const oldest = bucket.get(oldestKey)!;
|
|
112
|
+
bucket.delete(oldestKey);
|
|
113
|
+
evicted.push(oldest);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return evicted;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove and return every cached entry for a root (e.g. on teardown) so the
|
|
121
|
+
* caller can destroy them.
|
|
122
|
+
*/
|
|
123
|
+
clear(rootId: string): AreRootCacheEntry[] {
|
|
124
|
+
const bucket = this._cache.get(rootId);
|
|
125
|
+
if (!bucket) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const entries = [...bucket.values()];
|
|
129
|
+
bucket.clear();
|
|
130
|
+
this._cache.delete(rootId);
|
|
131
|
+
return entries;
|
|
132
|
+
}
|
|
133
|
+
}
|