dexie-reactive 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konstantin Kroner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,463 @@
1
+ # dexie-reactive
2
+
3
+ Shared, SSR-safe Dexie live query state for Vue 3 and Nuxt 3.
4
+
5
+ `dexie-reactive` wraps Dexie `liveQuery` with small Vue composables. One
6
+ component owns the real Dexie subscription as a producer. Other components can
7
+ subscribe to the same reactive state by key without creating another Dexie
8
+ subscription.
9
+
10
+ ## Why dexie-reactive?
11
+
12
+ Use `dexie-reactive` when multiple Vue components need to react to the same
13
+ Dexie query result without each component creating its own IndexedDB
14
+ subscription.
15
+
16
+ It provides:
17
+
18
+ - one producer-owned Dexie `liveQuery` per key
19
+ - shared Vue reactive state for all consumers
20
+ - duplicate producer protection
21
+ - SSR-safe client-only subscription behavior
22
+ - same-origin tab-to-tab updates through Dexie live query propagation
23
+ - persistent IndexedDB-backed state for query results without keeping the whole
24
+ database in memory
25
+ - stale result protection during restarts and component cleanup
26
+ - explicit loading and error state
27
+ - focused unit and browser integration test coverage
28
+
29
+ This keeps IndexedDB reactivity predictable in Vue and Nuxt apps while leaving
30
+ Dexie query construction fully under application control.
31
+
32
+ ## Decision Guide
33
+
34
+ | Use this package when | Avoid it when |
35
+ | ----------------------------------------------------------------------------- | ------------------------------------------------------------- |
36
+ | multiple components share the same IndexedDB query result | each component owns completely independent queries |
37
+ | you want explicit producer and consumer ownership | you want implicit global query caching |
38
+ | you need Nuxt-safe client-only IndexedDB behavior | you need server-side IndexedDB execution |
39
+ | you want Dexie queries to stay application-owned | you want a query builder or ORM abstraction |
40
+ | duplicate shared subscription ownership should fail immediately | duplicate subscriptions are acceptable in your design |
41
+ | you want IndexedDB-backed state that behaves like persistent shared app state | you need a general-purpose Pinia replacement for all UI state |
42
+ | you need same-origin tab-to-tab updates from Dexie writes | you need cross-device or backend synchronization |
43
+
44
+ ## What It Does Not Do
45
+
46
+ - It does not replace Dexie.
47
+ - It does not replace Pinia for transient UI state.
48
+ - It does not build or parse queries.
49
+ - It does not sync data to a backend.
50
+ - It does not sync data across devices, users, or origins.
51
+ - It does not provide persistence beyond IndexedDB.
52
+ - It does not create server-side data fetching.
53
+ - It does not hide duplicate ownership mistakes.
54
+
55
+ ## When To Use It
56
+
57
+ Use this package when a Vue or Nuxt app needs IndexedDB data that updates
58
+ reactively after Dexie writes.
59
+
60
+ - Use `useLiveQuery` in the component or composable that owns the query.
61
+ - Use `useLiveQuerySubscription` in components that only need to display the
62
+ already shared state.
63
+ - Use an explicit `key` when multiple components should share one subscription.
64
+ - Omit the key for local, non-shared live queries.
65
+
66
+ ## Installation
67
+
68
+ ```sh
69
+ npm install dexie-reactive dexie vue
70
+ ```
71
+
72
+ `dexie` and `vue` are peer dependencies. Nuxt users can install the package in a
73
+ Nuxt 3 project and import the composables directly from `dexie-reactive`.
74
+
75
+ ## Quick Start
76
+
77
+ Define your Dexie database once:
78
+
79
+ ```ts
80
+ // db.ts
81
+ import Dexie, { type EntityTable } from 'dexie'
82
+
83
+ export interface Friend {
84
+ id?: number
85
+ name: string
86
+ age: number
87
+ }
88
+
89
+ export interface AppDatabase extends Dexie {
90
+ friends: EntityTable<Friend, 'id'>
91
+ }
92
+
93
+ export const db = new Dexie('app-db') as AppDatabase
94
+
95
+ db.version(1).stores({
96
+ friends: '++id,name,age',
97
+ })
98
+ ```
99
+
100
+ Create a producer composable:
101
+
102
+ ```ts
103
+ // useOlderFriends.ts
104
+ import { useLiveQuery } from 'dexie-reactive'
105
+ import { db } from './db'
106
+
107
+ export function useOlderFriends() {
108
+ return useLiveQuery(() => db.friends.where('age').above(75).toArray(), {
109
+ key: 'older-friends',
110
+ })
111
+ }
112
+ ```
113
+
114
+ Use it in a producer component:
115
+
116
+ ```vue
117
+ <script setup lang="ts">
118
+ import { useOlderFriends } from './useOlderFriends'
119
+
120
+ const { data, loading, hasError } = useOlderFriends()
121
+ </script>
122
+
123
+ <template>
124
+ <p v-if="loading">Loading...</p>
125
+ <p v-else-if="hasError">Could not load friends.</p>
126
+ <ul v-else>
127
+ <li v-for="friend in data" :key="friend.id">
128
+ {{ friend.name }}
129
+ </li>
130
+ </ul>
131
+ </template>
132
+ ```
133
+
134
+ Use the same state from a consumer component:
135
+
136
+ ```vue
137
+ <script setup lang="ts">
138
+ import { useLiveQuerySubscription } from 'dexie-reactive'
139
+ import type { Friend } from './db'
140
+
141
+ const { data, loading, hasError } =
142
+ useLiveQuerySubscription<Friend>('older-friends')
143
+ </script>
144
+
145
+ <template>
146
+ <p v-if="loading">Loading...</p>
147
+ <p v-else-if="hasError">Could not load friends.</p>
148
+ <ul v-else>
149
+ <li v-for="friend in data" :key="friend.id">
150
+ {{ friend.name }}
151
+ </li>
152
+ </ul>
153
+ </template>
154
+ ```
155
+
156
+ ## Core Model
157
+
158
+ `useLiveQuery` is the producer. It owns the real Dexie live query subscription.
159
+
160
+ `useLiveQuerySubscription` is the consumer. It attaches to shared state by key
161
+ and never creates a Dexie subscription.
162
+
163
+ The consumer receives shared reactive state by key. It does not get a cloned
164
+ snapshot and it does not create a second Dexie subscription.
165
+
166
+ ## Public API
167
+
168
+ The package exports only the stable composables and public types:
169
+
170
+ ```ts
171
+ import {
172
+ useLiveQuery,
173
+ useLiveQuerySubscription,
174
+ type LiveQueryState,
175
+ type UseLiveQueryOptions,
176
+ } from 'dexie-reactive'
177
+ ```
178
+
179
+ ### `useLiveQuery(queryFn, options?)`
180
+
181
+ Creates and owns a Dexie live query subscription.
182
+
183
+ ```ts
184
+ function useLiveQuery<T>(
185
+ queryFn?:
186
+ | (() => T[] | Promise<T[]>)
187
+ | Ref<(() => T[] | Promise<T[]>) | null | undefined>
188
+ | null,
189
+ options?: {
190
+ key?: string
191
+ },
192
+ ): LiveQueryState<T>
193
+ ```
194
+
195
+ - `queryFn` is a function that returns an array or a promise of an array.
196
+ - `queryFn` may be passed directly or as a reactive function reference.
197
+ - `options.key` is optional. If omitted, a UUID key is generated and returned.
198
+ - Only one producer may exist for a key.
199
+ - The live query is created only in the browser.
200
+
201
+ ### `useLiveQuerySubscription(key)`
202
+
203
+ Consumes existing shared state by key.
204
+
205
+ ```ts
206
+ function useLiveQuerySubscription<T>(key: string): LiveQueryState<T>
207
+ ```
208
+
209
+ - Receives only a key.
210
+ - Never receives a query function.
211
+ - Never creates a Dexie live query subscription.
212
+ - Returns the same reactive refs as the producer once the producer exists.
213
+
214
+ ### Returned State
215
+
216
+ Both composables return:
217
+
218
+ ```ts
219
+ interface LiveQueryState<T> {
220
+ key: string
221
+ data: Ref<T[]>
222
+ loading: Ref<boolean>
223
+ hasError: Ref<boolean>
224
+ error?: Ref<unknown | undefined>
225
+ stop: () => void
226
+ restart: () => void
227
+ }
228
+ ```
229
+
230
+ `error` is available only in development mode. Production consumers should rely
231
+ on `hasError`.
232
+
233
+ ## Generated Key Usage
234
+
235
+ If no key is provided, `useLiveQuery` generates a UUID key and returns it.
236
+
237
+ ```ts
238
+ const friends = useLiveQuery(() => db.friends.toArray())
239
+
240
+ console.log(friends.key)
241
+ ```
242
+
243
+ Generated keys are useful for local, non-shared subscriptions. Use an explicit
244
+ key when another component needs to subscribe to the same state.
245
+
246
+ ## Error Handling
247
+
248
+ Errors are caught internally and do not throw to components.
249
+
250
+ ```ts
251
+ const friends = useLiveQuery(() => db.friends.toArray(), {
252
+ key: 'friends',
253
+ })
254
+
255
+ if (friends.hasError.value) {
256
+ // Render fallback UI or trigger app-level reporting.
257
+ }
258
+ ```
259
+
260
+ On failure:
261
+
262
+ - `hasError.value` becomes `true`
263
+ - `loading.value` becomes `false`
264
+ - `data.value` remains an array
265
+ - the original error is exposed only through `error` in development mode
266
+
267
+ ## Query Function Behavior
268
+
269
+ The package passes your function to Dexie `liveQuery`; it does not parse, build,
270
+ transform, or interpret Dexie queries.
271
+
272
+ ```ts
273
+ const query = () => db.friends.where('age').above(75).toArray()
274
+
275
+ const friends = useLiveQuery(query, { key: 'older-friends' })
276
+ ```
277
+
278
+ Reactive values used inside the query function are not tracked by
279
+ `dexie-reactive`. If external dependencies change, recreate the query function
280
+ reference.
281
+
282
+ ```ts
283
+ import { computed } from 'vue'
284
+
285
+ const minimumAge = ref(75)
286
+ const query = computed(
287
+ () => () => db.friends.where('age').above(minimumAge.value).toArray(),
288
+ )
289
+
290
+ const friends = useLiveQuery(query, { key: 'filtered-friends' })
291
+ ```
292
+
293
+ When the query function reference changes, the composable stops the current
294
+ subscription, resets state to `data = []`, `loading = true`, `hasError = false`,
295
+ and starts a new subscription.
296
+
297
+ ## SSR And Nuxt
298
+
299
+ The same package build works in plain Vue and Nuxt.
300
+
301
+ During SSR:
302
+
303
+ - no Dexie live query subscription is created
304
+ - shared state is scoped per runtime environment
305
+ - browser state is not shared with SSR
306
+ - SSR request state is isolated and cannot leak across requests
307
+
308
+ Use the composables in Nuxt components or composables, but expect live Dexie
309
+ updates only on the client because IndexedDB is a browser API.
310
+
311
+ ```vue
312
+ <script setup lang="ts">
313
+ import { useLiveQuery } from 'dexie-reactive'
314
+
315
+ const friends = useLiveQuery(() => db.friends.toArray(), {
316
+ key: 'friends',
317
+ })
318
+ </script>
319
+ ```
320
+
321
+ ## Lifecycle Controls
322
+
323
+ `stop()` unsubscribes from Dexie, sets `loading` to `false`, and keeps current
324
+ data.
325
+
326
+ `restart()` performs a full reset and starts a new subscription using the latest
327
+ query function.
328
+
329
+ For shared keys, these controls affect all consumers because they point to the
330
+ producer-owned state.
331
+
332
+ ## Flow Diagrams
333
+
334
+ ### Producer Flow
335
+
336
+ ```mermaid
337
+ flowchart TD
338
+ A["useLiveQuery(queryFn, options)"] --> B["Resolve or generate key"]
339
+ B --> C{"Key already exists?"}
340
+ C -->|Yes| D["Throw duplicate producer error"]
341
+ C -->|No| E["Create shared reactive state"]
342
+ E --> F["Store state in subscription map"]
343
+ F --> G["Emit registration message"]
344
+ G --> H{"Browser runtime?"}
345
+ H -->|No| I["Do not create Dexie subscription"]
346
+ H -->|Yes| J["Start Dexie liveQuery"]
347
+ J --> K["Apply latest result to shared refs"]
348
+ ```
349
+
350
+ ### Consumer Flow
351
+
352
+ ```mermaid
353
+ flowchart TD
354
+ A["useLiveQuerySubscription(key)"] --> B{"Key exists in subscription map?"}
355
+ B -->|Yes| C["Return existing shared reactive state"]
356
+ B -->|No| D["Create waiting reactive state"]
357
+ D --> E["Register waiting consumer by key"]
358
+ E --> F["Return waiting state"]
359
+ ```
360
+
361
+ ### Waiting Consumer Flow
362
+
363
+ ```mermaid
364
+ flowchart TD
365
+ A["Consumer subscribes before producer"] --> B["Waiting consumer is stored by key"]
366
+ B --> C["Producer later registers same key"]
367
+ C --> D["Registration message is emitted synchronously"]
368
+ D --> E["Waiting consumer attaches to producer refs"]
369
+ E --> F["Waiting entry is removed"]
370
+ ```
371
+
372
+ ### Duplicate Producer Error Flow
373
+
374
+ ```mermaid
375
+ flowchart TD
376
+ A["First useLiveQuery registers key"] --> B["subscriptionMap has key"]
377
+ B --> C["Second useLiveQuery uses same key"]
378
+ C --> D["No second Dexie subscription is created"]
379
+ D --> E["Error is thrown immediately"]
380
+ ```
381
+
382
+ ## Limitations
383
+
384
+ - Dexie queries must return arrays.
385
+ - Consumers cannot create subscriptions.
386
+ - Keys are identifiers for shared state, not a security boundary.
387
+ - Live queries run only in the browser.
388
+ - The package uses vanilla Dexie `liveQuery` semantics through the composables.
389
+ - It does not use framework-specific Dexie bindings such as React hooks.
390
+
391
+ ## Anti-Patterns
392
+
393
+ Do not create duplicate producers for the same key:
394
+
395
+ ```ts
396
+ useLiveQuery(() => db.friends.toArray(), { key: 'friends' })
397
+ useLiveQuery(() => db.friends.toArray(), { key: 'friends' }) // throws
398
+ ```
399
+
400
+ Do not pass query functions to consumers:
401
+
402
+ ```ts
403
+ useLiveQuerySubscription('friends') // correct
404
+ useLiveQuerySubscription(() => db.friends.toArray()) // incorrect
405
+ ```
406
+
407
+ Do not rely on reactive values inside a stable query function reference:
408
+
409
+ ```ts
410
+ const minimumAge = ref(75)
411
+
412
+ useLiveQuery(() => db.friends.where('age').above(minimumAge.value).toArray(), {
413
+ key: 'friends',
414
+ })
415
+ ```
416
+
417
+ Recreate the query function when dependencies change instead.
418
+
419
+ ## Demo And Browser Integration Test
420
+
421
+ The browser test app doubles as a small local demo:
422
+
423
+ ```sh
424
+ npm run test:browser:server
425
+ ```
426
+
427
+ Open the printed local URL and inspect IndexedDB under that same origin. The demo
428
+ database is named `dexie-reactive-browser` and uses a `friends` object store.
429
+
430
+ ## Testing Strategy
431
+
432
+ The unit test suite focuses on the shared live query contract:
433
+
434
+ - public API exports and returned reactive state shape
435
+ - browser singleton and SSR-isolated subscription scopes
436
+ - producer lifecycle for start, stop, restart, unsubscribe, and cleanup
437
+ - duplicate producer rejection without creating a second Dexie subscription
438
+ - consumer coordination for producer-first and waiting-consumer flows
439
+ - shared reactive state references instead of cloned consumer state
440
+ - stale result protection across stop, restart, scope disposal, and rapid query changes
441
+ - missing, invalid, and changing query function handling
442
+ - error, loading, and development-only error exposure behavior
443
+ - generated UUID key uniqueness
444
+ - Dexie `liveQuery` usage through the provided query callback
445
+
446
+ The browser integration suite mounts a minimal Vue app in Chromium with a real
447
+ Dexie IndexedDB database. It verifies producer and consumer components sharing
448
+ one key, database updates propagating to all mounted components, consumer
449
+ unmount/remount behavior, and duplicate producer errors in the browser runtime.
450
+
451
+ ## Scripts
452
+
453
+ - `npm run lint` checks the code with ESLint.
454
+ - `npm run format:check` verifies Prettier formatting.
455
+ - `npm run typecheck` runs TypeScript without emitting files.
456
+ - `npm run test` runs Vitest tests from `tests/*`.
457
+ - `npm run test:browser` runs Playwright browser integration tests.
458
+ - `npm run build` builds the package with unbuild.
459
+ - `npm run check` runs linting, formatting, type checking, tests, and build.
460
+
461
+ ## Git Hooks
462
+
463
+ Husky runs staged linting before commits and commitlint for commit messages.
@@ -0,0 +1,24 @@
1
+ import { Ref } from 'vue';
2
+
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ type LiveQueryQueryFunction<T> = () => MaybePromise<T[]>;
5
+ interface UseLiveQueryOptions {
6
+ key?: string;
7
+ }
8
+ interface LiveQueryState<T> {
9
+ key: string;
10
+ data: Ref<T[]>;
11
+ loading: Ref<boolean>;
12
+ hasError: Ref<boolean>;
13
+ error?: Ref<unknown | undefined>;
14
+ stop: () => void;
15
+ restart: () => void;
16
+ }
17
+ type LiveQueryQuerySource<T> = LiveQueryQueryFunction<T> | Ref<LiveQueryQueryFunction<T> | null | undefined> | null | undefined;
18
+
19
+ declare function useLiveQuery<T>(queryFn?: LiveQueryQuerySource<T>, options?: UseLiveQueryOptions): LiveQueryState<T>;
20
+
21
+ declare function useLiveQuerySubscription<T>(key: string): LiveQueryState<T>;
22
+
23
+ export { useLiveQuery, useLiveQuerySubscription };
24
+ export type { LiveQueryQueryFunction, LiveQueryQuerySource, LiveQueryState, MaybePromise, UseLiveQueryOptions };
@@ -0,0 +1,24 @@
1
+ import { Ref } from 'vue';
2
+
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ type LiveQueryQueryFunction<T> = () => MaybePromise<T[]>;
5
+ interface UseLiveQueryOptions {
6
+ key?: string;
7
+ }
8
+ interface LiveQueryState<T> {
9
+ key: string;
10
+ data: Ref<T[]>;
11
+ loading: Ref<boolean>;
12
+ hasError: Ref<boolean>;
13
+ error?: Ref<unknown | undefined>;
14
+ stop: () => void;
15
+ restart: () => void;
16
+ }
17
+ type LiveQueryQuerySource<T> = LiveQueryQueryFunction<T> | Ref<LiveQueryQueryFunction<T> | null | undefined> | null | undefined;
18
+
19
+ declare function useLiveQuery<T>(queryFn?: LiveQueryQuerySource<T>, options?: UseLiveQueryOptions): LiveQueryState<T>;
20
+
21
+ declare function useLiveQuerySubscription<T>(key: string): LiveQueryState<T>;
22
+
23
+ export { useLiveQuery, useLiveQuerySubscription };
24
+ export type { LiveQueryQueryFunction, LiveQueryQuerySource, LiveQueryState, MaybePromise, UseLiveQueryOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,252 @@
1
+ import { liveQuery } from 'dexie';
2
+ import { shallowReactive, ref, shallowRef, onScopeDispose, isRef, watch } from 'vue';
3
+
4
+ function createLiveQueryState(key) {
5
+ const state = shallowReactive({
6
+ key,
7
+ data: shallowRef([]),
8
+ loading: ref(false),
9
+ hasError: ref(false),
10
+ stop: () => {
11
+ },
12
+ restart: () => {
13
+ }
14
+ });
15
+ if (isDevelopmentEnvironment()) {
16
+ state.error = ref(void 0);
17
+ }
18
+ return state;
19
+ }
20
+ function isDevelopmentEnvironment() {
21
+ const runtime = globalThis;
22
+ if (!runtime.process) {
23
+ return false;
24
+ }
25
+ return runtime.process.env?.NODE_ENV === "development";
26
+ }
27
+
28
+ let browserSubscriptionScope;
29
+ function createSubscriptionScope() {
30
+ return {
31
+ subscriptionMap: /* @__PURE__ */ new Map(),
32
+ waitingConsumers: /* @__PURE__ */ new Map()
33
+ };
34
+ }
35
+ function resolveSubscriptionScope() {
36
+ if (typeof window === "undefined") {
37
+ return createSubscriptionScope();
38
+ }
39
+ browserSubscriptionScope ??= createSubscriptionScope();
40
+ return browserSubscriptionScope;
41
+ }
42
+ function registerLiveQueryProducer(scope, state, configureEntry) {
43
+ if (scope.subscriptionMap.has(state.key)) {
44
+ throw new Error(
45
+ `Duplicate live query producer for key "${state.key}". Only one useLiveQuery producer may own a key; useLiveQuerySubscription(key) to consume existing shared state.`
46
+ );
47
+ }
48
+ const entry = Object.assign(state, {
49
+ producer: {
50
+ active: true,
51
+ generation: 0
52
+ }
53
+ });
54
+ configureEntry?.(entry);
55
+ scope.subscriptionMap.set(
56
+ entry.key,
57
+ entry
58
+ );
59
+ emitSubscriptionRegistered(scope, entry);
60
+ return entry;
61
+ }
62
+ function unregisterLiveQueryProducer(scope, entry) {
63
+ const currentEntry = scope.subscriptionMap.get(entry.key);
64
+ if (currentEntry !== entry) {
65
+ return;
66
+ }
67
+ entry.producer.active = false;
68
+ scope.subscriptionMap.delete(entry.key);
69
+ }
70
+ function resolveLiveQuerySubscription(scope, key) {
71
+ const entry = scope.subscriptionMap.get(key);
72
+ if (entry) {
73
+ return entry;
74
+ }
75
+ const state = createLiveQueryState(key);
76
+ state.loading.value = true;
77
+ const waitingConsumer = {
78
+ attach: (registeredEntry) => {
79
+ attachToSharedState(state, registeredEntry);
80
+ }
81
+ };
82
+ addWaitingConsumer(scope, key, waitingConsumer);
83
+ onScopeDispose(() => {
84
+ removeWaitingConsumer(scope, key, waitingConsumer);
85
+ }, true);
86
+ return state;
87
+ }
88
+ function addWaitingConsumer(scope, key, waitingConsumer) {
89
+ const consumers = scope.waitingConsumers.get(key) ?? /* @__PURE__ */ new Set();
90
+ consumers.add(waitingConsumer);
91
+ scope.waitingConsumers.set(key, consumers);
92
+ }
93
+ function removeWaitingConsumer(scope, key, waitingConsumer) {
94
+ const consumers = scope.waitingConsumers.get(key);
95
+ if (!consumers) {
96
+ return;
97
+ }
98
+ consumers.delete(waitingConsumer);
99
+ if (consumers.size === 0) {
100
+ scope.waitingConsumers.delete(key);
101
+ }
102
+ }
103
+ function emitSubscriptionRegistered(scope, entry) {
104
+ const waitingConsumers = scope.waitingConsumers.get(entry.key);
105
+ if (!waitingConsumers) {
106
+ return;
107
+ }
108
+ for (const waitingConsumer of waitingConsumers) {
109
+ waitingConsumer.attach(entry);
110
+ }
111
+ scope.waitingConsumers.delete(entry.key);
112
+ }
113
+ function attachToSharedState(state, entry) {
114
+ state.data = entry.data;
115
+ state.loading = entry.loading;
116
+ state.hasError = entry.hasError;
117
+ state.error = entry.error;
118
+ state.stop = entry.stop;
119
+ state.restart = entry.restart;
120
+ }
121
+
122
+ function useLiveQuery(queryFn, options = {}) {
123
+ const scope = resolveSubscriptionScope();
124
+ const state = createLiveQueryState(options.key ?? crypto.randomUUID());
125
+ let subscription;
126
+ let latestQueryFn = resolveQueryFunction(queryFn);
127
+ const resetState = () => {
128
+ entry.data.value = [];
129
+ entry.loading.value = true;
130
+ entry.hasError.value = false;
131
+ clearError(entry);
132
+ };
133
+ const stopSubscription = () => {
134
+ subscription?.unsubscribe();
135
+ subscription = void 0;
136
+ incrementGeneration(entry);
137
+ entry.loading.value = false;
138
+ };
139
+ const startSubscription = () => {
140
+ stopSubscription();
141
+ const query = latestQueryFn;
142
+ if (!query) {
143
+ applyInactiveDefaults(entry);
144
+ return;
145
+ }
146
+ if (typeof query !== "function") {
147
+ applyInactiveErrorDefaults(entry);
148
+ return;
149
+ }
150
+ resetState();
151
+ const generation = incrementGeneration(entry);
152
+ if (typeof window === "undefined") {
153
+ entry.loading.value = false;
154
+ return;
155
+ }
156
+ try {
157
+ const observable = liveQuery(
158
+ () => query()
159
+ );
160
+ subscription = observable.subscribe({
161
+ next: (result) => {
162
+ if (!isLatestGeneration(entry, generation)) {
163
+ return;
164
+ }
165
+ applyResultDefaults(entry, result);
166
+ },
167
+ error: (error) => {
168
+ if (!isLatestGeneration(entry, generation)) {
169
+ return;
170
+ }
171
+ applyErrorDefaults(entry, error);
172
+ }
173
+ });
174
+ } catch (error) {
175
+ if (!isLatestGeneration(entry, generation)) {
176
+ return;
177
+ }
178
+ applyErrorDefaults(entry, error);
179
+ }
180
+ };
181
+ const restartSubscription = () => {
182
+ latestQueryFn = resolveQueryFunction(queryFn);
183
+ startSubscription();
184
+ };
185
+ const entry = registerLiveQueryProducer(scope, state, (registeredEntry) => {
186
+ registeredEntry.stop = stopSubscription;
187
+ registeredEntry.restart = restartSubscription;
188
+ });
189
+ if (isRef(queryFn)) {
190
+ watch(
191
+ queryFn,
192
+ (nextQueryFn) => {
193
+ latestQueryFn = nextQueryFn;
194
+ startSubscription();
195
+ },
196
+ { flush: "sync" }
197
+ );
198
+ }
199
+ startSubscription();
200
+ onScopeDispose(() => {
201
+ stopSubscription();
202
+ unregisterLiveQueryProducer(scope, entry);
203
+ }, true);
204
+ return entry;
205
+ }
206
+ function resolveQueryFunction(queryFn) {
207
+ return isRef(queryFn) ? queryFn.value : queryFn;
208
+ }
209
+ function incrementGeneration(entry) {
210
+ entry.producer.generation += 1;
211
+ return entry.producer.generation;
212
+ }
213
+ function isLatestGeneration(entry, generation) {
214
+ return entry.producer.active && entry.producer.generation === generation;
215
+ }
216
+ function clearError(entry) {
217
+ if (entry.error) {
218
+ entry.error.value = void 0;
219
+ }
220
+ }
221
+ function applyInactiveDefaults(entry) {
222
+ entry.data.value = [];
223
+ entry.loading.value = false;
224
+ entry.hasError.value = false;
225
+ clearError(entry);
226
+ }
227
+ function applyInactiveErrorDefaults(entry) {
228
+ applyInactiveDefaults(entry);
229
+ entry.hasError.value = true;
230
+ }
231
+ function applyErrorDefaults(entry, error) {
232
+ entry.loading.value = false;
233
+ entry.hasError.value = true;
234
+ setError(entry, error);
235
+ }
236
+ function applyResultDefaults(entry, result) {
237
+ entry.data.value = Array.isArray(result) ? result : [];
238
+ entry.loading.value = false;
239
+ entry.hasError.value = false;
240
+ clearError(entry);
241
+ }
242
+ function setError(entry, error) {
243
+ if (entry.error) {
244
+ entry.error.value = error ?? void 0;
245
+ }
246
+ }
247
+
248
+ function useLiveQuerySubscription(key) {
249
+ return resolveLiveQuerySubscription(resolveSubscriptionScope(), key);
250
+ }
251
+
252
+ export { useLiveQuery, useLiveQuerySubscription };
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "dexie-reactive",
3
+ "version": "0.0.1",
4
+ "description": "Shared, SSR-safe Dexie live query state for Vue 3 and Nuxt 3.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "keywords": [
9
+ "dexie",
10
+ "indexeddb",
11
+ "livequery",
12
+ "vue",
13
+ "vue3",
14
+ "nuxt",
15
+ "nuxt3",
16
+ "composable",
17
+ "reactive",
18
+ "shared-state",
19
+ "offline-first"
20
+ ],
21
+ "homepage": "https://github.com/Nessiahs/dexie-reactive#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/Nessiahs/dexie-reactive/issues"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Nessiahs/dexie-reactive.git"
28
+ },
29
+ "license": "MIT",
30
+ "author": "Konstantin Kroner",
31
+ "packageManager": "npm@11.6.2",
32
+ "engines": {
33
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
34
+ },
35
+ "main": "./dist/index.mjs",
36
+ "module": "./dist/index.mjs",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.mjs"
42
+ }
43
+ },
44
+ "sideEffects": false,
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "scripts": {
49
+ "build": "unbuild",
50
+ "check": "npm run lint && npm run format:check && npm run typecheck && npm run test:coverage && npm run build",
51
+ "lint": "eslint .",
52
+ "format": "prettier . --write",
53
+ "format:check": "prettier . --check",
54
+ "typecheck": "tsc --noEmit",
55
+ "commitlint": "commitlint --edit",
56
+ "stagedlint": "lint-staged",
57
+ "lint:staged": "lint-staged",
58
+ "test:browser": "playwright test",
59
+ "test:browser:server": "vite --config tests/browser/vite.config.mjs --host 127.0.0.1 --port 4173",
60
+ "prepare": "node .husky/install.mjs",
61
+ "prepublishOnly": "npm run build",
62
+ "test": "vitest run",
63
+ "test:coverage": "vitest run --coverage",
64
+ "test:watch": "vitest"
65
+ },
66
+ "peerDependencies": {
67
+ "dexie": "^4.4.2",
68
+ "vue": "^3.4.0 || ^3.5.0"
69
+ },
70
+ "devDependencies": {
71
+ "@commitlint/cli": "^20.5.3",
72
+ "@commitlint/config-conventional": "^20.5.3",
73
+ "@emnapi/core": "^1.10.0",
74
+ "@emnapi/runtime": "^1.10.0",
75
+ "@eslint/js": "^10.0.1",
76
+ "@playwright/test": "^1.59.1",
77
+ "@vitest/coverage-v8": "^4.1.5",
78
+ "dexie": "^4.4.2",
79
+ "esbuild": "^0.28.0",
80
+ "eslint": "^10.3.0",
81
+ "eslint-config-prettier": "^10.1.8",
82
+ "husky": "^9.1.7",
83
+ "lint-staged": "^16.4.0",
84
+ "prettier": "^3.8.3",
85
+ "typescript": "^5.9.3",
86
+ "typescript-eslint": "^8.59.1",
87
+ "unbuild": "^3.6.1",
88
+ "vite": "^8.0.10",
89
+ "vitest": "^4.1.5",
90
+ "vue": "^3.5.33"
91
+ },
92
+ "lint-staged": {
93
+ "*.{ts,tsx,js,mjs,cjs}": [
94
+ "eslint --fix",
95
+ "prettier --write"
96
+ ],
97
+ "*.{json,md}": "prettier --write"
98
+ }
99
+ }