@cavulsqa/reactive-vue 0.2.0 → 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.
package/README.md CHANGED
@@ -7,8 +7,6 @@ copy between apps.
7
7
  `useStaticQuery`, bound to Vue's lifecycle: fetch on mount, refetch on table change (debounced),
8
8
  in-flight deduplication by query key, retry with backoff, stale-while-revalidate, a per-instance
9
9
  cache window, cancel on unmount, and a deferred first read via a reactive `enabled`.
10
- - `uniqueQueryKey(prefix)` — a key that is never shared, so a query is never deduplicated against
11
- another.
12
10
  - An injectable `logger`, so failures reach your error service rather than only the console.
13
11
  - `createVueQueryMetrics()` — the recorder with its state in `reactive()`, plus a `useQueryMetrics`
14
12
  composable of derived counters for a dev-tools panel.
@@ -51,7 +49,7 @@ Then, at a call site:
51
49
  ```ts
52
50
  const { data, loading, error, refetch } = useReactiveQuery(
53
51
  () => rdb.selectFrom("sale_order").selectAll().execute(),
54
- { tables: ["sale_order"], queryKey: uniqueQueryKey("sale_order:list") },
52
+ { tables: ["sale_order"], queryKey: ["sale_order:list"] },
55
53
  );
56
54
  ```
57
55
 
@@ -66,15 +64,33 @@ createReactiveQuery({
66
64
  });
67
65
  ```
68
66
 
69
- ## queryKey is an identity, not a label
67
+ ## queryKey is an identity, built from arguments
70
68
 
71
- Two mounted queries sharing a `queryKey` await a single request and share its result. That is
72
- correct for the same list rendered twice, and wrong for two different queries that happen to be
73
- named alike — the second is handed the first's rows.
69
+ A key is an array, and two mounted queries whose keys hash alike await one request and share its
70
+ result. That is what you want — the same list rendered twice costs one query and it is only safe
71
+ because the key carries the arguments:
74
72
 
75
- So: `uniqueQueryKey("sale_order:list")` unless sharing is the point, in which case use a stable
76
- literal. As a backstop, mounting the same key against different `tables` logs a warning; pass
77
- `warnOnKeyConflict: false` to silence it.
73
+ ```ts
74
+ queryKey: ["sale_order", id]; // two detail pages, two identities
75
+ queryKey: ["sale_order:list"]; // one list, shared wherever it appears
76
+ ```
77
+
78
+ Refs inside a key are unwrapped and tracked, so the key can follow a filter. When it moves, the
79
+ query re-runs through the same debounce a table change uses — no `refetch()` on every keystroke:
80
+
81
+ ```ts
82
+ const term = ref("");
83
+ useReactiveQuery(() => search(term.value), {
84
+ tables: ["sale_order"],
85
+ queryKey: ["sale_order:search", term],
86
+ debounce: 200,
87
+ });
88
+ ```
89
+
90
+ A key may hold anything JSON can represent; object field order does not matter. A function or a
91
+ symbol throws rather than hashing to the same string as every other one. As a backstop, mounting
92
+ the same key against different `tables` logs a warning; pass `warnOnKeyConflict: false` to silence
93
+ it.
78
94
 
79
95
  ## What this does not do
80
96
 
@@ -96,7 +112,7 @@ competes for the one native database thread while the first screen loads:
96
112
  ```ts
97
113
  useReactiveQuery(load, {
98
114
  tables: ["partner"],
99
- queryKey: uniqueQueryKey("partner:list"),
115
+ queryKey: ["partner:list"],
100
116
  enabled: isTabActive, // a ref - activation starts the read and the subscription together
101
117
  });
102
118
  ```
@@ -1,4 +1,4 @@
1
- import { Ref } from "vue";
1
+ import { ComputedRef, Ref } from "vue";
2
2
  //#region src/framework7.d.ts
3
3
  /**
4
4
  * Framework7 emits these on the `.page` element. An app on another router passes its own.
@@ -17,8 +17,6 @@ declare function providePageVisibility(options?: ProvidePageVisibilityOptions):
17
17
  * The provider's own component reads its ref directly: `provide` is not visible to the component
18
18
  * that called it.
19
19
  */
20
- declare function usePageVisibility(): {
21
- value: boolean;
22
- };
20
+ declare function usePageVisibility(): Ref<boolean> | ComputedRef<boolean>;
23
21
  //#endregion
24
22
  export { FRAMEWORK7_PAGE_EVENTS, PageVisibilityEvents, ProvidePageVisibilityOptions, providePageVisibility, usePageVisibility };
@@ -1,11 +1,11 @@
1
- import { getCurrentInstance, inject, onBeforeUnmount, onMounted, provide, ref } from "vue";
1
+ import { computed, getCurrentInstance, inject, onBeforeUnmount, onMounted, provide, ref } from "vue";
2
2
  //#region src/framework7.ts
3
3
  const FRAMEWORK7_PAGE_EVENTS = {
4
4
  show: ["page:beforein", "page:tabshow"],
5
5
  hide: ["page:beforeout", "page:tabhide"]
6
6
  };
7
7
  const PAGE_VISIBILITY_KEY = Symbol("pageVisibility");
8
- const ALWAYS_VISIBLE = { value: true };
8
+ const ALWAYS_VISIBLE = computed(() => true);
9
9
  const ownVisibility = /* @__PURE__ */ new WeakMap();
10
10
  function resolvePageEl(node, pageSelector) {
11
11
  let current = node;
package/dist/index.d.mts CHANGED
@@ -1,18 +1,28 @@
1
- import { ComputedRef, Ref } from "vue";
2
- import { OnTableChangeFn, QueryMetric, QueryMetrics, QueryMetricsRecorder, ReactiveQueryOptions } from "@cavulsqa/reactive-db";
1
+ import { ComputedRef, MaybeRefOrGetter, Ref } from "vue";
2
+ import { OnTableChangeFn, QueryKey, QueryMetric, QueryMetrics, QueryMetricsRecorder, ReactiveQueryOptions, TableName } from "@cavulsqa/reactive-db";
3
3
  //#region src/reactiveQuery.d.ts
4
4
  interface ReactiveQueryLogger {
5
5
  debug: (message: string, ...details: unknown[]) => void;
6
6
  warn: (message: string, ...details: unknown[]) => void;
7
7
  error: (message: string, ...details: unknown[]) => void;
8
8
  }
9
- /**
10
- * `enabled` may be a ref, so a screen can defer its first read until it is actually shown.
11
- * Framework7 mounts every tab at startup; without this, a tab the user has not opened still
12
- * competes for the one native database thread while the first screen is loading.
13
- */
14
- type VueReactiveQueryOptions<T> = Omit<ReactiveQueryOptions<T>, "enabled"> & {
9
+ type VueReactiveQueryOptions<T, DB = Record<string, unknown>> = Omit<ReactiveQueryOptions<T, DB>, "enabled" | "queryKey" | "isVisible"> & {
10
+ /**
11
+ * `enabled` may be a ref, so a screen can defer its first read until it is actually shown.
12
+ * Framework7 mounts every tab at startup; without this, a tab the user has not opened still
13
+ * competes for the one database thread while the first screen is loading.
14
+ */
15
15
  enabled?: boolean | Ref<boolean> | ComputedRef<boolean>;
16
+ /**
17
+ * Identity, from the values the query reads: `["order", id]`, not `"order"`.
18
+ *
19
+ * Refs inside the key are unwrapped and tracked, so `["search", term]` re-keys the query as the
20
+ * term moves: the key follows the filter and brings the debounced refetch with it, instead of a
21
+ * manual `refetch()` on every keystroke.
22
+ */
23
+ queryKey: MaybeRefOrGetter<QueryKey>;
24
+ /** A real ref, because a plain object cannot be watched and a deferred refetch would never run. */
25
+ isVisible?: Ref<boolean> | ComputedRef<boolean>;
16
26
  };
17
27
  interface ReactiveQuery<T> {
18
28
  data: Ref<T | null>;
@@ -28,9 +38,9 @@ interface ReactiveQuery<T> {
28
38
  invalidate: () => Promise<void>;
29
39
  cancel: () => void;
30
40
  }
31
- interface CreateReactiveQueryDeps {
41
+ interface CreateReactiveQueryDeps<DB = Record<string, unknown>> {
32
42
  /** Subscribe to table changes. Wire this to the same change bus the writes emit on. */
33
- onTableChange: OnTableChangeFn;
43
+ onTableChange: OnTableChangeFn<DB>;
34
44
  /** Defaults to a no-op recorder. Pass `createVueQueryMetrics().recorder` to collect timings. */
35
45
  metrics?: QueryMetrics;
36
46
  /**
@@ -39,13 +49,10 @@ interface CreateReactiveQueryDeps {
39
49
  * router that mounts every tab up front, pass an adapter - `usePageVisibility` from
40
50
  * `@cavulsqa/reactive-vue/framework7` is one.
41
51
  */
42
- useVisibility?: () => {
43
- value: boolean;
44
- };
52
+ useVisibility?: () => Ref<boolean> | ComputedRef<boolean>;
45
53
  /**
46
- * Warn when two mounted queries share a `queryKey` but watch different tables - they are
47
- * different queries, so the second is handed the first's rows. Defaults to on outside
48
- * production.
54
+ * Warn when two mounted queries share a key but watch different tables - they are different
55
+ * queries, so the second is handed the first's rows. On unless turned off.
49
56
  */
50
57
  warnOnKeyConflict?: boolean;
51
58
  /**
@@ -54,29 +61,26 @@ interface CreateReactiveQueryDeps {
54
61
  */
55
62
  logger?: ReactiveQueryLogger;
56
63
  }
57
- interface ReactiveQueryComposables {
58
- useReactiveQuery: <T>(queryFn: () => Promise<T>, options: VueReactiveQueryOptions<T>) => ReactiveQuery<T>;
59
- useStructuralQuery: <T>(queryFn: () => Promise<T>, tables: string[], options: Omit<VueReactiveQueryOptions<T>, "tables" | "refetchOn">) => ReactiveQuery<T>;
60
- useStaticQuery: <T>(queryFn: () => Promise<T>, tables: string[], options: Omit<VueReactiveQueryOptions<T>, "tables" | "enabled">) => ReactiveQuery<T>;
64
+ interface ReactiveQueryComposables<DB = Record<string, unknown>> {
65
+ useReactiveQuery: <T>(queryFn: () => Promise<T>, options: VueReactiveQueryOptions<T, DB>) => ReactiveQuery<T>;
66
+ useStructuralQuery: <T>(queryFn: () => Promise<T>, tables: TableName<DB>[], options: Omit<VueReactiveQueryOptions<T, DB>, "tables" | "refetchOn">) => ReactiveQuery<T>;
67
+ useStaticQuery: <T>(queryFn: () => Promise<T>, tables: TableName<DB>[], options: Omit<VueReactiveQueryOptions<T, DB>, "tables" | "enabled">) => ReactiveQuery<T>;
61
68
  }
62
69
  /**
63
70
  * Binds the reactive-db primitives to Vue's lifecycle once, and returns the composables an app
64
71
  * calls everywhere. The change bus and the metrics recorder are app-owned singletons, so they are
65
72
  * injected here rather than created per query.
66
- */
67
- declare function createReactiveQuery(deps: CreateReactiveQueryDeps): ReactiveQueryComposables;
68
- //#endregion
69
- //#region src/queryKeys.d.ts
70
- /**
71
- * A key unique to this call, so the query is never deduplicated against another and its
72
- * `cacheTime` window is its own.
73
73
  *
74
- * A `queryKey` is an *identity*: two mounted queries sharing one await a single request and share
75
- * the result. That is what you want for the same list rendered twice, and wrong for two different
76
- * queries that happen to be named alike - the second would be handed the first's rows. Reach for a
77
- * stable literal only when sharing is the intent; otherwise call this.
74
+ * Pass the schema - `createReactiveQuery<Database>({...})` - and every `tables` entry is checked
75
+ * against it. A misspelt table name is the one mistake this library cannot report at run time: the
76
+ * query subscribes to nothing and simply never refetches.
77
+ *
78
+ * `deps` is `NoInfer` because `keyof DB` is not an inference site TypeScript can invert: handed an
79
+ * already-typed `onTableChange`, it solved DB as a union of one object per table, and every
80
+ * `tables` entry in the app then failed against `never`. Now the schema comes from the type
81
+ * argument or not at all.
78
82
  */
79
- declare function uniqueQueryKey(prefix: string): string;
83
+ declare function createReactiveQuery<DB = Record<string, unknown>>(deps: NoInfer<CreateReactiveQueryDeps<DB>>): ReactiveQueryComposables<DB>;
80
84
  //#endregion
81
85
  //#region src/queryMetrics.d.ts
82
86
  interface VueQueryMetricsOptions {
@@ -114,4 +118,4 @@ interface VueQueryMetrics {
114
118
  */
115
119
  declare function createVueQueryMetrics(options?: VueQueryMetricsOptions): VueQueryMetrics;
116
120
  //#endregion
117
- export { CreateReactiveQueryDeps, QueryMetricsView, ReactiveQuery, ReactiveQueryComposables, ReactiveQueryLogger, VueQueryMetrics, VueQueryMetricsOptions, VueReactiveQueryOptions, createReactiveQuery, createVueQueryMetrics, uniqueQueryKey };
121
+ export { CreateReactiveQueryDeps, QueryMetricsView, ReactiveQuery, ReactiveQueryComposables, ReactiveQueryLogger, VueQueryMetrics, VueQueryMetricsOptions, VueReactiveQueryOptions, createReactiveQuery, createVueQueryMetrics };
package/dist/index.mjs CHANGED
@@ -1,8 +1,32 @@
1
- import { computed, isRef, onMounted, onUnmounted, reactive, ref, watch } from "vue";
2
- import { calcRetryDelay, createQueryMetrics, createVisibilityGate, noopMetrics } from "@cavulsqa/reactive-db";
1
+ import { computed, isRef, onMounted, onUnmounted, reactive, ref, toValue, watch } from "vue";
2
+ import { calcRetryDelay, createQueryMetrics, createVisibilityGate, hashQueryKey, noopMetrics } from "@cavulsqa/reactive-db";
3
+ //#region src/queryKey.ts
4
+ /**
5
+ * Reads a key, unwrapping any refs inside it.
6
+ *
7
+ * `toValue` alone resolves the container, not its contents, so `["search", term]` hashed the ref
8
+ * object itself: the hash never moved when the term did, and reading `_value` rather than `.value`
9
+ * tracked nothing either - the key looked reactive and was not. Unwrapping here means the read
10
+ * happens inside the caller's `computed`, which is what makes the dependency real.
11
+ */
12
+ function resolveQueryKey(key) {
13
+ return toValue(key).map(unwrapRefs);
14
+ }
15
+ function unwrapRefs(value) {
16
+ if (isRef(value)) return unwrapRefs(value.value);
17
+ if (Array.isArray(value)) return value.map(unwrapRefs);
18
+ if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([field, inner]) => [field, unwrapRefs(inner)]));
19
+ return value;
20
+ }
21
+ function isPlainObject(value) {
22
+ if (value === null || typeof value !== "object") return false;
23
+ const prototype = Object.getPrototypeOf(value);
24
+ return prototype === Object.prototype || prototype === null;
25
+ }
26
+ //#endregion
3
27
  //#region src/reactiveQuery.ts
4
28
  const DEFAULT_DEBOUNCE = 100;
5
- const ALWAYS_VISIBLE = { value: true };
29
+ const ALWAYS_VISIBLE = computed(() => true);
6
30
  const consoleLogger = {
7
31
  debug: (message, ...details) => console.log(message, ...details),
8
32
  warn: (message, ...details) => console.warn(message, ...details),
@@ -12,6 +36,15 @@ const consoleLogger = {
12
36
  * Binds the reactive-db primitives to Vue's lifecycle once, and returns the composables an app
13
37
  * calls everywhere. The change bus and the metrics recorder are app-owned singletons, so they are
14
38
  * injected here rather than created per query.
39
+ *
40
+ * Pass the schema - `createReactiveQuery<Database>({...})` - and every `tables` entry is checked
41
+ * against it. A misspelt table name is the one mistake this library cannot report at run time: the
42
+ * query subscribes to nothing and simply never refetches.
43
+ *
44
+ * `deps` is `NoInfer` because `keyof DB` is not an inference site TypeScript can invert: handed an
45
+ * already-typed `onTableChange`, it solved DB as a union of one object per table, and every
46
+ * `tables` entry in the app then failed against `never`. Now the schema comes from the type
47
+ * argument or not at all.
15
48
  */
16
49
  function createReactiveQuery(deps) {
17
50
  const metrics = deps.metrics ?? noopMetrics;
@@ -20,24 +53,24 @@ function createReactiveQuery(deps) {
20
53
  const logger = deps.logger ?? consoleLogger;
21
54
  const inFlightQueries = /* @__PURE__ */ new Map();
22
55
  const watchedTablesByKey = /* @__PURE__ */ new Map();
23
- function claimKey(queryKey, tables) {
56
+ function claimKey(key, tables) {
24
57
  const signature = [...tables].sort().join(",");
25
- const existing = watchedTablesByKey.get(queryKey);
58
+ const existing = watchedTablesByKey.get(key);
26
59
  if (!existing) {
27
- watchedTablesByKey.set(queryKey, {
60
+ watchedTablesByKey.set(key, {
28
61
  tables: signature,
29
62
  holders: 1
30
63
  });
31
64
  return;
32
65
  }
33
- if (warnOnKeyConflict && existing.tables !== signature) logger.warn(`[useReactiveQuery] queryKey "${queryKey}" is mounted twice watching different tables ("${existing.tables}" and "${signature}"). These are different queries sharing an identity, so one will receive the other's result. Use uniqueQueryKey() for a key that is never shared.`);
66
+ if (warnOnKeyConflict && existing.tables !== signature) logger.warn(`[useReactiveQuery] the key ${key} is mounted twice watching different tables ("${existing.tables}" and "${signature}"). These are different queries sharing an identity, so one will receive the other's result. Add what distinguishes them to the key.`);
34
67
  existing.holders += 1;
35
68
  }
36
- function releaseKey(queryKey) {
37
- const existing = watchedTablesByKey.get(queryKey);
69
+ function releaseKey(key) {
70
+ const existing = watchedTablesByKey.get(key);
38
71
  if (!existing) return;
39
72
  existing.holders -= 1;
40
- if (existing.holders <= 0) watchedTablesByKey.delete(queryKey);
73
+ if (existing.holders <= 0) watchedTablesByKey.delete(key);
41
74
  }
42
75
  function useReactiveQuery(queryFn, options) {
43
76
  const data = ref(null);
@@ -55,7 +88,7 @@ function createReactiveQuery(deps) {
55
88
  const debounceMs = options.debounce ?? DEFAULT_DEBOUNCE;
56
89
  const enabledRef = isRef(options.enabled) ? options.enabled : ref(options.enabled !== false);
57
90
  const fetchOnMount = options.fetchOnMount !== false;
58
- const queryKey = options.queryKey;
91
+ const keyHash = computed(() => hashQueryKey(resolveQueryKey(options.queryKey)));
59
92
  const cancelOnUnmount = options.cancelOnUnmount !== false;
60
93
  const maxRetries = options.retry === false ? 0 : options.retry ?? 0;
61
94
  const cacheAge = computed(() => cacheClock.value - lastFetchTime.value);
@@ -94,10 +127,11 @@ function createReactiveQuery(deps) {
94
127
  });
95
128
  }
96
129
  async function executeQueryWithRetry(showLoading = true, attempt = 0, force = false) {
97
- if (!force && inFlightQueries.has(queryKey)) {
98
- if (options.debug) logger.debug(`[useReactiveQuery] Deduping query: ${queryKey}`);
130
+ const key = keyHash.value;
131
+ if (!force && inFlightQueries.has(key)) {
132
+ if (options.debug) logger.debug(`[useReactiveQuery] Deduping query: ${key}`);
99
133
  try {
100
- data.value = await inFlightQueries.get(queryKey);
134
+ data.value = await inFlightQueries.get(key);
101
135
  metrics.recordCacheHit();
102
136
  } catch {}
103
137
  return;
@@ -110,7 +144,7 @@ function createReactiveQuery(deps) {
110
144
  retryCount.value = attempt;
111
145
  const startTime = performance.now();
112
146
  const queryPromise = queryFn();
113
- inFlightQueries.set(queryKey, queryPromise);
147
+ inFlightQueries.set(key, queryPromise);
114
148
  try {
115
149
  const result = await queryPromise;
116
150
  if (requestId !== activeRequestId) return;
@@ -119,7 +153,7 @@ function createReactiveQuery(deps) {
119
153
  isStale.value = false;
120
154
  retryCount.value = 0;
121
155
  const duration = performance.now() - startTime;
122
- metrics.recordQuery(queryKey, duration);
156
+ metrics.recordQuery(key, duration);
123
157
  if (options.debug) logger.debug(`[useReactiveQuery] Query executed (${duration.toFixed(1)}ms):`, result);
124
158
  options.onSuccess?.(result);
125
159
  } catch (err) {
@@ -127,7 +161,7 @@ function createReactiveQuery(deps) {
127
161
  const errorObj = err instanceof Error ? err : new Error(String(err));
128
162
  if (attempt < maxRetries) {
129
163
  const delay = calcRetryDelay(attempt, options.retryDelay);
130
- inFlightQueries.delete(queryKey);
164
+ inFlightQueries.delete(key);
131
165
  if (options.debug) logger.debug(`[useReactiveQuery] Retry ${attempt + 1}/${maxRetries} in ${delay}ms`);
132
166
  await waitForRetry(delay);
133
167
  if (disposed || requestId !== activeRequestId) return;
@@ -136,14 +170,14 @@ function createReactiveQuery(deps) {
136
170
  error.value = errorObj;
137
171
  isStale.value = false;
138
172
  logger.error("[useReactiveQuery] Query failed:", err);
139
- metrics.recordError(queryKey);
173
+ metrics.recordError(key);
140
174
  options.onError?.(errorObj);
141
175
  } finally {
142
- loading.value = false;
143
- if (inFlightQueries.get(queryKey) === queryPromise) inFlightQueries.delete(queryKey);
176
+ if (requestId === activeRequestId) loading.value = false;
177
+ if (inFlightQueries.get(key) === queryPromise) inFlightQueries.delete(key);
144
178
  }
145
179
  }
146
- function scheduledRefetch() {
180
+ function scheduledRefetch(changedTable) {
147
181
  if (isCacheValidNow()) {
148
182
  if (options.debug) logger.debug("[useReactiveQuery] Cache valid, skipping refetch");
149
183
  metrics.recordCacheHit();
@@ -151,7 +185,7 @@ function createReactiveQuery(deps) {
151
185
  }
152
186
  if (debounceTimer) clearTimeout(debounceTimer);
153
187
  debounceTimer = setTimeout(() => {
154
- metrics.recordRefetch(options.tables[0] ?? "unknown");
188
+ if (changedTable) metrics.recordRefetch(changedTable);
155
189
  executeQueryWithRetry();
156
190
  debounceTimer = null;
157
191
  }, debounceMs);
@@ -183,30 +217,45 @@ function createReactiveQuery(deps) {
183
217
  const visibility = options.isVisible ?? resolveVisibility();
184
218
  let unsubscribe = null;
185
219
  let stopVisibilityWatch = null;
220
+ let stopKeyWatch = null;
221
+ let claimedKey = null;
186
222
  let active = false;
187
223
  function activate() {
188
224
  if (active) return;
189
225
  active = true;
190
- claimKey(queryKey, options.tables);
226
+ claimedKey = keyHash.value;
227
+ claimKey(claimedKey, options.tables);
191
228
  if (fetchOnMount) executeQueryWithRetry();
192
229
  unsubscribe = deps.onTableChange(options.tables, (event) => {
193
230
  if (options.debug) logger.debug("[useReactiveQuery] Table changed:", event);
194
231
  if (!shouldTriggerRefetch(event)) return;
195
- if (gate.recordChange(visibility.value) === "refetch") scheduledRefetch();
232
+ if (gate.recordChange(visibility.value) === "refetch") scheduledRefetch(event.table);
196
233
  });
197
- if (isRef(visibility)) stopVisibilityWatch = watch(visibility, (visible) => {
234
+ stopVisibilityWatch = watch(visibility, (visible) => {
198
235
  if (visible && gate.recordVisible() === "refetch") scheduledRefetch();
199
236
  });
237
+ stopKeyWatch = watch(keyHash, (next, previous) => {
238
+ releaseKey(previous);
239
+ claimKey(next, options.tables);
240
+ claimedKey = next;
241
+ lastFetchTime.value = 0;
242
+ scheduledRefetch();
243
+ });
200
244
  metrics.incrementListeners();
201
245
  }
202
246
  function deactivate() {
203
247
  if (!active) return;
204
248
  active = false;
205
- releaseKey(queryKey);
249
+ if (claimedKey !== null) {
250
+ releaseKey(claimedKey);
251
+ claimedKey = null;
252
+ }
206
253
  unsubscribe?.();
207
254
  unsubscribe = null;
208
255
  stopVisibilityWatch?.();
209
256
  stopVisibilityWatch = null;
257
+ stopKeyWatch?.();
258
+ stopKeyWatch = null;
210
259
  metrics.decrementListeners();
211
260
  }
212
261
  let stopEnabledWatch = null;
@@ -253,22 +302,6 @@ function createReactiveQuery(deps) {
253
302
  };
254
303
  }
255
304
  //#endregion
256
- //#region src/queryKeys.ts
257
- let instanceCounter = 0;
258
- /**
259
- * A key unique to this call, so the query is never deduplicated against another and its
260
- * `cacheTime` window is its own.
261
- *
262
- * A `queryKey` is an *identity*: two mounted queries sharing one await a single request and share
263
- * the result. That is what you want for the same list rendered twice, and wrong for two different
264
- * queries that happen to be named alike - the second would be handed the first's rows. Reach for a
265
- * stable literal only when sharing is the intent; otherwise call this.
266
- */
267
- function uniqueQueryKey(prefix) {
268
- instanceCounter += 1;
269
- return `${prefix}#${instanceCounter}`;
270
- }
271
- //#endregion
272
305
  //#region src/queryMetrics.ts
273
306
  const SLOWEST_QUERY_COUNT = 5;
274
307
  /**
@@ -321,4 +354,4 @@ function createVueQueryMetrics(options = {}) {
321
354
  };
322
355
  }
323
356
  //#endregion
324
- export { createReactiveQuery, createVueQueryMetrics, uniqueQueryKey };
357
+ export { createReactiveQuery, createVueQueryMetrics };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavulsqa/reactive-vue",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Vue bindings for @cavulsqa/reactive-db: a reactive query composable, Framework7 page visibility, and a reactive metrics view.",
5
5
  "keywords": [
6
6
  "framework7",
@@ -33,17 +33,17 @@
33
33
  "access": "public"
34
34
  },
35
35
  "devDependencies": {
36
- "@cavulsqa/reactive-db": "0.2.0",
37
- "@types/node": "^26.1.1",
36
+ "@cavulsqa/reactive-db": "1.0.0",
37
+ "@types/node": "^24.12.2",
38
38
  "bumpp": "^11.1.0",
39
39
  "happy-dom": "^20.11.6",
40
- "typescript": "^7.0.2",
40
+ "typescript": "^5.9.3",
41
41
  "vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
42
42
  "vite-plus": "0.3.0",
43
43
  "vue": "^3.5.41"
44
44
  },
45
45
  "peerDependencies": {
46
- "@cavulsqa/reactive-db": "^0.2.0",
46
+ "@cavulsqa/reactive-db": "^1.0.0",
47
47
  "vue": ">=3.5"
48
48
  },
49
49
  "scripts": {