batchkit 0.1.0 → 0.2.0-beta.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
@@ -1,6 +1,6 @@
1
1
  # batchkit
2
2
 
3
- Automatic batching for async operations.
3
+ Tiny batching library. Supports various scheduling and deduplication strategies.
4
4
 
5
5
  ## Installation
6
6
 
@@ -20,17 +20,13 @@ const users = batch(
20
20
  'id'
21
21
  )
22
22
 
23
- // These calls are automatically batched into ONE database query
23
+ // These calls are batched into one database query
24
24
  const [alice, bob] = await Promise.all([
25
25
  users.get(1),
26
26
  users.get(2),
27
27
  ])
28
28
  ```
29
29
 
30
- That's it. Two arguments:
31
- 1. A function that fetches many items at once
32
- 2. The field to match results by
33
-
34
30
  ## API
35
31
 
36
32
  ### `batch(fn, match, options?)`
@@ -140,7 +136,7 @@ Batches all calls within the same event loop tick:
140
136
  ```typescript
141
137
  const users = batch(fn, 'id')
142
138
 
143
- // All batched into ONE request
139
+ // these synchronous invocations are batched into one request
144
140
  users.get(1)
145
141
  users.get(2)
146
142
  users.get(3)
@@ -231,7 +227,7 @@ function UserAvatar({ userId }: { userId: string }) {
231
227
  return <img src={data?.avatar} />
232
228
  }
233
229
 
234
- // Rendering 100 UserAvatars = 1 HTTP request
230
+ // Rendering 100 UserAvatars from a service -> 1 HTTP request
235
231
  ```
236
232
 
237
233
  ### API with Rate Limits
@@ -248,7 +244,7 @@ const products = batch(
248
244
 
249
245
  ## TypeScript
250
246
 
251
- Full type inference:
247
+ Correctly infers types based on call site
252
248
 
253
249
  ```typescript
254
250
  type User = { id: number; name: string }
@@ -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,EAGN,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,CA0Rf"}
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,EAGN,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,CA8Rf"}
package/dist/index.js CHANGED
@@ -66,16 +66,32 @@ 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__;
72
+ }
73
+ return;
74
+ }
69
75
  function createTracer(name, handler) {
70
76
  let batchCounter = 0;
77
+ let registeredWithDevtools = false;
71
78
  function emit(event) {
72
- if (!handler)
73
- return;
79
+ const timestamp = performance.now();
74
80
  const fullEvent = {
75
81
  ...event,
76
- timestamp: performance.now()
82
+ timestamp
77
83
  };
78
- handler(fullEvent);
84
+ if (handler) {
85
+ handler(fullEvent);
86
+ }
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);
94
+ }
79
95
  }
80
96
  function nextBatchId() {
81
97
  return `${name ?? "batch"}-${++batchCounter}`;
@@ -153,11 +169,13 @@ function batch(fn, match, options = {}) {
153
169
  if (request.aborted)
154
170
  continue;
155
171
  const cacheKey = keyFn(request.key);
156
- if (!keyToRequests.has(cacheKey)) {
157
- keyToRequests.set(cacheKey, []);
172
+ let requests = keyToRequests.get(cacheKey);
173
+ if (!requests) {
174
+ requests = [];
175
+ keyToRequests.set(cacheKey, requests);
158
176
  uniqueKeys.push(request.key);
159
177
  }
160
- keyToRequests.get(cacheKey).push(request);
178
+ requests.push(request);
161
179
  }
162
180
  if (uniqueKeys.length === 0)
163
181
  return;
@@ -186,6 +204,8 @@ function batch(fn, match, options = {}) {
186
204
  for (const key of uniqueKeys) {
187
205
  const cacheKey = keyFn(key);
188
206
  const requests = keyToRequests.get(cacheKey);
207
+ if (!requests)
208
+ continue;
189
209
  const value = indexedMatcher(recordResults, key);
190
210
  for (const request of requests) {
191
211
  if (request.aborted)
@@ -205,6 +225,8 @@ function batch(fn, match, options = {}) {
205
225
  for (const key of uniqueKeys) {
206
226
  const cacheKey = keyFn(key);
207
227
  const requests = keyToRequests.get(cacheKey);
228
+ if (!requests)
229
+ continue;
208
230
  const value = matchFn(arrayResults, key);
209
231
  for (const request of requests) {
210
232
  if (request.aborted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "batchkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.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
@@ -104,11 +104,13 @@ export function batch<K, V>(
104
104
 
105
105
  const cacheKey = keyFn(request.key);
106
106
 
107
- if (!keyToRequests.has(cacheKey)) {
108
- keyToRequests.set(cacheKey, []);
107
+ let requests = keyToRequests.get(cacheKey);
108
+ if (!requests) {
109
+ requests = [];
110
+ keyToRequests.set(cacheKey, requests);
109
111
  uniqueKeys.push(request.key);
110
112
  }
111
- keyToRequests.get(cacheKey)!.push(request);
113
+ requests.push(request);
112
114
  }
113
115
 
114
116
  if (uniqueKeys.length === 0) return;
@@ -144,7 +146,8 @@ export function batch<K, V>(
144
146
  const recordResults = results as Record<string, V>;
145
147
  for (const key of uniqueKeys) {
146
148
  const cacheKey = keyFn(key);
147
- const requests = keyToRequests.get(cacheKey)!;
149
+ const requests = keyToRequests.get(cacheKey);
150
+ if (!requests) continue;
148
151
  const value = indexedMatcher(recordResults, key);
149
152
 
150
153
  for (const request of requests) {
@@ -169,7 +172,8 @@ export function batch<K, V>(
169
172
 
170
173
  for (const key of uniqueKeys) {
171
174
  const cacheKey = keyFn(key);
172
- const requests = keyToRequests.get(cacheKey)!;
175
+ const requests = keyToRequests.get(cacheKey);
176
+ if (!requests) continue;
173
177
  const value = matchFn(arrayResults, key);
174
178
 
175
179
  for (const request of requests) {
package/src/trace.ts CHANGED
@@ -1,20 +1,47 @@
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;
6
+ }
7
+
8
+ declare const window: { __BATCHKIT_DEVTOOLS__?: DevtoolsRegistry } | undefined;
9
+
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__;
14
+ }
15
+ return undefined;
16
+ }
17
+
3
18
  export function createTracer<K>(
4
19
  name: string | undefined,
5
20
  handler: TraceHandler<K> | undefined,
6
21
  ) {
7
22
  let batchCounter = 0;
23
+ let registeredWithDevtools = false;
8
24
 
9
25
  function emit(event: TraceEventData<K>) {
10
- if (!handler) return;
26
+ const timestamp = performance.now();
11
27
 
12
28
  const fullEvent = {
13
29
  ...event,
14
- timestamp: performance.now(),
30
+ timestamp,
15
31
  } as TraceEvent<K>;
16
32
 
17
- handler(fullEvent);
33
+ if (handler) {
34
+ handler(fullEvent);
35
+ }
36
+
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);
44
+ }
18
45
  }
19
46
 
20
47
  function nextBatchId(): string {
package/dist/batch.d.ts DELETED
@@ -1,3 +0,0 @@
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
package/dist/errors.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export declare class BatchError extends Error {
2
- constructor(message: string);
3
- }
4
- //# sourceMappingURL=errors.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B"}
package/dist/index.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export { batch } from './batch';
2
- export { BatchError } from './errors';
3
- export { indexed } from './indexed';
4
- export { onAnimationFrame, onIdle } from './schedulers';
5
- export type { Batcher, BatchFn, BatchOptions, GetOptions, Match, MatchFn, Scheduler, TraceEvent, TraceHandler, } from './types';
6
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
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;AAExD,YAAY,EACV,OAAO,EACP,OAAO,EACP,YAAY,EACZ,UAAU,EACV,KAAK,EACL,OAAO,EACP,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAC"}
package/dist/indexed.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export declare const indexed: unique symbol;
2
- //# sourceMappingURL=indexed.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"indexed.d.ts","sourceRoot":"","sources":["../src/indexed.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,EAAE,OAAO,MAA0B,CAAC"}
package/dist/match.d.ts DELETED
@@ -1,6 +0,0 @@
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
@@ -1 +0,0 @@
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,8 +0,0 @@
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
@@ -1 +0,0 @@
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"}
package/dist/trace.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import type { TraceEventData, TraceHandler } from './types';
2
- export declare function createTracer<K>(name: string | undefined, handler: TraceHandler<K> | undefined): {
3
- emit: (event: TraceEventData<K>) => void;
4
- nextBatchId: () => string;
5
- };
6
- //# sourceMappingURL=trace.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAExE,wBAAgB,YAAY,CAAC,CAAC,EAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS;kBAIf,eAAe,CAAC,CAAC;uBAWd,MAAM;EAK/B"}
package/dist/types.d.ts DELETED
@@ -1,61 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}