better-convex-nuxt 0.2.11 → 0.2.12

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/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "0.2.11",
7
+ "version": "0.2.12",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -1,4 +1,30 @@
1
+ import type { ConvexUser } from '../utils/types.js';
2
+ import type { createAuthClient } from 'better-auth/vue';
3
+ import type { ComputedRef, Ref } from 'vue';
1
4
  export type { ConvexUser } from '../utils/types.js';
5
+ type AuthClient = ReturnType<typeof createAuthClient>;
6
+ export interface UseConvexAuthReturn {
7
+ /** The JWT token for Convex authentication (readonly) */
8
+ token: Readonly<Ref<string | null>>;
9
+ /** The authenticated user data (readonly) */
10
+ user: Readonly<Ref<ConvexUser | null>>;
11
+ /** Whether the user is authenticated */
12
+ isAuthenticated: ComputedRef<boolean>;
13
+ /** Whether an auth operation is pending */
14
+ isPending: Readonly<Ref<boolean>>;
15
+ /** Auth error message if authentication failed (e.g., 401/403) */
16
+ authError: Readonly<Ref<string | null>>;
17
+ /**
18
+ * Signs out the user from both Better Auth and Convex.
19
+ * Clears local state immediately and calls Better Auth's signOut().
20
+ */
21
+ signOut: () => Promise<ReturnType<AuthClient['signOut']> extends Promise<infer T> ? T | null : null>;
22
+ /**
23
+ * Force refresh Convex auth state after login.
24
+ * Triggers fresh token fetch and updates reactive state.
25
+ */
26
+ refreshAuth: () => Promise<void>;
27
+ }
2
28
  /**
3
29
  * Composable for accessing Convex authentication state.
4
30
  *
@@ -30,54 +56,4 @@ export type { ConvexUser } from '../utils/types.js';
30
56
  * </template>
31
57
  * ```
32
58
  */
33
- export declare function useConvexAuth(): {
34
- /** The JWT token for Convex authentication (readonly) */
35
- token: Readonly<import("vue").Ref<string | null, string | null>>;
36
- /** The authenticated user data (readonly) */
37
- user: Readonly<import("vue").Ref<{
38
- readonly id: string;
39
- readonly name: string;
40
- readonly email: string;
41
- readonly emailVerified?: boolean | undefined;
42
- readonly image?: string | undefined;
43
- readonly createdAt?: string | undefined;
44
- readonly updatedAt?: string | undefined;
45
- } | null, {
46
- readonly id: string;
47
- readonly name: string;
48
- readonly email: string;
49
- readonly emailVerified?: boolean | undefined;
50
- readonly image?: string | undefined;
51
- readonly createdAt?: string | undefined;
52
- readonly updatedAt?: string | undefined;
53
- } | null>>;
54
- /** Whether the user is authenticated */
55
- isAuthenticated: import("vue").ComputedRef<boolean>;
56
- /** Whether an auth operation is pending */
57
- isPending: Readonly<import("vue").Ref<boolean, boolean>>;
58
- /** Auth error message if authentication failed (e.g., 401/403) */
59
- authError: Readonly<import("vue").Ref<string | null, string | null>>;
60
- /**
61
- * Signs out the user from both Better Auth and Convex.
62
- * Clears local state immediately and calls Better Auth's signOut().
63
- */
64
- signOut: () => Promise<{
65
- data: {
66
- success: boolean;
67
- };
68
- error: null;
69
- } | {
70
- data: null;
71
- error: {
72
- code?: string | undefined | undefined;
73
- message?: string | undefined | undefined;
74
- status: number;
75
- statusText: string;
76
- };
77
- } | null>;
78
- /**
79
- * Force refresh Convex auth state after login.
80
- * Triggers fresh token fetch and updates reactive state.
81
- */
82
- refreshAuth: () => Promise<void>;
83
- };
59
+ export declare function useConvexAuth(): UseConvexAuthReturn;
@@ -3,7 +3,7 @@ export function useConvexAuth() {
3
3
  const nuxtApp = useNuxtApp();
4
4
  const token = useState("convex:token", () => null);
5
5
  const user = useState("convex:user", () => null);
6
- const pending = useState("convex:pending", () => true);
6
+ const pending = useState("convex:pending", () => import.meta.client);
7
7
  const authError = useState("convex:authError", () => null);
8
8
  const isAuthenticated = computed(() => !!token.value && !!user.value);
9
9
  const signOut = async () => {
@@ -1,5 +1,5 @@
1
1
  import { useNuxtApp, useRuntimeConfig, useRequestEvent, useAsyncData, useState } from "#imports";
2
- import { computed, watch, triggerRef, onUnmounted, toValue, isRef, isReactive } from "vue";
2
+ import { computed, watch, triggerRef, onScopeDispose, getCurrentScope, toValue, isRef, isReactive } from "vue";
3
3
  import {
4
4
  getFunctionName,
5
5
  hashArgs,
@@ -7,9 +7,11 @@ import {
7
7
  parseConvexResponse,
8
8
  computeQueryStatus,
9
9
  fetchAuthToken,
10
+ createQueryBridge,
10
11
  registerSubscription,
11
12
  getSubscription,
12
- releaseSubscription
13
+ releaseSubscription,
14
+ ensureQueryBridge
13
15
  } from "../utils/convex-cache.js";
14
16
  import { createLogger, getLogLevel } from "../utils/logger.js";
15
17
  let devToolsRegistry = null;
@@ -137,7 +139,57 @@ export function useConvexQuery(query, args, options) {
137
139
  );
138
140
  });
139
141
  let registeredCacheKey = null;
140
- if (import.meta.client && subscribe) {
142
+ const cleanupScope = import.meta.client && subscribe ? getCurrentScope() : void 0;
143
+ let stopSharedDataWatch = null;
144
+ let stopSharedErrorWatch = null;
145
+ const cleanupSharedBridgeWatchers = () => {
146
+ if (stopSharedDataWatch) {
147
+ stopSharedDataWatch();
148
+ stopSharedDataWatch = null;
149
+ }
150
+ if (stopSharedErrorWatch) {
151
+ stopSharedErrorWatch();
152
+ stopSharedErrorWatch = null;
153
+ }
154
+ };
155
+ const attachSharedBridge = (entry) => {
156
+ cleanupSharedBridgeWatchers();
157
+ const bridge = ensureQueryBridge(entry);
158
+ const syncDataFromBridge = () => {
159
+ if (!bridge.hasRawData) return;
160
+ const transformedResult = applyTransform(bridge.rawData);
161
+ asyncData.data.value = transformedResult;
162
+ if (asyncData.error.value !== null) {
163
+ ;
164
+ asyncData.error.value = null;
165
+ }
166
+ triggerRef(asyncData.data);
167
+ };
168
+ const syncErrorFromBridge = () => {
169
+ const err = bridge.error;
170
+ if (!err) return;
171
+ const hasData = asyncData.data.value !== null && asyncData.data.value !== void 0;
172
+ if (!hasData) {
173
+ ;
174
+ asyncData.error.value = err;
175
+ }
176
+ };
177
+ stopSharedDataWatch = watch(
178
+ () => bridge.dataVersion.value,
179
+ () => {
180
+ syncDataFromBridge();
181
+ }
182
+ );
183
+ stopSharedErrorWatch = watch(
184
+ () => bridge.errorVersion.value,
185
+ () => {
186
+ syncErrorFromBridge();
187
+ }
188
+ );
189
+ syncDataFromBridge();
190
+ syncErrorFromBridge();
191
+ };
192
+ if (import.meta.client && subscribe && cleanupScope) {
141
193
  const setupSubscription = () => {
142
194
  const currentArgs = getArgs();
143
195
  if (currentArgs === "skip") {
@@ -152,21 +204,20 @@ export function useConvexQuery(query, args, options) {
152
204
  if (existingEntry) {
153
205
  existingEntry.refCount++;
154
206
  registeredCacheKey = currentCacheKey;
207
+ attachSharedBridge(existingEntry);
155
208
  logger.query({ name: fnName, event: "share", refCount: existingEntry.refCount, args: currentArgs });
156
209
  return;
157
210
  }
158
211
  try {
212
+ const localBridge = createQueryBridge();
159
213
  const unsubscribeFn = convex.onUpdate(
160
214
  query,
161
215
  currentArgs,
162
216
  (result) => {
163
- const transformedResult = applyTransform(result);
164
- asyncData.data.value = transformedResult;
165
- if (asyncData.error.value !== null) {
166
- ;
167
- asyncData.error.value = null;
168
- }
169
- triggerRef(asyncData.data);
217
+ localBridge.rawData = result;
218
+ localBridge.hasRawData = true;
219
+ localBridge.error = null;
220
+ localBridge.dataVersion.value += 1;
170
221
  logger.query({
171
222
  name: fnName,
172
223
  event: "update",
@@ -177,18 +228,15 @@ export function useConvexQuery(query, args, options) {
177
228
  if (import.meta.dev && devToolsRegistry) {
178
229
  devToolsRegistry.updateQueryStatus(currentCacheKey, {
179
230
  status: "success",
180
- data: transformedResult,
231
+ data: result,
181
232
  dataSource: "websocket"
182
233
  });
183
234
  }
184
235
  },
185
236
  (err) => {
237
+ localBridge.error = err;
238
+ localBridge.errorVersion.value += 1;
186
239
  logger.query({ name: fnName, event: "error", error: err });
187
- const hasData = asyncData.data.value !== null && asyncData.data.value !== void 0;
188
- if (!hasData) {
189
- ;
190
- asyncData.error.value = err;
191
- }
192
240
  if (import.meta.dev && devToolsRegistry) {
193
241
  devToolsRegistry.updateQueryStatus(currentCacheKey, {
194
242
  status: "error",
@@ -198,7 +246,13 @@ export function useConvexQuery(query, args, options) {
198
246
  }
199
247
  );
200
248
  registerSubscription(nuxtApp, currentCacheKey, unsubscribeFn);
249
+ const registeredEntry = getSubscription(nuxtApp, currentCacheKey);
250
+ if (!registeredEntry) {
251
+ throw new Error("[useConvexQuery] Failed to register subscription entry");
252
+ }
253
+ registeredEntry.queryBridge = localBridge;
201
254
  registeredCacheKey = currentCacheKey;
255
+ attachSharedBridge(registeredEntry);
202
256
  logger.query({ name: fnName, event: "subscribe", args: currentArgs });
203
257
  if (import.meta.dev && devToolsRegistry) {
204
258
  devToolsRegistry.registerQuery({
@@ -229,6 +283,7 @@ export function useConvexQuery(query, args, options) {
229
283
  (newArgs, oldArgs) => {
230
284
  if (hashArgs(newArgs) !== hashArgs(oldArgs)) {
231
285
  if (oldArgs !== "skip" && registeredCacheKey) {
286
+ cleanupSharedBridgeWatchers();
232
287
  const wasUnsubscribed = releaseSubscription(nuxtApp, registeredCacheKey);
233
288
  if (wasUnsubscribed) {
234
289
  logger.query({ name: fnName, event: "unsubscribe" });
@@ -246,8 +301,9 @@ export function useConvexQuery(query, args, options) {
246
301
  { deep: true }
247
302
  );
248
303
  }
249
- onUnmounted(() => {
304
+ onScopeDispose(() => {
250
305
  if (registeredCacheKey) {
306
+ cleanupSharedBridgeWatchers();
251
307
  const wasUnsubscribed = releaseSubscription(nuxtApp, registeredCacheKey);
252
308
  if (wasUnsubscribed) {
253
309
  logger.query({ name: fnName, event: "unsubscribe" });
@@ -256,8 +312,14 @@ export function useConvexQuery(query, args, options) {
256
312
  }
257
313
  }
258
314
  registeredCacheKey = null;
315
+ } else {
316
+ cleanupSharedBridgeWatchers();
259
317
  }
260
318
  });
319
+ } else if (import.meta.client && subscribe && import.meta.dev) {
320
+ console.warn(
321
+ `[useConvexQuery] Real-time subscriptions require a Vue component/effect scope for cleanup. Detected usage outside a component (e.g. route middleware/plugin). Falling back to one-shot query only. Pass { subscribe: false } to silence this warning.`
322
+ );
261
323
  }
262
324
  let resolvePromise;
263
325
  if (isSkipped.value) {
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Convex DevTools</title><link rel="stylesheet" href="/__convex_devtools__/_nuxt/entry.BiOLMZBG.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/BhO0ov6K.js"><script type="module" src="/__convex_devtools__/_nuxt/BhO0ov6K.js" crossorigin></script><script id="unhead:payload" type="application/json">{"title":"Convex DevTools"}</script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__convex_devtools__",buildId:"4441cf51-019c-4d8d-b387-2c168e8d08e3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772107968698,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Convex DevTools</title><link rel="stylesheet" href="/__convex_devtools__/_nuxt/entry.BiOLMZBG.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/BhO0ov6K.js"><script type="module" src="/__convex_devtools__/_nuxt/BhO0ov6K.js" crossorigin></script><script id="unhead:payload" type="application/json">{"title":"Convex DevTools"}</script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__convex_devtools__",buildId:"a5e03a13-122d-4d8e-b569-24b5d8908506",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772128215997,false]</script></body></html>
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Convex DevTools</title><link rel="stylesheet" href="/__convex_devtools__/_nuxt/entry.BiOLMZBG.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/BhO0ov6K.js"><script type="module" src="/__convex_devtools__/_nuxt/BhO0ov6K.js" crossorigin></script><script id="unhead:payload" type="application/json">{"title":"Convex DevTools"}</script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__convex_devtools__",buildId:"4441cf51-019c-4d8d-b387-2c168e8d08e3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772107968699,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Convex DevTools</title><link rel="stylesheet" href="/__convex_devtools__/_nuxt/entry.BiOLMZBG.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/BhO0ov6K.js"><script type="module" src="/__convex_devtools__/_nuxt/BhO0ov6K.js" crossorigin></script><script id="unhead:payload" type="application/json">{"title":"Convex DevTools"}</script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__convex_devtools__",buildId:"a5e03a13-122d-4d8e-b569-24b5d8908506",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772128215997,false]</script></body></html>
@@ -1 +1 @@
1
- {"id":"4441cf51-019c-4d8d-b387-2c168e8d08e3","timestamp":1772107966161}
1
+ {"id":"a5e03a13-122d-4d8e-b569-24b5d8908506","timestamp":1772128213809}
@@ -0,0 +1 @@
1
+ {"id":"a5e03a13-122d-4d8e-b569-24b5d8908506","timestamp":1772128213809,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Convex DevTools</title><link rel="stylesheet" href="/__convex_devtools__/_nuxt/entry.BiOLMZBG.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/BhO0ov6K.js"><script type="module" src="/__convex_devtools__/_nuxt/BhO0ov6K.js" crossorigin></script><script id="unhead:payload" type="application/json">{"title":"Convex DevTools"}</script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__convex_devtools__",buildId:"4441cf51-019c-4d8d-b387-2c168e8d08e3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772107968699,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Convex DevTools</title><link rel="stylesheet" href="/__convex_devtools__/_nuxt/entry.BiOLMZBG.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/BhO0ov6K.js"><script type="module" src="/__convex_devtools__/_nuxt/BhO0ov6K.js" crossorigin></script><script id="unhead:payload" type="application/json">{"title":"Convex DevTools"}</script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__convex_devtools__",buildId:"a5e03a13-122d-4d8e-b569-24b5d8908506",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772128215997,false]</script></body></html>
@@ -1,4 +1,5 @@
1
1
  import type { useNuxtApp } from '#app';
2
+ import { type ShallowRef } from 'vue';
2
3
  export { type QueryStatus, parseConvexResponse, computeQueryStatus, getFunctionName, hashArgs, getQueryKey, } from './convex-shared.js';
3
4
  type NuxtApp = ReturnType<typeof useNuxtApp>;
4
5
  /**
@@ -8,11 +9,30 @@ type NuxtApp = ReturnType<typeof useNuxtApp>;
8
9
  export interface SubscriptionEntry {
9
10
  unsubscribe: () => void;
10
11
  refCount: number;
12
+ queryBridge?: QuerySubscriptionBridge;
13
+ }
14
+ /**
15
+ * Shared query state for deduplicated useConvexQuery subscribers.
16
+ * Stores raw subscription data and reactive version counters so each subscriber
17
+ * can sync into its own local asyncData refs with its own transform().
18
+ */
19
+ export interface QuerySubscriptionBridge {
20
+ rawData: unknown;
21
+ hasRawData: boolean;
22
+ dataVersion: ShallowRef<number>;
23
+ error: Error | null;
24
+ errorVersion: ShallowRef<number>;
11
25
  }
12
26
  /**
13
27
  * Subscription cache stored on NuxtApp
14
28
  */
15
29
  export type SubscriptionCache = Record<string, SubscriptionEntry | undefined>;
30
+ export declare function createQueryBridge(): QuerySubscriptionBridge;
31
+ /**
32
+ * Ensure a deduplicated query subscription has a shared bridge payload.
33
+ * Used by useConvexQuery to fan out subscription updates to all subscribers.
34
+ */
35
+ export declare function ensureQueryBridge(entry: SubscriptionEntry): QuerySubscriptionBridge;
16
36
  export interface FetchAuthTokenOptions {
17
37
  /** Whether this is a public query (skip auth) */
18
38
  isPublic: boolean;
@@ -1,3 +1,4 @@
1
+ import { shallowRef } from "vue";
1
2
  export {
2
3
  parseConvexResponse,
3
4
  computeQueryStatus,
@@ -6,6 +7,21 @@ export {
6
7
  getQueryKey
7
8
  } from "./convex-shared.js";
8
9
  const subscriptionRegistry = /* @__PURE__ */ new WeakMap();
10
+ export function createQueryBridge() {
11
+ return {
12
+ rawData: void 0,
13
+ hasRawData: false,
14
+ dataVersion: shallowRef(0),
15
+ error: null,
16
+ errorVersion: shallowRef(0)
17
+ };
18
+ }
19
+ export function ensureQueryBridge(entry) {
20
+ if (!entry.queryBridge) {
21
+ entry.queryBridge = createQueryBridge();
22
+ }
23
+ return entry.queryBridge;
24
+ }
9
25
  export async function fetchAuthToken(options) {
10
26
  const { isPublic, cookieHeader, siteUrl, cachedToken } = options;
11
27
  if (isPublic) {
@@ -34,13 +34,21 @@ export function decodeUserFromJwt(token) {
34
34
  if (!payload.sub && !payload.userId && !payload.email) {
35
35
  return null;
36
36
  }
37
- return {
37
+ const user = {
38
38
  id: String(payload.sub || payload.userId || ""),
39
39
  name: String(payload.name || ""),
40
40
  email: String(payload.email || ""),
41
41
  emailVerified: typeof payload.emailVerified === "boolean" ? payload.emailVerified : void 0,
42
42
  image: typeof payload.image === "string" ? payload.image : void 0
43
43
  };
44
+ for (const [key, value] of Object.entries(payload)) {
45
+ if (key === "sub" || key === "userId" || key === "name" || key === "email" || key === "emailVerified" || key === "image" || key === "iss" || key === "aud" || key === "exp" || key === "nbf" || key === "iat" || key === "jti") {
46
+ continue;
47
+ }
48
+ ;
49
+ user[key] = value;
50
+ }
51
+ return user;
44
52
  }
45
53
  export function parseConvexResponse(response) {
46
54
  if (response && typeof response === "object") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-convex-nuxt",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Full-featured Convex integration for Nuxt with SSR, real-time subscriptions, authentication, and permissions",
5
5
  "keywords": [
6
6
  "authentication",
@@ -1 +0,0 @@
1
- {"id":"4441cf51-019c-4d8d-b387-2c168e8d08e3","timestamp":1772107966161,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}