@zakkster/lite-signal 1.0.5 → 1.0.6

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/Signal.d.ts CHANGED
@@ -165,3 +165,37 @@ export function batch<T>(fn: () => T): T;
165
165
  export function untrack<T>(fn: () => T): T;
166
166
  export function onCleanup(fn: () => void): void;
167
167
  export function stats(): RegistryStats;
168
+
169
+
170
+ /**
171
+ * Configuration options for the watch utility.
172
+ */
173
+ export interface WatchOptions {
174
+ /** * If true, fires the callback immediately upon registration
175
+ * with `oldValue` set to `undefined`.
176
+ */
177
+ immediate?: boolean;
178
+ }
179
+
180
+ /**
181
+ * Track a reactive source and run a callback whenever its evaluated value changes.
182
+ *
183
+ * Models Vue's `watch(source, callback)` and MobX's `reaction(predicate, effect)`.
184
+ * Internal reads inside the callback are untracked — they do not create reactive
185
+ * dependencies.
186
+ *
187
+ * @example
188
+ * const count = signal(0);
189
+ * const stop = watch(() => count() * 2, (next, prev) => {
190
+ * console.log(`Doubled count changed: ${prev} -> ${next}`);
191
+ * });
192
+ * * @param source A function that reads reactive values (e.g., a signal/computed getter).
193
+ * @param callback Fired when the source's value changes. Receives the new and previous values.
194
+ * @param options Optional configuration (e.g., `{ immediate: true }`).
195
+ * @returns Dispose function — call to stop watching and release the effect.
196
+ */
197
+ export function watch<T>(
198
+ source: () => T,
199
+ callback: (newValue: T, oldValue: T | undefined) => void,
200
+ options?: WatchOptions
201
+ ): () => void;
package/Signal.js CHANGED
@@ -1004,4 +1004,10 @@ export function onCleanup(fn) {
1004
1004
  /** @type {Registry["stats"]} */
1005
1005
  export function stats() {
1006
1006
  return defaultRegistry.stats();
1007
- }
1007
+ }
1008
+
1009
+ /**
1010
+ * Re-export of the user-land watch utility.
1011
+ * @see {@link watch} in Watch.js for full implementation details.
1012
+ */
1013
+ export {watch} from "./Watch.js"
package/Watch.js ADDED
@@ -0,0 +1,72 @@
1
+ import { effect, untrack } from "./Signal.js";
2
+
3
+ /**
4
+ * Sentinel for "first run" — distinguishes a legitimate `undefined` source value
5
+ * from the uninitialized state. Using `Symbol` instead of `undefined` ensures a
6
+ * source like `signal(undefined)` correctly fires `callback(undefined, undefined)`
7
+ * on first change rather than being treated as never-changed.
8
+ * @private
9
+ */
10
+ const UNINITIALIZED = Symbol("watch.uninitialized");
11
+
12
+ /**
13
+ * Track a reactive source and run a callback whenever its value changes.
14
+ *
15
+ * Models Vue's `watch(source, callback)` and MobX's `reaction(predicate, effect)`.
16
+ * The callback is invoked with `(newValue, oldValue)`. Internal reads inside the
17
+ * callback are untracked — they don't create reactive dependencies — so a callback
18
+ * that reads other signals to perform a side-effect won't re-fire when those
19
+ * unrelated signals change.
20
+ *
21
+ * Disposing the returned function detaches the underlying effect and stops the
22
+ * watcher.
23
+ *
24
+ * @example
25
+ * const count = signal(0);
26
+ * const stop = watch(count, (next, prev) => {
27
+ * console.log(`count changed: ${prev} -> ${next}`);
28
+ * });
29
+ * count.set(1); // logs: "count changed: 0 -> 1"
30
+ * count.set(2); // logs: "count changed: 1 -> 2"
31
+ * stop();
32
+ * count.set(3); // no log
33
+ *
34
+ * @example
35
+ * // Immediate fires the callback once on registration with `oldValue = undefined`
36
+ * watch(count, (next, prev) => console.log(next), { immediate: true });
37
+ *
38
+ * @param {() => T} source A function that reads reactive values (typically a
39
+ * signal/computed getter, or a closure combining several).
40
+ * @param {(newValue: T, oldValue: T | undefined) => void} callback
41
+ * Called when the source's value changes. Receives the
42
+ * new and previous values. Internal reads are untracked.
43
+ * @param {{ immediate?: boolean }} [options]
44
+ * `immediate: true` runs the callback once on registration
45
+ * with `oldValue = undefined`. Defaults to false.
46
+ * @returns {() => void} Dispose function — call to stop watching.
47
+ * @template T
48
+ */
49
+ export function watch(source, callback, options) {
50
+ const immediate = options !== undefined && options.immediate === true;
51
+ let oldValue = UNINITIALIZED;
52
+
53
+ return effect(() => {
54
+ // Track the source — this read registers the dependency.
55
+ const newValue = source();
56
+
57
+ // Invoke the callback without registering further dependencies.
58
+ untrack(() => {
59
+ if (oldValue === UNINITIALIZED) {
60
+ if (immediate) callback(newValue, undefined);
61
+ } else if (!Object.is(newValue, oldValue)) {
62
+ // Guard for raw inline getters: the effect re-runs whenever any read
63
+ // dep changes, but the projected source value may be unchanged. Vue's
64
+ // `watch` and MobX's `reaction` both short-circuit here. Wrapping the
65
+ // source in a `computed` would also suppress this via the equality
66
+ // check inside computed itself; the guard makes that wrapping optional.
67
+ callback(newValue, oldValue);
68
+ }
69
+ oldValue = newValue;
70
+ });
71
+ });
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-signal",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Zero-GC reactive graph. Monomorphic object pool, versioned push-pull propagation, 32-bit modular versioning. Built for hot paths and long-running processes.",
5
5
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
6
6
  "license": "MIT",
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "Signal.js",
20
20
  "Signal.d.ts",
21
+ "Watch.js",
21
22
  "README.md",
22
23
  "llms.txt",
23
24
  "LICENSE.txt"