@zingage/scheduling 1.1.0 → 1.2.0

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.
Files changed (42) hide show
  1. package/README.md +184 -5
  2. package/dist/batcher.d.ts +231 -0
  3. package/dist/batcher.d.ts.map +1 -0
  4. package/dist/batcher.js +391 -0
  5. package/dist/batcher.js.map +1 -0
  6. package/dist/collapsed-task-creator.d.ts +235 -0
  7. package/dist/collapsed-task-creator.d.ts.map +1 -0
  8. package/dist/collapsed-task-creator.js +491 -0
  9. package/dist/collapsed-task-creator.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/logger.d.ts +16 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/logger.js +14 -0
  17. package/dist/logger.js.map +1 -0
  18. package/dist/pending-promises-tracker.d.ts +32 -0
  19. package/dist/pending-promises-tracker.d.ts.map +1 -0
  20. package/dist/pending-promises-tracker.js +60 -0
  21. package/dist/pending-promises-tracker.js.map +1 -0
  22. package/dist/performance-now.d.ts +14 -0
  23. package/dist/performance-now.d.ts.map +1 -0
  24. package/dist/performance-now.js +10 -0
  25. package/dist/performance-now.js.map +1 -0
  26. package/dist/scheduling-contract-error.d.ts +9 -0
  27. package/dist/scheduling-contract-error.d.ts.map +1 -0
  28. package/dist/scheduling-contract-error.js +9 -0
  29. package/dist/scheduling-contract-error.js.map +1 -0
  30. package/dist/wrap-with-task-collapsing.d.ts +30 -0
  31. package/dist/wrap-with-task-collapsing.d.ts.map +1 -0
  32. package/dist/wrap-with-task-collapsing.js +80 -0
  33. package/dist/wrap-with-task-collapsing.js.map +1 -0
  34. package/package.json +10 -4
  35. package/src/batcher.ts +614 -0
  36. package/src/collapsed-task-creator.ts +774 -0
  37. package/src/index.ts +22 -0
  38. package/src/logger.ts +32 -0
  39. package/src/pending-promises-tracker.ts +62 -0
  40. package/src/performance-now.ts +21 -0
  41. package/src/scheduling-contract-error.ts +8 -0
  42. package/src/wrap-with-task-collapsing.ts +103 -0
@@ -0,0 +1,391 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { partition } from "es-toolkit";
3
+ import { defaultLoggersByComponent } from "./logger.js";
4
+ import { PendingPromisesTracker } from "./pending-promises-tracker.js";
5
+ import { performanceNow } from "./performance-now.js";
6
+ import { SchedulingContractError } from "./scheduling-contract-error.js";
7
+ /**
8
+ * Throws a {@link SchedulingContractError} unless a batched call returned
9
+ * exactly one result per request.
10
+ */
11
+ function assertResultCount(results, requestCount) {
12
+ if (results.length !== requestCount) {
13
+ throw new SchedulingContractError("Result count must match request count");
14
+ }
15
+ }
16
+ function asError(error) {
17
+ return error instanceof Error
18
+ ? error
19
+ : new Error("Batch dispatch failed", { cause: error });
20
+ }
21
+ /** Settles `result` with `outcome`. */
22
+ export function settleResult(result, outcome) {
23
+ if (outcome.ok) {
24
+ result.resolve(outcome.value);
25
+ }
26
+ else {
27
+ // Cancellation can reject the result before anyone awaits it.
28
+ // oxlint-disable-next-line no-void -- Marks the rejection handled; the caller still gets the rejected promise.
29
+ void result.promise.catch(() => { });
30
+ result.reject(outcome.error);
31
+ }
32
+ }
33
+ /** The default `maxBatchSize`. */
34
+ export const DEFAULT_MAX_BATCH_SIZE = 20;
35
+ /**
36
+ * Queues work and dispatches it in batches. The batcher does not share work,
37
+ * and it knows nothing of `CollapsedTaskCreator`. To share work, compose the
38
+ * two: a creator's `schedule` enqueues one item per started task, for example
39
+ * `batcher.enqueue(toStart.map((view) => ({input: view, signal:
40
+ * view.signal})))`. Then `dispatch` receives each live view, and can read
41
+ * its `collapsedRequestCount` at dispatch time. For work that is never
42
+ * shared, such as a cache's reads from its store, use a batcher directly, so
43
+ * that each request gets its own entry.
44
+ *
45
+ * Entries wait in automatic batches, which follow DataLoader's rules. A new
46
+ * batch schedules its own dispatch with `batchScheduleFn` when it gets its
47
+ * first entry. Once it holds `maxBatchSize` queued entries, the next entry
48
+ * opens a new batch with its own scheduled dispatch. A full batch does not
49
+ * dispatch early: it waits for its own callback. A caller that wants its
50
+ * work run at once calls `dispatchNow` with its new items, and with the
51
+ * handles of queued entries it also waits for, such as entries from other
52
+ * callers' enqueues. That dispatch is never split, and its new items never
53
+ * wait in a batch.
54
+ *
55
+ * Cancellation removes a queued entry: when its item's signal aborts, the
56
+ * entry leaves the queue and its result rejects with the signal's reason.
57
+ * Dispatched work always completes.
58
+ *
59
+ * Shutdown has two steps, because an owner can still accept work after its
60
+ * shutdown starts:
61
+ *
62
+ * 1. `flush` sends every pending batch at once, and drops `batchScheduleFn`,
63
+ * so later batches use the default schedule, which always fires. An owner
64
+ * calls it when its shutdown starts, because its `batchScheduleFn` may
65
+ * stop firing then.
66
+ * 2. `close` flushes too, and resolves once the batcher is idle. Then the
67
+ * batcher is closed. An owner calls it after it stops accepting work. An
68
+ * enqueue after that is a bug in the owner, so it logs a warning, but the
69
+ * work still runs.
70
+ *
71
+ * Every entry's result settles, including on cancellation. So a caller that
72
+ * must hold a resource for an item's whole life acquires it before `enqueue`
73
+ * and releases it in a `.finally` on the item's result. The batcher has no
74
+ * hook for this.
75
+ */
76
+ export class Batcher {
77
+ #maxBatchSize;
78
+ #preserveAsyncContext;
79
+ /** Set to undefined by {@link flush}. */
80
+ #batchScheduleFn;
81
+ #logger;
82
+ #dispatch;
83
+ /**
84
+ * The batch that new entries join while it has room. Undefined before the
85
+ * first enqueue, and after the open batch dispatches.
86
+ */
87
+ #openBatch;
88
+ /**
89
+ * Batches whose dispatch is scheduled and has not run, so `flush` and
90
+ * `#drain` can dispatch them early.
91
+ */
92
+ #pendingBatches = new Set();
93
+ /** Dispatches that have not settled, so `#drain` can wait for them. */
94
+ #running = new PendingPromisesTracker();
95
+ /**
96
+ * `"closing"` from the call to `close` until the batcher is idle. Then
97
+ * `"closed"`, and `enqueue` warns.
98
+ */
99
+ #status = "open";
100
+ /** The promise that `close` returns, so repeated calls share it. */
101
+ #closed;
102
+ constructor(options) {
103
+ const maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
104
+ if (maxBatchSize !== Infinity &&
105
+ (!Number.isInteger(maxBatchSize) || maxBatchSize < 1)) {
106
+ throw new TypeError("maxBatchSize must be a positive integer");
107
+ }
108
+ this.#maxBatchSize = maxBatchSize;
109
+ this.#preserveAsyncContext = options.preserveAsyncContext ?? false;
110
+ this.#batchScheduleFn = options.batchScheduleFn;
111
+ this.#logger = options.logger ?? defaultLoggersByComponent.batcher;
112
+ this.#dispatch = options.dispatch;
113
+ }
114
+ /**
115
+ * Queues one entry per item, in order, and returns each item's result and
116
+ * handle.
117
+ *
118
+ * Each entry joins the open automatic batch, or opens a new one when that
119
+ * batch is full. An item whose signal already aborted is not queued: its
120
+ * result is rejected with the signal's reason, and it has no handle. The
121
+ * other items are not affected. After the batcher has closed, it also logs
122
+ * a warning.
123
+ */
124
+ enqueue(items) {
125
+ if (this.#status === "closed") {
126
+ this.#logger("batcher", "warn", "enqueue called after close", {
127
+ itemCount: items.length,
128
+ });
129
+ }
130
+ // Batches that this call opens. Their dispatches are scheduled only once
131
+ // every entry is in place: a `batchScheduleFn` that runs its callback
132
+ // synchronously would otherwise dispatch a batch before its entries
133
+ // joined, and strand them in a dispatched batch.
134
+ const opened = [];
135
+ // One call runs at one time, so its entries share it.
136
+ const enqueuedAt = performanceNow();
137
+ const enqueued = items.map((item) => {
138
+ // Like a queued entry that is cancelled, an item that is already
139
+ // cancelled is rejected, not sent.
140
+ if (item.signal?.aborted) {
141
+ const result = Promise.withResolvers();
142
+ settleResult(result, { ok: false, error: item.signal.reason });
143
+ return { result: result.promise, handle: undefined };
144
+ }
145
+ const { isNew: batchIsNew, batch } = this.#joinableBatch();
146
+ if (batchIsNew) {
147
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Collects the batches that this call opens, in order.
148
+ opened.push(batch);
149
+ }
150
+ const entry = this.#makeEntryInBatch(item, batch, enqueuedAt);
151
+ return { result: entry.result.promise, handle: entry };
152
+ });
153
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Schedules each opened batch for its side effect.
154
+ opened.forEach((batch) => this.#scheduleDispatch(batch));
155
+ return enqueued;
156
+ }
157
+ #makeEntryInBatch(item, batch, enqueuedAt) {
158
+ const entry = {
159
+ item,
160
+ batch,
161
+ enqueuedAt,
162
+ state: "queued",
163
+ result: Promise.withResolvers(),
164
+ cleanup: (() => {
165
+ const { signal } = item;
166
+ if (!signal)
167
+ return undefined;
168
+ const onAbort = () => {
169
+ if (entry.state !== "queued")
170
+ return;
171
+ this.#settle(entry, { ok: false, error: signal.reason });
172
+ };
173
+ signal.addEventListener("abort", onAbort, { once: true });
174
+ return () => signal.removeEventListener("abort", onAbort);
175
+ })(),
176
+ };
177
+ // Each entry joins its batch here, so that `batch.entries` and
178
+ // `batch.queuedCount` agree from the moment `entry.batch` is set. A
179
+ // separate placement pass would leave a window in which a queued entry has
180
+ // no batch.
181
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- The batch owns its entry list, and the entry joins it here.
182
+ batch.entries.push(entry);
183
+ batch.queuedCount += 1;
184
+ return entry;
185
+ }
186
+ /**
187
+ * Dispatches the still-queued entries among `handles`, plus the new
188
+ * `items`, as one call, which is never split. It skips entries that are
189
+ * running or settled. A caller can pass handles from other callers'
190
+ * enqueues, so one dispatch covers all of the work it waits for.
191
+ *
192
+ * The new items never wait in a batch, so a `batchScheduleFn` cannot send
193
+ * them early. An item whose signal already aborted is rejected and not
194
+ * sent. Returns each item's result, in order. After close, it logs a
195
+ * warning when it gets items, as `enqueue` does.
196
+ *
197
+ * It drops repeated handles, because one call can hold the same handle
198
+ * more than once: two requests of a call that share one task get the same
199
+ * handle, and the entry must be sent once.
200
+ */
201
+ dispatchNow(handles, items = []) {
202
+ if (this.#status === "closed" && items.length > 0) {
203
+ this.#logger("batcher", "warn", "dispatchNow called after close", {
204
+ itemCount: items.length,
205
+ });
206
+ }
207
+ const results = items.map((it) => ({
208
+ result: Promise.withResolvers(),
209
+ signal: it.signal,
210
+ input: it.input,
211
+ }));
212
+ const [immediateItems, abortedItems] = partition(results, ({ signal }) => !signal?.aborted);
213
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Settles each aborted item for its side effect.
214
+ abortedItems.forEach(({ signal, result }) => {
215
+ settleResult(result, { ok: false, error: signal?.reason });
216
+ });
217
+ this.#send([
218
+ ...[...new Set(handles)].map((entry) => ({
219
+ kind: "entry",
220
+ entry,
221
+ })),
222
+ ...immediateItems.map(({ input, result }) => ({
223
+ kind: "immediate",
224
+ input,
225
+ result,
226
+ })),
227
+ ], false);
228
+ // oxlint-disable-next-line typescript/promise-function-async -- Returns the existing promises; an async wrapper would add new ones.
229
+ return results.map((r) => r.result.promise);
230
+ }
231
+ /**
232
+ * Dispatches every pending batch now, and drops `batchScheduleFn`, which
233
+ * the owner may stop during shutdown. Later batches use the default
234
+ * schedule. The batcher stays open: later enqueues do not warn.
235
+ */
236
+ flush() {
237
+ this.#batchScheduleFn = undefined;
238
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Dispatches each pending batch for its side effect.
239
+ this.#pendingBatches.forEach((batch) => batch.dispatch());
240
+ }
241
+ /**
242
+ * Flushes, then resolves when the batcher is idle. Then the batcher is
243
+ * closed, and later enqueues log a warning. Repeated calls return the same
244
+ * promise.
245
+ */
246
+ // oxlint-disable-next-line typescript/promise-function-async -- Repeated calls must return the same promise.
247
+ close() {
248
+ if (this.#closed)
249
+ return this.#closed;
250
+ this.#status = "closing";
251
+ this.flush();
252
+ this.#closed = this.#drain().then(() => {
253
+ this.#status = "closed";
254
+ });
255
+ return this.#closed;
256
+ }
257
+ /**
258
+ * Waits until no batch is pending and no dispatch is running. It loops
259
+ * because a dispatch can enqueue more work into this batcher before it
260
+ * settles, for example a producer that requests other keys from its own
261
+ * wrapper.
262
+ */
263
+ async #drain() {
264
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Loops until idle, because a dispatch can enqueue more work.
265
+ while (this.#running.size > 0 || this.#pendingBatches.size > 0) {
266
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Dispatches each pending batch for its side effect.
267
+ this.#pendingBatches.forEach((batch) => batch.dispatch());
268
+ await this.#running.drain();
269
+ }
270
+ }
271
+ /**
272
+ * Returns the open batch when it has room, as DataLoader's
273
+ * `getCurrentBatch` does. Otherwise it opens a new batch, and reports it as
274
+ * new: the caller schedules a new batch's dispatch.
275
+ */
276
+ #joinableBatch() {
277
+ const open = this.#openBatch;
278
+ if (open && open.queuedCount < this.#maxBatchSize) {
279
+ return { batch: open, isNew: false };
280
+ }
281
+ const dispatch = () => this.#dispatchBatch(batch);
282
+ const batch = {
283
+ entries: [],
284
+ queuedCount: 0,
285
+ dispatch: this.#preserveAsyncContext
286
+ ? AsyncLocalStorage.bind(dispatch)
287
+ : dispatch,
288
+ };
289
+ this.#openBatch = batch;
290
+ this.#pendingBatches.add(batch);
291
+ return { batch, isNew: true };
292
+ }
293
+ /** Schedules a batch's dispatch with `batchScheduleFn`, or the default. */
294
+ #scheduleDispatch(batch) {
295
+ const { dispatch } = batch;
296
+ if (this.#batchScheduleFn)
297
+ this.#batchScheduleFn(dispatch);
298
+ // oxlint-disable-next-line no-void -- Schedules the dispatch; nothing awaits it.
299
+ else
300
+ void Promise.resolve().then(() => process.nextTick(dispatch));
301
+ }
302
+ /**
303
+ * The only code that changes `entry.state` after `enqueue` creates the
304
+ * entry. Keep it that way: it keeps `Batch.queuedCount` correct. When the
305
+ * entry leaves the queued state, it frees the entry's slot in its batch.
306
+ * From any other state, it only sets the new state.
307
+ */
308
+ #setEntryState(entry, state) {
309
+ if (entry.state === "queued")
310
+ entry.batch.queuedCount -= 1;
311
+ entry.state = state;
312
+ }
313
+ /**
314
+ * Marks an entry settled, runs its cleanup, and settles its result with
315
+ * `outcome`. The outcome is explicit because an error can be any value,
316
+ * such as an abort reason that is a string.
317
+ */
318
+ #settle(entry, outcome) {
319
+ this.#setEntryState(entry, "settled");
320
+ entry.cleanup?.();
321
+ entry.cleanup = undefined;
322
+ settleResult(entry.result, outcome);
323
+ }
324
+ /**
325
+ * Sends a batch's still-queued entries, once. A batch leaves
326
+ * `#pendingBatches` only here, so membership marks it undispatched.
327
+ */
328
+ #dispatchBatch(batch) {
329
+ if (!this.#pendingBatches.delete(batch))
330
+ return;
331
+ if (this.#openBatch === batch)
332
+ this.#openBatch = undefined;
333
+ this.#send(batch.entries.map((entry) => ({ kind: "entry", entry })), true);
334
+ batch.entries.length = 0;
335
+ }
336
+ /**
337
+ * Sends `selected` as one call, in the current async context. It skips
338
+ * entries that are no longer queued, and sends every immediate item.
339
+ * `autoBatched` says whether an automatic batch sends them.
340
+ */
341
+ #send(selected, autoBatched) {
342
+ const toSend = selected.filter((it) => it.kind === "immediate" || it.entry.state === "queued");
343
+ if (toSend.length === 0)
344
+ return;
345
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Settles each sent item for its side effect.
346
+ toSend.forEach((it) => {
347
+ if (it.kind === "entry")
348
+ this.#setEntryState(it.entry, "running");
349
+ });
350
+ const settle = (it, outcome) => {
351
+ if (it.kind === "entry")
352
+ this.#settle(it.entry, outcome);
353
+ else
354
+ settleResult(it.result, outcome);
355
+ };
356
+ const now = performanceNow();
357
+ // oxlint-disable-next-line typescript/promise-function-async -- Promise.try adopts this promise; an async wrapper would delay settling by more microtasks.
358
+ const run = () => this.#dispatch(toSend.map((it) => it.kind === "entry"
359
+ ? {
360
+ input: it.entry.item.input,
361
+ queueWaitMs: now - it.entry.enqueuedAt,
362
+ }
363
+ : { input: it.input, queueWaitMs: 0 }), { autoBatched });
364
+ // Track the operation before `dispatch` runs. A `dispatch` that calls
365
+ // `close` synchronously must find this operation in `#running`, or `close`
366
+ // could resolve before the dispatch settles.
367
+ // oxlint-disable-next-line no-void -- The tracker keeps the operation for close; nothing else awaits it.
368
+ void this.#running.run(
369
+ // oxlint-disable-next-line typescript/promise-function-async -- Promise.try adopts the dispatch promise; an async wrapper would add microtasks.
370
+ () => Promise.try(run)
371
+ .then((results) => {
372
+ assertResultCount(results, toSend.length);
373
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Settles each sent item for its side effect.
374
+ toSend.forEach((it, index) => {
375
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- assertResultCount checked the length. A runtime check would be wrong, because Result can include undefined.
376
+ const result = results[index];
377
+ settle(it, result instanceof Error
378
+ ? { ok: false, error: result }
379
+ : { ok: true, value: result });
380
+ });
381
+ })
382
+ .catch((error) => {
383
+ const failure = asError(error);
384
+ // oxlint-disable-next-line zingage/no-restricted-syntax -- Settles each sent item for its side effect.
385
+ toSend.forEach((it) => {
386
+ settle(it, { ok: false, error: failure });
387
+ });
388
+ }));
389
+ }
390
+ }
391
+ //# sourceMappingURL=batcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batcher.js","sourceRoot":"","sources":["../src/batcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,235 @@
1
+ /** A task as the creator shows it to `schedule`. */
2
+ export type SharedTaskView<Input> = Readonly<Pick<TaskStart<Input>, "input" | "signal" | "collapsedRequestCount" | "startReason">>;
3
+ type TaskRequest<Input> = {
4
+ input: Input;
5
+ signal?: AbortSignal | undefined;
6
+ };
7
+ /** What `startOrRideTasks` returns for one task request. */
8
+ export type TaskRequestOutcome<Result, Handle> = {
9
+ readonly result: Promise<Result>;
10
+ /**
11
+ * The handle of the request's task. Undefined for a request that was
12
+ * already cancelled, which gets no task.
13
+ */
14
+ readonly handle: Handle | undefined;
15
+ readonly rode: boolean;
16
+ };
17
+ /**
18
+ * What `startOrRideTasks` returns: one outcome per task request, at the same
19
+ * position. A tuple of requests gives a tuple of outcomes of the same length.
20
+ */
21
+ export type TaskRequestOutcomes<Requests extends readonly unknown[], Result, Handle> = {
22
+ -readonly [K in keyof Requests]: TaskRequestOutcome<Result, Handle>;
23
+ };
24
+ /**
25
+ * The `schedule` option of a {@link CollapsedTaskCreator}. `context` is the value the
26
+ * `startOrRideTasks` call passed.
27
+ */
28
+ export type ScheduleTasks<Input, Result, Handle, Context = void> = (toStart: readonly SharedTaskView<Input>[], context: Context) => readonly Readonly<{
29
+ result: Promise<Result>;
30
+ handle: Handle;
31
+ }>[];
32
+ /**
33
+ * Why a task request started a task instead of riding one:
34
+ * - `noKey`: `makeKey` returned `undefined`, so the request never shares work.
35
+ * - `noPendingTask`: no task for its key was pending.
36
+ * - `pendingTaskTooOld`: the pending task for its key started more than
37
+ * `collapseTasksMs` ago.
38
+ * - `pendingTaskCancelled`: the pending task for its key was cancelled.
39
+ */
40
+ export type TaskStartReason = "noKey" | "noPendingTask" | "pendingTaskTooOld" | "pendingTaskCancelled";
41
+ /** The parts of a task that exist before its `schedule` call returns. */
42
+ type TaskStart<Input> = {
43
+ /**
44
+ * Why the task request that started the task did not ride one. It is for
45
+ * diagnostics, for example a log line that `schedule` writes.
46
+ */
47
+ readonly startReason: TaskStartReason;
48
+ /**
49
+ * Compared with `collapseTasksMs`, so no task request rides a task that is
50
+ * too old.
51
+ */
52
+ readonly startedAt: number;
53
+ /**
54
+ * The input of the task request that STARTED the task. Task requests that
55
+ * ride the task never replace it, so per-request labels such as a producer
56
+ * trigger keep the starting task request's value.
57
+ */
58
+ readonly input: Input;
59
+ /**
60
+ * Aborts when every request on the task is cancelled. A task request is
61
+ * cancelled when its own signal aborts. A task request without a signal can
62
+ * never be cancelled, so it keeps the task's signal from aborting. Its
63
+ * reason is the signal reason of the last request to be cancelled.
64
+ *
65
+ * Undefined when the request that started the task has no signal: the task
66
+ * can never be cancelled, so it needs no signal, and `schedule` skips any
67
+ * cancellation work. The converse does not hold. A defined signal can still
68
+ * never abort, when a later request without a signal makes the task
69
+ * uncancellable after `schedule` received the signal.
70
+ *
71
+ * The lookup in `startOrRideTasks` also reads it, so no request rides a
72
+ * cancelled task.
73
+ */
74
+ readonly signal: AbortSignal | undefined;
75
+ /**
76
+ * The number of task requests that have ridden the task so far. Each one is
77
+ * a request that sharing served without starting new work. The task request
78
+ * that started the task does not count, and a cancelled rider still does.
79
+ *
80
+ * `schedule` receives this object itself, not a copy, so a reader sees
81
+ * every ride up to the time it reads the field. A call records its rides
82
+ * only after its `schedule` returns, so `schedule` never sees its own
83
+ * call's rides. Only the creator writes it: `SharedTaskView` makes it
84
+ * readonly.
85
+ */
86
+ collapsedRequestCount: number;
87
+ };
88
+ /**
89
+ * Lets requests for the same work share one pending task, with optional
90
+ * cancellation.
91
+ *
92
+ * Imagine you have a function that, when called, kicks off a task, and
93
+ * returns a promise representing the result of the task. Now, suppose the task
94
+ * is expensive, but not effectful, like fetching from a REST API. If your code
95
+ * attempts to run this task many times in quick succession, or many times in
96
+ * parallel, this can be wasteful (if the result will always or likely be the
97
+ * same) and/or overload whatever's doing work as part of the task (eg the API).
98
+ *
99
+ * There are a large number of npm packages that make it easy to better schedule
100
+ * this work (e.g., queueing some parallel runs of the task behind others, if
101
+ * the number of tasks already running exceeds some concurrency limit) or to
102
+ * reuse the results of some runs of the task for others (by debouncing or
103
+ * throttling calls to the function that starts the task). However, none of them
104
+ * have quite the -- admittedly a bit quirky -- semantics that we want, and that
105
+ * we implement here.
106
+ *
107
+ * Specifically, a task request reuses a pending task, instead of starting a
108
+ * new one, if and only if: 1) its input has the same key as the task's input
109
+ * [a la memoize]; 2) the task started no more than `collapseTasksMs` ago [a la
110
+ * throttle]; and 3) the task is still running [unique], and is not cancelled.
111
+ * In other words, it memoizes task creation, but then reverts to starting a
112
+ * new task after a certain number of milliseconds or after the last-started
113
+ * task finishes, whichever is first. So it's sort of a combination of memoize,
114
+ * throttle, and promise chaining.
115
+ *
116
+ * Beyond that collapsing, it adds three things:
117
+ *
118
+ * - One `startOrRideTasks` call takes many task requests. Each task request
119
+ * either starts a new task or rides a pending task for its input's key. The
120
+ * tasks that one call starts are scheduled together, in one `schedule` call,
121
+ * so a bulk caller's new inputs become one unit of work. Task requests that
122
+ * ride start nothing.
123
+ * - Each task has an optional AbortSignal that aborts when every request on the
124
+ * task is cancelled. `schedule` passes it to whatever runs the work, such as
125
+ * a batcher's queue or a producer that supports cancellation. See
126
+ * `SharedTaskView.signal`.
127
+ * - It counts the task requests that ride each task, for diagnostics.
128
+ *
129
+ * For the simple case, with one call per task request and no cancellation,
130
+ * `wrapWithTaskCollapsing` wraps a task-starting function with this class.
131
+ *
132
+ * Terms:
133
+ * - An input is the value that a task's work needs, such as a producer request.
134
+ * `makeKey` derives its key.
135
+ * - A task request is one input plus an optional signal that cancels the task
136
+ * request. It is one item of `startOrRideTasks`'s `taskRequests`.
137
+ * - A task request starts a task or rides one. A task's requests are the task
138
+ * request that started it and every task request that rides it.
139
+ * - A task request is cancelled when its signal aborts. A task is cancelled
140
+ * when all of its requests are cancelled.
141
+ *
142
+ * The creator itself does not queue, batch or limit work. The `schedule`
143
+ * callback owns that.
144
+ *
145
+ * A task stops being ridable when it settles, when it is cancelled, or when it
146
+ * started more than `collapseTasksMs` ago. A cancelled task can still be
147
+ * running, but a new task request must not ride work that is being cancelled.
148
+ */
149
+ export declare class CollapsedTaskCreator<Input, Result, Handle, Key = string, Context = void> {
150
+ #private;
151
+ constructor(options: {
152
+ /**
153
+ * A task request rides a pending task for its input's key only if that
154
+ * task started no more than this many milliseconds ago.
155
+ */
156
+ collapseTasksMs: number;
157
+ /**
158
+ * Converts an input into the key that decides which pending task its task
159
+ * request may ride. Return `undefined` for an input that must not share
160
+ * work with any other, for example because its key cannot be computed.
161
+ * Its task request then starts its own task, which no task request can
162
+ * ride. Keys match as `Map` keys do (SameValueZero), so an object key
163
+ * matches only itself.
164
+ */
165
+ makeKey: (input: Input) => Key | undefined;
166
+ /**
167
+ * Schedules the tasks that one `startOrRideTasks` call starts, in task
168
+ * request order, as one unit of work. It returns one entry per task, in
169
+ * order: the task's result, and an opaque handle that every request on
170
+ * the task gets back.
171
+ *
172
+ * Every call uses this same function. It receives the `context` that the
173
+ * `startOrRideTasks` call passed, for example to learn how the caller
174
+ * wants the work run: batched with later calls, or at once. Usually it
175
+ * only queues the work and arms a flush. A caller that wants the work run
176
+ * at once then dispatches it, using the handles that `startOrRideTasks`
177
+ * returns. Those handles also cover the queued tasks the call rode, so
178
+ * one dispatch can cover all of the call's work.
179
+ *
180
+ * A `schedule` that runs work synchronously may call `startOrRideTasks`
181
+ * again, for example through a producer that reads other keys. See
182
+ * `startOrRideTasks` for the limit on that.
183
+ */
184
+ schedule: ScheduleTasks<Input, Result, Handle, Context>;
185
+ });
186
+ /**
187
+ * Each task request rides a pending task for its input's key, or starts a
188
+ * new task. Schedules the new tasks with one `schedule` call. When every
189
+ * task request rides, it does not call `schedule`.
190
+ *
191
+ * A task request whose signal is already aborted, or aborts during
192
+ * `makeKey`, starts and rides nothing. Its `result` rejects with the
193
+ * signal's reason, and the call's other task requests are not affected.
194
+ *
195
+ * It changes no state until `schedule` returns, except that it holds the
196
+ * tasks it rides. So when `makeKey` throws, `schedule` throws, or
197
+ * `schedule` returns the wrong number of entries, it releases those tasks,
198
+ * throws, and changes nothing else. It also holds
199
+ * each task it starts until it has recorded all of its task requests (see
200
+ * `Task.hold`).
201
+ *
202
+ * The tasks this call starts become ridable only after `schedule`
203
+ * returns. A `schedule` may call `startOrRideTasks` again, for example by
204
+ * running a producer that reads other keys. But a nested call that
205
+ * requests a key this call is starting throws a `SchedulingContractError`:
206
+ * that request depends on its own result.
207
+ *
208
+ * @param taskRequests Each task request's input, and an optional signal
209
+ * that cancels the task request.
210
+ * @param context Passed unchanged to `schedule`, if this call starts any
211
+ * task. A nested call passes its own context. A creator whose `schedule`
212
+ * needs none keeps the default `Context`, `void`, so its callers omit
213
+ * it. TypeScript lets a caller omit a trailing argument only when its
214
+ * type accepts `void`; `undefined` does not count.
215
+ * @returns One entry per task request, in order. `result` is the task's
216
+ * shared result. It does not reject when only this task request is
217
+ * cancelled, because the task's other requests may still need the
218
+ * result; a caller races its own signal. `handle` is the handle that the
219
+ * task's `schedule` returned. A caller that wants its work run at once
220
+ * can dispatch the still-queued tasks behind these handles, including
221
+ * the tasks it rode. It is undefined for a cancelled task request.
222
+ * `rode` is true when the task request rode a task that it did not
223
+ * start.
224
+ */
225
+ startOrRideTasks<const Requests extends readonly Readonly<TaskRequest<Input>>[]>(taskRequests: Requests, context: Context): TaskRequestOutcomes<Requests, Result, Handle>;
226
+ /**
227
+ * Waits until every task has settled, including cancelled ones. It loops
228
+ * because work that settles can start new tasks, for example a producer
229
+ * that reads other keys. Those tasks join `#live` after the snapshot that
230
+ * one `allSettled` waits on.
231
+ */
232
+ drain(): Promise<void>;
233
+ }
234
+ export {};
235
+ //# sourceMappingURL=collapsed-task-creator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collapsed-task-creator.d.ts","sourceRoot":"","sources":["../src/collapsed-task-creator.ts"],"names":[],"mappings":"AAGA,oDAAoD;AACpD,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI,QAAQ,CAC1C,IAAI,CACF,SAAS,CAAC,KAAK,CAAC,EAChB,OAAO,GAAG,QAAQ,GAAG,uBAAuB,GAAG,aAAa,CAC7D,CACF,CAAC;AAEF,KAAK,WAAW,CAAC,KAAK,IAAI;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAE,CAAC;AAE7E,4DAA4D;AAC5D,MAAM,MAAM,kBAAkB,CAAC,MAAM,EAAE,MAAM,IAAI;IAC/C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,mBAAmB,CAC7B,QAAQ,SAAS,SAAS,OAAO,EAAE,EACnC,MAAM,EACN,MAAM,IACJ;IAAE,CAAC,UAAU,CAAC,IAAI,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC;CAAE,CAAC;AAE5E;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,IAAI,CACjE,OAAO,EAAE,SAAS,cAAc,CAAC,KAAK,CAAC,EAAE,EACzC,OAAO,EAAE,OAAO,KACb,SAAS,QAAQ,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,CAAC;AAEtE;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,eAAe,GACf,mBAAmB,GACnB,sBAAsB,CAAC;AAE3B,yEAAyE;AACzE,KAAK,SAAS,CAAC,KAAK,IAAI;IACtB;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IACzC;;;;;;;;;;OAUG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAyLF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,qBAAa,oBAAoB,CAC/B,KAAK,EACL,MAAM,EACN,MAAM,EACN,GAAG,GAAG,MAAM,EACZ,OAAO,GAAG,IAAI;;IAwCd,YAAY,OAAO,EAAE;QACnB;;;WAGG;QACH,eAAe,EAAE,MAAM,CAAC;QACxB;;;;;;;WAOG;QACH,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,GAAG,GAAG,SAAS,CAAC;QAC3C;;;;;;;;;;;;;;;;;WAiBG;QACH,QAAQ,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;KACzD,EAIA;IAkDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,gBAAgB,CACd,KAAK,CAAC,QAAQ,SAAS,SAAS,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAE9D,YAAY,EAAE,QAAQ,EACtB,OAAO,EAAE,OAAO,GACf,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAwO/C;IAED;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAM3B;CACF"}