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,657 @@
|
|
|
1
|
+
# React & RSC Reference
|
|
2
|
+
|
|
3
|
+
> Prerequisites: `setup/react.md`, `setup/next.md`
|
|
4
|
+
|
|
5
|
+
Covers all kitcn React client, TanStack Query integration, and Next.js RSC patterns. Assumes TanStack Query baseline knowledge.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### createCRPCContext
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// src/lib/convex/crpc.tsx
|
|
13
|
+
import { api } from '@convex/api';
|
|
14
|
+
import { createCRPCContext } from 'kitcn/react';
|
|
15
|
+
|
|
16
|
+
export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
|
|
17
|
+
api,
|
|
18
|
+
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
|
|
19
|
+
transformer, // optional — Date always enabled ($date wire tag). Use createTaggedTransformer for extra codecs.
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Export | Description |
|
|
24
|
+
|--------|-------------|
|
|
25
|
+
| `CRPCProvider` | Context provider — wraps children with cRPC proxy |
|
|
26
|
+
| `useCRPC` | Hook → cRPC proxy for `queryOptions`/`mutationOptions`/`infiniteQueryOptions` |
|
|
27
|
+
| `useCRPCClient` | Hook → typed vanilla client for imperative `client.path.query()`/`mutate()` |
|
|
28
|
+
|
|
29
|
+
### QueryClient
|
|
30
|
+
|
|
31
|
+
cRPC auto-sets `staleTime: Infinity`, `refetch*: false` per query (Convex pushes via WebSocket — never stale).
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// src/lib/convex/query-client.ts
|
|
35
|
+
import { defaultShouldDehydrateQuery, QueryCache, QueryClient } from '@tanstack/react-query';
|
|
36
|
+
import { isCRPCClientError, isCRPCError } from 'kitcn/crpc';
|
|
37
|
+
import SuperJSON from 'superjson';
|
|
38
|
+
|
|
39
|
+
// Shared hydration config for SSR (client + server)
|
|
40
|
+
export const hydrationConfig = {
|
|
41
|
+
dehydrate: {
|
|
42
|
+
serializeData: SuperJSON.serialize,
|
|
43
|
+
shouldDehydrateQuery: (query) =>
|
|
44
|
+
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
|
|
45
|
+
shouldRedactErrors: () => false,
|
|
46
|
+
},
|
|
47
|
+
hydrate: { deserializeData: SuperJSON.deserialize },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function createQueryClient() {
|
|
51
|
+
return new QueryClient({
|
|
52
|
+
queryCache: new QueryCache({
|
|
53
|
+
onError: (error) => {
|
|
54
|
+
if (isCRPCClientError(error)) {
|
|
55
|
+
console.log(`[CRPC] ${error.code}:`, error.functionName);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
defaultOptions: {
|
|
60
|
+
...hydrationConfig,
|
|
61
|
+
mutations: {
|
|
62
|
+
onError: (err) => {
|
|
63
|
+
const error = err as Error & { data?: { message?: string } };
|
|
64
|
+
toast.error(error.data?.message || error.message);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
queries: {
|
|
68
|
+
retry: (failureCount, error) => {
|
|
69
|
+
if (isCRPCError(error)) return false; // don't retry deterministic errors
|
|
70
|
+
return failureCount < 3;
|
|
71
|
+
},
|
|
72
|
+
retryDelay: (i) => Math.min(2000 * 2 ** i, 30_000),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Provider Hierarchy
|
|
80
|
+
|
|
81
|
+
**Without auth:**
|
|
82
|
+
```tsx
|
|
83
|
+
// src/lib/convex/convex-provider.tsx
|
|
84
|
+
'use client';
|
|
85
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
86
|
+
import { ConvexProvider, ConvexReactClient, getQueryClientSingleton, getConvexQueryClientSingleton } from 'kitcn/react';
|
|
87
|
+
import { CRPCProvider } from '@/lib/convex/crpc';
|
|
88
|
+
import { createQueryClient } from '@/lib/convex/query-client';
|
|
89
|
+
|
|
90
|
+
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
|
91
|
+
|
|
92
|
+
export function AppConvexProvider({ children }) {
|
|
93
|
+
return (
|
|
94
|
+
<ConvexProvider client={convex}>
|
|
95
|
+
<QueryProvider>{children}</QueryProvider>
|
|
96
|
+
</ConvexProvider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function QueryProvider({ children }) {
|
|
101
|
+
const queryClient = getQueryClientSingleton(createQueryClient);
|
|
102
|
+
const convexQueryClient = getConvexQueryClientSingleton({ convex, queryClient });
|
|
103
|
+
return (
|
|
104
|
+
<QueryClientProvider client={queryClient}>
|
|
105
|
+
<CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
|
|
106
|
+
{children}
|
|
107
|
+
</CRPCProvider>
|
|
108
|
+
</QueryClientProvider>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**With auth** — swap `ConvexProvider` for `ConvexAuthProvider`:
|
|
114
|
+
```tsx
|
|
115
|
+
import { ConvexAuthProvider } from 'kitcn/auth/client';
|
|
116
|
+
import { ConvexReactClient, getConvexQueryClientSingleton, getQueryClientSingleton, useAuthStore } from 'kitcn/react';
|
|
117
|
+
|
|
118
|
+
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
|
119
|
+
|
|
120
|
+
export function AppConvexProvider({ children, token }: { children: ReactNode; token?: string }) {
|
|
121
|
+
const router = useRouter();
|
|
122
|
+
return (
|
|
123
|
+
<ConvexAuthProvider
|
|
124
|
+
authClient={authClient}
|
|
125
|
+
client={convex}
|
|
126
|
+
initialToken={token}
|
|
127
|
+
onMutationUnauthorized={() => router.push('/login')}
|
|
128
|
+
onQueryUnauthorized={() => router.push('/login')}
|
|
129
|
+
>
|
|
130
|
+
<QueryProvider>{children}</QueryProvider>
|
|
131
|
+
</ConvexAuthProvider>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function QueryProvider({ children }) {
|
|
136
|
+
const authStore = useAuthStore(); // pass to singleton
|
|
137
|
+
const queryClient = getQueryClientSingleton(createQueryClient);
|
|
138
|
+
const convexQueryClient = getConvexQueryClientSingleton({ authStore, convex, queryClient });
|
|
139
|
+
return (
|
|
140
|
+
<QueryClientProvider client={queryClient}>
|
|
141
|
+
<CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
|
|
142
|
+
{children}
|
|
143
|
+
</CRPCProvider>
|
|
144
|
+
</QueryClientProvider>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Singleton Helpers
|
|
150
|
+
|
|
151
|
+
| Helper | Behavior |
|
|
152
|
+
|--------|----------|
|
|
153
|
+
| `getQueryClientSingleton(factory)` | Same instance on client, fresh per SSR request |
|
|
154
|
+
| `getConvexQueryClientSingleton(opts)` | Creates/connects ConvexQueryClient bridge |
|
|
155
|
+
|
|
156
|
+
`getConvexQueryClientSingleton` options:
|
|
157
|
+
- `convex` — ConvexReactClient
|
|
158
|
+
- `queryClient` — TanStack QueryClient
|
|
159
|
+
- `authStore` — from `useAuthStore()` (auth apps only)
|
|
160
|
+
- `unsubscribeDelay` — ms before unsubscribing after unmount (default 3000). Covers StrictMode + quick back-nav.
|
|
161
|
+
|
|
162
|
+
### ConvexQueryClient (Bridge)
|
|
163
|
+
|
|
164
|
+
Bridges WebSocket subscriptions → TanStack Query cache. Push model (not pull):
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
useQuery() → WebSocket subscription → real-time updates → cache always fresh
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Defaults: `staleTime: Infinity`, `gcTime: 5min`, `refetchOnMount: false`, `refetchOnWindowFocus: false`.
|
|
171
|
+
|
|
172
|
+
Lifecycle: Mount → subscribe → unmount → wait `unsubscribeDelay` → unsubscribe (cache persists for `gcTime`).
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Queries
|
|
177
|
+
|
|
178
|
+
### queryOptions
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const crpc = useCRPC();
|
|
182
|
+
const { data } = useQuery(crpc.user.list.queryOptions({}));
|
|
183
|
+
const { data } = useQuery(crpc.user.get.queryOptions({ id }));
|
|
184
|
+
const { data } = useQuery(crpc.user.get.queryOptions({ id }, { enabled: !!id, placeholderData: null }));
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Signature: `crpc.path.queryOptions(args, options?)`
|
|
188
|
+
|
|
189
|
+
cRPC-specific options beyond standard TanStack Query:
|
|
190
|
+
|
|
191
|
+
| Option | Type | Default | Description |
|
|
192
|
+
|--------|------|---------|-------------|
|
|
193
|
+
| `skipUnauth` | `boolean` | `false` | Skip query when not authenticated (returns undefined) |
|
|
194
|
+
| `subscribe` | `boolean` | `true` | Enable real-time WebSocket subscription |
|
|
195
|
+
|
|
196
|
+
**With `select`** — spread options, add select separately:
|
|
197
|
+
```ts
|
|
198
|
+
const { data } = useSuspenseQuery({
|
|
199
|
+
...crpc.http.health.queryOptions(),
|
|
200
|
+
select: (data) => data.status, // data: string
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Real-time Subscriptions
|
|
205
|
+
|
|
206
|
+
Default ON. Every `queryOptions` subscribes to Convex WebSocket. Disable with `subscribe: false`:
|
|
207
|
+
```ts
|
|
208
|
+
useQuery(crpc.analytics.getReport.queryOptions({ period }, { subscribe: false }));
|
|
209
|
+
// refresh manually:
|
|
210
|
+
queryClient.invalidateQueries(crpc.analytics.getReport.queryFilter());
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Auth-Aware Queries
|
|
214
|
+
|
|
215
|
+
**`skipUnauth`** — client-side: returns undefined when not authenticated:
|
|
216
|
+
```ts
|
|
217
|
+
useQuery(crpc.user.getCurrentUser.queryOptions({}, { skipUnauth: true }));
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**`meta({ auth })` on procedures** — controls query behavior during auth loading:
|
|
221
|
+
|
|
222
|
+
| Procedure type | Auth loading | Logged out |
|
|
223
|
+
|----------------|-------------|------------|
|
|
224
|
+
| `publicQuery` | Runs immediately | Runs |
|
|
225
|
+
| `optionalAuthQuery` (auth: 'optional') | **Waits** | Runs |
|
|
226
|
+
| `authQuery` (auth: 'required') | **Waits** | **Skips** |
|
|
227
|
+
|
|
228
|
+
The procedure builders (`authQuery`, `publicQuery`, etc.) already include correct `.meta()` settings.
|
|
229
|
+
|
|
230
|
+
### Conditional Queries
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
// enabled
|
|
234
|
+
useQuery(crpc.user.getSettings.queryOptions({ userId: user?.id }, { enabled: !!user }));
|
|
235
|
+
|
|
236
|
+
// skipToken
|
|
237
|
+
import { skipToken } from '@tanstack/react-query';
|
|
238
|
+
useQuery(crpc.user.get.queryOptions(userId ? { id: userId } : skipToken));
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Query Keys & Filters
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const queryKey = crpc.user.list.queryKey({}); // ['convexQuery', 'user:list', {}]
|
|
245
|
+
const data = queryClient.getQueryData(queryKey);
|
|
246
|
+
|
|
247
|
+
const filter = crpc.user.list.queryFilter({}, { predicate: (q) => q.state.dataUpdatedAt > Date.now() - 60000 });
|
|
248
|
+
queryClient.invalidateQueries(filter);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Imperative Calls
|
|
252
|
+
|
|
253
|
+
Three methods:
|
|
254
|
+
|
|
255
|
+
| Method | Context | Caching | Use Case |
|
|
256
|
+
|--------|---------|---------|----------|
|
|
257
|
+
| `client.*.query()` | Anywhere | None | Direct calls, no cache |
|
|
258
|
+
| `crpc.*.queryOptions()` | Render only | Cache | Components (uses hooks) |
|
|
259
|
+
| `crpc.*.staticQueryOptions()` | Anywhere | Cache | Prefetch, event handlers |
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
// useCRPCClient — direct calls
|
|
263
|
+
const client = useCRPCClient();
|
|
264
|
+
const user = await client.user.get.query({ id });
|
|
265
|
+
await client.user.update.mutate({ id, name: 'test' });
|
|
266
|
+
|
|
267
|
+
// staticQueryOptions — prefetch in event handlers (no hooks, no reactive auth)
|
|
268
|
+
const handleMouseEnter = () => {
|
|
269
|
+
queryClient.prefetchQuery(crpc.user.get.staticQueryOptions({ id }));
|
|
270
|
+
};
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Actions as Queries
|
|
274
|
+
|
|
275
|
+
Actions (external API calls) auto-detected, no subscription:
|
|
276
|
+
```ts
|
|
277
|
+
const { data } = useQuery(crpc.ai.analyze.queryOptions({ documentId }));
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Mutations
|
|
283
|
+
|
|
284
|
+
### mutationOptions
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const crpc = useCRPC();
|
|
288
|
+
const mutation = useMutation(crpc.user.create.mutationOptions());
|
|
289
|
+
const mutation = useMutation(crpc.user.update.mutationOptions({
|
|
290
|
+
onSuccess: (data) => toast.success('Updated'),
|
|
291
|
+
onError: (error) => toast.error(error.data?.message ?? 'Failed'),
|
|
292
|
+
}));
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Signature: `crpc.path.mutationOptions(options?)` — standard TanStack mutation options except `mutationFn`.
|
|
296
|
+
|
|
297
|
+
### Mutation Keys
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const key = crpc.user.create.mutationKey(); // ['convexMutation', 'user:create']
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Common Patterns
|
|
304
|
+
|
|
305
|
+
**Toast promise:**
|
|
306
|
+
```ts
|
|
307
|
+
toast.promise(mutation.mutateAsync({ title }), {
|
|
308
|
+
loading: 'Creating...', success: 'Created!',
|
|
309
|
+
error: (e) => e.data?.message ?? 'Failed',
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Form with cleanup:**
|
|
314
|
+
```ts
|
|
315
|
+
const mutation = useMutation(crpc.user.update.mutationOptions({
|
|
316
|
+
onSuccess: () => { form.reset(); closeModal(); toast.success('Updated'); },
|
|
317
|
+
}));
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Inline callbacks:**
|
|
321
|
+
```ts
|
|
322
|
+
mutation.mutate({ id }, {
|
|
323
|
+
onSuccess: () => router.push('/sessions'),
|
|
324
|
+
onError: () => toast.error('Delete failed'),
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Actions as Mutations
|
|
329
|
+
|
|
330
|
+
Actions work with `mutationOptions` for external API calls (no real-time):
|
|
331
|
+
```ts
|
|
332
|
+
const scrape = useMutation(crpc.scraper.scrapeLink.mutationOptions());
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Infinite Queries
|
|
338
|
+
|
|
339
|
+
Import `useInfiniteQuery` from `kitcn/react` (wraps TanStack with Convex subscription logic):
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import { useInfiniteQuery } from 'kitcn/react';
|
|
343
|
+
|
|
344
|
+
const crpc = useCRPC();
|
|
345
|
+
const { data, fetchNextPage, hasNextPage, isLoading, status } = useInfiniteQuery(
|
|
346
|
+
crpc.session.list.infiniteQueryOptions({ userId })
|
|
347
|
+
);
|
|
348
|
+
// data is flattened T[] — all loaded items
|
|
349
|
+
// status: 'LoadingFirstPage' | 'LoadingMore' | 'CanLoadMore' | 'Exhausted'
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### infiniteQueryOptions
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
crpc.path.infiniteQueryOptions(args, options?)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| Option | Type | Description |
|
|
359
|
+
|--------|------|-------------|
|
|
360
|
+
| `limit` | `number` | Items per page (optional if `.paginated(limit)` on server, must be ≤ server limit) |
|
|
361
|
+
| `skipUnauth` | `boolean` | Skip when unauthenticated |
|
|
362
|
+
|
|
363
|
+
Access server limit: `crpc.session.list.meta.limit`
|
|
364
|
+
|
|
365
|
+
### Backend Setup
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
// convex/functions/session.ts
|
|
369
|
+
export const list = publicQuery
|
|
370
|
+
.input(z.object({ userId: z.string().optional() }))
|
|
371
|
+
.paginated({ limit: 20, item: SessionSchema })
|
|
372
|
+
.query(async ({ ctx, input }) => {
|
|
373
|
+
// input.cursor and input.limit auto-added
|
|
374
|
+
return ctx.orm.query.session.findMany({
|
|
375
|
+
where: input.userId ? { userId: input.userId } : undefined,
|
|
376
|
+
orderBy: { createdAt: 'desc' },
|
|
377
|
+
cursor: input.cursor,
|
|
378
|
+
limit: input.limit,
|
|
379
|
+
});
|
|
380
|
+
// output auto-wrapped as { continueCursor, isDone, page }
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`.paginated({ limit, item })`:
|
|
385
|
+
- Adds `cursor` (string|null) and `limit` (number) to input
|
|
386
|
+
- Auto-sets output schema: `{ continueCursor: string, isDone: boolean, page: T[] }`
|
|
387
|
+
- Must be called before `.query()`
|
|
388
|
+
|
|
389
|
+
### Return Value
|
|
390
|
+
|
|
391
|
+
See [Infinite Query Return Value](#infinite-query-return-value) in the API Reference below.
|
|
392
|
+
|
|
393
|
+
### Prefetching
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
await queryClient.prefetchQuery(crpc.session.list.infiniteQueryOptions({ userId }));
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Placeholder Data
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const { data, isPlaceholderData } = useInfiniteQuery(
|
|
403
|
+
crpc.session.list.infiniteQueryOptions({}, {
|
|
404
|
+
placeholderData: Array.from({ length: crpc.session.list.meta.limit }).map((_, i) => ({
|
|
405
|
+
id: i.toString() as Id<'session'>, token: 'Loading...', expiresAt: 0,
|
|
406
|
+
})),
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Real-time & Error Recovery
|
|
412
|
+
|
|
413
|
+
Each page maintains its own WebSocket subscription. Auto-recovers on `InvalidCursor` (resets to page 0) and `splitCursor` (auto-splits page). Pagination state persists in `queryClient` for scroll restoration.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Error Handling
|
|
418
|
+
|
|
419
|
+
### Server Errors
|
|
420
|
+
|
|
421
|
+
`CRPCError` thrown server-side → arrives as `ConvexError` on client. Access via `error.data`:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
// Server: throw new CRPCError({ code: 'NOT_FOUND', message: 'Post not found' });
|
|
425
|
+
// Client:
|
|
426
|
+
const { error, isError } = useQuery(crpc.posts.get.queryOptions({ id }));
|
|
427
|
+
if (isError) toast.error(error.data?.message ?? 'Something went wrong');
|
|
428
|
+
|
|
429
|
+
// Mutation callback:
|
|
430
|
+
crpc.posts.create.mutationOptions({ onError: (error) => toast.error(error.data?.message ?? 'Failed') });
|
|
431
|
+
|
|
432
|
+
// Try/catch:
|
|
433
|
+
const error = err as Error & { data?: { message?: string } };
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Client Errors
|
|
437
|
+
|
|
438
|
+
`CRPCClientError` — thrown client-side when queries are skipped (auth):
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import { CRPCClientError, isCRPCClientError, isCRPCErrorCode } from 'kitcn/crpc';
|
|
442
|
+
|
|
443
|
+
if (isCRPCClientError(error)) {
|
|
444
|
+
error.code; // 'UNAUTHORIZED'
|
|
445
|
+
error.functionName; // 'user:getSettings'
|
|
446
|
+
}
|
|
447
|
+
if (isCRPCErrorCode(error, 'UNAUTHORIZED')) router.push('/login');
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
| Code | Description |
|
|
451
|
+
|------|-------------|
|
|
452
|
+
| `UNAUTHORIZED` | Missing authentication |
|
|
453
|
+
| `FORBIDDEN` | Not authorized |
|
|
454
|
+
| `NOT_FOUND` | Resource not found |
|
|
455
|
+
| `BAD_REQUEST` | Invalid input |
|
|
456
|
+
| `TOO_MANY_REQUESTS` | Rate limited |
|
|
457
|
+
|
|
458
|
+
### Global Error Handling
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
new QueryClient({
|
|
462
|
+
queryCache: new QueryCache({
|
|
463
|
+
onError: (error) => {
|
|
464
|
+
if (isCRPCClientError(error)) console.log(`[CRPC] ${error.code}:`, error.functionName);
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Type Inference
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import type { Api, ApiInputs, ApiOutputs } from '@convex/api';
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Bracket notation:
|
|
479
|
+
```ts
|
|
480
|
+
type User = ApiOutputs['user']['get'];
|
|
481
|
+
type GetUserArgs = ApiInputs['user']['get'];
|
|
482
|
+
type OrgMember = ApiOutputs['organization']['members']['list'][number]; // array item
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Next.js Setup
|
|
488
|
+
|
|
489
|
+
### Caller Factory
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
// src/lib/convex/server.ts
|
|
493
|
+
import { api } from '@convex/api';
|
|
494
|
+
import { convexBetterAuth } from 'kitcn/auth/nextjs';
|
|
495
|
+
|
|
496
|
+
export const { createContext, createCaller, handler } = convexBetterAuth({
|
|
497
|
+
api,
|
|
498
|
+
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
| Export | Description |
|
|
503
|
+
|--------|-------------|
|
|
504
|
+
| `createContext` | RSC context with auth |
|
|
505
|
+
| `createCaller` | Server-side caller factory |
|
|
506
|
+
| `handler` | Next.js API route handler (`export const { GET, POST } = handler;`) |
|
|
507
|
+
|
|
508
|
+
Options: `api`, `convexSiteUrl`, `auth.jwtCache` (default true), `auth.isUnauthorized`.
|
|
509
|
+
|
|
510
|
+
### Client Provider with Auth
|
|
511
|
+
|
|
512
|
+
```tsx
|
|
513
|
+
// layout.tsx
|
|
514
|
+
const token = await caller.getToken();
|
|
515
|
+
return <ConvexProvider token={token}>{children}</ConvexProvider>;
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### API Route
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
// src/app/api/auth/[...all]/route.ts
|
|
522
|
+
import { handler } from '@/lib/convex/server';
|
|
523
|
+
export const { GET, POST } = handler;
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## RSC Patterns
|
|
529
|
+
|
|
530
|
+
### RSC Setup
|
|
531
|
+
|
|
532
|
+
```tsx
|
|
533
|
+
// src/lib/convex/rsc.tsx
|
|
534
|
+
import 'server-only';
|
|
535
|
+
import { createServerCRPCProxy, getServerQueryClientOptions } from 'kitcn/rsc';
|
|
536
|
+
import { cache } from 'react';
|
|
537
|
+
import { headers } from 'next/headers';
|
|
538
|
+
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
|
|
539
|
+
import { hydrationConfig } from './query-client';
|
|
540
|
+
import { createCaller, createContext } from './server';
|
|
541
|
+
|
|
542
|
+
const createRSCContext = cache(async () => createContext({ headers: await headers() }));
|
|
543
|
+
|
|
544
|
+
// Direct server calls (not cached/hydrated)
|
|
545
|
+
export const caller = createCaller(createRSCContext);
|
|
546
|
+
|
|
547
|
+
// Server cRPC proxy (queryOptions only, no mutations)
|
|
548
|
+
export const crpc = createServerCRPCProxy({ api });
|
|
549
|
+
|
|
550
|
+
// Server QueryClient with HTTP-based fetching
|
|
551
|
+
const createServerQueryClient = () => new QueryClient({
|
|
552
|
+
defaultOptions: {
|
|
553
|
+
...hydrationConfig,
|
|
554
|
+
...getServerQueryClientOptions({
|
|
555
|
+
getToken: caller.getToken,
|
|
556
|
+
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
|
|
557
|
+
}),
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
export const getQueryClient = cache(createServerQueryClient);
|
|
561
|
+
|
|
562
|
+
// Fire-and-forget prefetch
|
|
563
|
+
export function prefetch<T extends { queryKey: readonly unknown[] }>(opts: T): void {
|
|
564
|
+
void getQueryClient().prefetchQuery(opts);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Hydration wrapper
|
|
568
|
+
export function HydrateClient({ children }: { children: React.ReactNode }) {
|
|
569
|
+
return (
|
|
570
|
+
<HydrationBoundary state={dehydrate(getQueryClient())}>
|
|
571
|
+
{children}
|
|
572
|
+
</HydrationBoundary>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Awaited fetch + hydration (equivalent to Convex preloadQuery)
|
|
577
|
+
export function preloadQuery<T>(options: FetchQueryOptions<T>): Promise<T> {
|
|
578
|
+
return getQueryClient().fetchQuery(options);
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Three RSC Patterns
|
|
583
|
+
|
|
584
|
+
| Pattern | Blocking | Returns data | Client hydration | Use case |
|
|
585
|
+
|---------|----------|-------------|------------------|----------|
|
|
586
|
+
| `prefetch` | No | No (void) | Yes | Client-only data, non-blocking |
|
|
587
|
+
| `caller` | Yes | Yes | **No** | Server-only logic (redirects, auth checks, sensitive data) |
|
|
588
|
+
| `preloadQuery` | Yes | Yes | Yes | Server + client data (metadata, 404 checks) |
|
|
589
|
+
|
|
590
|
+
**prefetch** (preferred — non-blocking, client owns data):
|
|
591
|
+
```tsx
|
|
592
|
+
export default async function PostsPage() {
|
|
593
|
+
prefetch(crpc.posts.list.queryOptions({}));
|
|
594
|
+
return <HydrateClient><PostList /></HydrateClient>;
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**caller** (server-only, not hydrated):
|
|
599
|
+
```tsx
|
|
600
|
+
const user = await caller.user.getSessionUser({});
|
|
601
|
+
if (!user?.isAdmin) redirect('/');
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**preloadQuery** (awaited + hydrated — use sparingly):
|
|
605
|
+
```tsx
|
|
606
|
+
const post = await preloadQuery(crpc.posts.get.queryOptions({ id }));
|
|
607
|
+
if (!post) notFound();
|
|
608
|
+
return <HydrateClient><h1>{post.title}</h1><PostContent /></HydrateClient>;
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Auth-Aware Prefetching
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
prefetch(crpc.user.getCurrentUser.queryOptions({}, { skipUnauth: true }));
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### Multiple Prefetches
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
prefetch(crpc.user.getCurrentUser.queryOptions({}, { skipUnauth: true }));
|
|
621
|
+
prefetch(crpc.posts.list.queryOptions({}));
|
|
622
|
+
prefetch(crpc.stats.dashboard.queryOptions({}));
|
|
623
|
+
return <HydrateClient><Dashboard /></HydrateClient>;
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Metadata Generation
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
export async function generateMetadata({ params }) {
|
|
630
|
+
const { id } = await params;
|
|
631
|
+
const post = await preloadQuery(crpc.posts.get.queryOptions({ id }));
|
|
632
|
+
return { title: post?.title ?? 'Not Found', description: post?.excerpt };
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### HydrateClient Placement
|
|
637
|
+
|
|
638
|
+
Must wrap ALL client components that use prefetched queries. Server and client proxies generate identical query keys (`['convexQuery', funcRef, args]`).
|
|
639
|
+
|
|
640
|
+
### Data Ownership Caveat
|
|
641
|
+
|
|
642
|
+
Don't render `preloadQuery` data in BOTH Server and Client components — the server-rendered part can't be revalidated by React Query. Prefer `prefetch` (let client own data) unless you need server-side access (metadata, 404, redirects).
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## API Reference
|
|
647
|
+
|
|
648
|
+
### Infinite Query Return Value
|
|
649
|
+
|
|
650
|
+
| Property | Type | Description |
|
|
651
|
+
|----------|------|-------------|
|
|
652
|
+
| `data` | `T[]` | Flattened array of all items |
|
|
653
|
+
| `pages` | `T[][]` | Raw page arrays |
|
|
654
|
+
| `fetchNextPage` | `(limit?) => void` | Load next page |
|
|
655
|
+
| `hasNextPage` | `boolean` | More pages exist |
|
|
656
|
+
| `status` | `PaginationStatus` | `'LoadingFirstPage' \| 'LoadingMore' \| 'CanLoadMore' \| 'Exhausted'` |
|
|
657
|
+
| `isPlaceholderData` | `boolean` | Showing placeholder |
|