@confect/react 6.0.0 → 7.0.0

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 CHANGED
@@ -1,5 +1,132 @@
1
1
  # @confect/react
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 90094d0: Add typed errors to Confect functions (queries, mutations, and actions). Declare an optional `error` schema in `FunctionSpec` and recover it as a typed value at every call site—`useQuery`, `useMutation`, `useAction`, `HttpClient`, `WebSocketClient`, and `TestConfect`—without paying for it on functions that don't fail.
8
+
9
+ Typed errors travel across the function boundary as Convex's native [`ConvexError`](https://docs.convex.dev/functions/error-handling/application-errors#throwing-application-errors): the encoded error sits in `ConvexError.data`, leaving the `returns` channel unsullied and preserving native Convex semantics for non-Confect callers of the same API.
10
+
11
+ ### Authoring a function with typed errors
12
+
13
+ `FunctionSpec` constructors now accept an optional `error` schema. To support multiple error shapes, combine them with `Schema.Union`.
14
+
15
+ ```ts
16
+ import { FunctionSpec, GenericId, GroupSpec } from "@confect/core";
17
+ import { Schema } from "effect";
18
+
19
+ export class NoteNotFound extends Schema.TaggedError<NoteNotFound>()(
20
+ "NoteNotFound",
21
+ { noteId: GenericId.GenericId("notes") },
22
+ ) {}
23
+
24
+ export const notes = GroupSpec.make("notes").addFunction(
25
+ FunctionSpec.publicQuery({
26
+ name: "getOrFail",
27
+ args: Schema.Struct({ noteId: GenericId.GenericId("notes") }),
28
+ returns: Notes.Doc,
29
+ error: NoteNotFound,
30
+ }),
31
+ );
32
+ ```
33
+
34
+ The `FunctionImpl` for that ref can now `Effect.fail` (or `mapError` to) any value matching the declared schema. Whichever invocation path the caller takes—`useQuery`/`useMutation`/`useAction`, `HttpClient`, `WebSocketClient`, or `TestConfect`—Confect encodes the failure, transports it via `ConvexError`, and surfaces the decoded value in the appropriate channel for that call site.
35
+
36
+ ```ts
37
+ import { FunctionImpl } from "@confect/server";
38
+ import { Effect } from "effect";
39
+ import api from "../_generated/api";
40
+ import { DatabaseReader } from "../_generated/services";
41
+ import { NoteNotFound } from "./notes.spec";
42
+
43
+ const getOrFail = FunctionImpl.make(api, "notes", "getOrFail", ({ noteId }) =>
44
+ Effect.gen(function* () {
45
+ const reader = yield* DatabaseReader;
46
+ return yield* reader
47
+ .table("notes")
48
+ .get(noteId)
49
+ .pipe(Effect.mapError(() => new NoteNotFound({ noteId })));
50
+ }),
51
+ );
52
+ ```
53
+
54
+ ### Consuming a typed error
55
+
56
+ `@confect/js` (`HttpClient`, `WebSocketClient`) and `@confect/test` (`TestConfect`) surface the decoded error in the `Effect` error channel alongside the existing `HttpClientError`/`WebSocketClientError`/`ParseError`:
57
+
58
+ ```ts
59
+ HttpClient.query(refs.public.notes.getOrFail, { noteId });
60
+ // Effect.Effect<Note, NoteNotFound | HttpClientError | ParseError>
61
+ ```
62
+
63
+ ### `@confect/react`—breaking changes
64
+
65
+ `useQuery`, `useMutation`, and `useAction` now expose typed errors, and `useQuery` returns a tagged result type instead of `Returns | undefined`.
66
+
67
+ **`useQuery` now returns `QueryResult<A, E>`.** Loading and (when an `error` schema is declared) failure are reified as variants alongside success. Match on the result with `QueryResult.match`:
68
+
69
+ Before:
70
+
71
+ ```tsx
72
+ const notes = useQuery(refs.public.notes.list, {});
73
+ if (notes === undefined) return <p>Loading…</p>;
74
+ return <NoteList notes={notes} />;
75
+ ```
76
+
77
+ After:
78
+
79
+ ```tsx
80
+ import { QueryResult, useQuery } from "@confect/react";
81
+
82
+ const notes = useQuery(refs.public.notes.list, {});
83
+ return QueryResult.match(notes, {
84
+ onLoading: (skipped) => (skipped ? null : <p>Loading…</p>),
85
+ onSuccess: (notes) => <NoteList notes={notes} />,
86
+ });
87
+ ```
88
+
89
+ The `Loading` variant carries a `skipped: boolean` flag, exposed as the argument to `onLoading`. It distinguishes a query that is genuinely in flight (`skipped: false`) from one that is sitting idle because `"skip"` was passed as its args (`skipped: true`)—a distinction `convex/react`'s plain `undefined` return value cannot make. Use it to render a loading indicator only when work is actually happening, and an empty/placeholder state otherwise.
90
+
91
+ When the ref declares an `error` schema, `onFailure` becomes required and receives the decoded typed error:
92
+
93
+ ```tsx
94
+ const lookup = useQuery(refs.public.notes.getOrFail, { noteId });
95
+ QueryResult.match(lookup, {
96
+ onLoading: (skipped) => (skipped ? null : "Looking up…"),
97
+ onSuccess: (note) => `Found: ${note.text}`,
98
+ onFailure: (error) => `Note ${error.noteId} not found.`,
99
+ });
100
+ ```
101
+
102
+ `QueryResult` is a Confect-native type exported from `@confect/react`.
103
+
104
+ **`useMutation` and `useAction` return `Promise<Either<A, E>>` when the ref declares an `error` schema.** Refs without an `error` schema continue to resolve to `Promise<A>`, matching the prior shape and `convex/react`'s behavior.
105
+
106
+ ```ts
107
+ const deleteOrFail = useMutation(refs.public.notes.deleteOrFail);
108
+ const result = await deleteOrFail({ noteId });
109
+ // Either.Either<null, NoteNotFound | Forbidden>
110
+ Either.match(result, {
111
+ onLeft: (error) => /* typed error */,
112
+ onRight: (value) => /* success */,
113
+ });
114
+
115
+ const deleteNote = useMutation(refs.public.notes.delete_); // no `error` schema
116
+ await deleteNote({ noteId }); // Promise<null>, as before
117
+ ```
118
+
119
+ Unspecified failures continue to reject the promise.
120
+
121
+ ### Migration
122
+ - For each `useQuery` call site, replace `result === undefined` checks and direct property access with `QueryResult.match` (or the lower-level `QueryResult.isLoading`/`isSuccess`/`isFailure` predicates).
123
+ - For each `useMutation`/`useAction` call site whose ref now declares an `error` schema, unwrap the resolved `Either` (e.g. with `Either.match`); call sites against refs without an `error` schema need no change.
124
+
125
+ ### Patch Changes
126
+
127
+ - Updated dependencies [90094d0]
128
+ - @confect/core@7.0.0
129
+
3
130
  ## 6.0.0
4
131
 
5
132
  ### Minor Changes
@@ -0,0 +1,71 @@
1
+ import { Pipeable } from "effect";
2
+
3
+ //#region src/QueryResult.d.ts
4
+ declare namespace QueryResult_d_exports {
5
+ export { Failure, Loading, QueryResult, Success, fail, isFailure, isLoading, isQueryResult, isSuccess, load, match, succeed };
6
+ }
7
+ declare const TypeId = "@confect/react/QueryResult";
8
+ type TypeId = typeof TypeId;
9
+ /**
10
+ * A `QueryResult` represents the result of a Confect query.
11
+ *
12
+ * @typeParam A - The type of the decoded `returns` value in the `Success` variant.
13
+ * @typeParam E - The type of the decoded typed error in the `Failure` variant.
14
+ */
15
+ type QueryResult<A, E = never> = Loading<A, E> | Success<A, E> | Failure<A, E>;
16
+ declare namespace QueryResult {
17
+ interface Proto<A, E> extends Pipeable.Pipeable {
18
+ readonly [TypeId]: {
19
+ readonly E: (_: never) => E;
20
+ readonly A: (_: never) => A;
21
+ };
22
+ }
23
+ type Success<R> = R extends QueryResult<infer A, infer _E> ? A : never;
24
+ type Failure<R> = R extends QueryResult<infer _A, infer E> ? E : never;
25
+ }
26
+ interface Loading<A, E = never> extends QueryResult.Proto<A, E> {
27
+ readonly _tag: "Loading";
28
+ readonly skipped: boolean;
29
+ }
30
+ interface Success<A, E = never> extends QueryResult.Proto<A, E> {
31
+ readonly _tag: "Success";
32
+ readonly value: A;
33
+ }
34
+ interface Failure<A, E = never> extends QueryResult.Proto<A, E> {
35
+ readonly _tag: "Failure";
36
+ readonly error: E;
37
+ }
38
+ declare const isQueryResult: (u: unknown) => u is QueryResult<unknown, unknown>;
39
+ declare const load: <A = never, E = never>(skipped: boolean) => Loading<A, E>;
40
+ declare const succeed: <A, E = never>(value: A) => Success<A, E>;
41
+ declare const fail: <E, A = never>(error: E) => Failure<A, E>;
42
+ declare const isLoading: <A, E>(queryResult: QueryResult<A, E>) => queryResult is Loading<A, E>;
43
+ declare const isSuccess: <A, E>(queryResult: QueryResult<A, E>) => queryResult is Success<A, E>;
44
+ declare const isFailure: <A, E>(queryResult: QueryResult<A, E>) => queryResult is Failure<A, E>;
45
+ type MatchOptions<A, E, X, Y, Z> = {
46
+ readonly onLoading: (skipped: boolean) => X;
47
+ readonly onSuccess: (value: A) => Y;
48
+ } & ([E] extends [never] ? {} : {
49
+ readonly onFailure: (error: E) => Z;
50
+ });
51
+ type MatchReturns<E, X, Y, Z> = [E] extends [never] ? X | Y : X | Y | Z;
52
+ /**
53
+ * Matches a {@link QueryResult} to the appropriate handler based on its tag. If
54
+ * the provided `QueryResult` cannot fail (i.e. `E` is `never`), `onFailure` is
55
+ * not required.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * const result = QueryResult.match(queryResult, {
60
+ * onLoading: (skipped) => skipped ? null : <p>Loading…</p>,
61
+ * onSuccess: (value) => <p>{value.text}</p>,
62
+ * onFailure: (error) => <p>Error: {error.message}</p>,
63
+ * });
64
+ */
65
+ declare const match: {
66
+ <A, E, X, Y, Z = never>(options: MatchOptions<A, E, X, Y, Z>): (self: QueryResult<A, E>) => MatchReturns<E, X, Y, Z>;
67
+ <A, E, X, Y, Z = never>(self: QueryResult<A, E>, options: MatchOptions<A, E, X, Y, Z>): MatchReturns<E, X, Y, Z>;
68
+ };
69
+ //#endregion
70
+ export { QueryResult, QueryResult_d_exports };
71
+ //# sourceMappingURL=QueryResult.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QueryResult.d.ts","names":[],"sources":["../src/QueryResult.ts"],"mappings":";;;;;;cAEM,MAAA;AAAA,KACD,MAAA,UAAgB,MAAA;;;;;;;KAQT,WAAA,iBACR,OAAA,CAAQ,CAAA,EAAG,CAAA,IACX,OAAA,CAAQ,CAAA,EAAG,CAAA,IACX,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,kBAEU,WAAA;EAAA,UACN,KAAA,eAAoB,QAAA,CAAS,QAAA;IAAA,UAClC,MAAA;MAAA,SACC,CAAA,GAAI,CAAA,YAAa,CAAA;MAAA,SACjB,CAAA,GAAI,CAAA,YAAa,CAAA;IAAA;EAAA;EAAA,KAKlB,OAAA,MAAa,CAAA,SAAU,WAAA,sBAAiC,CAAA;EAAA,KAGxD,OAAA,MAAa,CAAA,SAAU,WAAA,sBAAiC,CAAA;AAAA;AAAA,UAGrD,OAAA,uBAA8B,WAAA,CAAY,KAAA,CAAM,CAAA,EAAG,CAAA;EAAA,SACzD,IAAA;EAAA,SACA,OAAA;AAAA;AAAA,UAGM,OAAA,uBAA8B,WAAA,CAAY,KAAA,CAAM,CAAA,EAAG,CAAA;EAAA,SACzD,IAAA;EAAA,SACA,KAAA,EAAO,CAAA;AAAA;AAAA,UAGD,OAAA,uBAA8B,WAAA,CAAY,KAAA,CAAM,CAAA,EAAG,CAAA;EAAA,SACzD,IAAA;EAAA,SACA,KAAA,EAAO,CAAA;AAAA;AAAA,cAGL,aAAA,GAAiB,CAAA,cAAa,CAAA,IAAK,WAAA;AAAA,cA8CnC,IAAA,yBAA8B,OAAA,cAAmB,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,cAM5D,OAAA,iBAAyB,KAAA,EAAO,CAAA,KAAI,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,cAM/C,IAAA,iBAAsB,KAAA,EAAO,CAAA,KAAI,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,cAM5C,SAAA,SACX,WAAA,EAAa,WAAA,CAAY,CAAA,EAAG,CAAA,MAC3B,WAAA,IAAe,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,cAEhB,SAAA,SACX,WAAA,EAAa,WAAA,CAAY,CAAA,EAAG,CAAA,MAC3B,WAAA,IAAe,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,cAEhB,SAAA,SACX,WAAA,EAAa,WAAA,CAAY,CAAA,EAAG,CAAA,MAC3B,WAAA,IAAe,OAAA,CAAQ,CAAA,EAAG,CAAA;AAAA,KAExB,YAAA;EAAA,SACM,SAAA,GAAY,OAAA,cAAqB,CAAA;EAAA,SACjC,SAAA,GAAY,KAAA,EAAO,CAAA,KAAM,CAAA;AAAA,MAC9B,CAAA;EAAA,SAAqC,SAAA,GAAY,KAAA,EAAO,CAAA,KAAM,CAAA;AAAA;AAAA,KAE/D,YAAA,gBAA4B,CAAA,oBAAqB,CAAA,GAAI,CAAA,GAAI,CAAA,GAAI,CAAA,GAAI,CAAA;;;;;;;;;;AA/GtE;;;;cA8Ha,KAAA;EAAA,wBAET,OAAA,EAAS,YAAA,CAAa,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,KAChC,IAAA,EAAM,WAAA,CAAY,CAAA,EAAG,CAAA,MAAO,YAAA,CAAa,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA;EAAA,wBAEpD,IAAA,EAAM,WAAA,CAAY,CAAA,EAAG,CAAA,GACrB,OAAA,EAAS,YAAA,CAAa,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,IACjC,YAAA,CAAa,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA;AAAA"}
@@ -0,0 +1,82 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
+ import { Equal, Function, Hash, Pipeable, Predicate, identity } from "effect";
3
+
4
+ //#region src/QueryResult.ts
5
+ var QueryResult_exports = /* @__PURE__ */ __exportAll({
6
+ fail: () => fail,
7
+ isFailure: () => isFailure,
8
+ isLoading: () => isLoading,
9
+ isQueryResult: () => isQueryResult,
10
+ isSuccess: () => isSuccess,
11
+ load: () => load,
12
+ match: () => match,
13
+ succeed: () => succeed
14
+ });
15
+ const TypeId = "@confect/react/QueryResult";
16
+ const isQueryResult = (u) => Predicate.hasProperty(u, TypeId);
17
+ const QueryResultProto = {
18
+ [TypeId]: {
19
+ E: identity,
20
+ A: identity
21
+ },
22
+ pipe(...args) {
23
+ return Pipeable.pipeArguments(this, args);
24
+ },
25
+ [Equal.symbol](that) {
26
+ if (this._tag !== that._tag) return false;
27
+ switch (this._tag) {
28
+ case "Loading": return this.skipped === that.skipped;
29
+ case "Success": return Equal.equals(this.value, that.value);
30
+ case "Failure": return Equal.equals(this.error, that.error);
31
+ }
32
+ },
33
+ [Hash.symbol]() {
34
+ const tagHash = Hash.string(this._tag);
35
+ switch (this._tag) {
36
+ case "Loading": return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.skipped)));
37
+ case "Success": return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.value)));
38
+ case "Failure": return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.error)));
39
+ }
40
+ }
41
+ };
42
+ const load = (skipped) => Object.assign(Object.create(QueryResultProto), {
43
+ _tag: "Loading",
44
+ skipped
45
+ });
46
+ const succeed = (value) => Object.assign(Object.create(QueryResultProto), {
47
+ _tag: "Success",
48
+ value
49
+ });
50
+ const fail = (error) => Object.assign(Object.create(QueryResultProto), {
51
+ _tag: "Failure",
52
+ error
53
+ });
54
+ const isLoading = (queryResult) => queryResult._tag === "Loading";
55
+ const isSuccess = (queryResult) => queryResult._tag === "Success";
56
+ const isFailure = (queryResult) => queryResult._tag === "Failure";
57
+ /**
58
+ * Matches a {@link QueryResult} to the appropriate handler based on its tag. If
59
+ * the provided `QueryResult` cannot fail (i.e. `E` is `never`), `onFailure` is
60
+ * not required.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const result = QueryResult.match(queryResult, {
65
+ * onLoading: (skipped) => skipped ? null : <p>Loading…</p>,
66
+ * onSuccess: (value) => <p>{value.text}</p>,
67
+ * onFailure: (error) => <p>Error: {error.message}</p>,
68
+ * });
69
+ */
70
+ const match = Function.dual(2, (self, options) => {
71
+ switch (self._tag) {
72
+ case "Loading": return options.onLoading(self.skipped);
73
+ case "Success": return options.onSuccess(self.value);
74
+ case "Failure":
75
+ if (Predicate.hasProperty(options, "onFailure")) return options.onFailure(self.error);
76
+ throw new Error("`onFailure` is required when error schema is provided");
77
+ }
78
+ });
79
+
80
+ //#endregion
81
+ export { QueryResult_exports, fail, load, succeed };
82
+ //# sourceMappingURL=QueryResult.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QueryResult.js","names":[],"sources":["../src/QueryResult.ts"],"sourcesContent":["import { Equal, Function, Hash, identity, Pipeable, Predicate } from \"effect\";\n\nconst TypeId = \"@confect/react/QueryResult\";\ntype TypeId = typeof TypeId;\n\n/**\n * A `QueryResult` represents the result of a Confect query.\n *\n * @typeParam A - The type of the decoded `returns` value in the `Success` variant.\n * @typeParam E - The type of the decoded typed error in the `Failure` variant.\n */\nexport type QueryResult<A, E = never> =\n | Loading<A, E>\n | Success<A, E>\n | Failure<A, E>;\n\nexport declare namespace QueryResult {\n export interface Proto<A, E> extends Pipeable.Pipeable {\n readonly [TypeId]: {\n readonly E: (_: never) => E;\n readonly A: (_: never) => A;\n };\n }\n\n // eslint-disable-next-line @typescript-eslint/no-shadow\n export type Success<R> = R extends QueryResult<infer A, infer _E> ? A : never;\n\n // eslint-disable-next-line @typescript-eslint/no-shadow\n export type Failure<R> = R extends QueryResult<infer _A, infer E> ? E : never;\n}\n\nexport interface Loading<A, E = never> extends QueryResult.Proto<A, E> {\n readonly _tag: \"Loading\";\n readonly skipped: boolean;\n}\n\nexport interface Success<A, E = never> extends QueryResult.Proto<A, E> {\n readonly _tag: \"Success\";\n readonly value: A;\n}\n\nexport interface Failure<A, E = never> extends QueryResult.Proto<A, E> {\n readonly _tag: \"Failure\";\n readonly error: E;\n}\n\nexport const isQueryResult = (u: unknown): u is QueryResult<unknown, unknown> =>\n Predicate.hasProperty(u, TypeId);\n\nconst QueryResultProto = {\n [TypeId]: {\n E: identity,\n A: identity,\n },\n pipe(this: QueryResult<any, any>, ...args: ReadonlyArray<unknown>) {\n return Pipeable.pipeArguments(\n this,\n args as unknown as Parameters<typeof Pipeable.pipeArguments>[1],\n );\n },\n [Equal.symbol](\n this: QueryResult<any, any>,\n that: QueryResult<any, any>,\n ): boolean {\n if (this._tag !== that._tag) {\n return false;\n }\n switch (this._tag) {\n case \"Loading\":\n return this.skipped === (that as Loading<any, any>).skipped;\n case \"Success\":\n return Equal.equals(this.value, (that as Success<any, any>).value);\n case \"Failure\":\n return Equal.equals(this.error, (that as Failure<any, any>).error);\n }\n },\n [Hash.symbol](this: QueryResult<any, any>): number {\n const tagHash = Hash.string(this._tag);\n switch (this._tag) {\n case \"Loading\":\n return Hash.cached(\n this,\n Hash.combine(tagHash)(Hash.hash(this.skipped)),\n );\n case \"Success\":\n return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.value)));\n case \"Failure\":\n return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.error)));\n }\n },\n};\n\nexport const load = <A = never, E = never>(skipped: boolean): Loading<A, E> =>\n Object.assign(Object.create(QueryResultProto), {\n _tag: \"Loading\" as const,\n skipped,\n });\n\nexport const succeed = <A, E = never>(value: A): Success<A, E> =>\n Object.assign(Object.create(QueryResultProto), {\n _tag: \"Success\" as const,\n value,\n });\n\nexport const fail = <E, A = never>(error: E): Failure<A, E> =>\n Object.assign(Object.create(QueryResultProto), {\n _tag: \"Failure\" as const,\n error,\n });\n\nexport const isLoading = <A, E>(\n queryResult: QueryResult<A, E>,\n): queryResult is Loading<A, E> => queryResult._tag === \"Loading\";\n\nexport const isSuccess = <A, E>(\n queryResult: QueryResult<A, E>,\n): queryResult is Success<A, E> => queryResult._tag === \"Success\";\n\nexport const isFailure = <A, E>(\n queryResult: QueryResult<A, E>,\n): queryResult is Failure<A, E> => queryResult._tag === \"Failure\";\n\ntype MatchOptions<A, E, X, Y, Z> = {\n readonly onLoading: (skipped: boolean) => X;\n readonly onSuccess: (value: A) => Y;\n} & ([E] extends [never] ? {} : { readonly onFailure: (error: E) => Z });\n\ntype MatchReturns<E, X, Y, Z> = [E] extends [never] ? X | Y : X | Y | Z;\n\n/**\n * Matches a {@link QueryResult} to the appropriate handler based on its tag. If\n * the provided `QueryResult` cannot fail (i.e. `E` is `never`), `onFailure` is\n * not required.\n *\n * @example\n * ```tsx\n * const result = QueryResult.match(queryResult, {\n * onLoading: (skipped) => skipped ? null : <p>Loading…</p>,\n * onSuccess: (value) => <p>{value.text}</p>,\n * onFailure: (error) => <p>Error: {error.message}</p>,\n * });\n */\nexport const match: {\n <A, E, X, Y, Z = never>(\n options: MatchOptions<A, E, X, Y, Z>,\n ): (self: QueryResult<A, E>) => MatchReturns<E, X, Y, Z>;\n <A, E, X, Y, Z = never>(\n self: QueryResult<A, E>,\n options: MatchOptions<A, E, X, Y, Z>,\n ): MatchReturns<E, X, Y, Z>;\n} = Function.dual(\n 2,\n <A, E, X, Y, Z = never>(\n self: QueryResult<A, E>,\n options: MatchOptions<A, E, X, Y, Z>,\n ): MatchReturns<E, X, Y, Z> => {\n switch (self._tag) {\n case \"Loading\":\n return options.onLoading(self.skipped);\n case \"Success\":\n return options.onSuccess(self.value);\n case \"Failure\": {\n if (Predicate.hasProperty(options, \"onFailure\")) {\n return options.onFailure(self.error) as MatchReturns<E, X, Y, Z>;\n }\n throw new Error(\n \"`onFailure` is required when error schema is provided\",\n );\n }\n }\n },\n);\n"],"mappings":";;;;;;;;;;;;;;AAEA,MAAM,SAAS;AA4Cf,MAAa,iBAAiB,MAC5B,UAAU,YAAY,GAAG,OAAO;AAElC,MAAM,mBAAmB;EACtB,SAAS;EACR,GAAG;EACH,GAAG;EACJ;CACD,KAAkC,GAAG,MAA8B;AACjE,SAAO,SAAS,cACd,MACA,KACD;;CAEH,CAAC,MAAM,QAEL,MACS;AACT,MAAI,KAAK,SAAS,KAAK,KACrB,QAAO;AAET,UAAQ,KAAK,MAAb;GACE,KAAK,UACH,QAAO,KAAK,YAAa,KAA2B;GACtD,KAAK,UACH,QAAO,MAAM,OAAO,KAAK,OAAQ,KAA2B,MAAM;GACpE,KAAK,UACH,QAAO,MAAM,OAAO,KAAK,OAAQ,KAA2B,MAAM;;;CAGxE,CAAC,KAAK,UAA6C;EACjD,MAAM,UAAU,KAAK,OAAO,KAAK,KAAK;AACtC,UAAQ,KAAK,MAAb;GACE,KAAK,UACH,QAAO,KAAK,OACV,MACA,KAAK,QAAQ,QAAQ,CAAC,KAAK,KAAK,KAAK,QAAQ,CAAC,CAC/C;GACH,KAAK,UACH,QAAO,KAAK,OAAO,MAAM,KAAK,QAAQ,QAAQ,CAAC,KAAK,KAAK,KAAK,MAAM,CAAC,CAAC;GACxE,KAAK,UACH,QAAO,KAAK,OAAO,MAAM,KAAK,QAAQ,QAAQ,CAAC,KAAK,KAAK,KAAK,MAAM,CAAC,CAAC;;;CAG7E;AAED,MAAa,QAA8B,YACzC,OAAO,OAAO,OAAO,OAAO,iBAAiB,EAAE;CAC7C,MAAM;CACN;CACD,CAAC;AAEJ,MAAa,WAAyB,UACpC,OAAO,OAAO,OAAO,OAAO,iBAAiB,EAAE;CAC7C,MAAM;CACN;CACD,CAAC;AAEJ,MAAa,QAAsB,UACjC,OAAO,OAAO,OAAO,OAAO,iBAAiB,EAAE;CAC7C,MAAM;CACN;CACD,CAAC;AAEJ,MAAa,aACX,gBACiC,YAAY,SAAS;AAExD,MAAa,aACX,gBACiC,YAAY,SAAS;AAExD,MAAa,aACX,gBACiC,YAAY,SAAS;;;;;;;;;;;;;;AAsBxD,MAAa,QAQT,SAAS,KACX,IAEE,MACA,YAC6B;AAC7B,SAAQ,KAAK,MAAb;EACE,KAAK,UACH,QAAO,QAAQ,UAAU,KAAK,QAAQ;EACxC,KAAK,UACH,QAAO,QAAQ,UAAU,KAAK,MAAM;EACtC,KAAK;AACH,OAAI,UAAU,YAAY,SAAS,YAAY,CAC7C,QAAO,QAAQ,UAAU,KAAK,MAAM;AAEtC,SAAM,IAAI,MACR,wDACD;;EAIR"}
@@ -0,0 +1,18 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) {
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ }
11
+ if (!no_symbols) {
12
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ }
14
+ return target;
15
+ };
16
+
17
+ //#endregion
18
+ export { __exportAll };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,39 @@
1
+ import { QueryResult, QueryResult_d_exports } from "./QueryResult.js";
1
2
  import { Ref } from "@confect/core";
3
+ import { Either } from "effect";
2
4
 
3
5
  //#region src/index.d.ts
6
+ type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [never] ? Promise<Ref.Returns<Ref_>> : Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>>;
4
7
  type UseQueryArgs<Query extends Ref.AnyPublicQuery> = keyof Ref.Args<Query> extends never ? [args?: Ref.Args<Query> | "skip"] : [args: Ref.Args<Query> | "skip"];
5
- declare const useQuery: <Query extends Ref.AnyPublicQuery>(ref: Query, ...rest: UseQueryArgs<Query>) => Ref.Returns<Query> | undefined;
6
- declare const useMutation: <Mutation extends Ref.AnyPublicMutation>(ref: Mutation) => (...args: Ref.OptionalArgs<Mutation>) => Promise<Ref.Returns<Mutation>>;
7
- declare const useAction: <Action extends Ref.AnyPublicAction>(ref: Action) => (...args: Ref.OptionalArgs<Action>) => Promise<Ref.Returns<Action>>;
8
+ declare const useQuery: <Query extends Ref.AnyPublicQuery>(ref: Query, ...rest: UseQueryArgs<Query>) => QueryResult<Ref.Returns<Query>, Ref.Error<Query>>;
9
+ /**
10
+ * Returns a function that invokes the provided `Ref`'s mutation.
11
+ *
12
+ * If the `Ref` declares an `error` schema, the returned promise resolves to an
13
+ * `Either` with the decoded `returns` value on the right and the decoded error
14
+ * on the left.
15
+ *
16
+ * If the `Ref` does not declare an `error` schema, the promise resolves
17
+ * directly to the decoded `returns` value, matching the behavior of
18
+ * `useMutation` from `convex/react`.
19
+ *
20
+ * Any other failure rejects the promise.
21
+ */
22
+ declare const useMutation: <Mutation extends Ref.AnyPublicMutation>(ref: Mutation) => ((...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>);
23
+ /**
24
+ * Returns a function that invokes the provided `Ref`'s action.
25
+ *
26
+ * If the `Ref` declares an `error` schema, the returned promise resolves to an
27
+ * `Either` with the decoded `returns` value on the right and the decoded error
28
+ * on the left.
29
+ *
30
+ * If the `Ref` does not declare an `error` schema, the promise resolves
31
+ * directly to the decoded `returns` value, matching the behavior of
32
+ * `useMutation` from `convex/react`.
33
+ *
34
+ * Any other failure rejects the promise.
35
+ */
36
+ declare const useAction: <Action extends Ref.AnyPublicAction>(ref: Action) => ((...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>);
8
37
  //#endregion
9
- export { useAction, useMutation, useQuery };
38
+ export { InvokeReturn, QueryResult_d_exports as QueryResult, useAction, useMutation, useQuery };
10
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;KAOK,YAAA,eAA2B,GAAA,CAAI,cAAA,UAC5B,GAAA,CAAI,IAAA,CAAK,KAAA,mBACV,IAAA,GAAO,GAAA,CAAI,IAAA,CAAK,KAAA,eAChB,IAAA,EAAM,GAAA,CAAI,IAAA,CAAK,KAAA;AAAA,cAET,QAAA,iBAA0B,GAAA,CAAI,cAAA,EACzC,GAAA,EAAK,KAAA,KACF,IAAA,EAAM,YAAA,CAAa,KAAA,MACrB,GAAA,CAAI,OAAA,CAAQ,KAAA;AAAA,cAoBF,WAAA,oBAAgC,GAAA,CAAI,iBAAA,EAC/C,GAAA,EAAK,QAAA,SAMA,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,QAAA,MACzB,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,QAAA;AAAA,cAWZ,SAAA,kBAA4B,GAAA,CAAI,eAAA,EAAiB,GAAA,EAAK,MAAA,SAItD,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,MAAA,MAAU,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;KAYY,YAAA,cAA0B,GAAA,CAAI,GAAA,KAAQ,GAAA,CAAI,KAAA,CAAM,IAAA,qBAGxD,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,IAAA,KACpB,OAAA,CAAQ,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,OAAA,CAAQ,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,KAElD,YAAA,eAA2B,GAAA,CAAI,cAAA,UAC5B,GAAA,CAAI,IAAA,CAAK,KAAA,mBACV,IAAA,GAAO,GAAA,CAAI,IAAA,CAAK,KAAA,eAChB,IAAA,EAAM,GAAA,CAAI,IAAA,CAAK,KAAA;AAAA,cAET,QAAA,iBAA0B,GAAA,CAAI,cAAA,EACzC,GAAA,EAAK,KAAA,KACF,IAAA,EAAM,YAAA,CAAa,KAAA,MACrB,WAAA,CAAwB,GAAA,CAAI,OAAA,CAAQ,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,KAAA;;;;;;;;;;;;;;cA6C5C,WAAA,oBAAgC,GAAA,CAAI,iBAAA,EAC/C,GAAA,EAAK,QAAA,UACC,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,QAAA,MAAc,YAAA,CAAa,QAAA;;;;;;;;;;;;;;cA2B7C,SAAA,kBAA4B,GAAA,CAAI,eAAA,EAC3C,GAAA,EAAK,MAAA,UACC,IAAA,EAAM,GAAA,CAAI,YAAA,CAAa,MAAA,MAAY,YAAA,CAAa,MAAA"}
package/dist/index.js CHANGED
@@ -1,27 +1,65 @@
1
+ import { QueryResult_exports, fail, load, succeed } from "./QueryResult.js";
1
2
  import { Ref } from "@confect/core";
2
3
  import { useAction as useAction$1, useMutation as useMutation$1, useQuery as useQuery$1 } from "convex/react";
4
+ import { Cause, Effect, Either, Exit, Option } from "effect";
3
5
 
4
6
  //#region src/index.ts
5
7
  const useQuery = (ref, ...rest) => {
6
8
  const functionReference = Ref.getFunctionReference(ref);
7
9
  const args = rest[0];
8
- const encodedReturnsOrUndefined = useQuery$1(functionReference, args === "skip" ? "skip" : Ref.encodeArgsSync(ref, args ?? {}));
9
- if (encodedReturnsOrUndefined === void 0) return;
10
- return Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined);
10
+ const encodedArgs = args === "skip" ? "skip" : Ref.encodeArgsSync(ref, args ?? {});
11
+ try {
12
+ const encodedReturnsOrUndefined = useQuery$1(functionReference, encodedArgs);
13
+ if (encodedReturnsOrUndefined === void 0) return load(args === "skip");
14
+ return succeed(Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined));
15
+ } catch (error) {
16
+ if (Ref.isConvexError(error)) {
17
+ const decoded = Ref.decodeErrorSync(ref, error.data);
18
+ if (Option.isSome(decoded)) return fail(decoded.value);
19
+ }
20
+ throw error;
21
+ }
11
22
  };
23
+ /**
24
+ * Returns a function that invokes the provided `Ref`'s mutation.
25
+ *
26
+ * If the `Ref` declares an `error` schema, the returned promise resolves to an
27
+ * `Either` with the decoded `returns` value on the right and the decoded error
28
+ * on the left.
29
+ *
30
+ * If the `Ref` does not declare an `error` schema, the promise resolves
31
+ * directly to the decoded `returns` value, matching the behavior of
32
+ * `useMutation` from `convex/react`.
33
+ *
34
+ * Any other failure rejects the promise.
35
+ */
12
36
  const useMutation = (ref) => {
13
37
  const actualMutation = useMutation$1(Ref.getFunctionReference(ref));
14
- return (...args) => {
15
- return actualMutation(Ref.encodeArgsSync(ref, args[0] ?? {})).then((result) => Ref.decodeReturnsSync(ref, result));
16
- };
38
+ return ((...args) => invokeAsEither(ref, (_, encodedArgs) => actualMutation(encodedArgs), args).then((either) => Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either)));
17
39
  };
40
+ /**
41
+ * Returns a function that invokes the provided `Ref`'s action.
42
+ *
43
+ * If the `Ref` declares an `error` schema, the returned promise resolves to an
44
+ * `Either` with the decoded `returns` value on the right and the decoded error
45
+ * on the left.
46
+ *
47
+ * If the `Ref` does not declare an `error` schema, the promise resolves
48
+ * directly to the decoded `returns` value, matching the behavior of
49
+ * `useMutation` from `convex/react`.
50
+ *
51
+ * Any other failure rejects the promise.
52
+ */
18
53
  const useAction = (ref) => {
19
54
  const actualAction = useAction$1(Ref.getFunctionReference(ref));
20
- return (...args) => {
21
- return actualAction(Ref.encodeArgsSync(ref, args[0] ?? {})).then((result) => Ref.decodeReturnsSync(ref, result));
22
- };
55
+ return ((...args) => invokeAsEither(ref, (_, encodedArgs) => actualAction(encodedArgs), args).then((either) => Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either)));
56
+ };
57
+ const invokeAsEither = async (ref, invoke, args) => {
58
+ const exit = await Effect.runPromiseExit(Ref.runWithCodec(ref, args[0] ?? {}, invoke).pipe(Effect.catchTag("ParseError", Effect.die), Effect.either));
59
+ if (Exit.isSuccess(exit)) return exit.value;
60
+ throw Cause.squash(exit.cause);
23
61
  };
24
62
 
25
63
  //#endregion
26
- export { useAction, useMutation, useQuery };
64
+ export { QueryResult_exports as QueryResult, useAction, useMutation, useQuery };
27
65
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["useConvexQuery","useConvexMutation","useConvexAction"],"sources":["../src/index.ts"],"sourcesContent":["import { Ref } from \"@confect/core\";\nimport {\n useAction as useConvexAction,\n useMutation as useConvexMutation,\n useQuery as useConvexQuery,\n} from \"convex/react\";\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): Ref.Returns<Query> | undefined => {\n const functionReference = Ref.getFunctionReference(ref);\n const args = rest[0];\n const encodedArgs =\n args === \"skip\"\n ? \"skip\"\n : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);\n\n const encodedReturnsOrUndefined = useConvexQuery(\n functionReference,\n encodedArgs,\n );\n\n if (encodedReturnsOrUndefined === undefined) {\n return undefined;\n }\n\n return Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined);\n};\n\nexport const useMutation = <Mutation extends Ref.AnyPublicMutation>(\n ref: Mutation,\n) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualMutation = useConvexMutation(functionReference);\n\n return (\n ...args: Ref.OptionalArgs<Mutation>\n ): Promise<Ref.Returns<Mutation>> => {\n const encodedArgs = Ref.encodeArgsSync(\n ref,\n (args[0] ?? {}) as Ref.Args<Mutation>,\n );\n return actualMutation(encodedArgs).then((result) =>\n Ref.decodeReturnsSync(ref, result),\n );\n };\n};\n\nexport const useAction = <Action extends Ref.AnyPublicAction>(ref: Action) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualAction = useConvexAction(functionReference);\n\n return (...args: Ref.OptionalArgs<Action>): Promise<Ref.Returns<Action>> => {\n const encodedArgs = Ref.encodeArgsSync(\n ref,\n (args[0] ?? {}) as Ref.Args<Action>,\n );\n return actualAction(encodedArgs).then((result) =>\n Ref.decodeReturnsSync(ref, result),\n );\n };\n};\n"],"mappings":";;;;AAYA,MAAa,YACX,KACA,GAAG,SACgC;CACnC,MAAM,oBAAoB,IAAI,qBAAqB,IAAI;CACvD,MAAM,OAAO,KAAK;CAMlB,MAAM,4BAA4BA,WAChC,mBALA,SAAS,SACL,SACA,IAAI,eAAe,KAAM,QAAQ,EAAE,CAAqB,CAK7D;AAED,KAAI,8BAA8B,OAChC;AAGF,QAAO,IAAI,kBAAkB,KAAK,0BAA0B;;AAG9D,MAAa,eACX,QACG;CAEH,MAAM,iBAAiBC,cADG,IAAI,qBAAqB,IAAI,CACI;AAE3D,SACE,GAAG,SACgC;AAKnC,SAAO,eAJa,IAAI,eACtB,KACC,KAAK,MAAM,EAAE,CACf,CACiC,CAAC,MAAM,WACvC,IAAI,kBAAkB,KAAK,OAAO,CACnC;;;AAIL,MAAa,aAAiD,QAAgB;CAE5E,MAAM,eAAeC,YADK,IAAI,qBAAqB,IAAI,CACA;AAEvD,SAAQ,GAAG,SAAiE;AAK1E,SAAO,aAJa,IAAI,eACtB,KACC,KAAK,MAAM,EAAE,CACf,CAC+B,CAAC,MAAM,WACrC,IAAI,kBAAkB,KAAK,OAAO,CACnC"}
1
+ {"version":3,"file":"index.js","names":["useConvexQuery","QueryResult.load","QueryResult.succeed","QueryResult.fail","useConvexMutation","useConvexAction"],"sources":["../src/index.ts"],"sourcesContent":["import { Ref } from \"@confect/core\";\nimport {\n useAction as useConvexAction,\n useMutation as useConvexMutation,\n useQuery as useConvexQuery,\n} from \"convex/react\";\nimport { Cause, Effect, Either, Exit, Option } from \"effect\";\n\nimport * as QueryResult from \"./QueryResult\";\n\nexport { QueryResult };\n\nexport type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [\n never,\n]\n ? Promise<Ref.Returns<Ref_>>\n : Promise<Either.Either<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 encodedArgs =\n args === \"skip\"\n ? \"skip\"\n : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);\n\n try {\n const encodedReturnsOrUndefined = useConvexQuery(\n functionReference,\n encodedArgs,\n );\n\n if (encodedReturnsOrUndefined === undefined) {\n return QueryResult.load(args === \"skip\");\n }\n\n return QueryResult.succeed(\n Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),\n );\n } catch (error) {\n if (Ref.isConvexError(error)) {\n const decoded = Ref.decodeErrorSync(ref, error.data);\n if (Option.isSome(decoded)) {\n return QueryResult.fail(decoded.value);\n }\n }\n throw error;\n }\n};\n\n/**\n * Returns a function that invokes the provided `Ref`'s mutation.\n *\n * If the `Ref` declares an `error` schema, the returned promise resolves to an\n * `Either` with the decoded `returns` value on the right and the decoded error\n * on the left.\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): ((...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>) => {\n const functionReference = Ref.getFunctionReference(ref);\n const actualMutation = useConvexMutation(functionReference);\n\n return ((...args: Ref.OptionalArgs<Mutation>) =>\n invokeAsEither(\n ref,\n (_, encodedArgs) => actualMutation(encodedArgs),\n args,\n ).then((either) =>\n Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),\n )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;\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 an\n * `Either` with the decoded `returns` value on the right and the decoded error\n * on the left.\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 ((...args: Ref.OptionalArgs<Action>) =>\n invokeAsEither(\n ref,\n (_, encodedArgs) => actualAction(encodedArgs),\n args,\n ).then((either) =>\n Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),\n )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>;\n};\n\nconst invokeAsEither = async <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<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>> => {\n const exit = await Effect.runPromiseExit(\n Ref.runWithCodec(ref, (args[0] ?? {}) as Ref.Args<Ref_>, invoke).pipe(\n Effect.catchTag(\"ParseError\", Effect.die),\n Effect.either,\n ),\n );\n if (Exit.isSuccess(exit)) return exit.value;\n throw Cause.squash(exit.cause);\n};\n"],"mappings":";;;;;;AAuBA,MAAa,YACX,KACA,GAAG,SAC+D;CAClE,MAAM,oBAAoB,IAAI,qBAAqB,IAAI;CACvD,MAAM,OAAO,KAAK;CAClB,MAAM,cACJ,SAAS,SACL,SACA,IAAI,eAAe,KAAM,QAAQ,EAAE,CAAqB;AAE9D,KAAI;EACF,MAAM,4BAA4BA,WAChC,mBACA,YACD;AAED,MAAI,8BAA8B,OAChC,QAAOC,KAAiB,SAAS,OAAO;AAG1C,SAAOC,QACL,IAAI,kBAAkB,KAAK,0BAA0B,CACtD;UACM,OAAO;AACd,MAAI,IAAI,cAAc,MAAM,EAAE;GAC5B,MAAM,UAAU,IAAI,gBAAgB,KAAK,MAAM,KAAK;AACpD,OAAI,OAAO,OAAO,QAAQ,CACxB,QAAOC,KAAiB,QAAQ,MAAM;;AAG1C,QAAM;;;;;;;;;;;;;;;;AAiBV,MAAa,eACX,QACsE;CAEtE,MAAM,iBAAiBC,cADG,IAAI,qBAAqB,IAAI,CACI;AAE3D,UAAS,GAAG,SACV,eACE,MACC,GAAG,gBAAgB,eAAe,YAAY,EAC/C,KACD,CAAC,MAAM,WACN,IAAI,eAAe,IAAI,GAAG,SAAS,OAAO,WAAW,OAAO,CAC7D;;;;;;;;;;;;;;;AAgBL,MAAa,aACX,QACkE;CAElE,MAAM,eAAeC,YADK,IAAI,qBAAqB,IAAI,CACA;AAEvD,UAAS,GAAG,SACV,eACE,MACC,GAAG,gBAAgB,aAAa,YAAY,EAC7C,KACD,CAAC,MAAM,WACN,IAAI,eAAe,IAAI,GAAG,SAAS,OAAO,WAAW,OAAO,CAC7D;;AAGL,MAAM,iBAAiB,OACrB,KACA,QAIA,SAC+D;CAC/D,MAAM,OAAO,MAAM,OAAO,eACxB,IAAI,aAAa,KAAM,KAAK,MAAM,EAAE,EAAqB,OAAO,CAAC,KAC/D,OAAO,SAAS,cAAc,OAAO,IAAI,EACzC,OAAO,OACR,CACF;AACD,KAAI,KAAK,UAAU,KAAK,CAAE,QAAO,KAAK;AACtC,OAAM,MAAM,OAAO,KAAK,MAAM"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@confect/react",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "Client-side bindings for React apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/index.d.ts",
26
26
  "default": "./dist/index.js"
27
27
  },
28
+ "./*": {
29
+ "types": "./dist/*.d.ts",
30
+ "default": "./dist/*.js"
31
+ },
28
32
  "./package.json": "./package.json"
29
33
  },
30
34
  "keywords": [
@@ -35,19 +39,24 @@
35
39
  "author": "RJ Dellecese",
36
40
  "license": "ISC",
37
41
  "devDependencies": {
42
+ "@effect/vitest": "0.27.0",
38
43
  "@eslint/js": "10.0.1",
39
44
  "@types/node": "25.3.3",
45
+ "convex": "1.33.1",
46
+ "effect": "3.19.19",
40
47
  "eslint": "10.0.2",
41
48
  "prettier": "3.8.1",
42
49
  "tsdown": "0.20.3",
43
50
  "typescript": "5.9.3",
44
- "typescript-eslint": "8.56.1"
51
+ "typescript-eslint": "8.56.1",
52
+ "vite": "7.3.1",
53
+ "vitest": "3.2.4"
45
54
  },
46
55
  "peerDependencies": {
47
56
  "convex": "^1.30.0",
48
57
  "effect": "^3.19.16",
49
58
  "react": "^18.0.0 || ^19.0.0",
50
- "@confect/core": "6.0.0"
59
+ "@confect/core": "7.0.0"
51
60
  },
52
61
  "engines": {
53
62
  "node": ">=22",
@@ -0,0 +1,172 @@
1
+ import { Equal, Function, Hash, identity, Pipeable, Predicate } from "effect";
2
+
3
+ const TypeId = "@confect/react/QueryResult";
4
+ type TypeId = typeof TypeId;
5
+
6
+ /**
7
+ * A `QueryResult` represents the result of a Confect query.
8
+ *
9
+ * @typeParam A - The type of the decoded `returns` value in the `Success` variant.
10
+ * @typeParam E - The type of the decoded typed error in the `Failure` variant.
11
+ */
12
+ export type QueryResult<A, E = never> =
13
+ | Loading<A, E>
14
+ | Success<A, E>
15
+ | Failure<A, E>;
16
+
17
+ export declare namespace QueryResult {
18
+ export interface Proto<A, E> extends Pipeable.Pipeable {
19
+ readonly [TypeId]: {
20
+ readonly E: (_: never) => E;
21
+ readonly A: (_: never) => A;
22
+ };
23
+ }
24
+
25
+ // eslint-disable-next-line @typescript-eslint/no-shadow
26
+ export type Success<R> = R extends QueryResult<infer A, infer _E> ? A : never;
27
+
28
+ // eslint-disable-next-line @typescript-eslint/no-shadow
29
+ export type Failure<R> = R extends QueryResult<infer _A, infer E> ? E : never;
30
+ }
31
+
32
+ export interface Loading<A, E = never> extends QueryResult.Proto<A, E> {
33
+ readonly _tag: "Loading";
34
+ readonly skipped: boolean;
35
+ }
36
+
37
+ export interface Success<A, E = never> extends QueryResult.Proto<A, E> {
38
+ readonly _tag: "Success";
39
+ readonly value: A;
40
+ }
41
+
42
+ export interface Failure<A, E = never> extends QueryResult.Proto<A, E> {
43
+ readonly _tag: "Failure";
44
+ readonly error: E;
45
+ }
46
+
47
+ export const isQueryResult = (u: unknown): u is QueryResult<unknown, unknown> =>
48
+ Predicate.hasProperty(u, TypeId);
49
+
50
+ const QueryResultProto = {
51
+ [TypeId]: {
52
+ E: identity,
53
+ A: identity,
54
+ },
55
+ pipe(this: QueryResult<any, any>, ...args: ReadonlyArray<unknown>) {
56
+ return Pipeable.pipeArguments(
57
+ this,
58
+ args as unknown as Parameters<typeof Pipeable.pipeArguments>[1],
59
+ );
60
+ },
61
+ [Equal.symbol](
62
+ this: QueryResult<any, any>,
63
+ that: QueryResult<any, any>,
64
+ ): boolean {
65
+ if (this._tag !== that._tag) {
66
+ return false;
67
+ }
68
+ switch (this._tag) {
69
+ case "Loading":
70
+ return this.skipped === (that as Loading<any, any>).skipped;
71
+ case "Success":
72
+ return Equal.equals(this.value, (that as Success<any, any>).value);
73
+ case "Failure":
74
+ return Equal.equals(this.error, (that as Failure<any, any>).error);
75
+ }
76
+ },
77
+ [Hash.symbol](this: QueryResult<any, any>): number {
78
+ const tagHash = Hash.string(this._tag);
79
+ switch (this._tag) {
80
+ case "Loading":
81
+ return Hash.cached(
82
+ this,
83
+ Hash.combine(tagHash)(Hash.hash(this.skipped)),
84
+ );
85
+ case "Success":
86
+ return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.value)));
87
+ case "Failure":
88
+ return Hash.cached(this, Hash.combine(tagHash)(Hash.hash(this.error)));
89
+ }
90
+ },
91
+ };
92
+
93
+ export const load = <A = never, E = never>(skipped: boolean): Loading<A, E> =>
94
+ Object.assign(Object.create(QueryResultProto), {
95
+ _tag: "Loading" as const,
96
+ skipped,
97
+ });
98
+
99
+ export const succeed = <A, E = never>(value: A): Success<A, E> =>
100
+ Object.assign(Object.create(QueryResultProto), {
101
+ _tag: "Success" as const,
102
+ value,
103
+ });
104
+
105
+ export const fail = <E, A = never>(error: E): Failure<A, E> =>
106
+ Object.assign(Object.create(QueryResultProto), {
107
+ _tag: "Failure" as const,
108
+ error,
109
+ });
110
+
111
+ export const isLoading = <A, E>(
112
+ queryResult: QueryResult<A, E>,
113
+ ): queryResult is Loading<A, E> => queryResult._tag === "Loading";
114
+
115
+ export const isSuccess = <A, E>(
116
+ queryResult: QueryResult<A, E>,
117
+ ): queryResult is Success<A, E> => queryResult._tag === "Success";
118
+
119
+ export const isFailure = <A, E>(
120
+ queryResult: QueryResult<A, E>,
121
+ ): queryResult is Failure<A, E> => queryResult._tag === "Failure";
122
+
123
+ type MatchOptions<A, E, X, Y, Z> = {
124
+ readonly onLoading: (skipped: boolean) => X;
125
+ readonly onSuccess: (value: A) => Y;
126
+ } & ([E] extends [never] ? {} : { readonly onFailure: (error: E) => Z });
127
+
128
+ type MatchReturns<E, X, Y, Z> = [E] extends [never] ? X | Y : X | Y | Z;
129
+
130
+ /**
131
+ * Matches a {@link QueryResult} to the appropriate handler based on its tag. If
132
+ * the provided `QueryResult` cannot fail (i.e. `E` is `never`), `onFailure` is
133
+ * not required.
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * const result = QueryResult.match(queryResult, {
138
+ * onLoading: (skipped) => skipped ? null : <p>Loading…</p>,
139
+ * onSuccess: (value) => <p>{value.text}</p>,
140
+ * onFailure: (error) => <p>Error: {error.message}</p>,
141
+ * });
142
+ */
143
+ export const match: {
144
+ <A, E, X, Y, Z = never>(
145
+ options: MatchOptions<A, E, X, Y, Z>,
146
+ ): (self: QueryResult<A, E>) => MatchReturns<E, X, Y, Z>;
147
+ <A, E, X, Y, Z = never>(
148
+ self: QueryResult<A, E>,
149
+ options: MatchOptions<A, E, X, Y, Z>,
150
+ ): MatchReturns<E, X, Y, Z>;
151
+ } = Function.dual(
152
+ 2,
153
+ <A, E, X, Y, Z = never>(
154
+ self: QueryResult<A, E>,
155
+ options: MatchOptions<A, E, X, Y, Z>,
156
+ ): MatchReturns<E, X, Y, Z> => {
157
+ switch (self._tag) {
158
+ case "Loading":
159
+ return options.onLoading(self.skipped);
160
+ case "Success":
161
+ return options.onSuccess(self.value);
162
+ case "Failure": {
163
+ if (Predicate.hasProperty(options, "onFailure")) {
164
+ return options.onFailure(self.error) as MatchReturns<E, X, Y, Z>;
165
+ }
166
+ throw new Error(
167
+ "`onFailure` is required when error schema is provided",
168
+ );
169
+ }
170
+ }
171
+ },
172
+ );
package/src/index.ts CHANGED
@@ -4,6 +4,17 @@ import {
4
4
  useMutation as useConvexMutation,
5
5
  useQuery as useConvexQuery,
6
6
  } from "convex/react";
7
+ import { Cause, Effect, Either, Exit, Option } from "effect";
8
+
9
+ import * as QueryResult from "./QueryResult";
10
+
11
+ export { QueryResult };
12
+
13
+ export type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [
14
+ never,
15
+ ]
16
+ ? Promise<Ref.Returns<Ref_>>
17
+ : Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>>;
7
18
 
8
19
  type UseQueryArgs<Query extends Ref.AnyPublicQuery> =
9
20
  keyof Ref.Args<Query> extends never
@@ -13,7 +24,7 @@ type UseQueryArgs<Query extends Ref.AnyPublicQuery> =
13
24
  export const useQuery = <Query extends Ref.AnyPublicQuery>(
14
25
  ref: Query,
15
26
  ...rest: UseQueryArgs<Query>
16
- ): Ref.Returns<Query> | undefined => {
27
+ ): QueryResult.QueryResult<Ref.Returns<Query>, Ref.Error<Query>> => {
17
28
  const functionReference = Ref.getFunctionReference(ref);
18
29
  const args = rest[0];
19
30
  const encodedArgs =
@@ -21,48 +32,102 @@ export const useQuery = <Query extends Ref.AnyPublicQuery>(
21
32
  ? "skip"
22
33
  : Ref.encodeArgsSync(ref, (args ?? {}) as Ref.Args<Query>);
23
34
 
24
- const encodedReturnsOrUndefined = useConvexQuery(
25
- functionReference,
26
- encodedArgs,
27
- );
35
+ try {
36
+ const encodedReturnsOrUndefined = useConvexQuery(
37
+ functionReference,
38
+ encodedArgs,
39
+ );
28
40
 
29
- if (encodedReturnsOrUndefined === undefined) {
30
- return undefined;
31
- }
41
+ if (encodedReturnsOrUndefined === undefined) {
42
+ return QueryResult.load(args === "skip");
43
+ }
32
44
 
33
- return Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined);
45
+ return QueryResult.succeed(
46
+ Ref.decodeReturnsSync(ref, encodedReturnsOrUndefined),
47
+ );
48
+ } catch (error) {
49
+ if (Ref.isConvexError(error)) {
50
+ const decoded = Ref.decodeErrorSync(ref, error.data);
51
+ if (Option.isSome(decoded)) {
52
+ return QueryResult.fail(decoded.value);
53
+ }
54
+ }
55
+ throw error;
56
+ }
34
57
  };
35
58
 
59
+ /**
60
+ * Returns a function that invokes the provided `Ref`'s mutation.
61
+ *
62
+ * If the `Ref` declares an `error` schema, the returned promise resolves to an
63
+ * `Either` with the decoded `returns` value on the right and the decoded error
64
+ * on the left.
65
+ *
66
+ * If the `Ref` does not declare an `error` schema, the promise resolves
67
+ * directly to the decoded `returns` value, matching the behavior of
68
+ * `useMutation` from `convex/react`.
69
+ *
70
+ * Any other failure rejects the promise.
71
+ */
36
72
  export const useMutation = <Mutation extends Ref.AnyPublicMutation>(
37
73
  ref: Mutation,
38
- ) => {
74
+ ): ((...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>) => {
39
75
  const functionReference = Ref.getFunctionReference(ref);
40
76
  const actualMutation = useConvexMutation(functionReference);
41
77
 
42
- return (
43
- ...args: Ref.OptionalArgs<Mutation>
44
- ): Promise<Ref.Returns<Mutation>> => {
45
- const encodedArgs = Ref.encodeArgsSync(
78
+ return ((...args: Ref.OptionalArgs<Mutation>) =>
79
+ invokeAsEither(
46
80
  ref,
47
- (args[0] ?? {}) as Ref.Args<Mutation>,
48
- );
49
- return actualMutation(encodedArgs).then((result) =>
50
- Ref.decodeReturnsSync(ref, result),
51
- );
52
- };
81
+ (_, encodedArgs) => actualMutation(encodedArgs),
82
+ args,
83
+ ).then((either) =>
84
+ Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
85
+ )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;
53
86
  };
54
87
 
55
- export const useAction = <Action extends Ref.AnyPublicAction>(ref: Action) => {
88
+ /**
89
+ * Returns a function that invokes the provided `Ref`'s action.
90
+ *
91
+ * If the `Ref` declares an `error` schema, the returned promise resolves to an
92
+ * `Either` with the decoded `returns` value on the right and the decoded error
93
+ * on the left.
94
+ *
95
+ * If the `Ref` does not declare an `error` schema, the promise resolves
96
+ * directly to the decoded `returns` value, matching the behavior of
97
+ * `useMutation` from `convex/react`.
98
+ *
99
+ * Any other failure rejects the promise.
100
+ */
101
+ export const useAction = <Action extends Ref.AnyPublicAction>(
102
+ ref: Action,
103
+ ): ((...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>) => {
56
104
  const functionReference = Ref.getFunctionReference(ref);
57
105
  const actualAction = useConvexAction(functionReference);
58
106
 
59
- return (...args: Ref.OptionalArgs<Action>): Promise<Ref.Returns<Action>> => {
60
- const encodedArgs = Ref.encodeArgsSync(
107
+ return ((...args: Ref.OptionalArgs<Action>) =>
108
+ invokeAsEither(
61
109
  ref,
62
- (args[0] ?? {}) as Ref.Args<Action>,
63
- );
64
- return actualAction(encodedArgs).then((result) =>
65
- Ref.decodeReturnsSync(ref, result),
66
- );
67
- };
110
+ (_, encodedArgs) => actualAction(encodedArgs),
111
+ args,
112
+ ).then((either) =>
113
+ Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
114
+ )) as (...args: Ref.OptionalArgs<Action>) => InvokeReturn<Action>;
115
+ };
116
+
117
+ const invokeAsEither = async <Ref_ extends Ref.Any>(
118
+ ref: Ref_,
119
+ invoke: (
120
+ fnRef: Ref.FunctionReference<Ref_>,
121
+ encodedArgs: unknown,
122
+ ) => PromiseLike<unknown>,
123
+ args: Ref.OptionalArgs<Ref_>,
124
+ ): Promise<Either.Either<Ref.Returns<Ref_>, Ref.Error<Ref_>>> => {
125
+ const exit = await Effect.runPromiseExit(
126
+ Ref.runWithCodec(ref, (args[0] ?? {}) as Ref.Args<Ref_>, invoke).pipe(
127
+ Effect.catchTag("ParseError", Effect.die),
128
+ Effect.either,
129
+ ),
130
+ );
131
+ if (Exit.isSuccess(exit)) return exit.value;
132
+ throw Cause.squash(exit.cause);
68
133
  };