@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.
Files changed (33) hide show
  1. package/CLAUDE.md +64 -0
  2. package/README.md +81 -0
  3. package/dist/module.d.mts +1 -1
  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/types/index.d.ts +1 -0
  30. package/dist/runtime/types/module.d.ts +7 -0
  31. package/dist/runtime/types/sync.d.ts +127 -0
  32. package/dist/runtime/types/sync.js +0 -0
  33. package/package.json +5 -4
@@ -0,0 +1,250 @@
1
+ import { getCurrentScope, onScopeDispose, readonly, ref, shallowRef, watch } from "#imports";
2
+ import { createLtSyncEngine } from "../lib/sync.js";
3
+ import { subscribeLtSyncHints } from "../lib/sync-hints.js";
4
+ import { claimLtSyncOwner, LT_SYNC_CHANNEL_NAME } from "../lib/sync-store.js";
5
+ import { useLtAuth } from "./auth/use-lt-auth.js";
6
+ const BACKOFF_START_MS = 1e3;
7
+ const BACKOFF_MAX_MS = 6e4;
8
+ const LOCAL_CHANGE_DELAY_MS = 300;
9
+ const emptyResult = (overrides = {}) => ({
10
+ applied: 0,
11
+ conflicts: [],
12
+ forbidden: false,
13
+ needsLogin: false,
14
+ offline: false,
15
+ rejected: [],
16
+ resynced: false,
17
+ retried: 0,
18
+ ...overrides
19
+ });
20
+ export function useLtOfflineSync(model, options = {}) {
21
+ const records = shallowRef([]);
22
+ const pendingCount = ref(0);
23
+ const offline = ref(false);
24
+ const syncing = ref(false);
25
+ const needsLogin = ref(false);
26
+ const forbidden = ref(false);
27
+ const conflicts = shallowRef([]);
28
+ const rejected = shallowRef([]);
29
+ const error = ref(null);
30
+ const lastSyncedAt = ref(null);
31
+ const state = {
32
+ conflicts: readonly(conflicts),
33
+ error: readonly(error),
34
+ forbidden: readonly(forbidden),
35
+ lastSyncedAt: readonly(lastSyncedAt),
36
+ needsLogin: readonly(needsLogin),
37
+ offline: readonly(offline),
38
+ pendingCount: readonly(pendingCount),
39
+ // A shallow ref typed read-only: consumers cannot reassign it but can bind single records
40
+ records,
41
+ rejected: readonly(rejected),
42
+ syncing: readonly(syncing)
43
+ };
44
+ if (import.meta.server || typeof indexedDB === "undefined") {
45
+ const browserOnly = () => Promise.reject(new Error("useLtOfflineSync: offline sync runs in the browser only"));
46
+ return { ...state, create: browserOnly, get: async () => void 0, remove: browserOnly, sync: async () => emptyResult(), update: browserOnly };
47
+ }
48
+ const auto = options.auto !== false;
49
+ const { user } = useLtAuth();
50
+ const engine = createLtSyncEngine({ idFormat: options.idFormat, model, onConflict: options.onConflict, pageLimit: options.pageLimit, tenantId: options.tenantId });
51
+ const channel = typeof BroadcastChannel === "undefined" ? void 0 : new BroadcastChannel(LT_SYNC_CHANNEL_NAME);
52
+ let ownerId = null;
53
+ let claiming = null;
54
+ const currentUserId = () => user.value?.id ? String(user.value.id) : null;
55
+ async function ensureOwner() {
56
+ const id = currentUserId();
57
+ if (!id) {
58
+ ownerId = null;
59
+ return false;
60
+ }
61
+ if (id === ownerId) return true;
62
+ if (claiming?.id !== id) {
63
+ const pending = { done: claimLtSyncOwner(id), id };
64
+ claiming = pending;
65
+ void pending.done.finally(() => {
66
+ if (claiming === pending) claiming = null;
67
+ });
68
+ }
69
+ await claiming.done;
70
+ if (currentUserId() !== id) return ensureOwner();
71
+ ownerId = id;
72
+ return true;
73
+ }
74
+ async function refresh() {
75
+ if (!await ensureOwner()) {
76
+ records.value = [];
77
+ pendingCount.value = 0;
78
+ return;
79
+ }
80
+ const [list, pending] = await Promise.all([engine.list(), engine.pendingCount()]);
81
+ records.value = list;
82
+ pendingCount.value = pending;
83
+ }
84
+ async function owned(fn) {
85
+ if (!await ensureOwner()) throw new Error("useLtOfflineSync: no signed-in user owns the local data");
86
+ return fn();
87
+ }
88
+ let timer;
89
+ let backoffMs = 0;
90
+ let disposed = false;
91
+ let closeHints;
92
+ let hintsStopped = null;
93
+ function schedule(ms) {
94
+ if (disposed) return;
95
+ clearTimeout(timer);
96
+ timer = setTimeout(() => void sync().catch(() => void 0), ms);
97
+ }
98
+ function backOff(retryAfterMs = 0) {
99
+ backoffMs = Math.min(backoffMs ? backoffMs * 2 : BACKOFF_START_MS, BACKOFF_MAX_MS);
100
+ if (auto && !(typeof navigator !== "undefined" && navigator.onLine === false)) schedule(Math.max(retryAfterMs, backoffMs));
101
+ }
102
+ async function sync() {
103
+ if (!await ensureOwner()) {
104
+ needsLogin.value = true;
105
+ return emptyResult({ needsLogin: true });
106
+ }
107
+ syncing.value = true;
108
+ try {
109
+ const result = await engine.sync();
110
+ error.value = null;
111
+ offline.value = result.offline;
112
+ needsLogin.value = result.needsLogin;
113
+ forbidden.value = result.forbidden;
114
+ conflicts.value = result.conflicts;
115
+ rejected.value = result.rejected;
116
+ if (result.offline || result.retryAfterMs !== void 0 || result.retried) {
117
+ backOff(result.retryAfterMs);
118
+ } else if (!result.needsLogin) {
119
+ backoffMs = 0;
120
+ lastSyncedAt.value = /* @__PURE__ */ new Date();
121
+ if (hintsStopped === "unauthorized") {
122
+ hintsStopped = null;
123
+ startHints();
124
+ }
125
+ }
126
+ return result;
127
+ } catch (caught) {
128
+ error.value = caught instanceof Error ? caught : new Error(String(caught));
129
+ throw caught;
130
+ } finally {
131
+ syncing.value = false;
132
+ await refresh();
133
+ channel?.postMessage({ scope: engine.scope, type: "changed" });
134
+ }
135
+ }
136
+ const autoSync = () => {
137
+ if (auto) void sync().catch(() => void 0);
138
+ };
139
+ async function afterLocalChange() {
140
+ channel?.postMessage({ scope: engine.scope, type: "changed" });
141
+ await refresh();
142
+ if (auto) schedule(LOCAL_CHANGE_DELAY_MS);
143
+ }
144
+ function startHints() {
145
+ if (!auto || options.hints === false || closeHints || hintsStopped || disposed) return;
146
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
147
+ if (!user.value?.id) return;
148
+ closeHints = subscribeLtSyncHints({
149
+ model,
150
+ // Hints sent while the stream was down are not replayed: every (re)connect pulls once
151
+ onConnect: autoSync,
152
+ onHint: autoSync,
153
+ onStop: (reason) => {
154
+ stopHints();
155
+ hintsStopped = reason;
156
+ },
157
+ tenantId: options.tenantId
158
+ });
159
+ }
160
+ function stopHints() {
161
+ closeHints?.();
162
+ closeHints = void 0;
163
+ }
164
+ const onOnline = () => {
165
+ offline.value = false;
166
+ backoffMs = 0;
167
+ autoSync();
168
+ };
169
+ const onOffline = () => {
170
+ offline.value = true;
171
+ };
172
+ const onVisibility = () => {
173
+ if (document.visibilityState === "hidden") {
174
+ stopHints();
175
+ return;
176
+ }
177
+ startHints();
178
+ autoSync();
179
+ };
180
+ const onMessage = (event) => {
181
+ if (event.data?.type === "cleared") {
182
+ ownerId = null;
183
+ void refresh();
184
+ } else if (event.data?.type === "changed" && event.data.scope === engine.scope) {
185
+ void refresh();
186
+ }
187
+ };
188
+ if (typeof window !== "undefined") {
189
+ window.addEventListener("online", onOnline);
190
+ window.addEventListener("offline", onOffline);
191
+ window.addEventListener("focus", autoSync);
192
+ document.addEventListener("visibilitychange", onVisibility);
193
+ if (navigator.onLine === false) offline.value = true;
194
+ }
195
+ channel?.addEventListener("message", onMessage);
196
+ const stopUserWatch = watch(
197
+ () => user.value?.id ?? null,
198
+ (id, previous) => {
199
+ if (id === previous) return;
200
+ stopHints();
201
+ hintsStopped = hintsStopped === "disabled" ? "disabled" : null;
202
+ void refresh().then(() => {
203
+ if (!id) return;
204
+ startHints();
205
+ autoSync();
206
+ });
207
+ }
208
+ );
209
+ if (getCurrentScope()) {
210
+ onScopeDispose(() => {
211
+ disposed = true;
212
+ clearTimeout(timer);
213
+ stopHints();
214
+ stopUserWatch();
215
+ channel?.removeEventListener("message", onMessage);
216
+ channel?.close();
217
+ if (typeof window !== "undefined") {
218
+ window.removeEventListener("online", onOnline);
219
+ window.removeEventListener("offline", onOffline);
220
+ window.removeEventListener("focus", autoSync);
221
+ document.removeEventListener("visibilitychange", onVisibility);
222
+ }
223
+ });
224
+ }
225
+ void refresh().then(() => {
226
+ startHints();
227
+ autoSync();
228
+ });
229
+ return {
230
+ ...state,
231
+ async create(data) {
232
+ const record = await owned(() => engine.create(data));
233
+ await afterLocalChange();
234
+ return record;
235
+ },
236
+ async get(id) {
237
+ return await ensureOwner() ? engine.get(id) : void 0;
238
+ },
239
+ async remove(id) {
240
+ await owned(() => engine.remove(id));
241
+ await afterLocalChange();
242
+ },
243
+ sync,
244
+ async update(id, data) {
245
+ const record = await owned(() => engine.update(id, data));
246
+ await afterLocalChange();
247
+ return record;
248
+ }
249
+ };
250
+ }
@@ -1,3 +1,5 @@
1
1
  export { attemptLtJwtSwitch, clearLtAuthCookies, createLtAuthFetch, getLtApiBase, getLtAuthCookieNames, getLtAuthMode, getLtJwtToken, isLtAuthenticated, LT_AUTH_STATE_COOKIE_DEFAULT, LT_JWT_TOKEN_COOKIE_DEFAULT, ltAuthFetch, setLtAuthMode, setLtJwtToken, } from './auth-state.js';
2
2
  export { clearLtAuthPluginRegistry, createLtAuthClient, getOrCreateLtAuthClient, getLtAuthPluginRegistry, registerLtAuthPlugins, resetLtAuthClientSingleton, type LtAuthClient, } from './auth-client.js';
3
3
  export { buildLtAiUrl, getLtAiBasePath, ltAiRequest, ltAiResponseError, parseLtAiSseStream } from './ai.js';
4
+ export { buildLtSyncUrl, flushLtSyncBeforeSignOut, getLtSyncBasePath, ltSyncObjectId, ltSyncUuidV7 } from './sync.js';
5
+ export { deleteLtSyncDatabase } from './sync-store.js';
@@ -22,3 +22,5 @@ export {
22
22
  resetLtAuthClientSingleton
23
23
  } from "./auth-client.js";
24
24
  export { buildLtAiUrl, getLtAiBasePath, ltAiRequest, ltAiResponseError, parseLtAiSseStream } from "./ai.js";
25
+ export { buildLtSyncUrl, flushLtSyncBeforeSignOut, getLtSyncBasePath, ltSyncObjectId, ltSyncUuidV7 } from "./sync.js";
26
+ export { deleteLtSyncDatabase } from "./sync-store.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The hint stream of the offline sync: `GET <basePath>/hints` as server-sent events (protocol §9).
3
+ *
4
+ * Read with `fetch` and a `ReadableStream`, not `EventSource`: an EventSource cannot send an
5
+ * `Authorization` header, so in JWT mode (the fallback `ltAuthFetch` switches to when the cookie
6
+ * is refused) it would never be signed in (decision E2 of DEV-3917).
7
+ *
8
+ * The server ends a stream on purpose before its first hint after the re-check interval (30 s by
9
+ * default) and does not replay that hint. So every (re)connect calls `onConnect`, and the caller
10
+ * pulls once — without it a change would wait for the next focus or start whenever hints are
11
+ * further apart than the interval.
12
+ */
13
+ export interface LtSyncHintsOptions {
14
+ /** `fetch` for the stream (default: `ltAuthFetch`) */
15
+ fetch?: (input: string, init: RequestInit) => Promise<Response>;
16
+ /** The models to hear about; a model the caller may not pull simply never sends a hint */
17
+ models: string[];
18
+ /** Every (re)connect. Hints sent while disconnected are not replayed: pull once (§9) */
19
+ onConnect: () => void;
20
+ /** A change of `model` was committed: pulling is worthwhile */
21
+ onHint: (model: string) => void;
22
+ /** The stream stopped for good: the server switched SSE off (`404`) or the session ended (`401`) */
23
+ onStop?: (reason: 'disabled' | 'unauthorized') => void;
24
+ /** Restrict to one tenant (`X-Tenant-Id`) */
25
+ tenantId?: string;
26
+ }
27
+ /** One instance listening for the hints of its model */
28
+ export interface LtSyncHintSubscriber {
29
+ model: string;
30
+ onConnect: () => void;
31
+ onHint: () => void;
32
+ onStop?: (reason: 'disabled' | 'unauthorized') => void;
33
+ tenantId?: string;
34
+ }
35
+ /**
36
+ * Listen for the hints of a model on the ONE stream this tab keeps per tenant, for every model in
37
+ * use (`?models=` takes a list, §9). A stream per `useLtOfflineSync()` call would hold a connection
38
+ * each; over HTTP/1.1 a browser allows six per host, so six instances on one page would stall every
39
+ * other request to the API, sign-in and sign-out included. Answers the unsubscribe.
40
+ */
41
+ export declare function subscribeLtSyncHints(subscriber: LtSyncHintSubscriber): () => void;
42
+ /**
43
+ * Open the hint stream and keep it open — reconnecting after the server ends it, backing off after
44
+ * failures — until the returned `close()` is called or the stream stops for good (`onStop`).
45
+ */
46
+ export declare function openLtSyncHints(options: LtSyncHintsOptions): () => void;
@@ -0,0 +1,147 @@
1
+ import { ltAuthFetch } from "./auth-state.js";
2
+ import { buildLtSyncUrl } from "./sync.js";
3
+ const LINE_LIMIT = 1024 * 1024;
4
+ const DEFAULT_RETRY_MS = 3e3;
5
+ const MAX_BACKOFF_MS = 6e4;
6
+ async function readEvents(body, onEvent, signal) {
7
+ const reader = body.getReader();
8
+ const decoder = new TextDecoder();
9
+ let buffer = "";
10
+ let event = { data: "", event: "message" };
11
+ const dispatchLine = (line) => {
12
+ if (line === "") {
13
+ if (event.data || event.retry !== void 0) onEvent(event);
14
+ event = { data: "", event: "message" };
15
+ return;
16
+ }
17
+ if (line.startsWith(":")) return;
18
+ const colon = line.indexOf(":");
19
+ const field = colon === -1 ? line : line.slice(0, colon);
20
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
21
+ if (field === "event") event.event = value;
22
+ else if (field === "data") event.data = event.data ? `${event.data}
23
+ ${value}` : value;
24
+ else if (field === "retry" && /^\d+$/.test(value)) event.retry = Number(value);
25
+ };
26
+ const onAbort = () => void reader.cancel().catch(() => void 0);
27
+ signal.addEventListener("abort", onAbort);
28
+ try {
29
+ for (; ; ) {
30
+ const { done, value } = await reader.read();
31
+ if (done) return;
32
+ buffer += decoder.decode(value, { stream: true });
33
+ let newline = buffer.search(/\r?\n|\r/);
34
+ while (newline !== -1) {
35
+ const line = buffer.slice(0, newline);
36
+ const skip = buffer[newline] === "\r" && buffer[newline + 1] === "\n" ? 2 : 1;
37
+ buffer = buffer.slice(newline + skip);
38
+ dispatchLine(line);
39
+ newline = buffer.search(/\r?\n|\r/);
40
+ }
41
+ if (buffer.length > LINE_LIMIT) throw new Error("sync hint stream: line exceeds 1 MiB");
42
+ }
43
+ } finally {
44
+ signal.removeEventListener("abort", onAbort);
45
+ reader.releaseLock();
46
+ }
47
+ }
48
+ const hubs = /* @__PURE__ */ new Map();
49
+ export function subscribeLtSyncHints(subscriber) {
50
+ const key = subscriber.tenantId ?? "";
51
+ let hub = hubs.get(key);
52
+ if (!hub) {
53
+ hub = { models: "", subscribers: /* @__PURE__ */ new Set() };
54
+ hubs.set(key, hub);
55
+ }
56
+ const joined = hub;
57
+ joined.subscribers.add(subscriber);
58
+ connectHub(key, joined);
59
+ return () => {
60
+ if (!joined.subscribers.delete(subscriber)) return;
61
+ if (joined.subscribers.size === 0) {
62
+ joined.close?.();
63
+ if (hubs.get(key) === joined) hubs.delete(key);
64
+ return;
65
+ }
66
+ connectHub(key, joined);
67
+ };
68
+ }
69
+ function connectHub(key, hub) {
70
+ const models = [...new Set([...hub.subscribers].map((subscriber) => subscriber.model))].sort().join(",");
71
+ if (hub.close && hub.models === models) return;
72
+ hub.close?.();
73
+ hub.models = models;
74
+ hub.close = openLtSyncHints({
75
+ models: models.split(","),
76
+ onConnect: () => {
77
+ for (const subscriber of [...hub.subscribers]) subscriber.onConnect();
78
+ },
79
+ onHint: (model) => {
80
+ for (const subscriber of [...hub.subscribers]) if (subscriber.model === model) subscriber.onHint();
81
+ },
82
+ onStop: (reason) => {
83
+ hub.close = void 0;
84
+ hub.models = "";
85
+ for (const subscriber of [...hub.subscribers]) subscriber.onStop?.(reason);
86
+ },
87
+ tenantId: key || void 0
88
+ });
89
+ }
90
+ export function openLtSyncHints(options) {
91
+ const { models, onConnect, onHint, onStop, tenantId } = options;
92
+ const fetchImpl = options.fetch ?? ((input, init) => ltAuthFetch(input, init));
93
+ let closed = false;
94
+ let controller;
95
+ let timer;
96
+ let wake;
97
+ let retryMs = DEFAULT_RETRY_MS;
98
+ let failures = 0;
99
+ const wait = (ms) => new Promise((resolve) => {
100
+ wake = resolve;
101
+ timer = setTimeout(resolve, ms);
102
+ });
103
+ const loop = async () => {
104
+ while (!closed) {
105
+ controller = new AbortController();
106
+ try {
107
+ const query = new URLSearchParams({ models: models.join(",") });
108
+ const headers = { accept: "text/event-stream" };
109
+ if (tenantId) headers["x-tenant-id"] = tenantId;
110
+ const response = await fetchImpl(buildLtSyncUrl(`/hints?${query}`), { headers, method: "GET", signal: controller.signal });
111
+ if (response.status === 404 || response.status === 401) {
112
+ await response.body?.cancel().catch(() => void 0);
113
+ if (!closed) onStop?.(response.status === 404 ? "disabled" : "unauthorized");
114
+ return;
115
+ }
116
+ if (!response.ok || !response.body) throw new Error(`sync hint stream answered ${response.status}`);
117
+ failures = 0;
118
+ onConnect();
119
+ await readEvents(
120
+ response.body,
121
+ (event) => {
122
+ if (event.retry !== void 0) retryMs = event.retry;
123
+ if (event.event !== "change") return;
124
+ try {
125
+ const model = JSON.parse(event.data).model;
126
+ if (typeof model === "string") onHint(model);
127
+ } catch {
128
+ }
129
+ },
130
+ controller.signal
131
+ );
132
+ if (!closed) await wait(retryMs);
133
+ } catch {
134
+ if (closed) return;
135
+ failures++;
136
+ await wait(Math.min(retryMs * 2 ** Math.min(failures - 1, 5), MAX_BACKOFF_MS));
137
+ }
138
+ }
139
+ };
140
+ void loop();
141
+ return () => {
142
+ closed = true;
143
+ controller?.abort();
144
+ clearTimeout(timer);
145
+ wake?.();
146
+ };
147
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * IndexedDB storage of the offline sync (`useLtOfflineSync`).
3
+ *
4
+ * ONE database per origin holds every synchronized model: records, the outbox of unsent changes,
5
+ * and meta values (cursors, the owning user). One database is what makes sign-out safe — the
6
+ * nest-server's `SECURITY.md` asks a client to delete every record, cursor and unsent change, and
7
+ * deleting the whole database is the one operation that cannot miss a model.
8
+ *
9
+ * Every tab of the origin shares the database; IndexedDB serializes their read-write transactions.
10
+ * A transaction stays open only while `fn` awaits requests of that same transaction, so nothing
11
+ * inside `run()` may await anything else (a fetch, a timer, an app hook).
12
+ */
13
+ import type { LtSyncRecord } from '../types/sync.js';
14
+ /** Name of the database every model shares */
15
+ export declare const LT_SYNC_DB_NAME = "lt-offline-sync";
16
+ /** BroadcastChannel the tabs of an origin use to tell each other about changes */
17
+ export declare const LT_SYNC_CHANNEL_NAME = "lt-offline-sync";
18
+ /** An outbox entry: a protocol push item (§6.2) plus local bookkeeping */
19
+ export interface LtSyncOutboxItem {
20
+ /** Set once a push carried the item; it is never changed again, a repeat must carry the same change (§6.5) */
21
+ attempted?: boolean;
22
+ baseVersion?: number;
23
+ data?: Record<string, unknown>;
24
+ id: string;
25
+ mutationId: string;
26
+ op: 'delete' | 'upsert';
27
+ seq?: number;
28
+ }
29
+ /** What `run()` hands its callback: the transaction's operations, scoped to one model */
30
+ export interface LtSyncStoreTx {
31
+ addOutbox: (item: LtSyncOutboxItem) => Promise<unknown>;
32
+ /** Delete every local record of the model except `keepIds` */
33
+ clearRecords: (keepIds: Set<string>) => Promise<void>;
34
+ deleteMeta: (key: string) => Promise<unknown>;
35
+ deleteRecord: (id: string) => Promise<unknown>;
36
+ getMeta: <V = unknown>(key: string) => Promise<undefined | V>;
37
+ getRecord: (id: string) => Promise<LtSyncRecord | undefined>;
38
+ /** The model's unsent changes in the order they were made */
39
+ listOutbox: () => Promise<LtSyncOutboxItem[]>;
40
+ listRecords: () => Promise<LtSyncRecord[]>;
41
+ putOutbox: (item: LtSyncOutboxItem) => Promise<unknown>;
42
+ putRecord: (record: LtSyncRecord) => Promise<unknown>;
43
+ removeOutbox: (mutationId: string) => Promise<void>;
44
+ setMeta: (key: string, value: unknown) => Promise<unknown>;
45
+ }
46
+ /**
47
+ * The data no longer belongs to the user a sync run started for: signed out (database deleted) or
48
+ * another account claimed it. The run's write was refused, nothing of it was stored.
49
+ */
50
+ export declare class LtSyncOwnerChangedError extends Error {
51
+ constructor();
52
+ }
53
+ /**
54
+ * Run `fn` in one read-write transaction over the scope's records, outbox and meta values. `scope`
55
+ * is a model, or a model and tenant (`note#<tenantId>`).
56
+ *
57
+ * With `owner`, the transaction first checks that the data still belongs to that user and refuses
58
+ * otherwise (`LtSyncOwnerChangedError`). A sync answer can arrive after sign-out deleted the
59
+ * database, or after another account claimed it — in this tab or another one. Unfenced, it would
60
+ * reopen the database and write the previous user's records for the next user to see.
61
+ */
62
+ export declare function runLtSyncTx<R>(model: string, fn: (tx: LtSyncStoreTx) => Promise<R>, options?: {
63
+ owner?: string;
64
+ }): Promise<R>;
65
+ /** The scopes (model, or model and tenant) that hold unsent changes, in no particular order */
66
+ export declare function listLtSyncPendingModels(): Promise<string[]>;
67
+ /** The user the local data belongs to, if any */
68
+ export declare function getLtSyncOwner(): Promise<string | undefined>;
69
+ /**
70
+ * Bind the local data to the signed-in user, in ONE transaction. The same user keeps it. Anybody
71
+ * else — another user, or nobody at all while data is present — means it is not this user's: every
72
+ * model is wiped before anything is shown or pulled (nest-server `SECURITY.md`). Data without an
73
+ * owner can only be a write that outlived a sign-out, which is exactly what must not be inherited.
74
+ * Answers whether it wiped.
75
+ */
76
+ export declare function claimLtSyncOwner(userId: string): Promise<boolean>;
77
+ /**
78
+ * Delete every record, cursor and unsent change of every model — on sign-out. Other tabs close
79
+ * their connection on `versionchange` and are told via the channel. A sync answer arriving later
80
+ * finds no owner and is refused (`runLtSyncTx` with `owner`).
81
+ */
82
+ export declare function deleteLtSyncDatabase(): Promise<void>;
83
+ /** Messages the tabs exchange */
84
+ export type LtSyncMessage = {
85
+ scope: string;
86
+ type: 'changed';
87
+ } | {
88
+ type: 'cleared';
89
+ };