@confect/react 10.0.0-next.21 → 10.0.0-next.22
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 +109 -40
- package/dist/PaginatedQueryResult.d.ts +4 -4
- package/dist/PaginatedQueryResult.js.map +1 -1
- package/dist/StreamPagination.d.ts +127 -0
- package/dist/StreamPagination.d.ts.map +1 -0
- package/dist/StreamPagination.js +170 -0
- package/dist/StreamPagination.js.map +1 -0
- package/dist/index.d.ts +31 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -8
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/PaginatedQueryResult.ts +4 -4
- package/src/StreamPagination.ts +369 -0
- package/src/index.ts +287 -12
package/src/index.ts
CHANGED
|
@@ -6,24 +6,31 @@ import type { usePaginatedQuery as useConvexPaginatedQuery } from "convex/react"
|
|
|
6
6
|
import {
|
|
7
7
|
useAction as useConvexAction,
|
|
8
8
|
useMutation as useConvexMutation,
|
|
9
|
+
useQueries as useConvexQueries,
|
|
9
10
|
useQuery as useConvexQuery,
|
|
10
11
|
type ReactMutation as ConvexReactMutation,
|
|
11
12
|
} from "convex/react";
|
|
12
|
-
import type
|
|
13
|
-
import type
|
|
13
|
+
import { getFunctionName, type FunctionReference } from "convex/server";
|
|
14
|
+
import { ConvexError, convexToJson, type Value } from "convex/values";
|
|
14
15
|
import * as Cause from "effect/Cause";
|
|
15
16
|
import * as Effect from "effect/Effect";
|
|
16
17
|
import * as Result from "effect/Result";
|
|
17
18
|
import * as Exit from "effect/Exit";
|
|
18
19
|
import * as Match from "effect/Match";
|
|
19
20
|
import * as Option from "effect/Option";
|
|
20
|
-
import { useCallback, useMemo } from "react";
|
|
21
|
+
import { useCallback, useMemo, useState } from "react";
|
|
21
22
|
|
|
22
23
|
import * as OptimisticLocalStore from "./OptimisticLocalStore";
|
|
23
24
|
import * as PaginatedQueryResult from "./PaginatedQueryResult";
|
|
24
25
|
import * as QueryResult from "./QueryResult";
|
|
26
|
+
import * as StreamPagination from "./StreamPagination";
|
|
25
27
|
|
|
26
|
-
export {
|
|
28
|
+
export {
|
|
29
|
+
OptimisticLocalStore,
|
|
30
|
+
PaginatedQueryResult,
|
|
31
|
+
QueryResult,
|
|
32
|
+
StreamPagination,
|
|
33
|
+
};
|
|
27
34
|
|
|
28
35
|
export type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [
|
|
29
36
|
never,
|
|
@@ -162,8 +169,8 @@ const useConvexPaginatedQueryInternal = (
|
|
|
162
169
|
const MINIMUM_CONVEX_VERSION = "1.36.0";
|
|
163
170
|
|
|
164
171
|
/**
|
|
165
|
-
* The non-throwing mode of `usePaginatedQueryInternal
|
|
166
|
-
* `throwOnError` parameter
|
|
172
|
+
* The non-throwing mode of `usePaginatedQueryInternal`—its fourth,
|
|
173
|
+
* `throwOnError` parameter—arrived in convex 1.36.0. Convex 1.32 through
|
|
167
174
|
* 1.35 export the same symbol taking only three parameters, where a fourth
|
|
168
175
|
* argument is silently ignored and errors are always thrown.
|
|
169
176
|
*
|
|
@@ -222,7 +229,7 @@ export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
|
222
229
|
`usePaginatedQuery requires convex >= ${MINIMUM_CONVEX_VERSION}, but found ` +
|
|
223
230
|
`${convexVersion}. Earlier versions always throw paginated query errors ` +
|
|
224
231
|
"instead of returning them, so a declared `error` schema cannot be " +
|
|
225
|
-
"surfaced as a `Failure
|
|
232
|
+
"surfaced as a `Failure`—upgrade the `convex` package.",
|
|
226
233
|
);
|
|
227
234
|
}
|
|
228
235
|
|
|
@@ -253,7 +260,7 @@ export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
|
253
260
|
// failures included, so results are decoded unconditionally.
|
|
254
261
|
//
|
|
255
262
|
// Decoding allocates fresh items, so key on the referentially stable
|
|
256
|
-
// `results` the Convex hook provides
|
|
263
|
+
// `results` the Convex hook provides—the same identity-preservation
|
|
257
264
|
// rationale as in `useQuery` above.
|
|
258
265
|
const { results: encodedResults } = convexResult;
|
|
259
266
|
const decodedResults = useMemo(
|
|
@@ -292,8 +299,7 @@ export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
|
292
299
|
Match.when("LoadingMore", () =>
|
|
293
300
|
PaginatedQueryResult.loadingMore({ results: decodedResults }),
|
|
294
301
|
),
|
|
295
|
-
// `CanLoadMore` is the only status whose `loadMore` does anything
|
|
296
|
-
// the Convex hook returns an intentional no-op for all the others, so
|
|
302
|
+
// `CanLoadMore` is the only status whose `loadMore` does anything—the Convex hook returns an intentional no-op for all the others, so
|
|
297
303
|
// they carry no callback at all.
|
|
298
304
|
Match.when("CanLoadMore", () =>
|
|
299
305
|
PaginatedQueryResult.canLoadMore({
|
|
@@ -306,8 +312,7 @@ export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
|
306
312
|
),
|
|
307
313
|
Match.exhaustive,
|
|
308
314
|
),
|
|
309
|
-
// The memo produces `Variants`, the conditional return type's superset
|
|
310
|
-
// when `E` is `never` the `Failure` arm is unreachable at runtime, which
|
|
315
|
+
// The memo produces `Variants`, the conditional return type's superset—when `E` is `never` the `Failure` arm is unreachable at runtime, which
|
|
311
316
|
// is exactly what the conditional encodes.
|
|
312
317
|
[ref, skipped, decodedResults, status, loadMore, error],
|
|
313
318
|
) as PaginatedQueryResult.PaginatedQueryResult<
|
|
@@ -316,6 +321,276 @@ export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
|
316
321
|
>;
|
|
317
322
|
};
|
|
318
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Whether a paginated query error means the stored cursors no longer match
|
|
326
|
+
* the query (a data-dependent query changed underneath us), calling for a
|
|
327
|
+
* full pagination reset rather than a failure.
|
|
328
|
+
*/
|
|
329
|
+
const isInvalidCursorError = (error: Error): boolean =>
|
|
330
|
+
error.message.includes("InvalidCursor") ||
|
|
331
|
+
(error instanceof ConvexError &&
|
|
332
|
+
typeof error.data === "object" &&
|
|
333
|
+
error.data !== null &&
|
|
334
|
+
// Both Convex's built-in pagination (which also sets
|
|
335
|
+
// `isConvexSystemError`) and `QueryStream.paginate` signal an invalid
|
|
336
|
+
// cursor with this data shape—the same check `convex/react` performs.
|
|
337
|
+
(error.data as { paginationError?: unknown }).paginationError ===
|
|
338
|
+
"InvalidCursor");
|
|
339
|
+
|
|
340
|
+
const NO_ITEMS: ReadonlyArray<unknown> = [];
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* EXPERIMENTAL—endCursor-pinned reactive pagination (see
|
|
344
|
+
* `notes/stream-based-querying.md`). Use it with paginated queries whose
|
|
345
|
+
* handlers paginate via `QueryStream.paginate`: those don't write the query
|
|
346
|
+
* journal that {@link usePaginatedQuery}'s built-in reactivity relies on,
|
|
347
|
+
* so gap-free pages must be maintained by the client instead. (It works
|
|
348
|
+
* with any paginated query honoring the `endCursor` protocol field,
|
|
349
|
+
* including the built-in `paginate`.)
|
|
350
|
+
*
|
|
351
|
+
* Each loaded page is pinned to a fixed index range by re-subscribing it
|
|
352
|
+
* with its `continueCursor` echoed back as `endCursor`—pages then grow
|
|
353
|
+
* and shrink reactively but always meet exactly, and a page that outgrows
|
|
354
|
+
* `initialNumItems` is split in two. This is the mechanism of
|
|
355
|
+
* `convex-helpers/react`'s `usePaginatedQuery`, re-expressed over the pure
|
|
356
|
+
* {@link StreamPagination} state machine, with args/items/errors codec'd
|
|
357
|
+
* through the ref's schemas exactly like {@link usePaginatedQuery}.
|
|
358
|
+
*/
|
|
359
|
+
export const useStreamPaginatedQuery = <
|
|
360
|
+
Query extends Ref.AnyPublicPaginatedQuery,
|
|
361
|
+
>(
|
|
362
|
+
ref: Query,
|
|
363
|
+
args: UsePaginatedQueryArgs<Query>,
|
|
364
|
+
options: {
|
|
365
|
+
readonly initialNumItems: number;
|
|
366
|
+
/**
|
|
367
|
+
* Per-page read budgets forwarded as `paginationOpts.maximumRowsRead`
|
|
368
|
+
* and `paginationOpts.maximumBytesRead`: a page that would scan more
|
|
369
|
+
* rows, or read more bytes, than these returns truncated with
|
|
370
|
+
* `SplitRequired` (and the hook splits it) instead of exceeding
|
|
371
|
+
* Convex's query limits on a filter-heavy stream.
|
|
372
|
+
*/
|
|
373
|
+
readonly maximumRowsRead?: number;
|
|
374
|
+
readonly maximumBytesRead?: number;
|
|
375
|
+
},
|
|
376
|
+
): PaginatedQueryResult.PaginatedQueryResult<
|
|
377
|
+
PaginatedQueryItem<Query>,
|
|
378
|
+
Ref.Error<Query>
|
|
379
|
+
> => {
|
|
380
|
+
// `useQueries` requires a *referentially stable* queries object while
|
|
381
|
+
// nothing has changed: its subscription memo keys on identity, and its
|
|
382
|
+
// `useSubscription` re-runs a render-phase state update whenever the
|
|
383
|
+
// subscription changes—an unstable input loops the render. Callers
|
|
384
|
+
// typically pass a fresh `args` literal each render, so everything that
|
|
385
|
+
// feeds the queries object is stabilized by *value* here: the function
|
|
386
|
+
// reference by `ref`, and the encoded args by their serialized form (as
|
|
387
|
+
// `convex/react`'s own paginated hook does).
|
|
388
|
+
const functionReference = useMemo(() => Ref.getFunctionReference(ref), [ref]);
|
|
389
|
+
const skipped = args === "skip";
|
|
390
|
+
|
|
391
|
+
const rawEncodedArgs =
|
|
392
|
+
args === "skip"
|
|
393
|
+
? undefined
|
|
394
|
+
: (Ref.encodePaginatedQueryArgsSync(
|
|
395
|
+
ref,
|
|
396
|
+
args as unknown as Omit<Ref.Args<Query>, "paginationOpts">,
|
|
397
|
+
) as Record<string, Value>);
|
|
398
|
+
const encodedArgsKey =
|
|
399
|
+
rawEncodedArgs === undefined
|
|
400
|
+
? "skip"
|
|
401
|
+
: JSON.stringify(convexToJson(rawEncodedArgs));
|
|
402
|
+
const encodedArgs = useMemo(
|
|
403
|
+
() => rawEncodedArgs,
|
|
404
|
+
// The serialized form stands in for the freshly allocated object.
|
|
405
|
+
[ref, encodedArgsKey],
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// A change of query, args, skippedness, or page size restarts pagination
|
|
409
|
+
// from scratch.
|
|
410
|
+
const resetKey = useMemo(
|
|
411
|
+
() =>
|
|
412
|
+
JSON.stringify([
|
|
413
|
+
getFunctionName(functionReference),
|
|
414
|
+
encodedArgsKey,
|
|
415
|
+
options.initialNumItems,
|
|
416
|
+
options.maximumRowsRead ?? null,
|
|
417
|
+
options.maximumBytesRead ?? null,
|
|
418
|
+
]),
|
|
419
|
+
[
|
|
420
|
+
functionReference,
|
|
421
|
+
encodedArgsKey,
|
|
422
|
+
options.initialNumItems,
|
|
423
|
+
options.maximumRowsRead,
|
|
424
|
+
options.maximumBytesRead,
|
|
425
|
+
],
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const freshState = () =>
|
|
429
|
+
skipped
|
|
430
|
+
? StreamPagination.empty
|
|
431
|
+
: StreamPagination.initial(options.initialNumItems, options);
|
|
432
|
+
|
|
433
|
+
const [tracked, setTracked] = useState(() => ({
|
|
434
|
+
resetKey,
|
|
435
|
+
state: freshState(),
|
|
436
|
+
}));
|
|
437
|
+
// Render-phase reset—the React-sanctioned derived-state-from-props
|
|
438
|
+
// pattern, also how `convex/react`'s own paginated hook resets.
|
|
439
|
+
let current = tracked;
|
|
440
|
+
if (current.resetKey !== resetKey) {
|
|
441
|
+
current = { resetKey, state: freshState() };
|
|
442
|
+
setTracked(current);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const applyTransitions = useCallback(
|
|
446
|
+
(
|
|
447
|
+
transitions: ReadonlyArray<
|
|
448
|
+
(state: StreamPagination.State) => StreamPagination.State
|
|
449
|
+
>,
|
|
450
|
+
) =>
|
|
451
|
+
setTracked((previous) => ({
|
|
452
|
+
resetKey: previous.resetKey,
|
|
453
|
+
state: transitions.reduce(
|
|
454
|
+
(state, transition) => transition(state),
|
|
455
|
+
previous.state,
|
|
456
|
+
),
|
|
457
|
+
})),
|
|
458
|
+
[],
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const queries = useMemo(
|
|
462
|
+
() =>
|
|
463
|
+
StreamPagination.pageRequests(current.state, (page) => ({
|
|
464
|
+
query: functionReference,
|
|
465
|
+
args: { ...encodedArgs, paginationOpts: page },
|
|
466
|
+
})),
|
|
467
|
+
[current.state, functionReference, encodedArgs],
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const resultsObject = useConvexQueries(
|
|
471
|
+
queries as unknown as Parameters<typeof useConvexQueries>[0],
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const interpretation = useMemo(
|
|
475
|
+
() =>
|
|
476
|
+
StreamPagination.interpret(
|
|
477
|
+
current.state,
|
|
478
|
+
resultsObject as StreamPagination.Results,
|
|
479
|
+
{ initialNumItems: options.initialNumItems, isInvalidCursorError },
|
|
480
|
+
),
|
|
481
|
+
[current.state, resultsObject, options.initialNumItems],
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Apply the transitions this render discovered (completed split swaps,
|
|
485
|
+
// new splits of overgrown pages) or restart after an invalid cursor—render-phase state updates, as in `convex-helpers`' hook. Both settle:
|
|
486
|
+
// the updated state no longer produces the same discovery.
|
|
487
|
+
if (interpretation._tag === "ResetRequired") {
|
|
488
|
+
setTracked((previous) => ({
|
|
489
|
+
resetKey: previous.resetKey,
|
|
490
|
+
state: freshState(),
|
|
491
|
+
}));
|
|
492
|
+
} else if (
|
|
493
|
+
interpretation._tag === "Interpreted" &&
|
|
494
|
+
interpretation.transitions.length > 0
|
|
495
|
+
) {
|
|
496
|
+
applyTransitions(interpretation.transitions);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const encodedItems =
|
|
500
|
+
interpretation._tag === "ResetRequired" ? NO_ITEMS : interpretation.items;
|
|
501
|
+
// Page results keep their identity while unchanged (only the page that
|
|
502
|
+
// actually updated is a fresh object), so caching decodes per encoded
|
|
503
|
+
// item makes a single-page update decode only that page's items rather
|
|
504
|
+
// than every loaded page's.
|
|
505
|
+
const decodeCache = useMemo(
|
|
506
|
+
() => new WeakMap<object, PaginatedQueryItem<Query>>(),
|
|
507
|
+
[ref],
|
|
508
|
+
);
|
|
509
|
+
const decodedResults = useMemo(
|
|
510
|
+
() =>
|
|
511
|
+
encodedItems.map((item): PaginatedQueryItem<Query> => {
|
|
512
|
+
if (typeof item !== "object" || item === null) {
|
|
513
|
+
return Ref.decodePaginationPageSync(ref, [
|
|
514
|
+
item,
|
|
515
|
+
])[0] as PaginatedQueryItem<Query>;
|
|
516
|
+
}
|
|
517
|
+
const cached = decodeCache.get(item);
|
|
518
|
+
if (cached !== undefined) {
|
|
519
|
+
return cached;
|
|
520
|
+
}
|
|
521
|
+
const decoded = Ref.decodePaginationPageSync(ref, [
|
|
522
|
+
item,
|
|
523
|
+
])[0] as PaginatedQueryItem<Query>;
|
|
524
|
+
decodeCache.set(item, decoded);
|
|
525
|
+
return decoded;
|
|
526
|
+
}),
|
|
527
|
+
[ref, encodedItems, decodeCache],
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
return useMemo((): PaginatedQueryResult.Variants<
|
|
531
|
+
PaginatedQueryItem<Query>,
|
|
532
|
+
Ref.Error<Query>
|
|
533
|
+
> => {
|
|
534
|
+
if (interpretation._tag === "ResetRequired") {
|
|
535
|
+
return PaginatedQueryResult.loadingFirstPage({ skipped });
|
|
536
|
+
}
|
|
537
|
+
if (interpretation._tag === "Failed") {
|
|
538
|
+
const error = interpretation.error;
|
|
539
|
+
if (Ref.isConvexError(error)) {
|
|
540
|
+
const decoded = Ref.decodeErrorOption(ref, error.data);
|
|
541
|
+
if (Option.isSome(decoded)) {
|
|
542
|
+
return PaginatedQueryResult.failure({
|
|
543
|
+
error: decoded.value,
|
|
544
|
+
results: decodedResults,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Unknown errors still throw, to be caught by an error boundary.
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const { lastResult } = interpretation;
|
|
553
|
+
if (Option.isNone(lastResult) && current.state.pageKeys.length <= 1) {
|
|
554
|
+
return PaginatedQueryResult.loadingFirstPage({ skipped });
|
|
555
|
+
}
|
|
556
|
+
if (
|
|
557
|
+
Option.isNone(lastResult) ||
|
|
558
|
+
StreamPagination.isLastPageSplitting(current.state)
|
|
559
|
+
) {
|
|
560
|
+
return PaginatedQueryResult.loadingMore({ results: decodedResults });
|
|
561
|
+
}
|
|
562
|
+
if (lastResult.value.isDone) {
|
|
563
|
+
return PaginatedQueryResult.exhausted({ results: decodedResults });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const continueCursor = lastResult.value.continueCursor;
|
|
567
|
+
// A per-result guard, as in `convex-helpers`: calling `loadMore`
|
|
568
|
+
// repeatedly before the next render is a single load.
|
|
569
|
+
let alreadyLoadingMore = false;
|
|
570
|
+
return PaginatedQueryResult.canLoadMore({
|
|
571
|
+
results: decodedResults,
|
|
572
|
+
loadMore: (numItems: number) => {
|
|
573
|
+
if (!alreadyLoadingMore) {
|
|
574
|
+
alreadyLoadingMore = true;
|
|
575
|
+
applyTransitions([
|
|
576
|
+
StreamPagination.loadMore(continueCursor, numItems, options),
|
|
577
|
+
]);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}, [
|
|
582
|
+
interpretation,
|
|
583
|
+
decodedResults,
|
|
584
|
+
current.state,
|
|
585
|
+
skipped,
|
|
586
|
+
ref,
|
|
587
|
+
applyTransitions,
|
|
588
|
+
]) as PaginatedQueryResult.PaginatedQueryResult<
|
|
589
|
+
PaginatedQueryItem<Query>,
|
|
590
|
+
Ref.Error<Query>
|
|
591
|
+
>;
|
|
592
|
+
};
|
|
593
|
+
|
|
319
594
|
/**
|
|
320
595
|
* An optimistic update for a Confect mutation. Mirrors Convex's
|
|
321
596
|
* `OptimisticUpdate`, but receives a Confect {@link OptimisticLocalStore} and
|