signalium 0.2.2 → 0.2.4

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.
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { state, asyncComputed } from './utils/instrumented.js';
2
+ import { state, asyncComputed, computed } from './utils/instrumented.js';
3
3
  import { AsyncResult } from '../signals';
4
4
 
5
5
  const sleep = (ms = 0) => new Promise(r => setTimeout(r, ms));
@@ -131,7 +131,7 @@ describe('Async Signal functionality', () => {
131
131
  const result = a.get() + b.get();
132
132
 
133
133
  if (result === 4) {
134
- await sleep(100);
134
+ await sleep(10);
135
135
  }
136
136
 
137
137
  return result;
@@ -163,7 +163,7 @@ describe('Async Signal functionality', () => {
163
163
  resolve: 1,
164
164
  });
165
165
 
166
- await sleep(200);
166
+ await sleep(20);
167
167
 
168
168
  expect(c).toHaveValueAndCounts(result(5, 'success', 'resolved'), {
169
169
  compute: 3,
@@ -179,7 +179,7 @@ describe('Async Signal functionality', () => {
179
179
  async () => {
180
180
  const result = a.get() + b.get();
181
181
 
182
- await sleep(50);
182
+ await sleep(10);
183
183
 
184
184
  return result;
185
185
  },
@@ -200,7 +200,7 @@ describe('Async Signal functionality', () => {
200
200
  resolve: 0,
201
201
  });
202
202
 
203
- await sleep(60);
203
+ await sleep(20);
204
204
 
205
205
  expect(c).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
206
206
  compute: 1,
@@ -211,13 +211,13 @@ describe('Async Signal functionality', () => {
211
211
  describe('Awaiting', () => {
212
212
  test('Awaiting a computed will resolve the value', async () => {
213
213
  const compA = asyncComputed(async () => {
214
- await sleep(20);
214
+ await sleep(10);
215
215
 
216
216
  return 1;
217
217
  });
218
218
 
219
219
  const compB = asyncComputed(async () => {
220
- await sleep(20);
220
+ await sleep(10);
221
221
 
222
222
  return 2;
223
223
  });
@@ -243,7 +243,7 @@ describe('Async Signal functionality', () => {
243
243
  resolve: 0,
244
244
  });
245
245
 
246
- await sleep(30);
246
+ await sleep(20);
247
247
 
248
248
  // Check to make sure we don't resolve early after the first task completes
249
249
  expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
@@ -251,7 +251,7 @@ describe('Async Signal functionality', () => {
251
251
  resolve: 0,
252
252
  });
253
253
 
254
- await sleep(30);
254
+ await sleep(20);
255
255
 
256
256
  expect(compC).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
257
257
  compute: 3,
@@ -261,7 +261,7 @@ describe('Async Signal functionality', () => {
261
261
 
262
262
  test('Awaiting a computed can handle errors', async () => {
263
263
  const compA = asyncComputed(async () => {
264
- await sleep(20);
264
+ await sleep(10);
265
265
 
266
266
  throw 'error';
267
267
  });
@@ -285,12 +285,142 @@ describe('Async Signal functionality', () => {
285
285
  resolve: 0,
286
286
  });
287
287
 
288
- await sleep(50);
288
+ await sleep(15);
289
289
 
290
290
  expect(compC).toHaveValueAndCounts(result(undefined, 'error', 'initial', 'error'), {
291
291
  compute: 2,
292
292
  resolve: 0,
293
293
  });
294
294
  });
295
+
296
+ test('Awaiting a computed does not let valid values override errors', async () => {
297
+ const compA = asyncComputed(async () => {
298
+ await sleep(10);
299
+
300
+ throw 'error';
301
+ });
302
+
303
+ const compB = asyncComputed(async () => {
304
+ await sleep(20);
305
+
306
+ return 2;
307
+ });
308
+
309
+ const compC = asyncComputed(async () => {
310
+ const aResult = compA.get();
311
+ const bResult = compB.get();
312
+
313
+ const b = bResult.await();
314
+ const a = aResult.await();
315
+
316
+ return a + b;
317
+ });
318
+
319
+ // Pull once to start the computation, trigger the computation
320
+ expect(compC).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
321
+ compute: 1,
322
+ resolve: 0,
323
+ });
324
+
325
+ await sleep(40);
326
+
327
+ expect(compC).toHaveValueAndCounts(result(undefined, 'error', 'initial', 'error'), {
328
+ compute: 2,
329
+ resolve: 0,
330
+ });
331
+ });
332
+
333
+ test('Await can be composed and nested', async () => {
334
+ const compA = asyncComputed('compA', async () => {
335
+ await sleep(20);
336
+ return 1;
337
+ });
338
+
339
+ const compB = asyncComputed('compB', async () => {
340
+ await sleep(20);
341
+ return 2;
342
+ });
343
+
344
+ const compC = computed('compC', () => {
345
+ const resultA = compA.get();
346
+ const resultB = compB.get();
347
+
348
+ return {
349
+ awaitA: resultA.await,
350
+ awaitB: resultB.await,
351
+ };
352
+ });
353
+
354
+ const compD = asyncComputed('compD', async () => {
355
+ const { awaitA, awaitB } = compC.get();
356
+ const a = awaitA();
357
+ const b = awaitB();
358
+
359
+ return a + b;
360
+ });
361
+
362
+ // Pull once to start the computation, trigger the computation
363
+ expect(compD).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
364
+ compute: 1,
365
+ resolve: 0,
366
+ });
367
+
368
+ await sleep(30);
369
+
370
+ expect(compD).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
371
+ compute: 2,
372
+ resolve: 1,
373
+ });
374
+ });
375
+
376
+ test('Await works with intermediate state', async () => {
377
+ const compA = asyncComputed('compA', async () => {
378
+ await sleep(20);
379
+ return 1;
380
+ });
381
+
382
+ const compB = asyncComputed('compB', async () => {
383
+ await sleep(40);
384
+ return 2;
385
+ });
386
+
387
+ const compC = computed('compC', () => {
388
+ const resultA = compA.get();
389
+ const resultB = compB.get();
390
+
391
+ return {
392
+ awaitA: resultA.await,
393
+ awaitB: resultB.await,
394
+ };
395
+ });
396
+
397
+ const compD = asyncComputed('compD', async () => {
398
+ const { awaitA, awaitB } = compC.get();
399
+ const a = awaitA();
400
+ const b = awaitB();
401
+
402
+ return a + b;
403
+ });
404
+
405
+ // Pull once to start the computation, trigger the computation
406
+ expect(compD).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
407
+ compute: 1,
408
+ resolve: 0,
409
+ });
410
+
411
+ await sleep(30);
412
+
413
+ expect(compD).toHaveValueAndCounts(result(undefined, 'pending', 'initial'), {
414
+ compute: 2,
415
+ resolve: 0,
416
+ });
417
+
418
+ await sleep(30);
419
+
420
+ expect(compD).toHaveValueAndCounts(result(3, 'success', 'resolved'), {
421
+ compute: 3,
422
+ resolve: 1,
423
+ });
424
+ });
295
425
  });
296
426
  });
@@ -227,7 +227,7 @@ describe('Subscription Signal functionality', () => {
227
227
  await nextTick();
228
228
 
229
229
  expect(w).toHaveCounts({ effect: 2 });
230
- expect(c).toHaveCounts({ get: 2, compute: 1 });
230
+ expect(c).toHaveValueAndCounts(123, { get: 3, compute: 1 });
231
231
  expect(s).toHaveValueAndCounts(123, {
232
232
  subscribe: 1,
233
233
  });
@@ -393,7 +393,7 @@ describe('Subscription Signal functionality', () => {
393
393
 
394
394
  return {
395
395
  update() {
396
- set(a.get() + 1);
396
+ set(a.get());
397
397
  },
398
398
  };
399
399
  },
@@ -419,7 +419,7 @@ describe('Subscription Signal functionality', () => {
419
419
 
420
420
  await nextTick();
421
421
 
422
- expect(s).toHaveValueAndCounts(2, {
422
+ expect(s).toHaveValueAndCounts(1, {
423
423
  subscribe: 1,
424
424
  update: 1,
425
425
  });
@@ -1,4 +1,4 @@
1
- import { expect } from 'vitest';
1
+ import { expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import {
3
3
  state as createState,
4
4
  computed as createComputed,
@@ -187,12 +187,12 @@ export function computed<T>(
187
187
  return wrapper;
188
188
  }
189
189
 
190
- export function asyncComputed<T>(name: string, compute: SignalAsyncCompute<T>, opts?: SignalOptions<T>): Signal<T>;
190
+ export function asyncComputed<T>(name: string, compute: SignalAsyncCompute<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
191
191
  export function asyncComputed<T>(
192
192
  name: string,
193
193
  compute: SignalAsyncCompute<T>,
194
194
  opts: SignalOptionsWithInit<T>,
195
- ): Signal<T>;
195
+ ): AsyncSignal<T>;
196
196
  export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts?: SignalOptions<T>): AsyncSignal<T>;
197
197
  export function asyncComputed<T>(compute: SignalAsyncCompute<T>, opts: SignalOptionsWithInit<T>): AsyncSignal<T>;
198
198
  export function asyncComputed<T>(
package/src/config.ts CHANGED
@@ -1,27 +1,27 @@
1
- let currentWatcherFlush: ReturnType<typeof setTimeout> | null = null;
2
- let currentDisconnectFlush: ReturnType<typeof setTimeout> | ReturnType<typeof requestIdleCallback> | null = null;
1
+ let currentFlush: ReturnType<typeof setTimeout> | null = null;
3
2
 
4
3
  export type FlushCallback = () => Promise<void>;
5
4
 
6
- const idleCallback =
7
- typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb: () => void) => setTimeout(cb, 0);
5
+ export type FlushFn = (fn: () => Promise<void>) => void;
8
6
 
9
- export let scheduleWatchers: (flushWatchers: FlushCallback) => void = flushWatchers => {
10
- if (currentWatcherFlush !== null) return;
7
+ export let scheduleFlush: FlushFn = flushWatchers => {
8
+ if (currentFlush !== null) return;
11
9
 
12
- currentWatcherFlush = setTimeout(() => {
13
- currentWatcherFlush = null;
10
+ currentFlush = setTimeout(() => {
11
+ currentFlush = null;
14
12
 
15
13
  flushWatchers();
16
14
  }, 0);
17
15
  };
18
16
 
19
- export let scheduleDisconnects: (flushDisconnects: FlushCallback) => void = flushDisconnects => {
20
- if (currentDisconnectFlush !== null) return;
17
+ export const setScheduleFlush = (flushFn: FlushFn) => {
18
+ scheduleFlush = flushFn;
19
+ };
20
+
21
+ export type BatchFn = (fn: () => void) => void;
21
22
 
22
- currentDisconnectFlush = idleCallback(() => {
23
- currentDisconnectFlush = null;
23
+ export let runBatch: BatchFn = fn => fn();
24
24
 
25
- flushDisconnects();
26
- });
25
+ export const setRunBatch = (batchFn: BatchFn) => {
26
+ runBatch = batchFn;
27
27
  };
package/src/scheduling.ts CHANGED
@@ -1,39 +1,26 @@
1
- import type { ComputedSignal } from './signals.js';
2
- import { scheduleWatchers, scheduleDisconnects } from './config.js';
1
+ import { ComputedSignal } from './signals.js';
2
+ import { scheduleFlush, runBatch } from './config.js';
3
3
 
4
- let PENDING_FLUSH_WATCHERS: {
5
- promise: Promise<void>;
6
- resolve: () => void;
7
- } | null = null;
4
+ let PENDING_DIRTIES: ComputedSignal<any>[] = [];
5
+ let PENDING_PULLS: ComputedSignal<any>[] = [];
6
+ let PENDING_WATCHERS: ComputedSignal<any>[] = [];
7
+ let PENDING_DISCONNECTS = new Map<ComputedSignal<any>, number>();
8
8
 
9
- const PENDING_WATCHERS: ComputedSignal<any>[] = [];
10
- const PENDING_DISCONNECTS: Map<ComputedSignal<any>, number> = new Map();
9
+ const microtask = () => Promise.resolve();
11
10
 
12
11
  export const scheduleWatcher = (watcher: ComputedSignal<any>) => {
13
12
  PENDING_WATCHERS.push(watcher);
14
-
15
- if (PENDING_FLUSH_WATCHERS === null) {
16
- let resolve: () => void;
17
-
18
- const promise = new Promise<void>(r => {
19
- resolve = r;
20
- });
21
-
22
- PENDING_FLUSH_WATCHERS = { promise, resolve: resolve! };
23
- }
24
-
25
- scheduleWatchers(flushWatchers);
13
+ scheduleFlush(flushWatchers);
26
14
  };
27
15
 
28
- const flushWatchers = async () => {
29
- PENDING_FLUSH_WATCHERS!.resolve();
30
- PENDING_FLUSH_WATCHERS = null;
31
- let watcher;
32
- while ((watcher = PENDING_WATCHERS.shift())) {
33
- watcher._check();
34
- }
16
+ export const scheduleDirty = (signal: ComputedSignal<any>) => {
17
+ PENDING_DIRTIES.push(signal);
18
+ scheduleFlush(flushWatchers);
19
+ };
35
20
 
36
- PENDING_WATCHERS.length = 0;
21
+ export const schedulePull = (signal: ComputedSignal<any>) => {
22
+ PENDING_PULLS.push(signal);
23
+ scheduleFlush(flushWatchers);
37
24
  };
38
25
 
39
26
  export const scheduleDisconnect = (disconnect: ComputedSignal<any>) => {
@@ -41,14 +28,35 @@ export const scheduleDisconnect = (disconnect: ComputedSignal<any>) => {
41
28
 
42
29
  PENDING_DISCONNECTS.set(disconnect, current + 1);
43
30
 
44
- scheduleDisconnects(flushDisconnects);
31
+ scheduleFlush(flushWatchers);
45
32
  };
46
33
 
47
- const flushDisconnects = async () => {
48
- await PENDING_FLUSH_WATCHERS?.promise;
49
- for (const [signal, count] of PENDING_DISCONNECTS) {
50
- signal._disconnect(count);
34
+ const flushWatchers = async () => {
35
+ while (PENDING_DIRTIES.length > 0 || PENDING_PULLS.length > 0) {
36
+ for (const dirty of PENDING_DIRTIES) {
37
+ dirty._dirtyConsumers();
38
+ }
39
+
40
+ for (const pull of PENDING_PULLS) {
41
+ pull._check();
42
+ }
43
+
44
+ PENDING_DIRTIES = [];
45
+ PENDING_PULLS = [];
46
+
47
+ await microtask();
51
48
  }
52
49
 
53
- PENDING_DISCONNECTS.clear();
50
+ runBatch(() => {
51
+ for (const watcher of PENDING_WATCHERS) {
52
+ watcher._check();
53
+ }
54
+
55
+ for (const [signal, count] of PENDING_DISCONNECTS) {
56
+ signal._disconnect(count);
57
+ }
58
+
59
+ PENDING_WATCHERS = [];
60
+ PENDING_DISCONNECTS.clear();
61
+ });
54
62
  };