@xmachines/play-signals 2.2.0 → 4.0.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/README.md +4 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/watch-signal.d.ts +26 -2
- package/dist/watch-signal.d.ts.map +1 -1
- package/dist/watch-signal.js +56 -5
- package/dist/watch-signal.js.map +1 -1
- package/package.json +14 -9
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
TC39 Signals polyfill for XMachines. It gives the Play Architecture fine-grained reactive state primitives. The primitives propagate state without a glitch and without a subscription.
|
|
4
4
|
|
|
5
|
-
[](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/MIT) [](https://www.npmjs.com/package/@xmachines/play-signals)
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -115,39 +115,18 @@ person.set({ name: "Alice", age: 30 });
|
|
|
115
115
|
| Export | Kind | Description |
|
|
116
116
|
| ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------ |
|
|
117
117
|
| `Signal` | namespace | Full TC39 Signals namespace (`State`, `Computed`, `subtle.Watcher`) re-exported from `signal-polyfill` |
|
|
118
|
-
| `watchSignal(signal, onValue)` | function | The memory-safe subscription helper. It returns a
|
|
118
|
+
| `watchSignal(signal, onValue)` | function | The memory-safe subscription helper. It returns a `Cleanup` |
|
|
119
119
|
| `SignalState<T>` | interface | Shape of `Signal.State<T>` (`.get()`, `.set()`) |
|
|
120
120
|
| `SignalComputed<T>` | interface | Shape of `Signal.Computed<T>` (`.get()`) |
|
|
121
121
|
| `SignalWatcher` | interface | Shape of `Signal.subtle.Watcher` (`.watch()`, `.unwatch()`, `.getPending()`) |
|
|
122
122
|
| `SignalOptions<T>` | interface | The options object for the `Signal.State` constructor (`equals?`) |
|
|
123
123
|
| `ComputedOptions<T>` | interface | The options object for the `Signal.Computed` constructor (`equals?`) |
|
|
124
124
|
| `WatcherNotify` | type | The callback signature of the `Signal.subtle.Watcher` notify function |
|
|
125
|
-
|
|
126
|
-
## Testing
|
|
127
|
-
|
|
128
|
-
Run tests for this package in isolation:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
# From this package directory
|
|
132
|
-
pnpm test
|
|
133
|
-
|
|
134
|
-
# Watch mode
|
|
135
|
-
pnpm test -- --watch
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
From the monorepo root:
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
# Run tests for this package
|
|
142
|
-
pnpm --filter @xmachines/play-signals test
|
|
143
|
-
|
|
144
|
-
# Run with coverage (lines ≥ 90 %, functions ≥ 90 %, branches ≥ 85 %, statements ≥ 90 %)
|
|
145
|
-
pnpm run test:coverage
|
|
146
|
-
```
|
|
125
|
+
| `Cleanup` | type | The release that `watchSignal` returns, re-exported from `@xmachines/play` |
|
|
147
126
|
|
|
148
127
|
## Requirements
|
|
149
128
|
|
|
150
|
-
- **Node.js** `>=
|
|
129
|
+
- **Node.js** `>= 24.0.0`
|
|
151
130
|
- **TypeScript** `5.7+` (for a consumer that uses TypeScript)
|
|
152
131
|
|
|
153
132
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -52,4 +52,5 @@
|
|
|
52
52
|
export { Signal } from "signal-polyfill";
|
|
53
53
|
export { watchSignal } from "./watch-signal.js";
|
|
54
54
|
export type { SignalState, SignalComputed, SignalWatcher, SignalOptions, ComputedOptions, WatcherNotify, } from "./types.js";
|
|
55
|
+
export { type Cleanup } from "@xmachines/play";
|
|
55
56
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,YAAY,EACX,WAAW,EACX,cAAc,EACd,aAAa,EACb,aAAa,EACb,eAAe,EACf,aAAa,GACb,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,YAAY,EACX,WAAW,EACX,cAAc,EACd,aAAa,EACb,aAAa,EACb,eAAe,EACf,aAAa,GACb,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -52,4 +52,9 @@
|
|
|
52
52
|
// Re-export the complete Signal namespace of the official polyfill
|
|
53
53
|
export { Signal } from "signal-polyfill";
|
|
54
54
|
export { watchSignal } from "./watch-signal.js";
|
|
55
|
+
// The release protocol of @xmachines/play. This package's own published `.d.ts` names
|
|
56
|
+
// exactly these, so a consumer reads them from here and needs no second manifest
|
|
57
|
+
// entry. It names `asCleanup` nowhere: a consumer of this package RECEIVES a
|
|
58
|
+
// release, and builds one only with @xmachines/play itself.
|
|
59
|
+
export {} from "@xmachines/play";
|
|
55
60
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,mEAAmE;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,mEAAmE;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAWhD,sFAAsF;AACtF,iFAAiF;AACjF,6EAA6E;AAC7E,4DAA4D;AAC5D,OAAO,EAAgB,MAAM,iBAAiB,CAAC"}
|
package/dist/watch-signal.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Cleanup } from "@xmachines/play";
|
|
1
2
|
import { Signal } from "signal-polyfill";
|
|
2
3
|
/**
|
|
3
4
|
* Subscribes to one signal, with the canonical one-shot watcher lifecycle.
|
|
@@ -12,10 +13,33 @@ import { Signal } from "signal-polyfill";
|
|
|
12
13
|
* once when the cleanup runs before the microtask fires.
|
|
13
14
|
* - The `needsEnqueue` guard removes the duplicates of rapid synchronous signal
|
|
14
15
|
* changes: one batch of synchronous changes queues one microtask only.
|
|
16
|
+
* - The watcher arms again BEFORE the callback: a callback that THROWS keeps the watch, a
|
|
17
|
+
* callback that RELEASES the watch leaves no watcher attached to the signal, and a
|
|
18
|
+
* callback that WRITES the signal reaches the caller with the value that it wrote.
|
|
19
|
+
* - The watcher arms with NO argument: `watch(signal)` records the edge a second time on
|
|
20
|
+
* every emission, so the edge list of the watcher and the consumer list of the signal
|
|
21
|
+
* grow by one for each change, for the life of the watch.
|
|
22
|
+
*
|
|
23
|
+
* A CAUTION for a callback that writes the signal that it watches: the write reaches the
|
|
24
|
+
* callback, so a callback that writes a NEW value on every call runs again for ever, and
|
|
25
|
+
* the microtask queue drains never. That is the loop of the caller, and this function
|
|
26
|
+
* bounds it not — a bound here would drop the write of a caller that repairs its state
|
|
27
|
+
* one time, which is the case that the order above exists to serve. A caller that repairs
|
|
28
|
+
* from a failure must repair to a state that succeeds, or it must stop after one attempt.
|
|
29
|
+
* A write of the SAME value ends the sequence by itself, because a signal notifies for a
|
|
30
|
+
* value that it holds already never.
|
|
31
|
+
*
|
|
32
|
+
* The return is a {@link Cleanup}: a function, and a `Disposable`. A caller that keeps
|
|
33
|
+
* the release in a field and runs it from a teardown keeps that code. A caller inside one
|
|
34
|
+
* scope writes `using` and writes no teardown:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* using stop = watchSignal(count, render);
|
|
38
|
+
* ```
|
|
15
39
|
*
|
|
16
40
|
* @param signal - The `Signal.State` or `Signal.Computed` to subscribe to.
|
|
17
41
|
* @param onValue - The watcher calls it with the current signal value after each change.
|
|
18
|
-
* @returns The
|
|
42
|
+
* @returns The release. It removes the watcher.
|
|
19
43
|
*/
|
|
20
|
-
export declare function watchSignal<T>(signal: Signal.State<T> | Signal.Computed<T>, onValue: (value: T) => void):
|
|
44
|
+
export declare function watchSignal<T>(signal: Signal.State<T> | Signal.Computed<T>, onValue: (value: T) => void): Cleanup;
|
|
21
45
|
//# sourceMappingURL=watch-signal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-signal.d.ts","sourceRoot":"","sources":["../src/watch-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC
|
|
1
|
+
{"version":3,"file":"watch-signal.d.ts","sourceRoot":"","sources":["../src/watch-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAC5C,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GACzB,OAAO,CAqDT"}
|
package/dist/watch-signal.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asCleanup } from "@xmachines/play";
|
|
1
2
|
import { Signal } from "signal-polyfill";
|
|
2
3
|
/**
|
|
3
4
|
* Subscribes to one signal, with the canonical one-shot watcher lifecycle.
|
|
@@ -12,10 +13,33 @@ import { Signal } from "signal-polyfill";
|
|
|
12
13
|
* once when the cleanup runs before the microtask fires.
|
|
13
14
|
* - The `needsEnqueue` guard removes the duplicates of rapid synchronous signal
|
|
14
15
|
* changes: one batch of synchronous changes queues one microtask only.
|
|
16
|
+
* - The watcher arms again BEFORE the callback: a callback that THROWS keeps the watch, a
|
|
17
|
+
* callback that RELEASES the watch leaves no watcher attached to the signal, and a
|
|
18
|
+
* callback that WRITES the signal reaches the caller with the value that it wrote.
|
|
19
|
+
* - The watcher arms with NO argument: `watch(signal)` records the edge a second time on
|
|
20
|
+
* every emission, so the edge list of the watcher and the consumer list of the signal
|
|
21
|
+
* grow by one for each change, for the life of the watch.
|
|
22
|
+
*
|
|
23
|
+
* A CAUTION for a callback that writes the signal that it watches: the write reaches the
|
|
24
|
+
* callback, so a callback that writes a NEW value on every call runs again for ever, and
|
|
25
|
+
* the microtask queue drains never. That is the loop of the caller, and this function
|
|
26
|
+
* bounds it not — a bound here would drop the write of a caller that repairs its state
|
|
27
|
+
* one time, which is the case that the order above exists to serve. A caller that repairs
|
|
28
|
+
* from a failure must repair to a state that succeeds, or it must stop after one attempt.
|
|
29
|
+
* A write of the SAME value ends the sequence by itself, because a signal notifies for a
|
|
30
|
+
* value that it holds already never.
|
|
31
|
+
*
|
|
32
|
+
* The return is a {@link Cleanup}: a function, and a `Disposable`. A caller that keeps
|
|
33
|
+
* the release in a field and runs it from a teardown keeps that code. A caller inside one
|
|
34
|
+
* scope writes `using` and writes no teardown:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* using stop = watchSignal(count, render);
|
|
38
|
+
* ```
|
|
15
39
|
*
|
|
16
40
|
* @param signal - The `Signal.State` or `Signal.Computed` to subscribe to.
|
|
17
41
|
* @param onValue - The watcher calls it with the current signal value after each change.
|
|
18
|
-
* @returns The
|
|
42
|
+
* @returns The release. It removes the watcher.
|
|
19
43
|
*/
|
|
20
44
|
export function watchSignal(signal, onValue) {
|
|
21
45
|
let disposed = false;
|
|
@@ -29,19 +53,46 @@ export function watchSignal(signal, onValue) {
|
|
|
29
53
|
return;
|
|
30
54
|
needsEnqueue = true;
|
|
31
55
|
watcher.getPending();
|
|
56
|
+
// Arm the watcher BEFORE the callback, and not after it. Three cases need this
|
|
57
|
+
// order, and no case needs the other one.
|
|
58
|
+
//
|
|
59
|
+
// A callback that WRITES the signal notifies the watcher on that write — a
|
|
60
|
+
// renderer that gives its host an error handler, and the host repairs the actor
|
|
61
|
+
// from that handler. The watcher must accept the notification at that moment. A
|
|
62
|
+
// line after the callback arms it too late: the write finds the watcher still
|
|
63
|
+
// notified, the polyfill drops the notification, and the line then clears the
|
|
64
|
+
// flag for a change that reaches nobody. Measured: a callback that reads 1 and
|
|
65
|
+
// writes 2 reads 3 next, and the value 2 reaches the caller never.
|
|
66
|
+
//
|
|
67
|
+
// A callback that THROWS keeps the watch, because this line ran already. Before
|
|
68
|
+
// it, such a callback left the watcher attached to the signal and never armed:
|
|
69
|
+
// no later change reached the callback, the caller followed the signal no more,
|
|
70
|
+
// in silence and for the life of the signal.
|
|
71
|
+
//
|
|
72
|
+
// A callback that RELEASES the watch from inside itself leaves no watcher on the
|
|
73
|
+
// signal: the release detaches the edge, and this line adds none.
|
|
74
|
+
//
|
|
75
|
+
// NO argument. `watch()` with no signal resets the notified state alone, which is
|
|
76
|
+
// what this line needs: the edge to the signal stands from the `watch(signal)`
|
|
77
|
+
// below. `watch(signal)` records the edge a SECOND time — the watcher appends the
|
|
78
|
+
// producer at the next free index, and it resets that index never — so the edge
|
|
79
|
+
// list of the watcher and the live-consumer list of the signal grow by one for
|
|
80
|
+
// EVERY emission. Measured with the polyfill: 50 emissions leave 51 entries in
|
|
81
|
+
// each list. A view that streams therefore leaks one entry for each chunk, and
|
|
82
|
+
// each later change walks the whole list.
|
|
83
|
+
watcher.watch();
|
|
32
84
|
onValue(signal.get());
|
|
33
|
-
watcher.watch(signal);
|
|
34
85
|
});
|
|
35
86
|
});
|
|
36
87
|
watcher.watch(signal);
|
|
37
|
-
return () => {
|
|
88
|
+
return asCleanup(() => {
|
|
38
89
|
disposed = true;
|
|
39
90
|
try {
|
|
40
91
|
watcher.unwatch(signal);
|
|
41
92
|
}
|
|
42
93
|
catch {
|
|
43
|
-
// Ignore an error of a detached watcher. The
|
|
94
|
+
// Ignore an error of a detached watcher. The release therefore stays idempotent.
|
|
44
95
|
}
|
|
45
|
-
};
|
|
96
|
+
});
|
|
46
97
|
}
|
|
47
98
|
//# sourceMappingURL=watch-signal.js.map
|
package/dist/watch-signal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-signal.js","sourceRoot":"","sources":["../src/watch-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC
|
|
1
|
+
{"version":3,"file":"watch-signal.js","sourceRoot":"","sources":["../src/watch-signal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,UAAU,WAAW,CAC1B,MAA4C,EAC5C,OAA2B;IAE3B,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,YAAY,GAAG,IAAI,CAAC;IAExB,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;QAC9C,IAAI,QAAQ,IAAI,CAAC,YAAY;YAAE,OAAO;QACtC,YAAY,GAAG,KAAK,CAAC;QACrB,cAAc,CAAC,GAAG,EAAE;YACnB,IAAI,QAAQ;gBAAE,OAAO;YACrB,YAAY,GAAG,IAAI,CAAC;YACpB,OAAO,CAAC,UAAU,EAAE,CAAC;YACrB,+EAA+E;YAC/E,0CAA0C;YAC1C,EAAE;YACF,2EAA2E;YAC3E,gFAAgF;YAChF,gFAAgF;YAChF,8EAA8E;YAC9E,8EAA8E;YAC9E,+EAA+E;YAC/E,mEAAmE;YACnE,EAAE;YACF,gFAAgF;YAChF,+EAA+E;YAC/E,gFAAgF;YAChF,6CAA6C;YAC7C,EAAE;YACF,iFAAiF;YACjF,kEAAkE;YAClE,EAAE;YACF,kFAAkF;YAClF,+EAA+E;YAC/E,kFAAkF;YAClF,gFAAgF;YAChF,+EAA+E;YAC/E,+EAA+E;YAC/E,+EAA+E;YAC/E,0CAA0C;YAC1C,OAAO,CAAC,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEtB,OAAO,SAAS,CAAC,GAAG,EAAE;QACrB,QAAQ,GAAG,IAAI,CAAC;QAChB,IAAI,CAAC;YACJ,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACR,iFAAiF;QAClF,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-signals",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "TC39 Signals polyfill for XMachines - Fine-grained reactive state primitives",
|
|
6
6
|
"keywords": [
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"test": "vitest",
|
|
44
44
|
"test:coverage": "vitest run --coverage",
|
|
45
45
|
"lint": "oxlint .",
|
|
46
|
+
"lint:security": "node ../../scripts/semgrep-scan.mjs",
|
|
46
47
|
"lint:fix": "oxlint --fix .",
|
|
47
48
|
"format": "oxfmt .",
|
|
48
49
|
"format:check": "oxfmt --check ."
|
|
@@ -51,15 +52,19 @@
|
|
|
51
52
|
"signal-polyfill": "^0.2.2"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@testing-library/jest-dom": "^
|
|
55
|
-
"@types/node": "^26.2
|
|
56
|
-
"@vitest/browser-playwright": "^
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
55
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
56
|
+
"@types/node": "^26.6.2",
|
|
57
|
+
"@vitest/browser-playwright": "^5.0.1",
|
|
58
|
+
"@xmachines/play": "4.0.0",
|
|
59
|
+
"oxfmt": "^0.68.0",
|
|
60
|
+
"oxlint": "^1.83.0",
|
|
61
|
+
"vite": "^8.3.0",
|
|
62
|
+
"vitest": "^5.0.1"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@xmachines/play": "4.0.0"
|
|
61
66
|
},
|
|
62
67
|
"engines": {
|
|
63
|
-
"node": ">=
|
|
68
|
+
"node": ">=24.0.0"
|
|
64
69
|
}
|
|
65
70
|
}
|