signalium 0.1.0 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # signalium
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 033a814: Add await and invalidate to async signals
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  ### 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,10 @@ export interface Signal<T = unknown> {
10
10
  export interface WriteableSignal<T> extends Signal<T> {
11
11
  set(value: T): void;
12
12
  }
13
+ export interface AsyncSignal<T> extends Signal<AsyncResult<T>> {
14
+ invalidate(): void;
15
+ await(): T;
16
+ }
13
17
  export type SignalCompute<T> = (prev: T | undefined) => T;
14
18
  export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
15
19
  export type SignalWatcherEffect = () => void;
@@ -57,6 +61,8 @@ export declare class ComputedSignal<T> {
57
61
  _ref: WeakRef<ComputedSignal<T>>;
58
62
  constructor(type: SignalType, compute: SignalCompute<T> | SignalAsyncCompute<T> | SignalSubscribe<T> | undefined, equals?: SignalEquals<T>, initValue?: T);
59
63
  get(): T | AsyncResult<T>;
64
+ invalidate(): void;
65
+ await(): T;
60
66
  _check(shouldWatch?: boolean): number;
61
67
  _run(wasConnected: boolean, isConnected: boolean, shouldConnect: boolean): void;
62
68
  _resetDirty(): void;
@@ -91,13 +97,15 @@ declare class StateSignal<T> implements StateSignal<T> {
91
97
  }
92
98
  export declare function state<T>(initialValue: T, opts?: SignalOptions<T>): StateSignal<T>;
93
99
  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>>;
100
+ export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
101
+ export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts: SignalOptionsWithInit<T>): AsyncSignal<T>;
96
102
  export declare function subscription<T>(subscribe: SignalSubscribe<T>, opts?: SignalOptions<T>): Signal<T | undefined>;
97
103
  export declare function subscription<T>(subscribe: SignalSubscribe<T>, opts: SignalOptionsWithInit<T>): Signal<T>;
98
104
  export interface Watcher {
99
105
  disconnect(): void;
106
+ subscribe(subscriber: () => void): () => void;
100
107
  }
101
108
  export declare function watcher(fn: () => void): Watcher;
109
+ export declare function isTracking(): boolean;
102
110
  export declare function untrack<T = void>(fn: () => T): T;
103
111
  export {};
package/dist/signals.js CHANGED
@@ -3,9 +3,11 @@ 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_WAITING_STATE = false;
6
7
  let CURRENT_SEEN;
7
8
  let id = 0;
8
9
  const SUBSCRIPTIONS = new WeakMap();
10
+ const WAITING = Symbol();
9
11
  let linkPool;
10
12
  function linkNewDep(dep, sub, nextDep, depsTail, ord) {
11
13
  let newLink;
@@ -94,7 +96,6 @@ function clearTrack(link, shouldDisconnect) {
94
96
  link = nextDep;
95
97
  } while (link !== undefined);
96
98
  }
97
- // const registry = new FinalizationRegistry(poolLink);
98
99
  export class ComputedSignal {
99
100
  id = id++;
100
101
  _type;
@@ -150,6 +151,27 @@ export class ComputedSignal {
150
151
  }
151
152
  return this._currentValue;
152
153
  }
154
+ invalidate() {
155
+ this._state = 2 /* SignalState.Dirty */;
156
+ this._dirty();
157
+ }
158
+ await() {
159
+ if (this._type !== 2 /* SignalType.Async */) {
160
+ throw new Error('Cannot await non-async signal');
161
+ }
162
+ if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== 2 /* SignalType.Async */) {
163
+ 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).');
164
+ }
165
+ CURRENT_WAITING_STATE = true;
166
+ const value = this.get();
167
+ if (value.isPending) {
168
+ throw WAITING;
169
+ }
170
+ else if (value.isError) {
171
+ throw value.error;
172
+ }
173
+ return value.result;
174
+ }
153
175
  _check(shouldWatch = false) {
154
176
  let state = this._state;
155
177
  let connectedCount = this._connectedCount;
@@ -233,10 +255,41 @@ export class ComputedSignal {
233
255
  isError: false,
234
256
  isSuccess: false,
235
257
  });
236
- const nextValue = this._compute(value?.result);
237
- if ('then' in nextValue) {
258
+ let nextValue;
259
+ try {
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 (typeof CURRENT_WAITING_STATE !== 'boolean') {
272
+ if (!value.isPending) {
273
+ value.isPending = true;
274
+ value.isError = false;
275
+ value.isSuccess = false;
276
+ this._version++;
277
+ }
278
+ CURRENT_WAITING_STATE.finally(() => this._check());
279
+ if (nextValue instanceof Promise) {
280
+ nextValue.catch(e => {
281
+ if (e !== WAITING) {
282
+ value.error = e;
283
+ value.isPending = false;
284
+ value.isError = true;
285
+ this._version++;
286
+ }
287
+ });
288
+ }
289
+ }
290
+ else if (nextValue instanceof Promise) {
238
291
  const currentVersion = ++this._version;
239
- nextValue.then(result => {
292
+ nextValue = nextValue.then(result => {
240
293
  if (currentVersion !== this._version) {
241
294
  return;
242
295
  }
@@ -247,7 +300,7 @@ export class ComputedSignal {
247
300
  this._version++;
248
301
  this._dirtyConsumers();
249
302
  }, error => {
250
- if (currentVersion !== this._version) {
303
+ if (currentVersion !== this._version || error === WAITING) {
251
304
  return;
252
305
  }
253
306
  value.error = error;
@@ -256,6 +309,9 @@ export class ComputedSignal {
256
309
  this._version++;
257
310
  this._dirtyConsumers();
258
311
  });
312
+ if (CURRENT_WAITING_STATE === true) {
313
+ CURRENT_WAITING_STATE = nextValue;
314
+ }
259
315
  value.isPending = true;
260
316
  value.isError = false;
261
317
  value.isSuccess = false;
@@ -447,14 +503,31 @@ export function subscription(subscribe, opts) {
447
503
  return new ComputedSignal(1 /* SignalType.Subscription */, subscribe, opts?.equals, opts?.initValue);
448
504
  }
449
505
  export function watcher(fn) {
450
- const watcher = new ComputedSignal(3 /* SignalType.Watcher */, fn);
506
+ const subscribers = new Set();
507
+ const watcher = new ComputedSignal(3 /* SignalType.Watcher */, () => {
508
+ fn();
509
+ untrack(() => {
510
+ for (const subscriber of subscribers) {
511
+ subscriber();
512
+ }
513
+ });
514
+ });
451
515
  scheduleWatcher(watcher);
452
516
  return {
453
517
  disconnect() {
454
518
  scheduleDisconnect(watcher);
455
519
  },
520
+ subscribe(subscriber) {
521
+ subscribers.add(subscriber);
522
+ return () => {
523
+ subscribers.delete(subscriber);
524
+ };
525
+ },
456
526
  };
457
527
  }
528
+ export function isTracking() {
529
+ return CURRENT_CONSUMER !== undefined;
530
+ }
458
531
  export function untrack(fn) {
459
532
  const prevConsumer = CURRENT_CONSUMER;
460
533
  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.1.1",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/pzuraq/signalium",
6
6
  "description": "Chain-reactivity at critical mass",
@@ -19,7 +19,7 @@ const result = <T>(
19
19
  isSuccess: promiseState === 'success',
20
20
  }) as AsyncResult<T>;
21
21
 
22
- describe.skip('Async Signal functionality', () => {
22
+ describe('Async Signal functionality', () => {
23
23
  test('Can run basic computed', async () => {
24
24
  const a = state(1);
25
25
  const b = state(2);
@@ -165,333 +165,99 @@ describe.skip('Async Signal functionality', () => {
165
165
  });
166
166
  });
167
167
 
168
- describe('Nesting', () => {
169
- test('Can nest computeds', () => {
170
- const a = state(1);
171
- const b = state(2);
172
- const c = state(2);
168
+ describe('Awaiting', () => {
169
+ test('Awaiting a computed will resolve the value', async () => {
170
+ const compA = asyncComputed(async () => {
171
+ await sleep(20);
173
172
 
174
- const inner = computed(() => {
175
- return a.get() + b.get();
173
+ return 1;
176
174
  });
177
175
 
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);
176
+ const compB = asyncComputed(async () => {
177
+ await sleep(20);
194
178
 
195
- const inner = computed(() => {
196
- return a.get() + b.get();
179
+ return 2;
197
180
  });
198
181
 
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);
182
+ const compC = asyncComputed(async () => {
183
+ const a = compA.await();
184
+ const b = compB.await();
216
185
 
217
- const inner = computed(() => {
218
- return a.get() + b.get();
186
+ return a + b;
219
187
  });
220
188
 
221
- const outer = computed(() => {
222
- return inner.get() + c.get();
189
+ // Pull once to start the computation, trigger the computation
190
+ expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
191
+ compute: 1,
192
+ resolve: 0,
223
193
  });
224
194
 
225
- expect(inner).toHaveValueAndCounts(3, { compute: 1 });
226
- expect(outer).toHaveValueAndCounts(5, { compute: 1 });
195
+ await nextTick();
227
196
 
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();
242
- });
243
-
244
- const mid = computed(() => {
245
- return inner.get() + c.get();
246
- });
247
-
248
- const outer = computed(() => {
249
- return mid.get() + d.get();
197
+ // Check after a tick to make sure we didn't resolve early
198
+ expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
199
+ compute: 1,
200
+ resolve: 0,
250
201
  });
251
202
 
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();
285
- });
286
-
287
- const outer1 = computed(() => {
288
- return inner.get() + c.get();
289
- });
290
-
291
- const outer2 = computed(() => {
292
- return inner.get() + d.get();
293
- });
294
-
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);
306
-
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 });
203
+ await sleep(30);
316
204
 
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();
205
+ // Check to make sure we don't resolve early after the first task completes
206
+ expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
207
+ compute: 2,
208
+ resolve: 0,
332
209
  });
333
210
 
334
- const outer1 = computed('outer1', () => {
335
- return inner.get() + c.get();
336
- });
211
+ await sleep(30);
337
212
 
338
- const outer2 = computed('outer2', () => {
339
- return inner.get() + d.get();
213
+ expect(compC).toHaveValueAndCounts(result(3, 'success', true), {
214
+ compute: 3,
215
+ resolve: 1,
340
216
  });
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
217
  });
365
218
 
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);
219
+ test('Awaiting a computed can handle errors', async () => {
220
+ const compA = asyncComputed(async () => {
221
+ await sleep(20);
371
222
 
372
- const inner1 = computed('inner1', () => {
373
- return a.get() + b.get();
223
+ throw 'error';
374
224
  });
375
225
 
376
- const inner2 = computed('inner2', () => {
377
- return c.get();
378
- });
226
+ const compB = asyncComputed(async () => {
227
+ await sleep(20);
379
228
 
380
- const inner3 = computed('inner3', () => {
381
- return d.get();
229
+ return 2;
382
230
  });
383
231
 
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);
232
+ const compC = asyncComputed(async () => {
233
+ const a = compA.await();
234
+ const b = compB.await();
411
235
 
412
- const inner1 = computed(() => {
413
- return a.get() + b.get();
236
+ return a + b;
414
237
  });
415
238
 
416
- const inner2 = computed(() => {
417
- return c.get() + d.get();
239
+ // Pull once to start the computation, trigger the computation
240
+ expect(compC).toHaveValueAndCounts(result(undefined, 'pending', false), {
241
+ compute: 1,
242
+ resolve: 0,
418
243
  });
419
244
 
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);
245
+ await sleep(50);
432
246
 
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
- },
247
+ expect(compC).toHaveValueAndCounts(
448
248
  {
449
- equals(prev, next) {
450
- return Math.abs(prev - next) < 2;
451
- },
452
- },
453
- );
454
-
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();
249
+ result: undefined,
250
+ error: 'error',
251
+ isPending: false,
252
+ isReady: false,
253
+ isError: true,
254
+ isSuccess: false,
471
255
  },
472
256
  {
473
- equals(prev, next) {
474
- return Math.abs(prev - next) < 2;
475
- },
257
+ compute: 2,
258
+ resolve: 0,
476
259
  },
477
260
  );
478
-
479
- const outer1 = computed(() => {
480
- return inner.get() + c.get();
481
- });
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
261
  });
496
262
  });
497
263
  });
@@ -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
 
@@ -17,6 +17,7 @@ import {
17
17
  AsyncReady,
18
18
  Watcher,
19
19
  SignalWatcherEffect,
20
+ AsyncSignal,
20
21
  } from '../../signals.js';
21
22
 
22
23
  class SignalCounts {
@@ -194,13 +195,13 @@ export function asyncComputed<T>(
194
195
  compute: SignalAsyncCompute<T>,
195
196
  opts: SignalOptionsWithInit<T>,
196
197
  ): 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>;
198
+ export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
199
+ export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts: SignalOptionsWithInit<T>): AsyncSignal<T>;
199
200
  export function asyncComputed<T>(
200
201
  nameOrCompute: string | SignalAsyncCompute<T>,
201
202
  computeOrOpts?: SignalCompute<T> | Partial<SignalOptionsWithInit<T>>,
202
203
  maybeOpts?: Partial<SignalOptionsWithInit<T>>,
203
- ): Signal<AsyncResult<T>> | Signal<AsyncReady<T>> {
204
+ ): AsyncSignal<T> {
204
205
  const name = typeof nameOrCompute === 'string' ? nameOrCompute : 'unlabeled';
205
206
  const compute = typeof nameOrCompute === 'string' ? (computeOrOpts as SignalCompute<T>) : nameOrCompute;
206
207
  const opts = typeof nameOrCompute === 'string' ? maybeOpts : (computeOrOpts as SignalOptions<T>);
@@ -225,6 +226,14 @@ export function asyncComputed<T>(
225
226
  counts.get++;
226
227
  return s.get();
227
228
  },
229
+
230
+ invalidate() {
231
+ s.invalidate();
232
+ },
233
+
234
+ await() {
235
+ return s.await();
236
+ },
228
237
  };
229
238
 
230
239
  COUNTS.set(wrapper, counts);
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_WAITING_STATE: Promise<unknown> | boolean = false;
7
8
  let CURRENT_SEEN: WeakSet<ComputedSignal<any>> | undefined;
8
9
 
9
10
  let id = 0;
@@ -23,6 +24,11 @@ export interface WriteableSignal<T> extends Signal<T> {
23
24
  set(value: T): void;
24
25
  }
25
26
 
27
+ export interface AsyncSignal<T> extends Signal<AsyncResult<T>> {
28
+ invalidate(): void;
29
+ await(): T;
30
+ }
31
+
26
32
  export type SignalCompute<T> = (prev: T | undefined) => T;
27
33
 
28
34
  export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
@@ -54,6 +60,8 @@ const enum SignalState {
54
60
  Dirty,
55
61
  }
56
62
 
63
+ const WAITING = Symbol();
64
+
57
65
  interface Link {
58
66
  sub: WeakRef<ComputedSignal<any>>;
59
67
  dep: ComputedSignal<any>;
@@ -172,8 +180,6 @@ function clearTrack(link: Link, shouldDisconnect: boolean): void {
172
180
  } while (link !== undefined);
173
181
  }
174
182
 
175
- // const registry = new FinalizationRegistry(poolLink);
176
-
177
183
  export class ComputedSignal<T> {
178
184
  id = id++;
179
185
  _type: SignalType;
@@ -253,6 +259,34 @@ export class ComputedSignal<T> {
253
259
  return this._currentValue!;
254
260
  }
255
261
 
262
+ invalidate() {
263
+ this._state = SignalState.Dirty;
264
+ this._dirty();
265
+ }
266
+
267
+ await() {
268
+ if (this._type !== SignalType.Async) {
269
+ throw new Error('Cannot await non-async signal');
270
+ }
271
+
272
+ if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== SignalType.Async) {
273
+ throw new Error(
274
+ '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).',
275
+ );
276
+ }
277
+
278
+ CURRENT_WAITING_STATE = true;
279
+ const value = this.get() as AsyncResult<T>;
280
+
281
+ if (value.isPending) {
282
+ throw WAITING;
283
+ } else if (value.isError) {
284
+ throw value.error;
285
+ }
286
+
287
+ return value.result as T;
288
+ }
289
+
256
290
  _check(shouldWatch = false): number {
257
291
  let state = this._state;
258
292
  let connectedCount = this._connectedCount;
@@ -357,12 +391,45 @@ export class ComputedSignal<T> {
357
391
  isSuccess: false,
358
392
  });
359
393
 
360
- const nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
394
+ let nextValue;
395
+
396
+ try {
397
+ nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
398
+ } catch (e) {
399
+ if (e !== WAITING) {
400
+ value.error = e;
401
+ value.isPending = false;
402
+ value.isError = true;
403
+ this._version++;
404
+ }
361
405
 
362
- if ('then' in (nextValue as Promise<T>)) {
406
+ break;
407
+ }
408
+
409
+ if (typeof CURRENT_WAITING_STATE !== 'boolean') {
410
+ if (!value.isPending) {
411
+ value.isPending = true;
412
+ value.isError = false;
413
+ value.isSuccess = false;
414
+ this._version++;
415
+ }
416
+
417
+ CURRENT_WAITING_STATE.finally(() => this._check());
418
+
419
+ if (nextValue instanceof Promise) {
420
+ nextValue.catch(e => {
421
+ if (e !== WAITING) {
422
+ value.error = e;
423
+ value.isPending = false;
424
+ value.isError = true;
425
+ this._version++;
426
+ }
427
+ });
428
+ }
429
+ } else if (nextValue instanceof Promise) {
363
430
  const currentVersion = ++this._version;
364
431
 
365
- (nextValue as Promise<T>).then(
432
+ nextValue = nextValue.then(
366
433
  result => {
367
434
  if (currentVersion !== this._version) {
368
435
  return;
@@ -378,7 +445,7 @@ export class ComputedSignal<T> {
378
445
  this._dirtyConsumers();
379
446
  },
380
447
  error => {
381
- if (currentVersion !== this._version) {
448
+ if (currentVersion !== this._version || error === WAITING) {
382
449
  return;
383
450
  }
384
451
 
@@ -390,6 +457,10 @@ export class ComputedSignal<T> {
390
457
  },
391
458
  );
392
459
 
460
+ if (CURRENT_WAITING_STATE === true) {
461
+ CURRENT_WAITING_STATE = nextValue;
462
+ }
463
+
393
464
  value.isPending = true;
394
465
  value.isError = false;
395
466
  value.isSuccess = false;
@@ -632,19 +703,16 @@ export function computed<T>(compute: (prev: T | undefined) => T, opts?: SignalOp
632
703
  return new ComputedSignal(SignalType.Computed, compute, opts?.equals) as Signal<T>;
633
704
  }
634
705
 
635
- export function asyncComputed<T>(
636
- compute: (prev: T | undefined) => Promise<T>,
637
- opts?: SignalOptions<T>,
638
- ): Signal<AsyncResult<T>>;
706
+ export function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
639
707
  export function asyncComputed<T>(
640
708
  compute: (prev: T | undefined) => Promise<T>,
641
709
  opts: SignalOptionsWithInit<T>,
642
- ): Signal<AsyncReady<T>>;
710
+ ): AsyncSignal<T>;
643
711
  export function asyncComputed<T>(
644
712
  compute: (prev: T | undefined) => Promise<T>,
645
713
  opts?: Partial<SignalOptionsWithInit<T>>,
646
- ): Signal<AsyncResult<T>> {
647
- return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as Signal<AsyncResult<T>>;
714
+ ): AsyncSignal<T> {
715
+ return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as AsyncSignal<T>;
648
716
  }
649
717
 
650
718
  export function subscription<T>(subscribe: SignalSubscribe<T>, opts?: SignalOptions<T>): Signal<T | undefined>;
@@ -655,10 +723,20 @@ export function subscription<T>(subscribe: SignalSubscribe<T>, opts?: Partial<Si
655
723
 
656
724
  export interface Watcher {
657
725
  disconnect(): void;
726
+ subscribe(subscriber: () => void): () => void;
658
727
  }
659
728
 
660
729
  export function watcher(fn: () => void): Watcher {
661
- const watcher = new ComputedSignal(SignalType.Watcher, fn);
730
+ const subscribers = new Set<() => void>();
731
+ const watcher = new ComputedSignal(SignalType.Watcher, () => {
732
+ fn();
733
+
734
+ untrack(() => {
735
+ for (const subscriber of subscribers) {
736
+ subscriber();
737
+ }
738
+ });
739
+ });
662
740
 
663
741
  scheduleWatcher(watcher);
664
742
 
@@ -666,9 +744,21 @@ export function watcher(fn: () => void): Watcher {
666
744
  disconnect() {
667
745
  scheduleDisconnect(watcher);
668
746
  },
747
+
748
+ subscribe(subscriber: () => void) {
749
+ subscribers.add(subscriber);
750
+
751
+ return () => {
752
+ subscribers.delete(subscriber);
753
+ };
754
+ },
669
755
  };
670
756
  }
671
757
 
758
+ export function isTracking(): boolean {
759
+ return CURRENT_CONSUMER !== undefined;
760
+ }
761
+
672
762
  export function untrack<T = void>(fn: () => T): T {
673
763
  const prevConsumer = CURRENT_CONSUMER;
674
764
  const prevOrd = CURRENT_ORD;