@carlonicora/nextjs-jsonapi 6.12.2 → 6.13.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/dist/{ApiData-DPKNfY-9.d.mts → ApiData-Be8yxCOX.d.mts} +2 -0
- package/dist/{ApiData-DPKNfY-9.d.ts → ApiData-Be8yxCOX.d.ts} +2 -0
- package/dist/{BlockNoteEditor-75ED2CN2.mjs → BlockNoteEditor-22Q4DJBU.mjs} +20 -8
- package/dist/BlockNoteEditor-22Q4DJBU.mjs.map +1 -0
- package/dist/{BlockNoteEditor-7M5QW2YQ.js → BlockNoteEditor-2VLPSEKS.js} +34 -22
- package/dist/BlockNoteEditor-2VLPSEKS.js.map +1 -0
- package/dist/{BlockNoteViewer-WGCDB7IJ.js → BlockNoteViewer-2XYASA5Y.js} +8 -5
- package/dist/BlockNoteViewer-2XYASA5Y.js.map +1 -0
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs → BlockNoteViewer-NBW2H6LC.mjs} +7 -4
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs.map → BlockNoteViewer-NBW2H6LC.mjs.map} +1 -1
- package/dist/{JsonApiRequest-A66XK5WT.mjs → JsonApiRequest-F6TMJ42G.mjs} +2 -2
- package/dist/JsonApiRequest-WISJZSDE.js +26 -0
- package/dist/{JsonApiRequest-FZGPLNUF.js.map → JsonApiRequest-WISJZSDE.js.map} +1 -1
- package/dist/billing/index.js +499 -494
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +13 -8
- package/dist/billing/index.mjs.map +1 -1
- package/dist/chunk-27LQDGNP.mjs +317 -0
- package/dist/chunk-27LQDGNP.mjs.map +1 -0
- package/dist/{chunk-V3YDVJ63.js → chunk-2H342HNN.js} +948 -921
- package/dist/chunk-2H342HNN.js.map +1 -0
- package/dist/{chunk-KSYQEKJX.js → chunk-2SV4SS6N.js} +14 -14
- package/dist/{chunk-KSYQEKJX.js.map → chunk-2SV4SS6N.js.map} +1 -1
- package/dist/{chunk-4FNGWALA.mjs → chunk-47BBY5KA.mjs} +6 -4
- package/dist/{chunk-4FNGWALA.mjs.map → chunk-47BBY5KA.mjs.map} +1 -1
- package/dist/chunk-6JUCIWDC.mjs +570 -0
- package/dist/chunk-6JUCIWDC.mjs.map +1 -0
- package/dist/chunk-7DBJTNSU.mjs +1049 -0
- package/dist/chunk-7DBJTNSU.mjs.map +1 -0
- package/dist/chunk-7JZHPMGW.mjs +704 -0
- package/dist/chunk-7JZHPMGW.mjs.map +1 -0
- package/dist/{chunk-L6PYFQTO.js → chunk-7ZHTPFFT.js} +16 -14
- package/dist/chunk-7ZHTPFFT.js.map +1 -0
- package/dist/chunk-B6JGKDP2.js +214 -0
- package/dist/chunk-B6JGKDP2.js.map +1 -0
- package/dist/{chunk-3YW35YHQ.mjs → chunk-BH6JWIKI.mjs} +5 -5
- package/dist/chunk-BZ5J2FIV.js +570 -0
- package/dist/chunk-BZ5J2FIV.js.map +1 -0
- package/dist/{chunk-V5MI7RNB.mjs → chunk-DGWOVYEC.mjs} +2 -2
- package/dist/chunk-DOCIFE6A.mjs +212 -0
- package/dist/chunk-DOCIFE6A.mjs.map +1 -0
- package/dist/chunk-GB6DBFYY.js +25 -0
- package/dist/chunk-GB6DBFYY.js.map +1 -0
- package/dist/{chunk-RXQYEEKZ.mjs → chunk-H4NLDX5G.mjs} +51 -24
- package/dist/chunk-H4NLDX5G.mjs.map +1 -0
- package/dist/{chunk-EWCY6QJA.mjs → chunk-JJMMT24D.mjs} +37 -3
- package/dist/chunk-JJMMT24D.mjs.map +1 -0
- package/dist/{chunk-JTPRJNT2.js → chunk-KMW7UNL2.js} +38 -3
- package/dist/chunk-KMW7UNL2.js.map +1 -0
- package/dist/chunk-MHB36MYE.js +317 -0
- package/dist/chunk-MHB36MYE.js.map +1 -0
- package/dist/{chunk-FF4EIQ6H.js → chunk-MOGEGWXW.js} +1039 -1680
- package/dist/chunk-MOGEGWXW.js.map +1 -0
- package/dist/{chunk-AA52ORW2.mjs → chunk-P445QGOC.mjs} +61 -702
- package/dist/chunk-P445QGOC.mjs.map +1 -0
- package/dist/chunk-PV4Z5W6V.js +212 -0
- package/dist/chunk-PV4Z5W6V.js.map +1 -0
- package/dist/chunk-PXDUJ47N.js +1049 -0
- package/dist/chunk-PXDUJ47N.js.map +1 -0
- package/dist/{chunk-GHE4VT2R.js → chunk-QKNGZDSQ.js} +2 -2
- package/dist/{chunk-GHE4VT2R.js.map → chunk-QKNGZDSQ.js.map} +1 -1
- package/dist/chunk-W62MHDWM.js +704 -0
- package/dist/chunk-W62MHDWM.js.map +1 -0
- package/dist/chunk-XXP3TQHF.mjs +25 -0
- package/dist/chunk-XXP3TQHF.mjs.map +1 -0
- package/dist/chunk-YRKIFYOE.mjs +214 -0
- package/dist/chunk-YRKIFYOE.mjs.map +1 -0
- package/dist/{chunk-O3PDL2CV.js → chunk-Z4POQWFG.js} +40 -6
- package/dist/chunk-Z4POQWFG.js.map +1 -0
- package/dist/{chunk-PI56A4DG.mjs → chunk-ZLTOUZN2.mjs} +37 -2
- package/dist/chunk-ZLTOUZN2.mjs.map +1 -0
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +14 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +24 -19
- package/dist/components/index.d.mts +7 -1
- package/dist/components/index.d.ts +7 -1
- package/dist/components/index.js +11 -7
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -6
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +11 -7
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +10 -6
- package/dist/core/index.d.mts +128 -2
- package/dist/core/index.d.ts +128 -2
- package/dist/core/index.js +17 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +22 -9
- package/dist/email-automations/index.js +101 -96
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +13 -8
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +128 -123
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +13 -8
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +11 -8
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +9 -6
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.js +158 -153
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +14 -9
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +30 -26
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +10 -6
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/help/index.js +44 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +10 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +134 -129
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +13 -8
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -11
- package/dist/lifecycle-4GUOTB46.js +23 -0
- package/dist/lifecycle-4GUOTB46.js.map +1 -0
- package/dist/lifecycle-LKGPRBS7.mjs +23 -0
- package/dist/local-transport-7H3SPZSA.js +15 -0
- package/dist/local-transport-7H3SPZSA.js.map +1 -0
- package/dist/local-transport-QYFKA6AN.mjs +15 -0
- package/dist/local-transport-QYFKA6AN.mjs.map +1 -0
- package/dist/offline/index.d.mts +607 -0
- package/dist/offline/index.d.ts +607 -0
- package/dist/offline/index.js +1052 -0
- package/dist/offline/index.js.map +1 -0
- package/dist/offline/index.mjs +1052 -0
- package/dist/offline/index.mjs.map +1 -0
- package/dist/offline/sqlite/device-worker.mjs +62 -0
- package/dist/offline/sqlite/index.mjs +17392 -0
- package/dist/offline/sqlite/sqlite3-opfs-async-proxy.js +881 -0
- package/dist/offline/sqlite/sqlite3.wasm +0 -0
- package/dist/request-KTBV6HHH.js +10 -0
- package/dist/request-KTBV6HHH.js.map +1 -0
- package/dist/request-SQOUNWZ4.mjs +10 -0
- package/dist/request-SQOUNWZ4.mjs.map +1 -0
- package/dist/server/cachedGet.d.mts +1 -1
- package/dist/server/cachedGet.d.ts +1 -1
- package/dist/server/cachedGet.stub.d.mts +1 -1
- package/dist/server/cachedGet.stub.d.ts +1 -1
- package/dist/server/index.d.mts +28 -2
- package/dist/server/index.d.ts +28 -2
- package/dist/server/index.js +18 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -8
- package/dist/{useSocket-IRHtk2xc.d.ts → useSocket-Bo9M0t0J.d.ts} +3 -1
- package/dist/{useSocket-C8fqU4od.d.mts → useSocket-J5qanQPN.d.mts} +3 -1
- package/dist/write-through-FDQ7N4YI.js +15 -0
- package/dist/write-through-FDQ7N4YI.js.map +1 -0
- package/dist/write-through-R25VQMH6.mjs +15 -0
- package/dist/write-through-R25VQMH6.mjs.map +1 -0
- package/package.json +7 -1
- package/pwa/sw.js +277 -26
- package/src/client/request.ts +37 -0
- package/src/components/editors/BlockNoteEditor.tsx +14 -0
- package/src/components/editors/__tests__/BlockNoteEditor.empty-change.spec.tsx +205 -0
- package/src/contexts/SocketContext.tsx +7 -1
- package/src/contexts/__tests__/SocketContext.session.spec.tsx +52 -5
- package/src/core/factories/RehydrationFactory.ts +28 -8
- package/src/core/factories/__tests__/RehydrationFactory.spec.ts +33 -0
- package/src/core/index.ts +3 -0
- package/src/core/interfaces/ApiData.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/auth/components/forms/Logout.tsx +8 -1
- package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +62 -0
- package/src/hooks/__tests__/useSocket.pause.spec.ts +39 -0
- package/src/hooks/useSocket.ts +20 -1
- package/src/offline/__tests__/answer-builders.spec.ts +176 -0
- package/src/offline/__tests__/device-switch.spec.ts +179 -0
- package/src/offline/__tests__/generic-answers.spec.ts +380 -0
- package/src/offline/__tests__/local-transport.spec.ts +150 -0
- package/src/offline/__tests__/logout.spec.ts +122 -0
- package/src/offline/__tests__/offline-fixtures.ts +89 -0
- package/src/offline/__tests__/router.spec.ts +56 -0
- package/src/offline/__tests__/state.spec.ts +62 -0
- package/src/offline/__tests__/write-through.spec.ts +137 -0
- package/src/offline/config.ts +229 -0
- package/src/offline/configure.ts +14 -0
- package/src/offline/device-switch.ts +75 -0
- package/src/offline/generic-answers.ts +706 -0
- package/src/offline/index.ts +67 -0
- package/src/offline/local-transport.ts +189 -0
- package/src/offline/logout.ts +41 -0
- package/src/offline/router.ts +41 -0
- package/src/offline/server/__tests__/build-offline-manifest.spec.ts +76 -0
- package/src/offline/server/build-offline-manifest.ts +49 -0
- package/src/offline/state-hooks.ts +10 -0
- package/src/offline/state.ts +143 -0
- package/src/offline/store/DeviceStore.ts +112 -0
- package/src/offline/store/__tests__/device-store-lifecycle.spec.ts +343 -0
- package/src/offline/store/__tests__/shared-database.spec.ts +151 -0
- package/src/offline/store/__tests__/sql-device-store.spec.ts +399 -0
- package/src/offline/store/lifecycle.ts +315 -0
- package/src/offline/store/schema.ts +101 -0
- package/src/offline/store/shared-database.ts +321 -0
- package/src/offline/store/sql-device-store.ts +469 -0
- package/src/offline/store/worker-client.ts +71 -0
- package/src/offline/store/worker.ts +87 -0
- package/src/offline/sync/__tests__/conflicts.spec.ts +185 -0
- package/src/offline/sync/__tests__/queue-drain.spec.ts +364 -0
- package/src/offline/sync/__tests__/runtime.spec.ts +337 -0
- package/src/offline/sync/__tests__/sync-fixtures.ts +103 -0
- package/src/offline/sync/__tests__/sync-root.spec.ts +727 -0
- package/src/offline/sync/artwork-cache.ts +67 -0
- package/src/offline/sync/conflicts.ts +211 -0
- package/src/offline/sync/counts.ts +39 -0
- package/src/offline/sync/drain-queue.ts +246 -0
- package/src/offline/sync/known-roots.ts +37 -0
- package/src/offline/sync/models/OfflineChangeSet.ts +34 -0
- package/src/offline/sync/models/OfflineChangeSetInterface.ts +54 -0
- package/src/offline/sync/models/RootSync.ts +68 -0
- package/src/offline/sync/models/RootSyncInterface.ts +20 -0
- package/src/offline/sync/models/__tests__/OfflineChangeSet.spec.ts +47 -0
- package/src/offline/sync/models/__tests__/RootSync.spec.ts +54 -0
- package/src/offline/sync/models/index.ts +5 -0
- package/src/offline/sync/models/offline.modules.ts +14 -0
- package/src/offline/sync/runtime.ts +226 -0
- package/src/offline/sync/session-refresh.ts +32 -0
- package/src/offline/sync/sync-root.ts +336 -0
- package/src/offline/sync/waiting.ts +40 -0
- package/src/offline/types.ts +115 -0
- package/src/offline/write-through.ts +189 -0
- package/src/pwa/__tests__/sw.spec.ts +358 -1
- package/src/server/index.ts +5 -0
- package/src/unified/__tests__/JsonApiRequest.offline.spec.ts +243 -0
- package/dist/BlockNoteEditor-75ED2CN2.mjs.map +0 -1
- package/dist/BlockNoteEditor-7M5QW2YQ.js.map +0 -1
- package/dist/BlockNoteViewer-WGCDB7IJ.js.map +0 -1
- package/dist/JsonApiRequest-FZGPLNUF.js +0 -26
- package/dist/chunk-AA52ORW2.mjs.map +0 -1
- package/dist/chunk-EWCY6QJA.mjs.map +0 -1
- package/dist/chunk-FF4EIQ6H.js.map +0 -1
- package/dist/chunk-JTPRJNT2.js.map +0 -1
- package/dist/chunk-L6PYFQTO.js.map +0 -1
- package/dist/chunk-O3PDL2CV.js.map +0 -1
- package/dist/chunk-PI56A4DG.mjs.map +0 -1
- package/dist/chunk-RXQYEEKZ.mjs.map +0 -1
- package/dist/chunk-V3YDVJ63.js.map +0 -1
- package/dist/request-5RLK2AM3.mjs +0 -9
- package/dist/request-EUGTI3BE.js +0 -9
- package/dist/request-EUGTI3BE.js.map +0 -1
- /package/dist/{JsonApiRequest-A66XK5WT.mjs.map → JsonApiRequest-F6TMJ42G.mjs.map} +0 -0
- /package/dist/{chunk-3YW35YHQ.mjs.map → chunk-BH6JWIKI.mjs.map} +0 -0
- /package/dist/{chunk-V5MI7RNB.mjs.map → chunk-DGWOVYEC.mjs.map} +0 -0
- /package/dist/{request-5RLK2AM3.mjs.map → lifecycle-LKGPRBS7.mjs.map} +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { attachBrowserOnlineEvents, getOfflineState, setOfflineState } from "./state";
|
|
2
|
+
import type { DeviceStore } from "./store/DeviceStore";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_DEVICE_WORKER_URL,
|
|
5
|
+
DEFAULT_SQLITE_MODULE_URL,
|
|
6
|
+
NeedsConnectionError,
|
|
7
|
+
type OfflineConfig,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
// Kept free of store, worker and React imports: `directFetch` reads `isOfflineConfigured()` on
|
|
11
|
+
// every request, so this module is the only offline code an app that never opts in loads.
|
|
12
|
+
|
|
13
|
+
let _config: OfflineConfig | null = null;
|
|
14
|
+
let _opener: ((userId: string) => Promise<DeviceStore>) | null = null;
|
|
15
|
+
// Registered by the `./offline` entry (configure.ts), so the store code is reachable only from it.
|
|
16
|
+
let _defaultOpener: ((userId: string) => Promise<DeviceStore>) | null = null;
|
|
17
|
+
const _stores = new Map<string, Promise<DeviceStore>>();
|
|
18
|
+
// Users whose device store must not be opened again (logged out) until they sign in again.
|
|
19
|
+
const _blockedUsers = new Set<string>();
|
|
20
|
+
// Per user: whether offline mode is on on this device (`requireOptIn`). Read from localStorage
|
|
21
|
+
// once, since `isOfflineConfigured()` runs on every request.
|
|
22
|
+
const _enabledCache = new Map<string, boolean>();
|
|
23
|
+
|
|
24
|
+
const ENABLED_KEY_PREFIX = "nja-offline-enabled:";
|
|
25
|
+
const LAST_SYNC_KEY_PREFIX = "nja-offline-last-sync:";
|
|
26
|
+
|
|
27
|
+
function readStorage(key: string): string | null {
|
|
28
|
+
try {
|
|
29
|
+
return typeof localStorage === "undefined" ? null : localStorage.getItem(key);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeStorage(key: string, value: string | null): void {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof localStorage === "undefined") return;
|
|
38
|
+
if (value === null) localStorage.removeItem(key);
|
|
39
|
+
else localStorage.setItem(key, value);
|
|
40
|
+
} catch {
|
|
41
|
+
// Storage can be blocked or full: the switch then lasts for this page only.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The signed-in user's device store cannot be used: it could not be opened (opfs-sahpool holds
|
|
47
|
+
* one connection per origin, so a second tab cannot open it) or the user logged out. A request
|
|
48
|
+
* answered from the device copy then needs a connection (503 "needs-connection").
|
|
49
|
+
*/
|
|
50
|
+
export class DeviceStoreUnavailableError extends NeedsConnectionError {
|
|
51
|
+
constructor(message: string = "The device copy is not available in this tab") {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "DeviceStoreUnavailableError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* This browser cannot keep the device copy at all: not a secure context (https or `localhost`),
|
|
59
|
+
* no private file storage (`navigator.storage.getDirectory`), or the worker reports the OPFS
|
|
60
|
+
* APIs missing. Offline mode switches off for the page (`markOfflineUnavailable`). A failure on
|
|
61
|
+
* a browser that can keep the copy (the file held by another tab) is a `DeviceStoreUnavailableError`.
|
|
62
|
+
*/
|
|
63
|
+
export class OfflineUnavailableError extends DeviceStoreUnavailableError {
|
|
64
|
+
constructor(reason: string) {
|
|
65
|
+
super(`Offline mode isn't available in this browser: ${reason}`);
|
|
66
|
+
this.name = "OfflineUnavailableError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Switches offline mode off for this page, once: one console warning, `available: false` in the
|
|
72
|
+
* offline state. From then on `isOfflineConfigured()` answers false, so no request, sync, drain,
|
|
73
|
+
* write-through or lock check touches the device copy again.
|
|
74
|
+
*/
|
|
75
|
+
export function markOfflineUnavailable(error: OfflineUnavailableError): void {
|
|
76
|
+
if (!getOfflineState().available) return;
|
|
77
|
+
console.warn(`[offline] ${error.message}. Offline mode is off in this browser.`);
|
|
78
|
+
setOfflineState({ available: false });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Whether this browser can keep the device copy (true until found otherwise). */
|
|
82
|
+
export function isOfflineAvailable(): boolean {
|
|
83
|
+
return getOfflineState().available;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Logout: the user's store is never opened again in this page until `unblockOfflineStore`. */
|
|
87
|
+
export function blockOfflineStore(userId: string): void {
|
|
88
|
+
_blockedUsers.add(userId);
|
|
89
|
+
_stores.delete(userId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A sign-in (`onUserSignedIn`): the user's store may be opened again. */
|
|
93
|
+
export function unblockOfflineStore(userId: string): void {
|
|
94
|
+
_blockedUsers.delete(userId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isOfflineStoreBlocked(userId: string): boolean {
|
|
98
|
+
return _blockedUsers.has(userId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Opts the app into offline mode. Without this call nothing offline runs. */
|
|
102
|
+
export function configureOffline(config: OfflineConfig): void {
|
|
103
|
+
_config = config;
|
|
104
|
+
attachBrowserOnlineEvents();
|
|
105
|
+
refreshOfflineDeviceState();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Configured, the browser can keep the device copy (see `markOfflineUnavailable`), and offline
|
|
110
|
+
* mode is on on this device for the signed-in user (see `requireOptIn`).
|
|
111
|
+
*/
|
|
112
|
+
export function isOfflineConfigured(): boolean {
|
|
113
|
+
return _config !== null && isOfflineAvailable() && isOfflineEnabledOnDevice(_config.getUserId());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether offline mode is on on this device for `userId`. Always true without
|
|
118
|
+
* `OfflineConfig.requireOptIn`; otherwise the stored switch, false with no user.
|
|
119
|
+
*/
|
|
120
|
+
export function isOfflineEnabledOnDevice(userId?: string | null): boolean {
|
|
121
|
+
if (!_config?.requireOptIn) return true;
|
|
122
|
+
if (!userId) return false;
|
|
123
|
+
let enabled = _enabledCache.get(userId);
|
|
124
|
+
if (enabled === undefined) {
|
|
125
|
+
enabled = readStorage(ENABLED_KEY_PREFIX + userId) === "1";
|
|
126
|
+
_enabledCache.set(userId, enabled);
|
|
127
|
+
}
|
|
128
|
+
return enabled;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Stores the device switch for `userId` (`turnOnOffline` / `turnOffOffline`). */
|
|
132
|
+
export function setOfflineEnabledOnDevice(userId: string, enabled: boolean): void {
|
|
133
|
+
writeStorage(ENABLED_KEY_PREFIX + userId, enabled ? "1" : null);
|
|
134
|
+
_enabledCache.set(userId, enabled);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** When `userId`'s last full sync on this device finished (epoch ms), or null. */
|
|
138
|
+
export function readOfflineLastSync(userId: string): number | null {
|
|
139
|
+
const value = Number(readStorage(LAST_SYNC_KEY_PREFIX + userId));
|
|
140
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Stores, or with null removes, when `userId`'s last full sync finished. */
|
|
144
|
+
export function writeOfflineLastSync(userId: string, at: number | null): void {
|
|
145
|
+
writeStorage(LAST_SYNC_KEY_PREFIX + userId, at === null ? null : String(at));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reads the device switch and the last sync time of the signed-in user into the offline state
|
|
150
|
+
* (`enabled`, `lastSyncedAt`). Does nothing before `configureOffline`.
|
|
151
|
+
*/
|
|
152
|
+
export function refreshOfflineDeviceState(): void {
|
|
153
|
+
if (!_config) return;
|
|
154
|
+
const userId = _config.getUserId();
|
|
155
|
+
if (userId) _enabledCache.delete(userId);
|
|
156
|
+
setOfflineState({
|
|
157
|
+
enabled: isOfflineEnabledOnDevice(userId),
|
|
158
|
+
lastSyncedAt: userId ? readOfflineLastSync(userId) : null,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getOfflineConfig(): OfflineConfig {
|
|
163
|
+
if (!_config) throw new Error("Offline mode is not configured. Call configureOffline() first.");
|
|
164
|
+
return _config;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The browser opener, set by the `./offline` entry's `configureOffline`. */
|
|
168
|
+
export function setDefaultDeviceStoreOpener(opener: (userId: string) => Promise<DeviceStore>): void {
|
|
169
|
+
_defaultOpener = opener;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The URL the device worker loads SQLite from (`OfflineConfig.sqliteModuleUrl`). */
|
|
173
|
+
export function getSqliteModuleUrl(): string {
|
|
174
|
+
return _config?.sqliteModuleUrl ?? DEFAULT_SQLITE_MODULE_URL;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The URL the device worker is started from (`OfflineConfig.deviceWorkerUrl`). */
|
|
178
|
+
export function getDeviceWorkerUrl(): string {
|
|
179
|
+
return _config?.deviceWorkerUrl ?? DEFAULT_DEVICE_WORKER_URL;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Replaces how a user's device store is opened (tests pass `openMemoryDeviceStore`). */
|
|
183
|
+
export function setDeviceStoreOpener(opener: ((userId: string) => Promise<DeviceStore>) | null): void {
|
|
184
|
+
_opener = opener;
|
|
185
|
+
_stores.clear();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The signed-in user's device store, opened once per user id. Only the signed-in user's file is
|
|
190
|
+
* ever opened; with no signed-in user the request needs a connection.
|
|
191
|
+
*/
|
|
192
|
+
export async function getOfflineStore(): Promise<DeviceStore> {
|
|
193
|
+
const userId = getOfflineConfig().getUserId();
|
|
194
|
+
if (!userId) throw new NeedsConnectionError("No signed-in user");
|
|
195
|
+
if (!isOfflineEnabledOnDevice(userId)) throw new DeviceStoreUnavailableError("Offline mode is off on this device");
|
|
196
|
+
if (_blockedUsers.has(userId)) throw new DeviceStoreUnavailableError("The device copy was deleted at logout");
|
|
197
|
+
if (!isOfflineAvailable()) throw new OfflineUnavailableError("it was found unable to keep the device copy");
|
|
198
|
+
|
|
199
|
+
let store = _stores.get(userId);
|
|
200
|
+
if (!store) {
|
|
201
|
+
const opener = _opener ?? _defaultOpener;
|
|
202
|
+
if (!opener) throw new Error("No device store opener: configure offline mode through the ./offline entry.");
|
|
203
|
+
store = opener(userId).catch((error) => {
|
|
204
|
+
if (error instanceof OfflineUnavailableError) markOfflineUnavailable(error);
|
|
205
|
+
if (error instanceof NeedsConnectionError) throw error;
|
|
206
|
+
console.warn("[offline] the device copy could not be opened", error);
|
|
207
|
+
throw new DeviceStoreUnavailableError(
|
|
208
|
+
`The device copy could not be opened: ${String((error as Error)?.message ?? error)}`,
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
_stores.set(userId, store);
|
|
212
|
+
store.catch(() => _stores.delete(userId));
|
|
213
|
+
}
|
|
214
|
+
return store;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Forgets the opened store of a user (logout, deleted file). */
|
|
218
|
+
export function forgetOfflineStore(userId: string): void {
|
|
219
|
+
_stores.delete(userId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Test helper: back to "not configured". */
|
|
223
|
+
export function resetOfflineConfig(): void {
|
|
224
|
+
_config = null;
|
|
225
|
+
_opener = null;
|
|
226
|
+
_stores.clear();
|
|
227
|
+
_blockedUsers.clear();
|
|
228
|
+
_enabledCache.clear();
|
|
229
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { configureOffline as configureOfflineLayer, setDefaultDeviceStoreOpener } from "./config";
|
|
2
|
+
import type { OfflineConfig } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Opts the app into offline mode (the `./offline` entry's `configureOffline`). It also registers
|
|
6
|
+
* the browser device-store opener, so the store, the worker and SQLite are reachable only from
|
|
7
|
+
* this entry, never from the request path every other entry shares.
|
|
8
|
+
*/
|
|
9
|
+
export function configureOffline(config: OfflineConfig): void {
|
|
10
|
+
setDefaultDeviceStoreOpener((userId) =>
|
|
11
|
+
import("./store/lifecycle").then((lifecycle) => lifecycle.openDeviceStore(userId)),
|
|
12
|
+
);
|
|
13
|
+
configureOfflineLayer(config);
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forgetOfflineStore,
|
|
3
|
+
getOfflineConfig,
|
|
4
|
+
getOfflineStore,
|
|
5
|
+
isOfflineAvailable,
|
|
6
|
+
setOfflineEnabledOnDevice,
|
|
7
|
+
writeOfflineLastSync,
|
|
8
|
+
} from "./config";
|
|
9
|
+
import { setOfflineState } from "./state";
|
|
10
|
+
import { deleteImagesCaches } from "./sync/artwork-cache";
|
|
11
|
+
import { stopOfflineRuntime } from "./sync/runtime";
|
|
12
|
+
|
|
13
|
+
function signedInUserId(): string {
|
|
14
|
+
const userId = getOfflineConfig().getUserId();
|
|
15
|
+
if (!userId) throw new Error("No signed-in user");
|
|
16
|
+
return userId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Turns offline mode on on this device for the signed-in user (`OfflineConfig.requireOptIn`).
|
|
21
|
+
* It does not start the runtime: the app starts it once `enabled` is true in the offline state.
|
|
22
|
+
*/
|
|
23
|
+
export async function turnOnOffline(): Promise<void> {
|
|
24
|
+
const userId = signedInUserId();
|
|
25
|
+
setOfflineEnabledOnDevice(userId, true);
|
|
26
|
+
setOfflineState({ enabled: true });
|
|
27
|
+
try {
|
|
28
|
+
// Asks the browser not to evict the device copy. Best effort only.
|
|
29
|
+
await navigator.storage?.persist?.();
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignored: the copy works without it.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Turns offline mode off on this device for the signed-in user and deletes this device's copy
|
|
37
|
+
* (database and cached images). Refuses, changing nothing, while the copy has changes still to
|
|
38
|
+
* send: the answer then carries how many.
|
|
39
|
+
*/
|
|
40
|
+
export async function turnOffOffline(): Promise<{ turnedOff: true } | { turnedOff: false; pending: number }> {
|
|
41
|
+
const userId = signedInUserId();
|
|
42
|
+
|
|
43
|
+
if (isOfflineAvailable()) {
|
|
44
|
+
let pending = 0;
|
|
45
|
+
try {
|
|
46
|
+
const store = await getOfflineStore();
|
|
47
|
+
pending = (await store.queue()).length;
|
|
48
|
+
} catch {
|
|
49
|
+
// The copy cannot be opened here (held by another tab): the marker file written while
|
|
50
|
+
// changes wait still says whether deleting it would lose any. The count is then unknown: 1.
|
|
51
|
+
const { hasUnsentChanges } = await import("./store/lifecycle");
|
|
52
|
+
if (await hasUnsentChanges(userId).catch(() => false)) pending = 1;
|
|
53
|
+
}
|
|
54
|
+
if (pending > 0) return { turnedOff: false, pending };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Off first, so a sync still running cannot open, and so recreate, the deleted file.
|
|
58
|
+
setOfflineEnabledOnDevice(userId, false);
|
|
59
|
+
|
|
60
|
+
if (isOfflineAvailable()) {
|
|
61
|
+
stopOfflineRuntime();
|
|
62
|
+
try {
|
|
63
|
+
const { deleteDeviceStore } = await import("./store/lifecycle");
|
|
64
|
+
await deleteDeviceStore(userId);
|
|
65
|
+
await deleteImagesCaches();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn("[offline] the device copy could not be deleted", error);
|
|
68
|
+
}
|
|
69
|
+
forgetOfflineStore(userId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
writeOfflineLastSync(userId, null);
|
|
73
|
+
setOfflineState({ enabled: false, syncProgress: null, lastSyncedAt: null, rootCount: null });
|
|
74
|
+
return { turnedOff: true };
|
|
75
|
+
}
|