@gaganref/confect-react 0.0.0-stage → 10.3.1-fork.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,321 @@
1
+ import * as Equal from "effect/Equal";
2
+ import * as Function from "effect/Function";
3
+ import * as Hash from "effect/Hash";
4
+ import * as Match from "effect/Match";
5
+ import * as Pipeable from "effect/Pipeable";
6
+ import * as Predicate from "effect/Predicate";
7
+
8
+ const TypeId = "~@confect/react/PaginatedQueryResult";
9
+ type TypeId = typeof TypeId;
10
+
11
+ /**
12
+ * A `PaginatedQueryResult` represents the result of a Confect paginated query.
13
+ * The non-`Failure` variants mirror the statuses of `usePaginatedQuery` from
14
+ * `convex/react` and all carry `results` and `isLoading`, so the common UI
15
+ * shape—render the list, show a spinner while loading—needs only an
16
+ * {@link isFailure} early-out and plain field access.
17
+ *
18
+ * Every variant carries `results`, including `Failure`: when a later page
19
+ * fails, the pages already loaded are still worth rendering alongside the
20
+ * error, so they are not discarded. `Failure` additionally carries the query's
21
+ * decoded typed error.
22
+ *
23
+ * `loadMore` appears only on `CanLoadMore`, the one state it can make progress
24
+ * from. The underlying Convex hook exposes it on every status, but calling it
25
+ * while a page is in flight, once the list is exhausted, or after a failure is
26
+ * an intentional no-op there; narrowing to `CanLoadMore`—via
27
+ * {@link isCanLoadMore} or {@link match}—makes that statically apparent instead
28
+ * of silently dropping the call.
29
+ *
30
+ * When the query declares no `error` schema (`E` is `never`), the `Failure`
31
+ * variant is excluded from the type entirely, so `isLoading` is accessible
32
+ * without any narrowing.
33
+ *
34
+ * @typeParam Item - The type of a decoded page item.
35
+ * @typeParam E - The type of the decoded typed error in the `Failure` variant.
36
+ */
37
+ export type PaginatedQueryResult<Item, E = never> = [E] extends [never]
38
+ ?
39
+ | LoadingFirstPage<Item, E>
40
+ | LoadingMore<Item, E>
41
+ | CanLoadMore<Item, E>
42
+ | Exhausted<Item, E>
43
+ :
44
+ | LoadingFirstPage<Item, E>
45
+ | LoadingMore<Item, E>
46
+ | CanLoadMore<Item, E>
47
+ | Exhausted<Item, E>
48
+ | Failure<Item, E>;
49
+
50
+ /**
51
+ * Every variant regardless of `E`—the parameter type for guards and
52
+ * {@link match}, since `PaginatedQueryResult` itself excludes `Failure` when `E`
53
+ * is `never`.
54
+ */
55
+ export type Variants<Item, E = never> =
56
+ | LoadingFirstPage<Item, E>
57
+ | LoadingMore<Item, E>
58
+ | CanLoadMore<Item, E>
59
+ | Exhausted<Item, E>
60
+ | Failure<Item, E>;
61
+
62
+ export declare namespace PaginatedQueryResult {
63
+ // eslint-disable-next-line @typescript-eslint/no-shadow
64
+ export interface Proto<out Item, out E> extends Pipeable.Pipeable {
65
+ readonly [TypeId]: TypeId;
66
+ readonly "~Item": Item;
67
+ readonly "~E": E;
68
+ }
69
+
70
+ export type Item<R> = R extends { readonly "~Item": infer Item_ }
71
+ ? Item_
72
+ : never;
73
+
74
+ // eslint-disable-next-line @typescript-eslint/no-shadow
75
+ export type Failure<R> = R extends { readonly "~E": infer E } ? E : never;
76
+ }
77
+
78
+ export interface LoadingFirstPage<
79
+ Item,
80
+ E = never,
81
+ > extends PaginatedQueryResult.Proto<Item, E> {
82
+ readonly _tag: "LoadingFirstPage";
83
+ readonly skipped: boolean;
84
+ readonly results: ReadonlyArray<Item>;
85
+ readonly isLoading: true;
86
+ }
87
+
88
+ export interface LoadingMore<
89
+ Item,
90
+ E = never,
91
+ > extends PaginatedQueryResult.Proto<Item, E> {
92
+ readonly _tag: "LoadingMore";
93
+ readonly results: ReadonlyArray<Item>;
94
+ readonly isLoading: true;
95
+ }
96
+
97
+ export interface CanLoadMore<
98
+ Item,
99
+ E = never,
100
+ > extends PaginatedQueryResult.Proto<Item, E> {
101
+ readonly _tag: "CanLoadMore";
102
+ readonly results: ReadonlyArray<Item>;
103
+ readonly isLoading: false;
104
+ readonly loadMore: (numItems: number) => void;
105
+ }
106
+
107
+ export interface Exhausted<Item, E = never> extends PaginatedQueryResult.Proto<
108
+ Item,
109
+ E
110
+ > {
111
+ readonly _tag: "Exhausted";
112
+ readonly results: ReadonlyArray<Item>;
113
+ readonly isLoading: false;
114
+ }
115
+
116
+ export interface Failure<Item, E = never> extends PaginatedQueryResult.Proto<
117
+ Item,
118
+ E
119
+ > {
120
+ readonly _tag: "Failure";
121
+ readonly error: E;
122
+ /**
123
+ * The pages loaded before the failure.
124
+ */
125
+ readonly results: ReadonlyArray<Item>;
126
+ }
127
+
128
+ export const isPaginatedQueryResult = (
129
+ u: unknown,
130
+ ): u is Variants<unknown, unknown> => Predicate.hasProperty(u, TypeId);
131
+
132
+ const PaginatedQueryResultProto = {
133
+ [TypeId]: TypeId,
134
+ pipe(this: Variants<any, any>, ...args: ReadonlyArray<unknown>) {
135
+ return Pipeable.pipeArguments(
136
+ this,
137
+ args as unknown as Parameters<typeof Pipeable.pipeArguments>[1],
138
+ );
139
+ },
140
+ /**
141
+ * `CanLoadMore`'s `loadMore` is excluded from equality and hashing, like
142
+ * function fields generally: two results that render identically are equal
143
+ * even though their callbacks differ by identity.
144
+ */
145
+ [Equal.symbol](this: Variants<any, any>, that: Variants<any, any>): boolean {
146
+ if (this._tag !== that._tag) {
147
+ return false;
148
+ }
149
+ return Match.value(this).pipe(
150
+ Match.tag(
151
+ "LoadingFirstPage",
152
+ (self) => self.skipped === (that as LoadingFirstPage<any, any>).skipped,
153
+ ),
154
+ Match.tag("LoadingMore", "CanLoadMore", "Exhausted", (self) =>
155
+ Equal.equals(self.results, that.results),
156
+ ),
157
+ Match.tag(
158
+ "Failure",
159
+ (self) =>
160
+ Equal.equals(self.error, (that as Failure<any, any>).error) &&
161
+ Equal.equals(self.results, (that as Failure<any, any>).results),
162
+ ),
163
+ Match.exhaustive,
164
+ );
165
+ },
166
+ [Hash.symbol](this: Variants<any, any>): number {
167
+ const tagHash = Hash.string(this._tag);
168
+ return Match.value(this).pipe(
169
+ Match.tag("LoadingFirstPage", (self) =>
170
+ Hash.combine(tagHash)(Hash.hash(self.skipped)),
171
+ ),
172
+ Match.tag("LoadingMore", "CanLoadMore", "Exhausted", (self) =>
173
+ Hash.combine(tagHash)(Hash.hash(self.results)),
174
+ ),
175
+ Match.tag("Failure", (self) =>
176
+ Hash.combine(tagHash)(
177
+ Hash.combine(Hash.hash(self.error))(Hash.hash(self.results)),
178
+ ),
179
+ ),
180
+ Match.exhaustive,
181
+ );
182
+ },
183
+ };
184
+
185
+ const noResults: ReadonlyArray<never> = [];
186
+
187
+ export const loadingFirstPage = <Item = never, E = never>(options: {
188
+ skipped: boolean;
189
+ }): LoadingFirstPage<Item, E> =>
190
+ Object.assign(Object.create(PaginatedQueryResultProto), {
191
+ _tag: "LoadingFirstPage" as const,
192
+ skipped: options.skipped,
193
+ results: noResults,
194
+ isLoading: true as const,
195
+ });
196
+
197
+ export const loadingMore = <Item, E = never>(options: {
198
+ results: ReadonlyArray<Item>;
199
+ }): LoadingMore<Item, E> =>
200
+ Object.assign(Object.create(PaginatedQueryResultProto), {
201
+ _tag: "LoadingMore" as const,
202
+ results: options.results,
203
+ isLoading: true as const,
204
+ });
205
+
206
+ export const canLoadMore = <Item, E = never>(options: {
207
+ results: ReadonlyArray<Item>;
208
+ loadMore: (numItems: number) => void;
209
+ }): CanLoadMore<Item, E> =>
210
+ Object.assign(Object.create(PaginatedQueryResultProto), {
211
+ _tag: "CanLoadMore" as const,
212
+ results: options.results,
213
+ isLoading: false as const,
214
+ loadMore: options.loadMore,
215
+ });
216
+
217
+ export const exhausted = <Item, E = never>(options: {
218
+ results: ReadonlyArray<Item>;
219
+ }): Exhausted<Item, E> =>
220
+ Object.assign(Object.create(PaginatedQueryResultProto), {
221
+ _tag: "Exhausted" as const,
222
+ results: options.results,
223
+ isLoading: false as const,
224
+ });
225
+
226
+ export const failure = <E, Item = never>(options: {
227
+ error: E;
228
+ results: ReadonlyArray<Item>;
229
+ }): Failure<Item, E> =>
230
+ Object.assign(Object.create(PaginatedQueryResultProto), {
231
+ _tag: "Failure" as const,
232
+ error: options.error,
233
+ results: options.results,
234
+ });
235
+
236
+ export const isLoadingFirstPage = <Item, E>(
237
+ result: Variants<Item, E>,
238
+ ): result is LoadingFirstPage<Item, E> => result._tag === "LoadingFirstPage";
239
+
240
+ export const isLoadingMore = <Item, E>(
241
+ result: Variants<Item, E>,
242
+ ): result is LoadingMore<Item, E> => result._tag === "LoadingMore";
243
+
244
+ export const isCanLoadMore = <Item, E>(
245
+ result: Variants<Item, E>,
246
+ ): result is CanLoadMore<Item, E> => result._tag === "CanLoadMore";
247
+
248
+ export const isExhausted = <Item, E>(
249
+ result: Variants<Item, E>,
250
+ ): result is Exhausted<Item, E> => result._tag === "Exhausted";
251
+
252
+ export const isFailure = <Item, E>(
253
+ result: Variants<Item, E>,
254
+ ): result is Failure<Item, E> => result._tag === "Failure";
255
+
256
+ export const isLoading = <Item, E>(
257
+ result: Variants<Item, E>,
258
+ ): result is LoadingFirstPage<Item, E> | LoadingMore<Item, E> =>
259
+ result._tag === "LoadingFirstPage" || result._tag === "LoadingMore";
260
+
261
+ type MatchOptions<Item, E, V, W, X, Y, Z> = {
262
+ readonly onLoadingFirstPage: (skipped: boolean) => V;
263
+ readonly onLoadingMore: (results: ReadonlyArray<Item>) => W;
264
+ readonly onCanLoadMore: (
265
+ results: ReadonlyArray<Item>,
266
+ loadMore: (numItems: number) => void,
267
+ ) => X;
268
+ readonly onExhausted: (results: ReadonlyArray<Item>) => Y;
269
+ } & ([E] extends [never]
270
+ ? {}
271
+ : {
272
+ readonly onFailure: (error: E, results: ReadonlyArray<Item>) => Z;
273
+ });
274
+
275
+ type MatchReturns<E, V, W, X, Y, Z> = [E] extends [never]
276
+ ? V | W | X | Y
277
+ : V | W | X | Y | Z;
278
+
279
+ /**
280
+ * Matches a {@link PaginatedQueryResult} to the appropriate handler based on its
281
+ * tag. If the provided result cannot fail (i.e. `E` is `never`), `onFailure` is
282
+ * not required.
283
+ */
284
+ export const match: {
285
+ <Item, E, V, W, X, Y, Z = never>(
286
+ options: MatchOptions<Item, E, V, W, X, Y, Z>,
287
+ ): (self: Variants<Item, E>) => MatchReturns<E, V, W, X, Y, Z>;
288
+ <Item, E, V, W, X, Y, Z = never>(
289
+ self: Variants<Item, E>,
290
+ options: MatchOptions<Item, E, V, W, X, Y, Z>,
291
+ ): MatchReturns<E, V, W, X, Y, Z>;
292
+ } = Function.dual(
293
+ 2,
294
+ <Item, E, V, W, X, Y, Z = never>(
295
+ self: Variants<Item, E>,
296
+ options: MatchOptions<Item, E, V, W, X, Y, Z>,
297
+ ): MatchReturns<E, V, W, X, Y, Z> =>
298
+ Match.value(self).pipe(
299
+ Match.tag("LoadingFirstPage", (loadingFirstPage_) =>
300
+ options.onLoadingFirstPage(loadingFirstPage_.skipped),
301
+ ),
302
+ Match.tag("LoadingMore", (loadingMore_) =>
303
+ options.onLoadingMore(loadingMore_.results),
304
+ ),
305
+ Match.tag("CanLoadMore", (canLoadMore_) =>
306
+ options.onCanLoadMore(canLoadMore_.results, canLoadMore_.loadMore),
307
+ ),
308
+ Match.tag("Exhausted", (exhausted_) =>
309
+ options.onExhausted(exhausted_.results),
310
+ ),
311
+ Match.tag("Failure", (failure_) => {
312
+ if (Predicate.hasProperty(options, "onFailure")) {
313
+ return options.onFailure(failure_.error, failure_.results);
314
+ }
315
+ throw new Error(
316
+ "`onFailure` is required when error schema is provided",
317
+ );
318
+ }),
319
+ Match.exhaustive,
320
+ ) as MatchReturns<E, V, W, X, Y, Z>,
321
+ );
@@ -0,0 +1,177 @@
1
+ import * as Equal from "effect/Equal";
2
+ import * as Function from "effect/Function";
3
+ import * as Hash from "effect/Hash";
4
+ import * as Match from "effect/Match";
5
+ import * as Pipeable from "effect/Pipeable";
6
+ import * as Predicate from "effect/Predicate";
7
+
8
+ const TypeId = "~@confect/react/QueryResult";
9
+ type TypeId = typeof TypeId;
10
+
11
+ /**
12
+ * A `QueryResult` represents the result of a Confect query.
13
+ *
14
+ * @typeParam A - The type of the decoded `returns` value in the `Success`
15
+ * variant.
16
+ * @typeParam E - The type of the decoded typed error in the `Failure` variant.
17
+ */
18
+ export type QueryResult<A, E = never> =
19
+ | Loading<A, E>
20
+ | Success<A, E>
21
+ | Failure<A, E>;
22
+
23
+ export declare namespace QueryResult {
24
+ export interface Proto<out A, out E> extends Pipeable.Pipeable {
25
+ readonly [TypeId]: TypeId;
26
+ readonly "~A": A;
27
+ readonly "~E": E;
28
+ }
29
+
30
+ // eslint-disable-next-line @typescript-eslint/no-shadow
31
+ export type Success<R> = R extends { readonly "~A": infer A } ? A : never;
32
+
33
+ // eslint-disable-next-line @typescript-eslint/no-shadow
34
+ export type Failure<R> = R extends { readonly "~E": infer E } ? E : never;
35
+ }
36
+
37
+ export interface Loading<A, E = never> extends QueryResult.Proto<A, E> {
38
+ readonly _tag: "Loading";
39
+ readonly skipped: boolean;
40
+ }
41
+
42
+ export interface Success<A, E = never> extends QueryResult.Proto<A, E> {
43
+ readonly _tag: "Success";
44
+ readonly value: A;
45
+ }
46
+
47
+ export interface Failure<A, E = never> extends QueryResult.Proto<A, E> {
48
+ readonly _tag: "Failure";
49
+ readonly error: E;
50
+ }
51
+
52
+ export const isQueryResult = (u: unknown): u is QueryResult<unknown, unknown> =>
53
+ Predicate.hasProperty(u, TypeId);
54
+
55
+ const QueryResultProto = {
56
+ [TypeId]: TypeId,
57
+ pipe(this: QueryResult<any, any>, ...args: ReadonlyArray<unknown>) {
58
+ return Pipeable.pipeArguments(
59
+ this,
60
+ args as unknown as Parameters<typeof Pipeable.pipeArguments>[1],
61
+ );
62
+ },
63
+ [Equal.symbol](
64
+ this: QueryResult<any, any>,
65
+ that: QueryResult<any, any>,
66
+ ): boolean {
67
+ if (this._tag !== that._tag) {
68
+ return false;
69
+ }
70
+ return Match.value(this).pipe(
71
+ Match.tag(
72
+ "Loading",
73
+ (self) => self.skipped === (that as Loading<any, any>).skipped,
74
+ ),
75
+ Match.tag("Success", (self) =>
76
+ Equal.equals(self.value, (that as Success<any, any>).value),
77
+ ),
78
+ Match.tag("Failure", (self) =>
79
+ Equal.equals(self.error, (that as Failure<any, any>).error),
80
+ ),
81
+ Match.exhaustive,
82
+ );
83
+ },
84
+ [Hash.symbol](this: QueryResult<any, any>): number {
85
+ const tagHash = Hash.string(this._tag);
86
+ return Match.value(this).pipe(
87
+ Match.tag("Loading", (self) =>
88
+ Hash.combine(tagHash)(Hash.hash(self.skipped)),
89
+ ),
90
+ Match.tag("Success", (self) =>
91
+ Hash.combine(tagHash)(Hash.hash(self.value)),
92
+ ),
93
+ Match.tag("Failure", (self) =>
94
+ Hash.combine(tagHash)(Hash.hash(self.error)),
95
+ ),
96
+ Match.exhaustive,
97
+ );
98
+ },
99
+ };
100
+
101
+ export const load = <A = never, E = never>(skipped: boolean): Loading<A, E> =>
102
+ Object.assign(Object.create(QueryResultProto), {
103
+ _tag: "Loading" as const,
104
+ skipped,
105
+ });
106
+
107
+ export const succeed = <A, E = never>(value: A): Success<A, E> =>
108
+ Object.assign(Object.create(QueryResultProto), {
109
+ _tag: "Success" as const,
110
+ value,
111
+ });
112
+
113
+ export const fail = <E, A = never>(error: E): Failure<A, E> =>
114
+ Object.assign(Object.create(QueryResultProto), {
115
+ _tag: "Failure" as const,
116
+ error,
117
+ });
118
+
119
+ export const isLoading = <A, E>(
120
+ queryResult: QueryResult<A, E>,
121
+ ): queryResult is Loading<A, E> => queryResult._tag === "Loading";
122
+
123
+ export const isSuccess = <A, E>(
124
+ queryResult: QueryResult<A, E>,
125
+ ): queryResult is Success<A, E> => queryResult._tag === "Success";
126
+
127
+ export const isFailure = <A, E>(
128
+ queryResult: QueryResult<A, E>,
129
+ ): queryResult is Failure<A, E> => queryResult._tag === "Failure";
130
+
131
+ type MatchOptions<A, E, X, Y, Z> = {
132
+ readonly onLoading: (skipped: boolean) => X;
133
+ readonly onSuccess: (value: A) => Y;
134
+ } & ([E] extends [never] ? {} : { readonly onFailure: (error: E) => Z });
135
+
136
+ type MatchReturns<E, X, Y, Z> = [E] extends [never] ? X | Y : X | Y | Z;
137
+
138
+ /**
139
+ * Matches a {@link QueryResult} to the appropriate handler based on its tag. If
140
+ * the provided `QueryResult` cannot fail (i.e. `E` is `never`), `onFailure` is
141
+ * not required.
142
+ *
143
+ * @example
144
+ * const result = QueryResult.match(queryResult, {
145
+ * onLoading: (skipped) => (skipped ? null : <p>Loading…</p>),
146
+ * onSuccess: (value) => <p>{value.text}</p>,
147
+ * onFailure: (error) => <p>Error: {error.message}</p>,
148
+ * });
149
+ */
150
+ export const match: {
151
+ <A, E, X, Y, Z = never>(
152
+ options: MatchOptions<A, E, X, Y, Z>,
153
+ ): (self: QueryResult<A, E>) => MatchReturns<E, X, Y, Z>;
154
+ <A, E, X, Y, Z = never>(
155
+ self: QueryResult<A, E>,
156
+ options: MatchOptions<A, E, X, Y, Z>,
157
+ ): MatchReturns<E, X, Y, Z>;
158
+ } = Function.dual(
159
+ 2,
160
+ <A, E, X, Y, Z = never>(
161
+ self: QueryResult<A, E>,
162
+ options: MatchOptions<A, E, X, Y, Z>,
163
+ ): MatchReturns<E, X, Y, Z> =>
164
+ Match.value(self).pipe(
165
+ Match.tag("Loading", (loading) => options.onLoading(loading.skipped)),
166
+ Match.tag("Success", (success) => options.onSuccess(success.value)),
167
+ Match.tag("Failure", (failure) => {
168
+ if (Predicate.hasProperty(options, "onFailure")) {
169
+ return options.onFailure(failure.error);
170
+ }
171
+ throw new Error(
172
+ "`onFailure` is required when error schema is provided",
173
+ );
174
+ }),
175
+ Match.exhaustive,
176
+ ) as MatchReturns<E, X, Y, Z>,
177
+ );