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 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 AsyncPending {
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>): Signal<AsyncResult<T>>;
95
- export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts: SignalOptionsWithInit<T>): Signal<AsyncReady<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
- const nextValue = this._compute(value?.result);
237
- if ('then' in nextValue) {
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 watcher = new ComputedSignal(3 /* SignalType.Watcher */, fn);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalium",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/pzuraq/signalium",
6
6
  "description": "Chain-reactivity at critical mass",
@@ -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: undefined,
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.skip('Async Signal functionality', () => {
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('Nesting', () => {
169
- test('Can nest computeds', () => {
170
- const a = state(1);
171
- const b = state(2);
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
- const inner = computed(() => {
175
- return a.get() + b.get();
176
+ return 1;
176
177
  });
177
178
 
178
- const outer = computed(() => {
179
- return inner.get() + c.get();
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
- c.set(3);
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 mid = computed(() => {
245
- return inner.get() + c.get();
246
- });
185
+ const compC = asyncComputed(async () => {
186
+ const a = compA.get().await();
187
+ const b = compB.get().await();
247
188
 
248
- const outer = computed(() => {
249
- return mid.get() + d.get();
189
+ return a + b;
250
190
  });
251
191
 
252
- expect(inner).toHaveValueAndCounts(3, { compute: 1 });
253
- expect(mid).toHaveValueAndCounts(5, { compute: 1 });
254
- expect(outer).toHaveValueAndCounts(7, { compute: 1 });
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
- const outer1 = computed(() => {
288
- return inner.get() + c.get();
289
- });
198
+ await nextTick();
290
199
 
291
- const outer2 = computed(() => {
292
- return inner.get() + d.get();
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
- expect(inner).toHaveValueAndCounts(3, { compute: 1 });
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
- expect(inner).toHaveValueAndCounts(5, { compute: 3 });
308
- expect(outer2).toHaveValueAndCounts(7, { compute: 3 });
309
- expect(outer1).toHaveValueAndCounts(7, { compute: 3 });
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
- const outer1 = computed('outer1', () => {
335
- return inner.get() + c.get();
336
- });
214
+ await sleep(30);
337
215
 
338
- const outer2 = computed('outer2', () => {
339
- return inner.get() + d.get();
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('it continues propagation if any child is different', () => {
367
- const a = state(1);
368
- const b = state(2);
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
- const inner1 = computed('inner1', () => {
373
- return a.get() + b.get();
226
+ throw 'error';
374
227
  });
375
228
 
376
- const inner2 = computed('inner2', () => {
377
- return c.get();
378
- });
229
+ const compB = asyncComputed(async () => {
230
+ await sleep(20);
379
231
 
380
- const inner3 = computed('inner3', () => {
381
- return d.get();
232
+ return 2;
382
233
  });
383
234
 
384
- const outer = computed('outer', () => {
385
- return inner1.get() + inner2.get() + inner3.get();
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
- const inner1 = computed(() => {
413
- return a.get() + b.get();
239
+ return a + b;
414
240
  });
415
241
 
416
- const inner2 = computed(() => {
417
- return c.get() + d.get();
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
- const outer = computed(() => {
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(c).toHaveValueAndCounts(3, { compute: 1 });
456
-
457
- a.set(2);
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.skip('Basic Signal functionality', () => {
4
+ describe('Basic Signal functionality', () => {
5
5
  test('Can run basic computed', () => {
6
6
  const a = state(1);
7
7
  const b = state(2);
@@ -620,7 +620,6 @@ describe('Subscription Signal functionality', () => {
620
620
 
621
621
  w.disconnect();
622
622
  let w2 = watcher(() => {
623
- console.log('test');
624
623
  s.get();
625
624
  });
626
625
 
@@ -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>): Signal<T>;
198
- export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts: SignalOptionsWithInit<T>): Signal<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
- ): Signal<AsyncResult<T>> | Signal<AsyncReady<T>> {
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
@@ -1,5 +1,6 @@
1
1
  export type {
2
2
  Signal,
3
+ AsyncSignal,
3
4
  WriteableSignal,
4
5
  SignalCompute,
5
6
  SignalAsyncCompute,
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
- const nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
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 ('then' in (nextValue as Promise<T>)) {
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
- (nextValue as Promise<T>).then(
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 AsyncPending {
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
- ): Signal<AsyncReady<T>>;
707
+ ): AsyncSignal<T>;
643
708
  export function asyncComputed<T>(
644
709
  compute: (prev: T | undefined) => Promise<T>,
645
710
  opts?: Partial<SignalOptionsWithInit<T>>,
646
- ): Signal<AsyncResult<T>> {
647
- return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as Signal<AsyncResult<T>>;
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 watcher = new ComputedSignal(SignalType.Watcher, fn);
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;