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 +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/signals.d.ts +10 -2
- package/dist/signals.js +79 -6
- package/package.json +1 -1
- package/src/__tests__/async.test.ts +56 -290
- package/src/__tests__/basic.test.ts +1 -1
- package/src/__tests__/subscription.test.ts +0 -1
- package/src/__tests__/utils/instrumented.ts +12 -3
- package/src/index.ts +1 -0
- package/src/signals.ts +104 -14
package/CHANGELOG.md
CHANGED
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>):
|
95
|
-
export declare function asyncComputed<T>(compute: (prev: T | undefined) => Promise<T>, opts: SignalOptionsWithInit<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
|
-
|
237
|
-
|
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
|
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
@@ -19,7 +19,7 @@ const result = <T>(
|
|
19
19
|
isSuccess: promiseState === 'success',
|
20
20
|
}) as AsyncResult<T>;
|
21
21
|
|
22
|
-
describe
|
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('
|
169
|
-
test('
|
170
|
-
const
|
171
|
-
|
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
|
-
|
175
|
-
return a.get() + b.get();
|
173
|
+
return 1;
|
176
174
|
});
|
177
175
|
|
178
|
-
const
|
179
|
-
|
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
|
-
|
196
|
-
return a.get() + b.get();
|
179
|
+
return 2;
|
197
180
|
});
|
198
181
|
|
199
|
-
const
|
200
|
-
|
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
|
-
|
218
|
-
return a.get() + b.get();
|
186
|
+
return a + b;
|
219
187
|
});
|
220
188
|
|
221
|
-
|
222
|
-
|
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
|
-
|
226
|
-
expect(outer).toHaveValueAndCounts(5, { compute: 1 });
|
195
|
+
await nextTick();
|
227
196
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
335
|
-
return inner.get() + c.get();
|
336
|
-
});
|
211
|
+
await sleep(30);
|
337
212
|
|
338
|
-
|
339
|
-
|
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('
|
367
|
-
const
|
368
|
-
|
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
|
-
|
373
|
-
return a.get() + b.get();
|
223
|
+
throw 'error';
|
374
224
|
});
|
375
225
|
|
376
|
-
const
|
377
|
-
|
378
|
-
});
|
226
|
+
const compB = asyncComputed(async () => {
|
227
|
+
await sleep(20);
|
379
228
|
|
380
|
-
|
381
|
-
return d.get();
|
229
|
+
return 2;
|
382
230
|
});
|
383
231
|
|
384
|
-
const
|
385
|
-
|
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
|
-
|
413
|
-
return a.get() + b.get();
|
236
|
+
return a + b;
|
414
237
|
});
|
415
238
|
|
416
|
-
|
417
|
-
|
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
|
-
|
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(
|
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
|
-
|
450
|
-
|
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
|
-
|
474
|
-
|
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
|
4
|
+
describe('Basic Signal functionality', () => {
|
5
5
|
test('Can run basic computed', () => {
|
6
6
|
const a = state(1);
|
7
7
|
const b = state(2);
|
@@ -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>):
|
198
|
-
export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts: SignalOptionsWithInit<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
|
-
):
|
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
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
):
|
710
|
+
): AsyncSignal<T>;
|
643
711
|
export function asyncComputed<T>(
|
644
712
|
compute: (prev: T | undefined) => Promise<T>,
|
645
713
|
opts?: Partial<SignalOptionsWithInit<T>>,
|
646
|
-
):
|
647
|
-
return new ComputedSignal(SignalType.Async, compute, opts?.equals, opts?.initValue) as
|
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
|
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;
|