signalium 0.2.0 → 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 +6 -0
- package/dist/signals.d.ts +2 -0
- package/dist/signals.js +34 -29
- package/package.json +1 -1
- package/src/__tests__/async.test.ts +64 -24
- package/src/signals.ts +45 -36
package/CHANGELOG.md
CHANGED
package/dist/signals.d.ts
CHANGED
@@ -76,6 +76,7 @@ export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
|
76
76
|
isReady: false;
|
77
77
|
isError: boolean;
|
78
78
|
isSuccess: boolean;
|
79
|
+
didResolve: boolean;
|
79
80
|
}
|
80
81
|
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
81
82
|
result: T;
|
@@ -84,6 +85,7 @@ export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
|
84
85
|
isReady: true;
|
85
86
|
isError: boolean;
|
86
87
|
isSuccess: boolean;
|
88
|
+
didResolve: boolean;
|
87
89
|
}
|
88
90
|
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|
89
91
|
declare class StateSignal<T> implements StateSignal<T> {
|
package/dist/signals.js
CHANGED
@@ -115,8 +115,39 @@ export class ComputedSignal {
|
|
115
115
|
this._type = type;
|
116
116
|
this._compute = compute;
|
117
117
|
this._equals = equals ?? ((a, b) => a === b);
|
118
|
-
this._currentValue = initValue;
|
119
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
|
+
};
|
120
151
|
}
|
121
152
|
get() {
|
122
153
|
let prevTracked = false;
|
@@ -226,34 +257,7 @@ export class ComputedSignal {
|
|
226
257
|
break;
|
227
258
|
}
|
228
259
|
case 2 /* SignalType.Async */: {
|
229
|
-
const value = this._currentValue
|
230
|
-
(this._currentValue = {
|
231
|
-
result: undefined,
|
232
|
-
error: undefined,
|
233
|
-
isPending: true,
|
234
|
-
isReady: false,
|
235
|
-
isError: false,
|
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
|
-
},
|
256
|
-
});
|
260
|
+
const value = this._currentValue;
|
257
261
|
let nextValue;
|
258
262
|
try {
|
259
263
|
CURRENT_IS_WAITING = false;
|
@@ -294,6 +298,7 @@ export class ComputedSignal {
|
|
294
298
|
}
|
295
299
|
value.result = result;
|
296
300
|
value.isReady = true;
|
301
|
+
value.didResolve = true;
|
297
302
|
value.isPending = false;
|
298
303
|
value.isSuccess = true;
|
299
304
|
this._version++;
|
package/package.json
CHANGED
@@ -8,16 +8,19 @@ 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
12
|
error?: any,
|
13
13
|
): AsyncResult<T> =>
|
14
14
|
({
|
15
15
|
result: value,
|
16
16
|
error,
|
17
17
|
isPending: promiseState === 'pending',
|
18
|
-
isReady,
|
19
18
|
isError: promiseState === 'error',
|
20
19
|
isSuccess: promiseState === 'success',
|
20
|
+
|
21
|
+
isReady: readyState === 'ready' || readyState === 'resolved',
|
22
|
+
didResolve: readyState === 'resolved',
|
23
|
+
|
21
24
|
await: expect.any(Function),
|
22
25
|
invalidate: expect.any(Function),
|
23
26
|
}) as AsyncResult<T>;
|
@@ -31,20 +34,20 @@ describe('Async Signal functionality', () => {
|
|
31
34
|
return a.get() + b.get();
|
32
35
|
});
|
33
36
|
|
34
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
37
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
35
38
|
compute: 1,
|
36
39
|
resolve: 0,
|
37
40
|
});
|
38
41
|
|
39
42
|
await nextTick();
|
40
43
|
|
41
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
44
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
42
45
|
compute: 1,
|
43
46
|
resolve: 1,
|
44
47
|
});
|
45
48
|
|
46
49
|
// stability
|
47
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
50
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
48
51
|
compute: 1,
|
49
52
|
resolve: 1,
|
50
53
|
});
|
@@ -58,28 +61,28 @@ describe('Async Signal functionality', () => {
|
|
58
61
|
return a.get() + b.get();
|
59
62
|
});
|
60
63
|
|
61
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
64
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
62
65
|
compute: 1,
|
63
66
|
resolve: 0,
|
64
67
|
});
|
65
68
|
|
66
69
|
await nextTick();
|
67
70
|
|
68
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
71
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
69
72
|
compute: 1,
|
70
73
|
resolve: 1,
|
71
74
|
});
|
72
75
|
|
73
76
|
a.set(2);
|
74
77
|
|
75
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
78
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
76
79
|
compute: 2,
|
77
80
|
resolve: 1,
|
78
81
|
});
|
79
82
|
|
80
83
|
await nextTick();
|
81
84
|
|
82
|
-
expect(c).toHaveValueAndCounts(result(4, 'success',
|
85
|
+
expect(c).toHaveValueAndCounts(result(4, 'success', 'resolved'), {
|
83
86
|
compute: 2,
|
84
87
|
resolve: 2,
|
85
88
|
});
|
@@ -93,28 +96,28 @@ describe('Async Signal functionality', () => {
|
|
93
96
|
return a.get() + b.get();
|
94
97
|
});
|
95
98
|
|
96
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
99
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
97
100
|
compute: 1,
|
98
101
|
resolve: 0,
|
99
102
|
});
|
100
103
|
|
101
104
|
await nextTick();
|
102
105
|
|
103
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
106
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
104
107
|
compute: 1,
|
105
108
|
resolve: 1,
|
106
109
|
});
|
107
110
|
|
108
111
|
a.set(1);
|
109
112
|
|
110
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
113
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
111
114
|
compute: 1,
|
112
115
|
resolve: 1,
|
113
116
|
});
|
114
117
|
|
115
118
|
await nextTick();
|
116
119
|
|
117
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
120
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
118
121
|
compute: 1,
|
119
122
|
resolve: 1,
|
120
123
|
});
|
@@ -134,40 +137,77 @@ describe('Async Signal functionality', () => {
|
|
134
137
|
return result;
|
135
138
|
});
|
136
139
|
|
137
|
-
expect(c).toHaveValueAndCounts(result(undefined, 'pending',
|
140
|
+
expect(c).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
138
141
|
compute: 1,
|
139
142
|
resolve: 0,
|
140
143
|
});
|
141
144
|
|
142
145
|
await nextTick();
|
143
146
|
|
144
|
-
expect(c).toHaveValueAndCounts(result(3, 'success',
|
147
|
+
expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
145
148
|
compute: 1,
|
146
149
|
resolve: 1,
|
147
150
|
});
|
148
151
|
|
149
152
|
a.set(2);
|
150
153
|
|
151
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
154
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
152
155
|
compute: 2,
|
153
156
|
resolve: 1,
|
154
157
|
});
|
155
158
|
|
156
159
|
a.set(3);
|
157
160
|
|
158
|
-
expect(c).toHaveValueAndCounts(result(3, 'pending',
|
161
|
+
expect(c).toHaveValueAndCounts(result(3, 'pending', 'resolved'), {
|
159
162
|
compute: 3,
|
160
163
|
resolve: 1,
|
161
164
|
});
|
162
165
|
|
163
166
|
await sleep(200);
|
164
167
|
|
165
|
-
expect(c).toHaveValueAndCounts(result(5, 'success',
|
168
|
+
expect(c).toHaveValueAndCounts(result(5, 'success', 'resolved'), {
|
166
169
|
compute: 3,
|
167
170
|
resolve: 3,
|
168
171
|
});
|
169
172
|
});
|
170
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
|
+
|
171
211
|
describe('Awaiting', () => {
|
172
212
|
test('Awaiting a computed will resolve the value', async () => {
|
173
213
|
const compA = asyncComputed(async () => {
|
@@ -190,7 +230,7 @@ describe('Async Signal functionality', () => {
|
|
190
230
|
});
|
191
231
|
|
192
232
|
// Pull once to start the computation, trigger the computation
|
193
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
233
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
194
234
|
compute: 1,
|
195
235
|
resolve: 0,
|
196
236
|
});
|
@@ -198,7 +238,7 @@ describe('Async Signal functionality', () => {
|
|
198
238
|
await nextTick();
|
199
239
|
|
200
240
|
// Check after a tick to make sure we didn't resolve early
|
201
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
241
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
202
242
|
compute: 1,
|
203
243
|
resolve: 0,
|
204
244
|
});
|
@@ -206,14 +246,14 @@ describe('Async Signal functionality', () => {
|
|
206
246
|
await sleep(30);
|
207
247
|
|
208
248
|
// Check to make sure we don't resolve early after the first task completes
|
209
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
249
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
210
250
|
compute: 2,
|
211
251
|
resolve: 0,
|
212
252
|
});
|
213
253
|
|
214
254
|
await sleep(30);
|
215
255
|
|
216
|
-
expect(compC).toHaveValueAndCounts(result(3, 'success',
|
256
|
+
expect(compC).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
|
217
257
|
compute: 3,
|
218
258
|
resolve: 1,
|
219
259
|
});
|
@@ -240,14 +280,14 @@ describe('Async Signal functionality', () => {
|
|
240
280
|
});
|
241
281
|
|
242
282
|
// Pull once to start the computation, trigger the computation
|
243
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'pending',
|
283
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
|
244
284
|
compute: 1,
|
245
285
|
resolve: 0,
|
246
286
|
});
|
247
287
|
|
248
288
|
await sleep(50);
|
249
289
|
|
250
|
-
expect(compC).toHaveValueAndCounts(result(undefined, 'error',
|
290
|
+
expect(compC).toHaveValueAndCounts(result(undefined, 'error', 'initial', 'error'), {
|
251
291
|
compute: 2,
|
252
292
|
resolve: 0,
|
253
293
|
});
|
package/src/signals.ts
CHANGED
@@ -208,8 +208,48 @@ export class ComputedSignal<T> {
|
|
208
208
|
this._type = type;
|
209
209
|
this._compute = compute;
|
210
210
|
this._equals = equals ?? ((a, b) => a === b);
|
211
|
-
this._currentValue = initValue;
|
212
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>);
|
213
253
|
}
|
214
254
|
|
215
255
|
get(): T | AsyncResult<T> {
|
@@ -350,41 +390,7 @@ export class ComputedSignal<T> {
|
|
350
390
|
}
|
351
391
|
|
352
392
|
case SignalType.Async: {
|
353
|
-
const value: AsyncResult<T> =
|
354
|
-
(this._currentValue as AsyncResult<T>) ??
|
355
|
-
(this._currentValue = {
|
356
|
-
result: undefined,
|
357
|
-
error: undefined,
|
358
|
-
isPending: true,
|
359
|
-
isReady: false,
|
360
|
-
isError: false,
|
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
|
-
},
|
387
|
-
});
|
393
|
+
const value: AsyncResult<T> = this._currentValue as AsyncResult<T>;
|
388
394
|
|
389
395
|
let nextValue;
|
390
396
|
|
@@ -431,6 +437,7 @@ export class ComputedSignal<T> {
|
|
431
437
|
|
432
438
|
value.result = result;
|
433
439
|
value.isReady = true;
|
440
|
+
value.didResolve = true;
|
434
441
|
|
435
442
|
value.isPending = false;
|
436
443
|
value.isSuccess = true;
|
@@ -639,6 +646,7 @@ export interface AsyncPending<T> extends AsyncBaseResult<T> {
|
|
639
646
|
isReady: false;
|
640
647
|
isError: boolean;
|
641
648
|
isSuccess: boolean;
|
649
|
+
didResolve: boolean;
|
642
650
|
}
|
643
651
|
|
644
652
|
export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
@@ -648,6 +656,7 @@ export interface AsyncReady<T> extends AsyncBaseResult<T> {
|
|
648
656
|
isReady: true;
|
649
657
|
isError: boolean;
|
650
658
|
isSuccess: boolean;
|
659
|
+
didResolve: boolean;
|
651
660
|
}
|
652
661
|
|
653
662
|
export type AsyncResult<T> = AsyncPending<T> | AsyncReady<T>;
|