convex-helpers 0.1.92 → 0.1.93

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
@@ -662,10 +662,15 @@ In addition to `getPage`, convex-helpers provides a function
662
662
  but does not subscribe the query to the end cursor automatically.
663
663
 
664
664
  The syntax and interface for `paginator` is so similar to `.paginate` that it is
665
- nearly a drop-in replacement and can even be used with `usePaginatedQuery`.
665
+ nearly a drop-in replacement and can even be used with `usePaginatedQuery`[^1].
666
666
  This makes it more suitable for non-reactive pagination usecases,
667
667
  such as iterating data in a mutation. Note: it supports `withIndex` but not `filter`.
668
668
 
669
+ [^1]:
670
+ Note: if you want gapless pagination, use the `usePaginatedQuery` hook in
671
+ `"convex-helpers/react"`, or if you're also using the cached query helpers, pass
672
+ `customPagination: true` for that version.
673
+
669
674
  For more information on reactive pagination and end cursors, see
670
675
  https://stack.convex.dev/fully-reactive-pagination
671
676
  and
@@ -722,11 +727,12 @@ and paginate the result.
722
727
  of documents, ordered by indexed fields.
723
728
 
724
729
  The cool thing about QueryStreams is you can make more QueryStreams from them,
725
- with operations equivalent to SQL's `UNION ALL`, `WHERE`, and
726
- `JOIN`. These operations preserve order, so the result
727
- is still a valid QueryStream. You can combine streams as much as you want, and
728
- finally treat it like a Convex query to get documents with `.first()`,
729
- `.collect()`, or `.paginate()`.
730
+ with operations equivalent to SQL's `UNION ALL`, `WHERE`, and `JOIN`.
731
+ These operations preserve order, so the result is still a valid QueryStream.
732
+ You can combine streams as much as you want, and finally treat it like a
733
+ Convex query to get documents with `.first()`, `.collect()`, or `.paginate()`.
734
+ See [this Stack post](https://stack.convex.dev/translate-sql-into-convex-queries)
735
+ for examples of translating SQL queries into Convex queries.
730
736
 
731
737
  For example, if you have a stream of "messages created by user1" and a stream
732
738
  of "messages created by user2", you can get a stream of
@@ -735,6 +741,9 @@ by creation time (or whatever the order is of the index you're using). You can
735
741
  then filter the merged stream to get a stream of "messages created by user1 or user2 that are unread". Then you
736
742
  can paginate the result.
737
743
 
744
+ See [this Stack post](https://stack.convex.dev/merging-streams-of-convex-data)
745
+ for more information.
746
+
738
747
  Concrete functions you can use:
739
748
 
740
749
  - `stream` constructs a stream using the same syntax as `DatabaseReader`.
@@ -746,9 +755,11 @@ Concrete functions you can use:
746
755
  - Once your stream is set up, you can get documents from it with the normal
747
756
  Convex query methods: `.first()`, `.collect()`, `.paginate()`, etc.
748
757
 
749
- Beware if using `.paginate()` with streams in reactive queries, as it has the
750
- same problems as [`paginator` and `getPage`](#manual-pagination): you need to
751
- pass in `endCursor` to prevent holes or overlaps between the pages.
758
+ Note: if using `.paginate()` with streams in reactive queries, use the
759
+ `usePaginatedQuery` hook from `"convex-helpers/react"`, or if you're also using
760
+ the cached query helpers, pass `customPagination: true` for that version.
761
+ It has the same behavior as [`paginator` and `getPage`](#manual-pagination) in
762
+ that you need to pass in `endCursor` to prevent holes or overlaps between pages.
752
763
 
753
764
  ### Example 1: Paginate all messages by a fixed set of authors
754
765
 
@@ -920,19 +931,23 @@ server for some expiration period even after app `useQuery` hooks have all
920
931
  unmounted. This allows very fast reloading of unevicted values during
921
932
  navigation changes, view changes, etc.
922
933
 
934
+ Note: unlike other forms of caching, subscription caching will mean strictly
935
+ more bandwidth usage, because it will keep the subscription open even after
936
+ the component unmounts. This is for optimizing the user experience, not database
937
+ bandwidth.
938
+
923
939
  Related files:
924
940
 
925
941
  - [cache.ts](./react/cache.ts) re-exports things so you can import from a single convenient location.
926
942
  - [provider.tsx](./react/cache/provider.tsx) contains `ConvexQueryCacheProvider`,
927
943
  a configurable cache provider you put in your react app's root.
928
944
  - [hooks.ts](./react/cache/hooks.ts) contains cache-enabled drop-in
929
- replacements for both `useQuery` and `useQueries` from `convex/react`.
945
+ replacements for `useQuery`, `usePaginatedQuery`, and `useQueries`.
930
946
 
931
947
  To use the cache, first make sure to put a `<ConvexQueryCacheProvider>`
932
948
  inside `<ConvexProvider>` in your react component tree:
933
949
 
934
- ```jsx
935
-
950
+ ```tsx
936
951
  import { ConvexQueryCacheProvider } from "convex-helpers/react/cache";
937
952
  // For Next.js, import from "convex-helpers/react/cache/provider"; instead
938
953
 
@@ -966,7 +981,7 @@ This provider takes three optional props:
966
981
  Finally, you can utilize `useQuery` (and `useQueries`) just the same as
967
982
  their `convex/react` equivalents.
968
983
 
969
- ```jsx
984
+ ```tsx
970
985
  import { useQuery } from "convex-helpers/react/cache";
971
986
  // For Next.js, import from "convex-helpers/react/cache/hooks"; instead
972
987
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "convex-helpers",
3
- "version": "0.1.92",
3
+ "version": "0.1.93",
4
4
  "description": "A collection of useful code to complement the official convex package.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -105,13 +105,10 @@ export declare function useQuery<Query extends FunctionReference<"query">>(query
105
105
  * @param args - The arguments object for the query function, excluding
106
106
  * the `paginationOpts` property. That property is injected by this hook.
107
107
  * @param options - An object specifying the `initialNumItems` to be loaded in
108
- * the first page, and the `latestPageSize` to use.
109
- * @param options.latestPageSize controls how the latest page (the first page
110
- * until another page is loaded) size grows. With "fixed", the page size will
111
- * stay at the size specified by `initialNumItems` / `loadMore`. With "grow",
112
- * the page size will grow as new items are added within the range of the initial
113
- * page. Once multiple pages are loaded, all but the last page will grow, in
114
- * order to provide seamless pagination. See the docs for more details.
108
+ * the first page, and the `customPagination` to use.
109
+ * @param options.customPagination - Set this to true when you are using
110
+ * `stream` or `paginator` helpers on the server. This enables gapless
111
+ * pagination by connecting the pages explicitly when calling `loadMore`.
115
112
  * @returns A {@link UsePaginatedQueryResult} that includes the currently loaded
116
113
  * items, the status of the pagination, and a `loadMore` function.
117
114
  *
@@ -119,6 +116,9 @@ export declare function useQuery<Query extends FunctionReference<"query">>(query
119
116
  */
120
117
  export declare function usePaginatedQuery<Query extends PaginatedQueryReference>(query: Query, args: PaginatedQueryArgs<Query> | "skip", options: {
121
118
  initialNumItems: number;
122
- latestPageSize?: "grow" | "fixed";
119
+ /**
120
+ * Set this to true if you are using the `stream` or `paginator` helpers.
121
+ */
122
+ customPagination?: boolean;
123
123
  }): UsePaginatedQueryReturnType<Query>;
124
124
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAEV,iBAAiB,EACjB,kBAAkB,EAInB,MAAM,eAAe,CAAC;AAkBvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,SAAS,GAAG,KAAK,CAAC,CAiCzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC/D,KAAK,EAAE,KAAK,EACZ,GAAG,SAAS,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAC1C,kBAAkB,CAAC,KAAK,CAAC,GAAG,SAAS,CAoBvC;AAoHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,uBAAuB,EACrE,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,GAAG,MAAM,EACxC,OAAO,EAAE;IACP,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC,GACA,2BAA2B,CAAC,KAAK,CAAC,CAqPpC"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAEV,iBAAiB,EACjB,kBAAkB,EAInB,MAAM,eAAe,CAAC;AAkBvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,iBAAiB,GACzB,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,SAAS,GAAG,KAAK,CAAC,CAiCzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC/D,KAAK,EAAE,KAAK,EACZ,GAAG,SAAS,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAC1C,kBAAkB,CAAC,KAAK,CAAC,GAAG,SAAS,CAoBvC;AAoHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,uBAAuB,EACrE,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,GAAG,MAAM,EACxC,OAAO,EAAE;IACP,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GACA,2BAA2B,CAAC,KAAK,CAAC,CA2PpC"}
@@ -234,13 +234,10 @@ const completeSplitQuery = (key) => (prevState) => {
234
234
  * @param args - The arguments object for the query function, excluding
235
235
  * the `paginationOpts` property. That property is injected by this hook.
236
236
  * @param options - An object specifying the `initialNumItems` to be loaded in
237
- * the first page, and the `latestPageSize` to use.
238
- * @param options.latestPageSize controls how the latest page (the first page
239
- * until another page is loaded) size grows. With "fixed", the page size will
240
- * stay at the size specified by `initialNumItems` / `loadMore`. With "grow",
241
- * the page size will grow as new items are added within the range of the initial
242
- * page. Once multiple pages are loaded, all but the last page will grow, in
243
- * order to provide seamless pagination. See the docs for more details.
237
+ * the first page, and the `customPagination` to use.
238
+ * @param options.customPagination - Set this to true when you are using
239
+ * `stream` or `paginator` helpers on the server. This enables gapless
240
+ * pagination by connecting the pages explicitly when calling `loadMore`.
244
241
  * @returns A {@link UsePaginatedQueryResult} that includes the currently loaded
245
242
  * items, the status of the pagination, and a `loadMore` function.
246
243
  *
@@ -296,10 +293,10 @@ export function usePaginatedQuery(query, args, options) {
296
293
  const [state, setState] = useState(createInitialState);
297
294
  // `currState` is the state that we'll render based on.
298
295
  let currState = state;
299
- if (getFunctionName(query) !== getFunctionName(state.query) ||
296
+ if (skip !== state.skip ||
297
+ getFunctionName(query) !== getFunctionName(state.query) ||
300
298
  JSON.stringify(convexToJson(argsObject)) !==
301
- JSON.stringify(convexToJson(state.args)) ||
302
- skip !== state.skip) {
299
+ JSON.stringify(convexToJson(state.args))) {
303
300
  currState = createInitialState();
304
301
  setState(currState);
305
302
  }
@@ -346,8 +343,11 @@ export function usePaginatedQuery(query, args, options) {
346
343
  else if (currResult.splitCursor &&
347
344
  (currResult.pageStatus === "SplitRecommended" ||
348
345
  currResult.pageStatus === "SplitRequired" ||
349
- currResult.page.length > options.initialNumItems * 2)) {
350
- // If a single page has more than double the expected number of items,
346
+ (options.customPagination
347
+ ? // For custom pagination, we eagerly split the page when it grows.
348
+ currResult.page.length > options.initialNumItems
349
+ : currResult.page.length > options.initialNumItems * 2))) {
350
+ // If a single page has more than 1.5x the expected number of items,
351
351
  // or if the server requests a split, split the page into two.
352
352
  setState(splitQuery(pageKey, currResult.splitCursor, currResult.continueCursor));
353
353
  }
@@ -411,7 +411,10 @@ export function usePaginatedQuery(query, args, options) {
411
411
  const queries = { ...prevState.queries };
412
412
  let ongoingSplits = prevState.ongoingSplits;
413
413
  let pageKeys = prevState.pageKeys;
414
- if (options.latestPageSize !== "grow") {
414
+ if (options.customPagination) {
415
+ // Connect the current last page to the next page
416
+ // by setting the endCursor of the last page to the continueCursor
417
+ // of the next page.
415
418
  const lastPageKey = prevState.pageKeys.at(-1);
416
419
  const boundLastPageKey = nextPageKey;
417
420
  queries[boundLastPageKey] = {
@@ -457,7 +460,7 @@ export function usePaginatedQuery(query, args, options) {
457
460
  }
458
461
  },
459
462
  };
460
- }, [maybeLastResult, currState.nextPageKey, options.latestPageSize]);
463
+ }, [maybeLastResult, currState.nextPageKey, options.customPagination]);
461
464
  return {
462
465
  results,
463
466
  ...statusObject,
@@ -314,13 +314,10 @@ const completeSplitQuery =
314
314
  * @param args - The arguments object for the query function, excluding
315
315
  * the `paginationOpts` property. That property is injected by this hook.
316
316
  * @param options - An object specifying the `initialNumItems` to be loaded in
317
- * the first page, and the `latestPageSize` to use.
318
- * @param options.latestPageSize controls how the latest page (the first page
319
- * until another page is loaded) size grows. With "fixed", the page size will
320
- * stay at the size specified by `initialNumItems` / `loadMore`. With "grow",
321
- * the page size will grow as new items are added within the range of the initial
322
- * page. Once multiple pages are loaded, all but the last page will grow, in
323
- * order to provide seamless pagination. See the docs for more details.
317
+ * the first page, and the `customPagination` to use.
318
+ * @param options.customPagination - Set this to true when you are using
319
+ * `stream` or `paginator` helpers on the server. This enables gapless
320
+ * pagination by connecting the pages explicitly when calling `loadMore`.
324
321
  * @returns A {@link UsePaginatedQueryResult} that includes the currently loaded
325
322
  * items, the status of the pagination, and a `loadMore` function.
326
323
  *
@@ -331,7 +328,10 @@ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
331
328
  args: PaginatedQueryArgs<Query> | "skip",
332
329
  options: {
333
330
  initialNumItems: number;
334
- latestPageSize?: "grow" | "fixed";
331
+ /**
332
+ * Set this to true if you are using the `stream` or `paginator` helpers.
333
+ */
334
+ customPagination?: boolean;
335
335
  },
336
336
  ): UsePaginatedQueryReturnType<Query> {
337
337
  if (
@@ -391,10 +391,10 @@ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
391
391
  // `currState` is the state that we'll render based on.
392
392
  let currState = state;
393
393
  if (
394
+ skip !== state.skip ||
394
395
  getFunctionName(query) !== getFunctionName(state.query) ||
395
396
  JSON.stringify(convexToJson(argsObject as Value)) !==
396
- JSON.stringify(convexToJson(state.args)) ||
397
- skip !== state.skip
397
+ JSON.stringify(convexToJson(state.args))
398
398
  ) {
399
399
  currState = createInitialState();
400
400
  setState(currState);
@@ -455,9 +455,12 @@ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
455
455
  currResult.splitCursor &&
456
456
  (currResult.pageStatus === "SplitRecommended" ||
457
457
  currResult.pageStatus === "SplitRequired" ||
458
- currResult.page.length > options.initialNumItems * 2)
458
+ (options.customPagination
459
+ ? // For custom pagination, we eagerly split the page when it grows.
460
+ currResult.page.length > options.initialNumItems
461
+ : currResult.page.length > options.initialNumItems * 2))
459
462
  ) {
460
- // If a single page has more than double the expected number of items,
463
+ // If a single page has more than 1.5x the expected number of items,
461
464
  // or if the server requests a split, split the page into two.
462
465
  setState(
463
466
  splitQuery(
@@ -527,7 +530,10 @@ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
527
530
  const queries = { ...prevState.queries };
528
531
  let ongoingSplits = prevState.ongoingSplits;
529
532
  let pageKeys = prevState.pageKeys;
530
- if (options.latestPageSize !== "grow") {
533
+ if (options.customPagination) {
534
+ // Connect the current last page to the next page
535
+ // by setting the endCursor of the last page to the continueCursor
536
+ // of the next page.
531
537
  const lastPageKey = prevState.pageKeys.at(-1)!;
532
538
  const boundLastPageKey = nextPageKey;
533
539
  queries[boundLastPageKey] = {
@@ -572,7 +578,7 @@ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
572
578
  }
573
579
  },
574
580
  } as const;
575
- }, [maybeLastResult, currState.nextPageKey, options.latestPageSize]);
581
+ }, [maybeLastResult, currState.nextPageKey, options.customPagination]);
576
582
 
577
583
  return {
578
584
  results,
package/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OptionalRestArgsOrSkip } from "convex/react";
1
+ import type { OptionalRestArgsOrSkip, PaginatedQueryArgs, PaginatedQueryReference, UsePaginatedQueryReturnType } from "convex/react";
2
2
  import { useQueries } from "convex/react";
3
3
  import type { FunctionReference, FunctionReturnType } from "convex/server";
4
4
  /**
@@ -117,4 +117,29 @@ export declare function makeUseQueryWithStatus(useQueriesHook: typeof useQueries
117
117
  isPending: false;
118
118
  isError: true;
119
119
  };
120
+ /**
121
+ * This is a clone of the `usePaginatedQuery` hook from `convex/react` made for
122
+ * use with the `stream` and `paginator` helpers, which don't automatically
123
+ * "grow" until you explicitly pass the `endCursor` arg.
124
+ *
125
+ * For these, we wait to set the end cursor until `loadMore` is called.
126
+ * So the first page will be a fixed size until the first call to `loadMore`,
127
+ * at which point the second page will start where the first page ended, and the
128
+ * first page will explicitly "pin" that end cursor. From then on, the last page
129
+ * will also be a fixed size until the next call to `loadMore`. This is less
130
+ * noticeable because typically the first page is the only page that grows.
131
+ *
132
+ * To use the cached query helpers, you can use those directly and pass
133
+ * `customPagination: true` in the options.
134
+ *
135
+ * Docs copied from {@link usePaginatedQueryOriginal} until `returns` block:
136
+ *
137
+ * @param query - a {@link server.FunctionReference} for the public query to run
138
+ * like `api.dir1.dir2.filename.func`.
139
+ * @param args - The arguments to the query function or the string "skip" if the
140
+ * query should not be loaded.
141
+ */
142
+ export declare function usePaginatedQuery<Query extends PaginatedQueryReference>(query: Query, args: PaginatedQueryArgs<Query> | "skip", options: {
143
+ initialNumItems: number;
144
+ }): UsePaginatedQueryReturnType<Query>;
120
145
  //# sourceMappingURL=react.d.ts.map
package/react.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["react.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAgC,MAAM,cAAc,CAAC;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAK3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,QAAQ,GA0CM,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,SACxD,KAAK,gBACE,sBAAsB,CAAC,KAAK,CAAC,KAEzC;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,AArEmD,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,OAAO,UAAU,IAC7C,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC/D,OAAO,KAAK,EACZ,GAAG,WAAW,sBAAsB,CAAC,KAAK,CAAC,KAEzC;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,CAuDN"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["react.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,kBAAkB,EAClB,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,cAAc,CAAC;AAGtB,OAAO,EAEL,UAAU,EAEX,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAInB,MAAM,eAAe,CAAC;AAOvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,QAAQ,GA0CM,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,SACxD,KAAK,gBACE,sBAAsB,CAAC,KAAK,CAAC,KAEzC;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,AArEmD,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,OAAO,UAAU,IAC7C,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC/D,OAAO,KAAK,EACZ,GAAG,WAAW,sBAAsB,CAAC,KAAK,CAAC,KAEzC;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC;CAChB,GACD;IACE,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;IACjB,SAAS,EAAE,KAAK,CAAC;IACjB,OAAO,EAAE,IAAI,CAAC;CACf,CAuDN;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,uBAAuB,EACrE,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,kBAAkB,CAAC,KAAK,CAAC,GAAG,MAAM,EACxC,OAAO,EAAE;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,GACnC,2BAA2B,CAAC,KAAK,CAAC,CA6OpC"}
package/react.js CHANGED
@@ -1,6 +1,8 @@
1
- import { useQueries, useQuery as useQueryOriginal } from "convex/react";
1
+ import { ConvexError } from "convex/values";
2
+ import { convexToJson } from "convex/values";
3
+ import { useConvex, useQueries, useQuery as useQueryOriginal, } from "convex/react";
2
4
  import { getFunctionName } from "convex/server";
3
- import { useMemo } from "react";
5
+ import { useMemo, useState } from "react";
4
6
  /**
5
7
  * Use in place of `useQuery` from "convex/react" to fetch data from a query
6
8
  * function but instead returns `{ status, data, error, isSuccess, isPending, isError}`.
@@ -131,3 +133,297 @@ export function makeUseQueryWithStatus(useQueriesHook) {
131
133
  };
132
134
  };
133
135
  }
136
+ /**
137
+ * This is a clone of the `usePaginatedQuery` hook from `convex/react` made for
138
+ * use with the `stream` and `paginator` helpers, which don't automatically
139
+ * "grow" until you explicitly pass the `endCursor` arg.
140
+ *
141
+ * For these, we wait to set the end cursor until `loadMore` is called.
142
+ * So the first page will be a fixed size until the first call to `loadMore`,
143
+ * at which point the second page will start where the first page ended, and the
144
+ * first page will explicitly "pin" that end cursor. From then on, the last page
145
+ * will also be a fixed size until the next call to `loadMore`. This is less
146
+ * noticeable because typically the first page is the only page that grows.
147
+ *
148
+ * To use the cached query helpers, you can use those directly and pass
149
+ * `customPagination: true` in the options.
150
+ *
151
+ * Docs copied from {@link usePaginatedQueryOriginal} until `returns` block:
152
+ *
153
+ * @param query - a {@link server.FunctionReference} for the public query to run
154
+ * like `api.dir1.dir2.filename.func`.
155
+ * @param args - The arguments to the query function or the string "skip" if the
156
+ * query should not be loaded.
157
+ */
158
+ export function usePaginatedQuery(query, args, options) {
159
+ if (typeof options?.initialNumItems !== "number" ||
160
+ options.initialNumItems <= 0) {
161
+ throw new Error(`\`options.initialNumItems\` must be a positive number. Received \`${options?.initialNumItems}\`.`);
162
+ }
163
+ const skip = args === "skip";
164
+ const argsObject = skip ? {} : args;
165
+ const queryName = getFunctionName(query);
166
+ const createInitialState = useMemo(() => {
167
+ return () => {
168
+ return {
169
+ query,
170
+ args: argsObject,
171
+ nextPageKey: 1,
172
+ pageKeys: skip ? [] : [0],
173
+ queries: skip
174
+ ? {}
175
+ : {
176
+ 0: {
177
+ query,
178
+ args: {
179
+ ...argsObject,
180
+ paginationOpts: {
181
+ numItems: options.initialNumItems,
182
+ cursor: null,
183
+ },
184
+ },
185
+ },
186
+ },
187
+ ongoingSplits: {},
188
+ skip,
189
+ };
190
+ };
191
+ // ESLint doesn't like that we're stringifying the args. We do this because
192
+ // we want to avoid rerendering if the args are a different
193
+ // object that serializes to the same result.
194
+ // eslint-disable-next-line react-hooks/exhaustive-deps
195
+ }, [
196
+ // eslint-disable-next-line react-hooks/exhaustive-deps
197
+ JSON.stringify(convexToJson(argsObject)),
198
+ queryName,
199
+ options.initialNumItems,
200
+ skip,
201
+ ]);
202
+ const [state, setState] = useState(createInitialState);
203
+ // `currState` is the state that we'll render based on.
204
+ let currState = state;
205
+ if (skip !== state.skip ||
206
+ getFunctionName(query) !== getFunctionName(state.query) ||
207
+ JSON.stringify(convexToJson(argsObject)) !==
208
+ JSON.stringify(convexToJson(state.args))) {
209
+ currState = createInitialState();
210
+ setState(currState);
211
+ }
212
+ const convexClient = useConvex();
213
+ const logger = convexClient.logger;
214
+ const resultsObject = useQueries(currState.queries);
215
+ const [results, maybeLastResult] = useMemo(() => {
216
+ let currResult = undefined;
217
+ const allItems = [];
218
+ for (const pageKey of currState.pageKeys) {
219
+ currResult = resultsObject[pageKey];
220
+ if (currResult === undefined) {
221
+ break;
222
+ }
223
+ if (currResult instanceof Error) {
224
+ if (currResult.message.includes("InvalidCursor") ||
225
+ (currResult instanceof ConvexError &&
226
+ typeof currResult.data === "object" &&
227
+ currResult.data?.isConvexSystemError === true &&
228
+ currResult.data?.paginationError === "InvalidCursor")) {
229
+ // - InvalidCursor: If the cursor is invalid, probably the paginated
230
+ // database query was data-dependent and changed underneath us. The
231
+ // cursor in the params or journal no longer matches the current
232
+ // database query.
233
+ // In all cases, we want to restart pagination to throw away all our
234
+ // existing cursors.
235
+ logger.warn("usePaginatedQuery hit error, resetting pagination state: " +
236
+ currResult.message);
237
+ setState(createInitialState);
238
+ return [[], undefined];
239
+ }
240
+ else {
241
+ throw currResult;
242
+ }
243
+ }
244
+ const ongoingSplit = currState.ongoingSplits[pageKey];
245
+ if (ongoingSplit !== undefined) {
246
+ if (resultsObject[ongoingSplit[0]] !== undefined &&
247
+ resultsObject[ongoingSplit[1]] !== undefined) {
248
+ // Both pages of the split have results now. Swap them in.
249
+ setState(completeSplitQuery(pageKey));
250
+ }
251
+ }
252
+ else if (currResult.splitCursor &&
253
+ (currResult.pageStatus === "SplitRecommended" ||
254
+ currResult.pageStatus === "SplitRequired" ||
255
+ // For custom pagination, we eagerly split the page when it grows.
256
+ currResult.page.length > options.initialNumItems)) {
257
+ setState(splitQuery(pageKey, currResult.splitCursor, currResult.continueCursor));
258
+ }
259
+ if (currResult.pageStatus === "SplitRequired") {
260
+ // If pageStatus is 'SplitRequired', it means the server was not able to
261
+ // fetch the full page. So we stop results before the incomplete
262
+ // page and return 'LoadingMore' while the page is splitting.
263
+ return [allItems, undefined];
264
+ }
265
+ allItems.push(...currResult.page);
266
+ }
267
+ return [allItems, currResult];
268
+ }, [
269
+ resultsObject,
270
+ currState.pageKeys,
271
+ currState.ongoingSplits,
272
+ options.initialNumItems,
273
+ createInitialState,
274
+ logger,
275
+ ]);
276
+ const statusObject = useMemo(() => {
277
+ if (maybeLastResult === undefined) {
278
+ if (currState.nextPageKey === 1) {
279
+ return {
280
+ status: "LoadingFirstPage",
281
+ isLoading: true,
282
+ loadMore: (_numItems) => {
283
+ // Intentional noop.
284
+ },
285
+ };
286
+ }
287
+ else {
288
+ return {
289
+ status: "LoadingMore",
290
+ isLoading: true,
291
+ loadMore: (_numItems) => {
292
+ // Intentional noop.
293
+ },
294
+ };
295
+ }
296
+ }
297
+ if (maybeLastResult.isDone) {
298
+ return {
299
+ status: "Exhausted",
300
+ isLoading: false,
301
+ loadMore: (_numItems) => {
302
+ // Intentional noop.
303
+ },
304
+ };
305
+ }
306
+ const continueCursor = maybeLastResult.continueCursor;
307
+ let alreadyLoadingMore = false;
308
+ return {
309
+ status: "CanLoadMore",
310
+ isLoading: false,
311
+ loadMore: (numItems) => {
312
+ if (!alreadyLoadingMore) {
313
+ alreadyLoadingMore = true;
314
+ setState((prevState) => {
315
+ let nextPageKey = prevState.nextPageKey;
316
+ const queries = { ...prevState.queries };
317
+ let ongoingSplits = prevState.ongoingSplits;
318
+ // Connect the current last page to the next page
319
+ // by setting the endCursor of the last page to the continueCursor
320
+ // of the next page.
321
+ const lastPageKey = prevState.pageKeys.at(-1);
322
+ const boundLastPageKey = nextPageKey;
323
+ queries[boundLastPageKey] = {
324
+ query: prevState.query,
325
+ args: {
326
+ ...prevState.args,
327
+ paginationOpts: {
328
+ ...queries[lastPageKey].args
329
+ .paginationOpts,
330
+ endCursor: continueCursor,
331
+ },
332
+ },
333
+ };
334
+ nextPageKey++;
335
+ ongoingSplits = {
336
+ ...ongoingSplits,
337
+ [lastPageKey]: [boundLastPageKey, nextPageKey],
338
+ };
339
+ queries[nextPageKey] = {
340
+ query: prevState.query,
341
+ args: {
342
+ ...prevState.args,
343
+ paginationOpts: {
344
+ numItems,
345
+ cursor: continueCursor,
346
+ },
347
+ },
348
+ };
349
+ nextPageKey++;
350
+ return {
351
+ ...prevState,
352
+ nextPageKey,
353
+ queries,
354
+ ongoingSplits,
355
+ };
356
+ });
357
+ }
358
+ },
359
+ };
360
+ }, [maybeLastResult, currState.nextPageKey]);
361
+ return {
362
+ results,
363
+ ...statusObject,
364
+ };
365
+ }
366
+ function splitQuery(key, splitCursor, continueCursor) {
367
+ return (prevState) => {
368
+ const queries = { ...prevState.queries };
369
+ const splitKey1 = prevState.nextPageKey;
370
+ const splitKey2 = prevState.nextPageKey + 1;
371
+ const nextPageKey = prevState.nextPageKey + 2;
372
+ queries[splitKey1] = {
373
+ query: prevState.query,
374
+ args: {
375
+ ...prevState.args,
376
+ paginationOpts: {
377
+ ...prevState.queries[key].args.paginationOpts,
378
+ endCursor: splitCursor,
379
+ },
380
+ },
381
+ };
382
+ queries[splitKey2] = {
383
+ query: prevState.query,
384
+ args: {
385
+ ...prevState.args,
386
+ paginationOpts: {
387
+ ...prevState.queries[key].args.paginationOpts,
388
+ cursor: splitCursor,
389
+ endCursor: continueCursor,
390
+ },
391
+ },
392
+ };
393
+ const ongoingSplits = { ...prevState.ongoingSplits };
394
+ ongoingSplits[key] = [splitKey1, splitKey2];
395
+ return {
396
+ ...prevState,
397
+ nextPageKey,
398
+ queries,
399
+ ongoingSplits,
400
+ };
401
+ };
402
+ }
403
+ function completeSplitQuery(key) {
404
+ return (prevState) => {
405
+ const completedSplit = prevState.ongoingSplits[key];
406
+ if (completedSplit === undefined) {
407
+ return prevState;
408
+ }
409
+ const queries = { ...prevState.queries };
410
+ delete queries[key];
411
+ const ongoingSplits = { ...prevState.ongoingSplits };
412
+ delete ongoingSplits[key];
413
+ let pageKeys = prevState.pageKeys.slice();
414
+ const pageIndex = prevState.pageKeys.findIndex((v) => v === key);
415
+ if (pageIndex >= 0) {
416
+ pageKeys = [
417
+ ...prevState.pageKeys.slice(0, pageIndex),
418
+ ...completedSplit,
419
+ ...prevState.pageKeys.slice(pageIndex + 1),
420
+ ];
421
+ }
422
+ return {
423
+ ...prevState,
424
+ queries,
425
+ pageKeys,
426
+ ongoingSplits,
427
+ };
428
+ };
429
+ }
package/react.ts CHANGED
@@ -1,9 +1,28 @@
1
- import type { OptionalRestArgsOrSkip } from "convex/react";
2
- import { useQueries, useQuery as useQueryOriginal } from "convex/react";
3
- import type { FunctionReference, FunctionReturnType } from "convex/server";
1
+ import type {
2
+ OptionalRestArgsOrSkip,
3
+ PaginatedQueryArgs,
4
+ PaginatedQueryReference,
5
+ UsePaginatedQueryReturnType,
6
+ } from "convex/react";
7
+ import { ConvexError } from "convex/values";
8
+ import { convexToJson } from "convex/values";
9
+ import {
10
+ useConvex,
11
+ useQueries,
12
+ useQuery as useQueryOriginal,
13
+ } from "convex/react";
14
+ import type {
15
+ FunctionReference,
16
+ FunctionReturnType,
17
+ PaginationOptions,
18
+ paginationOptsValidator,
19
+ PaginationResult,
20
+ } from "convex/server";
21
+ import type { Infer } from "convex/values";
4
22
  import { getFunctionName } from "convex/server";
5
- import { useMemo } from "react";
23
+ import { useMemo, useState } from "react";
6
24
  import type { EmptyObject } from "./index.js";
25
+ import type { Value } from "convex/values";
7
26
 
8
27
  /**
9
28
  * Use in place of `useQuery` from "convex/react" to fetch data from a query
@@ -163,3 +182,370 @@ export function makeUseQueryWithStatus(useQueriesHook: typeof useQueries) {
163
182
  };
164
183
  };
165
184
  }
185
+
186
+ /**
187
+ * This is a clone of the `usePaginatedQuery` hook from `convex/react` made for
188
+ * use with the `stream` and `paginator` helpers, which don't automatically
189
+ * "grow" until you explicitly pass the `endCursor` arg.
190
+ *
191
+ * For these, we wait to set the end cursor until `loadMore` is called.
192
+ * So the first page will be a fixed size until the first call to `loadMore`,
193
+ * at which point the second page will start where the first page ended, and the
194
+ * first page will explicitly "pin" that end cursor. From then on, the last page
195
+ * will also be a fixed size until the next call to `loadMore`. This is less
196
+ * noticeable because typically the first page is the only page that grows.
197
+ *
198
+ * To use the cached query helpers, you can use those directly and pass
199
+ * `customPagination: true` in the options.
200
+ *
201
+ * Docs copied from {@link usePaginatedQueryOriginal} until `returns` block:
202
+ *
203
+ * @param query - a {@link server.FunctionReference} for the public query to run
204
+ * like `api.dir1.dir2.filename.func`.
205
+ * @param args - The arguments to the query function or the string "skip" if the
206
+ * query should not be loaded.
207
+ */
208
+ export function usePaginatedQuery<Query extends PaginatedQueryReference>(
209
+ query: Query,
210
+ args: PaginatedQueryArgs<Query> | "skip",
211
+ options: { initialNumItems: number },
212
+ ): UsePaginatedQueryReturnType<Query> {
213
+ if (
214
+ typeof options?.initialNumItems !== "number" ||
215
+ options.initialNumItems <= 0
216
+ ) {
217
+ throw new Error(
218
+ `\`options.initialNumItems\` must be a positive number. Received \`${options?.initialNumItems}\`.`,
219
+ );
220
+ }
221
+ const skip = args === "skip";
222
+ const argsObject = skip ? {} : args;
223
+ const queryName = getFunctionName(query);
224
+ const createInitialState = useMemo(() => {
225
+ return () => {
226
+ return {
227
+ query,
228
+ args: argsObject as Record<string, Value>,
229
+ nextPageKey: 1,
230
+ pageKeys: skip ? [] : [0],
231
+ queries: skip
232
+ ? ({} as UsePaginatedQueryState["queries"])
233
+ : {
234
+ 0: {
235
+ query,
236
+ args: {
237
+ ...argsObject,
238
+ paginationOpts: {
239
+ numItems: options.initialNumItems,
240
+ cursor: null,
241
+ },
242
+ },
243
+ },
244
+ },
245
+ ongoingSplits: {},
246
+ skip,
247
+ };
248
+ };
249
+ // ESLint doesn't like that we're stringifying the args. We do this because
250
+ // we want to avoid rerendering if the args are a different
251
+ // object that serializes to the same result.
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ }, [
254
+ // eslint-disable-next-line react-hooks/exhaustive-deps
255
+ JSON.stringify(convexToJson(argsObject as Value)),
256
+ queryName,
257
+ options.initialNumItems,
258
+ skip,
259
+ ]);
260
+
261
+ const [state, setState] =
262
+ useState<UsePaginatedQueryState>(createInitialState);
263
+
264
+ // `currState` is the state that we'll render based on.
265
+ let currState = state;
266
+ if (
267
+ skip !== state.skip ||
268
+ getFunctionName(query) !== getFunctionName(state.query) ||
269
+ JSON.stringify(convexToJson(argsObject as Value)) !==
270
+ JSON.stringify(convexToJson(state.args))
271
+ ) {
272
+ currState = createInitialState();
273
+ setState(currState);
274
+ }
275
+ const convexClient = useConvex();
276
+ const logger = convexClient.logger;
277
+
278
+ const resultsObject = useQueries(currState.queries);
279
+
280
+ const [results, maybeLastResult]: [
281
+ Value[],
282
+ undefined | PaginationResult<Value>,
283
+ ] = useMemo(() => {
284
+ let currResult: PaginationResult<Value> | undefined = undefined;
285
+
286
+ const allItems: Value[] = [];
287
+ for (const pageKey of currState.pageKeys) {
288
+ currResult = resultsObject[pageKey];
289
+ if (currResult === undefined) {
290
+ break;
291
+ }
292
+
293
+ if (currResult instanceof Error) {
294
+ if (
295
+ currResult.message.includes("InvalidCursor") ||
296
+ (currResult instanceof ConvexError &&
297
+ typeof currResult.data === "object" &&
298
+ currResult.data?.isConvexSystemError === true &&
299
+ currResult.data?.paginationError === "InvalidCursor")
300
+ ) {
301
+ // - InvalidCursor: If the cursor is invalid, probably the paginated
302
+ // database query was data-dependent and changed underneath us. The
303
+ // cursor in the params or journal no longer matches the current
304
+ // database query.
305
+
306
+ // In all cases, we want to restart pagination to throw away all our
307
+ // existing cursors.
308
+ logger.warn(
309
+ "usePaginatedQuery hit error, resetting pagination state: " +
310
+ currResult.message,
311
+ );
312
+ setState(createInitialState);
313
+ return [[], undefined];
314
+ } else {
315
+ throw currResult;
316
+ }
317
+ }
318
+ const ongoingSplit = currState.ongoingSplits[pageKey];
319
+ if (ongoingSplit !== undefined) {
320
+ if (
321
+ resultsObject[ongoingSplit[0]] !== undefined &&
322
+ resultsObject[ongoingSplit[1]] !== undefined
323
+ ) {
324
+ // Both pages of the split have results now. Swap them in.
325
+ setState(completeSplitQuery(pageKey));
326
+ }
327
+ } else if (
328
+ currResult.splitCursor &&
329
+ (currResult.pageStatus === "SplitRecommended" ||
330
+ currResult.pageStatus === "SplitRequired" ||
331
+ // For custom pagination, we eagerly split the page when it grows.
332
+ currResult.page.length > options.initialNumItems)
333
+ ) {
334
+ setState(
335
+ splitQuery(
336
+ pageKey,
337
+ currResult.splitCursor,
338
+ currResult.continueCursor,
339
+ ),
340
+ );
341
+ }
342
+ if (currResult.pageStatus === "SplitRequired") {
343
+ // If pageStatus is 'SplitRequired', it means the server was not able to
344
+ // fetch the full page. So we stop results before the incomplete
345
+ // page and return 'LoadingMore' while the page is splitting.
346
+ return [allItems, undefined];
347
+ }
348
+ allItems.push(...currResult.page);
349
+ }
350
+ return [allItems, currResult];
351
+ }, [
352
+ resultsObject,
353
+ currState.pageKeys,
354
+ currState.ongoingSplits,
355
+ options.initialNumItems,
356
+ createInitialState,
357
+ logger,
358
+ ]);
359
+
360
+ const statusObject = useMemo(() => {
361
+ if (maybeLastResult === undefined) {
362
+ if (currState.nextPageKey === 1) {
363
+ return {
364
+ status: "LoadingFirstPage",
365
+ isLoading: true,
366
+ loadMore: (_numItems: number) => {
367
+ // Intentional noop.
368
+ },
369
+ } as const;
370
+ } else {
371
+ return {
372
+ status: "LoadingMore",
373
+ isLoading: true,
374
+ loadMore: (_numItems: number) => {
375
+ // Intentional noop.
376
+ },
377
+ } as const;
378
+ }
379
+ }
380
+ if (maybeLastResult.isDone) {
381
+ return {
382
+ status: "Exhausted",
383
+ isLoading: false,
384
+ loadMore: (_numItems: number) => {
385
+ // Intentional noop.
386
+ },
387
+ } as const;
388
+ }
389
+ const continueCursor = maybeLastResult.continueCursor;
390
+ let alreadyLoadingMore = false;
391
+ return {
392
+ status: "CanLoadMore",
393
+ isLoading: false,
394
+ loadMore: (numItems: number) => {
395
+ if (!alreadyLoadingMore) {
396
+ alreadyLoadingMore = true;
397
+ setState((prevState) => {
398
+ let nextPageKey = prevState.nextPageKey;
399
+ const queries = { ...prevState.queries };
400
+ let ongoingSplits = prevState.ongoingSplits;
401
+ // Connect the current last page to the next page
402
+ // by setting the endCursor of the last page to the continueCursor
403
+ // of the next page.
404
+ const lastPageKey = prevState.pageKeys.at(-1)!;
405
+ const boundLastPageKey = nextPageKey;
406
+ queries[boundLastPageKey] = {
407
+ query: prevState.query,
408
+ args: {
409
+ ...prevState.args,
410
+ paginationOpts: {
411
+ ...(queries[lastPageKey]!.args
412
+ .paginationOpts as unknown as PaginationOptions),
413
+ endCursor: continueCursor,
414
+ },
415
+ },
416
+ };
417
+ nextPageKey++;
418
+ ongoingSplits = {
419
+ ...ongoingSplits,
420
+ [lastPageKey]: [boundLastPageKey, nextPageKey],
421
+ };
422
+ queries[nextPageKey] = {
423
+ query: prevState.query,
424
+ args: {
425
+ ...prevState.args,
426
+ paginationOpts: {
427
+ numItems,
428
+ cursor: continueCursor,
429
+ },
430
+ },
431
+ };
432
+ nextPageKey++;
433
+ return {
434
+ ...prevState,
435
+ nextPageKey,
436
+ queries,
437
+ ongoingSplits,
438
+ };
439
+ });
440
+ }
441
+ },
442
+ } as const;
443
+ }, [maybeLastResult, currState.nextPageKey]);
444
+
445
+ return {
446
+ results,
447
+ ...statusObject,
448
+ };
449
+ }
450
+
451
+ /**
452
+ * A {@link server.FunctionReference} that is usable with {@link usePaginatedQuery}.
453
+ *
454
+ * This function reference must:
455
+ * - Refer to a public query
456
+ * - Have an argument named "paginationOpts" of type {@link server.PaginationOptions}
457
+ * - Have a return type of {@link server.PaginationResult}.
458
+ *
459
+ * @public
460
+ */
461
+
462
+ // Incrementing integer for each page queried in the usePaginatedQuery hook.
463
+ type QueryPageKey = number;
464
+
465
+ type UsePaginatedQueryState = {
466
+ query: FunctionReference<"query">;
467
+ args: Record<string, Value>;
468
+ nextPageKey: QueryPageKey;
469
+ pageKeys: QueryPageKey[];
470
+ queries: Record<
471
+ QueryPageKey,
472
+ {
473
+ query: FunctionReference<"query">;
474
+ // Use the validator type as a test that it matches the args
475
+ // we generate.
476
+ args: { paginationOpts: Infer<typeof paginationOptsValidator> };
477
+ }
478
+ >;
479
+ ongoingSplits: Record<QueryPageKey, [QueryPageKey, QueryPageKey]>;
480
+ skip: boolean;
481
+ };
482
+
483
+ function splitQuery(
484
+ key: QueryPageKey,
485
+ splitCursor: string,
486
+ continueCursor: string,
487
+ ) {
488
+ return (prevState: UsePaginatedQueryState) => {
489
+ const queries = { ...prevState.queries };
490
+ const splitKey1 = prevState.nextPageKey;
491
+ const splitKey2 = prevState.nextPageKey + 1;
492
+ const nextPageKey = prevState.nextPageKey + 2;
493
+ queries[splitKey1] = {
494
+ query: prevState.query,
495
+ args: {
496
+ ...prevState.args,
497
+ paginationOpts: {
498
+ ...prevState.queries[key]!.args.paginationOpts,
499
+ endCursor: splitCursor,
500
+ },
501
+ },
502
+ };
503
+ queries[splitKey2] = {
504
+ query: prevState.query,
505
+ args: {
506
+ ...prevState.args,
507
+ paginationOpts: {
508
+ ...prevState.queries[key]!.args.paginationOpts,
509
+ cursor: splitCursor,
510
+ endCursor: continueCursor,
511
+ },
512
+ },
513
+ };
514
+ const ongoingSplits = { ...prevState.ongoingSplits };
515
+ ongoingSplits[key] = [splitKey1, splitKey2];
516
+ return {
517
+ ...prevState,
518
+ nextPageKey,
519
+ queries,
520
+ ongoingSplits,
521
+ };
522
+ };
523
+ }
524
+
525
+ function completeSplitQuery(key: QueryPageKey) {
526
+ return (prevState: UsePaginatedQueryState) => {
527
+ const completedSplit = prevState.ongoingSplits[key];
528
+ if (completedSplit === undefined) {
529
+ return prevState;
530
+ }
531
+ const queries = { ...prevState.queries };
532
+ delete queries[key];
533
+ const ongoingSplits = { ...prevState.ongoingSplits };
534
+ delete ongoingSplits[key];
535
+ let pageKeys = prevState.pageKeys.slice();
536
+ const pageIndex = prevState.pageKeys.findIndex((v) => v === key);
537
+ if (pageIndex >= 0) {
538
+ pageKeys = [
539
+ ...prevState.pageKeys.slice(0, pageIndex),
540
+ ...completedSplit,
541
+ ...prevState.pageKeys.slice(pageIndex + 1),
542
+ ];
543
+ }
544
+ return {
545
+ ...prevState,
546
+ queries,
547
+ pageKeys,
548
+ ongoingSplits,
549
+ };
550
+ };
551
+ }
@@ -115,8 +115,8 @@ export declare const migrationsTable: import("convex/server").TableDefinition<im
115
115
  workerId?: GenericId<"_scheduled_functions"> | undefined;
116
116
  latestEnd?: number | undefined;
117
117
  cursor: string | null;
118
- name: string;
119
118
  isDone: boolean;
119
+ name: string;
120
120
  table: string;
121
121
  processed: number;
122
122
  latestStart: number;
@@ -129,7 +129,7 @@ export declare const migrationsTable: import("convex/server").TableDefinition<im
129
129
  processed: import("convex/values").VFloat64<number, "required">;
130
130
  latestStart: import("convex/values").VFloat64<number, "required">;
131
131
  latestEnd: import("convex/values").VFloat64<number | undefined, "optional">;
132
- }, "required", "cursor" | "name" | "isDone" | "table" | "workerId" | "processed" | "latestStart" | "latestEnd">, {
132
+ }, "required", "cursor" | "isDone" | "name" | "table" | "workerId" | "processed" | "latestStart" | "latestEnd">, {
133
133
  name: ["name", "_creationTime"];
134
134
  }, {}, {}>;
135
135
  declare const migrationArgs: {
@@ -283,8 +283,8 @@ export declare function cancelMigration<DataModel extends GenericDataModel>(ctx:
283
283
  workerId?: GenericId<"_scheduled_functions"> | undefined;
284
284
  latestEnd?: number | undefined;
285
285
  cursor: string | null;
286
- name: string;
287
286
  isDone: boolean;
287
+ name: string;
288
288
  table: string;
289
289
  processed: number;
290
290
  latestStart: number;
@@ -293,8 +293,8 @@ export declare function cancelMigration<DataModel extends GenericDataModel>(ctx:
293
293
  workerId?: GenericId<"_scheduled_functions"> | undefined;
294
294
  latestEnd?: number | undefined;
295
295
  cursor: string | null;
296
- name: string;
297
296
  isDone: boolean;
297
+ name: string;
298
298
  table: string;
299
299
  processed: number;
300
300
  latestStart: number;
package/validators.d.ts CHANGED
@@ -25,7 +25,7 @@ export declare const literals: <V extends string | number | boolean | bigint, T
25
25
  * @param x The validator to make nullable. As in, it can be the value or null.
26
26
  * @returns A new validator that can be the value or null.
27
27
  */
28
- export declare const nullable: <V extends Validator<any, "required", any>>(x: V) => VUnion<(V | import("convex/values").VNull<null, "required">)["type"], [import("convex/values").VNull<null, "required">, V], "required", (V | import("convex/values").VNull<null, "required">)["fieldPaths"]>;
28
+ export declare const nullable: <V extends Validator<any, "required", any>>(x: V) => VUnion<(import("convex/values").VNull<null, "required"> | V)["type"], [import("convex/values").VNull<null, "required">, V], "required", (import("convex/values").VNull<null, "required"> | V)["fieldPaths"]>;
29
29
  /**
30
30
  * partial helps you define an object of optional validators more concisely.
31
31
  *