signalium 0.1.1 → 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,11 @@
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
+
3
9
  ## 0.1.1
4
10
 
5
11
  ### Patch Changes
package/dist/signals.d.ts CHANGED
@@ -10,10 +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 interface AsyncSignal<T> extends Signal<AsyncResult<T>> {
14
- invalidate(): void;
15
- await(): T;
16
- }
13
+ export type AsyncSignal<T> = Signal<AsyncResult<T>>;
17
14
  export type SignalCompute<T> = (prev: T | undefined) => T;
18
15
  export type SignalAsyncCompute<T> = (prev: T | undefined) => T | Promise<T>;
19
16
  export type SignalWatcherEffect = () => void;
@@ -61,8 +58,6 @@ export declare class ComputedSignal<T> {
61
58
  _ref: WeakRef<ComputedSignal<T>>;
62
59
  constructor(type: SignalType, compute: SignalCompute<T> | SignalAsyncCompute<T> | SignalSubscribe<T> | undefined, equals?: SignalEquals<T>, initValue?: T);
63
60
  get(): T | AsyncResult<T>;
64
- invalidate(): void;
65
- await(): T;
66
61
  _check(shouldWatch?: boolean): number;
67
62
  _run(wasConnected: boolean, isConnected: boolean, shouldConnect: boolean): void;
68
63
  _resetDirty(): void;
@@ -70,7 +65,11 @@ export declare class ComputedSignal<T> {
70
65
  _dirtyConsumers(): void;
71
66
  _disconnect(count?: number): void;
72
67
  }
73
- export interface AsyncPending {
68
+ export interface AsyncBaseResult<T> {
69
+ invalidate(): void;
70
+ await(): T;
71
+ }
72
+ export interface AsyncPending<T> extends AsyncBaseResult<T> {
74
73
  result: undefined;
75
74
  error: unknown;
76
75
  isPending: boolean;
@@ -78,7 +77,7 @@ export interface AsyncPending {
78
77
  isError: boolean;
79
78
  isSuccess: boolean;
80
79
  }
81
- export interface AsyncReady<T> {
80
+ export interface AsyncReady<T> extends AsyncBaseResult<T> {
82
81
  result: T;
83
82
  error: unknown;
84
83
  isPending: boolean;
@@ -86,7 +85,7 @@ export interface AsyncReady<T> {
86
85
  isError: boolean;
87
86
  isSuccess: boolean;
88
87
  }
89
- export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
88
+ export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
90
89
  declare class StateSignal<T> implements StateSignal<T> {
91
90
  private _value;
92
91
  private _equals;
package/dist/signals.js CHANGED
@@ -3,10 +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
+ let CURRENT_IS_WAITING = false;
7
7
  let CURRENT_SEEN;
8
8
  let id = 0;
9
9
  const SUBSCRIPTIONS = new WeakMap();
10
+ const ACTIVE_ASYNCS = new WeakMap();
10
11
  const WAITING = Symbol();
11
12
  let linkPool;
12
13
  function linkNewDep(dep, sub, nextDep, depsTail, ord) {
@@ -151,27 +152,6 @@ export class ComputedSignal {
151
152
  }
152
153
  return this._currentValue;
153
154
  }
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
- }
175
155
  _check(shouldWatch = false) {
176
156
  let state = this._state;
177
157
  let connectedCount = this._connectedCount;
@@ -254,9 +234,29 @@ export class ComputedSignal {
254
234
  isReady: false,
255
235
  isError: false,
256
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
+ },
257
256
  });
258
257
  let nextValue;
259
258
  try {
259
+ CURRENT_IS_WAITING = false;
260
260
  nextValue = this._compute(value?.result);
261
261
  }
262
262
  catch (e) {
@@ -268,16 +268,15 @@ export class ComputedSignal {
268
268
  }
269
269
  break;
270
270
  }
271
- if (typeof CURRENT_WAITING_STATE !== 'boolean') {
271
+ if (CURRENT_IS_WAITING) {
272
272
  if (!value.isPending) {
273
273
  value.isPending = true;
274
274
  value.isError = false;
275
275
  value.isSuccess = false;
276
276
  this._version++;
277
277
  }
278
- CURRENT_WAITING_STATE.finally(() => this._check());
279
278
  if (nextValue instanceof Promise) {
280
- nextValue.catch(e => {
279
+ nextValue.catch((e) => {
281
280
  if (e !== WAITING) {
282
281
  value.error = e;
283
282
  value.isPending = false;
@@ -309,9 +308,7 @@ export class ComputedSignal {
309
308
  this._version++;
310
309
  this._dirtyConsumers();
311
310
  });
312
- if (CURRENT_WAITING_STATE === true) {
313
- CURRENT_WAITING_STATE = nextValue;
314
- }
311
+ ACTIVE_ASYNCS.set(this, nextValue);
315
312
  value.isPending = true;
316
313
  value.isError = false;
317
314
  value.isSuccess = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalium",
3
- "version": "0.1.1",
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,14 +9,17 @@ 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
25
  describe('Async Signal functionality', () => {
@@ -180,8 +183,8 @@ describe('Async Signal functionality', () => {
180
183
  });
181
184
 
182
185
  const compC = asyncComputed(async () => {
183
- const a = compA.await();
184
- const b = compB.await();
186
+ const a = compA.get().await();
187
+ const b = compB.get().await();
185
188
 
186
189
  return a + b;
187
190
  });
@@ -230,8 +233,8 @@ describe('Async Signal functionality', () => {
230
233
  });
231
234
 
232
235
  const compC = asyncComputed(async () => {
233
- const a = compA.await();
234
- const b = compB.await();
236
+ const a = compA.get().await();
237
+ const b = compB.get().await();
235
238
 
236
239
  return a + b;
237
240
  });
@@ -244,20 +247,10 @@ describe('Async Signal functionality', () => {
244
247
 
245
248
  await sleep(50);
246
249
 
247
- expect(compC).toHaveValueAndCounts(
248
- {
249
- result: undefined,
250
- error: 'error',
251
- isPending: false,
252
- isReady: false,
253
- isError: true,
254
- isSuccess: false,
255
- },
256
- {
257
- compute: 2,
258
- resolve: 0,
259
- },
260
- );
250
+ expect(compC).toHaveValueAndCounts(result(undefined, 'error', false, 'error'), {
251
+ compute: 2,
252
+ resolve: 0,
253
+ });
261
254
  });
262
255
  });
263
256
  });
@@ -10,11 +10,9 @@ 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,
20
18
  AsyncSignal,
@@ -226,14 +224,6 @@ export function asyncComputed<T>(
226
224
  counts.get++;
227
225
  return s.get();
228
226
  },
229
-
230
- invalidate() {
231
- s.invalidate();
232
- },
233
-
234
- await() {
235
- return s.await();
236
- },
237
227
  };
238
228
 
239
229
  COUNTS.set(wrapper, counts);
package/src/signals.ts CHANGED
@@ -4,7 +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
+ let CURRENT_IS_WAITING: boolean = false;
8
8
  let CURRENT_SEEN: WeakSet<ComputedSignal<any>> | undefined;
9
9
 
10
10
  let id = 0;
@@ -24,10 +24,7 @@ export interface WriteableSignal<T> extends Signal<T> {
24
24
  set(value: T): void;
25
25
  }
26
26
 
27
- export interface AsyncSignal<T> extends Signal<AsyncResult<T>> {
28
- invalidate(): void;
29
- await(): T;
30
- }
27
+ export type AsyncSignal<T> = Signal<AsyncResult<T>>;
31
28
 
32
29
  export type SignalCompute<T> = (prev: T | undefined) => T;
33
30
 
@@ -53,6 +50,7 @@ export interface SignalOptionsWithInit<T> extends SignalOptions<T> {
53
50
  }
54
51
 
55
52
  const SUBSCRIPTIONS = new WeakMap<ComputedSignal<any>, SignalSubscription | undefined | void>();
53
+ const ACTIVE_ASYNCS = new WeakMap<ComputedSignal<any>, Promise<unknown>>();
56
54
 
57
55
  const enum SignalState {
58
56
  Clean,
@@ -259,34 +257,6 @@ export class ComputedSignal<T> {
259
257
  return this._currentValue!;
260
258
  }
261
259
 
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
-
290
260
  _check(shouldWatch = false): number {
291
261
  let state = this._state;
292
262
  let connectedCount = this._connectedCount;
@@ -380,7 +350,7 @@ export class ComputedSignal<T> {
380
350
  }
381
351
 
382
352
  case SignalType.Async: {
383
- const value =
353
+ const value: AsyncResult<T> =
384
354
  (this._currentValue as AsyncResult<T>) ??
385
355
  (this._currentValue = {
386
356
  result: undefined,
@@ -389,11 +359,37 @@ export class ComputedSignal<T> {
389
359
  isReady: false,
390
360
  isError: false,
391
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
+ },
392
387
  });
393
388
 
394
389
  let nextValue;
395
390
 
396
391
  try {
392
+ CURRENT_IS_WAITING = false;
397
393
  nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
398
394
  } catch (e) {
399
395
  if (e !== WAITING) {
@@ -406,7 +402,7 @@ export class ComputedSignal<T> {
406
402
  break;
407
403
  }
408
404
 
409
- if (typeof CURRENT_WAITING_STATE !== 'boolean') {
405
+ if (CURRENT_IS_WAITING) {
410
406
  if (!value.isPending) {
411
407
  value.isPending = true;
412
408
  value.isError = false;
@@ -414,10 +410,8 @@ export class ComputedSignal<T> {
414
410
  this._version++;
415
411
  }
416
412
 
417
- CURRENT_WAITING_STATE.finally(() => this._check());
418
-
419
413
  if (nextValue instanceof Promise) {
420
- nextValue.catch(e => {
414
+ nextValue.catch((e: unknown) => {
421
415
  if (e !== WAITING) {
422
416
  value.error = e;
423
417
  value.isPending = false;
@@ -457,9 +451,7 @@ export class ComputedSignal<T> {
457
451
  },
458
452
  );
459
453
 
460
- if (CURRENT_WAITING_STATE === true) {
461
- CURRENT_WAITING_STATE = nextValue;
462
- }
454
+ ACTIVE_ASYNCS.set(this, nextValue);
463
455
 
464
456
  value.isPending = true;
465
457
  value.isError = false;
@@ -635,7 +627,12 @@ export class ComputedSignal<T> {
635
627
  }
636
628
  }
637
629
 
638
- export interface AsyncPending {
630
+ export interface AsyncBaseResult<T> {
631
+ invalidate(): void;
632
+ await(): T;
633
+ }
634
+
635
+ export interface AsyncPending<T> extends AsyncBaseResult<T> {
639
636
  result: undefined;
640
637
  error: unknown;
641
638
  isPending: boolean;
@@ -644,7 +641,7 @@ export interface AsyncPending {
644
641
  isSuccess: boolean;
645
642
  }
646
643
 
647
- export interface AsyncReady<T> {
644
+ export interface AsyncReady<T> extends AsyncBaseResult<T> {
648
645
  result: T;
649
646
  error: unknown;
650
647
  isPending: boolean;
@@ -653,7 +650,7 @@ export interface AsyncReady<T> {
653
650
  isSuccess: boolean;
654
651
  }
655
652
 
656
- export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
653
+ export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
657
654
 
658
655
  class StateSignal<T> implements StateSignal<T> {
659
656
  private _consumers: WeakRef<ComputedSignal<unknown>>[] = [];