better-convex-nuxt 0.2.4 → 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.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "0.2.4",
7
+ "version": "0.2.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -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;
@@ -137,7 +136,7 @@ export function useConvexQuery(query, args, options) {
137
136
  // Simplified: != null covers both null and undefined
138
137
  );
139
138
  });
140
- let unsubscribeFn = null;
139
+ let registeredCacheKey = null;
141
140
  if (import.meta.client && subscribe) {
142
141
  const setupSubscription = () => {
143
142
  const currentArgs = getArgs();
@@ -149,12 +148,15 @@ export function useConvexQuery(query, args, options) {
149
148
  return;
150
149
  }
151
150
  const currentCacheKey = getCacheKey();
152
- if (hasSubscription(nuxtApp, currentCacheKey)) {
151
+ const existingEntry = getSubscription(nuxtApp, currentCacheKey);
152
+ if (existingEntry) {
153
+ existingEntry.refCount++;
154
+ registeredCacheKey = currentCacheKey;
153
155
  return;
154
156
  }
155
157
  try {
156
158
  updateCount = 0;
157
- unsubscribeFn = convex.onUpdate(
159
+ const unsubscribeFn = convex.onUpdate(
158
160
  query,
159
161
  currentArgs,
160
162
  (result) => {
@@ -196,6 +198,7 @@ export function useConvexQuery(query, args, options) {
196
198
  }
197
199
  );
198
200
  registerSubscription(nuxtApp, currentCacheKey, unsubscribeFn);
201
+ registeredCacheKey = currentCacheKey;
199
202
  logger.event({
200
203
  event: "subscription:change",
201
204
  env: "client",
@@ -240,17 +243,18 @@ export function useConvexQuery(query, args, options) {
240
243
  () => toValue(args),
241
244
  (newArgs, oldArgs) => {
242
245
  if (hashArgs(newArgs) !== hashArgs(oldArgs)) {
243
- if (oldArgs !== "skip" && unsubscribeFn) {
244
- const oldCacheKey = getQueryKey(query, oldArgs);
245
- logger.event({
246
- event: "subscription:change",
247
- env: "client",
248
- name: fnName,
249
- state: "unsubscribed",
250
- updates_received: updateCount
251
- });
252
- cleanupSubscription(nuxtApp, oldCacheKey);
253
- 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;
254
258
  }
255
259
  if (newArgs !== "skip") {
256
260
  setupSubscription();
@@ -261,19 +265,19 @@ export function useConvexQuery(query, args, options) {
261
265
  );
262
266
  }
263
267
  onUnmounted(() => {
264
- if (unsubscribeFn) {
265
- const currentCacheKey = getCacheKey();
266
- logger.event({
267
- event: "subscription:change",
268
- env: "client",
269
- name: fnName,
270
- state: "unsubscribed",
271
- updates_received: updateCount
272
- });
273
- removeFromSubscriptionCache(nuxtApp, currentCacheKey);
274
- unsubscribeFn();
275
- unsubscribeFn = null;
276
- 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;
277
281
  }
278
282
  });
279
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:"3bfccd49-ba9e-4cb1-a2c9-c8dd3a9b8463",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768401019499,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:"3bfccd49-ba9e-4cb1-a2c9-c8dd3a9b8463",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768401019499,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":"3bfccd49-ba9e-4cb1-a2c9-c8dd3a9b8463","timestamp":1768401017389}
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:"3bfccd49-ba9e-4cb1-a2c9-c8dd3a9b8463",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1768401019500,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>
@@ -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.4",
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",
@@ -1 +0,0 @@
1
- {"id":"3bfccd49-ba9e-4cb1-a2c9-c8dd3a9b8463","timestamp":1768401017389,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}