kitcn 0.33.0 → 0.33.2

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.
@@ -487,8 +487,8 @@ type ConvexQueryHookOptions = {
487
487
  /** Skip query silently when unauthenticated (default: false, calls onQueryUnauthorized) */skipUnauth?: boolean; /** Set to false to fetch once without subscribing (default: true) */
488
488
  subscribe?: boolean;
489
489
  };
490
- /** Extract input args without cursor/limit (user's filter args only) */
491
- type InfiniteQueryInput<TInput> = Omit<TInput, 'cursor' | 'limit'>;
490
+ /** Extract user args without hook-owned pagination transport fields */
491
+ type InfiniteQueryInput<TInput> = Omit<TInput, 'cursor' | 'endCursor' | 'limit'>;
492
492
  /** Extract item type from PaginationResult<T> */
493
493
  type ExtractPaginatedItem<TOutput> = TOutput extends {
494
494
  page: (infer T)[];
@@ -946,7 +946,7 @@ declare const getConvexQueryClientSingleton: ({
946
946
  * Pagination state persisted in queryClient.
947
947
  * Enables scroll restoration when navigating back to a paginated list.
948
948
  *
949
- * Uses flat { cursor, limit } structure like tRPC.
949
+ * Uses flat { cursor, endCursor, limit } structure like tRPC.
950
950
  */
951
951
  type PaginationState = {
952
952
  id: number;
@@ -955,13 +955,12 @@ type PaginationState = {
955
955
  queries: Record<number, {
956
956
  /** Flat pagination args - tRPC style */args: Record<string, unknown> & {
957
957
  cursor: string | null;
958
+ endCursor?: string | null;
958
959
  limit?: number; /** Internal pagination ID for subscription management */
959
960
  __paginationId?: number;
960
961
  };
961
- endCursor?: string | null;
962
962
  }>;
963
- version: number; /** Recovery key to prevent infinite recovery loops */
964
- autoRecoveryAttempted?: string;
963
+ version: number;
965
964
  };
966
965
  type PaginationStatus = 'CanLoadMore' | 'Exhausted' | 'LoadingFirstPage' | 'LoadingMore';
967
966
  /** Fields we override or omit from TanStack Query's UseQueryResult */
@@ -2432,6 +2432,12 @@ const getConvexQueryClientSingleton = ({ authStore, convex, queryClient, symbolK
2432
2432
  //#endregion
2433
2433
  //#region src/internal/pagination.ts
2434
2434
  const shouldSplitPaginationPage = (page, initialNumItems) => Boolean(page.splitCursor) && (page.pageStatus === "SplitRecommended" || page.pageStatus === "SplitRequired" || initialNumItems !== void 0 && page.page.length > initialNumItems * 2);
2435
+ const isInvalidPaginationCursor = (error) => {
2436
+ if (error instanceof Error && error.message.includes("InvalidCursor")) return true;
2437
+ if (!error || typeof error !== "object" || !("data" in error)) return false;
2438
+ const data = error.data;
2439
+ return Boolean(data && typeof data === "object" && "isConvexSystemError" in data && data.isConvexSystemError === true && "paginationError" in data && data.paginationError === "InvalidCursor");
2440
+ };
2435
2441
 
2436
2442
  //#endregion
2437
2443
  //#region src/react/use-infinite-query.ts
@@ -2439,76 +2445,22 @@ const shouldSplitPaginationPage = (page, initialNumItems) => Boolean(page.splitC
2439
2445
  const PAGINATION_KEY_PREFIX = "__pagination__";
2440
2446
  let paginationIdCounter = 0;
2441
2447
  const createPaginationId = () => ++paginationIdCounter;
2442
- /** Build a unique key for recovery attempt detection */
2443
- const buildRecoveryKey = (pageKeys, page0Cursor, page0UpdatedAt) => JSON.stringify({
2444
- pageKeys,
2445
- page0Cursor,
2446
- page0UpdatedAt
2447
- });
2448
2448
  /**
2449
2449
  * Hook for auto-recovering from stale cursors after WebSocket reconnection.
2450
2450
  *
2451
2451
  * When Convex WebSocket reconnects, page 0 (cursor: null) resubscribes and
2452
2452
  * gets fresh data. However, pages 1+ may have stale cursors that fail.
2453
2453
  *
2454
- * This hook detects this pattern and creates a recovery page that fetches
2455
- * enough items to cover the lost pages, preserving the user's scroll position.
2454
+ * This hook discards the invalid cursor chain and restarts from page one.
2456
2455
  */
2457
- const useStaleCursorRecovery = ({ argsObject, combined, limit, setState, state }) => {
2456
+ const useStaleCursorRecovery = ({ combined, resetPagination }) => {
2458
2457
  useEffect(() => {
2459
2458
  if (!combined.isFetchNextPageError) return;
2460
- const page0Result = combined._rawResults[0];
2461
- const page0Data = page0Result?.data;
2462
- const page0UpdatedAt = page0Result?.dataUpdatedAt ?? 0;
2463
- const hasPage0Data = page0Data !== void 0 && !page0Result?.isError;
2464
- const hasSubsequentErrors = combined._rawResults.slice(1).some((q) => q?.isError && !q?.isFetching);
2465
- if (!hasPage0Data || !hasSubsequentErrors || !page0Data?.continueCursor) return;
2466
- const recoveryKey = buildRecoveryKey(state.pageKeys, page0Data.continueCursor, page0UpdatedAt);
2467
- if (state.autoRecoveryAttempted === recoveryKey) return;
2468
- const erroredPageKeys = state.pageKeys.filter((_, i) => i > 0 && combined._rawResults[i]?.isError);
2469
- const itemsToRecover = erroredPageKeys.reduce((sum, key) => {
2470
- return sum + (state.queries[key]?.args?.limit ?? limit ?? 20);
2471
- }, 0);
2472
- console.warn("[Pagination] Auto-recovering from stale cursors", {
2473
- erroredPages: erroredPageKeys.length,
2474
- itemsToRecover
2475
- });
2476
- setState((prev) => ({
2477
- ...prev,
2478
- id: prev.id,
2479
- nextPageKey: 2,
2480
- pageKeys: [prev.pageKeys[0], 1],
2481
- queries: {
2482
- [prev.pageKeys[0]]: prev.queries[prev.pageKeys[0]],
2483
- 1: { args: {
2484
- ...argsObject,
2485
- cursor: page0Data.continueCursor,
2486
- limit: Math.min(itemsToRecover + (limit ?? 20), 500),
2487
- __paginationId: prev.id
2488
- } }
2489
- },
2490
- version: prev.version + 1,
2491
- autoRecoveryAttempted: recoveryKey
2492
- }));
2459
+ if (combined._rawResults.slice(1).some((result) => result?.isError && !result.isFetching && isInvalidPaginationCursor(result.error))) resetPagination();
2493
2460
  }, [
2494
2461
  combined.isFetchNextPageError,
2495
2462
  combined._rawResults,
2496
- state.pageKeys,
2497
- state.queries,
2498
- state.autoRecoveryAttempted,
2499
- argsObject,
2500
- limit,
2501
- setState
2502
- ]);
2503
- useEffect(() => {
2504
- if ((combined.status === "CanLoadMore" || combined.status === "Exhausted") && state.autoRecoveryAttempted) setState((prev) => ({
2505
- ...prev,
2506
- autoRecoveryAttempted: void 0
2507
- }));
2508
- }, [
2509
- combined.status,
2510
- state.autoRecoveryAttempted,
2511
- setState
2463
+ resetPagination
2512
2464
  ]);
2513
2465
  };
2514
2466
  /**
@@ -2706,7 +2658,7 @@ const useInfiniteQueryInternal = (query, args, options) => {
2706
2658
  error: results.find((r) => r.isError)?.error ?? null,
2707
2659
  isError: results.some((r) => r.isError),
2708
2660
  isFetching,
2709
- isFetchNextPageError: results.length > 1 && (results.at(-1)?.isError ?? false),
2661
+ isFetchNextPageError: results.slice(1).some((result) => result.isError),
2710
2662
  isPlaceholderData,
2711
2663
  isRefetching: isFetching && allItems.length > 0 && !isPlaceholderData,
2712
2664
  _rawResults: results
@@ -2714,11 +2666,8 @@ const useInfiniteQueryInternal = (query, args, options) => {
2714
2666
  }, [placeholderData])
2715
2667
  });
2716
2668
  useStaleCursorRecovery({
2717
- argsObject,
2718
2669
  combined,
2719
- limit,
2720
- setState,
2721
- state
2670
+ resetPagination: useCallback(() => setState(createInitialState()), [createInitialState, setState])
2722
2671
  });
2723
2672
  useEffect(() => {
2724
2673
  for (let i = 0; i < combined._rawResults.length; i++) {
@@ -2727,15 +2676,17 @@ const useInfiniteQueryInternal = (query, args, options) => {
2727
2676
  const page = pageQuery.data;
2728
2677
  const pageKey = state.pageKeys[i];
2729
2678
  const pageState = state.queries[pageKey];
2730
- if (shouldSplitPaginationPage(page, limit) && pageState && !pageState.endCursor) {
2679
+ if (shouldSplitPaginationPage(page, limit) && pageState && pageState.args.endCursor !== page.splitCursor) {
2731
2680
  setState((prev) => {
2732
2681
  const currentPageState = prev.queries[pageKey];
2733
- if (!currentPageState || currentPageState.endCursor) return prev;
2682
+ if (!currentPageState || currentPageState.args.endCursor === page.splitCursor) return prev;
2734
2683
  const newKey = prev.nextPageKey;
2735
2684
  const splitCursor = page.splitCursor;
2685
+ const endCursor = currentPageState.args.endCursor ?? page.continueCursor;
2736
2686
  const splitPageArgs = {
2737
2687
  ...argsObject,
2738
2688
  cursor: splitCursor,
2689
+ endCursor,
2739
2690
  limit: currentPageState.args.limit,
2740
2691
  __paginationId: prev.id
2741
2692
  };
@@ -2750,7 +2701,10 @@ const useInfiniteQueryInternal = (query, args, options) => {
2750
2701
  ...prev.queries,
2751
2702
  [pageKey]: {
2752
2703
  ...currentPageState,
2753
- endCursor: splitCursor
2704
+ args: {
2705
+ ...currentPageState.args,
2706
+ endCursor: splitCursor
2707
+ }
2754
2708
  },
2755
2709
  [newKey]: { args: splitPageArgs }
2756
2710
  }
@@ -1,7 +1,7 @@
1
1
  import { n as DeepPartial, o as Simplify, r as DistributiveOmit } from "../types-Bqi_ivSd.js";
2
2
  import { L as DataTransformerOptions, _ as CRPCHttpRouter, c as HttpQueryKey, k as HttpProcedure, o as HttpMutationKey, y as HttpRouterRecord } from "../http-types-BhhPZtwa.js";
3
3
  import { g as UnsetMarker } from "../types-Bkeicpo6.js";
4
- import { A as HttpInputArgs, C as ReservedMutationOptions$1, S as ReservedInfiniteQueryOptions, T as StaticQueryOptsParam, _ as IsPaginated, b as PaginatedFnMeta, c as ConvexMutationKey, d as ConvexQueryMeta, f as EmptyObject, g as InfiniteQueryInput, l as ConvexQueryHookOptions, m as FUNC_REF_SYMBOL, o as ConvexActionKey, p as ExtractPaginatedItem, s as ConvexInfiniteQueryMeta, u as ConvexQueryKey, w as ReservedQueryOptions$1, y as MutationVariables } from "../types-BXpu7xIU.js";
4
+ import { A as HttpInputArgs, C as ReservedMutationOptions$1, S as ReservedInfiniteQueryOptions, T as StaticQueryOptsParam, _ as IsPaginated, b as PaginatedFnMeta, c as ConvexMutationKey, d as ConvexQueryMeta, f as EmptyObject, g as InfiniteQueryInput, l as ConvexQueryHookOptions, m as FUNC_REF_SYMBOL, o as ConvexActionKey, p as ExtractPaginatedItem, s as ConvexInfiniteQueryMeta, u as ConvexQueryKey, w as ReservedQueryOptions$1, y as MutationVariables } from "../types-C7JkpfeY.js";
5
5
  import { FunctionArgs, FunctionReference, FunctionReturnType } from "convex/server";
6
6
  import { z } from "zod";
7
7
  import { DefaultError, QueryFilters, SkipToken, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
@@ -1,4 +1,4 @@
1
- import { $ as ActionProcedureBuilder, A as CRPCError, B as RuntimeEnv, C as createProcedureHandlerFactory, Ct as zCustomQuery, D as WithHttpRouter, Dt as zodToConvex, E as typedProcedureResolver, Et as zodOutputToConvexFields, F as getCRPCErrorFromUnknown, G as createLazyCaller, H as ConvexContext, I as getHTTPStatusCodeFromError, J as ServerCaller, K as CallerMeta, L as isCRPCError, M as CRPCErrorData, N as CRPC_ERROR_CODES_BY_KEY, O as inferApiInputs, Ot as zodToConvexFields, P as CRPC_ERROR_CODE_TO_HTTP, Q as getGeneratedValue, R as toCRPCError, S as createProcedureCallerFactory, St as zCustomMutation, T as getGeneratedFunctionReference, Tt as zodOutputToConvex, U as createCallerFactory, V as createEnv, W as LazyCaller, X as createApiLeaf, Y as createServerCaller, Z as createGeneratedFunctionReference, _ as ProcedureSchedulableCallerFromRegistry, _t as ZodValidatorFromConvex, a as CreateProcedureCallerFactoryOptions, at as initCRPC, b as createGenericCallerFactory, bt as withSystemFields, c as GeneratedRegistryCallerFactory, ct as extractPathParams, d as GeneratedRegistryHandlerForContext, dt as ConvexValidatorFromZod, et as CRPCFunctionTypeHint, f as ProcedureActionCallerFromRegistry, ft as ConvexValidatorFromZodOutput, g as ProcedureFromFunctionReference, gt as ZodFromValidatorBase, h as ProcedureDefinition, ht as Zid, i as registerProcedureNameLookup, it as createMiddlewareFactory, j as CRPCErrorCode, k as inferApiOutputs, l as GeneratedRegistryCallerForContext, lt as handleHttpError, m as ProcedureCallerFromRegistry, mt as ZCustomCtx, n as ProcedureNameLookup, nt as ProcedureBuilder, o as GeneratedProcedureRegistry, ot as HttpProcedureBuilder, p as ProcedureCaller, pt as CustomBuilder, q as CallerOpts, r as inferProcedureNameFromCallsite, rt as QueryProcedureBuilder, s as GeneratedProcedureRegistryEntry, st as createHttpProcedureBuilder, t as ProcedureNameEntry, tt as MutationProcedureBuilder, u as GeneratedRegistryHandlerFactory, ut as matchPathParams, v as ProcedureScheduleCallerFromRegistry, vt as convexToZod, w as defineProcedure, wt as zid, x as createGenericHandlerFactory, xt as zCustomAction, y as createGeneratedRegistryRuntime, yt as convexToZodFields, z as CreateEnvOptions } from "../procedure-name-DPYyLiVS.js";
1
+ import { $ as ActionProcedureBuilder, A as CRPCError, B as RuntimeEnv, C as createProcedureHandlerFactory, Ct as zCustomQuery, D as WithHttpRouter, Dt as zodToConvex, E as typedProcedureResolver, Et as zodOutputToConvexFields, F as getCRPCErrorFromUnknown, G as createLazyCaller, H as ConvexContext, I as getHTTPStatusCodeFromError, J as ServerCaller, K as CallerMeta, L as isCRPCError, M as CRPCErrorData, N as CRPC_ERROR_CODES_BY_KEY, O as inferApiInputs, Ot as zodToConvexFields, P as CRPC_ERROR_CODE_TO_HTTP, Q as getGeneratedValue, R as toCRPCError, S as createProcedureCallerFactory, St as zCustomMutation, T as getGeneratedFunctionReference, Tt as zodOutputToConvex, U as createCallerFactory, V as createEnv, W as LazyCaller, X as createApiLeaf, Y as createServerCaller, Z as createGeneratedFunctionReference, _ as ProcedureSchedulableCallerFromRegistry, _t as ZodValidatorFromConvex, a as CreateProcedureCallerFactoryOptions, at as initCRPC, b as createGenericCallerFactory, bt as withSystemFields, c as GeneratedRegistryCallerFactory, ct as extractPathParams, d as GeneratedRegistryHandlerForContext, dt as ConvexValidatorFromZod, et as CRPCFunctionTypeHint, f as ProcedureActionCallerFromRegistry, ft as ConvexValidatorFromZodOutput, g as ProcedureFromFunctionReference, gt as ZodFromValidatorBase, h as ProcedureDefinition, ht as Zid, i as registerProcedureNameLookup, it as createMiddlewareFactory, j as CRPCErrorCode, k as inferApiOutputs, l as GeneratedRegistryCallerForContext, lt as handleHttpError, m as ProcedureCallerFromRegistry, mt as ZCustomCtx, n as ProcedureNameLookup, nt as ProcedureBuilder, o as GeneratedProcedureRegistry, ot as HttpProcedureBuilder, p as ProcedureCaller, pt as CustomBuilder, q as CallerOpts, r as inferProcedureNameFromCallsite, rt as QueryProcedureBuilder, s as GeneratedProcedureRegistryEntry, st as createHttpProcedureBuilder, t as ProcedureNameEntry, tt as MutationProcedureBuilder, u as GeneratedRegistryHandlerFactory, ut as matchPathParams, v as ProcedureScheduleCallerFromRegistry, vt as convexToZod, w as defineProcedure, wt as zid, x as createGenericHandlerFactory, xt as zCustomAction, y as createGeneratedRegistryRuntime, yt as convexToZodFields, z as CreateEnvOptions } from "../procedure-name-DRSOwAJY.js";
2
2
  import { A as HttpProcedureBuilderDef, C as extractRouteMap, D as HttpHandlerOpts, E as HttpActionHandler, M as InferHttpInput, N as ProcedureMeta, O as HttpMethod, S as createHttpRouterFactory, T as HttpActionConstructor, _ as CRPCHttpRouter, b as HttpRouterWithHono, j as HttpRouteDefinition, k as HttpProcedure, v as HttpRouterDef, w as CRPCHonoHandler, x as createHttpRouter, y as HttpRouterRecord } from "../http-types-BhhPZtwa.js";
3
3
  import { a as MergeZodObjects, c as MiddlewareMarker, d as MiddlewareProcedureType, f as MiddlewareResult, g as UnsetMarker, h as Simplify, i as IntersectIfDefined, l as MiddlewareNext, m as ResolveIfSet, n as AnyMiddlewareBuilder, o as MiddlewareBuilder, p as Overwrite, r as GetRawInputFn, s as MiddlewareFunction, t as AnyMiddleware, u as MiddlewareProcedureInfo } from "../types-Bkeicpo6.js";
4
4
  import { a as isMutationCtx, c as isSchedulerCtx, d as requireQueryCtx, f as requireRunMutationCtx, i as isActionCtx, l as requireActionCtx, n as RunMutationCtx, o as isQueryCtx, p as requireSchedulerCtx, r as SchedulerCtx, s as isRunMutationCtx, t as GenericCtx, u as requireMutationCtx } from "../context-utils-Cbv4r0AA.js";
@@ -1,6 +1,6 @@
1
1
  import { a as isMutationCtx, c as isSchedulerCtx, d as requireQueryCtx, f as requireRunMutationCtx, i as isActionCtx, l as requireActionCtx, n as createGeneratedFunctionReference, o as isQueryCtx, p as requireSchedulerCtx, r as getGeneratedValue, s as isRunMutationCtx, t as createApiLeaf, u as requireMutationCtx } from "../api-entry-Buvjl9hn.js";
2
2
  import { n as createLazyCaller, r as createServerCaller, t as createCallerFactory } from "../caller-factory-C-N540BC.js";
3
- import { A as zid, C as toCRPCError, D as zCustomAction, E as withSystemFields, M as zodOutputToConvexFields, N as zodToConvex, O as zCustomMutation, P as zodToConvexFields, S as isCRPCError, T as convexToZodFields, _ as CRPCError, a as createMiddlewareFactory, b as getCRPCErrorFromUnknown, c as registerProcedureNameLookup, d as createHttpRouterFactory, f as extractRouteMap, g as matchPathParams, h as handleHttpError, i as QueryProcedureBuilder, j as zodOutputToConvex, k as zCustomQuery, l as HttpRouterWithHono, m as extractPathParams, n as MutationProcedureBuilder, o as initCRPC, p as createHttpProcedureBuilder, r as ProcedureBuilder, s as inferProcedureNameFromCallsite, t as ActionProcedureBuilder, u as createHttpRouter, v as CRPC_ERROR_CODES_BY_KEY, w as convexToZod, x as getHTTPStatusCodeFromError, y as CRPC_ERROR_CODE_TO_HTTP } from "../builder-Cyd5nCxZ.js";
4
- import { a as createProcedureHandlerFactory, c as typedProcedureResolver, i as createProcedureCallerFactory, l as createEnv, n as createGenericCallerFactory, o as defineProcedure, r as createGenericHandlerFactory, s as getGeneratedFunctionReference, t as createGeneratedRegistryRuntime } from "../procedure-caller-DgMiD0-a.js";
3
+ import { A as zid, C as toCRPCError, D as zCustomAction, E as withSystemFields, M as zodOutputToConvexFields, N as zodToConvex, O as zCustomMutation, P as zodToConvexFields, S as isCRPCError, T as convexToZodFields, _ as CRPCError, a as createMiddlewareFactory, b as getCRPCErrorFromUnknown, c as registerProcedureNameLookup, d as createHttpRouterFactory, f as extractRouteMap, g as matchPathParams, h as handleHttpError, i as QueryProcedureBuilder, j as zodOutputToConvex, k as zCustomQuery, l as HttpRouterWithHono, m as extractPathParams, n as MutationProcedureBuilder, o as initCRPC, p as createHttpProcedureBuilder, r as ProcedureBuilder, s as inferProcedureNameFromCallsite, t as ActionProcedureBuilder, u as createHttpRouter, v as CRPC_ERROR_CODES_BY_KEY, w as convexToZod, x as getHTTPStatusCodeFromError, y as CRPC_ERROR_CODE_TO_HTTP } from "../builder-DDYTlmTB.js";
4
+ import { a as createProcedureHandlerFactory, c as typedProcedureResolver, i as createProcedureCallerFactory, l as createEnv, n as createGenericCallerFactory, o as defineProcedure, r as createGenericHandlerFactory, s as getGeneratedFunctionReference, t as createGeneratedRegistryRuntime } from "../procedure-caller-DgGF_hA8.js";
5
5
 
6
6
  export { ActionProcedureBuilder, CRPCError, CRPC_ERROR_CODES_BY_KEY, CRPC_ERROR_CODE_TO_HTTP, HttpRouterWithHono, MutationProcedureBuilder, ProcedureBuilder, QueryProcedureBuilder, convexToZod, convexToZodFields, createApiLeaf, createCallerFactory, createEnv, createGeneratedFunctionReference, createGeneratedRegistryRuntime, createGenericCallerFactory, createGenericHandlerFactory, createHttpProcedureBuilder, createHttpRouter, createHttpRouterFactory, createLazyCaller, createMiddlewareFactory, createProcedureCallerFactory, createProcedureHandlerFactory, createServerCaller, defineProcedure, extractPathParams, extractRouteMap, getCRPCErrorFromUnknown, getGeneratedFunctionReference, getGeneratedValue, getHTTPStatusCodeFromError, handleHttpError, inferProcedureNameFromCallsite, initCRPC, isActionCtx, isCRPCError, isMutationCtx, isQueryCtx, isRunMutationCtx, isSchedulerCtx, matchPathParams, registerProcedureNameLookup, requireActionCtx, requireMutationCtx, requireQueryCtx, requireRunMutationCtx, requireSchedulerCtx, toCRPCError, typedProcedureResolver, withSystemFields, zCustomAction, zCustomMutation, zCustomQuery, zid, zodOutputToConvex, zodOutputToConvexFields, zodToConvex, zodToConvexFields };
@@ -60,8 +60,8 @@ type ConvexQueryHookOptions = {
60
60
  /** Skip query silently when unauthenticated (default: false, calls onQueryUnauthorized) */skipUnauth?: boolean; /** Set to false to fetch once without subscribing (default: true) */
61
61
  subscribe?: boolean;
62
62
  };
63
- /** Extract input args without cursor/limit (user's filter args only) */
64
- type InfiniteQueryInput<TInput> = Omit<TInput, 'cursor' | 'limit'>;
63
+ /** Extract user args without hook-owned pagination transport fields */
64
+ type InfiniteQueryInput<TInput> = Omit<TInput, 'cursor' | 'endCursor' | 'limit'>;
65
65
  /** Extract item type from PaginationResult<T> */
66
66
  type ExtractPaginatedItem<TOutput> = TOutput extends {
67
67
  page: (infer T)[];
@@ -1429,7 +1429,7 @@ declare const getConvexQueryClientSingleton: ({
1429
1429
  * Pagination state persisted in queryClient.
1430
1430
  * Enables scroll restoration when navigating back to a paginated list.
1431
1431
  *
1432
- * Uses flat { cursor, limit } structure like tRPC.
1432
+ * Uses flat { cursor, endCursor, limit } structure like tRPC.
1433
1433
  */
1434
1434
  type PaginationState = {
1435
1435
  id: number;
@@ -1438,13 +1438,12 @@ type PaginationState = {
1438
1438
  queries: Record<number, {
1439
1439
  /** Flat pagination args - tRPC style */args: Record<string, unknown> & {
1440
1440
  cursor: string | null;
1441
+ endCursor?: string | null;
1441
1442
  limit?: number; /** Internal pagination ID for subscription management */
1442
1443
  __paginationId?: number;
1443
1444
  };
1444
- endCursor?: string | null;
1445
1445
  }>;
1446
- version: number; /** Recovery key to prevent infinite recovery loops */
1447
- autoRecoveryAttempted?: string;
1446
+ version: number;
1448
1447
  };
1449
1448
  type PaginationStatus = 'CanLoadMore' | 'Exhausted' | 'LoadingFirstPage' | 'LoadingMore';
1450
1449
  /** Return type for infinite query hooks */
@@ -2935,6 +2935,12 @@ const getConvexQueryClientSingleton = ({ authStore, convex, queryClient, symbolK
2935
2935
  //#endregion
2936
2936
  //#region src/internal/pagination.ts
2937
2937
  const shouldSplitPaginationPage = (page, initialNumItems) => Boolean(page.splitCursor) && (page.pageStatus === "SplitRecommended" || page.pageStatus === "SplitRequired" || initialNumItems !== void 0 && page.page.length > initialNumItems * 2);
2938
+ const isInvalidPaginationCursor = (error) => {
2939
+ if (error instanceof Error && error.message.includes("InvalidCursor")) return true;
2940
+ if (!error || typeof error !== "object" || !("data" in error)) return false;
2941
+ const data = error.data;
2942
+ return Boolean(data && typeof data === "object" && "isConvexSystemError" in data && data.isConvexSystemError === true && "paginationError" in data && data.paginationError === "InvalidCursor");
2943
+ };
2938
2944
 
2939
2945
  //#endregion
2940
2946
  //#region src/solid/create-queries-results.ts
@@ -3020,7 +3026,7 @@ const aggregatePages = (results, hasPlaceholderData) => {
3020
3026
  error,
3021
3027
  failureReason: error,
3022
3028
  isError: results.some((r) => r.isError),
3023
- isFetchNextPageError: results.length > 1 && (results.at(-1)?.isError ?? false),
3029
+ isFetchNextPageError: results.slice(1).some((result) => result.isError),
3024
3030
  isFetching,
3025
3031
  isLoading: status === "LoadingFirstPage",
3026
3032
  isPlaceholderData,
@@ -3030,72 +3036,18 @@ const aggregatePages = (results, hasPlaceholderData) => {
3030
3036
  status
3031
3037
  };
3032
3038
  };
3033
- /** Build a unique key for recovery attempt detection */
3034
- const buildRecoveryKey = (pageKeys, page0Cursor, page0UpdatedAt) => JSON.stringify({
3035
- pageKeys,
3036
- page0Cursor,
3037
- page0UpdatedAt
3038
- });
3039
3039
  /**
3040
3040
  * Hook for auto-recovering from stale cursors after WebSocket reconnection.
3041
3041
  *
3042
3042
  * When Convex WebSocket reconnects, page 0 (cursor: null) resubscribes and
3043
3043
  * gets fresh data. However, pages 1+ may have stale cursors that fail.
3044
3044
  *
3045
- * This hook detects this pattern and creates a recovery page that fetches
3046
- * enough items to cover the lost pages, preserving the user's scroll position.
3045
+ * This hook discards the invalid cursor chain and restarts from page one.
3047
3046
  */
3048
- const useStaleCursorRecovery = ({ argsObject, combined, limit, pageResults, setState, state }) => {
3049
- createEffect(on([
3050
- () => combined.isFetchNextPageError,
3051
- pageResults,
3052
- () => state().pageKeys,
3053
- () => state().queries,
3054
- () => state().autoRecoveryAttempted,
3055
- argsObject
3056
- ], () => {
3047
+ const useStaleCursorRecovery = ({ combined, pageResults, resetPagination }) => {
3048
+ createEffect(on([() => combined.isFetchNextPageError, pageResults], () => {
3057
3049
  if (!combined.isFetchNextPageError) return;
3058
- const results = pageResults();
3059
- const page0Result = results[0];
3060
- const page0Data = page0Result?.data;
3061
- const page0UpdatedAt = page0Result?.dataUpdatedAt ?? 0;
3062
- const hasPage0Data = page0Data !== void 0 && !page0Result?.isError;
3063
- const hasSubsequentErrors = results.slice(1).some((q) => q?.isError && !q?.isFetching);
3064
- if (!hasPage0Data || !hasSubsequentErrors || !page0Data?.continueCursor) return;
3065
- const currentState = state();
3066
- const recoveryKey = buildRecoveryKey(currentState.pageKeys, page0Data.continueCursor, page0UpdatedAt);
3067
- if (currentState.autoRecoveryAttempted === recoveryKey) return;
3068
- const erroredPageKeys = currentState.pageKeys.filter((_, i) => i > 0 && results[i]?.isError);
3069
- const itemsToRecover = erroredPageKeys.reduce((sum, key) => {
3070
- return sum + (currentState.queries[key]?.args?.limit ?? limit ?? 20);
3071
- }, 0);
3072
- console.warn("[Pagination] Auto-recovering from stale cursors", {
3073
- erroredPages: erroredPageKeys.length,
3074
- itemsToRecover
3075
- });
3076
- setState((prev) => ({
3077
- ...prev,
3078
- id: prev.id,
3079
- nextPageKey: 2,
3080
- pageKeys: [prev.pageKeys[0], 1],
3081
- queries: {
3082
- [prev.pageKeys[0]]: prev.queries[prev.pageKeys[0]],
3083
- 1: { args: {
3084
- ...argsObject(),
3085
- cursor: page0Data.continueCursor,
3086
- limit: Math.min(itemsToRecover + (limit ?? 20), 500),
3087
- __paginationId: prev.id
3088
- } }
3089
- },
3090
- version: prev.version + 1,
3091
- autoRecoveryAttempted: recoveryKey
3092
- }));
3093
- }));
3094
- createEffect(on([() => combined.status, () => state().autoRecoveryAttempted], () => {
3095
- if ((combined.status === "CanLoadMore" || combined.status === "Exhausted") && state().autoRecoveryAttempted) setState((prev) => ({
3096
- ...prev,
3097
- autoRecoveryAttempted: void 0
3098
- }));
3050
+ if (pageResults().slice(1).some((result) => result?.isError && !result.isFetching && isInvalidPaginationCursor(result.error))) resetPagination();
3099
3051
  }));
3100
3052
  };
3101
3053
  /**
@@ -3232,12 +3184,9 @@ const useInfiniteQueryInternal = (query, args, options) => {
3232
3184
  const [combined, setCombined] = createStore(derive());
3233
3185
  createRenderEffect(() => setCombined(derive()));
3234
3186
  useStaleCursorRecovery({
3235
- argsObject,
3236
3187
  combined,
3237
- limit,
3238
3188
  pageResults,
3239
- setState,
3240
- state
3189
+ resetPagination: () => setState(createInitialState())
3241
3190
  });
3242
3191
  createEffect(on([
3243
3192
  pageResults,
@@ -3252,15 +3201,17 @@ const useInfiniteQueryInternal = (query, args, options) => {
3252
3201
  const page = pageQuery.data;
3253
3202
  const pageKey = state().pageKeys[i];
3254
3203
  const pageState = state().queries[pageKey];
3255
- if (shouldSplitPaginationPage(page, limit) && pageState && !pageState.endCursor) {
3204
+ if (shouldSplitPaginationPage(page, limit) && pageState && pageState.args.endCursor !== page.splitCursor) {
3256
3205
  setState((prev) => {
3257
3206
  const currentPageState = prev.queries[pageKey];
3258
- if (!currentPageState || currentPageState.endCursor) return prev;
3207
+ if (!currentPageState || currentPageState.args.endCursor === page.splitCursor) return prev;
3259
3208
  const newKey = prev.nextPageKey;
3260
3209
  const splitCursor = page.splitCursor;
3210
+ const endCursor = currentPageState.args.endCursor ?? page.continueCursor;
3261
3211
  const splitPageArgs = {
3262
3212
  ...argsObject(),
3263
3213
  cursor: splitCursor,
3214
+ endCursor,
3264
3215
  limit: currentPageState.args.limit,
3265
3216
  __paginationId: prev.id
3266
3217
  };
@@ -3275,7 +3226,10 @@ const useInfiniteQueryInternal = (query, args, options) => {
3275
3226
  ...prev.queries,
3276
3227
  [pageKey]: {
3277
3228
  ...currentPageState,
3278
- endCursor: splitCursor
3229
+ args: {
3230
+ ...currentPageState.args,
3231
+ endCursor: splitCursor
3232
+ }
3279
3233
  },
3280
3234
  [newKey]: { args: splitPageArgs }
3281
3235
  }
@@ -148,8 +148,8 @@ type PaginationOpts = {
148
148
  maximumRowsRead?: number;
149
149
  maximumBytesRead?: number;
150
150
  };
151
- /** Extract input args without cursor/limit (user's filter args only) */
152
- type InfiniteQueryInput<TInput> = Omit<TInput, 'cursor' | 'limit'>;
151
+ /** Extract user args without hook-owned pagination transport fields */
152
+ type InfiniteQueryInput<TInput> = Omit<TInput, 'cursor' | 'endCursor' | 'limit'>;
153
153
  /** Extract item type from PaginationResult<T> */
154
154
  type ExtractPaginatedItem<TOutput> = TOutput extends {
155
155
  page: (infer T)[];