@praxisjs/runtime 0.2.15 → 0.2.17

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @praxisjs/runtime
2
2
 
3
+ ## 0.2.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 9aeec6a: Optimize DOM patching in the runtime for lower allocation and fewer reflows.
8
+
9
+ **element.ts** — prop iteration now uses `for...in` instead of `Object.entries()`, avoiding the intermediate key-value array allocation per element mount.
10
+
11
+ **reactive.ts** — `normalizeToNodes` (returned a new array on every call) is replaced by `collectNodes` (accumulates into a caller-supplied array), removing one allocation per reactive update. Node insertion is batched through `nodesToFragment`: when multiple nodes are produced, they are appended to a `DocumentFragment` and inserted in a single `insertBefore` call. Bulk removal of existing nodes uses `Range.deleteContents()` instead of individual `removeChild` calls, reducing the number of reflow-triggering operations to one.
12
+
13
+ No changes to the public API.
14
+
15
+ - Updated dependencies [74d414a]
16
+ - @praxisjs/core@1.6.0
17
+ - @praxisjs/decorators@1.0.1
18
+
19
+ ## 0.2.16
20
+
21
+ ### Patch Changes
22
+
23
+ - bb4d00a: Fix `mountReactive` silently dropping DOM updates when the reactive node lives inside a component.
24
+
25
+ `mountReactive` captured `parent` (the container passed by `mountComponent`, a `DocumentFragment`) by closure. After `mountComponent` returns, the fragment's child nodes are transferred to the actual DOM element — leaving the fragment empty. When a signal change triggered the reactive effect to re-run, `parent.insertBefore(n, end)` failed silently because `end` was no longer a child of the stale `parent` reference.
26
+
27
+ The fix uses `end.parentNode ?? parent` as the insertion anchor, which always resolves to the live DOM parent after the fragment has been consumed. This affects any reactive expression (`{() => ...}`) returned from a component's `render()` method, including `@Lazy`, `@StateMachine` state-dependent renders, and the `onError` pattern from the docs.
28
+
29
+ - Updated dependencies [954e456]
30
+ - Updated dependencies [a0bf339]
31
+ - Updated dependencies [a0bf339]
32
+ - Updated dependencies [a8df1e1]
33
+ - @praxisjs/decorators@1.0.0
34
+
3
35
  ## 0.2.15
4
36
 
5
37
  ### Patch Changes
@@ -54,7 +86,6 @@
54
86
  - 6c353ba: Add `untrack` utility and isolate component mounting from outer reactive contexts
55
87
 
56
88
  **`@praxisjs/core`** exports two new functions from the public API:
57
-
58
89
  - `peek(signal)` — reads a signal once without subscribing to it (was already in `/internal`, now public)
59
90
  - `untrack(fn)` — runs a function with no active effect, suppressing all signal tracking inside it
60
91
 
@@ -71,7 +102,6 @@
71
102
  ```
72
103
 
73
104
  **`@praxisjs/runtime`** — `mountComponent` now runs entirely inside `untrack`. This fixes a bug where components mounted inside a reactive context (e.g. the router) would accidentally subscribe the outer effect to any signal read during construction or render. The symptoms were:
74
-
75
105
  - Eager reads like `description={this.count}` in JSX causing the router to re-mount the component on every state change, resetting state to its initial value
76
106
  - `@Debug()` (and any decorator that reads a signal in its `addInitializer`) triggering the same re-mount loop
77
107
 
@@ -155,7 +185,6 @@
155
185
  ### Minor Changes
156
186
 
157
187
  - bb0d4f8: **Refactor decorator system and component architecture across PraxisJS packages**
158
-
159
188
  - Replaced legacy decorator signatures (`constructor`, `target`, `propertyKey`, method descriptor) with the standard TC39 decorator context API (`ClassDecoratorContext`, `ClassFieldDecoratorContext`, `ClassMethodDecoratorContext`) across `@praxisjs/decorators`, `@praxisjs/store`, `@praxisjs/concurrent`, `@praxisjs/router`, `@praxisjs/motion`, `@praxisjs/di`, and `@praxisjs/fsm`.
160
189
  - Introduced `StatefulComponent` and `StatelessComponent` as the new base classes, replacing the deprecated `BaseComponent`/`Function Component` pattern, across `@praxisjs/core`, `@praxisjs/runtime`, `@praxisjs/devtools`, and templates.
161
190
  - Implemented core rendering functionality in `@praxisjs/runtime` (`mountChildren`, `mountComponent`, reactive scope management) and removed the deprecated `renderer.ts`.
package/dist/element.js CHANGED
@@ -3,10 +3,10 @@ import { createElement } from "./dom/create";
3
3
  import { applyProp } from "./dom/props";
4
4
  export function mountElement(tag, props, scope) {
5
5
  const el = createElement(tag);
6
- for (const [key, val] of Object.entries(props)) {
6
+ for (const key in props) {
7
7
  if (key === "children")
8
8
  continue;
9
- applyProp(el, key, val, scope);
9
+ applyProp(el, key, props[key], scope);
10
10
  }
11
11
  if (props.children !== undefined) {
12
12
  mountChildren(el, props.children, scope);
@@ -1 +1 @@
1
- {"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,KAA8B,EAC9B,KAAY;IAEZ,MAAM,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAE9B,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,IAAI,GAAG,KAAK,UAAU;YAAE,SAAS;QACjC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,aAAa,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC"}
1
+ {"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,KAA8B,EAC9B,KAAY;IAEZ,MAAM,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAE9B,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,GAAG,KAAK,UAAU;YAAE,SAAS;QACjC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,aAAa,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAYrC,wBAAgB,aAAa,CAC3B,MAAM,EAAE,IAAI,EACZ,EAAE,EAAE,MAAM,OAAO,EACjB,WAAW,EAAE,KAAK,GACjB,IAAI,CA0BN"}
1
+ {"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAmBrC,wBAAgB,aAAa,CAC3B,MAAM,EAAE,IAAI,EACZ,EAAE,EAAE,MAAM,OAAO,EACjB,WAAW,EAAE,KAAK,GACjB,IAAI,CAoCN"}
package/dist/reactive.js CHANGED
@@ -1,32 +1,54 @@
1
1
  import { runInScope } from "./context";
2
- function normalizeToNodes(value) {
2
+ function collectNodes(value, out) {
3
3
  if (value === null || value === undefined || value === false)
4
- return [];
5
- if (value instanceof Node)
6
- return [value];
7
- if (Array.isArray(value))
8
- return value.flatMap(normalizeToNodes);
4
+ return;
5
+ if (value instanceof Node) {
6
+ out.push(value);
7
+ return;
8
+ }
9
+ if (Array.isArray(value)) {
10
+ for (const v of value)
11
+ collectNodes(v, out);
12
+ return;
13
+ }
9
14
  if (typeof value === "string" || typeof value === "number") {
10
- return [document.createTextNode(String(value))];
15
+ out.push(document.createTextNode(String(value)));
16
+ return;
11
17
  }
12
- return [];
18
+ }
19
+ function nodesToFragment(nodes) {
20
+ if (nodes.length === 1)
21
+ return nodes[0];
22
+ const fragment = document.createDocumentFragment();
23
+ for (const n of nodes)
24
+ fragment.appendChild(n);
25
+ return fragment;
13
26
  }
14
27
  export function mountReactive(parent, fn, parentScope) {
15
- const end = document.createComment("reactive");
28
+ const end = document.createComment("");
16
29
  parent.appendChild(end);
17
30
  let currentNodes = [];
18
31
  let childScope = parentScope.fork();
19
32
  parentScope.effect(() => {
20
33
  childScope.dispose();
21
34
  childScope = parentScope.fork();
22
- for (const n of currentNodes) {
23
- n.parentNode?.removeChild(n);
24
- }
25
- currentNodes = [];
26
35
  const result = runInScope(childScope, fn);
27
- const newNodes = normalizeToNodes(result);
28
- for (const n of newNodes) {
29
- parent.insertBefore(n, end);
36
+ const newNodes = [];
37
+ collectNodes(result, newNodes);
38
+ const anchor = end.parentNode ?? parent;
39
+ if (currentNodes.length > 0) {
40
+ if (currentNodes.length === 1) {
41
+ anchor.removeChild(currentNodes[0]);
42
+ }
43
+ else {
44
+ const range = document.createRange();
45
+ range.setStartBefore(currentNodes[0]);
46
+ range.setEndAfter(currentNodes[currentNodes.length - 1]);
47
+ range.deleteContents();
48
+ }
49
+ }
50
+ if (newNodes.length > 0) {
51
+ anchor.insertBefore(nodesToFragment(newNodes), end);
30
52
  }
31
53
  currentNodes = newNodes;
32
54
  });
@@ -1 +1 @@
1
- {"version":3,"file":"reactive.js","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAIvC,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,EAAE,CAAC;IACxE,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC3D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,MAAY,EACZ,EAAiB,EACjB,WAAkB;IAElB,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAExB,IAAI,YAAY,GAAW,EAAE,CAAC;IAC9B,IAAI,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;IAEpC,WAAW,CAAC,MAAM,CAAC,GAAG,EAAE;QACtB,UAAU,CAAC,OAAO,EAAE,CAAC;QACrB,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;QAEhC,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;YAC7B,CAAC,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;QACD,YAAY,GAAG,EAAE,CAAC;QAElB,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAE1C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,MAAM,CAAC,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9B,CAAC;QACD,YAAY,GAAG,QAAQ,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"reactive.js","sourceRoot":"","sources":["../src/reactive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAIvC,SAAS,YAAY,CAAC,KAAc,EAAE,GAAW;IAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO;IACrE,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IACvD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAAC,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,YAAY,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAAC,OAAO;IAAC,CAAC;IAClF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC3D,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACjD,OAAO;IACT,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,QAAQ,CAAC,sBAAsB,EAAE,CAAC;IACnD,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAC/C,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,MAAY,EACZ,EAAiB,EACjB,WAAkB;IAElB,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAExB,IAAI,YAAY,GAAW,EAAE,CAAC;IAC9B,IAAI,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;IAEpC,WAAW,CAAC,MAAM,CAAC,GAAG,EAAE;QACtB,UAAU,CAAC,OAAO,EAAE,CAAC;QACrB,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;QAEhC,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAW,EAAE,CAAC;QAC5B,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAE/B,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,IAAI,MAAM,CAAC;QAExC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC7B,MAAkB,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;gBACrC,KAAK,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;gBACtC,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;gBACzD,KAAK,CAAC,cAAc,EAAE,CAAC;YACzB,CAAC;QACH,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC;QACtD,CAAC;QAED,YAAY,GAAG,QAAQ,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/runtime",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,9 +14,9 @@
14
14
  "typescript": "^5.9.3"
15
15
  },
16
16
  "dependencies": {
17
- "@praxisjs/core": "1.5.0",
18
- "@praxisjs/decorators": "0.8.1",
19
- "@praxisjs/shared": "0.2.0"
17
+ "@praxisjs/core": "1.6.0",
18
+ "@praxisjs/shared": "0.2.0",
19
+ "@praxisjs/decorators": "1.0.1"
20
20
  },
21
21
  "scripts": {
22
22
  "build": "tsc",
package/src/element.ts CHANGED
@@ -11,9 +11,9 @@ export function mountElement(
11
11
  ): HTMLElement | SVGElement {
12
12
  const el = createElement(tag);
13
13
 
14
- for (const [key, val] of Object.entries(props)) {
14
+ for (const key in props) {
15
15
  if (key === "children") continue;
16
- applyProp(el, key, val, scope);
16
+ applyProp(el, key, props[key], scope);
17
17
  }
18
18
 
19
19
  if (props.children !== undefined) {
package/src/reactive.ts CHANGED
@@ -2,14 +2,21 @@ import { runInScope } from "./context";
2
2
 
3
3
  import type { Scope } from "./scope";
4
4
 
5
- function normalizeToNodes(value: unknown): Node[] {
6
- if (value === null || value === undefined || value === false) return [];
7
- if (value instanceof Node) return [value];
8
- if (Array.isArray(value)) return value.flatMap(normalizeToNodes);
5
+ function collectNodes(value: unknown, out: Node[]): void {
6
+ if (value === null || value === undefined || value === false) return;
7
+ if (value instanceof Node) { out.push(value); return; }
8
+ if (Array.isArray(value)) { for (const v of value) collectNodes(v, out); return; }
9
9
  if (typeof value === "string" || typeof value === "number") {
10
- return [document.createTextNode(String(value))];
10
+ out.push(document.createTextNode(String(value)));
11
+ return;
11
12
  }
12
- return [];
13
+ }
14
+
15
+ function nodesToFragment(nodes: Node[]): Node {
16
+ if (nodes.length === 1) return nodes[0];
17
+ const fragment = document.createDocumentFragment();
18
+ for (const n of nodes) fragment.appendChild(n);
19
+ return fragment;
13
20
  }
14
21
 
15
22
  export function mountReactive(
@@ -17,7 +24,7 @@ export function mountReactive(
17
24
  fn: () => unknown,
18
25
  parentScope: Scope,
19
26
  ): void {
20
- const end = document.createComment("reactive");
27
+ const end = document.createComment("");
21
28
  parent.appendChild(end);
22
29
 
23
30
  let currentNodes: Node[] = [];
@@ -27,17 +34,27 @@ export function mountReactive(
27
34
  childScope.dispose();
28
35
  childScope = parentScope.fork();
29
36
 
30
- for (const n of currentNodes) {
31
- n.parentNode?.removeChild(n);
32
- }
33
- currentNodes = [];
34
-
35
37
  const result = runInScope(childScope, fn);
36
- const newNodes = normalizeToNodes(result);
38
+ const newNodes: Node[] = [];
39
+ collectNodes(result, newNodes);
40
+
41
+ const anchor = end.parentNode ?? parent;
42
+
43
+ if (currentNodes.length > 0) {
44
+ if (currentNodes.length === 1) {
45
+ (anchor as Element).removeChild(currentNodes[0]);
46
+ } else {
47
+ const range = document.createRange();
48
+ range.setStartBefore(currentNodes[0]);
49
+ range.setEndAfter(currentNodes[currentNodes.length - 1]);
50
+ range.deleteContents();
51
+ }
52
+ }
37
53
 
38
- for (const n of newNodes) {
39
- parent.insertBefore(n, end);
54
+ if (newNodes.length > 0) {
55
+ anchor.insertBefore(nodesToFragment(newNodes), end);
40
56
  }
57
+
41
58
  currentNodes = newNodes;
42
59
  });
43
60