@robohall/react-query-factory 1.2.0 → 2.1.0

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,108 +1,274 @@
1
1
  # @robohall/react-query-factory
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@robohall/react-query-factory)](https://www.npmjs.com/package/@robohall/react-query-factory)
4
- [![bundle size](https://img.shields.io/bundlephobia/minzip/@robohall/react-query-factory)](https://bundlephobia.com/package/@robohall/react-query-factory)
4
+ ![minified](https://img.shields.io/badge/minified-%3C_12_kB-blue)
5
+ ![gzipped](https://img.shields.io/badge/gzipped-%3C_3_kB-blue)
5
6
  [![license](https://img.shields.io/npm/l/@robohall/react-query-factory)](./LICENSE)
6
7
 
7
- 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>
8
11
 
9
- 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 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.
10
15
 
11
16
  ---
12
17
 
13
- ## Installation
18
+ ## The problem
14
19
 
15
- ```bash
16
- npm install @robohall/react-query-factory
17
- # 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
+ }
18
31
  ```
19
32
 
20
- ---
33
+ Works, but the key only exists inside the hook. Prefetching in a route loader, invalidating after a mutation, or fetching imperatively all require knowing `['instances', params]` without calling the hook.
21
34
 
22
- ## Quick start
35
+ ### Step 2 — `queryOptions` for colocation
23
36
 
24
- Define a factory once, call it in any component:
37
+ TanStack's `queryOptions` helper moves the key and fn into a shared object:
25
38
 
26
39
  ```typescript
27
- import {
28
- EC2Client,
29
- DescribeInstancesCommand,
30
- type DescribeInstancesCommandInput,
31
- } from '@aws-sdk/client-ec2';
32
- import { queryFactory } from '@robohall/react-query-factory';
33
- import { useQuery } from '@tanstack/react-query';
40
+ const instancesOptions = (params: DescribeInstancesCommandInput) =>
41
+ queryOptions({
42
+ queryKey: ['instances', params],
43
+ queryFn: () => fetchInstances(params),
44
+ });
45
+
46
+ useQuery(instancesOptions(params));
47
+ queryClient.prefetchQuery(instancesOptions(params));
48
+ queryClient.invalidateQueries(instancesOptions(params));
49
+ ```
34
50
 
35
- const ec2 = new EC2Client({ region: 'us-east-1' });
51
+ This is genuinely good this library builds on the same pattern. But once you need multiple related queries, the cracks show.
36
52
 
37
- const describeInstances = queryFactory({
38
- queryKey: ['ec2:DescribeInstances'],
39
- queryFn: (params: DescribeInstancesCommandInput, ctx) =>
40
- ec2.send(new DescribeInstancesCommand(params), { abortSignal: ctx.signal }),
53
+ ### Step 3 — derived queries and key coordination
54
+
55
+ 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`:
56
+
57
+ ```typescript
58
+ const { data: running } = useQuery({
59
+ ...instancesOptions(params),
60
+ select: data => data.filter(i => i.state === 'running'),
41
61
  });
62
+ ```
42
63
 
43
- function InstanceList() {
44
- const { data } = useQuery(
45
- describeInstances({ Filters: [{ Name: 'instance-state-name', Values: ['running'] }] })
46
- );
47
- // query key: ['ec2:DescribeInstances', { Filters: [...] }]
48
- }
64
+ 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:
65
+
66
+ ```typescript
67
+ // If the key structure ever changes, every site breaks.
68
+ queryClient.invalidateQueries({ queryKey: ['instances'] });
49
69
  ```
50
70
 
51
- `describeInstances({ ... })` returns a plain object `{ queryKey, queryFn, staleTime, … }` — that you spread or pass directly to `useQuery`. The factory does not touch your query client.
71
+ ### Step 4paginated APIs
52
72
 
53
- ---
73
+ `DescribeInstances` returns at most `MaxResults` instances per call. To get them all, you need to loop. The usual options:
54
74
 
55
- ## Crawling
75
+ **Put the loop in `queryFn`:**
76
+
77
+ ```typescript
78
+ const instancesOptions = params =>
79
+ queryOptions({
80
+ queryKey: ['instances', params],
81
+ queryFn: async () => {
82
+ let all: Instance[] = [];
83
+ let nextToken: string | undefined;
84
+ do {
85
+ const page = await ec2.send(
86
+ new DescribeInstancesCommand({ ...params, NextToken: nextToken }),
87
+ );
88
+ all = [
89
+ ...all,
90
+ ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
91
+ ];
92
+ nextToken = page.NextToken;
93
+ } while (nextToken);
94
+ return all;
95
+ },
96
+ });
97
+ ```
98
+
99
+ 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.
100
+
101
+ **Use `useInfiniteQuery`:**
56
102
 
57
- `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.
103
+ ```typescript
104
+ const instancesInfiniteOptions = params =>
105
+ infiniteQueryOptions({
106
+ queryKey: ['instances', 'infinite', params],
107
+ queryFn: ({ pageParam }) =>
108
+ ec2.send(
109
+ new DescribeInstancesCommand({ ...params, NextToken: pageParam }),
110
+ ),
111
+ getNextPageParam: r => r.NextToken,
112
+ initialPageParam: undefined,
113
+ });
114
+
115
+ // Caller still has to flatten, auto-advance, manage hasNextPage...
116
+ const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
117
+ instancesInfiniteOptions(params),
118
+ );
119
+ const allInstances = data?.pages.flatMap(
120
+ page => page.Reservations?.flatMap(r => r.Instances ?? []) ?? [],
121
+ );
122
+ ```
123
+
124
+ 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.
58
125
 
59
- 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:
126
+ ### What's missing
127
+
128
+ - Define the query **once**: key, queryFn, pagination config
129
+ - Let each **call site** decide how much to crawl (e.g. 50 records or all of them)
130
+ - Optionally have `useQuery` crawl and return the **accumulated result** instead of a single page
131
+ - Use **async iterables** as `queryFn` — pass a paginator function directly, no cursor wiring required
132
+ - Have `.infinite()` available on the **same factory**, no duplication
133
+ - Have derived queries **share the cache entry** automatically
134
+ - Have **scoped invalidation** through key composition — bust the whole namespace or just one param set and its children
135
+
136
+ ---
137
+
138
+ ## The solution
60
139
 
61
140
  ```typescript
62
- import type { Instance, DescribeInstancesCommandInput } from '@aws-sdk/client-ec2';
141
+ import { queryFactory } from '@robohall/react-query-factory';
63
142
 
64
143
  const describeInstances = queryFactory({
65
144
  queryKey: ['ec2:DescribeInstances'],
66
145
  queryFn: (params: DescribeInstancesCommandInput, ctx) =>
67
146
  ec2.send(
68
147
  new DescribeInstancesCommand({ ...params, NextToken: ctx.pageParam }),
69
- { abortSignal: ctx.signal },
148
+ {
149
+ abortSignal: ctx.signal,
150
+ },
70
151
  ),
71
- getNextPageParam: response => response.NextToken,
152
+ getNextPageParam: r => r.NextToken,
72
153
  initialPageParam: undefined as string | undefined,
73
- shouldFetchNextPage: () => true,
74
154
  reduce: (acc, page): Instance[] => [
75
155
  ...(acc ?? []),
76
156
  ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
77
157
  ],
158
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
159
+ opts.minResults == null || instances.length < opts.minResults,
78
160
  });
79
161
 
80
- function InstanceList() {
81
- // one useQuery call; data is Instance[], not DescribeInstancesResponse[]
82
- const { data } = useQuery(describeInstances({ MaxResults: 20 }));
83
- }
162
+ // useQuery — crawls all pages, data is Instance[]
163
+ const { data } = useQuery(describeInstances({ MaxResults: 20 }));
164
+
165
+ // Stop at 50 — separate cache entry, independent crawl
166
+ const { data } = useQuery(
167
+ describeInstances({ MaxResults: 20 }, { minResults: 50 }),
168
+ );
169
+
170
+ // UI-driven pagination — same factory, no duplication
171
+ const { data, fetchNextPage } = useInfiniteQuery(
172
+ describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 }),
173
+ );
174
+
175
+ // Derived view — shares the cache entry, no extra API call
176
+ const runningInstances = queryFactory(describeInstances, {
177
+ select: instances => instances.filter(i => i.State?.Name === 'running'),
178
+ });
179
+ const { data: running } = useQuery(runningInstances({ MaxResults: 20 }));
180
+
181
+ // Prefetch in a route loader
182
+ await queryClient.prefetchQuery(describeInstances({ MaxResults: 20 }));
183
+
184
+ // Bust everything in the namespace
185
+ queryClient.invalidateQueries(describeInstances());
186
+
187
+ // Bust only this param set — cascades to runningInstances and any other child
188
+ queryClient.invalidateQueries(describeInstances({ MaxResults: 20 }));
189
+ ```
190
+
191
+ `describeInstances({ ... })` returns a plain `{ queryKey, queryFn, ... }` object — pass it directly to `useQuery`, `useInfiniteQuery`, `prefetchQuery`, or `getQueryData`. The factory doesn't touch your query client.
192
+
193
+ ---
194
+
195
+ ## Installation
196
+
197
+ ```bash
198
+ npm install @robohall/react-query-factory
199
+ # peer dependency: @tanstack/react-query >= 5.0.0
84
200
  ```
85
201
 
86
- `shouldFetchNextPage` also accepts a `crawlOptions` object passed at call time, letting each call site control the crawl independently:
202
+ ---
203
+
204
+ ## Crawling
205
+
206
+ `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[]`).
207
+
208
+ 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:
87
209
 
88
210
  ```typescript
89
211
  const describeInstances = queryFactory({
90
212
  // ...
91
- reduce: (acc, page): Instance[] => [...(acc ?? []), ...page.Reservations.flatMap(r => r.Instances)],
92
213
  shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
93
214
  opts.minResults == null || instances.length < opts.minResults,
94
215
  });
95
216
 
96
- // fetch all pages
217
+ // two separate cache entries — crawl independently
97
218
  const { data: all } = useQuery(describeInstances({ MaxResults: 20 }));
98
-
99
- // stop after accumulating at least 50 instances (≥ 3 API calls)
100
219
  const { data: partial } = useQuery(
101
- describeInstances({ MaxResults: 20 }, { minResults: 50 })
220
+ describeInstances({ MaxResults: 20 }, { minResults: 50 }),
102
221
  );
103
222
  ```
104
223
 
105
- `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.
224
+ ---
225
+
226
+ ## Async iterator queryFns
227
+
228
+ 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:
229
+
230
+ ```typescript
231
+ import { paginateDescribeInstances } from '@aws-sdk/client-ec2';
232
+
233
+ const describeInstances = queryFactory({
234
+ queryKey: ['ec2:DescribeInstances'],
235
+ queryFn: (params: DescribeInstancesCommandInput) =>
236
+ paginateDescribeInstances({ client: ec2 }, params),
237
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
238
+ opts.minResults == null || instances.length < opts.minResults,
239
+ reduce: (acc, page: DescribeInstancesResponse): Instance[] => [
240
+ ...(acc ?? []),
241
+ ...(page.Reservations?.flatMap(r => r.Instances ?? []) ?? []),
242
+ ],
243
+ });
244
+ ```
245
+
246
+ `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.
247
+
248
+ 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:
249
+
250
+ ```typescript
251
+ const describeInstances = queryFactory({
252
+ queryKey: ['ec2:DescribeInstances'],
253
+ queryFn: (params: DescribeInstancesCommandInput, ctx) =>
254
+ paginateDescribeInstances(
255
+ { client: ec2 },
256
+ { ...params, StartingToken: ctx.pageParam },
257
+ ),
258
+ getNextPageParam: page => page.NextToken,
259
+ initialPageParam: undefined as string | undefined,
260
+ shouldFetchNextPage: (instances, opts: { minResults?: number }) =>
261
+ opts.minResults == null || instances.length < opts.minResults,
262
+ reduce: (acc, page): Instance[] => [
263
+ ...(acc ?? []),
264
+ ...(page.Instances ?? []),
265
+ ],
266
+ });
267
+
268
+ const { data, fetchNextPage } = useInfiniteQuery(
269
+ describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 }),
270
+ );
271
+ ```
106
272
 
107
273
  ---
108
274
 
@@ -135,22 +301,41 @@ const findInstance = queryFactory(describeInstances, {
135
301
  !instances.some(i => i.InstanceId === opts.instanceId),
136
302
  });
137
303
 
138
- // query key: ['ec2:DescribeInstances', 'find', { MaxResults: 20 }, { instanceId: 'i-0abc123def456' }]
304
+ // query key: ['ec2:DescribeInstances', { MaxResults: 20 }, 'find', { instanceId: 'i-0abc123def456' }]
139
305
  // crawls pages until the target instance appears, then stops
140
306
  const { data } = useQuery(
141
- findInstance({ MaxResults: 20 }, { instanceId: 'i-0abc123def456' })
307
+ findInstance({ MaxResults: 20 }, { instanceId: 'i-0abc123def456' }),
142
308
  );
143
309
  ```
144
310
 
145
- Because `findInstance`'s key is nested under `['ec2:DescribeInstances']`, a single invalidation call busts the parent and all children:
311
+ **Invalidation broad and scoped:**
312
+
313
+ Child keys follow the ordering `[...parentNS, params, ...childSegments]`, which means the parent key for a given set of params is always a strict prefix of every child key for those same params:
314
+
315
+ ```
316
+ describeInstances({ MaxResults: 20 })
317
+ → ['ec2:DescribeInstances', { MaxResults: 20 }]
318
+
319
+ runningInstances({ MaxResults: 20 }) // select child, no own segments
320
+ → ['ec2:DescribeInstances', { MaxResults: 20 }] (same entry — select is not in the key)
321
+
322
+ findInstance({ MaxResults: 20 }, { instanceId: 'i-abc' })
323
+ → ['ec2:DescribeInstances', { MaxResults: 20 }, 'find', { instanceId: 'i-abc' }]
324
+ // └── params ──────┘ └── own segs ──────────────────┘
325
+ ```
326
+
327
+ This unlocks two invalidation granularities with no extra bookkeeping:
146
328
 
147
329
  ```typescript
148
- // after a runInstances/terminateInstances mutationinvalidates everything in the namespace.
149
- // Calling the factory with no args produces just the namespace key; TanStack prefix-matches it
150
- // against all entries, so describeInstances, runningInstances, and findInstance are all busted.
330
+ // Broad: zero-arg returns the namespace busts every variant, every param set
151
331
  await queryClient.invalidateQueries(describeInstances());
332
+
333
+ // Scoped: parent call with params — busts the parent and every child for those params only
334
+ await queryClient.invalidateQueries(describeInstances({ MaxResults: 20 }));
152
335
  ```
153
336
 
337
+ The scoped form is particularly useful after a targeted mutation: invalidate the one resource that changed without touching unrelated cache entries.
338
+
154
339
  ---
155
340
 
156
341
  ## Infinite queries
@@ -160,13 +345,14 @@ Every factory exposes a `.infinite()` method that returns `useInfiniteQuery`-com
160
345
  ```typescript
161
346
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
162
347
  // load 50 instances per UI page, each backed by up to 5 DescribeInstances calls
163
- describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 })
348
+ describeInstances.infinite({ MaxResults: 20 }, { minResults: 50 }),
164
349
  );
165
350
 
166
351
  // data.pages is Instance[][], one array per virtual page
167
352
  ```
168
353
 
169
354
  The `.infinite()` key includes an `'infinite'` segment to keep it separate from the regular `useQuery` cache entry:
355
+
170
356
  - `describeInstances({ MaxResults: 20 })` → `['ec2:DescribeInstances', { MaxResults: 20 }]`
171
357
  - `describeInstances.infinite({ MaxResults: 20 })` → `['ec2:DescribeInstances', 'infinite', { MaxResults: 20 }]`
172
358
 
@@ -187,6 +373,7 @@ queryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>(
187
373
  ### `queryFactory(parent, config)`
188
374
 
189
375
  Creates a child factory. Two overloads:
376
+
190
377
  - **With a new `queryFn`** — inherits key namespace and standard options; crawling config must be re-declared if needed.
191
378
  - **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.
192
379
 
@@ -194,17 +381,17 @@ Creates a child factory. Two overloads:
194
381
 
195
382
  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.
196
383
 
197
- | Field | Type | Notes |
198
- |---|---|---|
199
- | `queryKey` | `QueryKey` | Namespace segments. Params are appended at call time. |
200
- | `queryFn` | `(params: TParams, ctx: QueryFunctionContext) => TData \| Promise<TData>` | Same as TanStack, with an extra leading `params` argument. |
201
- | `select` | `(data: TData) => TSelected` | Exact TanStack API. Composed automatically on child factories. |
202
- | `getNextPageParam` | `GetNextPageParamFunction<TPageParam, TData>` | Exact TanStack API. Required (with `shouldFetchNextPage`) to activate crawling. Required (with `initialPageParam`) for `.infinite()`. |
203
- | `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. |
204
- | `getPreviousPageParam` | `GetPreviousPageParamFunction<TPageParam, TData>` | Exact TanStack API. Passed through on `.infinite()`. |
205
- | `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. |
206
- | `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[]`). |
207
- | + 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. |
384
+ | Field | Type | Notes |
385
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
386
+ | `queryKey` | `QueryKey` | Namespace segments. Params are appended at call time. |
387
+ | `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. |
388
+ | `select` | `(data: TData) => TSelected` | Exact TanStack API. Composed automatically on child factories. |
389
+ | `getNextPageParam` | `GetNextPageParamFunction<TPageParam, TData>` | Exact TanStack API. Required (with `shouldFetchNextPage`) to activate cursor-based crawling. Required (with `initialPageParam`) for `.infinite()`. |
390
+ | `initialPageParam` | `TPageParam` | Exact TanStack API. Drives `TPageParam` inference. Required for `.infinite()` to work at runtime. |
391
+ | `getPreviousPageParam` | `GetPreviousPageParamFunction<TPageParam, TData>` | Exact TanStack API. Passed through on `.infinite()`. |
392
+ | `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. |
393
+ | `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[]`). |
394
+ | + all `StandardQueryOptions` fields | | `staleTime`, `gcTime`, `retry`, `enabled`, `refetchOnWindowFocus`, `placeholderData`, `initialData`, `meta`, etc. Function-form callbacks are supported wherever TanStack accepts them. |
208
395
 
209
396
  ### `QueryFactory<TParams, TData, TError, TSelected, TPageParam, TCrawlOptions>`
210
397
 
@@ -227,10 +414,8 @@ Return type of `factory.infinite(params)`. Pass directly to `useInfiniteQuery()`
227
414
 
228
415
  ## Running the sandbox
229
416
 
230
- 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.
231
-
232
417
  ```bash
233
418
  npm run sandbox
234
419
  ```
235
420
 
236
- This starts a Vite dev server. Navigate to the URL it prints (typically `http://localhost:5173`).
421
+ 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,9 +28,11 @@ 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
- const base = Array.isArray(namespace) ? namespace : [namespace];
33
- const withParams = params === void 0 ? base : [...base, params];
35
+ const withParams = params === void 0 ? namespace : [...namespace, params];
34
36
  if (!crawlOptions) return withParams;
35
37
  let defined;
36
38
  for (const key in crawlOptions) {
@@ -40,34 +42,69 @@ function resolveKey(namespace, params, crawlOptions) {
40
42
  }
41
43
  return defined ? [...withParams, defined] : withParams;
42
44
  }
45
+ function buildChildKey(parentKey, ownSegments, params, crawlOptions, infinite) {
46
+ let defined;
47
+ for (const key in crawlOptions) {
48
+ if (crawlOptions[key] !== void 0) {
49
+ (defined != null ? defined : defined = {})[key] = crawlOptions[key];
50
+ }
51
+ }
52
+ if (params === void 0 && !defined) return [...parentKey];
53
+ const withParams = params !== void 0 ? [...parentKey, params] : [...parentKey];
54
+ const withOwn = [...withParams, ...ownSegments];
55
+ const withInfinite = infinite ? [...withOwn, "infinite"] : withOwn;
56
+ return defined ? [...withInfinite, defined] : withInfinite;
57
+ }
43
58
  function wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, select) {
44
59
  return (lastPage, allPages, lastPageParam, allPageParams) => {
45
60
  const combined = select ? select(lastPage) : lastPage;
46
- if (!shouldFetchNextPage(combined, crawlOptions))
47
- return void 0;
61
+ if (!shouldFetchNextPage(combined, crawlOptions)) return void 0;
48
62
  return getNextPageParam(lastPage, allPages, lastPageParam, allPageParams);
49
63
  };
50
64
  }
51
65
  function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce) {
52
66
  return async (params, crawlOptions, context) => {
53
- 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
+ }
54
89
  const pages = [];
55
90
  const pageParams = [];
56
91
  let currentParam = initialPageParam;
57
92
  let acc = void 0;
58
- const ctx = { ...context, pageParam: currentParam };
93
+ let nextResult = initialResult;
59
94
  while (true) {
60
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
61
- ctx.pageParam = currentParam;
62
- const page = await queryFn(params, ctx);
95
+ const page = await nextResult;
63
96
  pages.push(page);
64
97
  pageParams.push(currentParam);
65
98
  if (reduce) acc = reduce(acc, page);
66
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
99
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
67
100
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
101
+ if (!getNextPageParam) break;
68
102
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
69
103
  if (nextParam == null) break;
70
104
  currentParam = nextParam;
105
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
106
+ ctx.pageParam = currentParam;
107
+ nextResult = queryFn(params, ctx);
71
108
  }
72
109
  if (reduce) {
73
110
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
@@ -78,26 +115,47 @@ function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shoul
78
115
  }
79
116
  function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNextPage, reduce) {
80
117
  return async (params, crawlOptions, context) => {
81
- 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
+ let acc2 = void 0;
126
+ let nextBatchParam2 = null;
127
+ for await (const page of initialResult) {
128
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
129
+ pages2.push(page);
130
+ acc2 = reduce(acc2, page);
131
+ const nextParam = getNextPageParam(page, pages2, void 0, []);
132
+ nextBatchParam2 = nextParam != null ? nextParam : null;
133
+ if (nextParam == null) break;
134
+ if (!shouldFetchNextPage(acc2, crawlOptions)) break;
135
+ }
136
+ if (acc2 === void 0) throw new DOMException("Aborted", "AbortError");
137
+ return { data: acc2, nextPageParam: nextBatchParam2 };
138
+ }
82
139
  const pages = [];
83
140
  const pageParams = [];
84
141
  let currentParam = context.pageParam;
85
142
  let acc = void 0;
86
143
  let nextBatchParam = null;
87
- const ctx = { ...context, pageParam: currentParam };
144
+ let nextResult = initialResult;
88
145
  while (true) {
89
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
90
- ctx.pageParam = currentParam;
91
- const page = await queryFn(params, ctx);
146
+ const page = await nextResult;
92
147
  pages.push(page);
93
148
  pageParams.push(currentParam);
94
149
  acc = reduce(acc, page);
95
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
150
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
96
151
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
97
152
  nextBatchParam = nextParam != null ? nextParam : null;
98
153
  if (nextParam == null) break;
99
154
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
100
155
  currentParam = nextParam;
156
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
157
+ ctx.pageParam = currentParam;
158
+ nextResult = queryFn(params, ctx);
101
159
  }
102
160
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
103
161
  return { data: acc, nextPageParam: nextBatchParam };
@@ -106,6 +164,7 @@ function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNext
106
164
  function buildFactory(cfg) {
107
165
  const {
108
166
  queryKey: namespace,
167
+ parentKey,
109
168
  queryFn: rawQueryFn,
110
169
  select,
111
170
  getNextPageParam,
@@ -115,8 +174,10 @@ function buildFactory(cfg) {
115
174
  reduce,
116
175
  standardOptions
117
176
  } = cfg;
118
- const hasCrawling = rawQueryFn !== void 0 && getNextPageParam !== void 0 && shouldFetchNextPage !== void 0;
119
- const hasInfiniteCrawling = hasCrawling && reduce !== void 0;
177
+ const ownSegments = parentKey !== void 0 ? namespace.slice(parentKey.length) : namespace;
178
+ const infiniteNamespace = [...namespace, "infinite"];
179
+ const hasCrawling = rawQueryFn !== void 0 && shouldFetchNextPage !== void 0;
180
+ const hasInfiniteCrawling = hasCrawling && reduce !== void 0 && getNextPageParam !== void 0;
120
181
  const crawlingFn = hasCrawling ? buildCrawlingQueryFn(
121
182
  rawQueryFn,
122
183
  getNextPageParam,
@@ -130,7 +191,6 @@ function buildFactory(cfg) {
130
191
  shouldFetchNextPage,
131
192
  reduce
132
193
  ) : void 0;
133
- const infiniteNamespace = [...resolveKey(namespace, void 0), "infinite"];
134
194
  const envelopeSelect = infiniteCrawlingFn ? (data) => ({
135
195
  ...data,
136
196
  pages: data.pages.map((e) => select ? select(e.data) : e.data)
@@ -140,7 +200,7 @@ function buildFactory(cfg) {
140
200
  pages: data.pages.map(select)
141
201
  }) : void 0;
142
202
  const factory = function(params, crawlOptions = {}) {
143
- const queryKey = resolveKey(namespace, params, crawlOptions);
203
+ const queryKey = parentKey !== void 0 ? buildChildKey(parentKey, ownSegments, params, crawlOptions) : resolveKey(namespace, params, crawlOptions);
144
204
  const resolvedQueryFn = crawlingFn ? (ctx) => crawlingFn(params, crawlOptions, ctx) : rawQueryFn ? (ctx) => rawQueryFn(params, ctx) : void 0;
145
205
  return {
146
206
  ...standardOptions,
@@ -151,7 +211,7 @@ function buildFactory(cfg) {
151
211
  };
152
212
  };
153
213
  factory.infinite = function(params, crawlOptions = {}) {
154
- const queryKey = resolveKey(infiniteNamespace, params, crawlOptions);
214
+ const queryKey = parentKey !== void 0 ? buildChildKey(parentKey, ownSegments, params, crawlOptions, true) : resolveKey(infiniteNamespace, params, crawlOptions);
155
215
  if (infiniteCrawlingFn) {
156
216
  return {
157
217
  ...standardOptions,
@@ -236,6 +296,7 @@ function queryFactory(configOrParent, childConfig) {
236
296
  } = childConfig;
237
297
  return buildFactory({
238
298
  queryKey: composedNamespace,
299
+ parentKey: parentCfg.queryKey,
239
300
  queryFn: hasNewQueryFn ? childConfig.queryFn : parentCfg.queryFn,
240
301
  select: resolvedSelect,
241
302
  ...crawling,
package/dist/index.mjs CHANGED
@@ -2,9 +2,11 @@
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
- const base = Array.isArray(namespace) ? namespace : [namespace];
7
- const withParams = params === void 0 ? base : [...base, params];
9
+ const withParams = params === void 0 ? namespace : [...namespace, params];
8
10
  if (!crawlOptions) return withParams;
9
11
  let defined;
10
12
  for (const key in crawlOptions) {
@@ -14,34 +16,69 @@ function resolveKey(namespace, params, crawlOptions) {
14
16
  }
15
17
  return defined ? [...withParams, defined] : withParams;
16
18
  }
19
+ function buildChildKey(parentKey, ownSegments, params, crawlOptions, infinite) {
20
+ let defined;
21
+ for (const key in crawlOptions) {
22
+ if (crawlOptions[key] !== void 0) {
23
+ (defined != null ? defined : defined = {})[key] = crawlOptions[key];
24
+ }
25
+ }
26
+ if (params === void 0 && !defined) return [...parentKey];
27
+ const withParams = params !== void 0 ? [...parentKey, params] : [...parentKey];
28
+ const withOwn = [...withParams, ...ownSegments];
29
+ const withInfinite = infinite ? [...withOwn, "infinite"] : withOwn;
30
+ return defined ? [...withInfinite, defined] : withInfinite;
31
+ }
17
32
  function wrapGetNextPageParam(getNextPageParam, shouldFetchNextPage, crawlOptions, select) {
18
33
  return (lastPage, allPages, lastPageParam, allPageParams) => {
19
34
  const combined = select ? select(lastPage) : lastPage;
20
- if (!shouldFetchNextPage(combined, crawlOptions))
21
- return void 0;
35
+ if (!shouldFetchNextPage(combined, crawlOptions)) return void 0;
22
36
  return getNextPageParam(lastPage, allPages, lastPageParam, allPageParams);
23
37
  };
24
38
  }
25
39
  function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shouldFetchNextPage, reduce) {
26
40
  return async (params, crawlOptions, context) => {
27
- 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
+ }
28
63
  const pages = [];
29
64
  const pageParams = [];
30
65
  let currentParam = initialPageParam;
31
66
  let acc = void 0;
32
- const ctx = { ...context, pageParam: currentParam };
67
+ let nextResult = initialResult;
33
68
  while (true) {
34
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
35
- ctx.pageParam = currentParam;
36
- const page = await queryFn(params, ctx);
69
+ const page = await nextResult;
37
70
  pages.push(page);
38
71
  pageParams.push(currentParam);
39
72
  if (reduce) acc = reduce(acc, page);
40
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
73
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
41
74
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
75
+ if (!getNextPageParam) break;
42
76
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
43
77
  if (nextParam == null) break;
44
78
  currentParam = nextParam;
79
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
80
+ ctx.pageParam = currentParam;
81
+ nextResult = queryFn(params, ctx);
45
82
  }
46
83
  if (reduce) {
47
84
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
@@ -52,26 +89,47 @@ function buildCrawlingQueryFn(queryFn, getNextPageParam, initialPageParam, shoul
52
89
  }
53
90
  function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNextPage, reduce) {
54
91
  return async (params, crawlOptions, context) => {
55
- 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
+ let acc2 = void 0;
100
+ let nextBatchParam2 = null;
101
+ for await (const page of initialResult) {
102
+ if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
103
+ pages2.push(page);
104
+ acc2 = reduce(acc2, page);
105
+ const nextParam = getNextPageParam(page, pages2, void 0, []);
106
+ nextBatchParam2 = nextParam != null ? nextParam : null;
107
+ if (nextParam == null) break;
108
+ if (!shouldFetchNextPage(acc2, crawlOptions)) break;
109
+ }
110
+ if (acc2 === void 0) throw new DOMException("Aborted", "AbortError");
111
+ return { data: acc2, nextPageParam: nextBatchParam2 };
112
+ }
56
113
  const pages = [];
57
114
  const pageParams = [];
58
115
  let currentParam = context.pageParam;
59
116
  let acc = void 0;
60
117
  let nextBatchParam = null;
61
- const ctx = { ...context, pageParam: currentParam };
118
+ let nextResult = initialResult;
62
119
  while (true) {
63
- if ((_a = context.signal) == null ? void 0 : _a.aborted) break;
64
- ctx.pageParam = currentParam;
65
- const page = await queryFn(params, ctx);
120
+ const page = await nextResult;
66
121
  pages.push(page);
67
122
  pageParams.push(currentParam);
68
123
  acc = reduce(acc, page);
69
- if ((_b = context.signal) == null ? void 0 : _b.aborted) break;
124
+ if ((_c = context.signal) == null ? void 0 : _c.aborted) break;
70
125
  const nextParam = getNextPageParam(page, pages, currentParam, pageParams);
71
126
  nextBatchParam = nextParam != null ? nextParam : null;
72
127
  if (nextParam == null) break;
73
128
  if (!shouldFetchNextPage(acc, crawlOptions)) break;
74
129
  currentParam = nextParam;
130
+ if ((_d = context.signal) == null ? void 0 : _d.aborted) break;
131
+ ctx.pageParam = currentParam;
132
+ nextResult = queryFn(params, ctx);
75
133
  }
76
134
  if (acc === void 0) throw new DOMException("Aborted", "AbortError");
77
135
  return { data: acc, nextPageParam: nextBatchParam };
@@ -80,6 +138,7 @@ function buildInfiniteCrawlingQueryFn(queryFn, getNextPageParam, shouldFetchNext
80
138
  function buildFactory(cfg) {
81
139
  const {
82
140
  queryKey: namespace,
141
+ parentKey,
83
142
  queryFn: rawQueryFn,
84
143
  select,
85
144
  getNextPageParam,
@@ -89,8 +148,10 @@ function buildFactory(cfg) {
89
148
  reduce,
90
149
  standardOptions
91
150
  } = cfg;
92
- const hasCrawling = rawQueryFn !== void 0 && getNextPageParam !== void 0 && shouldFetchNextPage !== void 0;
93
- const hasInfiniteCrawling = hasCrawling && reduce !== void 0;
151
+ const ownSegments = parentKey !== void 0 ? namespace.slice(parentKey.length) : namespace;
152
+ const infiniteNamespace = [...namespace, "infinite"];
153
+ const hasCrawling = rawQueryFn !== void 0 && shouldFetchNextPage !== void 0;
154
+ const hasInfiniteCrawling = hasCrawling && reduce !== void 0 && getNextPageParam !== void 0;
94
155
  const crawlingFn = hasCrawling ? buildCrawlingQueryFn(
95
156
  rawQueryFn,
96
157
  getNextPageParam,
@@ -104,7 +165,6 @@ function buildFactory(cfg) {
104
165
  shouldFetchNextPage,
105
166
  reduce
106
167
  ) : void 0;
107
- const infiniteNamespace = [...resolveKey(namespace, void 0), "infinite"];
108
168
  const envelopeSelect = infiniteCrawlingFn ? (data) => ({
109
169
  ...data,
110
170
  pages: data.pages.map((e) => select ? select(e.data) : e.data)
@@ -114,7 +174,7 @@ function buildFactory(cfg) {
114
174
  pages: data.pages.map(select)
115
175
  }) : void 0;
116
176
  const factory = function(params, crawlOptions = {}) {
117
- const queryKey = resolveKey(namespace, params, crawlOptions);
177
+ const queryKey = parentKey !== void 0 ? buildChildKey(parentKey, ownSegments, params, crawlOptions) : resolveKey(namespace, params, crawlOptions);
118
178
  const resolvedQueryFn = crawlingFn ? (ctx) => crawlingFn(params, crawlOptions, ctx) : rawQueryFn ? (ctx) => rawQueryFn(params, ctx) : void 0;
119
179
  return {
120
180
  ...standardOptions,
@@ -125,7 +185,7 @@ function buildFactory(cfg) {
125
185
  };
126
186
  };
127
187
  factory.infinite = function(params, crawlOptions = {}) {
128
- const queryKey = resolveKey(infiniteNamespace, params, crawlOptions);
188
+ const queryKey = parentKey !== void 0 ? buildChildKey(parentKey, ownSegments, params, crawlOptions, true) : resolveKey(infiniteNamespace, params, crawlOptions);
129
189
  if (infiniteCrawlingFn) {
130
190
  return {
131
191
  ...standardOptions,
@@ -210,6 +270,7 @@ function queryFactory(configOrParent, childConfig) {
210
270
  } = childConfig;
211
271
  return buildFactory({
212
272
  queryKey: composedNamespace,
273
+ parentKey: parentCfg.queryKey,
213
274
  queryFn: hasNewQueryFn ? childConfig.queryFn : parentCfg.queryFn,
214
275
  select: resolvedSelect,
215
276
  ...crawling,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robohall/react-query-factory",
3
- "version": "1.2.0",
3
+ "version": "2.1.0",
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"