@adaas/are-html 0.0.21 → 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.
- package/.conf/tsconfig.base.json +1 -0
- package/.conf/tsconfig.browser.json +1 -0
- package/.conf/tsconfig.node.json +1 -0
- package/dist/browser/index.d.mts +45 -2
- package/dist/browser/index.mjs +170 -10
- package/dist/browser/index.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.mts +37 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.ts +37 -1
- package/dist/node/directives/AreDirectiveFor.directive.js +85 -4
- package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.mjs +85 -4
- package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.d.mts +8 -1
- package/dist/node/engine/AreHTML.lifecycle.d.ts +8 -1
- package/dist/node/engine/AreHTML.lifecycle.js +46 -3
- package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.mjs +46 -3
- package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
- package/dist/node/helpers/AreScheduler.helper.d.mts +39 -0
- package/dist/node/helpers/AreScheduler.helper.d.ts +39 -0
- package/dist/node/helpers/AreScheduler.helper.js +40 -0
- package/dist/node/helpers/AreScheduler.helper.js.map +1 -0
- package/dist/node/helpers/AreScheduler.helper.mjs +40 -0
- package/dist/node/helpers/AreScheduler.helper.mjs.map +1 -0
- package/dist/node/lib/AreRoot/AreRoot.component.js +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
- package/examples/dashboard/dist/index.html +1 -1
- package/examples/dashboard/dist/{mq19zxz4-mnlgmd.js → mqh9ryml-xat335.js} +1922 -1316
- package/examples/dashboard/src/concept.ts +3 -2
- package/examples/for-perf/concept.ts +45 -0
- package/examples/for-perf/containers/UI.container.ts +161 -0
- package/examples/for-perf/dist/index.html +270 -0
- package/examples/for-perf/dist/mqh9ryde-m243t8.js +15223 -0
- package/examples/for-perf/dist/mqh9ryfo-6a8d0o.js +15223 -0
- package/examples/for-perf/dist/mqh9ryfq-4pf5cv.js +15223 -0
- package/examples/for-perf/public/index.html +270 -0
- package/examples/for-perf/src/components/PerfApp.component.ts +37 -0
- package/examples/for-perf/src/components/PerfControls.component.ts +34 -0
- package/examples/for-perf/src/components/PerfGrid.component.ts +225 -0
- package/examples/for-perf/src/components/PerfHeader.component.ts +34 -0
- package/examples/for-perf/src/components/PerfStats.component.ts +43 -0
- package/examples/for-perf/src/concept.ts +94 -0
- package/examples/jumpstart/dist/index.html +1 -1
- package/examples/jumpstart/dist/{mq7hqrxy-4kus50.js → mq7mgf58-vbf07e.js} +269 -91
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mq7k53th-qiwy4x.js → mqh9ryc9-dkcbkx.js} +1726 -1419
- package/jest.config.ts +1 -0
- package/package.json +10 -9
- package/src/directives/AreDirectiveFor.directive.ts +141 -10
- package/src/engine/AreHTML.lifecycle.ts +83 -6
- package/src/helpers/AreScheduler.helper.ts +61 -0
- package/src/lib/AreRoot/AreRoot.component.ts +4 -1
- package/tsconfig.json +1 -0
package/jest.config.ts
CHANGED
|
@@ -17,6 +17,7 @@ const config: Config.InitialOptions = {
|
|
|
17
17
|
"@adaas/are-html/instructions/(.*)": "<rootDir>/src/instructions/$1",
|
|
18
18
|
"@adaas/are-html/watchers/(.*)": "<rootDir>/src/watchers/$1",
|
|
19
19
|
"@adaas/are-html/signals/(.*)": "<rootDir>/src/signals/$1",
|
|
20
|
+
"@adaas/are-html/helpers/(.*)": "<rootDir>/src/helpers/$1",
|
|
20
21
|
// Custom lib exports
|
|
21
22
|
"@adaas/are-html/style/(.*)": "<rootDir>/src/lib/AreStyle/$1",
|
|
22
23
|
"@adaas/are-html/directive/(.*)": "<rootDir>/src/lib/AreDirective/$1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaas/are-html",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "A-Concept Rendering Engine (ARE) is a powerful rendering engine designed to work seamlessly with the A-Concept framework. This library provides an HTML engine implementation of ARE, enabling developers to create dynamic and interactive user interfaces for web applications using standard HTML syntax.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a-concept",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"example:signal-routing": "nodemon ./examples/signal-routing/concept.ts",
|
|
74
74
|
"example:component-styles": "nodemon ./examples/component-styles/concept.ts",
|
|
75
75
|
"example:auxta": "nodemon ./examples/auxta/concept.ts",
|
|
76
|
+
"example:for-perf": "nodemon ./examples/for-perf/concept.ts",
|
|
76
77
|
"test:test": "nodemon ./src/test/app.ts",
|
|
77
78
|
"release": " npm run build && git add . && git commit -m \"new version created :: $(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g')\" && npm version patch && npm publish --access public",
|
|
78
79
|
"preversion": "echo test",
|
|
@@ -82,16 +83,16 @@
|
|
|
82
83
|
"build": "tsup --config tsup.config.ts"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
85
|
-
"@adaas/a-concept": "^0.3.
|
|
86
|
-
"@adaas/a-frame": "^0.1.
|
|
87
|
-
"@adaas/a-utils": "^0.3.
|
|
88
|
-
"@adaas/are": "^0.0.
|
|
86
|
+
"@adaas/a-concept": "^0.3.29",
|
|
87
|
+
"@adaas/a-frame": "^0.1.17",
|
|
88
|
+
"@adaas/a-utils": "^0.3.34",
|
|
89
|
+
"@adaas/are": "^0.0.23"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
91
|
-
"@adaas/a-concept": "^0.3.
|
|
92
|
-
"@adaas/a-frame": "^0.1.
|
|
93
|
-
"@adaas/a-utils": "^0.3.
|
|
94
|
-
"@adaas/are": "^0.0.
|
|
92
|
+
"@adaas/a-concept": "^0.3.29",
|
|
93
|
+
"@adaas/a-frame": "^0.1.17",
|
|
94
|
+
"@adaas/a-utils": "^0.3.34",
|
|
95
|
+
"@adaas/are": "^0.0.23",
|
|
95
96
|
"@types/chai": "^4.3.14",
|
|
96
97
|
"@types/jest": "^29.5.12",
|
|
97
98
|
"@types/mocha": "^10.0.6",
|
|
@@ -7,6 +7,7 @@ import { AddCommentInstruction } from "@adaas/are-html/instructions/AddComment.i
|
|
|
7
7
|
import { AreHTMLNode } from "@adaas/are-html/node";
|
|
8
8
|
import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
|
|
9
9
|
import { A_Frame } from "@adaas/a-frame/core";
|
|
10
|
+
import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
type AreForExpression = {
|
|
@@ -17,6 +18,12 @@ type AreForExpression = {
|
|
|
17
18
|
trackExpr: string | undefined;
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Per-`$for` reentrancy state used to serialize chunked (async) renders.
|
|
23
|
+
* Keyed by the directive attribute instance (one per `$for` in the template).
|
|
24
|
+
*/
|
|
25
|
+
type AreForRenderState = { running: boolean; pending: boolean };
|
|
26
|
+
|
|
20
27
|
|
|
21
28
|
@A_Frame.Define({
|
|
22
29
|
namespace: 'a-are-html',
|
|
@@ -25,6 +32,32 @@ type AreForExpression = {
|
|
|
25
32
|
@AreDirective.Priority(1)
|
|
26
33
|
export class AreDirectiveFor extends AreDirective {
|
|
27
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Lists whose number of NEW item nodes is at or below this threshold render
|
|
37
|
+
* fully synchronously — byte-for-byte the previous behavior. Typical UIs
|
|
38
|
+
* (menus, small tables) are therefore completely unaffected; only genuinely
|
|
39
|
+
* large lists pay the (tiny) scheduling cost to keep the main thread responsive.
|
|
40
|
+
*/
|
|
41
|
+
private static readonly SYNC_THRESHOLD = 100;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Per-chunk time budget (ms). During a large-list render we mount item nodes
|
|
45
|
+
* until this much time has elapsed, then yield to the browser so it can paint
|
|
46
|
+
* and process input before the next chunk. ~16ms targets one animation frame.
|
|
47
|
+
*/
|
|
48
|
+
private static readonly CHUNK_BUDGET_MS = 16;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-attribute serialization state. A new update() that arrives while a
|
|
52
|
+
* chunked render of the SAME `$for` is still in flight does NOT start a second
|
|
53
|
+
* concurrent pass (which could interleave mutations on the shared children
|
|
54
|
+
* list); instead it marks `pending` and the in-flight run re-runs once more
|
|
55
|
+
* with the latest data when it finishes. This guarantees the children list is
|
|
56
|
+
* only ever mutated by one pass at a time and the final state always reflects
|
|
57
|
+
* the most recent store value.
|
|
58
|
+
*/
|
|
59
|
+
private static readonly renderState = new WeakMap<object, AreForRenderState>();
|
|
60
|
+
|
|
28
61
|
|
|
29
62
|
@AreDirective.Transform
|
|
30
63
|
transform(
|
|
@@ -122,7 +155,40 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
122
155
|
@A_Inject(AreStore) store: AreStore,
|
|
123
156
|
@A_Inject(AreScene) scene: AreScene,
|
|
124
157
|
...args: any[]
|
|
125
|
-
): void {
|
|
158
|
+
): void | Promise<void> {
|
|
159
|
+
/**
|
|
160
|
+
* Serialize chunked renders per `$for`. If a previous large-list render
|
|
161
|
+
* is still streaming item nodes across macrotasks, do NOT start a second
|
|
162
|
+
* concurrent pass — that would interleave two diffs over the same shared
|
|
163
|
+
* children list (and leave half-compiled item nodes that the next diff
|
|
164
|
+
* would wrongly "reuse"). Mark a pass as pending instead; the in-flight
|
|
165
|
+
* run re-diffs once more from the latest store value when it completes.
|
|
166
|
+
*/
|
|
167
|
+
let state = AreDirectiveFor.renderState.get(attribute);
|
|
168
|
+
if (!state) {
|
|
169
|
+
state = { running: false, pending: false };
|
|
170
|
+
AreDirectiveFor.renderState.set(attribute, state);
|
|
171
|
+
}
|
|
172
|
+
if (state.running) {
|
|
173
|
+
state.pending = true;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return this.performUpdate(attribute, store, scene, state);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Core of the `$for` update: re-diff the source array against the current
|
|
182
|
+
* children, reconcile reused/removed items, then mount the new ones (small
|
|
183
|
+
* lists synchronously, large lists time-sliced). Never called while another
|
|
184
|
+
* pass for the same `$for` is in flight (see `update`).
|
|
185
|
+
*/
|
|
186
|
+
private performUpdate(
|
|
187
|
+
attribute: AreDirectiveAttribute,
|
|
188
|
+
store: AreStore,
|
|
189
|
+
scene: AreScene,
|
|
190
|
+
state: AreForRenderState,
|
|
191
|
+
): void | Promise<void> {
|
|
126
192
|
/**
|
|
127
193
|
* Re-evaluate the source array.
|
|
128
194
|
*/
|
|
@@ -167,9 +233,13 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
167
233
|
remaining.add(child);
|
|
168
234
|
}
|
|
169
235
|
|
|
170
|
-
// ── 2. Walk desired list; reuse existing or
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
// ── 2. Walk desired list; reuse existing or record items to create ──
|
|
237
|
+
// NOTE: new item nodes are NOT spawned here. Spawning (cloneWithScope +
|
|
238
|
+
// subtree init + scene activation) is the dominant cost of a large
|
|
239
|
+
// render, so it is deferred into the time-sliced loop below alongside
|
|
240
|
+
// transform/compile/mount. Existing (keyed) children are reconciled in
|
|
241
|
+
// place synchronously — that is cheap and keeps reused rows stable.
|
|
242
|
+
const toCreate: Array<{ item: any; idx: number }> = [];
|
|
173
243
|
|
|
174
244
|
for (let i = 0; i < newArray.length; i++) {
|
|
175
245
|
const item = newArray[i];
|
|
@@ -189,11 +259,8 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
189
259
|
[key]: item,
|
|
190
260
|
[index || 'index']: i,
|
|
191
261
|
};
|
|
192
|
-
desired.push(existing);
|
|
193
262
|
} else {
|
|
194
|
-
|
|
195
|
-
desired.push(itemNode);
|
|
196
|
-
newOnes.push(itemNode);
|
|
263
|
+
toCreate.push({ item, idx: i });
|
|
197
264
|
}
|
|
198
265
|
}
|
|
199
266
|
|
|
@@ -206,8 +273,13 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
206
273
|
owner.removeChild(child);
|
|
207
274
|
}
|
|
208
275
|
|
|
209
|
-
// ── 4.
|
|
210
|
-
|
|
276
|
+
// ── 4. Create + mount the new item nodes. ───────────────────────────
|
|
277
|
+
// `spawnItemNode` appends to `owner.children` immediately, so iterating
|
|
278
|
+
// `toCreate` in source order preserves list order (reused children keep
|
|
279
|
+
// their positions, new rows are appended in order) — identical to the
|
|
280
|
+
// previous synchronous behavior.
|
|
281
|
+
const createItem = (desc: { item: any; idx: number }) => {
|
|
282
|
+
const child = this.spawnItemNode(attribute.template!, owner, key, index, desc.item, desc.idx);
|
|
211
283
|
child.transform();
|
|
212
284
|
child.compile();
|
|
213
285
|
// While detached, stop after compile: the item's instructions are
|
|
@@ -215,6 +287,65 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
215
287
|
// the correct container once the condition becomes truthy. Mounting
|
|
216
288
|
// here would hoist the item to the nearest mounted ancestor.
|
|
217
289
|
if (attached) child.mount();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Small lists → fully synchronous, identical to the previous behavior.
|
|
293
|
+
if (toCreate.length <= AreDirectiveFor.SYNC_THRESHOLD) {
|
|
294
|
+
for (const desc of toCreate) createItem(desc);
|
|
295
|
+
return this.finishUpdate(attribute, store, scene, state);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Large lists → time-sliced render. Create item nodes until the frame
|
|
299
|
+
// budget elapses, then yield to the browser (zero-delay macrotask) so
|
|
300
|
+
// it can paint and stay responsive instead of blocking for the whole
|
|
301
|
+
// batch. The `state.running` flag (see `update`) prevents any other
|
|
302
|
+
// update() for this `$for` from interleaving while we stream.
|
|
303
|
+
state.running = true;
|
|
304
|
+
let cursor = 0;
|
|
305
|
+
|
|
306
|
+
const processChunk = (): void | Promise<void> => {
|
|
307
|
+
try {
|
|
308
|
+
const start = AreSchedulerHelper.now();
|
|
309
|
+
while (cursor < toCreate.length) {
|
|
310
|
+
createItem(toCreate[cursor]);
|
|
311
|
+
cursor++;
|
|
312
|
+
if (AreSchedulerHelper.now() - start >= AreDirectiveFor.CHUNK_BUDGET_MS) break;
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
// Never leave the `$for` wedged in the running state on failure,
|
|
316
|
+
// or every future update would be silently deferred forever.
|
|
317
|
+
state.running = false;
|
|
318
|
+
state.pending = false;
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (cursor < toCreate.length) {
|
|
323
|
+
return new Promise<void>(resolve => {
|
|
324
|
+
AreSchedulerHelper.scheduleMacrotask(() => resolve(processChunk()));
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return this.finishUpdate(attribute, store, scene, state);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return processChunk();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Completes an update pass. If another update() arrived while a chunked
|
|
336
|
+
* render was streaming, run exactly one more pass now from the latest store
|
|
337
|
+
* value so the final DOM always reflects the most recent data.
|
|
338
|
+
*/
|
|
339
|
+
private finishUpdate(
|
|
340
|
+
attribute: AreDirectiveAttribute,
|
|
341
|
+
store: AreStore,
|
|
342
|
+
scene: AreScene,
|
|
343
|
+
state: AreForRenderState,
|
|
344
|
+
): void | Promise<void> {
|
|
345
|
+
state.running = false;
|
|
346
|
+
if (state.pending) {
|
|
347
|
+
state.pending = false;
|
|
348
|
+
return this.performUpdate(attribute, store, scene, state);
|
|
218
349
|
}
|
|
219
350
|
}
|
|
220
351
|
|
|
@@ -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.
|
|
115
|
+
* 1. Render the root of this mount itself.
|
|
107
116
|
*/
|
|
108
117
|
node.interpret();
|
|
118
|
+
|
|
109
119
|
/**
|
|
110
|
-
* 2.
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
@@ -160,7 +160,10 @@ export class AreRoot extends Are {
|
|
|
160
160
|
child.transform();
|
|
161
161
|
|
|
162
162
|
child.compile();
|
|
163
|
-
|
|
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();
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
169
|
|
package/tsconfig.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@adaas/are-html/instructions/*": ["src/instructions/*"],
|
|
16
16
|
"@adaas/are-html/watchers/*": ["src/watchers/*"],
|
|
17
17
|
"@adaas/are-html/signals/*": ["src/signals/*"],
|
|
18
|
+
"@adaas/are-html/helpers/*": ["src/helpers/*"],
|
|
18
19
|
// Custom Lib Exports
|
|
19
20
|
"@adaas/are-html/style/*": ["src/lib/AreStyle/*"],
|
|
20
21
|
"@adaas/are-html/directive/*": ["src/lib/AreDirective/*"],
|