better-convex-nuxt 0.2.3 → 0.2.5

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.d.mts CHANGED
@@ -36,7 +36,7 @@ interface AuthCacheOptions {
36
36
  interface ModuleOptions {
37
37
  /** Convex deployment URL (WebSocket) - e.g., https://your-app.convex.cloud */
38
38
  url?: string;
39
- /** Convex site URL (HTTP/Auth) - e.g., https://your-app.convex.site. Auto-derived from url if not set. */
39
+ /** Convex site URL (HTTP/Auth) - e.g., https://your-app.convex.site. Required for authentication. */
40
40
  siteUrl?: string;
41
41
  /**
42
42
  * Additional trusted origins for CORS validation on the auth proxy.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "0.2.3",
7
+ "version": "0.2.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -42,7 +42,7 @@ const module$1 = defineNuxtModule({
42
42
  if (options.siteUrl && !isValidUrl(options.siteUrl)) {
43
43
  logger.warn(`Invalid Convex site URL format: "${options.siteUrl}". Expected a valid URL like "https://your-app.convex.site"`);
44
44
  }
45
- const derivedSiteUrl = options.siteUrl || (options.url?.replace(".convex.cloud", ".convex.site") ?? "");
45
+ const derivedSiteUrl = options.siteUrl || "";
46
46
  const convexConfig = defu(
47
47
  nuxt.options.runtimeConfig.public.convex,
48
48
  {
@@ -8,9 +8,8 @@ import {
8
8
  computeQueryStatus,
9
9
  fetchAuthToken,
10
10
  registerSubscription,
11
- hasSubscription,
12
- removeFromSubscriptionCache,
13
- cleanupSubscription
11
+ getSubscription,
12
+ releaseSubscription
14
13
  } from "../utils/convex-cache.js";
15
14
  import { createModuleLogger, getLoggingOptions } from "../utils/logger.js";
16
15
  let devToolsRegistryPromise = null;
@@ -130,12 +129,14 @@ export function useConvexQuery(query, args, options) {
130
129
  const status = computed(() => {
131
130
  return computeQueryStatus(
132
131
  isSkipped.value,
133
- asyncData.error.value !== null,
132
+ asyncData.error.value != null,
133
+ // != catches both null AND undefined (strict !== would fail on undefined)
134
134
  pending.value,
135
- asyncData.data.value !== null && asyncData.data.value !== void 0
135
+ asyncData.data.value != null
136
+ // Simplified: != null covers both null and undefined
136
137
  );
137
138
  });
138
- let unsubscribeFn = null;
139
+ let registeredCacheKey = null;
139
140
  if (import.meta.client && subscribe) {
140
141
  const setupSubscription = () => {
141
142
  const currentArgs = getArgs();
@@ -147,12 +148,15 @@ export function useConvexQuery(query, args, options) {
147
148
  return;
148
149
  }
149
150
  const currentCacheKey = getCacheKey();
150
- if (hasSubscription(nuxtApp, currentCacheKey)) {
151
+ const existingEntry = getSubscription(nuxtApp, currentCacheKey);
152
+ if (existingEntry) {
153
+ existingEntry.refCount++;
154
+ registeredCacheKey = currentCacheKey;
151
155
  return;
152
156
  }
153
157
  try {
154
158
  updateCount = 0;
155
- unsubscribeFn = convex.onUpdate(
159
+ const unsubscribeFn = convex.onUpdate(
156
160
  query,
157
161
  currentArgs,
158
162
  (result) => {
@@ -194,6 +198,7 @@ export function useConvexQuery(query, args, options) {
194
198
  }
195
199
  );
196
200
  registerSubscription(nuxtApp, currentCacheKey, unsubscribeFn);
201
+ registeredCacheKey = currentCacheKey;
197
202
  logger.event({
198
203
  event: "subscription:change",
199
204
  env: "client",
@@ -238,17 +243,18 @@ export function useConvexQuery(query, args, options) {
238
243
  () => toValue(args),
239
244
  (newArgs, oldArgs) => {
240
245
  if (hashArgs(newArgs) !== hashArgs(oldArgs)) {
241
- if (oldArgs !== "skip" && unsubscribeFn) {
242
- const oldCacheKey = getQueryKey(query, oldArgs);
243
- logger.event({
244
- event: "subscription:change",
245
- env: "client",
246
- name: fnName,
247
- state: "unsubscribed",
248
- updates_received: updateCount
249
- });
250
- cleanupSubscription(nuxtApp, oldCacheKey);
251
- unsubscribeFn = null;
246
+ if (oldArgs !== "skip" && registeredCacheKey) {
247
+ const wasUnsubscribed = releaseSubscription(nuxtApp, registeredCacheKey);
248
+ if (wasUnsubscribed) {
249
+ logger.event({
250
+ event: "subscription:change",
251
+ env: "client",
252
+ name: fnName,
253
+ state: "unsubscribed",
254
+ updates_received: updateCount
255
+ });
256
+ }
257
+ registeredCacheKey = null;
252
258
  }
253
259
  if (newArgs !== "skip") {
254
260
  setupSubscription();
@@ -259,19 +265,19 @@ export function useConvexQuery(query, args, options) {
259
265
  );
260
266
  }
261
267
  onUnmounted(() => {
262
- if (unsubscribeFn) {
263
- const currentCacheKey = getCacheKey();
264
- logger.event({
265
- event: "subscription:change",
266
- env: "client",
267
- name: fnName,
268
- state: "unsubscribed",
269
- updates_received: updateCount
270
- });
271
- removeFromSubscriptionCache(nuxtApp, currentCacheKey);
272
- unsubscribeFn();
273
- unsubscribeFn = null;
274
- devToolsRegistry?.unregisterQuery(currentCacheKey);
268
+ if (registeredCacheKey) {
269
+ const wasUnsubscribed = releaseSubscription(nuxtApp, registeredCacheKey);
270
+ if (wasUnsubscribed) {
271
+ logger.event({
272
+ event: "subscription:change",
273
+ env: "client",
274
+ name: fnName,
275
+ state: "unsubscribed",
276
+ updates_received: updateCount
277
+ });
278
+ devToolsRegistry?.unregisterQuery(registeredCacheKey);
279
+ }
280
+ registeredCacheKey = null;
275
281
  }
276
282
  });
277
283
  }
@@ -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.CXPHzcHp.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/DRR5kXdS.js"><script type="module" src="/__convex_devtools__/_nuxt/DRR5kXdS.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:"2402f38c-d12f-4c0b-846f-2ad2f104951f",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768333345265,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.CXPHzcHp.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/DRR5kXdS.js"><script type="module" src="/__convex_devtools__/_nuxt/DRR5kXdS.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:"e830e57d-37c5-4c85-b066-5cda3df527f3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768510467224,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.CXPHzcHp.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/DRR5kXdS.js"><script type="module" src="/__convex_devtools__/_nuxt/DRR5kXdS.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:"2402f38c-d12f-4c0b-846f-2ad2f104951f",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768333345266,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.CXPHzcHp.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/DRR5kXdS.js"><script type="module" src="/__convex_devtools__/_nuxt/DRR5kXdS.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:"e830e57d-37c5-4c85-b066-5cda3df527f3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768510467224,false]</script></body></html>
@@ -1 +1 @@
1
- {"id":"2402f38c-d12f-4c0b-846f-2ad2f104951f","timestamp":1768333343049}
1
+ {"id":"e830e57d-37c5-4c85-b066-5cda3df527f3","timestamp":1768510464889}
@@ -0,0 +1 @@
1
+ {"id":"e830e57d-37c5-4c85-b066-5cda3df527f3","timestamp":1768510464889,"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.CXPHzcHp.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/DRR5kXdS.js"><script type="module" src="/__convex_devtools__/_nuxt/DRR5kXdS.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:"2402f38c-d12f-4c0b-846f-2ad2f104951f",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768333345266,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.CXPHzcHp.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__convex_devtools__/_nuxt/DRR5kXdS.js"><script type="module" src="/__convex_devtools__/_nuxt/DRR5kXdS.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:"e830e57d-37c5-4c85-b066-5cda3df527f3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768510467224,false]</script></body></html>
@@ -34,7 +34,7 @@ export default defineNuxtPlugin((nuxtApp) => {
34
34
  if (nuxtApp._convexInitialized) return;
35
35
  nuxtApp._convexInitialized = true;
36
36
  const convexUrl = config.public.convex?.url;
37
- const siteUrl = config.public.convex?.siteUrl || convexUrl?.replace(".convex.cloud", ".convex.site");
37
+ const siteUrl = config.public.convex?.siteUrl;
38
38
  if (!convexUrl) {
39
39
  logger.event({
40
40
  event: "plugin:init",
@@ -3,6 +3,25 @@ import type { ConvexClient } from 'convex/browser'
3
3
 
4
4
  type AuthClient = ReturnType<typeof createAuthClient>
5
5
 
6
+ /**
7
+ * Convex module public runtime config
8
+ */
9
+ export interface ConvexPublicRuntimeConfig {
10
+ /** Convex deployment URL (WebSocket) */
11
+ url?: string
12
+ /** Convex site URL (HTTP/Auth) */
13
+ siteUrl?: string
14
+ /** Routes that should skip auth checks */
15
+ skipAuthRoutes?: string[]
16
+ /** Logging options */
17
+ logging?: {
18
+ enabled?: boolean | 'debug'
19
+ format?: 'pretty' | 'json'
20
+ }
21
+ /** Index signature for compatibility with Record<string, unknown> */
22
+ [key: string]: unknown
23
+ }
24
+
6
25
  declare module '#app' {
7
26
  interface NuxtApp {
8
27
  $convex: ConvexClient
@@ -19,4 +38,19 @@ declare module 'vue' {
19
38
  }
20
39
  }
21
40
 
41
+ declare module 'nuxt/schema' {
42
+ interface PublicRuntimeConfig {
43
+ convex?: ConvexPublicRuntimeConfig
44
+ }
45
+ interface RuntimeConfig {
46
+ convexDevtoolsPath?: string
47
+ }
48
+ interface NuxtConfig {
49
+ convex?: ConvexPublicRuntimeConfig
50
+ }
51
+ interface NuxtOptions {
52
+ convex?: ConvexPublicRuntimeConfig
53
+ }
54
+ }
55
+
22
56
  export {}
@@ -1,10 +1,18 @@
1
1
  import { type useNuxtApp } from '#app';
2
2
  export { type QueryStatus, parseConvexResponse, computeQueryStatus, getFunctionName, hashArgs, getQueryKey, } from './convex-shared.js';
3
3
  type NuxtApp = ReturnType<typeof useNuxtApp>;
4
+ /**
5
+ * Subscription entry with reference counting.
6
+ * Multiple components can share the same subscription.
7
+ */
8
+ export interface SubscriptionEntry {
9
+ unsubscribe: () => void;
10
+ refCount: number;
11
+ }
4
12
  /**
5
13
  * Subscription cache stored on NuxtApp
6
14
  */
7
- export type SubscriptionCache = Record<string, (() => void) | undefined>;
15
+ export type SubscriptionCache = Record<string, SubscriptionEntry | undefined>;
8
16
  export interface FetchAuthTokenOptions {
9
17
  /** Whether this is a public query (skip auth) */
10
18
  isPublic: boolean;
@@ -58,13 +66,15 @@ export declare function getCachedAuthToken(): string | undefined;
58
66
  */
59
67
  export declare function getSubscriptionCache(nuxtApp: NuxtApp): SubscriptionCache;
60
68
  /**
61
- * Register a subscription in the cache.
69
+ * Register a subscription in the cache with reference counting.
70
+ * If a subscription already exists, increments the ref count instead of replacing.
62
71
  *
63
72
  * @param nuxtApp - The NuxtApp instance
64
73
  * @param cacheKey - Unique key for this subscription
65
74
  * @param unsubscribe - The unsubscribe function
75
+ * @returns true if this component should manage the subscription (first registrant), false if joining existing
66
76
  */
67
- export declare function registerSubscription(nuxtApp: NuxtApp, cacheKey: string, unsubscribe: () => void): void;
77
+ export declare function registerSubscription(nuxtApp: NuxtApp, cacheKey: string, unsubscribe: () => void): boolean;
68
78
  /**
69
79
  * Check if a subscription already exists in the cache.
70
80
  *
@@ -73,23 +83,35 @@ export declare function registerSubscription(nuxtApp: NuxtApp, cacheKey: string,
73
83
  * @returns True if subscription exists
74
84
  */
75
85
  export declare function hasSubscription(nuxtApp: NuxtApp, cacheKey: string): boolean;
86
+ /**
87
+ * Get the current subscription entry from the cache.
88
+ *
89
+ * @param nuxtApp - The NuxtApp instance
90
+ * @param cacheKey - Unique key for this subscription
91
+ * @returns The subscription entry if it exists, undefined otherwise
92
+ */
93
+ export declare function getSubscription(nuxtApp: NuxtApp, cacheKey: string): SubscriptionEntry | undefined;
94
+ /**
95
+ * Decrement reference count and cleanup subscription if no more references.
96
+ * Returns true if the subscription was actually unsubscribed.
97
+ *
98
+ * @param nuxtApp - The NuxtApp instance
99
+ * @param cacheKey - Unique key for this subscription
100
+ * @returns true if subscription was unsubscribed, false if still has references
101
+ */
102
+ export declare function releaseSubscription(nuxtApp: NuxtApp, cacheKey: string): boolean;
76
103
  /**
77
104
  * Clean up a subscription from the cache.
78
105
  * Calls the unsubscribe function and removes from cache.
106
+ * DEPRECATED: Use releaseSubscription for ref-counted cleanup.
79
107
  *
80
108
  * @param nuxtApp - The NuxtApp instance
81
109
  * @param cacheKey - Unique key for this subscription
82
- *
83
- * @example
84
- * ```ts
85
- * // In cleanup/unmount
86
- * cleanupSubscription(nuxtApp, cacheKey)
87
- * ```
88
110
  */
89
111
  export declare function cleanupSubscription(nuxtApp: NuxtApp, cacheKey: string): void;
90
112
  /**
91
113
  * Remove a subscription from the cache without calling unsubscribe.
92
- * Use this when you've already called unsubscribe manually.
114
+ * DEPRECATED: Use releaseSubscription for ref-counted cleanup.
93
115
  *
94
116
  * @param nuxtApp - The NuxtApp instance
95
117
  * @param cacheKey - Unique key for this subscription
@@ -46,17 +46,41 @@ export function getSubscriptionCache(nuxtApp) {
46
46
  }
47
47
  export function registerSubscription(nuxtApp, cacheKey, unsubscribe) {
48
48
  const cache = getSubscriptionCache(nuxtApp);
49
- cache[cacheKey] = unsubscribe;
49
+ const existing = cache[cacheKey];
50
+ if (existing) {
51
+ existing.refCount++;
52
+ return false;
53
+ }
54
+ cache[cacheKey] = { unsubscribe, refCount: 1 };
55
+ return true;
50
56
  }
51
57
  export function hasSubscription(nuxtApp, cacheKey) {
52
58
  const cache = getSubscriptionCache(nuxtApp);
53
59
  return !!cache[cacheKey];
54
60
  }
61
+ export function getSubscription(nuxtApp, cacheKey) {
62
+ const cache = getSubscriptionCache(nuxtApp);
63
+ return cache[cacheKey];
64
+ }
65
+ export function releaseSubscription(nuxtApp, cacheKey) {
66
+ const cache = getSubscriptionCache(nuxtApp);
67
+ const entry = cache[cacheKey];
68
+ if (!entry) {
69
+ return false;
70
+ }
71
+ entry.refCount--;
72
+ if (entry.refCount <= 0) {
73
+ entry.unsubscribe();
74
+ cache[cacheKey] = void 0;
75
+ return true;
76
+ }
77
+ return false;
78
+ }
55
79
  export function cleanupSubscription(nuxtApp, cacheKey) {
56
80
  const cache = getSubscriptionCache(nuxtApp);
57
- const unsubscribe = cache[cacheKey];
58
- if (unsubscribe) {
59
- unsubscribe();
81
+ const entry = cache[cacheKey];
82
+ if (entry) {
83
+ entry.unsubscribe();
60
84
  cache[cacheKey] = void 0;
61
85
  }
62
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-convex-nuxt",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Full-featured Convex integration for Nuxt with SSR, real-time subscriptions, authentication, and permissions",
5
5
  "keywords": [
6
6
  "authentication",
@@ -57,7 +57,7 @@
57
57
  "dev": "npm run dev:prepare && nuxi dev playground",
58
58
  "dev:build": "nuxi build playground",
59
59
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
60
- "release": "npm run lint && npm run test && npm run prepack && git fetch --prune --prune-tags --tags --force origin && changelogen --release && npm publish && git push --follow-tags",
60
+ "release": "npm run lint && npm run test && npm run prepack && git tag -l | xargs git tag -d && git fetch --tags origin && changelogen --release && npm publish && git push --follow-tags",
61
61
  "lint": "eslint .",
62
62
  "format": "oxfmt",
63
63
  "format:check": "oxfmt --check",
@@ -1 +0,0 @@
1
- {"id":"2402f38c-d12f-4c0b-846f-2ad2f104951f","timestamp":1768333343049,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}