@robohall/react-query-factory 2.0.0 → 2.1.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
@@ -5,105 +5,293 @@
5
5
  ![gzipped](https://img.shields.io/badge/gzipped-%3C_3_kB-blue)
6
6
  [![license](https://img.shields.io/npm/l/@robohall/react-query-factory)](./LICENSE)
7
7
 
8
- A factory function for TanStack Query configs. Instead of calling `useQuery` with ad-hoc options, you define a factory once and call it anywhere — getting consistent cache keys, automatic pagination crawling, and `useInfiniteQuery` support for free. TanStack's API stays fully exposed.
8
+ <p align="center">
9
+ <a href="https://roberth26.github.io/react-query-factory/"><strong>Visit the Sandbox</strong></a>
10
+ </p>
9
11
 
10
- Zero runtime dependenciesall TanStack imports are type-only and erased at compile time.
12
+ TanStack Query handles caching, syncing, and invalidation. What it doesn't do is crawl paginated APIs for you. This library adds that a factory function that wraps your `queryFn` with a configurable crawl loop so `useQuery` can return accumulated results instead of a single page. The `queryFn` can be a plain async function or an async iterable (e.g. an AWS SDK paginator), with no cursor wiring required in the latter case. The same factory produces `useInfiniteQuery` options, composes into child factories that share the cache, and exposes scope-aware invalidation keys. TanStack's API stays fully exposed at every call site.
13
+
14
+ Zero runtime dependencies.
11
15
 
12
16
  ---
13
17
 
14
- ## Installation
18
+ ## The problem
15
19
 
16
- ```bash
17
- npm install @robohall/react-query-factory
18
- # peer dependency: @tanstack/react-query >= 5.0.0
20
+ ### Step 1 — wrap `useQuery` in a custom hook
21
+
22
+ The first instinct when a query is reused across components:
23
+
24
+ ```typescript
25
+ function useInstances(params: DescribeInstancesCommandInput) {
26
+ return useQuery({
27
+ queryKey: ['instances', params],
28
+ queryFn: () => fetchInstances(params),
29
+ });
30
+ }
19
31
  ```
20
32
 
21
- ---
33
+ Works, until requirements grow. You need a `select` option — so the hook grows a generic. You need a `useInfiniteQuery` variant — so you write a second hook with a key differentiator to avoid a cache collision. You need to prefetch in a route loader — but the key is trapped inside the hook.
22
34
 
23
- ## Quick start
35
+ ```typescript
36
+ function useInstances<TSelected = Instance[]>(
37
+ params: DescribeInstancesCommandInput,
38
+ options?: { select?: (data: Instance[]) => TSelected },
39
+ ) {
40
+ return useQuery({
41
+ queryKey: ['instances', params],
42
+ queryFn: () => fetchInstances(params),
43
+ select: options?.select,
44
+ });
45
+ }
46
+
47
+ // separate hook, duplicated key and queryFn, must stay in sync manually
48
+ function useInstancesInfinite(params: DescribeInstancesCommandInput) {
49
+ return useInfiniteQuery({
50
+ queryKey: ['instances', 'infinite', params],
51
+ // ...
52
+ });
53
+ }
54
+ ```
24
55
 
25
- Define a factory once, call it in any component:
56
+ The generics multiply with every new transform. The key is still trapped — prefetching and invalidation still can't reach it from outside.
57
+
58
+ ### Step 2 — `queryOptions` for colocation
59
+
60
+ TanStack's `queryOptions` helper moves the key and fn into a shared object:
26
61
 
27
62
  ```typescript
28
- import {
29
- EC2Client,
30
- DescribeInstancesCommand,
31
- type DescribeInstancesCommandInput,
32
- } from '@aws-sdk/client-ec2';
33
- import { queryFactory } from '@robohall/react-query-factory';
34
- import { useQuery } from '@tanstack/react-query';
63
+ const instancesOptions = (params: DescribeInstancesCommandInput) =>
64
+ queryOptions({
65
+ queryKey: ['instances', params],
66
+ queryFn: () => fetchInstances(params),
67
+ });
68
+
69
+ useQuery(instancesOptions(params));
70
+ queryClient.prefetchQuery(instancesOptions(params));
71
+ queryClient.invalidateQueries(instancesOptions(params));
72
+ ```
35
73
 
36
- const ec2 = new EC2Client({ region: 'us-east-1' });
74
+ This is genuinely good this library builds on the same pattern. But once you need multiple related queries, the cracks show.
37
75
 
38
- const describeInstances = queryFactory({
39
- queryKey: ['ec2:DescribeInstances'],
40
- queryFn: (params: DescribeInstancesCommandInput, ctx) =>
41
- ec2.send(new DescribeInstancesCommand(params), { abortSignal: ctx.signal }),
76
+ ### Step 3 — derived queries and key coordination
77
+
78
+ Say you want a running-instances view that shares the same cache entry as the full list. The natural move is to spread the base options and override `select`:
79
+
80
+ ```typescript
81
+ const { data: running } = useQuery({
82
+ ...instancesOptions(params),
83
+ select: data => data.filter(i => i.state === 'running'),
42
84
  });
85
+ ```
43
86
 
44
- function InstanceList() {
45
- const { data } = useQuery(
46
- describeInstances({ Filters: [{ Name: 'instance-state-name', Values: ['running'] }] })
47
- );
48
- // query key: ['ec2:DescribeInstances', { Filters: [...] }]
49
- }
87
+ No key or `queryFn` duplication — this is the right approach. But `select` can only be applied at the call site, not captured in `instancesOptions` itself. And after a mutation you still need a magic string to bust the cache:
88
+
89
+ ```typescript
90
+ // If the key structure ever changes, every site breaks.
91
+ queryClient.invalidateQueries({ queryKey: ['instances'] });
50
92
  ```
51
93
 
52
- `describeInstances({ ... })` returns a plain object `{ queryKey, queryFn, staleTime, … }` — that you spread or pass directly to `useQuery`. The factory does not touch your query client.
94
+ ### Step 4paginated APIs
53
95
 
54
- ---
96
+ `DescribeInstances` returns at most `MaxResults` instances per call. To get them all, you need to loop. The usual options:
55
97
 
56
- ## Crawling
98
+ **Put the loop in `queryFn`:**
99
+
100
+ ```typescript
101
+ const instancesOptions = params =>
102
+ queryOptions({
103
+ queryKey: ['instances', params],
104
+ queryFn: async () => {
105
+ let all: Instance[] = [];
106
+ let nextToken: string | undefined;
107
+ do {
108
+ const page = await ec2.send(
109
+ new DescribeInstancesCommand({ ...params, NextToken: nextToken }),
110
+ );
111
+ all = [
112
+ ...all,
113
+ ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
114
+ ];
115
+ nextToken = page.NextToken;
116
+ } while (nextToken);
117
+ return all;
118
+ },
119
+ });
120
+ ```
57
121
 
58
- `DescribeInstances` is paginated. If you have more than 20 instances, one call won't get them all. The standard approach chaining `fetchNextPage` calls, accumulating results, checking `NextToken` is correct but tedious to repeat everywhere.
122
+ The crawl logic is now baked in. Every call site gets all pages you can't stop at 50 for a dropdown while fetching all for a table. The loop gets copy-pasted into every paginated query.
59
123
 
60
- Add `getNextPageParam` and `shouldFetchNextPage` to activate crawling — those two are the only required pieces. `initialPageParam` types `ctx.pageParam` in your `queryFn` (without it and without a `getNextPageParam` that provides inference, `ctx.pageParam` is `never`). `reduce` folds crawled pages into a single value; without it the result is an array of all fetched raw pages (`TData[]`). **`shouldFetchNextPage`** is called after each page — return `true` to keep fetching, `false` to stop. Use `() => true` to walk every page:
124
+ **Use `useInfiniteQuery`:**
61
125
 
62
126
  ```typescript
63
- import type { Instance, DescribeInstancesCommandInput } from '@aws-sdk/client-ec2';
127
+ const instancesInfiniteOptions = params =>
128
+ infiniteQueryOptions({
129
+ queryKey: ['instances', 'infinite', params],
130
+ queryFn: ({ pageParam }) =>
131
+ ec2.send(
132
+ new DescribeInstancesCommand({ ...params, NextToken: pageParam }),
133
+ ),
134
+ getNextPageParam: r => r.NextToken,
135
+ initialPageParam: undefined,
136
+ });
137
+
138
+ // Caller still has to flatten, auto-advance, manage hasNextPage...
139
+ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
140
+ instancesInfiniteOptions(params),
141
+ );
142
+ const allInstances = data?.pages.flatMap(
143
+ page => page.Reservations?.flatMap(r => r.Instances ?? []) ?? [],
144
+ );
145
+ ```
146
+
147
+ Now you have two separate factories that duplicate the key and queryFn and need to stay in sync. `useQuery` and `useInfiniteQuery` are separate cache entries. Derived queries, invalidation, and prefetching all have to be wired up independently for each.
148
+
149
+ ### What's missing
150
+
151
+ - Define the query **once**: key, queryFn, pagination config
152
+ - Let each **call site** decide how much to crawl (e.g. 50 records, all of them, or none)
153
+ - Optionally have `useQuery` crawl and return the **accumulated result** instead of a single page
154
+ - Use **async iterables** as `queryFn` — pass a paginator function directly, no cursor wiring required
155
+ - Have `.infinite()` available on the **same factory**, no duplication
156
+ - Have derived queries **share the cache entry** automatically
157
+ - Have **scoped invalidation** through key composition — bust the whole namespace or just one param set and its children
158
+
159
+ ---
160
+
161
+ ## The solution
162
+
163
+ ```typescript
164
+ import { queryFactory } from '@robohall/react-query-factory';
64
165
 
65
166
  const describeInstances = queryFactory({
66
167
  queryKey: ['ec2:DescribeInstances'],
67
168
  queryFn: (params: DescribeInstancesCommandInput, ctx) =>
68
169
  ec2.send(
69
170
  new DescribeInstancesCommand({ ...params, NextToken: ctx.pageParam }),
70
- { abortSignal: ctx.signal },
171
+ {
172
+ abortSignal: ctx.signal,
173
+ },
71
174
  ),
72
- getNextPageParam: response => response.NextToken,
175
+ getNextPageParam: r => r.NextToken,
73
176
  initialPageParam: undefined as string | undefined,
74
- shouldFetchNextPage: () => true,
75
177
  reduce: (acc, page): Instance[] => [
76
178
  ...(acc ?? []),
77
179
  ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
78
180
  ],
181
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
182
+ opts.minResults == null || instances.length < opts.minResults,
79
183
  });
80
184
 
81
- function InstanceList() {
82
- // one useQuery call; data is Instance[], not DescribeInstancesResponse[]
83
- const { data } = useQuery(describeInstances({ MaxResults: 20 }));
84
- }
185
+ // useQuery — crawls all pages, data is Instance[]
186
+ const { data } = useQuery(describeInstances({ MaxResults: 20 }));
187
+
188
+ // Stop at 50 — separate cache entry, independent crawl
189
+ const { data } = useQuery(
190
+ describeInstances({ MaxResults: 20 }, { minResults: 50 }),
191
+ );
192
+
193
+ // UI-driven pagination — same factory, no duplication
194
+ const { data, fetchNextPage } = useInfiniteQuery(
195
+ describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 }),
196
+ );
197
+
198
+ // Derived view — shares the cache entry, no extra API call
199
+ const runningInstances = queryFactory(describeInstances, {
200
+ select: instances => instances.filter(i => i.State?.Name === 'running'),
201
+ });
202
+ const { data: running } = useQuery(runningInstances({ MaxResults: 20 }));
203
+
204
+ // Prefetch in a route loader
205
+ await queryClient.prefetchQuery(describeInstances({ MaxResults: 20 }));
206
+
207
+ // Bust everything in the namespace
208
+ queryClient.invalidateQueries(describeInstances());
209
+
210
+ // Bust only this param set — cascades to runningInstances and any other child
211
+ queryClient.invalidateQueries(describeInstances({ MaxResults: 20 }));
85
212
  ```
86
213
 
87
- `shouldFetchNextPage` also accepts a `crawlOptions` object passed at call time, letting each call site control the crawl independently:
214
+ `describeInstances({ ... })` returns a plain `{ queryKey, queryFn, ... }` object pass it directly to `useQuery`, `useInfiniteQuery`, `prefetchQuery`, or `getQueryData`. The factory doesn't touch your query client.
215
+
216
+ ---
217
+
218
+ ## Installation
219
+
220
+ ```bash
221
+ npm install @robohall/react-query-factory
222
+ # peer dependency: @tanstack/react-query >= 5.0.0
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Crawling
228
+
229
+ `shouldFetchNextPage` is called after each page — return `true` to keep fetching, `false` to stop. `getNextPageParam` and `initialPageParam` follow the exact TanStack API. `reduce` folds pages into a single accumulated value; without it the result is an array of raw pages (`TData[]`).
230
+
231
+ The `crawlOptions` argument passed at call time is forwarded to `shouldFetchNextPage` and appended to the query key, so different call sites crawl independently and never share a cache entry:
88
232
 
89
233
  ```typescript
90
234
  const describeInstances = queryFactory({
91
235
  // ...
92
- reduce: (acc, page): Instance[] => [...(acc ?? []), ...page.Reservations.flatMap(r => r.Instances)],
93
236
  shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
94
237
  opts.minResults == null || instances.length < opts.minResults,
95
238
  });
96
239
 
97
- // fetch all pages
240
+ // two separate cache entries — crawl independently
98
241
  const { data: all } = useQuery(describeInstances({ MaxResults: 20 }));
99
-
100
- // stop after accumulating at least 50 instances (≥ 3 API calls)
101
242
  const { data: partial } = useQuery(
102
- describeInstances({ MaxResults: 20 }, { minResults: 50 })
243
+ describeInstances({ MaxResults: 20 }, { minResults: 50 }),
103
244
  );
104
245
  ```
105
246
 
106
- `crawlOptions` is appended to the query key, so `describeInstances({}, { minResults: 50 })` and `describeInstances({}, { minResults: 200 })` are separate cache entries — they crawl independently and never collide.
247
+ ---
248
+
249
+ ## Async iterator queryFns
250
+
251
+ When `queryFn` returns an `AsyncIterable`, the library detects it automatically and drives the crawl with `for await...of` instead of the cursor loop. This means `getNextPageParam` and `initialPageParam` are not required — the iterator manages its own cursor. AWS SDK v3 paginator functions (`paginateDescribeInstances`, etc.) are a common source of async iterables:
252
+
253
+ ```typescript
254
+ import { paginateDescribeInstances } from '@aws-sdk/client-ec2';
255
+
256
+ const describeInstances = queryFactory({
257
+ queryKey: ['ec2:DescribeInstances'],
258
+ queryFn: (params: DescribeInstancesCommandInput) =>
259
+ paginateDescribeInstances({ client: ec2 }, params),
260
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
261
+ opts.minResults == null || instances.length < opts.minResults,
262
+ reduce: (acc, page: DescribeInstancesResponse): Instance[] => [
263
+ ...(acc ?? []),
264
+ ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
265
+ ],
266
+ });
267
+ ```
268
+
269
+ `shouldFetchNextPage` still controls early stopping — the library calls `next()` on the iterator only when it returns `true`. Any source of `AsyncIterable<TPage>` works, not just AWS SDK paginators.
270
+
271
+ For `.infinite()` mode, `getNextPageParam` is required to capture the next virtual page's starting cursor from the last yielded item. AWS SDK v3 paginators accept a `startingToken` to resume from a specific position — wire `ctx.pageParam` to it:
272
+
273
+ ```typescript
274
+ const describeInstances = queryFactory({
275
+ queryKey: ['ec2:DescribeInstances'],
276
+ queryFn: (params: DescribeInstancesCommandInput, ctx) =>
277
+ paginateDescribeInstances(
278
+ { client: ec2 },
279
+ { ...params, StartingToken: ctx.pageParam },
280
+ ),
281
+ getNextPageParam: page => page.NextToken,
282
+ initialPageParam: undefined as string | undefined,
283
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
284
+ opts.minResults == null || instances.length < opts.minResults,
285
+ reduce: (acc, page): Instance[] => [
286
+ ...(acc ?? []),
287
+ ...(page.Instances ?? []),
288
+ ],
289
+ });
290
+
291
+ const { data, fetchNextPage } = useInfiniteQuery(
292
+ describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 }),
293
+ );
294
+ ```
107
295
 
108
296
  ---
109
297
 
@@ -139,7 +327,7 @@ const findInstance = queryFactory(describeInstances, {
139
327
  // query key: ['ec2:DescribeInstances', { MaxResults: 20 }, 'find', { instanceId: 'i-0abc123def456' }]
140
328
  // crawls pages until the target instance appears, then stops
141
329
  const { data } = useQuery(
142
- findInstance({ MaxResults: 20 }, { instanceId: 'i-0abc123def456' })
330
+ findInstance({ MaxResults: 20 }, { instanceId: 'i-0abc123def456' }),
143
331
  );
144
332
  ```
145
333
 
@@ -180,18 +368,17 @@ Every factory exposes a `.infinite()` method that returns `useInfiniteQuery`-com
180
368
  ```typescript
181
369
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
182
370
  // load 50 instances per UI page, each backed by up to 5 DescribeInstances calls
183
- describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 })
371
+ describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 }),
184
372
  );
185
373
 
186
374
  // data.pages is Instance[][], one array per virtual page
187
375
  ```
188
376
 
189
377
  The `.infinite()` key includes an `'infinite'` segment to keep it separate from the regular `useQuery` cache entry:
378
+
190
379
  - `describeInstances({ MaxResults: 20 })` → `['ec2:DescribeInstances', { MaxResults: 20 }]`
191
380
  - `describeInstances.infinite({ MaxResults: 20 })` → `['ec2:DescribeInstances', 'infinite', { MaxResults: 20 }]`
192
381
 
193
- Child factories place `params` before their own key segments so that the parent key is always a prefix of the child key for the same params — enabling per-call-site scoped invalidation.
194
-
195
382
  ---
196
383
 
197
384
  ## Public API
@@ -209,6 +396,7 @@ queryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>(
209
396
  ### `queryFactory(parent, config)`
210
397
 
211
398
  Creates a child factory. Two overloads:
399
+
212
400
  - **With a new `queryFn`** — inherits key namespace and standard options; crawling config must be re-declared if needed.
213
401
  - **Without a `queryFn`** — inherits everything; accepts `queryKey`, `select`, standard options, and any crawling fields (`shouldFetchNextPage`, `reduce`, `getNextPageParam`, `getPreviousPageParam`, `initialPageParam`) to override the parent's. `select` is composed with the parent's.
214
402
 
@@ -216,17 +404,17 @@ Creates a child factory. Two overloads:
216
404
 
217
405
  All fields except `reduce` and `shouldFetchNextPage` are the standard TanStack Query API — the same types and semantics you'd pass to `useQuery` or `useInfiniteQuery`. The factory doesn't reinvent them; it just requires certain combinations to be present in order to activate crawling.
218
406
 
219
- | Field | Type | Notes |
220
- |---|---|---|
221
- | `queryKey` | `QueryKey` | Namespace segments. Params are appended at call time. |
222
- | `queryFn` | `(params: TParams, ctx: QueryFunctionContext) => TData \| Promise<TData>` | Same as TanStack, with an extra leading `params` argument. |
223
- | `select` | `(data: TData) => TSelected` | Exact TanStack API. Composed automatically on child factories. |
224
- | `getNextPageParam` | `GetNextPageParamFunction<TPageParam, TData>` | Exact TanStack API. Required (with `shouldFetchNextPage`) to activate crawling. Required (with `initialPageParam`) for `.infinite()`. |
225
- | `initialPageParam` | `TPageParam` | Exact TanStack API. Drives `TPageParam` inference — without it and without a `getNextPageParam` that provides inference, `ctx.pageParam` is typed `never`. Required for `.infinite()` to work at runtime. |
226
- | `getPreviousPageParam` | `GetPreviousPageParamFunction<TPageParam, TData>` | Exact TanStack API. Passed through on `.infinite()`. |
227
- | `shouldFetchNextPage` | `(combined: TSelected \| undefined, crawlOptions: TCrawlOptions) => boolean` | Library addition. **Required (with `getNextPageParam`) to activate crawling.** Called after each page — return `true` to keep fetching, `false` to stop. |
228
- | `reduce` | `(acc: TSelected \| undefined, page: TData) => TSelected` | Library addition. Optional. Folds crawled pages into a single `TSelected` value; when omitted the result is an array of all fetched raw pages (`TData[]`). |
229
- | + all `StandardQueryOptions` fields | | All options accepted by TanStack's `useQuery` / `useInfiniteQuery` except `queryKey`, `queryFn`, and `select` (which the factory owns). Includes `staleTime`, `gcTime`, `retry`, `retryOnMount`, `enabled`, `refetchOnWindowFocus`, `refetchOnReconnect`, `refetchOnMount`, `refetchInterval`, `refetchIntervalInBackground`, `networkMode`, `notifyOnChangeProps`, `throwOnError`, `structuralSharing`, `initialData`, `initialDataUpdatedAt`, `placeholderData`, `queryKeyHashFn`, `persister`, `meta`, `maxPages`, `experimental_prefetchInRender`. Function-form callbacks (e.g. `enabled: (query) => boolean`) are supported wherever TanStack accepts them. |
407
+ | Field | Type | Notes |
408
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
409
+ | `queryKey` | `QueryKey` | Namespace segments. Params are appended at call time. |
410
+ | `queryFn` | `(params: TParams, ctx: QueryFunctionContext) => TData \| Promise<TData> \| AsyncIterable<TData>` | Same as TanStack, with an extra leading `params` argument. Returns an `AsyncIterable` to use iterator-based crawling. |
411
+ | `select` | `(data: TData) => TSelected` | Exact TanStack API. Composed automatically on child factories. |
412
+ | `getNextPageParam` | `GetNextPageParamFunction<TPageParam, TData>` | Exact TanStack API. Required (with `shouldFetchNextPage`) to activate cursor-based crawling. Required (with `initialPageParam`) for `.infinite()`. |
413
+ | `initialPageParam` | `TPageParam` | Exact TanStack API. Drives `TPageParam` inference. Required for `.infinite()` to work at runtime. |
414
+ | `getPreviousPageParam` | `GetPreviousPageParamFunction<TPageParam, TData>` | Exact TanStack API. Passed through on `.infinite()`. |
415
+ | `shouldFetchNextPage` | `(combined: TSelected \| undefined, crawlOptions: TCrawlOptions) => boolean` | Library addition. **Required to activate crawling.** Called after each page — return `true` to keep fetching, `false` to stop. |
416
+ | `reduce` | `(acc: TSelected \| undefined, page: TData) => TSelected` | Library addition. Optional. Folds crawled pages into a single `TSelected` value; when omitted the result is an array of all fetched raw pages (`TData[]`). |
417
+ | + all `StandardQueryOptions` fields | | `staleTime`, `gcTime`, `retry`, `enabled`, `refetchOnWindowFocus`, `placeholderData`, `initialData`, `meta`, etc. Function-form callbacks are supported wherever TanStack accepts them. |
230
418
 
231
419
  ### `QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>`
232
420
 
@@ -249,10 +437,8 @@ Return type of `factory.infinite(params)`. Pass directly to `useInfiniteQuery()`
249
437
 
250
438
  ## Running the sandbox
251
439
 
252
- The sandbox contains six interactive demos using a mock paginated API: basic single-page fetch, full crawl, factory composition, infinite query with per-page crawling, early-stop target search, and namespace-based cache invalidation.
253
-
254
440
  ```bash
255
441
  npm run sandbox
256
442
  ```
257
443
 
258
- This starts a Vite dev server. Navigate to the URL it prints (typically `http://localhost:5173`).
444
+ Starts a Vite dev server with interactive demos covering every pattern: basic single-page fetch, async iterator queryFns, crawl-then-render, render-while-crawling, on-demand infinite pagination, client-side search with early stopping, factory composition, and scoped cache invalidation.
package/dist/index.d.mts CHANGED
@@ -156,5 +156,39 @@ declare function queryFactory<TChildParams extends TParentParams, TData = unknow
156
156
  reduce?: (accumulator: TChildSelected | undefined, page: TData) => TChildSelected;
157
157
  shouldFetchNextPage?: (combined: TParentHasReduce extends true ? TChildSelected : TChildSelected | undefined, crawlOptions: TChildCrawlOptions) => boolean;
158
158
  }): QueryFactory<TChildParams, TData, TError, TChildSelected, TPageParam, TChildCrawlOptions, TParentHasReduce>;
159
+ /**
160
+ * Creates a standalone factory whose queryFn returns an AsyncIterable (e.g. an AWS SDK v3
161
+ * paginator). The library drives the crawl with `for await...of`; `getNextPageParam` and
162
+ * `initialPageParam` are not required for `useQuery` mode. When `reduce` is present,
163
+ * `shouldFetchNextPage` receives `TSelected` (never undefined).
164
+ */
165
+ declare function queryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(config: StandardQueryOptions<TError, TData> & {
166
+ queryKey: QueryKey;
167
+ queryFn: (params: TParams, context: QueryFunctionContext<QueryKey, [
168
+ unknown
169
+ ] extends [TPageParam] ? never : TPageParam>) => AsyncIterable<TData>;
170
+ select?: (data: TData) => TSelected;
171
+ getNextPageParam?: GetNextPageParamFunction<TPageParam, TData>;
172
+ initialPageParam?: TPageParam;
173
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
174
+ reduce: (accumulator: TSelected | undefined, page: TData) => TSelected;
175
+ shouldFetchNextPage?: (combined: TSelected, crawlOptions: TCrawlOptions) => boolean;
176
+ }): QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions, true>;
177
+ /**
178
+ * Creates a standalone factory whose queryFn returns an AsyncIterable, without a `reduce`
179
+ * function. Result is `TData[]` (one element per yielded page).
180
+ */
181
+ declare function queryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(config: StandardQueryOptions<TError, TData> & {
182
+ queryKey: QueryKey;
183
+ queryFn: (params: TParams, context: QueryFunctionContext<QueryKey, [
184
+ unknown
185
+ ] extends [TPageParam] ? never : TPageParam>) => AsyncIterable<TData>;
186
+ select?: (data: TData) => TSelected;
187
+ getNextPageParam?: GetNextPageParamFunction<TPageParam, TData>;
188
+ initialPageParam?: TPageParam;
189
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
190
+ reduce?: never;
191
+ shouldFetchNextPage: (combined: TSelected | undefined, crawlOptions: TCrawlOptions) => boolean;
192
+ }): QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions, false>;
159
193
 
160
194
  export { type QueryFactory, type QueryFactoryConfig, type ResolvedInfiniteOptions, type ResolvedQueryOptions, type StandardQueryOptions, queryFactory };
package/dist/index.d.ts CHANGED
@@ -156,5 +156,39 @@ declare function queryFactory<TChildParams extends TParentParams, TData = unknow
156
156
  reduce?: (accumulator: TChildSelected | undefined, page: TData) => TChildSelected;
157
157
  shouldFetchNextPage?: (combined: TParentHasReduce extends true ? TChildSelected : TChildSelected | undefined, crawlOptions: TChildCrawlOptions) => boolean;
158
158
  }): QueryFactory<TChildParams, TData, TError, TChildSelected, TPageParam, TChildCrawlOptions, TParentHasReduce>;
159
+ /**
160
+ * Creates a standalone factory whose queryFn returns an AsyncIterable (e.g. an AWS SDK v3
161
+ * paginator). The library drives the crawl with `for await...of`; `getNextPageParam` and
162
+ * `initialPageParam` are not required for `useQuery` mode. When `reduce` is present,
163
+ * `shouldFetchNextPage` receives `TSelected` (never undefined).
164
+ */
165
+ declare function queryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(config: StandardQueryOptions<TError, TData> & {
166
+ queryKey: QueryKey;
167
+ queryFn: (params: TParams, context: QueryFunctionContext<QueryKey, [
168
+ unknown
169
+ ] extends [TPageParam] ? never : TPageParam>) => AsyncIterable<TData>;
170
+ select?: (data: TData) => TSelected;
171
+ getNextPageParam?: GetNextPageParamFunction<TPageParam, TData>;
172
+ initialPageParam?: TPageParam;
173
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
174
+ reduce: (accumulator: TSelected | undefined, page: TData) => TSelected;
175
+ shouldFetchNextPage?: (combined: TSelected, crawlOptions: TCrawlOptions) => boolean;
176
+ }): QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions, true>;
177
+ /**
178
+ * Creates a standalone factory whose queryFn returns an AsyncIterable, without a `reduce`
179
+ * function. Result is `TData[]` (one element per yielded page).
180
+ */
181
+ declare function queryFactory<TParams = void, TData = unknown, TError = Error, TSelected = TData, TPageParam = unknown, TCrawlOptions extends Record<string, unknown> = Record<string, unknown>>(config: StandardQueryOptions<TError, TData> & {
182
+ queryKey: QueryKey;
183
+ queryFn: (params: TParams, context: QueryFunctionContext<QueryKey, [
184
+ unknown
185
+ ] extends [TPageParam] ? never : TPageParam>) => AsyncIterable<TData>;
186
+ select?: (data: TData) => TSelected;
187
+ getNextPageParam?: GetNextPageParamFunction<TPageParam, TData>;
188
+ initialPageParam?: TPageParam;
189
+ getPreviousPageParam?: GetPreviousPageParamFunction<TPageParam, TData>;
190
+ reduce?: never;
191
+ shouldFetchNextPage: (combined: TSelected | undefined, crawlOptions: TCrawlOptions) => boolean;
192
+ }): QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions, false>;
159
193
 
160
194
  export { type QueryFactory, type QueryFactoryConfig, type ResolvedInfiniteOptions, type ResolvedQueryOptions, type StandardQueryOptions, queryFactory };
package/dist/index.js CHANGED
@@ -28,6 +28,9 @@ module.exports = __toCommonJS(index_exports);
28
28
  var FACTORY_CONFIG = /* @__PURE__ */ Symbol("factoryConfig");
29
29
  var getEnvelopeNextPageParam = (envelope) => envelope.nextPageParam;
30
30
  var noNextPage = () => void 0;
31
+ function isAsyncIterable(value) {
32
+ return value != null && typeof value[Symbol.asyncIterator] === "function";
33
+ }
31
34
  function resolveKey(namespace, params, crawlOptions) {
32
35
  const withParams = params === void 0 ? namespace : [...namespace, params];
33
36
  if (!crawlOptions) return withParams;
@@ -55,31 +58,53 @@ function buildChildKey(parentKey, ownSegments, params, crawlOptions, infinite) {
55
58
  function wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, select) {
56
59
  return (lastPage, allPages, lastPageParam, allPageParams) => {
57
60
  const combined = select ? select(lastPage) : lastPage;
58
- if (!shouldFetchNextPage(combined, crawlOptions))
59
- return void 0;
61
+ if (!shouldFetchNextPage(combined, crawlOptions)) return void 0;
60
62
  return getNextPageParam(lastPage, allPages, lastPageParam, allPageParams);
61
63
  };
62
64
  }
63
65
  function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce) {
64
66
  return async (params, crawlOptions, context) => {
65
- var _a, _b;
67
+ var _a, _b, _c, _d;
68
+ if ((_a = context.signal) == null ? void 0 : _a.aborted) {
69
+ if (reduce) throw new DOMException("Aborted", "AbortError");
70
+ return [];
71
+ }
72
+ const ctx = { ...context, pageParam: initialPageParam };
73
+ const initialResult = queryFn(params, ctx);
74
+ if (isAsyncIterable(initialResult)) {
75
+ const pages2 = [];
76
+ let acc2 = void 0;
77
+ for await (const page of initialResult) {
78
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
79
+ pages2.push(page);
80
+ if (reduce) acc2 = reduce(acc2, page);
81
+ if (!shouldFetchNextPage(acc2, crawlOptions)) break;
82
+ }
83
+ if (reduce) {
84
+ if (acc2 === void 0) throw new DOMException("Aborted", "AbortError");
85
+ return acc2;
86
+ }
87
+ return pages2;
88
+ }
66
89
  const pages = [];
67
90
  const pageParams = [];
68
91
  let currentParam = initialPageParam;
69
92
  let acc = void 0;
70
- const ctx = { ...context, pageParam: currentParam };
93
+ let nextResult = initialResult;
71
94
  while (true) {
72
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
73
- ctx.pageParam = currentParam;
74
- const page = await queryFn(params, ctx);
95
+ const page = await nextResult;
75
96
  pages.push(page);
76
97
  pageParams.push(currentParam);
77
98
  if (reduce) acc = reduce(acc, page);
78
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
99
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
79
100
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
101
+ if (!getNextPageParam) break;
80
102
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
81
103
  if (nextParam == null) break;
82
104
  currentParam = nextParam;
105
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
106
+ ctx.pageParam = currentParam;
107
+ nextResult = queryFn(params, ctx);
83
108
  }
84
109
  if (reduce) {
85
110
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
@@ -90,26 +115,50 @@ function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shoul
90
115
  }
91
116
  function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNextPage, reduce) {
92
117
  return async (params, crawlOptions, context) => {
93
- var _a, _b;
118
+ var _a, _b, _c, _d;
119
+ if ((_a = context.signal) == null ? void 0 : _a.aborted)
120
+ throw new DOMException("Aborted", "AbortError");
121
+ const ctx = { ...context, pageParam: context.pageParam };
122
+ const initialResult = queryFn(params, ctx);
123
+ if (isAsyncIterable(initialResult)) {
124
+ const pages2 = [];
125
+ const startParam = context.pageParam;
126
+ let acc2 = void 0;
127
+ let nextBatchParam2 = null;
128
+ for await (const page of initialResult) {
129
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
130
+ pages2.push(page);
131
+ acc2 = reduce(acc2, page);
132
+ const nextParam = getNextPageParam(page, pages2, startParam, [
133
+ startParam
134
+ ]);
135
+ nextBatchParam2 = nextParam != null ? nextParam : null;
136
+ if (nextParam == null) break;
137
+ if (!shouldFetchNextPage(acc2, crawlOptions)) break;
138
+ }
139
+ if (acc2 === void 0) throw new DOMException("Aborted", "AbortError");
140
+ return { data: acc2, nextPageParam: nextBatchParam2 };
141
+ }
94
142
  const pages = [];
95
143
  const pageParams = [];
96
144
  let currentParam = context.pageParam;
97
145
  let acc = void 0;
98
146
  let nextBatchParam = null;
99
- const ctx = { ...context, pageParam: currentParam };
147
+ let nextResult = initialResult;
100
148
  while (true) {
101
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
102
- ctx.pageParam = currentParam;
103
- const page = await queryFn(params, ctx);
149
+ const page = await nextResult;
104
150
  pages.push(page);
105
151
  pageParams.push(currentParam);
106
152
  acc = reduce(acc, page);
107
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
108
153
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
109
154
  nextBatchParam = nextParam != null ? nextParam : null;
155
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
110
156
  if (nextParam == null) break;
111
157
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
112
158
  currentParam = nextParam;
159
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
160
+ ctx.pageParam = currentParam;
161
+ nextResult = queryFn(params, ctx);
113
162
  }
114
163
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
115
164
  return { data: acc, nextPageParam: nextBatchParam };
@@ -130,8 +179,8 @@ function buildFactory(cfg) {
130
179
  } = cfg;
131
180
  const ownSegments = parentKey !== void 0 ? namespace.slice(parentKey.length) : namespace;
132
181
  const infiniteNamespace = [...namespace, "infinite"];
133
- const hasCrawling = rawQueryFn !== void 0 && getNextPageParam !== void 0 && shouldFetchNextPage !== void 0;
134
- const hasInfiniteCrawling = hasCrawling && reduce !== void 0;
182
+ const hasCrawling = rawQueryFn !== void 0 && shouldFetchNextPage !== void 0;
183
+ const hasInfiniteCrawling = hasCrawling && reduce !== void 0 && getNextPageParam !== void 0;
135
184
  const crawlingFn = hasCrawling ? buildCrawlingQueryFn(
136
185
  rawQueryFn,
137
186
  getNextPageParam,
@@ -233,7 +282,7 @@ function queryFactory(configOrParent, childConfig) {
233
282
  } : {
234
283
  getNextPageParam: (_b = childConfig.getNextPageParam) != null ? _b : parentCfg.getNextPageParam,
235
284
  getPreviousPageParam: (_c = childConfig.getPreviousPageParam) != null ? _c : parentCfg.getPreviousPageParam,
236
- initialPageParam: childConfig.initialPageParam !== void 0 ? childConfig.initialPageParam : parentCfg.initialPageParam,
285
+ initialPageParam: "initialPageParam" in childConfig ? childConfig.initialPageParam : parentCfg.initialPageParam,
237
286
  shouldFetchNextPage: (_d = childConfig.shouldFetchNextPage) != null ? _d : parentCfg.shouldFetchNextPage,
238
287
  reduce: (_e = childConfig.reduce) != null ? _e : parentCfg.reduce
239
288
  };
package/dist/index.mjs CHANGED
@@ -2,6 +2,9 @@
2
2
  var FACTORY_CONFIG = /* @__PURE__ */ Symbol("factoryConfig");
3
3
  var getEnvelopeNextPageParam = (envelope) => envelope.nextPageParam;
4
4
  var noNextPage = () => void 0;
5
+ function isAsyncIterable(value) {
6
+ return value != null && typeof value[Symbol.asyncIterator] === "function";
7
+ }
5
8
  function resolveKey(namespace, params, crawlOptions) {
6
9
  const withParams = params === void 0 ? namespace : [...namespace, params];
7
10
  if (!crawlOptions) return withParams;
@@ -29,31 +32,53 @@ function buildChildKey(parentKey, ownSegments, params, crawlOptions, infinite) {
29
32
  function wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, select) {
30
33
  return (lastPage, allPages, lastPageParam, allPageParams) => {
31
34
  const combined = select ? select(lastPage) : lastPage;
32
- if (!shouldFetchNextPage(combined, crawlOptions))
33
- return void 0;
35
+ if (!shouldFetchNextPage(combined, crawlOptions)) return void 0;
34
36
  return getNextPageParam(lastPage, allPages, lastPageParam, allPageParams);
35
37
  };
36
38
  }
37
39
  function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce) {
38
40
  return async (params, crawlOptions, context) => {
39
- var _a, _b;
41
+ var _a, _b, _c, _d;
42
+ if ((_a = context.signal) == null ? void 0 : _a.aborted) {
43
+ if (reduce) throw new DOMException("Aborted", "AbortError");
44
+ return [];
45
+ }
46
+ const ctx = { ...context, pageParam: initialPageParam };
47
+ const initialResult = queryFn(params, ctx);
48
+ if (isAsyncIterable(initialResult)) {
49
+ const pages2 = [];
50
+ let acc2 = void 0;
51
+ for await (const page of initialResult) {
52
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
53
+ pages2.push(page);
54
+ if (reduce) acc2 = reduce(acc2, page);
55
+ if (!shouldFetchNextPage(acc2, crawlOptions)) break;
56
+ }
57
+ if (reduce) {
58
+ if (acc2 === void 0) throw new DOMException("Aborted", "AbortError");
59
+ return acc2;
60
+ }
61
+ return pages2;
62
+ }
40
63
  const pages = [];
41
64
  const pageParams = [];
42
65
  let currentParam = initialPageParam;
43
66
  let acc = void 0;
44
- const ctx = { ...context, pageParam: currentParam };
67
+ let nextResult = initialResult;
45
68
  while (true) {
46
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
47
- ctx.pageParam = currentParam;
48
- const page = await queryFn(params, ctx);
69
+ const page = await nextResult;
49
70
  pages.push(page);
50
71
  pageParams.push(currentParam);
51
72
  if (reduce) acc = reduce(acc, page);
52
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
73
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
53
74
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
75
+ if (!getNextPageParam) break;
54
76
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
55
77
  if (nextParam == null) break;
56
78
  currentParam = nextParam;
79
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
80
+ ctx.pageParam = currentParam;
81
+ nextResult = queryFn(params, ctx);
57
82
  }
58
83
  if (reduce) {
59
84
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
@@ -64,26 +89,50 @@ function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shoul
64
89
  }
65
90
  function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNextPage, reduce) {
66
91
  return async (params, crawlOptions, context) => {
67
- var _a, _b;
92
+ var _a, _b, _c, _d;
93
+ if ((_a = context.signal) == null ? void 0 : _a.aborted)
94
+ throw new DOMException("Aborted", "AbortError");
95
+ const ctx = { ...context, pageParam: context.pageParam };
96
+ const initialResult = queryFn(params, ctx);
97
+ if (isAsyncIterable(initialResult)) {
98
+ const pages2 = [];
99
+ const startParam = context.pageParam;
100
+ let acc2 = void 0;
101
+ let nextBatchParam2 = null;
102
+ for await (const page of initialResult) {
103
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
104
+ pages2.push(page);
105
+ acc2 = reduce(acc2, page);
106
+ const nextParam = getNextPageParam(page, pages2, startParam, [
107
+ startParam
108
+ ]);
109
+ nextBatchParam2 = nextParam != null ? nextParam : null;
110
+ if (nextParam == null) break;
111
+ if (!shouldFetchNextPage(acc2, crawlOptions)) break;
112
+ }
113
+ if (acc2 === void 0) throw new DOMException("Aborted", "AbortError");
114
+ return { data: acc2, nextPageParam: nextBatchParam2 };
115
+ }
68
116
  const pages = [];
69
117
  const pageParams = [];
70
118
  let currentParam = context.pageParam;
71
119
  let acc = void 0;
72
120
  let nextBatchParam = null;
73
- const ctx = { ...context, pageParam: currentParam };
121
+ let nextResult = initialResult;
74
122
  while (true) {
75
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
76
- ctx.pageParam = currentParam;
77
- const page = await queryFn(params, ctx);
123
+ const page = await nextResult;
78
124
  pages.push(page);
79
125
  pageParams.push(currentParam);
80
126
  acc = reduce(acc, page);
81
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
82
127
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
83
128
  nextBatchParam = nextParam != null ? nextParam : null;
129
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
84
130
  if (nextParam == null) break;
85
131
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
86
132
  currentParam = nextParam;
133
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
134
+ ctx.pageParam = currentParam;
135
+ nextResult = queryFn(params, ctx);
87
136
  }
88
137
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
89
138
  return { data: acc, nextPageParam: nextBatchParam };
@@ -104,8 +153,8 @@ function buildFactory(cfg) {
104
153
  } = cfg;
105
154
  const ownSegments = parentKey !== void 0 ? namespace.slice(parentKey.length) : namespace;
106
155
  const infiniteNamespace = [...namespace, "infinite"];
107
- const hasCrawling = rawQueryFn !== void 0 && getNextPageParam !== void 0 && shouldFetchNextPage !== void 0;
108
- const hasInfiniteCrawling = hasCrawling && reduce !== void 0;
156
+ const hasCrawling = rawQueryFn !== void 0 && shouldFetchNextPage !== void 0;
157
+ const hasInfiniteCrawling = hasCrawling && reduce !== void 0 && getNextPageParam !== void 0;
109
158
  const crawlingFn = hasCrawling ? buildCrawlingQueryFn(
110
159
  rawQueryFn,
111
160
  getNextPageParam,
@@ -207,7 +256,7 @@ function queryFactory(configOrParent, childConfig) {
207
256
  } : {
208
257
  getNextPageParam: (_b = childConfig.getNextPageParam) != null ? _b : parentCfg.getNextPageParam,
209
258
  getPreviousPageParam: (_c = childConfig.getPreviousPageParam) != null ? _c : parentCfg.getPreviousPageParam,
210
- initialPageParam: childConfig.initialPageParam !== void 0 ? childConfig.initialPageParam : parentCfg.initialPageParam,
259
+ initialPageParam: "initialPageParam" in childConfig ? childConfig.initialPageParam : parentCfg.initialPageParam,
211
260
  shouldFetchNextPage: (_d = childConfig.shouldFetchNextPage) != null ? _d : parentCfg.shouldFetchNextPage,
212
261
  reduce: (_e = childConfig.reduce) != null ? _e : parentCfg.reduce
213
262
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robohall/react-query-factory",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "A factory abstraction for TanStack Query (React Query) with composable keys, crawling support, and automatic infinite query generation",
5
5
  "author": "Robert Hall",
6
6
  "license": "MIT",
@@ -35,13 +35,15 @@
35
35
  "test": "vitest run",
36
36
  "test:watch": "vitest",
37
37
  "prepublishOnly": "npm run build && npm test",
38
- "sandbox": "cd sandbox && npm run dev"
38
+ "sandbox": "cd sandbox && npm run dev",
39
+ "format": "prettier --write ."
39
40
  },
40
41
  "peerDependencies": {
41
42
  "@tanstack/react-query": ">=5.0.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@tanstack/react-query": "^5.0.0",
46
+ "prettier": "^3.8.3",
45
47
  "tsup": "^8.0.0",
46
48
  "typescript": "^5.0.0",
47
49
  "vitest": "^2.0.0"