@hoosk/minisignals 1.0.4 → 1.0.5

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @hoosk/minisignals
2
2
 
3
- The framework-agnostic core of the `minisignals` library.
3
+ The framework-agnostic core of the `minisignals` library.
4
4
 
5
- This package provides the fundamental reactive primitives: `signal`, `effect`, and `computed`. It has zero dependencies and is built for maximum performance and a minimal footprint.
5
+ This package provides the fundamental reactive primitives: `signal`, `effect`, `computed`, `batch`, and `untracked`. It has zero dependencies and is built for maximum performance and a minimal footprint.
6
6
 
7
7
  ## Installation
8
8
 
@@ -13,7 +13,7 @@ npm install @hoosk/minisignals
13
13
  ## Basic Usage
14
14
 
15
15
  ```typescript
16
- import { signal, effect, computed } from '@hoosk/minisignals';
16
+ import { signal, effect, computed, batch, untracked } from '@hoosk/minisignals';
17
17
 
18
18
  const count = signal(0);
19
19
  const double = computed(() => count.value * 2);
@@ -23,6 +23,12 @@ effect(() => {
23
23
  });
24
24
 
25
25
  count.value = 1; // Logs: Count: 1, Double: 2
26
+
27
+ // Batch multiple writes into a single notification pass
28
+ batch(() => {
29
+ count.value = 10;
30
+ count.value = 20;
31
+ }); // Logs once: Count: 20, Double: 40
26
32
  ```
27
33
 
28
- For full documentation and advanced features (like Lazy Evaluation and Memory Safety), please refer to the [main repository README](../../README.md).
34
+ For full documentation and advanced features, please refer to the [main repository README](../../README.md).
@@ -1,5 +1,5 @@
1
- export interface Computed<T> {
2
- readonly value: T;
1
+ import { ReadonlySignal } from './signal';
2
+ export interface Computed<T> extends ReadonlySignal<T> {
3
3
  /**
4
4
  * Stops the internal effect from observing dependencies.
5
5
  * Call this when the computed signal is no longer needed to prevent memory leaks.
@@ -14,4 +14,3 @@ export interface Computed<T> {
14
14
  * @returns A read-only signal with a dispose method.
15
15
  */
16
16
  export declare function computed<T>(fn: () => T): Computed<T>;
17
- //# sourceMappingURL=computed.d.ts.map
package/dist/computed.js CHANGED
@@ -9,14 +9,16 @@ import { effect } from './effect';
9
9
  */
10
10
  export function computed(fn) {
11
11
  let initialized = false;
12
- let internalSignal;
12
+ let internalSignal = null;
13
13
  let unsubscribe = null;
14
14
  let isDisposed = false;
15
15
  return {
16
16
  get value() {
17
17
  if (isDisposed) {
18
- if (!initialized)
19
- return fn();
18
+ if (!initialized) {
19
+ internalSignal = signal(fn());
20
+ initialized = true;
21
+ }
20
22
  return internalSignal.value;
21
23
  }
22
24
  if (!initialized) {
package/dist/effect.d.ts CHANGED
@@ -2,13 +2,12 @@ export type SubscriberSet = Set<() => void>;
2
2
  interface ActiveEffect {
3
3
  run: () => void;
4
4
  deps: Set<SubscriberSet>;
5
+ isRunning: boolean;
6
+ needsRerun: boolean;
5
7
  }
6
- export declare let activeEffect: ActiveEffect | null;
7
- /**
8
- * Creates an effect that runs immediately and tracks dependencies.
9
- * @param fn The function to execute.
10
- * @returns An unsubscribe function.
11
- */
8
+ export declare function getActiveEffect(): ActiveEffect | null;
9
+ export declare function scheduleSubs(subscribers: SubscriberSet): void;
10
+ export declare function batch<T>(fn: () => T): T;
11
+ export declare function untracked<T>(fn: () => T): T;
12
12
  export declare function effect(fn: () => void): () => void;
13
13
  export {};
14
- //# sourceMappingURL=effect.d.ts.map
package/dist/effect.js CHANGED
@@ -1,29 +1,96 @@
1
- export let activeEffect = null;
1
+ let _activeEffect = null;
2
+ export function getActiveEffect() {
3
+ return _activeEffect;
4
+ }
5
+ // --- Batch scheduling ---
6
+ let _batchDepth = 0;
7
+ const _pendingNotifications = new Set();
8
+ export function scheduleSubs(subscribers) {
9
+ if (_batchDepth > 0) {
10
+ for (const sub of subscribers) {
11
+ _pendingNotifications.add(sub);
12
+ }
13
+ }
14
+ else {
15
+ for (const sub of [...subscribers]) {
16
+ sub();
17
+ }
18
+ }
19
+ }
20
+ export function batch(fn) {
21
+ _batchDepth++;
22
+ try {
23
+ return fn();
24
+ }
25
+ finally {
26
+ _batchDepth--;
27
+ if (_batchDepth === 0) {
28
+ const pending = [..._pendingNotifications];
29
+ _pendingNotifications.clear();
30
+ for (const sub of pending) {
31
+ sub();
32
+ }
33
+ }
34
+ }
35
+ }
36
+ // --- Untracked read ---
37
+ export function untracked(fn) {
38
+ const previous = _activeEffect;
39
+ _activeEffect = null;
40
+ try {
41
+ return fn();
42
+ }
43
+ finally {
44
+ _activeEffect = previous;
45
+ }
46
+ }
47
+ // --- Effect ---
2
48
  /**
3
49
  * Creates an effect that runs immediately and tracks dependencies.
4
50
  * @param fn The function to execute.
5
51
  * @returns An unsubscribe function.
6
52
  */
53
+ const MAX_EFFECT_ITERATIONS = 100;
7
54
  export function effect(fn) {
8
55
  const effectObj = {
9
56
  run() {
10
- cleanup(effectObj);
11
- const previousEffect = activeEffect;
12
- activeEffect = effectObj;
57
+ if (effectObj.isRunning) {
58
+ effectObj.needsRerun = true;
59
+ return;
60
+ }
61
+ effectObj.isRunning = true;
62
+ let iterations = 0;
13
63
  try {
14
- fn();
64
+ do {
65
+ if (++iterations > MAX_EFFECT_ITERATIONS) {
66
+ throw new Error('Circular dependency detected: effect exceeded maximum re-run limit');
67
+ }
68
+ effectObj.needsRerun = false;
69
+ cleanup(effectObj);
70
+ const previousEffect = _activeEffect;
71
+ _activeEffect = effectObj;
72
+ try {
73
+ fn();
74
+ }
75
+ finally {
76
+ _activeEffect = previousEffect;
77
+ }
78
+ } while (effectObj.needsRerun);
15
79
  }
16
80
  finally {
17
- activeEffect = previousEffect;
81
+ effectObj.isRunning = false;
18
82
  }
19
83
  },
20
- deps: new Set()
84
+ deps: new Set(),
85
+ isRunning: false,
86
+ needsRerun: false,
21
87
  };
22
88
  function cleanup(eff) {
23
89
  for (const depSet of eff.deps) {
24
90
  depSet.delete(eff.run);
25
91
  }
26
92
  eff.deps.clear();
93
+ _pendingNotifications.delete(eff.run);
27
94
  }
28
95
  effectObj.run();
29
96
  return () => cleanup(effectObj);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from './signal';
2
2
  export * from './effect';
3
3
  export * from './computed';
4
- //# sourceMappingURL=index.d.ts.map
package/dist/signal.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- export interface Signal<T> {
1
+ export interface ReadonlySignal<T> {
2
+ readonly value: T;
3
+ }
4
+ export interface Signal<T> extends ReadonlySignal<T> {
2
5
  value: T;
3
6
  }
4
7
  /**
@@ -7,4 +10,3 @@ export interface Signal<T> {
7
10
  * @returns An object with a reactive .value property.
8
11
  */
9
12
  export declare function signal<T>(initialValue: T): Signal<T>;
10
- //# sourceMappingURL=signal.d.ts.map
package/dist/signal.js CHANGED
@@ -1,4 +1,4 @@
1
- import { activeEffect } from './effect';
1
+ import { getActiveEffect, scheduleSubs } from './effect';
2
2
  /**
3
3
  * Creates a reactive signal.
4
4
  * @param initialValue The starting value.
@@ -9,16 +9,17 @@ export function signal(initialValue) {
9
9
  let _value = initialValue;
10
10
  return {
11
11
  get value() {
12
- if (activeEffect) {
13
- subscribers.add(activeEffect.run);
14
- activeEffect.deps.add(subscribers);
12
+ const active = getActiveEffect();
13
+ if (active) {
14
+ subscribers.add(active.run);
15
+ active.deps.add(subscribers);
15
16
  }
16
17
  return _value;
17
18
  },
18
19
  set value(newValue) {
19
20
  if (_value !== newValue) {
20
21
  _value = newValue;
21
- [...subscribers].forEach(sub => sub());
22
+ scheduleSubs(subscribers);
22
23
  }
23
24
  }
24
25
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoosk/minisignals",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Minimal agnostic signals library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -1 +0,0 @@
1
- {"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../src/computed.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB;;;OAGG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAmCpD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"effect.d.ts","sourceRoot":"","sources":["../src/effect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;AAE5C,UAAU,YAAY;IACpB,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,IAAI,EAAE,GAAG,CAAC,aAAa,CAAC,CAAC;CAC1B;AAED,eAAO,IAAI,YAAY,EAAE,YAAY,GAAG,IAAW,CAAC;AAEpD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAyBjD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,CAAC;CACV;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAmBpD"}