@lenne.tech/nuxt-extensions 1.20.1 → 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.
- package/CLAUDE.md +64 -0
- package/README.md +81 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -1
- package/dist/runtime/components/transition/LtTransitionFade.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionFade.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionFadeScale.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionFadeScale.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlide.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlide.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideBottom.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideBottom.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideRevert.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideRevert.vue.d.ts +5 -3
- package/dist/runtime/composables/auth/use-lt-auth.js +16 -1
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/use-lt-offline-sync.d.ts +21 -0
- package/dist/runtime/composables/use-lt-offline-sync.js +250 -0
- package/dist/runtime/lib/index.d.ts +2 -0
- package/dist/runtime/lib/index.js +2 -0
- package/dist/runtime/lib/sync-hints.d.ts +46 -0
- package/dist/runtime/lib/sync-hints.js +147 -0
- package/dist/runtime/lib/sync-store.d.ts +89 -0
- package/dist/runtime/lib/sync-store.js +149 -0
- package/dist/runtime/lib/sync.d.ts +89 -0
- package/dist/runtime/lib/sync.js +369 -0
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/module.d.ts +7 -0
- package/dist/runtime/types/sync.d.ts +127 -0
- package/dist/runtime/types/sync.js +0 -0
- package/package.json +5 -4
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
|
@@ -3,7 +3,7 @@ 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.
|
|
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";
|
package/dist/module.json
CHANGED
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.
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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>;
|