@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
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @zingage/scheduling
2
2
 
3
+ Helpers that decide when work runs in Node.js. The package has two parts:
4
+
5
+ - [Time slicing](#time-slicing): collection helpers that yield to the event loop, so long synchronous work does not block it.
6
+ - [Task collapsing and batching](#task-collapsing-and-batching): classes that decide which callers share work, how work is sent in batches, and when an owner can close.
7
+
8
+ ## Time slicing
9
+
3
10
  Cooperative collection helpers for synchronous work and generator callbacks in Node.js. Each helper defaults to a new independent budget with a 4 ms processing allowance.
4
11
 
5
12
  ```ts
@@ -23,7 +30,7 @@ await Promise.all([
23
30
  ]);
24
31
  ```
25
32
 
26
- ## Budget semantics
33
+ ### Budget semantics
27
34
 
28
35
  The package measures elapsed time around synchronous callbacks and result accumulation. It subtracts that measured time from the budget. Waiting at a yield and unrelated work between measured steps do not consume the budget.
29
36
 
@@ -31,13 +38,13 @@ If A uses 1 ms and finishes, connected B gets the remaining 3 ms. If A exhausts
31
38
 
32
39
  One callback or iterator step cannot be interrupted. Nested helpers started inside a synchronous callback cannot interrupt that containing callback. Their work counts once toward a shared budget. Garbage collection and operating-system pauses inside a measured step count as elapsed processing time. This is not CPU-time accounting or a hard event-loop delay bound. Work outside the helpers does not count. `timeSlicedMap` and `timeSlicedForEach` also accept generator callbacks.
33
40
 
34
- ## Synchronous callbacks
41
+ ### Synchronous callbacks
35
42
 
36
43
  Ordinary callbacks, predicates, key functions, and `measure` work must be synchronous. The types accept any result, including generic results such as `(item) => item`. TypeScript cannot reject a promise result without also rejecting generic results. So the rule is checked at runtime: if a non-generator callback returns a promise or other thenable (an object or function with a `then` method), the helper throws a `TypeError` before it uses the value. An `async` callback compiles, and throws on its first call. For async work, use a generator callback with `yield* waitFor(...)`.
37
44
 
38
45
  The constructor is `new TimeSliceBudget(sliceMs = DEFAULT_TIME_SLICE_MS, now = performance.now, yieldToEventLoop = setImmediate)`. `sliceMs` must be positive and finite. `now` must be a monotonic clock in milliseconds. The budget binds the clock to `performance` because Node's default clock requires that receiver. The default yield function is Node's promise-based `setImmediate`. A custom yield function must let the event loop advance; `Promise.resolve()` does not provide that behavior. Optionally pass a budget directly as each helper's last argument to share its allowance.
39
46
 
40
- ## Helpers
47
+ ### Helpers
41
48
 
42
49
  | Helper | Result |
43
50
  | -------------------------- | ---------------------------------------------------------------------------- |
@@ -55,7 +62,7 @@ All helpers accept synchronous iterables. Callbacks receive the item and its zer
55
62
 
56
63
  The package does not infer parent operations or use global budgets. It does not serialize mutations or impose ordering between concurrent calls. Backend consumers use this package. The former `time-sliced-map.ts` utility has been removed.
57
64
 
58
- ## Generator callbacks
65
+ ### Generator callbacks
59
66
 
60
67
  `timeSlicedMap` and `timeSlicedForEach` finish each generator callback before starting the next item. Use `yield* waitFor(promise)` to suspend with an inferred result type:
61
68
 
@@ -76,7 +83,7 @@ The budget measures every `next()` and `throw()` call, including synchronous wor
76
83
 
77
84
  Generator callbacks may also yield promises directly, but their resumed values have type `unknown`. Use `waitFor` for typed values. A synchronous segment cannot be interrupted. Break expensive callbacks into smaller generator segments. Nested helpers share accounting, but cannot interrupt the synchronous segment that starts them.
78
85
 
79
- ## Sorting
86
+ ### Sorting
80
87
 
81
88
  `timeSlicedSort(items, compare, budget?)` returns a new array and preserves the
82
89
  input order of values that compare equal. It uses a stable merge sort and checks
@@ -96,3 +103,175 @@ const sorted = await timeSlicedSort(
96
103
 
97
104
  For async sort keys, use generator callbacks with `timeSlicedMap` to load the
98
105
  keys, then pass a synchronous comparator to `timeSlicedSort`.
106
+
107
+ ## Task collapsing and batching
108
+
109
+ These parts decide when work runs, which callers share it, and when an owner
110
+ can close. None of them knows what the work is. Their owners supply that
111
+ knowledge through callbacks. `@zingage/cache` uses them for its Store reads
112
+ and producer calls.
113
+
114
+ | Export | What it does |
115
+ | --------------------------------------------------- | -------------------------------------------------------------------------- |
116
+ | [`CollapsedTaskCreator`](#collapsedtaskcreator) | Lets requests with the same key share one pending task, with cancellation. |
117
+ | [`wrapWithTaskCollapsing`](#wrapwithtaskcollapsing) | Wraps a task-starting function so that equal calls share a pending task. |
118
+ | [`Batcher`](#batcher) | Collects items and sends them to one `dispatch` function in batches. |
119
+ | [`PendingPromisesTracker`](#pendingpromisestracker) | Tracks promises until they settle, so that `close` can wait for them. |
120
+
121
+ The pieces compose like this:
122
+
123
+ - A caller that sends each task at once uses a `CollapsedTaskCreator` alone.
124
+ It has nothing to batch.
125
+ - A caller that sends tasks in bulk uses a `CollapsedTaskCreator` in front of
126
+ a `Batcher`. The creator decides which requests start new tasks. The
127
+ batcher groups the new tasks into calls.
128
+ - A caller that never shares work, such as a cache that reads from its store,
129
+ uses a `Batcher` alone.
130
+
131
+ `Batcher` and `wrapWithTaskCollapsing` accept a `Logger`. By default, they
132
+ write to the [`debug`](https://www.npmjs.com/package/debug) namespaces
133
+ `@zingage/scheduling:batcher` and `@zingage/scheduling:collapsed-task-creator`.
134
+ A callback that breaks a contract of this package, for example a `dispatch`
135
+ that returns the wrong number of results, causes a `SchedulingContractError`.
136
+
137
+ ### CollapsedTaskCreator
138
+
139
+ A `CollapsedTaskCreator` takes a batch of task requests in one
140
+ `startOrRideTasks` call. Each task request is an input plus an optional
141
+ `AbortSignal`. For each one, the creator either starts a new task or rides a
142
+ pending task with the same key.
143
+
144
+ - `makeKey` derives each input's key. A key of `undefined` means the input
145
+ never shares work.
146
+ - A task request rides a pending task only if the task started no more than
147
+ `collapseTasksMs` ago, has not settled, and is not cancelled.
148
+ - The tasks that one call starts go to the owner's `schedule` callback in one
149
+ call. So a bulk caller's new inputs become one unit of work.
150
+ - Each task has its own signal. It aborts only when every request on the task
151
+ is cancelled. A request without a signal can never be cancelled, so it keeps
152
+ the task alive.
153
+ - The creator counts the requests that ride each task, for diagnostics.
154
+ - `drain` waits until every task has settled, including tasks started while it
155
+ waits.
156
+
157
+ The creator does not queue, batch, or limit work. The `schedule` callback owns
158
+ that.
159
+
160
+ ### wrapWithTaskCollapsing
161
+
162
+ `wrapWithTaskCollapsing` wraps a function that starts a task and returns a
163
+ promise. A later call with the same arguments gets the pending task's promise
164
+ instead of a new task, but only while the task still runs and only within
165
+ `collapseTasksMs` of its start. It is a mix of memoizing, throttling, and
166
+ promise sharing. It runs on `CollapsedTaskCreator`, with one task request per
167
+ call.
168
+
169
+ It does not support cancellation, so an individual caller cannot cancel a
170
+ task that other callers share. Use `CollapsedTaskCreator` when you need
171
+ cancellation.
172
+
173
+ ```ts
174
+ import { wrapWithTaskCollapsing } from "@zingage/scheduling";
175
+
176
+ const fetchUser = wrapWithTaskCollapsing(
177
+ async (id: string) => api.getUser(id),
178
+ 1_000,
179
+ ([id]) => id,
180
+ );
181
+ ```
182
+
183
+ ### PendingPromisesTracker
184
+
185
+ An owner tracks each piece of accepted work, then drains the tracker in its
186
+ `close`.
187
+
188
+ - `track(promise)` tracks a promise until it settles. Tracking never creates
189
+ an unhandled rejection.
190
+ - `run(start)` tracks before it calls `start`, and turns a synchronous throw
191
+ into a rejection.
192
+ - `drain()` waits until no tracked promise is pending, including promises
193
+ tracked while it waits.
194
+
195
+ The tracker has no closed state. Each owner decides what happens to work that
196
+ arrives after its `close` starts, and in what order it drains its trackers.
197
+
198
+ ### Batcher
199
+
200
+ `Batcher` collects units of work and sends them to one `dispatch` function in
201
+ batches. It replaces [DataLoader](https://github.com/graphql/dataloader) in
202
+ `@zingage/cache`.
203
+
204
+ #### Same as DataLoader
205
+
206
+ When no signal aborts and nobody calls `flush`, `close` or `dispatchNow`, a
207
+ `Batcher` behaves like a DataLoader built with `cache: false`:
208
+
209
+ - `enqueue` corresponds to `load`, or to `loadMany` for several items.
210
+ - A batch opens with its first entry and schedules its own dispatch.
211
+ - The default schedule is DataLoader's. The batch dispatches in a
212
+ `process.nextTick` that a resolved promise queues. So the batch takes every
213
+ entry from the current event-loop turn and from the promise jobs that the
214
+ turn queues.
215
+ - `batchScheduleFn` replaces the default schedule, as in DataLoader. It
216
+ receives the batch's dispatch callback.
217
+ - A batch takes entries until it holds `maxBatchSize` of them. The next entry
218
+ opens a new batch, with its own scheduled dispatch. A full batch does not
219
+ dispatch early.
220
+ - `dispatch` returns a promise of one result per item, in order. An `Error`
221
+ in a result slot rejects only that item. A rejection, or a synchronous
222
+ throw, rejects every item in the batch with the same error.
223
+ - A repeated input is sent again. The batcher never shares work between
224
+ items. `CollapsedTaskCreator` does that.
225
+
226
+ [`batcher.dataloader.test.ts`](./src/batcher.dataloader.test.ts) holds
227
+ DataLoader's own tests, ported to the batcher. The file header lists the
228
+ tests that it leaves out, and why.
229
+
230
+ #### Added to DataLoader
231
+
232
+ - **Cancellation.** An item can carry an `AbortSignal`. When the signal
233
+ aborts while the item waits in a batch, the item leaves the batch and its
234
+ result rejects with the signal's reason. The item also frees its slot, so
235
+ that a later entry can join the batch. An item whose signal already
236
+ aborted is rejected at once and never queued. Work that has dispatched
237
+ always completes.
238
+ - **`dispatchNow`.** It sends new items at once, without a batch. It can also
239
+ pull queued entries forward: the caller passes the handles that `enqueue`
240
+ returned, including handles from other callers. One `dispatchNow` call is
241
+ one `dispatch` call, which `maxBatchSize` never splits.
242
+ - **`flush`.** It dispatches every pending batch now. It also drops
243
+ `batchScheduleFn`, so later batches use the default schedule. An owner
244
+ calls it when its shutdown starts, because its own schedule may stop
245
+ firing then.
246
+ - **`close`.** It flushes, then resolves once no batch is pending and no
247
+ dispatch is running. It waits for work that a dispatch enqueues into the
248
+ same batcher. After `close`, `enqueue` and `dispatchNow` log a warning, but
249
+ the work still runs.
250
+ - **Dispatch facts.** `dispatch` receives each item's queue wait in
251
+ milliseconds, and an `autoBatched` flag that says whether a batch or
252
+ `dispatchNow` sent the call.
253
+ - **`preserveAsyncContext`.** When it is on, each batch dispatches in the
254
+ async context of the `enqueue` call that opened the batch. Use it when
255
+ `batchScheduleFn` runs its callbacks from an unrelated context, or when
256
+ `flush` must not run a batch in the closer's context.
257
+
258
+ #### Not in the Batcher
259
+
260
+ - **No cache.** The batcher has no `clear`, `clearAll`, `prime`,
261
+ `cacheKeyFn` or `cacheMap`. Its owner and `CollapsedTaskCreator` own
262
+ caching and work sharing.
263
+ - **No `name` and no `batch: false`.** Use `maxBatchSize: 1` to send each item
264
+ alone.
265
+
266
+ #### Known differences from DataLoader
267
+
268
+ These differences remain on purpose. No test covers them.
269
+
270
+ | Behavior | DataLoader | Batcher |
271
+ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
272
+ | Default `maxBatchSize` | No limit | 20 (`DEFAULT_MAX_BATCH_SIZE`) |
273
+ | A `maxBatchSize` that is not an integer, such as `1.5` | Accepted | Throws a `TypeError` |
274
+ | `dispatch` throws or rejects with a value that is not an `Error` | Rejects each load with that value | Rejects each item with an `Error` whose `cause` is that value |
275
+ | `dispatch` throws synchronously, or resolves with a value of the wrong shape or length | Rejects each load with DataLoader's own `TypeError` message | Rejects each item with the original error, or with a `SchedulingContractError` |
276
+ | When results settle | One microtask after the `dispatch` promise resolves | About two microtasks later, because `Promise.try` adopts the `dispatch` promise |
277
+ | `batchScheduleFn` runs its callback synchronously | Strands the entry in a dispatched batch, so its load never settles | Schedules a call's batches only after all of the call's entries are in place |
@@ -0,0 +1,231 @@
1
+ import { type Logger } from "./logger.js";
2
+ import { type PerformanceNow } from "./performance-now.js";
3
+ /**
4
+ * Accumulation settings for a {@link Batcher}. An owner can expose them to its
5
+ * own users, for example as a cache's read batching options.
6
+ */
7
+ export type BatchOptions = {
8
+ /**
9
+ * The most requests one automatic dispatch takes. Defaults to 20. A direct
10
+ * (non-automatic) dispatch is never split.
11
+ */
12
+ maxBatchSize?: number;
13
+ /**
14
+ * Schedules the dispatch of each new automatic batch. It receives the
15
+ * batch's dispatch callback. Defaults to the next tick after the current
16
+ * microtasks, so requests from one event-loop turn share a batch.
17
+ */
18
+ batchScheduleFn?: (dispatch: () => void) => void;
19
+ /**
20
+ * Runs each automatic batch in the async context of the `enqueue` call
21
+ * that opened it. Off by default, because the default schedule, and a
22
+ * `batchScheduleFn` built on Node's timers or promises, already run the
23
+ * batch in that context. Turn it on when `batchScheduleFn` runs its
24
+ * callbacks from an unrelated context, or when a batch that `flush` or
25
+ * `close` sends early must keep its first request's context instead of
26
+ * the closer's. It costs one `AsyncResource` per batch.
27
+ */
28
+ preserveAsyncContext?: boolean;
29
+ };
30
+ /** One unit of work that a caller gives to {@link Batcher.enqueue}. */
31
+ export type BatchItem<Input> = {
32
+ readonly input: Input;
33
+ /**
34
+ * Cancels the item while it is queued. When it aborts, the item leaves the
35
+ * queue and its result rejects with the signal's reason. Omit it for work
36
+ * that can never be cancelled.
37
+ */
38
+ readonly signal?: AbortSignal | undefined;
39
+ };
40
+ /** One item as the batcher's `dispatch` receives it. */
41
+ export type DispatchedItem<Input> = {
42
+ readonly input: Input;
43
+ /** Milliseconds from enqueue to dispatch. */
44
+ readonly queueWaitMs: number;
45
+ };
46
+ /** Facts about one dispatch call, the same for every item in it. */
47
+ export type DispatchInfo = {
48
+ /**
49
+ * True when an automatic batch sent the items, false when `dispatchNow`
50
+ * sent them.
51
+ */
52
+ readonly autoBatched: boolean;
53
+ };
54
+ /**
55
+ * One automatic batch. Like a DataLoader batch, it gets its own scheduled
56
+ * dispatch when it is created, and it takes entries until it holds
57
+ * `maxBatchSize` queued entries or dispatches.
58
+ */
59
+ type Batch<Input, Result> = {
60
+ /**
61
+ * Every entry that joined the batch, so the batch's dispatch can find the
62
+ * ones still queued. Emptied once the batch dispatches, so that a
63
+ * long-lived handle does not keep its settled siblings alive.
64
+ */
65
+ readonly entries: Entry<Input, Result>[];
66
+ /**
67
+ * The number of `entries` whose state is still `"queued"`. Until dispatch
68
+ * empties `entries`, it equals
69
+ * `entries.filter((e) => e.state === "queued").length`: an entry joins
70
+ * `entries` only in `#makeEntryInBatch`, which counts it, and it leaves the
71
+ * queued state only through `#setEntryState`, which uncounts it.
72
+ *
73
+ * It decides whether the open batch has room, so a cancelled entry, or one
74
+ * that `dispatchNow` sent, frees its slot for a new entry. The count is
75
+ * stored rather than computed because `enqueue` checks it once per entry.
76
+ * Computing it would cost one pass over the batch per entry, which is
77
+ * quadratic for a large batch, such as one with `maxBatchSize: Infinity`.
78
+ */
79
+ queuedCount: number;
80
+ /**
81
+ * Dispatches the batch. `batchScheduleFn` receives it, and `flush` and
82
+ * `#drain` call it, so every path that dispatches the batch runs in the
83
+ * same async context. With `preserveAsyncContext`, it is bound to the
84
+ * context of the `enqueue` call that opened the batch.
85
+ */
86
+ readonly dispatch: () => void;
87
+ };
88
+ /**
89
+ * One queued unit of work. `Batcher.enqueue` returns it as an opaque handle,
90
+ * which a caller passes back to `Batcher.dispatchNow`.
91
+ */
92
+ type Entry<Input, Result> = {
93
+ readonly item: BatchItem<Input>;
94
+ /**
95
+ * The automatic batch that holds the entry. When the entry leaves the
96
+ * queued state, by cancellation or by `dispatchNow`, `#setEntryState`
97
+ * uses it to free the entry's slot in that batch.
98
+ */
99
+ readonly batch: Batch<Input, Result>;
100
+ /** `performance.now()` at enqueue, for the dispatch's queue wait. */
101
+ readonly enqueuedAt: PerformanceNow;
102
+ /**
103
+ * Makes each entry dispatch and settle at most once. An entry can be
104
+ * reached by its batch's dispatch, by `dispatchNow`, and by its signal's
105
+ * abort, in any order; each acts only on a `"queued"` entry.
106
+ */
107
+ state: "queued" | "running" | "settled";
108
+ readonly result: PromiseWithResolvers<Result>;
109
+ /**
110
+ * Removes the listener attached to the abort of the item's signal, if the
111
+ * item has a signal. Runs once, at settle.
112
+ */
113
+ cleanup: (() => void) | undefined;
114
+ };
115
+ export type Outcome<Result> = {
116
+ ok: true;
117
+ value: Result;
118
+ } | {
119
+ ok: false;
120
+ error: unknown;
121
+ };
122
+ /** Settles `result` with `outcome`. */
123
+ export declare function settleResult<Result>(result: PromiseWithResolvers<Result>, outcome: Outcome<Result>): void;
124
+ /** The handle that {@link Batcher.enqueue} returns for one entry. */
125
+ export type BatchHandle<Input, Result> = Entry<Input, Result>;
126
+ /** What {@link Batcher.enqueue} returns for one item. */
127
+ export type Enqueued<Input, Result> = {
128
+ readonly result: Promise<Result>;
129
+ /**
130
+ * The handle to pass to `dispatchNow`. Undefined only when `result` is
131
+ * already rejected: the item's signal had aborted before `enqueue`, so
132
+ * the item was never queued.
133
+ */
134
+ readonly handle: BatchHandle<Input, Result> | undefined;
135
+ };
136
+ /** The default `maxBatchSize`. */
137
+ export declare const DEFAULT_MAX_BATCH_SIZE = 20;
138
+ /**
139
+ * Queues work and dispatches it in batches. The batcher does not share work,
140
+ * and it knows nothing of `CollapsedTaskCreator`. To share work, compose the
141
+ * two: a creator's `schedule` enqueues one item per started task, for example
142
+ * `batcher.enqueue(toStart.map((view) => ({input: view, signal:
143
+ * view.signal})))`. Then `dispatch` receives each live view, and can read
144
+ * its `collapsedRequestCount` at dispatch time. For work that is never
145
+ * shared, such as a cache's reads from its store, use a batcher directly, so
146
+ * that each request gets its own entry.
147
+ *
148
+ * Entries wait in automatic batches, which follow DataLoader's rules. A new
149
+ * batch schedules its own dispatch with `batchScheduleFn` when it gets its
150
+ * first entry. Once it holds `maxBatchSize` queued entries, the next entry
151
+ * opens a new batch with its own scheduled dispatch. A full batch does not
152
+ * dispatch early: it waits for its own callback. A caller that wants its
153
+ * work run at once calls `dispatchNow` with its new items, and with the
154
+ * handles of queued entries it also waits for, such as entries from other
155
+ * callers' enqueues. That dispatch is never split, and its new items never
156
+ * wait in a batch.
157
+ *
158
+ * Cancellation removes a queued entry: when its item's signal aborts, the
159
+ * entry leaves the queue and its result rejects with the signal's reason.
160
+ * Dispatched work always completes.
161
+ *
162
+ * Shutdown has two steps, because an owner can still accept work after its
163
+ * shutdown starts:
164
+ *
165
+ * 1. `flush` sends every pending batch at once, and drops `batchScheduleFn`,
166
+ * so later batches use the default schedule, which always fires. An owner
167
+ * calls it when its shutdown starts, because its `batchScheduleFn` may
168
+ * stop firing then.
169
+ * 2. `close` flushes too, and resolves once the batcher is idle. Then the
170
+ * batcher is closed. An owner calls it after it stops accepting work. An
171
+ * enqueue after that is a bug in the owner, so it logs a warning, but the
172
+ * work still runs.
173
+ *
174
+ * Every entry's result settles, including on cancellation. So a caller that
175
+ * must hold a resource for an item's whole life acquires it before `enqueue`
176
+ * and releases it in a `.finally` on the item's result. The batcher has no
177
+ * hook for this.
178
+ */
179
+ export declare class Batcher<Input, Result> {
180
+ #private;
181
+ constructor(options: BatchOptions & {
182
+ /**
183
+ * Runs one call for the given items. It resolves with one result or
184
+ * Error per item, in order. A rejection fails every item in the call.
185
+ */
186
+ dispatch: (items: readonly DispatchedItem<Input>[], info: DispatchInfo) => Promise<readonly (Result | Error)[]>;
187
+ /** Receives the warning for an enqueue after close. */
188
+ logger?: Logger;
189
+ });
190
+ /**
191
+ * Queues one entry per item, in order, and returns each item's result and
192
+ * handle.
193
+ *
194
+ * Each entry joins the open automatic batch, or opens a new one when that
195
+ * batch is full. An item whose signal already aborted is not queued: its
196
+ * result is rejected with the signal's reason, and it has no handle. The
197
+ * other items are not affected. After the batcher has closed, it also logs
198
+ * a warning.
199
+ */
200
+ enqueue(items: readonly BatchItem<Input>[]): Enqueued<Input, Result>[];
201
+ /**
202
+ * Dispatches the still-queued entries among `handles`, plus the new
203
+ * `items`, as one call, which is never split. It skips entries that are
204
+ * running or settled. A caller can pass handles from other callers'
205
+ * enqueues, so one dispatch covers all of the work it waits for.
206
+ *
207
+ * The new items never wait in a batch, so a `batchScheduleFn` cannot send
208
+ * them early. An item whose signal already aborted is rejected and not
209
+ * sent. Returns each item's result, in order. After close, it logs a
210
+ * warning when it gets items, as `enqueue` does.
211
+ *
212
+ * It drops repeated handles, because one call can hold the same handle
213
+ * more than once: two requests of a call that share one task get the same
214
+ * handle, and the entry must be sent once.
215
+ */
216
+ dispatchNow(handles: Iterable<BatchHandle<Input, Result>>, items?: readonly BatchItem<Input>[]): Promise<Result>[];
217
+ /**
218
+ * Dispatches every pending batch now, and drops `batchScheduleFn`, which
219
+ * the owner may stop during shutdown. Later batches use the default
220
+ * schedule. The batcher stays open: later enqueues do not warn.
221
+ */
222
+ flush(): void;
223
+ /**
224
+ * Flushes, then resolves when the batcher is idle. Then the batcher is
225
+ * closed, and later enqueues log a warning. Repeated calls return the same
226
+ * promise.
227
+ */
228
+ close(): Promise<void>;
229
+ }
230
+ export {};
231
+ //# sourceMappingURL=batcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batcher.d.ts","sourceRoot":"","sources":["../src/batcher.ts"],"names":[],"mappings":"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"}