kitcn 0.0.1 → 0.12.1
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/bin/intent.js +3 -0
- package/dist/aggregate/index.d.ts +388 -0
- package/dist/aggregate/index.js +37 -0
- package/dist/api-entry-BckXqaLb.js +66 -0
- package/dist/auth/client/index.d.ts +37 -0
- package/dist/auth/client/index.js +217 -0
- package/dist/auth/config/index.d.ts +45 -0
- package/dist/auth/config/index.js +24 -0
- package/dist/auth/generated/index.d.ts +2 -0
- package/dist/auth/generated/index.js +3 -0
- package/dist/auth/http/index.d.ts +64 -0
- package/dist/auth/http/index.js +461 -0
- package/dist/auth/index.d.ts +221 -0
- package/dist/auth/index.js +1398 -0
- package/dist/auth/nextjs/index.d.ts +50 -0
- package/dist/auth/nextjs/index.js +81 -0
- package/dist/auth-store-Cljlmdmi.js +197 -0
- package/dist/builder-CBdG5W6A.js +1974 -0
- package/dist/caller-factory-cTXNvYdz.js +216 -0
- package/dist/cli.mjs +13264 -0
- package/dist/codegen-lF80HSWu.mjs +3416 -0
- package/dist/context-utils-HPC5nXzx.d.ts +17 -0
- package/dist/create-schema-odyF4kCy.js +156 -0
- package/dist/create-schema-orm-DOyiNDCx.js +246 -0
- package/dist/crpc/index.d.ts +105 -0
- package/dist/crpc/index.js +169 -0
- package/dist/customFunctions-C0voKmtx.js +144 -0
- package/dist/error-BZEnI7Sq.js +41 -0
- package/dist/generated-contract-disabled-Cih4eITO.js +50 -0
- package/dist/generated-contract-disabled-D-sOFy92.d.ts +354 -0
- package/dist/http-types-DqJubRPJ.d.ts +292 -0
- package/dist/meta-utils-0Pu0Nrap.js +117 -0
- package/dist/middleware-BUybuv9n.d.ts +34 -0
- package/dist/middleware-C2qTZ3V7.js +84 -0
- package/dist/orm/index.d.ts +17 -0
- package/dist/orm/index.js +10713 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +3 -0
- package/dist/procedure-caller-DtxLmGwA.d.ts +1467 -0
- package/dist/procedure-caller-MWcxhQDv.js +349 -0
- package/dist/query-context-B8o6-8kC.js +1518 -0
- package/dist/query-context-CFZqIvD7.d.ts +42 -0
- package/dist/query-options-Dw7cOyXl.js +121 -0
- package/dist/ratelimit/index.d.ts +269 -0
- package/dist/ratelimit/index.js +856 -0
- package/dist/ratelimit/react/index.d.ts +76 -0
- package/dist/ratelimit/react/index.js +183 -0
- package/dist/react/index.d.ts +1284 -0
- package/dist/react/index.js +2526 -0
- package/dist/rsc/index.d.ts +276 -0
- package/dist/rsc/index.js +233 -0
- package/dist/runtime-CtvJPkur.js +2453 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +6 -0
- package/dist/solid/index.d.ts +1221 -0
- package/dist/solid/index.js +2940 -0
- package/dist/transformer-DtDhR3Lc.js +194 -0
- package/dist/types-BTb_4BaU.d.ts +42 -0
- package/dist/types-BiJE7qxR.d.ts +4 -0
- package/dist/types-DEJpkIhw.d.ts +88 -0
- package/dist/types-HhO_R6pd.d.ts +213 -0
- package/dist/validators-B7oIJCAp.js +279 -0
- package/dist/validators-vzRKjBJC.d.ts +88 -0
- package/dist/watcher.mjs +96 -0
- package/dist/where-clause-compiler-DdjN63Io.d.ts +4756 -0
- package/package.json +107 -34
- package/skills/convex/SKILL.md +486 -0
- package/skills/convex/references/features/aggregates.md +353 -0
- package/skills/convex/references/features/auth-admin.md +446 -0
- package/skills/convex/references/features/auth-organizations.md +1141 -0
- package/skills/convex/references/features/auth-polar.md +579 -0
- package/skills/convex/references/features/auth.md +470 -0
- package/skills/convex/references/features/create-plugins.md +153 -0
- package/skills/convex/references/features/http.md +676 -0
- package/skills/convex/references/features/migrations.md +162 -0
- package/skills/convex/references/features/orm.md +1166 -0
- package/skills/convex/references/features/react.md +657 -0
- package/skills/convex/references/features/scheduling.md +267 -0
- package/skills/convex/references/features/testing.md +209 -0
- package/skills/convex/references/setup/auth.md +501 -0
- package/skills/convex/references/setup/biome.md +190 -0
- package/skills/convex/references/setup/doc-guidelines.md +145 -0
- package/skills/convex/references/setup/index.md +761 -0
- package/skills/convex/references/setup/next.md +116 -0
- package/skills/convex/references/setup/react.md +175 -0
- package/skills/convex/references/setup/server.md +473 -0
- package/skills/convex/references/setup/start.md +67 -0
- package/LICENSE +0 -21
- package/README.md +0 -0
- package/dist/index.d.mts +0 -5
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,2940 @@
|
|
|
1
|
+
import { getFunctionName } from "convex/server";
|
|
2
|
+
import { Show, createContext, createEffect, createMemo, createSignal, on, onCleanup, onMount, useContext } from "solid-js";
|
|
3
|
+
import { createComponent, memo } from "solid-js/web";
|
|
4
|
+
import { createStore } from "solid-js/store";
|
|
5
|
+
import { notifyManager, skipToken, useQueries, useQueryClient } from "@tanstack/solid-query";
|
|
6
|
+
import { ConvexClient, ConvexHttpClient } from "convex/browser";
|
|
7
|
+
import { hashKey } from "@tanstack/query-core";
|
|
8
|
+
import { convexToJson } from "convex/values";
|
|
9
|
+
|
|
10
|
+
//#region src/crpc/error.ts
|
|
11
|
+
/**
|
|
12
|
+
* Client-side CRPC error.
|
|
13
|
+
* Mirrors backend CRPCError pattern with typed error codes.
|
|
14
|
+
*/
|
|
15
|
+
var CRPCClientError = class extends Error {
|
|
16
|
+
name = "CRPCClientError";
|
|
17
|
+
code;
|
|
18
|
+
functionName;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
super(opts.message ?? `${opts.code}: ${opts.functionName}`);
|
|
21
|
+
this.code = opts.code;
|
|
22
|
+
this.functionName = opts.functionName;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Type guard for CRPCClientError */
|
|
26
|
+
const isCRPCClientError = (error) => error instanceof CRPCClientError;
|
|
27
|
+
/** Default unauthorized detection - checks UNAUTHORIZED code */
|
|
28
|
+
const defaultIsUnauthorized = (error) => {
|
|
29
|
+
if (!error || typeof error !== "object") return false;
|
|
30
|
+
if ("data" in error) {
|
|
31
|
+
const data = error.data;
|
|
32
|
+
if (data && typeof data === "object" && "code" in data) return data.code === "UNAUTHORIZED";
|
|
33
|
+
}
|
|
34
|
+
if ("code" in error) return error.code === "UNAUTHORIZED";
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/solid/auth-store.tsx
|
|
40
|
+
/** @jsxImportSource solid-js */
|
|
41
|
+
/**
|
|
42
|
+
* Auth Store - Generic auth state management with Solid stores
|
|
43
|
+
*
|
|
44
|
+
* Provides token storage and auth callback configuration.
|
|
45
|
+
* App configures handlers, lib hooks consume state.
|
|
46
|
+
*/
|
|
47
|
+
const FetchAccessTokenContext = createContext(null);
|
|
48
|
+
/** Get fetchAccessToken from context (available immediately, no race condition) */
|
|
49
|
+
const useFetchAccessToken = () => useContext(FetchAccessTokenContext);
|
|
50
|
+
/**
|
|
51
|
+
* Context that holds auth result from ConvexAuthBridge.
|
|
52
|
+
* Allows @convex-dev/auth users to use skipUnauth queries without better-auth.
|
|
53
|
+
*/
|
|
54
|
+
const ConvexAuthBridgeContext = createContext(null);
|
|
55
|
+
/** Get auth from bridge context (null if no bridge configured) */
|
|
56
|
+
const useConvexAuthBridge = () => useContext(ConvexAuthBridgeContext);
|
|
57
|
+
/** Decode JWT expiration (ms timestamp) from token */
|
|
58
|
+
function decodeJwtExp(token) {
|
|
59
|
+
try {
|
|
60
|
+
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
61
|
+
return payload.exp ? payload.exp * 1e3 : null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const defaultState = {
|
|
67
|
+
onMutationUnauthorized: () => {
|
|
68
|
+
throw new CRPCClientError({
|
|
69
|
+
code: "UNAUTHORIZED",
|
|
70
|
+
functionName: "mutation"
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
onQueryUnauthorized: () => {},
|
|
74
|
+
isUnauthorized: defaultIsUnauthorized,
|
|
75
|
+
token: null,
|
|
76
|
+
expiresAt: null,
|
|
77
|
+
isLoading: true,
|
|
78
|
+
isAuthenticated: false
|
|
79
|
+
};
|
|
80
|
+
const AuthStoreContext = createContext(null);
|
|
81
|
+
function useAuthStore() {
|
|
82
|
+
const ctx = useContext(AuthStoreContext);
|
|
83
|
+
if (!ctx) return {
|
|
84
|
+
get: (key) => defaultState[key],
|
|
85
|
+
set: () => {},
|
|
86
|
+
store: null
|
|
87
|
+
};
|
|
88
|
+
return ctx;
|
|
89
|
+
}
|
|
90
|
+
function useAuthValue(key) {
|
|
91
|
+
return useAuthStore().get(key);
|
|
92
|
+
}
|
|
93
|
+
function AuthProvider(props) {
|
|
94
|
+
const [state, setState] = createStore({
|
|
95
|
+
...defaultState,
|
|
96
|
+
...props.initialValues,
|
|
97
|
+
...props.isUnauthorized && { isUnauthorized: props.isUnauthorized },
|
|
98
|
+
...props.onMutationUnauthorized && { onMutationUnauthorized: props.onMutationUnauthorized },
|
|
99
|
+
...props.onQueryUnauthorized && { onQueryUnauthorized: props.onQueryUnauthorized }
|
|
100
|
+
});
|
|
101
|
+
const store = {
|
|
102
|
+
get: (key) => state[key],
|
|
103
|
+
set: (key, value) => setState(key, value),
|
|
104
|
+
store: state
|
|
105
|
+
};
|
|
106
|
+
return createComponent(AuthStoreContext.Provider, {
|
|
107
|
+
value: store,
|
|
108
|
+
get children() {
|
|
109
|
+
return props.children;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Safe wrapper that doesn't throw when used outside auth provider.
|
|
115
|
+
* Returns { isAuthenticated: false, isLoading: false } when no auth provider.
|
|
116
|
+
*
|
|
117
|
+
* Supports both:
|
|
118
|
+
* - better-auth users (via AuthProvider)
|
|
119
|
+
* - @convex-dev/auth users (via ConvexAuthBridge)
|
|
120
|
+
*/
|
|
121
|
+
function useSafeConvexAuth() {
|
|
122
|
+
const authStore = useAuthStore();
|
|
123
|
+
const bridgeAuth = useConvexAuthBridge();
|
|
124
|
+
return {
|
|
125
|
+
get isAuthenticated() {
|
|
126
|
+
if (authStore.store) return authStore.get("isAuthenticated");
|
|
127
|
+
if (bridgeAuth !== null) return bridgeAuth.isAuthenticated;
|
|
128
|
+
return false;
|
|
129
|
+
},
|
|
130
|
+
get isLoading() {
|
|
131
|
+
if (authStore.store) return authStore.get("isLoading");
|
|
132
|
+
if (bridgeAuth !== null) return bridgeAuth.isLoading;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Bridge component that provides auth state via context.
|
|
139
|
+
* @internal
|
|
140
|
+
*/
|
|
141
|
+
function ConvexAuthBridge(props) {
|
|
142
|
+
return createComponent(ConvexAuthBridgeContext.Provider, {
|
|
143
|
+
value: {
|
|
144
|
+
get isLoading() {
|
|
145
|
+
return props.isLoading;
|
|
146
|
+
},
|
|
147
|
+
get isAuthenticated() {
|
|
148
|
+
return props.isAuthenticated;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
get children() {
|
|
152
|
+
return props.children;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const useAuth = () => {
|
|
157
|
+
const authStore = useAuthStore();
|
|
158
|
+
const bridgeAuth = useConvexAuthBridge();
|
|
159
|
+
return {
|
|
160
|
+
get hasSession() {
|
|
161
|
+
if (authStore.store) return !!authStore.get("token");
|
|
162
|
+
return false;
|
|
163
|
+
},
|
|
164
|
+
get isAuthenticated() {
|
|
165
|
+
if (authStore.store) return authStore.get("isAuthenticated");
|
|
166
|
+
if (bridgeAuth !== null) return bridgeAuth.isAuthenticated;
|
|
167
|
+
return false;
|
|
168
|
+
},
|
|
169
|
+
get isLoading() {
|
|
170
|
+
if (authStore.store) return authStore.get("isLoading");
|
|
171
|
+
if (bridgeAuth !== null) return bridgeAuth.isLoading;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/** Check if user maybe has auth (optimistic, has token) */
|
|
177
|
+
const useMaybeAuth = () => {
|
|
178
|
+
const auth = useAuth();
|
|
179
|
+
return () => auth.hasSession;
|
|
180
|
+
};
|
|
181
|
+
/** Check if user is authenticated (server-verified) */
|
|
182
|
+
const useIsAuth = () => {
|
|
183
|
+
const auth = useAuth();
|
|
184
|
+
return () => auth.isAuthenticated;
|
|
185
|
+
};
|
|
186
|
+
const useAuthGuard = () => {
|
|
187
|
+
const authStore = useAuthStore();
|
|
188
|
+
const bridgeAuth = useConvexAuthBridge();
|
|
189
|
+
return (callback) => {
|
|
190
|
+
let isAuthenticated = false;
|
|
191
|
+
if (authStore.store) isAuthenticated = authStore.get("isAuthenticated");
|
|
192
|
+
else if (bridgeAuth !== null) isAuthenticated = bridgeAuth.isAuthenticated;
|
|
193
|
+
if (!isAuthenticated) {
|
|
194
|
+
authStore.get("onMutationUnauthorized")();
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return callback ? void callback() : false;
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
/** Render children only when maybe has auth (optimistic) */
|
|
201
|
+
function MaybeAuthenticated(props) {
|
|
202
|
+
const isAuth = useMaybeAuth();
|
|
203
|
+
return createComponent(Show, {
|
|
204
|
+
get when() {
|
|
205
|
+
return isAuth();
|
|
206
|
+
},
|
|
207
|
+
get children() {
|
|
208
|
+
return props.children;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Render children only when authenticated (server-verified) */
|
|
213
|
+
function Authenticated(props) {
|
|
214
|
+
const isAuth = useIsAuth();
|
|
215
|
+
return createComponent(Show, {
|
|
216
|
+
get when() {
|
|
217
|
+
return isAuth();
|
|
218
|
+
},
|
|
219
|
+
get children() {
|
|
220
|
+
return props.children;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** Render children only when maybe not auth (optimistic) */
|
|
225
|
+
function MaybeUnauthenticated(props) {
|
|
226
|
+
const isAuth = useMaybeAuth();
|
|
227
|
+
return createComponent(Show, {
|
|
228
|
+
get when() {
|
|
229
|
+
return !isAuth();
|
|
230
|
+
},
|
|
231
|
+
get children() {
|
|
232
|
+
return props.children;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** Render children only when not authenticated (server-verified) */
|
|
237
|
+
function Unauthenticated(props) {
|
|
238
|
+
const auth = useAuth();
|
|
239
|
+
return createComponent(Show, {
|
|
240
|
+
get when() {
|
|
241
|
+
return memo(() => !!!auth.isAuthenticated)() && !auth.isLoading;
|
|
242
|
+
},
|
|
243
|
+
get children() {
|
|
244
|
+
return props.children;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/solid/auth.ts
|
|
251
|
+
const MetaContext = createContext(void 0);
|
|
252
|
+
/**
|
|
253
|
+
* Hook to access the meta object from context.
|
|
254
|
+
* Returns undefined if meta was not provided.
|
|
255
|
+
*/
|
|
256
|
+
function useMeta() {
|
|
257
|
+
return useContext(MetaContext);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Hook to get function metadata from the meta index.
|
|
261
|
+
*/
|
|
262
|
+
function useFnMeta() {
|
|
263
|
+
const meta = useMeta();
|
|
264
|
+
return (namespace, fnName) => meta?.[namespace]?.[fnName];
|
|
265
|
+
}
|
|
266
|
+
/** Get auth type from meta for a function */
|
|
267
|
+
function getAuthType(meta, funcName) {
|
|
268
|
+
const [namespace, fnName] = funcName.split(":");
|
|
269
|
+
return meta?.[namespace]?.[fnName]?.auth;
|
|
270
|
+
}
|
|
271
|
+
/** Hook to compute auth-based skip logic for queries */
|
|
272
|
+
function useAuthSkip(funcRef, opts) {
|
|
273
|
+
const auth = useSafeConvexAuth();
|
|
274
|
+
const authType = getAuthType(useMeta(), getFunctionName(funcRef));
|
|
275
|
+
const authLoadingApplies = authType === "optional" || authType === "required";
|
|
276
|
+
return {
|
|
277
|
+
authType,
|
|
278
|
+
get isAuthLoading() {
|
|
279
|
+
return auth.isLoading;
|
|
280
|
+
},
|
|
281
|
+
get isAuthenticated() {
|
|
282
|
+
return auth.isAuthenticated;
|
|
283
|
+
},
|
|
284
|
+
get shouldSkip() {
|
|
285
|
+
const isAuthLoading = auth.isLoading;
|
|
286
|
+
const isAuthenticated = auth.isAuthenticated;
|
|
287
|
+
return opts?.enabled === false || authLoadingApplies && isAuthLoading || authType === "required" && !isAuthenticated && !isAuthLoading || !isAuthenticated && !isAuthLoading && !!opts?.skipUnauth;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/shared/meta-utils.ts
|
|
294
|
+
const metaCache = /* @__PURE__ */ new WeakMap();
|
|
295
|
+
const nonMetaLeafKeys = new Set(["functionRef", "ref"]);
|
|
296
|
+
function isRecord(value) {
|
|
297
|
+
return typeof value === "object" && value !== null;
|
|
298
|
+
}
|
|
299
|
+
function isFunctionType(value) {
|
|
300
|
+
return value === "query" || value === "mutation" || value === "action";
|
|
301
|
+
}
|
|
302
|
+
function isMetaScalar(value) {
|
|
303
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
304
|
+
}
|
|
305
|
+
function extractLeafMeta(value) {
|
|
306
|
+
const type = value.type;
|
|
307
|
+
if (!isFunctionType(type)) return;
|
|
308
|
+
const result = { type };
|
|
309
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
310
|
+
if (key === "type" || nonMetaLeafKeys.has(key) || key.startsWith("_")) continue;
|
|
311
|
+
if (entry === void 0) continue;
|
|
312
|
+
if (isMetaScalar(entry)) result[key] = entry;
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
function getHttpRoutes(api) {
|
|
317
|
+
const routes = api._http;
|
|
318
|
+
if (!isRecord(routes)) return;
|
|
319
|
+
const normalized = {};
|
|
320
|
+
for (const [routeKey, routeValue] of Object.entries(routes)) {
|
|
321
|
+
if (!isRecord(routeValue)) continue;
|
|
322
|
+
const routePath = routeValue.path;
|
|
323
|
+
const routeMethod = routeValue.method;
|
|
324
|
+
if (typeof routePath === "string" && typeof routeMethod === "string") normalized[routeKey] = {
|
|
325
|
+
path: routePath,
|
|
326
|
+
method: routeMethod
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return normalized;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Build a metadata index from merged API leaves.
|
|
333
|
+
* Supports both generated `api` objects and plain metadata fixtures.
|
|
334
|
+
*/
|
|
335
|
+
function buildMetaIndex(api) {
|
|
336
|
+
const cached = metaCache.get(api);
|
|
337
|
+
if (cached) return cached;
|
|
338
|
+
const meta = {};
|
|
339
|
+
const httpRoutes = getHttpRoutes(api);
|
|
340
|
+
if (httpRoutes) meta._http = httpRoutes;
|
|
341
|
+
const walk = (node, path) => {
|
|
342
|
+
for (const [key, value] of Object.entries(node)) {
|
|
343
|
+
if (key.startsWith("_")) continue;
|
|
344
|
+
if (!isRecord(value)) continue;
|
|
345
|
+
const leafMeta = extractLeafMeta(value);
|
|
346
|
+
if (leafMeta) {
|
|
347
|
+
if (path.length === 0) continue;
|
|
348
|
+
const namespace = path.join("/");
|
|
349
|
+
meta[namespace] ??= {};
|
|
350
|
+
meta[namespace][key] = leafMeta;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
walk(value, [...path, key]);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
walk(api, []);
|
|
357
|
+
metaCache.set(api, meta);
|
|
358
|
+
return meta;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get a function reference from the API object by traversing the path.
|
|
362
|
+
*/
|
|
363
|
+
function getFuncRef(api, path) {
|
|
364
|
+
let current = api;
|
|
365
|
+
for (const key of path) if (current && typeof current === "object") {
|
|
366
|
+
const next = current[key];
|
|
367
|
+
if (next === void 0) throw new Error(`Invalid path: ${path.join(".")}`);
|
|
368
|
+
current = next;
|
|
369
|
+
} else throw new Error(`Invalid path: ${path.join(".")}`);
|
|
370
|
+
if (current && typeof current === "object") {
|
|
371
|
+
const maybeFunctionRef = current.functionRef;
|
|
372
|
+
if (maybeFunctionRef && typeof maybeFunctionRef === "object") return maybeFunctionRef;
|
|
373
|
+
}
|
|
374
|
+
if (!current || typeof current !== "object") throw new Error(`Invalid function reference at path: ${path.join(".")}`);
|
|
375
|
+
return current;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Get function type from meta using path.
|
|
379
|
+
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
|
|
380
|
+
*
|
|
381
|
+
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
|
|
382
|
+
* @param meta - The meta object from codegen
|
|
383
|
+
* @returns Function type or 'query' as default
|
|
384
|
+
*/
|
|
385
|
+
function getFunctionType(path, source) {
|
|
386
|
+
if (path.length < 2) return "query";
|
|
387
|
+
const meta = buildMetaIndex(source);
|
|
388
|
+
const fnName = path.at(-1);
|
|
389
|
+
const fnType = meta[path.slice(0, -1).join("/")]?.[fnName]?.type;
|
|
390
|
+
if (fnType === "query" || fnType === "mutation" || fnType === "action") return fnType;
|
|
391
|
+
return "query";
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get function metadata from meta using path.
|
|
395
|
+
* Supports nested paths like ['items', 'queries', 'list'] → namespace='items/queries', fn='list'
|
|
396
|
+
*
|
|
397
|
+
* @param path - Path segments like ['todos', 'create'] or ['items', 'queries', 'list']
|
|
398
|
+
* @param meta - The meta object from codegen
|
|
399
|
+
* @returns Function metadata or undefined
|
|
400
|
+
*/
|
|
401
|
+
function getFunctionMeta(path, source) {
|
|
402
|
+
if (path.length < 2) return;
|
|
403
|
+
const meta = buildMetaIndex(source);
|
|
404
|
+
const fnName = path.at(-1);
|
|
405
|
+
return meta[path.slice(0, -1).join("/")]?.[fnName];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/crpc/http-types.ts
|
|
410
|
+
/** HTTP client error */
|
|
411
|
+
var HttpClientError = class extends Error {
|
|
412
|
+
name = "HttpClientError";
|
|
413
|
+
code;
|
|
414
|
+
status;
|
|
415
|
+
procedureName;
|
|
416
|
+
constructor(opts) {
|
|
417
|
+
super(opts.message ?? `${opts.code}: ${opts.procedureName}`);
|
|
418
|
+
this.code = opts.code;
|
|
419
|
+
this.status = opts.status;
|
|
420
|
+
this.procedureName = opts.procedureName;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/crpc/http-client.ts
|
|
426
|
+
/**
|
|
427
|
+
* HTTP Client Helpers
|
|
428
|
+
*
|
|
429
|
+
* Framework-agnostic utilities for executing HTTP requests
|
|
430
|
+
* against Convex HTTP endpoints.
|
|
431
|
+
*/
|
|
432
|
+
/** Reserved keys that are not part of JSON body */
|
|
433
|
+
const RESERVED_KEYS = new Set([
|
|
434
|
+
"params",
|
|
435
|
+
"searchParams",
|
|
436
|
+
"form",
|
|
437
|
+
"fetch",
|
|
438
|
+
"init",
|
|
439
|
+
"headers"
|
|
440
|
+
]);
|
|
441
|
+
/**
|
|
442
|
+
* Replace URL path parameters with actual values.
|
|
443
|
+
* e.g., '/users/:id' with { id: '123' } -> '/users/123'
|
|
444
|
+
*/
|
|
445
|
+
function replaceUrlParam(url, params) {
|
|
446
|
+
return url.replace(/:(\w+)/g, (_, key) => {
|
|
447
|
+
const value = params[key];
|
|
448
|
+
return value !== void 0 ? encodeURIComponent(value) : `:${key}`;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Build URLSearchParams from query object.
|
|
453
|
+
* Handles array values as multiple params with same key (like Hono).
|
|
454
|
+
*/
|
|
455
|
+
function buildSearchParams(query) {
|
|
456
|
+
const params = new URLSearchParams();
|
|
457
|
+
for (const [key, value] of Object.entries(query)) if (Array.isArray(value)) for (const v of value) params.append(key, v);
|
|
458
|
+
else if (value !== void 0 && value !== null) params.append(key, value);
|
|
459
|
+
return params;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Hono-style HTTP request executor.
|
|
463
|
+
* Processes args in the same way as Hono's ClientRequestImpl.fetch().
|
|
464
|
+
*/
|
|
465
|
+
async function executeHttpRequest(opts) {
|
|
466
|
+
const { method, path } = opts.route;
|
|
467
|
+
const args = opts.args ?? {};
|
|
468
|
+
let rBody;
|
|
469
|
+
let cType;
|
|
470
|
+
if (args.form) {
|
|
471
|
+
const form = new FormData();
|
|
472
|
+
for (const [k, v] of Object.entries(args.form)) if (Array.isArray(v)) for (const v2 of v) form.append(k, v2);
|
|
473
|
+
else form.append(k, v);
|
|
474
|
+
rBody = form;
|
|
475
|
+
} else {
|
|
476
|
+
const jsonBody = {};
|
|
477
|
+
for (const [key, value] of Object.entries(args)) if (!RESERVED_KEYS.has(key) && value !== void 0) jsonBody[key] = value;
|
|
478
|
+
if (Object.keys(jsonBody).length > 0) {
|
|
479
|
+
rBody = JSON.stringify(opts.transformer.input.serialize(jsonBody));
|
|
480
|
+
cType = "application/json";
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const argsClientOpts = {};
|
|
484
|
+
if (args.fetch) argsClientOpts.fetch = args.fetch;
|
|
485
|
+
if (args.init) argsClientOpts.init = args.init;
|
|
486
|
+
if (args.headers) argsClientOpts.headers = args.headers;
|
|
487
|
+
const mergedClientOpts = {
|
|
488
|
+
...opts.clientOpts,
|
|
489
|
+
...argsClientOpts
|
|
490
|
+
};
|
|
491
|
+
const resolvedBaseHeaders = typeof opts.baseHeaders === "function" ? await opts.baseHeaders() : opts.baseHeaders;
|
|
492
|
+
const headerValues = { ...typeof mergedClientOpts.headers === "function" ? await mergedClientOpts.headers() : mergedClientOpts.headers };
|
|
493
|
+
if (cType) headerValues["Content-Type"] = cType;
|
|
494
|
+
const finalHeaders = {};
|
|
495
|
+
if (resolvedBaseHeaders) {
|
|
496
|
+
for (const [key, value] of Object.entries(resolvedBaseHeaders)) if (value !== void 0) finalHeaders[key] = value;
|
|
497
|
+
}
|
|
498
|
+
Object.assign(finalHeaders, headerValues);
|
|
499
|
+
let url = opts.convexSiteUrl + path;
|
|
500
|
+
if (args.params) url = opts.convexSiteUrl + replaceUrlParam(path, args.params);
|
|
501
|
+
if (args.searchParams) {
|
|
502
|
+
const queryString = buildSearchParams(args.searchParams).toString();
|
|
503
|
+
if (queryString) url = `${url}?${queryString}`;
|
|
504
|
+
}
|
|
505
|
+
const methodUpperCase = method.toUpperCase();
|
|
506
|
+
const setBody = !(methodUpperCase === "GET" || methodUpperCase === "HEAD");
|
|
507
|
+
const response = await (mergedClientOpts.fetch ?? opts.baseFetch ?? globalThis.fetch)(url, {
|
|
508
|
+
body: setBody ? rBody : void 0,
|
|
509
|
+
method: methodUpperCase,
|
|
510
|
+
headers: finalHeaders,
|
|
511
|
+
...mergedClientOpts.init
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
const errorData = await response.json().catch(() => ({ error: {
|
|
515
|
+
code: "UNKNOWN",
|
|
516
|
+
message: response.statusText
|
|
517
|
+
} }));
|
|
518
|
+
const errorCode = errorData?.error?.code || "UNKNOWN";
|
|
519
|
+
const errorMessage = errorData?.error?.message || response.statusText;
|
|
520
|
+
throw new HttpClientError({
|
|
521
|
+
code: errorCode,
|
|
522
|
+
status: response.status,
|
|
523
|
+
procedureName: opts.procedureName,
|
|
524
|
+
message: errorMessage
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (response.headers.get("content-length") === "0" || response.status === 204) return;
|
|
528
|
+
if ((response.headers.get("content-type") || "").includes("application/json")) return opts.transformer.output.deserialize(await response.json());
|
|
529
|
+
return response.text();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/crpc/transformer.ts
|
|
534
|
+
const isPlainObject = (value) => {
|
|
535
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
536
|
+
const prototype = Object.getPrototypeOf(value);
|
|
537
|
+
return prototype === Object.prototype || prototype === null;
|
|
538
|
+
};
|
|
539
|
+
const CODEC_MARKER_KEY = "__crpc";
|
|
540
|
+
const CODEC_MARKER_VALUE = 1;
|
|
541
|
+
const CODEC_TAG_KEY = "t";
|
|
542
|
+
const CODEC_VALUE_KEY = "v";
|
|
543
|
+
const hasOnlyCodecPayloadKeys = (value) => {
|
|
544
|
+
let keyCount = 0;
|
|
545
|
+
for (const key in value) {
|
|
546
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
547
|
+
keyCount += 1;
|
|
548
|
+
if (key !== CODEC_MARKER_KEY && key !== CODEC_TAG_KEY && key !== CODEC_VALUE_KEY) return false;
|
|
549
|
+
if (keyCount > 3) return false;
|
|
550
|
+
}
|
|
551
|
+
return keyCount === 3;
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* Date wire tag (Convex-style reserved key).
|
|
555
|
+
*/
|
|
556
|
+
const DATE_CODEC_TAG = "$date";
|
|
557
|
+
/**
|
|
558
|
+
* Built-in Date codec.
|
|
559
|
+
*/
|
|
560
|
+
const dateWireCodec = {
|
|
561
|
+
tag: DATE_CODEC_TAG,
|
|
562
|
+
isType: (value) => value instanceof Date,
|
|
563
|
+
encode: (value) => value.getTime(),
|
|
564
|
+
decode: (value) => {
|
|
565
|
+
if (typeof value !== "number") return value;
|
|
566
|
+
return new Date(value);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
/**
|
|
570
|
+
* Build a recursive tagged transformer from codecs.
|
|
571
|
+
*/
|
|
572
|
+
const createTaggedTransformer = (codecs) => {
|
|
573
|
+
const codecByTag = /* @__PURE__ */ new Map();
|
|
574
|
+
for (const codec of codecs) {
|
|
575
|
+
if (!codec.tag.startsWith("$")) throw new Error(`Invalid wire codec tag '${codec.tag}'. Tags must start with '$'.`);
|
|
576
|
+
if (codecByTag.has(codec.tag)) throw new Error(`Duplicate wire codec tag '${codec.tag}'.`);
|
|
577
|
+
codecByTag.set(codec.tag, codec);
|
|
578
|
+
}
|
|
579
|
+
const serialize = (value) => {
|
|
580
|
+
for (const codec of codecs) if (codec.isType(value)) return {
|
|
581
|
+
[CODEC_MARKER_KEY]: CODEC_MARKER_VALUE,
|
|
582
|
+
[CODEC_TAG_KEY]: codec.tag,
|
|
583
|
+
[CODEC_VALUE_KEY]: serialize(codec.encode(value))
|
|
584
|
+
};
|
|
585
|
+
if (Array.isArray(value)) {
|
|
586
|
+
let result;
|
|
587
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
588
|
+
const item = value[index];
|
|
589
|
+
const serialized = serialize(item);
|
|
590
|
+
if (serialized !== item) {
|
|
591
|
+
if (!result) result = value.slice();
|
|
592
|
+
result[index] = serialized;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return result ?? value;
|
|
596
|
+
}
|
|
597
|
+
if (isPlainObject(value)) {
|
|
598
|
+
let result;
|
|
599
|
+
for (const key in value) {
|
|
600
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
601
|
+
const nested = value[key];
|
|
602
|
+
const serialized = serialize(nested);
|
|
603
|
+
if (serialized !== nested) {
|
|
604
|
+
if (!result) result = { ...value };
|
|
605
|
+
result[key] = serialized;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return result ?? value;
|
|
609
|
+
}
|
|
610
|
+
return value;
|
|
611
|
+
};
|
|
612
|
+
const deserialize = (value) => {
|
|
613
|
+
if (Array.isArray(value)) {
|
|
614
|
+
let result;
|
|
615
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
616
|
+
const item = value[index];
|
|
617
|
+
const deserialized = deserialize(item);
|
|
618
|
+
if (deserialized !== item) {
|
|
619
|
+
if (!result) result = value.slice();
|
|
620
|
+
result[index] = deserialized;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return result ?? value;
|
|
624
|
+
}
|
|
625
|
+
if (isPlainObject(value)) {
|
|
626
|
+
const marker = value[CODEC_MARKER_KEY];
|
|
627
|
+
const tag = value[CODEC_TAG_KEY];
|
|
628
|
+
if (marker === CODEC_MARKER_VALUE && typeof tag === "string" && CODEC_VALUE_KEY in value && hasOnlyCodecPayloadKeys(value)) {
|
|
629
|
+
const codec = codecByTag.get(tag);
|
|
630
|
+
if (codec) return codec.decode(deserialize(value[CODEC_VALUE_KEY]));
|
|
631
|
+
}
|
|
632
|
+
let result;
|
|
633
|
+
for (const key in value) {
|
|
634
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
635
|
+
const nested = value[key];
|
|
636
|
+
const deserialized = deserialize(nested);
|
|
637
|
+
if (deserialized !== nested) {
|
|
638
|
+
if (!result) result = { ...value };
|
|
639
|
+
result[key] = deserialized;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return result ?? value;
|
|
643
|
+
}
|
|
644
|
+
return value;
|
|
645
|
+
};
|
|
646
|
+
return {
|
|
647
|
+
serialize,
|
|
648
|
+
deserialize
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* Default cRPC transformer (Date-enabled).
|
|
653
|
+
*/
|
|
654
|
+
const defaultCRPCTransformer = createTaggedTransformer([dateWireCodec]);
|
|
655
|
+
const DEFAULT_COMBINED_TRANSFORMER = {
|
|
656
|
+
input: defaultCRPCTransformer,
|
|
657
|
+
output: defaultCRPCTransformer
|
|
658
|
+
};
|
|
659
|
+
/**
|
|
660
|
+
* Normalize transformer config to split input/output shape.
|
|
661
|
+
*/
|
|
662
|
+
const normalizeCustomTransformer = (transformer) => {
|
|
663
|
+
if (!transformer) return;
|
|
664
|
+
if ("input" in transformer && "output" in transformer) return transformer;
|
|
665
|
+
return {
|
|
666
|
+
input: transformer,
|
|
667
|
+
output: transformer
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
/**
|
|
671
|
+
* Compose user transformer with default Date transformer.
|
|
672
|
+
*
|
|
673
|
+
* Date transformer is always active:
|
|
674
|
+
* - serialize: user -> default(Date)
|
|
675
|
+
* - deserialize: default(Date) -> user
|
|
676
|
+
*/
|
|
677
|
+
const composeWithDefault = (transformer) => {
|
|
678
|
+
if (!transformer) return defaultCRPCTransformer;
|
|
679
|
+
return {
|
|
680
|
+
serialize: (value) => defaultCRPCTransformer.serialize(transformer.serialize(value)),
|
|
681
|
+
deserialize: (value) => transformer.deserialize(defaultCRPCTransformer.deserialize(value))
|
|
682
|
+
};
|
|
683
|
+
};
|
|
684
|
+
const transformerCache = /* @__PURE__ */ new WeakMap();
|
|
685
|
+
/**
|
|
686
|
+
* Normalize transformer config to split input/output shape.
|
|
687
|
+
* User transformers are additive and always composed with default Date handling.
|
|
688
|
+
*/
|
|
689
|
+
const getTransformer = (transformer) => {
|
|
690
|
+
if (!transformer) return DEFAULT_COMBINED_TRANSFORMER;
|
|
691
|
+
const cacheKey = transformer;
|
|
692
|
+
const cached = transformerCache.get(cacheKey);
|
|
693
|
+
if (cached) return cached;
|
|
694
|
+
const custom = normalizeCustomTransformer(transformer);
|
|
695
|
+
const resolved = {
|
|
696
|
+
input: composeWithDefault(custom?.input),
|
|
697
|
+
output: composeWithDefault(custom?.output)
|
|
698
|
+
};
|
|
699
|
+
transformerCache.set(cacheKey, resolved);
|
|
700
|
+
return resolved;
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
//#endregion
|
|
704
|
+
//#region src/solid/http-proxy.ts
|
|
705
|
+
/**
|
|
706
|
+
* Create a recursive proxy for HTTP routes with TanStack Query integration.
|
|
707
|
+
*
|
|
708
|
+
* Terminal methods:
|
|
709
|
+
* - GET endpoints: `queryOptions`, `queryKey`
|
|
710
|
+
* - POST/PUT/PATCH/DELETE: `mutationOptions`, `mutationKey`
|
|
711
|
+
*/
|
|
712
|
+
function createRecursiveHttpProxy(opts, path = []) {
|
|
713
|
+
return new Proxy(() => {}, { get(_, prop) {
|
|
714
|
+
if (typeof prop === "symbol") return;
|
|
715
|
+
if (prop === "then") return;
|
|
716
|
+
const routeKey = path.join(".");
|
|
717
|
+
const route = opts.routes[routeKey];
|
|
718
|
+
if (prop === "query") {
|
|
719
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
720
|
+
return async (args) => {
|
|
721
|
+
try {
|
|
722
|
+
return await executeHttpRequest({
|
|
723
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
724
|
+
route,
|
|
725
|
+
procedureName: routeKey,
|
|
726
|
+
args,
|
|
727
|
+
baseHeaders: opts.headers,
|
|
728
|
+
baseFetch: opts.fetch,
|
|
729
|
+
transformer: opts.transformer
|
|
730
|
+
});
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
if (prop === "mutate") {
|
|
738
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
739
|
+
return async (args) => {
|
|
740
|
+
try {
|
|
741
|
+
return await executeHttpRequest({
|
|
742
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
743
|
+
route,
|
|
744
|
+
procedureName: routeKey,
|
|
745
|
+
args,
|
|
746
|
+
baseHeaders: opts.headers,
|
|
747
|
+
baseFetch: opts.fetch,
|
|
748
|
+
transformer: opts.transformer
|
|
749
|
+
});
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
752
|
+
throw error;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (prop === "queryOptions") {
|
|
757
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
758
|
+
if (route.method !== "GET") throw new Error(`queryOptions is only available for GET endpoints, got ${route.method} for ${routeKey}`);
|
|
759
|
+
return (args, queryOpts) => ({
|
|
760
|
+
...queryOpts,
|
|
761
|
+
queryKey: [
|
|
762
|
+
"httpQuery",
|
|
763
|
+
routeKey,
|
|
764
|
+
args
|
|
765
|
+
],
|
|
766
|
+
queryFn: async () => {
|
|
767
|
+
try {
|
|
768
|
+
return await executeHttpRequest({
|
|
769
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
770
|
+
route,
|
|
771
|
+
procedureName: routeKey,
|
|
772
|
+
args,
|
|
773
|
+
baseHeaders: opts.headers,
|
|
774
|
+
baseFetch: opts.fetch,
|
|
775
|
+
transformer: opts.transformer
|
|
776
|
+
});
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
if (prop === "queryKey") return (args) => {
|
|
785
|
+
return args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0) ? [
|
|
786
|
+
"httpQuery",
|
|
787
|
+
routeKey,
|
|
788
|
+
args
|
|
789
|
+
] : ["httpQuery", routeKey];
|
|
790
|
+
};
|
|
791
|
+
if (prop === "queryFilter") return (args, filters) => {
|
|
792
|
+
const hasArgs = args !== void 0 && !(typeof args === "object" && args !== null && Object.keys(args).length === 0);
|
|
793
|
+
return {
|
|
794
|
+
...filters,
|
|
795
|
+
queryKey: hasArgs ? [
|
|
796
|
+
"httpQuery",
|
|
797
|
+
routeKey,
|
|
798
|
+
args
|
|
799
|
+
] : ["httpQuery", routeKey]
|
|
800
|
+
};
|
|
801
|
+
};
|
|
802
|
+
if (prop === "mutationOptions") {
|
|
803
|
+
if (!route) throw new Error(`Unknown HTTP procedure: ${routeKey}`);
|
|
804
|
+
return (mutationOpts) => ({
|
|
805
|
+
...mutationOpts,
|
|
806
|
+
mutationKey: ["httpMutation", routeKey],
|
|
807
|
+
mutationFn: async (args) => {
|
|
808
|
+
try {
|
|
809
|
+
return await executeHttpRequest({
|
|
810
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
811
|
+
route,
|
|
812
|
+
procedureName: routeKey,
|
|
813
|
+
args,
|
|
814
|
+
baseHeaders: opts.headers,
|
|
815
|
+
baseFetch: opts.fetch,
|
|
816
|
+
transformer: opts.transformer
|
|
817
|
+
});
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (opts.onError && error instanceof HttpClientError) opts.onError(error);
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (prop === "mutationKey") return () => ["httpMutation", routeKey];
|
|
826
|
+
return createRecursiveHttpProxy(opts, [...path, prop]);
|
|
827
|
+
} });
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Create an HTTP proxy with TanStack Query integration for SolidJS.
|
|
831
|
+
*
|
|
832
|
+
* Returns a proxy that provides:
|
|
833
|
+
* - `queryOptions` for GET endpoints (no subscription)
|
|
834
|
+
* - `mutationOptions` for POST/PUT/PATCH/DELETE endpoints
|
|
835
|
+
*
|
|
836
|
+
* @example
|
|
837
|
+
* ```ts
|
|
838
|
+
* const httpProxy = createHttpProxy<AppRouter>({
|
|
839
|
+
* convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL,
|
|
840
|
+
* routes: httpRoutes,
|
|
841
|
+
* });
|
|
842
|
+
*
|
|
843
|
+
* // GET endpoint
|
|
844
|
+
* const opts = httpProxy.todos.get.queryOptions({ id: '123' });
|
|
845
|
+
* const query = createQuery(() => opts);
|
|
846
|
+
*
|
|
847
|
+
* // POST endpoint
|
|
848
|
+
* const mutation = createMutation(() => httpProxy.todos.create.mutationOptions());
|
|
849
|
+
* await mutation.mutateAsync({ title: 'New todo' });
|
|
850
|
+
* ```
|
|
851
|
+
*/
|
|
852
|
+
function createHttpProxy(opts) {
|
|
853
|
+
const transformer = getTransformer(opts.transformer);
|
|
854
|
+
return createRecursiveHttpProxy({
|
|
855
|
+
convexSiteUrl: opts.convexSiteUrl,
|
|
856
|
+
routes: opts.routes,
|
|
857
|
+
headers: opts.headers,
|
|
858
|
+
fetch: opts.fetch,
|
|
859
|
+
onError: opts.onError,
|
|
860
|
+
transformer
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
//#endregion
|
|
865
|
+
//#region src/crpc/query-options.ts
|
|
866
|
+
/**
|
|
867
|
+
* Query options factory for Convex query function subscriptions.
|
|
868
|
+
* Requires `convexQueryClient.queryFn()` set as the default `queryFn` globally.
|
|
869
|
+
*/
|
|
870
|
+
function convexQuery(funcRef, args, meta, opts) {
|
|
871
|
+
const finalArgs = args ?? {};
|
|
872
|
+
const isSkip = finalArgs === "skip";
|
|
873
|
+
const funcName = getFunctionName(funcRef);
|
|
874
|
+
const [namespace, fnName] = funcName.split(":");
|
|
875
|
+
const authType = meta?.[namespace]?.[fnName]?.auth;
|
|
876
|
+
const skipUnauth = opts?.skipUnauth;
|
|
877
|
+
return {
|
|
878
|
+
queryKey: [
|
|
879
|
+
"convexQuery",
|
|
880
|
+
funcName,
|
|
881
|
+
isSkip ? "skip" : finalArgs
|
|
882
|
+
],
|
|
883
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
884
|
+
refetchInterval: false,
|
|
885
|
+
refetchOnMount: false,
|
|
886
|
+
refetchOnReconnect: false,
|
|
887
|
+
refetchOnWindowFocus: false,
|
|
888
|
+
...isSkip ? { enabled: false } : {},
|
|
889
|
+
meta: {
|
|
890
|
+
authType,
|
|
891
|
+
skipUnauth,
|
|
892
|
+
subscribe: true
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Query options factory for Convex action functions.
|
|
898
|
+
* Actions are NOT reactive - they follow normal TanStack Query semantics.
|
|
899
|
+
*
|
|
900
|
+
* @example
|
|
901
|
+
* ```ts
|
|
902
|
+
* useQuery(convexAction(api.ai.generate, { prompt }))
|
|
903
|
+
* ```
|
|
904
|
+
*
|
|
905
|
+
* @example With additional options (use spread):
|
|
906
|
+
* ```ts
|
|
907
|
+
* useQuery({
|
|
908
|
+
* ...convexAction(api.files.process, { fileId }),
|
|
909
|
+
* staleTime: 60_000
|
|
910
|
+
* });
|
|
911
|
+
* ```
|
|
912
|
+
*/
|
|
913
|
+
function convexAction(funcRef, args, meta, opts) {
|
|
914
|
+
const finalArgs = args ?? {};
|
|
915
|
+
const isSkip = finalArgs === "skip";
|
|
916
|
+
const funcName = getFunctionName(funcRef);
|
|
917
|
+
const [namespace, fnName] = funcName.split(":");
|
|
918
|
+
const authType = meta?.[namespace]?.[fnName]?.auth;
|
|
919
|
+
const skipUnauth = opts?.skipUnauth;
|
|
920
|
+
return {
|
|
921
|
+
queryKey: [
|
|
922
|
+
"convexAction",
|
|
923
|
+
funcName,
|
|
924
|
+
isSkip ? {} : finalArgs
|
|
925
|
+
],
|
|
926
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
927
|
+
refetchInterval: false,
|
|
928
|
+
refetchOnMount: false,
|
|
929
|
+
refetchOnReconnect: false,
|
|
930
|
+
refetchOnWindowFocus: false,
|
|
931
|
+
...isSkip ? { enabled: false } : {},
|
|
932
|
+
meta: {
|
|
933
|
+
authType,
|
|
934
|
+
skipUnauth,
|
|
935
|
+
subscribe: false
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Infinite query options factory for paginated Convex queries.
|
|
941
|
+
* Server-safe (non-hook) - can be used in RSC.
|
|
942
|
+
*
|
|
943
|
+
* Uses flat { cursor, limit } input like tRPC.
|
|
944
|
+
*/
|
|
945
|
+
function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
|
|
946
|
+
const { limit, skipUnauth, enabled, ...queryOptions } = opts;
|
|
947
|
+
const finalArgs = args === "skip" ? {} : args;
|
|
948
|
+
const isSkip = args === "skip";
|
|
949
|
+
const funcName = getFunctionName(funcRef);
|
|
950
|
+
const [namespace, fnName] = funcName.split(":");
|
|
951
|
+
const authType = (meta?.[namespace]?.[fnName])?.auth;
|
|
952
|
+
const firstPageArgs = {
|
|
953
|
+
...finalArgs,
|
|
954
|
+
cursor: null,
|
|
955
|
+
limit
|
|
956
|
+
};
|
|
957
|
+
const finalEnabled = enabled === false || isSkip ? false : void 0;
|
|
958
|
+
return {
|
|
959
|
+
queryKey: [
|
|
960
|
+
"convexQuery",
|
|
961
|
+
funcName,
|
|
962
|
+
firstPageArgs
|
|
963
|
+
],
|
|
964
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
965
|
+
refetchInterval: false,
|
|
966
|
+
refetchOnMount: false,
|
|
967
|
+
refetchOnReconnect: false,
|
|
968
|
+
refetchOnWindowFocus: false,
|
|
969
|
+
...queryOptions,
|
|
970
|
+
...finalEnabled === false ? { enabled: false } : {},
|
|
971
|
+
meta: {
|
|
972
|
+
authType,
|
|
973
|
+
skipUnauth,
|
|
974
|
+
subscribe: true,
|
|
975
|
+
queryName: funcName,
|
|
976
|
+
args: finalArgs,
|
|
977
|
+
limit
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/crpc/types.ts
|
|
984
|
+
/** Symbol key for attaching FunctionReference to options (non-serializable) */
|
|
985
|
+
const FUNC_REF_SYMBOL = Symbol.for("convex.funcRef");
|
|
986
|
+
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/solid/convex-solid.tsx
|
|
989
|
+
/** @jsxImportSource solid-js */
|
|
990
|
+
/**
|
|
991
|
+
* Convex Provider for SolidJS
|
|
992
|
+
*
|
|
993
|
+
* Provides ConvexClient via context and auth integration.
|
|
994
|
+
*/
|
|
995
|
+
const ConvexContext = createContext();
|
|
996
|
+
/** Get the ConvexClient instance from context */
|
|
997
|
+
function useConvex() {
|
|
998
|
+
const client = useContext(ConvexContext);
|
|
999
|
+
if (!client) throw new Error("useConvex must be used within a ConvexProvider");
|
|
1000
|
+
return client;
|
|
1001
|
+
}
|
|
1002
|
+
function ConvexProvider(props) {
|
|
1003
|
+
return createComponent(ConvexContext.Provider, {
|
|
1004
|
+
get value() {
|
|
1005
|
+
return props.client;
|
|
1006
|
+
},
|
|
1007
|
+
get children() {
|
|
1008
|
+
return props.children;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function ConvexProviderWithAuth(props) {
|
|
1013
|
+
const client = props.client;
|
|
1014
|
+
const [isConvexLoading, setIsConvexLoading] = createSignal(true);
|
|
1015
|
+
const [isConvexAuthenticated, setIsConvexAuthenticated] = createSignal(false);
|
|
1016
|
+
createEffect(() => {
|
|
1017
|
+
const auth = props.useAuth();
|
|
1018
|
+
const loading = auth.isLoading;
|
|
1019
|
+
const authenticated = auth.isAuthenticated;
|
|
1020
|
+
if (loading) return;
|
|
1021
|
+
if (!authenticated) {
|
|
1022
|
+
client.clearAuth();
|
|
1023
|
+
setIsConvexLoading(false);
|
|
1024
|
+
setIsConvexAuthenticated(false);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
client.setAuth(auth.fetchAccessToken, (isAuth) => {
|
|
1028
|
+
setIsConvexLoading(false);
|
|
1029
|
+
setIsConvexAuthenticated(isAuth);
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
onCleanup(() => {
|
|
1033
|
+
client.clearAuth();
|
|
1034
|
+
});
|
|
1035
|
+
return createComponent(ConvexContext.Provider, {
|
|
1036
|
+
get value() {
|
|
1037
|
+
return props.client;
|
|
1038
|
+
},
|
|
1039
|
+
get children() {
|
|
1040
|
+
return createComponent(ConvexAuthBridge, {
|
|
1041
|
+
get isAuthenticated() {
|
|
1042
|
+
return isConvexAuthenticated();
|
|
1043
|
+
},
|
|
1044
|
+
get isLoading() {
|
|
1045
|
+
return isConvexLoading();
|
|
1046
|
+
},
|
|
1047
|
+
get children() {
|
|
1048
|
+
return props.children;
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/** Hook returning auth state from Convex */
|
|
1055
|
+
function useConvexAuth() {
|
|
1056
|
+
const bridge = useConvexAuthBridge();
|
|
1057
|
+
if (!bridge) return {
|
|
1058
|
+
isLoading: false,
|
|
1059
|
+
isAuthenticated: false
|
|
1060
|
+
};
|
|
1061
|
+
return bridge;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
//#endregion
|
|
1065
|
+
//#region src/solid/use-query-options.ts
|
|
1066
|
+
/**
|
|
1067
|
+
* Query options factories for Convex functions (SolidJS).
|
|
1068
|
+
* Port of the React version — uses ConvexClient directly instead of convex/react hooks.
|
|
1069
|
+
*
|
|
1070
|
+
* Note: In @tanstack/solid-query, UseMutationOptions and UseQueryOptions are
|
|
1071
|
+
* Accessor wrappers. We use SolidMutationOptions and SolidQueryOptions (plain
|
|
1072
|
+
* object types) for parameters and return values.
|
|
1073
|
+
*/
|
|
1074
|
+
/**
|
|
1075
|
+
* Hook that returns query options for use with useQuery.
|
|
1076
|
+
* Handles skipUnauth by setting enabled: false when unauthorized.
|
|
1077
|
+
*
|
|
1078
|
+
* @example
|
|
1079
|
+
* ```tsx
|
|
1080
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }));
|
|
1081
|
+
* ```
|
|
1082
|
+
*
|
|
1083
|
+
* @example With skipToken for conditional queries
|
|
1084
|
+
* ```tsx
|
|
1085
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, userId ? { id: userId } : skipToken));
|
|
1086
|
+
* ```
|
|
1087
|
+
*
|
|
1088
|
+
* @example With skipUnauth
|
|
1089
|
+
* ```tsx
|
|
1090
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { skipUnauth: true }));
|
|
1091
|
+
* ```
|
|
1092
|
+
*
|
|
1093
|
+
* @example With TanStack Query options
|
|
1094
|
+
* ```tsx
|
|
1095
|
+
* const { data } = useQuery(useConvexQueryOptions(api.user.get, { id }, { enabled: !!id, placeholderData: [] }));
|
|
1096
|
+
* ```
|
|
1097
|
+
*/
|
|
1098
|
+
function useConvexQueryOptions(funcRef, args, options) {
|
|
1099
|
+
const isSkipped = args === skipToken;
|
|
1100
|
+
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
|
|
1101
|
+
const authSkip = useAuthSkip(funcRef, {
|
|
1102
|
+
enabled: isSkipped ? false : enabled,
|
|
1103
|
+
skipUnauth: options?.skipUnauth
|
|
1104
|
+
});
|
|
1105
|
+
const baseOptions = convexQuery(funcRef, isSkipped ? {} : args);
|
|
1106
|
+
const { skipUnauth: _, subscribe, ...queryOptions } = options ?? {};
|
|
1107
|
+
return {
|
|
1108
|
+
...baseOptions,
|
|
1109
|
+
...queryOptions,
|
|
1110
|
+
enabled: isSkipped ? false : !authSkip.shouldSkip,
|
|
1111
|
+
meta: {
|
|
1112
|
+
...baseOptions.meta,
|
|
1113
|
+
authType: authSkip.authType,
|
|
1114
|
+
subscribe: subscribe !== false
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Hook that returns infinite query options for use with useInfiniteQuery.
|
|
1120
|
+
* Handles auth type detection from meta and skipUnauth.
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```tsx
|
|
1124
|
+
* const { data } = useInfiniteQuery(
|
|
1125
|
+
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20 })
|
|
1126
|
+
* );
|
|
1127
|
+
* ```
|
|
1128
|
+
*
|
|
1129
|
+
* @example With skipToken for conditional queries
|
|
1130
|
+
* ```tsx
|
|
1131
|
+
* const { data } = useInfiniteQuery(
|
|
1132
|
+
* useConvexInfiniteQueryOptions(api.posts.list, userId ? { userId } : skipToken, { limit: 20 })
|
|
1133
|
+
* );
|
|
1134
|
+
* ```
|
|
1135
|
+
*
|
|
1136
|
+
* @example With skipUnauth
|
|
1137
|
+
* ```tsx
|
|
1138
|
+
* const { data } = useInfiniteQuery(
|
|
1139
|
+
* useConvexInfiniteQueryOptions(api.posts.list, { userId }, { limit: 20, skipUnauth: true })
|
|
1140
|
+
* );
|
|
1141
|
+
* ```
|
|
1142
|
+
*/
|
|
1143
|
+
function useConvexInfiniteQueryOptions(funcRef, args, opts) {
|
|
1144
|
+
const meta = useMeta();
|
|
1145
|
+
const isSkipped = args === skipToken;
|
|
1146
|
+
const enabledOpt = typeof opts.enabled === "function" ? void 0 : opts.enabled;
|
|
1147
|
+
const authSkip = useAuthSkip(funcRef, {
|
|
1148
|
+
enabled: isSkipped ? false : enabledOpt,
|
|
1149
|
+
skipUnauth: opts.skipUnauth
|
|
1150
|
+
});
|
|
1151
|
+
const enabled = isSkipped || authSkip.shouldSkip ? false : enabledOpt;
|
|
1152
|
+
const baseOptions = convexInfiniteQueryOptions(funcRef, isSkipped ? {} : args, {
|
|
1153
|
+
...opts,
|
|
1154
|
+
enabled
|
|
1155
|
+
}, meta);
|
|
1156
|
+
return {
|
|
1157
|
+
...baseOptions,
|
|
1158
|
+
meta: {
|
|
1159
|
+
...baseOptions.meta,
|
|
1160
|
+
authType: authSkip.authType
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Hook that returns query options for using an action as a one-shot query.
|
|
1166
|
+
* Actions don't support WebSocket subscriptions - they're one-time calls.
|
|
1167
|
+
*
|
|
1168
|
+
* @example
|
|
1169
|
+
* ```tsx
|
|
1170
|
+
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }));
|
|
1171
|
+
* ```
|
|
1172
|
+
*
|
|
1173
|
+
* @example With skipToken for conditional queries
|
|
1174
|
+
* ```tsx
|
|
1175
|
+
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, id ? { id } : skipToken));
|
|
1176
|
+
* ```
|
|
1177
|
+
*
|
|
1178
|
+
* @example With skipUnauth
|
|
1179
|
+
* ```tsx
|
|
1180
|
+
* const { data } = useQuery(useConvexActionQueryOptions(api.ai.analyze, { id }, { skipUnauth: true }));
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
function useConvexActionQueryOptions(action, args, options) {
|
|
1184
|
+
const isSkipped = args === skipToken;
|
|
1185
|
+
const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
|
|
1186
|
+
const authSkip = useAuthSkip(action, {
|
|
1187
|
+
enabled: isSkipped ? false : enabled,
|
|
1188
|
+
skipUnauth: options?.skipUnauth
|
|
1189
|
+
});
|
|
1190
|
+
const baseOptions = convexAction(action, isSkipped ? {} : args);
|
|
1191
|
+
const { skipUnauth: _, ...queryOptions } = options ?? {};
|
|
1192
|
+
return {
|
|
1193
|
+
...baseOptions,
|
|
1194
|
+
...queryOptions,
|
|
1195
|
+
enabled: isSkipped ? false : !authSkip.shouldSkip
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Hook that returns mutation options for use with useMutation.
|
|
1200
|
+
* Wraps the Convex mutation with auth guard logic.
|
|
1201
|
+
*
|
|
1202
|
+
* @example
|
|
1203
|
+
* ```tsx
|
|
1204
|
+
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update));
|
|
1205
|
+
* ```
|
|
1206
|
+
*
|
|
1207
|
+
* @example With TanStack Query options
|
|
1208
|
+
* ```tsx
|
|
1209
|
+
* const { mutate } = useMutation(useConvexMutationOptions(api.user.update, {
|
|
1210
|
+
* onSuccess: () => toast.success('Updated!'),
|
|
1211
|
+
* }));
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
function useConvexMutationOptions(mutation, options, transformer) {
|
|
1215
|
+
const guard = useAuthGuard();
|
|
1216
|
+
const getMeta = useFnMeta();
|
|
1217
|
+
const name = getFunctionName(mutation);
|
|
1218
|
+
const [namespace, fnName] = name.split(":");
|
|
1219
|
+
const authType = getMeta(namespace, fnName)?.auth;
|
|
1220
|
+
const convex = useConvex();
|
|
1221
|
+
const resolvedTransformer = getTransformer(transformer);
|
|
1222
|
+
return {
|
|
1223
|
+
...options,
|
|
1224
|
+
mutationFn: async (args) => {
|
|
1225
|
+
if (authType === "required" && guard()) throw new CRPCClientError({
|
|
1226
|
+
code: "UNAUTHORIZED",
|
|
1227
|
+
functionName: name
|
|
1228
|
+
});
|
|
1229
|
+
return convex.mutation(mutation, resolvedTransformer.input.serialize(args));
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Hook that returns action options for use with useMutation.
|
|
1235
|
+
* Wraps the Convex action with auth guard logic.
|
|
1236
|
+
*
|
|
1237
|
+
* @example
|
|
1238
|
+
* ```tsx
|
|
1239
|
+
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate));
|
|
1240
|
+
* ```
|
|
1241
|
+
*
|
|
1242
|
+
* @example With TanStack Query options
|
|
1243
|
+
* ```tsx
|
|
1244
|
+
* const { mutate } = useMutation(useConvexActionOptions(api.ai.generate, {
|
|
1245
|
+
* onSuccess: (data) => console.info(data),
|
|
1246
|
+
* }));
|
|
1247
|
+
* ```
|
|
1248
|
+
*/
|
|
1249
|
+
function useConvexActionOptions(action, options, transformer) {
|
|
1250
|
+
const guard = useAuthGuard();
|
|
1251
|
+
const getMeta = useFnMeta();
|
|
1252
|
+
const name = getFunctionName(action);
|
|
1253
|
+
const [namespace, fnName] = name.split(":");
|
|
1254
|
+
const authType = getMeta(namespace, fnName)?.auth;
|
|
1255
|
+
const convex = useConvex();
|
|
1256
|
+
const resolvedTransformer = getTransformer(transformer);
|
|
1257
|
+
return {
|
|
1258
|
+
...options,
|
|
1259
|
+
mutationFn: async (args) => {
|
|
1260
|
+
if (authType === "required" && guard()) throw new CRPCClientError({
|
|
1261
|
+
code: "UNAUTHORIZED",
|
|
1262
|
+
functionName: name
|
|
1263
|
+
});
|
|
1264
|
+
return convex.action(action, resolvedTransformer.input.serialize(args));
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Hook that returns upload mutation options for use with useMutation.
|
|
1270
|
+
* Generates a presigned URL, then uploads the file directly to storage.
|
|
1271
|
+
*
|
|
1272
|
+
* @example
|
|
1273
|
+
* ```tsx
|
|
1274
|
+
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl));
|
|
1275
|
+
* mutate({ file, ...otherArgs });
|
|
1276
|
+
* ```
|
|
1277
|
+
*
|
|
1278
|
+
* @example With TanStack Query options
|
|
1279
|
+
* ```tsx
|
|
1280
|
+
* const { mutate } = useMutation(useUploadMutationOptions(api.storage.generateUrl, {
|
|
1281
|
+
* onSuccess: (result) => console.info('Uploaded:', result.key),
|
|
1282
|
+
* }));
|
|
1283
|
+
* ```
|
|
1284
|
+
*/
|
|
1285
|
+
function useUploadMutationOptions(generateUrlMutation, options) {
|
|
1286
|
+
const convex = useConvex();
|
|
1287
|
+
return {
|
|
1288
|
+
...options,
|
|
1289
|
+
mutationFn: async ({ file, ...args }) => {
|
|
1290
|
+
const result = await convex.mutation(generateUrlMutation, args);
|
|
1291
|
+
const { url } = result;
|
|
1292
|
+
const response = await fetch(url, {
|
|
1293
|
+
body: file,
|
|
1294
|
+
headers: { "Content-Type": file.type },
|
|
1295
|
+
method: "PUT"
|
|
1296
|
+
});
|
|
1297
|
+
if (!response.ok) throw new Error(`Upload failed: ${response.statusText}`);
|
|
1298
|
+
return result;
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
//#endregion
|
|
1304
|
+
//#region src/solid/proxy.ts
|
|
1305
|
+
/**
|
|
1306
|
+
* CRPC Recursive Proxy
|
|
1307
|
+
*
|
|
1308
|
+
* Creates a tRPC-like proxy that wraps Convex API functions with
|
|
1309
|
+
* TanStack Query options builders.
|
|
1310
|
+
*
|
|
1311
|
+
* @example
|
|
1312
|
+
* ```ts
|
|
1313
|
+
* const crpc = createCRPCOptionsProxy(api);
|
|
1314
|
+
* const opts = crpc.user.get.queryOptions({ id: '123' });
|
|
1315
|
+
* const { data } = useQuery(opts);
|
|
1316
|
+
* ```
|
|
1317
|
+
*/
|
|
1318
|
+
/** Get query key prefix based on function type */
|
|
1319
|
+
function getQueryKeyPrefix(path, meta) {
|
|
1320
|
+
if (getFunctionType(path, meta) === "action") return "convexAction";
|
|
1321
|
+
return "convexQuery";
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Create a recursive proxy that accumulates path segments.
|
|
1325
|
+
*/
|
|
1326
|
+
function createRecursiveProxy(api, path, meta, transformer) {
|
|
1327
|
+
return new Proxy(() => {}, { get(_target, prop) {
|
|
1328
|
+
if (typeof prop === "symbol") return;
|
|
1329
|
+
if (prop === "then") return;
|
|
1330
|
+
if (prop === "queryOptions") return (args = {}, opts) => {
|
|
1331
|
+
const funcRef = getFuncRef(api, path);
|
|
1332
|
+
if (getFunctionType(path, meta) === "action") return useConvexActionQueryOptions(funcRef, args, opts);
|
|
1333
|
+
return useConvexQueryOptions(funcRef, args, opts);
|
|
1334
|
+
};
|
|
1335
|
+
if (prop === "staticQueryOptions") return (args = {}, opts) => {
|
|
1336
|
+
const funcRef = getFuncRef(api, path);
|
|
1337
|
+
const fnType = getFunctionType(path, meta);
|
|
1338
|
+
const finalArgs = args === skipToken ? "skip" : args;
|
|
1339
|
+
if (fnType === "action") return convexAction(funcRef, finalArgs, meta, opts);
|
|
1340
|
+
return convexQuery(funcRef, finalArgs, meta, opts);
|
|
1341
|
+
};
|
|
1342
|
+
if (prop === "queryKey") return (args = {}) => {
|
|
1343
|
+
const funcName = getFunctionName(getFuncRef(api, path));
|
|
1344
|
+
return [
|
|
1345
|
+
getQueryKeyPrefix(path, meta),
|
|
1346
|
+
funcName,
|
|
1347
|
+
args
|
|
1348
|
+
];
|
|
1349
|
+
};
|
|
1350
|
+
if (prop === "queryFilter") return (args, filters) => {
|
|
1351
|
+
const funcName = getFunctionName(getFuncRef(api, path));
|
|
1352
|
+
const prefix = getQueryKeyPrefix(path, meta);
|
|
1353
|
+
return {
|
|
1354
|
+
...filters,
|
|
1355
|
+
queryKey: [
|
|
1356
|
+
prefix,
|
|
1357
|
+
funcName,
|
|
1358
|
+
args
|
|
1359
|
+
]
|
|
1360
|
+
};
|
|
1361
|
+
};
|
|
1362
|
+
if (prop === "infiniteQueryOptions") return (args = {}, opts = {}) => {
|
|
1363
|
+
const funcRef = getFuncRef(api, path);
|
|
1364
|
+
const options = useConvexInfiniteQueryOptions(funcRef, args, opts);
|
|
1365
|
+
Object.defineProperty(options, FUNC_REF_SYMBOL, {
|
|
1366
|
+
value: funcRef,
|
|
1367
|
+
enumerable: false,
|
|
1368
|
+
configurable: false
|
|
1369
|
+
});
|
|
1370
|
+
return options;
|
|
1371
|
+
};
|
|
1372
|
+
if (prop === "infiniteQueryKey") return (args) => {
|
|
1373
|
+
return [
|
|
1374
|
+
"convexQuery",
|
|
1375
|
+
getFunctionName(getFuncRef(api, path)),
|
|
1376
|
+
args ?? {}
|
|
1377
|
+
];
|
|
1378
|
+
};
|
|
1379
|
+
if (prop === "meta" && path.length >= 2) return getFunctionMeta(path, meta);
|
|
1380
|
+
if (prop === "mutationKey") return () => {
|
|
1381
|
+
return ["convexMutation", getFunctionName(getFuncRef(api, path))];
|
|
1382
|
+
};
|
|
1383
|
+
if (prop === "mutationOptions") return (opts) => {
|
|
1384
|
+
const funcRef = getFuncRef(api, path);
|
|
1385
|
+
if (getFunctionType(path, meta) === "action") return useConvexActionOptions(funcRef, opts, transformer);
|
|
1386
|
+
return useConvexMutationOptions(funcRef, opts, transformer);
|
|
1387
|
+
};
|
|
1388
|
+
return createRecursiveProxy(api, [...path, prop], meta, transformer);
|
|
1389
|
+
} });
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Create a CRPC proxy for a Convex API object.
|
|
1393
|
+
*
|
|
1394
|
+
* The proxy provides a tRPC-like interface for accessing Convex functions
|
|
1395
|
+
* with TanStack Query options builders.
|
|
1396
|
+
*
|
|
1397
|
+
* @param api - The Convex API object (from `@convex/api`)
|
|
1398
|
+
* @param meta - Generated function metadata for runtime type detection
|
|
1399
|
+
* @returns A typed proxy with queryOptions/mutationOptions methods
|
|
1400
|
+
*
|
|
1401
|
+
* @example
|
|
1402
|
+
* ```tsx
|
|
1403
|
+
* import { api } from '@convex/api';
|
|
1404
|
+
*
|
|
1405
|
+
* // Usually you should use createCRPCContext({ api }) instead.
|
|
1406
|
+
* // createCRPCOptionsProxy is a low-level helper.
|
|
1407
|
+
* const crpc = createCRPCOptionsProxy(api, {} as any);
|
|
1408
|
+
*
|
|
1409
|
+
* function MyComponent() {
|
|
1410
|
+
* const { data } = useQuery(crpc.user.get.queryOptions({ id }));
|
|
1411
|
+
* const { mutate } = useMutation(crpc.user.update.mutationOptions());
|
|
1412
|
+
* }
|
|
1413
|
+
* ```
|
|
1414
|
+
*/
|
|
1415
|
+
function createCRPCOptionsProxy(api, meta, transformer) {
|
|
1416
|
+
return createRecursiveProxy(api, [], meta, transformer);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/solid/vanilla-client.ts
|
|
1421
|
+
/**
|
|
1422
|
+
* Create a recursive proxy for vanilla (direct) calls.
|
|
1423
|
+
*/
|
|
1424
|
+
function createRecursiveVanillaProxy(api, path, meta, convexClient, transformer) {
|
|
1425
|
+
return new Proxy(() => {}, { get(_target, prop) {
|
|
1426
|
+
if (typeof prop === "symbol") return;
|
|
1427
|
+
if (prop === "then") return;
|
|
1428
|
+
if (prop === "query") return async (args = {}) => {
|
|
1429
|
+
const funcRef = getFuncRef(api, path);
|
|
1430
|
+
const fnType = getFunctionType(path, meta);
|
|
1431
|
+
const wireArgs = transformer.input.serialize(args);
|
|
1432
|
+
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
|
|
1433
|
+
return transformer.output.deserialize(await convexClient.query(funcRef, wireArgs));
|
|
1434
|
+
};
|
|
1435
|
+
if (prop === "onUpdate") return (args = {}, callback, onError) => {
|
|
1436
|
+
const funcRef = getFuncRef(api, path);
|
|
1437
|
+
return convexClient.onUpdate(funcRef, transformer.input.serialize(args), callback ? (result) => callback(transformer.output.deserialize(result)) : () => {}, onError);
|
|
1438
|
+
};
|
|
1439
|
+
if (prop === "mutate") return async (args = {}) => {
|
|
1440
|
+
const funcRef = getFuncRef(api, path);
|
|
1441
|
+
const fnType = getFunctionType(path, meta);
|
|
1442
|
+
const wireArgs = transformer.input.serialize(args);
|
|
1443
|
+
if (fnType === "action") return transformer.output.deserialize(await convexClient.action(funcRef, wireArgs));
|
|
1444
|
+
return transformer.output.deserialize(await convexClient.mutation(funcRef, wireArgs));
|
|
1445
|
+
};
|
|
1446
|
+
return createRecursiveVanillaProxy(api, [...path, prop], meta, convexClient, transformer);
|
|
1447
|
+
} });
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Create a vanilla CRPC proxy for direct procedural calls.
|
|
1451
|
+
*
|
|
1452
|
+
* The proxy provides a tRPC-like interface for imperative Convex function calls.
|
|
1453
|
+
*
|
|
1454
|
+
* @param api - The Convex API object (from `@convex/api`)
|
|
1455
|
+
* @param meta - Generated function metadata for runtime type detection
|
|
1456
|
+
* @param convexClient - The ConvexClient instance
|
|
1457
|
+
* @returns A typed proxy with query/mutate methods
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* ```tsx
|
|
1461
|
+
* const client = createVanillaCRPCProxy(api, meta, convexClient);
|
|
1462
|
+
*
|
|
1463
|
+
* // Direct calls (no Solid Query)
|
|
1464
|
+
* const user = await client.user.get.query({ id });
|
|
1465
|
+
* await client.user.update.mutate({ id, name: 'test' });
|
|
1466
|
+
* ```
|
|
1467
|
+
*/
|
|
1468
|
+
function createVanillaCRPCProxy(api, meta, convexClient, transformer) {
|
|
1469
|
+
return createRecursiveVanillaProxy(api, [], meta, convexClient, getTransformer(transformer));
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
//#endregion
|
|
1473
|
+
//#region src/solid/context.tsx
|
|
1474
|
+
/** @jsxImportSource solid-js */
|
|
1475
|
+
/**
|
|
1476
|
+
* CRPC Context and Provider
|
|
1477
|
+
*
|
|
1478
|
+
* Provides Solid context for the CRPC proxy, similar to tRPC's createTRPCContext.
|
|
1479
|
+
*/
|
|
1480
|
+
const ConvexQueryClientContext = createContext(null);
|
|
1481
|
+
/** Access ConvexQueryClient (e.g., for logout cleanup) */
|
|
1482
|
+
const useConvexQueryClient = () => useContext(ConvexQueryClientContext);
|
|
1483
|
+
/** Headers record that allows empty objects and optional properties */
|
|
1484
|
+
/**
|
|
1485
|
+
* Extract HTTP router from TApi['http'] if present (optional).
|
|
1486
|
+
* Uses NonNullable to handle optional http property.
|
|
1487
|
+
*/
|
|
1488
|
+
/**
|
|
1489
|
+
* Create CRPC context, provider, and hooks for a Convex API.
|
|
1490
|
+
*
|
|
1491
|
+
* @param options - Configuration object containing api and optional HTTP settings
|
|
1492
|
+
* @returns Object with CRPCProvider, useCRPC, and useCRPCClient
|
|
1493
|
+
*
|
|
1494
|
+
* @example
|
|
1495
|
+
* ```tsx
|
|
1496
|
+
* // lib/crpc.ts
|
|
1497
|
+
* import { api } from '@convex/api';
|
|
1498
|
+
* import { createCRPCContext } from 'kitcn/solid';
|
|
1499
|
+
*
|
|
1500
|
+
* // Works for both regular Convex functions and generated HTTP router types
|
|
1501
|
+
* export const { useCRPC } = createCRPCContext({
|
|
1502
|
+
* api,
|
|
1503
|
+
* convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL,
|
|
1504
|
+
* });
|
|
1505
|
+
*
|
|
1506
|
+
* // components/user-profile.tsx
|
|
1507
|
+
* function UserProfile({ id }) {
|
|
1508
|
+
* const crpc = useCRPC();
|
|
1509
|
+
* const { data } = createQuery(() => crpc.user.get.queryOptions({ id }));
|
|
1510
|
+
*
|
|
1511
|
+
* // HTTP endpoints (if configured)
|
|
1512
|
+
* const { data: httpData } = createQuery(() => crpc.http.todos.get.queryOptions({ id }));
|
|
1513
|
+
* }
|
|
1514
|
+
* ```
|
|
1515
|
+
*/
|
|
1516
|
+
function createCRPCContext(options) {
|
|
1517
|
+
const { api, ...httpOptions } = options;
|
|
1518
|
+
const meta = buildMetaIndex(api);
|
|
1519
|
+
const CRPCProxyContext = createContext(null);
|
|
1520
|
+
const VanillaClientContext = createContext(null);
|
|
1521
|
+
const HttpProxyContext = createContext(void 0);
|
|
1522
|
+
function CRPCProvider(props) {
|
|
1523
|
+
const authStore = useAuthStore();
|
|
1524
|
+
const fetchAccessToken = useFetchAccessToken();
|
|
1525
|
+
const proxy = createCRPCOptionsProxy(api, meta, options.transformer);
|
|
1526
|
+
const vanillaClient = createVanillaCRPCProxy(api, meta, props.convexClient, options.transformer);
|
|
1527
|
+
const httpProxy = (() => {
|
|
1528
|
+
if (!httpOptions.convexSiteUrl || !meta._http) return;
|
|
1529
|
+
return createHttpProxy({
|
|
1530
|
+
convexSiteUrl: httpOptions.convexSiteUrl,
|
|
1531
|
+
routes: meta._http,
|
|
1532
|
+
headers: async () => {
|
|
1533
|
+
const token = authStore.get("token");
|
|
1534
|
+
const expiresAt = authStore.get("expiresAt");
|
|
1535
|
+
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
|
|
1536
|
+
if (token && expiresAt && timeRemaining >= 6e4) return {
|
|
1537
|
+
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
|
|
1538
|
+
Authorization: `Bearer ${token}`
|
|
1539
|
+
};
|
|
1540
|
+
if (fetchAccessToken) {
|
|
1541
|
+
const newToken = await fetchAccessToken({ forceRefreshToken: !!expiresAt });
|
|
1542
|
+
if (newToken) return {
|
|
1543
|
+
...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers,
|
|
1544
|
+
Authorization: `Bearer ${newToken}`
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
return { ...typeof httpOptions.headers === "function" ? await httpOptions.headers() : httpOptions.headers };
|
|
1548
|
+
},
|
|
1549
|
+
fetch: httpOptions.fetch,
|
|
1550
|
+
onError: httpOptions.onError,
|
|
1551
|
+
transformer: options.transformer
|
|
1552
|
+
});
|
|
1553
|
+
})();
|
|
1554
|
+
return createComponent(ConvexQueryClientContext.Provider, {
|
|
1555
|
+
get value() {
|
|
1556
|
+
return props.convexQueryClient;
|
|
1557
|
+
},
|
|
1558
|
+
get children() {
|
|
1559
|
+
return createComponent(MetaContext.Provider, {
|
|
1560
|
+
value: meta,
|
|
1561
|
+
get children() {
|
|
1562
|
+
return createComponent(VanillaClientContext.Provider, {
|
|
1563
|
+
value: vanillaClient,
|
|
1564
|
+
get children() {
|
|
1565
|
+
return createComponent(HttpProxyContext.Provider, {
|
|
1566
|
+
value: httpProxy,
|
|
1567
|
+
get children() {
|
|
1568
|
+
return createComponent(CRPCProxyContext.Provider, {
|
|
1569
|
+
value: proxy,
|
|
1570
|
+
get children() {
|
|
1571
|
+
return props.children;
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
function useCRPC() {
|
|
1584
|
+
const ctx = useContext(CRPCProxyContext);
|
|
1585
|
+
const httpProxy = useContext(HttpProxyContext);
|
|
1586
|
+
if (!ctx) throw new Error("useCRPC must be used within CRPCProvider");
|
|
1587
|
+
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
|
|
1588
|
+
if (prop === "http") return httpProxy;
|
|
1589
|
+
return Reflect.get(target, prop);
|
|
1590
|
+
} });
|
|
1591
|
+
return ctx;
|
|
1592
|
+
}
|
|
1593
|
+
function useCRPCClient() {
|
|
1594
|
+
const ctx = useContext(VanillaClientContext);
|
|
1595
|
+
const httpProxy = useContext(HttpProxyContext);
|
|
1596
|
+
if (!ctx) throw new Error("useCRPCClient must be used within CRPCProvider");
|
|
1597
|
+
if (httpProxy) return new Proxy(ctx, { get(target, prop) {
|
|
1598
|
+
if (prop === "http") return httpProxy;
|
|
1599
|
+
return Reflect.get(target, prop);
|
|
1600
|
+
} });
|
|
1601
|
+
return ctx;
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
CRPCProvider,
|
|
1605
|
+
useCRPC,
|
|
1606
|
+
useCRPCClient
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/crpc/auth-error.ts
|
|
1612
|
+
/**
|
|
1613
|
+
* Auth Mutation Error
|
|
1614
|
+
*
|
|
1615
|
+
* Framework-agnostic error class for Better Auth mutations.
|
|
1616
|
+
*/
|
|
1617
|
+
/**
|
|
1618
|
+
* Error thrown when a Better Auth mutation fails.
|
|
1619
|
+
* Contains the original error details from Better Auth.
|
|
1620
|
+
*/
|
|
1621
|
+
var AuthMutationError = class extends Error {
|
|
1622
|
+
/** Error code from Better Auth (e.g., 'INVALID_PASSWORD', 'EMAIL_ALREADY_REGISTERED') */
|
|
1623
|
+
code;
|
|
1624
|
+
/** HTTP status code */
|
|
1625
|
+
status;
|
|
1626
|
+
/** HTTP status text */
|
|
1627
|
+
statusText;
|
|
1628
|
+
constructor(authError) {
|
|
1629
|
+
super(authError.message || authError.statusText);
|
|
1630
|
+
this.name = "AuthMutationError";
|
|
1631
|
+
this.code = authError.code;
|
|
1632
|
+
this.status = authError.status;
|
|
1633
|
+
this.statusText = authError.statusText;
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
/**
|
|
1637
|
+
* Type guard to check if an error is an AuthMutationError.
|
|
1638
|
+
*/
|
|
1639
|
+
function isAuthMutationError(error) {
|
|
1640
|
+
return error instanceof AuthMutationError;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
//#endregion
|
|
1644
|
+
//#region src/solid/auth-mutations.ts
|
|
1645
|
+
/** Poll until JWT token exists (auth complete) (max 5s) */
|
|
1646
|
+
const waitForAuth = async (store, timeout = 5e3) => {
|
|
1647
|
+
const start = Date.now();
|
|
1648
|
+
while (Date.now() - start < timeout) {
|
|
1649
|
+
if (store.get("token")) return true;
|
|
1650
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1651
|
+
}
|
|
1652
|
+
return false;
|
|
1653
|
+
};
|
|
1654
|
+
const authStateTimeoutError = () => new AuthMutationError({
|
|
1655
|
+
code: "AUTH_STATE_TIMEOUT",
|
|
1656
|
+
message: "Authentication did not complete. Try again.",
|
|
1657
|
+
status: 401,
|
|
1658
|
+
statusText: "UNAUTHORIZED"
|
|
1659
|
+
});
|
|
1660
|
+
const ensureAuth = async (store) => {
|
|
1661
|
+
if (await waitForAuth(store)) return;
|
|
1662
|
+
throw authStateTimeoutError();
|
|
1663
|
+
};
|
|
1664
|
+
const readReturnedToken = (value) => {
|
|
1665
|
+
if (!value || typeof value !== "object") return null;
|
|
1666
|
+
const record = value;
|
|
1667
|
+
if (typeof record.token === "string" && record.token.length > 0) return record.token;
|
|
1668
|
+
return readReturnedToken(record.data) ?? readReturnedToken(record.session);
|
|
1669
|
+
};
|
|
1670
|
+
const seedReturnedToken = (store, value) => {
|
|
1671
|
+
const token = readReturnedToken(value);
|
|
1672
|
+
if (!token) return;
|
|
1673
|
+
store.set("token", token);
|
|
1674
|
+
store.set("expiresAt", decodeJwtExp(token));
|
|
1675
|
+
};
|
|
1676
|
+
/**
|
|
1677
|
+
* Create mutation option hooks from a better-auth client.
|
|
1678
|
+
*
|
|
1679
|
+
* @example
|
|
1680
|
+
* ```tsx
|
|
1681
|
+
* // lib/auth-client.ts
|
|
1682
|
+
* import { createAuthMutations } from 'kitcn/solid';
|
|
1683
|
+
*
|
|
1684
|
+
* export const authClient = createAuthClient({...});
|
|
1685
|
+
*
|
|
1686
|
+
* export const {
|
|
1687
|
+
* useSignOutMutationOptions,
|
|
1688
|
+
* useSignInSocialMutationOptions,
|
|
1689
|
+
* useSignInMutationOptions,
|
|
1690
|
+
* useSignUpMutationOptions,
|
|
1691
|
+
* } = createAuthMutations(authClient);
|
|
1692
|
+
*
|
|
1693
|
+
* // components/header.tsx
|
|
1694
|
+
* const signOutMutation = createMutation(() => useSignOutMutationOptions({
|
|
1695
|
+
* onSuccess: () => navigate('/login'),
|
|
1696
|
+
* }));
|
|
1697
|
+
* ```
|
|
1698
|
+
*/
|
|
1699
|
+
function createAuthMutations(authClient) {
|
|
1700
|
+
const useSignOutMutationOptions = ((options) => {
|
|
1701
|
+
const convexQueryClient = useConvexQueryClient();
|
|
1702
|
+
const authStoreApi = useAuthStore();
|
|
1703
|
+
return {
|
|
1704
|
+
...options,
|
|
1705
|
+
mutationFn: async (args) => {
|
|
1706
|
+
authStoreApi.set("isAuthenticated", false);
|
|
1707
|
+
convexQueryClient?.unsubscribeAuthQueries();
|
|
1708
|
+
const res = await authClient.signOut(args);
|
|
1709
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1710
|
+
authStoreApi.set("token", null);
|
|
1711
|
+
authStoreApi.set("expiresAt", null);
|
|
1712
|
+
return res;
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
});
|
|
1716
|
+
const useSignInSocialMutationOptions = ((options) => {
|
|
1717
|
+
const authStoreApi = useAuthStore();
|
|
1718
|
+
return {
|
|
1719
|
+
...options,
|
|
1720
|
+
mutationFn: async (args) => {
|
|
1721
|
+
const res = await authClient.signIn.social(args);
|
|
1722
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1723
|
+
seedReturnedToken(authStoreApi, res);
|
|
1724
|
+
await ensureAuth(authStoreApi);
|
|
1725
|
+
return res;
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
});
|
|
1729
|
+
const useSignInMutationOptions = ((options) => {
|
|
1730
|
+
const authStoreApi = useAuthStore();
|
|
1731
|
+
return {
|
|
1732
|
+
...options,
|
|
1733
|
+
mutationFn: async (args) => {
|
|
1734
|
+
const res = await authClient.signIn.email(args);
|
|
1735
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1736
|
+
seedReturnedToken(authStoreApi, res);
|
|
1737
|
+
await ensureAuth(authStoreApi);
|
|
1738
|
+
return res;
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
});
|
|
1742
|
+
const useSignUpMutationOptions = ((options) => {
|
|
1743
|
+
const authStoreApi = useAuthStore();
|
|
1744
|
+
return {
|
|
1745
|
+
...options,
|
|
1746
|
+
mutationFn: async (args) => {
|
|
1747
|
+
const res = await authClient.signUp.email(args);
|
|
1748
|
+
if (res?.error) throw new AuthMutationError(res.error);
|
|
1749
|
+
seedReturnedToken(authStoreApi, res);
|
|
1750
|
+
await ensureAuth(authStoreApi);
|
|
1751
|
+
return res;
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1755
|
+
return {
|
|
1756
|
+
useSignOutMutationOptions,
|
|
1757
|
+
useSignInSocialMutationOptions,
|
|
1758
|
+
useSignInMutationOptions,
|
|
1759
|
+
useSignUpMutationOptions
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
//#endregion
|
|
1764
|
+
//#region src/internal/query-key.ts
|
|
1765
|
+
/**
|
|
1766
|
+
* Shared query key utilities for Convex + TanStack Query.
|
|
1767
|
+
* This file has NO React dependencies so it can be imported in both
|
|
1768
|
+
* server (RSC) and client contexts.
|
|
1769
|
+
*/
|
|
1770
|
+
/**
|
|
1771
|
+
* Check if query key is for a Convex query function.
|
|
1772
|
+
* Format: ['convexQuery', 'namespace:functionName', { args }]
|
|
1773
|
+
*/
|
|
1774
|
+
function isConvexQuery(queryKey) {
|
|
1775
|
+
return queryKey.length >= 2 && queryKey[0] === "convexQuery";
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Check if query key is for a Convex action function.
|
|
1779
|
+
* Format: ['convexAction', 'namespace:functionName', { args }]
|
|
1780
|
+
*/
|
|
1781
|
+
function isConvexAction$1(queryKey) {
|
|
1782
|
+
return queryKey.length >= 2 && queryKey[0] === "convexAction";
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Create stable hash for Convex query keys.
|
|
1786
|
+
* Uses Convex's JSON serialization for consistent argument hashing.
|
|
1787
|
+
*/
|
|
1788
|
+
function hashConvexQuery(queryKey) {
|
|
1789
|
+
const [, funcName, args] = queryKey;
|
|
1790
|
+
return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`;
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Create stable hash for Convex action keys.
|
|
1794
|
+
* Uses Convex's JSON serialization for consistent argument hashing.
|
|
1795
|
+
*/
|
|
1796
|
+
function hashConvexAction(queryKey) {
|
|
1797
|
+
const [, funcName, args] = queryKey;
|
|
1798
|
+
return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
//#endregion
|
|
1802
|
+
//#region src/internal/hash.ts
|
|
1803
|
+
/**
|
|
1804
|
+
* Create a hash function for TanStack Query that handles Convex keys.
|
|
1805
|
+
*/
|
|
1806
|
+
function createHashFn(fallback = hashKey) {
|
|
1807
|
+
return (queryKey) => {
|
|
1808
|
+
if (isConvexQuery(queryKey)) return hashConvexQuery(queryKey);
|
|
1809
|
+
if (isConvexAction$1(queryKey)) return hashConvexAction(queryKey);
|
|
1810
|
+
return fallback(queryKey);
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region src/solid/client.ts
|
|
1816
|
+
/**
|
|
1817
|
+
* ConvexQueryClient - Real-time subscription bridge for TanStack Query + Convex (SolidJS)
|
|
1818
|
+
*
|
|
1819
|
+
* ## Why This Exists
|
|
1820
|
+
*
|
|
1821
|
+
* TanStack Query is request-based (fetch once, cache, refetch on stale).
|
|
1822
|
+
* Convex is subscription-based (WebSocket push updates in real-time).
|
|
1823
|
+
*
|
|
1824
|
+
* This client bridges the two by:
|
|
1825
|
+
* 1. Listening to TanStack Query cache events (query added/removed)
|
|
1826
|
+
* 2. Creating Convex WebSocket subscriptions for each active query
|
|
1827
|
+
* 3. Pushing Convex updates into TanStack Query cache
|
|
1828
|
+
*
|
|
1829
|
+
* ## Architecture
|
|
1830
|
+
*
|
|
1831
|
+
* ```
|
|
1832
|
+
* useConvexQuery (hook)
|
|
1833
|
+
* |
|
|
1834
|
+
* v
|
|
1835
|
+
* TanStack Query Cache <---- ConvexQueryClient listens to cache events
|
|
1836
|
+
* | |
|
|
1837
|
+
* | v
|
|
1838
|
+
* | Convex WebSocket subscriptions
|
|
1839
|
+
* | |
|
|
1840
|
+
* | v
|
|
1841
|
+
* +---------------------- Real-time updates pushed to cache
|
|
1842
|
+
* ```
|
|
1843
|
+
*
|
|
1844
|
+
* ## Subscription Lifecycle
|
|
1845
|
+
*
|
|
1846
|
+
* WebSocket subscriptions are decoupled from cache retention:
|
|
1847
|
+
* - Subscribe when query has observers (component mounted)
|
|
1848
|
+
* - Unsubscribe immediately when last observer removed (component unmounted)
|
|
1849
|
+
* - Cache data persists for gcTime (default 5 min) for instant back-navigation
|
|
1850
|
+
* - On remount: show cached data instantly, re-subscribe for fresh updates
|
|
1851
|
+
*
|
|
1852
|
+
* ## SSR Support
|
|
1853
|
+
*
|
|
1854
|
+
* On server: Uses ConvexHttpClient for one-shot queries (no WebSocket).
|
|
1855
|
+
* On client: Uses ConvexClient with WebSocket subscriptions.
|
|
1856
|
+
*
|
|
1857
|
+
* @module
|
|
1858
|
+
*/
|
|
1859
|
+
const isServer = typeof window === "undefined";
|
|
1860
|
+
/**
|
|
1861
|
+
* Check if query is marked as skipped (used when auth required but not authenticated).
|
|
1862
|
+
* Skipped queries have 'skip' as the third element instead of args.
|
|
1863
|
+
*/
|
|
1864
|
+
function isConvexSkipped(queryKey) {
|
|
1865
|
+
return queryKey.length >= 2 && ["convexQuery", "convexAction"].includes(queryKey[0]) && queryKey[2] === "skip";
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Check if query key is for a Convex action function.
|
|
1869
|
+
* Format: ['convexAction', 'namespace:functionName', { args }]
|
|
1870
|
+
*/
|
|
1871
|
+
function isConvexAction(queryKey) {
|
|
1872
|
+
return queryKey.length >= 2 && queryKey[0] === "convexAction";
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Bridges TanStack Query with Convex real-time subscriptions (SolidJS).
|
|
1876
|
+
*
|
|
1877
|
+
* Uses `ConvexClient.onUpdate()` instead of `ConvexReactClient.watchQuery()`.
|
|
1878
|
+
*/
|
|
1879
|
+
var ConvexQueryClient = class {
|
|
1880
|
+
/** Convex client for WebSocket subscriptions (client) and one-shot queries */
|
|
1881
|
+
convexClient;
|
|
1882
|
+
/**
|
|
1883
|
+
* Active WebSocket subscriptions, keyed by TanStack query hash.
|
|
1884
|
+
* Each subscription has:
|
|
1885
|
+
* - getCurrentValue: getter for latest query result
|
|
1886
|
+
* - unsubscribe: cleanup function to remove the subscription
|
|
1887
|
+
* - queryKey: original query key for cache updates
|
|
1888
|
+
* - lastError: most recent error from onError callback
|
|
1889
|
+
*/
|
|
1890
|
+
subscriptions;
|
|
1891
|
+
/** Cleanup function for QueryCache subscription */
|
|
1892
|
+
unsubscribe;
|
|
1893
|
+
/**
|
|
1894
|
+
* Pending unsubscribes with timeout IDs.
|
|
1895
|
+
* Used to debounce unsubscribe/subscribe cycles.
|
|
1896
|
+
*/
|
|
1897
|
+
pendingUnsubscribes = /* @__PURE__ */ new Map();
|
|
1898
|
+
/** HTTP client for SSR queries (no WebSocket on server) */
|
|
1899
|
+
serverHttpClient;
|
|
1900
|
+
/** TanStack QueryClient reference */
|
|
1901
|
+
_queryClient;
|
|
1902
|
+
/** SSR query mode: 'consistent' guarantees same timestamp, 'inconsistent' is faster */
|
|
1903
|
+
ssrQueryMode;
|
|
1904
|
+
/** Auth store for checking auth state */
|
|
1905
|
+
authStore;
|
|
1906
|
+
/** Delay before unsubscribing when query has no observers */
|
|
1907
|
+
unsubscribeDelay;
|
|
1908
|
+
/** Payload transformer used across request/response boundaries. */
|
|
1909
|
+
transformer;
|
|
1910
|
+
/** Stored URL for creating HTTP client on server */
|
|
1911
|
+
convexUrl;
|
|
1912
|
+
/** Runtime-safe accessor for pending unsubscribe map (defensive for HMR edge cases) */
|
|
1913
|
+
getPendingUnsubscribesMap() {
|
|
1914
|
+
const self = this;
|
|
1915
|
+
if (!self.pendingUnsubscribes) self.pendingUnsubscribes = /* @__PURE__ */ new Map();
|
|
1916
|
+
return self.pendingUnsubscribes;
|
|
1917
|
+
}
|
|
1918
|
+
/** Cancel a pending delayed unsubscribe for a query hash. */
|
|
1919
|
+
cancelPendingUnsubscribe(queryHash) {
|
|
1920
|
+
const pendingUnsubscribes = this.getPendingUnsubscribesMap();
|
|
1921
|
+
const timeoutId = pendingUnsubscribes.get(queryHash);
|
|
1922
|
+
if (!timeoutId) return;
|
|
1923
|
+
clearTimeout(timeoutId);
|
|
1924
|
+
pendingUnsubscribes.delete(queryHash);
|
|
1925
|
+
}
|
|
1926
|
+
/** Unsubscribe a live Convex subscription (if present) and remove it from the subscription map. */
|
|
1927
|
+
unsubscribeQueryByHash(queryHash) {
|
|
1928
|
+
const sub = this.subscriptions[queryHash];
|
|
1929
|
+
if (!sub) return;
|
|
1930
|
+
sub.unsubscribe();
|
|
1931
|
+
delete this.subscriptions[queryHash];
|
|
1932
|
+
}
|
|
1933
|
+
/** Update auth store (for HMR where store may reset) */
|
|
1934
|
+
updateAuthStore(authStore) {
|
|
1935
|
+
this.authStore = authStore;
|
|
1936
|
+
}
|
|
1937
|
+
/** Get current auth state from store */
|
|
1938
|
+
getAuthState() {
|
|
1939
|
+
if (!this.authStore) return;
|
|
1940
|
+
return {
|
|
1941
|
+
isLoading: this.authStore.get("isLoading"),
|
|
1942
|
+
isAuthenticated: this.authStore.get("isAuthenticated"),
|
|
1943
|
+
onUnauthorized: this.authStore.get("onQueryUnauthorized"),
|
|
1944
|
+
isUnauthorized: this.authStore.get("isUnauthorized")
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Check if subscription should be skipped due to auth state.
|
|
1949
|
+
* Needed for useSuspenseQuery which ignores enabled: false.
|
|
1950
|
+
*/
|
|
1951
|
+
shouldSkipSubscription(authType) {
|
|
1952
|
+
if (!authType || !this.authStore) return false;
|
|
1953
|
+
const authState = this.getAuthState();
|
|
1954
|
+
if (authState?.isLoading) return true;
|
|
1955
|
+
if (authType === "required" && !authState?.isAuthenticated) return true;
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
/** Get QueryClient, throwing if not connected */
|
|
1959
|
+
get queryClient() {
|
|
1960
|
+
if (!this._queryClient) throw new Error("ConvexQueryClient not connected to TanStack QueryClient.");
|
|
1961
|
+
return this._queryClient;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Create a ConvexQueryClient.
|
|
1965
|
+
*
|
|
1966
|
+
* @param client - Convex URL string or existing ConvexClient
|
|
1967
|
+
* @param options - Configuration options
|
|
1968
|
+
*/
|
|
1969
|
+
constructor(client, options = {}) {
|
|
1970
|
+
if (typeof client === "string") {
|
|
1971
|
+
this.convexClient = new ConvexClient(client);
|
|
1972
|
+
this.convexUrl = client;
|
|
1973
|
+
} else {
|
|
1974
|
+
this.convexClient = client;
|
|
1975
|
+
this.convexUrl = client.client.url;
|
|
1976
|
+
}
|
|
1977
|
+
this.ssrQueryMode = options.dangerouslyUseInconsistentQueriesDuringSSR ? "inconsistent" : "consistent";
|
|
1978
|
+
this.subscriptions = {};
|
|
1979
|
+
this.authStore = options.authStore;
|
|
1980
|
+
this.unsubscribeDelay = options.unsubscribeDelay ?? 3e3;
|
|
1981
|
+
this.transformer = getTransformer(options.transformer);
|
|
1982
|
+
if (options.queryClient) {
|
|
1983
|
+
this._queryClient = options.queryClient;
|
|
1984
|
+
this.unsubscribe = this.subscribeInner(options.queryClient.getQueryCache());
|
|
1985
|
+
}
|
|
1986
|
+
if (isServer) this.serverHttpClient = new ConvexHttpClient(this.convexUrl, { fetch: options.serverFetch });
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Connect to TanStack QueryClient.
|
|
1990
|
+
* Starts listening to cache events for subscription management.
|
|
1991
|
+
*/
|
|
1992
|
+
connect(queryClient) {
|
|
1993
|
+
if (this._queryClient === queryClient && this.unsubscribe) return;
|
|
1994
|
+
if (this.unsubscribe) this.unsubscribe();
|
|
1995
|
+
this._queryClient = queryClient;
|
|
1996
|
+
this.unsubscribe = this.subscribeInner(queryClient.getQueryCache());
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Clean up all subscriptions.
|
|
2000
|
+
* Call this when the client is no longer needed.
|
|
2001
|
+
*/
|
|
2002
|
+
destroy() {
|
|
2003
|
+
this.unsubscribe?.();
|
|
2004
|
+
for (const timeoutId of this.getPendingUnsubscribesMap().values()) clearTimeout(timeoutId);
|
|
2005
|
+
this.getPendingUnsubscribesMap().clear();
|
|
2006
|
+
for (const sub of Object.values(this.subscriptions)) sub.unsubscribe();
|
|
2007
|
+
this.subscriptions = {};
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Unsubscribe from all auth-required queries.
|
|
2011
|
+
* Call before logout to prevent UNAUTHORIZED errors during session invalidation.
|
|
2012
|
+
*/
|
|
2013
|
+
unsubscribeAuthQueries() {
|
|
2014
|
+
for (const queryHash of Object.keys(this.subscriptions)) if ((this.queryClient.getQueryCache().get(queryHash)?.meta)?.authType === "required") {
|
|
2015
|
+
this.cancelPendingUnsubscribe(queryHash);
|
|
2016
|
+
this.unsubscribeQueryByHash(queryHash);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Batch update all subscriptions.
|
|
2021
|
+
* Called internally when Convex client reconnects.
|
|
2022
|
+
*/
|
|
2023
|
+
onUpdate = () => {
|
|
2024
|
+
notifyManager.batch(() => {
|
|
2025
|
+
for (const key of Object.keys(this.subscriptions)) this.onUpdateQueryKeyHash(key);
|
|
2026
|
+
});
|
|
2027
|
+
};
|
|
2028
|
+
/**
|
|
2029
|
+
* Handle Convex subscription update for a specific query.
|
|
2030
|
+
* Reads latest value from getCurrentValue and updates TanStack cache.
|
|
2031
|
+
*
|
|
2032
|
+
* @param queryHash - TanStack query hash identifying the query
|
|
2033
|
+
*/
|
|
2034
|
+
onUpdateQueryKeyHash(queryHash) {
|
|
2035
|
+
const subscription = this.subscriptions[queryHash];
|
|
2036
|
+
if (!subscription) throw new Error(`Internal ConvexQueryClient error: onUpdateQueryKeyHash called for ${queryHash}`);
|
|
2037
|
+
const query = this.queryClient.getQueryCache().get(queryHash);
|
|
2038
|
+
if (!query) return;
|
|
2039
|
+
const { queryKey, getCurrentValue, lastError } = subscription;
|
|
2040
|
+
let result;
|
|
2041
|
+
if (lastError !== void 0) result = {
|
|
2042
|
+
ok: false,
|
|
2043
|
+
error: lastError
|
|
2044
|
+
};
|
|
2045
|
+
else result = {
|
|
2046
|
+
ok: true,
|
|
2047
|
+
value: getCurrentValue()
|
|
2048
|
+
};
|
|
2049
|
+
if (result.ok) {
|
|
2050
|
+
const existingData = this.queryClient.getQueryData(queryKey);
|
|
2051
|
+
if (result.value !== null && result.value !== void 0 || !(existingData !== null && existingData !== void 0)) this.queryClient.setQueryData(queryKey, this.transformer.output.deserialize(result.value));
|
|
2052
|
+
} else {
|
|
2053
|
+
const { error } = result;
|
|
2054
|
+
const authState = this.getAuthState();
|
|
2055
|
+
const meta = query.meta;
|
|
2056
|
+
const isUnauthorized = authState?.isUnauthorized(error) ?? false;
|
|
2057
|
+
if (isUnauthorized && meta?.skipUnauth) {
|
|
2058
|
+
this.queryClient.setQueryData(queryKey, this.transformer.output.deserialize(null));
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
query.setState({
|
|
2062
|
+
error,
|
|
2063
|
+
errorUpdateCount: query.state.errorUpdateCount + 1,
|
|
2064
|
+
errorUpdatedAt: Date.now(),
|
|
2065
|
+
fetchFailureCount: query.state.fetchFailureCount + 1,
|
|
2066
|
+
fetchFailureReason: error,
|
|
2067
|
+
fetchStatus: "idle",
|
|
2068
|
+
status: "error"
|
|
2069
|
+
}, { meta: "set by ConvexQueryClient" });
|
|
2070
|
+
if (isUnauthorized && authState?.isAuthenticated) {
|
|
2071
|
+
const [, funcName] = queryKey;
|
|
2072
|
+
authState.onUnauthorized({ queryName: funcName });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Subscribe to TanStack QueryCache events.
|
|
2078
|
+
* Creates/removes Convex WebSocket subscriptions as queries are added/removed.
|
|
2079
|
+
*
|
|
2080
|
+
* @param queryCache - TanStack QueryCache to subscribe to
|
|
2081
|
+
* @returns Cleanup function to unsubscribe
|
|
2082
|
+
*/
|
|
2083
|
+
subscribeInner(queryCache) {
|
|
2084
|
+
if (isServer) return () => {};
|
|
2085
|
+
return queryCache.subscribe((event) => {
|
|
2086
|
+
if (!isConvexQuery(event.query.queryKey)) return;
|
|
2087
|
+
if (isConvexSkipped(event.query.queryKey)) return;
|
|
2088
|
+
switch (event.type) {
|
|
2089
|
+
case "removed":
|
|
2090
|
+
this.cancelPendingUnsubscribe(event.query.queryHash);
|
|
2091
|
+
this.unsubscribeQueryByHash(event.query.queryHash);
|
|
2092
|
+
break;
|
|
2093
|
+
case "added": {
|
|
2094
|
+
const meta = event.query.meta;
|
|
2095
|
+
if (meta?.subscribe === false) break;
|
|
2096
|
+
const [, funcName, args] = event.query.queryKey;
|
|
2097
|
+
if (event.query.getObserversCount() === 0) break;
|
|
2098
|
+
if (this.shouldSkipSubscription(meta?.authType)) break;
|
|
2099
|
+
this.createSubscription(event.query.queryHash, funcName, args, event.query.queryKey);
|
|
2100
|
+
break;
|
|
2101
|
+
}
|
|
2102
|
+
case "observerAdded": {
|
|
2103
|
+
this.cancelPendingUnsubscribe(event.query.queryHash);
|
|
2104
|
+
if (this.subscriptions[event.query.queryHash]) break;
|
|
2105
|
+
if (event.query.options.enabled === false) break;
|
|
2106
|
+
const meta = event.query.meta;
|
|
2107
|
+
if (meta?.subscribe === false) break;
|
|
2108
|
+
const [, funcName, args] = event.query.queryKey;
|
|
2109
|
+
if (this.shouldSkipSubscription(meta?.authType)) break;
|
|
2110
|
+
this.createSubscription(event.query.queryHash, funcName, args, event.query.queryKey);
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
case "observerRemoved":
|
|
2114
|
+
if (event.query.getObserversCount() === 0 && this.subscriptions[event.query.queryHash]) {
|
|
2115
|
+
const queryHash = event.query.queryHash;
|
|
2116
|
+
const timeoutId = setTimeout(() => {
|
|
2117
|
+
this.getPendingUnsubscribesMap().delete(queryHash);
|
|
2118
|
+
if (event.query.getObserversCount() === 0) this.unsubscribeQueryByHash(queryHash);
|
|
2119
|
+
}, this.unsubscribeDelay);
|
|
2120
|
+
this.getPendingUnsubscribesMap().set(queryHash, timeoutId);
|
|
2121
|
+
}
|
|
2122
|
+
break;
|
|
2123
|
+
case "observerResultsUpdated": break;
|
|
2124
|
+
case "updated":
|
|
2125
|
+
if (event.action.type === "setState" && event.action.setStateOptions?.meta === "set by ConvexQueryClient") break;
|
|
2126
|
+
break;
|
|
2127
|
+
case "observerOptionsUpdated": {
|
|
2128
|
+
const isDisabled = event.query.options.enabled === false;
|
|
2129
|
+
const isSubscribed = !!this.subscriptions[event.query.queryHash];
|
|
2130
|
+
if (isDisabled && isSubscribed) {
|
|
2131
|
+
this.cancelPendingUnsubscribe(event.query.queryHash);
|
|
2132
|
+
this.unsubscribeQueryByHash(event.query.queryHash);
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
if (isSubscribed || isDisabled) break;
|
|
2136
|
+
const meta = event.query.meta;
|
|
2137
|
+
if (meta?.subscribe === false) break;
|
|
2138
|
+
const [, funcName, args] = event.query.queryKey;
|
|
2139
|
+
if (this.shouldSkipSubscription(meta?.authType)) break;
|
|
2140
|
+
this.createSubscription(event.query.queryHash, funcName, args, event.query.queryKey);
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Create a WebSocket subscription via ConvexClient.onUpdate().
|
|
2148
|
+
* Stores the subscription in the subscriptions map.
|
|
2149
|
+
*/
|
|
2150
|
+
createSubscription(queryHash, funcName, args, queryKey) {
|
|
2151
|
+
this.subscriptions[queryHash] = {
|
|
2152
|
+
getCurrentValue: () => void 0,
|
|
2153
|
+
unsubscribe: () => {},
|
|
2154
|
+
queryKey,
|
|
2155
|
+
lastError: void 0
|
|
2156
|
+
};
|
|
2157
|
+
const unsub = this.convexClient.onUpdate(funcName, this.transformer.input.serialize(args), () => {
|
|
2158
|
+
if (this.subscriptions[queryHash]) this.subscriptions[queryHash].lastError = void 0;
|
|
2159
|
+
this.onUpdateQueryKeyHash(queryHash);
|
|
2160
|
+
}, (error) => {
|
|
2161
|
+
if (this.subscriptions[queryHash]) this.subscriptions[queryHash].lastError = error;
|
|
2162
|
+
this.onUpdateQueryKeyHash(queryHash);
|
|
2163
|
+
});
|
|
2164
|
+
this.subscriptions[queryHash].getCurrentValue = unsub.getCurrentValue.bind(unsub);
|
|
2165
|
+
this.subscriptions[queryHash].unsubscribe = () => unsub();
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Create default queryFn for TanStack QueryClient.
|
|
2169
|
+
*
|
|
2170
|
+
* Handles:
|
|
2171
|
+
* - Convex queries and actions
|
|
2172
|
+
* - Auth checking (throws CRPCClientError if unauthorized)
|
|
2173
|
+
* - SSR via HTTP client
|
|
2174
|
+
* - Client via WebSocket client
|
|
2175
|
+
*
|
|
2176
|
+
* @param otherFetch - Fallback queryFn for non-Convex queries
|
|
2177
|
+
* @returns QueryFunction compatible with TanStack Query
|
|
2178
|
+
*/
|
|
2179
|
+
queryFn(otherFetch = throwBecauseNotConvexQuery) {
|
|
2180
|
+
return async (context) => {
|
|
2181
|
+
const { queryKey, meta: rawMeta } = context;
|
|
2182
|
+
const meta = rawMeta;
|
|
2183
|
+
if (isConvexSkipped(queryKey)) throw new Error("Skipped query should not actually run, should { enabled: false }");
|
|
2184
|
+
if (isConvexQuery(queryKey)) {
|
|
2185
|
+
const [, funcName, args] = queryKey;
|
|
2186
|
+
const wireArgs = this.transformer.input.serialize(args);
|
|
2187
|
+
const skipUnauth = meta?.skipUnauth ?? false;
|
|
2188
|
+
if (meta?.authType === "required" && !isServer && this.authStore) {
|
|
2189
|
+
const authState = this.getAuthState();
|
|
2190
|
+
if (authState && !authState.isLoading && !authState.isAuthenticated) {
|
|
2191
|
+
if (skipUnauth) return null;
|
|
2192
|
+
authState.onUnauthorized({ queryName: funcName });
|
|
2193
|
+
throw new CRPCClientError({
|
|
2194
|
+
code: "UNAUTHORIZED",
|
|
2195
|
+
functionName: funcName
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
try {
|
|
2200
|
+
if (isServer) {
|
|
2201
|
+
if (this.ssrQueryMode === "consistent") return this.transformer.output.deserialize(await this.serverHttpClient.consistentQuery(funcName, wireArgs));
|
|
2202
|
+
return this.transformer.output.deserialize(await this.serverHttpClient.query(funcName, wireArgs));
|
|
2203
|
+
}
|
|
2204
|
+
return this.transformer.output.deserialize(await this.convexClient.query(funcName, wireArgs));
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
if (skipUnauth && defaultIsUnauthorized(error)) return null;
|
|
2207
|
+
throw error;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
if (isConvexAction(queryKey)) {
|
|
2211
|
+
const [, funcName, args] = queryKey;
|
|
2212
|
+
const wireArgs = this.transformer.input.serialize(args);
|
|
2213
|
+
const skipUnauth = meta?.skipUnauth ?? false;
|
|
2214
|
+
if (meta?.authType === "required" && !isServer && this.authStore) {
|
|
2215
|
+
const authState = this.getAuthState();
|
|
2216
|
+
if (authState && !authState.isLoading && !authState.isAuthenticated) {
|
|
2217
|
+
if (skipUnauth) return null;
|
|
2218
|
+
authState.onUnauthorized({ queryName: funcName });
|
|
2219
|
+
throw new CRPCClientError({
|
|
2220
|
+
code: "UNAUTHORIZED",
|
|
2221
|
+
functionName: funcName
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
try {
|
|
2226
|
+
if (isServer) return this.transformer.output.deserialize(await this.serverHttpClient.action(funcName, wireArgs));
|
|
2227
|
+
return this.transformer.output.deserialize(await this.convexClient.action(funcName, wireArgs));
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
if (skipUnauth && defaultIsUnauthorized(error)) return null;
|
|
2230
|
+
throw error;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return otherFetch(context);
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Create hash function for TanStack QueryClient.
|
|
2238
|
+
*
|
|
2239
|
+
* Uses Convex-specific hashing for Convex queries to ensure
|
|
2240
|
+
* consistent cache keys across serialization.
|
|
2241
|
+
*
|
|
2242
|
+
* @param fallback - Fallback hash function for non-Convex queries
|
|
2243
|
+
* @returns Hash function compatible with TanStack Query
|
|
2244
|
+
*/
|
|
2245
|
+
hashFn(fallback) {
|
|
2246
|
+
return createHashFn(fallback);
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
/** Default fallback queryFn that throws for non-Convex queries */
|
|
2250
|
+
function throwBecauseNotConvexQuery(context) {
|
|
2251
|
+
throw new Error(`Query key is not for a Convex Query: ${context.queryKey}`);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
//#endregion
|
|
2255
|
+
//#region src/solid/convex-auth-provider.tsx
|
|
2256
|
+
/** @jsxImportSource solid-js */
|
|
2257
|
+
/**
|
|
2258
|
+
* Unified Convex + Better Auth provider for SolidJS
|
|
2259
|
+
*
|
|
2260
|
+
* Port of the React ConvexAuthProvider to SolidJS.
|
|
2261
|
+
* Handles token sync and auth callbacks.
|
|
2262
|
+
*/
|
|
2263
|
+
const defaultMutationHandler = () => {
|
|
2264
|
+
throw new CRPCClientError({
|
|
2265
|
+
code: "UNAUTHORIZED",
|
|
2266
|
+
functionName: "mutation"
|
|
2267
|
+
});
|
|
2268
|
+
};
|
|
2269
|
+
const hasActiveSessionData = (session) => {
|
|
2270
|
+
if (!session || typeof session !== "object") return false;
|
|
2271
|
+
return Boolean(session.session);
|
|
2272
|
+
};
|
|
2273
|
+
/**
|
|
2274
|
+
* Unified auth provider for Convex + Better Auth (SolidJS).
|
|
2275
|
+
* Handles token sync and auth callbacks.
|
|
2276
|
+
*
|
|
2277
|
+
* Structure: AuthProvider wraps ConvexAuthProviderInner so that
|
|
2278
|
+
* useAuthStore() is available when creating fetchAccessToken.
|
|
2279
|
+
*/
|
|
2280
|
+
function ConvexAuthProvider(props) {
|
|
2281
|
+
useOTTHandler(props.authClient);
|
|
2282
|
+
const tokenValues = () => ({
|
|
2283
|
+
expiresAt: props.initialToken ? decodeJwtExp(props.initialToken) : null,
|
|
2284
|
+
token: props.initialToken ?? null
|
|
2285
|
+
});
|
|
2286
|
+
return createComponent(AuthProvider, {
|
|
2287
|
+
get initialValues() {
|
|
2288
|
+
return tokenValues();
|
|
2289
|
+
},
|
|
2290
|
+
get isUnauthorized() {
|
|
2291
|
+
return props.isUnauthorized ?? defaultIsUnauthorized;
|
|
2292
|
+
},
|
|
2293
|
+
get onMutationUnauthorized() {
|
|
2294
|
+
return props.onMutationUnauthorized ?? defaultMutationHandler;
|
|
2295
|
+
},
|
|
2296
|
+
get onQueryUnauthorized() {
|
|
2297
|
+
return props.onQueryUnauthorized ?? (() => {});
|
|
2298
|
+
},
|
|
2299
|
+
get children() {
|
|
2300
|
+
return createComponent(ConvexAuthProviderInner, {
|
|
2301
|
+
get authClient() {
|
|
2302
|
+
return props.authClient;
|
|
2303
|
+
},
|
|
2304
|
+
get client() {
|
|
2305
|
+
return props.client;
|
|
2306
|
+
},
|
|
2307
|
+
get children() {
|
|
2308
|
+
return props.children;
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Inner provider that has access to AuthStore via useAuthStore().
|
|
2316
|
+
* Creates fetchAccessToken and passes it through context.
|
|
2317
|
+
*/
|
|
2318
|
+
function ConvexAuthProviderInner(props) {
|
|
2319
|
+
const authStore = useAuthStore();
|
|
2320
|
+
const sessionAccessor = props.authClient.useSession();
|
|
2321
|
+
let pendingTokenPromise = null;
|
|
2322
|
+
createEffect(() => {
|
|
2323
|
+
const sessionState = sessionAccessor();
|
|
2324
|
+
const session = sessionState.data;
|
|
2325
|
+
const isPending = sessionState.isPending;
|
|
2326
|
+
if (!hasActiveSessionData(session) && !isPending) {
|
|
2327
|
+
authStore.set("token", null);
|
|
2328
|
+
authStore.set("expiresAt", null);
|
|
2329
|
+
authStore.set("isAuthenticated", false);
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
const fetchAccessToken = async ({ forceRefreshToken = false } = {}) => {
|
|
2333
|
+
const sessionState = sessionAccessor();
|
|
2334
|
+
const currentSession = sessionState.data;
|
|
2335
|
+
const currentIsPending = sessionState.isPending;
|
|
2336
|
+
if (!hasActiveSessionData(currentSession)) {
|
|
2337
|
+
if (!currentIsPending) {
|
|
2338
|
+
authStore.set("token", null);
|
|
2339
|
+
authStore.set("expiresAt", null);
|
|
2340
|
+
}
|
|
2341
|
+
return authStore.get("token");
|
|
2342
|
+
}
|
|
2343
|
+
const cachedToken = authStore.get("token");
|
|
2344
|
+
const expiresAt = authStore.get("expiresAt");
|
|
2345
|
+
const timeRemaining = expiresAt ? expiresAt - Date.now() : 0;
|
|
2346
|
+
if (!forceRefreshToken && cachedToken && expiresAt && timeRemaining >= 6e4) return cachedToken;
|
|
2347
|
+
if (!forceRefreshToken && pendingTokenPromise) return pendingTokenPromise;
|
|
2348
|
+
const fetchOptions = { throw: false };
|
|
2349
|
+
if (cachedToken && decodeJwtExp(cachedToken) === null) fetchOptions.headers = { Authorization: `Bearer ${cachedToken}` };
|
|
2350
|
+
pendingTokenPromise = props.authClient.convex.token({ fetchOptions }).then((result) => {
|
|
2351
|
+
const jwt = result.data?.token || null;
|
|
2352
|
+
if (jwt) {
|
|
2353
|
+
const exp = decodeJwtExp(jwt);
|
|
2354
|
+
authStore.set("token", jwt);
|
|
2355
|
+
authStore.set("expiresAt", exp);
|
|
2356
|
+
return jwt;
|
|
2357
|
+
}
|
|
2358
|
+
authStore.set("token", null);
|
|
2359
|
+
authStore.set("expiresAt", null);
|
|
2360
|
+
return null;
|
|
2361
|
+
}).catch((error) => {
|
|
2362
|
+
authStore.set("token", null);
|
|
2363
|
+
authStore.set("expiresAt", null);
|
|
2364
|
+
console.error("[fetchAccessToken] error", error);
|
|
2365
|
+
return null;
|
|
2366
|
+
}).finally(() => {
|
|
2367
|
+
pendingTokenPromise = null;
|
|
2368
|
+
});
|
|
2369
|
+
return pendingTokenPromise;
|
|
2370
|
+
};
|
|
2371
|
+
const useAuth = () => {
|
|
2372
|
+
const sessionState = sessionAccessor();
|
|
2373
|
+
const hasSession = hasActiveSessionData(sessionState.data);
|
|
2374
|
+
const sessionMissing = !hasSession && !sessionState.isPending;
|
|
2375
|
+
const token = authStore.get("token");
|
|
2376
|
+
return {
|
|
2377
|
+
isLoading: sessionState.isPending && !token,
|
|
2378
|
+
isAuthenticated: sessionMissing ? false : hasSession || token !== null,
|
|
2379
|
+
fetchAccessToken
|
|
2380
|
+
};
|
|
2381
|
+
};
|
|
2382
|
+
return createComponent(FetchAccessTokenContext.Provider, {
|
|
2383
|
+
value: fetchAccessToken,
|
|
2384
|
+
get children() {
|
|
2385
|
+
return createComponent(ConvexProviderWithAuth, {
|
|
2386
|
+
get client() {
|
|
2387
|
+
return props.client;
|
|
2388
|
+
},
|
|
2389
|
+
useAuth,
|
|
2390
|
+
get children() {
|
|
2391
|
+
return createComponent(AuthStateSync, { get children() {
|
|
2392
|
+
return props.children;
|
|
2393
|
+
} });
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Syncs auth state from useConvexAuth() to the auth store.
|
|
2401
|
+
* MUST be inside ConvexProviderWithAuth to access useConvexAuth().
|
|
2402
|
+
*
|
|
2403
|
+
* Defensive isLoading computation handles SSR hydration race:
|
|
2404
|
+
* 1. SSR sets token from cookie
|
|
2405
|
+
* 2. Client hydrates
|
|
2406
|
+
* 3. Better Auth's useSession() briefly returns null before loading cookie
|
|
2407
|
+
* 4. Convex sets isConvexAuthenticated = false (no auth to wait for)
|
|
2408
|
+
* 5. Without defensive check, we'd sync { isLoading: false, isAuthenticated: false }
|
|
2409
|
+
* 6. Queries would throw UNAUTHORIZED before token is validated
|
|
2410
|
+
*/
|
|
2411
|
+
function AuthStateSync(props) {
|
|
2412
|
+
const convexAuth = useConvexAuth();
|
|
2413
|
+
const authStore = useAuthStore();
|
|
2414
|
+
createEffect(() => {
|
|
2415
|
+
const hasTokenButNotAuth = !!authStore.get("token") && !convexAuth.isAuthenticated;
|
|
2416
|
+
const isLoading = convexAuth.isLoading || hasTokenButNotAuth;
|
|
2417
|
+
authStore.set("isLoading", isLoading);
|
|
2418
|
+
authStore.set("isAuthenticated", convexAuth.isAuthenticated);
|
|
2419
|
+
});
|
|
2420
|
+
return props.children;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Handles cross-domain one-time token (OTT) verification.
|
|
2424
|
+
*/
|
|
2425
|
+
function useOTTHandler(authClient) {
|
|
2426
|
+
onMount(async () => {
|
|
2427
|
+
if (typeof window === "undefined" || !window.location?.href) return;
|
|
2428
|
+
const url = new URL(window.location.href);
|
|
2429
|
+
const token = url.searchParams.get("ott");
|
|
2430
|
+
if (token) {
|
|
2431
|
+
const authClientWithCrossDomain = authClient;
|
|
2432
|
+
url.searchParams.delete("ott");
|
|
2433
|
+
window.history.replaceState({}, "", url);
|
|
2434
|
+
const session = (await authClientWithCrossDomain.crossDomain.oneTimeToken.verify({ token })).data?.session;
|
|
2435
|
+
if (session) {
|
|
2436
|
+
await authClient.getSession({ fetchOptions: { headers: { Authorization: `Bearer ${session.token}` } } });
|
|
2437
|
+
authClientWithCrossDomain.updateSession();
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
//#endregion
|
|
2444
|
+
//#region src/solid/singleton.ts
|
|
2445
|
+
const globalStore = globalThis;
|
|
2446
|
+
/** Get/create QueryClient singleton (fresh on SSR, singleton on client) */
|
|
2447
|
+
const getQueryClientSingleton = (factory, symbolKey = "convex.queryClient") => {
|
|
2448
|
+
const key = Symbol.for(symbolKey);
|
|
2449
|
+
if (typeof window === "undefined") return factory();
|
|
2450
|
+
if (!globalStore[key]) globalStore[key] = factory();
|
|
2451
|
+
return globalStore[key];
|
|
2452
|
+
};
|
|
2453
|
+
/** Get/create ConvexQueryClient singleton (fresh on SSR, singleton on client) */
|
|
2454
|
+
const getConvexQueryClientSingleton = ({ authStore, convex, queryClient, symbolKey = "convex.convexQueryClient", unsubscribeDelay, transformer }) => {
|
|
2455
|
+
const key = Symbol.for(symbolKey);
|
|
2456
|
+
const isServer = typeof window === "undefined";
|
|
2457
|
+
let client;
|
|
2458
|
+
if (isServer) client = new ConvexQueryClient(convex, {
|
|
2459
|
+
authStore,
|
|
2460
|
+
unsubscribeDelay,
|
|
2461
|
+
transformer
|
|
2462
|
+
});
|
|
2463
|
+
else {
|
|
2464
|
+
if (globalStore[key]) globalStore[key].updateAuthStore(authStore);
|
|
2465
|
+
else globalStore[key] = new ConvexQueryClient(convex, {
|
|
2466
|
+
authStore,
|
|
2467
|
+
unsubscribeDelay,
|
|
2468
|
+
transformer
|
|
2469
|
+
});
|
|
2470
|
+
client = globalStore[key];
|
|
2471
|
+
client.connect(queryClient);
|
|
2472
|
+
}
|
|
2473
|
+
const currentOpts = queryClient.getDefaultOptions();
|
|
2474
|
+
queryClient.setDefaultOptions({
|
|
2475
|
+
...currentOpts,
|
|
2476
|
+
queries: {
|
|
2477
|
+
...currentOpts.queries,
|
|
2478
|
+
queryFn: client.queryFn(),
|
|
2479
|
+
queryKeyHashFn: client.hashFn()
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
return client;
|
|
2483
|
+
};
|
|
2484
|
+
|
|
2485
|
+
//#endregion
|
|
2486
|
+
//#region src/solid/use-infinite-query.ts
|
|
2487
|
+
const PAGINATION_KEY_PREFIX = "__pagination__";
|
|
2488
|
+
const paginationIdStore = /* @__PURE__ */ new Map();
|
|
2489
|
+
let paginationIdCounter = 0;
|
|
2490
|
+
const getOrCreatePaginationId = (storeKey) => {
|
|
2491
|
+
const existing = paginationIdStore.get(storeKey);
|
|
2492
|
+
if (existing !== void 0) return existing;
|
|
2493
|
+
const newId = ++paginationIdCounter;
|
|
2494
|
+
paginationIdStore.set(storeKey, newId);
|
|
2495
|
+
return newId;
|
|
2496
|
+
};
|
|
2497
|
+
/** Build a unique key for recovery attempt detection */
|
|
2498
|
+
const buildRecoveryKey = (pageKeys, page0Cursor, page0UpdatedAt) => JSON.stringify({
|
|
2499
|
+
pageKeys,
|
|
2500
|
+
page0Cursor,
|
|
2501
|
+
page0UpdatedAt
|
|
2502
|
+
});
|
|
2503
|
+
/**
|
|
2504
|
+
* Hook for auto-recovering from stale cursors after WebSocket reconnection.
|
|
2505
|
+
*
|
|
2506
|
+
* When Convex WebSocket reconnects, page 0 (cursor: null) resubscribes and
|
|
2507
|
+
* gets fresh data. However, pages 1+ may have stale cursors that fail.
|
|
2508
|
+
*
|
|
2509
|
+
* This hook detects this pattern and creates a recovery page that fetches
|
|
2510
|
+
* enough items to cover the lost pages, preserving the user's scroll position.
|
|
2511
|
+
*/
|
|
2512
|
+
const useStaleCursorRecovery = ({ argsObject, combined, limit, setState, state }) => {
|
|
2513
|
+
createEffect(on([
|
|
2514
|
+
() => combined.isFetchNextPageError,
|
|
2515
|
+
() => combined._rawResults,
|
|
2516
|
+
() => state().pageKeys,
|
|
2517
|
+
() => state().queries,
|
|
2518
|
+
() => state().autoRecoveryAttempted,
|
|
2519
|
+
argsObject
|
|
2520
|
+
], () => {
|
|
2521
|
+
if (!combined.isFetchNextPageError) return;
|
|
2522
|
+
const page0Result = combined._rawResults[0];
|
|
2523
|
+
const page0Data = page0Result?.data;
|
|
2524
|
+
const page0UpdatedAt = page0Result?.dataUpdatedAt ?? 0;
|
|
2525
|
+
const hasPage0Data = page0Data !== void 0 && !page0Result?.isError;
|
|
2526
|
+
const hasSubsequentErrors = combined._rawResults.slice(1).some((q) => q?.isError && !q?.isFetching);
|
|
2527
|
+
if (!hasPage0Data || !hasSubsequentErrors || !page0Data?.continueCursor) return;
|
|
2528
|
+
const currentState = state();
|
|
2529
|
+
const recoveryKey = buildRecoveryKey(currentState.pageKeys, page0Data.continueCursor, page0UpdatedAt);
|
|
2530
|
+
if (currentState.autoRecoveryAttempted === recoveryKey) return;
|
|
2531
|
+
const erroredPageKeys = currentState.pageKeys.filter((_, i) => i > 0 && combined._rawResults[i]?.isError);
|
|
2532
|
+
const itemsToRecover = erroredPageKeys.reduce((sum, key) => {
|
|
2533
|
+
return sum + (currentState.queries[key]?.args?.limit ?? limit ?? 20);
|
|
2534
|
+
}, 0);
|
|
2535
|
+
console.warn("[Pagination] Auto-recovering from stale cursors", {
|
|
2536
|
+
erroredPages: erroredPageKeys.length,
|
|
2537
|
+
itemsToRecover
|
|
2538
|
+
});
|
|
2539
|
+
setState((prev) => ({
|
|
2540
|
+
...prev,
|
|
2541
|
+
id: prev.id,
|
|
2542
|
+
nextPageKey: 2,
|
|
2543
|
+
pageKeys: [prev.pageKeys[0], 1],
|
|
2544
|
+
queries: {
|
|
2545
|
+
[prev.pageKeys[0]]: prev.queries[prev.pageKeys[0]],
|
|
2546
|
+
1: { args: {
|
|
2547
|
+
...argsObject(),
|
|
2548
|
+
cursor: page0Data.continueCursor,
|
|
2549
|
+
limit: Math.min(itemsToRecover + (limit ?? 20), 500),
|
|
2550
|
+
__paginationId: prev.id
|
|
2551
|
+
} }
|
|
2552
|
+
},
|
|
2553
|
+
version: prev.version + 1,
|
|
2554
|
+
autoRecoveryAttempted: recoveryKey
|
|
2555
|
+
}));
|
|
2556
|
+
}));
|
|
2557
|
+
createEffect(on([() => combined.status, () => state().autoRecoveryAttempted], () => {
|
|
2558
|
+
if ((combined.status === "CanLoadMore" || combined.status === "Exhausted") && state().autoRecoveryAttempted) setState((prev) => ({
|
|
2559
|
+
...prev,
|
|
2560
|
+
autoRecoveryAttempted: void 0
|
|
2561
|
+
}));
|
|
2562
|
+
}));
|
|
2563
|
+
};
|
|
2564
|
+
/**
|
|
2565
|
+
* Internal infinite query hook using TanStack Query + convexQuery.
|
|
2566
|
+
* Each page gets:
|
|
2567
|
+
* - Convex WebSocket subscription (real-time reactivity)
|
|
2568
|
+
* - TanStack Query retry on timeout errors
|
|
2569
|
+
*
|
|
2570
|
+
* Use `useInfiniteQuery` for the public API with auth handling.
|
|
2571
|
+
*/
|
|
2572
|
+
const useInfiniteQueryInternal = (query, args, options) => {
|
|
2573
|
+
const { limit, enabled, placeholderData, ...queryOptions } = options;
|
|
2574
|
+
const safeAuth = useSafeConvexAuth();
|
|
2575
|
+
const meta = useMeta();
|
|
2576
|
+
const queryClient = useQueryClient();
|
|
2577
|
+
const prefetchedFirstPage = createMemo(() => {
|
|
2578
|
+
const serverQueryKey = [
|
|
2579
|
+
"convexQuery",
|
|
2580
|
+
getFunctionName(query),
|
|
2581
|
+
{
|
|
2582
|
+
...args,
|
|
2583
|
+
cursor: null,
|
|
2584
|
+
limit
|
|
2585
|
+
}
|
|
2586
|
+
];
|
|
2587
|
+
return queryClient.getQueryData(serverQueryKey) ?? null;
|
|
2588
|
+
});
|
|
2589
|
+
const skip = createMemo(() => !prefetchedFirstPage() && (safeAuth.isLoading || enabled === false));
|
|
2590
|
+
const getPaginationState = (key) => {
|
|
2591
|
+
const queryKey = [PAGINATION_KEY_PREFIX, key];
|
|
2592
|
+
return queryClient.getQueryData(queryKey);
|
|
2593
|
+
};
|
|
2594
|
+
const setPaginationState = (key, paginationState) => {
|
|
2595
|
+
const queryKey = [PAGINATION_KEY_PREFIX, key];
|
|
2596
|
+
queryClient.setQueryData(queryKey, paginationState);
|
|
2597
|
+
};
|
|
2598
|
+
const argsObject = createMemo(() => skip() ? {} : args);
|
|
2599
|
+
const storeKey = createMemo(() => JSON.stringify({
|
|
2600
|
+
query: getFunctionName(query),
|
|
2601
|
+
args: argsObject()
|
|
2602
|
+
}));
|
|
2603
|
+
const createInitialState = () => {
|
|
2604
|
+
const id = getOrCreatePaginationId(storeKey());
|
|
2605
|
+
return {
|
|
2606
|
+
id,
|
|
2607
|
+
nextPageKey: 1,
|
|
2608
|
+
pageKeys: skip() ? [] : [0],
|
|
2609
|
+
queries: skip() ? {} : { 0: { args: {
|
|
2610
|
+
...argsObject(),
|
|
2611
|
+
cursor: null,
|
|
2612
|
+
limit,
|
|
2613
|
+
__paginationId: id
|
|
2614
|
+
} } },
|
|
2615
|
+
version: 0
|
|
2616
|
+
};
|
|
2617
|
+
};
|
|
2618
|
+
let prevArgs = null;
|
|
2619
|
+
const computeInitialState = () => {
|
|
2620
|
+
if (skip()) return {
|
|
2621
|
+
id: 0,
|
|
2622
|
+
nextPageKey: 1,
|
|
2623
|
+
pageKeys: [],
|
|
2624
|
+
queries: {},
|
|
2625
|
+
version: 0
|
|
2626
|
+
};
|
|
2627
|
+
const existingState = getPaginationState(storeKey());
|
|
2628
|
+
if (existingState) return existingState;
|
|
2629
|
+
return createInitialState();
|
|
2630
|
+
};
|
|
2631
|
+
const [state, setLocalState] = createSignal(computeInitialState());
|
|
2632
|
+
const setState = (updater) => {
|
|
2633
|
+
setLocalState((prev) => {
|
|
2634
|
+
const newState = typeof updater === "function" ? updater(prev) : updater;
|
|
2635
|
+
setPaginationState(storeKey(), newState);
|
|
2636
|
+
return newState;
|
|
2637
|
+
});
|
|
2638
|
+
};
|
|
2639
|
+
createEffect(on([skip, storeKey], () => {
|
|
2640
|
+
const prev = prevArgs;
|
|
2641
|
+
const isFirstRun = prev === null;
|
|
2642
|
+
const currentStoreKey = storeKey();
|
|
2643
|
+
const currentSkip = skip();
|
|
2644
|
+
const argsChanged = prev !== null && (prev.storeKey !== currentStoreKey || prev.skip !== currentSkip);
|
|
2645
|
+
const skipBecameFalse = prev?.skip && !currentSkip;
|
|
2646
|
+
prevArgs = {
|
|
2647
|
+
storeKey: currentStoreKey,
|
|
2648
|
+
skip: currentSkip
|
|
2649
|
+
};
|
|
2650
|
+
if (currentSkip) return;
|
|
2651
|
+
if (isFirstRun) {
|
|
2652
|
+
setPaginationState(currentStoreKey, state());
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
if (skipBecameFalse) {
|
|
2656
|
+
const existingState = getPaginationState(currentStoreKey);
|
|
2657
|
+
if (existingState) {
|
|
2658
|
+
setLocalState(existingState);
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const newState = createInitialState();
|
|
2662
|
+
setLocalState(newState);
|
|
2663
|
+
setPaginationState(currentStoreKey, newState);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (argsChanged) {
|
|
2667
|
+
const existingState = getPaginationState(currentStoreKey);
|
|
2668
|
+
if (existingState) {
|
|
2669
|
+
setLocalState(existingState);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const newState = createInitialState();
|
|
2673
|
+
setLocalState(newState);
|
|
2674
|
+
setPaginationState(currentStoreKey, newState);
|
|
2675
|
+
}
|
|
2676
|
+
}));
|
|
2677
|
+
const tanstackQueries = createMemo(() => state().pageKeys.map((key, index) => {
|
|
2678
|
+
const pageArgs = state().queries[key]?.args;
|
|
2679
|
+
return {
|
|
2680
|
+
...convexQuery(query, pageArgs ? (({ __paginationId, ...rest }) => rest)(pageArgs) : "skip", meta),
|
|
2681
|
+
enabled: !skip() && !!state().queries[key],
|
|
2682
|
+
structuralSharing: false,
|
|
2683
|
+
...queryOptions ?? {},
|
|
2684
|
+
...index === 0 && prefetchedFirstPage() ? { initialData: prefetchedFirstPage() } : {},
|
|
2685
|
+
...index === 0 && placeholderData ? { placeholderData: {
|
|
2686
|
+
page: placeholderData,
|
|
2687
|
+
isDone: false,
|
|
2688
|
+
continueCursor: null
|
|
2689
|
+
} } : {}
|
|
2690
|
+
};
|
|
2691
|
+
}));
|
|
2692
|
+
const combined = useQueries(() => ({
|
|
2693
|
+
queries: tanstackQueries(),
|
|
2694
|
+
combine: (results) => {
|
|
2695
|
+
const allItems = [];
|
|
2696
|
+
const pages = [];
|
|
2697
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2698
|
+
let lastPage;
|
|
2699
|
+
let paginationStatus = "LoadingFirstPage";
|
|
2700
|
+
for (let i = 0; i < results.length; i++) {
|
|
2701
|
+
const pageQuery = results[i];
|
|
2702
|
+
if (pageQuery.isLoading || pageQuery.data === void 0) {
|
|
2703
|
+
paginationStatus = i === 0 ? "LoadingFirstPage" : "LoadingMore";
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
const page = pageQuery.data;
|
|
2707
|
+
lastPage = page;
|
|
2708
|
+
pages.push(page.page);
|
|
2709
|
+
for (const item of page.page) {
|
|
2710
|
+
const id = item._id || item.id;
|
|
2711
|
+
if (id && seenIds.has(id)) continue;
|
|
2712
|
+
if (id) seenIds.add(id);
|
|
2713
|
+
allItems.push(item);
|
|
2714
|
+
}
|
|
2715
|
+
paginationStatus = page.isDone ? "Exhausted" : "CanLoadMore";
|
|
2716
|
+
}
|
|
2717
|
+
const isPlaceholderData = results[0]?.isPlaceholderData ?? !!placeholderData;
|
|
2718
|
+
const isFetching = results.some((r) => r.isFetching);
|
|
2719
|
+
return {
|
|
2720
|
+
data: allItems,
|
|
2721
|
+
dataUpdatedAt: Math.max(...results.map((r) => r.dataUpdatedAt ?? 0)),
|
|
2722
|
+
lastPage,
|
|
2723
|
+
pages,
|
|
2724
|
+
status: paginationStatus,
|
|
2725
|
+
error: results.find((r) => r.isError)?.error ?? null,
|
|
2726
|
+
isError: results.some((r) => r.isError),
|
|
2727
|
+
isFetching,
|
|
2728
|
+
isFetchNextPageError: results.length > 1 && (results.at(-1)?.isError ?? false),
|
|
2729
|
+
isPlaceholderData,
|
|
2730
|
+
isRefetching: isFetching && allItems.length > 0 && !isPlaceholderData,
|
|
2731
|
+
isLoading: paginationStatus === "LoadingFirstPage",
|
|
2732
|
+
failureReason: results.find((r) => r.isError)?.error ?? null,
|
|
2733
|
+
_rawResults: results
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
}));
|
|
2737
|
+
useStaleCursorRecovery({
|
|
2738
|
+
argsObject,
|
|
2739
|
+
combined,
|
|
2740
|
+
limit,
|
|
2741
|
+
setState,
|
|
2742
|
+
state
|
|
2743
|
+
});
|
|
2744
|
+
createEffect(on([
|
|
2745
|
+
() => combined._rawResults,
|
|
2746
|
+
() => state().pageKeys,
|
|
2747
|
+
() => state().queries,
|
|
2748
|
+
argsObject
|
|
2749
|
+
], () => {
|
|
2750
|
+
for (let i = 0; i < combined._rawResults.length; i++) {
|
|
2751
|
+
const pageQuery = combined._rawResults[i];
|
|
2752
|
+
if (pageQuery.data) {
|
|
2753
|
+
const page = pageQuery.data;
|
|
2754
|
+
const pageKey = state().pageKeys[i];
|
|
2755
|
+
const pageState = state().queries[pageKey];
|
|
2756
|
+
if (page.splitCursor && pageState && !pageState.endCursor) {
|
|
2757
|
+
setState((prev) => {
|
|
2758
|
+
const currentPageState = prev.queries[pageKey];
|
|
2759
|
+
if (!currentPageState || currentPageState.endCursor) return prev;
|
|
2760
|
+
const newKey = prev.nextPageKey;
|
|
2761
|
+
const splitCursor = page.splitCursor;
|
|
2762
|
+
const splitPageArgs = {
|
|
2763
|
+
...argsObject(),
|
|
2764
|
+
cursor: splitCursor,
|
|
2765
|
+
limit: currentPageState.args.limit,
|
|
2766
|
+
__paginationId: prev.id
|
|
2767
|
+
};
|
|
2768
|
+
const pageKeyIndex = prev.pageKeys.indexOf(pageKey);
|
|
2769
|
+
const newPageKeys = [...prev.pageKeys];
|
|
2770
|
+
newPageKeys.splice(pageKeyIndex + 1, 0, newKey);
|
|
2771
|
+
return {
|
|
2772
|
+
...prev,
|
|
2773
|
+
nextPageKey: newKey + 1,
|
|
2774
|
+
pageKeys: newPageKeys,
|
|
2775
|
+
queries: {
|
|
2776
|
+
...prev.queries,
|
|
2777
|
+
[pageKey]: {
|
|
2778
|
+
...currentPageState,
|
|
2779
|
+
endCursor: splitCursor
|
|
2780
|
+
},
|
|
2781
|
+
[newKey]: { args: splitPageArgs }
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
});
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}));
|
|
2790
|
+
const loadMore = (pageLimit) => {
|
|
2791
|
+
if (combined.status !== "CanLoadMore" || !combined.lastPage?.continueCursor) return;
|
|
2792
|
+
setState((prev) => {
|
|
2793
|
+
const newKey = prev.nextPageKey;
|
|
2794
|
+
return {
|
|
2795
|
+
...prev,
|
|
2796
|
+
nextPageKey: newKey + 1,
|
|
2797
|
+
pageKeys: [...prev.pageKeys, newKey],
|
|
2798
|
+
queries: {
|
|
2799
|
+
...prev.queries,
|
|
2800
|
+
[newKey]: { args: {
|
|
2801
|
+
...argsObject(),
|
|
2802
|
+
cursor: combined.lastPage.continueCursor,
|
|
2803
|
+
limit: pageLimit,
|
|
2804
|
+
__paginationId: prev.id
|
|
2805
|
+
} }
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
});
|
|
2809
|
+
};
|
|
2810
|
+
const hasNextPage = createMemo(() => combined.status === "CanLoadMore");
|
|
2811
|
+
const isFetchingNextPage = createMemo(() => combined.status === "LoadingMore");
|
|
2812
|
+
return {
|
|
2813
|
+
get data() {
|
|
2814
|
+
return combined.data;
|
|
2815
|
+
},
|
|
2816
|
+
get error() {
|
|
2817
|
+
return combined.error instanceof Error ? combined.error : null;
|
|
2818
|
+
},
|
|
2819
|
+
get failureReason() {
|
|
2820
|
+
return combined.failureReason ?? null;
|
|
2821
|
+
},
|
|
2822
|
+
get isFetchNextPageError() {
|
|
2823
|
+
return combined.isFetchNextPageError;
|
|
2824
|
+
},
|
|
2825
|
+
get isError() {
|
|
2826
|
+
return combined.isError;
|
|
2827
|
+
},
|
|
2828
|
+
get isFetching() {
|
|
2829
|
+
return combined.isFetching;
|
|
2830
|
+
},
|
|
2831
|
+
get isFetchingNextPage() {
|
|
2832
|
+
return isFetchingNextPage();
|
|
2833
|
+
},
|
|
2834
|
+
get isLoading() {
|
|
2835
|
+
return combined.isLoading;
|
|
2836
|
+
},
|
|
2837
|
+
get isPlaceholderData() {
|
|
2838
|
+
return combined.isPlaceholderData;
|
|
2839
|
+
},
|
|
2840
|
+
get isRefetching() {
|
|
2841
|
+
return combined.isRefetching;
|
|
2842
|
+
},
|
|
2843
|
+
fetchNextPage: (n) => loadMore(n ?? limit),
|
|
2844
|
+
get hasNextPage() {
|
|
2845
|
+
return hasNextPage();
|
|
2846
|
+
},
|
|
2847
|
+
get pages() {
|
|
2848
|
+
return combined.pages;
|
|
2849
|
+
},
|
|
2850
|
+
get status() {
|
|
2851
|
+
return combined.status;
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
};
|
|
2855
|
+
/**
|
|
2856
|
+
* Infinite query hook using cRPC-style options.
|
|
2857
|
+
* Accepts options from `crpc.posts.list.infiniteQueryOptions()`.
|
|
2858
|
+
*
|
|
2859
|
+
* @example
|
|
2860
|
+
* ```tsx
|
|
2861
|
+
* const crpc = useCRPC();
|
|
2862
|
+
* const { data, fetchNextPage } = useInfiniteQuery(
|
|
2863
|
+
* crpc.posts.list.infiniteQueryOptions({ userId }, { limit: 20 })
|
|
2864
|
+
* );
|
|
2865
|
+
* ```
|
|
2866
|
+
*/
|
|
2867
|
+
function useInfiniteQuery(infiniteOptions) {
|
|
2868
|
+
const query = infiniteOptions[FUNC_REF_SYMBOL];
|
|
2869
|
+
const onQueryUnauthorized = useAuthValue("onQueryUnauthorized");
|
|
2870
|
+
const safeAuth = useSafeConvexAuth();
|
|
2871
|
+
const { queryKey: _queryKey, staleTime: _staleTime, refetchInterval: _refetchInterval, refetchOnMount: _refetchOnMount, refetchOnReconnect: _refetchOnReconnect, refetchOnWindowFocus: _refetchOnWindowFocus, enabled: factoryEnabled, meta, ...queryOptions } = infiniteOptions;
|
|
2872
|
+
const { queryName, args, limit, authType, skipUnauth } = meta;
|
|
2873
|
+
const skipUnauthFinal = skipUnauth ?? false;
|
|
2874
|
+
const isUnauthorized = authType === "required" && !safeAuth.isLoading && !safeAuth.isAuthenticated;
|
|
2875
|
+
const shouldSkip = factoryEnabled === false || authType === "required" && safeAuth.isLoading || authType === "required" && !safeAuth.isAuthenticated;
|
|
2876
|
+
const authError = createMemo(() => {
|
|
2877
|
+
if (isUnauthorized && !skipUnauthFinal) return new CRPCClientError({
|
|
2878
|
+
code: "UNAUTHORIZED",
|
|
2879
|
+
functionName: queryName
|
|
2880
|
+
});
|
|
2881
|
+
return null;
|
|
2882
|
+
});
|
|
2883
|
+
createEffect(() => {
|
|
2884
|
+
if (isUnauthorized && !skipUnauthFinal) onQueryUnauthorized({ queryName });
|
|
2885
|
+
});
|
|
2886
|
+
const result = useInfiniteQueryInternal(query, args, {
|
|
2887
|
+
limit,
|
|
2888
|
+
...queryOptions,
|
|
2889
|
+
enabled: !shouldSkip
|
|
2890
|
+
});
|
|
2891
|
+
const authLoadingApplies = authType === "optional" || authType === "required";
|
|
2892
|
+
const isSkippedUnauth = isUnauthorized && skipUnauthFinal;
|
|
2893
|
+
return {
|
|
2894
|
+
get data() {
|
|
2895
|
+
return isSkippedUnauth ? [] : result.data;
|
|
2896
|
+
},
|
|
2897
|
+
get pages() {
|
|
2898
|
+
return isSkippedUnauth ? [] : result.pages;
|
|
2899
|
+
},
|
|
2900
|
+
get error() {
|
|
2901
|
+
return authError() ?? result.error;
|
|
2902
|
+
},
|
|
2903
|
+
get isError() {
|
|
2904
|
+
return authError() ? true : result.isError;
|
|
2905
|
+
},
|
|
2906
|
+
get isPlaceholderData() {
|
|
2907
|
+
return isSkippedUnauth ? false : result.isPlaceholderData;
|
|
2908
|
+
},
|
|
2909
|
+
get isLoading() {
|
|
2910
|
+
const ae = authError();
|
|
2911
|
+
const isClientError = isCRPCClientError(result.error);
|
|
2912
|
+
return authLoadingApplies && safeAuth.isLoading || !isClientError && !ae && !isSkippedUnauth && result.isLoading;
|
|
2913
|
+
},
|
|
2914
|
+
get isFetching() {
|
|
2915
|
+
return result.isFetching;
|
|
2916
|
+
},
|
|
2917
|
+
get isFetchingNextPage() {
|
|
2918
|
+
return result.isFetchingNextPage;
|
|
2919
|
+
},
|
|
2920
|
+
get isFetchNextPageError() {
|
|
2921
|
+
return result.isFetchNextPageError;
|
|
2922
|
+
},
|
|
2923
|
+
get isRefetching() {
|
|
2924
|
+
return result.isRefetching;
|
|
2925
|
+
},
|
|
2926
|
+
get failureReason() {
|
|
2927
|
+
return result.failureReason;
|
|
2928
|
+
},
|
|
2929
|
+
fetchNextPage: result.fetchNextPage,
|
|
2930
|
+
get hasNextPage() {
|
|
2931
|
+
return result.hasNextPage;
|
|
2932
|
+
},
|
|
2933
|
+
get status() {
|
|
2934
|
+
return result.status;
|
|
2935
|
+
}
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
//#endregion
|
|
2940
|
+
export { AuthMutationError, AuthProvider, Authenticated, ConvexAuthBridge, ConvexAuthProvider, ConvexProvider, ConvexProviderWithAuth, ConvexQueryClient, FetchAccessTokenContext, MaybeAuthenticated, MaybeUnauthenticated, MetaContext, Unauthenticated, createAuthMutations, createCRPCContext, createCRPCOptionsProxy, createHttpProxy, createVanillaCRPCProxy, decodeJwtExp, getAuthType, getConvexQueryClientSingleton, getQueryClientSingleton, isAuthMutationError, useAuth, useAuthGuard, useAuthSkip, useAuthStore, useAuthValue, useConvex, useConvexActionOptions, useConvexActionQueryOptions, useConvexAuth, useConvexAuthBridge, useConvexInfiniteQueryOptions, useConvexMutationOptions, useConvexQueryClient, useConvexQueryOptions, useFetchAccessToken, useFnMeta, useInfiniteQuery, useIsAuth, useMaybeAuth, useMeta, useSafeConvexAuth, useUploadMutationOptions };
|