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 +6 -0
- package/dist/signals.d.ts +8 -9
- package/dist/signals.js +25 -28
- package/package.json +1 -1
- package/src/__tests__/async.test.ts +12 -19
- package/src/__tests__/utils/instrumented.ts +0 -10
- package/src/signals.ts +41 -44
package/CHANGELOG.md
CHANGED
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
|
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
|
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
|
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 (
|
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
|
-
|
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
@@ -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
|
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
|
-
|
250
|
-
|
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
|
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
|
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 (
|
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
|
-
|
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
|
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>>[] = [];
|