batchkit 0.3.7 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/batch.d.ts.map +1 -1
- package/dist/index.js +111 -45
- package/dist/match.d.ts +2 -1
- package/dist/match.d.ts.map +1 -1
- package/dist/trace.d.ts +1 -0
- package/dist/trace.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/batch.ts +124 -32
- package/src/match.ts +8 -1
- package/src/trace.ts +8 -33
- package/src/types.ts +4 -1
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ await users.flush() // Don't wait for scheduler
|
|
|
92
92
|
Abort the in-flight batch:
|
|
93
93
|
|
|
94
94
|
```typescript
|
|
95
|
-
users.abort() // All pending requests reject with AbortError
|
|
95
|
+
users.abort() // All pending and in-flight requests reject with AbortError
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
## Matching Results
|
package/dist/batch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,OAAO,EACP,OAAO,EACP,YAAY,EAEZ,KAAK,EAIN,MAAM,SAAS,CAAC;AAEjB,wBAAgB,KAAK,CAAC,CAAC,EAAE,CAAC,EACxB,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EACjB,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAClB,OAAO,GAAE,YAAY,CAAC,CAAC,CAAM,GAC5B,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,OAAO,EACP,OAAO,EACP,YAAY,EAEZ,KAAK,EAIN,MAAM,SAAS,CAAC;AAEjB,wBAAgB,KAAK,CAAC,CAAC,EAAE,CAAC,EACxB,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EACjB,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAClB,OAAO,GAAE,YAAY,CAAC,CAAC,CAAM,GAC5B,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CA6Yf"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@ function normalizeMatch(match) {
|
|
|
20
20
|
if (isIndexed(match)) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
+
if (typeof match === "symbol") {
|
|
24
|
+
throw new BatchError("Unsupported symbol match. Use `indexed` for Record responses.");
|
|
25
|
+
}
|
|
23
26
|
if (isKeyMatch(match)) {
|
|
24
27
|
const key = match;
|
|
25
28
|
return (results, requestedKey) => {
|
|
@@ -67,46 +70,31 @@ function onIdle(options) {
|
|
|
67
70
|
|
|
68
71
|
// src/trace.ts
|
|
69
72
|
var devtoolsHook = null;
|
|
70
|
-
var pendingBatchers = [];
|
|
71
73
|
function __setDevtoolsHook(hook) {
|
|
72
74
|
devtoolsHook = hook;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
fn: pending.fn,
|
|
77
|
-
name: pending.name,
|
|
78
|
-
stack: pending.stack
|
|
79
|
-
});
|
|
80
|
-
pending.setEmitter(emitter);
|
|
81
|
-
}
|
|
82
|
-
pendingBatchers.length = 0;
|
|
83
|
-
} else if (!hook) {
|
|
84
|
-
pendingBatchers.length = 0;
|
|
85
|
-
}
|
|
75
|
+
}
|
|
76
|
+
function hasDevtoolsHook() {
|
|
77
|
+
return devtoolsHook != null;
|
|
86
78
|
}
|
|
87
79
|
function registerBatcher(info, setEmitter) {
|
|
88
80
|
if (devtoolsHook) {
|
|
89
81
|
const emitter = devtoolsHook.onBatcherCreated(info);
|
|
90
82
|
setEmitter(emitter);
|
|
91
|
-
} else {
|
|
92
|
-
pendingBatchers.push({ ...info, setEmitter });
|
|
93
83
|
}
|
|
94
84
|
}
|
|
95
85
|
function createTracer(name, handler, getDevtoolsEmitter) {
|
|
96
86
|
let batchCounter = 0;
|
|
97
87
|
function emit(event) {
|
|
88
|
+
const devtoolsEmitter = getDevtoolsEmitter?.();
|
|
89
|
+
if (!handler && !devtoolsEmitter)
|
|
90
|
+
return;
|
|
98
91
|
const timestamp = performance.now();
|
|
99
92
|
const fullEvent = {
|
|
100
93
|
...event,
|
|
101
94
|
timestamp
|
|
102
95
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
const devtoolsEmitter = getDevtoolsEmitter?.();
|
|
107
|
-
if (devtoolsEmitter) {
|
|
108
|
-
devtoolsEmitter(fullEvent);
|
|
109
|
-
}
|
|
96
|
+
handler?.(fullEvent);
|
|
97
|
+
devtoolsEmitter?.(fullEvent);
|
|
110
98
|
}
|
|
111
99
|
function nextBatchId() {
|
|
112
100
|
return `${name ?? "batch"}-${++batchCounter}`;
|
|
@@ -126,20 +114,45 @@ function batch(fn, match, options = {}) {
|
|
|
126
114
|
} = options;
|
|
127
115
|
const scheduler = schedule ?? (waitMs ? wait(waitMs) : microtask);
|
|
128
116
|
let devtoolsEmitter;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
let creationStack;
|
|
118
|
+
let didTryRegisterDevtools = false;
|
|
119
|
+
function tryRegisterDevtools() {
|
|
120
|
+
if (didTryRegisterDevtools)
|
|
121
|
+
return;
|
|
122
|
+
if (!hasDevtoolsHook())
|
|
123
|
+
return;
|
|
124
|
+
didTryRegisterDevtools = true;
|
|
125
|
+
creationStack ??= new Error().stack;
|
|
126
|
+
registerBatcher({ fn, name, stack: creationStack }, (emitter) => {
|
|
127
|
+
devtoolsEmitter = emitter;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
tryRegisterDevtools();
|
|
131
|
+
const tracer = createTracer(name, traceHandler, () => {
|
|
132
|
+
if (!devtoolsEmitter) {
|
|
133
|
+
tryRegisterDevtools();
|
|
134
|
+
}
|
|
135
|
+
return devtoolsEmitter;
|
|
132
136
|
});
|
|
133
|
-
const tracer = createTracer(name, traceHandler, () => devtoolsEmitter);
|
|
134
137
|
const matchFn = normalizeMatch(match);
|
|
135
138
|
const isIndexedMatch = isIndexed(match);
|
|
136
139
|
const indexedMatcher = isIndexedMatch ? createIndexedMatcher() : null;
|
|
137
140
|
let queue = [];
|
|
138
141
|
const pendingKeys = new Set;
|
|
142
|
+
const activeRequests = new Set;
|
|
139
143
|
let cleanup = null;
|
|
140
144
|
let isScheduled = false;
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
const inFlightChunks = new Set;
|
|
146
|
+
const requestToInFlightChunk = new WeakMap;
|
|
147
|
+
function createAbortError() {
|
|
148
|
+
return new DOMException("Aborted", "AbortError");
|
|
149
|
+
}
|
|
150
|
+
function rejectAsAborted(request) {
|
|
151
|
+
if (request.aborted)
|
|
152
|
+
return;
|
|
153
|
+
request.aborted = true;
|
|
154
|
+
request.reject(createAbortError());
|
|
155
|
+
}
|
|
143
156
|
function scheduleDispatch() {
|
|
144
157
|
if (isScheduled || queue.length === 0)
|
|
145
158
|
return;
|
|
@@ -168,6 +181,9 @@ function batch(fn, match, options = {}) {
|
|
|
168
181
|
}
|
|
169
182
|
isScheduled = false;
|
|
170
183
|
const batch2 = activeQueue;
|
|
184
|
+
for (const request of batch2) {
|
|
185
|
+
activeRequests.add(request);
|
|
186
|
+
}
|
|
171
187
|
queue = [];
|
|
172
188
|
pendingKeys.clear();
|
|
173
189
|
const chunks = [];
|
|
@@ -200,20 +216,29 @@ function batch(fn, match, options = {}) {
|
|
|
200
216
|
}
|
|
201
217
|
if (uniqueKeys.length === 0)
|
|
202
218
|
return;
|
|
203
|
-
inFlightRequests = chunk;
|
|
204
219
|
tracer.emit({
|
|
205
220
|
type: "dispatch",
|
|
206
221
|
batchId,
|
|
207
222
|
keys: uniqueKeys
|
|
208
223
|
});
|
|
209
|
-
|
|
210
|
-
const signal =
|
|
224
|
+
const controller = new AbortController;
|
|
225
|
+
const signal = controller.signal;
|
|
226
|
+
const inFlight = { batchId, controller, requests: chunk };
|
|
227
|
+
inFlightChunks.add(inFlight);
|
|
228
|
+
for (const request of chunk) {
|
|
229
|
+
requestToInFlightChunk.set(request, inFlight);
|
|
230
|
+
}
|
|
211
231
|
const startedAt = performance.now();
|
|
212
232
|
try {
|
|
213
233
|
const results = await fn(uniqueKeys, signal);
|
|
214
234
|
const duration = performance.now() - startedAt;
|
|
215
235
|
if (signal.aborted) {
|
|
216
236
|
tracer.emit({ type: "abort", batchId });
|
|
237
|
+
for (const requests of keyToRequests.values()) {
|
|
238
|
+
for (const request of requests) {
|
|
239
|
+
rejectAsAborted(request);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
217
242
|
return;
|
|
218
243
|
}
|
|
219
244
|
tracer.emit({
|
|
@@ -280,8 +305,7 @@ function batch(fn, match, options = {}) {
|
|
|
280
305
|
}
|
|
281
306
|
}
|
|
282
307
|
} finally {
|
|
283
|
-
|
|
284
|
-
inFlightRequests = [];
|
|
308
|
+
inFlightChunks.delete(inFlight);
|
|
285
309
|
}
|
|
286
310
|
}
|
|
287
311
|
function getSingle(key, options2) {
|
|
@@ -296,8 +320,29 @@ function batch(fn, match, options = {}) {
|
|
|
296
320
|
} else {
|
|
297
321
|
pendingKeys.add(cacheKey);
|
|
298
322
|
}
|
|
299
|
-
return new Promise((
|
|
300
|
-
|
|
323
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
324
|
+
let settled = false;
|
|
325
|
+
let removeAbortListener = null;
|
|
326
|
+
let request;
|
|
327
|
+
const resolve = (value) => {
|
|
328
|
+
if (settled)
|
|
329
|
+
return;
|
|
330
|
+
settled = true;
|
|
331
|
+
removeAbortListener?.();
|
|
332
|
+
activeRequests.delete(request);
|
|
333
|
+
requestToInFlightChunk.delete(request);
|
|
334
|
+
resolvePromise(value);
|
|
335
|
+
};
|
|
336
|
+
const reject = (error) => {
|
|
337
|
+
if (settled)
|
|
338
|
+
return;
|
|
339
|
+
settled = true;
|
|
340
|
+
removeAbortListener?.();
|
|
341
|
+
activeRequests.delete(request);
|
|
342
|
+
requestToInFlightChunk.delete(request);
|
|
343
|
+
rejectPromise(error);
|
|
344
|
+
};
|
|
345
|
+
request = {
|
|
301
346
|
key,
|
|
302
347
|
resolve,
|
|
303
348
|
reject,
|
|
@@ -307,15 +352,34 @@ function batch(fn, match, options = {}) {
|
|
|
307
352
|
queue.push(request);
|
|
308
353
|
if (externalSignal) {
|
|
309
354
|
const onAbort = () => {
|
|
355
|
+
const inFlight = requestToInFlightChunk.get(request);
|
|
310
356
|
request.aborted = true;
|
|
311
|
-
reject(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
357
|
+
reject(createAbortError());
|
|
358
|
+
if (inFlight) {
|
|
359
|
+
const allChunkRequestsAborted = inFlight.requests.every((r) => r.aborted);
|
|
360
|
+
if (allChunkRequestsAborted) {
|
|
361
|
+
inFlight.controller.abort();
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
const allQueuedAborted = queue.length > 0 && queue.every((r) => r.aborted);
|
|
365
|
+
if (allQueuedAborted) {
|
|
366
|
+
queue = [];
|
|
367
|
+
pendingKeys.clear();
|
|
368
|
+
if (cleanup) {
|
|
369
|
+
cleanup();
|
|
370
|
+
cleanup = null;
|
|
371
|
+
}
|
|
372
|
+
isScheduled = false;
|
|
373
|
+
}
|
|
316
374
|
}
|
|
317
375
|
};
|
|
318
376
|
externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
377
|
+
removeAbortListener = () => {
|
|
378
|
+
externalSignal.removeEventListener("abort", onAbort);
|
|
379
|
+
};
|
|
380
|
+
if (externalSignal.aborted) {
|
|
381
|
+
onAbort();
|
|
382
|
+
}
|
|
319
383
|
}
|
|
320
384
|
scheduleDispatch();
|
|
321
385
|
});
|
|
@@ -336,13 +400,15 @@ function batch(fn, match, options = {}) {
|
|
|
336
400
|
}
|
|
337
401
|
function abort() {
|
|
338
402
|
for (const request of queue) {
|
|
339
|
-
request
|
|
340
|
-
|
|
403
|
+
rejectAsAborted(request);
|
|
404
|
+
}
|
|
405
|
+
for (const request of activeRequests) {
|
|
406
|
+
rejectAsAborted(request);
|
|
341
407
|
}
|
|
342
408
|
queue = [];
|
|
343
409
|
pendingKeys.clear();
|
|
344
|
-
|
|
345
|
-
|
|
410
|
+
for (const chunk of inFlightChunks) {
|
|
411
|
+
chunk.controller.abort();
|
|
346
412
|
}
|
|
347
413
|
if (cleanup) {
|
|
348
414
|
cleanup();
|
package/dist/match.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { indexed } from './indexed';
|
|
1
2
|
import type { Match, MatchFn } from './types';
|
|
2
|
-
export declare function isIndexed<K, V>(match: Match<K, V>): match is
|
|
3
|
+
export declare function isIndexed<K, V>(match: Match<K, V>): match is typeof indexed;
|
|
3
4
|
export declare function isKeyMatch<K, V>(match: Match<K, V>): match is keyof V;
|
|
4
5
|
export declare function normalizeMatch<K, V>(match: Match<K, V>): MatchFn<K, V> | null;
|
|
5
6
|
export declare function createIndexedMatcher<K, V>(): (results: Record<string, V>, key: K) => V | undefined;
|
package/dist/match.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAE9C,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,IAAI,OAAO,OAAO,CAE3E;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,IAAI,MAAM,CAAC,CAErE;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAwB7E;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,CAAC,KAAK,CAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAC1B,GAAG,EAAE,CAAC,KACH,CAAC,GAAG,SAAS,CAEjB"}
|
package/dist/trace.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface DevtoolsHook {
|
|
|
9
9
|
}): ((event: TraceEvent) => void) | undefined;
|
|
10
10
|
}
|
|
11
11
|
export declare function __setDevtoolsHook(hook: DevtoolsHook | null): void;
|
|
12
|
+
export declare function hasDevtoolsHook(): boolean;
|
|
12
13
|
export declare function registerBatcher(info: {
|
|
13
14
|
fn: {
|
|
14
15
|
toString(): string;
|
package/dist/trace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAExE,MAAM,WAAW,YAAY;IAC3B,gBAAgB,CAAC,IAAI,EAAE;QACrB,EAAE,EAAE;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAC;QAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;QACzB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;KAC3B,GAAG,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC/C;
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAExE,MAAM,WAAW,YAAY;IAC3B,gBAAgB,CAAC,IAAI,EAAE;QACrB,EAAE,EAAE;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAC;QAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;QACzB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;KAC3B,GAAG,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC/C;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAEjE;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE;IACJ,EAAE,EAAE;QAAE,QAAQ,IAAI,MAAM,CAAA;KAAE,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B,EACD,UAAU,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,SAAS,KAAK,IAAI,GACvE,IAAI,CAKN;AAED,wBAAgB,YAAY,CAAC,CAAC,EAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,EACpC,kBAAkB,EACd,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC,GACpD,SAAS;kBAIQ,eAAe,CAAC,CAAC;uBAad,MAAM;EAK/B"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type BatchFn<K, V> = (keys: K[], signal: AbortSignal) => Promise<V[] | Record<string, V>>;
|
|
2
|
-
export type Match<K, V> = keyof V |
|
|
2
|
+
export type Match<K, V> = keyof V | typeof import('./indexed').indexed | MatchFn<K, V>;
|
|
3
3
|
export type MatchFn<K, V> = (results: V[], key: K) => V | undefined;
|
|
4
4
|
export type IndexedMatchFn<K, V> = (results: Record<string, V>, key: K) => V | undefined;
|
|
5
5
|
export type Scheduler = (dispatch: () => void) => () => void;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,CAC1B,IAAI,EAAE,CAAC,EAAE,EACT,MAAM,EAAE,WAAW,KAChB,OAAO,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/batch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BatchError } from './errors';
|
|
2
2
|
import { createIndexedMatcher, isIndexed, normalizeMatch } from './match';
|
|
3
3
|
import { microtask, wait } from './schedulers';
|
|
4
|
-
import { createTracer, registerBatcher } from './trace';
|
|
4
|
+
import { createTracer, hasDevtoolsHook, registerBatcher } from './trace';
|
|
5
5
|
import type {
|
|
6
6
|
Batcher,
|
|
7
7
|
BatchFn,
|
|
@@ -30,13 +30,30 @@ export function batch<K, V>(
|
|
|
30
30
|
const scheduler: Scheduler = schedule ?? (waitMs ? wait(waitMs) : microtask);
|
|
31
31
|
|
|
32
32
|
let devtoolsEmitter: ((event: TraceEvent) => void) | undefined;
|
|
33
|
-
|
|
33
|
+
let creationStack: string | undefined;
|
|
34
|
+
let didTryRegisterDevtools = false;
|
|
35
|
+
|
|
36
|
+
function tryRegisterDevtools(): void {
|
|
37
|
+
if (didTryRegisterDevtools) return;
|
|
38
|
+
if (!hasDevtoolsHook()) return;
|
|
39
|
+
didTryRegisterDevtools = true;
|
|
40
|
+
creationStack ??= new Error().stack;
|
|
41
|
+
registerBatcher({ fn, name, stack: creationStack }, (emitter) => {
|
|
42
|
+
devtoolsEmitter = emitter;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
46
|
+
// If devtools are already installed, register immediately.
|
|
47
|
+
tryRegisterDevtools();
|
|
38
48
|
|
|
39
|
-
const tracer = createTracer(name, traceHandler, () =>
|
|
49
|
+
const tracer = createTracer(name, traceHandler, () => {
|
|
50
|
+
// Allow devtools to be mounted after the batcher is created without
|
|
51
|
+
// retaining global references or capturing stacks unless a hook exists.
|
|
52
|
+
if (!devtoolsEmitter) {
|
|
53
|
+
tryRegisterDevtools();
|
|
54
|
+
}
|
|
55
|
+
return devtoolsEmitter;
|
|
56
|
+
});
|
|
40
57
|
|
|
41
58
|
const matchFn = normalizeMatch(match);
|
|
42
59
|
const isIndexedMatch = isIndexed(match);
|
|
@@ -44,10 +61,31 @@ export function batch<K, V>(
|
|
|
44
61
|
|
|
45
62
|
let queue: PendingRequest<K, V>[] = [];
|
|
46
63
|
const pendingKeys = new Set<unknown>();
|
|
64
|
+
const activeRequests = new Set<PendingRequest<K, V>>();
|
|
47
65
|
let cleanup: (() => void) | null = null;
|
|
48
66
|
let isScheduled = false;
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
|
|
68
|
+
interface InFlightChunk {
|
|
69
|
+
readonly batchId: string;
|
|
70
|
+
readonly controller: AbortController;
|
|
71
|
+
readonly requests: readonly PendingRequest<K, V>[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const inFlightChunks = new Set<InFlightChunk>();
|
|
75
|
+
const requestToInFlightChunk = new WeakMap<
|
|
76
|
+
PendingRequest<K, V>,
|
|
77
|
+
InFlightChunk
|
|
78
|
+
>();
|
|
79
|
+
|
|
80
|
+
function createAbortError(): DOMException {
|
|
81
|
+
return new DOMException('Aborted', 'AbortError');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rejectAsAborted(request: PendingRequest<K, V>): void {
|
|
85
|
+
if (request.aborted) return;
|
|
86
|
+
request.aborted = true;
|
|
87
|
+
request.reject(createAbortError());
|
|
88
|
+
}
|
|
51
89
|
|
|
52
90
|
function scheduleDispatch(): void {
|
|
53
91
|
if (isScheduled || queue.length === 0) return;
|
|
@@ -83,6 +121,9 @@ export function batch<K, V>(
|
|
|
83
121
|
isScheduled = false;
|
|
84
122
|
|
|
85
123
|
const batch = activeQueue;
|
|
124
|
+
for (const request of batch) {
|
|
125
|
+
activeRequests.add(request);
|
|
126
|
+
}
|
|
86
127
|
queue = [];
|
|
87
128
|
pendingKeys.clear();
|
|
88
129
|
|
|
@@ -125,16 +166,19 @@ export function batch<K, V>(
|
|
|
125
166
|
|
|
126
167
|
if (uniqueKeys.length === 0) return;
|
|
127
168
|
|
|
128
|
-
inFlightRequests = chunk;
|
|
129
|
-
|
|
130
169
|
tracer.emit({
|
|
131
170
|
type: 'dispatch',
|
|
132
171
|
batchId,
|
|
133
172
|
keys: uniqueKeys,
|
|
134
173
|
});
|
|
135
174
|
|
|
136
|
-
|
|
137
|
-
const signal =
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const signal = controller.signal;
|
|
177
|
+
const inFlight: InFlightChunk = { batchId, controller, requests: chunk };
|
|
178
|
+
inFlightChunks.add(inFlight);
|
|
179
|
+
for (const request of chunk) {
|
|
180
|
+
requestToInFlightChunk.set(request, inFlight);
|
|
181
|
+
}
|
|
138
182
|
|
|
139
183
|
const startedAt = performance.now();
|
|
140
184
|
|
|
@@ -145,6 +189,11 @@ export function batch<K, V>(
|
|
|
145
189
|
|
|
146
190
|
if (signal.aborted) {
|
|
147
191
|
tracer.emit({ type: 'abort', batchId });
|
|
192
|
+
for (const requests of keyToRequests.values()) {
|
|
193
|
+
for (const request of requests) {
|
|
194
|
+
rejectAsAborted(request);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
148
197
|
return;
|
|
149
198
|
}
|
|
150
199
|
|
|
@@ -221,8 +270,7 @@ export function batch<K, V>(
|
|
|
221
270
|
}
|
|
222
271
|
}
|
|
223
272
|
} finally {
|
|
224
|
-
|
|
225
|
-
inFlightRequests = [];
|
|
273
|
+
inFlightChunks.delete(inFlight);
|
|
226
274
|
}
|
|
227
275
|
}
|
|
228
276
|
|
|
@@ -243,8 +291,30 @@ export function batch<K, V>(
|
|
|
243
291
|
pendingKeys.add(cacheKey);
|
|
244
292
|
}
|
|
245
293
|
|
|
246
|
-
return new Promise<V>((
|
|
247
|
-
|
|
294
|
+
return new Promise<V>((resolvePromise, rejectPromise) => {
|
|
295
|
+
let settled = false;
|
|
296
|
+
let removeAbortListener: (() => void) | null = null;
|
|
297
|
+
let request!: PendingRequest<K, V>;
|
|
298
|
+
|
|
299
|
+
const resolve = (value: V) => {
|
|
300
|
+
if (settled) return;
|
|
301
|
+
settled = true;
|
|
302
|
+
removeAbortListener?.();
|
|
303
|
+
activeRequests.delete(request);
|
|
304
|
+
requestToInFlightChunk.delete(request);
|
|
305
|
+
resolvePromise(value);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const reject = (error: Error) => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
settled = true;
|
|
311
|
+
removeAbortListener?.();
|
|
312
|
+
activeRequests.delete(request);
|
|
313
|
+
requestToInFlightChunk.delete(request);
|
|
314
|
+
rejectPromise(error);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
request = {
|
|
248
318
|
key,
|
|
249
319
|
resolve,
|
|
250
320
|
reject,
|
|
@@ -256,23 +326,42 @@ export function batch<K, V>(
|
|
|
256
326
|
|
|
257
327
|
if (externalSignal) {
|
|
258
328
|
const onAbort = () => {
|
|
329
|
+
const inFlight = requestToInFlightChunk.get(request);
|
|
259
330
|
request.aborted = true;
|
|
260
|
-
reject(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
331
|
+
reject(createAbortError());
|
|
332
|
+
|
|
333
|
+
if (inFlight) {
|
|
334
|
+
const allChunkRequestsAborted = inFlight.requests.every(
|
|
335
|
+
(r) => r.aborted,
|
|
336
|
+
);
|
|
337
|
+
if (allChunkRequestsAborted) {
|
|
338
|
+
inFlight.controller.abort();
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
const allQueuedAborted =
|
|
342
|
+
queue.length > 0 && queue.every((r) => r.aborted);
|
|
343
|
+
if (allQueuedAborted) {
|
|
344
|
+
queue = [];
|
|
345
|
+
pendingKeys.clear();
|
|
346
|
+
if (cleanup) {
|
|
347
|
+
cleanup();
|
|
348
|
+
cleanup = null;
|
|
349
|
+
}
|
|
350
|
+
isScheduled = false;
|
|
351
|
+
}
|
|
272
352
|
}
|
|
273
353
|
};
|
|
274
354
|
|
|
275
355
|
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
356
|
+
removeAbortListener = () => {
|
|
357
|
+
externalSignal.removeEventListener('abort', onAbort);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Signal may have been aborted by user callbacks (trace handler, key function)
|
|
361
|
+
// that run between the initial check and listener registration.
|
|
362
|
+
if (externalSignal.aborted) {
|
|
363
|
+
onAbort();
|
|
364
|
+
}
|
|
276
365
|
}
|
|
277
366
|
|
|
278
367
|
scheduleDispatch();
|
|
@@ -299,14 +388,17 @@ export function batch<K, V>(
|
|
|
299
388
|
|
|
300
389
|
function abort(): void {
|
|
301
390
|
for (const request of queue) {
|
|
302
|
-
request
|
|
303
|
-
|
|
391
|
+
rejectAsAborted(request);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
for (const request of activeRequests) {
|
|
395
|
+
rejectAsAborted(request);
|
|
304
396
|
}
|
|
305
397
|
queue = [];
|
|
306
398
|
pendingKeys.clear();
|
|
307
399
|
|
|
308
|
-
|
|
309
|
-
|
|
400
|
+
for (const chunk of inFlightChunks) {
|
|
401
|
+
chunk.controller.abort();
|
|
310
402
|
}
|
|
311
403
|
|
|
312
404
|
if (cleanup) {
|
package/src/match.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { BatchError } from './errors';
|
|
1
2
|
import { indexed } from './indexed';
|
|
2
3
|
import type { Match, MatchFn } from './types';
|
|
3
4
|
|
|
4
|
-
export function isIndexed<K, V>(match: Match<K, V>): match is
|
|
5
|
+
export function isIndexed<K, V>(match: Match<K, V>): match is typeof indexed {
|
|
5
6
|
return match === indexed;
|
|
6
7
|
}
|
|
7
8
|
|
|
@@ -15,6 +16,12 @@ export function normalizeMatch<K, V>(match: Match<K, V>): MatchFn<K, V> | null {
|
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
if (typeof match === 'symbol') {
|
|
20
|
+
throw new BatchError(
|
|
21
|
+
'Unsupported symbol match. Use `indexed` for Record responses.',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
if (isKeyMatch<K, V>(match)) {
|
|
19
26
|
const key = match as string | number;
|
|
20
27
|
return (results: V[], requestedKey: K) => {
|
package/src/trace.ts
CHANGED
|
@@ -8,33 +8,14 @@ export interface DevtoolsHook {
|
|
|
8
8
|
}): ((event: TraceEvent) => void) | undefined;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
interface PendingBatcher {
|
|
12
|
-
fn: { toString(): string };
|
|
13
|
-
name: string | undefined;
|
|
14
|
-
stack: string | undefined;
|
|
15
|
-
setEmitter: (emitter: ((event: TraceEvent) => void) | undefined) => void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
let devtoolsHook: DevtoolsHook | null = null;
|
|
19
|
-
const pendingBatchers: PendingBatcher[] = [];
|
|
20
12
|
|
|
21
13
|
export function __setDevtoolsHook(hook: DevtoolsHook | null): void {
|
|
22
14
|
devtoolsHook = hook;
|
|
15
|
+
}
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const emitter = hook.onBatcherCreated({
|
|
27
|
-
fn: pending.fn,
|
|
28
|
-
name: pending.name,
|
|
29
|
-
stack: pending.stack,
|
|
30
|
-
});
|
|
31
|
-
pending.setEmitter(emitter);
|
|
32
|
-
}
|
|
33
|
-
pendingBatchers.length = 0;
|
|
34
|
-
} else if (!hook) {
|
|
35
|
-
// clear pending batchers when hook is removed
|
|
36
|
-
pendingBatchers.length = 0;
|
|
37
|
-
}
|
|
17
|
+
export function hasDevtoolsHook(): boolean {
|
|
18
|
+
return devtoolsHook != null;
|
|
38
19
|
}
|
|
39
20
|
|
|
40
21
|
export function registerBatcher(
|
|
@@ -48,8 +29,6 @@ export function registerBatcher(
|
|
|
48
29
|
if (devtoolsHook) {
|
|
49
30
|
const emitter = devtoolsHook.onBatcherCreated(info);
|
|
50
31
|
setEmitter(emitter);
|
|
51
|
-
} else {
|
|
52
|
-
pendingBatchers.push({ ...info, setEmitter });
|
|
53
32
|
}
|
|
54
33
|
}
|
|
55
34
|
|
|
@@ -63,20 +42,16 @@ export function createTracer<K>(
|
|
|
63
42
|
let batchCounter = 0;
|
|
64
43
|
|
|
65
44
|
function emit(event: TraceEventData<K>) {
|
|
45
|
+
const devtoolsEmitter = getDevtoolsEmitter?.();
|
|
46
|
+
if (!handler && !devtoolsEmitter) return;
|
|
47
|
+
|
|
66
48
|
const timestamp = performance.now();
|
|
67
49
|
const fullEvent = {
|
|
68
50
|
...event,
|
|
69
51
|
timestamp,
|
|
70
52
|
} as TraceEvent<K>;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
handler(fullEvent);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const devtoolsEmitter = getDevtoolsEmitter?.();
|
|
77
|
-
if (devtoolsEmitter) {
|
|
78
|
-
devtoolsEmitter(fullEvent);
|
|
79
|
-
}
|
|
53
|
+
handler?.(fullEvent);
|
|
54
|
+
devtoolsEmitter?.(fullEvent);
|
|
80
55
|
}
|
|
81
56
|
|
|
82
57
|
function nextBatchId(): string {
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,10 @@ export type BatchFn<K, V> = (
|
|
|
3
3
|
signal: AbortSignal,
|
|
4
4
|
) => Promise<V[] | Record<string, V>>;
|
|
5
5
|
|
|
6
|
-
export type Match<K, V> =
|
|
6
|
+
export type Match<K, V> =
|
|
7
|
+
| keyof V
|
|
8
|
+
| typeof import('./indexed').indexed
|
|
9
|
+
| MatchFn<K, V>;
|
|
7
10
|
|
|
8
11
|
export type MatchFn<K, V> = (results: V[], key: K) => V | undefined;
|
|
9
12
|
|