@confect/react 10.0.0-next.22 → 10.0.0-next.24
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/CHANGELOG.md +10 -0
- package/dist/PaginatedQueryResult.d.ts +16 -14
- package/dist/PaginatedQueryResult.d.ts.map +1 -1
- package/dist/PaginatedQueryResult.js +3 -3
- package/dist/PaginatedQueryResult.js.map +1 -1
- package/dist/QueryResult.d.ts +7 -7
- package/dist/QueryResult.d.ts.map +1 -1
- package/dist/QueryResult.js +5 -6
- package/dist/QueryResult.js.map +1 -1
- package/dist/StreamPagination.d.ts +83 -51
- package/dist/StreamPagination.d.ts.map +1 -1
- package/dist/StreamPagination.js +33 -21
- package/dist/StreamPagination.js.map +1 -1
- package/dist/index.d.ts +16 -16
- package/dist/index.js +17 -17
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/src/PaginatedQueryResult.ts +16 -14
- package/src/QueryResult.ts +7 -7
- package/src/StreamPagination.ts +86 -52
- package/src/index.ts +24 -24
package/src/QueryResult.ts
CHANGED
|
@@ -11,7 +11,8 @@ type TypeId = typeof TypeId;
|
|
|
11
11
|
/**
|
|
12
12
|
* A `QueryResult` represents the result of a Confect query.
|
|
13
13
|
*
|
|
14
|
-
* @typeParam A - The type of the decoded `returns` value in the `Success`
|
|
14
|
+
* @typeParam A - The type of the decoded `returns` value in the `Success`
|
|
15
|
+
* variant.
|
|
15
16
|
* @typeParam E - The type of the decoded typed error in the `Failure` variant.
|
|
16
17
|
*/
|
|
17
18
|
export type QueryResult<A, E = never> =
|
|
@@ -140,12 +141,11 @@ type MatchReturns<E, X, Y, Z> = [E] extends [never] ? X | Y : X | Y | Z;
|
|
|
140
141
|
* not required.
|
|
141
142
|
*
|
|
142
143
|
* @example
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* });
|
|
144
|
+
* const result = QueryResult.match(queryResult, {
|
|
145
|
+
* onLoading: (skipped) => (skipped ? null : <p>Loading…</p>),
|
|
146
|
+
* onSuccess: (value) => <p>{value.text}</p>,
|
|
147
|
+
* onFailure: (error) => <p>Error: {error.message}</p>,
|
|
148
|
+
* });
|
|
149
149
|
*/
|
|
150
150
|
export const match: {
|
|
151
151
|
<A, E, X, Y, Z = never>(
|
package/src/StreamPagination.ts
CHANGED
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
* EXPERIMENTAL—the pure state machine behind {@link useStreamPaginatedQuery}
|
|
3
3
|
* (see `notes/stream-based-querying.md` in the repo root).
|
|
4
4
|
*
|
|
5
|
-
* Reactive pagination without the query journal: every loaded page—including
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Reactive pagination without the query journal: every loaded page—including
|
|
6
|
+
* the first—is pinned to a fixed index range by echoing its `continueCursor`
|
|
7
|
+
* back as the next subscription's `endCursor` (the last page pins to the
|
|
8
|
+
* end-of-stream sentinel once exhausted), so adjacent pages stay exactly
|
|
9
|
+
* contiguous and pages never shed items as data changes—the "range-defined
|
|
10
|
+
* pages" the Fully Reactive Pagination article calls for, and the mechanism of
|
|
11
|
+
* `convex-helpers/react`'s `usePaginatedQuery`.
|
|
10
12
|
*
|
|
11
|
-
* Pinning a freshly loaded page, loading more, and splitting an overgrown
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Pinning a freshly loaded page, loading more, and splitting an overgrown page
|
|
14
|
+
* are the same transition shape: the affected page keeps rendering while its
|
|
15
|
+
* replacement subscriptions ("ongoing split") load, and is swapped out once all
|
|
16
|
+
* of them have results.
|
|
15
17
|
*
|
|
16
|
-
* This module is framework-free (it imports only `effect/*` and a type
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* This module is framework-free (it imports only `effect/*` and a type from
|
|
19
|
+
* `convex/server`); it lives here for now because the React hook is its only
|
|
20
|
+
* consumer—a second client (e.g. `@confect/foldkit`) should prompt a move down
|
|
21
|
+
* to `@confect/js`.
|
|
20
22
|
*/
|
|
21
23
|
import type { PaginationResult } from "convex/server";
|
|
22
24
|
import * as Array from "effect/Array";
|
|
@@ -24,27 +26,35 @@ import * as Option from "effect/Option";
|
|
|
24
26
|
import { pipe } from "effect/Function";
|
|
25
27
|
import * as Record from "effect/Record";
|
|
26
28
|
|
|
27
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The `paginationOpts` one subscribed page queries with.
|
|
31
|
+
*/
|
|
28
32
|
export interface PageRequest {
|
|
29
33
|
readonly numItems: number;
|
|
30
34
|
readonly cursor: string | null;
|
|
31
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Present once the page is pinned to a fixed range.
|
|
37
|
+
*/
|
|
32
38
|
readonly endCursor?: string;
|
|
33
39
|
/**
|
|
34
|
-
* Per-page read budgets, forwarded to the server so a scan-heavy page
|
|
35
|
-
*
|
|
40
|
+
* Per-page read budgets, forwarded to the server so a scan-heavy page fails
|
|
41
|
+
* over to `SplitRequired` instead of exceeding query limits.
|
|
36
42
|
*/
|
|
37
43
|
readonly maximumRowsRead?: number;
|
|
38
44
|
readonly maximumBytesRead?: number;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The per-page read budgets every growing page is requested with.
|
|
49
|
+
*/
|
|
42
50
|
export interface ReadBudget {
|
|
43
51
|
readonly maximumRowsRead?: number | undefined;
|
|
44
52
|
readonly maximumBytesRead?: number | undefined;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* A fresh growing-page request (no `endCursor`).
|
|
57
|
+
*/
|
|
48
58
|
const growingRequest = (
|
|
49
59
|
numItems: number,
|
|
50
60
|
cursor: string | null,
|
|
@@ -62,9 +72,13 @@ const growingRequest = (
|
|
|
62
72
|
|
|
63
73
|
export interface State {
|
|
64
74
|
readonly nextPageKey: number;
|
|
65
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Page keys in display order.
|
|
77
|
+
*/
|
|
66
78
|
readonly pageKeys: ReadonlyArray<string>;
|
|
67
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The request each subscribed page queries with, by page key.
|
|
81
|
+
*/
|
|
68
82
|
readonly pages: Record.ReadonlyRecord<string, PageRequest>;
|
|
69
83
|
/**
|
|
70
84
|
* Pages being replaced by one (a pin) or two (a split) narrower
|
|
@@ -74,7 +88,9 @@ export interface State {
|
|
|
74
88
|
readonly ongoingSplits: Record.ReadonlyRecord<string, ReadonlyArray<string>>;
|
|
75
89
|
}
|
|
76
90
|
|
|
77
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The skipped state: nothing subscribed.
|
|
93
|
+
*/
|
|
78
94
|
export const empty: State = {
|
|
79
95
|
nextPageKey: 0,
|
|
80
96
|
pageKeys: [],
|
|
@@ -82,7 +98,9 @@ export const empty: State = {
|
|
|
82
98
|
ongoingSplits: Record.empty(),
|
|
83
99
|
};
|
|
84
100
|
|
|
85
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* One growing (unpinned) page from the start of the stream.
|
|
103
|
+
*/
|
|
86
104
|
export const initial = (
|
|
87
105
|
initialNumItems: number,
|
|
88
106
|
budget: ReadBudget = {},
|
|
@@ -96,9 +114,9 @@ export const initial = (
|
|
|
96
114
|
/**
|
|
97
115
|
* Pin a freshly loaded growing page at its `continueCursor` (or, once
|
|
98
116
|
* exhausted, at the end-of-stream sentinel), so it stops being a sliding
|
|
99
|
-
* window: a pinned page grows and shrinks with the data in its range but
|
|
100
|
-
*
|
|
101
|
-
*
|
|
117
|
+
* window: a pinned page grows and shrinks with the data in its range but never
|
|
118
|
+
* sheds items past its edges. Modeled as a one-replacement split so the
|
|
119
|
+
* original keeps rendering until the pinned twin has a result.
|
|
102
120
|
*/
|
|
103
121
|
export const pin =
|
|
104
122
|
(key: string, endCursor: string): ((state: State) => State) =>
|
|
@@ -123,9 +141,9 @@ export const pin =
|
|
|
123
141
|
});
|
|
124
142
|
|
|
125
143
|
/**
|
|
126
|
-
* Pin the growing last page at `continueCursor` and start a new growing
|
|
127
|
-
*
|
|
128
|
-
*
|
|
144
|
+
* Pin the growing last page at `continueCursor` and start a new growing page
|
|
145
|
+
* from there. Modeled as a split of the last page into its pinned replacement
|
|
146
|
+
* and the new page, so the swap waits for both.
|
|
129
147
|
*/
|
|
130
148
|
export const loadMore =
|
|
131
149
|
(
|
|
@@ -181,10 +199,10 @@ export const loadMore =
|
|
|
181
199
|
);
|
|
182
200
|
|
|
183
201
|
/**
|
|
184
|
-
* Split the page at `key` in two at `splitCursor`. The first replacement
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* growing).
|
|
202
|
+
* Split the page at `key` in two at `splitCursor`. The first replacement pins
|
|
203
|
+
* at the split point; the second keeps the page's own `endCursor`—for a pinned
|
|
204
|
+
* page its original end (so a truncated `SplitRequired` page loses none of its
|
|
205
|
+
* range), and for a growing page no end at all (it keeps growing).
|
|
188
206
|
*/
|
|
189
207
|
export const split =
|
|
190
208
|
(key: string, splitCursor: string): ((state: State) => State) =>
|
|
@@ -210,7 +228,9 @@ export const split =
|
|
|
210
228
|
},
|
|
211
229
|
});
|
|
212
230
|
|
|
213
|
-
/**
|
|
231
|
+
/**
|
|
232
|
+
* Swap a completed split's replacements in for the original page.
|
|
233
|
+
*/
|
|
214
234
|
export const completeSplit =
|
|
215
235
|
(key: string): ((state: State) => State) =>
|
|
216
236
|
(state) =>
|
|
@@ -226,19 +246,25 @@ export const completeSplit =
|
|
|
226
246
|
}),
|
|
227
247
|
});
|
|
228
248
|
|
|
229
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Whether the last (rendered) page is currently being split or pinned.
|
|
251
|
+
*/
|
|
230
252
|
export const isLastPageSplitting = (state: State): boolean =>
|
|
231
253
|
Option.exists(Array.last(state.pageKeys), (lastKey) =>
|
|
232
254
|
Record.has(state.ongoingSplits, lastKey),
|
|
233
255
|
);
|
|
234
256
|
|
|
235
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* Build one subscription request per subscribed page.
|
|
259
|
+
*/
|
|
236
260
|
export const pageRequests = <A>(
|
|
237
261
|
state: State,
|
|
238
262
|
f: (page: PageRequest) => A,
|
|
239
263
|
): Record.ReadonlyRecord<string, A> => Record.map(state.pages, f);
|
|
240
264
|
|
|
241
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* The per-page results a render observes, keyed like {@link pageRequests}.
|
|
267
|
+
*/
|
|
242
268
|
export type Results = Record.ReadonlyRecord<
|
|
243
269
|
string,
|
|
244
270
|
PageResult | Error | undefined
|
|
@@ -249,32 +275,40 @@ export type Results = Record.ReadonlyRecord<
|
|
|
249
275
|
// -----------------------------------------------------------------------------
|
|
250
276
|
|
|
251
277
|
/**
|
|
252
|
-
* The wire shape of one loaded page—`PaginationResult` from
|
|
253
|
-
*
|
|
278
|
+
* The wire shape of one loaded page—`PaginationResult` from `convex/server`, so
|
|
279
|
+
* the protocol has a single source of truth.
|
|
254
280
|
*/
|
|
255
281
|
export type PageResult = PaginationResult<unknown>;
|
|
256
282
|
|
|
257
283
|
/**
|
|
258
|
-
* What one render pass derives from the subscribed pages' results: the
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
284
|
+
* What one render pass derives from the subscribed pages' results: the items to
|
|
285
|
+
* show, the trailing complete result (absent while the tail is loading or
|
|
286
|
+
* force-splitting), state transitions to apply, and whether a failure or an
|
|
287
|
+
* invalid cursor was hit.
|
|
262
288
|
*/
|
|
263
289
|
export type Interpretation =
|
|
264
290
|
| { readonly _tag: "ResetRequired" }
|
|
265
291
|
| {
|
|
266
292
|
readonly _tag: "Failed";
|
|
267
293
|
readonly error: unknown;
|
|
268
|
-
/**
|
|
294
|
+
/**
|
|
295
|
+
* Encoded items loaded before the failing page.
|
|
296
|
+
*/
|
|
269
297
|
readonly items: ReadonlyArray<unknown>;
|
|
270
298
|
}
|
|
271
299
|
| {
|
|
272
300
|
readonly _tag: "Interpreted";
|
|
273
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* Encoded items across the loaded pages, in order.
|
|
303
|
+
*/
|
|
274
304
|
readonly items: ReadonlyArray<unknown>;
|
|
275
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* The last page's result, when every page has loaded completely.
|
|
307
|
+
*/
|
|
276
308
|
readonly lastResult: Option.Option<PageResult>;
|
|
277
|
-
/**
|
|
309
|
+
/**
|
|
310
|
+
* Split/swap transitions this render discovered.
|
|
311
|
+
*/
|
|
278
312
|
readonly transitions: ReadonlyArray<(state: State) => State>;
|
|
279
313
|
};
|
|
280
314
|
|
|
@@ -285,12 +319,12 @@ const interpreted = (
|
|
|
285
319
|
): Interpretation => ({ _tag: "Interpreted", items, lastResult, transitions });
|
|
286
320
|
|
|
287
321
|
/**
|
|
288
|
-
* Walk the pages in display order, concatenating their items and
|
|
289
|
-
*
|
|
290
|
-
*
|
|
322
|
+
* Walk the pages in display order, concatenating their items and collecting the
|
|
323
|
+
* transitions to apply (completed split swaps, recommended splits, eager splits
|
|
324
|
+
* of pages that outgrew `initialNumItems`).
|
|
291
325
|
*
|
|
292
|
-
* `isInvalidCursorError` marks errors that call for a full pagination
|
|
293
|
-
*
|
|
326
|
+
* `isInvalidCursorError` marks errors that call for a full pagination reset
|
|
327
|
+
* rather than a failure (the cursor no longer matches the query).
|
|
294
328
|
*/
|
|
295
329
|
export const interpret = (
|
|
296
330
|
state: State,
|
package/src/index.ts
CHANGED
|
@@ -115,8 +115,8 @@ export type PaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =
|
|
|
115
115
|
: never;
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* The item type for a paginated query: the element type of the `page` field
|
|
119
|
-
*
|
|
118
|
+
* The item type for a paginated query: the element type of the `page` field of
|
|
119
|
+
* the ref's returns.
|
|
120
120
|
*/
|
|
121
121
|
export type PaginatedQueryItem<Query extends Ref.AnyPublicPaginatedQuery> =
|
|
122
122
|
Ref.Returns<Query>["page"][number];
|
|
@@ -170,12 +170,12 @@ const MINIMUM_CONVEX_VERSION = "1.36.0";
|
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* The non-throwing mode of `usePaginatedQueryInternal`—its fourth,
|
|
173
|
-
* `throwOnError` parameter—arrived in convex 1.36.0. Convex 1.32 through
|
|
174
|
-
*
|
|
175
|
-
*
|
|
173
|
+
* `throwOnError` parameter—arrived in convex 1.36.0. Convex 1.32 through 1.35
|
|
174
|
+
* export the same symbol taking only three parameters, where a fourth argument
|
|
175
|
+
* is silently ignored and errors are always thrown.
|
|
176
176
|
*
|
|
177
|
-
* The function itself cannot be interrogated for this: `Function.length`
|
|
178
|
-
*
|
|
177
|
+
* The function itself cannot be interrogated for this: `Function.length` counts
|
|
178
|
+
* parameters up to the first one with a default, and 1.36.0 declares
|
|
179
179
|
* `throwOnError = true`, so both shapes report an arity of 3. The package
|
|
180
180
|
* version is the only reliable signal.
|
|
181
181
|
*/
|
|
@@ -322,9 +322,9 @@ export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
|
322
322
|
};
|
|
323
323
|
|
|
324
324
|
/**
|
|
325
|
-
* Whether a paginated query error means the stored cursors no longer match
|
|
326
|
-
*
|
|
327
|
-
*
|
|
325
|
+
* Whether a paginated query error means the stored cursors no longer match the
|
|
326
|
+
* query (a data-dependent query changed underneath us), calling for a full
|
|
327
|
+
* pagination reset rather than a failure.
|
|
328
328
|
*/
|
|
329
329
|
const isInvalidCursorError = (error: Error): boolean =>
|
|
330
330
|
error.message.includes("InvalidCursor") ||
|
|
@@ -343,18 +343,18 @@ const NO_ITEMS: ReadonlyArray<unknown> = [];
|
|
|
343
343
|
* EXPERIMENTAL—endCursor-pinned reactive pagination (see
|
|
344
344
|
* `notes/stream-based-querying.md`). Use it with paginated queries whose
|
|
345
345
|
* handlers paginate via `QueryStream.paginate`: those don't write the query
|
|
346
|
-
* journal that {@link usePaginatedQuery}'s built-in reactivity relies on,
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
346
|
+
* journal that {@link usePaginatedQuery}'s built-in reactivity relies on, so
|
|
347
|
+
* gap-free pages must be maintained by the client instead. (It works with any
|
|
348
|
+
* paginated query honoring the `endCursor` protocol field, including the
|
|
349
|
+
* built-in `paginate`.)
|
|
350
350
|
*
|
|
351
|
-
* Each loaded page is pinned to a fixed index range by re-subscribing it
|
|
352
|
-
*
|
|
353
|
-
*
|
|
351
|
+
* Each loaded page is pinned to a fixed index range by re-subscribing it with
|
|
352
|
+
* its `continueCursor` echoed back as `endCursor`—pages then grow and shrink
|
|
353
|
+
* reactively but always meet exactly, and a page that outgrows
|
|
354
354
|
* `initialNumItems` is split in two. This is the mechanism of
|
|
355
355
|
* `convex-helpers/react`'s `usePaginatedQuery`, re-expressed over the pure
|
|
356
|
-
* {@link StreamPagination} state machine, with args/items/errors codec'd
|
|
357
|
-
*
|
|
356
|
+
* {@link StreamPagination} state machine, with args/items/errors codec'd through
|
|
357
|
+
* the ref's schemas exactly like {@link usePaginatedQuery}.
|
|
358
358
|
*/
|
|
359
359
|
export const useStreamPaginatedQuery = <
|
|
360
360
|
Query extends Ref.AnyPublicPaginatedQuery,
|
|
@@ -364,11 +364,11 @@ export const useStreamPaginatedQuery = <
|
|
|
364
364
|
options: {
|
|
365
365
|
readonly initialNumItems: number;
|
|
366
366
|
/**
|
|
367
|
-
* Per-page read budgets forwarded as `paginationOpts.maximumRowsRead`
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
367
|
+
* Per-page read budgets forwarded as `paginationOpts.maximumRowsRead` and
|
|
368
|
+
* `paginationOpts.maximumBytesRead`: a page that would scan more rows, or
|
|
369
|
+
* read more bytes, than these returns truncated with `SplitRequired` (and
|
|
370
|
+
* the hook splits it) instead of exceeding Convex's query limits on a
|
|
371
|
+
* filter-heavy stream.
|
|
372
372
|
*/
|
|
373
373
|
readonly maximumRowsRead?: number;
|
|
374
374
|
readonly maximumBytesRead?: number;
|