batchkit 0.3.6 → 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 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
@@ -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,CAiTf"}
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
- if (hook && pendingBatchers.length > 0) {
74
- for (const pending of pendingBatchers) {
75
- const emitter = hook.onBatcherCreated({
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
- if (handler) {
104
- handler(fullEvent);
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
- const creationStack = new Error().stack;
130
- registerBatcher({ fn, name, stack: creationStack }, (emitter) => {
131
- devtoolsEmitter = emitter;
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
- let currentAbortController = null;
142
- let inFlightRequests = [];
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
- currentAbortController = new AbortController;
210
- const signal = currentAbortController.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
- currentAbortController = null;
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((resolve, reject) => {
300
- const request = {
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(new DOMException("Aborted", "AbortError"));
312
- const allPendingAborted = queue.every((r) => r.aborted);
313
- const allInFlightAborted = inFlightRequests.length > 0 && inFlightRequests.every((r) => r.aborted);
314
- if (allPendingAborted && allInFlightAborted && currentAbortController) {
315
- currentAbortController.abort();
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.aborted = true;
340
- request.reject(new DOMException("Aborted", "AbortError"));
403
+ rejectAsAborted(request);
404
+ }
405
+ for (const request of activeRequests) {
406
+ rejectAsAborted(request);
341
407
  }
342
408
  queue = [];
343
409
  pendingKeys.clear();
344
- if (currentAbortController) {
345
- currentAbortController.abort();
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 symbol;
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;
@@ -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,MAAM,CAEnE;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,CAkB7E;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"}
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;
@@ -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;AAYD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAiBjE;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,CAON;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;uBAiBd,MAAM;EAK/B"}
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 | symbol | MatchFn<K, 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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "batchkit",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "A modern TypeScript library for batching async operations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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
- const creationStack = new Error().stack;
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
- registerBatcher({ fn, name, stack: creationStack }, (emitter) => {
36
- devtoolsEmitter = emitter;
37
- });
46
+ // If devtools are already installed, register immediately.
47
+ tryRegisterDevtools();
38
48
 
39
- const tracer = createTracer(name, traceHandler, () => devtoolsEmitter);
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
- let currentAbortController: AbortController | null = null;
50
- let inFlightRequests: PendingRequest<K, V>[] = [];
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
- currentAbortController = new AbortController();
137
- const signal = currentAbortController.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
- currentAbortController = null;
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>((resolve, reject) => {
247
- const request: PendingRequest<K, V> = {
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(new DOMException('Aborted', 'AbortError'));
261
-
262
- const allPendingAborted = queue.every((r) => r.aborted);
263
- const allInFlightAborted =
264
- inFlightRequests.length > 0 &&
265
- inFlightRequests.every((r) => r.aborted);
266
- if (
267
- allPendingAborted &&
268
- allInFlightAborted &&
269
- currentAbortController
270
- ) {
271
- currentAbortController.abort();
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.aborted = true;
303
- request.reject(new DOMException('Aborted', 'AbortError'));
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
- if (currentAbortController) {
309
- currentAbortController.abort();
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 symbol {
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
- if (hook && pendingBatchers.length > 0) {
25
- for (const pending of pendingBatchers) {
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
- if (handler) {
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> = keyof V | symbol | MatchFn<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