@gaganref/confect-react 10.3.1-fork.13 → 10.3.1-fork.15

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.
@@ -13,7 +13,9 @@
13
13
  * Pinning a freshly loaded page, loading more, and splitting an overgrown page
14
14
  * are the same transition shape: the affected page keeps rendering while its
15
15
  * replacement subscriptions ("ongoing split") load, and is swapped out once all
16
- * of them have results.
16
+ * of them have results. A failed replacement fails the render at once, still
17
+ * showing the affected page and the pages after it, and the swap waits for a
18
+ * retry.
17
19
  *
18
20
  * This module is framework-free (it imports only `effect/*` and a type from
19
21
  * `convex/server`); it lives here for now because the React hook is its only
@@ -116,9 +118,14 @@ export type Results = Record.ReadonlyRecord<string, PageResult | Error | undefin
116
118
  export type PageResult = PaginationResult<unknown>;
117
119
  /**
118
120
  * What one render pass derives from the subscribed pages' results: the items to
119
- * show, the trailing complete result (absent while the tail is loading or
120
- * force-splitting), state transitions to apply, and whether a failure or an
121
- * invalid cursor was hit.
121
+ * show, whether the first page has loaded, the trailing complete result (absent
122
+ * while the tail is loading or force-splitting), state transitions to apply
123
+ * (also while a failure shows, so the other pages keep splitting and swapping),
124
+ * and whether a failure or an invalid cursor was hit.
125
+ *
126
+ * `firstPageLoaded` holds once the head page has its full result. Splits can
127
+ * leave several pages ahead of the first loaded item, so neither the item count
128
+ * nor the page count tells a loading first page apart from a loaded empty one.
122
129
  */
123
130
  export type Interpretation = {
124
131
  readonly _tag: "ResetRequired";
@@ -126,15 +133,21 @@ export type Interpretation = {
126
133
  readonly _tag: "Failed";
127
134
  readonly error: unknown;
128
135
  /**
129
- * Encoded items loaded before the failing page.
136
+ * Encoded items across the loaded pages, in order. A page whose
137
+ * replacement failed is still shown, and stays subscribed with the
138
+ * replacement so that a retry re-requests only the replacement. A page
139
+ * whose own result failed ends the items, since its range is unknown.
130
140
  */
131
141
  readonly items: ReadonlyArray<unknown>;
142
+ readonly firstPageLoaded: boolean;
143
+ readonly transitions: ReadonlyArray<(state: State) => State>;
132
144
  } | {
133
145
  readonly _tag: "Interpreted";
134
146
  /**
135
147
  * Encoded items across the loaded pages, in order.
136
148
  */
137
149
  readonly items: ReadonlyArray<unknown>;
150
+ readonly firstPageLoaded: boolean;
138
151
  /**
139
152
  * The last page's result, when every page has loaded completely.
140
153
  */
@@ -1 +1 @@
1
- {"version":3,"file":"StreamPagination.d.ts","sourceRoot":"","sources":["../src/StreamPagination.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"StreamPagination.d.ts","sourceRoot":"","sources":["../src/StreamPagination.ts"],"names":[],"mappings":"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"}
@@ -140,12 +140,6 @@ const isLastPageSplitting = (state) => Option.exists(Array.last(state.pageKeys),
140
140
  * Build one subscription request per subscribed page.
141
141
  */
142
142
  const pageRequests = (state, f) => Record.map(state.pages, f);
143
- const interpreted = (items, lastResult, transitions) => ({
144
- _tag: "Interpreted",
145
- items,
146
- lastResult,
147
- transitions
148
- });
149
143
  /**
150
144
  * Walk the pages in display order, concatenating their items and collecting the
151
145
  * transitions to apply (completed split swaps, recommended splits, eager splits
@@ -155,26 +149,48 @@ const interpreted = (items, lastResult, transitions) => ({
155
149
  * rather than a failure (the cursor no longer matches the query).
156
150
  */
157
151
  const interpret = (state, results, options) => {
158
- const go = (remaining, items, transitions, lastResult) => Option.match(Array.head(remaining), {
159
- onNone: () => interpreted(items, lastResult, transitions),
152
+ const resultOf = (key) => Record.get(results, key).pipe(Option.flatMap(Option.fromNullishOr), Option.getOrUndefined);
153
+ const firstPageLoaded = Option.exists(Array.head(state.pageKeys), (key) => {
154
+ const result = resultOf(key);
155
+ return result !== void 0 && !(result instanceof Error) && result.pageStatus !== "SplitRequired";
156
+ });
157
+ const finish = (items, lastResult, transitions, errors) => Array.some(errors, options.isInvalidCursorError) ? { _tag: "ResetRequired" } : Option.match(Array.head(errors), {
158
+ onNone: () => ({
159
+ _tag: "Interpreted",
160
+ items,
161
+ firstPageLoaded,
162
+ lastResult,
163
+ transitions
164
+ }),
165
+ onSome: (error) => ({
166
+ _tag: "Failed",
167
+ error,
168
+ items,
169
+ firstPageLoaded,
170
+ transitions
171
+ })
172
+ });
173
+ const go = (remaining, items, transitions, lastResult, errors) => Option.match(Array.head(remaining), {
174
+ onNone: () => finish(items, lastResult, transitions, errors),
160
175
  onSome: (pageKey) => {
161
- const result = Record.get(results, pageKey).pipe(Option.flatMap(Option.fromNullishOr), Option.getOrUndefined);
162
- if (result === void 0) return interpreted(items, Option.none(), transitions);
163
- if (result instanceof Error) return options.isInvalidCursorError(result) ? { _tag: "ResetRequired" } : {
164
- _tag: "Failed",
165
- error: result,
166
- items
167
- };
168
- const hasResult = (key) => Record.get(results, key).pipe(Option.flatMap(Option.fromNullishOr), Option.isSome);
176
+ const result = resultOf(pageKey);
177
+ if (result === void 0) return finish(items, Option.none(), transitions, errors);
178
+ if (result instanceof Error) return finish(items, Option.none(), transitions, Array.append(errors, result));
179
+ const shown = result.pageStatus === "SplitRequired" ? items : Array.appendAll(items, result.page);
169
180
  const ongoingSplit = Record.get(state.ongoingSplits, pageKey);
181
+ const replacementResults = Option.match(ongoingSplit, {
182
+ onNone: () => [],
183
+ onSome: Array.map(resultOf)
184
+ });
185
+ const nextErrors = Array.appendAll(errors, Array.filter(replacementResults, (replacement) => replacement instanceof Error));
170
186
  const nextTransitions = Option.match(ongoingSplit, {
171
- onSome: (replacements) => Array.every(replacements, hasResult) ? Array.append(transitions, completeSplit(pageKey)) : transitions,
187
+ onSome: () => Array.every(replacementResults, (replacement) => replacement !== void 0 && !(replacement instanceof Error)) ? Array.append(transitions, completeSplit(pageKey)) : transitions,
172
188
  onNone: () => typeof result.splitCursor === "string" && (result.pageStatus === "SplitRecommended" || result.pageStatus === "SplitRequired" || result.page.length > options.initialNumItems) ? Array.append(transitions, split(pageKey, result.splitCursor)) : Option.exists(Record.get(state.pages, pageKey), (request) => request.endCursor === void 0) && result.pageStatus !== "SplitRequired" ? Array.append(transitions, pin(pageKey, result.continueCursor)) : transitions
173
189
  });
174
- return result.pageStatus === "SplitRequired" ? interpreted(items, Option.none(), nextTransitions) : go(Array.drop(remaining, 1), Array.appendAll(items, result.page), nextTransitions, Option.some(result));
190
+ return result.pageStatus === "SplitRequired" ? finish(shown, Option.none(), nextTransitions, nextErrors) : go(Array.drop(remaining, 1), shown, nextTransitions, Option.some(result), nextErrors);
175
191
  }
176
192
  });
177
- return go(state.pageKeys, [], [], Option.none());
193
+ return go(state.pageKeys, [], [], Option.none(), []);
178
194
  };
179
195
  //#endregion
180
196
  export { StreamPagination_exports, completeSplit, empty, initial, interpret, isLastPageSplitting, loadMore, pageRequests, pin, split };
@@ -1 +1 @@
1
- {"version":3,"file":"StreamPagination.js","names":[],"sources":["../src/StreamPagination.ts"],"sourcesContent":["/**\n * EXPERIMENTAL—the pure state machine behind {@link useStreamPaginatedQuery}\n * (see `notes/stream-based-querying.md` in the repo root).\n *\n * Reactive pagination without the query journal: every loaded page—including\n * the first—is pinned to a fixed index range by echoing its `continueCursor`\n * back as the next subscription's `endCursor` (the last page pins to the\n * end-of-stream sentinel once exhausted), so adjacent pages stay exactly\n * contiguous and pages never shed items as data changes—the \"range-defined\n * pages\" the Fully Reactive Pagination article calls for, and the mechanism of\n * `convex-helpers/react`'s `usePaginatedQuery`.\n *\n * Pinning a freshly loaded page, loading more, and splitting an overgrown page\n * are the same transition shape: the affected page keeps rendering while its\n * replacement subscriptions (\"ongoing split\") load, and is swapped out once all\n * of them have results.\n *\n * This module is framework-free (it imports only `effect/*` and a type from\n * `convex/server`); it lives here for now because the React hook is its only\n * consumer—a second client (e.g. `@confect/foldkit`) should prompt a move down\n * to `@confect/js`.\n */\nimport type { PaginationResult } from \"convex/server\";\nimport * as Array from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { pipe } from \"effect/Function\";\nimport * as Record from \"effect/Record\";\n\n/**\n * The `paginationOpts` one subscribed page queries with.\n */\nexport interface PageRequest {\n readonly numItems: number;\n readonly cursor: string | null;\n /**\n * Present once the page is pinned to a fixed range.\n */\n readonly endCursor?: string;\n /**\n * Per-page read budgets, forwarded to the server so a scan-heavy page fails\n * over to `SplitRequired` instead of exceeding query limits.\n */\n readonly maximumRowsRead?: number;\n readonly maximumBytesRead?: number;\n}\n\n/**\n * The per-page read budgets every growing page is requested with.\n */\nexport interface ReadBudget {\n readonly maximumRowsRead?: number | undefined;\n readonly maximumBytesRead?: number | undefined;\n}\n\n/**\n * A fresh growing-page request (no `endCursor`).\n */\nconst growingRequest = (\n numItems: number,\n cursor: string | null,\n budget: ReadBudget,\n): PageRequest => ({\n numItems,\n cursor,\n ...(budget.maximumRowsRead === undefined\n ? {}\n : { maximumRowsRead: budget.maximumRowsRead }),\n ...(budget.maximumBytesRead === undefined\n ? {}\n : { maximumBytesRead: budget.maximumBytesRead }),\n});\n\nexport interface State {\n readonly nextPageKey: number;\n /**\n * Page keys in display order.\n */\n readonly pageKeys: ReadonlyArray<string>;\n /**\n * The request each subscribed page queries with, by page key.\n */\n readonly pages: Record.ReadonlyRecord<string, PageRequest>;\n /**\n * Pages being replaced by one (a pin) or two (a split) narrower\n * subscriptions: the original stays subscribed (and rendered) until every\n * replacement has a result.\n */\n readonly ongoingSplits: Record.ReadonlyRecord<string, ReadonlyArray<string>>;\n}\n\n/**\n * The skipped state: nothing subscribed.\n */\nexport const empty: State = {\n nextPageKey: 0,\n pageKeys: [],\n pages: Record.empty(),\n ongoingSplits: Record.empty(),\n};\n\n/**\n * One growing (unpinned) page from the start of the stream.\n */\nexport const initial = (\n initialNumItems: number,\n budget: ReadBudget = {},\n): State => ({\n nextPageKey: 1,\n pageKeys: [\"0\"],\n pages: Record.singleton(\"0\", growingRequest(initialNumItems, null, budget)),\n ongoingSplits: Record.empty(),\n});\n\n/**\n * Pin a freshly loaded growing page at its `continueCursor` (or, once\n * exhausted, at the end-of-stream sentinel), so it stops being a sliding\n * window: a pinned page grows and shrinks with the data in its range but never\n * sheds items past its edges. Modeled as a one-replacement split so the\n * original keeps rendering until the pinned twin has a result.\n */\nexport const pin =\n (key: string, endCursor: string): ((state: State) => State) =>\n (state) =>\n Option.match(Record.get(state.pages, key), {\n onNone: () => state,\n onSome: (page) => {\n if (\n page.endCursor !== undefined ||\n Record.has(state.ongoingSplits, key)\n ) {\n return state;\n }\n const pinnedKey = String(state.nextPageKey);\n return {\n nextPageKey: state.nextPageKey + 1,\n pageKeys: state.pageKeys,\n pages: Record.set(state.pages, pinnedKey, { ...page, endCursor }),\n ongoingSplits: Record.set(state.ongoingSplits, key, [pinnedKey]),\n };\n },\n });\n\n/**\n * Pin the growing last page at `continueCursor` and start a new growing page\n * from there. Modeled as a split of the last page into its pinned replacement\n * and the new page, so the swap waits for both.\n */\nexport const loadMore =\n (\n continueCursor: string,\n numItems: number,\n budget: ReadBudget = {},\n ): ((state: State) => State) =>\n (state) =>\n pipe(\n Option.Do,\n Option.bind(\"lastKey\", () => Array.last(state.pageKeys)),\n Option.bind(\"lastPage\", ({ lastKey }) =>\n Record.get(state.pages, lastKey),\n ),\n Option.match({\n onNone: () => state,\n onSome: ({ lastKey, lastPage }) => {\n if (lastPage.endCursor !== undefined) {\n // The last page is already pinned (the usual case, since pages\n // pin themselves on first load): just append a new growing page.\n const nextKey = String(state.nextPageKey);\n return {\n nextPageKey: state.nextPageKey + 1,\n pageKeys: Array.append(state.pageKeys, nextKey),\n pages: Record.set(\n state.pages,\n nextKey,\n growingRequest(numItems, continueCursor, budget),\n ),\n ongoingSplits: state.ongoingSplits,\n };\n }\n const pinnedKey = String(state.nextPageKey);\n const nextKey = String(state.nextPageKey + 1);\n return {\n nextPageKey: state.nextPageKey + 2,\n pageKeys: state.pageKeys,\n pages: pipe(\n state.pages,\n Record.set(pinnedKey, { ...lastPage, endCursor: continueCursor }),\n Record.set(\n nextKey,\n growingRequest(numItems, continueCursor, budget),\n ),\n ),\n ongoingSplits: Record.set(state.ongoingSplits, lastKey, [\n pinnedKey,\n nextKey,\n ]),\n };\n },\n }),\n );\n\n/**\n * Split the page at `key` in two at `splitCursor`. The first replacement pins\n * at the split point; the second keeps the page's own `endCursor`—for a pinned\n * page its original end (so a truncated `SplitRequired` page loses none of its\n * range), and for a growing page no end at all (it keeps growing).\n */\nexport const split =\n (key: string, splitCursor: string): ((state: State) => State) =>\n (state) =>\n Option.match(Record.get(state.pages, key), {\n onNone: () => state,\n onSome: (page) => {\n const firstKey = String(state.nextPageKey);\n const secondKey = String(state.nextPageKey + 1);\n return {\n nextPageKey: state.nextPageKey + 2,\n pageKeys: state.pageKeys,\n pages: pipe(\n state.pages,\n Record.set(firstKey, { ...page, endCursor: splitCursor }),\n Record.set(secondKey, { ...page, cursor: splitCursor }),\n ),\n ongoingSplits: Record.set(state.ongoingSplits, key, [\n firstKey,\n secondKey,\n ]),\n };\n },\n });\n\n/**\n * Swap a completed split's replacements in for the original page.\n */\nexport const completeSplit =\n (key: string): ((state: State) => State) =>\n (state) =>\n Option.match(Record.get(state.ongoingSplits, key), {\n onNone: () => state,\n onSome: (replacements) => ({\n nextPageKey: state.nextPageKey,\n pageKeys: Array.flatMap(state.pageKeys, (pageKey) =>\n pageKey === key ? replacements : [pageKey],\n ),\n pages: Record.remove(state.pages, key),\n ongoingSplits: Record.remove(state.ongoingSplits, key),\n }),\n });\n\n/**\n * Whether the last (rendered) page is currently being split or pinned.\n */\nexport const isLastPageSplitting = (state: State): boolean =>\n Option.exists(Array.last(state.pageKeys), (lastKey) =>\n Record.has(state.ongoingSplits, lastKey),\n );\n\n/**\n * Build one subscription request per subscribed page.\n */\nexport const pageRequests = <A>(\n state: State,\n f: (page: PageRequest) => A,\n): Record.ReadonlyRecord<string, A> => Record.map(state.pages, f);\n\n/**\n * The per-page results a render observes, keyed like {@link pageRequests}.\n */\nexport type Results = Record.ReadonlyRecord<\n string,\n PageResult | Error | undefined\n>;\n\n// -----------------------------------------------------------------------------\n// Interpretation\n// -----------------------------------------------------------------------------\n\n/**\n * The wire shape of one loaded page—`PaginationResult` from `convex/server`, so\n * the protocol has a single source of truth.\n */\nexport type PageResult = PaginationResult<unknown>;\n\n/**\n * What one render pass derives from the subscribed pages' results: the items to\n * show, the trailing complete result (absent while the tail is loading or\n * force-splitting), state transitions to apply, and whether a failure or an\n * invalid cursor was hit.\n */\nexport type Interpretation =\n | { readonly _tag: \"ResetRequired\" }\n | {\n readonly _tag: \"Failed\";\n readonly error: unknown;\n /**\n * Encoded items loaded before the failing page.\n */\n readonly items: ReadonlyArray<unknown>;\n }\n | {\n readonly _tag: \"Interpreted\";\n /**\n * Encoded items across the loaded pages, in order.\n */\n readonly items: ReadonlyArray<unknown>;\n /**\n * The last page's result, when every page has loaded completely.\n */\n readonly lastResult: Option.Option<PageResult>;\n /**\n * Split/swap transitions this render discovered.\n */\n readonly transitions: ReadonlyArray<(state: State) => State>;\n };\n\nconst interpreted = (\n items: ReadonlyArray<unknown>,\n lastResult: Option.Option<PageResult>,\n transitions: ReadonlyArray<(state: State) => State>,\n): Interpretation => ({ _tag: \"Interpreted\", items, lastResult, transitions });\n\n/**\n * Walk the pages in display order, concatenating their items and collecting the\n * transitions to apply (completed split swaps, recommended splits, eager splits\n * of pages that outgrew `initialNumItems`).\n *\n * `isInvalidCursorError` marks errors that call for a full pagination reset\n * rather than a failure (the cursor no longer matches the query).\n */\nexport const interpret = (\n state: State,\n results: Record.ReadonlyRecord<string, PageResult | Error | undefined>,\n options: {\n readonly initialNumItems: number;\n readonly isInvalidCursorError: (error: Error) => boolean;\n },\n): Interpretation => {\n const go = (\n remaining: ReadonlyArray<string>,\n items: ReadonlyArray<unknown>,\n transitions: ReadonlyArray<(s: State) => State>,\n lastResult: Option.Option<PageResult>,\n ): Interpretation =>\n Option.match(Array.head(remaining), {\n onNone: () => interpreted(items, lastResult, transitions),\n onSome: (pageKey) => {\n const result = Record.get(results, pageKey).pipe(\n Option.flatMap(Option.fromNullishOr),\n Option.getOrUndefined,\n );\n if (result === undefined) {\n // The trailing pages are still loading.\n return interpreted(items, Option.none(), transitions);\n }\n if (result instanceof Error) {\n return options.isInvalidCursorError(result)\n ? { _tag: \"ResetRequired\" }\n : { _tag: \"Failed\", error: result, items };\n }\n\n const hasResult = (key: string) =>\n Record.get(results, key).pipe(\n Option.flatMap(Option.fromNullishOr),\n Option.isSome,\n );\n const ongoingSplit = Record.get(state.ongoingSplits, pageKey);\n const nextTransitions = Option.match(ongoingSplit, {\n onSome: (replacements) =>\n // Swap the replacements in once all of them have results.\n Array.every(replacements, hasResult)\n ? Array.append(transitions, completeSplit(pageKey))\n : transitions,\n onNone: () =>\n typeof result.splitCursor === \"string\" &&\n (result.pageStatus === \"SplitRecommended\" ||\n result.pageStatus === \"SplitRequired\" ||\n result.page.length > options.initialNumItems)\n ? Array.append(transitions, split(pageKey, result.splitCursor))\n : // A loaded growing page pins itself to the range it just\n // served (`END_CURSOR` once exhausted), so it stops being a\n // sliding window that sheds items as new documents arrive.\n Option.exists(\n Record.get(state.pages, pageKey),\n (request) => request.endCursor === undefined,\n ) && result.pageStatus !== \"SplitRequired\"\n ? Array.append(transitions, pin(pageKey, result.continueCursor))\n : transitions,\n });\n\n // A force-split page couldn't be fetched in full: show the items\n // before it and report the tail as still loading.\n return result.pageStatus === \"SplitRequired\"\n ? interpreted(items, Option.none(), nextTransitions)\n : go(\n Array.drop(remaining, 1),\n Array.appendAll(items, result.page),\n nextTransitions,\n Option.some(result),\n );\n },\n });\n\n return go(state.pageKeys, [], [], Option.none());\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"StreamPagination.js","names":[],"sources":["../src/StreamPagination.ts"],"sourcesContent":["/**\n * EXPERIMENTAL—the pure state machine behind {@link useStreamPaginatedQuery}\n * (see `notes/stream-based-querying.md` in the repo root).\n *\n * Reactive pagination without the query journal: every loaded page—including\n * the first—is pinned to a fixed index range by echoing its `continueCursor`\n * back as the next subscription's `endCursor` (the last page pins to the\n * end-of-stream sentinel once exhausted), so adjacent pages stay exactly\n * contiguous and pages never shed items as data changes—the \"range-defined\n * pages\" the Fully Reactive Pagination article calls for, and the mechanism of\n * `convex-helpers/react`'s `usePaginatedQuery`.\n *\n * Pinning a freshly loaded page, loading more, and splitting an overgrown page\n * are the same transition shape: the affected page keeps rendering while its\n * replacement subscriptions (\"ongoing split\") load, and is swapped out once all\n * of them have results. A failed replacement fails the render at once, still\n * showing the affected page and the pages after it, and the swap waits for a\n * retry.\n *\n * This module is framework-free (it imports only `effect/*` and a type from\n * `convex/server`); it lives here for now because the React hook is its only\n * consumer—a second client (e.g. `@confect/foldkit`) should prompt a move down\n * to `@confect/js`.\n */\nimport type { PaginationResult } from \"convex/server\";\nimport * as Array from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport { pipe } from \"effect/Function\";\nimport * as Record from \"effect/Record\";\n\n/**\n * The `paginationOpts` one subscribed page queries with.\n */\nexport interface PageRequest {\n readonly numItems: number;\n readonly cursor: string | null;\n /**\n * Present once the page is pinned to a fixed range.\n */\n readonly endCursor?: string;\n /**\n * Per-page read budgets, forwarded to the server so a scan-heavy page fails\n * over to `SplitRequired` instead of exceeding query limits.\n */\n readonly maximumRowsRead?: number;\n readonly maximumBytesRead?: number;\n}\n\n/**\n * The per-page read budgets every growing page is requested with.\n */\nexport interface ReadBudget {\n readonly maximumRowsRead?: number | undefined;\n readonly maximumBytesRead?: number | undefined;\n}\n\n/**\n * A fresh growing-page request (no `endCursor`).\n */\nconst growingRequest = (\n numItems: number,\n cursor: string | null,\n budget: ReadBudget,\n): PageRequest => ({\n numItems,\n cursor,\n ...(budget.maximumRowsRead === undefined\n ? {}\n : { maximumRowsRead: budget.maximumRowsRead }),\n ...(budget.maximumBytesRead === undefined\n ? {}\n : { maximumBytesRead: budget.maximumBytesRead }),\n});\n\nexport interface State {\n readonly nextPageKey: number;\n /**\n * Page keys in display order.\n */\n readonly pageKeys: ReadonlyArray<string>;\n /**\n * The request each subscribed page queries with, by page key.\n */\n readonly pages: Record.ReadonlyRecord<string, PageRequest>;\n /**\n * Pages being replaced by one (a pin) or two (a split) narrower\n * subscriptions: the original stays subscribed (and rendered) until every\n * replacement has a result.\n */\n readonly ongoingSplits: Record.ReadonlyRecord<string, ReadonlyArray<string>>;\n}\n\n/**\n * The skipped state: nothing subscribed.\n */\nexport const empty: State = {\n nextPageKey: 0,\n pageKeys: [],\n pages: Record.empty(),\n ongoingSplits: Record.empty(),\n};\n\n/**\n * One growing (unpinned) page from the start of the stream.\n */\nexport const initial = (\n initialNumItems: number,\n budget: ReadBudget = {},\n): State => ({\n nextPageKey: 1,\n pageKeys: [\"0\"],\n pages: Record.singleton(\"0\", growingRequest(initialNumItems, null, budget)),\n ongoingSplits: Record.empty(),\n});\n\n/**\n * Pin a freshly loaded growing page at its `continueCursor` (or, once\n * exhausted, at the end-of-stream sentinel), so it stops being a sliding\n * window: a pinned page grows and shrinks with the data in its range but never\n * sheds items past its edges. Modeled as a one-replacement split so the\n * original keeps rendering until the pinned twin has a result.\n */\nexport const pin =\n (key: string, endCursor: string): ((state: State) => State) =>\n (state) =>\n Option.match(Record.get(state.pages, key), {\n onNone: () => state,\n onSome: (page) => {\n if (\n page.endCursor !== undefined ||\n Record.has(state.ongoingSplits, key)\n ) {\n return state;\n }\n const pinnedKey = String(state.nextPageKey);\n return {\n nextPageKey: state.nextPageKey + 1,\n pageKeys: state.pageKeys,\n pages: Record.set(state.pages, pinnedKey, { ...page, endCursor }),\n ongoingSplits: Record.set(state.ongoingSplits, key, [pinnedKey]),\n };\n },\n });\n\n/**\n * Pin the growing last page at `continueCursor` and start a new growing page\n * from there. Modeled as a split of the last page into its pinned replacement\n * and the new page, so the swap waits for both.\n */\nexport const loadMore =\n (\n continueCursor: string,\n numItems: number,\n budget: ReadBudget = {},\n ): ((state: State) => State) =>\n (state) =>\n pipe(\n Option.Do,\n Option.bind(\"lastKey\", () => Array.last(state.pageKeys)),\n Option.bind(\"lastPage\", ({ lastKey }) =>\n Record.get(state.pages, lastKey),\n ),\n Option.match({\n onNone: () => state,\n onSome: ({ lastKey, lastPage }) => {\n if (lastPage.endCursor !== undefined) {\n // The last page is already pinned (the usual case, since pages\n // pin themselves on first load): just append a new growing page.\n const nextKey = String(state.nextPageKey);\n return {\n nextPageKey: state.nextPageKey + 1,\n pageKeys: Array.append(state.pageKeys, nextKey),\n pages: Record.set(\n state.pages,\n nextKey,\n growingRequest(numItems, continueCursor, budget),\n ),\n ongoingSplits: state.ongoingSplits,\n };\n }\n const pinnedKey = String(state.nextPageKey);\n const nextKey = String(state.nextPageKey + 1);\n return {\n nextPageKey: state.nextPageKey + 2,\n pageKeys: state.pageKeys,\n pages: pipe(\n state.pages,\n Record.set(pinnedKey, { ...lastPage, endCursor: continueCursor }),\n Record.set(\n nextKey,\n growingRequest(numItems, continueCursor, budget),\n ),\n ),\n ongoingSplits: Record.set(state.ongoingSplits, lastKey, [\n pinnedKey,\n nextKey,\n ]),\n };\n },\n }),\n );\n\n/**\n * Split the page at `key` in two at `splitCursor`. The first replacement pins\n * at the split point; the second keeps the page's own `endCursor`—for a pinned\n * page its original end (so a truncated `SplitRequired` page loses none of its\n * range), and for a growing page no end at all (it keeps growing).\n */\nexport const split =\n (key: string, splitCursor: string): ((state: State) => State) =>\n (state) =>\n Option.match(Record.get(state.pages, key), {\n onNone: () => state,\n onSome: (page) => {\n const firstKey = String(state.nextPageKey);\n const secondKey = String(state.nextPageKey + 1);\n return {\n nextPageKey: state.nextPageKey + 2,\n pageKeys: state.pageKeys,\n pages: pipe(\n state.pages,\n Record.set(firstKey, { ...page, endCursor: splitCursor }),\n Record.set(secondKey, { ...page, cursor: splitCursor }),\n ),\n ongoingSplits: Record.set(state.ongoingSplits, key, [\n firstKey,\n secondKey,\n ]),\n };\n },\n });\n\n/**\n * Swap a completed split's replacements in for the original page.\n */\nexport const completeSplit =\n (key: string): ((state: State) => State) =>\n (state) =>\n Option.match(Record.get(state.ongoingSplits, key), {\n onNone: () => state,\n onSome: (replacements) => ({\n nextPageKey: state.nextPageKey,\n pageKeys: Array.flatMap(state.pageKeys, (pageKey) =>\n pageKey === key ? replacements : [pageKey],\n ),\n pages: Record.remove(state.pages, key),\n ongoingSplits: Record.remove(state.ongoingSplits, key),\n }),\n });\n\n/**\n * Whether the last (rendered) page is currently being split or pinned.\n */\nexport const isLastPageSplitting = (state: State): boolean =>\n Option.exists(Array.last(state.pageKeys), (lastKey) =>\n Record.has(state.ongoingSplits, lastKey),\n );\n\n/**\n * Build one subscription request per subscribed page.\n */\nexport const pageRequests = <A>(\n state: State,\n f: (page: PageRequest) => A,\n): Record.ReadonlyRecord<string, A> => Record.map(state.pages, f);\n\n/**\n * The per-page results a render observes, keyed like {@link pageRequests}.\n */\nexport type Results = Record.ReadonlyRecord<\n string,\n PageResult | Error | undefined\n>;\n\n// -----------------------------------------------------------------------------\n// Interpretation\n// -----------------------------------------------------------------------------\n\n/**\n * The wire shape of one loaded page—`PaginationResult` from `convex/server`, so\n * the protocol has a single source of truth.\n */\nexport type PageResult = PaginationResult<unknown>;\n\n/**\n * What one render pass derives from the subscribed pages' results: the items to\n * show, whether the first page has loaded, the trailing complete result (absent\n * while the tail is loading or force-splitting), state transitions to apply\n * (also while a failure shows, so the other pages keep splitting and swapping),\n * and whether a failure or an invalid cursor was hit.\n *\n * `firstPageLoaded` holds once the head page has its full result. Splits can\n * leave several pages ahead of the first loaded item, so neither the item count\n * nor the page count tells a loading first page apart from a loaded empty one.\n */\nexport type Interpretation =\n | { readonly _tag: \"ResetRequired\" }\n | {\n readonly _tag: \"Failed\";\n readonly error: unknown;\n /**\n * Encoded items across the loaded pages, in order. A page whose\n * replacement failed is still shown, and stays subscribed with the\n * replacement so that a retry re-requests only the replacement. A page\n * whose own result failed ends the items, since its range is unknown.\n */\n readonly items: ReadonlyArray<unknown>;\n readonly firstPageLoaded: boolean;\n readonly transitions: ReadonlyArray<(state: State) => State>;\n }\n | {\n readonly _tag: \"Interpreted\";\n /**\n * Encoded items across the loaded pages, in order.\n */\n readonly items: ReadonlyArray<unknown>;\n readonly firstPageLoaded: boolean;\n /**\n * The last page's result, when every page has loaded completely.\n */\n readonly lastResult: Option.Option<PageResult>;\n /**\n * Split/swap transitions this render discovered.\n */\n readonly transitions: ReadonlyArray<(state: State) => State>;\n };\n\n/**\n * Walk the pages in display order, concatenating their items and collecting the\n * transitions to apply (completed split swaps, recommended splits, eager splits\n * of pages that outgrew `initialNumItems`).\n *\n * `isInvalidCursorError` marks errors that call for a full pagination reset\n * rather than a failure (the cursor no longer matches the query).\n */\nexport const interpret = (\n state: State,\n results: Record.ReadonlyRecord<string, PageResult | Error | undefined>,\n options: {\n readonly initialNumItems: number;\n readonly isInvalidCursorError: (error: Error) => boolean;\n },\n): Interpretation => {\n const resultOf = (key: string) =>\n Record.get(results, key).pipe(\n Option.flatMap(Option.fromNullishOr),\n Option.getOrUndefined,\n );\n const firstPageLoaded = Option.exists(Array.head(state.pageKeys), (key) => {\n const result = resultOf(key);\n return (\n result !== undefined &&\n !(result instanceof Error) &&\n result.pageStatus !== \"SplitRequired\"\n );\n });\n // The first error in display order is the one reported.\n const finish = (\n items: ReadonlyArray<unknown>,\n lastResult: Option.Option<PageResult>,\n transitions: ReadonlyArray<(state: State) => State>,\n errors: ReadonlyArray<Error>,\n ): Interpretation =>\n Array.some(errors, options.isInvalidCursorError)\n ? { _tag: \"ResetRequired\" }\n : Option.match(Array.head(errors), {\n onNone: () => ({\n _tag: \"Interpreted\",\n items,\n firstPageLoaded,\n lastResult,\n transitions,\n }),\n onSome: (error) => ({\n _tag: \"Failed\",\n error,\n items,\n firstPageLoaded,\n transitions,\n }),\n });\n\n const go = (\n remaining: ReadonlyArray<string>,\n items: ReadonlyArray<unknown>,\n transitions: ReadonlyArray<(s: State) => State>,\n lastResult: Option.Option<PageResult>,\n errors: ReadonlyArray<Error>,\n ): Interpretation =>\n Option.match(Array.head(remaining), {\n onNone: () => finish(items, lastResult, transitions, errors),\n onSome: (pageKey) => {\n const result = resultOf(pageKey);\n if (result === undefined) {\n // The trailing pages are still loading.\n return finish(items, Option.none(), transitions, errors);\n }\n if (result instanceof Error) {\n return finish(\n items,\n Option.none(),\n transitions,\n Array.append(errors, result),\n );\n }\n // A force-split page couldn't be fetched in full: show the items\n // before it and report the tail as still loading.\n const shown =\n result.pageStatus === \"SplitRequired\"\n ? items\n : Array.appendAll(items, result.page);\n\n const ongoingSplit = Record.get(state.ongoingSplits, pageKey);\n // A failed replacement leaves this page shown and its range pinned, so\n // the pages after it stay contiguous and the walk goes on.\n const replacementResults = Option.match(ongoingSplit, {\n onNone: () => [],\n onSome: Array.map(resultOf),\n });\n const nextErrors = Array.appendAll(\n errors,\n Array.filter(\n replacementResults,\n (replacement) => replacement instanceof Error,\n ),\n );\n const nextTransitions = Option.match(ongoingSplit, {\n onSome: () =>\n // Swap the replacements in once all of them have loaded.\n Array.every(\n replacementResults,\n (replacement) =>\n replacement !== undefined && !(replacement instanceof Error),\n )\n ? Array.append(transitions, completeSplit(pageKey))\n : transitions,\n onNone: () =>\n typeof result.splitCursor === \"string\" &&\n (result.pageStatus === \"SplitRecommended\" ||\n result.pageStatus === \"SplitRequired\" ||\n result.page.length > options.initialNumItems)\n ? Array.append(transitions, split(pageKey, result.splitCursor))\n : // A loaded growing page pins itself to the range it just\n // served (`END_CURSOR` once exhausted), so it stops being a\n // sliding window that sheds items as new documents arrive.\n Option.exists(\n Record.get(state.pages, pageKey),\n (request) => request.endCursor === undefined,\n ) && result.pageStatus !== \"SplitRequired\"\n ? Array.append(transitions, pin(pageKey, result.continueCursor))\n : transitions,\n });\n\n return result.pageStatus === \"SplitRequired\"\n ? finish(shown, Option.none(), nextTransitions, nextErrors)\n : go(\n Array.drop(remaining, 1),\n shown,\n nextTransitions,\n Option.some(result),\n nextErrors,\n );\n },\n });\n\n return go(state.pageKeys, [], [], Option.none(), []);\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -187,7 +187,7 @@ const useStreamPaginatedQuery = (ref, args, options) => {
187
187
  resetKey: previous.resetKey,
188
188
  state: freshState()
189
189
  }));
190
- else if (interpretation._tag === "Interpreted" && interpretation.transitions.length > 0) applyTransitions(interpretation.transitions);
190
+ else if (interpretation.transitions.length > 0) applyTransitions(interpretation.transitions);
191
191
  const encodedItems = interpretation._tag === "ResetRequired" ? NO_ITEMS : interpretation.items;
192
192
  const decodeCache = useMemo(() => /* @__PURE__ */ new WeakMap(), [ref]);
193
193
  const decodedResults = useMemo(() => encodedItems.map((item) => {
@@ -215,8 +215,8 @@ const useStreamPaginatedQuery = (ref, args, options) => {
215
215
  }
216
216
  throw error;
217
217
  }
218
+ if (!interpretation.firstPageLoaded) return loadingFirstPage({ skipped });
218
219
  const { lastResult } = interpretation;
219
- if (Option.isNone(lastResult) && current.state.pageKeys.length <= 1) return loadingFirstPage({ skipped });
220
220
  if (Option.isNone(lastResult) || isLastPageSplitting(current.state)) return loadingMore({ results: decodedResults });
221
221
  if (lastResult.value.isDone) return exhausted({ results: decodedResults });
222
222
  const continueCursor = lastResult.value.continueCursor;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["useConvexQuery","QueryResult.load","QueryResult.succeed","QueryResult.fail","convexVersion","PaginatedQueryResult.failure","PaginatedQueryResult.loadingFirstPage","PaginatedQueryResult.loadingMore","PaginatedQueryResult.canLoadMore","PaginatedQueryResult.exhausted","StreamPagination.empty","StreamPagination.initial","StreamPagination.pageRequests","useConvexQueries","StreamPagination.interpret","StreamPagination.isLastPageSplitting","StreamPagination.loadMore","OptimisticLocalStore.make","useConvexMutation","useConvexAction"],"sources":["../src/index.ts"],"sourcesContent":["import { Ref } from \"@confect/core\";\nimport { version as convexVersion } from \"convex\";\nimport type { OptimisticUpdate as ConvexOptimisticUpdate } from \"convex/browser\";\nimport * as ConvexReact from \"convex/react\";\nimport type { usePaginatedQuery as useConvexPaginatedQuery } from \"convex/react\";\nimport {\n useAction as useConvexAction,\n useMutation as useConvexMutation,\n useQueries as useConvexQueries,\n useQuery as useConvexQuery,\n type ReactMutation as ConvexReactMutation,\n} from \"convex/react\";\nimport { getFunctionName, type FunctionReference } from \"convex/server\";\nimport { ConvexError, convexToJson, type Value } from \"convex/values\";\nimport * as Cause from \"effect/Cause\";\nimport * as Effect from \"effect/Effect\";\nimport * as Result from \"effect/Result\";\nimport * as Exit from \"effect/Exit\";\nimport * as Match from \"effect/Match\";\nimport * as Option from \"effect/Option\";\nimport { useCallback, useMemo, useState } from \"react\";\n\nimport * as OptimisticLocalStore from \"./OptimisticLocalStore.js\";\nimport * as PaginatedQueryResult from \"./PaginatedQueryResult.js\";\nimport * as QueryResult from \"./QueryResult.js\";\nimport * as StreamPagination from \"./StreamPagination.js\";\n\nexport {\n OptimisticLocalStore,\n PaginatedQueryResult,\n QueryResult,\n StreamPagination,\n};\n\nexport type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [\n never,\n]\n ? Promise<Ref.Returns<Ref_>>\n : Promise<Result.Result<Ref.Returns<Ref_>, Ref.Error<Ref_>>>;\n\ntype UseQueryArgs<Query extends Ref.AnyPublicQuery> =\n keyof Ref.Args<Query> extends never\n ? [args?: Ref.Args<Query> | \"skip\"]\n : [args: Ref.Args<Query> | \"skip\"];\n\nexport const useQuery = <Query extends Ref.AnyPublicQuery>(\n ref: Query,\n ...rest: UseQueryArgs<Query>\n): QueryResult.QueryResult<Ref.Returns<Query>, Ref.Error<Query>> => {\n const functionReference = Ref.getFunctionReference(ref);\n const args = rest[0];\n const skipped = args === \"skip\";\n const encodedArgs = skipped\n ? \"skip\"\n : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);\n\n // `useConvexQuery` returns a referentially stable value while the underlying\n // Convex result is unchanged, and throws a stable error when the query\n // fails. We capture either outcome as a `Result` and decode/wrap it inside\n // `useMemo` so that the returned `QueryResult` keeps a stable identity across\n // renders when nothing has actually changed. Decoding on every render would\n // hand consumers a fresh object each time, breaking effects and memoization\n // that depend on the result's identity.\n const encodedReturnsOrError: Result.Result<unknown, unknown> = Result.try(\n () => useConvexQuery(functionReference, encodedArgs),\n );\n\n return useMemo(\n () =>\n Result.match(encodedReturnsOrError, {\n onSuccess: (encodedReturnsOrUndefined) =>\n encodedReturnsOrUndefined === undefined\n ? QueryResult.load(skipped)\n : QueryResult.succeed(\n Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),\n ),\n onFailure: (error) => {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorOption(ref, error.data);\n if (Option.isSome(decoded)) {\n return QueryResult.fail(decoded.value);\n }\n }\n throw error;\n },\n }),\n // `Result.try` allocates a fresh wrapper each render, so we key the memo on\n // the stable value it carries (the Convex result or thrown error) rather\n // than the wrapper itself; the decoded result is a function of that value,\n // `ref`, and `skipped`.\n [ref, skipped, Result.merge(encodedReturnsOrError)],\n );\n};\n\n/**\n * Like `Omit`, but implemented with key remapping so that optional modifiers\n * are preserved and index signatures don't reintroduce the omitted key's\n * literal type. Mirrors `BetterOmit` from `convex/server`.\n */\ntype OmitKey<T, K extends PropertyKey> = {\n [P in keyof T as P extends K ? never : P]: T[P];\n};\n\n/**\n * The args accepted by {@link usePaginatedQuery}: the ref's args without\n * `paginationOpts`, which the underlying Convex hook manages itself. When no\n * args remain, resolves to `Record<string, never>` (only the empty object is\n * accepted) rather than `{}`, which would accept any non-nullish value.\n */\nexport type PaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =\n OmitKey<Ref.Args<Query>, \"paginationOpts\"> extends infer Args_\n ? keyof Args_ extends never\n ? Record<string, never>\n : Args_\n : never;\n\n/**\n * The item type for a paginated query: the element type of the `page` field of\n * the ref's returns.\n */\nexport type PaginatedQueryItem<Query extends Ref.AnyPublicPaginatedQuery> =\n Ref.Returns<Query>[\"page\"][number];\n\n/**\n * The args parameter of {@link usePaginatedQuery}: the ref's args without\n * `paginationOpts`, or `\"skip\"` to skip the query.\n */\nexport type UsePaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =\n | PaginatedQueryArgs<Query>\n | \"skip\";\n\nexport type PaginatedQueryOptions = Parameters<\n typeof useConvexPaginatedQuery\n>[2];\n\n/**\n * The `user`-facing result of `usePaginatedQueryInternal` from `convex/react`,\n * including the `\"Error\"` status its non-throwing mode can return.\n */\ninterface ConvexPaginatedQueryInternalUser {\n readonly results: ReadonlyArray<unknown>;\n readonly status:\n | \"LoadingFirstPage\"\n | \"LoadingMore\"\n | \"CanLoadMore\"\n | \"Exhausted\"\n | \"Error\";\n readonly isLoading: boolean;\n readonly loadMore: (numItems: number) => void;\n readonly error?: unknown;\n}\n\ntype UseConvexPaginatedQueryInternal = (\n query: FunctionReference<\"query\">,\n args: Record<string, Value> | \"skip\",\n options: PaginatedQueryOptions,\n throwOnError: boolean,\n) => { user: ConvexPaginatedQueryInternalUser };\n\n// `usePaginatedQueryInternal` is runtime-exported by convex/react but absent\n// from its public type declarations. It is the only way to observe query\n// errors as values: the public positional `usePaginatedQuery` hard-codes\n// throwOnError and throws mid-hook-sequence, which cannot be caught without\n// breaking React's hook-order invariant.\nconst useConvexPaginatedQueryInternal = (\n ConvexReact as unknown as Record<string, unknown>\n).usePaginatedQueryInternal as UseConvexPaginatedQueryInternal | undefined;\n\nconst MINIMUM_CONVEX_VERSION = \"1.36.0\";\n\n/**\n * The non-throwing mode of `usePaginatedQueryInternal`—its fourth,\n * `throwOnError` parameter—arrived in convex 1.36.0. Convex 1.32 through 1.35\n * export the same symbol taking only three parameters, where a fourth argument\n * is silently ignored and errors are always thrown.\n *\n * The function itself cannot be interrogated for this: `Function.length` counts\n * parameters up to the first one with a default, and 1.36.0 declares\n * `throwOnError = true`, so both shapes report an arity of 3. The package\n * version is the only reliable signal.\n */\nconst supportsErrorsAsValues = ((): boolean => {\n const [major, minor] = convexVersion.split(\".\").map(Number);\n if (\n major === undefined ||\n minor === undefined ||\n Number.isNaN(major) ||\n Number.isNaN(minor)\n ) {\n return false;\n }\n const [minimumMajor, minimumMinor] = MINIMUM_CONVEX_VERSION.split(\".\").map(\n Number,\n ) as [number, number];\n return (\n major > minimumMajor || (major === minimumMajor && minor >= minimumMinor)\n );\n})();\n\n/**\n * Load data reactively from a paginated query defined with\n * `FunctionSpec.publicPaginatedQuery`, mirroring the ergonomics of\n * `usePaginatedQuery` from `convex/react`.\n *\n * Args are encoded via the ref's user-args schema (`paginationOpts` is managed\n * by the Convex hook, not the caller), and each loaded page is decoded via the\n * ref's item schema.\n *\n * Returns a {@link PaginatedQueryResult.PaginatedQueryResult}: the loaded\n * variants carry `results` and `isLoading`, and `CanLoadMore` additionally\n * carries `loadMore`; if the `Ref` declares an `error` schema and the query\n * fails with that typed error, the decoded error is returned as the `Failure`\n * variant, which also carries the pages loaded before the failure. Unknown\n * errors are thrown, to be caught by an error boundary.\n */\nexport const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(\n ref: Query,\n args: UsePaginatedQueryArgs<Query>,\n options: PaginatedQueryOptions,\n): PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n> => {\n if (\n useConvexPaginatedQueryInternal === undefined ||\n !supportsErrorsAsValues\n ) {\n throw new Error(\n `usePaginatedQuery requires convex >= ${MINIMUM_CONVEX_VERSION}, but found ` +\n `${convexVersion}. Earlier versions always throw paginated query errors ` +\n \"instead of returning them, so a declared `error` schema cannot be \" +\n \"surfaced as a `Failure`—upgrade the `convex` package.\",\n );\n }\n\n const functionReference = Ref.getFunctionReference(ref);\n const skipped = args === \"skip\";\n\n // The encoded output is the wire form Convex's hook forwards verbatim, not\n // the ref's decoded args type.\n const encodedArgs = useMemo(\n () =>\n args === \"skip\"\n ? (\"skip\" as const)\n : (Ref.encodePaginatedQueryArgsSync(\n ref,\n args as unknown as Omit<Ref.Args<Query>, \"paginationOpts\">,\n ) as Record<string, Value>),\n [ref, args],\n );\n\n const { user: convexResult } = useConvexPaginatedQueryInternal(\n functionReference,\n encodedArgs,\n options,\n false,\n );\n\n // The Convex hook carries the pages loaded so far on every status,\n // failures included, so results are decoded unconditionally.\n //\n // Decoding allocates fresh items, so key on the referentially stable\n // `results` the Convex hook provides—the same identity-preservation\n // rationale as in `useQuery` above.\n const { results: encodedResults } = convexResult;\n const decodedResults = useMemo(\n () => Ref.decodePaginationPageSync(ref, encodedResults),\n [ref, encodedResults],\n );\n\n const { status, loadMore } = convexResult;\n const error = status === \"Error\" ? convexResult.error : undefined;\n\n return useMemo(\n (): PaginatedQueryResult.Variants<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n > =>\n // `status` is a bare string union rather than a tagged one, so the arms\n // match on literals.\n Match.value(status).pipe(\n Match.when(\"Error\", () => {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorOption(ref, error.data);\n if (Option.isSome(decoded)) {\n return PaginatedQueryResult.failure({\n error: decoded.value,\n results: decodedResults,\n });\n }\n }\n // Unknown errors still throw. All hooks have run by this point, so\n // an aborted render here is hook-order-safe.\n throw error;\n }),\n Match.when(\"LoadingFirstPage\", () =>\n PaginatedQueryResult.loadingFirstPage({ skipped }),\n ),\n Match.when(\"LoadingMore\", () =>\n PaginatedQueryResult.loadingMore({ results: decodedResults }),\n ),\n // `CanLoadMore` is the only status whose `loadMore` does anything—the Convex hook returns an intentional no-op for all the others, so\n // they carry no callback at all.\n Match.when(\"CanLoadMore\", () =>\n PaginatedQueryResult.canLoadMore({\n results: decodedResults,\n loadMore,\n }),\n ),\n Match.when(\"Exhausted\", () =>\n PaginatedQueryResult.exhausted({ results: decodedResults }),\n ),\n Match.exhaustive,\n ),\n // The memo produces `Variants`, the conditional return type's superset—when `E` is `never` the `Failure` arm is unreachable at runtime, which\n // is exactly what the conditional encodes.\n [ref, skipped, decodedResults, status, loadMore, error],\n ) as PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n >;\n};\n\n/**\n * Whether a paginated query error means the stored cursors no longer match the\n * query (a data-dependent query changed underneath us), calling for a full\n * pagination reset rather than a failure.\n */\nconst isInvalidCursorError = (error: Error): boolean =>\n error.message.includes(\"InvalidCursor\") ||\n (error instanceof ConvexError &&\n typeof error.data === \"object\" &&\n error.data !== null &&\n // Both Convex's built-in pagination (which also sets\n // `isConvexSystemError`) and `QueryStream.paginate` signal an invalid\n // cursor with this data shape—the same check `convex/react` performs.\n (error.data as { paginationError?: unknown }).paginationError ===\n \"InvalidCursor\");\n\nconst NO_ITEMS: ReadonlyArray<unknown> = [];\n\n/**\n * EXPERIMENTAL—endCursor-pinned reactive pagination (see\n * `notes/stream-based-querying.md`). Use it with paginated queries whose\n * handlers paginate via `QueryStream.paginate`: those don't write the query\n * journal that {@link usePaginatedQuery}'s built-in reactivity relies on, so\n * gap-free pages must be maintained by the client instead. (It works with any\n * paginated query honoring the `endCursor` protocol field, including the\n * built-in `paginate`.)\n *\n * Each loaded page is pinned to a fixed index range by re-subscribing it with\n * its `continueCursor` echoed back as `endCursor`—pages then grow and shrink\n * reactively but always meet exactly, and a page that outgrows\n * `initialNumItems` is split in two. This is the mechanism of\n * `convex-helpers/react`'s `usePaginatedQuery`, re-expressed over the pure\n * {@link StreamPagination} state machine, with args/items/errors codec'd through\n * the ref's schemas exactly like {@link usePaginatedQuery}.\n */\nexport const useStreamPaginatedQuery = <\n Query extends Ref.AnyPublicPaginatedQuery,\n>(\n ref: Query,\n args: UsePaginatedQueryArgs<Query>,\n options: {\n readonly initialNumItems: number;\n /**\n * Per-page read budgets forwarded as `paginationOpts.maximumRowsRead` and\n * `paginationOpts.maximumBytesRead`: a page that would scan more rows, or\n * read more bytes, than these returns truncated with `SplitRequired` (and\n * the hook splits it) instead of exceeding Convex's query limits on a\n * filter-heavy stream.\n */\n readonly maximumRowsRead?: number;\n readonly maximumBytesRead?: number;\n },\n): PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n> => {\n // `useQueries` requires a *referentially stable* queries object while\n // nothing has changed: its subscription memo keys on identity, and its\n // `useSubscription` re-runs a render-phase state update whenever the\n // subscription changes—an unstable input loops the render. Callers\n // typically pass a fresh `args` literal each render, so everything that\n // feeds the queries object is stabilized by *value* here: the function\n // reference by `ref`, and the encoded args by their serialized form (as\n // `convex/react`'s own paginated hook does).\n const functionReference = useMemo(() => Ref.getFunctionReference(ref), [ref]);\n const skipped = args === \"skip\";\n\n const rawEncodedArgs =\n args === \"skip\"\n ? undefined\n : (Ref.encodePaginatedQueryArgsSync(\n ref,\n args as unknown as Omit<Ref.Args<Query>, \"paginationOpts\">,\n ) as Record<string, Value>);\n const encodedArgsKey =\n rawEncodedArgs === undefined\n ? \"skip\"\n : JSON.stringify(convexToJson(rawEncodedArgs));\n const encodedArgs = useMemo(\n () => rawEncodedArgs,\n // The serialized form stands in for the freshly allocated object.\n [ref, encodedArgsKey],\n );\n\n // A change of query, args, skippedness, or page size restarts pagination\n // from scratch.\n const resetKey = useMemo(\n () =>\n JSON.stringify([\n getFunctionName(functionReference),\n encodedArgsKey,\n options.initialNumItems,\n options.maximumRowsRead ?? null,\n options.maximumBytesRead ?? null,\n ]),\n [\n functionReference,\n encodedArgsKey,\n options.initialNumItems,\n options.maximumRowsRead,\n options.maximumBytesRead,\n ],\n );\n\n const freshState = () =>\n skipped\n ? StreamPagination.empty\n : StreamPagination.initial(options.initialNumItems, options);\n\n const [tracked, setTracked] = useState(() => ({\n resetKey,\n state: freshState(),\n }));\n // Render-phase reset—the React-sanctioned derived-state-from-props\n // pattern, also how `convex/react`'s own paginated hook resets.\n let current = tracked;\n if (current.resetKey !== resetKey) {\n current = { resetKey, state: freshState() };\n setTracked(current);\n }\n\n const applyTransitions = useCallback(\n (\n transitions: ReadonlyArray<\n (state: StreamPagination.State) => StreamPagination.State\n >,\n ) =>\n setTracked((previous) => ({\n resetKey: previous.resetKey,\n state: transitions.reduce(\n (state, transition) => transition(state),\n previous.state,\n ),\n })),\n [],\n );\n\n const queries = useMemo(\n () =>\n StreamPagination.pageRequests(current.state, (page) => ({\n query: functionReference,\n args: { ...encodedArgs, paginationOpts: page },\n })),\n [current.state, functionReference, encodedArgs],\n );\n\n const resultsObject = useConvexQueries(\n queries as unknown as Parameters<typeof useConvexQueries>[0],\n );\n\n const interpretation = useMemo(\n () =>\n StreamPagination.interpret(\n current.state,\n resultsObject as StreamPagination.Results,\n { initialNumItems: options.initialNumItems, isInvalidCursorError },\n ),\n [current.state, resultsObject, options.initialNumItems],\n );\n\n // Apply the transitions this render discovered (completed split swaps,\n // new splits of overgrown pages) or restart after an invalid cursor—render-phase state updates, as in `convex-helpers`' hook. Both settle:\n // the updated state no longer produces the same discovery.\n if (interpretation._tag === \"ResetRequired\") {\n setTracked((previous) => ({\n resetKey: previous.resetKey,\n state: freshState(),\n }));\n } else if (\n interpretation._tag === \"Interpreted\" &&\n interpretation.transitions.length > 0\n ) {\n applyTransitions(interpretation.transitions);\n }\n\n const encodedItems =\n interpretation._tag === \"ResetRequired\" ? NO_ITEMS : interpretation.items;\n // Page results keep their identity while unchanged (only the page that\n // actually updated is a fresh object), so caching decodes per encoded\n // item makes a single-page update decode only that page's items rather\n // than every loaded page's.\n const decodeCache = useMemo(\n () => new WeakMap<object, PaginatedQueryItem<Query>>(),\n [ref],\n );\n const decodedResults = useMemo(\n () =>\n encodedItems.map((item): PaginatedQueryItem<Query> => {\n if (typeof item !== \"object\" || item === null) {\n return Ref.decodePaginationPageSync(ref, [\n item,\n ])[0] as PaginatedQueryItem<Query>;\n }\n const cached = decodeCache.get(item);\n if (cached !== undefined) {\n return cached;\n }\n const decoded = Ref.decodePaginationPageSync(ref, [\n item,\n ])[0] as PaginatedQueryItem<Query>;\n decodeCache.set(item, decoded);\n return decoded;\n }),\n [ref, encodedItems, decodeCache],\n );\n\n return useMemo((): PaginatedQueryResult.Variants<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n > => {\n if (interpretation._tag === \"ResetRequired\") {\n return PaginatedQueryResult.loadingFirstPage({ skipped });\n }\n if (interpretation._tag === \"Failed\") {\n const error = interpretation.error;\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorOption(ref, error.data);\n if (Option.isSome(decoded)) {\n return PaginatedQueryResult.failure({\n error: decoded.value,\n results: decodedResults,\n });\n }\n }\n // Unknown errors still throw, to be caught by an error boundary.\n throw error;\n }\n\n const { lastResult } = interpretation;\n if (Option.isNone(lastResult) && current.state.pageKeys.length <= 1) {\n return PaginatedQueryResult.loadingFirstPage({ skipped });\n }\n if (\n Option.isNone(lastResult) ||\n StreamPagination.isLastPageSplitting(current.state)\n ) {\n return PaginatedQueryResult.loadingMore({ results: decodedResults });\n }\n if (lastResult.value.isDone) {\n return PaginatedQueryResult.exhausted({ results: decodedResults });\n }\n\n const continueCursor = lastResult.value.continueCursor;\n // A per-result guard, as in `convex-helpers`: calling `loadMore`\n // repeatedly before the next render is a single load.\n let alreadyLoadingMore = false;\n return PaginatedQueryResult.canLoadMore({\n results: decodedResults,\n loadMore: (numItems: number) => {\n if (!alreadyLoadingMore) {\n alreadyLoadingMore = true;\n applyTransitions([\n StreamPagination.loadMore(continueCursor, numItems, options),\n ]);\n }\n },\n });\n }, [\n interpretation,\n decodedResults,\n current.state,\n skipped,\n ref,\n applyTransitions,\n ]) as PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n >;\n};\n\n/**\n * An optimistic update for a Confect mutation. Mirrors Convex's\n * `OptimisticUpdate`, but receives a Confect {@link OptimisticLocalStore} and\n * the decoded mutation `args`.\n */\nexport type OptimisticUpdate<Mutation extends Ref.AnyPublicMutation> = (\n localStore: OptimisticLocalStore.OptimisticLocalStore,\n args: Ref.Args<Mutation>,\n) => void;\n\n/**\n * The handle returned by {@link useMutation}. It is callable like the function\n * returned by Convex's `useMutation`, and additionally exposes\n * `withOptimisticUpdate` for attaching an optimistic update. Mirrors the\n * `ReactMutation` type from `convex/react`.\n */\nexport interface ReactMutation<Mutation extends Ref.AnyPublicMutation> {\n (...args: Ref.OptionalArgs<Mutation>): InvokeReturn<Mutation>;\n withOptimisticUpdate(\n optimisticUpdate: OptimisticUpdate<Mutation>,\n ): ReactMutation<Mutation>;\n}\n\nconst makeReactMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n convexReactMutation: ConvexReactMutation<\n Ref.FunctionReference<Mutation> & FunctionReference<\"mutation\">\n >,\n): ReactMutation<Mutation> => {\n const callable = ((...args: Ref.OptionalArgs<Mutation>) =>\n invokeAsResult(\n ref,\n (_, encodedArgs) => convexReactMutation(encodedArgs as never),\n args,\n ).then((result) =>\n Ref.hasErrorSchema(ref) ? result : Result.getOrThrow(result),\n )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;\n\n const withOptimisticUpdate = (\n optimisticUpdate: OptimisticUpdate<Mutation>,\n ): ReactMutation<Mutation> => {\n const wrappedUpdate: ConvexOptimisticUpdate<Record<string, Value>> = (\n convexLocalStore,\n encodedArgs,\n ) => {\n const decodedArgs = Ref.decodeArgsSync(ref, encodedArgs);\n optimisticUpdate(\n OptimisticLocalStore.make(convexLocalStore),\n decodedArgs,\n );\n };\n const nextConvexReactMutation =\n convexReactMutation.withOptimisticUpdate(wrappedUpdate);\n return makeReactMutation(ref, nextConvexReactMutation);\n };\n\n return Object.assign(callable, { withOptimisticUpdate });\n};\n\n/**\n * Returns a {@link ReactMutation} handle for the provided `Ref`'s mutation. The\n * handle is callable to invoke the mutation, and exposes `withOptimisticUpdate`\n * for attaching an optimistic update, mirroring `useMutation` from\n * `convex/react`.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to a\n * `Result` with the decoded `returns` value in the `Success` and the decoded\n * error in the `Failure`.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n): ReactMutation<Mutation> => {\n const functionReference = Ref.getFunctionReference(ref);\n const convexReactMutation = useConvexMutation(functionReference);\n\n return useMemo(\n () =>\n makeReactMutation(\n ref,\n convexReactMutation as ConvexReactMutation<\n Ref.FunctionReference<Mutation> & FunctionReference<\"mutation\">\n >,\n ),\n [ref, convexReactMutation],\n );\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s action.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to a\n * `Result` with the decoded `returns` value in the `Success` and the decoded\n * error in the `Failure`.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useAction = <Action extends Ref.AnyPublicAction>(\n ref: Action,\n): ((...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualAction = useConvexAction(functionReference);\n\n return useCallback(\n ((...args: Ref.OptionalArgs<Action>) =>\n invokeAsResult(\n ref,\n (_, encodedArgs) => actualAction(encodedArgs),\n args,\n ).then((result) =>\n Ref.hasErrorSchema(ref) ? result : Result.getOrThrow(result),\n )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>,\n [ref, actualAction],\n );\n};\n\nconst invokeAsResult = <Ref_ extends Ref.Any>(\n ref: Ref_,\n invoke: (\n fnRef: Ref.FunctionReference<Ref_>,\n encodedArgs: unknown,\n ) => PromiseLike<unknown>,\n args: Ref.OptionalArgs<Ref_>,\n): Promise<Result.Result<Ref.Returns<Ref_>, Ref.Error<Ref_>>> => {\n const exitPromise = Effect.runPromiseExit(\n Ref.runWithCodec(ref, (args[0] ?? {}) as Ref.Args<Ref_>, invoke).pipe(\n Effect.catchTag(\"SchemaError\", Effect.die),\n Effect.result,\n ),\n );\n return exitPromise.then((exit) => {\n if (Exit.isSuccess(exit)) return exit.value;\n throw Cause.squash(exit.cause);\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["useConvexQuery","QueryResult.load","QueryResult.succeed","QueryResult.fail","convexVersion","PaginatedQueryResult.failure","PaginatedQueryResult.loadingFirstPage","PaginatedQueryResult.loadingMore","PaginatedQueryResult.canLoadMore","PaginatedQueryResult.exhausted","StreamPagination.empty","StreamPagination.initial","StreamPagination.pageRequests","useConvexQueries","StreamPagination.interpret","StreamPagination.isLastPageSplitting","StreamPagination.loadMore","OptimisticLocalStore.make","useConvexMutation","useConvexAction"],"sources":["../src/index.ts"],"sourcesContent":["import { Ref } from \"@confect/core\";\nimport { version as convexVersion } from \"convex\";\nimport type { OptimisticUpdate as ConvexOptimisticUpdate } from \"convex/browser\";\nimport * as ConvexReact from \"convex/react\";\nimport type { usePaginatedQuery as useConvexPaginatedQuery } from \"convex/react\";\nimport {\n useAction as useConvexAction,\n useMutation as useConvexMutation,\n useQueries as useConvexQueries,\n useQuery as useConvexQuery,\n type ReactMutation as ConvexReactMutation,\n} from \"convex/react\";\nimport { getFunctionName, type FunctionReference } from \"convex/server\";\nimport { ConvexError, convexToJson, type Value } from \"convex/values\";\nimport * as Cause from \"effect/Cause\";\nimport * as Effect from \"effect/Effect\";\nimport * as Result from \"effect/Result\";\nimport * as Exit from \"effect/Exit\";\nimport * as Match from \"effect/Match\";\nimport * as Option from \"effect/Option\";\nimport { useCallback, useMemo, useState } from \"react\";\n\nimport * as OptimisticLocalStore from \"./OptimisticLocalStore.js\";\nimport * as PaginatedQueryResult from \"./PaginatedQueryResult.js\";\nimport * as QueryResult from \"./QueryResult.js\";\nimport * as StreamPagination from \"./StreamPagination.js\";\n\nexport {\n OptimisticLocalStore,\n PaginatedQueryResult,\n QueryResult,\n StreamPagination,\n};\n\nexport type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [\n never,\n]\n ? Promise<Ref.Returns<Ref_>>\n : Promise<Result.Result<Ref.Returns<Ref_>, Ref.Error<Ref_>>>;\n\ntype UseQueryArgs<Query extends Ref.AnyPublicQuery> =\n keyof Ref.Args<Query> extends never\n ? [args?: Ref.Args<Query> | \"skip\"]\n : [args: Ref.Args<Query> | \"skip\"];\n\nexport const useQuery = <Query extends Ref.AnyPublicQuery>(\n ref: Query,\n ...rest: UseQueryArgs<Query>\n): QueryResult.QueryResult<Ref.Returns<Query>, Ref.Error<Query>> => {\n const functionReference = Ref.getFunctionReference(ref);\n const args = rest[0];\n const skipped = args === \"skip\";\n const encodedArgs = skipped\n ? \"skip\"\n : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);\n\n // `useConvexQuery` returns a referentially stable value while the underlying\n // Convex result is unchanged, and throws a stable error when the query\n // fails. We capture either outcome as a `Result` and decode/wrap it inside\n // `useMemo` so that the returned `QueryResult` keeps a stable identity across\n // renders when nothing has actually changed. Decoding on every render would\n // hand consumers a fresh object each time, breaking effects and memoization\n // that depend on the result's identity.\n const encodedReturnsOrError: Result.Result<unknown, unknown> = Result.try(\n () => useConvexQuery(functionReference, encodedArgs),\n );\n\n return useMemo(\n () =>\n Result.match(encodedReturnsOrError, {\n onSuccess: (encodedReturnsOrUndefined) =>\n encodedReturnsOrUndefined === undefined\n ? QueryResult.load(skipped)\n : QueryResult.succeed(\n Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),\n ),\n onFailure: (error) => {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorOption(ref, error.data);\n if (Option.isSome(decoded)) {\n return QueryResult.fail(decoded.value);\n }\n }\n throw error;\n },\n }),\n // `Result.try` allocates a fresh wrapper each render, so we key the memo on\n // the stable value it carries (the Convex result or thrown error) rather\n // than the wrapper itself; the decoded result is a function of that value,\n // `ref`, and `skipped`.\n [ref, skipped, Result.merge(encodedReturnsOrError)],\n );\n};\n\n/**\n * Like `Omit`, but implemented with key remapping so that optional modifiers\n * are preserved and index signatures don't reintroduce the omitted key's\n * literal type. Mirrors `BetterOmit` from `convex/server`.\n */\ntype OmitKey<T, K extends PropertyKey> = {\n [P in keyof T as P extends K ? never : P]: T[P];\n};\n\n/**\n * The args accepted by {@link usePaginatedQuery}: the ref's args without\n * `paginationOpts`, which the underlying Convex hook manages itself. When no\n * args remain, resolves to `Record<string, never>` (only the empty object is\n * accepted) rather than `{}`, which would accept any non-nullish value.\n */\nexport type PaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =\n OmitKey<Ref.Args<Query>, \"paginationOpts\"> extends infer Args_\n ? keyof Args_ extends never\n ? Record<string, never>\n : Args_\n : never;\n\n/**\n * The item type for a paginated query: the element type of the `page` field of\n * the ref's returns.\n */\nexport type PaginatedQueryItem<Query extends Ref.AnyPublicPaginatedQuery> =\n Ref.Returns<Query>[\"page\"][number];\n\n/**\n * The args parameter of {@link usePaginatedQuery}: the ref's args without\n * `paginationOpts`, or `\"skip\"` to skip the query.\n */\nexport type UsePaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =\n | PaginatedQueryArgs<Query>\n | \"skip\";\n\nexport type PaginatedQueryOptions = Parameters<\n typeof useConvexPaginatedQuery\n>[2];\n\n/**\n * The `user`-facing result of `usePaginatedQueryInternal` from `convex/react`,\n * including the `\"Error\"` status its non-throwing mode can return.\n */\ninterface ConvexPaginatedQueryInternalUser {\n readonly results: ReadonlyArray<unknown>;\n readonly status:\n | \"LoadingFirstPage\"\n | \"LoadingMore\"\n | \"CanLoadMore\"\n | \"Exhausted\"\n | \"Error\";\n readonly isLoading: boolean;\n readonly loadMore: (numItems: number) => void;\n readonly error?: unknown;\n}\n\ntype UseConvexPaginatedQueryInternal = (\n query: FunctionReference<\"query\">,\n args: Record<string, Value> | \"skip\",\n options: PaginatedQueryOptions,\n throwOnError: boolean,\n) => { user: ConvexPaginatedQueryInternalUser };\n\n// `usePaginatedQueryInternal` is runtime-exported by convex/react but absent\n// from its public type declarations. It is the only way to observe query\n// errors as values: the public positional `usePaginatedQuery` hard-codes\n// throwOnError and throws mid-hook-sequence, which cannot be caught without\n// breaking React's hook-order invariant.\nconst useConvexPaginatedQueryInternal = (\n ConvexReact as unknown as Record<string, unknown>\n).usePaginatedQueryInternal as UseConvexPaginatedQueryInternal | undefined;\n\nconst MINIMUM_CONVEX_VERSION = \"1.36.0\";\n\n/**\n * The non-throwing mode of `usePaginatedQueryInternal`—its fourth,\n * `throwOnError` parameter—arrived in convex 1.36.0. Convex 1.32 through 1.35\n * export the same symbol taking only three parameters, where a fourth argument\n * is silently ignored and errors are always thrown.\n *\n * The function itself cannot be interrogated for this: `Function.length` counts\n * parameters up to the first one with a default, and 1.36.0 declares\n * `throwOnError = true`, so both shapes report an arity of 3. The package\n * version is the only reliable signal.\n */\nconst supportsErrorsAsValues = ((): boolean => {\n const [major, minor] = convexVersion.split(\".\").map(Number);\n if (\n major === undefined ||\n minor === undefined ||\n Number.isNaN(major) ||\n Number.isNaN(minor)\n ) {\n return false;\n }\n const [minimumMajor, minimumMinor] = MINIMUM_CONVEX_VERSION.split(\".\").map(\n Number,\n ) as [number, number];\n return (\n major > minimumMajor || (major === minimumMajor && minor >= minimumMinor)\n );\n})();\n\n/**\n * Load data reactively from a paginated query defined with\n * `FunctionSpec.publicPaginatedQuery`, mirroring the ergonomics of\n * `usePaginatedQuery` from `convex/react`.\n *\n * Args are encoded via the ref's user-args schema (`paginationOpts` is managed\n * by the Convex hook, not the caller), and each loaded page is decoded via the\n * ref's item schema.\n *\n * Returns a {@link PaginatedQueryResult.PaginatedQueryResult}: the loaded\n * variants carry `results` and `isLoading`, and `CanLoadMore` additionally\n * carries `loadMore`; if the `Ref` declares an `error` schema and the query\n * fails with that typed error, the decoded error is returned as the `Failure`\n * variant, which also carries the pages loaded before the failure. Unknown\n * errors are thrown, to be caught by an error boundary.\n */\nexport const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(\n ref: Query,\n args: UsePaginatedQueryArgs<Query>,\n options: PaginatedQueryOptions,\n): PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n> => {\n if (\n useConvexPaginatedQueryInternal === undefined ||\n !supportsErrorsAsValues\n ) {\n throw new Error(\n `usePaginatedQuery requires convex >= ${MINIMUM_CONVEX_VERSION}, but found ` +\n `${convexVersion}. Earlier versions always throw paginated query errors ` +\n \"instead of returning them, so a declared `error` schema cannot be \" +\n \"surfaced as a `Failure`—upgrade the `convex` package.\",\n );\n }\n\n const functionReference = Ref.getFunctionReference(ref);\n const skipped = args === \"skip\";\n\n // The encoded output is the wire form Convex's hook forwards verbatim, not\n // the ref's decoded args type.\n const encodedArgs = useMemo(\n () =>\n args === \"skip\"\n ? (\"skip\" as const)\n : (Ref.encodePaginatedQueryArgsSync(\n ref,\n args as unknown as Omit<Ref.Args<Query>, \"paginationOpts\">,\n ) as Record<string, Value>),\n [ref, args],\n );\n\n const { user: convexResult } = useConvexPaginatedQueryInternal(\n functionReference,\n encodedArgs,\n options,\n false,\n );\n\n // The Convex hook carries the pages loaded so far on every status,\n // failures included, so results are decoded unconditionally.\n //\n // Decoding allocates fresh items, so key on the referentially stable\n // `results` the Convex hook provides—the same identity-preservation\n // rationale as in `useQuery` above.\n const { results: encodedResults } = convexResult;\n const decodedResults = useMemo(\n () => Ref.decodePaginationPageSync(ref, encodedResults),\n [ref, encodedResults],\n );\n\n const { status, loadMore } = convexResult;\n const error = status === \"Error\" ? convexResult.error : undefined;\n\n return useMemo(\n (): PaginatedQueryResult.Variants<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n > =>\n // `status` is a bare string union rather than a tagged one, so the arms\n // match on literals.\n Match.value(status).pipe(\n Match.when(\"Error\", () => {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorOption(ref, error.data);\n if (Option.isSome(decoded)) {\n return PaginatedQueryResult.failure({\n error: decoded.value,\n results: decodedResults,\n });\n }\n }\n // Unknown errors still throw. All hooks have run by this point, so\n // an aborted render here is hook-order-safe.\n throw error;\n }),\n Match.when(\"LoadingFirstPage\", () =>\n PaginatedQueryResult.loadingFirstPage({ skipped }),\n ),\n Match.when(\"LoadingMore\", () =>\n PaginatedQueryResult.loadingMore({ results: decodedResults }),\n ),\n // `CanLoadMore` is the only status whose `loadMore` does anything—the Convex hook returns an intentional no-op for all the others, so\n // they carry no callback at all.\n Match.when(\"CanLoadMore\", () =>\n PaginatedQueryResult.canLoadMore({\n results: decodedResults,\n loadMore,\n }),\n ),\n Match.when(\"Exhausted\", () =>\n PaginatedQueryResult.exhausted({ results: decodedResults }),\n ),\n Match.exhaustive,\n ),\n // The memo produces `Variants`, the conditional return type's superset—when `E` is `never` the `Failure` arm is unreachable at runtime, which\n // is exactly what the conditional encodes.\n [ref, skipped, decodedResults, status, loadMore, error],\n ) as PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n >;\n};\n\n/**\n * Whether a paginated query error means the stored cursors no longer match the\n * query (a data-dependent query changed underneath us), calling for a full\n * pagination reset rather than a failure.\n */\nconst isInvalidCursorError = (error: Error): boolean =>\n error.message.includes(\"InvalidCursor\") ||\n (error instanceof ConvexError &&\n typeof error.data === \"object\" &&\n error.data !== null &&\n // Both Convex's built-in pagination (which also sets\n // `isConvexSystemError`) and `QueryStream.paginate` signal an invalid\n // cursor with this data shape—the same check `convex/react` performs.\n (error.data as { paginationError?: unknown }).paginationError ===\n \"InvalidCursor\");\n\nconst NO_ITEMS: ReadonlyArray<unknown> = [];\n\n/**\n * EXPERIMENTAL—endCursor-pinned reactive pagination (see\n * `notes/stream-based-querying.md`). Use it with paginated queries whose\n * handlers paginate via `QueryStream.paginate`: those don't write the query\n * journal that {@link usePaginatedQuery}'s built-in reactivity relies on, so\n * gap-free pages must be maintained by the client instead. (It works with any\n * paginated query honoring the `endCursor` protocol field, including the\n * built-in `paginate`.)\n *\n * Each loaded page is pinned to a fixed index range by re-subscribing it with\n * its `continueCursor` echoed back as `endCursor`—pages then grow and shrink\n * reactively but always meet exactly, and a page that outgrows\n * `initialNumItems` is split in two. This is the mechanism of\n * `convex-helpers/react`'s `usePaginatedQuery`, re-expressed over the pure\n * {@link StreamPagination} state machine, with args/items/errors codec'd through\n * the ref's schemas exactly like {@link usePaginatedQuery}.\n */\nexport const useStreamPaginatedQuery = <\n Query extends Ref.AnyPublicPaginatedQuery,\n>(\n ref: Query,\n args: UsePaginatedQueryArgs<Query>,\n options: {\n readonly initialNumItems: number;\n /**\n * Per-page read budgets forwarded as `paginationOpts.maximumRowsRead` and\n * `paginationOpts.maximumBytesRead`: a page that would scan more rows, or\n * read more bytes, than these returns truncated with `SplitRequired` (and\n * the hook splits it) instead of exceeding Convex's query limits on a\n * filter-heavy stream.\n */\n readonly maximumRowsRead?: number;\n readonly maximumBytesRead?: number;\n },\n): PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n> => {\n // `useQueries` requires a *referentially stable* queries object while\n // nothing has changed: its subscription memo keys on identity, and its\n // `useSubscription` re-runs a render-phase state update whenever the\n // subscription changes—an unstable input loops the render. Callers\n // typically pass a fresh `args` literal each render, so everything that\n // feeds the queries object is stabilized by *value* here: the function\n // reference by `ref`, and the encoded args by their serialized form (as\n // `convex/react`'s own paginated hook does).\n const functionReference = useMemo(() => Ref.getFunctionReference(ref), [ref]);\n const skipped = args === \"skip\";\n\n const rawEncodedArgs =\n args === \"skip\"\n ? undefined\n : (Ref.encodePaginatedQueryArgsSync(\n ref,\n args as unknown as Omit<Ref.Args<Query>, \"paginationOpts\">,\n ) as Record<string, Value>);\n const encodedArgsKey =\n rawEncodedArgs === undefined\n ? \"skip\"\n : JSON.stringify(convexToJson(rawEncodedArgs));\n const encodedArgs = useMemo(\n () => rawEncodedArgs,\n // The serialized form stands in for the freshly allocated object.\n [ref, encodedArgsKey],\n );\n\n // A change of query, args, skippedness, or page size restarts pagination\n // from scratch.\n const resetKey = useMemo(\n () =>\n JSON.stringify([\n getFunctionName(functionReference),\n encodedArgsKey,\n options.initialNumItems,\n options.maximumRowsRead ?? null,\n options.maximumBytesRead ?? null,\n ]),\n [\n functionReference,\n encodedArgsKey,\n options.initialNumItems,\n options.maximumRowsRead,\n options.maximumBytesRead,\n ],\n );\n\n const freshState = () =>\n skipped\n ? StreamPagination.empty\n : StreamPagination.initial(options.initialNumItems, options);\n\n const [tracked, setTracked] = useState(() => ({\n resetKey,\n state: freshState(),\n }));\n // Render-phase reset—the React-sanctioned derived-state-from-props\n // pattern, also how `convex/react`'s own paginated hook resets.\n let current = tracked;\n if (current.resetKey !== resetKey) {\n current = { resetKey, state: freshState() };\n setTracked(current);\n }\n\n const applyTransitions = useCallback(\n (\n transitions: ReadonlyArray<\n (state: StreamPagination.State) => StreamPagination.State\n >,\n ) =>\n setTracked((previous) => ({\n resetKey: previous.resetKey,\n state: transitions.reduce(\n (state, transition) => transition(state),\n previous.state,\n ),\n })),\n [],\n );\n\n const queries = useMemo(\n () =>\n StreamPagination.pageRequests(current.state, (page) => ({\n query: functionReference,\n args: { ...encodedArgs, paginationOpts: page },\n })),\n [current.state, functionReference, encodedArgs],\n );\n\n const resultsObject = useConvexQueries(\n queries as unknown as Parameters<typeof useConvexQueries>[0],\n );\n\n const interpretation = useMemo(\n () =>\n StreamPagination.interpret(\n current.state,\n resultsObject as StreamPagination.Results,\n { initialNumItems: options.initialNumItems, isInvalidCursorError },\n ),\n [current.state, resultsObject, options.initialNumItems],\n );\n\n // Apply the transitions this render discovered (completed split swaps,\n // new splits of overgrown pages) or restart after an invalid cursor—render-phase state updates, as in `convex-helpers`' hook. Both settle:\n // the updated state no longer produces the same discovery.\n if (interpretation._tag === \"ResetRequired\") {\n setTracked((previous) => ({\n resetKey: previous.resetKey,\n state: freshState(),\n }));\n } else if (interpretation.transitions.length > 0) {\n applyTransitions(interpretation.transitions);\n }\n\n const encodedItems =\n interpretation._tag === \"ResetRequired\" ? NO_ITEMS : interpretation.items;\n // Page results keep their identity while unchanged (only the page that\n // actually updated is a fresh object), so caching decodes per encoded\n // item makes a single-page update decode only that page's items rather\n // than every loaded page's.\n const decodeCache = useMemo(\n () => new WeakMap<object, PaginatedQueryItem<Query>>(),\n [ref],\n );\n const decodedResults = useMemo(\n () =>\n encodedItems.map((item): PaginatedQueryItem<Query> => {\n if (typeof item !== \"object\" || item === null) {\n return Ref.decodePaginationPageSync(ref, [\n item,\n ])[0] as PaginatedQueryItem<Query>;\n }\n const cached = decodeCache.get(item);\n if (cached !== undefined) {\n return cached;\n }\n const decoded = Ref.decodePaginationPageSync(ref, [\n item,\n ])[0] as PaginatedQueryItem<Query>;\n decodeCache.set(item, decoded);\n return decoded;\n }),\n [ref, encodedItems, decodeCache],\n );\n\n return useMemo((): PaginatedQueryResult.Variants<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n > => {\n if (interpretation._tag === \"ResetRequired\") {\n return PaginatedQueryResult.loadingFirstPage({ skipped });\n }\n if (interpretation._tag === \"Failed\") {\n const error = interpretation.error;\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorOption(ref, error.data);\n if (Option.isSome(decoded)) {\n return PaginatedQueryResult.failure({\n error: decoded.value,\n results: decodedResults,\n });\n }\n }\n // Unknown errors still throw, to be caught by an error boundary.\n throw error;\n }\n\n if (!interpretation.firstPageLoaded) {\n return PaginatedQueryResult.loadingFirstPage({ skipped });\n }\n const { lastResult } = interpretation;\n if (\n Option.isNone(lastResult) ||\n StreamPagination.isLastPageSplitting(current.state)\n ) {\n return PaginatedQueryResult.loadingMore({ results: decodedResults });\n }\n if (lastResult.value.isDone) {\n return PaginatedQueryResult.exhausted({ results: decodedResults });\n }\n\n const continueCursor = lastResult.value.continueCursor;\n // A per-result guard, as in `convex-helpers`: calling `loadMore`\n // repeatedly before the next render is a single load.\n let alreadyLoadingMore = false;\n return PaginatedQueryResult.canLoadMore({\n results: decodedResults,\n loadMore: (numItems: number) => {\n if (!alreadyLoadingMore) {\n alreadyLoadingMore = true;\n applyTransitions([\n StreamPagination.loadMore(continueCursor, numItems, options),\n ]);\n }\n },\n });\n }, [\n interpretation,\n decodedResults,\n current.state,\n skipped,\n ref,\n applyTransitions,\n ]) as PaginatedQueryResult.PaginatedQueryResult<\n PaginatedQueryItem<Query>,\n Ref.Error<Query>\n >;\n};\n\n/**\n * An optimistic update for a Confect mutation. Mirrors Convex's\n * `OptimisticUpdate`, but receives a Confect {@link OptimisticLocalStore} and\n * the decoded mutation `args`.\n */\nexport type OptimisticUpdate<Mutation extends Ref.AnyPublicMutation> = (\n localStore: OptimisticLocalStore.OptimisticLocalStore,\n args: Ref.Args<Mutation>,\n) => void;\n\n/**\n * The handle returned by {@link useMutation}. It is callable like the function\n * returned by Convex's `useMutation`, and additionally exposes\n * `withOptimisticUpdate` for attaching an optimistic update. Mirrors the\n * `ReactMutation` type from `convex/react`.\n */\nexport interface ReactMutation<Mutation extends Ref.AnyPublicMutation> {\n (...args: Ref.OptionalArgs<Mutation>): InvokeReturn<Mutation>;\n withOptimisticUpdate(\n optimisticUpdate: OptimisticUpdate<Mutation>,\n ): ReactMutation<Mutation>;\n}\n\nconst makeReactMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n convexReactMutation: ConvexReactMutation<\n Ref.FunctionReference<Mutation> & FunctionReference<\"mutation\">\n >,\n): ReactMutation<Mutation> => {\n const callable = ((...args: Ref.OptionalArgs<Mutation>) =>\n invokeAsResult(\n ref,\n (_, encodedArgs) => convexReactMutation(encodedArgs as never),\n args,\n ).then((result) =>\n Ref.hasErrorSchema(ref) ? result : Result.getOrThrow(result),\n )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;\n\n const withOptimisticUpdate = (\n optimisticUpdate: OptimisticUpdate<Mutation>,\n ): ReactMutation<Mutation> => {\n const wrappedUpdate: ConvexOptimisticUpdate<Record<string, Value>> = (\n convexLocalStore,\n encodedArgs,\n ) => {\n const decodedArgs = Ref.decodeArgsSync(ref, encodedArgs);\n optimisticUpdate(\n OptimisticLocalStore.make(convexLocalStore),\n decodedArgs,\n );\n };\n const nextConvexReactMutation =\n convexReactMutation.withOptimisticUpdate(wrappedUpdate);\n return makeReactMutation(ref, nextConvexReactMutation);\n };\n\n return Object.assign(callable, { withOptimisticUpdate });\n};\n\n/**\n * Returns a {@link ReactMutation} handle for the provided `Ref`'s mutation. The\n * handle is callable to invoke the mutation, and exposes `withOptimisticUpdate`\n * for attaching an optimistic update, mirroring `useMutation` from\n * `convex/react`.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to a\n * `Result` with the decoded `returns` value in the `Success` and the decoded\n * error in the `Failure`.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n): ReactMutation<Mutation> => {\n const functionReference = Ref.getFunctionReference(ref);\n const convexReactMutation = useConvexMutation(functionReference);\n\n return useMemo(\n () =>\n makeReactMutation(\n ref,\n convexReactMutation as ConvexReactMutation<\n Ref.FunctionReference<Mutation> & FunctionReference<\"mutation\">\n >,\n ),\n [ref, convexReactMutation],\n );\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s action.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to a\n * `Result` with the decoded `returns` value in the `Success` and the decoded\n * error in the `Failure`.\n *\n * If the `Ref` does not declare an `error` schema, the promise resolves\n * directly to the decoded `returns` value, matching the behavior of\n * `useMutation` from `convex/react`.\n *\n * Any other failure rejects the promise.\n */\nexport const useAction = <Action extends Ref.AnyPublicAction>(\n ref: Action,\n): ((...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualAction = useConvexAction(functionReference);\n\n return useCallback(\n ((...args: Ref.OptionalArgs<Action>) =>\n invokeAsResult(\n ref,\n (_, encodedArgs) => actualAction(encodedArgs),\n args,\n ).then((result) =>\n Ref.hasErrorSchema(ref) ? result : Result.getOrThrow(result),\n )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>,\n [ref, actualAction],\n );\n};\n\nconst invokeAsResult = <Ref_ extends Ref.Any>(\n ref: Ref_,\n invoke: (\n fnRef: Ref.FunctionReference<Ref_>,\n encodedArgs: unknown,\n ) => PromiseLike<unknown>,\n args: Ref.OptionalArgs<Ref_>,\n): Promise<Result.Result<Ref.Returns<Ref_>, Ref.Error<Ref_>>> => {\n const exitPromise = Effect.runPromiseExit(\n Ref.runWithCodec(ref, (args[0] ?? {}) as Ref.Args<Ref_>, invoke).pipe(\n Effect.catchTag(\"SchemaError\", Effect.die),\n Effect.result,\n ),\n );\n return exitPromise.then((exit) => {\n if (Exit.isSuccess(exit)) return exit.value;\n throw Cause.squash(exit.cause);\n });\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@gaganref/confect-react",
3
- "version": "10.3.1-fork.13",
3
+ "version": "10.3.1-fork.15",
4
4
  "peerDependencies": {
5
- "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.13",
5
+ "@confect/core": "npm:@gaganref/confect-core@10.3.1-fork.15",
6
6
  "convex": "^1.36.0",
7
7
  "effect": "^4.0.0",
8
8
  "react": "^18.0.0 || ^19.0.0"
@@ -13,7 +13,9 @@
13
13
  * Pinning a freshly loaded page, loading more, and splitting an overgrown page
14
14
  * are the same transition shape: the affected page keeps rendering while its
15
15
  * replacement subscriptions ("ongoing split") load, and is swapped out once all
16
- * of them have results.
16
+ * of them have results. A failed replacement fails the render at once, still
17
+ * showing the affected page and the pages after it, and the swap waits for a
18
+ * retry.
17
19
  *
18
20
  * This module is framework-free (it imports only `effect/*` and a type from
19
21
  * `convex/server`); it lives here for now because the React hook is its only
@@ -282,9 +284,14 @@ export type PageResult = PaginationResult<unknown>;
282
284
 
283
285
  /**
284
286
  * What one render pass derives from the subscribed pages' results: the items to
285
- * show, the trailing complete result (absent while the tail is loading or
286
- * force-splitting), state transitions to apply, and whether a failure or an
287
- * invalid cursor was hit.
287
+ * show, whether the first page has loaded, the trailing complete result (absent
288
+ * while the tail is loading or force-splitting), state transitions to apply
289
+ * (also while a failure shows, so the other pages keep splitting and swapping),
290
+ * and whether a failure or an invalid cursor was hit.
291
+ *
292
+ * `firstPageLoaded` holds once the head page has its full result. Splits can
293
+ * leave several pages ahead of the first loaded item, so neither the item count
294
+ * nor the page count tells a loading first page apart from a loaded empty one.
288
295
  */
289
296
  export type Interpretation =
290
297
  | { readonly _tag: "ResetRequired" }
@@ -292,9 +299,14 @@ export type Interpretation =
292
299
  readonly _tag: "Failed";
293
300
  readonly error: unknown;
294
301
  /**
295
- * Encoded items loaded before the failing page.
302
+ * Encoded items across the loaded pages, in order. A page whose
303
+ * replacement failed is still shown, and stays subscribed with the
304
+ * replacement so that a retry re-requests only the replacement. A page
305
+ * whose own result failed ends the items, since its range is unknown.
296
306
  */
297
307
  readonly items: ReadonlyArray<unknown>;
308
+ readonly firstPageLoaded: boolean;
309
+ readonly transitions: ReadonlyArray<(state: State) => State>;
298
310
  }
299
311
  | {
300
312
  readonly _tag: "Interpreted";
@@ -302,6 +314,7 @@ export type Interpretation =
302
314
  * Encoded items across the loaded pages, in order.
303
315
  */
304
316
  readonly items: ReadonlyArray<unknown>;
317
+ readonly firstPageLoaded: boolean;
305
318
  /**
306
319
  * The last page's result, when every page has loaded completely.
307
320
  */
@@ -312,12 +325,6 @@ export type Interpretation =
312
325
  readonly transitions: ReadonlyArray<(state: State) => State>;
313
326
  };
314
327
 
315
- const interpreted = (
316
- items: ReadonlyArray<unknown>,
317
- lastResult: Option.Option<PageResult>,
318
- transitions: ReadonlyArray<(state: State) => State>,
319
- ): Interpretation => ({ _tag: "Interpreted", items, lastResult, transitions });
320
-
321
328
  /**
322
329
  * Walk the pages in display order, concatenating their items and collecting the
323
330
  * transitions to apply (completed split swaps, recommended splits, eager splits
@@ -334,39 +341,97 @@ export const interpret = (
334
341
  readonly isInvalidCursorError: (error: Error) => boolean;
335
342
  },
336
343
  ): Interpretation => {
344
+ const resultOf = (key: string) =>
345
+ Record.get(results, key).pipe(
346
+ Option.flatMap(Option.fromNullishOr),
347
+ Option.getOrUndefined,
348
+ );
349
+ const firstPageLoaded = Option.exists(Array.head(state.pageKeys), (key) => {
350
+ const result = resultOf(key);
351
+ return (
352
+ result !== undefined &&
353
+ !(result instanceof Error) &&
354
+ result.pageStatus !== "SplitRequired"
355
+ );
356
+ });
357
+ // The first error in display order is the one reported.
358
+ const finish = (
359
+ items: ReadonlyArray<unknown>,
360
+ lastResult: Option.Option<PageResult>,
361
+ transitions: ReadonlyArray<(state: State) => State>,
362
+ errors: ReadonlyArray<Error>,
363
+ ): Interpretation =>
364
+ Array.some(errors, options.isInvalidCursorError)
365
+ ? { _tag: "ResetRequired" }
366
+ : Option.match(Array.head(errors), {
367
+ onNone: () => ({
368
+ _tag: "Interpreted",
369
+ items,
370
+ firstPageLoaded,
371
+ lastResult,
372
+ transitions,
373
+ }),
374
+ onSome: (error) => ({
375
+ _tag: "Failed",
376
+ error,
377
+ items,
378
+ firstPageLoaded,
379
+ transitions,
380
+ }),
381
+ });
382
+
337
383
  const go = (
338
384
  remaining: ReadonlyArray<string>,
339
385
  items: ReadonlyArray<unknown>,
340
386
  transitions: ReadonlyArray<(s: State) => State>,
341
387
  lastResult: Option.Option<PageResult>,
388
+ errors: ReadonlyArray<Error>,
342
389
  ): Interpretation =>
343
390
  Option.match(Array.head(remaining), {
344
- onNone: () => interpreted(items, lastResult, transitions),
391
+ onNone: () => finish(items, lastResult, transitions, errors),
345
392
  onSome: (pageKey) => {
346
- const result = Record.get(results, pageKey).pipe(
347
- Option.flatMap(Option.fromNullishOr),
348
- Option.getOrUndefined,
349
- );
393
+ const result = resultOf(pageKey);
350
394
  if (result === undefined) {
351
395
  // The trailing pages are still loading.
352
- return interpreted(items, Option.none(), transitions);
396
+ return finish(items, Option.none(), transitions, errors);
353
397
  }
354
398
  if (result instanceof Error) {
355
- return options.isInvalidCursorError(result)
356
- ? { _tag: "ResetRequired" }
357
- : { _tag: "Failed", error: result, items };
399
+ return finish(
400
+ items,
401
+ Option.none(),
402
+ transitions,
403
+ Array.append(errors, result),
404
+ );
358
405
  }
406
+ // A force-split page couldn't be fetched in full: show the items
407
+ // before it and report the tail as still loading.
408
+ const shown =
409
+ result.pageStatus === "SplitRequired"
410
+ ? items
411
+ : Array.appendAll(items, result.page);
359
412
 
360
- const hasResult = (key: string) =>
361
- Record.get(results, key).pipe(
362
- Option.flatMap(Option.fromNullishOr),
363
- Option.isSome,
364
- );
365
413
  const ongoingSplit = Record.get(state.ongoingSplits, pageKey);
414
+ // A failed replacement leaves this page shown and its range pinned, so
415
+ // the pages after it stay contiguous and the walk goes on.
416
+ const replacementResults = Option.match(ongoingSplit, {
417
+ onNone: () => [],
418
+ onSome: Array.map(resultOf),
419
+ });
420
+ const nextErrors = Array.appendAll(
421
+ errors,
422
+ Array.filter(
423
+ replacementResults,
424
+ (replacement) => replacement instanceof Error,
425
+ ),
426
+ );
366
427
  const nextTransitions = Option.match(ongoingSplit, {
367
- onSome: (replacements) =>
368
- // Swap the replacements in once all of them have results.
369
- Array.every(replacements, hasResult)
428
+ onSome: () =>
429
+ // Swap the replacements in once all of them have loaded.
430
+ Array.every(
431
+ replacementResults,
432
+ (replacement) =>
433
+ replacement !== undefined && !(replacement instanceof Error),
434
+ )
370
435
  ? Array.append(transitions, completeSplit(pageKey))
371
436
  : transitions,
372
437
  onNone: () =>
@@ -386,18 +451,17 @@ export const interpret = (
386
451
  : transitions,
387
452
  });
388
453
 
389
- // A force-split page couldn't be fetched in full: show the items
390
- // before it and report the tail as still loading.
391
454
  return result.pageStatus === "SplitRequired"
392
- ? interpreted(items, Option.none(), nextTransitions)
455
+ ? finish(shown, Option.none(), nextTransitions, nextErrors)
393
456
  : go(
394
457
  Array.drop(remaining, 1),
395
- Array.appendAll(items, result.page),
458
+ shown,
396
459
  nextTransitions,
397
460
  Option.some(result),
461
+ nextErrors,
398
462
  );
399
463
  },
400
464
  });
401
465
 
402
- return go(state.pageKeys, [], [], Option.none());
466
+ return go(state.pageKeys, [], [], Option.none(), []);
403
467
  };
package/src/index.ts CHANGED
@@ -489,10 +489,7 @@ export const useStreamPaginatedQuery = <
489
489
  resetKey: previous.resetKey,
490
490
  state: freshState(),
491
491
  }));
492
- } else if (
493
- interpretation._tag === "Interpreted" &&
494
- interpretation.transitions.length > 0
495
- ) {
492
+ } else if (interpretation.transitions.length > 0) {
496
493
  applyTransitions(interpretation.transitions);
497
494
  }
498
495
 
@@ -549,10 +546,10 @@ export const useStreamPaginatedQuery = <
549
546
  throw error;
550
547
  }
551
548
 
552
- const { lastResult } = interpretation;
553
- if (Option.isNone(lastResult) && current.state.pageKeys.length <= 1) {
549
+ if (!interpretation.firstPageLoaded) {
554
550
  return PaginatedQueryResult.loadingFirstPage({ skipped });
555
551
  }
552
+ const { lastResult } = interpretation;
556
553
  if (
557
554
  Option.isNone(lastResult) ||
558
555
  StreamPagination.isLastPageSplitting(current.state)