@xmachines/play-signals 1.0.0-beta.1

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 ADDED
@@ -0,0 +1,109 @@
1
+ # @xmachines/play-signals
2
+
3
+ **Canonical Signals substrate for XMachines with Stage 1 API isolation**
4
+
5
+ `@xmachines/play-signals` re-exports `Signal` from `signal-polyfill` as the single import boundary for XMachines packages.
6
+
7
+ ## Why This Package Exists
8
+
9
+ - Keep the raw `Signal` API as the canonical substrate surface.
10
+ - Isolate Stage 1 proposal churn behind one package boundary.
11
+ - Preserve Play invariants: Signal-only reactivity, passive infrastructure, and event-only mutation paths.
12
+
13
+ This package does not add business behavior to signals. Adapters and renderers observe signals and forward events; they do not mutate business state directly.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @xmachines/play-signals
19
+ ```
20
+
21
+ ## Current Exports
22
+
23
+ - `Signal` (re-export from `signal-polyfill`)
24
+ - Type exports from `src/types.ts`: `SignalState`, `SignalComputed`, `SignalWatcher`, `SignalOptions`, `ComputedOptions`, `WatcherNotify`
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { Signal } from "@xmachines/play-signals";
30
+
31
+ const count = new Signal.State(0);
32
+ const doubled = new Signal.Computed(() => count.get() * 2);
33
+
34
+ const watcher = new Signal.subtle.Watcher(() => {
35
+ queueMicrotask(() => {
36
+ const pending = watcher.getPending();
37
+ for (const signal of pending) {
38
+ signal.get();
39
+ }
40
+ watcher.watch(...pending);
41
+ });
42
+ });
43
+
44
+ watcher.watch(doubled);
45
+ doubled.get();
46
+
47
+ count.set(2);
48
+
49
+ const dispose = () => {
50
+ watcher.unwatch(doubled);
51
+ };
52
+
53
+ void dispose;
54
+ ```
55
+
56
+ ## Canonical Watcher Lifecycle
57
+
58
+ Use one lifecycle pattern everywhere (React, Vue, Solid, router bridges, helper wrappers):
59
+
60
+ 1. `notify` callback runs.
61
+ 2. Schedule work with `queueMicrotask`.
62
+ 3. Drain `watcher.getPending()`.
63
+ 4. Perform reads/effects.
64
+ 5. Re-arm watcher with `watch()` or `watch(...signals)`.
65
+
66
+ Watcher notifications are one-shot. If you do not re-arm, you will miss future updates.
67
+
68
+ ## Cleanup Contract
69
+
70
+ Always dispose explicitly. Do not rely on GC-only cleanup guidance.
71
+
72
+ - If you called `watch(...)`, call `unwatch(...)` in teardown.
73
+ - Framework lifecycles (`useEffect` cleanup, `onUnmounted`, `onCleanup`) must unwatch.
74
+ - Bridge lifecycles (`disconnect`, `dispose`) must unwatch and unsubscribe.
75
+
76
+ ## Optional Helper Direction
77
+
78
+ Raw `Signal` remains canonical. Helper APIs are optional, additive guidance for consistency:
79
+
80
+ - `watchSignals(signals, onChange, options)`
81
+ - `createSignalEffect(effect, options)`
82
+ - `toSubscribable(signal, options)`
83
+
84
+ These helpers are intended to codify lifecycle-safe watcher scheduling and deterministic teardown. They do not replace direct `Signal` usage.
85
+
86
+ ## API Surface
87
+
88
+ - `Signal.State<T>`: writable signal state (`get`, `set`)
89
+ - `Signal.Computed<T>`: lazy memoized derivations
90
+ - `Signal.subtle.Watcher`: low-level watcher (`watch`, `unwatch`, `getPending`)
91
+
92
+ Complete generated API docs: [docs/api/@xmachines/play-signals](../../docs/api/@xmachines/play-signals)
93
+
94
+ ## Architecture Notes
95
+
96
+ - **Signal-Only Reactivity (INV-05):** Signals are the reactive substrate.
97
+ - **Passive Infrastructure (INV-04):** Adapters and frameworks only observe/forward.
98
+ - **Actor Authority (INV-01):** Business validity and transitions stay in actors.
99
+ - **Event-only mutation path:** Signals are not a business mutation channel.
100
+
101
+ ## Resources
102
+
103
+ - [TC39 Signals Proposal](https://github.com/tc39/proposal-signals)
104
+ - [signal-polyfill](https://github.com/proposal-signals/signal-polyfill)
105
+ - [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md)
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TC39 Signals Polyfill for XMachines Play Architecture
3
+ *
4
+ * Provides fine-grained reactive state primitives based on the TC39 Signals proposal (Stage 1).
5
+ * This package isolates the TC39 polyfill to protect the codebase from Stage 1 API changes.
6
+ *
7
+ * **Architectural Context:** Implements **Signal-Only Reactivity (INV-05)** by providing
8
+ * the reactive primitives that enable Actor-to-Infrastructure communication without
9
+ * subscriptions or event emitters. All state propagation in Play Architecture uses
10
+ * TC39 Signals for automatic dependency tracking and glitch-free updates.
11
+ *
12
+ * @packageDocumentation
13
+ * @module @xmachines/play-signals
14
+ *
15
+ * @example
16
+ * Basic Signal usage
17
+ * ```typescript
18
+ * import { Signal } from "@xmachines/play-signals";
19
+ *
20
+ * // Create state signal
21
+ * const count = new Signal.State(0);
22
+ *
23
+ * // Create computed signal
24
+ * const doubled = new Signal.Computed(() => count.get() * 2);
25
+ *
26
+ * // Observe changes
27
+ * const watcher = new Signal.subtle.Watcher(() => {
28
+ * console.log('Count:', count.get(), 'Doubled:', doubled.get());
29
+ * });
30
+ * watcher.watch(count);
31
+ *
32
+ * count.set(5); // Logs: Count: 5 Doubled: 10
33
+ * ```
34
+ *
35
+ * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1 - Invariant INV-05}
36
+ * @see {@link https://github.com/tc39/proposal-signals | TC39 Signals Proposal}
37
+ *
38
+ * @remarks
39
+ * **Stage 1 Status:** TC39 Signals is currently Stage 1 in the TC39 process. This package
40
+ * uses the official `signal-polyfill` reference implementation to isolate the codebase
41
+ * from potential API changes as the proposal evolves. All signal imports should go through
42
+ * this package to maintain isolation.
43
+ *
44
+ * **Why Isolation:** By re-exporting the polyfill through this dedicated package, we can
45
+ * update the polyfill version or adapt to API changes in one place without touching
46
+ * consuming packages. This architectural decision protects against Stage 1 API churn.
47
+ */
48
+ export { Signal } from "signal-polyfill";
49
+ export type { SignalState, SignalComputed, SignalWatcher, SignalOptions, ComputedOptions, WatcherNotify, } from "./types.js";
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGzC,YAAY,EACX,WAAW,EACX,cAAc,EACd,aAAa,EACb,aAAa,EACb,eAAe,EACf,aAAa,GACb,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * TC39 Signals Polyfill for XMachines Play Architecture
3
+ *
4
+ * Provides fine-grained reactive state primitives based on the TC39 Signals proposal (Stage 1).
5
+ * This package isolates the TC39 polyfill to protect the codebase from Stage 1 API changes.
6
+ *
7
+ * **Architectural Context:** Implements **Signal-Only Reactivity (INV-05)** by providing
8
+ * the reactive primitives that enable Actor-to-Infrastructure communication without
9
+ * subscriptions or event emitters. All state propagation in Play Architecture uses
10
+ * TC39 Signals for automatic dependency tracking and glitch-free updates.
11
+ *
12
+ * @packageDocumentation
13
+ * @module @xmachines/play-signals
14
+ *
15
+ * @example
16
+ * Basic Signal usage
17
+ * ```typescript
18
+ * import { Signal } from "@xmachines/play-signals";
19
+ *
20
+ * // Create state signal
21
+ * const count = new Signal.State(0);
22
+ *
23
+ * // Create computed signal
24
+ * const doubled = new Signal.Computed(() => count.get() * 2);
25
+ *
26
+ * // Observe changes
27
+ * const watcher = new Signal.subtle.Watcher(() => {
28
+ * console.log('Count:', count.get(), 'Doubled:', doubled.get());
29
+ * });
30
+ * watcher.watch(count);
31
+ *
32
+ * count.set(5); // Logs: Count: 5 Doubled: 10
33
+ * ```
34
+ *
35
+ * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1 - Invariant INV-05}
36
+ * @see {@link https://github.com/tc39/proposal-signals | TC39 Signals Proposal}
37
+ *
38
+ * @remarks
39
+ * **Stage 1 Status:** TC39 Signals is currently Stage 1 in the TC39 process. This package
40
+ * uses the official `signal-polyfill` reference implementation to isolate the codebase
41
+ * from potential API changes as the proposal evolves. All signal imports should go through
42
+ * this package to maintain isolation.
43
+ *
44
+ * **Why Isolation:** By re-exporting the polyfill through this dedicated package, we can
45
+ * update the polyfill version or adapt to API changes in one place without touching
46
+ * consuming packages. This architectural decision protects against Stage 1 API churn.
47
+ */
48
+ // Re-export complete Signal namespace from official polyfill
49
+ export { Signal } from "signal-polyfill";
50
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,6DAA6D;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Type definitions for TC39 Signals API
3
+ *
4
+ * These types align with the signal-polyfill implementation and TC39 proposal.
5
+ * Note: Signal is Stage 1 - API may change in future spec updates.
6
+ *
7
+ * @see {@link https://github.com/tc39/proposal-signals | TC39 Signals Proposal}
8
+ */
9
+ /**
10
+ * Options for creating Signal.State
11
+ *
12
+ * @param equals - Optional custom equality function for determining if value changed
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { Signal } from "@xmachines/play-signals";
17
+ * import type { SignalOptions } from "@xmachines/play-signals";
18
+ *
19
+ * const options: SignalOptions<number> = {
20
+ * equals: (a, b) => a === b
21
+ * };
22
+ * const count = new Signal.State(0, options);
23
+ * ```
24
+ */
25
+ export interface SignalOptions<T> {
26
+ /**
27
+ * Custom equality function for determining if value changed
28
+ * @param a - Previous value
29
+ * @param b - New value
30
+ * @returns true if values are equal (no notification needed)
31
+ */
32
+ equals?: (a: T, b: T) => boolean;
33
+ }
34
+ /**
35
+ * Writable state signal holding a single reactive value
36
+ *
37
+ * Signal.State is the fundamental primitive for reactive state. Calling `get()` within
38
+ * a computed signal or watcher automatically tracks the state as a dependency. Calling
39
+ * `set()` notifies all dependent computations and watchers.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import { Signal } from "@xmachines/play-signals";
44
+ *
45
+ * const name = new Signal.State('Alice');
46
+ * console.log(name.get()); // 'Alice'
47
+ * name.set('Bob');
48
+ * console.log(name.get()); // 'Bob'
49
+ * ```
50
+ */
51
+ export interface SignalState<T> {
52
+ /**
53
+ * Read current value and track as dependency
54
+ *
55
+ * @returns Current value of the signal
56
+ */
57
+ get(): T;
58
+ /**
59
+ * Write new value and notify watchers if changed
60
+ *
61
+ * @param value - New value to set
62
+ */
63
+ set(value: T): void;
64
+ }
65
+ /**
66
+ * Options for creating Signal.Computed
67
+ *
68
+ * @param equals - Optional custom equality function for memoization
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { Signal } from "@xmachines/play-signals";
73
+ * import type { ComputedOptions } from "@xmachines/play-signals";
74
+ *
75
+ * const options: ComputedOptions<string> = {
76
+ * equals: (a, b) => a.toLowerCase() === b.toLowerCase()
77
+ * };
78
+ * ```
79
+ */
80
+ export interface ComputedOptions<T> {
81
+ /**
82
+ * Custom equality function for memoization
83
+ */
84
+ equals?: (a: T, b: T) => boolean;
85
+ }
86
+ /**
87
+ * Lazily-evaluated, memoized computed signal
88
+ *
89
+ * Signal.Computed automatically tracks dependencies when its callback is executed.
90
+ * The computation is memoized and only re-runs when dependencies change. This enables
91
+ * automatic dependency tracking without manual subscription management.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * import { Signal } from "@xmachines/play-signals";
96
+ *
97
+ * const count = new Signal.State(0);
98
+ * const doubled = new Signal.Computed(() => count.get() * 2);
99
+ *
100
+ * console.log(doubled.get()); // 0
101
+ * count.set(5);
102
+ * console.log(doubled.get()); // 10 (recomputed)
103
+ * console.log(doubled.get()); // 10 (memoized, not recomputed)
104
+ * ```
105
+ */
106
+ export interface SignalComputed<T> {
107
+ /**
108
+ * Read computed value (recalculates only if dependencies changed)
109
+ *
110
+ * @returns Computed value based on current dependencies
111
+ */
112
+ get(): T;
113
+ }
114
+ /**
115
+ * Notification callback for Signal.subtle.Watcher
116
+ *
117
+ * Invoked when watched signals change. Use microtask batching pattern to coalesce
118
+ * rapid updates (see signal-polyfill README for best practices).
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import { Signal } from "@xmachines/play-signals";
123
+ *
124
+ * const notify: WatcherNotify = () => {
125
+ * queueMicrotask(() => {
126
+ * const pending = watcher.getPending();
127
+ * // Process pending signal changes
128
+ * });
129
+ * };
130
+ * const watcher = new Signal.subtle.Watcher(notify);
131
+ * ```
132
+ */
133
+ export type WatcherNotify = () => void;
134
+ /**
135
+ * Watcher for observing signal changes and scheduling effects
136
+ *
137
+ * Signal.subtle.Watcher enables observing multiple signals and batching updates.
138
+ * This is the low-level primitive used by frameworks to implement reactive effects.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * import { Signal } from "@xmachines/play-signals";
143
+ *
144
+ * const count = new Signal.State(0);
145
+ * const doubled = new Signal.Computed(() => count.get() * 2);
146
+ *
147
+ * const watcher = new Signal.subtle.Watcher(() => {
148
+ * queueMicrotask(() => {
149
+ * const pending = watcher.getPending();
150
+ * console.log('Signals changed:', pending.length);
151
+ * });
152
+ * });
153
+ *
154
+ * watcher.watch(count);
155
+ * watcher.watch(doubled);
156
+ *
157
+ * count.set(5); // Notification scheduled via microtask
158
+ * ```
159
+ */
160
+ export interface SignalWatcher {
161
+ /**
162
+ * Start watching a signal for changes
163
+ *
164
+ * @param signal - Signal to observe (State or Computed)
165
+ */
166
+ watch(signal: SignalState<unknown> | SignalComputed<unknown>): void;
167
+ /**
168
+ * Stop watching a signal
169
+ *
170
+ * @param signal - Signal to stop observing
171
+ */
172
+ unwatch(signal: SignalState<unknown> | SignalComputed<unknown>): void;
173
+ /**
174
+ * Get signals that changed since last check
175
+ *
176
+ * @returns Array of signals that have pending updates
177
+ */
178
+ getPending(): Array<SignalState<unknown> | SignalComputed<unknown>>;
179
+ }
180
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC7B;;;;OAIG;IACH,GAAG,IAAI,CAAC,CAAC;IAET;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IACjC;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC;IAChC;;;;OAIG;IACH,GAAG,IAAI,CAAC,CAAC;CACT;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;OAIG;IACH,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAEpE;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAEtE;;;;OAIG;IACH,UAAU,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;CACpE"}
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type definitions for TC39 Signals API
3
+ *
4
+ * These types align with the signal-polyfill implementation and TC39 proposal.
5
+ * Note: Signal is Stage 1 - API may change in future spec updates.
6
+ *
7
+ * @see {@link https://github.com/tc39/proposal-signals | TC39 Signals Proposal}
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@xmachines/play-signals",
3
+ "version": "1.0.0-beta.1",
4
+ "private": false,
5
+ "description": "TC39 Signals polyfill for XMachines - Fine-grained reactive state primitives",
6
+ "keywords": [
7
+ "reactive",
8
+ "signals",
9
+ "state-management",
10
+ "tc39",
11
+ "xmachines"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "XMachines Contributors",
15
+ "files": [
16
+ "dist",
17
+ "src",
18
+ "README.md"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "scripts": {
28
+ "build": "tsc --build",
29
+ "clean": "rm -rf dist *.tsbuildinfo",
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "vitest run",
32
+ "lint": "oxlint .",
33
+ "lint:fix": "oxlint --fix .",
34
+ "format": "oxfmt .",
35
+ "format:check": "oxfmt --check .",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "signal-polyfill": "^0.2.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^25.4.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=22.0.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * TC39 Signals Polyfill for XMachines Play Architecture
3
+ *
4
+ * Provides fine-grained reactive state primitives based on the TC39 Signals proposal (Stage 1).
5
+ * This package isolates the TC39 polyfill to protect the codebase from Stage 1 API changes.
6
+ *
7
+ * **Architectural Context:** Implements **Signal-Only Reactivity (INV-05)** by providing
8
+ * the reactive primitives that enable Actor-to-Infrastructure communication without
9
+ * subscriptions or event emitters. All state propagation in Play Architecture uses
10
+ * TC39 Signals for automatic dependency tracking and glitch-free updates.
11
+ *
12
+ * @packageDocumentation
13
+ * @module @xmachines/play-signals
14
+ *
15
+ * @example
16
+ * Basic Signal usage
17
+ * ```typescript
18
+ * import { Signal } from "@xmachines/play-signals";
19
+ *
20
+ * // Create state signal
21
+ * const count = new Signal.State(0);
22
+ *
23
+ * // Create computed signal
24
+ * const doubled = new Signal.Computed(() => count.get() * 2);
25
+ *
26
+ * // Observe changes
27
+ * const watcher = new Signal.subtle.Watcher(() => {
28
+ * console.log('Count:', count.get(), 'Doubled:', doubled.get());
29
+ * });
30
+ * watcher.watch(count);
31
+ *
32
+ * count.set(5); // Logs: Count: 5 Doubled: 10
33
+ * ```
34
+ *
35
+ * @see {@link https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md | RFC Play v1 - Invariant INV-05}
36
+ * @see {@link https://github.com/tc39/proposal-signals | TC39 Signals Proposal}
37
+ *
38
+ * @remarks
39
+ * **Stage 1 Status:** TC39 Signals is currently Stage 1 in the TC39 process. This package
40
+ * uses the official `signal-polyfill` reference implementation to isolate the codebase
41
+ * from potential API changes as the proposal evolves. All signal imports should go through
42
+ * this package to maintain isolation.
43
+ *
44
+ * **Why Isolation:** By re-exporting the polyfill through this dedicated package, we can
45
+ * update the polyfill version or adapt to API changes in one place without touching
46
+ * consuming packages. This architectural decision protects against Stage 1 API churn.
47
+ */
48
+
49
+ // Re-export complete Signal namespace from official polyfill
50
+ export { Signal } from "signal-polyfill";
51
+
52
+ // Re-export TypeScript types for convenience
53
+ export type {
54
+ SignalState,
55
+ SignalComputed,
56
+ SignalWatcher,
57
+ SignalOptions,
58
+ ComputedOptions,
59
+ WatcherNotify,
60
+ } from "./types.js";
package/src/types.ts ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Type definitions for TC39 Signals API
3
+ *
4
+ * These types align with the signal-polyfill implementation and TC39 proposal.
5
+ * Note: Signal is Stage 1 - API may change in future spec updates.
6
+ *
7
+ * @see {@link https://github.com/tc39/proposal-signals | TC39 Signals Proposal}
8
+ */
9
+
10
+ /**
11
+ * Options for creating Signal.State
12
+ *
13
+ * @param equals - Optional custom equality function for determining if value changed
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { Signal } from "@xmachines/play-signals";
18
+ * import type { SignalOptions } from "@xmachines/play-signals";
19
+ *
20
+ * const options: SignalOptions<number> = {
21
+ * equals: (a, b) => a === b
22
+ * };
23
+ * const count = new Signal.State(0, options);
24
+ * ```
25
+ */
26
+ export interface SignalOptions<T> {
27
+ /**
28
+ * Custom equality function for determining if value changed
29
+ * @param a - Previous value
30
+ * @param b - New value
31
+ * @returns true if values are equal (no notification needed)
32
+ */
33
+ equals?: (a: T, b: T) => boolean;
34
+ }
35
+
36
+ /**
37
+ * Writable state signal holding a single reactive value
38
+ *
39
+ * Signal.State is the fundamental primitive for reactive state. Calling `get()` within
40
+ * a computed signal or watcher automatically tracks the state as a dependency. Calling
41
+ * `set()` notifies all dependent computations and watchers.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { Signal } from "@xmachines/play-signals";
46
+ *
47
+ * const name = new Signal.State('Alice');
48
+ * console.log(name.get()); // 'Alice'
49
+ * name.set('Bob');
50
+ * console.log(name.get()); // 'Bob'
51
+ * ```
52
+ */
53
+ export interface SignalState<T> {
54
+ /**
55
+ * Read current value and track as dependency
56
+ *
57
+ * @returns Current value of the signal
58
+ */
59
+ get(): T;
60
+
61
+ /**
62
+ * Write new value and notify watchers if changed
63
+ *
64
+ * @param value - New value to set
65
+ */
66
+ set(value: T): void;
67
+ }
68
+
69
+ /**
70
+ * Options for creating Signal.Computed
71
+ *
72
+ * @param equals - Optional custom equality function for memoization
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * import { Signal } from "@xmachines/play-signals";
77
+ * import type { ComputedOptions } from "@xmachines/play-signals";
78
+ *
79
+ * const options: ComputedOptions<string> = {
80
+ * equals: (a, b) => a.toLowerCase() === b.toLowerCase()
81
+ * };
82
+ * ```
83
+ */
84
+ export interface ComputedOptions<T> {
85
+ /**
86
+ * Custom equality function for memoization
87
+ */
88
+ equals?: (a: T, b: T) => boolean;
89
+ }
90
+
91
+ /**
92
+ * Lazily-evaluated, memoized computed signal
93
+ *
94
+ * Signal.Computed automatically tracks dependencies when its callback is executed.
95
+ * The computation is memoized and only re-runs when dependencies change. This enables
96
+ * automatic dependency tracking without manual subscription management.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import { Signal } from "@xmachines/play-signals";
101
+ *
102
+ * const count = new Signal.State(0);
103
+ * const doubled = new Signal.Computed(() => count.get() * 2);
104
+ *
105
+ * console.log(doubled.get()); // 0
106
+ * count.set(5);
107
+ * console.log(doubled.get()); // 10 (recomputed)
108
+ * console.log(doubled.get()); // 10 (memoized, not recomputed)
109
+ * ```
110
+ */
111
+ export interface SignalComputed<T> {
112
+ /**
113
+ * Read computed value (recalculates only if dependencies changed)
114
+ *
115
+ * @returns Computed value based on current dependencies
116
+ */
117
+ get(): T;
118
+ }
119
+
120
+ /**
121
+ * Notification callback for Signal.subtle.Watcher
122
+ *
123
+ * Invoked when watched signals change. Use microtask batching pattern to coalesce
124
+ * rapid updates (see signal-polyfill README for best practices).
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * import { Signal } from "@xmachines/play-signals";
129
+ *
130
+ * const notify: WatcherNotify = () => {
131
+ * queueMicrotask(() => {
132
+ * const pending = watcher.getPending();
133
+ * // Process pending signal changes
134
+ * });
135
+ * };
136
+ * const watcher = new Signal.subtle.Watcher(notify);
137
+ * ```
138
+ */
139
+ export type WatcherNotify = () => void;
140
+
141
+ /**
142
+ * Watcher for observing signal changes and scheduling effects
143
+ *
144
+ * Signal.subtle.Watcher enables observing multiple signals and batching updates.
145
+ * This is the low-level primitive used by frameworks to implement reactive effects.
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * import { Signal } from "@xmachines/play-signals";
150
+ *
151
+ * const count = new Signal.State(0);
152
+ * const doubled = new Signal.Computed(() => count.get() * 2);
153
+ *
154
+ * const watcher = new Signal.subtle.Watcher(() => {
155
+ * queueMicrotask(() => {
156
+ * const pending = watcher.getPending();
157
+ * console.log('Signals changed:', pending.length);
158
+ * });
159
+ * });
160
+ *
161
+ * watcher.watch(count);
162
+ * watcher.watch(doubled);
163
+ *
164
+ * count.set(5); // Notification scheduled via microtask
165
+ * ```
166
+ */
167
+ export interface SignalWatcher {
168
+ /**
169
+ * Start watching a signal for changes
170
+ *
171
+ * @param signal - Signal to observe (State or Computed)
172
+ */
173
+ watch(signal: SignalState<unknown> | SignalComputed<unknown>): void;
174
+
175
+ /**
176
+ * Stop watching a signal
177
+ *
178
+ * @param signal - Signal to stop observing
179
+ */
180
+ unwatch(signal: SignalState<unknown> | SignalComputed<unknown>): void;
181
+
182
+ /**
183
+ * Get signals that changed since last check
184
+ *
185
+ * @returns Array of signals that have pending updates
186
+ */
187
+ getPending(): Array<SignalState<unknown> | SignalComputed<unknown>>;
188
+ }