@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.
- package/README.md +184 -5
- package/dist/batcher.d.ts +231 -0
- package/dist/batcher.d.ts.map +1 -0
- package/dist/batcher.js +391 -0
- package/dist/batcher.js.map +1 -0
- package/dist/collapsed-task-creator.d.ts +235 -0
- package/dist/collapsed-task-creator.d.ts.map +1 -0
- package/dist/collapsed-task-creator.js +491 -0
- package/dist/collapsed-task-creator.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/pending-promises-tracker.d.ts +32 -0
- package/dist/pending-promises-tracker.d.ts.map +1 -0
- package/dist/pending-promises-tracker.js +60 -0
- package/dist/pending-promises-tracker.js.map +1 -0
- package/dist/performance-now.d.ts +14 -0
- package/dist/performance-now.d.ts.map +1 -0
- package/dist/performance-now.js +10 -0
- package/dist/performance-now.js.map +1 -0
- package/dist/scheduling-contract-error.d.ts +9 -0
- package/dist/scheduling-contract-error.d.ts.map +1 -0
- package/dist/scheduling-contract-error.js +9 -0
- package/dist/scheduling-contract-error.js.map +1 -0
- package/dist/wrap-with-task-collapsing.d.ts +30 -0
- package/dist/wrap-with-task-collapsing.d.ts.map +1 -0
- package/dist/wrap-with-task-collapsing.js +80 -0
- package/dist/wrap-with-task-collapsing.js.map +1 -0
- package/package.json +10 -4
- package/src/batcher.ts +614 -0
- package/src/collapsed-task-creator.ts +774 -0
- package/src/index.ts +22 -0
- package/src/logger.ts +32 -0
- package/src/pending-promises-tracker.ts +62 -0
- package/src/performance-now.ts +21 -0
- package/src/scheduling-contract-error.ts +8 -0
- package/src/wrap-with-task-collapsing.ts +103 -0
package/dist/batcher.js
ADDED
|
@@ -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"}
|