@praxisjs/core 1.5.0 → 1.6.0

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,21 @@
1
1
  # @praxisjs/core
2
2
 
3
+ ## 1.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 74d414a: Rewrite the reactivity engine for clarity and performance.
8
+
9
+ **signal()** — subscribers are now stored in a compact `SubList` representation (`null | Effect | Effect[]`) that avoids `Set` allocation for the common zero- and single-subscriber cases. `set`, `update` and `subscribe` are defined as regular closures, making the signal factory straightforward to read and extend.
10
+
11
+ **computed()** — the factory now uses a plain closure instead of the previous property-on-function pattern. Downstream computeds (chain propagation) and leaf effects from `.subscribe()` are tracked in separate holders so dirty notification never needs to inspect subscriber types. The `recompute` callback is created lazily — only when the computed is first read inside a reactive context.
12
+
13
+ **effect()** — simplified to a standard closure with named `run` and `stop` functions. The `stopped` and `cleanup` state is held naturally in the closure scope.
14
+
15
+ **batch()** — uses a pre-allocated, module-level effects array that is reused across batch calls, eliminating one `Set` allocation per `batch()` invocation.
16
+
17
+ No breaking changes to the public API (`signal`, `computed`, `effect`, `batch`, `untrack`, `peek`).
18
+
3
19
  ## 1.5.0
4
20
 
5
21
  ### Minor Changes
@@ -62,7 +78,6 @@
62
78
  - 6c353ba: Add `untrack` utility and isolate component mounting from outer reactive contexts
63
79
 
64
80
  **`@praxisjs/core`** exports two new functions from the public API:
65
-
66
81
  - `peek(signal)` — reads a signal once without subscribing to it (was already in `/internal`, now public)
67
82
  - `untrack(fn)` — runs a function with no active effect, suppressing all signal tracking inside it
68
83
 
@@ -79,7 +94,6 @@
79
94
  ```
80
95
 
81
96
  **`@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:
82
-
83
97
  - 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
84
98
  - `@Debug()` (and any decorator that reads a signal in its `addInitializer`) triggering the same re-mount loop
85
99
 
@@ -103,39 +117,31 @@
103
117
  - 3372878: Migrate all packages from functional APIs to a decorator-first design.
104
118
 
105
119
  **`@praxisjs/core`**
106
-
107
120
  - Added `Composable` abstract base class for building class-based composables
108
121
  - Removed `resource`, `createResource`, `Resource`, `ResourceStatus`, `ResourceOptions` from public exports — use `@Resource` from `@praxisjs/decorators` instead
109
122
 
110
123
  **`@praxisjs/motion`**
111
-
112
124
  - Replaced `useMotion`, `tween`, `spring`, `createTransition`, `Animate`, `easings`, `resolveEasing` with `@Tween` and `@Spring` decorators
113
125
 
114
126
  **`@praxisjs/di`**
115
-
116
127
  - Replaced `useService` and `createScope` with a `@Scope` decorator
117
128
  - Renamed exported type `Scope` to `ScopeType` to free the name for the new decorator
118
129
 
119
130
  **`@praxisjs/fsm`**
120
-
121
131
  - Removed `createMachine` — use the `@StateMachine` and `@Transition` decorators directly
122
132
 
123
133
  **`@praxisjs/router`**
124
-
125
134
  - Removed `createRouter`, `lazy`, `useRouter`, `useParams`, `useQuery`, `useLocation`
126
135
  - Added `@RouterConfig`, `@Lazy`, `@InjectRouter`, `@Params`, `@Query`, `@Location` decorators
127
136
 
128
137
  **`@praxisjs/store`**
129
-
130
138
  - Removed `createStore` — use the `@Store` and `@UseStore` decorators directly
131
139
 
132
140
  **`@praxisjs/composables`**
133
-
134
141
  - Replaced all `use*` composable functions with class-based composables extending `Composable`:
135
142
  `WindowSize`, `ScrollPosition`, `ElementSize`, `Intersection`, `Focus`, `MediaQuery`, `ColorScheme`, `Mouse`, `KeyCombo`, `Idle`, `Clipboard`, `Geolocation`, `TimeAgo`, `Pagination`
136
143
 
137
144
  **`@praxisjs/concurrent`**
138
-
139
145
  - Removed `task`, `queue`, `pool` and their instance types — use `@Task`, `@Queue`, `@Pool` decorators instead
140
146
 
141
147
  ## 0.4.2
@@ -173,7 +179,6 @@
173
179
  ### Minor Changes
174
180
 
175
181
  - bb0d4f8: **Refactor decorator system and component architecture across PraxisJS packages**
176
-
177
182
  - 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`.
178
183
  - 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.
179
184
  - Implemented core rendering functionality in `@praxisjs/runtime` (`mountChildren`, `mountComponent`, reactive scope management) and removed the deprecated `renderer.ts`.
@@ -1 +1 @@
1
- {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAIvC,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAElD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,QAgBnC"}
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAKvC,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAElD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,QAWnC"}
@@ -1,25 +1,23 @@
1
- let batchQueue = null;
1
+ const batchEffects = [];
2
+ let batchDepth = 0;
2
3
  export function isBatching() {
3
- return batchQueue !== null;
4
+ return batchDepth > 0;
4
5
  }
5
6
  export function enqueueEffect(effect) {
6
- batchQueue?.add(effect);
7
+ if (!batchEffects.includes(effect))
8
+ batchEffects.push(effect);
7
9
  }
8
10
  export function batch(fn) {
9
- const isOuter = batchQueue === null;
10
- if (isOuter) {
11
- batchQueue = new Set();
12
- }
11
+ batchDepth++;
13
12
  try {
14
13
  fn();
15
14
  }
16
15
  finally {
17
- if (isOuter && batchQueue) {
18
- const effectsToRun = batchQueue;
19
- batchQueue = null;
20
- effectsToRun.forEach((eff) => {
21
- eff();
22
- });
16
+ if (--batchDepth === 0) {
17
+ const n = batchEffects.length;
18
+ for (let i = 0; i < n; i++)
19
+ batchEffects[i]();
20
+ batchEffects.length = 0;
23
21
  }
24
22
  }
25
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAEA,IAAI,UAAU,GAAuB,IAAI,CAAC;AAE1C,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,KAAK,IAAI,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAc;IAClC,MAAM,OAAO,GAAG,UAAU,KAAK,IAAI,CAAC;IACpC,IAAI,OAAO,EAAE,CAAC;QACZ,UAAU,GAAG,IAAI,GAAG,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,CAAC;QACH,EAAE,EAAE,CAAC;IACP,CAAC;YAAS,CAAC;QACT,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;YAC1B,MAAM,YAAY,GAAG,UAAU,CAAC;YAChC,UAAU,GAAG,IAAI,CAAC;YAClB,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC3B,GAAG,EAAE,CAAC;YACR,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAa,EAAE,CAAC;AAClC,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAc;IAClC,UAAU,EAAE,CAAC;IACb,IAAI,CAAC;QACH,EAAE,EAAE,CAAC;IACP,CAAC;YAAS,CAAC;QACT,IAAI,EAAE,UAAU,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC;YAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;gBAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9C,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -1,3 +1,3 @@
1
1
  import type { Computed } from "@praxisjs/shared";
2
- export declare function computed<T>(computeFn: () => T): Computed<T>;
2
+ export declare function computed<T>(getter: () => T): Computed<T>;
3
3
  //# sourceMappingURL=computed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAMjD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CA8D3D"}
1
+ {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAWjD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CA4DxD"}
@@ -1,37 +1,50 @@
1
1
  import { activeEffect, runEffect } from "./effect";
2
- export function computed(computeFn) {
2
+ import { addSub, removeSub, notifySubs } from "./signal";
3
+ function isComputedRecompute(eff) {
4
+ return eff.__isComputedRecompute === true;
5
+ }
6
+ export function computed(getter) {
3
7
  let cachedValue;
4
8
  let dirty = true;
5
- const subscribers = new Set();
9
+ const computedHolder = { subs: null };
10
+ const leafHolder = { subs: null };
6
11
  let scheduled = false;
7
- const recompute = Object.assign(() => {
8
- dirty = true;
9
- for (const sub of [...subscribers]) {
10
- if (sub.__isComputedRecompute) {
11
- sub(); // propagate dirty synchronously through computed chain
12
+ let recompute = null;
13
+ function getRecompute() {
14
+ if (recompute !== null)
15
+ return recompute;
16
+ recompute = function markDirty() {
17
+ if (dirty)
18
+ return;
19
+ dirty = true;
20
+ if (computedHolder.subs !== null)
21
+ notifySubs(computedHolder.subs, false);
22
+ if (leafHolder.subs !== null && !scheduled) {
23
+ scheduled = true;
24
+ queueMicrotask(() => {
25
+ scheduled = false;
26
+ if (leafHolder.subs !== null)
27
+ notifySubs(leafHolder.subs, false);
28
+ });
12
29
  }
13
- }
14
- if (!scheduled) {
15
- scheduled = true;
16
- queueMicrotask(() => {
17
- scheduled = false;
18
- for (const sub of [...subscribers]) {
19
- if (!sub.__isComputedRecompute) {
20
- sub(); // notify leaf effects (DOM, @Watch, subscribe)
21
- }
22
- }
23
- });
24
- }
25
- }, { __isComputedRecompute: true });
30
+ };
31
+ recompute.__isComputedRecompute = true;
32
+ return recompute;
33
+ }
26
34
  function read() {
27
- if (activeEffect) {
28
- subscribers.add(activeEffect);
35
+ if (activeEffect !== null) {
36
+ if (isComputedRecompute(activeEffect)) {
37
+ addSub(computedHolder, activeEffect);
38
+ }
39
+ else {
40
+ addSub(leafHolder, activeEffect);
41
+ }
29
42
  }
30
43
  if (dirty) {
31
44
  const prevEffect = activeEffect;
32
- runEffect(recompute);
45
+ runEffect(getRecompute());
33
46
  try {
34
- cachedValue = computeFn();
47
+ cachedValue = getter();
35
48
  dirty = false;
36
49
  }
37
50
  finally {
@@ -40,17 +53,14 @@ export function computed(computeFn) {
40
53
  }
41
54
  return cachedValue;
42
55
  }
43
- function subscribe(fn) {
44
- const wrappedEffect = () => {
45
- fn(read());
46
- };
47
- subscribers.add(wrappedEffect);
48
- wrappedEffect();
49
- return () => subscribers.delete(wrappedEffect);
50
- }
51
- const computedSignal = read;
52
- computedSignal.subscribe = subscribe;
53
- computedSignal.__isComputed = true;
54
- return computedSignal;
56
+ const c = read;
57
+ c.subscribe = (fn) => {
58
+ const wrapped = () => { fn(read()); };
59
+ addSub(leafHolder, wrapped);
60
+ wrapped();
61
+ return () => { removeSub(leafHolder, wrapped); };
62
+ };
63
+ c.__isComputed = true;
64
+ return c;
55
65
  }
56
66
  //# sourceMappingURL=computed.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"computed.js","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAe,MAAM,UAAU,CAAC;AAIhE,MAAM,UAAU,QAAQ,CAAI,SAAkB;IAC5C,IAAI,WAAc,CAAC;IACnB,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,MAAM,SAAS,GAAsB,MAAM,CAAC,MAAM,CAChD,GAAG,EAAE;QACH,KAAK,GAAG,IAAI,CAAC;QACb,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC;YACnC,IAAK,GAAkC,CAAC,qBAAqB,EAAE,CAAC;gBAC9D,GAAG,EAAE,CAAC,CAAC,uDAAuD;YAChE,CAAC;QACH,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,SAAS,GAAG,IAAI,CAAC;YACjB,cAAc,CAAC,GAAG,EAAE;gBAClB,SAAS,GAAG,KAAK,CAAC;gBAClB,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC;oBACnC,IAAI,CAAE,GAAkC,CAAC,qBAAqB,EAAE,CAAC;wBAC/D,GAAG,EAAE,CAAC,CAAC,+CAA+C;oBACxD,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,EACD,EAAE,qBAAqB,EAAE,IAAa,EAAE,CACzC,CAAC;IAEF,SAAS,IAAI;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,UAAU,GAAG,YAAY,CAAC;YAChC,SAAS,CAAC,SAAS,CAAC,CAAC;YACrB,IAAI,CAAC;gBACH,WAAW,GAAG,SAAS,EAAE,CAAC;gBAC1B,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;oBAAS,CAAC;gBACT,SAAS,CAAC,UAAU,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,SAAS,SAAS,CAAC,EAAsB;QACvC,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QACb,CAAC,CAAC;QACF,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC/B,aAAa,EAAE,CAAC;QAChB,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,cAAc,GAAG,IAAmB,CAAC;IAC3C,cAAc,CAAC,SAAS,GAAG,SAAS,CAAC;IACrC,cAAc,CAAC,YAAY,GAAG,IAAI,CAAC;IAEnC,OAAO,cAAc,CAAC;AACxB,CAAC"}
1
+ {"version":3,"file":"computed.js","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAe,MAAM,UAAU,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAgB,MAAM,UAAU,CAAC;AAIvE,SAAS,mBAAmB,CAAC,GAAW;IACtC,OAAQ,GAAkC,CAAC,qBAAqB,KAAK,IAAI,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,QAAQ,CAAI,MAAe;IACzC,IAAI,WAA0B,CAAC;IAC/B,IAAI,KAAK,GAAG,IAAI,CAAC;IAEjB,MAAM,cAAc,GAAG,EAAE,IAAI,EAAE,IAAe,EAAE,CAAC;IACjD,MAAM,UAAU,GAAG,EAAE,IAAI,EAAE,IAAe,EAAE,CAAC;IAC7C,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,IAAI,SAAS,GAA6B,IAAI,CAAC;IAE/C,SAAS,YAAY;QACnB,IAAI,SAAS,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QACzC,SAAS,GAAG,SAAS,SAAS;YAC5B,IAAI,KAAK;gBAAE,OAAO;YAClB,KAAK,GAAG,IAAI,CAAC;YACb,IAAI,cAAc,CAAC,IAAI,KAAK,IAAI;gBAAE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACzE,IAAI,UAAU,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBAC3C,SAAS,GAAG,IAAI,CAAC;gBACjB,cAAc,CAAC,GAAG,EAAE;oBAClB,SAAS,GAAG,KAAK,CAAC;oBAClB,IAAI,UAAU,CAAC,IAAI,KAAK,IAAI;wBAAE,UAAU,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBACnE,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAsB,CAAC;QACvB,SAAS,CAAC,qBAAqB,GAAG,IAAI,CAAC;QACvC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,SAAS,IAAI;QACX,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YAC1B,IAAI,mBAAmB,CAAC,YAAY,CAAC,EAAE,CAAC;gBACtC,MAAM,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;YACvC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QACD,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,UAAU,GAAG,YAAY,CAAC;YAChC,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;YAC1B,IAAI,CAAC;gBACH,WAAW,GAAG,MAAM,EAAE,CAAC;gBACvB,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;oBAAS,CAAC;gBACT,SAAS,CAAC,UAAU,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QACD,OAAO,WAAgB,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,GAAG,IAAmB,CAAC;IAE9B,CAAC,CAAC,SAAS,GAAG,CAAC,EAAsB,EAAE,EAAE;QACvC,MAAM,OAAO,GAAW,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC5B,OAAO,EAAE,CAAC;QACV,OAAO,GAAG,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC,CAAC;IAEF,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../../src/signal/effect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC;AAEhC,eAAO,IAAI,YAAY,EAAE,MAAM,GAAG,IAAW,CAAC;AAG9C,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,QAUnC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,QAE9C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQzC;AAGD,KAAK,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;AAEnC,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,OAAO,cAuBvC"}
1
+ {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../../src/signal/effect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC;AAEhC,eAAO,IAAI,YAAY,EAAE,MAAM,GAAG,IAAW,CAAC;AAG9C,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,QASnC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,QAE9C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQzC;AAGD,KAAK,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC;AAEnC,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,OAAO,SAkBd,IAAI,CAK7B"}
@@ -1,15 +1,14 @@
1
1
  export let activeEffect = null;
2
2
  const effectStack = [];
3
3
  export function track(effect) {
4
- const subscriber = effect;
5
- effectStack.push(subscriber);
6
- activeEffect = subscriber;
4
+ effectStack.push(effect);
5
+ activeEffect = effect;
7
6
  try {
8
- subscriber();
7
+ effect();
9
8
  }
10
9
  finally {
11
10
  effectStack.pop();
12
- activeEffect = effectStack[effectStack.length - 1] || null;
11
+ activeEffect = effectStack[effectStack.length - 1] ?? null;
13
12
  }
14
13
  }
15
14
  export function runEffect(effect) {
@@ -28,21 +27,21 @@ export function untrack(fn) {
28
27
  export function effect(fn) {
29
28
  let cleanup;
30
29
  let stopped = false;
31
- const wrappedEffect = () => {
30
+ function run() {
32
31
  if (stopped)
33
32
  return;
34
33
  cleanup?.();
35
- const prevEffect = activeEffect;
36
- activeEffect = wrappedEffect;
34
+ const prev = activeEffect;
35
+ activeEffect = run;
37
36
  try {
38
37
  cleanup = fn();
39
38
  }
40
39
  finally {
41
- activeEffect = prevEffect;
40
+ activeEffect = prev;
42
41
  }
43
- };
44
- wrappedEffect();
45
- return () => {
42
+ }
43
+ run();
44
+ return function stop() {
46
45
  stopped = true;
47
46
  cleanup?.();
48
47
  cleanup = undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/signal/effect.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,IAAI,YAAY,GAAkB,IAAI,CAAC;AAC9C,MAAM,WAAW,GAAa,EAAE,CAAC;AAEjC,MAAM,UAAU,KAAK,CAAC,MAAc;IAClC,MAAM,UAAU,GAAG,MAAM,CAAC;IAC1B,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,YAAY,GAAG,UAAU,CAAC;IAC1B,IAAI,CAAC;QACH,UAAU,EAAE,CAAC;IACf,CAAC;YAAS,CAAC;QACT,WAAW,CAAC,GAAG,EAAE,CAAC;QAClB,YAAY,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;IAC7D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAqB;IAC7C,YAAY,GAAG,MAAM,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,OAAO,CAAI,EAAW;IACpC,MAAM,IAAI,GAAG,YAAY,CAAC;IAC1B,YAAY,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,GAAG,IAAI,CAAC;IACtB,CAAC;AACH,CAAC;AAKD,MAAM,UAAU,MAAM,CAAC,EAAiB;IACtC,IAAI,OAAgB,CAAC;IACrB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,MAAM,aAAa,GAAG,GAAG,EAAE;QACzB,IAAI,OAAO;YAAE,OAAO;QACpB,OAAO,EAAE,EAAE,CAAC;QACZ,MAAM,UAAU,GAAG,YAAY,CAAC;QAChC,YAAY,GAAG,aAAa,CAAC;QAC7B,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,EAAE,CAAC;QACjB,CAAC;gBAAS,CAAC;YACT,YAAY,GAAG,UAAU,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC;IAEF,aAAa,EAAE,CAAC;IAEhB,OAAO,GAAG,EAAE;QACV,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,EAAE,EAAE,CAAC;QACZ,OAAO,GAAG,SAAS,CAAC;IACtB,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"effect.js","sourceRoot":"","sources":["../../src/signal/effect.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,IAAI,YAAY,GAAkB,IAAI,CAAC;AAC9C,MAAM,WAAW,GAAa,EAAE,CAAC;AAEjC,MAAM,UAAU,KAAK,CAAC,MAAc;IAClC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,GAAG,MAAM,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,EAAE,CAAC;IACX,CAAC;YAAS,CAAC;QACT,WAAW,CAAC,GAAG,EAAE,CAAC;QAClB,YAAY,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;IAC7D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAqB;IAC7C,YAAY,GAAG,MAAM,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,OAAO,CAAI,EAAW;IACpC,MAAM,IAAI,GAAG,YAAY,CAAC;IAC1B,YAAY,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,GAAG,IAAI,CAAC;IACtB,CAAC;AACH,CAAC;AAKD,MAAM,UAAU,MAAM,CAAC,EAAiB;IACtC,IAAI,OAAgB,CAAC;IACrB,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,SAAS,GAAG;QACV,IAAI,OAAO;YAAE,OAAO;QACpB,OAAO,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,YAAY,CAAC;QAC1B,YAAY,GAAG,GAAG,CAAC;QACnB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,EAAE,CAAC;QACjB,CAAC;gBAAS,CAAC;YACT,YAAY,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;IAED,GAAG,EAAE,CAAC;IAEN,OAAO,SAAS,IAAI;QAClB,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,EAAE,EAAE,CAAC;QACZ,OAAO,GAAG,SAAS,CAAC;IACtB,CAAC,CAAC;AACJ,CAAC"}
@@ -1,3 +1,12 @@
1
1
  import type { Signal } from "@praxisjs/shared";
2
+ import { type Effect } from "./effect";
3
+ export type SubList = Effect | Effect[] | null;
4
+ export declare function addSub(holder: {
5
+ subs: SubList;
6
+ }, eff: Effect): void;
7
+ export declare function removeSub(holder: {
8
+ subs: SubList;
9
+ }, eff: Effect): void;
10
+ export declare function notifySubs(subs: Exclude<SubList, null>, batching: boolean): void;
2
11
  export declare function signal<T>(initialValue: T): Signal<T>;
3
12
  //# sourceMappingURL=signal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAK/C,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAiDpD"}
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAErD,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;AAE/C,wBAAgB,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CASnE;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAStE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAoBhF;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CA4BpD"}
@@ -1,50 +1,84 @@
1
1
  import { isBatching, enqueueEffect } from "./batch";
2
2
  import { activeEffect } from "./effect";
3
+ export function addSub(holder, eff) {
4
+ const subs = holder.subs;
5
+ if (subs === null) {
6
+ holder.subs = eff;
7
+ }
8
+ else if (typeof subs === "function") {
9
+ if (subs !== eff)
10
+ holder.subs = [subs, eff];
11
+ }
12
+ else {
13
+ if (!subs.includes(eff))
14
+ subs.push(eff);
15
+ }
16
+ }
17
+ export function removeSub(holder, eff) {
18
+ const subs = holder.subs;
19
+ if (subs === null)
20
+ return;
21
+ if (typeof subs === "function") {
22
+ if (subs === eff)
23
+ holder.subs = null;
24
+ }
25
+ else {
26
+ const idx = subs.indexOf(eff);
27
+ if (idx >= 0)
28
+ subs.splice(idx, 1);
29
+ }
30
+ }
31
+ export function notifySubs(subs, batching) {
32
+ if (typeof subs === "function") {
33
+ if (batching)
34
+ enqueueEffect(subs);
35
+ else
36
+ subs();
37
+ return;
38
+ }
39
+ if (batching) {
40
+ for (const sub of subs)
41
+ enqueueEffect(sub);
42
+ return;
43
+ }
44
+ let lastError;
45
+ let hasError = false;
46
+ for (const sub of subs) {
47
+ try {
48
+ sub();
49
+ }
50
+ catch (e) {
51
+ lastError = e;
52
+ hasError = true;
53
+ }
54
+ }
55
+ if (hasError)
56
+ throw lastError;
57
+ }
3
58
  export function signal(initialValue) {
4
59
  let value = initialValue;
5
- const subscribers = new Set();
60
+ const holder = { subs: null };
6
61
  function read() {
7
- if (activeEffect) {
8
- subscribers.add(activeEffect);
9
- }
62
+ if (activeEffect !== null)
63
+ addSub(holder, activeEffect);
10
64
  return value;
11
65
  }
12
- function set(newValue) {
66
+ const s = read;
67
+ s.set = (newValue) => {
13
68
  if (Object.is(value, newValue))
14
69
  return;
15
70
  value = newValue;
16
- if (isBatching()) {
17
- [...subscribers].forEach((sub) => { enqueueEffect(sub); });
18
- return;
19
- }
20
- const errors = [];
21
- [...subscribers].forEach((sub) => {
22
- try {
23
- sub();
24
- }
25
- catch (e) {
26
- errors.push(e);
27
- }
28
- });
29
- if (errors.length > 0)
30
- throw errors[errors.length - 1];
31
- }
32
- function update(fn) {
33
- set(fn(value));
34
- }
35
- function subscribe(fn) {
36
- const wrapped = () => {
37
- fn(value);
38
- };
39
- subscribers.add(wrapped);
71
+ if (holder.subs !== null)
72
+ notifySubs(holder.subs, isBatching());
73
+ };
74
+ s.update = (fn) => { s.set(fn(value)); };
75
+ s.subscribe = (fn) => {
76
+ const wrapped = () => { fn(value); };
77
+ addSub(holder, wrapped);
40
78
  wrapped();
41
- return () => subscribers.delete(wrapped);
42
- }
43
- const signal = read;
44
- signal.set = set;
45
- signal.update = update;
46
- signal.subscribe = subscribe;
47
- signal.__isSignal = true;
48
- return signal;
79
+ return () => { removeSub(holder, wrapped); };
80
+ };
81
+ s.__isSignal = true;
82
+ return s;
49
83
  }
50
84
  //# sourceMappingURL=signal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;AAErD,MAAM,UAAU,MAAM,CAAI,YAAe;IACvC,IAAI,KAAK,GAAG,YAAY,CAAC;IACzB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,SAAS,IAAI;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS,GAAG,CAAC,QAAW;QACtB,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO;QACvC,KAAK,GAAG,QAAQ,CAAC;QACjB,IAAI,UAAU,EAAE,EAAE,CAAC;YACjB,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAc,EAAE,CAAC;QAC7B,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC/B,IAAI,CAAC;gBACH,GAAG,EAAE,CAAC;YACR,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,SAAS,MAAM,CAAC,EAAkB;QAChC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACjB,CAAC;IAED,SAAS,SAAS,CAAC,EAAsB;QACvC,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,EAAE,CAAC,KAAK,CAAC,CAAC;QACZ,CAAC,CAAC;QACF,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACzB,OAAO,EAAE,CAAC;QACV,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,MAAM,GAAG,IAAiB,CAAC;IACjC,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;IAEzB,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisjs/core",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,29 +1,25 @@
1
1
  import type { Effect } from "./effect";
2
2
 
3
- let batchQueue: Set<Effect> | null = null;
3
+ const batchEffects: Effect[] = [];
4
+ let batchDepth = 0;
4
5
 
5
6
  export function isBatching(): boolean {
6
- return batchQueue !== null;
7
+ return batchDepth > 0;
7
8
  }
8
9
 
9
10
  export function enqueueEffect(effect: Effect): void {
10
- batchQueue?.add(effect);
11
+ if (!batchEffects.includes(effect)) batchEffects.push(effect);
11
12
  }
12
13
 
13
14
  export function batch(fn: () => void) {
14
- const isOuter = batchQueue === null;
15
- if (isOuter) {
16
- batchQueue = new Set();
17
- }
15
+ batchDepth++;
18
16
  try {
19
17
  fn();
20
18
  } finally {
21
- if (isOuter && batchQueue) {
22
- const effectsToRun = batchQueue;
23
- batchQueue = null;
24
- effectsToRun.forEach((eff) => {
25
- eff();
26
- });
19
+ if (--batchDepth === 0) {
20
+ const n = batchEffects.length;
21
+ for (let i = 0; i < n; i++) batchEffects[i]();
22
+ batchEffects.length = 0;
27
23
  }
28
24
  }
29
25
  }
@@ -1,69 +1,72 @@
1
1
  import type { Computed } from "@praxisjs/shared";
2
2
 
3
3
  import { activeEffect, runEffect, type Effect } from "./effect";
4
+ import { addSub, removeSub, notifySubs, type SubList } from "./signal";
4
5
 
5
6
  type ComputedRecompute = Effect & { __isComputedRecompute: true };
6
7
 
7
- export function computed<T>(computeFn: () => T): Computed<T> {
8
- let cachedValue: T;
8
+ function isComputedRecompute(eff: Effect): boolean {
9
+ return (eff as Partial<ComputedRecompute>).__isComputedRecompute === true;
10
+ }
11
+
12
+ export function computed<T>(getter: () => T): Computed<T> {
13
+ let cachedValue: T | undefined;
9
14
  let dirty = true;
10
- const subscribers = new Set<Effect>();
15
+
16
+ const computedHolder = { subs: null as SubList };
17
+ const leafHolder = { subs: null as SubList };
11
18
  let scheduled = false;
12
19
 
13
- const recompute: ComputedRecompute = Object.assign(
14
- () => {
20
+ let recompute: ComputedRecompute | null = null;
21
+
22
+ function getRecompute(): ComputedRecompute {
23
+ if (recompute !== null) return recompute;
24
+ recompute = function markDirty(): void {
25
+ if (dirty) return;
15
26
  dirty = true;
16
- for (const sub of [...subscribers]) {
17
- if ((sub as Partial<ComputedRecompute>).__isComputedRecompute) {
18
- sub(); // propagate dirty synchronously through computed chain
19
- }
20
- }
21
- if (!scheduled) {
27
+ if (computedHolder.subs !== null) notifySubs(computedHolder.subs, false);
28
+ if (leafHolder.subs !== null && !scheduled) {
22
29
  scheduled = true;
23
30
  queueMicrotask(() => {
24
31
  scheduled = false;
25
- for (const sub of [...subscribers]) {
26
- if (!(sub as Partial<ComputedRecompute>).__isComputedRecompute) {
27
- sub(); // notify leaf effects (DOM, @Watch, subscribe)
28
- }
29
- }
32
+ if (leafHolder.subs !== null) notifySubs(leafHolder.subs, false);
30
33
  });
31
34
  }
32
- },
33
- { __isComputedRecompute: true as const },
34
- );
35
+ } as ComputedRecompute;
36
+ recompute.__isComputedRecompute = true;
37
+ return recompute;
38
+ }
35
39
 
36
- function read() {
37
- if (activeEffect) {
38
- subscribers.add(activeEffect);
40
+ function read(): T {
41
+ if (activeEffect !== null) {
42
+ if (isComputedRecompute(activeEffect)) {
43
+ addSub(computedHolder, activeEffect);
44
+ } else {
45
+ addSub(leafHolder, activeEffect);
46
+ }
39
47
  }
40
-
41
48
  if (dirty) {
42
49
  const prevEffect = activeEffect;
43
- runEffect(recompute);
50
+ runEffect(getRecompute());
44
51
  try {
45
- cachedValue = computeFn();
52
+ cachedValue = getter();
46
53
  dirty = false;
47
54
  } finally {
48
55
  runEffect(prevEffect);
49
56
  }
50
57
  }
51
-
52
- return cachedValue;
58
+ return cachedValue as T;
53
59
  }
54
60
 
55
- function subscribe(fn: (value: T) => void) {
56
- const wrappedEffect = () => {
57
- fn(read());
58
- };
59
- subscribers.add(wrappedEffect);
60
- wrappedEffect();
61
- return () => subscribers.delete(wrappedEffect);
62
- }
61
+ const c = read as Computed<T>;
63
62
 
64
- const computedSignal = read as Computed<T>;
65
- computedSignal.subscribe = subscribe;
66
- computedSignal.__isComputed = true;
63
+ c.subscribe = (fn: (value: T) => void) => {
64
+ const wrapped: Effect = () => { fn(read()); };
65
+ addSub(leafHolder, wrapped);
66
+ wrapped();
67
+ return () => { removeSub(leafHolder, wrapped); };
68
+ };
67
69
 
68
- return computedSignal;
70
+ c.__isComputed = true;
71
+ return c;
69
72
  }
@@ -4,14 +4,13 @@ export let activeEffect: Effect | null = null;
4
4
  const effectStack: Effect[] = [];
5
5
 
6
6
  export function track(effect: Effect) {
7
- const subscriber = effect;
8
- effectStack.push(subscriber);
9
- activeEffect = subscriber;
7
+ effectStack.push(effect);
8
+ activeEffect = effect;
10
9
  try {
11
- subscriber();
10
+ effect();
12
11
  } finally {
13
12
  effectStack.pop();
14
- activeEffect = effectStack[effectStack.length - 1] || null;
13
+ activeEffect = effectStack[effectStack.length - 1] ?? null;
15
14
  }
16
15
  }
17
16
 
@@ -36,21 +35,21 @@ export function effect(fn: () => Cleanup) {
36
35
  let cleanup: Cleanup;
37
36
  let stopped = false;
38
37
 
39
- const wrappedEffect = () => {
38
+ function run(): void {
40
39
  if (stopped) return;
41
40
  cleanup?.();
42
- const prevEffect = activeEffect;
43
- activeEffect = wrappedEffect;
41
+ const prev = activeEffect;
42
+ activeEffect = run;
44
43
  try {
45
44
  cleanup = fn();
46
45
  } finally {
47
- activeEffect = prevEffect;
46
+ activeEffect = prev;
48
47
  }
49
- };
48
+ }
50
49
 
51
- wrappedEffect();
50
+ run();
52
51
 
53
- return () => {
52
+ return function stop(): void {
54
53
  stopped = true;
55
54
  cleanup?.();
56
55
  cleanup = undefined;
@@ -3,53 +3,78 @@ import type { Signal } from "@praxisjs/shared";
3
3
  import { isBatching, enqueueEffect } from "./batch";
4
4
  import { activeEffect, type Effect } from "./effect";
5
5
 
6
+ export type SubList = Effect | Effect[] | null;
7
+
8
+ export function addSub(holder: { subs: SubList }, eff: Effect): void {
9
+ const subs = holder.subs;
10
+ if (subs === null) {
11
+ holder.subs = eff;
12
+ } else if (typeof subs === "function") {
13
+ if (subs !== eff) holder.subs = [subs, eff];
14
+ } else {
15
+ if (!subs.includes(eff)) subs.push(eff);
16
+ }
17
+ }
18
+
19
+ export function removeSub(holder: { subs: SubList }, eff: Effect): void {
20
+ const subs = holder.subs;
21
+ if (subs === null) return;
22
+ if (typeof subs === "function") {
23
+ if (subs === eff) holder.subs = null;
24
+ } else {
25
+ const idx = subs.indexOf(eff);
26
+ if (idx >= 0) subs.splice(idx, 1);
27
+ }
28
+ }
29
+
30
+ export function notifySubs(subs: Exclude<SubList, null>, batching: boolean): void {
31
+ if (typeof subs === "function") {
32
+ if (batching) enqueueEffect(subs); else subs();
33
+ return;
34
+ }
35
+ if (batching) {
36
+ for (const sub of subs) enqueueEffect(sub);
37
+ return;
38
+ }
39
+ let lastError: unknown;
40
+ let hasError = false;
41
+ for (const sub of subs) {
42
+ try {
43
+ sub();
44
+ } catch (e) {
45
+ lastError = e;
46
+ hasError = true;
47
+ }
48
+ }
49
+ if (hasError) throw lastError;
50
+ }
51
+
6
52
  export function signal<T>(initialValue: T): Signal<T> {
7
53
  let value = initialValue;
8
- const subscribers = new Set<Effect>();
54
+ const holder = { subs: null as SubList };
9
55
 
10
- function read() {
11
- if (activeEffect) {
12
- subscribers.add(activeEffect);
13
- }
56
+ function read(): T {
57
+ if (activeEffect !== null) addSub(holder, activeEffect);
14
58
  return value;
15
59
  }
16
60
 
17
- function set(newValue: T) {
61
+ const s = read as Signal<T>;
62
+
63
+ s.set = (newValue: T) => {
18
64
  if (Object.is(value, newValue)) return;
19
65
  value = newValue;
20
- if (isBatching()) {
21
- [...subscribers].forEach((sub) => { enqueueEffect(sub); });
22
- return;
23
- }
24
- const errors: unknown[] = [];
25
- [...subscribers].forEach((sub) => {
26
- try {
27
- sub();
28
- } catch (e) {
29
- errors.push(e);
30
- }
31
- });
32
- if (errors.length > 0) throw errors[errors.length - 1];
33
- }
66
+ if (holder.subs !== null) notifySubs(holder.subs, isBatching());
67
+ };
34
68
 
35
- function update(fn: (prev: T) => T) {
36
- set(fn(value));
37
- }
69
+ s.update = (fn: (prev: T) => T) => { s.set(fn(value)); };
38
70
 
39
- function subscribe(fn: (value: T) => void) {
40
- const wrapped = () => {
41
- fn(value);
42
- };
43
- subscribers.add(wrapped);
71
+ s.subscribe = (fn: (value: T) => void) => {
72
+ const wrapped: Effect = () => { fn(value); };
73
+ addSub(holder, wrapped);
44
74
  wrapped();
45
- return () => subscribers.delete(wrapped);
46
- }
47
-
48
- const signal = read as Signal<T>;
49
- signal.set = set;
50
- signal.update = update;
51
- signal.subscribe = subscribe;
52
- signal.__isSignal = true;
75
+ return () => { removeSub(holder, wrapped); };
76
+ };
53
77
 
54
- return signal;
78
+ s.__isSignal = true;
79
+ return s;
55
80
  }