use-convex 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +84 -27
  2. package/dist/module.d.mts +5 -3
  3. package/dist/module.mjs +35 -1
  4. package/dist/runtime/composables/useAuth.d.ts +5 -5
  5. package/dist/runtime/composables/useAuth.js +11 -24
  6. package/dist/runtime/composables/useAuthToken.d.ts +1 -1
  7. package/dist/runtime/composables/useAuthToken.js +2 -10
  8. package/dist/runtime/composables/useConvex.js +1 -1
  9. package/dist/runtime/composables/useConvexAction.js +6 -18
  10. package/dist/runtime/composables/useConvexAuth.js +4 -13
  11. package/dist/runtime/composables/useConvexFileUpload.d.ts +43 -0
  12. package/dist/runtime/composables/useConvexFileUpload.js +102 -0
  13. package/dist/runtime/composables/useConvexGate.js +3 -9
  14. package/dist/runtime/composables/useConvexMutation.js +7 -17
  15. package/dist/runtime/composables/useConvexPaginatedQuery.d.ts +1 -1
  16. package/dist/runtime/composables/useConvexPaginatedQuery.js +7 -29
  17. package/dist/runtime/composables/useConvexQueries.d.ts +12 -7
  18. package/dist/runtime/composables/useConvexQueries.js +1 -3
  19. package/dist/runtime/composables/useConvexQuery.js +6 -12
  20. package/dist/runtime/composables/useConvexR2Upload.d.ts +40 -0
  21. package/dist/runtime/composables/useConvexR2Upload.js +86 -0
  22. package/dist/runtime/plugin.client.js +3 -7
  23. package/dist/runtime/plugin.devtools.client.d.ts +15 -2
  24. package/dist/runtime/plugin.devtools.client.js +18 -4
  25. package/dist/runtime/plugin.server.js +3 -7
  26. package/dist/runtime/server/api/convex/auth/session.js +7 -12
  27. package/dist/runtime/server/auth.js +1 -9
  28. package/dist/runtime/server/authCookies.js +8 -30
  29. package/dist/runtime/server/convexConfig.d.ts +12 -0
  30. package/dist/runtime/server/convexConfig.js +9 -0
  31. package/dist/runtime/server/fetch.d.ts +2 -4
  32. package/dist/runtime/server/fetch.js +3 -13
  33. package/dist/runtime/server/routes/__convex_devtools.get.d.ts +6 -2
  34. package/dist/runtime/server/routes/__convex_devtools.get.js +184 -16
  35. package/dist/runtime/server/sameOrigin.js +1 -5
  36. package/dist/runtime/utils/authCookie.d.ts +0 -2
  37. package/dist/runtime/utils/authCookie.js +0 -1
  38. package/dist/runtime/utils/authState.js +1 -6
  39. package/dist/runtime/utils/authStorage.js +1 -1
  40. package/dist/runtime/utils/context.js +3 -2
  41. package/dist/runtime/utils/convexDashboard.d.ts +44 -0
  42. package/dist/runtime/utils/convexDashboard.js +77 -0
  43. package/dist/runtime/utils/errors.d.ts +14 -0
  44. package/dist/runtime/utils/errors.js +25 -0
  45. package/dist/runtime/utils/oauthRedirect.js +3 -2
  46. package/dist/runtime/utils/paginatedOptimistic.js +4 -22
  47. package/dist/runtime/utils/payloadCache.d.ts +6 -0
  48. package/dist/runtime/utils/payloadCache.js +7 -0
  49. package/dist/runtime/utils/pendingError.d.ts +9 -0
  50. package/dist/runtime/utils/pendingError.js +23 -0
  51. package/dist/types.d.mts +7 -3
  52. package/package.json +36 -20
package/README.md CHANGED
@@ -6,8 +6,10 @@ SSR snapshots hydrate through the Nuxt payload, then the browser overlays a live
6
6
 
7
7
  ## Install
8
8
 
9
+ [`use-convex` on npm](https://www.npmjs.com/package/use-convex)
10
+
9
11
  ```bash
10
- pnpm add use-convex convex
12
+ npm i use-convex convex
11
13
  ```
12
14
 
13
15
  ```ts
@@ -59,14 +61,14 @@ const { data, pending, error, refresh } = await useConvexQuery(
59
61
  )
60
62
  ```
61
63
 
62
- | Option | Default | Purpose |
63
- |---|---|---|
64
- | `key` | function name + args | Nuxt payload / cache key |
65
- | `server` | `convex.server` (`true`) | SSR HttpClient snapshot |
66
- | `lazy` | `false` | Non-blocking on client navigation |
67
- | `live` | `true` | Subscribe after hydration |
68
- | `authenticated` | `false` | Wait for Convex auth before live subscribe |
69
- | `token` | cookie / none | Per-request JWT for SSR |
64
+ | Option | Default | Purpose |
65
+ | --------------- | ------------------------ | ------------------------------------------ |
66
+ | `key` | function name + args | Nuxt payload / cache key |
67
+ | `server` | `convex.server` (`true`) | SSR HttpClient snapshot |
68
+ | `lazy` | `false` | Non-blocking on client navigation |
69
+ | `live` | `true` | Subscribe after hydration |
70
+ | `authenticated` | `false` | Wait for Convex auth before live subscribe |
71
+ | `token` | cookie / none | Per-request JWT for SSR |
70
72
 
71
73
  Args accept the query's `FunctionArgs`, `'skip'`, or a `MaybeRefOrGetter` of either.
72
74
 
@@ -85,9 +87,7 @@ prewarmQuery(api.tasks.list, {})
85
87
  ```ts
86
88
  const results = useConvexQueries(() => ({
87
89
  inbox: { query: api.messages.list, args: { channel: 'inbox' } },
88
- later: selectedId.value
89
- ? { query: api.messages.get, args: { id: selectedId.value } }
90
- : 'skip',
90
+ later: selectedId.value ? { query: api.messages.get, args: { id: selectedId.value } } : 'skip',
91
91
  }))
92
92
  ```
93
93
 
@@ -135,6 +135,46 @@ Also available: `insertAtBottomIfLoaded`, `insertAtPosition`, `optimisticallyUpd
135
135
 
136
136
  `useConvex()` returns the browser `ConvexClient` when you need an escape hatch. `useConvexConnectionState()` is a reactive WebSocket `ConnectionState`.
137
137
 
138
+ ## File uploads
139
+
140
+ Convex file storage is a three-step client flow: generate a short-lived upload URL, `POST` the file bytes, then save the returned `storageId` in a mutation. `useConvexFileUpload` wraps that for Nuxt (browser-only, with `pending` / `error` / `progress`).
141
+
142
+ ```ts
143
+ const { upload, pending, error, progress } = useConvexFileUpload({
144
+ generateUploadUrl: api.files.generateUploadUrl,
145
+ saveFile: api.files.save, // ({ storageId, name, contentType, size, ... }) => Id<"files">
146
+ })
147
+
148
+ await upload(file)
149
+ // optional extra save args: await upload(file, { caption: '…' })
150
+ ```
151
+
152
+ Your Convex mutations must enforce auth — never expose an unauthenticated `generateUploadUrl`. Prefer storing `storageId` (and resolving URLs with `ctx.storage.getUrl`) over persisting raw public URLs. See the playground `files` module and the `/files` page for a full list/upload/delete example.
153
+
154
+ ### Cloudflare R2 (`@convex-dev/r2`)
155
+
156
+ For larger objects or R2-backed apps, use `useConvexR2Upload` — the Vue counterpart of `@convex-dev/r2/react`'s `useUploadFile`. It does not depend on the R2 package at runtime; pass the `clientApi()` exports from your Convex app:
157
+
158
+ ```ts
159
+ // convex/r2.ts
160
+ import { R2 } from '@convex-dev/r2'
161
+ import { components } from './_generated/api'
162
+
163
+ const r2 = new R2(components.r2)
164
+ export const { generateUploadUrl, syncMetadata } = r2.clientApi({
165
+ checkUpload: async (ctx) => {
166
+ /* auth */
167
+ },
168
+ })
169
+ ```
170
+
171
+ ```ts
172
+ const { upload, pending, error, progress } = useConvexR2Upload(api.r2)
173
+ const key = await upload(file)
174
+ ```
175
+
176
+ That runs `generateUploadUrl` → `PUT` to the signed URL → `syncMetadata({ key })`, with XHR progress. Use built-in `useConvexFileUpload` for Convex storage; use this helper when you adopt the R2 component.
177
+
138
178
  ## Auth
139
179
 
140
180
  ### Convex Auth
@@ -158,6 +198,7 @@ const { signIn, signOut } = useAuth()
158
198
 
159
199
  <template>
160
200
  <AuthLoading>Resolving…</AuthLoading>
201
+ <AuthRefreshing>Refreshing session…</AuthRefreshing>
161
202
  <Authenticated>
162
203
  <!-- signed-in shell -->
163
204
  </Authenticated>
@@ -167,7 +208,7 @@ const { signIn, signOut } = useAuth()
167
208
  </template>
168
209
  ```
169
210
 
170
- Prefer `<Authenticated>` (or `showAuthedUi`) so SSR HTML does not flash the sign-in form. Gate private queries with `{ authenticated: true }`.
211
+ Prefer `<Authenticated>` (or `showAuthedUi`) so SSR HTML does not flash the sign-in form. Gate private queries with `{ authenticated: true }`. `<AuthRefreshing>` shows only while an authenticated session is refreshing a rejected token (same as React).
171
212
 
172
213
  ```ts
173
214
  await signIn('password', { email, password, flow: 'signIn' })
@@ -214,7 +255,7 @@ export default defineNuxtRouteMiddleware(() => {
214
255
  })
215
256
  ```
216
257
 
217
- `useAuthToken()` returns the current JWT for authenticated HTTP calls. `useConvexGate()` exposes `{ showAuthedUi, showLoading, showSignedOut, showRefreshing }` if you prefer flags over layout components.
258
+ `useAuthToken()` returns the current JWT for authenticated HTTP calls. `useConvexGate()` exposes `{ showAuthedUi, showLoading, showSignedOut, showRefreshing }` if you prefer flags over layout components (`Authenticated` / `Unauthenticated` / `AuthLoading` / `AuthRefreshing`).
218
259
 
219
260
  ### Bring your own (Clerk, Auth0, custom)
220
261
 
@@ -236,7 +277,9 @@ const { data } = await useConvexQuery(api.tasks.list, {}, { authenticated: true
236
277
  Optional SSR cookie (you must write the JWT after sign-in):
237
278
 
238
279
  ```ts
239
- auth: { cookie: 'convex_jwt' }
280
+ auth: {
281
+ cookie: 'convex_jwt'
282
+ }
240
283
  ```
241
284
 
242
285
  ## Server routes
@@ -280,6 +323,19 @@ convex: {
280
323
 
281
324
  `url` also reads `NUXT_PUBLIC_CONVEX_URL` via `runtimeConfig.public.convex.url`, so you can change the deployment without rebuilding.
282
325
 
326
+ ## Nuxt DevTools
327
+
328
+ In development, a **Convex** tab appears in [Nuxt DevTools](https://devtools.nuxt.com). It embeds the official [hosted dashboard](https://docs.convex.dev/platform-apis/embedded-dashboard) for `*.convex.cloud` URLs and shows module config / tips.
329
+
330
+ - **Open dashboard** deep-links to `dashboard.convex.dev` (uses your existing Convex login).
331
+ - **Auto-login** in the embed needs `CONVEX_DEPLOY_KEY` in the Nuxt process environment (e.g. `.env.local`). Without it, the iframe shows Convex’s credential form.
332
+ - A deploy key inlined into the DevTools page is visible to anyone who can reach your local `nuxt dev` server — only set it for trusted local machines.
333
+ - Local / self-hosted backends are not embedded; use the Open dashboard link or the CLI dashboard instead.
334
+
335
+ ## Contributing
336
+
337
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). Lint/format/test use **Vite+** (`vp`) — Oxlint, Oxfmt, and Vitest — while Nuxt module build/dev stay on `nuxt-module-build` / `nuxi`.
338
+
283
339
  ## Playground
284
340
 
285
341
  `playground/` is a normal Nuxt app that consumes this module (workspace-linked).
@@ -287,7 +343,7 @@ convex: {
287
343
  ```bash
288
344
  pnpm install
289
345
 
290
- # Terminal 1 — Convex backend (writes NUXT_PUBLIC_CONVEX_URL to .env.local)
346
+ # Terminal 1 — Convex backend (writes CONVEX_URL to playground/.env.local)
291
347
  pnpm run dev:backend
292
348
  # first time: npx @convex-dev/auth # JWT_PRIVATE_KEY + JWKS
293
349
 
@@ -295,11 +351,12 @@ pnpm run dev:backend
295
351
  pnpm run dev
296
352
  ```
297
353
 
298
- | Route | What it shows |
299
- |---|---|
300
- | `/` | SSR snapshot + live overlay (sign up, CRUD todos) |
301
- | `/server` | Nitro `fetchQuery` / `fetchMutation` / `fetchAction` |
302
- | `/extras` | `live: false`, pagination, action, connection state |
354
+ | Route | What it shows |
355
+ | --------- | ----------------------------------------------------- |
356
+ | `/` | SSR snapshot + live overlay (sign up, CRUD todos) |
357
+ | `/server` | Nitro `fetchQuery` / `fetchMutation` / `fetchAction` |
358
+ | `/files` | `useConvexFileUpload` (upload, list, preview, delete) |
359
+ | `/extras` | `live: false`, pagination, action, connection state |
303
360
 
304
361
  On `/server`: `GET /api/health` is public; `GET`/`POST /api/tasks` use the cookie JWT; `POST /api/shout` is a public `fetchAction` demo.
305
362
 
@@ -307,12 +364,12 @@ On `/server`: `GET /api/health` is public; `GET`/`POST /api/tasks` use the cooki
307
364
 
308
365
  Releases run from `.github/workflows/release.yml` when commits land on `main`. Version bumps follow [conventional commits](https://www.conventionalcommits.org/):
309
366
 
310
- | Commit | Release |
311
- |---|---|
312
- | `fix:` | patch |
313
- | `feat:` | minor |
314
- | `feat!:` or `BREAKING CHANGE:` | major |
315
- | `chore:`, `docs:`, `ci:`, … | none |
367
+ | Commit | Release |
368
+ | ------------------------------ | ------- |
369
+ | `fix:` | patch |
370
+ | `feat:` | minor |
371
+ | `feat!:` or `BREAKING CHANGE:` | major |
372
+ | `chore:`, `docs:`, `ci:`, … | none |
316
373
 
317
374
  semantic-release publishes `use-convex` to npm, tags `vX.Y.Z`, and opens a GitHub Release. The repo `package.json` version is not committed back.
318
375
 
package/dist/module.d.mts CHANGED
@@ -3,9 +3,11 @@ import * as convex_browser from 'convex/browser';
3
3
  export { AuthTokens, SignInResult, UseAuthReturn } from '../dist/runtime/composables/useAuth.js';
4
4
  export { ConvexQueryArgs, UseConvexQueryOptions, UseConvexQueryReturn } from '../dist/runtime/composables/useConvexQuery.js';
5
5
  export { AuthTokenFetcher, UseConvexAuthReturn, UseConvexAuthSetupOptions } from '../dist/runtime/composables/useConvexAuth.js';
6
- export { UseConvexMutationOptions } from '../dist/runtime/composables/useConvexMutation.js';
7
- export { PaginatedQueryReference, PaginationStatus, UseConvexPaginatedQueryOptions, UseConvexPaginatedQueryReturn } from '../dist/runtime/composables/useConvexPaginatedQuery.js';
8
- export { ConvexQueriesRequest, ConvexQueriesResult } from '../dist/runtime/composables/useConvexQueries.js';
6
+ export { OptimisticUpdate, UseConvexMutationOptions } from '../dist/runtime/composables/useConvexMutation.js';
7
+ export { ConvexFileUploadExtraArgs, ConvexFileUploadMeta, UseConvexFileUploadOptions } from '../dist/runtime/composables/useConvexFileUpload.js';
8
+ export { ConvexR2UploadApi, ConvexR2UploadProgress } from '../dist/runtime/composables/useConvexR2Upload.js';
9
+ export { PaginatedQueryArgs, PaginatedQueryItem, PaginatedQueryReference, PaginationStatus, UseConvexPaginatedQueryOptions, UseConvexPaginatedQueryReturn } from '../dist/runtime/composables/useConvexPaginatedQuery.js';
10
+ export { ConvexQueriesRequest, ConvexQueriesResult, ConvexQueryRequestEntry } from '../dist/runtime/composables/useConvexQueries.js';
9
11
  export { ConvexFetchOptions } from '../dist/runtime/server/fetch.js';
10
12
  export { ConvexAuthContext, ConvexNuxtContext } from '../dist/runtime/utils/context.js';
11
13
  export { ConnectionState } from '../dist/runtime/composables/useConvexConnectionState.js';
package/dist/module.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  import { defineNuxtModule, createResolver, addPlugin, addComponent, addServerScanDir, addServerHandler, addImports, addServerImports, addTypeTemplate } from '@nuxt/kit';
2
2
  import { defu } from 'defu';
3
3
  import { resolveAuthCookieName } from '../dist/runtime/utils/authStorage.js';
4
+ import { suggestedCloudUrlFromMismatch } from '../dist/runtime/utils/convexDashboard.js';
5
+ import { warnStaleLocalConvexUrl } from '../dist/runtime/utils/errors.js';
4
6
 
5
7
  const module$1 = defineNuxtModule({
6
8
  meta: {
@@ -26,6 +28,25 @@ const module$1 = defineNuxtModule({
26
28
  client: options.client,
27
29
  auth
28
30
  });
31
+ if (nuxt.options.dev) {
32
+ const publicUrl = nuxt.options.runtimeConfig.public.convex?.url ?? "";
33
+ const suggestedCloudUrl = suggestedCloudUrlFromMismatch(
34
+ publicUrl,
35
+ process.env.CONVEX_DEPLOYMENT
36
+ );
37
+ if (suggestedCloudUrl) {
38
+ warnStaleLocalConvexUrl(publicUrl, suggestedCloudUrl);
39
+ }
40
+ nuxt.options.runtimeConfig.convexDevtools = defu(
41
+ nuxt.options.runtimeConfig.convexDevtools ?? {},
42
+ {
43
+ deployKey: process.env.CONVEX_DEPLOY_KEY ?? "",
44
+ deployment: process.env.CONVEX_DEPLOYMENT ?? "",
45
+ // Fallback when public.convex.url was empty at config eval time.
46
+ url: process.env.NUXT_PUBLIC_CONVEX_URL ?? process.env.CONVEX_URL ?? ""
47
+ }
48
+ );
49
+ }
29
50
  const resolver = createResolver(import.meta.url);
30
51
  const runtimeDir = resolver.resolve("./runtime");
31
52
  const useConvexAuthProvider = auth?.provider === "convex-auth";
@@ -57,7 +78,12 @@ const module$1 = defineNuxtModule({
57
78
  mode: "client"
58
79
  });
59
80
  }
60
- for (const name of ["Authenticated", "Unauthenticated", "AuthLoading"]) {
81
+ for (const name of [
82
+ "Authenticated",
83
+ "Unauthenticated",
84
+ "AuthLoading",
85
+ "AuthRefreshing"
86
+ ]) {
61
87
  addComponent({
62
88
  name,
63
89
  filePath: resolver.resolve(`./runtime/components/${name}.vue`)
@@ -103,6 +129,14 @@ const module$1 = defineNuxtModule({
103
129
  name: "useConvexMutation",
104
130
  from: resolver.resolve("./runtime/composables/useConvexMutation")
105
131
  },
132
+ {
133
+ name: "useConvexFileUpload",
134
+ from: resolver.resolve("./runtime/composables/useConvexFileUpload")
135
+ },
136
+ {
137
+ name: "useConvexR2Upload",
138
+ from: resolver.resolve("./runtime/composables/useConvexR2Upload")
139
+ },
106
140
  {
107
141
  name: "useConvexAction",
108
142
  from: resolver.resolve("./runtime/composables/useConvexAction")
@@ -43,17 +43,17 @@ export declare function signIn(provider?: string, params?: FormData | Record<str
43
43
  * Sign out and clear local tokens + SSR JWT cookie.
44
44
  */
45
45
  export declare function signOut(): Promise<void>;
46
- /** @internal Used by the Convex Auth client plugin. */
46
+ /** @internal */
47
47
  export declare function hydrateAuthFromStorage(): void;
48
- /** @internal Token fetcher for `useConvexAuth({ fetchToken })`. */
48
+ /** @internal */
49
49
  export declare function getAuthToken({ forceRefreshToken, }: {
50
50
  forceRefreshToken: boolean;
51
51
  }): Promise<string | null>;
52
- /** @internal Sync check for OAuth `?code=` + stored verifier (no await). */
52
+ /** @internal */
53
53
  export declare function hasPendingOAuthCallback(): boolean;
54
- /** @internal Used by the Convex Auth client plugin. */
54
+ /** @internal */
55
55
  export declare function consumeOAuthCodeFromUrl(): Promise<boolean>;
56
- /** @internal Provider session flags for `useConvexAuth`. */
56
+ /** @internal */
57
57
  export declare function useAuthProviderState(): {
58
58
  isLoading: ComputedRef<boolean>;
59
59
  hasSession: ComputedRef<boolean>;
@@ -1,19 +1,11 @@
1
1
  import { ConvexHttpClient } from "convex/browser";
2
2
  import { makeFunctionReference } from "convex/server";
3
- import {
4
- computed,
5
- nextTick
6
- } from "vue";
7
- import {
8
- useRuntimeConfig,
9
- useState
10
- } from "nuxt/app";
3
+ import { computed, nextTick } from "vue";
4
+ import { useRuntimeConfig, useState } from "nuxt/app";
11
5
  import { useConvexAuth } from "./useConvexAuth.js";
6
+ import { missingConvexUrlError, unreachableConvexUrlError } from "../utils/errors.js";
12
7
  import { withRefreshMutex } from "../utils/authMutex.js";
13
- import {
14
- useAuthJwtCookie,
15
- useAuthPresentCookie
16
- } from "../utils/authCookie.js";
8
+ import { useAuthJwtCookie, useAuthPresentCookie } from "../utils/authCookie.js";
17
9
  import {
18
10
  flattenSignInParams,
19
11
  isHttpOnlyAuth,
@@ -28,9 +20,7 @@ import {
28
20
  import { tryUseConvexContext } from "../utils/context.js";
29
21
  import { parseOAuthRedirect } from "../utils/oauthRedirect.js";
30
22
  const authSignIn = makeFunctionReference("auth:signIn");
31
- const authSignOut = makeFunctionReference(
32
- "auth:signOut"
33
- );
23
+ const authSignOut = makeFunctionReference("auth:signOut");
34
24
  const RETRY_BACKOFF = [500, 2e3];
35
25
  const RETRY_JITTER = 100;
36
26
  export function useAuth() {
@@ -65,7 +55,7 @@ export async function signIn(provider, params) {
65
55
  const session = useAuthSession();
66
56
  const convexUrl = session.convexUrl;
67
57
  if (!convexUrl) {
68
- throw new Error("Convex URL is not configured");
58
+ throw missingConvexUrlError("signIn");
69
59
  }
70
60
  session.pending.value = true;
71
61
  session.error.value = null;
@@ -94,7 +84,7 @@ export async function signIn(provider, params) {
94
84
  }
95
85
  return { signingIn: false };
96
86
  } catch (cause) {
97
- const message = cause instanceof Error ? cause.message : "Authentication failed";
87
+ const message = isNetworkError(cause) && convexUrl ? unreachableConvexUrlError(convexUrl).message : cause instanceof Error ? cause.message : "Authentication failed";
98
88
  session.error.value = message;
99
89
  throw cause;
100
90
  } finally {
@@ -319,13 +309,10 @@ async function fetchAuthSession() {
319
309
  }
320
310
  async function fetchAuthSessionToken() {
321
311
  try {
322
- return await $fetch(
323
- AUTH_SESSION_PATH,
324
- {
325
- method: "POST",
326
- body: { getToken: true }
327
- }
328
- );
312
+ return await $fetch(AUTH_SESSION_PATH, {
313
+ method: "POST",
314
+ body: { getToken: true }
315
+ });
329
316
  } catch {
330
317
  return { hasSession: false, token: null };
331
318
  }
@@ -21,4 +21,4 @@ export declare function useAuthToken(): ComputedRef<string | null>;
21
21
  */
22
22
  export declare function requireConvexAuthMiddleware(options?: {
23
23
  redirectTo?: string;
24
- }): ReturnType<typeof navigateTo> | void;
24
+ }): ReturnType<typeof navigateTo> | undefined;
@@ -1,9 +1,5 @@
1
1
  import { navigateTo } from "nuxt/app";
2
- import {
3
- computed,
4
- ref,
5
- watch
6
- } from "vue";
2
+ import { computed, ref, watch } from "vue";
7
3
  import { useConvexAuth } from "./useConvexAuth.js";
8
4
  import { tryUseConvexContext } from "../utils/context.js";
9
5
  export function useAuthToken() {
@@ -42,11 +38,7 @@ export function useAuthToken() {
42
38
  }
43
39
  };
44
40
  watch(
45
- [
46
- () => auth.isAuthenticated.value,
47
- () => auth.isRefreshing.value,
48
- () => auth.isLoading.value
49
- ],
41
+ [() => auth.isAuthenticated.value, () => auth.isRefreshing.value, () => auth.isLoading.value],
50
42
  () => {
51
43
  void refresh();
52
44
  },
@@ -3,7 +3,7 @@ export function useConvex() {
3
3
  const ctx = useConvexContext();
4
4
  if (!ctx.client) {
5
5
  throw new Error(
6
- "[convex-nuxt] useConvex() is only available in the browser. Use fetchQuery on the server."
6
+ "[use-convex] useConvex() is only available in the browser. Use fetchQuery on the server."
7
7
  );
8
8
  }
9
9
  return ctx.client;
@@ -1,30 +1,18 @@
1
- import { computed, ref, toValue } from "vue";
1
+ import { toValue } from "vue";
2
2
  import { useConvexContext } from "../utils/context.js";
3
+ import { createPendingErrorState } from "../utils/pendingError.js";
3
4
  export function useConvexAction(action) {
4
5
  const ctx = useConvexContext();
5
- const error = ref(null);
6
- const pendingCount = ref(0);
6
+ const { error, pending, withPending } = createPendingErrorState();
7
7
  const run = async (args = {}) => {
8
8
  if (import.meta.server || !ctx.client) {
9
- throw new Error(
10
- "[convex-nuxt] useConvexAction can only run in the browser."
11
- );
12
- }
13
- pendingCount.value++;
14
- error.value = null;
15
- try {
16
- return await ctx.client.action(action, toValue(args));
17
- } catch (cause) {
18
- const err = cause instanceof Error ? cause : new Error(String(cause));
19
- error.value = err;
20
- throw err;
21
- } finally {
22
- pendingCount.value--;
9
+ throw new Error("[use-convex] useConvexAction can only run in the browser.");
23
10
  }
11
+ return await withPending(() => ctx.client.action(action, toValue(args)));
24
12
  };
25
13
  return {
26
14
  run,
27
15
  error,
28
- pending: computed(() => pendingCount.value > 0)
16
+ pending
29
17
  };
30
18
  }
@@ -1,8 +1,4 @@
1
- import {
2
- computed,
3
- toValue,
4
- watch
5
- } from "vue";
1
+ import { computed, toValue, watch } from "vue";
6
2
  import { useHasSsrSessionRef } from "../utils/authCookie.js";
7
3
  import { useConvexContext } from "../utils/context.js";
8
4
  import { resolveConvexAuthState } from "../utils/authState.js";
@@ -10,9 +6,7 @@ export function useConvexAuth(setup) {
10
6
  const ctx = useConvexContext();
11
7
  const auth = ctx.auth;
12
8
  const hasSsrSession = useHasSsrSessionRef();
13
- const showAuthedUi = computed(
14
- () => auth.isAuthenticated.value || hasSsrSession.value
15
- );
9
+ const showAuthedUi = computed(() => auth.isAuthenticated.value || hasSsrSession.value);
16
10
  if (setup) {
17
11
  if (import.meta.server) {
18
12
  return {
@@ -26,7 +20,7 @@ export function useConvexAuth(setup) {
26
20
  const client = ctx.client;
27
21
  if (!client) {
28
22
  throw new Error(
29
- "[convex-nuxt] useConvexAuth({ fetchToken }) requires ConvexClient (browser only)."
23
+ "[use-convex] useConvexAuth({ fetchToken }) requires ConvexClient (browser only)."
30
24
  );
31
25
  }
32
26
  auth.fetchToken = setup.fetchToken;
@@ -67,10 +61,7 @@ export function useConvexAuth(setup) {
67
61
  );
68
62
  };
69
63
  watch(
70
- () => [
71
- toValue(setup.isLoading) ?? false,
72
- toValue(setup.isAuthenticated) ?? true
73
- ],
64
+ () => [toValue(setup.isLoading) ?? false, toValue(setup.isAuthenticated) ?? true],
74
65
  syncFromProvider,
75
66
  { immediate: true }
76
67
  );
@@ -0,0 +1,43 @@
1
+ import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
2
+ /** Built-in fields the composable always passes into `saveFile`. */
3
+ export type ConvexFileUploadMeta = {
4
+ storageId: string;
5
+ name: string;
6
+ contentType: string;
7
+ size: number;
8
+ };
9
+ export type ConvexFileUploadExtraArgs<SaveFile extends FunctionReference<'mutation'>> = Omit<FunctionArgs<SaveFile>, 'storageId' | 'name' | 'contentType' | 'size'>;
10
+ export interface UseConvexFileUploadOptions<GenerateUploadUrl extends FunctionReference<'mutation'>, SaveFile extends FunctionReference<'mutation'>> {
11
+ /**
12
+ * Mutation that returns a short-lived Convex upload URL
13
+ * (`ctx.storage.generateUploadUrl()`). Must enforce auth itself.
14
+ */
15
+ generateUploadUrl: GenerateUploadUrl;
16
+ /**
17
+ * Mutation that persists `storageId` + file metadata after the POST succeeds.
18
+ * Must accept at least `storageId`, `name`, `contentType`, and `size`.
19
+ */
20
+ saveFile: SaveFile;
21
+ }
22
+ /**
23
+ * POST a file to a Convex-generated upload URL with upload progress.
24
+ * Convex expects `Content-Type` matching the file and returns `{ storageId }`.
25
+ */
26
+ export declare function postFileToUploadUrl(uploadUrl: string, file: File, onProgress?: (fraction: number) => void): Promise<string>;
27
+ /**
28
+ * Browser helper for Convex's three-step file upload flow:
29
+ * generate upload URL → POST file → save `storageId` via mutation.
30
+ *
31
+ * Returns `{ upload, pending, error, progress }`. `progress` is `0..1` while
32
+ * the bytes are in flight, otherwise `null`.
33
+ *
34
+ * Safe to call during SSR setup — work only runs when `upload()` is invoked
35
+ * in the browser. Your Convex mutations must enforce auth; never expose an
36
+ * unauthenticated `generateUploadUrl`.
37
+ */
38
+ export declare function useConvexFileUpload<GenerateUploadUrl extends FunctionReference<'mutation'>, SaveFile extends FunctionReference<'mutation'>>(options: UseConvexFileUploadOptions<GenerateUploadUrl, SaveFile>): {
39
+ upload: (file: File, extra?: ConvexFileUploadExtraArgs<SaveFile> extends Record<string, never> ? undefined : ConvexFileUploadExtraArgs<SaveFile>) => Promise<FunctionReturnType<SaveFile>>;
40
+ error: import("vue").Ref<Error | null, Error | null>;
41
+ pending: import("vue").ComputedRef<boolean>;
42
+ progress: import("vue").ComputedRef<number | null>;
43
+ };
@@ -0,0 +1,102 @@
1
+ import { computed, ref } from "vue";
2
+ import { useConvexContext } from "../utils/context.js";
3
+ function isUploadUrl(value) {
4
+ return typeof value === "string" && value.length > 0;
5
+ }
6
+ function parseStorageId(body) {
7
+ if (typeof body === "object" && body !== null && "storageId" in body && typeof body.storageId === "string" && body.storageId.length > 0) {
8
+ return body.storageId;
9
+ }
10
+ throw new Error("[use-convex] Upload response missing storageId.");
11
+ }
12
+ export function postFileToUploadUrl(uploadUrl, file, onProgress) {
13
+ return new Promise((resolve, reject) => {
14
+ const xhr = new XMLHttpRequest();
15
+ xhr.open("POST", uploadUrl);
16
+ xhr.responseType = "json";
17
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
18
+ xhr.upload.onprogress = (event) => {
19
+ if (!event.lengthComputable || !onProgress) {
20
+ return;
21
+ }
22
+ if (event.total > 0) {
23
+ onProgress(event.loaded / event.total);
24
+ }
25
+ };
26
+ xhr.onload = () => {
27
+ if (xhr.status < 200 || xhr.status >= 300) {
28
+ reject(
29
+ new Error(
30
+ `[use-convex] File upload failed (${xhr.status} ${xhr.statusText || "error"}).`
31
+ )
32
+ );
33
+ return;
34
+ }
35
+ try {
36
+ const body = typeof xhr.response === "string" ? JSON.parse(xhr.response) : xhr.response;
37
+ resolve(parseStorageId(body));
38
+ } catch (cause) {
39
+ const err = cause instanceof Error ? cause : new Error(String(cause));
40
+ reject(err);
41
+ }
42
+ };
43
+ xhr.onerror = () => {
44
+ reject(new Error("[use-convex] File upload network error."));
45
+ };
46
+ xhr.onabort = () => {
47
+ reject(new Error("[use-convex] File upload aborted."));
48
+ };
49
+ xhr.send(file);
50
+ });
51
+ }
52
+ export function useConvexFileUpload(options) {
53
+ const ctx = useConvexContext();
54
+ const error = ref(null);
55
+ const pendingCount = ref(0);
56
+ const progress = ref(null);
57
+ const upload = async (file, extra) => {
58
+ if (import.meta.server || !ctx.client) {
59
+ throw new Error("[use-convex] useConvexFileUpload can only run in the browser.");
60
+ }
61
+ if (!(file instanceof File)) {
62
+ throw new TypeError("[use-convex] useConvexFileUpload expects a File.");
63
+ }
64
+ pendingCount.value++;
65
+ error.value = null;
66
+ progress.value = null;
67
+ try {
68
+ const uploadUrl = await ctx.client.mutation(
69
+ options.generateUploadUrl,
70
+ {}
71
+ );
72
+ if (!isUploadUrl(uploadUrl)) {
73
+ throw new Error("[use-convex] generateUploadUrl must return a non-empty string URL.");
74
+ }
75
+ const storageId = await postFileToUploadUrl(uploadUrl, file, (fraction) => {
76
+ progress.value = fraction;
77
+ });
78
+ progress.value = 1;
79
+ const saveArgs = {
80
+ storageId,
81
+ name: file.name,
82
+ contentType: file.type || "application/octet-stream",
83
+ size: file.size,
84
+ ...extra
85
+ };
86
+ return await ctx.client.mutation(options.saveFile, saveArgs);
87
+ } catch (cause) {
88
+ const err = cause instanceof Error ? cause : new Error(String(cause));
89
+ error.value = err;
90
+ throw err;
91
+ } finally {
92
+ pendingCount.value--;
93
+ progress.value = null;
94
+ }
95
+ };
96
+ return {
97
+ upload,
98
+ error,
99
+ pending: computed(() => pendingCount.value > 0),
100
+ progress: computed(() => progress.value)
101
+ };
102
+ }
@@ -8,14 +8,8 @@ export function useConvexGate() {
8
8
  isRefreshing: auth.isRefreshing,
9
9
  hasSsrSession: auth.hasSsrSession,
10
10
  showAuthedUi: auth.showAuthedUi,
11
- showLoading: computed(
12
- () => auth.isLoading.value && !auth.showAuthedUi.value
13
- ),
14
- showSignedOut: computed(
15
- () => !auth.showAuthedUi.value && !auth.isLoading.value
16
- ),
17
- showRefreshing: computed(
18
- () => auth.isAuthenticated.value && auth.isRefreshing.value
19
- )
11
+ showLoading: computed(() => auth.isLoading.value && !auth.showAuthedUi.value),
12
+ showSignedOut: computed(() => !auth.showAuthedUi.value && !auth.isLoading.value),
13
+ showRefreshing: computed(() => auth.isAuthenticated.value && auth.isRefreshing.value)
20
14
  };
21
15
  }