@bb-labs/convex-cache 0.0.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/README.md +342 -0
- package/dist/cli/fns/dev.d.ts +11 -0
- package/dist/cli/fns/dev.js +59 -0
- package/dist/cli/fns/fns/convex-runner.d.ts +26 -0
- package/dist/cli/fns/fns/convex-runner.js +71 -0
- package/dist/cli/fns/fns/generate-z-schema/fns/build-schema-map.d.ts +8 -0
- package/dist/cli/fns/fns/generate-z-schema/fns/build-schema-map.js +39 -0
- package/dist/cli/fns/fns/generate-z-schema/fns/load-convex.d.ts +7 -0
- package/dist/cli/fns/fns/generate-z-schema/fns/load-convex.js +49 -0
- package/dist/cli/fns/fns/generate-z-schema/generate.d.ts +7 -0
- package/dist/cli/fns/fns/generate-z-schema/generate.js +16 -0
- package/dist/cli/fns/fns/generate-z-schema/utils/build-schema-file.d.ts +13 -0
- package/dist/cli/fns/fns/generate-z-schema/utils/build-schema-file.js +42 -0
- package/dist/cli/fns/fns/generate-z-schema/utils/extract-schema.d.ts +3 -0
- package/dist/cli/fns/fns/generate-z-schema/utils/extract-schema.js +84 -0
- package/dist/cli/fns/fns/generate-z-schema/utils/find-fn-ref.d.ts +5 -0
- package/dist/cli/fns/fns/generate-z-schema/utils/find-fn-ref.js +27 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +25 -0
- package/dist/cli/lib/convex-config.d.ts +21 -0
- package/dist/cli/lib/convex-config.js +37 -0
- package/dist/cli/lib/dir-watcher.d.ts +49 -0
- package/dist/cli/lib/dir-watcher.js +97 -0
- package/dist/cli/lib/package-cmds.d.ts +10 -0
- package/dist/cli/lib/package-cmds.js +17 -0
- package/dist/cli/lib/resolve-bun.d.ts +4 -0
- package/dist/cli/lib/resolve-bun.js +24 -0
- package/dist/cli/lib/shell-runner.d.ts +61 -0
- package/dist/cli/lib/shell-runner.js +133 -0
- package/dist/convex-cache/adapters/next/fns/build-preloader.d.ts +5 -0
- package/dist/convex-cache/adapters/next/fns/build-preloader.js +6 -0
- package/dist/convex-cache/adapters/next/fns/preload-query.d.ts +12 -0
- package/dist/convex-cache/adapters/next/fns/preload-query.js +27 -0
- package/dist/convex-cache/adapters/next/hooks/hooks.d.ts +8 -0
- package/dist/convex-cache/adapters/next/hooks/hooks.js +9 -0
- package/dist/convex-cache/adapters/next/index.d.ts +1 -0
- package/dist/convex-cache/adapters/next/index.js +1 -0
- package/dist/convex-cache/adapters/next/lib/revalidate-query-cache.d.ts +13 -0
- package/dist/convex-cache/adapters/next/lib/revalidate-query-cache.js +21 -0
- package/dist/convex-cache/adapters/next/server-fns/preload-query.d.ts +16 -0
- package/dist/convex-cache/adapters/next/server-fns/preload-query.js +32 -0
- package/dist/convex-cache/adapters/next/server-fns/revalidate-cache.d.ts +3 -0
- package/dist/convex-cache/adapters/next/server-fns/revalidate-cache.js +6 -0
- package/dist/convex-cache/adapters/next/server.d.ts +2 -0
- package/dist/convex-cache/adapters/next/server.js +2 -0
- package/dist/convex-cache/adapters/next/types/preloaded.d.ts +8 -0
- package/dist/convex-cache/adapters/next/types/preloaded.js +1 -0
- package/dist/convex-cache/adapters/next/utils/cache-profile.d.ts +1 -0
- package/dist/convex-cache/adapters/next/utils/cache-profile.js +1 -0
- package/dist/convex-cache/adapters/react/hooks/hooks.d.ts +8 -0
- package/dist/convex-cache/adapters/react/hooks/hooks.js +12 -0
- package/dist/convex-cache/adapters/react/index.d.ts +2 -0
- package/dist/convex-cache/adapters/react/index.js +2 -0
- package/dist/convex-cache/adapters/react/provider/provider.d.ts +13 -0
- package/dist/convex-cache/adapters/react/provider/provider.js +16 -0
- package/dist/convex-cache/core/client-cache/helpers/hooks/use-client-cache.d.ts +10 -0
- package/dist/convex-cache/core/client-cache/helpers/hooks/use-client-cache.js +16 -0
- package/dist/convex-cache/core/client-cache/helpers/hooks/use-query-key.d.ts +7 -0
- package/dist/convex-cache/core/client-cache/helpers/hooks/use-query-key.js +5 -0
- package/dist/convex-cache/core/client-cache/queries/paginated-query.d.ts +13 -0
- package/dist/convex-cache/core/client-cache/queries/paginated-query.js +37 -0
- package/dist/convex-cache/core/client-cache/queries/query.d.ts +10 -0
- package/dist/convex-cache/core/client-cache/queries/query.js +23 -0
- package/dist/convex-cache/core/helpers/utils/convert-paginated-schema.d.ts +13 -0
- package/dist/convex-cache/core/helpers/utils/convert-paginated-schema.js +45 -0
- package/dist/convex-cache/core/helpers/utils/fetch-schema-from-map.d.ts +21 -0
- package/dist/convex-cache/core/helpers/utils/fetch-schema-from-map.js +16 -0
- package/dist/convex-cache/core/helpers/utils/query-key.d.ts +10 -0
- package/dist/convex-cache/core/helpers/utils/query-key.js +16 -0
- package/dist/convex-cache/core/server-cache/queries/paginated-query.d.ts +15 -0
- package/dist/convex-cache/core/server-cache/queries/paginated-query.js +16 -0
- package/dist/convex-cache/core/server-cache/queries/query.d.ts +12 -0
- package/dist/convex-cache/core/server-cache/queries/query.js +16 -0
- package/dist/convex-cache/core/types/index.d.ts +2 -0
- package/dist/convex-cache/core/types/index.js +2 -0
- package/dist/convex-cache/core/types/types/paginated-query.d.ts +8 -0
- package/dist/convex-cache/core/types/types/paginated-query.js +2 -0
- package/dist/convex-cache/core/types/types/query.d.ts +5 -0
- package/dist/convex-cache/core/types/types/query.js +1 -0
- package/dist/convex-cache/index.d.ts +1 -0
- package/dist/convex-cache/index.js +1 -0
- package/dist/convex-cache/types/schema-map.d.ts +4 -0
- package/dist/convex-cache/types/schema-map.js +1 -0
- package/dist/validation/index.d.ts +10 -0
- package/dist/validation/index.js +83 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
## Introduction
|
|
2
|
+
|
|
3
|
+
A library for caching Convex queries on the client and server, providing supercharged performance for your Convex-powered applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Client-side caching** - Cache query results in the browser using IndexedDb
|
|
8
|
+
- ⚡ **Server-side caching** - Cache query results in server components
|
|
9
|
+
- 🔄 **Automatic revalidation** - Smart cache invalidation and revalidation strategies
|
|
10
|
+
- 📄 **Paginated query support** - Built-in support for paginated queries
|
|
11
|
+
- ✅ **Type-safe** - Full TypeScript support with Zod schema validation
|
|
12
|
+
- 🛠️ **CLI tool** - Development tooling for generating schemas from Convex functions
|
|
13
|
+
- ⚛️ **React & Next.js adapters** - Easy integration with React and Next.js applications
|
|
14
|
+
- 🛠️ **Composable** - Easy extension for other frameworks not currently supported using adapters
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install convex-cache
|
|
20
|
+
# or
|
|
21
|
+
bun add convex-cache
|
|
22
|
+
# or
|
|
23
|
+
yarn add convex-cache
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
`convex-cache` provides two caching strategies: client-side caching for instant page loads and server-side caching for optimized server rendering. Both strategies automatically revalidate when data changes on Convex. Devs can use either one or both simultaneously depending on their project requirements.
|
|
29
|
+
|
|
30
|
+
### Client Cache
|
|
31
|
+
|
|
32
|
+
The client cache stores query results in IndexedDb, enabling instant page loads. When a component requests data, the cache is checked first. If found, cached data is returned immediately while a background revalidation ensures freshness. On cache miss, data is fetched from Convex, validated against schemas, stored in IndexedDb, and returned to the component.
|
|
33
|
+
|
|
34
|
+
<a aria-label="Client Cache Flowchart" href="https://github.com/bigbang-sdk/convex-cache">
|
|
35
|
+
<picture>
|
|
36
|
+
<source srcset="https://raw.githubusercontent.com/bigbang-sdk/convex-cache/refs/heads/main/assets/flowchart/client-cache-dark.png" media="(prefers-color-scheme: dark)" />
|
|
37
|
+
<source srcset="https://raw.githubusercontent.com/bigbang-sdk/convex-cache/refs/heads/main/assets/flowchart/client-cache-light.png" media="(prefers-color-scheme: light)" />
|
|
38
|
+
<img src="https://raw.githubusercontent.com/bigbang-sdk/convex-cache/refs/heads/main/assets/flowchart/client-cache-light.png" alt="Client Cache Flowchart" referrerpolicy="no-referrer-when-downgrade" />
|
|
39
|
+
</picture>
|
|
40
|
+
</a>
|
|
41
|
+
|
|
42
|
+
**Flow:**
|
|
43
|
+
|
|
44
|
+
1. User (in Singapore) requests `example.com` from Next.js
|
|
45
|
+
2. Next.js serves the page (without data) from the nearest edge location (Singapore - very fast)
|
|
46
|
+
3. Page requests and receives the data from local DB (ultra fast - on the same device)
|
|
47
|
+
4. Page connects to Convex (USA)
|
|
48
|
+
5. Convex streams changes
|
|
49
|
+
6. `convex-cache` automatically updates the cache in local DB
|
|
50
|
+
|
|
51
|
+
**Note:**
|
|
52
|
+
|
|
53
|
+
1. If a user isn’t connected to Convex when the data for a query changes, their next visit will show stale data (from their local DB). However, it is immediately revalidated once the page connects to Convex.
|
|
54
|
+
|
|
55
|
+
### Server Cache
|
|
56
|
+
|
|
57
|
+
### Next.js
|
|
58
|
+
|
|
59
|
+
The server cache (with Next.js) leverages Next.js's native caching system, integrating seamlessly with Cache Components and Partial Pre-Rendering (PPR). Queries are preloaded in server components and cached at the Next.js level. When data changes on Convex, the Next.js cache is automatically revalidated, ensuring pages always serve fresh data.
|
|
60
|
+
|
|
61
|
+
<a aria-label="Server Cache Flowchart" href="https://github.com/bigbang-sdk/convex-cache">
|
|
62
|
+
<picture>
|
|
63
|
+
<source srcset="https://raw.githubusercontent.com/bigbang-sdk/convex-cache/refs/heads/main/assets/flowchart/server-cache-dark.png" media="(prefers-color-scheme: dark)" />
|
|
64
|
+
<source srcset="https://raw.githubusercontent.com/bigbang-sdk/convex-cache/refs/heads/main/assets/flowchart/server-cache-light.png" media="(prefers-color-scheme: light)" />
|
|
65
|
+
<img src="https://raw.githubusercontent.com/bigbang-sdk/convex-cache/refs/heads/main/assets/flowchart/server-cache-light.png" alt="Server Cache Flowchart" referrerpolicy="no-referrer-when-downgrade" />
|
|
66
|
+
</picture>
|
|
67
|
+
</a>
|
|
68
|
+
|
|
69
|
+
**Flow:**
|
|
70
|
+
|
|
71
|
+
1. User (in Singapore) requests `example.com` from Next.js
|
|
72
|
+
2. Next.js serves the page (with cached data) from the nearest edge location (Singapore - very fast)
|
|
73
|
+
3. Page connects to Convex (USA)
|
|
74
|
+
4. Convex streams changes
|
|
75
|
+
5. `convex-cache` automatically revalidates the cache in Next.js's native cache using an internal server action
|
|
76
|
+
|
|
77
|
+
**Note:**
|
|
78
|
+
|
|
79
|
+
1. The Next.js cache only revalidates if at least one user is connected to Convex when a query’s data changes. If someone is connected, all later users get fresh data.
|
|
80
|
+
2. If no users are connected when the data changes, the next `preloadQuery` will return stale data. However, it will be revalidated instantly once the page connects to Convex.
|
|
81
|
+
|
|
82
|
+
### Other frameworks for server caching
|
|
83
|
+
|
|
84
|
+
`convex-cache` currently includes adapter for Next.js. However, custom adapters can be built for other frameworks / patterns by leveraging code from the `core` folder in `convex-cache` source code.
|
|
85
|
+
|
|
86
|
+
## Setup
|
|
87
|
+
|
|
88
|
+
### Generate Schema Map
|
|
89
|
+
|
|
90
|
+
Use `convex-cache dev` instead of `convex dev` to generate a schema map from your Convex functions:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx convex-cache dev
|
|
94
|
+
# or
|
|
95
|
+
bunx convex-cache dev
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This watches your Convex directory, runs `convex dev`, and automatically generates Zod schemas when functions change. The schema map will be available at `convex/_generated/schemaMap.ts` (or `.js` for JavaScript projects).
|
|
99
|
+
|
|
100
|
+
The schema map ensures that if you change the shape of the data returned from your queries, existing caches with previous shape do not cause errors for clients.
|
|
101
|
+
|
|
102
|
+
## How to Use
|
|
103
|
+
|
|
104
|
+
### Client Cache
|
|
105
|
+
|
|
106
|
+
Client-side caching stores query results in IndexedDb, providing **supercharged** performance. With client-side caching enabled, the first page load is instant. The cache is automatically revalidated when data changes on Convex.
|
|
107
|
+
|
|
108
|
+
#### Step 1: Setup React Provider
|
|
109
|
+
|
|
110
|
+
Wrap your app with `ConvexCacheProvider`:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
114
|
+
import { ConvexCacheProvider } from "convex-cache/react";
|
|
115
|
+
import { schemaMap } from "./convex/_generated/schemaMap";
|
|
116
|
+
|
|
117
|
+
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
|
118
|
+
|
|
119
|
+
export function App({ children }) {
|
|
120
|
+
return (
|
|
121
|
+
<ConvexProvider client={convex}>
|
|
122
|
+
<ConvexCacheProvider schemaMap={schemaMap}>
|
|
123
|
+
{children}
|
|
124
|
+
</ConvexCacheProvider>
|
|
125
|
+
</ConvexProvider>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Step 2: Use Cached Queries in React
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { useCachedQueryClient } from "convex-cache/react";
|
|
134
|
+
import { api } from "./convex/_generated/api";
|
|
135
|
+
|
|
136
|
+
function UserProfile({ userId }) {
|
|
137
|
+
const user = useCachedQueryClient({
|
|
138
|
+
query: api.queries.getUser,
|
|
139
|
+
args: { userId },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!user) return <div>Loading...</div>;
|
|
143
|
+
|
|
144
|
+
return <div>{user.name}</div>;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Server Cache
|
|
149
|
+
|
|
150
|
+
Server-side caching allows you to preload queries in Next.js server components and cache them for improved performance. `preloadQuery` caches query results in Next.js's native cache and works seamlessly with Cache Components and Partial Pre-Rendering (PPR). The cache on Next.js servers and edge is automatically revalidated when data changes on Convex.
|
|
151
|
+
|
|
152
|
+
#### Step 1: Setup Preloader
|
|
153
|
+
|
|
154
|
+
Create a preloader function using `buildPreloader`:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { buildPreloader } from "convex-cache/next/server";
|
|
158
|
+
import { schemaMap } from "./convex/_generated/schemaMap";
|
|
159
|
+
|
|
160
|
+
const preloadQuery = buildPreloader(schemaMap);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Step 2: Preload Queries in Server Components
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { preloadQuery } from "./path/to/preloader";
|
|
167
|
+
import { useCachedQueryServer } from "convex-cache/next";
|
|
168
|
+
import { api } from "./convex/_generated/api";
|
|
169
|
+
|
|
170
|
+
export default async function UserPage({ params }) {
|
|
171
|
+
const preloadedData = await preloadQuery({
|
|
172
|
+
query: api.queries.getUser,
|
|
173
|
+
args: { userId: params.userId },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return <UserProfile preloadedData={preloadedData} />;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function UserProfile({ preloadedData }) {
|
|
180
|
+
const user = useCachedQueryServer({
|
|
181
|
+
query: api.queries.getUser,
|
|
182
|
+
args: { userId: params.userId },
|
|
183
|
+
preloadedData,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return <div>{user.name}</div>;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## API Reference
|
|
191
|
+
|
|
192
|
+
### Client Cache Hooks (`convex-cache/react`)
|
|
193
|
+
|
|
194
|
+
#### `useCachedQueryClient`
|
|
195
|
+
|
|
196
|
+
Hook for using cached queries in React client components. Results are cached in IndexedDb and automatically revalidated when data changes on Convex.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { useCachedQueryClient } from "convex-cache/react";
|
|
200
|
+
import { api } from "./convex/_generated/api";
|
|
201
|
+
|
|
202
|
+
function UserProfile({ userId }) {
|
|
203
|
+
const user = useCachedQueryClient({
|
|
204
|
+
query: api.queries.getUser,
|
|
205
|
+
args: { userId },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!user) return <div>Loading...</div>;
|
|
209
|
+
|
|
210
|
+
return <div>{user.name}</div>;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Parameters:**
|
|
215
|
+
|
|
216
|
+
- `query`: The Convex query function reference
|
|
217
|
+
- `args`: The query arguments object
|
|
218
|
+
|
|
219
|
+
**Returns:** The query result, or `undefined` while loading
|
|
220
|
+
|
|
221
|
+
#### `useCachedPaginatedQueryClient`
|
|
222
|
+
|
|
223
|
+
Hook for using cached paginated queries in React client components.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { useCachedPaginatedQueryClient } from "convex-cache/react";
|
|
227
|
+
import { api } from "./convex/_generated/api";
|
|
228
|
+
|
|
229
|
+
function UserList() {
|
|
230
|
+
const { results, status, loadMore } = useCachedPaginatedQueryClient({
|
|
231
|
+
query: api.queries.listUsers,
|
|
232
|
+
args: { filter: "active" },
|
|
233
|
+
options: { initialNumItems: 10 },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (status === "LoadingFirstPage") return <div>Loading...</div>;
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div>
|
|
240
|
+
{results.map((user) => (
|
|
241
|
+
<div key={user.id}>{user.name}</div>
|
|
242
|
+
))}
|
|
243
|
+
<button onClick={loadMore}>Load More</button>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Parameters:**
|
|
250
|
+
|
|
251
|
+
- `query`: The Convex paginated query function reference
|
|
252
|
+
- `args`: The query arguments object (excluding pagination options)
|
|
253
|
+
- `options`: Pagination options
|
|
254
|
+
- `initialNumItems`: Number of items to load initially
|
|
255
|
+
|
|
256
|
+
**Returns:** An object with:
|
|
257
|
+
|
|
258
|
+
- `results`: Array of paginated results
|
|
259
|
+
- `status`: Loading status (`"LoadingFirstPage"` | `"CanLoadMore"` | `"Exhausted"`)
|
|
260
|
+
- `loadMore`: Function to load the next page
|
|
261
|
+
- `isLoading`: Boolean indicating if more items are being loaded
|
|
262
|
+
|
|
263
|
+
### Server Cache Hooks (`convex-cache/next`)
|
|
264
|
+
|
|
265
|
+
#### `useCachedQueryServer`
|
|
266
|
+
|
|
267
|
+
Hook for using cached queries in Next.js server components. Requires preloaded data from `preloadQuery` and automatically revalidates the Next.js cache when data changes.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { useCachedQueryServer } from "convex-cache/next";
|
|
271
|
+
import { api } from "./convex/_generated/api";
|
|
272
|
+
|
|
273
|
+
function UserProfile({ preloadedData, userId }) {
|
|
274
|
+
const user = useCachedQueryServer({
|
|
275
|
+
query: api.queries.getUser,
|
|
276
|
+
args: { userId },
|
|
277
|
+
preloadedData,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return <div>{user.name}</div>;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Parameters:**
|
|
285
|
+
|
|
286
|
+
- `query`: The Convex query function reference
|
|
287
|
+
- `args`: The query arguments object
|
|
288
|
+
- `preloadedData`: Preloaded data from `preloadQuery`
|
|
289
|
+
|
|
290
|
+
**Returns:** The query result
|
|
291
|
+
|
|
292
|
+
#### `useCachedPaginatedQueryServer`
|
|
293
|
+
|
|
294
|
+
Hook for using cached paginated queries in Next.js server components. Requires preloaded data and supports pagination with automatic cache revalidation.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { useCachedPaginatedQueryServer } from "convex-cache/next";
|
|
298
|
+
import { api } from "./convex/_generated/api";
|
|
299
|
+
|
|
300
|
+
function UserList({ preloadedData }) {
|
|
301
|
+
const { results, status, loadMore } = useCachedPaginatedQueryServer({
|
|
302
|
+
query: api.queries.listUsers,
|
|
303
|
+
args: { filter: "active" },
|
|
304
|
+
options: { initialNumItems: 10 },
|
|
305
|
+
preloadedData,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div>
|
|
310
|
+
{results.map((user) => (
|
|
311
|
+
<div key={user.id}>{user.name}</div>
|
|
312
|
+
))}
|
|
313
|
+
{status === "CanLoadMore" && (
|
|
314
|
+
<button onClick={loadMore}>Load More</button>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Parameters:**
|
|
322
|
+
|
|
323
|
+
- `query`: The Convex paginated query function reference
|
|
324
|
+
- `args`: The query arguments object (excluding pagination options)
|
|
325
|
+
- `options`: Pagination options
|
|
326
|
+
- `initialNumItems`: Number of items to load initially
|
|
327
|
+
- `preloadedData`: Preloaded data from `preloadQuery`
|
|
328
|
+
|
|
329
|
+
**Returns:** An object with:
|
|
330
|
+
|
|
331
|
+
- `results`: Array of paginated results
|
|
332
|
+
- `status`: Loading status (`"LoadingFirstPage"` | `"CanLoadMore"` | `"Exhausted"`)
|
|
333
|
+
- `loadMore`: Function to load the next page
|
|
334
|
+
- `isLoading`: Boolean indicating if more items are being loaded
|
|
335
|
+
|
|
336
|
+
## Zod Support
|
|
337
|
+
|
|
338
|
+
If you prefer using Zod schemas instead of Convex validators, you can set up Convex with Zod. This allows you to define your query schemas using Zod's validation library and works seamlessly with `convex-cache`. See the [Zod Validation documentation](https://github.com/get-convex/convex-helpers/blob/main/packages/convex-helpers/README.md#zod-validation) for setup instructions.
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface DevOptions {
|
|
2
|
+
/** If true, only generate schemas and skip running convex dev. */
|
|
3
|
+
schemaOnly?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Start a one-off Convex dev run and then watch the Convex directory
|
|
7
|
+
* for changes, re-running Convex + schema generation on each change.
|
|
8
|
+
* If schemaOnly is true, skip the Convex dev step and only generate schemas.
|
|
9
|
+
*/
|
|
10
|
+
export declare const dev: ({ schemaOnly }: DevOptions) => Promise<void>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/cli/fns/dev.ts
|
|
2
|
+
import { DirWatcher } from "../lib/dir-watcher.js";
|
|
3
|
+
import { ConvexRunner } from "./fns/convex-runner.js";
|
|
4
|
+
import { getConvexDir } from "../lib/convex-config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Start a one-off Convex dev run and then watch the Convex directory
|
|
7
|
+
* for changes, re-running Convex + schema generation on each change.
|
|
8
|
+
* If schemaOnly is true, skip the Convex dev step and only generate schemas.
|
|
9
|
+
*/
|
|
10
|
+
export const dev = async ({ schemaOnly = false }) => {
|
|
11
|
+
const convexRunner = new ConvexRunner({
|
|
12
|
+
skipConvex: schemaOnly,
|
|
13
|
+
});
|
|
14
|
+
// Initial run – if this fails, don't bother watching.
|
|
15
|
+
try {
|
|
16
|
+
await convexRunner.runOnce();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
console.error("⚠️ Initial run failed. Not starting watcher.", err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const shouldIgnore = (rel) => rel.startsWith("_generated") || rel.startsWith(".") || rel.endsWith("~") || rel.endsWith(".swp");
|
|
23
|
+
// Queue rebuilds so we never have overlapping runs.
|
|
24
|
+
let rebuildPromise = Promise.resolve();
|
|
25
|
+
const watcher = new DirWatcher({
|
|
26
|
+
rootDir: getConvexDir(),
|
|
27
|
+
recursive: true,
|
|
28
|
+
debounceMs: 200,
|
|
29
|
+
shouldIgnore,
|
|
30
|
+
onChange: async () => {
|
|
31
|
+
const previous = rebuildPromise;
|
|
32
|
+
rebuildPromise = (async () => {
|
|
33
|
+
// Cancel whatever is currently running (if anything),
|
|
34
|
+
// then wait for the previous run to fully settle before starting a new one.
|
|
35
|
+
convexRunner.cancel();
|
|
36
|
+
await previous.catch(() => {
|
|
37
|
+
// ignore errors from previous run, they are logged elsewhere
|
|
38
|
+
});
|
|
39
|
+
await convexRunner.runOnce();
|
|
40
|
+
})();
|
|
41
|
+
// Make DirWatcher wait for the rebuild to finish (for nicer logs).
|
|
42
|
+
await rebuildPromise;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
watcher.start();
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
watcher.stop();
|
|
48
|
+
convexRunner.cancel();
|
|
49
|
+
};
|
|
50
|
+
// Graceful shutdown handling.
|
|
51
|
+
process.on("SIGINT", () => {
|
|
52
|
+
cleanup();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
});
|
|
55
|
+
process.on("SIGTERM", () => {
|
|
56
|
+
cleanup();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ConvexRunnerOptions {
|
|
2
|
+
skipConvex?: boolean;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Orchestrates running the Convex CLI and regenerating the Zod schema.
|
|
6
|
+
* Uses ShellRunner under the hood and ensures only one run happens at a time.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ConvexRunner {
|
|
9
|
+
private readonly tasks;
|
|
10
|
+
private readonly skipConvex;
|
|
11
|
+
private isRunning;
|
|
12
|
+
constructor({ skipConvex }: ConvexRunnerOptions);
|
|
13
|
+
get running(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Cancel any in-flight Convex task run.
|
|
16
|
+
* This will cause the current runOnce() to reject with a "cancelled" error.
|
|
17
|
+
*/
|
|
18
|
+
cancel: () => void;
|
|
19
|
+
private resolveCurrentPath;
|
|
20
|
+
/**
|
|
21
|
+
* Run the Convex command once, then regenerate the Zod schema.
|
|
22
|
+
* If a run is already in progress, this is a no-op.
|
|
23
|
+
* If skipConvex is true, skips the Convex command and only generates schemas.
|
|
24
|
+
*/
|
|
25
|
+
runOnce: () => Promise<void>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { ShellRunner, ShellRunnerError } from "../../lib/shell-runner.js";
|
|
4
|
+
import { runCmd, runFileCmd } from "../../lib/package-cmds.js";
|
|
5
|
+
/**
|
|
6
|
+
* Orchestrates running the Convex CLI and regenerating the Zod schema.
|
|
7
|
+
* Uses ShellRunner under the hood and ensures only one run happens at a time.
|
|
8
|
+
*/
|
|
9
|
+
export class ConvexRunner {
|
|
10
|
+
constructor({ skipConvex = false }) {
|
|
11
|
+
this.tasks = new ShellRunner();
|
|
12
|
+
this.isRunning = false;
|
|
13
|
+
/**
|
|
14
|
+
* Cancel any in-flight Convex task run.
|
|
15
|
+
* This will cause the current runOnce() to reject with a "cancelled" error.
|
|
16
|
+
*/
|
|
17
|
+
this.cancel = () => {
|
|
18
|
+
if (!this.isRunning)
|
|
19
|
+
return;
|
|
20
|
+
this.tasks.cancelAll();
|
|
21
|
+
};
|
|
22
|
+
this.resolveCurrentPath = () => {
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
return __dirname;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Run the Convex command once, then regenerate the Zod schema.
|
|
29
|
+
* If a run is already in progress, this is a no-op.
|
|
30
|
+
* If skipConvex is true, skips the Convex command and only generates schemas.
|
|
31
|
+
*/
|
|
32
|
+
this.runOnce = async () => {
|
|
33
|
+
if (this.isRunning)
|
|
34
|
+
return;
|
|
35
|
+
this.isRunning = true;
|
|
36
|
+
this.tasks.resetCancelled();
|
|
37
|
+
try {
|
|
38
|
+
if (!this.skipConvex) {
|
|
39
|
+
const convexCmd = await runCmd("convex dev --once");
|
|
40
|
+
await this.tasks.run(convexCmd, {
|
|
41
|
+
kind: "convex",
|
|
42
|
+
label: "➡️ Uploading functions to Convex...",
|
|
43
|
+
successMessage: "✅ Convex functions uploaded",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const generateSchemaPath = path.join(this.resolveCurrentPath(), "generate-z-schema/generate.js");
|
|
47
|
+
const generateSchemaCmd = await runFileCmd(generateSchemaPath);
|
|
48
|
+
await this.tasks.run(generateSchemaCmd, {
|
|
49
|
+
kind: "schema",
|
|
50
|
+
label: "➡️ Generating Zod schemas...",
|
|
51
|
+
successMessage: "✅ Zod schemas generated",
|
|
52
|
+
});
|
|
53
|
+
console.log(`\n👀 Watching for any changes...`);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Ignore cancellations from ShellRunner, log everything else.
|
|
57
|
+
if (!(err instanceof ShellRunnerError && err.message === "cancelled") && (typeof err !== "object" || err === null || err.message !== "cancelled")) {
|
|
58
|
+
console.error("⚠️ Convex run failed:", err);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
this.isRunning = false;
|
|
63
|
+
this.tasks.resetCancelled();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
this.skipConvex = skipConvex;
|
|
67
|
+
}
|
|
68
|
+
get running() {
|
|
69
|
+
return this.isRunning;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnyApi } from "convex/server";
|
|
2
|
+
import type { SchemaEntry } from "../generate";
|
|
3
|
+
export declare const buildSchemaMap: ({ files, api, convexDir }: {
|
|
4
|
+
files: string[];
|
|
5
|
+
api: AnyApi;
|
|
6
|
+
convexDir: string;
|
|
7
|
+
}) => Promise<void>;
|
|
8
|
+
export declare const writeSchemasFile: (schemaEntries: SchemaEntry[], convexDir: string) => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// lib/build-schema-map.ts
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { extractSchema } from "../utils/extract-schema";
|
|
5
|
+
import { buildSchemaFileTs, buildSchemaFileJs, buildSchemaDtsFile } from "../utils/build-schema-file";
|
|
6
|
+
import { shouldUseTs } from "../../../../lib/convex-config.js";
|
|
7
|
+
export const buildSchemaMap = async ({ files, api, convexDir }) => {
|
|
8
|
+
const perFileEntries = await Promise.all(files.map((file) => extractSchema(file, api, convexDir)));
|
|
9
|
+
const byName = new Map();
|
|
10
|
+
for (const list of perFileEntries) {
|
|
11
|
+
for (const entry of list) {
|
|
12
|
+
byName.set(entry.fnName, entry);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const schemaEntries = [...byName.values()];
|
|
16
|
+
if (schemaEntries.length === 0) {
|
|
17
|
+
console.warn("⚠️ No Convex functions with `__returnsJson` found.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
writeSchemasFile(schemaEntries, convexDir);
|
|
21
|
+
};
|
|
22
|
+
// ---------- file writer ----------
|
|
23
|
+
export const writeSchemasFile = (schemaEntries, convexDir) => {
|
|
24
|
+
const hostConvexGeneratedDir = path.join(convexDir, "_generated");
|
|
25
|
+
fs.mkdirSync(hostConvexGeneratedDir, { recursive: true });
|
|
26
|
+
let useTs = shouldUseTs();
|
|
27
|
+
if (useTs) {
|
|
28
|
+
// TS project: generate schemaMap.ts only
|
|
29
|
+
const tsContent = buildSchemaFileTs(schemaEntries);
|
|
30
|
+
fs.writeFileSync(path.join(hostConvexGeneratedDir, "schemaMap.ts"), tsContent);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Default: generate schemaMap.js + schemaMap.d.ts
|
|
34
|
+
const jsContent = buildSchemaFileJs(schemaEntries);
|
|
35
|
+
const dtsContent = buildSchemaDtsFile();
|
|
36
|
+
fs.writeFileSync(path.join(hostConvexGeneratedDir, "schemaMap.js"), jsContent);
|
|
37
|
+
fs.writeFileSync(path.join(hostConvexGeneratedDir, "schemaMap.d.ts"), dtsContent);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getConvexDir } from "../../../../lib/convex-config.js";
|
|
6
|
+
export const loadConvex = async () => {
|
|
7
|
+
const convexDir = getConvexDir();
|
|
8
|
+
if (!fs.existsSync(convexDir)) {
|
|
9
|
+
console.warn('⚠️ No "convex" directory found in this project. Skipping schema generation.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const api = await loadConvexApi(convexDir);
|
|
13
|
+
if (!api)
|
|
14
|
+
return;
|
|
15
|
+
const files = await findConvexSourceFiles(convexDir);
|
|
16
|
+
return { convexDir, api, files };
|
|
17
|
+
};
|
|
18
|
+
export const loadConvexApi = async (convexDir) => {
|
|
19
|
+
const candidates = [
|
|
20
|
+
path.join(convexDir, "_generated", "api.mjs"),
|
|
21
|
+
path.join(convexDir, "_generated", "api.js"),
|
|
22
|
+
path.join(convexDir, "_generated", "api.cjs"),
|
|
23
|
+
path.join(convexDir, "_generated", "api.ts"),
|
|
24
|
+
];
|
|
25
|
+
const existing = candidates.find((p) => fs.existsSync(p));
|
|
26
|
+
if (!existing) {
|
|
27
|
+
console.warn(["⚠️ No convex/_generated/api.{ts,js,mjs,cjs} found in this project.", '⚠️ Skipping schema extraction. Run "npx convex dev" or "npx convex codegen" first.'].join("\n"));
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const mod = await import(pathToFileURL(existing).href);
|
|
32
|
+
if (!("api" in mod)) {
|
|
33
|
+
console.warn(`⚠️ Found ${existing} but it does not export "api". Skipping.`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return mod.api;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.warn(`⚠️ Failed to import ${existing}:`, err);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const findConvexSourceFiles = async (convexDir) => {
|
|
44
|
+
return fg("**/*.{ts,js}", {
|
|
45
|
+
cwd: convexDir,
|
|
46
|
+
ignore: ["_generated/**/*.{ts,js}"],
|
|
47
|
+
absolute: true,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { loadConvex } from "./fns/load-convex";
|
|
2
|
+
import { buildSchemaMap } from "./fns/build-schema-map";
|
|
3
|
+
const generateZSchema = async () => {
|
|
4
|
+
const loaded = await loadConvex();
|
|
5
|
+
if (!loaded)
|
|
6
|
+
return;
|
|
7
|
+
const { api, files, convexDir } = loaded;
|
|
8
|
+
await buildSchemaMap({ files, api, convexDir });
|
|
9
|
+
};
|
|
10
|
+
const main = async () => {
|
|
11
|
+
await generateZSchema();
|
|
12
|
+
};
|
|
13
|
+
main().catch((err) => {
|
|
14
|
+
console.error("⚠️ Bun generate-z-schema failed:", err);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SchemaEntry } from "../generate";
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript version: used when convex.json has `"fileType": "ts"`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const buildSchemaFileTs: (schemaEntries: SchemaEntry[]) => string;
|
|
6
|
+
/**
|
|
7
|
+
* JavaScript version: default when not TypeScript.
|
|
8
|
+
*/
|
|
9
|
+
export declare const buildSchemaFileJs: (schemaEntries: SchemaEntry[]) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Declaration file for JS projects: just types.
|
|
12
|
+
*/
|
|
13
|
+
export declare const buildSchemaDtsFile: () => string;
|