@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 +254 -69
- package/dist/index.d.mts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +82 -21
- package/dist/index.mjs +82 -21
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,108 +1,274 @@
|
|
|
1
1
|
# @robohall/react-query-factory
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@robohall/react-query-factory)
|
|
4
|
-
|
|
4
|
+

|
|
5
|
+

|
|
5
6
|
[](./LICENSE)
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
18
|
+
## The problem
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
35
|
+
### Step 2 — `queryOptions` for colocation
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
TanStack's `queryOptions` helper moves the key and fn into a shared object:
|
|
25
38
|
|
|
26
39
|
```typescript
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
71
|
+
### Step 4 — paginated 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
148
|
+
{
|
|
149
|
+
abortSignal: ctx.signal,
|
|
150
|
+
},
|
|
70
151
|
),
|
|
71
|
-
getNextPageParam:
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
198
|
-
|
|
199
|
-
| `queryKey`
|
|
200
|
-
| `queryFn`
|
|
201
|
-
| `select`
|
|
202
|
-
| `getNextPageParam`
|
|
203
|
-
| `initialPageParam`
|
|
204
|
-
| `getPreviousPageParam`
|
|
205
|
-
| `shouldFetchNextPage`
|
|
206
|
-
| `reduce`
|
|
207
|
-
| + all `StandardQueryOptions` fields |
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
93
|
+
let nextResult = initialResult;
|
|
59
94
|
while (true) {
|
|
60
|
-
|
|
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 ((
|
|
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
|
-
|
|
144
|
+
let nextResult = initialResult;
|
|
88
145
|
while (true) {
|
|
89
|
-
|
|
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 ((
|
|
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
|
|
119
|
-
const
|
|
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
|
|
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
|
-
|
|
67
|
+
let nextResult = initialResult;
|
|
33
68
|
while (true) {
|
|
34
|
-
|
|
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 ((
|
|
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
|
-
|
|
118
|
+
let nextResult = initialResult;
|
|
62
119
|
while (true) {
|
|
63
|
-
|
|
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 ((
|
|
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
|
|
93
|
-
const
|
|
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.
|
|
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"
|