@lenne.tech/nuxt-extensions 1.20.0 → 1.21.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.
Files changed (35) hide show
  1. package/CLAUDE.md +64 -0
  2. package/README.md +81 -0
  3. package/dist/module.d.mts +3 -3
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +14 -1
  6. package/dist/runtime/components/transition/LtTransitionFade.d.vue.ts +5 -3
  7. package/dist/runtime/components/transition/LtTransitionFade.vue.d.ts +5 -3
  8. package/dist/runtime/components/transition/LtTransitionFadeScale.d.vue.ts +5 -3
  9. package/dist/runtime/components/transition/LtTransitionFadeScale.vue.d.ts +5 -3
  10. package/dist/runtime/components/transition/LtTransitionSlide.d.vue.ts +5 -3
  11. package/dist/runtime/components/transition/LtTransitionSlide.vue.d.ts +5 -3
  12. package/dist/runtime/components/transition/LtTransitionSlideBottom.d.vue.ts +5 -3
  13. package/dist/runtime/components/transition/LtTransitionSlideBottom.vue.d.ts +5 -3
  14. package/dist/runtime/components/transition/LtTransitionSlideRevert.d.vue.ts +5 -3
  15. package/dist/runtime/components/transition/LtTransitionSlideRevert.vue.d.ts +5 -3
  16. package/dist/runtime/composables/auth/use-lt-auth.js +16 -1
  17. package/dist/runtime/composables/index.d.ts +1 -0
  18. package/dist/runtime/composables/index.js +1 -0
  19. package/dist/runtime/composables/use-lt-offline-sync.d.ts +21 -0
  20. package/dist/runtime/composables/use-lt-offline-sync.js +250 -0
  21. package/dist/runtime/lib/index.d.ts +2 -0
  22. package/dist/runtime/lib/index.js +2 -0
  23. package/dist/runtime/lib/sync-hints.d.ts +46 -0
  24. package/dist/runtime/lib/sync-hints.js +147 -0
  25. package/dist/runtime/lib/sync-store.d.ts +89 -0
  26. package/dist/runtime/lib/sync-store.js +149 -0
  27. package/dist/runtime/lib/sync.d.ts +89 -0
  28. package/dist/runtime/lib/sync.js +369 -0
  29. package/dist/runtime/plugins/form-label-association.client.d.ts +2 -1
  30. package/dist/runtime/plugins/pre-hydration-input.client.d.ts +2 -1
  31. package/dist/runtime/types/index.d.ts +1 -0
  32. package/dist/runtime/types/module.d.ts +7 -0
  33. package/dist/runtime/types/sync.d.ts +127 -0
  34. package/dist/runtime/types/sync.js +0 -0
  35. package/package.json +9 -8
package/CLAUDE.md CHANGED
@@ -45,6 +45,12 @@ src/
45
45
  | `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry for editor sidebars |
46
46
  | `useLtAiAdmin()` | Admin CRUD: connections, preferences, budget limits, slots, prompt hints, interactions — server-gated |
47
47
 
48
+ ### Offline sync (nest-server sync module, opt-in)
49
+
50
+ | Composable | Purpose |
51
+ |-----------|---------|
52
+ | `useLtOfflineSync(model, options?)` | Offline read/write of one model in IndexedDB; push/pull against `/sync`, hints, conflicts, tabs, owner check |
53
+
48
54
  ## Module Configuration
49
55
 
50
56
  Configure in `nuxt.config.ts` (top-level key is `ltExtensions`):
@@ -76,6 +82,10 @@ export default defineNuxtConfig({
76
82
  enabled: true,
77
83
  basePath: '/ai', // Must match the nest-server AI controller
78
84
  },
85
+ sync: {
86
+ enabled: false, // opt-in: useLtOfflineSync() + sign-out cleanup
87
+ basePath: '/sync', // Must match the nest-server sync module
88
+ },
79
89
  formLabelAssociation: {
80
90
  enabled: true, // repair <label for> broken by an SSR/client useId() divergence
81
91
  maxRepairMs: 1500, // how long to keep repairing deferred subtrees
@@ -112,6 +122,60 @@ The API base URL is resolved at runtime from `NUXT_PUBLIC_API_URL` (client + SSR
112
122
  - `useLtAiAdmin()` is exposed as a regular composable; admin gating is enforced server-side (`@Restricted(ADMIN)`). Render admin UI behind a frontend route guard for UX, but trust the backend for authorization.
113
123
  - The chat composable's `messages` ref is a shallow `Readonly<Ref<LtAiMessage[]>>` so child components can bind individual messages while preserving streaming reactivity.
114
124
 
125
+ ## Offline Sync
126
+
127
+ `useLtOfflineSync()` is the client of the nest-server sync module (>= 12.2.0); the protocol is
128
+ `docs/offline-sync-protocol.md` in nest-server. Both ends are opt-in: `ltExtensions.sync.enabled`
129
+ here, a sync model (`@PersistedModel({ sync })` + `@SyncService`) on the API.
130
+
131
+ **The local data belongs to the signed-in user, and only explicit sign-out deletes it:**
132
+
133
+ - `useLtAuth().signOut()` sends what is unsent (at most 5 s), signs out, then deletes the whole
134
+ IndexedDB database `lt-offline-sync`. **A custom sign-out that bypasses it must call
135
+ `flushLtSyncBeforeSignOut(userId)` and `deleteLtSyncDatabase()` (from `/lib`) itself** — otherwise
136
+ the user's records stay on the device.
137
+ - **The flush sends only the signing-out user's own changes.** After an expired session the outbox
138
+ of the PREVIOUS user is still there; pushed under the current session it would land in the
139
+ current user's account. `flushLtSyncBeforeSignOut` compares the stored owner with `userId`.
140
+ - **An expired session deletes nothing.** The 401 interceptor calls `clearUser()`, not `signOut()`,
141
+ on purpose: the protocol promises that after signing in again the same changes go out without
142
+ duplicates. Never hook the sync cleanup into `clearUser()` — that would delete unsent work on
143
+ every expired session.
144
+ - Another user wipes the data before anything is shown or pulled (`claimLtSyncOwner`, one
145
+ transaction); so does data WITHOUT an owner. Nobody signed in sees nothing.
146
+ - **Every write of a sync run is fenced by the owner it started for** (`runLtSyncTx(…, { owner })`).
147
+ An answer arriving after sign-out deleted the database, or after another account claimed it,
148
+ would otherwise reopen the database and write the previous user's records for the next one.
149
+ - Local data is kept per model AND tenant (`ltSyncScope`): the server stamps a create into the
150
+ tenant of its `X-Tenant-Id`, so one tenant's unsent change must never go out under another.
151
+
152
+ **Rules that keep the engine correct (`src/runtime/lib/sync.ts`):**
153
+
154
+ - A push carries at most ONE change per record, and the next one goes out only once the one before
155
+ it is `applied` or `merged`. Its `baseVersion` assumes exactly that; sent while the first is open
156
+ (`retry`) or after it was refused (`rejected`, `not_found`, `conflict`), it can meet another
157
+ writer's version with the same number and be applied on top of it. So it waits behind a `retry`
158
+ and is dropped after a refusal (protocol §11 since nest-server 12.3.0, which also guards this
159
+ server-side within one request).
160
+ - An outbox item a push has carried (`attempted`) is never edited again; a later edit becomes a new
161
+ item. Read-and-mark happens in one IndexedDB transaction.
162
+ - `onConflict` runs outside any IndexedDB transaction (an awaited hook would let the transaction
163
+ auto-commit). Nothing inside `runLtSyncTx()` may await anything but that transaction's requests.
164
+ - Hints are read with `fetch`, not `EventSource` (no `Authorization` header in JWT mode). Every
165
+ reconnect pulls once: the server ends the stream on purpose and never replays the hint it missed.
166
+ - ONE hint stream per tab and tenant (`subscribeLtSyncHints`), for every model in use. A stream per
167
+ composable call holds a connection each; over HTTP/1.1 six of them stall every API request.
168
+ - A refused change (`rejected`) to a record the server has drops the cursor, so the pull right after
169
+ restores the server's state; otherwise the device would keep showing the refused value.
170
+
171
+ **Tests:** `test/sync-engine.test.ts` runs the protocol's five client scenarios (offline
172
+ create/change/delete, conflict, two tabs, expired session, cleared storage) against
173
+ `test/fixtures/sync-server.ts` with `fake-indexeddb`. The browser E2E against a real nest-server
174
+ lives in the nuxt-base-starter (DEV-3919).
175
+
176
+ A service worker must never cache `/sync` or `/iam` responses (nest-server
177
+ `src/core/modules/sync/SECURITY.md`).
178
+
115
179
  ## Testing Helpers
116
180
 
117
181
  Import from `@lenne.tech/nuxt-extensions/testing`:
package/README.md CHANGED
@@ -79,6 +79,7 @@ overrides:
79
79
  - **Better-Auth Integration** - Login, 2FA, Passkey/WebAuthn support
80
80
  - **Cookie/JWT Dual-Mode** - Automatic fallback from cookies to JWT
81
81
  - **TUS File Upload** - Resumable uploads with pause/resume
82
+ - **Offline Sync** - Read and write offline, reconcile with the nest-server sync module (opt-in)
82
83
  - **Transition Components** - Ready-to-use Vue transition wrappers
83
84
  - **i18n Support** - English and German translations (works without i18n too)
84
85
  - **Auto-imports** - All composables and components are auto-imported
@@ -196,6 +197,12 @@ export default defineNuxtConfig({
196
197
  enabled: true, // Enable AI composables / auto-imports
197
198
  basePath: '/ai', // Must match the nest-server AI controller
198
199
  },
200
+
201
+ // Offline sync (opt-in, see "Offline Sync" below)
202
+ sync: {
203
+ enabled: false, // Enable useLtOfflineSync() and the sign-out cleanup
204
+ basePath: '/sync', // Must match the nest-server sync module
205
+ },
199
206
  },
200
207
  });
201
208
  ```
@@ -722,6 +729,79 @@ All AI DTOs are exported as `LtAi*` types. The streaming endpoint is consumed vi
722
729
  > is enforced server-side (`@Restricted(ADMIN)`). Render admin UI behind a frontend
723
730
  > route guard for UX, but trust the backend for authorization.
724
731
 
732
+ ## Offline Sync
733
+
734
+ Read and write while disconnected, reconcile when back. `useLtOfflineSync()` is the client of the
735
+ `@lenne.tech/nest-server` **sync module** (12.2.0 or later); the protocol is
736
+ `docs/offline-sync-protocol.md` in nest-server. It is opt-in, both here and on the server:
737
+
738
+ ```typescript
739
+ // nuxt.config.ts
740
+ ltExtensions: {
741
+ sync: { enabled: true, basePath: '/sync' }, // basePath must match the nest-server sync module
742
+ }
743
+ ```
744
+
745
+ The API needs a sync model too: `@PersistedModel({ sync })` on the model and `@SyncService` on its
746
+ CrudService (nest-server `src/core/modules/sync/README.md`).
747
+
748
+ ```vue
749
+ <script setup lang="ts">
750
+ const { create, needsLogin, offline, pendingCount, records, remove } = useLtOfflineSync<{ title: string }>('note');
751
+
752
+ // Works offline: the change shows at once and waits in the outbox until the next sync
753
+ const add = (title: string) => create({ title });
754
+ </script>
755
+
756
+ <template>
757
+ <p v-if="offline">Offline — {{ pendingCount }} changes waiting</p>
758
+ <p v-if="needsLogin">Please sign in again; nothing is lost</p>
759
+ <ul>
760
+ <li v-for="note in records" :key="note.id">
761
+ {{ note.data?.title }} <button @click="remove(note.id)">Delete</button>
762
+ </li>
763
+ </ul>
764
+ </template>
765
+ ```
766
+
767
+ **What it does:**
768
+
769
+ - Keeps records, cursors and unsent changes in IndexedDB. Each change carries a `mutationId` and
770
+ the `baseVersion` it is based on, so a repeated send never duplicates and a stale one conflicts.
771
+ - Syncs at start, on `online`, on focus, shortly after a local change, and after a server hint
772
+ (`GET <basePath>/hints`, read with `fetch` so it signs in with a cookie and a JWT alike). Every
773
+ reconnect of the hint stream pulls once: the server ends the stream on purpose and does not
774
+ replay what it missed. All instances of a tab share one stream per tenant, and a hidden tab
775
+ keeps none open.
776
+ - Handles every item status and HTTP status of the protocol: `401` keeps everything (sign in again,
777
+ the same changes go out without duplicates), `410` drops the local records and pulls everything
778
+ again while keeping unsent changes, `403` on a pull drops the model's records, `429`/`503` and
779
+ network errors back off. A refused change is dropped and the record returns to the server's state.
780
+ - Tabs share the data: one syncs at a time (Web Locks), the others show the result
781
+ (BroadcastChannel).
782
+
783
+ **Options:** `idFormat` (`'objectId'` default, `'uuidv7'` for models declared with it),
784
+ `onConflict` (default: the server wins; return `{ data }` to send your answer again on top of the
785
+ server's version), `tenantId` (`X-Tenant-Id`; records and unsent changes are kept per tenant, so a
786
+ change made under one tenant never goes out under another), `pageLimit`, `hints: false`,
787
+ `auto: false` (only `sync()` syncs).
788
+
789
+ **Whose data it is.** The local data belongs to the signed-in user:
790
+
791
+ | Event | Local data |
792
+ |-------|-----------|
793
+ | `useLtAuth().signOut()` | The user's own unsent changes are sent (at most 5 s), then the whole database is deleted |
794
+ | Another user signs in | Deleted before anything is shown or pulled |
795
+ | Session expired, same user signs in again | Kept; the outbox goes out without duplicates |
796
+ | Nobody signed in | Nothing shown, changes refused |
797
+
798
+ A sync answer that arrives after the sign-out or the account switch is discarded, not stored. A
799
+ sign-out flow that bypasses `useLtAuth().signOut()` must do the same itself:
800
+ `flushLtSyncBeforeSignOut(userId)` and `deleteLtSyncDatabase()` from
801
+ `@lenne.tech/nuxt-extensions/lib`. The flush sends only when the local data belongs to `userId`.
802
+ `pendingCount` lets the app warn before signing out. A service worker must never cache responses
803
+ of `/sync` or `/iam` (nest-server `src/core/modules/sync/SECURITY.md`).
804
+
725
805
  ## i18n Support
726
806
 
727
807
  The package works **with or without** `@nuxtjs/i18n`:
@@ -753,6 +833,7 @@ The package works **with or without** `@nuxtjs/i18n`:
753
833
  | `useLtAiPrompts()` | User-facing prompt snippet CRUD (`'user'` / `'tenant'` scope) |
754
834
  | `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry |
755
835
  | `useLtAiAdmin()` | Admin CRUD for connections, preferences, budget limits, slots, prompt hints, interactions |
836
+ | `useLtOfflineSync()` | Offline read/write of one model, synced with the nest-server sync module (opt-in) |
756
837
 
757
838
  ### Components
758
839
 
package/dist/module.d.mts CHANGED
@@ -1,12 +1,12 @@
1
- import * as _nuxt_schema from '@nuxt/schema';
1
+ import * as nuxt_schema from 'nuxt/schema';
2
2
  import { LtExtensionsModuleOptions } from '../dist/runtime/types/index.js';
3
3
  export * from '../dist/runtime/types/index.js';
4
4
 
5
5
  declare const name = "@lenne.tech/nuxt-extensions";
6
- declare const version = "1.20.0";
6
+ declare const version = "1.21.0";
7
7
  declare const configKey = "ltExtensions";
8
8
  declare const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
9
9
  declare const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
10
- declare const _default: _nuxt_schema.NuxtModule<LtExtensionsModuleOptions, LtExtensionsModuleOptions, false>;
10
+ declare const _default: nuxt_schema.NuxtModule<LtExtensionsModuleOptions, LtExtensionsModuleOptions, false>;
11
11
 
12
12
  export { DEFAULT_LT_AUTH_STATE_COOKIE, DEFAULT_LT_JWT_TOKEN_COOKIE, configKey, _default as default, name, version };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "configKey": "ltExtensions",
3
3
  "name": "@lenne.tech/nuxt-extensions",
4
- "version": "1.20.0",
4
+ "version": "1.21.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -2,7 +2,7 @@ import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComp
2
2
  import { pathToFileURL } from 'node:url';
3
3
 
4
4
  const name = "@lenne.tech/nuxt-extensions";
5
- const version = "1.20.0";
5
+ const version = "1.21.0";
6
6
  const configKey = "ltExtensions";
7
7
  const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
8
8
  const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
@@ -46,6 +46,11 @@ const defaultOptions = {
46
46
  preHydrationInput: {
47
47
  enabled: true
48
48
  },
49
+ // Opt-in: an app without offline sync gets no auto-import and no sign-out cleanup
50
+ sync: {
51
+ basePath: "/sync",
52
+ enabled: false
53
+ },
49
54
  tus: {
50
55
  defaultChunkSize: 5 * 1024 * 1024,
51
56
  // 5MB
@@ -74,6 +79,7 @@ const module$1 = defineNuxtModule({
74
79
  formLabelAssociation: { ...defaultOptions.formLabelAssociation, ...options.formLabelAssociation },
75
80
  i18n: { ...defaultOptions.i18n, ...options.i18n },
76
81
  preHydrationInput: { ...defaultOptions.preHydrationInput, ...options.preHydrationInput },
82
+ sync: { ...defaultOptions.sync, ...options.sync },
77
83
  tus: { ...defaultOptions.tus, ...options.tus }
78
84
  };
79
85
  const passkeyAvailable = !!await tryResolveModule("@better-auth/passkey/client", [new URL(import.meta.url), pathToFileURL(`${nuxt.options.rootDir}/`)]);
@@ -146,6 +152,10 @@ const module$1 = defineNuxtModule({
146
152
  enabled: resolvedOptions.preHydrationInput?.enabled ?? true,
147
153
  maxRestoreMs: resolvedOptions.preHydrationInput?.maxRestoreMs ?? 1500
148
154
  },
155
+ sync: {
156
+ basePath: resolvedOptions.sync?.basePath || "/sync",
157
+ enabled: resolvedOptions.sync?.enabled ?? false
158
+ },
149
159
  tus: {
150
160
  defaultChunkSize: resolvedOptions.tus?.defaultChunkSize || 5 * 1024 * 1024,
151
161
  defaultEndpoint: resolvedOptions.tus?.defaultEndpoint || "/files/upload",
@@ -203,6 +213,9 @@ const module$1 = defineNuxtModule({
203
213
  { name: "parseLtAiSseStream", from: resolve("./runtime/lib/ai") }
204
214
  ]);
205
215
  }
216
+ if (resolvedOptions.sync?.enabled) {
217
+ addImports([{ name: "useLtOfflineSync", from: resolve("./runtime/composables/use-lt-offline-sync") }]);
218
+ }
206
219
  const transitionComponents = ["LtTransitionFade", "LtTransitionSlide", "LtTransitionSlideBottom", "LtTransitionSlideRevert", "LtTransitionFadeScale"];
207
220
  for (const componentName of transitionComponents) {
208
221
  addComponent({
@@ -21,10 +21,12 @@ type __VLS_Props = {
21
21
  /** Duration in ms for the enter animation (default: 100) */
22
22
  startDuration?: `${number}` | number;
23
23
  };
24
- declare var __VLS_7: {};
25
- type __VLS_Slots = {} & {
26
- default?: (props: typeof __VLS_7) => any;
24
+ declare const __VLS_template: () => {
25
+ __VLS_Slots: {
26
+ default?: (props: {}) => any;
27
+ };
27
28
  };
29
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
28
30
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
29
31
  leaveDuration: number | `${number}`;
30
32
  startDuration: number | `${number}`;
@@ -21,10 +21,12 @@ type __VLS_Props = {
21
21
  /** Duration in ms for the enter animation (default: 100) */
22
22
  startDuration?: `${number}` | number;
23
23
  };
24
- declare var __VLS_7: {};
25
- type __VLS_Slots = {} & {
26
- default?: (props: typeof __VLS_7) => any;
24
+ declare const __VLS_template: () => {
25
+ __VLS_Slots: {
26
+ default?: (props: {}) => any;
27
+ };
27
28
  };
29
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
28
30
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
29
31
  leaveDuration: number | `${number}`;
30
32
  startDuration: number | `${number}`;
@@ -24,10 +24,12 @@ type __VLS_Props = {
24
24
  /** Duration in ms for the enter animation (default: 100) */
25
25
  startDuration?: `${number}` | number;
26
26
  };
27
- declare var __VLS_7: {};
28
- type __VLS_Slots = {} & {
29
- default?: (props: typeof __VLS_7) => any;
27
+ declare const __VLS_template: () => {
28
+ __VLS_Slots: {
29
+ default?: (props: {}) => any;
30
+ };
30
31
  };
32
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
31
33
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
32
34
  leaveDuration: number | `${number}`;
33
35
  startDuration: number | `${number}`;
@@ -24,10 +24,12 @@ type __VLS_Props = {
24
24
  /** Duration in ms for the enter animation (default: 100) */
25
25
  startDuration?: `${number}` | number;
26
26
  };
27
- declare var __VLS_7: {};
28
- type __VLS_Slots = {} & {
29
- default?: (props: typeof __VLS_7) => any;
27
+ declare const __VLS_template: () => {
28
+ __VLS_Slots: {
29
+ default?: (props: {}) => any;
30
+ };
30
31
  };
32
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
31
33
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
32
34
  leaveDuration: number | `${number}`;
33
35
  startDuration: number | `${number}`;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,8 +1,17 @@
1
- import { useNuxtApp, useCookie, useState, useRequestHeaders, ref, computed, watch } from "#imports";
1
+ import { useNuxtApp, useCookie, useState, useRequestHeaders, useRuntimeConfig, ref, computed, watch } from "#imports";
2
2
  import { ltArrayBufferToBase64Url, ltBase64UrlToUint8Array } from "../../utils/crypto.js";
3
3
  import { clearLtAuthCookies, getLtApiBase, getLtAuthCookieNames, resolveLtAuthState } from "../../lib/auth-state.js";
4
4
  import { useLtAuthClient } from "../use-lt-auth-client.js";
5
5
  const VALIDATE_SESSION_TIMEOUT_MS = 1e4;
6
+ const LT_SYNC_SIGN_OUT_FLUSH_MS = 5e3;
7
+ function isLtSyncEnabled() {
8
+ if (!import.meta.client) return false;
9
+ try {
10
+ return !!useRuntimeConfig().public.ltExtensions?.sync?.enabled;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
6
15
  let staleJwtRefresh = null;
7
16
  function useTranslation() {
8
17
  const nuxtApp = useNuxtApp();
@@ -291,8 +300,14 @@ export function useLtAuth() {
291
300
  const signOut = async (options) => {
292
301
  isLoading.value = true;
293
302
  try {
303
+ const sync = isLtSyncEnabled() ? await import("../../lib/sync.js") : null;
304
+ await sync?.flushLtSyncBeforeSignOut(user.value?.id ? String(user.value.id) : void 0, LT_SYNC_SIGN_OUT_FLUSH_MS);
294
305
  const result = await authClient.signOut(options);
295
306
  clearUser();
307
+ if (sync) {
308
+ const { deleteLtSyncDatabase } = await import("../../lib/sync-store.js");
309
+ await deleteLtSyncDatabase().catch((error) => console.error("[LtAuth] Could not delete the local offline-sync data on sign-out:", error));
310
+ }
296
311
  return result;
297
312
  } finally {
298
313
  isLoading.value = false;
@@ -12,3 +12,4 @@ export { useLtAiUsage } from './use-lt-ai-usage.js';
12
12
  export { useLtAiAdmin } from './use-lt-ai-admin.js';
13
13
  export { useLtAiPrompts } from './use-lt-ai-prompts.js';
14
14
  export { useLtAiPlaceholders } from './use-lt-ai-placeholders.js';
15
+ export { useLtOfflineSync } from './use-lt-offline-sync.js';
@@ -12,3 +12,4 @@ export { useLtAiUsage } from "./use-lt-ai-usage.js";
12
12
  export { useLtAiAdmin } from "./use-lt-ai-admin.js";
13
13
  export { useLtAiPrompts } from "./use-lt-ai-prompts.js";
14
14
  export { useLtAiPlaceholders } from "./use-lt-ai-placeholders.js";
15
+ export { useLtOfflineSync } from "./use-lt-offline-sync.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Offline sync of one model with the nest-server sync module (>= 12.2.0).
3
+ *
4
+ * Records live in IndexedDB; changes made offline wait in an outbox and go out with the next sync.
5
+ * The composable syncs at start, on `online`, on focus and after a server hint, and pulls once on
6
+ * every reconnect of the hint stream (protocol §9). Tabs coordinate: one syncs at a time (Web Locks),
7
+ * the others pick up the result (BroadcastChannel); without either API it stays correct, only busier.
8
+ *
9
+ * The local data belongs to the signed-in user. A different user wipes it before anything is shown
10
+ * or pulled; `useLtAuth().signOut()` sends what is unsent and then deletes it. An expired session
11
+ * deletes nothing: after signing in again the outbox goes out without duplicates (protocol §4).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const notes = useLtOfflineSync<{ title: string }>('note');
16
+ * await notes.create({ title: 'Einkaufen' });
17
+ * // notes.records.value, notes.pendingCount.value, notes.offline.value, notes.needsLogin.value
18
+ * ```
19
+ */
20
+ import type { UseLtOfflineSyncOptions, UseLtOfflineSyncReturn } from '../types/sync.js';
21
+ export declare function useLtOfflineSync<T extends Record<string, unknown> = Record<string, unknown>>(model: string, options?: UseLtOfflineSyncOptions<T>): UseLtOfflineSyncReturn<T>;