@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 +109 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +180 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +60 -0
- package/src/types.ts +188 -0
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
|
package/dist/index.d.ts
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
|
+
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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|