@confect/react 10.0.0-next.0 → 10.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -0
- package/dist/OptimisticLocalStore.js +1 -2
- package/dist/OptimisticLocalStore.js.map +1 -1
- package/dist/PaginatedQueryResult.d.ts +118 -0
- package/dist/PaginatedQueryResult.d.ts.map +1 -0
- package/dist/PaginatedQueryResult.js +95 -0
- package/dist/PaginatedQueryResult.js.map +1 -0
- package/dist/QueryResult.d.ts.map +1 -1
- package/dist/QueryResult.js +9 -22
- package/dist/QueryResult.js.map +1 -1
- package/dist/_virtual/_rolldown/runtime.js +6 -11
- package/dist/index.d.ts +46 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -5
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.src.tsbuildinfo +1 -1
- package/package.json +17 -16
- package/src/PaginatedQueryResult.ts +322 -0
- package/src/QueryResult.ts +38 -29
- package/src/index.ts +238 -2
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Ref } from "@confect/core";
|
|
2
|
+
import { version as convexVersion } from "convex";
|
|
2
3
|
import type { OptimisticUpdate as ConvexOptimisticUpdate } from "convex/browser";
|
|
4
|
+
import * as ConvexReact from "convex/react";
|
|
5
|
+
import type { usePaginatedQuery as useConvexPaginatedQuery } from "convex/react";
|
|
3
6
|
import {
|
|
4
7
|
useAction as useConvexAction,
|
|
5
8
|
useMutation as useConvexMutation,
|
|
@@ -12,13 +15,15 @@ import * as Cause from "effect/Cause";
|
|
|
12
15
|
import * as Effect from "effect/Effect";
|
|
13
16
|
import * as Result from "effect/Result";
|
|
14
17
|
import * as Exit from "effect/Exit";
|
|
18
|
+
import * as Match from "effect/Match";
|
|
15
19
|
import * as Option from "effect/Option";
|
|
16
20
|
import { useCallback, useMemo } from "react";
|
|
17
21
|
|
|
18
22
|
import * as OptimisticLocalStore from "./OptimisticLocalStore";
|
|
23
|
+
import * as PaginatedQueryResult from "./PaginatedQueryResult";
|
|
19
24
|
import * as QueryResult from "./QueryResult";
|
|
20
25
|
|
|
21
|
-
export { OptimisticLocalStore, QueryResult };
|
|
26
|
+
export { OptimisticLocalStore, PaginatedQueryResult, QueryResult };
|
|
22
27
|
|
|
23
28
|
export type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [
|
|
24
29
|
never,
|
|
@@ -64,7 +69,7 @@ export const useQuery = <Query extends Ref.AnyPublicQuery>(
|
|
|
64
69
|
),
|
|
65
70
|
onFailure: (error) => {
|
|
66
71
|
if (Ref.isConvexError(error)) {
|
|
67
|
-
const decoded = Ref.
|
|
72
|
+
const decoded = Ref.decodeErrorOption(ref, error.data);
|
|
68
73
|
if (Option.isSome(decoded)) {
|
|
69
74
|
return QueryResult.fail(decoded.value);
|
|
70
75
|
}
|
|
@@ -80,6 +85,237 @@ export const useQuery = <Query extends Ref.AnyPublicQuery>(
|
|
|
80
85
|
);
|
|
81
86
|
};
|
|
82
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Like `Omit`, but implemented with key remapping so that optional modifiers
|
|
90
|
+
* are preserved and index signatures don't reintroduce the omitted key's
|
|
91
|
+
* literal type. Mirrors `BetterOmit` from `convex/server`.
|
|
92
|
+
*/
|
|
93
|
+
type OmitKey<T, K extends PropertyKey> = {
|
|
94
|
+
[P in keyof T as P extends K ? never : P]: T[P];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The args accepted by {@link usePaginatedQuery}: the ref's args without
|
|
99
|
+
* `paginationOpts`, which the underlying Convex hook manages itself. When no
|
|
100
|
+
* args remain, resolves to `Record<string, never>` (only the empty object is
|
|
101
|
+
* accepted) rather than `{}`, which would accept any non-nullish value.
|
|
102
|
+
*/
|
|
103
|
+
export type PaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =
|
|
104
|
+
OmitKey<Ref.Args<Query>, "paginationOpts"> extends infer Args_
|
|
105
|
+
? keyof Args_ extends never
|
|
106
|
+
? Record<string, never>
|
|
107
|
+
: Args_
|
|
108
|
+
: never;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The item type for a paginated query: the element type of the `page` field
|
|
112
|
+
* of the ref's returns.
|
|
113
|
+
*/
|
|
114
|
+
export type PaginatedQueryItem<Query extends Ref.AnyPublicPaginatedQuery> =
|
|
115
|
+
Ref.Returns<Query>["page"][number];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The args parameter of {@link usePaginatedQuery}: the ref's args without
|
|
119
|
+
* `paginationOpts`, or `"skip"` to skip the query.
|
|
120
|
+
*/
|
|
121
|
+
export type UsePaginatedQueryArgs<Query extends Ref.AnyPublicPaginatedQuery> =
|
|
122
|
+
| PaginatedQueryArgs<Query>
|
|
123
|
+
| "skip";
|
|
124
|
+
|
|
125
|
+
export type PaginatedQueryOptions = Parameters<
|
|
126
|
+
typeof useConvexPaginatedQuery
|
|
127
|
+
>[2];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The `user`-facing result of `usePaginatedQueryInternal` from `convex/react`,
|
|
131
|
+
* including the `"Error"` status its non-throwing mode can return.
|
|
132
|
+
*/
|
|
133
|
+
interface ConvexPaginatedQueryInternalUser {
|
|
134
|
+
readonly results: ReadonlyArray<unknown>;
|
|
135
|
+
readonly status:
|
|
136
|
+
| "LoadingFirstPage"
|
|
137
|
+
| "LoadingMore"
|
|
138
|
+
| "CanLoadMore"
|
|
139
|
+
| "Exhausted"
|
|
140
|
+
| "Error";
|
|
141
|
+
readonly isLoading: boolean;
|
|
142
|
+
readonly loadMore: (numItems: number) => void;
|
|
143
|
+
readonly error?: unknown;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
type UseConvexPaginatedQueryInternal = (
|
|
147
|
+
query: FunctionReference<"query">,
|
|
148
|
+
args: Record<string, Value> | "skip",
|
|
149
|
+
options: PaginatedQueryOptions,
|
|
150
|
+
throwOnError: boolean,
|
|
151
|
+
) => { user: ConvexPaginatedQueryInternalUser };
|
|
152
|
+
|
|
153
|
+
// `usePaginatedQueryInternal` is runtime-exported by convex/react but absent
|
|
154
|
+
// from its public type declarations. It is the only way to observe query
|
|
155
|
+
// errors as values: the public positional `usePaginatedQuery` hard-codes
|
|
156
|
+
// throwOnError and throws mid-hook-sequence, which cannot be caught without
|
|
157
|
+
// breaking React's hook-order invariant.
|
|
158
|
+
const useConvexPaginatedQueryInternal = (
|
|
159
|
+
ConvexReact as unknown as Record<string, unknown>
|
|
160
|
+
).usePaginatedQueryInternal as UseConvexPaginatedQueryInternal | undefined;
|
|
161
|
+
|
|
162
|
+
const MINIMUM_CONVEX_VERSION = "1.36.0";
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The non-throwing mode of `usePaginatedQueryInternal` — its fourth,
|
|
166
|
+
* `throwOnError` parameter — arrived in convex 1.36.0. Convex 1.32 through
|
|
167
|
+
* 1.35 export the same symbol taking only three parameters, where a fourth
|
|
168
|
+
* argument is silently ignored and errors are always thrown.
|
|
169
|
+
*
|
|
170
|
+
* The function itself cannot be interrogated for this: `Function.length`
|
|
171
|
+
* counts parameters up to the first one with a default, and 1.36.0 declares
|
|
172
|
+
* `throwOnError = true`, so both shapes report an arity of 3. The package
|
|
173
|
+
* version is the only reliable signal.
|
|
174
|
+
*/
|
|
175
|
+
const supportsErrorsAsValues = ((): boolean => {
|
|
176
|
+
const [major, minor] = String(convexVersion).split(".").map(Number);
|
|
177
|
+
if (
|
|
178
|
+
major === undefined ||
|
|
179
|
+
minor === undefined ||
|
|
180
|
+
Number.isNaN(major) ||
|
|
181
|
+
Number.isNaN(minor)
|
|
182
|
+
) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const [minimumMajor, minimumMinor] = MINIMUM_CONVEX_VERSION.split(".").map(
|
|
186
|
+
Number,
|
|
187
|
+
) as [number, number];
|
|
188
|
+
return (
|
|
189
|
+
major > minimumMajor || (major === minimumMajor && minor >= minimumMinor)
|
|
190
|
+
);
|
|
191
|
+
})();
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load data reactively from a paginated query defined with
|
|
195
|
+
* `FunctionSpec.publicPaginatedQuery`, mirroring the ergonomics of
|
|
196
|
+
* `usePaginatedQuery` from `convex/react`.
|
|
197
|
+
*
|
|
198
|
+
* Args are encoded via the ref's user-args schema (`paginationOpts` is managed
|
|
199
|
+
* by the Convex hook, not the caller), and each loaded page is decoded via the
|
|
200
|
+
* ref's item schema.
|
|
201
|
+
*
|
|
202
|
+
* Returns a {@link PaginatedQueryResult.PaginatedQueryResult}: the loaded
|
|
203
|
+
* variants carry `results` and `isLoading`, and `CanLoadMore` additionally
|
|
204
|
+
* carries `loadMore`; if the `Ref` declares an `error` schema and the query
|
|
205
|
+
* fails with that typed error, the decoded error is returned as the `Failure`
|
|
206
|
+
* variant, which also carries the pages loaded before the failure. Unknown
|
|
207
|
+
* errors are thrown, to be caught by an error boundary.
|
|
208
|
+
*/
|
|
209
|
+
export const usePaginatedQuery = <Query extends Ref.AnyPublicPaginatedQuery>(
|
|
210
|
+
ref: Query,
|
|
211
|
+
args: UsePaginatedQueryArgs<Query>,
|
|
212
|
+
options: PaginatedQueryOptions,
|
|
213
|
+
): PaginatedQueryResult.PaginatedQueryResult<
|
|
214
|
+
PaginatedQueryItem<Query>,
|
|
215
|
+
Ref.Error<Query>
|
|
216
|
+
> => {
|
|
217
|
+
if (
|
|
218
|
+
useConvexPaginatedQueryInternal === undefined ||
|
|
219
|
+
!supportsErrorsAsValues
|
|
220
|
+
) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`usePaginatedQuery requires convex >= ${MINIMUM_CONVEX_VERSION}, but found ` +
|
|
223
|
+
`${convexVersion}. Earlier versions always throw paginated query errors ` +
|
|
224
|
+
"instead of returning them, so a declared `error` schema cannot be " +
|
|
225
|
+
"surfaced as a `Failure` — upgrade the `convex` package.",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const functionReference = Ref.getFunctionReference(ref);
|
|
230
|
+
const skipped = args === "skip";
|
|
231
|
+
|
|
232
|
+
// The encoded output is the wire form Convex's hook forwards verbatim, not
|
|
233
|
+
// the ref's decoded args type.
|
|
234
|
+
const encodedArgs = useMemo(
|
|
235
|
+
() =>
|
|
236
|
+
args === "skip"
|
|
237
|
+
? ("skip" as const)
|
|
238
|
+
: (Ref.encodePaginatedQueryArgsSync(
|
|
239
|
+
ref,
|
|
240
|
+
args as unknown as Omit<Ref.Args<Query>, "paginationOpts">,
|
|
241
|
+
) as Record<string, Value>),
|
|
242
|
+
[ref, args],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const { user: convexResult } = useConvexPaginatedQueryInternal(
|
|
246
|
+
functionReference,
|
|
247
|
+
encodedArgs,
|
|
248
|
+
options,
|
|
249
|
+
false,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// The Convex hook carries the pages loaded so far on every status,
|
|
253
|
+
// failures included, so results are decoded unconditionally.
|
|
254
|
+
//
|
|
255
|
+
// Decoding allocates fresh items, so key on the referentially stable
|
|
256
|
+
// `results` the Convex hook provides — the same identity-preservation
|
|
257
|
+
// rationale as in `useQuery` above.
|
|
258
|
+
const { results: encodedResults } = convexResult;
|
|
259
|
+
const decodedResults = useMemo(
|
|
260
|
+
() => Ref.decodePaginationPageSync(ref, encodedResults),
|
|
261
|
+
[ref, encodedResults],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const { status, loadMore } = convexResult;
|
|
265
|
+
const error = status === "Error" ? convexResult.error : undefined;
|
|
266
|
+
|
|
267
|
+
return useMemo(
|
|
268
|
+
(): PaginatedQueryResult.Variants<
|
|
269
|
+
PaginatedQueryItem<Query>,
|
|
270
|
+
Ref.Error<Query>
|
|
271
|
+
> =>
|
|
272
|
+
// `status` is a bare string union rather than a tagged one, so the arms
|
|
273
|
+
// match on literals.
|
|
274
|
+
Match.value(status).pipe(
|
|
275
|
+
Match.when("Error", () => {
|
|
276
|
+
if (Ref.isConvexError(error)) {
|
|
277
|
+
const decoded = Ref.decodeErrorOption(ref, error.data);
|
|
278
|
+
if (Option.isSome(decoded)) {
|
|
279
|
+
return PaginatedQueryResult.failure({
|
|
280
|
+
error: decoded.value,
|
|
281
|
+
results: decodedResults,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Unknown errors still throw. All hooks have run by this point, so
|
|
286
|
+
// an aborted render here is hook-order-safe.
|
|
287
|
+
throw error;
|
|
288
|
+
}),
|
|
289
|
+
Match.when("LoadingFirstPage", () =>
|
|
290
|
+
PaginatedQueryResult.loadingFirstPage({ skipped }),
|
|
291
|
+
),
|
|
292
|
+
Match.when("LoadingMore", () =>
|
|
293
|
+
PaginatedQueryResult.loadingMore({ results: decodedResults }),
|
|
294
|
+
),
|
|
295
|
+
// `CanLoadMore` is the only status whose `loadMore` does anything —
|
|
296
|
+
// the Convex hook returns an intentional no-op for all the others, so
|
|
297
|
+
// they carry no callback at all.
|
|
298
|
+
Match.when("CanLoadMore", () =>
|
|
299
|
+
PaginatedQueryResult.canLoadMore({
|
|
300
|
+
results: decodedResults,
|
|
301
|
+
loadMore,
|
|
302
|
+
}),
|
|
303
|
+
),
|
|
304
|
+
Match.when("Exhausted", () =>
|
|
305
|
+
PaginatedQueryResult.exhausted({ results: decodedResults }),
|
|
306
|
+
),
|
|
307
|
+
Match.exhaustive,
|
|
308
|
+
),
|
|
309
|
+
// The memo produces `Variants`, the conditional return type's superset —
|
|
310
|
+
// when `E` is `never` the `Failure` arm is unreachable at runtime, which
|
|
311
|
+
// is exactly what the conditional encodes.
|
|
312
|
+
[ref, skipped, decodedResults, status, loadMore, error],
|
|
313
|
+
) as PaginatedQueryResult.PaginatedQueryResult<
|
|
314
|
+
PaginatedQueryItem<Query>,
|
|
315
|
+
Ref.Error<Query>
|
|
316
|
+
>;
|
|
317
|
+
};
|
|
318
|
+
|
|
83
319
|
/**
|
|
84
320
|
* An optimistic update for a Confect mutation. Mirrors Convex's
|
|
85
321
|
* `OptimisticUpdate`, but receives a Confect {@link OptimisticLocalStore} and
|