batchkit 0.2.0 → 0.3.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.
@@ -0,0 +1,3 @@
1
+ import type { Batcher, BatchFn, BatchOptions, Match } from './types';
2
+ export declare function batch<K, V>(fn: BatchFn<K, V>, match: Match<K, V>, options?: BatchOptions<K>): Batcher<K, V>;
3
+ //# sourceMappingURL=batch.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,4 @@
1
+ export declare class BatchError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,8 @@
1
+ export { batch } from './batch';
2
+ export { BatchError } from './errors';
3
+ export { indexed } from './indexed';
4
+ export { onAnimationFrame, onIdle } from './schedulers';
5
+ export type { DevtoolsHook } from './trace';
6
+ export { __setDevtoolsHook } from './trace';
7
+ export type { Batcher, BatchFn, BatchOptions, GetOptions, Match, MatchFn, Scheduler, TraceEvent, TraceEventData, TraceHandler, } from './types';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5C,YAAY,EACV,OAAO,EACP,OAAO,EACP,YAAY,EACZ,UAAU,EACV,KAAK,EACL,OAAO,EACP,SAAS,EACT,UAAU,EACV,cAAc,EACd,YAAY,GACb,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ function isIndexed(match) {
14
14
  return match === indexed;
15
15
  }
16
16
  function isKeyMatch(match) {
17
- return typeof match === "string";
17
+ return typeof match === "string" || typeof match === "number";
18
18
  }
19
19
  function normalizeMatch(match) {
20
20
  if (isIndexed(match)) {
@@ -66,15 +66,34 @@ function onIdle(options) {
66
66
  }
67
67
 
68
68
  // src/trace.ts
69
- function getDevtools() {
70
- if (typeof window !== "undefined" && window.__BATCHKIT_DEVTOOLS__) {
71
- return window.__BATCHKIT_DEVTOOLS__;
69
+ var devtoolsHook = null;
70
+ var pendingBatchers = [];
71
+ function __setDevtoolsHook(hook) {
72
+ 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
+ }
86
+ }
87
+ function registerBatcher(info, setEmitter) {
88
+ if (devtoolsHook) {
89
+ const emitter = devtoolsHook.onBatcherCreated(info);
90
+ setEmitter(emitter);
91
+ } else {
92
+ pendingBatchers.push({ ...info, setEmitter });
72
93
  }
73
- return;
74
94
  }
75
- function createTracer(name, handler) {
95
+ function createTracer(name, handler, getDevtoolsEmitter) {
76
96
  let batchCounter = 0;
77
- let registeredWithDevtools = false;
78
97
  function emit(event) {
79
98
  const timestamp = performance.now();
80
99
  const fullEvent = {
@@ -84,13 +103,9 @@ function createTracer(name, handler) {
84
103
  if (handler) {
85
104
  handler(fullEvent);
86
105
  }
87
- const devtools = getDevtools();
88
- if (devtools && name) {
89
- if (!registeredWithDevtools) {
90
- devtools.register({ name, registeredAt: timestamp });
91
- registeredWithDevtools = true;
92
- }
93
- devtools.emit(name, fullEvent);
106
+ const devtoolsEmitter = getDevtoolsEmitter?.();
107
+ if (devtoolsEmitter) {
108
+ devtoolsEmitter(fullEvent);
94
109
  }
95
110
  }
96
111
  function nextBatchId() {
@@ -110,7 +125,12 @@ function batch(fn, match, options = {}) {
110
125
  trace: traceHandler
111
126
  } = options;
112
127
  const scheduler = schedule ?? (waitMs ? wait(waitMs) : microtask);
113
- const tracer = createTracer(name, traceHandler);
128
+ let devtoolsEmitter;
129
+ const creationStack = new Error().stack;
130
+ registerBatcher({ fn, name, stack: creationStack }, (emitter) => {
131
+ devtoolsEmitter = emitter;
132
+ });
133
+ const tracer = createTracer(name, traceHandler, () => devtoolsEmitter);
114
134
  const matchFn = normalizeMatch(match);
115
135
  const isIndexedMatch = isIndexed(match);
116
136
  const indexedMatcher = isIndexedMatch ? createIndexedMatcher() : null;
@@ -119,6 +139,7 @@ function batch(fn, match, options = {}) {
119
139
  let cleanup = null;
120
140
  let isScheduled = false;
121
141
  let currentAbortController = null;
142
+ let inFlightRequests = [];
122
143
  function scheduleDispatch() {
123
144
  if (isScheduled || queue.length === 0)
124
145
  return;
@@ -179,6 +200,7 @@ function batch(fn, match, options = {}) {
179
200
  }
180
201
  if (uniqueKeys.length === 0)
181
202
  return;
203
+ inFlightRequests = chunk;
182
204
  tracer.emit({
183
205
  type: "dispatch",
184
206
  batchId,
@@ -259,6 +281,7 @@ function batch(fn, match, options = {}) {
259
281
  }
260
282
  } finally {
261
283
  currentAbortController = null;
284
+ inFlightRequests = [];
262
285
  }
263
286
  }
264
287
  function getSingle(key, options2) {
@@ -286,8 +309,9 @@ function batch(fn, match, options = {}) {
286
309
  const onAbort = () => {
287
310
  request.aborted = true;
288
311
  reject(new DOMException("Aborted", "AbortError"));
289
- const allAborted = queue.every((r) => r.aborted);
290
- if (allAborted && currentAbortController) {
312
+ const allPendingAborted = queue.every((r) => r.aborted);
313
+ const allInFlightAborted = inFlightRequests.length > 0 && inFlightRequests.every((r) => r.aborted);
314
+ if (allPendingAborted && allInFlightAborted && currentAbortController) {
291
315
  currentAbortController.abort();
292
316
  }
293
317
  };
@@ -338,5 +362,6 @@ export {
338
362
  onAnimationFrame,
339
363
  indexed,
340
364
  batch,
365
+ __setDevtoolsHook,
341
366
  BatchError
342
367
  };
@@ -0,0 +1,2 @@
1
+ export declare const indexed: unique symbol;
2
+ //# sourceMappingURL=indexed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indexed.d.ts","sourceRoot":"","sources":["../src/indexed.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,EAAE,OAAO,MAA0B,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Match, MatchFn } from './types';
2
+ export declare function isIndexed<K, V>(match: Match<K, V>): match is symbol;
3
+ export declare function isKeyMatch<K, V>(match: Match<K, V>): match is keyof V;
4
+ export declare function normalizeMatch<K, V>(match: Match<K, V>): MatchFn<K, V> | null;
5
+ export declare function createIndexedMatcher<K, V>(): (results: Record<string, V>, key: K) => V | undefined;
6
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,8 @@
1
+ import type { Scheduler } from './types';
2
+ export declare const microtask: Scheduler;
3
+ export declare function wait(ms: number): Scheduler;
4
+ export declare const onAnimationFrame: Scheduler;
5
+ export declare function onIdle(options?: {
6
+ timeout?: number;
7
+ }): Scheduler;
8
+ //# sourceMappingURL=schedulers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedulers.d.ts","sourceRoot":"","sources":["../src/schedulers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,eAAO,MAAM,SAAS,EAAE,SAYvB,CAAC;AAEF,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAK1C;AAED,eAAO,MAAM,gBAAgB,EAAE,SAG9B,CAAC;AAEF,wBAAgB,MAAM,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAahE"}
@@ -0,0 +1,23 @@
1
+ import type { TraceEvent, TraceEventData, TraceHandler } from './types';
2
+ export interface DevtoolsHook {
3
+ onBatcherCreated(info: {
4
+ fn: {
5
+ toString(): string;
6
+ };
7
+ name: string | undefined;
8
+ stack: string | undefined;
9
+ }): ((event: TraceEvent) => void) | undefined;
10
+ }
11
+ export declare function __setDevtoolsHook(hook: DevtoolsHook | null): void;
12
+ export declare function registerBatcher(info: {
13
+ fn: {
14
+ toString(): string;
15
+ };
16
+ name: string | undefined;
17
+ stack: string | undefined;
18
+ }, setEmitter: (emitter: ((event: TraceEvent) => void) | undefined) => void): void;
19
+ export declare function createTracer<K>(name: string | undefined, handler: TraceHandler<K> | undefined, getDevtoolsEmitter: (() => ((event: TraceEvent<K>) => void) | undefined) | undefined): {
20
+ emit: (event: TraceEventData<K>) => void;
21
+ nextBatchId: () => string;
22
+ };
23
+ //# sourceMappingURL=trace.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,61 @@
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>;
3
+ export type MatchFn<K, V> = (results: V[], key: K) => V | undefined;
4
+ export type IndexedMatchFn<K, V> = (results: Record<string, V>, key: K) => V | undefined;
5
+ export type Scheduler = (dispatch: () => void) => () => void;
6
+ export type TraceHandler<K = unknown> = (event: TraceEvent<K>) => void;
7
+ export type TraceEventData<K = unknown> = {
8
+ type: 'get';
9
+ key: K;
10
+ } | {
11
+ type: 'dedup';
12
+ key: K;
13
+ } | {
14
+ type: 'schedule';
15
+ batchId: string;
16
+ size: number;
17
+ } | {
18
+ type: 'dispatch';
19
+ batchId: string;
20
+ keys: K[];
21
+ } | {
22
+ type: 'resolve';
23
+ batchId: string;
24
+ duration: number;
25
+ } | {
26
+ type: 'error';
27
+ batchId: string;
28
+ error: Error;
29
+ } | {
30
+ type: 'abort';
31
+ batchId: string;
32
+ };
33
+ export type TraceEvent<K = unknown> = TraceEventData<K> & {
34
+ timestamp: number;
35
+ };
36
+ export interface BatchOptions<K = unknown> {
37
+ wait?: number;
38
+ schedule?: Scheduler;
39
+ max?: number;
40
+ key?: (k: K) => unknown;
41
+ name?: string;
42
+ trace?: TraceHandler<K>;
43
+ }
44
+ export interface GetOptions {
45
+ signal?: AbortSignal;
46
+ }
47
+ export interface Batcher<K, V> {
48
+ get(key: K, options?: GetOptions): Promise<V>;
49
+ get(keys: K[], options?: GetOptions): Promise<V[]>;
50
+ flush(): Promise<void>;
51
+ abort(): void;
52
+ readonly name?: string;
53
+ }
54
+ export interface PendingRequest<K, V> {
55
+ key: K;
56
+ resolve: (value: V) => void;
57
+ reject: (error: Error) => void;
58
+ signal?: AbortSignal;
59
+ aborted: boolean;
60
+ }
61
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
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.2.0",
3
+ "version": "0.3.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 } from './trace';
4
+ import { createTracer, registerBatcher } from './trace';
5
5
  import type {
6
6
  Batcher,
7
7
  BatchFn,
@@ -10,6 +10,7 @@ import type {
10
10
  Match,
11
11
  PendingRequest,
12
12
  Scheduler,
13
+ TraceEvent,
13
14
  } from './types';
14
15
 
15
16
  export function batch<K, V>(
@@ -27,7 +28,15 @@ export function batch<K, V>(
27
28
  } = options;
28
29
 
29
30
  const scheduler: Scheduler = schedule ?? (waitMs ? wait(waitMs) : microtask);
30
- const tracer = createTracer(name, traceHandler);
31
+
32
+ let devtoolsEmitter: ((event: TraceEvent) => void) | undefined;
33
+ const creationStack = new Error().stack;
34
+
35
+ registerBatcher({ fn, name, stack: creationStack }, (emitter) => {
36
+ devtoolsEmitter = emitter;
37
+ });
38
+
39
+ const tracer = createTracer(name, traceHandler, () => devtoolsEmitter);
31
40
 
32
41
  const matchFn = normalizeMatch(match);
33
42
  const isIndexedMatch = isIndexed(match);
@@ -38,6 +47,7 @@ export function batch<K, V>(
38
47
  let cleanup: (() => void) | null = null;
39
48
  let isScheduled = false;
40
49
  let currentAbortController: AbortController | null = null;
50
+ let inFlightRequests: PendingRequest<K, V>[] = [];
41
51
 
42
52
  function scheduleDispatch(): void {
43
53
  if (isScheduled || queue.length === 0) return;
@@ -115,6 +125,8 @@ export function batch<K, V>(
115
125
 
116
126
  if (uniqueKeys.length === 0) return;
117
127
 
128
+ inFlightRequests = chunk;
129
+
118
130
  tracer.emit({
119
131
  type: 'dispatch',
120
132
  batchId,
@@ -210,6 +222,7 @@ export function batch<K, V>(
210
222
  }
211
223
  } finally {
212
224
  currentAbortController = null;
225
+ inFlightRequests = [];
213
226
  }
214
227
  }
215
228
 
@@ -246,8 +259,15 @@ export function batch<K, V>(
246
259
  request.aborted = true;
247
260
  reject(new DOMException('Aborted', 'AbortError'));
248
261
 
249
- const allAborted = queue.every((r) => r.aborted);
250
- if (allAborted && currentAbortController) {
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
+ ) {
251
271
  currentAbortController.abort();
252
272
  }
253
273
  };
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export { batch } from './batch';
2
2
  export { BatchError } from './errors';
3
3
  export { indexed } from './indexed';
4
4
  export { onAnimationFrame, onIdle } from './schedulers';
5
+ export type { DevtoolsHook } from './trace';
6
+ export { __setDevtoolsHook } from './trace';
5
7
 
6
8
  export type {
7
9
  Batcher,
@@ -12,5 +14,6 @@ export type {
12
14
  MatchFn,
13
15
  Scheduler,
14
16
  TraceEvent,
17
+ TraceEventData,
15
18
  TraceHandler,
16
19
  } from './types';
package/src/match.ts CHANGED
@@ -6,7 +6,7 @@ export function isIndexed<K, V>(match: Match<K, V>): match is symbol {
6
6
  }
7
7
 
8
8
  export function isKeyMatch<K, V>(match: Match<K, V>): match is keyof V {
9
- return typeof match === 'string';
9
+ return typeof match === 'string' || typeof match === 'number';
10
10
  }
11
11
 
12
12
  export function normalizeMatch<K, V>(match: Match<K, V>): MatchFn<K, V> | null {
@@ -16,11 +16,11 @@ export function normalizeMatch<K, V>(match: Match<K, V>): MatchFn<K, V> | null {
16
16
  }
17
17
 
18
18
  if (isKeyMatch<K, V>(match)) {
19
- const key = match;
19
+ const key = match as string | number;
20
20
  return (results: V[], requestedKey: K) => {
21
21
  return results.find(
22
22
  (item) =>
23
- (item as Record<string, unknown>)[key as string] === requestedKey,
23
+ (item as Record<string | number, unknown>)[key] === requestedKey,
24
24
  );
25
25
  };
26
26
  }
package/src/trace.ts CHANGED
@@ -1,30 +1,69 @@
1
1
  import type { TraceEvent, TraceEventData, TraceHandler } from './types';
2
2
 
3
- interface DevtoolsRegistry {
4
- register(info: { name: string; registeredAt: number }): void;
5
- emit(name: string, event: unknown): void;
3
+ export interface DevtoolsHook {
4
+ onBatcherCreated(info: {
5
+ fn: { toString(): string };
6
+ name: string | undefined;
7
+ stack: string | undefined;
8
+ }): ((event: TraceEvent) => void) | undefined;
6
9
  }
7
10
 
8
- declare const window: { __BATCHKIT_DEVTOOLS__?: DevtoolsRegistry } | undefined;
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
+ let devtoolsHook: DevtoolsHook | null = null;
19
+ const pendingBatchers: PendingBatcher[] = [];
20
+
21
+ export function __setDevtoolsHook(hook: DevtoolsHook | null): void {
22
+ devtoolsHook = hook;
23
+
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
+ }
38
+ }
9
39
 
10
- function getDevtools(): DevtoolsRegistry | undefined {
11
- // biome-ignore lint/complexity/useOptionalChain: need typeof check for Node.js
12
- if (typeof window !== 'undefined' && window.__BATCHKIT_DEVTOOLS__) {
13
- return window.__BATCHKIT_DEVTOOLS__;
40
+ export function registerBatcher(
41
+ info: {
42
+ fn: { toString(): string };
43
+ name: string | undefined;
44
+ stack: string | undefined;
45
+ },
46
+ setEmitter: (emitter: ((event: TraceEvent) => void) | undefined) => void,
47
+ ): void {
48
+ if (devtoolsHook) {
49
+ const emitter = devtoolsHook.onBatcherCreated(info);
50
+ setEmitter(emitter);
51
+ } else {
52
+ pendingBatchers.push({ ...info, setEmitter });
14
53
  }
15
- return undefined;
16
54
  }
17
55
 
18
56
  export function createTracer<K>(
19
57
  name: string | undefined,
20
58
  handler: TraceHandler<K> | undefined,
59
+ getDevtoolsEmitter:
60
+ | (() => ((event: TraceEvent<K>) => void) | undefined)
61
+ | undefined,
21
62
  ) {
22
63
  let batchCounter = 0;
23
- let registeredWithDevtools = false;
24
64
 
25
65
  function emit(event: TraceEventData<K>) {
26
66
  const timestamp = performance.now();
27
-
28
67
  const fullEvent = {
29
68
  ...event,
30
69
  timestamp,
@@ -34,13 +73,9 @@ export function createTracer<K>(
34
73
  handler(fullEvent);
35
74
  }
36
75
 
37
- const devtools = getDevtools();
38
- if (devtools && name) {
39
- if (!registeredWithDevtools) {
40
- devtools.register({ name, registeredAt: timestamp });
41
- registeredWithDevtools = true;
42
- }
43
- devtools.emit(name, fullEvent);
76
+ const devtoolsEmitter = getDevtoolsEmitter?.();
77
+ if (devtoolsEmitter) {
78
+ devtoolsEmitter(fullEvent);
44
79
  }
45
80
  }
46
81