@confect/react 9.0.2 → 9.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@confect/react",
3
3
  "description": "Client-side bindings for React apps",
4
- "version": "9.0.2",
4
+ "version": "9.1.0",
5
5
  "author": "RJ Dellecese",
6
6
  "bugs": {
7
7
  "url": "https://github.com/rjdellecese/confect/issues"
@@ -57,7 +57,7 @@
57
57
  "convex": "^1.32.0",
58
58
  "effect": "^3.21.2",
59
59
  "react": "^18.0.0 || ^19.0.0",
60
- "@confect/core": "^9.0.2"
60
+ "@confect/core": "^9.1.0"
61
61
  },
62
62
  "repository": {
63
63
  "type": "git",
@@ -0,0 +1,70 @@
1
+ import { Ref } from "@confect/core";
2
+ import type { OptimisticLocalStore as ConvexOptimisticLocalStore } from "convex/browser";
3
+ import * as Option from "effect/Option";
4
+
5
+ /**
6
+ * The Confect counterpart to Convex's `OptimisticLocalStore`.
7
+ *
8
+ * Its methods accept Confect query `Ref`s and operate on decoded (Effect
9
+ * Schema) values rather than the encoded values stored by Convex. Query values
10
+ * are wrapped in `Option`: `Option.none()` represents a query that is not
11
+ * present in the store, and `Option.some(value)` carries the decoded value.
12
+ */
13
+ export interface OptimisticLocalStore {
14
+ getQuery<Query extends Ref.AnyPublicQuery>(
15
+ queryRef: Query,
16
+ ...args: Ref.OptionalArgs<Query>
17
+ ): Option.Option<Ref.Returns<Query>>;
18
+
19
+ getAllQueries<Query extends Ref.AnyPublicQuery>(
20
+ queryRef: Query,
21
+ ): Array<{
22
+ args: Ref.Args<Query>;
23
+ value: Option.Option<Ref.Returns<Query>>;
24
+ }>;
25
+
26
+ setQuery<Query extends Ref.AnyPublicQuery>(
27
+ queryRef: Query,
28
+ args: Ref.Args<Query>,
29
+ value: Option.Option<Ref.Returns<Query>>,
30
+ ): void;
31
+ }
32
+
33
+ /**
34
+ * Wraps Convex's `OptimisticLocalStore` so that it accepts Confect query `Ref`s
35
+ * and decoded values, handling the encoding/decoding against Convex's store.
36
+ */
37
+ export const make = (
38
+ convexLocalStore: ConvexOptimisticLocalStore,
39
+ ): OptimisticLocalStore => ({
40
+ getQuery: (queryRef, ...rest) => {
41
+ const functionReference = Ref.getFunctionReference(queryRef);
42
+ const args = (rest[0] ?? {}) as Ref.Args<typeof queryRef>;
43
+ const encodedArgs = Ref.encodeArgsSync(queryRef, args);
44
+ const encoded = convexLocalStore.getQuery(functionReference, encodedArgs);
45
+ return encoded === undefined
46
+ ? Option.none()
47
+ : Option.some(Ref.decodeReturnsSync(queryRef, encoded));
48
+ },
49
+ getAllQueries: (queryRef) => {
50
+ const functionReference = Ref.getFunctionReference(queryRef);
51
+ return convexLocalStore
52
+ .getAllQueries(functionReference)
53
+ .map(({ args, value }) => ({
54
+ args: Ref.decodeArgsSync(queryRef, args),
55
+ value:
56
+ value === undefined
57
+ ? Option.none()
58
+ : Option.some(Ref.decodeReturnsSync(queryRef, value)),
59
+ }));
60
+ },
61
+ setQuery: (queryRef, args, value) => {
62
+ const functionReference = Ref.getFunctionReference(queryRef);
63
+ const encodedArgs = Ref.encodeArgsSync(queryRef, args);
64
+ const encodedValue = Option.match(value, {
65
+ onNone: () => undefined,
66
+ onSome: (decoded) => Ref.encodeReturnsSync(queryRef, decoded),
67
+ });
68
+ convexLocalStore.setQuery(functionReference, encodedArgs, encodedValue);
69
+ },
70
+ });
package/src/index.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { Ref } from "@confect/core";
2
+ import type { OptimisticUpdate as ConvexOptimisticUpdate } from "convex/browser";
2
3
  import {
3
4
  useAction as useConvexAction,
4
5
  useMutation as useConvexMutation,
5
6
  useQuery as useConvexQuery,
7
+ type ReactMutation as ConvexReactMutation,
6
8
  } from "convex/react";
9
+ import type { FunctionReference } from "convex/server";
10
+ import type { Value } from "convex/values";
7
11
  import * as Cause from "effect/Cause";
8
12
  import * as Effect from "effect/Effect";
9
13
  import * as Either from "effect/Either";
@@ -11,9 +15,10 @@ import * as Exit from "effect/Exit";
11
15
  import * as Option from "effect/Option";
12
16
  import { useCallback, useMemo } from "react";
13
17
 
18
+ import * as OptimisticLocalStore from "./OptimisticLocalStore";
14
19
  import * as QueryResult from "./QueryResult";
15
20
 
16
- export { QueryResult };
21
+ export { OptimisticLocalStore, QueryResult };
17
22
 
18
23
  export type InvokeReturn<Ref_ extends Ref.Any> = [Ref.Error<Ref_>] extends [
19
24
  never,
@@ -76,7 +81,69 @@ export const useQuery = <Query extends Ref.AnyPublicQuery>(
76
81
  };
77
82
 
78
83
  /**
79
- * Returns a function that invokes the provided `Ref`'s mutation.
84
+ * An optimistic update for a Confect mutation. Mirrors Convex's
85
+ * `OptimisticUpdate`, but receives a Confect {@link OptimisticLocalStore} and
86
+ * the decoded mutation `args`.
87
+ */
88
+ export type OptimisticUpdate<Mutation extends Ref.AnyPublicMutation> = (
89
+ localStore: OptimisticLocalStore.OptimisticLocalStore,
90
+ args: Ref.Args<Mutation>,
91
+ ) => void;
92
+
93
+ /**
94
+ * The handle returned by {@link useMutation}. It is callable like the function
95
+ * returned by Convex's `useMutation`, and additionally exposes
96
+ * `withOptimisticUpdate` for attaching an optimistic update. Mirrors the
97
+ * `ReactMutation` type from `convex/react`.
98
+ */
99
+ export interface ReactMutation<Mutation extends Ref.AnyPublicMutation> {
100
+ (...args: Ref.OptionalArgs<Mutation>): InvokeReturn<Mutation>;
101
+ withOptimisticUpdate(
102
+ optimisticUpdate: OptimisticUpdate<Mutation>,
103
+ ): ReactMutation<Mutation>;
104
+ }
105
+
106
+ const makeReactMutation = <Mutation extends Ref.AnyPublicMutation>(
107
+ ref: Mutation,
108
+ convexReactMutation: ConvexReactMutation<
109
+ Ref.FunctionReference<Mutation> & FunctionReference<"mutation">
110
+ >,
111
+ ): ReactMutation<Mutation> => {
112
+ const callable = ((...args: Ref.OptionalArgs<Mutation>) =>
113
+ invokeAsEither(
114
+ ref,
115
+ (_, encodedArgs) => convexReactMutation(encodedArgs as never),
116
+ args,
117
+ ).then((either) =>
118
+ Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
119
+ )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>;
120
+
121
+ const withOptimisticUpdate = (
122
+ optimisticUpdate: OptimisticUpdate<Mutation>,
123
+ ): ReactMutation<Mutation> => {
124
+ const wrappedUpdate: ConvexOptimisticUpdate<Record<string, Value>> = (
125
+ convexLocalStore,
126
+ encodedArgs,
127
+ ) => {
128
+ const decodedArgs = Ref.decodeArgsSync(ref, encodedArgs);
129
+ optimisticUpdate(
130
+ OptimisticLocalStore.make(convexLocalStore),
131
+ decodedArgs,
132
+ );
133
+ };
134
+ const nextConvexReactMutation =
135
+ convexReactMutation.withOptimisticUpdate(wrappedUpdate);
136
+ return makeReactMutation(ref, nextConvexReactMutation);
137
+ };
138
+
139
+ return Object.assign(callable, { withOptimisticUpdate });
140
+ };
141
+
142
+ /**
143
+ * Returns a {@link ReactMutation} handle for the provided `Ref`'s mutation. The
144
+ * handle is callable to invoke the mutation, and exposes `withOptimisticUpdate`
145
+ * for attaching an optimistic update, mirroring `useMutation` from
146
+ * `convex/react`.
80
147
  *
81
148
  * If the `Ref` declares an `error` schema, the returned promise resolves to an
82
149
  * `Either` with the decoded `returns` value on the right and the decoded error
@@ -90,20 +157,19 @@ export const useQuery = <Query extends Ref.AnyPublicQuery>(
90
157
  */
91
158
  export const useMutation = <Mutation extends Ref.AnyPublicMutation>(
92
159
  ref: Mutation,
93
- ): ((...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>) => {
160
+ ): ReactMutation<Mutation> => {
94
161
  const functionReference = Ref.getFunctionReference(ref);
95
- const actualMutation = useConvexMutation(functionReference);
162
+ const convexReactMutation = useConvexMutation(functionReference);
96
163
 
97
- return useCallback(
98
- ((...args: Ref.OptionalArgs<Mutation>) =>
99
- invokeAsEither(
164
+ return useMemo(
165
+ () =>
166
+ makeReactMutation(
100
167
  ref,
101
- (_, encodedArgs) => actualMutation(encodedArgs),
102
- args,
103
- ).then((either) =>
104
- Ref.hasErrorSchema(ref) ? either : Either.getOrThrow(either),
105
- )) as (...args: Ref.OptionalArgs<Mutation>) => InvokeReturn<Mutation>,
106
- [ref, actualMutation],
168
+ convexReactMutation as ConvexReactMutation<
169
+ Ref.FunctionReference<Mutation> & FunctionReference<"mutation">
170
+ >,
171
+ ),
172
+ [ref, convexReactMutation],
107
173
  );
108
174
  };
109
175