@esmj/signals 0.0.4 → 0.1.2
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 +568 -2
- package/dist/index.d.mts +208 -58
- package/dist/index.d.ts +208 -58
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,3 +1,569 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @esmj/signals
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A tiny, fine-grained reactive signals library for JavaScript. Built as a lightweight wrapper around the [TC39 Signals proposal](https://github.com/tc39/proposal-signals), providing a ready-to-use API today that aligns with the future standard.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @esmj/signals
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { state, computed, effect } from '@esmj/signals';
|
|
15
|
+
|
|
16
|
+
const count = state(0);
|
|
17
|
+
const doubled = computed(() => count.get() * 2);
|
|
18
|
+
|
|
19
|
+
effect(() => {
|
|
20
|
+
console.log(`Count: ${count.get()}, Doubled: ${doubled.get()}`);
|
|
21
|
+
});
|
|
22
|
+
// logs: "Count: 0, Doubled: 0"
|
|
23
|
+
|
|
24
|
+
count.set(5);
|
|
25
|
+
// logs: "Count: 5, Doubled: 10"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Motivation
|
|
29
|
+
|
|
30
|
+
The [TC39 Signals proposal](https://github.com/tc39/proposal-signals) aims to bring reactive primitives to the JavaScript language. This library provides a lightweight implementation of the same concepts so you can start using signals today with minimal overhead. When the proposal lands natively, migration should be straightforward.
|
|
31
|
+
|
|
32
|
+
## API
|
|
33
|
+
|
|
34
|
+
### `state(value, options?)`
|
|
35
|
+
|
|
36
|
+
Creates a reactive signal (also exported as `createSignal`).
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import { state } from '@esmj/signals';
|
|
40
|
+
|
|
41
|
+
const name = state('Alice');
|
|
42
|
+
|
|
43
|
+
// Read the value
|
|
44
|
+
name.get(); // 'Alice'
|
|
45
|
+
|
|
46
|
+
// Write a new value
|
|
47
|
+
name.set('Bob');
|
|
48
|
+
name.get(); // 'Bob'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Options
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Description |
|
|
54
|
+
|--------|------|---------|-------------|
|
|
55
|
+
| `equals` | `(a, b) => boolean` | `Object.is` | Custom equality function. Notifications are skipped when `equals` returns `true`. |
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// Signal that always notifies on set, even with the same value
|
|
59
|
+
const counter = state(0, { equals: () => false });
|
|
60
|
+
|
|
61
|
+
// Signal with deep equality (e.g. using a library)
|
|
62
|
+
const data = state({ a: 1 }, { equals: deepEqual });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `computed(callback, options?)`
|
|
66
|
+
|
|
67
|
+
Creates a lazy, memoized derived signal. The callback is not executed until `.get()` is first called. Recomputation only occurs when a dependency changes.
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
import { state, computed } from '@esmj/signals';
|
|
71
|
+
|
|
72
|
+
const firstName = state('John');
|
|
73
|
+
const lastName = state('Doe');
|
|
74
|
+
|
|
75
|
+
const fullName = computed(() => `${firstName.get()} ${lastName.get()}`);
|
|
76
|
+
|
|
77
|
+
fullName.get(); // 'John Doe'
|
|
78
|
+
|
|
79
|
+
firstName.set('Jane');
|
|
80
|
+
fullName.get(); // 'Jane Doe'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Chained computeds
|
|
84
|
+
|
|
85
|
+
Computed signals can depend on other computed signals:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
const a = state(1);
|
|
89
|
+
const b = computed(() => a.get() * 2);
|
|
90
|
+
const c = computed(() => b.get() + 10);
|
|
91
|
+
|
|
92
|
+
c.get(); // 12
|
|
93
|
+
|
|
94
|
+
a.set(5);
|
|
95
|
+
c.get(); // 20
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Options
|
|
99
|
+
|
|
100
|
+
Same as `state` options (`equals`).
|
|
101
|
+
|
|
102
|
+
### `effect(callback, options?)`
|
|
103
|
+
|
|
104
|
+
Creates a side effect that automatically re-runs whenever its dependencies change. Returns a dispose function to stop the effect.
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
import { state, effect } from '@esmj/signals';
|
|
108
|
+
|
|
109
|
+
const count = state(0);
|
|
110
|
+
|
|
111
|
+
const dispose = effect(() => {
|
|
112
|
+
console.log('Count is:', count.get());
|
|
113
|
+
});
|
|
114
|
+
// logs: "Count is: 0"
|
|
115
|
+
|
|
116
|
+
count.set(1);
|
|
117
|
+
// logs: "Count is: 1"
|
|
118
|
+
|
|
119
|
+
// Stop the effect
|
|
120
|
+
dispose();
|
|
121
|
+
count.set(2);
|
|
122
|
+
// (nothing logged)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Explicit Resource Management (`using`)
|
|
126
|
+
|
|
127
|
+
The dispose function supports [`Symbol.dispose`](https://github.com/tc39/proposal-explicit-resource-management), enabling automatic cleanup with the `using` keyword:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
{
|
|
131
|
+
using dispose = effect(() => {
|
|
132
|
+
console.log('Count is:', count.get());
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
count.set(1);
|
|
136
|
+
// effect is active
|
|
137
|
+
}
|
|
138
|
+
// ← effect automatically disposed when block exits
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Cleanup / Destructor
|
|
142
|
+
|
|
143
|
+
If the effect callback returns a function, it will be called before each re-execution and on disposal:
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const visible = state(true);
|
|
147
|
+
|
|
148
|
+
const dispose = effect(() => {
|
|
149
|
+
if (visible.get()) {
|
|
150
|
+
const handler = () => console.log('clicked');
|
|
151
|
+
document.addEventListener('click', handler);
|
|
152
|
+
|
|
153
|
+
// Cleanup: runs before next effect execution or on dispose
|
|
154
|
+
return () => {
|
|
155
|
+
document.removeEventListener('click', handler);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
visible.set(false); // cleanup runs, listener removed
|
|
161
|
+
dispose();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `batch(callback)`
|
|
165
|
+
|
|
166
|
+
Batches multiple signal updates into a single notification. Computed signals and effects are only notified once after the batch completes, preventing intermediate (glitchy) states.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
import { state, computed, batch } from '@esmj/signals';
|
|
170
|
+
|
|
171
|
+
const a = state(1);
|
|
172
|
+
const b = state(2);
|
|
173
|
+
let computeCount = 0;
|
|
174
|
+
|
|
175
|
+
const sum = computed(() => {
|
|
176
|
+
computeCount++;
|
|
177
|
+
return a.get() + b.get();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
sum.get(); // 3, computeCount === 1
|
|
181
|
+
|
|
182
|
+
batch(() => {
|
|
183
|
+
a.set(10);
|
|
184
|
+
b.set(20);
|
|
185
|
+
// No recomputation happens here
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
sum.get(); // 30, computeCount === 2 (only one recomputation!)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Nested batches
|
|
192
|
+
|
|
193
|
+
Inner batches do not flush until the outermost batch completes:
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
batch(() => {
|
|
197
|
+
a.set(10);
|
|
198
|
+
batch(() => {
|
|
199
|
+
b.set(20);
|
|
200
|
+
c.set(30);
|
|
201
|
+
});
|
|
202
|
+
// Still batched — nothing flushed yet
|
|
203
|
+
});
|
|
204
|
+
// Now all three updates are flushed at once
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Efficient Updates (Pull-based Validation)
|
|
208
|
+
|
|
209
|
+
The library uses pull-based validation with revision tracking to avoid redundant recomputations in diamond dependency graphs:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
state A
|
|
213
|
+
/ \
|
|
214
|
+
computed B computed C
|
|
215
|
+
\ /
|
|
216
|
+
computed D
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When `A` changes, both `B` and `C` are marked dirty, which also marks `D` dirty. However, when `D.get()` is called, it first validates its sources by pulling their current values. Each source is validated recursively before `D` decides whether to recompute. This means `D` recomputes **exactly once**, not twice.
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
import { state, computed } from '@esmj/signals';
|
|
223
|
+
|
|
224
|
+
const a = state(1);
|
|
225
|
+
const b = computed(() => a.get() * 2);
|
|
226
|
+
const c = computed(() => a.get() * 3);
|
|
227
|
+
const d = computed(() => b.get() + c.get());
|
|
228
|
+
|
|
229
|
+
d.get(); // 5
|
|
230
|
+
|
|
231
|
+
a.set(2);
|
|
232
|
+
d.get(); // 10 — d recomputed only once, not twice
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Revision tracking
|
|
236
|
+
|
|
237
|
+
Every signal tracks a revision number that increments on each value change. This allows downstream computed signals to detect whether a source actually changed or if the dirty flag was a false alarm.
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
const s = state(1);
|
|
241
|
+
s.getRevision(); // 0
|
|
242
|
+
|
|
243
|
+
s.set(2);
|
|
244
|
+
s.getRevision(); // 1
|
|
245
|
+
|
|
246
|
+
// Same value — revision does not increment
|
|
247
|
+
s.set(2);
|
|
248
|
+
s.getRevision(); // 1
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `untrack(callback)`
|
|
252
|
+
|
|
253
|
+
Executes a callback without tracking any signal dependencies. Useful inside effects or computed signals when you want to read a signal without subscribing to it.
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
import { state, computed, untrack } from '@esmj/signals';
|
|
257
|
+
|
|
258
|
+
const a = state(1);
|
|
259
|
+
const b = state(2);
|
|
260
|
+
|
|
261
|
+
const result = computed(() => {
|
|
262
|
+
// `a` is tracked — changes to `a` will recompute
|
|
263
|
+
const aVal = a.get();
|
|
264
|
+
|
|
265
|
+
// `b` is NOT tracked — changes to `b` will NOT recompute
|
|
266
|
+
const bVal = untrack(() => b.get());
|
|
267
|
+
|
|
268
|
+
return aVal + bVal;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
result.get(); // 3
|
|
272
|
+
|
|
273
|
+
b.set(100);
|
|
274
|
+
result.get(); // 3 (not recomputed because b is untracked)
|
|
275
|
+
|
|
276
|
+
a.set(10);
|
|
277
|
+
result.get(); // 110 (recomputed, picks up current b value)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `signal.peek()`
|
|
281
|
+
|
|
282
|
+
Reads the current value of a signal without subscribing to it. Available on both `state` and `computed` signals. A concise alternative to `untrack(() => signal.get())`.
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
import { state, computed } from '@esmj/signals';
|
|
286
|
+
|
|
287
|
+
const count = state(5);
|
|
288
|
+
count.peek(); // 5 — no tracking
|
|
289
|
+
|
|
290
|
+
const doubled = computed(() => count.get() * 2);
|
|
291
|
+
doubled.peek(); // 10 — no tracking
|
|
292
|
+
|
|
293
|
+
// Useful inside computed/effects to read without creating a dependency
|
|
294
|
+
const a = state(1);
|
|
295
|
+
const b = state(2);
|
|
296
|
+
|
|
297
|
+
const result = computed(() => {
|
|
298
|
+
// a is tracked, b is not
|
|
299
|
+
return a.get() + b.peek();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
result.get(); // 3
|
|
303
|
+
|
|
304
|
+
b.set(100);
|
|
305
|
+
result.get(); // 3 (b is not tracked)
|
|
306
|
+
|
|
307
|
+
a.set(10);
|
|
308
|
+
result.get(); // 110 (recomputed, picks up current b)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### `watch(signal)` / `unwatch(signal)` / `getPending()`
|
|
312
|
+
|
|
313
|
+
Low-level API for building custom scheduling. Used internally to manage effect execution.
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
import { computed, watch, unwatch, getPending } from '@esmj/signals';
|
|
317
|
+
|
|
318
|
+
const c = computed(() => /* ... */);
|
|
319
|
+
|
|
320
|
+
// Register a signal with the global watcher
|
|
321
|
+
watch(c);
|
|
322
|
+
|
|
323
|
+
// Get all signals with pending updates
|
|
324
|
+
const pending = getPending();
|
|
325
|
+
pending.forEach((p) => p.get());
|
|
326
|
+
|
|
327
|
+
// Unregister a signal
|
|
328
|
+
unwatch(c);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### `createWatcher(notify)`
|
|
332
|
+
|
|
333
|
+
Creates a custom watcher with a custom notification strategy. Replaces the default watcher (which uses `queueMicrotask`).
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
import { createWatcher, getPending } from '@esmj/signals';
|
|
337
|
+
|
|
338
|
+
// Synchronous flush strategy
|
|
339
|
+
createWatcher(() => {
|
|
340
|
+
for (const pending of getPending()) {
|
|
341
|
+
pending.get();
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Or requestAnimationFrame-based strategy for UI
|
|
346
|
+
createWatcher(() => {
|
|
347
|
+
requestAnimationFrame(() => {
|
|
348
|
+
for (const pending of getPending()) {
|
|
349
|
+
pending.get();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### `onFlush(callback)`
|
|
356
|
+
|
|
357
|
+
Registers a one-shot callback that runs **once** after the next flush cycle completes (i.e. after all pending effects have run). Useful for DOM measurements, post-update coordination, or any work that depends on effects being settled.
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
import { state, effect, onFlush } from '@esmj/signals';
|
|
361
|
+
|
|
362
|
+
const count = state(0);
|
|
363
|
+
|
|
364
|
+
effect(() => {
|
|
365
|
+
document.title = `Count: ${count.get()}`;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
count.set(42);
|
|
369
|
+
|
|
370
|
+
onFlush(() => {
|
|
371
|
+
// DOM is now updated — safe to measure
|
|
372
|
+
console.log(document.title); // "Count: 42"
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Multiple callbacks are supported and run in registration order:
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
onFlush(() => console.log('first'));
|
|
380
|
+
onFlush(() => console.log('second'));
|
|
381
|
+
// After flush: "first", "second"
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Callbacks are one-shot — they do not persist across flush cycles:
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
onFlush(() => console.log('once'));
|
|
388
|
+
|
|
389
|
+
count.set(1);
|
|
390
|
+
// after flush: logs "once"
|
|
391
|
+
|
|
392
|
+
count.set(2);
|
|
393
|
+
// after flush: (nothing — callback was cleared)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### `afterFlush()`
|
|
397
|
+
|
|
398
|
+
Returns a promise that resolves after the next flush cycle completes. A convenience wrapper around `onFlush`. Especially useful in async code and tests:
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
import { state, effect, afterFlush } from '@esmj/signals';
|
|
402
|
+
|
|
403
|
+
const count = state(0);
|
|
404
|
+
|
|
405
|
+
effect(() => {
|
|
406
|
+
console.log(count.get());
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
count.set(42);
|
|
410
|
+
|
|
411
|
+
await afterFlush();
|
|
412
|
+
// All effects have run, all side effects settled
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Works seamlessly with `batch`:
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
import { state, effect, batch, afterFlush } from '@esmj/signals';
|
|
419
|
+
|
|
420
|
+
const a = state(1);
|
|
421
|
+
const b = state(2);
|
|
422
|
+
let sum = null;
|
|
423
|
+
|
|
424
|
+
effect(() => {
|
|
425
|
+
sum = a.get() + b.get();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
batch(() => {
|
|
429
|
+
a.set(10);
|
|
430
|
+
b.set(20);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await afterFlush();
|
|
434
|
+
console.log(sum); // 30
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Flush Strategy
|
|
438
|
+
|
|
439
|
+
Effects are scheduled to run via `queueMicrotask` after signal updates. This means they run **before the next paint** but **after the current synchronous code finishes**:
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const count = state(0);
|
|
443
|
+
let logged = null;
|
|
444
|
+
|
|
445
|
+
effect(() => {
|
|
446
|
+
logged = count.get();
|
|
447
|
+
});
|
|
448
|
+
// logged === 0
|
|
449
|
+
|
|
450
|
+
count.set(1);
|
|
451
|
+
// logged === 0 (microtask hasn't run yet)
|
|
452
|
+
|
|
453
|
+
await afterFlush();
|
|
454
|
+
// logged === 1 (microtask ran)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Multiple `set()` calls are coalesced — the effect runs only once:
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
count.set(1);
|
|
461
|
+
count.set(2);
|
|
462
|
+
count.set(3);
|
|
463
|
+
await afterFlush();
|
|
464
|
+
// effect ran once with count === 3
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Error Handling
|
|
468
|
+
|
|
469
|
+
Errors in `computed` callbacks are captured and re-thrown on `.get()` or `.peek()`. The error state is tracked separately from the value, so **state signals can hold `Error` objects as legitimate values**:
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
import { state, computed } from '@esmj/signals';
|
|
473
|
+
|
|
474
|
+
// State signals can store Error objects — they are values, not errors
|
|
475
|
+
const validationError = state(new Error('field required'));
|
|
476
|
+
validationError.get(); // Error { message: 'field required' } — returned, not thrown
|
|
477
|
+
|
|
478
|
+
// Computed signals throw when their callback throws
|
|
479
|
+
const a = state(0);
|
|
480
|
+
const safe = computed(() => {
|
|
481
|
+
if (a.get() === 0) {
|
|
482
|
+
throw new Error('Cannot be zero');
|
|
483
|
+
}
|
|
484
|
+
return 100 / a.get();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
safe.get();
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.log(e.message); // 'Cannot be zero'
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Recovers when dependency changes
|
|
494
|
+
a.set(5);
|
|
495
|
+
safe.get(); // 20
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Errors propagate through computed chains:
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
const source = state(0);
|
|
502
|
+
const a = computed(() => {
|
|
503
|
+
if (source.get() === 0) throw new Error('bad');
|
|
504
|
+
return source.get() * 2;
|
|
505
|
+
});
|
|
506
|
+
const b = computed(() => a.get() + 10);
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
b.get(); // throws 'bad' — propagated from a
|
|
510
|
+
} catch (e) {}
|
|
511
|
+
|
|
512
|
+
source.set(5);
|
|
513
|
+
b.get(); // 20 — recovered
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Cycle Detection
|
|
517
|
+
|
|
518
|
+
Circular dependencies between computed signals are detected and throw a clear error instead of causing a stack overflow:
|
|
519
|
+
|
|
520
|
+
```javascript
|
|
521
|
+
import { computed } from '@esmj/signals';
|
|
522
|
+
|
|
523
|
+
const a = computed(() => b.get() + 1);
|
|
524
|
+
const b = computed(() => a.get() + 1);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
a.get();
|
|
528
|
+
} catch (e) {
|
|
529
|
+
console.log(e.message); // 'Cycle detected in computed signal'
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
This applies to any cycle length — self-referencing, two-node, three-node, etc. Diamond dependencies (where multiple paths lead to the same signal without a cycle) are handled correctly and do not trigger false positives.
|
|
534
|
+
|
|
535
|
+
## TC39 Signals Proposal Alignment
|
|
536
|
+
|
|
537
|
+
This library follows the API shape and semantics of the [TC39 Signals proposal](https://github.com/tc39/proposal-signals):
|
|
538
|
+
|
|
539
|
+
| TC39 Proposal | @esmj/signals | Status |
|
|
540
|
+
|---------------|---------------|--------|
|
|
541
|
+
| `Signal.State` | `state` / `createSignal` | ✅ |
|
|
542
|
+
| `Signal.Computed` | `computed` | ✅ |
|
|
543
|
+
| `Signal.subtle.Watcher` | `createWatcher` / `watch` / `unwatch` | ✅ |
|
|
544
|
+
| `Signal.subtle.untrack` | `untrack` | ✅ |
|
|
545
|
+
| `Signal.subtle.Watcher.prototype.getPending` | `getPending` | ✅ |
|
|
546
|
+
| Effect (userland in proposal) | `effect` | ✅ |
|
|
547
|
+
| Batch (userland in proposal) | `batch` | ✅ |
|
|
548
|
+
|
|
549
|
+
## Exports
|
|
550
|
+
|
|
551
|
+
| Export | Description |
|
|
552
|
+
|--------|-------------|
|
|
553
|
+
| `state` | Create a reactive signal (alias: `createSignal`) |
|
|
554
|
+
| `createSignal` | Create a reactive signal |
|
|
555
|
+
| `computed` | Create a derived/memoized signal |
|
|
556
|
+
| `effect` | Create a reactive side effect |
|
|
557
|
+
| `batch` | Batch multiple updates |
|
|
558
|
+
| `untrack` | Read signals without tracking |
|
|
559
|
+
| `signal.peek()` | Read signal value without tracking |
|
|
560
|
+
| `watch` | Register a signal with the watcher |
|
|
561
|
+
| `unwatch` | Unregister a signal from the watcher |
|
|
562
|
+
| `getPending` | Get pending signals |
|
|
563
|
+
| `createWatcher` | Create a custom watcher |
|
|
564
|
+
| `onFlush` | Register a one-shot post-flush callback |
|
|
565
|
+
| `afterFlush` | Returns a promise that resolves after flush |
|
|
566
|
+
|
|
567
|
+
## License
|
|
568
|
+
|
|
569
|
+
[MIT](./LICENSE)
|