signalium 0.1.1 → 0.2.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 +12 -0
- package/dist/signals.d.ts +10 -9
- package/dist/signals.js +40 -38
- package/package.json +1 -1
- package/src/__tests__/async.test.ts +75 -42
- package/src/__tests__/utils/instrumented.ts +0 -10
- package/src/signals.ts +60 -54
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,23 +65,29 @@ 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;
|
77
76
|
isReady: false;
|
78
77
|
isError: boolean;
|
79
78
|
isSuccess: boolean;
|
79
|
+
didResolve: boolean;
|
80
80
|
}
|
81
|
-
export interface AsyncReady<T> {
|
81
|
+
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
82
82
|
result: T;
|
83
83
|
error: unknown;
|
84
84
|
isPending: boolean;
|
85
85
|
isReady: true;
|
86
86
|
isError: boolean;
|
87
87
|
isSuccess: boolean;
|
88
|
+
didResolve: boolean;
|
88
89
|
}
|
89
|
-
export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
|
90
|
+
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
90
91
|
declare class StateSignal<T> implements StateSignal<T> {
|
91
92
|
private _value;
|
92
93
|
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) {
|
@@ -114,8 +115,39 @@ export class ComputedSignal {
|
|
114
115
|
this._type = type;
|
115
116
|
this._compute = compute;
|
116
117
|
this._equals = equals ?? ((a, b) => a === b);
|
117
|
-
this._currentValue = initValue;
|
118
118
|
this._connectedCount = type === 3 /* SignalType.Watcher */ ? 1 : 0;
|
119
|
+
this._currentValue =
|
120
|
+
type !== 2 /* SignalType.Async */
|
121
|
+
? initValue
|
122
|
+
: {
|
123
|
+
result: initValue,
|
124
|
+
error: undefined,
|
125
|
+
isReady: initValue !== undefined,
|
126
|
+
isPending: true,
|
127
|
+
isError: false,
|
128
|
+
isSuccess: false,
|
129
|
+
didResolve: false,
|
130
|
+
invalidate: () => {
|
131
|
+
this._state = 2 /* SignalState.Dirty */;
|
132
|
+
this._dirty();
|
133
|
+
},
|
134
|
+
await: () => {
|
135
|
+
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== 2 /* SignalType.Async */) {
|
136
|
+
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).');
|
137
|
+
}
|
138
|
+
const value = this._currentValue;
|
139
|
+
if (value.isPending) {
|
140
|
+
const currentConsumer = CURRENT_CONSUMER;
|
141
|
+
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
142
|
+
CURRENT_IS_WAITING = true;
|
143
|
+
throw WAITING;
|
144
|
+
}
|
145
|
+
else if (value.isError) {
|
146
|
+
throw value.error;
|
147
|
+
}
|
148
|
+
return value.result;
|
149
|
+
},
|
150
|
+
};
|
119
151
|
}
|
120
152
|
get() {
|
121
153
|
let prevTracked = false;
|
@@ -151,27 +183,6 @@ export class ComputedSignal {
|
|
151
183
|
}
|
152
184
|
return this._currentValue;
|
153
185
|
}
|
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
186
|
_check(shouldWatch = false) {
|
176
187
|
let state = this._state;
|
177
188
|
let connectedCount = this._connectedCount;
|
@@ -246,17 +257,10 @@ export class ComputedSignal {
|
|
246
257
|
break;
|
247
258
|
}
|
248
259
|
case 2 /* SignalType.Async */: {
|
249
|
-
const value = this._currentValue
|
250
|
-
(this._currentValue = {
|
251
|
-
result: undefined,
|
252
|
-
error: undefined,
|
253
|
-
isPending: true,
|
254
|
-
isReady: false,
|
255
|
-
isError: false,
|
256
|
-
isSuccess: false,
|
257
|
-
});
|
260
|
+
const value = this._currentValue;
|
258
261
|
let nextValue;
|
259
262
|
try {
|
263
|
+
CURRENT_IS_WAITING = false;
|
260
264
|
nextValue = this._compute(value?.result);
|
261
265
|
}
|
262
266
|
catch (e) {
|
@@ -268,16 +272,15 @@ export class ComputedSignal {
|
|
268
272
|
}
|
269
273
|
break;
|
270
274
|
}
|
271
|
-
if (
|
275
|
+
if (CURRENT_IS_WAITING) {
|
272
276
|
if (!value.isPending) {
|
273
277
|
value.isPending = true;
|
274
278
|
value.isError = false;
|
275
279
|
value.isSuccess = false;
|
276
280
|
this._version++;
|
277
281
|
}
|
278
|
-
CURRENT_WAITING_STATE.finally(() => this._check());
|
279
282
|
if (nextValue instanceof Promise) {
|
280
|
-
nextValue.catch(e => {
|
283
|
+
nextValue.catch((e) => {
|
281
284
|
if (e !== WAITING) {
|
282
285
|
value.error = e;
|
283
286
|
value.isPending = false;
|
@@ -295,6 +298,7 @@ export class ComputedSignal {
|
|
295
298
|
}
|
296
299
|
value.result = result;
|
297
300
|
value.isReady = true;
|
301
|
+
value.didResolve = true;
|
298
302
|
value.isPending = false;
|
299
303
|
value.isSuccess = true;
|
300
304
|
this._version++;
|
@@ -309,9 +313,7 @@ export class ComputedSignal {
|
|
309
313
|
this._version++;
|
310
314
|
this._dirtyConsumers();
|
311
315
|
});
|
312
|
-
|
313
|
-
CURRENT_WAITING_STATE = nextValue;
|
314
|
-
}
|
316
|
+
ACTIVE_ASYNCS.set(this, nextValue);
|
315
317
|
value.isPending = true;
|
316
318
|
value.isError = false;
|
317
319
|
value.isSuccess = false;
|
package/package.json
CHANGED
@@ -8,15 +8,21 @@ const nextTick = () => new Promise(r => setTimeout(r, 0));
|
|
8
8
|
const result = <T>(
|
9
9
|
value: T | undefined,
|
10
10
|
promiseState: 'pending' | 'error' | 'success',
|
11
|
-
|
11
|
+
readyState: 'initial' | 'ready' | 'resolved',
|
12
|
+
error?: any,
|
12
13
|
): AsyncResult<T> =>
|
13
14
|
({
|
14
15
|
result: value,
|
15
|
-
error
|
16
|
+
error,
|
16
17
|
isPending: promiseState === 'pending',
|
17
|
-
isReady,
|
18
18
|
isError: promiseState === 'error',
|
19
19
|
isSuccess: promiseState === 'success',
|
20
|
+
|
21
|
+
isReady: readyState === 'ready' || readyState === 'resolved',
|
22
|
+
didResolve: readyState === 'resolved',
|
23
|
+
|
24
|
+
await: expect.any(Function),
|
25
|
+
invalidate: expect.any(Function),
|
20
26
|
}) as AsyncResult<T>;
|
21
27
|
|
22
28
|
describe('Async Signal functionality', () => {
|
@@ -28,20 +34,20 @@ describe('Async Signal functionality', () => {
|
|
28
34
|
return a.get() + b.get();
|
29
35
|
});
|
30
36
|
|
31
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
37
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
32
38
|
compute: 1,
|
33
39
|
resolve: 0,
|
34
40
|
});
|
35
41
|
|
36
42
|
await nextTick();
|
37
43
|
|
38
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
44
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
39
45
|
compute: 1,
|
40
46
|
resolve: 1,
|
41
47
|
});
|
42
48
|
|
43
49
|
// stability
|
44
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
50
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
45
51
|
compute: 1,
|
46
52
|
resolve: 1,
|
47
53
|
});
|
@@ -55,28 +61,28 @@ describe('Async Signal functionality', () => {
|
|
55
61
|
return a.get() + b.get();
|
56
62
|
});
|
57
63
|
|
58
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
64
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
59
65
|
compute: 1,
|
60
66
|
resolve: 0,
|
61
67
|
});
|
62
68
|
|
63
69
|
await nextTick();
|
64
70
|
|
65
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
71
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
66
72
|
compute: 1,
|
67
73
|
resolve: 1,
|
68
74
|
});
|
69
75
|
|
70
76
|
a.set(2);
|
71
77
|
|
72
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
78
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
73
79
|
compute: 2,
|
74
80
|
resolve: 1,
|
75
81
|
});
|
76
82
|
|
77
83
|
await nextTick();
|
78
84
|
|
79
|
-
expect(c).toHaveValueAndCounts(result(4, 'success',
|
85
|
+
expect(c).toHaveValueAndCounts(result(4, 'success', 'resolved'), {
|
80
86
|
compute: 2,
|
81
87
|
resolve: 2,
|
82
88
|
});
|
@@ -90,28 +96,28 @@ describe('Async Signal functionality', () => {
|
|
90
96
|
return a.get() + b.get();
|
91
97
|
});
|
92
98
|
|
93
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
99
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
94
100
|
compute: 1,
|
95
101
|
resolve: 0,
|
96
102
|
});
|
97
103
|
|
98
104
|
await nextTick();
|
99
105
|
|
100
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
106
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
101
107
|
compute: 1,
|
102
108
|
resolve: 1,
|
103
109
|
});
|
104
110
|
|
105
111
|
a.set(1);
|
106
112
|
|
107
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
113
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
108
114
|
compute: 1,
|
109
115
|
resolve: 1,
|
110
116
|
});
|
111
117
|
|
112
118
|
await nextTick();
|
113
119
|
|
114
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
120
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
115
121
|
compute: 1,
|
116
122
|
resolve: 1,
|
117
123
|
});
|
@@ -131,40 +137,77 @@ describe('Async Signal functionality', () => {
|
|
131
137
|
return result;
|
132
138
|
});
|
133
139
|
|
134
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
140
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
135
141
|
compute: 1,
|
136
142
|
resolve: 0,
|
137
143
|
});
|
138
144
|
|
139
145
|
await nextTick();
|
140
146
|
|
141
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
147
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
142
148
|
compute: 1,
|
143
149
|
resolve: 1,
|
144
150
|
});
|
145
151
|
|
146
152
|
a.set(2);
|
147
153
|
|
148
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
154
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
149
155
|
compute: 2,
|
150
156
|
resolve: 1,
|
151
157
|
});
|
152
158
|
|
153
159
|
a.set(3);
|
154
160
|
|
155
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
161
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
156
162
|
compute: 3,
|
157
163
|
resolve: 1,
|
158
164
|
});
|
159
165
|
|
160
166
|
await sleep(200);
|
161
167
|
|
162
|
-
expect(c).toHaveValueAndCounts(result(5, 'success',
|
168
|
+
expect(c).toHaveValueAndCounts(result(5, 'success', 'resolved'), {
|
163
169
|
compute: 3,
|
164
170
|
resolve: 3,
|
165
171
|
});
|
166
172
|
});
|
167
173
|
|
174
|
+
test('Can have initial value', async () => {
|
175
|
+
const a = state(1);
|
176
|
+
const b = state(2);
|
177
|
+
|
178
|
+
const c = asyncComputed(
|
179
|
+
async () => {
|
180
|
+
const result = a.get() + b.get();
|
181
|
+
|
182
|
+
await sleep(50);
|
183
|
+
|
184
|
+
return result;
|
185
|
+
},
|
186
|
+
{
|
187
|
+
initValue: 5,
|
188
|
+
},
|
189
|
+
);
|
190
|
+
|
191
|
+
expect(c).toHaveValueAndCounts(result(5, 'pending', 'ready'), {
|
192
|
+
compute: 1,
|
193
|
+
resolve: 0,
|
194
|
+
});
|
195
|
+
|
196
|
+
await nextTick();
|
197
|
+
|
198
|
+
expect(c).toHaveValueAndCounts(result(5, 'pending', 'ready'), {
|
199
|
+
compute: 1,
|
200
|
+
resolve: 0,
|
201
|
+
});
|
202
|
+
|
203
|
+
await sleep(60);
|
204
|
+
|
205
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
206
|
+
compute: 1,
|
207
|
+
resolve: 1,
|
208
|
+
});
|
209
|
+
});
|
210
|
+
|
168
211
|
describe('Awaiting', () => {
|
169
212
|
test('Awaiting a computed will resolve the value', async () => {
|
170
213
|
const compA = asyncComputed(async () => {
|
@@ -180,14 +223,14 @@ describe('Async Signal functionality', () => {
|
|
180
223
|
});
|
181
224
|
|
182
225
|
const compC = asyncComputed(async () => {
|
183
|
-
const a = compA.await();
|
184
|
-
const b = compB.await();
|
226
|
+
const a = compA.get().await();
|
227
|
+
const b = compB.get().await();
|
185
228
|
|
186
229
|
return a + b;
|
187
230
|
});
|
188
231
|
|
189
232
|
// Pull once to start the computation, trigger the computation
|
190
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
233
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
191
234
|
compute: 1,
|
192
235
|
resolve: 0,
|
193
236
|
});
|
@@ -195,7 +238,7 @@ describe('Async Signal functionality', () => {
|
|
195
238
|
await nextTick();
|
196
239
|
|
197
240
|
// Check after a tick to make sure we didn't resolve early
|
198
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
241
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
199
242
|
compute: 1,
|
200
243
|
resolve: 0,
|
201
244
|
});
|
@@ -203,14 +246,14 @@ describe('Async Signal functionality', () => {
|
|
203
246
|
await sleep(30);
|
204
247
|
|
205
248
|
// Check to make sure we don't resolve early after the first task completes
|
206
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
249
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
207
250
|
compute: 2,
|
208
251
|
resolve: 0,
|
209
252
|
});
|
210
253
|
|
211
254
|
await sleep(30);
|
212
255
|
|
213
|
-
expect(compC).toHaveValueAndCounts(result(3, 'success',
|
256
|
+
expect(compC).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
214
257
|
compute: 3,
|
215
258
|
resolve: 1,
|
216
259
|
});
|
@@ -230,34 +273,24 @@ describe('Async Signal functionality', () => {
|
|
230
273
|
});
|
231
274
|
|
232
275
|
const compC = asyncComputed(async () => {
|
233
|
-
const a = compA.await();
|
234
|
-
const b = compB.await();
|
276
|
+
const a = compA.get().await();
|
277
|
+
const b = compB.get().await();
|
235
278
|
|
236
279
|
return a + b;
|
237
280
|
});
|
238
281
|
|
239
282
|
// Pull once to start the computation, trigger the computation
|
240
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
283
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
241
284
|
compute: 1,
|
242
285
|
resolve: 0,
|
243
286
|
});
|
244
287
|
|
245
288
|
await sleep(50);
|
246
289
|
|
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
|
-
);
|
290
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'error', 'initial', 'error'), {
|
291
|
+
compute: 2,
|
292
|
+
resolve: 0,
|
293
|
+
});
|
261
294
|
});
|
262
295
|
});
|
263
296
|
});
|
@@ -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,
|
@@ -210,8 +208,48 @@ export class ComputedSignal<T> {
|
|
210
208
|
this._type = type;
|
211
209
|
this._compute = compute;
|
212
210
|
this._equals = equals ?? ((a, b) => a === b);
|
213
|
-
this._currentValue = initValue;
|
214
211
|
this._connectedCount = type === SignalType.Watcher ? 1 : 0;
|
212
|
+
|
213
|
+
this._currentValue =
|
214
|
+
type !== SignalType.Async
|
215
|
+
? initValue
|
216
|
+
: ({
|
217
|
+
result: initValue,
|
218
|
+
error: undefined,
|
219
|
+
isReady: initValue !== undefined,
|
220
|
+
|
221
|
+
isPending: true,
|
222
|
+
isError: false,
|
223
|
+
isSuccess: false,
|
224
|
+
didResolve: false,
|
225
|
+
|
226
|
+
invalidate: () => {
|
227
|
+
this._state = SignalState.Dirty;
|
228
|
+
this._dirty();
|
229
|
+
},
|
230
|
+
|
231
|
+
await: () => {
|
232
|
+
if (CURRENT_CONSUMER === undefined || CURRENT_CONSUMER._type !== SignalType.Async) {
|
233
|
+
throw new Error(
|
234
|
+
'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).',
|
235
|
+
);
|
236
|
+
}
|
237
|
+
|
238
|
+
const value = this._currentValue as AsyncResult<T>;
|
239
|
+
|
240
|
+
if (value.isPending) {
|
241
|
+
const currentConsumer = CURRENT_CONSUMER;
|
242
|
+
ACTIVE_ASYNCS.get(this)?.finally(() => currentConsumer._check());
|
243
|
+
|
244
|
+
CURRENT_IS_WAITING = true;
|
245
|
+
throw WAITING;
|
246
|
+
} else if (value.isError) {
|
247
|
+
throw value.error;
|
248
|
+
}
|
249
|
+
|
250
|
+
return value.result as T;
|
251
|
+
},
|
252
|
+
} as AsyncResult<T>);
|
215
253
|
}
|
216
254
|
|
217
255
|
get(): T | AsyncResult<T> {
|
@@ -259,34 +297,6 @@ export class ComputedSignal<T> {
|
|
259
297
|
return this._currentValue!;
|
260
298
|
}
|
261
299
|
|
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
300
|
_check(shouldWatch = false): number {
|
291
301
|
let state = this._state;
|
292
302
|
let connectedCount = this._connectedCount;
|
@@ -380,20 +390,12 @@ export class ComputedSignal<T> {
|
|
380
390
|
}
|
381
391
|
|
382
392
|
case SignalType.Async: {
|
383
|
-
const value =
|
384
|
-
(this._currentValue as AsyncResult<T>) ??
|
385
|
-
(this._currentValue = {
|
386
|
-
result: undefined,
|
387
|
-
error: undefined,
|
388
|
-
isPending: true,
|
389
|
-
isReady: false,
|
390
|
-
isError: false,
|
391
|
-
isSuccess: false,
|
392
|
-
});
|
393
|
+
const value: AsyncResult<T> = this._currentValue as AsyncResult<T>;
|
393
394
|
|
394
395
|
let nextValue;
|
395
396
|
|
396
397
|
try {
|
398
|
+
CURRENT_IS_WAITING = false;
|
397
399
|
nextValue = (this._compute as SignalAsyncCompute<T>)(value?.result);
|
398
400
|
} catch (e) {
|
399
401
|
if (e !== WAITING) {
|
@@ -406,7 +408,7 @@ export class ComputedSignal<T> {
|
|
406
408
|
break;
|
407
409
|
}
|
408
410
|
|
409
|
-
if (
|
411
|
+
if (CURRENT_IS_WAITING) {
|
410
412
|
if (!value.isPending) {
|
411
413
|
value.isPending = true;
|
412
414
|
value.isError = false;
|
@@ -414,10 +416,8 @@ export class ComputedSignal<T> {
|
|
414
416
|
this._version++;
|
415
417
|
}
|
416
418
|
|
417
|
-
CURRENT_WAITING_STATE.finally(() => this._check());
|
418
|
-
|
419
419
|
if (nextValue instanceof Promise) {
|
420
|
-
nextValue.catch(e => {
|
420
|
+
nextValue.catch((e: unknown) => {
|
421
421
|
if (e !== WAITING) {
|
422
422
|
value.error = e;
|
423
423
|
value.isPending = false;
|
@@ -437,6 +437,7 @@ export class ComputedSignal<T> {
|
|
437
437
|
|
438
438
|
value.result = result;
|
439
439
|
value.isReady = true;
|
440
|
+
value.didResolve = true;
|
440
441
|
|
441
442
|
value.isPending = false;
|
442
443
|
value.isSuccess = true;
|
@@ -457,9 +458,7 @@ export class ComputedSignal<T> {
|
|
457
458
|
},
|
458
459
|
);
|
459
460
|
|
460
|
-
|
461
|
-
CURRENT_WAITING_STATE = nextValue;
|
462
|
-
}
|
461
|
+
ACTIVE_ASYNCS.set(this, nextValue);
|
463
462
|
|
464
463
|
value.isPending = true;
|
465
464
|
value.isError = false;
|
@@ -635,25 +634,32 @@ export class ComputedSignal<T> {
|
|
635
634
|
}
|
636
635
|
}
|
637
636
|
|
638
|
-
export interface
|
637
|
+
export interface AsyncBaseResult<T> {
|
638
|
+
invalidate(): void;
|
639
|
+
await(): T;
|
640
|
+
}
|
641
|
+
|
642
|
+
export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
639
643
|
result: undefined;
|
640
644
|
error: unknown;
|
641
645
|
isPending: boolean;
|
642
646
|
isReady: false;
|
643
647
|
isError: boolean;
|
644
648
|
isSuccess: boolean;
|
649
|
+
didResolve: boolean;
|
645
650
|
}
|
646
651
|
|
647
|
-
export interface AsyncReady<T> {
|
652
|
+
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
648
653
|
result: T;
|
649
654
|
error: unknown;
|
650
655
|
isPending: boolean;
|
651
656
|
isReady: true;
|
652
657
|
isError: boolean;
|
653
658
|
isSuccess: boolean;
|
659
|
+
didResolve: boolean;
|
654
660
|
}
|
655
661
|
|
656
|
-
export type AsyncResult<T> = AsyncPending | AsyncReady<T>;
|
662
|
+
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
657
663
|
|
658
664
|
class StateSignal<T> implements StateSignal<T> {
|
659
665
|
private _consumers: WeakRef<ComputedSignal<unknown>>[] = [];
|