signalium 0.1.0 → 0.2.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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/signals.d.ts +12 -5
- package/dist/signals.js +76 -6
- package/package.json +1 -1
- package/src/__tests__/async.test.ts +54 -295
- package/src/__tests__/basic.test.ts +1 -1
- package/src/__tests__/subscription.test.ts +0 -1
- package/src/__tests__/utils/instrumented.ts +4 -5
- package/src/index.ts +1 -0
- package/src/signals.ts +105 -18
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# signalium
|
2
2
|
|
3
|
+
## 0.2.0
|
4
|
+
|
5
|
+
### Minor Changes
|
6
|
+
|
7
|
+
- 4696d06: Refactor await and invalidate to make them more composable
|
8
|
+
|
9
|
+
## 0.1.1
|
10
|
+
|
11
|
+
### Patch Changes
|
12
|
+
|
13
|
+
- 033a814: Add await and invalidate to async signals
|
14
|
+
|
3
15
|
## 0.1.0
|
4
16
|
|
5
17
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
export type { Signal, WriteableSignal, SignalCompute, SignalAsyncCompute, SignalSubscribe, SignalEquals, SignalOptions, SignalOptionsWithInit, SignalSubscription, SignalWatcherEffect, AsyncPending, AsyncReady, AsyncResult, } from './signals.js';
|
1
|
+
export type { Signal, AsyncSignal, WriteableSignal, SignalCompute, SignalAsyncCompute, SignalSubscribe, SignalEquals, SignalOptions, SignalOptionsWithInit, SignalSubscription, SignalWatcherEffect, AsyncPending, AsyncReady, AsyncResult, } from './signals.js';
|
2
2
|
export { state, computed, asyncComputed, subscription, watcher } from './signals.js';
|
package/dist/signals.d.ts
CHANGED
@@ -10,6 +10,7 @@ export interface Signal<T = unknown> {
|
|
10
10
|
export interface WriteableSignal<T> extends Signal<T> {
|
11
11
|
set(value: T): void;
|
12
12
|
}
|
13
|
+
export type AsyncSignal<T> = Signal<AsyncResult<T>>;
|
13
14
|
export type SignalCompute<T> = (prev: T | undefined) => T;
|
14
15
|
export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
|
15
16
|
export type SignalWatcherEffect = () => void;
|
@@ -64,7 +65,11 @@ export declare class ComputedSignal<T> {
|
|
64
65
|
_dirtyConsumers(): void;
|
65
66
|
_disconnect(count?: number): void;
|
66
67
|
}
|
67
|
-
export interface
|
68
|
+
export interface AsyncBaseResult<T> {
|
69
|
+
invalidate(): void;
|
70
|
+
await(): T;
|
71
|
+
}
|
72
|
+
export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
68
73
|
result: undefined;
|
69
74
|
error: unknown;
|
70
75
|
isPending: boolean;
|
@@ -72,7 +77,7 @@ export interface AsyncPending {
|
|
72
77
|
isError: boolean;
|
73
78
|
isSuccess: boolean;
|
74
79
|
}
|
75
|
-
export interface AsyncReady<T> {
|
80
|
+
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
76
81
|
result: T;
|
77
82
|
error: unknown;
|
78
83
|
isPending: boolean;
|
@@ -80,7 +85,7 @@ export interface AsyncReady<T> {
|
|
80
85
|
isError: boolean;
|
81
86
|
isSuccess: boolean;
|
82
87
|
}
|
83
|
-
export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
|
88
|
+
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
84
89
|
declare class StateSignal<T> implements StateSignal<T> {
|
85
90
|
private _value;
|
86
91
|
private _equals;
|
@@ -91,13 +96,15 @@ declare class StateSignal<T> implements StateSignal<T> {
|
|
91
96
|
}
|
92
97
|
export declare function state<T>(initialValue: T, opts?: SignalOptions<T>): StateSignal<T>;
|
93
98
|
export declare function computed<T>(compute: (prev: T | undefined) => T, opts?: SignalOptions<T>): Signal<T>;
|
94
|
-
export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts?: SignalOptions<T>):
|
95
|
-
export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts: SignalOptionsWithInit<T>):
|
99
|
+
export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
|
100
|
+
export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts: SignalOptionsWithInit<T>): AsyncSignal<T>;
|
96
101
|
export declare function subscription<T>(subscribe: SignalSubscribe<T>, opts?: SignalOptions<T>): Signal<T | undefined>;
|
97
102
|
export declare function subscription<T>(subscribe: SignalSubscribe<T>, opts: SignalOptionsWithInit<T>): Signal<T>;
|
98
103
|
export interface Watcher {
|
99
104
|
disconnect(): void;
|
105
|
+
subscribe(subscriber: () => void): () => void;
|
100
106
|
}
|
101
107
|
export declare function watcher(fn: () => void): Watcher;
|
108
|
+
export declare function isTracking(): boolean;
|
102
109
|
export declare function untrack<T = void>(fn: () => T): T;
|
103
110
|
export {};
|
package/dist/signals.js
CHANGED
@@ -3,9 +3,12 @@ let CURRENT_CONSUMER;
|
|
3
3
|
let CURRENT_DEP_TAIL;
|
4
4
|
let CURRENT_ORD = 0;
|
5
5
|
let CURRENT_IS_WATCHED = false;
|
6
|
+
let CURRENT_IS_WAITING = false;
|
6
7
|
let CURRENT_SEEN;
|
7
8
|
let id = 0;
|
8
9
|
const SUBSCRIPTIONS = new WeakMap();
|
10
|
+
const ACTIVE_ASYNCS = new WeakMap();
|
11
|
+
const WAITING = Symbol();
|
9
12
|
let linkPool;
|
10
13
|
function linkNewDep(dep, sub, nextDep, depsTail, ord) {
|
11
14
|
let newLink;
|
@@ -94,7 +97,6 @@ function clearTrack(link, shouldDisconnect) {
|
|
94
97
|
link = nextDep;
|
95
98
|
} while (link !== undefined);
|
96
99
|
}
|
97
|
-
// const registry = new FinalizationRegistry(poolLink);
|
98
100
|
export class ComputedSignal {
|
99
101
|
id = id++;
|
100
102
|
_type;
|
@@ -232,11 +234,61 @@ export class ComputedSignal {
|
|
232
234
|
isReady: false,
|
233
235
|
isError: false,
|
234
236
|
isSuccess: false,
|
237
|
+
invalidate: () => {
|
238
|
+
this._state = 2 /* SignalState.Dirty */;
|
239
|
+
this._dirty();
|
240
|
+
},
|
241
|
+
await: () => {
|
242
|
+
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== 2 /* SignalType.Async */) {
|
243
|
+
throw new Error('Cannot await an async signal outside of an async signal. If you are using an async function, you must use signal.await() for all async signals _before_ the first language-level `await` keyword statement (e.g. it must be synchronous).');
|
244
|
+
}
|
245
|
+
if (value.isPending) {
|
246
|
+
const currentConsumer = CURRENT_CONSUMER;
|
247
|
+
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
248
|
+
CURRENT_IS_WAITING = true;
|
249
|
+
throw WAITING;
|
250
|
+
}
|
251
|
+
else if (value.isError) {
|
252
|
+
throw value.error;
|
253
|
+
}
|
254
|
+
return value.result;
|
255
|
+
},
|
235
256
|
});
|
236
|
-
|
237
|
-
|
257
|
+
let nextValue;
|
258
|
+
try {
|
259
|
+
CURRENT_IS_WAITING = false;
|
260
|
+
nextValue = this._compute(value?.result);
|
261
|
+
}
|
262
|
+
catch (e) {
|
263
|
+
if (e !== WAITING) {
|
264
|
+
value.error = e;
|
265
|
+
value.isPending = false;
|
266
|
+
value.isError = true;
|
267
|
+
this._version++;
|
268
|
+
}
|
269
|
+
break;
|
270
|
+
}
|
271
|
+
if (CURRENT_IS_WAITING) {
|
272
|
+
if (!value.isPending) {
|
273
|
+
value.isPending = true;
|
274
|
+
value.isError = false;
|
275
|
+
value.isSuccess = false;
|
276
|
+
this._version++;
|
277
|
+
}
|
278
|
+
if (nextValue instanceof Promise) {
|
279
|
+
nextValue.catch((e) => {
|
280
|
+
if (e !== WAITING) {
|
281
|
+
value.error = e;
|
282
|
+
value.isPending = false;
|
283
|
+
value.isError = true;
|
284
|
+
this._version++;
|
285
|
+
}
|
286
|
+
});
|
287
|
+
}
|
288
|
+
}
|
289
|
+
else if (nextValue instanceof Promise) {
|
238
290
|
const currentVersion = ++this._version;
|
239
|
-
nextValue.then(result => {
|
291
|
+
nextValue = nextValue.then(result => {
|
240
292
|
if (currentVersion !== this._version) {
|
241
293
|
return;
|
242
294
|
}
|
@@ -247,7 +299,7 @@ export class ComputedSignal {
|
|
247
299
|
this._version++;
|
248
300
|
this._dirtyConsumers();
|
249
301
|
}, error => {
|
250
|
-
if (currentVersion !== this._version) {
|
302
|
+
if (currentVersion !== this._version || error === WAITING) {
|
251
303
|
return;
|
252
304
|
}
|
253
305
|
value.error = error;
|
@@ -256,6 +308,7 @@ export class ComputedSignal {
|
|
256
308
|
this._version++;
|
257
309
|
this._dirtyConsumers();
|
258
310
|
});
|
311
|
+
ACTIVE_ASYNCS.set(this, nextValue);
|
259
312
|
value.isPending = true;
|
260
313
|
value.isError = false;
|
261
314
|
value.isSuccess = false;
|
@@ -447,14 +500,31 @@ export function subscription(subscribe, opts) {
|
|
447
500
|
return new ComputedSignal(1 /* SignalType.Subscription */, subscribe, opts?.equals, opts?.initValue);
|
448
501
|
}
|
449
502
|
export function watcher(fn) {
|
450
|
-
const
|
503
|
+
const subscribers = new Set();
|
504
|
+
const watcher = new ComputedSignal(3 /* SignalType.Watcher */, () => {
|
505
|
+
fn();
|
506
|
+
untrack(() => {
|
507
|
+
for (const subscriber of subscribers) {
|
508
|
+
subscriber();
|
509
|
+
}
|
510
|
+
});
|
511
|
+
});
|
451
512
|
scheduleWatcher(watcher);
|
452
513
|
return {
|
453
514
|
disconnect() {
|
454
515
|
scheduleDisconnect(watcher);
|
455
516
|
},
|
517
|
+
subscribe(subscriber) {
|
518
|
+
subscribers.add(subscriber);
|
519
|
+
return () => {
|
520
|
+
subscribers.delete(subscriber);
|
521
|
+
};
|
522
|
+
},
|
456
523
|
};
|
457
524
|
}
|
525
|
+
export function isTracking() {
|
526
|
+
return CURRENT_CONSUMER !== undefined;
|
527
|
+
}
|
458
528
|
export function untrack(fn) {
|
459
529
|
const prevConsumer = CURRENT_CONSUMER;
|
460
530
|
const prevOrd = CURRENT_ORD;
|
package/package.json
CHANGED
@@ -9,17 +9,20 @@ const result = <T>(
|
|
9
9
|
value: T | undefined,
|
10
10
|
promiseState: 'pending' | 'error' | 'success',
|
11
11
|
isReady: boolean,
|
12
|
+
error?: any,
|
12
13
|
): AsyncResult<T> =>
|
13
14
|
({
|
14
15
|
result: value,
|
15
|
-
error
|
16
|
+
error,
|
16
17
|
isPending: promiseState === 'pending',
|
17
18
|
isReady,
|
18
19
|
isError: promiseState === 'error',
|
19
20
|
isSuccess: promiseState === 'success',
|
21
|
+
await: expect.any(Function),
|
22
|
+
invalidate: expect.any(Function),
|
20
23
|
}) as AsyncResult<T>;
|
21
24
|
|
22
|
-
describe
|
25
|
+
describe('Async Signal functionality', () => {
|
23
26
|
test('Can run basic computed', async () => {
|
24
27
|
const a = state(1);
|
25
28
|
const b = state(2);
|
@@ -165,333 +168,89 @@ describe.skip('Async Signal functionality', () => {
|
|
165
168
|
});
|
166
169
|
});
|
167
170
|
|
168
|
-
describe('
|
169
|
-
test('
|
170
|
-
const
|
171
|
-
|
172
|
-
const c = state(2);
|
171
|
+
describe('Awaiting', () => {
|
172
|
+
test('Awaiting a computed will resolve the value', async () => {
|
173
|
+
const compA = asyncComputed(async () => {
|
174
|
+
await sleep(20);
|
173
175
|
|
174
|
-
|
175
|
-
return a.get() + b.get();
|
176
|
+
return 1;
|
176
177
|
});
|
177
178
|
|
178
|
-
const
|
179
|
-
|
180
|
-
});
|
181
|
-
|
182
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
183
|
-
expect(outer).toHaveValueAndCounts(5, { compute: 1 });
|
184
|
-
|
185
|
-
// stability
|
186
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
187
|
-
expect(outer).toHaveValueAndCounts(5, { compute: 1 });
|
188
|
-
});
|
189
|
-
|
190
|
-
test('Can dirty inner computed and update parent', () => {
|
191
|
-
const a = state(1);
|
192
|
-
const b = state(2);
|
193
|
-
const c = state(2);
|
194
|
-
|
195
|
-
const inner = computed(() => {
|
196
|
-
return a.get() + b.get();
|
197
|
-
});
|
198
|
-
|
199
|
-
const outer = computed(() => {
|
200
|
-
return inner.get() + c.get();
|
201
|
-
});
|
202
|
-
|
203
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
204
|
-
expect(outer).toHaveValueAndCounts(5, { compute: 1 });
|
205
|
-
|
206
|
-
a.set(2);
|
207
|
-
|
208
|
-
expect(inner).toHaveValueAndCounts(4, { compute: 2 });
|
209
|
-
expect(outer).toHaveValueAndCounts(6, { compute: 2 });
|
210
|
-
});
|
211
|
-
|
212
|
-
test('Can dirty outer computed and inner stays cached', () => {
|
213
|
-
const a = state(1);
|
214
|
-
const b = state(2);
|
215
|
-
const c = state(2);
|
216
|
-
|
217
|
-
const inner = computed(() => {
|
218
|
-
return a.get() + b.get();
|
219
|
-
});
|
220
|
-
|
221
|
-
const outer = computed(() => {
|
222
|
-
return inner.get() + c.get();
|
223
|
-
});
|
224
|
-
|
225
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
226
|
-
expect(outer).toHaveValueAndCounts(5, { compute: 1 });
|
179
|
+
const compB = asyncComputed(async () => {
|
180
|
+
await sleep(20);
|
227
181
|
|
228
|
-
|
229
|
-
|
230
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
231
|
-
expect(outer).toHaveValueAndCounts(6, { compute: 2 });
|
232
|
-
});
|
233
|
-
|
234
|
-
test('Can nest multiple levels', () => {
|
235
|
-
const a = state(1);
|
236
|
-
const b = state(2);
|
237
|
-
const c = state(2);
|
238
|
-
const d = state(2);
|
239
|
-
|
240
|
-
const inner = computed(() => {
|
241
|
-
return a.get() + b.get();
|
182
|
+
return 2;
|
242
183
|
});
|
243
184
|
|
244
|
-
const
|
245
|
-
|
246
|
-
|
185
|
+
const compC = asyncComputed(async () => {
|
186
|
+
const a = compA.get().await();
|
187
|
+
const b = compB.get().await();
|
247
188
|
|
248
|
-
|
249
|
-
return mid.get() + d.get();
|
189
|
+
return a + b;
|
250
190
|
});
|
251
191
|
|
252
|
-
|
253
|
-
expect(
|
254
|
-
|
255
|
-
|
256
|
-
a.set(2);
|
257
|
-
|
258
|
-
expect(inner).toHaveValueAndCounts(4, { compute: 2 });
|
259
|
-
expect(mid).toHaveValueAndCounts(6, { compute: 2 });
|
260
|
-
expect(outer).toHaveValueAndCounts(8, { compute: 2 });
|
261
|
-
|
262
|
-
c.set(3);
|
263
|
-
|
264
|
-
expect(inner).toHaveValueAndCounts(4, { compute: 2 });
|
265
|
-
expect(mid).toHaveValueAndCounts(7, { compute: 3 });
|
266
|
-
expect(outer).toHaveValueAndCounts(9, { compute: 3 });
|
267
|
-
|
268
|
-
d.set(3);
|
269
|
-
|
270
|
-
expect(inner).toHaveValueAndCounts(4, { compute: 2 });
|
271
|
-
expect(mid).toHaveValueAndCounts(7, { compute: 3 });
|
272
|
-
expect(outer).toHaveValueAndCounts(10, { compute: 4 });
|
273
|
-
});
|
274
|
-
});
|
275
|
-
|
276
|
-
describe('Propagation', () => {
|
277
|
-
test('it works with multiple parents', () => {
|
278
|
-
const a = state(1);
|
279
|
-
const b = state(2);
|
280
|
-
const c = state(2);
|
281
|
-
const d = state(2);
|
282
|
-
|
283
|
-
const inner = computed(() => {
|
284
|
-
return a.get() + b.get();
|
192
|
+
// Pull once to start the computation, trigger the computation
|
193
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
|
194
|
+
compute: 1,
|
195
|
+
resolve: 0,
|
285
196
|
});
|
286
197
|
|
287
|
-
|
288
|
-
return inner.get() + c.get();
|
289
|
-
});
|
198
|
+
await nextTick();
|
290
199
|
|
291
|
-
|
292
|
-
|
200
|
+
// Check after a tick to make sure we didn't resolve early
|
201
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
|
202
|
+
compute: 1,
|
203
|
+
resolve: 0,
|
293
204
|
});
|
294
205
|
|
295
|
-
|
296
|
-
expect(outer1).toHaveValueAndCounts(5, { compute: 1 });
|
297
|
-
expect(outer2).toHaveValueAndCounts(5, { compute: 1 });
|
298
|
-
|
299
|
-
a.set(2);
|
300
|
-
|
301
|
-
expect(inner).toHaveValueAndCounts(4, { compute: 2 });
|
302
|
-
expect(outer1).toHaveValueAndCounts(6, { compute: 2 });
|
303
|
-
expect(outer2).toHaveValueAndCounts(6, { compute: 2 });
|
304
|
-
|
305
|
-
b.set(3);
|
206
|
+
await sleep(30);
|
306
207
|
|
307
|
-
|
308
|
-
expect(
|
309
|
-
|
310
|
-
|
311
|
-
c.set(3);
|
312
|
-
|
313
|
-
expect(inner).toHaveValueAndCounts(5, { compute: 3 });
|
314
|
-
expect(outer1).toHaveValueAndCounts(8, { compute: 4 });
|
315
|
-
expect(outer2).toHaveValueAndCounts(7, { compute: 3 });
|
316
|
-
|
317
|
-
d.set(3);
|
318
|
-
|
319
|
-
expect(inner).toHaveValueAndCounts(5, { compute: 3 });
|
320
|
-
expect(outer1).toHaveValueAndCounts(8, { compute: 4 });
|
321
|
-
expect(outer2).toHaveValueAndCounts(8, { compute: 4 });
|
322
|
-
});
|
323
|
-
|
324
|
-
test('it stops propagation if the result is the same', () => {
|
325
|
-
const a = state(1);
|
326
|
-
const b = state(2);
|
327
|
-
const c = state(2);
|
328
|
-
const d = state(2);
|
329
|
-
|
330
|
-
const inner = computed('inner', () => {
|
331
|
-
return a.get() + b.get();
|
208
|
+
// Check to make sure we don't resolve early after the first task completes
|
209
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
|
210
|
+
compute: 2,
|
211
|
+
resolve: 0,
|
332
212
|
});
|
333
213
|
|
334
|
-
|
335
|
-
return inner.get() + c.get();
|
336
|
-
});
|
214
|
+
await sleep(30);
|
337
215
|
|
338
|
-
|
339
|
-
|
216
|
+
expect(compC).toHaveValueAndCounts(result(3, 'success', true), {
|
217
|
+
compute: 3,
|
218
|
+
resolve: 1,
|
340
219
|
});
|
341
|
-
|
342
|
-
expect(() => {
|
343
|
-
expect(outer1).toHaveValueAndCounts(5, { compute: 1 });
|
344
|
-
expect(outer2).toHaveValueAndCounts(5, { compute: 1 });
|
345
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
346
|
-
}).toHaveComputedOrder(['outer1', 'inner', 'outer2']);
|
347
|
-
|
348
|
-
a.set(2);
|
349
|
-
b.set(1);
|
350
|
-
|
351
|
-
expect(() => {
|
352
|
-
expect(outer1).toHaveValueAndCounts(5, { compute: 1 });
|
353
|
-
expect(outer2).toHaveValueAndCounts(5, { compute: 1 });
|
354
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 2 });
|
355
|
-
}).toHaveComputedOrder(['inner']);
|
356
|
-
|
357
|
-
b.set(2);
|
358
|
-
|
359
|
-
expect(() => {
|
360
|
-
expect(outer2).toHaveValueAndCounts(6, { compute: 2 });
|
361
|
-
expect(outer1).toHaveValueAndCounts(6, { compute: 2 });
|
362
|
-
expect(inner).toHaveValueAndCounts(4, { compute: 3 });
|
363
|
-
}).toHaveComputedOrder(['inner', 'outer2', 'outer1']);
|
364
220
|
});
|
365
221
|
|
366
|
-
test('
|
367
|
-
const
|
368
|
-
|
369
|
-
const c = state(2);
|
370
|
-
const d = state(2);
|
222
|
+
test('Awaiting a computed can handle errors', async () => {
|
223
|
+
const compA = asyncComputed(async () => {
|
224
|
+
await sleep(20);
|
371
225
|
|
372
|
-
|
373
|
-
return a.get() + b.get();
|
226
|
+
throw 'error';
|
374
227
|
});
|
375
228
|
|
376
|
-
const
|
377
|
-
|
378
|
-
});
|
229
|
+
const compB = asyncComputed(async () => {
|
230
|
+
await sleep(20);
|
379
231
|
|
380
|
-
|
381
|
-
return d.get();
|
232
|
+
return 2;
|
382
233
|
});
|
383
234
|
|
384
|
-
const
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
expect(() => {
|
389
|
-
expect(outer).toHaveValueAndCounts(7, { compute: 1 });
|
390
|
-
}).toHaveComputedOrder(['outer', 'inner1', 'inner2', 'inner3']);
|
391
|
-
|
392
|
-
d.set(4);
|
393
|
-
a.set(2);
|
394
|
-
c.set(3);
|
395
|
-
b.set(1);
|
396
|
-
|
397
|
-
expect(() => {
|
398
|
-
expect(outer).toHaveValueAndCounts(10, { compute: 2 });
|
399
|
-
expect(inner1).toHaveCounts({ compute: 2 });
|
400
|
-
expect(inner2).toHaveCounts({ compute: 2 });
|
401
|
-
}).toHaveComputedOrder(['inner1', 'inner2', 'outer', 'inner3']);
|
402
|
-
});
|
403
|
-
});
|
404
|
-
|
405
|
-
describe('Laziness', () => {
|
406
|
-
test('it does not compute values that are not used', () => {
|
407
|
-
const a = state(1);
|
408
|
-
const b = state(2);
|
409
|
-
const c = state(2);
|
410
|
-
const d = state(2);
|
235
|
+
const compC = asyncComputed(async () => {
|
236
|
+
const a = compA.get().await();
|
237
|
+
const b = compB.get().await();
|
411
238
|
|
412
|
-
|
413
|
-
return a.get() + b.get();
|
239
|
+
return a + b;
|
414
240
|
});
|
415
241
|
|
416
|
-
|
417
|
-
|
242
|
+
// Pull once to start the computation, trigger the computation
|
243
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
|
244
|
+
compute: 1,
|
245
|
+
resolve: 0,
|
418
246
|
});
|
419
247
|
|
420
|
-
|
421
|
-
if (inner1.get() <= 3) {
|
422
|
-
return inner2.get();
|
423
|
-
} else {
|
424
|
-
return -1;
|
425
|
-
}
|
426
|
-
});
|
427
|
-
|
428
|
-
expect(outer).toHaveValueAndCounts(4, { compute: 1 });
|
429
|
-
|
430
|
-
a.set(2);
|
431
|
-
c.set(3);
|
432
|
-
|
433
|
-
expect(outer).toHaveValueAndCounts(-1, { compute: 2 });
|
434
|
-
expect(inner1).toHaveCounts({ compute: 2 });
|
435
|
-
expect(inner2).toHaveCounts({ compute: 1 });
|
436
|
-
});
|
437
|
-
});
|
438
|
-
|
439
|
-
describe('Equality', () => {
|
440
|
-
test('Does not update if value is the same (custom equality fn)', () => {
|
441
|
-
const a = state(1);
|
442
|
-
const b = state(2);
|
443
|
-
|
444
|
-
const c = computed(
|
445
|
-
() => {
|
446
|
-
return a.get() + b.get();
|
447
|
-
},
|
448
|
-
{
|
449
|
-
equals(prev, next) {
|
450
|
-
return Math.abs(prev - next) < 2;
|
451
|
-
},
|
452
|
-
},
|
453
|
-
);
|
248
|
+
await sleep(50);
|
454
249
|
|
455
|
-
expect(
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
expect(c).toHaveValueAndCounts(3, { compute: 2 });
|
460
|
-
});
|
461
|
-
|
462
|
-
test('It stops propagation if the result is the same (custom equality fn)', () => {
|
463
|
-
const a = state(1);
|
464
|
-
const b = state(2);
|
465
|
-
const c = state(2);
|
466
|
-
const d = state(2);
|
467
|
-
|
468
|
-
const inner = computed(
|
469
|
-
() => {
|
470
|
-
return a.get() + b.get();
|
471
|
-
},
|
472
|
-
{
|
473
|
-
equals(prev, next) {
|
474
|
-
return Math.abs(prev - next) < 2;
|
475
|
-
},
|
476
|
-
},
|
477
|
-
);
|
478
|
-
|
479
|
-
const outer1 = computed(() => {
|
480
|
-
return inner.get() + c.get();
|
250
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'error', false, 'error'), {
|
251
|
+
compute: 2,
|
252
|
+
resolve: 0,
|
481
253
|
});
|
482
|
-
|
483
|
-
const outer2 = computed(() => {
|
484
|
-
return inner.get() + d.get();
|
485
|
-
});
|
486
|
-
|
487
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 1 });
|
488
|
-
|
489
|
-
a.set(2);
|
490
|
-
b.set(2);
|
491
|
-
|
492
|
-
expect(inner).toHaveValueAndCounts(3, { compute: 2 });
|
493
|
-
expect(outer1).toHaveValueAndCounts(5, { compute: 1 });
|
494
|
-
expect(outer2).toHaveValueAndCounts(5, { compute: 1 });
|
495
254
|
});
|
496
255
|
});
|
497
256
|
});
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
2
2
|
import { state, computed } from './utils/instrumented.js';
|
3
3
|
|
4
|
-
describe
|
4
|
+
describe('Basic Signal functionality', () => {
|
5
5
|
test('Can run basic computed', () => {
|
6
6
|
const a = state(1);
|
7
7
|
const b = state(2);
|
@@ -10,13 +10,12 @@ import {
|
|
10
10
|
WriteableSignal,
|
11
11
|
SignalCompute,
|
12
12
|
SignalAsyncCompute,
|
13
|
-
AsyncResult,
|
14
13
|
SignalSubscribe,
|
15
14
|
SignalSubscription,
|
16
15
|
SignalOptionsWithInit,
|
17
|
-
AsyncReady,
|
18
16
|
Watcher,
|
19
17
|
SignalWatcherEffect,
|
18
|
+
AsyncSignal,
|
20
19
|
} from '../../signals.js';
|
21
20
|
|
22
21
|
class SignalCounts {
|
@@ -194,13 +193,13 @@ export function asyncComputed<T>(
|
|
194
193
|
compute: SignalAsyncCompute<T>,
|
195
194
|
opts: SignalOptionsWithInit<T>,
|
196
195
|
): Signal<T>;
|
197
|
-
export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts?: SignalOptions<T>):
|
198
|
-
export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts: SignalOptionsWithInit<T>):
|
196
|
+
export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
|
197
|
+
export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts: SignalOptionsWithInit<T>): AsyncSignal<T>;
|
199
198
|
export function asyncComputed<T>(
|
200
199
|
nameOrCompute: string | SignalAsyncCompute<T>,
|
201
200
|
computeOrOpts?: SignalCompute<T> | Partial<SignalOptionsWithInit<T>>,
|
202
201
|
maybeOpts?: Partial<SignalOptionsWithInit<T>>,
|
203
|
-
):
|
202
|
+
): AsyncSignal<T> {
|
204
203
|
const name = typeof nameOrCompute === 'string' ? nameOrCompute : 'unlabeled';
|
205
204
|
const compute = typeof nameOrCompute === 'string' ? (computeOrOpts as SignalCompute<T>) : nameOrCompute;
|
206
205
|
const opts = typeof nameOrCompute === 'string' ? maybeOpts : (computeOrOpts as SignalOptions<T>);
|
package/src/index.ts
CHANGED
package/src/signals.ts
CHANGED
@@ -4,6 +4,7 @@ let CURRENT_CONSUMER: ComputedSignal<any> | undefined;
|
|
4
4
|
let CURRENT_DEP_TAIL: Link | undefined;
|
5
5
|
let CURRENT_ORD: number = 0;
|
6
6
|
let CURRENT_IS_WATCHED: boolean = false;
|
7
|
+
let CURRENT_IS_WAITING: boolean = false;
|
7
8
|
let CURRENT_SEEN: WeakSet<ComputedSignal<any>> | undefined;
|
8
9
|
|
9
10
|
let id = 0;
|
@@ -23,6 +24,8 @@ export interface WriteableSignal<T> extends Signal<T> {
|
|
23
24
|
set(value: T): void;
|
24
25
|
}
|
25
26
|
|
27
|
+
export type AsyncSignal<T> = Signal<AsyncResult<T>>;
|
28
|
+
|
26
29
|
export type SignalCompute<T> = (prev: T | undefined) => T;
|
27
30
|
|
28
31
|
export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
|
@@ -47,6 +50,7 @@ export interface SignalOptionsWithInit<T> extends SignalOptions<T> {
|
|
47
50
|
}
|
48
51
|
|
49
52
|
const SUBSCRIPTIONS = new WeakMap<ComputedSignal<any>, SignalSubscription | undefined | void>();
|
53
|
+
const ACTIVE_ASYNCS = new WeakMap<ComputedSignal<any>, Promise<unknown>>();
|
50
54
|
|
51
55
|
const enum SignalState {
|
52
56
|
Clean,
|
@@ -54,6 +58,8 @@ const enum SignalState {
|
|
54
58
|
Dirty,
|
55
59
|
}
|
56
60
|
|
61
|
+
const WAITING = Symbol();
|
62
|
+
|
57
63
|
interface Link {
|
58
64
|
sub: WeakRef<ComputedSignal<any>>;
|
59
65
|
dep: ComputedSignal<any>;
|
@@ -172,8 +178,6 @@ function clearTrack(link: Link, shouldDisconnect: boolean): void {
|
|
172
178
|
} while (link !== undefined);
|
173
179
|
}
|
174
180
|
|
175
|
-
// const registry = new FinalizationRegistry(poolLink);
|
176
|
-
|
177
181
|
export class ComputedSignal<T> {
|
178
182
|
id = id++;
|
179
183
|
_type: SignalType;
|
@@ -346,7 +350,7 @@ export class ComputedSignal<T> {
|
|
346
350
|
}
|
347
351
|
|
348
352
|
case SignalType.Async: {
|
349
|
-
const value =
|
353
|
+
const value: AsyncResult<T> =
|
350
354
|
(this._currentValue as AsyncResult<T>) ??
|
351
355
|
(this._currentValue = {
|
352
356
|
result: undefined,
|
@@ -355,14 +359,71 @@ export class ComputedSignal<T> {
|
|
355
359
|
isReady: false,
|
356
360
|
isError: false,
|
357
361
|
isSuccess: false,
|
362
|
+
|
363
|
+
invalidate: () => {
|
364
|
+
this._state = SignalState.Dirty;
|
365
|
+
this._dirty();
|
366
|
+
},
|
367
|
+
|
368
|
+
await: () => {
|
369
|
+
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== SignalType.Async) {
|
370
|
+
throw new Error(
|
371
|
+
'Cannot await an async signal outside of an async signal. If you are using an async function, you must use signal.await() for all async signals _before_ the first language-level `await` keyword statement (e.g. it must be synchronous).',
|
372
|
+
);
|
373
|
+
}
|
374
|
+
|
375
|
+
if (value.isPending) {
|
376
|
+
const currentConsumer = CURRENT_CONSUMER;
|
377
|
+
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
378
|
+
|
379
|
+
CURRENT_IS_WAITING = true;
|
380
|
+
throw WAITING;
|
381
|
+
} else if (value.isError) {
|
382
|
+
throw value.error;
|
383
|
+
}
|
384
|
+
|
385
|
+
return value.result as T;
|
386
|
+
},
|
358
387
|
});
|
359
388
|
|
360
|
-
|
389
|
+
let nextValue;
|
390
|
+
|
391
|
+
try {
|
392
|
+
CURRENT_IS_WAITING = false;
|
393
|
+
nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
|
394
|
+
} catch (e) {
|
395
|
+
if (e !== WAITING) {
|
396
|
+
value.error = e;
|
397
|
+
value.isPending = false;
|
398
|
+
value.isError = true;
|
399
|
+
this._version++;
|
400
|
+
}
|
401
|
+
|
402
|
+
break;
|
403
|
+
}
|
361
404
|
|
362
|
-
if (
|
405
|
+
if (CURRENT_IS_WAITING) {
|
406
|
+
if (!value.isPending) {
|
407
|
+
value.isPending = true;
|
408
|
+
value.isError = false;
|
409
|
+
value.isSuccess = false;
|
410
|
+
this._version++;
|
411
|
+
}
|
412
|
+
|
413
|
+
if (nextValue instanceof Promise) {
|
414
|
+
nextValue.catch((e: unknown) => {
|
415
|
+
if (e !== WAITING) {
|
416
|
+
value.error = e;
|
417
|
+
value.isPending = false;
|
418
|
+
value.isError = true;
|
419
|
+
this._version++;
|
420
|
+
}
|
421
|
+
});
|
422
|
+
}
|
423
|
+
} else if (nextValue instanceof Promise) {
|
363
424
|
const currentVersion = ++this._version;
|
364
425
|
|
365
|
-
|
426
|
+
nextValue = nextValue.then(
|
366
427
|
result => {
|
367
428
|
if (currentVersion !== this._version) {
|
368
429
|
return;
|
@@ -378,7 +439,7 @@ export class ComputedSignal<T> {
|
|
378
439
|
this._dirtyConsumers();
|
379
440
|
},
|
380
441
|
error => {
|
381
|
-
if (currentVersion !== this._version) {
|
442
|
+
if (currentVersion !== this._version || error === WAITING) {
|
382
443
|
return;
|
383
444
|
}
|
384
445
|
|
@@ -390,6 +451,8 @@ export class ComputedSignal<T> {
|
|
390
451
|
},
|
391
452
|
);
|
392
453
|
|
454
|
+
ACTIVE_ASYNCS.set(this, nextValue);
|
455
|
+
|
393
456
|
value.isPending = true;
|
394
457
|
value.isError = false;
|
395
458
|
value.isSuccess = false;
|
@@ -564,7 +627,12 @@ export class ComputedSignal<T> {
|
|
564
627
|
}
|
565
628
|
}
|
566
629
|
|
567
|
-
export interface
|
630
|
+
export interface AsyncBaseResult<T> {
|
631
|
+
invalidate(): void;
|
632
|
+
await(): T;
|
633
|
+
}
|
634
|
+
|
635
|
+
export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
568
636
|
result: undefined;
|
569
637
|
error: unknown;
|
570
638
|
isPending: boolean;
|
@@ -573,7 +641,7 @@ export interface AsyncPending {
|
|
573
641
|
isSuccess: boolean;
|
574
642
|
}
|
575
643
|
|
576
|
-
export interface AsyncReady<T> {
|
644
|
+
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
577
645
|
result: T;
|
578
646
|
error: unknown;
|
579
647
|
isPending: boolean;
|
@@ -582,7 +650,7 @@ export interface AsyncReady<T> {
|
|
582
650
|
isSuccess: boolean;
|
583
651
|
}
|
584
652
|
|
585
|
-
export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
|
653
|
+
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
586
654
|
|
587
655
|
class StateSignal<T> implements StateSignal<T> {
|
588
656
|
private _consumers: WeakRef<ComputedSignal<unknown>>[] = [];
|
@@ -632,19 +700,16 @@ export function computed<T>(compute: (prev: T | undefined) => T, opts?: SignalOp
|
|
632
700
|
return new ComputedSignal(SignalType.Computed, compute, opts?.equals) as Signal<T>;
|
633
701
|
}
|
634
702
|
|
635
|
-
export function asyncComputed<T>(
|
636
|
-
compute: (prev: T | undefined) => Promise<T>,
|
637
|
-
opts?: SignalOptions<T>,
|
638
|
-
): Signal<AsyncResult<T>>;
|
703
|
+
export function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
|
639
704
|
export function asyncComputed<T>(
|
640
705
|
compute: (prev: T | undefined) => Promise<T>,
|
641
706
|
opts: SignalOptionsWithInit<T>,
|
642
|
-
):
|
707
|
+
): AsyncSignal<T>;
|
643
708
|
export function asyncComputed<T>(
|
644
709
|
compute: (prev: T | undefined) => Promise<T>,
|
645
710
|
opts?: Partial<SignalOptionsWithInit<T>>,
|
646
|
-
):
|
647
|
-
return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as
|
711
|
+
): AsyncSignal<T> {
|
712
|
+
return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as AsyncSignal<T>;
|
648
713
|
}
|
649
714
|
|
650
715
|
export function subscription<T>(subscribe: SignalSubscribe<T>, opts?: SignalOptions<T>): Signal<T | undefined>;
|
@@ -655,10 +720,20 @@ export function subscription<T>(subscribe: SignalSubscribe<T>, opts?: Partial<Si
|
|
655
720
|
|
656
721
|
export interface Watcher {
|
657
722
|
disconnect(): void;
|
723
|
+
subscribe(subscriber: () => void): () => void;
|
658
724
|
}
|
659
725
|
|
660
726
|
export function watcher(fn: () => void): Watcher {
|
661
|
-
const
|
727
|
+
const subscribers = new Set<() => void>();
|
728
|
+
const watcher = new ComputedSignal(SignalType.Watcher, () => {
|
729
|
+
fn();
|
730
|
+
|
731
|
+
untrack(() => {
|
732
|
+
for (const subscriber of subscribers) {
|
733
|
+
subscriber();
|
734
|
+
}
|
735
|
+
});
|
736
|
+
});
|
662
737
|
|
663
738
|
scheduleWatcher(watcher);
|
664
739
|
|
@@ -666,9 +741,21 @@ export function watcher(fn: () => void): Watcher {
|
|
666
741
|
disconnect() {
|
667
742
|
scheduleDisconnect(watcher);
|
668
743
|
},
|
744
|
+
|
745
|
+
subscribe(subscriber: () => void) {
|
746
|
+
subscribers.add(subscriber);
|
747
|
+
|
748
|
+
return () => {
|
749
|
+
subscribers.delete(subscriber);
|
750
|
+
};
|
751
|
+
},
|
669
752
|
};
|
670
753
|
}
|
671
754
|
|
755
|
+
export function isTracking(): boolean {
|
756
|
+
return CURRENT_CONSUMER !== undefined;
|
757
|
+
}
|
758
|
+
|
672
759
|
export function untrack<T = void>(fn: () => T): T {
|
673
760
|
const prevConsumer = CURRENT_CONSUMER;
|
674
761
|
const prevOrd = CURRENT_ORD;
|