shelving 1.195.1 → 1.196.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.195.1",
3
+ "version": "1.196.1",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,6 +8,7 @@ export type PayloadFetchCallback<P, R> = (payload: P, signal: AbortSignal) => R
8
8
  * @param payload The initial payload for the store.
9
9
  * @param value The initial value for the store, or `NONE` if it does not have one yet.
10
10
  * @param callback An optional callback that, if set, will be called with the current payload when the `fetch()` method is invoked to fetch the next value.
11
+ * @param debounce Delay in milliseconds before the fetch is triggered after a payload change. `busy` becomes `true` immediately; the actual fetch waits for the debounce period to expire. If the payload changes again before the delay expires the previous fetch is cancelled and the timer resets.
11
12
  */
12
13
  export declare class PayloadFetchStore<P, R> extends FetchStore<R> {
13
14
  /**
@@ -15,6 +16,6 @@ export declare class PayloadFetchStore<P, R> extends FetchStore<R> {
15
16
  * - New payloads can be set using `this.payload.value`
16
17
  */
17
18
  readonly payload: Store<P>;
18
- constructor(payload: P, value: R | typeof NONE, callback?: PayloadFetchCallback<P, R>);
19
+ constructor(payload: P | typeof NONE, value: R | typeof NONE, callback?: PayloadFetchCallback<P, R>, debounce?: number);
19
20
  [Symbol.asyncDispose](): Promise<void>;
20
21
  }
@@ -1,3 +1,4 @@
1
+ import { awaitAbort, awaitRace, getDelay } from "../util/async.js";
1
2
  import { awaitDispose } from "../util/dispose.js";
2
3
  import { FetchStore } from "./FetchStore.js";
3
4
  import { Store } from "./Store.js";
@@ -7,6 +8,7 @@ import { Store } from "./Store.js";
7
8
  * @param payload The initial payload for the store.
8
9
  * @param value The initial value for the store, or `NONE` if it does not have one yet.
9
10
  * @param callback An optional callback that, if set, will be called with the current payload when the `fetch()` method is invoked to fetch the next value.
11
+ * @param debounce Delay in milliseconds before the fetch is triggered after a payload change. `busy` becomes `true` immediately; the actual fetch waits for the debounce period to expire. If the payload changes again before the delay expires the previous fetch is cancelled and the timer resets.
10
12
  */
11
13
  export class PayloadFetchStore extends FetchStore {
12
14
  /**
@@ -15,9 +17,16 @@ export class PayloadFetchStore extends FetchStore {
15
17
  */
16
18
  payload;
17
19
  // Override to save initial payload and callback.
18
- constructor(payload, value, callback) {
20
+ constructor(payload, value, callback, debounce = 0) {
19
21
  const payloadStore = new Store(payload);
20
- super(value, callback && (signal => callback(payloadStore.value, signal)));
22
+ const fetch = callback &&
23
+ (async (signal) => {
24
+ if (debounce > 0)
25
+ await awaitRace(getDelay(debounce), awaitAbort(signal));
26
+ const value = payloadStore.loading ? await awaitRace(payloadStore.next, awaitAbort(signal)) : payloadStore.value;
27
+ return callback(value, signal);
28
+ });
29
+ super(value, fetch);
21
30
  this.payload = payloadStore;
22
31
  void _iterate(this);
23
32
  }
package/util/async.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ImmutableArray } from "./array.js";
2
- import type { ErrorCallback, ValueCallback } from "./function.js";
2
+ import { type ErrorCallback, type ValueCallback } from "./function.js";
3
3
  /** Is a value an asynchronous value implementing a `then()` function. */
4
4
  export declare function isAsync<T>(value: PromiseLike<T> | T): value is PromiseLike<T>;
5
5
  /** Is a value a synchronous value. */
@@ -62,3 +62,20 @@ export type Deferred<T = unknown> = {
62
62
  export declare function getDeferred<T = void>(): Deferred<T>;
63
63
  /** Get a promise that automatically resolves after a delay. */
64
64
  export declare function getDelay(ms: number): Promise<void>;
65
+ /**
66
+ * Get a promise that rejects with the signal's reason when an `AbortSignal` fires.
67
+ * - Rejects immediately if the signal is already aborted.
68
+ * - Use with `awaitRace()` to cancel a concurrent operation when a signal fires.
69
+ *
70
+ * @example await awaitRace(getDelay(300), awaitAbort(signal));
71
+ */
72
+ export declare function awaitAbort(signal: AbortSignal): Promise<never>;
73
+ /**
74
+ * Race promises like `Promise.race()` but silently swallow rejections from the losers.
75
+ * - Returns a promise that settles with the first input to settle, exactly like `Promise.race()`.
76
+ * - The losing inputs keep running (Promises cannot be cancelled), but their eventual rejection — if any — is silently absorbed instead of bubbling up as an unhandled rejection.
77
+ * - Built for cancellation/timeout patterns, where the loser's eventual fate is genuinely uninteresting once another arm has settled. Do not use when both arms might surface meaningful errors that the caller should see.
78
+ *
79
+ * @example await awaitRace(getDelay(300), awaitAbort(signal)); // delay or abort, no leaked ABORT rejection if delay wins
80
+ */
81
+ export declare function awaitRace<T>(...promises: Promise<T>[]): Promise<T>;
package/util/async.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Errors } from "../error/Errors.js";
2
2
  import { RequiredError } from "../error/RequiredError.js";
3
+ import { BLACKHOLE } from "./function.js";
3
4
  /** Is a value an asynchronous value implementing a `then()` function. */
4
5
  export function isAsync(value) {
5
6
  return typeof value === "object" && value !== null && typeof value.then === "function";
@@ -110,3 +111,33 @@ export function getDeferred() {
110
111
  export function getDelay(ms) {
111
112
  return new Promise(resolve => setTimeout(resolve, ms));
112
113
  }
114
+ /**
115
+ * Get a promise that rejects with the signal's reason when an `AbortSignal` fires.
116
+ * - Rejects immediately if the signal is already aborted.
117
+ * - Use with `awaitRace()` to cancel a concurrent operation when a signal fires.
118
+ *
119
+ * @example await awaitRace(getDelay(300), awaitAbort(signal));
120
+ */
121
+ export function awaitAbort(signal) {
122
+ const promise = new Promise((_, reject) => {
123
+ if (signal.aborted)
124
+ reject(signal.reason);
125
+ else
126
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
127
+ });
128
+ promise.catch(BLACKHOLE);
129
+ return promise;
130
+ }
131
+ /**
132
+ * Race promises like `Promise.race()` but silently swallow rejections from the losers.
133
+ * - Returns a promise that settles with the first input to settle, exactly like `Promise.race()`.
134
+ * - The losing inputs keep running (Promises cannot be cancelled), but their eventual rejection — if any — is silently absorbed instead of bubbling up as an unhandled rejection.
135
+ * - Built for cancellation/timeout patterns, where the loser's eventual fate is genuinely uninteresting once another arm has settled. Do not use when both arms might surface meaningful errors that the caller should see.
136
+ *
137
+ * @example await awaitRace(getDelay(300), awaitAbort(signal)); // delay or abort, no leaked ABORT rejection if delay wins
138
+ */
139
+ export function awaitRace(...promises) {
140
+ for (const promise of promises)
141
+ promise.catch(BLACKHOLE);
142
+ return Promise.race(promises);
143
+ }