@confect/react 10.0.0-next.22 → 10.0.0-next.23

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