@bodil/signal 0.3.5 → 0.4.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/dist/array.d.ts +149 -15
- package/dist/array.js +230 -53
- package/dist/array.js.map +1 -1
- package/dist/array.test.js +34 -1
- package/dist/array.test.js.map +1 -1
- package/dist/index.d.ts +1 -120
- package/dist/index.js +1 -197
- package/dist/index.js.map +1 -1
- package/dist/map.d.ts +130 -0
- package/dist/map.js +227 -0
- package/dist/map.js.map +1 -0
- package/dist/map.test.d.ts +1 -0
- package/dist/map.test.js +52 -0
- package/dist/map.test.js.map +1 -0
- package/dist/signal.d.ts +200 -0
- package/dist/signal.js +263 -0
- package/dist/signal.js.map +1 -0
- package/dist/signal.test.js +6 -6
- package/dist/signal.test.js.map +1 -1
- package/package.json +4 -1
- package/src/array.test.ts +36 -1
- package/src/array.ts +250 -67
- package/src/index.ts +1 -265
- package/src/map.test.ts +62 -0
- package/src/map.ts +263 -0
- package/src/signal.test.ts +7 -7
- package/src/signal.ts +361 -0
package/src/signal.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { AbortablePromise } from "@bodil/core/async";
|
|
2
|
+
import { toDisposable, type Disposifiable } from "@bodil/core/disposable";
|
|
3
|
+
import { Err, Ok, type Result } from "@bodil/opt";
|
|
4
|
+
import { Signal as SignalPolyfill } from "signal-polyfill";
|
|
5
|
+
|
|
6
|
+
import { ReadonlySignalArray, SignalArray } from "./array";
|
|
7
|
+
import { ReadonlySignalMap, SignalMap } from "./map";
|
|
8
|
+
|
|
9
|
+
const SystemSignal = ((globalThis as any).Signal as typeof SignalPolyfill) ?? SignalPolyfill;
|
|
10
|
+
|
|
11
|
+
/** @namespace */
|
|
12
|
+
export const subtle = SystemSignal.subtle;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
14
|
+
export namespace subtle {
|
|
15
|
+
/** @hidden */
|
|
16
|
+
export type Watcher = SignalPolyfill.subtle.Watcher;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Options<A> = {
|
|
20
|
+
/** @internal */
|
|
21
|
+
[subtle.unwatched]?: () => void;
|
|
22
|
+
/** @internal */
|
|
23
|
+
[subtle.watched]?: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* A function used to compare the new value of a signal with the previous
|
|
26
|
+
* value when the signal's value updates. If the function returns false,
|
|
27
|
+
* this will cause the signal's dependencies to update themselves with the
|
|
28
|
+
* new value, because the value is considered to have changed. If the
|
|
29
|
+
* function returns true, dependencies will not be updated, because no
|
|
30
|
+
* change is considered to have occurred.
|
|
31
|
+
*
|
|
32
|
+
* This option defaults to using {@link Object.is}.
|
|
33
|
+
*/
|
|
34
|
+
equals?: (a: A, b: A) => boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface ISignal<A> {
|
|
38
|
+
/**
|
|
39
|
+
* The current value of the signal.
|
|
40
|
+
*
|
|
41
|
+
* When this is read inside a computation function or an effect function,
|
|
42
|
+
* this signal is automatically added to the effect or computed signal as a
|
|
43
|
+
* dependency.
|
|
44
|
+
*/
|
|
45
|
+
readonly value: A;
|
|
46
|
+
/**
|
|
47
|
+
* Construct a {@link Signal.Computed} signal using a mapping function over
|
|
48
|
+
* the current value of this signal.
|
|
49
|
+
*/
|
|
50
|
+
map<B>(fn: (value: A) => B): Computed<B>;
|
|
51
|
+
/**
|
|
52
|
+
* Subscribe to changes to the value of this signal.
|
|
53
|
+
*/
|
|
54
|
+
on(callback: (value: A) => void): Disposable;
|
|
55
|
+
/**
|
|
56
|
+
* Get the current value of the signal.
|
|
57
|
+
*
|
|
58
|
+
* When this is called inside a computation function or an effect function,
|
|
59
|
+
* this signal is automatically added to the effect or computed signal as a
|
|
60
|
+
* dependency.
|
|
61
|
+
*/
|
|
62
|
+
get(): A;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A signal which contains a writable value.
|
|
67
|
+
*/
|
|
68
|
+
export class State<A> extends SystemSignal.State<A> implements ISignal<A> {
|
|
69
|
+
get value(): A {
|
|
70
|
+
return this.get();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
set value(value: A) {
|
|
74
|
+
this.set(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get(): A {
|
|
78
|
+
return super.get();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set the current value of the signal.
|
|
83
|
+
*
|
|
84
|
+
* This triggers an update of every dependency of the signal. Computed
|
|
85
|
+
* signals will recompute the next time they're read, and effects will be
|
|
86
|
+
* scheduled to run as soon as possible.
|
|
87
|
+
*/
|
|
88
|
+
set(value: A): void {
|
|
89
|
+
super.set(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @internal
|
|
94
|
+
* @hidden
|
|
95
|
+
*/
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
97
|
+
constructor(value: A, options?: Options<A>) {
|
|
98
|
+
super(value, options);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Update the current value of this signal using a function.
|
|
102
|
+
*/
|
|
103
|
+
update(fn: (value: A) => A): void {
|
|
104
|
+
this.set(SystemSignal.subtle.untrack(() => fn(this.get())));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get a read only version of this signal.
|
|
109
|
+
*/
|
|
110
|
+
readOnly(): Computed<A> {
|
|
111
|
+
return computed(() => this.get());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
map<B>(fn: (value: A) => B): Computed<B> {
|
|
115
|
+
return computed(() => fn(this.get()));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
on(callback: (value: A) => void): Disposable {
|
|
119
|
+
return subscribe(this, callback);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Test whether a value is a {@link Signal.State} signal.
|
|
124
|
+
*/
|
|
125
|
+
static is(v: unknown): v is State<unknown> {
|
|
126
|
+
return v instanceof State;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A signal which contains a reactive computation.
|
|
132
|
+
*
|
|
133
|
+
* @property get
|
|
134
|
+
* Get the current value of the signal.
|
|
135
|
+
*
|
|
136
|
+
* When this is called inside a computation function or an effect function,
|
|
137
|
+
* this signal is automatically added to the effect or computed signal as a
|
|
138
|
+
* dependency.
|
|
139
|
+
*/
|
|
140
|
+
export class Computed<A> extends SystemSignal.Computed<A> implements ISignal<A> {
|
|
141
|
+
get value(): A {
|
|
142
|
+
return this.get();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get(): A {
|
|
146
|
+
return super.get();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @internal
|
|
151
|
+
* @hidden
|
|
152
|
+
*/
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
154
|
+
constructor(computeFn: () => A, options?: Options<A>) {
|
|
155
|
+
super(computeFn, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
map<B>(fn: (value: A) => B): Computed<B> {
|
|
159
|
+
return computed(() => fn(this.get()));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
on(callback: (value: A) => void): Disposable {
|
|
163
|
+
return subscribe(this, callback);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Test whether a value is a {@link Signal.Computed} signal.
|
|
168
|
+
*/
|
|
169
|
+
static is(v: unknown): v is Computed<unknown> {
|
|
170
|
+
return v instanceof Computed;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A type representing any kind of signal, either a {@link Signal.State} or a
|
|
176
|
+
* {@link Signal.Computed}.
|
|
177
|
+
*/
|
|
178
|
+
export type Any<A> = State<A> | Computed<A>;
|
|
179
|
+
|
|
180
|
+
let effectNeedsEnqueue = true;
|
|
181
|
+
const effectWatcher = new SystemSignal.subtle.Watcher(() => {
|
|
182
|
+
if (effectNeedsEnqueue) {
|
|
183
|
+
effectNeedsEnqueue = false;
|
|
184
|
+
queueMicrotask(effectProcess);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
function effectProcess(): void {
|
|
189
|
+
effectNeedsEnqueue = true;
|
|
190
|
+
for (const sig of effectWatcher.getPending()) {
|
|
191
|
+
sig.get();
|
|
192
|
+
}
|
|
193
|
+
effectWatcher.watch();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Test whether the given value is a signal.
|
|
198
|
+
*
|
|
199
|
+
* @see {@link Signal.State.is}, {@link Signal.Computed.is}
|
|
200
|
+
*/
|
|
201
|
+
export function is(v: unknown): v is Any<unknown> {
|
|
202
|
+
return State.is(v) || Computed.is(v);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Construct a new {@link Signal.State} signal containing the provided value.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* const sig = Signal.state("Hello Joe!");
|
|
210
|
+
*/
|
|
211
|
+
export function from<A>(value: A, options?: Options<A>): State<A> {
|
|
212
|
+
return new State(value, options);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* {@inheritDoc from}
|
|
217
|
+
* @function
|
|
218
|
+
*/
|
|
219
|
+
export const state = from;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Construct a new {@link Signal.Computed} signal using the provided
|
|
223
|
+
* computation function.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* const sig1 = Signal.from(2);
|
|
227
|
+
* const sig2 = Signal.from(3);
|
|
228
|
+
* const sum = Signal.computed(() => sig1.get() + sig2.get());
|
|
229
|
+
* assert(sum.get() === 5);
|
|
230
|
+
* sig1.set(4);
|
|
231
|
+
* assert(sum.get() === 7);
|
|
232
|
+
*/
|
|
233
|
+
export function computed<A>(fn: (this: Computed<A>) => A, options?: Options<A>): Computed<A> {
|
|
234
|
+
return new Computed(fn, options);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Suscribe to a signal.
|
|
239
|
+
*
|
|
240
|
+
* The provided callback will be called every time the value of the
|
|
241
|
+
* signal changes.
|
|
242
|
+
*/
|
|
243
|
+
export function subscribe<A>(signal: Any<A>, callback: (value: A) => void): Disposable {
|
|
244
|
+
return effect(() => callback(signal.value));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create an effect responding to signal changes.
|
|
249
|
+
*
|
|
250
|
+
* The provided function will be called immediately, and again every
|
|
251
|
+
* time a signal that was read by the function changes.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* const sig = Signal.from("Hello Joe!");
|
|
255
|
+
* effect(() => console.log("Signal value is:", sig.get()));
|
|
256
|
+
* // prints "Signal value is: Hello Joe!"
|
|
257
|
+
* sig.set("Hello Mike!");
|
|
258
|
+
* // prints "Signal value is: Hello Mike!"
|
|
259
|
+
*/
|
|
260
|
+
export function effect(fn: () => Disposifiable | void): Disposable {
|
|
261
|
+
let cleanup: Disposable | undefined;
|
|
262
|
+
const computed = new Computed(() => {
|
|
263
|
+
if (cleanup !== undefined) {
|
|
264
|
+
cleanup[Symbol.dispose]();
|
|
265
|
+
}
|
|
266
|
+
const result = fn();
|
|
267
|
+
cleanup = result !== undefined ? toDisposable(result) : undefined;
|
|
268
|
+
});
|
|
269
|
+
effectWatcher.watch(computed);
|
|
270
|
+
computed.get();
|
|
271
|
+
return toDisposable(() => {
|
|
272
|
+
effectWatcher.unwatch(computed);
|
|
273
|
+
if (cleanup !== undefined) {
|
|
274
|
+
cleanup[Symbol.dispose]();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Construct a new {@link Signal.Computed} signal using an async
|
|
281
|
+
* computation function.
|
|
282
|
+
*
|
|
283
|
+
* This returns a promise which will resolve to a
|
|
284
|
+
* {@link Signal.Computed} signal once the promise returned by the
|
|
285
|
+
* computation function resolves, and will update itself whenever
|
|
286
|
+
* subsequent calls to the computation function resolve.
|
|
287
|
+
*
|
|
288
|
+
* The function is provided with an {@link AbortSignal} which any async
|
|
289
|
+
* jobs started from it should abide by. If a signal dependency changes
|
|
290
|
+
* while the job is running, the {@link AbortSignal} will be triggered
|
|
291
|
+
* and the job restarted.
|
|
292
|
+
*
|
|
293
|
+
* @experimental
|
|
294
|
+
*/
|
|
295
|
+
export function asyncComputed<A>(
|
|
296
|
+
fn: (abort: AbortSignal) => Promise<A>,
|
|
297
|
+
options?: Options<A>,
|
|
298
|
+
): Promise<Computed<A>> {
|
|
299
|
+
const result = Promise.withResolvers<Computed<A>>();
|
|
300
|
+
const stream = computed(() =>
|
|
301
|
+
AbortablePromise.run<A>((resolve, reject, abort) => {
|
|
302
|
+
try {
|
|
303
|
+
fn(abort).then(resolve, reject);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
reject(e as Error);
|
|
306
|
+
}
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
const sig = new State<Result<A, Error>>(Err(new Error()));
|
|
310
|
+
let job: AbortablePromise<A> | undefined = undefined;
|
|
311
|
+
let resolved = false;
|
|
312
|
+
const resolve = () => {
|
|
313
|
+
if (!resolved) {
|
|
314
|
+
resolved = true;
|
|
315
|
+
result.resolve(computed(() => sig.get().unwrapExact(), options));
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
effect(() => {
|
|
319
|
+
if (job !== undefined) {
|
|
320
|
+
job.abort();
|
|
321
|
+
}
|
|
322
|
+
job = stream.get();
|
|
323
|
+
job.then(
|
|
324
|
+
(next) => {
|
|
325
|
+
sig.set(Ok(next));
|
|
326
|
+
resolve();
|
|
327
|
+
},
|
|
328
|
+
(error) => {
|
|
329
|
+
if (job?.signal.aborted === true) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
sig.set(Err(error));
|
|
333
|
+
resolve();
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
return result.promise;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create a new {@link Signal.Array} and optionally populate it from the
|
|
342
|
+
* provided {@link Iterable}.
|
|
343
|
+
*/
|
|
344
|
+
export function array<A>(values?: Iterable<A>): SignalArray<A> {
|
|
345
|
+
return values === undefined ? new SignalArray() : SignalArray.from(values);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Create a new {@link Signal.Map} and optionally populate it from the
|
|
350
|
+
* provided {@link Iterable}.
|
|
351
|
+
*/
|
|
352
|
+
export function map<K, V>(entries?: Iterable<[K, V]>): SignalMap<K, V> {
|
|
353
|
+
return new SignalMap(entries);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {
|
|
357
|
+
SignalArray as Array,
|
|
358
|
+
ReadonlySignalArray as ReadonlyArray,
|
|
359
|
+
SignalMap as Map,
|
|
360
|
+
ReadonlySignalMap as ReadonlyMap,
|
|
361
|
+
};
|