@adaas/are-html 0.0.21 → 0.0.23

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 (154) 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 +214 -3
  5. package/dist/browser/index.mjs +787 -201
  6. package/dist/browser/index.mjs.map +1 -1
  7. package/dist/node/{AreBinding.attribute-doUvtOjc.d.mts → AreBinding.attribute-BWzEIw6H.d.mts} +45 -0
  8. package/dist/node/{AreBinding.attribute-Bm5LlOyE.d.ts → AreBinding.attribute-GpT-5Qmf.d.ts} +45 -0
  9. package/dist/node/attributes/AreBinding.attribute.d.mts +1 -1
  10. package/dist/node/attributes/AreBinding.attribute.d.ts +1 -1
  11. package/dist/node/attributes/AreDirective.attribute.d.mts +1 -1
  12. package/dist/node/attributes/AreDirective.attribute.d.ts +1 -1
  13. package/dist/node/attributes/AreEvent.attribute.d.mts +1 -1
  14. package/dist/node/attributes/AreEvent.attribute.d.ts +1 -1
  15. package/dist/node/attributes/AreStatic.attribute.d.mts +1 -1
  16. package/dist/node/attributes/AreStatic.attribute.d.ts +1 -1
  17. package/dist/node/directives/AreDirectiveFor.directive.d.mts +55 -2
  18. package/dist/node/directives/AreDirectiveFor.directive.d.ts +55 -2
  19. package/dist/node/directives/AreDirectiveFor.directive.js +141 -12
  20. package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
  21. package/dist/node/directives/AreDirectiveFor.directive.mjs +141 -12
  22. package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
  23. package/dist/node/directives/AreDirectiveIf.directive.d.mts +1 -1
  24. package/dist/node/directives/AreDirectiveIf.directive.d.ts +1 -1
  25. package/dist/node/directives/AreDirectiveShow.directive.d.mts +1 -1
  26. package/dist/node/directives/AreDirectiveShow.directive.d.ts +1 -1
  27. package/dist/node/engine/AreHTML.compiler.d.mts +1 -1
  28. package/dist/node/engine/AreHTML.compiler.d.ts +1 -1
  29. package/dist/node/engine/AreHTML.compiler.js +4 -0
  30. package/dist/node/engine/AreHTML.compiler.js.map +1 -1
  31. package/dist/node/engine/AreHTML.compiler.mjs +4 -0
  32. package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
  33. package/dist/node/engine/AreHTML.constants.d.mts +33 -1
  34. package/dist/node/engine/AreHTML.constants.d.ts +33 -1
  35. package/dist/node/engine/AreHTML.constants.js +166 -0
  36. package/dist/node/engine/AreHTML.constants.js.map +1 -1
  37. package/dist/node/engine/AreHTML.constants.mjs +165 -1
  38. package/dist/node/engine/AreHTML.constants.mjs.map +1 -1
  39. package/dist/node/engine/AreHTML.context.d.mts +66 -0
  40. package/dist/node/engine/AreHTML.context.d.ts +66 -0
  41. package/dist/node/engine/AreHTML.context.js +98 -0
  42. package/dist/node/engine/AreHTML.context.js.map +1 -1
  43. package/dist/node/engine/AreHTML.context.mjs +98 -0
  44. package/dist/node/engine/AreHTML.context.mjs.map +1 -1
  45. package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
  46. package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
  47. package/dist/node/engine/AreHTML.interpreter.js +66 -10
  48. package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
  49. package/dist/node/engine/AreHTML.interpreter.mjs +66 -10
  50. package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
  51. package/dist/node/engine/AreHTML.lifecycle.d.mts +2 -2
  52. package/dist/node/engine/AreHTML.lifecycle.d.ts +2 -2
  53. package/dist/node/engine/AreHTML.lifecycle.js +32 -4
  54. package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
  55. package/dist/node/engine/AreHTML.lifecycle.mjs +32 -4
  56. package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
  57. package/dist/node/engine/AreHTML.tokenizer.d.mts +1 -1
  58. package/dist/node/engine/AreHTML.tokenizer.d.ts +1 -1
  59. package/dist/node/engine/AreHTML.tokenizer.js +7 -1
  60. package/dist/node/engine/AreHTML.tokenizer.js.map +1 -1
  61. package/dist/node/engine/AreHTML.tokenizer.mjs +7 -1
  62. package/dist/node/engine/AreHTML.tokenizer.mjs.map +1 -1
  63. package/dist/node/engine/AreHTML.transformer.d.mts +1 -1
  64. package/dist/node/engine/AreHTML.transformer.d.ts +1 -1
  65. package/dist/node/helpers/AreScheduler.helper.d.mts +39 -0
  66. package/dist/node/helpers/AreScheduler.helper.d.ts +39 -0
  67. package/dist/node/helpers/AreScheduler.helper.js +40 -0
  68. package/dist/node/helpers/AreScheduler.helper.js.map +1 -0
  69. package/dist/node/helpers/AreScheduler.helper.mjs +40 -0
  70. package/dist/node/helpers/AreScheduler.helper.mjs.map +1 -0
  71. package/dist/node/index.d.mts +4 -3
  72. package/dist/node/index.d.ts +4 -3
  73. package/dist/node/index.js +7 -0
  74. package/dist/node/index.mjs +1 -0
  75. package/dist/node/instructions/AddStaticHTML.instruction.d.mts +8 -0
  76. package/dist/node/instructions/AddStaticHTML.instruction.d.ts +8 -0
  77. package/dist/node/instructions/AddStaticHTML.instruction.js +31 -0
  78. package/dist/node/instructions/AddStaticHTML.instruction.js.map +1 -0
  79. package/dist/node/instructions/AddStaticHTML.instruction.mjs +24 -0
  80. package/dist/node/instructions/AddStaticHTML.instruction.mjs.map +1 -0
  81. package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
  82. package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
  83. package/dist/node/instructions/AreHTML.instructions.constants.js +1 -0
  84. package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
  85. package/dist/node/instructions/AreHTML.instructions.constants.mjs +1 -0
  86. package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
  87. package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
  88. package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
  89. package/dist/node/lib/AreDirective/AreDirective.component.d.mts +1 -1
  90. package/dist/node/lib/AreDirective/AreDirective.component.d.ts +1 -1
  91. package/dist/node/lib/AreDirective/AreDirective.types.d.mts +1 -1
  92. package/dist/node/lib/AreDirective/AreDirective.types.d.ts +1 -1
  93. package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.mts +1 -1
  94. package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.ts +1 -1
  95. package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.mts +1 -1
  96. package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.ts +1 -1
  97. package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.mts +1 -1
  98. package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.ts +1 -1
  99. package/dist/node/lib/AreHTMLNode/AreHTMLNode.js +51 -0
  100. package/dist/node/lib/AreHTMLNode/AreHTMLNode.js.map +1 -1
  101. package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs +51 -0
  102. package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs.map +1 -1
  103. package/dist/node/lib/AreRoot/AreRoot.component.js +1 -1
  104. package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
  105. package/dist/node/lib/AreRoot/AreRoot.component.mjs +1 -1
  106. package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
  107. package/dist/node/nodes/AreComment.d.mts +1 -1
  108. package/dist/node/nodes/AreComment.d.ts +1 -1
  109. package/dist/node/nodes/AreComponent.d.mts +1 -1
  110. package/dist/node/nodes/AreComponent.d.ts +1 -1
  111. package/dist/node/nodes/AreInterpolation.d.mts +1 -1
  112. package/dist/node/nodes/AreInterpolation.d.ts +1 -1
  113. package/dist/node/nodes/AreRoot.d.mts +1 -1
  114. package/dist/node/nodes/AreRoot.d.ts +1 -1
  115. package/dist/node/nodes/AreText.d.mts +1 -1
  116. package/dist/node/nodes/AreText.d.ts +1 -1
  117. package/examples/dashboard/concept.ts +1 -1
  118. package/examples/dashboard/dist/index.html +1 -1
  119. package/examples/dashboard/dist/{mq19zxz4-mnlgmd.js → mqiw5sqa-ypckmj.js} +2275 -1323
  120. package/examples/dashboard/src/concept.ts +3 -2
  121. package/examples/for-perf/concept.ts +45 -0
  122. package/examples/for-perf/containers/UI.container.ts +161 -0
  123. package/examples/for-perf/dist/index.html +270 -0
  124. package/examples/for-perf/dist/mqj1mpf2-z4aokv.js +15664 -0
  125. package/examples/for-perf/dist/mqj1mpff-4fr7mw.js +15664 -0
  126. package/examples/for-perf/public/index.html +270 -0
  127. package/examples/for-perf/src/components/PerfApp.component.ts +37 -0
  128. package/examples/for-perf/src/components/PerfControls.component.ts +34 -0
  129. package/examples/for-perf/src/components/PerfGrid.component.ts +225 -0
  130. package/examples/for-perf/src/components/PerfHeader.component.ts +34 -0
  131. package/examples/for-perf/src/components/PerfStats.component.ts +43 -0
  132. package/examples/for-perf/src/concept.ts +94 -0
  133. package/examples/jumpstart/dist/index.html +1 -1
  134. package/examples/jumpstart/dist/{mq7hqrxy-4kus50.js → mq7mgf58-vbf07e.js} +269 -91
  135. package/examples/signal-routing/dist/index.html +1 -1
  136. package/examples/signal-routing/dist/{mq7k53th-qiwy4x.js → mqiwo23h-bhcolu.js} +2090 -1430
  137. package/jest.config.ts +1 -0
  138. package/package.json +10 -9
  139. package/src/directives/AreDirectiveFor.directive.ts +233 -19
  140. package/src/engine/AreHTML.compiler.ts +13 -0
  141. package/src/engine/AreHTML.constants.ts +142 -0
  142. package/src/engine/AreHTML.context.ts +112 -0
  143. package/src/engine/AreHTML.interpreter.ts +114 -13
  144. package/src/engine/AreHTML.lifecycle.ts +91 -7
  145. package/src/engine/AreHTML.tokenizer.ts +30 -1
  146. package/src/helpers/AreScheduler.helper.ts +61 -0
  147. package/src/index.ts +1 -0
  148. package/src/instructions/AddStaticHTML.instruction.ts +23 -0
  149. package/src/instructions/AreHTML.instructions.constants.ts +1 -0
  150. package/src/instructions/AreHTML.instructions.types.ts +9 -0
  151. package/src/lib/AreHTMLNode/AreHTMLNode.ts +74 -0
  152. package/src/lib/AreRoot/AreRoot.component.ts +4 -1
  153. package/tests/StaticIsland.test.ts +115 -0
  154. package/tsconfig.json +1 -0
@@ -54,6 +54,38 @@ export class AreHTMLEngineContext extends AreContext {
54
54
  */
55
55
  protected _container: Document;
56
56
 
57
+ /**
58
+ * Parsed-fragment cache for static islands (see AddStaticHTMLInstruction).
59
+ *
60
+ * Keyed by `hostTag\u0000markup`, each entry holds a `DocumentFragment` whose
61
+ * children were parsed by the browser exactly once — in the *correct element
62
+ * context* (the host tag), so table fragments (`<tr>`, `<td>`, …) and other
63
+ * context-sensitive content parse correctly. Repeated static islands with
64
+ * identical markup (e.g. list rows, reused components) clone the pre-parsed
65
+ * fragment instead of re-parsing the HTML string on every mount — turning an
66
+ * O(parse) operation into an O(clone) one.
67
+ */
68
+ protected _staticFragmentCache = new Map<string, DocumentFragment>();
69
+
70
+ /**
71
+ * Live-DOM attachments deferred while a mount pass is batching.
72
+ *
73
+ * A freshly-mounted subtree is built inside a *detached* root element, so
74
+ * every descendant `appendChild`/`insertBefore` happens off-document and
75
+ * triggers zero layout/paint invalidation. The single mutation that actually
76
+ * connects the built subtree to the live document is deferred and collected
77
+ * here, then flushed once when the batch closes — collapsing O(nodes) reflows
78
+ * into O(1) per mount root.
79
+ */
80
+ protected _pendingAttachments: Array<() => void> = [];
81
+
82
+ /**
83
+ * Depth of the currently open batching scopes. Re-entrant so that nested
84
+ * `beginBatch`/`endBatch` pairs flush exactly once, when the outermost scope
85
+ * closes.
86
+ */
87
+ protected _batchDepth = 0;
88
+
57
89
 
58
90
  constructor(props: Partial<AreHTMLContextConstructor>) {
59
91
  super(props.container?.body.innerHTML || props.source || '');
@@ -64,6 +96,86 @@ export class AreHTMLEngineContext extends AreContext {
64
96
  return this._container;
65
97
  }
66
98
 
99
+ /**
100
+ * `true` while a synchronous mount pass is batching live-DOM attachments.
101
+ * Interpreter handlers consult this to decide whether to attach an element
102
+ * immediately or hand the attachment to {@link deferAttach}.
103
+ */
104
+ get isBatching(): boolean {
105
+ return this._batchDepth > 0;
106
+ }
107
+
108
+ /**
109
+ * Opens a batching scope. Re-entrant: only the outermost matching
110
+ * {@link endBatch} flushes the deferred attachments, so a single mount pass
111
+ * connects its built subtree to the live DOM exactly once.
112
+ */
113
+ beginBatch(): void {
114
+ this._batchDepth++;
115
+ }
116
+
117
+ /**
118
+ * Registers a live-DOM attachment to run when the current batch flushes. If
119
+ * no batch is active the attachment runs immediately, preserving the original
120
+ * synchronous behaviour for updates that mount outside a batch.
121
+ *
122
+ * @param attach the DOM mutation that connects a built subtree to the document
123
+ */
124
+ deferAttach(attach: () => void): void {
125
+ if (this._batchDepth > 0) {
126
+ this._pendingAttachments.push(attach);
127
+ } else {
128
+ attach();
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Closes a batching scope. When the outermost scope closes, every deferred
134
+ * attachment runs in registration (document) order, connecting the built
135
+ * subtrees to the live DOM in a single pass.
136
+ */
137
+ endBatch(): void {
138
+ if (this._batchDepth === 0) return;
139
+ this._batchDepth--;
140
+ if (this._batchDepth > 0) return;
141
+
142
+ const pending = this._pendingAttachments;
143
+ this._pendingAttachments = [];
144
+ for (let i = 0; i < pending.length; i++) {
145
+ pending[i]();
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Returns a `DocumentFragment` containing the parsed form of `html`, parsed
151
+ * once in the context of `hostTag` (so context-sensitive content such as
152
+ * table rows/cells parses correctly) and cached thereafter. Callers should
153
+ * `cloneNode(true)` the returned fragment rather than mutating it, so the
154
+ * cache stays reusable.
155
+ *
156
+ * @param hostTag the tag name of the element the markup will be injected into
157
+ * @param html verbatim static-island inner markup
158
+ */
159
+ getStaticFragment(hostTag: string, html: string): DocumentFragment {
160
+ const key = `${hostTag}\u0000${html}`;
161
+ let fragment = this._staticFragmentCache.get(key);
162
+ if (!fragment) {
163
+ // Parse in the correct element context: the fragment-parsing
164
+ // algorithm uses the container element's tag to choose the right
165
+ // insertion mode (e.g. `<tbody>` legitimately allows `<tr>`).
166
+ const container = this._container.createElement(hostTag);
167
+ container.innerHTML = html;
168
+
169
+ fragment = this._container.createDocumentFragment();
170
+ while (container.firstChild) {
171
+ fragment.appendChild(container.firstChild);
172
+ }
173
+
174
+ this._staticFragmentCache.set(key, fragment);
175
+ }
176
+ return fragment;
177
+ }
178
+
67
179
 
68
180
  /**
69
181
  * Retrieves the DOM element associated with a given AreNode. This method looks up the node's ASEID in the nodeToHostElements map to find the corresponding DOM element. If the node is not found, it returns undefined. This allows the engine to efficiently access and manipulate the DOM elements that correspond to specific nodes in the AreNode tree, enabling dynamic updates and interactions based on the application state.
@@ -15,6 +15,7 @@ import { AddElementInstruction } from "@adaas/are-html/instructions/AddElement.i
15
15
  import { AddListenerInstruction } from "@adaas/are-html/instructions/AddListener.instruction";
16
16
  import { AddTextInstruction } from "@adaas/are-html/instructions/AddText.instruction";
17
17
  import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
18
+ import { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
18
19
  import { HideElementInstruction } from "@adaas/are-html/instructions/HideElement.instruction";
19
20
  import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
20
21
  import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
@@ -84,16 +85,25 @@ export class AreHTMLInterpreter extends AreInterpreter {
84
85
  ? context.container.createElementNS(SVG_NAMESPACE, tag)
85
86
  : context.container.createElement(tag);
86
87
 
87
- if (mountPoint.nodeType === Node.ELEMENT_NODE) {
88
+ // Index the element synchronously so descendants can resolve this
89
+ // node as their mount point during the same (still off-document) pass.
90
+ context.setInstructionElement(declaration, element);
91
+
92
+ const attach = mountPoint.nodeType === Node.ELEMENT_NODE
88
93
  // parent is a real element — just append
89
- mountPoint.appendChild(element);
90
- } else {
94
+ ? () => mountPoint.appendChild(element)
91
95
  // parent is an anchor (comment/text node) — insert before it
92
96
  // so content always appears before the anchor marker
93
- mountPoint.parentNode?.insertBefore(element, mountPoint);
94
- }
97
+ : () => { mountPoint.parentNode?.insertBefore(element, mountPoint); };
95
98
 
96
- context.setInstructionElement(declaration, element);
99
+ // When batching, only the mutation that touches the *live* document
100
+ // needs deferring; appends into an already-detached subtree are free
101
+ // and run immediately so the offline tree keeps taking shape.
102
+ if (context.isBatching && mountPoint.isConnected) {
103
+ context.deferAttach(attach);
104
+ } else {
105
+ attach();
106
+ }
97
107
 
98
108
  } else {
99
109
  const mountPoint = context.container.getElementById(node.id);
@@ -108,9 +118,17 @@ export class AreHTMLInterpreter extends AreInterpreter {
108
118
  ? context.container.createElementNS(SVG_NAMESPACE, tag)
109
119
  : context.container.createElement(tag);
110
120
 
111
- mountPoint.parentNode?.replaceChild(element, mountPoint);
112
-
113
121
  context.setInstructionElement(declaration, element);
122
+
123
+ const attach = () => { mountPoint.parentNode?.replaceChild(element, mountPoint); };
124
+
125
+ // The placeholder lives in the live DOM — defer the swap so the
126
+ // whole subtree built into `element` lands in one mutation.
127
+ if (context.isBatching && mountPoint.isConnected) {
128
+ context.deferAttach(attach);
129
+ } else {
130
+ attach();
131
+ }
114
132
  }
115
133
 
116
134
  // Register the element in the context index
@@ -135,7 +153,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
135
153
  ) {
136
154
  const element = context.getElementByInstruction(declaration);
137
155
 
138
- if (element && element.parentNode) {
156
+ // Skip the live-DOM detach when the element is already off-document: an
157
+ // ancestor removed earlier in the same unmount took this whole subtree
158
+ // with it, and a detached subtree is garbage-collected as a unit. The
159
+ // per-node removeChild would be wasted work. The index bookkeeping below
160
+ // must still run to drop the strong references that would pin it in memory.
161
+ if (element && element.parentNode && element.isConnected) {
139
162
  element.parentNode.removeChild(element);
140
163
  }
141
164
 
@@ -283,7 +306,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
283
306
 
284
307
  const { name } = mutation.payload;
285
308
 
286
- if (name && element.nodeType === Node.ELEMENT_NODE) {
309
+ // No point stripping attributes off an element that is already
310
+ // detached from the live document (it is about to be discarded with
311
+ // its whole subtree). Only touch attributes while the element is live.
312
+ if (name && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
287
313
  const colonIdx = name.indexOf(':');
288
314
  if (colonIdx > 0) {
289
315
  const ns = SVG_ATTRIBUTE_NS[name.slice(0, colonIdx)];
@@ -343,6 +369,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
343
369
 
344
370
  if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
345
371
 
372
+ // A detached element is being discarded with its subtree — restoring its
373
+ // inline display would be pointless work. Only restore while it is live.
374
+ if (!element.isConnected) return;
375
+
346
376
  const el = element as HTMLElement;
347
377
 
348
378
  // Restore the cached inline display. An explicit payload display, when
@@ -510,7 +540,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
510
540
  const listener = (mutation.payload as any)._callback as EventListenerOrEventListenerObject | undefined;
511
541
 
512
542
  if (listener) {
513
- element.removeEventListener(eventName, listener);
543
+ // Detaching a listener from an element that is already off-document is
544
+ // unnecessary — the element and its listeners are collected together.
545
+ // Still clear the context bookkeeping so no strong reference lingers.
546
+ if (element.isConnected) {
547
+ element.removeEventListener(eventName, listener);
548
+ }
514
549
  context.removeListener(element, name, listener);
515
550
  (mutation.payload as any)._callback = undefined;
516
551
  }
@@ -592,11 +627,73 @@ export class AreHTMLInterpreter extends AreInterpreter {
592
627
 
593
628
  if (!element) return;
594
629
 
595
- element.parentNode?.removeChild(element);
630
+ // Off-document text nodes vanish with their detached parent subtree, so
631
+ // skip the removeChild and just release the index reference.
632
+ if (element.isConnected) {
633
+ element.parentNode?.removeChild(element);
634
+ }
596
635
  context.removeInstructionElement(declaration);
597
636
  }
598
637
 
599
638
 
639
+ // ─────────────────────────────────────────────────────────────────────────────
640
+ // ── AddStaticHTML — Apply / Update / Revert ──────────────────────────────────
641
+ // ─────────────────────────────────────────────────────────────────────────────
642
+ // Materialises a fully static subtree (a "static island") onto its host
643
+ // element in a single pass. The tokenizer captured the inner markup verbatim
644
+ // instead of exploding it into per-element/per-text AreNodes, so here we hand
645
+ // the markup to the browser parser ONCE (via a cached <template>) and clone
646
+ // the pre-parsed fragment in. This collapses N DOM mutations into one and
647
+ // decodes HTML entities (e.g. &nbsp;) natively.
648
+ @A_Frame.Define({
649
+ description: 'Inject a static island\'s inner markup onto its host element in one pass via a cached, browser-parsed <template> clone. Decodes HTML entities natively.'
650
+ })
651
+ @AreInterpreter.Apply(AreHTMLInstructions.AddStaticHTML)
652
+ @AreInterpreter.Update(AreHTMLInstructions.AddStaticHTML)
653
+ addStaticHTML(
654
+ @A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
655
+ @A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
656
+ @A_Inject(A_Logger) logger?: A_Logger,
657
+ ) {
658
+ const element = context.getElementByInstruction(mutation.parent!);
659
+
660
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
661
+ throw new AreInterpreterError({
662
+ title: 'Element Not Found',
663
+ description: `Could not find a DOM element associated with the instruction ASEID "${mutation.parent}". Ensure the host element is rendered before materialising its static island.`
664
+ });
665
+ }
666
+
667
+ const el = element as HTMLElement;
668
+ const { html } = mutation.payload;
669
+
670
+ // Clear any previously injected content (Update path) before re-injecting.
671
+ el.textContent = '';
672
+
673
+ // Parse once (in the host tag's context) and clone the cached fragment.
674
+ const fragment = context.getStaticFragment(el.tagName.toLowerCase(), html);
675
+ el.appendChild(fragment.cloneNode(true));
676
+
677
+ logger?.debug('green', `Static island materialised onto <${(mutation.owner.parent ?? mutation.owner)?.aseid?.toString?.()}>`);
678
+ }
679
+
680
+
681
+ @A_Frame.Define({
682
+ description: 'Clear a static island\'s injected markup from its host element on revert.'
683
+ })
684
+ @AreInterpreter.Revert(AreHTMLInstructions.AddStaticHTML)
685
+ removeStaticHTML(
686
+ @A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
687
+ @A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
688
+ ) {
689
+ const element = context.getElementByInstruction(mutation.parent!);
690
+
691
+ if (element && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
692
+ (element as HTMLElement).textContent = '';
693
+ }
694
+ }
695
+
696
+
600
697
 
601
698
  @A_Frame.Define({
602
699
  description: 'Add a comment node to the DOM based on the provided declaration instruction.'
@@ -667,7 +764,11 @@ export class AreHTMLInterpreter extends AreInterpreter {
667
764
 
668
765
  if (!element) return;
669
766
 
670
- element.parentNode?.removeChild(element);
767
+ // Off-document comment anchors are discarded with their detached parent
768
+ // subtree; skip the removeChild and just release the index reference.
769
+ if (element.isConnected) {
770
+ element.parentNode?.removeChild(element);
771
+ }
671
772
  context.removeInstructionElement(declaration);
672
773
  }
673
774
 
@@ -92,7 +92,7 @@ export class AreHTMLLifecycle extends AreLifecycle {
92
92
 
93
93
  @A_Inject(A_Logger) logger?: A_Logger,
94
94
  ...args: any[]
95
- ) {
95
+ ): void | Promise<void> {
96
96
 
97
97
  logger?.debug(`[Mount] Component Trigger for <${node.aseid.entity}> with aseid :{${node.aseid.toString()}}`);
98
98
 
@@ -103,18 +103,102 @@ export class AreHTMLLifecycle extends AreLifecycle {
103
103
  if (scene.isInactive) return;
104
104
 
105
105
  /**
106
- * 1. We should simply run and render node itself.
106
+ * Open a batching scope for the whole (synchronous) mount pass. Every
107
+ * element created below is built into a *detached* root and attached to
108
+ * the live DOM only when the batch flushes — turning O(nodes) reflows into
109
+ * a single one per mount root. `try/finally` guarantees the batch closes
110
+ * (and the depth counter stays balanced) even if interpretation throws.
111
+ *
112
+ * The context is resolved from the node's scope (the scope the onMount
113
+ * feature runs in) so the override keeps the base `mount` signature.
107
114
  */
108
- node.interpret();
115
+ const context = node.scope.resolve<AreHTMLEngineContext>(AreHTMLEngineContext);
116
+
117
+ context?.beginBatch();
118
+
109
119
  /**
110
- * 2. Then go through all children of the node and mount the.
120
+ * `onAfterMount` must observe the node already connected to the live
121
+ * document (consumer components may measure layout / focus there). Since
122
+ * the subtree is built off-document and attached only when the batch
123
+ * flushes, we collect the post-order `onAfterMount` targets here and fire
124
+ * them once the flush has connected everything.
111
125
  */
112
- for (let i = 0; i < node.children.length; i++) {
113
- const child = node.children[i];
114
- child.mount();
126
+ const afterMountQueue: AreHTMLNode[] = [];
127
+
128
+ try {
129
+ /**
130
+ * 1. Render the root of this mount itself.
131
+ */
132
+ node.interpret();
133
+
134
+ /**
135
+ * 2. Walk the descendant subtree iteratively with an explicit enter/exit
136
+ * stack. We keep the iterative (non-recursive) shape — it is cheaper
137
+ * than deep recursion and gives us a single, clear place that owns the
138
+ * per-node hook ordering:
139
+ * - enter → onBeforeMount, then (if active) interpret + queue children
140
+ * - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
141
+ * post-order, matching the recursive `node.mount()` contract)
142
+ *
143
+ * [!] The initial mount is intentionally ATOMIC (fully synchronous). It
144
+ * does NOT time-slice / yield. Yielding mid-walk exposes a partially
145
+ * mounted tree to the event loop: any update dispatched during a gap
146
+ * (signal-driven re-render, async data load, etc.) interprets a node
147
+ * whose parent has not been mounted yet — producing
148
+ * `mount-point-not-found` and out-of-order DOM. The whole page must
149
+ * therefore appear in the DOM in one uninterrupted pass. Heavy lists
150
+ * are sliced at the source instead (see AreDirectiveFor), where the
151
+ * batching is reentrancy-safe.
152
+ */
153
+ interface MountFrame { node: AreHTMLNode; entered: boolean; }
154
+
155
+ const stack: MountFrame[] = [];
156
+ for (let i = node.children.length - 1; i >= 0; i--) {
157
+ stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
158
+ }
159
+
160
+ while (stack.length > 0) {
161
+ const frame = stack[stack.length - 1];
162
+ const current = frame.node;
163
+
164
+ if (frame.entered) {
165
+ // Post-order exit: the whole subtree below `current` is mounted.
166
+ // Defer the onAfterMount hook until after the batch flush so the
167
+ // node is connected to the live DOM when it runs.
168
+ stack.pop();
169
+ afterMountQueue.push(current);
170
+ continue;
171
+ }
172
+
173
+ frame.entered = true;
174
+
175
+ // onBeforeMount always fires (even for inactive nodes), matching the
176
+ // recursive AreNode.mount() semantics.
177
+ current.call(AreNodeFeatures.onBeforeMount, current.scope);
178
+
179
+ if (!current.scene.isInactive) {
180
+ current.interpret();
181
+ // Push children in reverse so they pop in document order.
182
+ for (let i = current.children.length - 1; i >= 0; i--) {
183
+ stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
184
+ }
185
+ }
186
+ }
187
+ } finally {
188
+ // Flush the deferred attachments — the fully built subtree lands in the
189
+ // live DOM in a single pass.
190
+ context?.endBatch();
191
+ }
192
+
193
+ // The subtree is now connected; fire onAfterMount in the original
194
+ // post-order, each with its node live in the document.
195
+ for (let i = 0; i < afterMountQueue.length; i++) {
196
+ const mounted = afterMountQueue[i];
197
+ mounted.call(AreNodeFeatures.onAfterMount, mounted.scope);
115
198
  }
116
199
  }
117
200
 
201
+
118
202
  @A_Feature.Extend({
119
203
  name: AreAttributeFeatures.Update,
120
204
  scope: [AreDirectiveAttribute],
@@ -8,6 +8,8 @@ import { AreEventAttribute } from "@adaas/are-html/attributes/AreEvent.attribute
8
8
  import { AreBindingAttribute } from "@adaas/are-html/attributes/AreBinding.attribute";
9
9
  import { AreStaticAttribute } from "@adaas/are-html/attributes/AreStatic.attribute";
10
10
  import { AreHTMLAttribute } from "../lib/AreHTMLAttribute/AreHTML.attribute";
11
+ import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
12
+ import { isStaticMarkup } from "./AreHTML.constants";
11
13
  import { A_Frame } from "@adaas/a-frame/core";
12
14
 
13
15
 
@@ -30,7 +32,34 @@ export class AreHTMLTokenizer extends AreTokenizer {
30
32
  @A_Inject(A_Logger) logger?: A_Logger
31
33
  ): void {
32
34
 
33
- super.tokenize(node, context, logger);
35
+ /**
36
+ * Static-island fast path.
37
+ *
38
+ * When a node's entire inner subtree is fully static — no `{{ }}`
39
+ * interpolations, no dynamic (`$`/`:`/`@`) attributes and only standard
40
+ * HTML tags — we skip exploding it into one child AreNode per element /
41
+ * text run and instead capture the inner markup verbatim. The interpreter
42
+ * later materialises it in a single pass (browser-parsed innerHTML /
43
+ * cached `<template>` clone), which:
44
+ * - eliminates N node + scope + scene + instruction allocations,
45
+ * - collapses N isolated DOM mutations into one, and
46
+ * - decodes HTML entities (e.g. `&nbsp;`) for free.
47
+ *
48
+ * Routing outlets (AreRootNode) are intentionally excluded — they own
49
+ * their content dynamically. The node's OWN attributes are still
50
+ * extracted below, so a dynamic attribute on the island root itself
51
+ * (e.g. `<div :class="x"> …static… </div>`) keeps working.
52
+ */
53
+ const isStaticIsland =
54
+ node instanceof AreComponentNode &&
55
+ !!node.content &&
56
+ isStaticMarkup(node.content);
57
+
58
+ if (isStaticIsland) {
59
+ (node as AreHTMLNode).markStatic(node.content);
60
+ } else {
61
+ super.tokenize(node, context, logger);
62
+ }
34
63
 
35
64
  context.startPerformance('attributeExtraction');
36
65
 
@@ -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
@@ -21,6 +21,7 @@ export * from './instructions/AddAttribute.instruction';
21
21
  export * from './instructions/AddElement.instruction';
22
22
  export * from './instructions/AddInterpolation.instruction';
23
23
  export * from './instructions/AddListener.instruction';
24
+ export * from './instructions/AddStaticHTML.instruction';
24
25
  export * from './instructions/AddStyle.instruction';
25
26
  export * from './instructions/AddText.instruction';
26
27
  export * from './instructions/HideElement.instruction';
@@ -0,0 +1,23 @@
1
+ import { A_Frame } from "@adaas/a-frame/core"
2
+ import { AreDeclaration, AreMutation, AreInstructionSerialized } from "@adaas/are";
3
+ import { AreHtmlAddStaticHTMLInstructionPayload } from "./AreHTML.instructions.types";
4
+ import { AreHTMLInstructions } from "./AreHTML.instructions.constants";
5
+
6
+
7
+ @A_Frame.Define({
8
+ namespace: 'a-are-html',
9
+ description: 'Materialises a fully static subtree (a "static island") onto its parent element in a single pass via browser-parsed innerHTML / a cached <template> clone. Apply injects the markup; revert clears it. Decodes HTML entities (e.g. &nbsp;) for free.'
10
+ })
11
+ export class AddStaticHTMLInstruction extends AreMutation<AreHtmlAddStaticHTMLInstructionPayload> {
12
+
13
+ constructor(
14
+ parent: AreDeclaration,
15
+ props: AreHtmlAddStaticHTMLInstructionPayload | AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>
16
+ ) {
17
+ if ('aseid' in props) {
18
+ super(props as AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>);
19
+ } else {
20
+ super(AreHTMLInstructions.AddStaticHTML, parent, props);
21
+ }
22
+ }
23
+ }
@@ -8,5 +8,6 @@ export const AreHTMLInstructions = {
8
8
  AddListener: '_AreHTML_AddListener',
9
9
  AddInterpolation: '_AreHTML_AddInterpolation',
10
10
  AddComment: '_AreHTML_AddComment',
11
+ AddStaticHTML: '_AreHTML_AddStaticHTML',
11
12
  HideElement: '_AreHTML_HideElement',
12
13
  } as const
@@ -34,6 +34,15 @@ export type AreHtmlAddStyleInstructionPayload = {
34
34
  styles: string;
35
35
  }
36
36
 
37
+ export type AreHtmlAddStaticHTMLInstructionPayload = {
38
+ /**
39
+ * Verbatim inner markup of a static island, materialised on the parent
40
+ * element in a single pass (browser-parsed `innerHTML` / cached `<template>`
41
+ * clone). Decodes HTML entities (e.g. `&nbsp;`) for free via the parser.
42
+ */
43
+ html: string;
44
+ }
45
+
37
46
  export type AreHtmlHideInstructionPayload = {
38
47
  /**
39
48
  * Optional explicit display value to restore when the element becomes