@carlonicora/nextjs-jsonapi 6.13.0 → 6.14.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/{BlockNoteEditor-2VLPSEKS.js → BlockNoteEditor-CUNH4LSQ.js} +25 -25
- package/dist/{BlockNoteEditor-2VLPSEKS.js.map → BlockNoteEditor-CUNH4LSQ.js.map} +1 -1
- package/dist/{BlockNoteEditor-22Q4DJBU.mjs → BlockNoteEditor-QIVV2OB4.mjs} +10 -10
- package/dist/{BlockNoteViewer-2XYASA5Y.js → BlockNoteViewer-APBOPT4B.js} +8 -8
- package/dist/{BlockNoteViewer-2XYASA5Y.js.map → BlockNoteViewer-APBOPT4B.js.map} +1 -1
- package/dist/{BlockNoteViewer-NBW2H6LC.mjs → BlockNoteViewer-HIUAJQRI.mjs} +7 -7
- package/dist/JsonApiRequest-G4J2WTSU.js +26 -0
- package/dist/{JsonApiRequest-WISJZSDE.js.map → JsonApiRequest-G4J2WTSU.js.map} +1 -1
- package/dist/{JsonApiRequest-F6TMJ42G.mjs → JsonApiRequest-IKKHWCPB.mjs} +2 -2
- package/dist/billing/index.js +497 -497
- package/dist/billing/index.mjs +9 -9
- package/dist/{chunk-7DBJTNSU.mjs → chunk-4RPGFQBA.mjs} +2 -2
- package/dist/{chunk-BZ5J2FIV.js → chunk-4XLDBOSE.js} +11 -11
- package/dist/{chunk-BZ5J2FIV.js.map → chunk-4XLDBOSE.js.map} +1 -1
- package/dist/{chunk-27LQDGNP.mjs → chunk-4ZJIUTIV.mjs} +6 -1
- package/dist/chunk-4ZJIUTIV.mjs.map +1 -0
- package/dist/{chunk-PXDUJ47N.js → chunk-6RCZ64BQ.js} +18 -18
- package/dist/{chunk-PXDUJ47N.js.map → chunk-6RCZ64BQ.js.map} +1 -1
- package/dist/{chunk-7JZHPMGW.mjs → chunk-A3RB47X4.mjs} +44 -20
- package/dist/chunk-A3RB47X4.mjs.map +1 -0
- package/dist/{chunk-DOCIFE6A.mjs → chunk-AUD6ZLBB.mjs} +3 -3
- package/dist/{chunk-GB6DBFYY.js → chunk-DGN3PR2F.js} +3 -3
- package/dist/{chunk-GB6DBFYY.js.map → chunk-DGN3PR2F.js.map} +1 -1
- package/dist/{chunk-7ZHTPFFT.js → chunk-DJXSCARG.js} +15 -15
- package/dist/{chunk-7ZHTPFFT.js.map → chunk-DJXSCARG.js.map} +1 -1
- package/dist/{chunk-QKNGZDSQ.js → chunk-GM4VZTWL.js} +2 -2
- package/dist/{chunk-QKNGZDSQ.js.map → chunk-GM4VZTWL.js.map} +1 -1
- package/dist/{chunk-2H342HNN.js → chunk-GSRTVIIK.js} +1184 -1178
- package/dist/chunk-GSRTVIIK.js.map +1 -0
- package/dist/{chunk-47BBY5KA.mjs → chunk-HAW5PNJG.mjs} +4 -4
- package/dist/{chunk-YRKIFYOE.mjs → chunk-KTGKW72G.mjs} +27 -8
- package/dist/chunk-KTGKW72G.mjs.map +1 -0
- package/dist/{chunk-PV4Z5W6V.js → chunk-KV4N7DEG.js} +19 -19
- package/dist/{chunk-PV4Z5W6V.js.map → chunk-KV4N7DEG.js.map} +1 -1
- package/dist/{chunk-ZLTOUZN2.mjs → chunk-M3N7LPHX.mjs} +4 -4
- package/dist/{chunk-DGWOVYEC.mjs → chunk-MQ7TXALH.mjs} +2 -2
- package/dist/{chunk-W62MHDWM.js → chunk-N5ZE2Z6I.js} +58 -34
- package/dist/chunk-N5ZE2Z6I.js.map +1 -0
- package/dist/{chunk-2SV4SS6N.js → chunk-OQQ7CZWT.js} +12 -12
- package/dist/{chunk-2SV4SS6N.js.map → chunk-OQQ7CZWT.js.map} +1 -1
- package/dist/{chunk-JJMMT24D.mjs → chunk-OUEYZOLB.mjs} +2 -2
- package/dist/{chunk-XXP3TQHF.mjs → chunk-OY65RYD3.mjs} +2 -2
- package/dist/{chunk-MOGEGWXW.js → chunk-PMESKNKL.js} +868 -868
- package/dist/{chunk-MOGEGWXW.js.map → chunk-PMESKNKL.js.map} +1 -1
- package/dist/{chunk-B6JGKDP2.js → chunk-QGVVF77L.js} +48 -29
- package/dist/chunk-QGVVF77L.js.map +1 -0
- package/dist/{chunk-BH6JWIKI.mjs → chunk-QIEF7HIL.mjs} +2 -2
- package/dist/{chunk-P445QGOC.mjs → chunk-RFKSJIQB.mjs} +7 -7
- package/dist/{chunk-6JUCIWDC.mjs → chunk-UOTHRJD2.mjs} +2 -2
- package/dist/{chunk-KMW7UNL2.js → chunk-WCEYT3GG.js} +8 -8
- package/dist/{chunk-KMW7UNL2.js.map → chunk-WCEYT3GG.js.map} +1 -1
- package/dist/{chunk-MHB36MYE.js → chunk-WUNSG3IN.js} +7 -2
- package/dist/chunk-WUNSG3IN.js.map +1 -0
- package/dist/{chunk-H4NLDX5G.mjs → chunk-YKNULEHB.mjs} +19 -13
- package/dist/chunk-YKNULEHB.mjs.map +1 -0
- package/dist/{chunk-Z4POQWFG.js → chunk-ZYSNJKOP.js} +4 -4
- package/dist/{chunk-Z4POQWFG.js.map → chunk-ZYSNJKOP.js.map} +1 -1
- package/dist/client/index.js +10 -10
- package/dist/client/index.mjs +9 -9
- package/dist/components/index.js +10 -10
- package/dist/components/index.mjs +9 -9
- package/dist/contexts/index.js +10 -10
- package/dist/contexts/index.mjs +9 -9
- package/dist/core/index.js +7 -7
- package/dist/core/index.mjs +6 -6
- package/dist/email-automations/index.js +99 -99
- package/dist/email-automations/index.mjs +9 -9
- package/dist/features/analytics/index.js +126 -126
- package/dist/features/analytics/index.mjs +9 -9
- package/dist/features/analytics/server.js +10 -10
- package/dist/features/analytics/server.mjs +8 -8
- package/dist/features/blog/index.js +156 -156
- package/dist/features/blog/index.mjs +9 -9
- package/dist/features/blog/server.js +28 -28
- package/dist/features/blog/server.mjs +7 -7
- package/dist/features/help/index.js +43 -43
- package/dist/features/help/index.mjs +9 -9
- package/dist/features/tokenusage/index.js +132 -132
- package/dist/features/tokenusage/index.mjs +9 -9
- package/dist/index.js +8 -8
- package/dist/index.mjs +7 -7
- package/dist/{lifecycle-LKGPRBS7.mjs → lifecycle-KI3PCJSE.mjs} +3 -3
- package/dist/lifecycle-TO65I7TO.js +23 -0
- package/dist/{lifecycle-4GUOTB46.js.map → lifecycle-TO65I7TO.js.map} +1 -1
- package/dist/local-transport-4YDJ6FQG.js +15 -0
- package/dist/{local-transport-7H3SPZSA.js.map → local-transport-4YDJ6FQG.js.map} +1 -1
- package/dist/local-transport-C7YTA4I7.mjs +15 -0
- package/dist/offline/index.d.mts +160 -4
- package/dist/offline/index.d.ts +160 -4
- package/dist/offline/index.js +210 -163
- package/dist/offline/index.js.map +1 -1
- package/dist/offline/index.mjs +76 -29
- package/dist/offline/index.mjs.map +1 -1
- package/dist/request-66FGJIHB.mjs +10 -0
- package/dist/request-6W6UGKJV.js +10 -0
- package/dist/{request-KTBV6HHH.js.map → request-6W6UGKJV.js.map} +1 -1
- package/dist/server/index.js +9 -9
- package/dist/server/index.mjs +8 -8
- package/dist/write-through-BAPS24GL.js +15 -0
- package/dist/{write-through-FDQ7N4YI.js.map → write-through-BAPS24GL.js.map} +1 -1
- package/dist/write-through-IWQKXYHE.mjs +15 -0
- package/package.json +1 -1
- package/src/features/onboarding/contexts/OnboardingContext.spec.tsx +42 -1
- package/src/features/onboarding/contexts/OnboardingContext.tsx +11 -1
- package/src/offline/__tests__/generic-answers.spec.ts +146 -3
- package/src/offline/__tests__/local-transport.spec.ts +15 -1
- package/src/offline/__tests__/offline-fixtures.ts +29 -2
- package/src/offline/__tests__/write-through.spec.ts +27 -1
- package/src/offline/config.ts +9 -0
- package/src/offline/generic-answers.ts +44 -17
- package/src/offline/index.ts +2 -0
- package/src/offline/local-transport.ts +5 -3
- package/src/offline/sync/__tests__/runtime.spec.ts +30 -1
- package/src/offline/sync/__tests__/sync-fixtures.ts +22 -0
- package/src/offline/sync/__tests__/sync-root.spec.ts +184 -1
- package/src/offline/sync/known-roots.ts +29 -7
- package/src/offline/sync/runtime.ts +3 -2
- package/src/offline/sync/sync-root.ts +70 -19
- package/src/offline/types.ts +39 -1
- package/src/offline/write-through.ts +6 -3
- package/dist/JsonApiRequest-WISJZSDE.js +0 -26
- package/dist/chunk-27LQDGNP.mjs.map +0 -1
- package/dist/chunk-2H342HNN.js.map +0 -1
- package/dist/chunk-7JZHPMGW.mjs.map +0 -1
- package/dist/chunk-B6JGKDP2.js.map +0 -1
- package/dist/chunk-H4NLDX5G.mjs.map +0 -1
- package/dist/chunk-MHB36MYE.js.map +0 -1
- package/dist/chunk-W62MHDWM.js.map +0 -1
- package/dist/chunk-YRKIFYOE.mjs.map +0 -1
- package/dist/lifecycle-4GUOTB46.js +0 -23
- package/dist/local-transport-7H3SPZSA.js +0 -15
- package/dist/local-transport-QYFKA6AN.mjs +0 -15
- package/dist/request-KTBV6HHH.js +0 -10
- package/dist/request-SQOUNWZ4.mjs +0 -10
- package/dist/write-through-FDQ7N4YI.js +0 -15
- package/dist/write-through-R25VQMH6.mjs +0 -15
- /package/dist/{BlockNoteEditor-22Q4DJBU.mjs.map → BlockNoteEditor-QIVV2OB4.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-NBW2H6LC.mjs.map → BlockNoteViewer-HIUAJQRI.mjs.map} +0 -0
- /package/dist/{JsonApiRequest-F6TMJ42G.mjs.map → JsonApiRequest-IKKHWCPB.mjs.map} +0 -0
- /package/dist/{chunk-7DBJTNSU.mjs.map → chunk-4RPGFQBA.mjs.map} +0 -0
- /package/dist/{chunk-DOCIFE6A.mjs.map → chunk-AUD6ZLBB.mjs.map} +0 -0
- /package/dist/{chunk-47BBY5KA.mjs.map → chunk-HAW5PNJG.mjs.map} +0 -0
- /package/dist/{chunk-ZLTOUZN2.mjs.map → chunk-M3N7LPHX.mjs.map} +0 -0
- /package/dist/{chunk-DGWOVYEC.mjs.map → chunk-MQ7TXALH.mjs.map} +0 -0
- /package/dist/{chunk-JJMMT24D.mjs.map → chunk-OUEYZOLB.mjs.map} +0 -0
- /package/dist/{chunk-XXP3TQHF.mjs.map → chunk-OY65RYD3.mjs.map} +0 -0
- /package/dist/{chunk-BH6JWIKI.mjs.map → chunk-QIEF7HIL.mjs.map} +0 -0
- /package/dist/{chunk-P445QGOC.mjs.map → chunk-RFKSJIQB.mjs.map} +0 -0
- /package/dist/{chunk-6JUCIWDC.mjs.map → chunk-UOTHRJD2.mjs.map} +0 -0
- /package/dist/{lifecycle-LKGPRBS7.mjs.map → lifecycle-KI3PCJSE.mjs.map} +0 -0
- /package/dist/{local-transport-QYFKA6AN.mjs.map → local-transport-C7YTA4I7.mjs.map} +0 -0
- /package/dist/{request-SQOUNWZ4.mjs.map → request-66FGJIHB.mjs.map} +0 -0
- /package/dist/{write-through-R25VQMH6.mjs.map → write-through-IWQKXYHE.mjs.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/offline/configure.ts","../../src/offline/sync/session-refresh.ts","../../src/offline/sync/sync-root.ts","../../src/offline/sync/counts.ts","../../src/offline/sync/drain-queue.ts","../../src/offline/sync/conflicts.ts","../../src/offline/sync/runtime.ts","../../src/offline/logout.ts","../../src/offline/device-switch.ts"],"sourcesContent":["import { configureOffline as configureOfflineLayer, setDefaultDeviceStoreOpener } from \"./config\";\nimport type { OfflineConfig } from \"./types\";\n\n/**\n * Opts the app into offline mode (the `./offline` entry's `configureOffline`). It also registers\n * the browser device-store opener, so the store, the worker and SQLite are reachable only from\n * this entry, never from the request path every other entry shares.\n */\nexport function configureOffline(config: OfflineConfig): void {\n setDefaultDeviceStoreOpener((userId) =>\n import(\"./store/lifecycle\").then((lifecycle) => lifecycle.openDeviceStore(userId)),\n );\n configureOfflineLayer(config);\n}\n","import { getCookie } from \"cookies-next\";\nimport { getI18nDefaultLocale, getI18nSupportedLocales } from \"../../i18n/config\";\nimport { getOfflineConfig } from \"../config\";\n\n/** The locale of the page the GM is on (first path segment), or the app's default locale. */\nexport function currentLocale(): string {\n if (typeof window === \"undefined\") return getI18nDefaultLocale();\n const first = window.location.pathname.split(\"/\").filter(Boolean)[0];\n return first && getI18nSupportedLocales().includes(first) ? first : getI18nDefaultLocale();\n}\n\nexport type SessionRefreshResult = \"ok\" | \"expired\" | \"unreachable\";\n\n/**\n * Renews the sign-in before the waiting changes are sent: one same-origin GET of the offline\n * shell (`cache: \"no-store\"`). It passes through the app's proxy, which renews the `token`\n * cookie; the shell path is public, so the proxy never redirects it. The sign-in has expired\n * when the `token` cookie is absent after the request.\n *\n * This is the one `fetch` of the offline layer: a request to the web app itself, not the API\n * (Carlo's decision, 2026-11-01).\n */\nexport async function refreshSession(): Promise<SessionRefreshResult> {\n try {\n await fetch(getOfflineConfig().shellPath(currentLocale()), { cache: \"no-store\", credentials: \"same-origin\" });\n } catch {\n return \"unreachable\";\n }\n\n const token = await getCookie(\"token\");\n return token ? \"ok\" : \"expired\";\n}\n","import { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { ApiRequestDataTypeInterface } from \"../../core/interfaces/ApiRequestDataTypeInterface\";\nimport { Modules } from \"../../core/registry/ModuleRegistry\";\nimport { JsonApiGet } from \"../../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, isOfflineConfigured, writeOfflineLastSync } from \"../config\";\nimport { canReachApi, setOfflineState } from \"../state\";\nimport type { DeviceStore, SkippedRecord, StoredRecord } from \"../store/DeviceStore\";\nimport { cacheArtworkImage, imageUrlOf, removeArtworkImage } from \"./artwork-cache\";\nimport { knownRootIds, refreshLockedRoots, rememberSyncRoots } from \"./known-roots\";\nimport { ROOT_SYNC_ENDPOINT, RootSync } from \"./models/RootSync\";\nimport { currentLocale } from \"./session-refresh\";\nimport { recordKeyOf as keyOf, waitingRecordKeys } from \"./waiting\";\n\nclass SyncInterruptedError extends Error {}\n\nfunction assertStillOnline(): void {\n if (!canReachApi()) throw new SyncInterruptedError(\"The connection dropped during the sync\");\n}\n\n/**\n * A record the GM cannot read (403) or that is gone (404) is skipped by this sync (it never\n * blocks the sync state). It is not stored; it is remembered with its `updatedAt` from the sync\n * answer, and a later sync asks for it again only once that `updatedAt` is newer.\n */\nconst isSkippable = (status: number) => status === 403 || status === 404;\n\nasync function fetchResources(\n module: ApiRequestDataTypeInterface,\n endpoint: string,\n skipped: string[],\n included: any[],\n): Promise<any[]> {\n const response = await JsonApiGet({ classKey: module, endpoint, language: currentLocale() });\n assertStillOnline();\n if (!response.ok) {\n if (isSkippable(response.response)) {\n skipped.push(endpoint);\n return [];\n }\n throw new Error(`[offline] GET ${endpoint} answered ${response.response}`);\n }\n included.push(...(response.raw?.included ?? []));\n const data = response.raw?.data;\n if (!data) return [];\n return Array.isArray(data) ? data : [data];\n}\n\nasync function imagesOfRoot(store: DeviceStore, rootId: string): Promise<{ type: string; id: string; url: string }[]> {\n const images: { type: string; id: string; url: string }[] = [];\n for (const typeConfig of getOfflineConfig().types) {\n for (const record of await store.listRecords({ type: typeConfig.module.name, rootId })) {\n const url = imageUrlOf(record.resource);\n if (url) images.push({ type: record.type, id: record.id, url });\n }\n }\n return images;\n}\n\n/**\n * Removes a root record from the device (records, links, sync state, images) unless it has waiting\n * changes. A shared record another root record still uses stays, with its image.\n */\nasync function removeRootIfIdle(store: DeviceStore, rootId: string): Promise<void> {\n if ((await store.queue()).some((entry) => entry.rootId === rootId)) return;\n const images = await imagesOfRoot(store, rootId);\n await store.removeRoot(rootId);\n for (const image of images) {\n if (!(await store.getRecord(image.type, image.id))) await removeArtworkImage(image.url);\n }\n}\n\n/**\n * Brings one root record's device copy up to date with the sync answer (C2): records changed since\n * the stored server time are fetched (C3), records gone from the root record are deleted, the links\n * are replaced, and artwork images are downloaded. The sync state is stored only once every\n * fetch succeeded (or answered 403/404). A record with a waiting change is never overwritten.\n */\nexport async function syncRoot(rootId: string): Promise<void> {\n const config = getOfflineConfig();\n const store = await getOfflineStore();\n const previous = await store.getSyncState(rootId);\n\n const syncEndpoint = new EndpointCreator({\n endpoint: config.rootModule,\n id: rootId,\n childEndpoint: ROOT_SYNC_ENDPOINT,\n }).generate();\n const syncResponse = await JsonApiGet({\n classKey: Modules.RootSync,\n endpoint: syncEndpoint,\n language: currentLocale(),\n });\n assertStillOnline();\n if (!syncResponse.ok) {\n // Deleted, or no longer the GM's: removed from the device unless changes still wait.\n if (isSkippable(syncResponse.response)) {\n await removeRootIfIdle(store, rootId);\n return;\n }\n throw new Error(`[offline] GET ${syncEndpoint} answered ${syncResponse.response}`);\n }\n\n const sync = syncResponse.data as RootSync;\n if (sync.locked) {\n // Locked online, locked offline: nothing is downloaded, waiting changes are kept.\n await store.setSyncState(rootId, {\n serverTime: previous.serverTime,\n locked: true,\n lockedFrom: sync.lockedFrom ?? null,\n });\n await refreshLockedRoots();\n return;\n }\n\n const typeNames = new Set(config.types.map((typeConfig) => typeConfig.module.name));\n const entries = sync.entries.filter((entry) => typeNames.has(entry.type));\n const entryUpdatedAt = new Map(entries.map((entry) => [keyOf(entry.type, entry.id), entry.updatedAt]));\n const since = previous.serverTime;\n /** Inside the `updatedSince` window: a list read with `updatedSince` brings it. */\n const inWindow = (entry: { updatedAt: Date }) => !since || entry.updatedAt.getTime() > since.getTime();\n\n // What the device holds for this root record now. A record is fetched when it changed since the\n // last sync, and also when the device does not hold it or holds it as stale (updatedAt cleared\n // while it had a waiting change, see below) — unless an earlier sync skipped it (403/404, or\n // absent from the answer) and it has not changed on the server since.\n const stored = new Map<string, StoredRecord>();\n for (const typeConfig of config.types) {\n for (const record of await store.listRecords({ type: typeConfig.module.name, rootId })) {\n stored.set(keyOf(record.type, record.id), record);\n }\n }\n const previouslySkipped = new Map(\n (await store.skippedRecords(rootId)).map((record) => [keyOf(record.type, record.id), record.updatedAt]),\n );\n const isStale = (entry: { type: string; id: string; updatedAt: Date }) => {\n const key = keyOf(entry.type, entry.id);\n const record = stored.get(key);\n if (record && record.updatedAt !== null) return false;\n const skippedAt = previouslySkipped.get(key);\n return !skippedAt || entry.updatedAt.getTime() > skippedAt.getTime();\n };\n const changed = entries.filter((entry) => inWindow(entry) || isStale(entry));\n\n const skipped: string[] = [];\n const fetched: any[] = [];\n // Records the answers include whose type the device does not sync itself (the root record's\n // owner): kept for display only, so a local answer can include them as the server does.\n const included: any[] = [];\n for (const typeConfig of config.types) {\n const changedOfType = changed.filter((entry) => entry.type === typeConfig.module.name);\n if (changedOfType.length === 0) continue;\n\n if (typeConfig.fetch === \"root-list\") {\n // One list read per type. With `updatedSince` when every changed record is inside the\n // window; without it when a record older than the window is missing or stale on the\n // device, so the whole list comes in the same single request (never one read per record).\n const endpoint = new EndpointCreator({\n endpoint: config.rootModule,\n id: rootId,\n childEndpoint: typeConfig.module,\n }).addAdditionalParam(\"fetchAll\", \"true\");\n if (since && changedOfType.every(inWindow)) endpoint.addAdditionalParam(\"updatedSince\", since.toISOString());\n fetched.push(...(await fetchResources(typeConfig.module, endpoint.generate(), skipped, included)));\n } else if (typeConfig.fetch === \"linked-list\") {\n // Read only as a list per target: the targets are the other ends of the changed records'\n // sync links of `linkKind`; each target's list is read once, and only the type's entries kept.\n if (!typeConfig.linkedList) throw new Error(`[offline] ${typeConfig.module.name}: linked-list needs linkedList`);\n const { linkKind, endpoint } = typeConfig.linkedList;\n const changedIds = new Set(changedOfType.map((entry) => entry.id));\n const targets = new Map<string, { type: string; id: string }>();\n for (const link of sync.links) {\n if (link.kind !== linkKind || link.fromType !== typeConfig.module.name || !changedIds.has(link.fromId))\n continue;\n targets.set(keyOf(link.toType, link.toId), { type: link.toType, id: link.toId });\n }\n for (const target of targets.values()) {\n const resources = await fetchResources(typeConfig.module, endpoint(rootId, target), skipped, included);\n fetched.push(\n ...resources.filter((resource) => resource?.type === typeConfig.module.name && changedIds.has(resource.id)),\n );\n }\n } else {\n for (const entry of changedOfType) {\n const endpoint = new EndpointCreator({ endpoint: typeConfig.module, id: entry.id }).generate();\n fetched.push(...(await fetchResources(typeConfig.module, endpoint, skipped, included)));\n }\n }\n }\n\n const fetchedKeys = new Set(fetched.map((resource) => keyOf(resource?.type, resource?.id)));\n const previousImageUrls = new Map<string, string>();\n const removedImageUrls: string[] = [];\n\n await store.transaction(async (tx) => {\n // A record with a waiting change (pending, sending, in conflict or failed) is never\n // overwritten nor deleted here. Read inside this transaction, so an edit queued while the\n // records were being fetched is seen.\n const waiting = waitingRecordKeys(await tx.queue(), typeNames);\n\n for (const typeConfig of config.types) {\n for (const record of await tx.listRecords({ type: typeConfig.module.name, rootId })) {\n const key = keyOf(record.type, record.id);\n if (waiting.has(key)) {\n // Marked stale: once the change stops waiting (sent, resolved, discarded), the next\n // sync fetches the record again, whatever its updatedAt.\n if (record.updatedAt !== null) await tx.putRecord({ ...record, updatedAt: null });\n continue;\n }\n if (entryUpdatedAt.has(key)) continue;\n // Leaves this root record; deleted only when no other root record keeps it (a shared record).\n await tx.removeRecordFromRoot(record.type, record.id, rootId);\n const url = imageUrlOf(record.resource);\n if (url && !(await tx.getRecord(record.type, record.id))) removedImageUrls.push(url);\n }\n }\n\n for (const resource of fetched) {\n const key = keyOf(resource?.type, resource?.id);\n if (!entryUpdatedAt.has(key)) continue;\n if (waiting.has(key)) continue;\n const existing = await tx.getRecord(resource.type, resource.id);\n const previousUrl = imageUrlOf(existing?.resource);\n if (previousUrl) previousImageUrls.set(key, previousUrl);\n await tx.putRecord({\n type: resource.type,\n id: resource.id,\n rootId,\n resource,\n updatedAt: entryUpdatedAt.get(key) ?? null,\n base: null,\n localOnly: false,\n });\n }\n\n for (const resource of included) {\n if (!resource?.type || !resource?.id || typeNames.has(resource.type)) continue;\n await tx.putRecord({\n type: resource.type,\n id: resource.id,\n rootId,\n resource,\n updatedAt: null,\n base: null,\n localOnly: false,\n });\n }\n\n // Remembered skips: a changed record no answer brought (403/404, or absent from the list), and\n // an earlier skip still in the root record that was not asked for again.\n const skippedRecords: SkippedRecord[] = [];\n const changedKeys = new Set(changed.map((entry) => keyOf(entry.type, entry.id)));\n for (const entry of entries) {\n const key = keyOf(entry.type, entry.id);\n if (waiting.has(key) || fetchedKeys.has(key)) continue;\n if (changedKeys.has(key)) skippedRecords.push({ type: entry.type, id: entry.id, updatedAt: entry.updatedAt });\n else {\n const skippedAt = previouslySkipped.get(key);\n if (skippedAt) skippedRecords.push({ type: entry.type, id: entry.id, updatedAt: skippedAt });\n }\n }\n await tx.setSkippedRecords(rootId, skippedRecords);\n\n await tx.replaceRootLinks(rootId, sync.links);\n await tx.setSyncState(rootId, {\n serverTime: sync.serverTime,\n locked: false,\n lockedFrom: sync.lockedFrom ?? null,\n });\n });\n\n if (skipped.length > 0) {\n console.warn(`[offline] root record ${rootId}: skipped records the GM cannot read:`, skipped.join(\", \"));\n }\n\n for (const url of removedImageUrls) await removeArtworkImage(url);\n for (const typeConfig of config.types) {\n for (const record of await store.listRecords({ type: typeConfig.module.name, rootId })) {\n const url = imageUrlOf(record.resource);\n if (url) await cacheArtworkImage(url, previousImageUrls.get(keyOf(record.type, record.id)));\n }\n }\n\n await refreshLockedRoots();\n}\n\n/**\n * Syncs every root record `listSyncRoots()` returns, then removes from the device the root records\n * that are no longer roots (unless they still have waiting changes). Does nothing offline.\n */\nexport async function syncAll(): Promise<void> {\n if (!isOfflineConfigured()) return;\n const config = getOfflineConfig();\n if (!config.getUserId() || !canReachApi()) return;\n\n let roots: string[];\n try {\n roots = await config.listSyncRoots();\n } catch (error) {\n console.warn(\"[offline] could not list the root records to sync\", error);\n return;\n }\n if (!canReachApi()) return;\n rememberSyncRoots(roots);\n\n const total = roots.length;\n setOfflineState({ syncProgress: { done: 0, total } });\n try {\n let done = 0;\n for (const rootId of roots) {\n try {\n await syncRoot(rootId);\n } catch (error) {\n console.warn(`[offline] sync of root record ${rootId} failed`, error);\n if (!canReachApi()) return;\n } finally {\n done += 1;\n setOfflineState({ syncProgress: { done, total } });\n }\n }\n\n // Every root record was tried: this counts as the device's last sync.\n const finishedAt = Date.now();\n const userId = config.getUserId();\n if (userId) writeOfflineLastSync(userId, finishedAt);\n setOfflineState({ lastSyncedAt: finishedAt, rootCount: total });\n\n const store = await getOfflineStore();\n const rootSet = new Set(roots);\n for (const rootId of await knownRootIds(store)) {\n if (!rootSet.has(rootId)) await removeRootIfIdle(store, rootId);\n }\n await refreshLockedRoots();\n } finally {\n setOfflineState({ syncProgress: null });\n }\n}\n","import { useSyncExternalStore } from \"react\";\nimport { getOfflineConfig, getOfflineStore, isOfflineConfigured } from \"../config\";\nimport { setOfflineState } from \"../state\";\nimport type { Conflict, DeviceStore } from \"../store/DeviceStore\";\n\nconst EMPTY: Conflict[] = [];\nlet _conflicts: Conflict[] = EMPTY;\nconst _listeners = new Set<() => void>();\n\nfunction subscribe(listener: () => void): () => void {\n _listeners.add(listener);\n return () => {\n _listeners.delete(listener);\n };\n}\n\n/** Re-reads the queue and the conflicts: updates `pending`/`conflicts` and the `useConflicts` list. */\nexport async function refreshOfflineCounts(store?: DeviceStore): Promise<void> {\n if (!store) {\n if (!isOfflineConfigured() || !getOfflineConfig().getUserId()) return;\n store = await getOfflineStore();\n }\n const entries = await store.queue();\n _conflicts = await store.conflicts();\n for (const listener of [..._listeners]) listener();\n setOfflineState({\n pending: entries.filter((entry) => entry.status !== \"conflict\").length,\n conflicts: _conflicts.length,\n });\n}\n\n/** The unresolved conflicts of the signed-in user's device. */\nexport function useConflicts(): Conflict[] {\n return useSyncExternalStore(\n subscribe,\n () => _conflicts,\n () => EMPTY,\n );\n}\n","import { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { Modules } from \"../../core/registry/ModuleRegistry\";\nimport { buildUrl, JsonApiPost } from \"../../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, isOfflineConfigured } from \"../config\";\nimport { allowWhileReconnecting, getOfflineState, setOfflineState } from \"../state\";\nimport type { DeviceStore, QueueEntry } from \"../store/DeviceStore\";\nimport { isRootLocked } from \"../store/lifecycle\";\nimport { writeThrough } from \"../write-through\";\nimport { refreshOfflineCounts } from \"./counts\";\nimport { OfflineChangeSet } from \"./models/OfflineChangeSet\";\nimport { OfflineChange, OfflineChangeResult } from \"./models/OfflineChangeSetInterface\";\nimport { currentLocale, refreshSession } from \"./session-refresh\";\nimport { recordKeysOfEntry, waitingRecordKeys } from \"./waiting\";\n\nexport type DrainResult = { applied: number; conflicts: number; stoppedForLogin: boolean };\n\nconst syncedTypeNames = () => new Set(getOfflineConfig().types.map((typeConfig) => typeConfig.module.name));\n\n/**\n * The body as sent: each resource's server-owned fields (AI summaries, tl;dr, `aiStatus`, …) are\n * left out of its attributes. The stored body and the stored record keep them.\n */\nfunction withoutServerOwnedFields(body: any): any {\n const data = body?.data;\n if (!data) return body;\n const owned = new Map(\n getOfflineConfig().types.map((typeConfig) => [typeConfig.module.name, typeConfig.serverOwnedFields]),\n );\n const strip = (resource: any) => {\n const fields = owned.get(resource?.type) ?? [];\n if (!resource?.attributes || fields.length === 0) return resource;\n const attributes = { ...resource.attributes };\n for (const field of fields) delete attributes[field];\n return { ...resource, attributes };\n };\n return { ...body, data: Array.isArray(data) ? data.map(strip) : strip(data) };\n}\n\nfunction toChange(entry: QueueEntry): OfflineChange {\n const change: OfflineChange = { seq: entry.seq, method: entry.method as OfflineChange[\"method\"], path: entry.path };\n if (entry.body !== undefined) change.body = withoutServerOwnedFields(entry.body);\n if (entry.recordType) change.recordType = entry.recordType;\n if (entry.recordId) change.recordId = entry.recordId;\n change.base = entry.base ?? null;\n if (entry.resolution) change.resolution = entry.resolution;\n return change;\n}\n\n/**\n * The merge the server settled, as a resource the record type's model can rehydrate: the server's\n * current resource with the merged attribute values laid over it (decision piece 2: the library\n * shapes stored resources). Undefined when the server gave no merge or no current record.\n */\nfunction mergedResource(current: any, merged: Record<string, unknown> | undefined): any | undefined {\n if (!merged || !current?.type) return undefined;\n return { ...current, attributes: { ...(current.attributes ?? {}), ...merged } };\n}\n\nfunction newPackageId(): string {\n return globalThis.crypto.randomUUID();\n}\n\n/**\n * The id of the package to send: the id of a package that never got its answer (the page\n * closed, the network dropped) when one of the entries carries it, so the server applies it\n * once; a fresh id otherwise.\n */\nfunction packageIdFor(entries: QueueEntry[]): string {\n return entries.find((entry) => entry.packageId)?.packageId ?? newPackageId();\n}\n\n/** The entries to send: pending, of unlocked root records, not behind a conflict on the same record. */\nasync function entriesToSend(store: DeviceStore): Promise<QueueEntry[]> {\n const all = await store.queue();\n const typeNames = syncedTypeNames();\n const blocked = waitingRecordKeys(\n all.filter((entry) => entry.status === \"conflict\"),\n typeNames,\n );\n const lockedIds = new Set(getOfflineState().lockedRootIds);\n const lockedByRoot = new Map<string, boolean>();\n const now = new Date();\n\n const result: QueueEntry[] = [];\n for (const entry of all) {\n // \"sending\" is a package that never got its answer (the page closed): it is sent again.\n if (entry.status !== \"pending\" && entry.status !== \"sending\") continue;\n if (recordKeysOfEntry(entry, typeNames).some((key) => blocked.has(key))) continue;\n\n if (!lockedByRoot.has(entry.rootId)) {\n lockedByRoot.set(\n entry.rootId,\n lockedIds.has(entry.rootId) || isRootLocked(await store.getSyncState(entry.rootId), now),\n );\n }\n if (lockedByRoot.get(entry.rootId)) continue;\n result.push(entry);\n }\n return result.sort((a, b) => a.seq - b.seq);\n}\n\nasync function applyResult(\n store: DeviceStore,\n entry: QueueEntry,\n result: OfflineChangeResult,\n): Promise<\"applied\" | \"conflict\" | \"pending\"> {\n if (result.status === \"applied\") {\n await store.removeQueueEntry(entry.seq);\n // The record's base is kept only while it has unsent changes; cleared first, so the\n // write-through below may store the server's answer (it never overwrites a record that\n // still has a base or a waiting change).\n const typeNames = syncedTypeNames();\n const stillWaiting = waitingRecordKeys(await store.queue(), typeNames);\n for (const key of recordKeysOfEntry(entry, typeNames)) {\n if (stillWaiting.has(key)) continue;\n const separator = key.indexOf(\":\");\n const record = await store.getRecord(key.slice(0, separator), key.slice(separator + 1));\n if (record && (record.base !== null || record.localOnly)) {\n await store.putRecord({ ...record, base: null, localOnly: false });\n }\n }\n if (result.response) {\n await writeThrough(\n { method: entry.method, url: entry.path },\n { ok: true, status: 200, statusText: \"OK\", data: result.response },\n );\n } else if (entry.method === \"DELETE\") {\n await writeThrough(\n { method: entry.method, url: entry.path },\n { ok: true, status: 204, statusText: \"OK\", data: undefined },\n );\n }\n return \"applied\";\n }\n\n if (result.status === \"conflict\" || result.status === \"could-not-apply\") {\n await store.addConflict({\n queueSeq: entry.seq,\n kind: result.status === \"could-not-apply\" ? \"could-not-apply\" : (result.conflict ?? \"fields\"),\n recordType: entry.recordType,\n recordId: entry.recordId,\n mine: entry.body ?? null,\n theirs: result.current ?? null,\n base: entry.base ?? null,\n merged: mergedResource(result.current, result.merged),\n fields: result.conflictFields ?? [],\n blocks: result.conflictBlocks,\n reason: result.detail,\n });\n await store.setQueueStatus(entry.seq, \"conflict\");\n return \"conflict\";\n }\n\n await store.setQueueStatus(entry.seq, \"pending\");\n return \"pending\";\n}\n\nasync function drain(): Promise<DrainResult> {\n const outcome: DrainResult = { applied: 0, conflicts: 0, stoppedForLogin: false };\n if (!isOfflineConfigured() || !getOfflineConfig().getUserId()) return outcome;\n\n const store = await getOfflineStore();\n const entries = await entriesToSend(store);\n if (entries.length === 0) return outcome;\n\n const session = await refreshSession();\n if (session === \"unreachable\") return outcome;\n if (session === \"expired\") return { ...outcome, stoppedForLogin: true };\n\n setOfflineState({ draining: true });\n const handled = new Set<number>();\n try {\n const packageId = packageIdFor(entries);\n // Stored with the entries before sending: an unanswered package keeps its id for the resend.\n await store.setQueuePackage(\n entries.map((entry) => entry.seq),\n packageId,\n );\n for (const entry of entries) await store.setQueueStatus(entry.seq, \"sending\");\n\n // ONE request with every waiting change, whatever its size: never split (Carlo, 2026-11-01).\n const endpoint = new EndpointCreator({ endpoint: Modules.OfflineChangeSet }).generate();\n // During a reconnect attempt the session is still offline: this request goes to the network.\n const release = allowWhileReconnecting(buildUrl(endpoint));\n let response: Awaited<ReturnType<typeof JsonApiPost>>;\n try {\n response = await JsonApiPost({\n classKey: Modules.OfflineChangeSet,\n endpoint,\n body: { id: packageId, changes: entries.map(toChange) },\n language: currentLocale(),\n });\n } finally {\n release();\n }\n\n if (!response.ok) {\n if (response.response === 401) outcome.stoppedForLogin = true;\n return outcome;\n }\n\n // The server answered for this package: every entry left in the queue leaves it.\n await store.setQueuePackage(\n entries.map((entry) => entry.seq),\n null,\n );\n\n const results = new Map((response.data as OfflineChangeSet).results.map((result) => [result.seq, result]));\n for (const entry of entries) {\n const result = results.get(entry.seq);\n const status = result ? await applyResult(store, entry, result) : \"pending\";\n if (!result) await store.setQueueStatus(entry.seq, \"pending\");\n handled.add(entry.seq);\n if (status === \"applied\") outcome.applied++;\n if (status === \"conflict\") outcome.conflicts++;\n }\n return outcome;\n } catch (error) {\n console.error(\"[offline] sending the waiting changes failed\", error);\n return outcome;\n } finally {\n // Nothing is dropped: every entry without an answer goes back to pending.\n for (const entry of entries) {\n if (!handled.has(entry.seq)) await store.setQueueStatus(entry.seq, \"pending\");\n }\n setOfflineState({ draining: false });\n await refreshOfflineCounts(store);\n }\n}\n\nlet _inFlight: Promise<DrainResult> | undefined;\n\n/**\n * Sends every waiting change of the unlocked root records in ONE `offline-change-sets` request,\n * after renewing the sign-in. Applied entries are removed and their answer written to the\n * device copy; conflicts become Conflict rows; waiting entries stay pending. A network error\n * or an expired sign-in stops it and keeps every entry.\n */\nexport function drainQueue(): Promise<DrainResult> {\n if (!_inFlight) {\n _inFlight = drain().finally(() => {\n _inFlight = undefined;\n });\n }\n return _inFlight;\n}\n","import { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { getOfflineConfig, getOfflineStore } from \"../config\";\nimport { getOfflineState } from \"../state\";\nimport type { Conflict, DeviceStore, QueueEntry } from \"../store/DeviceStore\";\nimport type { OfflineChangeResolution } from \"./models/OfflineChangeSetInterface\";\nimport { refreshOfflineCounts } from \"./counts\";\nimport { drainQueue } from \"./drain-queue\";\n\nexport { useConflicts } from \"./counts\";\n\nexport type ConflictChoice =\n | { kind: \"mine\" }\n | { kind: \"theirs\" }\n /**\n * Per conflicting field: whose value wins. `mergedBody`: the JSON:API document the record\n * type's model built with createJsonApi() holding only the fields chosen \"merged\".\n */\n | { kind: \"fields\"; choices: Record<string, \"mine\" | \"theirs\" | \"merged\">; mergedBody?: any }\n | { kind: \"restore\" }\n | { kind: \"accept-deletion\" }\n | { kind: \"delete-anyway\" }\n | { kind: \"retry\" }\n | { kind: \"discard\" };\n\nfunction updatedAtOf(resource: any): Date | null {\n const value = resource?.attributes?.updatedAt;\n if (!value) return null;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** Writes the chosen \"theirs\" and \"merged\" values of the conflicting fields into the stored record. */\nasync function applyChoicesToRecord(\n store: DeviceStore,\n conflict: Conflict,\n choices: Record<string, \"mine\" | \"theirs\" | \"merged\">,\n mergedBody: any,\n): Promise<void> {\n if (!conflict.recordType || !conflict.recordId) return;\n const record = await store.getRecord(conflict.recordType, conflict.recordId);\n if (!record) return;\n const attributes = { ...(record.resource?.attributes ?? {}) };\n const relationships = { ...(record.resource?.relationships ?? {}) };\n for (const [field, choice] of Object.entries(choices)) {\n const source = choice === \"theirs\" ? conflict.theirs : choice === \"merged\" ? mergedBody?.data : undefined;\n if (!source) continue;\n if (source.attributes && field in source.attributes) attributes[field] = source.attributes[field];\n else if (source.relationships && field in source.relationships) relationships[field] = source.relationships[field];\n }\n await store.putRecord({ ...record, resource: { ...record.resource, attributes, relationships } });\n}\n\nasync function rootIdOf(store: DeviceStore, conflict: Conflict, entry: QueueEntry | undefined): Promise<string> {\n if (entry) return entry.rootId;\n if (conflict.recordType && conflict.recordId) {\n const record = await store.getRecord(conflict.recordType, conflict.recordId);\n if (record) return record.rootId;\n }\n throw new Error(`[offline] conflict ${conflict.id} belongs to no root record on this device`);\n}\n\n/** The collection path of a record type, from the configured module (EndpointCreator). */\nfunction collectionPath(recordType: string): string {\n const module = getOfflineConfig().types.find((typeConfig) => typeConfig.module.name === recordType)?.module;\n return new EndpointCreator({ endpoint: module ?? recordType }).generate();\n}\n\n/** Queues the conflicting change again, unchanged apart from `resolution`, and drops the old entry. */\nasync function requeueOriginal(\n store: DeviceStore,\n conflict: Conflict,\n entry: QueueEntry | undefined,\n fallbackMethod: \"PATCH\" | \"DELETE\",\n resolution: OfflineChangeResolution,\n): Promise<void> {\n const method = entry?.method ?? fallbackMethod;\n const body = method === \"DELETE\" ? undefined : (entry?.body ?? conflict.mine ?? undefined);\n await store.enqueue({\n rootId: await rootIdOf(store, conflict, entry),\n method,\n path:\n entry?.path ??\n new EndpointCreator({ endpoint: collectionPath(conflict.recordType!), id: conflict.recordId }).generate(),\n ...(body !== undefined ? { body } : {}),\n recordType: entry?.recordType ?? conflict.recordType,\n recordId: entry?.recordId ?? conflict.recordId,\n base: entry ? entry.base : (conflict.base ?? null),\n // QueueEntry[\"resolution\"] is the store's copy of OfflineChange[\"resolution\"].\n resolution: resolution as QueueEntry[\"resolution\"],\n });\n if (entry) await store.removeQueueEntry(entry.seq);\n}\n\nasync function resolve(store: DeviceStore, conflict: Conflict, choice: ConflictChoice): Promise<void> {\n const entry = (await store.queue()).find((candidate) => candidate.seq === conflict.queueSeq);\n const dropEntry = async () => {\n if (entry) await store.removeQueueEntry(entry.seq);\n };\n\n switch (choice.kind) {\n case \"mine\":\n case \"fields\": {\n if (choice.kind === \"fields\" && Object.values(choice.choices).every((value) => value === \"theirs\")) {\n return resolve(store, conflict, { kind: \"theirs\" });\n }\n // The ORIGINAL change goes back (original body and base, so the server's three-way merge\n // keeps every field only the server changed), carrying how each conflicting field is settled.\n const resolution: OfflineChangeResolution =\n choice.kind === \"mine\"\n ? { kind: \"mine\" }\n : {\n kind: \"merged\",\n choices: choice.choices,\n ...(choice.mergedBody !== undefined ? { mergedBody: choice.mergedBody } : {}),\n };\n await requeueOriginal(store, conflict, entry, \"PATCH\", resolution);\n if (choice.kind === \"fields\") await applyChoicesToRecord(store, conflict, choice.choices, choice.mergedBody);\n break;\n }\n case \"theirs\": {\n // Only the device changes: the server's version replaces mine.\n await dropEntry();\n if (conflict.recordType && conflict.recordId) {\n if (conflict.theirs) {\n await store.putRecord({\n type: conflict.recordType,\n id: conflict.recordId,\n rootId: await rootIdOf(store, conflict, entry),\n resource: conflict.theirs,\n updatedAt: updatedAtOf(conflict.theirs),\n base: null,\n localOnly: false,\n });\n } else {\n await store.deleteRecord(conflict.recordType, conflict.recordId);\n }\n }\n break;\n }\n case \"restore\": {\n // Deleted on the server: created again with my version.\n if (!conflict.recordType) throw new Error(`[offline] conflict ${conflict.id} has no record to restore`);\n const body = conflict.mine ?? entry?.body;\n const rootId = await rootIdOf(store, conflict, entry);\n await store.enqueue({\n rootId,\n method: \"POST\",\n path: collectionPath(conflict.recordType),\n body,\n recordType: conflict.recordType,\n recordId: conflict.recordId,\n base: null,\n });\n await dropEntry();\n if (conflict.recordId) {\n const record = await store.getRecord(conflict.recordType, conflict.recordId);\n // localOnly keeps it on the device until the server has it again.\n if (record) await store.putRecord({ ...record, base: null, localOnly: true });\n else if (body?.data)\n await store.putRecord({\n type: conflict.recordType,\n id: conflict.recordId,\n rootId,\n resource: body.data,\n updatedAt: null,\n base: null,\n localOnly: true,\n });\n }\n break;\n }\n case \"accept-deletion\": {\n await dropEntry();\n if (conflict.recordType && conflict.recordId) await store.deleteRecord(conflict.recordType, conflict.recordId);\n break;\n }\n case \"delete-anyway\": {\n // The original DELETE with a resolution: the server skips the changed-on-server check.\n await requeueOriginal(store, conflict, entry, \"DELETE\", { kind: \"mine\" });\n break;\n }\n case \"retry\": {\n if (entry) await store.setQueueStatus(entry.seq, \"pending\");\n break;\n }\n case \"discard\": {\n await dropEntry();\n break;\n }\n }\n\n await store.removeConflict(conflict.id);\n}\n\n/**\n * Applies the GM's choice for one conflict. \"keep mine\", per-field choices, \"restore\" and \"delete anyway\"\n * queue one more change (sent with the next package); \"keep theirs\" and \"let the deletion\n * stand\" only update the device. Online, the queue is sent right away.\n */\nexport async function resolveConflict(id: string, choice: ConflictChoice): Promise<void> {\n const store = await getOfflineStore();\n const conflict = (await store.conflicts()).find((candidate) => candidate.id === id);\n if (!conflict) throw new Error(`[offline] no conflict ${id}`);\n\n await store.transaction((tx) => resolve(tx, conflict, choice));\n await refreshOfflineCounts(store);\n\n if (getOfflineState().online) {\n drainQueue().catch((error) => console.warn(\"[offline] sending the resolved change failed\", error));\n }\n}\n","import { getCookie } from \"cookies-next\";\nimport { getClientToken } from \"../../client/token\";\nimport { directFetch } from \"../../client/request\";\nimport { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { buildUrl } from \"../../unified/JsonApiRequest\";\nimport {\n getOfflineConfig,\n getOfflineStore,\n isOfflineConfigured,\n OfflineUnavailableError,\n refreshOfflineDeviceState,\n} from \"../config\";\nimport { getOfflineState, isReconnecting, setOfflineState, setReconnecting, subscribeOfflineState } from \"../state\";\nimport { OFFLINE_REPLAY_HEADER } from \"../types\";\nimport { refreshOfflineCounts } from \"./counts\";\nimport { drainQueue } from \"./drain-queue\";\nimport { knownRootIds, refreshLockedRoots } from \"./known-roots\";\nimport { ROOT_SYNC_ENDPOINT } from \"./models/RootSync\";\nimport { currentLocale } from \"./session-refresh\";\nimport { syncAll } from \"./sync-root\";\n\n/** How often the stored lock dates are checked again (a lock date can pass while offline). */\nconst LOCK_CHECK_INTERVAL_MS = 60_000;\n/** How often the API is probed while the session is offline but the browser says it is online. */\nconst PROBE_INTERVAL_MS = 30_000;\n\n/**\n * Asks the API whether it can be reached again. The request carries the replay header, so\n * `directFetch` sends it to the network instead of answering it from the device copy. Any\n * answer (even a 401) means the API is reachable; only a network error means it is not.\n */\nasync function probeApi(): Promise<boolean> {\n const config = getOfflineConfig();\n const store = await getOfflineStore();\n const [rootId] = await knownRootIds(store);\n const endpoint = rootId\n ? new EndpointCreator({ endpoint: config.rootModule, id: rootId, childEndpoint: ROOT_SYNC_ENDPOINT })\n : new EndpointCreator({ endpoint: config.rootModule }).addAdditionalParam(\"page[size]\", \"1\");\n\n const response = await directFetch({\n method: \"GET\",\n url: buildUrl(endpoint.generate()),\n token: await getClientToken(),\n language: currentLocale(),\n additionalHeaders: { [OFFLINE_REPLAY_HEADER]: \"1\" },\n });\n return !response.networkError;\n}\n\n/** Remembers (for the tab) the sign-in a reload for the login was already tried for. */\nconst LOGIN_RELOAD_KEY = \"nja-offline-login-reload\";\nlet _reloadedForSignIn: string | undefined;\n\n/** A short fingerprint of the sign-in (the `token` cookie), never the token itself. */\nasync function signInFingerprint(): Promise<string> {\n const token = await getCookie(\"token\");\n if (!token) return \"none\";\n let hash = 5381;\n for (const char of String(token)) hash = ((hash * 33) ^ char.charCodeAt(0)) >>> 0;\n return hash.toString(36);\n}\n\nconst readReloadMarker = (): string | null => {\n try {\n return window.sessionStorage.getItem(LOGIN_RELOAD_KEY);\n } catch {\n return null;\n }\n};\n\nconst writeReloadMarker = (value: string | null): void => {\n try {\n if (value === null) window.sessionStorage.removeItem(LOGIN_RELOAD_KEY);\n else window.sessionStorage.setItem(LOGIN_RELOAD_KEY, value);\n } catch {\n // Storage unavailable: the module flag still stops a loop within this page.\n }\n};\n\n/**\n * The sign-in expired: reloading sends the GM through the proxy to the login page. The queue\n * stays. Tried once per sign-in: when the API still refuses the same sign-in after the reload\n * (a 401 while the cookie exists), the GM stays on the page and the queue waits for a new\n * sign-in, instead of reloading in a loop.\n */\nasync function signInExpired(): Promise<void> {\n const signIn = await signInFingerprint();\n if (_reloadedForSignIn === signIn || readReloadMarker() === signIn) {\n console.warn(\"[offline] the sign-in expired; log in again to send the waiting changes\");\n return;\n }\n _reloadedForSignIn = signIn;\n writeReloadMarker(signIn);\n try {\n window.location.reload();\n } catch (error) {\n console.warn(\"[offline] the sign-in expired; log in again to send the waiting changes\", error);\n }\n}\n\n/**\n * Logs a failure of the runtime, except the browser being found unable to keep the device copy:\n * that is warned once (`markOfflineUnavailable`) and the runtime stops.\n */\nconst warnUnlessUnavailable =\n (message: string) =>\n (error: unknown): void => {\n if (!(error instanceof OfflineUnavailableError)) console.warn(message, error);\n };\n\nconst _runningRuntimes = new Set<() => void>();\n\n/** Stops every running offline runtime (timers, listeners). Called on logout. */\nexport function stopOfflineRuntime(): void {\n for (const stop of [..._runningRuntimes]) stop();\n}\n\n/**\n * Starts the offline runtime: drain-then-sync on open (online) and on every reconnect, syncAll\n * every `syncIntervalMs` while online, a probe of the API while the session is offline but the\n * browser is online (a probe that reaches the API runs the drain and the sync, and the session\n * goes online once they succeeded), and the lock dates checked every minute. Returns the stop\n * function.\n */\nexport function startOfflineRuntime(): () => void {\n refreshOfflineDeviceState();\n if (!isOfflineConfigured()) return () => {};\n const config = getOfflineConfig();\n\n let running: Promise<void> | undefined;\n let goingOnlineAfterSync = false;\n /**\n * Drain then sync. `reconnect`: a probe reached the API while the session is offline; the\n * session stays offline during the drain and the sync and goes online only when both ran\n * without a network failure (spec § 4: offline \"until a sync succeeds\").\n */\n const run = (reconnect: boolean): Promise<void> => {\n if (!running) {\n running = (async () => {\n let succeeded = false;\n if (reconnect) setReconnecting(true);\n try {\n const drained = await drainQueue();\n if (drained.stoppedForLogin) {\n await signInExpired();\n return;\n }\n writeReloadMarker(null);\n if (reconnect ? !isReconnecting() : !getOfflineState().online) return;\n await syncAll();\n succeeded = reconnect && isReconnecting();\n } catch (error) {\n warnUnlessUnavailable(\"[offline] background sync failed\")(error);\n } finally {\n if (reconnect) setReconnecting(false);\n running = undefined;\n if (succeeded) {\n goingOnlineAfterSync = true;\n setOfflineState({ online: true });\n goingOnlineAfterSync = false;\n // Changes queued while the attempt ran (an edit, a resolved conflict) waited because\n // the session was still offline: they are sent now, not at the next sync interval.\n if (getOfflineState().pending > 0) {\n drainQueue().catch(warnUnlessUnavailable(\"[offline] sending the waiting changes failed\"));\n }\n }\n }\n })();\n }\n return running;\n };\n const runOnline = () => run(false);\n\n let probing = false;\n const probe = async () => {\n if (probing || running || getOfflineState().online) return;\n if (typeof navigator !== \"undefined\" && navigator.onLine === false) return;\n probing = true;\n try {\n if (await probeApi()) await run(true);\n } catch (error) {\n warnUnlessUnavailable(\"[offline] probing the API failed\")(error);\n } finally {\n probing = false;\n }\n };\n\n // The browser says the network is back: probe the API now (the state stays offline until then).\n const onBrowserOnline = () => void probe();\n if (typeof window !== \"undefined\") window.addEventListener(\"online\", onBrowserOnline);\n\n let wasOnline = getOfflineState().online;\n const unsubscribe = subscribeOfflineState((state) => {\n // The browser cannot keep the device copy: offline mode is off for the page.\n if (!state.available) {\n stop();\n return;\n }\n // Online after a reconnect attempt: its drain and sync already ran.\n if (state.online && !wasOnline && !goingOnlineAfterSync) void runOnline();\n wasOnline = state.online;\n });\n\n void refreshLockedRoots().catch(warnUnlessUnavailable(\"[offline] lock check failed\"));\n void refreshOfflineCounts().catch(warnUnlessUnavailable(\"[offline] reading the queue failed\"));\n if (getOfflineState().online) void runOnline();\n\n const syncTimer = setInterval(() => {\n if (getOfflineState().online) void runOnline();\n }, config.syncIntervalMs);\n const probeTimer = setInterval(() => void probe(), PROBE_INTERVAL_MS);\n const lockTimer = setInterval(() => {\n void refreshLockedRoots().catch(warnUnlessUnavailable(\"[offline] lock check failed\"));\n }, LOCK_CHECK_INTERVAL_MS);\n\n const stop = () => {\n _runningRuntimes.delete(stop);\n clearInterval(syncTimer);\n clearInterval(probeTimer);\n clearInterval(lockTimer);\n if (typeof window !== \"undefined\") window.removeEventListener(\"online\", onBrowserOnline);\n unsubscribe();\n };\n _runningRuntimes.add(stop);\n return stop;\n}\n","import {\n blockOfflineStore,\n getOfflineConfig,\n getOfflineStore,\n isOfflineConfigured,\n setOfflineEnabledOnDevice,\n writeOfflineLastSync,\n} from \"./config\";\nimport { setOfflineState } from \"./state\";\nimport { deleteImagesCaches } from \"./sync/artwork-cache\";\nimport { stopOfflineRuntime } from \"./sync/runtime\";\n\n/**\n * How many changes the signed-in user's device still has to send (every queue entry: pending,\n * sending, in conflict or failed). The app warns before logging out when this is above 0.\n */\nexport async function prepareOfflineLogout(): Promise<{ pending: number }> {\n if (!isOfflineConfigured()) return { pending: 0 };\n if (!getOfflineConfig().getUserId()) return { pending: 0 };\n const store = await getOfflineStore();\n const entries = await store.queue();\n return { pending: entries.length };\n}\n\n/**\n * Deletes the user's device database and the cached artwork images. Call it right before logging\n * out. The runtime is stopped first and the store blocked until the next sign-in\n * (`onUserSignedIn`), so a sync still running cannot open, and so recreate, the deleted file.\n * The device switch (`requireOptIn`) is turned off too: after a logout nothing is downloaded again\n * until the user turns offline mode back on.\n */\nexport async function finishOfflineLogout(userId: string): Promise<void> {\n stopOfflineRuntime();\n blockOfflineStore(userId);\n const { deleteDeviceStore } = await import(\"./store/lifecycle\");\n await deleteDeviceStore(userId);\n await deleteImagesCaches();\n setOfflineEnabledOnDevice(userId, false);\n writeOfflineLastSync(userId, null);\n setOfflineState({ enabled: false, syncProgress: null, lastSyncedAt: null, rootCount: null });\n}\n","import {\n forgetOfflineStore,\n getOfflineConfig,\n getOfflineStore,\n isOfflineAvailable,\n setOfflineEnabledOnDevice,\n writeOfflineLastSync,\n} from \"./config\";\nimport { setOfflineState } from \"./state\";\nimport { deleteImagesCaches } from \"./sync/artwork-cache\";\nimport { stopOfflineRuntime } from \"./sync/runtime\";\n\nfunction signedInUserId(): string {\n const userId = getOfflineConfig().getUserId();\n if (!userId) throw new Error(\"No signed-in user\");\n return userId;\n}\n\n/**\n * Turns offline mode on on this device for the signed-in user (`OfflineConfig.requireOptIn`).\n * It does not start the runtime: the app starts it once `enabled` is true in the offline state.\n */\nexport async function turnOnOffline(): Promise<void> {\n const userId = signedInUserId();\n setOfflineEnabledOnDevice(userId, true);\n setOfflineState({ enabled: true });\n try {\n // Asks the browser not to evict the device copy. Best effort only.\n await navigator.storage?.persist?.();\n } catch {\n // Ignored: the copy works without it.\n }\n}\n\n/**\n * Turns offline mode off on this device for the signed-in user and deletes this device's copy\n * (database and cached images). Refuses, changing nothing, while the copy has changes still to\n * send: the answer then carries how many.\n */\nexport async function turnOffOffline(): Promise<{ turnedOff: true } | { turnedOff: false; pending: number }> {\n const userId = signedInUserId();\n\n if (isOfflineAvailable()) {\n let pending = 0;\n try {\n const store = await getOfflineStore();\n pending = (await store.queue()).length;\n } catch {\n // The copy cannot be opened here (held by another tab): the marker file written while\n // changes wait still says whether deleting it would lose any. The count is then unknown: 1.\n const { hasUnsentChanges } = await import(\"./store/lifecycle\");\n if (await hasUnsentChanges(userId).catch(() => false)) pending = 1;\n }\n if (pending > 0) return { turnedOff: false, pending };\n }\n\n // Off first, so a sync still running cannot open, and so recreate, the deleted file.\n setOfflineEnabledOnDevice(userId, false);\n\n if (isOfflineAvailable()) {\n stopOfflineRuntime();\n try {\n const { deleteDeviceStore } = await import(\"./store/lifecycle\");\n await deleteDeviceStore(userId);\n await deleteImagesCaches();\n } catch (error) {\n console.warn(\"[offline] the device copy could not be deleted\", error);\n }\n forgetOfflineStore(userId);\n }\n\n writeOfflineLastSync(userId, null);\n setOfflineState({ enabled: false, syncProgress: null, lastSyncedAt: null, rootCount: null });\n return { turnedOff: true };\n}\n"],"mappings":"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","names":["configureOffline","getCookie","getCookie","deleteDeviceStore","hasUnsentChanges","deleteDeviceStore"]}
|
|
1
|
+
{"version":3,"sources":["../../src/offline/configure.ts","../../src/offline/sync/session-refresh.ts","../../src/offline/sync/sync-root.ts","../../src/offline/sync/counts.ts","../../src/offline/sync/drain-queue.ts","../../src/offline/sync/conflicts.ts","../../src/offline/sync/runtime.ts","../../src/offline/logout.ts","../../src/offline/device-switch.ts"],"sourcesContent":["import { configureOffline as configureOfflineLayer, setDefaultDeviceStoreOpener } from \"./config\";\nimport type { OfflineConfig } from \"./types\";\n\n/**\n * Opts the app into offline mode (the `./offline` entry's `configureOffline`). It also registers\n * the browser device-store opener, so the store, the worker and SQLite are reachable only from\n * this entry, never from the request path every other entry shares.\n */\nexport function configureOffline(config: OfflineConfig): void {\n setDefaultDeviceStoreOpener((userId) =>\n import(\"./store/lifecycle\").then((lifecycle) => lifecycle.openDeviceStore(userId)),\n );\n configureOfflineLayer(config);\n}\n","import { getCookie } from \"cookies-next\";\nimport { getI18nDefaultLocale, getI18nSupportedLocales } from \"../../i18n/config\";\nimport { getOfflineConfig } from \"../config\";\n\n/** The locale of the page the GM is on (first path segment), or the app's default locale. */\nexport function currentLocale(): string {\n if (typeof window === \"undefined\") return getI18nDefaultLocale();\n const first = window.location.pathname.split(\"/\").filter(Boolean)[0];\n return first && getI18nSupportedLocales().includes(first) ? first : getI18nDefaultLocale();\n}\n\nexport type SessionRefreshResult = \"ok\" | \"expired\" | \"unreachable\";\n\n/**\n * Renews the sign-in before the waiting changes are sent: one same-origin GET of the offline\n * shell (`cache: \"no-store\"`). It passes through the app's proxy, which renews the `token`\n * cookie; the shell path is public, so the proxy never redirects it. The sign-in has expired\n * when the `token` cookie is absent after the request.\n *\n * This is the one `fetch` of the offline layer: a request to the web app itself, not the API\n * (Carlo's decision, 2026-11-01).\n */\nexport async function refreshSession(): Promise<SessionRefreshResult> {\n try {\n await fetch(getOfflineConfig().shellPath(currentLocale()), { cache: \"no-store\", credentials: \"same-origin\" });\n } catch {\n return \"unreachable\";\n }\n\n const token = await getCookie(\"token\");\n return token ? \"ok\" : \"expired\";\n}\n","import { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { ApiRequestDataTypeInterface } from \"../../core/interfaces/ApiRequestDataTypeInterface\";\nimport { Modules } from \"../../core/registry/ModuleRegistry\";\nimport { JsonApiGet } from \"../../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, isOfflineConfigured, rootKinds, writeOfflineLastSync } from \"../config\";\nimport { canReachApi, setOfflineState } from \"../state\";\nimport type { DeviceStore, SkippedRecord, StoredRecord } from \"../store/DeviceStore\";\nimport type { OfflineRootKind } from \"../types\";\nimport { cacheArtworkImage, imageUrlOf, removeArtworkImage } from \"./artwork-cache\";\nimport { knownRootIds, refreshLockedRoots, rememberSyncRoots } from \"./known-roots\";\nimport { ROOT_SYNC_ENDPOINT, RootSync } from \"./models/RootSync\";\nimport { currentLocale } from \"./session-refresh\";\nimport { recordKeyOf as keyOf, waitingRecordKeys } from \"./waiting\";\n\nclass SyncInterruptedError extends Error {}\n\nfunction assertStillOnline(): void {\n if (!canReachApi()) throw new SyncInterruptedError(\"The connection dropped during the sync\");\n}\n\n/**\n * A record the GM cannot read (403) or that is gone (404) is skipped by this sync (it never\n * blocks the sync state). It is not stored; it is remembered with its `updatedAt` from the sync\n * answer, and a later sync asks for it again only once that `updatedAt` is newer.\n */\nconst isSkippable = (status: number) => status === 403 || status === 404;\n\nasync function fetchResources(\n module: ApiRequestDataTypeInterface,\n endpoint: string,\n skipped: string[],\n included: any[],\n): Promise<any[]> {\n const response = await JsonApiGet({ classKey: module, endpoint, language: currentLocale() });\n assertStillOnline();\n if (!response.ok) {\n if (isSkippable(response.response)) {\n skipped.push(endpoint);\n return [];\n }\n throw new Error(`[offline] GET ${endpoint} answered ${response.response}`);\n }\n included.push(...(response.raw?.included ?? []));\n const data = response.raw?.data;\n if (!data) return [];\n return Array.isArray(data) ? data : [data];\n}\n\nasync function imagesOfRoot(store: DeviceStore, rootId: string): Promise<{ type: string; id: string; url: string }[]> {\n const images: { type: string; id: string; url: string }[] = [];\n for (const typeConfig of getOfflineConfig().types) {\n for (const record of await store.listRecords({ type: typeConfig.module.name, rootId })) {\n const url = imageUrlOf(record.resource);\n if (url) images.push({ type: record.type, id: record.id, url });\n }\n }\n return images;\n}\n\n/**\n * Removes a root record from the device (records, links, sync state, images) unless it has waiting\n * changes. A shared record another root record still uses stays, with its image.\n */\nasync function removeRootIfIdle(store: DeviceStore, rootId: string): Promise<void> {\n if ((await store.queue()).some((entry) => entry.rootId === rootId)) return;\n const images = await imagesOfRoot(store, rootId);\n await store.removeRoot(rootId);\n for (const image of images) {\n if (!(await store.getRecord(image.type, image.id))) await removeArtworkImage(image.url);\n }\n}\n\n/**\n * Brings one root record's device copy up to date with the sync answer (C2): records changed since\n * the stored server time are fetched (C3), records gone from the root record are deleted, the links\n * are replaced, and artwork images are downloaded. The sync state is stored only once every\n * fetch succeeded (or answered 403/404). A record with a waiting change is never overwritten.\n */\nexport async function syncRoot(rootId: string, kind?: OfflineRootKind): Promise<void> {\n const config = getOfflineConfig();\n const rootKind = kind ?? rootKinds(config)[0];\n const store = await getOfflineStore();\n const previous = await store.getSyncState(rootId);\n\n const syncEndpoint = new EndpointCreator({\n endpoint: rootKind.module,\n id: rootId,\n childEndpoint: ROOT_SYNC_ENDPOINT,\n }).generate();\n const syncResponse = await JsonApiGet({\n classKey: rootKind.syncModule ?? Modules.RootSync,\n endpoint: syncEndpoint,\n language: currentLocale(),\n });\n assertStillOnline();\n if (!syncResponse.ok) {\n // Deleted, or no longer the GM's: removed from the device unless changes still wait.\n if (isSkippable(syncResponse.response)) {\n await removeRootIfIdle(store, rootId);\n return;\n }\n throw new Error(`[offline] GET ${syncEndpoint} answered ${syncResponse.response}`);\n }\n\n const sync = syncResponse.data as RootSync;\n if (sync.locked) {\n // Locked online, locked offline: nothing is downloaded, waiting changes are kept.\n await store.setSyncState(rootId, {\n serverTime: previous.serverTime,\n locked: true,\n lockedFrom: sync.lockedFrom ?? null,\n });\n await refreshLockedRoots();\n return;\n }\n\n const typeNames = new Set(config.types.map((typeConfig) => typeConfig.module.name));\n const entries = sync.entries.filter((entry) => typeNames.has(entry.type));\n const entryUpdatedAt = new Map(entries.map((entry) => [keyOf(entry.type, entry.id), entry.updatedAt]));\n const since = previous.serverTime;\n /** Inside the `updatedSince` window: a list read with `updatedSince` brings it. */\n const inWindow = (entry: { updatedAt: Date }) => !since || entry.updatedAt.getTime() > since.getTime();\n\n // What the device holds for this root record now. A record is fetched when it changed since the\n // last sync, and also when the device does not hold it or holds it as stale (updatedAt cleared\n // while it had a waiting change, see below) — unless an earlier sync skipped it (403/404, or\n // absent from the answer) and it has not changed on the server since.\n const stored = new Map<string, StoredRecord>();\n for (const typeConfig of config.types) {\n for (const record of await store.listRecords({ type: typeConfig.module.name, rootId })) {\n stored.set(keyOf(record.type, record.id), record);\n }\n }\n const previouslySkipped = new Map(\n (await store.skippedRecords(rootId)).map((record) => [keyOf(record.type, record.id), record.updatedAt]),\n );\n const isStale = (entry: { type: string; id: string; updatedAt: Date }) => {\n const key = keyOf(entry.type, entry.id);\n const record = stored.get(key);\n if (record && record.updatedAt !== null) return false;\n const skippedAt = previouslySkipped.get(key);\n return !skippedAt || entry.updatedAt.getTime() > skippedAt.getTime();\n };\n const changed = entries.filter((entry) => inWindow(entry) || isStale(entry));\n\n const skipped: string[] = [];\n const fetched: any[] = [];\n // Records the answers include whose type the device does not sync itself (the root record's\n // owner): kept for display only, so a local answer can include them as the server does.\n const included: any[] = [];\n // The root record of the kind is fetched before the other types: a kind's `typeEndpoints` are\n // built from it.\n const rootTypeName = rootKind.module.name;\n const orderedTypes = [\n ...config.types.filter((typeConfig) => typeConfig.module.name === rootTypeName),\n ...config.types.filter((typeConfig) => typeConfig.module.name !== rootTypeName),\n ];\n const rootResource = async (): Promise<StoredRecord[\"resource\"]> => {\n const fresh = fetched.find((resource) => resource?.type === rootTypeName && resource?.id === rootId);\n if (fresh) return fresh;\n const storedRoot = await store.getRecord(rootTypeName, rootId);\n if (storedRoot) return storedRoot.resource;\n throw new Error(`[offline] ${rootTypeName} ${rootId}: the root record is needed to build the type endpoints`);\n };\n for (const typeConfig of orderedTypes) {\n const changedOfType = changed.filter((entry) => entry.type === typeConfig.module.name);\n if (changedOfType.length === 0) continue;\n\n const typeEndpoint = rootKind.typeEndpoints?.find((candidate) => candidate.module.name === typeConfig.module.name);\n if (typeEndpoint) {\n // One full list read under this kind, never with `updatedSince`.\n const endpoint = typeEndpoint.endpoint({ id: rootId, resource: await rootResource() });\n fetched.push(...(await fetchResources(typeConfig.module, endpoint, skipped, included)));\n } else if (typeConfig.fetch === \"root-list\") {\n // One list read per type. With `updatedSince` when every changed record is inside the\n // window; without it when a record older than the window is missing or stale on the\n // device, so the whole list comes in the same single request (never one read per record).\n const endpoint = new EndpointCreator({\n endpoint: rootKind.module,\n id: rootId,\n childEndpoint: typeConfig.module,\n }).addAdditionalParam(\"fetchAll\", \"true\");\n if (since && changedOfType.every(inWindow)) endpoint.addAdditionalParam(\"updatedSince\", since.toISOString());\n fetched.push(...(await fetchResources(typeConfig.module, endpoint.generate(), skipped, included)));\n } else if (typeConfig.fetch === \"linked-list\") {\n // Read only as a list per target: the targets are the other ends of the changed records'\n // sync links of `linkKind`; each target's list is read once, and only the type's entries kept.\n if (!typeConfig.linkedList) throw new Error(`[offline] ${typeConfig.module.name}: linked-list needs linkedList`);\n const { linkKind, endpoint } = typeConfig.linkedList;\n const changedIds = new Set(changedOfType.map((entry) => entry.id));\n const targets = new Map<string, { type: string; id: string }>();\n for (const link of sync.links) {\n if (link.kind !== linkKind || link.fromType !== typeConfig.module.name || !changedIds.has(link.fromId))\n continue;\n targets.set(keyOf(link.toType, link.toId), { type: link.toType, id: link.toId });\n }\n for (const target of targets.values()) {\n const resources = await fetchResources(typeConfig.module, endpoint(rootId, target), skipped, included);\n fetched.push(\n ...resources.filter((resource) => resource?.type === typeConfig.module.name && changedIds.has(resource.id)),\n );\n }\n } else {\n for (const entry of changedOfType) {\n const endpoint = new EndpointCreator({ endpoint: typeConfig.module, id: entry.id }).generate();\n fetched.push(...(await fetchResources(typeConfig.module, endpoint, skipped, included)));\n }\n }\n }\n\n const fetchedKeys = new Set(fetched.map((resource) => keyOf(resource?.type, resource?.id)));\n const previousImageUrls = new Map<string, string>();\n const removedImageUrls: string[] = [];\n\n await store.transaction(async (tx) => {\n // A record with a waiting change (pending, sending, in conflict or failed) is never\n // overwritten nor deleted here. Read inside this transaction, so an edit queued while the\n // records were being fetched is seen.\n const waiting = waitingRecordKeys(await tx.queue(), typeNames);\n\n for (const typeConfig of config.types) {\n for (const record of await tx.listRecords({ type: typeConfig.module.name, rootId })) {\n const key = keyOf(record.type, record.id);\n if (waiting.has(key)) {\n // Marked stale: once the change stops waiting (sent, resolved, discarded), the next\n // sync fetches the record again, whatever its updatedAt.\n if (record.updatedAt !== null) await tx.putRecord({ ...record, updatedAt: null });\n continue;\n }\n if (entryUpdatedAt.has(key)) continue;\n // Leaves this root record; deleted only when no other root record keeps it (a shared record).\n await tx.removeRecordFromRoot(record.type, record.id, rootId);\n const url = imageUrlOf(record.resource);\n if (url && !(await tx.getRecord(record.type, record.id))) removedImageUrls.push(url);\n }\n }\n\n for (const resource of fetched) {\n const key = keyOf(resource?.type, resource?.id);\n if (!entryUpdatedAt.has(key)) continue;\n if (waiting.has(key)) continue;\n const existing = await tx.getRecord(resource.type, resource.id);\n const previousUrl = imageUrlOf(existing?.resource);\n if (previousUrl) previousImageUrls.set(key, previousUrl);\n await tx.putRecord({\n type: resource.type,\n id: resource.id,\n rootId,\n resource,\n updatedAt: entryUpdatedAt.get(key) ?? null,\n base: null,\n localOnly: false,\n });\n }\n\n for (const resource of included) {\n if (!resource?.type || !resource?.id || typeNames.has(resource.type)) continue;\n await tx.putRecord({\n type: resource.type,\n id: resource.id,\n rootId,\n resource,\n updatedAt: null,\n base: null,\n localOnly: false,\n });\n }\n\n // Remembered skips: a changed record no answer brought (403/404, or absent from the list), and\n // an earlier skip still in the root record that was not asked for again.\n const skippedRecords: SkippedRecord[] = [];\n const changedKeys = new Set(changed.map((entry) => keyOf(entry.type, entry.id)));\n for (const entry of entries) {\n const key = keyOf(entry.type, entry.id);\n if (waiting.has(key) || fetchedKeys.has(key)) continue;\n if (changedKeys.has(key)) skippedRecords.push({ type: entry.type, id: entry.id, updatedAt: entry.updatedAt });\n else {\n const skippedAt = previouslySkipped.get(key);\n if (skippedAt) skippedRecords.push({ type: entry.type, id: entry.id, updatedAt: skippedAt });\n }\n }\n await tx.setSkippedRecords(rootId, skippedRecords);\n\n await tx.replaceRootLinks(rootId, sync.links);\n await tx.setSyncState(rootId, {\n serverTime: sync.serverTime,\n locked: false,\n lockedFrom: sync.lockedFrom ?? null,\n });\n });\n\n if (skipped.length > 0) {\n console.warn(`[offline] root record ${rootId}: skipped records the GM cannot read:`, skipped.join(\", \"));\n }\n\n for (const url of removedImageUrls) await removeArtworkImage(url);\n for (const typeConfig of config.types) {\n for (const record of await store.listRecords({ type: typeConfig.module.name, rootId })) {\n const url = imageUrlOf(record.resource);\n if (url) await cacheArtworkImage(url, previousImageUrls.get(keyOf(record.type, record.id)));\n }\n }\n\n await refreshLockedRoots();\n}\n\n/**\n * Syncs every root record of every kind (`listSyncRoots()` of the primary kind and of each\n * `extraRootKinds` entry), then removes from the device the known root records no kind lists\n * (unless they still have waiting changes). A kind whose list throws keeps its device copy\n * unchanged; the primary kind's list throwing stops the sync. Does nothing offline.\n */\nexport async function syncAll(): Promise<void> {\n if (!isOfflineConfigured()) return;\n const config = getOfflineConfig();\n if (!config.getUserId() || !canReachApi()) return;\n\n const [primaryKind, ...extraKinds] = rootKinds(config);\n let primaryRoots: string[];\n try {\n primaryRoots = await primaryKind.listSyncRoots();\n } catch (error) {\n console.warn(\"[offline] could not list the root records to sync\", error);\n return;\n }\n const work: { rootId: string; kind: OfflineRootKind }[] = primaryRoots.map((rootId) => ({\n rootId,\n kind: primaryKind,\n }));\n const extraRoots: string[] = [];\n // Kinds whose list failed: their stored roots are kept as they are.\n const unlistedKinds: OfflineRootKind[] = [];\n for (const kind of extraKinds) {\n try {\n const ids = await kind.listSyncRoots();\n extraRoots.push(...ids);\n work.push(...ids.map((rootId) => ({ rootId, kind })));\n } catch (error) {\n console.warn(`[offline] could not list the ${kind.module.name} root records to sync`, error);\n unlistedKinds.push(kind);\n }\n }\n if (!canReachApi()) return;\n const kept = new Set(work.map((item) => item.rootId));\n if (unlistedKinds.length > 0) {\n const store = await getOfflineStore();\n for (const kind of unlistedKinds) {\n for (const record of await store.listRecords({ type: kind.module.name })) {\n kept.add(record.id);\n extraRoots.push(record.id);\n }\n }\n }\n rememberSyncRoots([...kept], extraRoots);\n\n const total = work.length;\n setOfflineState({ syncProgress: { done: 0, total } });\n try {\n let done = 0;\n for (const { rootId, kind } of work) {\n try {\n await syncRoot(rootId, kind);\n } catch (error) {\n console.warn(`[offline] sync of root record ${rootId} failed`, error);\n if (!canReachApi()) return;\n } finally {\n done += 1;\n setOfflineState({ syncProgress: { done, total } });\n }\n }\n\n // Every root record was tried: this counts as the device's last sync. The root count is the\n // primary kind's (shown as the number of root records, e.g. campaigns).\n const finishedAt = Date.now();\n const userId = config.getUserId();\n if (userId) writeOfflineLastSync(userId, finishedAt);\n setOfflineState({ lastSyncedAt: finishedAt, rootCount: primaryRoots.length });\n\n const store = await getOfflineStore();\n for (const rootId of await knownRootIds(store)) {\n if (!kept.has(rootId)) await removeRootIfIdle(store, rootId);\n }\n await refreshLockedRoots();\n } finally {\n setOfflineState({ syncProgress: null });\n }\n}\n","import { useSyncExternalStore } from \"react\";\nimport { getOfflineConfig, getOfflineStore, isOfflineConfigured } from \"../config\";\nimport { setOfflineState } from \"../state\";\nimport type { Conflict, DeviceStore } from \"../store/DeviceStore\";\n\nconst EMPTY: Conflict[] = [];\nlet _conflicts: Conflict[] = EMPTY;\nconst _listeners = new Set<() => void>();\n\nfunction subscribe(listener: () => void): () => void {\n _listeners.add(listener);\n return () => {\n _listeners.delete(listener);\n };\n}\n\n/** Re-reads the queue and the conflicts: updates `pending`/`conflicts` and the `useConflicts` list. */\nexport async function refreshOfflineCounts(store?: DeviceStore): Promise<void> {\n if (!store) {\n if (!isOfflineConfigured() || !getOfflineConfig().getUserId()) return;\n store = await getOfflineStore();\n }\n const entries = await store.queue();\n _conflicts = await store.conflicts();\n for (const listener of [..._listeners]) listener();\n setOfflineState({\n pending: entries.filter((entry) => entry.status !== \"conflict\").length,\n conflicts: _conflicts.length,\n });\n}\n\n/** The unresolved conflicts of the signed-in user's device. */\nexport function useConflicts(): Conflict[] {\n return useSyncExternalStore(\n subscribe,\n () => _conflicts,\n () => EMPTY,\n );\n}\n","import { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { Modules } from \"../../core/registry/ModuleRegistry\";\nimport { buildUrl, JsonApiPost } from \"../../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, isOfflineConfigured } from \"../config\";\nimport { allowWhileReconnecting, getOfflineState, setOfflineState } from \"../state\";\nimport type { DeviceStore, QueueEntry } from \"../store/DeviceStore\";\nimport { isRootLocked } from \"../store/lifecycle\";\nimport { writeThrough } from \"../write-through\";\nimport { refreshOfflineCounts } from \"./counts\";\nimport { OfflineChangeSet } from \"./models/OfflineChangeSet\";\nimport { OfflineChange, OfflineChangeResult } from \"./models/OfflineChangeSetInterface\";\nimport { currentLocale, refreshSession } from \"./session-refresh\";\nimport { recordKeysOfEntry, waitingRecordKeys } from \"./waiting\";\n\nexport type DrainResult = { applied: number; conflicts: number; stoppedForLogin: boolean };\n\nconst syncedTypeNames = () => new Set(getOfflineConfig().types.map((typeConfig) => typeConfig.module.name));\n\n/**\n * The body as sent: each resource's server-owned fields (AI summaries, tl;dr, `aiStatus`, …) are\n * left out of its attributes. The stored body and the stored record keep them.\n */\nfunction withoutServerOwnedFields(body: any): any {\n const data = body?.data;\n if (!data) return body;\n const owned = new Map(\n getOfflineConfig().types.map((typeConfig) => [typeConfig.module.name, typeConfig.serverOwnedFields]),\n );\n const strip = (resource: any) => {\n const fields = owned.get(resource?.type) ?? [];\n if (!resource?.attributes || fields.length === 0) return resource;\n const attributes = { ...resource.attributes };\n for (const field of fields) delete attributes[field];\n return { ...resource, attributes };\n };\n return { ...body, data: Array.isArray(data) ? data.map(strip) : strip(data) };\n}\n\nfunction toChange(entry: QueueEntry): OfflineChange {\n const change: OfflineChange = { seq: entry.seq, method: entry.method as OfflineChange[\"method\"], path: entry.path };\n if (entry.body !== undefined) change.body = withoutServerOwnedFields(entry.body);\n if (entry.recordType) change.recordType = entry.recordType;\n if (entry.recordId) change.recordId = entry.recordId;\n change.base = entry.base ?? null;\n if (entry.resolution) change.resolution = entry.resolution;\n return change;\n}\n\n/**\n * The merge the server settled, as a resource the record type's model can rehydrate: the server's\n * current resource with the merged attribute values laid over it (decision piece 2: the library\n * shapes stored resources). Undefined when the server gave no merge or no current record.\n */\nfunction mergedResource(current: any, merged: Record<string, unknown> | undefined): any | undefined {\n if (!merged || !current?.type) return undefined;\n return { ...current, attributes: { ...(current.attributes ?? {}), ...merged } };\n}\n\nfunction newPackageId(): string {\n return globalThis.crypto.randomUUID();\n}\n\n/**\n * The id of the package to send: the id of a package that never got its answer (the page\n * closed, the network dropped) when one of the entries carries it, so the server applies it\n * once; a fresh id otherwise.\n */\nfunction packageIdFor(entries: QueueEntry[]): string {\n return entries.find((entry) => entry.packageId)?.packageId ?? newPackageId();\n}\n\n/** The entries to send: pending, of unlocked root records, not behind a conflict on the same record. */\nasync function entriesToSend(store: DeviceStore): Promise<QueueEntry[]> {\n const all = await store.queue();\n const typeNames = syncedTypeNames();\n const blocked = waitingRecordKeys(\n all.filter((entry) => entry.status === \"conflict\"),\n typeNames,\n );\n const lockedIds = new Set(getOfflineState().lockedRootIds);\n const lockedByRoot = new Map<string, boolean>();\n const now = new Date();\n\n const result: QueueEntry[] = [];\n for (const entry of all) {\n // \"sending\" is a package that never got its answer (the page closed): it is sent again.\n if (entry.status !== \"pending\" && entry.status !== \"sending\") continue;\n if (recordKeysOfEntry(entry, typeNames).some((key) => blocked.has(key))) continue;\n\n if (!lockedByRoot.has(entry.rootId)) {\n lockedByRoot.set(\n entry.rootId,\n lockedIds.has(entry.rootId) || isRootLocked(await store.getSyncState(entry.rootId), now),\n );\n }\n if (lockedByRoot.get(entry.rootId)) continue;\n result.push(entry);\n }\n return result.sort((a, b) => a.seq - b.seq);\n}\n\nasync function applyResult(\n store: DeviceStore,\n entry: QueueEntry,\n result: OfflineChangeResult,\n): Promise<\"applied\" | \"conflict\" | \"pending\"> {\n if (result.status === \"applied\") {\n await store.removeQueueEntry(entry.seq);\n // The record's base is kept only while it has unsent changes; cleared first, so the\n // write-through below may store the server's answer (it never overwrites a record that\n // still has a base or a waiting change).\n const typeNames = syncedTypeNames();\n const stillWaiting = waitingRecordKeys(await store.queue(), typeNames);\n for (const key of recordKeysOfEntry(entry, typeNames)) {\n if (stillWaiting.has(key)) continue;\n const separator = key.indexOf(\":\");\n const record = await store.getRecord(key.slice(0, separator), key.slice(separator + 1));\n if (record && (record.base !== null || record.localOnly)) {\n await store.putRecord({ ...record, base: null, localOnly: false });\n }\n }\n if (result.response) {\n await writeThrough(\n { method: entry.method, url: entry.path },\n { ok: true, status: 200, statusText: \"OK\", data: result.response },\n );\n } else if (entry.method === \"DELETE\") {\n await writeThrough(\n { method: entry.method, url: entry.path },\n { ok: true, status: 204, statusText: \"OK\", data: undefined },\n );\n }\n return \"applied\";\n }\n\n if (result.status === \"conflict\" || result.status === \"could-not-apply\") {\n await store.addConflict({\n queueSeq: entry.seq,\n kind: result.status === \"could-not-apply\" ? \"could-not-apply\" : (result.conflict ?? \"fields\"),\n recordType: entry.recordType,\n recordId: entry.recordId,\n mine: entry.body ?? null,\n theirs: result.current ?? null,\n base: entry.base ?? null,\n merged: mergedResource(result.current, result.merged),\n fields: result.conflictFields ?? [],\n blocks: result.conflictBlocks,\n reason: result.detail,\n });\n await store.setQueueStatus(entry.seq, \"conflict\");\n return \"conflict\";\n }\n\n await store.setQueueStatus(entry.seq, \"pending\");\n return \"pending\";\n}\n\nasync function drain(): Promise<DrainResult> {\n const outcome: DrainResult = { applied: 0, conflicts: 0, stoppedForLogin: false };\n if (!isOfflineConfigured() || !getOfflineConfig().getUserId()) return outcome;\n\n const store = await getOfflineStore();\n const entries = await entriesToSend(store);\n if (entries.length === 0) return outcome;\n\n const session = await refreshSession();\n if (session === \"unreachable\") return outcome;\n if (session === \"expired\") return { ...outcome, stoppedForLogin: true };\n\n setOfflineState({ draining: true });\n const handled = new Set<number>();\n try {\n const packageId = packageIdFor(entries);\n // Stored with the entries before sending: an unanswered package keeps its id for the resend.\n await store.setQueuePackage(\n entries.map((entry) => entry.seq),\n packageId,\n );\n for (const entry of entries) await store.setQueueStatus(entry.seq, \"sending\");\n\n // ONE request with every waiting change, whatever its size: never split (Carlo, 2026-11-01).\n const endpoint = new EndpointCreator({ endpoint: Modules.OfflineChangeSet }).generate();\n // During a reconnect attempt the session is still offline: this request goes to the network.\n const release = allowWhileReconnecting(buildUrl(endpoint));\n let response: Awaited<ReturnType<typeof JsonApiPost>>;\n try {\n response = await JsonApiPost({\n classKey: Modules.OfflineChangeSet,\n endpoint,\n body: { id: packageId, changes: entries.map(toChange) },\n language: currentLocale(),\n });\n } finally {\n release();\n }\n\n if (!response.ok) {\n if (response.response === 401) outcome.stoppedForLogin = true;\n return outcome;\n }\n\n // The server answered for this package: every entry left in the queue leaves it.\n await store.setQueuePackage(\n entries.map((entry) => entry.seq),\n null,\n );\n\n const results = new Map((response.data as OfflineChangeSet).results.map((result) => [result.seq, result]));\n for (const entry of entries) {\n const result = results.get(entry.seq);\n const status = result ? await applyResult(store, entry, result) : \"pending\";\n if (!result) await store.setQueueStatus(entry.seq, \"pending\");\n handled.add(entry.seq);\n if (status === \"applied\") outcome.applied++;\n if (status === \"conflict\") outcome.conflicts++;\n }\n return outcome;\n } catch (error) {\n console.error(\"[offline] sending the waiting changes failed\", error);\n return outcome;\n } finally {\n // Nothing is dropped: every entry without an answer goes back to pending.\n for (const entry of entries) {\n if (!handled.has(entry.seq)) await store.setQueueStatus(entry.seq, \"pending\");\n }\n setOfflineState({ draining: false });\n await refreshOfflineCounts(store);\n }\n}\n\nlet _inFlight: Promise<DrainResult> | undefined;\n\n/**\n * Sends every waiting change of the unlocked root records in ONE `offline-change-sets` request,\n * after renewing the sign-in. Applied entries are removed and their answer written to the\n * device copy; conflicts become Conflict rows; waiting entries stay pending. A network error\n * or an expired sign-in stops it and keeps every entry.\n */\nexport function drainQueue(): Promise<DrainResult> {\n if (!_inFlight) {\n _inFlight = drain().finally(() => {\n _inFlight = undefined;\n });\n }\n return _inFlight;\n}\n","import { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { getOfflineConfig, getOfflineStore } from \"../config\";\nimport { getOfflineState } from \"../state\";\nimport type { Conflict, DeviceStore, QueueEntry } from \"../store/DeviceStore\";\nimport type { OfflineChangeResolution } from \"./models/OfflineChangeSetInterface\";\nimport { refreshOfflineCounts } from \"./counts\";\nimport { drainQueue } from \"./drain-queue\";\n\nexport { useConflicts } from \"./counts\";\n\nexport type ConflictChoice =\n | { kind: \"mine\" }\n | { kind: \"theirs\" }\n /**\n * Per conflicting field: whose value wins. `mergedBody`: the JSON:API document the record\n * type's model built with createJsonApi() holding only the fields chosen \"merged\".\n */\n | { kind: \"fields\"; choices: Record<string, \"mine\" | \"theirs\" | \"merged\">; mergedBody?: any }\n | { kind: \"restore\" }\n | { kind: \"accept-deletion\" }\n | { kind: \"delete-anyway\" }\n | { kind: \"retry\" }\n | { kind: \"discard\" };\n\nfunction updatedAtOf(resource: any): Date | null {\n const value = resource?.attributes?.updatedAt;\n if (!value) return null;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** Writes the chosen \"theirs\" and \"merged\" values of the conflicting fields into the stored record. */\nasync function applyChoicesToRecord(\n store: DeviceStore,\n conflict: Conflict,\n choices: Record<string, \"mine\" | \"theirs\" | \"merged\">,\n mergedBody: any,\n): Promise<void> {\n if (!conflict.recordType || !conflict.recordId) return;\n const record = await store.getRecord(conflict.recordType, conflict.recordId);\n if (!record) return;\n const attributes = { ...(record.resource?.attributes ?? {}) };\n const relationships = { ...(record.resource?.relationships ?? {}) };\n for (const [field, choice] of Object.entries(choices)) {\n const source = choice === \"theirs\" ? conflict.theirs : choice === \"merged\" ? mergedBody?.data : undefined;\n if (!source) continue;\n if (source.attributes && field in source.attributes) attributes[field] = source.attributes[field];\n else if (source.relationships && field in source.relationships) relationships[field] = source.relationships[field];\n }\n await store.putRecord({ ...record, resource: { ...record.resource, attributes, relationships } });\n}\n\nasync function rootIdOf(store: DeviceStore, conflict: Conflict, entry: QueueEntry | undefined): Promise<string> {\n if (entry) return entry.rootId;\n if (conflict.recordType && conflict.recordId) {\n const record = await store.getRecord(conflict.recordType, conflict.recordId);\n if (record) return record.rootId;\n }\n throw new Error(`[offline] conflict ${conflict.id} belongs to no root record on this device`);\n}\n\n/** The collection path of a record type, from the configured module (EndpointCreator). */\nfunction collectionPath(recordType: string): string {\n const module = getOfflineConfig().types.find((typeConfig) => typeConfig.module.name === recordType)?.module;\n return new EndpointCreator({ endpoint: module ?? recordType }).generate();\n}\n\n/** Queues the conflicting change again, unchanged apart from `resolution`, and drops the old entry. */\nasync function requeueOriginal(\n store: DeviceStore,\n conflict: Conflict,\n entry: QueueEntry | undefined,\n fallbackMethod: \"PATCH\" | \"DELETE\",\n resolution: OfflineChangeResolution,\n): Promise<void> {\n const method = entry?.method ?? fallbackMethod;\n const body = method === \"DELETE\" ? undefined : (entry?.body ?? conflict.mine ?? undefined);\n await store.enqueue({\n rootId: await rootIdOf(store, conflict, entry),\n method,\n path:\n entry?.path ??\n new EndpointCreator({ endpoint: collectionPath(conflict.recordType!), id: conflict.recordId }).generate(),\n ...(body !== undefined ? { body } : {}),\n recordType: entry?.recordType ?? conflict.recordType,\n recordId: entry?.recordId ?? conflict.recordId,\n base: entry ? entry.base : (conflict.base ?? null),\n // QueueEntry[\"resolution\"] is the store's copy of OfflineChange[\"resolution\"].\n resolution: resolution as QueueEntry[\"resolution\"],\n });\n if (entry) await store.removeQueueEntry(entry.seq);\n}\n\nasync function resolve(store: DeviceStore, conflict: Conflict, choice: ConflictChoice): Promise<void> {\n const entry = (await store.queue()).find((candidate) => candidate.seq === conflict.queueSeq);\n const dropEntry = async () => {\n if (entry) await store.removeQueueEntry(entry.seq);\n };\n\n switch (choice.kind) {\n case \"mine\":\n case \"fields\": {\n if (choice.kind === \"fields\" && Object.values(choice.choices).every((value) => value === \"theirs\")) {\n return resolve(store, conflict, { kind: \"theirs\" });\n }\n // The ORIGINAL change goes back (original body and base, so the server's three-way merge\n // keeps every field only the server changed), carrying how each conflicting field is settled.\n const resolution: OfflineChangeResolution =\n choice.kind === \"mine\"\n ? { kind: \"mine\" }\n : {\n kind: \"merged\",\n choices: choice.choices,\n ...(choice.mergedBody !== undefined ? { mergedBody: choice.mergedBody } : {}),\n };\n await requeueOriginal(store, conflict, entry, \"PATCH\", resolution);\n if (choice.kind === \"fields\") await applyChoicesToRecord(store, conflict, choice.choices, choice.mergedBody);\n break;\n }\n case \"theirs\": {\n // Only the device changes: the server's version replaces mine.\n await dropEntry();\n if (conflict.recordType && conflict.recordId) {\n if (conflict.theirs) {\n await store.putRecord({\n type: conflict.recordType,\n id: conflict.recordId,\n rootId: await rootIdOf(store, conflict, entry),\n resource: conflict.theirs,\n updatedAt: updatedAtOf(conflict.theirs),\n base: null,\n localOnly: false,\n });\n } else {\n await store.deleteRecord(conflict.recordType, conflict.recordId);\n }\n }\n break;\n }\n case \"restore\": {\n // Deleted on the server: created again with my version.\n if (!conflict.recordType) throw new Error(`[offline] conflict ${conflict.id} has no record to restore`);\n const body = conflict.mine ?? entry?.body;\n const rootId = await rootIdOf(store, conflict, entry);\n await store.enqueue({\n rootId,\n method: \"POST\",\n path: collectionPath(conflict.recordType),\n body,\n recordType: conflict.recordType,\n recordId: conflict.recordId,\n base: null,\n });\n await dropEntry();\n if (conflict.recordId) {\n const record = await store.getRecord(conflict.recordType, conflict.recordId);\n // localOnly keeps it on the device until the server has it again.\n if (record) await store.putRecord({ ...record, base: null, localOnly: true });\n else if (body?.data)\n await store.putRecord({\n type: conflict.recordType,\n id: conflict.recordId,\n rootId,\n resource: body.data,\n updatedAt: null,\n base: null,\n localOnly: true,\n });\n }\n break;\n }\n case \"accept-deletion\": {\n await dropEntry();\n if (conflict.recordType && conflict.recordId) await store.deleteRecord(conflict.recordType, conflict.recordId);\n break;\n }\n case \"delete-anyway\": {\n // The original DELETE with a resolution: the server skips the changed-on-server check.\n await requeueOriginal(store, conflict, entry, \"DELETE\", { kind: \"mine\" });\n break;\n }\n case \"retry\": {\n if (entry) await store.setQueueStatus(entry.seq, \"pending\");\n break;\n }\n case \"discard\": {\n await dropEntry();\n break;\n }\n }\n\n await store.removeConflict(conflict.id);\n}\n\n/**\n * Applies the GM's choice for one conflict. \"keep mine\", per-field choices, \"restore\" and \"delete anyway\"\n * queue one more change (sent with the next package); \"keep theirs\" and \"let the deletion\n * stand\" only update the device. Online, the queue is sent right away.\n */\nexport async function resolveConflict(id: string, choice: ConflictChoice): Promise<void> {\n const store = await getOfflineStore();\n const conflict = (await store.conflicts()).find((candidate) => candidate.id === id);\n if (!conflict) throw new Error(`[offline] no conflict ${id}`);\n\n await store.transaction((tx) => resolve(tx, conflict, choice));\n await refreshOfflineCounts(store);\n\n if (getOfflineState().online) {\n drainQueue().catch((error) => console.warn(\"[offline] sending the resolved change failed\", error));\n }\n}\n","import { getCookie } from \"cookies-next\";\nimport { getClientToken } from \"../../client/token\";\nimport { directFetch } from \"../../client/request\";\nimport { EndpointCreator } from \"../../core/endpoint/EndpointCreator\";\nimport { buildUrl } from \"../../unified/JsonApiRequest\";\nimport {\n getOfflineConfig,\n getOfflineStore,\n isOfflineConfigured,\n OfflineUnavailableError,\n refreshOfflineDeviceState,\n} from \"../config\";\nimport { getOfflineState, isReconnecting, setOfflineState, setReconnecting, subscribeOfflineState } from \"../state\";\nimport { OFFLINE_REPLAY_HEADER } from \"../types\";\nimport { refreshOfflineCounts } from \"./counts\";\nimport { drainQueue } from \"./drain-queue\";\nimport { knownPrimaryRootIds, refreshLockedRoots } from \"./known-roots\";\nimport { ROOT_SYNC_ENDPOINT } from \"./models/RootSync\";\nimport { currentLocale } from \"./session-refresh\";\nimport { syncAll } from \"./sync-root\";\n\n/** How often the stored lock dates are checked again (a lock date can pass while offline). */\nconst LOCK_CHECK_INTERVAL_MS = 60_000;\n/** How often the API is probed while the session is offline but the browser says it is online. */\nconst PROBE_INTERVAL_MS = 30_000;\n\n/**\n * Asks the API whether it can be reached again. The request carries the replay header, so\n * `directFetch` sends it to the network instead of answering it from the device copy. Any\n * answer (even a 401) means the API is reachable; only a network error means it is not.\n */\nasync function probeApi(): Promise<boolean> {\n const config = getOfflineConfig();\n const store = await getOfflineStore();\n // A root record of the primary kind only (the extra kinds' roots answer elsewhere).\n const [rootId] = await knownPrimaryRootIds(store);\n const endpoint = rootId\n ? new EndpointCreator({ endpoint: config.rootModule, id: rootId, childEndpoint: ROOT_SYNC_ENDPOINT })\n : new EndpointCreator({ endpoint: config.rootModule }).addAdditionalParam(\"page[size]\", \"1\");\n\n const response = await directFetch({\n method: \"GET\",\n url: buildUrl(endpoint.generate()),\n token: await getClientToken(),\n language: currentLocale(),\n additionalHeaders: { [OFFLINE_REPLAY_HEADER]: \"1\" },\n });\n return !response.networkError;\n}\n\n/** Remembers (for the tab) the sign-in a reload for the login was already tried for. */\nconst LOGIN_RELOAD_KEY = \"nja-offline-login-reload\";\nlet _reloadedForSignIn: string | undefined;\n\n/** A short fingerprint of the sign-in (the `token` cookie), never the token itself. */\nasync function signInFingerprint(): Promise<string> {\n const token = await getCookie(\"token\");\n if (!token) return \"none\";\n let hash = 5381;\n for (const char of String(token)) hash = ((hash * 33) ^ char.charCodeAt(0)) >>> 0;\n return hash.toString(36);\n}\n\nconst readReloadMarker = (): string | null => {\n try {\n return window.sessionStorage.getItem(LOGIN_RELOAD_KEY);\n } catch {\n return null;\n }\n};\n\nconst writeReloadMarker = (value: string | null): void => {\n try {\n if (value === null) window.sessionStorage.removeItem(LOGIN_RELOAD_KEY);\n else window.sessionStorage.setItem(LOGIN_RELOAD_KEY, value);\n } catch {\n // Storage unavailable: the module flag still stops a loop within this page.\n }\n};\n\n/**\n * The sign-in expired: reloading sends the GM through the proxy to the login page. The queue\n * stays. Tried once per sign-in: when the API still refuses the same sign-in after the reload\n * (a 401 while the cookie exists), the GM stays on the page and the queue waits for a new\n * sign-in, instead of reloading in a loop.\n */\nasync function signInExpired(): Promise<void> {\n const signIn = await signInFingerprint();\n if (_reloadedForSignIn === signIn || readReloadMarker() === signIn) {\n console.warn(\"[offline] the sign-in expired; log in again to send the waiting changes\");\n return;\n }\n _reloadedForSignIn = signIn;\n writeReloadMarker(signIn);\n try {\n window.location.reload();\n } catch (error) {\n console.warn(\"[offline] the sign-in expired; log in again to send the waiting changes\", error);\n }\n}\n\n/**\n * Logs a failure of the runtime, except the browser being found unable to keep the device copy:\n * that is warned once (`markOfflineUnavailable`) and the runtime stops.\n */\nconst warnUnlessUnavailable =\n (message: string) =>\n (error: unknown): void => {\n if (!(error instanceof OfflineUnavailableError)) console.warn(message, error);\n };\n\nconst _runningRuntimes = new Set<() => void>();\n\n/** Stops every running offline runtime (timers, listeners). Called on logout. */\nexport function stopOfflineRuntime(): void {\n for (const stop of [..._runningRuntimes]) stop();\n}\n\n/**\n * Starts the offline runtime: drain-then-sync on open (online) and on every reconnect, syncAll\n * every `syncIntervalMs` while online, a probe of the API while the session is offline but the\n * browser is online (a probe that reaches the API runs the drain and the sync, and the session\n * goes online once they succeeded), and the lock dates checked every minute. Returns the stop\n * function.\n */\nexport function startOfflineRuntime(): () => void {\n refreshOfflineDeviceState();\n if (!isOfflineConfigured()) return () => {};\n const config = getOfflineConfig();\n\n let running: Promise<void> | undefined;\n let goingOnlineAfterSync = false;\n /**\n * Drain then sync. `reconnect`: a probe reached the API while the session is offline; the\n * session stays offline during the drain and the sync and goes online only when both ran\n * without a network failure (spec § 4: offline \"until a sync succeeds\").\n */\n const run = (reconnect: boolean): Promise<void> => {\n if (!running) {\n running = (async () => {\n let succeeded = false;\n if (reconnect) setReconnecting(true);\n try {\n const drained = await drainQueue();\n if (drained.stoppedForLogin) {\n await signInExpired();\n return;\n }\n writeReloadMarker(null);\n if (reconnect ? !isReconnecting() : !getOfflineState().online) return;\n await syncAll();\n succeeded = reconnect && isReconnecting();\n } catch (error) {\n warnUnlessUnavailable(\"[offline] background sync failed\")(error);\n } finally {\n if (reconnect) setReconnecting(false);\n running = undefined;\n if (succeeded) {\n goingOnlineAfterSync = true;\n setOfflineState({ online: true });\n goingOnlineAfterSync = false;\n // Changes queued while the attempt ran (an edit, a resolved conflict) waited because\n // the session was still offline: they are sent now, not at the next sync interval.\n if (getOfflineState().pending > 0) {\n drainQueue().catch(warnUnlessUnavailable(\"[offline] sending the waiting changes failed\"));\n }\n }\n }\n })();\n }\n return running;\n };\n const runOnline = () => run(false);\n\n let probing = false;\n const probe = async () => {\n if (probing || running || getOfflineState().online) return;\n if (typeof navigator !== \"undefined\" && navigator.onLine === false) return;\n probing = true;\n try {\n if (await probeApi()) await run(true);\n } catch (error) {\n warnUnlessUnavailable(\"[offline] probing the API failed\")(error);\n } finally {\n probing = false;\n }\n };\n\n // The browser says the network is back: probe the API now (the state stays offline until then).\n const onBrowserOnline = () => void probe();\n if (typeof window !== \"undefined\") window.addEventListener(\"online\", onBrowserOnline);\n\n let wasOnline = getOfflineState().online;\n const unsubscribe = subscribeOfflineState((state) => {\n // The browser cannot keep the device copy: offline mode is off for the page.\n if (!state.available) {\n stop();\n return;\n }\n // Online after a reconnect attempt: its drain and sync already ran.\n if (state.online && !wasOnline && !goingOnlineAfterSync) void runOnline();\n wasOnline = state.online;\n });\n\n void refreshLockedRoots().catch(warnUnlessUnavailable(\"[offline] lock check failed\"));\n void refreshOfflineCounts().catch(warnUnlessUnavailable(\"[offline] reading the queue failed\"));\n if (getOfflineState().online) void runOnline();\n\n const syncTimer = setInterval(() => {\n if (getOfflineState().online) void runOnline();\n }, config.syncIntervalMs);\n const probeTimer = setInterval(() => void probe(), PROBE_INTERVAL_MS);\n const lockTimer = setInterval(() => {\n void refreshLockedRoots().catch(warnUnlessUnavailable(\"[offline] lock check failed\"));\n }, LOCK_CHECK_INTERVAL_MS);\n\n const stop = () => {\n _runningRuntimes.delete(stop);\n clearInterval(syncTimer);\n clearInterval(probeTimer);\n clearInterval(lockTimer);\n if (typeof window !== \"undefined\") window.removeEventListener(\"online\", onBrowserOnline);\n unsubscribe();\n };\n _runningRuntimes.add(stop);\n return stop;\n}\n","import {\n blockOfflineStore,\n getOfflineConfig,\n getOfflineStore,\n isOfflineConfigured,\n setOfflineEnabledOnDevice,\n writeOfflineLastSync,\n} from \"./config\";\nimport { setOfflineState } from \"./state\";\nimport { deleteImagesCaches } from \"./sync/artwork-cache\";\nimport { stopOfflineRuntime } from \"./sync/runtime\";\n\n/**\n * How many changes the signed-in user's device still has to send (every queue entry: pending,\n * sending, in conflict or failed). The app warns before logging out when this is above 0.\n */\nexport async function prepareOfflineLogout(): Promise<{ pending: number }> {\n if (!isOfflineConfigured()) return { pending: 0 };\n if (!getOfflineConfig().getUserId()) return { pending: 0 };\n const store = await getOfflineStore();\n const entries = await store.queue();\n return { pending: entries.length };\n}\n\n/**\n * Deletes the user's device database and the cached artwork images. Call it right before logging\n * out. The runtime is stopped first and the store blocked until the next sign-in\n * (`onUserSignedIn`), so a sync still running cannot open, and so recreate, the deleted file.\n * The device switch (`requireOptIn`) is turned off too: after a logout nothing is downloaded again\n * until the user turns offline mode back on.\n */\nexport async function finishOfflineLogout(userId: string): Promise<void> {\n stopOfflineRuntime();\n blockOfflineStore(userId);\n const { deleteDeviceStore } = await import(\"./store/lifecycle\");\n await deleteDeviceStore(userId);\n await deleteImagesCaches();\n setOfflineEnabledOnDevice(userId, false);\n writeOfflineLastSync(userId, null);\n setOfflineState({ enabled: false, syncProgress: null, lastSyncedAt: null, rootCount: null });\n}\n","import {\n forgetOfflineStore,\n getOfflineConfig,\n getOfflineStore,\n isOfflineAvailable,\n setOfflineEnabledOnDevice,\n writeOfflineLastSync,\n} from \"./config\";\nimport { setOfflineState } from \"./state\";\nimport { deleteImagesCaches } from \"./sync/artwork-cache\";\nimport { stopOfflineRuntime } from \"./sync/runtime\";\n\nfunction signedInUserId(): string {\n const userId = getOfflineConfig().getUserId();\n if (!userId) throw new Error(\"No signed-in user\");\n return userId;\n}\n\n/**\n * Turns offline mode on on this device for the signed-in user (`OfflineConfig.requireOptIn`).\n * It does not start the runtime: the app starts it once `enabled` is true in the offline state.\n */\nexport async function turnOnOffline(): Promise<void> {\n const userId = signedInUserId();\n setOfflineEnabledOnDevice(userId, true);\n setOfflineState({ enabled: true });\n try {\n // Asks the browser not to evict the device copy. Best effort only.\n await navigator.storage?.persist?.();\n } catch {\n // Ignored: the copy works without it.\n }\n}\n\n/**\n * Turns offline mode off on this device for the signed-in user and deletes this device's copy\n * (database and cached images). Refuses, changing nothing, while the copy has changes still to\n * send: the answer then carries how many.\n */\nexport async function turnOffOffline(): Promise<{ turnedOff: true } | { turnedOff: false; pending: number }> {\n const userId = signedInUserId();\n\n if (isOfflineAvailable()) {\n let pending = 0;\n try {\n const store = await getOfflineStore();\n pending = (await store.queue()).length;\n } catch {\n // The copy cannot be opened here (held by another tab): the marker file written while\n // changes wait still says whether deleting it would lose any. The count is then unknown: 1.\n const { hasUnsentChanges } = await import(\"./store/lifecycle\");\n if (await hasUnsentChanges(userId).catch(() => false)) pending = 1;\n }\n if (pending > 0) return { turnedOff: false, pending };\n }\n\n // Off first, so a sync still running cannot open, and so recreate, the deleted file.\n setOfflineEnabledOnDevice(userId, false);\n\n if (isOfflineAvailable()) {\n stopOfflineRuntime();\n try {\n const { deleteDeviceStore } = await import(\"./store/lifecycle\");\n await deleteDeviceStore(userId);\n await deleteImagesCaches();\n } catch (error) {\n console.warn(\"[offline] the device copy could not be deleted\", error);\n }\n forgetOfflineStore(userId);\n }\n\n writeOfflineLastSync(userId, null);\n setOfflineState({ enabled: false, syncProgress: null, lastSyncedAt: null, rootCount: null });\n return { turnedOff: true };\n}\n"],"mappings":"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","names":["configureOffline","getCookie","getCookie","deleteDeviceStore","hasUnsentChanges","deleteDeviceStore"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkWCEYT3GGjs = require('./chunk-WCEYT3GG.js');
|
|
5
|
+
require('./chunk-WUNSG3IN.js');
|
|
6
|
+
require('./chunk-7QVYU63E.js');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
exports.directFetch = _chunkWCEYT3GGjs.directFetch;
|
|
10
|
+
//# sourceMappingURL=request-6W6UGKJV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/request-
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/request-6W6UGKJV.js"],"names":[],"mappings":"AAAA,qFAAY;AACZ;AACE;AACF,sDAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B;AACE;AACF,mDAAC","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/request-6W6UGKJV.js"}
|
package/dist/server/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkZYSNJKOPjs = require('../chunk-ZYSNJKOP.js');
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
var _chunkIK65TDQHjs = require('../chunk-IK65TDQH.js');
|
|
@@ -18,7 +18,7 @@ var _chunkIK65TDQHjs = require('../chunk-IK65TDQH.js');
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
var
|
|
21
|
+
var _chunkOQQ7CZWTjs = require('../chunk-OQQ7CZWT.js');
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
var _chunk6TUB7Y6Tjs = require('../chunk-6TUB7Y6T.js');
|
|
@@ -36,15 +36,15 @@ require('../chunk-G4JR3SFV.js');
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
var
|
|
40
|
-
require('../chunk-
|
|
41
|
-
require('../chunk-
|
|
42
|
-
require('../chunk-
|
|
39
|
+
var _chunkPMESKNKLjs = require('../chunk-PMESKNKL.js');
|
|
40
|
+
require('../chunk-4XLDBOSE.js');
|
|
41
|
+
require('../chunk-KV4N7DEG.js');
|
|
42
|
+
require('../chunk-WCEYT3GG.js');
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
var
|
|
47
|
-
require('../chunk-
|
|
46
|
+
var _chunkWUNSG3INjs = require('../chunk-WUNSG3IN.js');
|
|
47
|
+
require('../chunk-GM4VZTWL.js');
|
|
48
48
|
require('../chunk-LXKSUWAV.js');
|
|
49
49
|
require('../chunk-7QVYU63E.js');
|
|
50
50
|
|
|
@@ -76,5 +76,5 @@ require('../chunk-7QVYU63E.js');
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
exports.CACHE_PROFILES = _chunkIK65TDQHjs.CACHE_PROFILES; exports.DEFAULT_SQLITE_MODULE_URL =
|
|
79
|
+
exports.CACHE_PROFILES = _chunkIK65TDQHjs.CACHE_PROFILES; exports.DEFAULT_SQLITE_MODULE_URL = _chunkWUNSG3INjs.DEFAULT_SQLITE_MODULE_URL; exports.OFFLINE_SQLITE_FILES = _chunkWUNSG3INjs.OFFLINE_SQLITE_FILES; exports.ServerAuthService = _chunkPMESKNKLjs.AuthService; exports.ServerCompanyService = _chunkPMESKNKLjs.CompanyService; exports.ServerContentService = _chunkPMESKNKLjs.ContentService; exports.ServerFeatureService = _chunkPMESKNKLjs.FeatureService; exports.ServerJsonApiDelete = _chunkOQQ7CZWTjs.ServerJsonApiDelete; exports.ServerJsonApiGet = _chunkOQQ7CZWTjs.ServerJsonApiGet; exports.ServerJsonApiPatch = _chunkOQQ7CZWTjs.ServerJsonApiPatch; exports.ServerJsonApiPost = _chunkOQQ7CZWTjs.ServerJsonApiPost; exports.ServerJsonApiPut = _chunkOQQ7CZWTjs.ServerJsonApiPut; exports.ServerNotificationService = _chunkPMESKNKLjs.NotificationService; exports.ServerPushService = _chunkPMESKNKLjs.PushService; exports.ServerRoleService = _chunkPMESKNKLjs.RoleService; exports.ServerS3Service = _chunkPMESKNKLjs.S3Service; exports.ServerSession = _chunkZYSNJKOPjs.ServerSession; exports.ServerUserService = _chunkPMESKNKLjs.UserService; exports.buildOfflineManifest = _chunkZYSNJKOPjs.buildOfflineManifest; exports.configureServerJsonApi = _chunkOQQ7CZWTjs.configureServerJsonApi; exports.getServerApiUrl = _chunkOQQ7CZWTjs.getServerApiUrl; exports.getServerAppUrl = _chunkOQQ7CZWTjs.getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = _chunkOQQ7CZWTjs.getServerTrackablePages; exports.invalidateCacheTag = _chunkZYSNJKOPjs.invalidateCacheTag; exports.invalidateCacheTags = _chunkZYSNJKOPjs.invalidateCacheTags; exports.serverRequest = _chunk6TUB7Y6Tjs.serverRequest; exports.updateCacheTag = _chunkZYSNJKOPjs.updateCacheTag;
|
|
80
80
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
invalidateCacheTag,
|
|
5
5
|
invalidateCacheTags,
|
|
6
6
|
updateCacheTag
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-OUEYZOLB.mjs";
|
|
8
8
|
import {
|
|
9
9
|
CACHE_PROFILES
|
|
10
10
|
} from "../chunk-T7SAW3UW.mjs";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
getServerApiUrl,
|
|
19
19
|
getServerAppUrl,
|
|
20
20
|
getServerTrackablePages
|
|
21
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-QIEF7HIL.mjs";
|
|
22
22
|
import {
|
|
23
23
|
serverRequest
|
|
24
24
|
} from "../chunk-E2LENTUH.mjs";
|
|
@@ -36,15 +36,15 @@ import {
|
|
|
36
36
|
RoleService,
|
|
37
37
|
S3Service,
|
|
38
38
|
UserService
|
|
39
|
-
} from "../chunk-
|
|
40
|
-
import "../chunk-
|
|
41
|
-
import "../chunk-
|
|
42
|
-
import "../chunk-
|
|
39
|
+
} from "../chunk-RFKSJIQB.mjs";
|
|
40
|
+
import "../chunk-UOTHRJD2.mjs";
|
|
41
|
+
import "../chunk-AUD6ZLBB.mjs";
|
|
42
|
+
import "../chunk-M3N7LPHX.mjs";
|
|
43
43
|
import {
|
|
44
44
|
DEFAULT_SQLITE_MODULE_URL,
|
|
45
45
|
OFFLINE_SQLITE_FILES
|
|
46
|
-
} from "../chunk-
|
|
47
|
-
import "../chunk-
|
|
46
|
+
} from "../chunk-4ZJIUTIV.mjs";
|
|
47
|
+
import "../chunk-MQ7TXALH.mjs";
|
|
48
48
|
import "../chunk-AUXK7QSA.mjs";
|
|
49
49
|
import "../chunk-PAWJFY3S.mjs";
|
|
50
50
|
export {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
var _chunkQGVVF77Ljs = require('./chunk-QGVVF77L.js');
|
|
4
|
+
require('./chunk-6RCZ64BQ.js');
|
|
5
|
+
require('./chunk-N5ZE2Z6I.js');
|
|
6
|
+
require('./chunk-KV4N7DEG.js');
|
|
7
|
+
require('./chunk-WCEYT3GG.js');
|
|
8
|
+
require('./chunk-WUNSG3IN.js');
|
|
9
|
+
require('./chunk-GM4VZTWL.js');
|
|
10
|
+
require('./chunk-LXKSUWAV.js');
|
|
11
|
+
require('./chunk-7QVYU63E.js');
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
exports.writeThrough = _chunkQGVVF77Ljs.writeThrough;
|
|
15
|
+
//# sourceMappingURL=write-through-BAPS24GL.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-BAPS24GL.js"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B,+BAA4B;AAC5B;AACE;AACF,qDAAC","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/write-through-BAPS24GL.js"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeThrough
|
|
3
|
+
} from "./chunk-KTGKW72G.mjs";
|
|
4
|
+
import "./chunk-4RPGFQBA.mjs";
|
|
5
|
+
import "./chunk-A3RB47X4.mjs";
|
|
6
|
+
import "./chunk-AUD6ZLBB.mjs";
|
|
7
|
+
import "./chunk-M3N7LPHX.mjs";
|
|
8
|
+
import "./chunk-4ZJIUTIV.mjs";
|
|
9
|
+
import "./chunk-MQ7TXALH.mjs";
|
|
10
|
+
import "./chunk-AUXK7QSA.mjs";
|
|
11
|
+
import "./chunk-PAWJFY3S.mjs";
|
|
12
|
+
export {
|
|
13
|
+
writeThrough
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=write-through-IWQKXYHE.mjs.map
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ const driverMock = vi.hoisted(() => {
|
|
|
20
20
|
movePrevious: vi.fn(),
|
|
21
21
|
moveTo: vi.fn(),
|
|
22
22
|
destroy: vi.fn(),
|
|
23
|
+
isActive: vi.fn(() => true),
|
|
23
24
|
};
|
|
24
25
|
const state: { config?: Hooks } = {};
|
|
25
26
|
const driver = vi.fn((config: Hooks) => {
|
|
@@ -53,7 +54,8 @@ describe("OnboardingProvider on driver.js", () => {
|
|
|
53
54
|
driverMock.driver.mockClear();
|
|
54
55
|
driverMock.instance.drive.mockClear();
|
|
55
56
|
driverMock.instance.destroy.mockClear();
|
|
56
|
-
driverMock.instance.moveNext.
|
|
57
|
+
driverMock.instance.moveNext.mockReset();
|
|
58
|
+
driverMock.instance.isActive.mockImplementation(() => true);
|
|
57
59
|
driverMock.state.config = undefined;
|
|
58
60
|
});
|
|
59
61
|
|
|
@@ -167,6 +169,45 @@ describe("OnboardingProvider on driver.js", () => {
|
|
|
167
169
|
expect(onTourEnd).toHaveBeenCalledTimes(1);
|
|
168
170
|
});
|
|
169
171
|
|
|
172
|
+
it("ends the tour when Next finds every remaining step's target missing", () => {
|
|
173
|
+
/* driver.js skips missing targets and, when none is left, destroys itself without
|
|
174
|
+
calling onDestroyed: without this the tour stayed "active" with no card on screen. */
|
|
175
|
+
const { wrapper, onTourEnd } = wrap();
|
|
176
|
+
const { result } = renderHook(() => useOnboarding(), { wrapper });
|
|
177
|
+
act(() => result.current.startTour("t", steps));
|
|
178
|
+
const config = driverMock.state.config!;
|
|
179
|
+
const popoverWrapper = document.createElement("div");
|
|
180
|
+
act(() => {
|
|
181
|
+
config.steps[0].onHighlightStarted?.();
|
|
182
|
+
config.steps[0].popover.onPopoverRender({ wrapper: popoverWrapper });
|
|
183
|
+
});
|
|
184
|
+
driverMock.instance.moveNext.mockImplementation(() => driverMock.instance.isActive.mockImplementation(() => false));
|
|
185
|
+
|
|
186
|
+
const next = Array.from(popoverWrapper.querySelectorAll("button")).find((b) => b.textContent === "Next")!;
|
|
187
|
+
act(() => next.click());
|
|
188
|
+
|
|
189
|
+
expect(driverMock.instance.moveNext).toHaveBeenCalled();
|
|
190
|
+
expect(result.current.isTourActive).toBe(false);
|
|
191
|
+
expect(result.current.activeTourId).toBeNull();
|
|
192
|
+
expect(onTourEnd).toHaveBeenCalledWith("t", "dismissed");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("keeps the tour when Next moves to a step that is shown", () => {
|
|
196
|
+
const { wrapper, onTourEnd } = wrap();
|
|
197
|
+
const { result } = renderHook(() => useOnboarding(), { wrapper });
|
|
198
|
+
act(() => result.current.startTour("t", steps));
|
|
199
|
+
const config = driverMock.state.config!;
|
|
200
|
+
const popoverWrapper = document.createElement("div");
|
|
201
|
+
act(() => {
|
|
202
|
+
config.steps[0].onHighlightStarted?.();
|
|
203
|
+
config.steps[0].popover.onPopoverRender({ wrapper: popoverWrapper });
|
|
204
|
+
});
|
|
205
|
+
const next = Array.from(popoverWrapper.querySelectorAll("button")).find((b) => b.textContent === "Next")!;
|
|
206
|
+
act(() => next.click());
|
|
207
|
+
expect(result.current.isTourActive).toBe(true);
|
|
208
|
+
expect(onTourEnd).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
170
211
|
it("reports 'dismissed' when driver is destroyed without a card action", () => {
|
|
171
212
|
const { wrapper, onTourEnd } = wrap();
|
|
172
213
|
const { result } = renderHook(() => useOnboarding(), { wrapper });
|