@carlonicora/nextjs-jsonapi 6.13.1 → 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.
Files changed (149) hide show
  1. package/dist/{BlockNoteEditor-UBVEUFNR.js → BlockNoteEditor-CUNH4LSQ.js} +25 -25
  2. package/dist/{BlockNoteEditor-UBVEUFNR.js.map → BlockNoteEditor-CUNH4LSQ.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-VXMDSQAA.mjs → BlockNoteEditor-QIVV2OB4.mjs} +10 -10
  4. package/dist/{BlockNoteViewer-2XYASA5Y.js → BlockNoteViewer-APBOPT4B.js} +8 -8
  5. package/dist/{BlockNoteViewer-2XYASA5Y.js.map → BlockNoteViewer-APBOPT4B.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-NBW2H6LC.mjs → BlockNoteViewer-HIUAJQRI.mjs} +7 -7
  7. package/dist/JsonApiRequest-G4J2WTSU.js +26 -0
  8. package/dist/{JsonApiRequest-WISJZSDE.js.map → JsonApiRequest-G4J2WTSU.js.map} +1 -1
  9. package/dist/{JsonApiRequest-F6TMJ42G.mjs → JsonApiRequest-IKKHWCPB.mjs} +2 -2
  10. package/dist/billing/index.js +497 -497
  11. package/dist/billing/index.mjs +9 -9
  12. package/dist/{chunk-7DBJTNSU.mjs → chunk-4RPGFQBA.mjs} +2 -2
  13. package/dist/{chunk-BZ5J2FIV.js → chunk-4XLDBOSE.js} +11 -11
  14. package/dist/{chunk-BZ5J2FIV.js.map → chunk-4XLDBOSE.js.map} +1 -1
  15. package/dist/{chunk-27LQDGNP.mjs → chunk-4ZJIUTIV.mjs} +6 -1
  16. package/dist/chunk-4ZJIUTIV.mjs.map +1 -0
  17. package/dist/{chunk-PXDUJ47N.js → chunk-6RCZ64BQ.js} +18 -18
  18. package/dist/{chunk-PXDUJ47N.js.map → chunk-6RCZ64BQ.js.map} +1 -1
  19. package/dist/{chunk-7JZHPMGW.mjs → chunk-A3RB47X4.mjs} +44 -20
  20. package/dist/chunk-A3RB47X4.mjs.map +1 -0
  21. package/dist/{chunk-DOCIFE6A.mjs → chunk-AUD6ZLBB.mjs} +3 -3
  22. package/dist/{chunk-GB6DBFYY.js → chunk-DGN3PR2F.js} +3 -3
  23. package/dist/{chunk-GB6DBFYY.js.map → chunk-DGN3PR2F.js.map} +1 -1
  24. package/dist/{chunk-7ZHTPFFT.js → chunk-DJXSCARG.js} +15 -15
  25. package/dist/{chunk-7ZHTPFFT.js.map → chunk-DJXSCARG.js.map} +1 -1
  26. package/dist/{chunk-QKNGZDSQ.js → chunk-GM4VZTWL.js} +2 -2
  27. package/dist/{chunk-QKNGZDSQ.js.map → chunk-GM4VZTWL.js.map} +1 -1
  28. package/dist/{chunk-PYRZCKC2.js → chunk-GSRTVIIK.js} +923 -923
  29. package/dist/{chunk-PYRZCKC2.js.map → chunk-GSRTVIIK.js.map} +1 -1
  30. package/dist/{chunk-47BBY5KA.mjs → chunk-HAW5PNJG.mjs} +4 -4
  31. package/dist/{chunk-YRKIFYOE.mjs → chunk-KTGKW72G.mjs} +27 -8
  32. package/dist/chunk-KTGKW72G.mjs.map +1 -0
  33. package/dist/{chunk-PV4Z5W6V.js → chunk-KV4N7DEG.js} +19 -19
  34. package/dist/{chunk-PV4Z5W6V.js.map → chunk-KV4N7DEG.js.map} +1 -1
  35. package/dist/{chunk-ZLTOUZN2.mjs → chunk-M3N7LPHX.mjs} +4 -4
  36. package/dist/{chunk-DGWOVYEC.mjs → chunk-MQ7TXALH.mjs} +2 -2
  37. package/dist/{chunk-W62MHDWM.js → chunk-N5ZE2Z6I.js} +58 -34
  38. package/dist/chunk-N5ZE2Z6I.js.map +1 -0
  39. package/dist/{chunk-2SV4SS6N.js → chunk-OQQ7CZWT.js} +12 -12
  40. package/dist/{chunk-2SV4SS6N.js.map → chunk-OQQ7CZWT.js.map} +1 -1
  41. package/dist/{chunk-JJMMT24D.mjs → chunk-OUEYZOLB.mjs} +2 -2
  42. package/dist/{chunk-XXP3TQHF.mjs → chunk-OY65RYD3.mjs} +2 -2
  43. package/dist/{chunk-MOGEGWXW.js → chunk-PMESKNKL.js} +868 -868
  44. package/dist/{chunk-MOGEGWXW.js.map → chunk-PMESKNKL.js.map} +1 -1
  45. package/dist/{chunk-B6JGKDP2.js → chunk-QGVVF77L.js} +48 -29
  46. package/dist/chunk-QGVVF77L.js.map +1 -0
  47. package/dist/{chunk-BH6JWIKI.mjs → chunk-QIEF7HIL.mjs} +2 -2
  48. package/dist/{chunk-P445QGOC.mjs → chunk-RFKSJIQB.mjs} +7 -7
  49. package/dist/{chunk-6JUCIWDC.mjs → chunk-UOTHRJD2.mjs} +2 -2
  50. package/dist/{chunk-KMW7UNL2.js → chunk-WCEYT3GG.js} +8 -8
  51. package/dist/{chunk-KMW7UNL2.js.map → chunk-WCEYT3GG.js.map} +1 -1
  52. package/dist/{chunk-MHB36MYE.js → chunk-WUNSG3IN.js} +7 -2
  53. package/dist/chunk-WUNSG3IN.js.map +1 -0
  54. package/dist/{chunk-N4RZZKL5.mjs → chunk-YKNULEHB.mjs} +12 -12
  55. package/dist/{chunk-Z4POQWFG.js → chunk-ZYSNJKOP.js} +4 -4
  56. package/dist/{chunk-Z4POQWFG.js.map → chunk-ZYSNJKOP.js.map} +1 -1
  57. package/dist/client/index.js +10 -10
  58. package/dist/client/index.mjs +9 -9
  59. package/dist/components/index.js +10 -10
  60. package/dist/components/index.mjs +9 -9
  61. package/dist/contexts/index.js +10 -10
  62. package/dist/contexts/index.mjs +9 -9
  63. package/dist/core/index.js +7 -7
  64. package/dist/core/index.mjs +6 -6
  65. package/dist/email-automations/index.js +99 -99
  66. package/dist/email-automations/index.mjs +9 -9
  67. package/dist/features/analytics/index.js +126 -126
  68. package/dist/features/analytics/index.mjs +9 -9
  69. package/dist/features/analytics/server.js +10 -10
  70. package/dist/features/analytics/server.mjs +8 -8
  71. package/dist/features/blog/index.js +156 -156
  72. package/dist/features/blog/index.mjs +9 -9
  73. package/dist/features/blog/server.js +28 -28
  74. package/dist/features/blog/server.mjs +7 -7
  75. package/dist/features/help/index.js +43 -43
  76. package/dist/features/help/index.mjs +9 -9
  77. package/dist/features/tokenusage/index.js +132 -132
  78. package/dist/features/tokenusage/index.mjs +9 -9
  79. package/dist/index.js +8 -8
  80. package/dist/index.mjs +7 -7
  81. package/dist/{lifecycle-LKGPRBS7.mjs → lifecycle-KI3PCJSE.mjs} +3 -3
  82. package/dist/lifecycle-TO65I7TO.js +23 -0
  83. package/dist/{lifecycle-4GUOTB46.js.map → lifecycle-TO65I7TO.js.map} +1 -1
  84. package/dist/local-transport-4YDJ6FQG.js +15 -0
  85. package/dist/{local-transport-7H3SPZSA.js.map → local-transport-4YDJ6FQG.js.map} +1 -1
  86. package/dist/local-transport-C7YTA4I7.mjs +15 -0
  87. package/dist/offline/index.d.mts +160 -4
  88. package/dist/offline/index.d.ts +160 -4
  89. package/dist/offline/index.js +210 -163
  90. package/dist/offline/index.js.map +1 -1
  91. package/dist/offline/index.mjs +76 -29
  92. package/dist/offline/index.mjs.map +1 -1
  93. package/dist/request-66FGJIHB.mjs +10 -0
  94. package/dist/request-6W6UGKJV.js +10 -0
  95. package/dist/{request-KTBV6HHH.js.map → request-6W6UGKJV.js.map} +1 -1
  96. package/dist/server/index.js +9 -9
  97. package/dist/server/index.mjs +8 -8
  98. package/dist/write-through-BAPS24GL.js +15 -0
  99. package/dist/{write-through-FDQ7N4YI.js.map → write-through-BAPS24GL.js.map} +1 -1
  100. package/dist/write-through-IWQKXYHE.mjs +15 -0
  101. package/package.json +1 -1
  102. package/src/offline/__tests__/generic-answers.spec.ts +146 -3
  103. package/src/offline/__tests__/local-transport.spec.ts +15 -1
  104. package/src/offline/__tests__/offline-fixtures.ts +29 -2
  105. package/src/offline/__tests__/write-through.spec.ts +27 -1
  106. package/src/offline/config.ts +9 -0
  107. package/src/offline/generic-answers.ts +44 -17
  108. package/src/offline/index.ts +2 -0
  109. package/src/offline/local-transport.ts +5 -3
  110. package/src/offline/sync/__tests__/runtime.spec.ts +30 -1
  111. package/src/offline/sync/__tests__/sync-fixtures.ts +22 -0
  112. package/src/offline/sync/__tests__/sync-root.spec.ts +184 -1
  113. package/src/offline/sync/known-roots.ts +29 -7
  114. package/src/offline/sync/runtime.ts +3 -2
  115. package/src/offline/sync/sync-root.ts +70 -19
  116. package/src/offline/types.ts +39 -1
  117. package/src/offline/write-through.ts +6 -3
  118. package/dist/JsonApiRequest-WISJZSDE.js +0 -26
  119. package/dist/chunk-27LQDGNP.mjs.map +0 -1
  120. package/dist/chunk-7JZHPMGW.mjs.map +0 -1
  121. package/dist/chunk-B6JGKDP2.js.map +0 -1
  122. package/dist/chunk-MHB36MYE.js.map +0 -1
  123. package/dist/chunk-W62MHDWM.js.map +0 -1
  124. package/dist/chunk-YRKIFYOE.mjs.map +0 -1
  125. package/dist/lifecycle-4GUOTB46.js +0 -23
  126. package/dist/local-transport-7H3SPZSA.js +0 -15
  127. package/dist/local-transport-QYFKA6AN.mjs +0 -15
  128. package/dist/request-KTBV6HHH.js +0 -10
  129. package/dist/request-SQOUNWZ4.mjs +0 -10
  130. package/dist/write-through-FDQ7N4YI.js +0 -15
  131. package/dist/write-through-R25VQMH6.mjs +0 -15
  132. /package/dist/{BlockNoteEditor-VXMDSQAA.mjs.map → BlockNoteEditor-QIVV2OB4.mjs.map} +0 -0
  133. /package/dist/{BlockNoteViewer-NBW2H6LC.mjs.map → BlockNoteViewer-HIUAJQRI.mjs.map} +0 -0
  134. /package/dist/{JsonApiRequest-F6TMJ42G.mjs.map → JsonApiRequest-IKKHWCPB.mjs.map} +0 -0
  135. /package/dist/{chunk-7DBJTNSU.mjs.map → chunk-4RPGFQBA.mjs.map} +0 -0
  136. /package/dist/{chunk-DOCIFE6A.mjs.map → chunk-AUD6ZLBB.mjs.map} +0 -0
  137. /package/dist/{chunk-47BBY5KA.mjs.map → chunk-HAW5PNJG.mjs.map} +0 -0
  138. /package/dist/{chunk-ZLTOUZN2.mjs.map → chunk-M3N7LPHX.mjs.map} +0 -0
  139. /package/dist/{chunk-DGWOVYEC.mjs.map → chunk-MQ7TXALH.mjs.map} +0 -0
  140. /package/dist/{chunk-JJMMT24D.mjs.map → chunk-OUEYZOLB.mjs.map} +0 -0
  141. /package/dist/{chunk-XXP3TQHF.mjs.map → chunk-OY65RYD3.mjs.map} +0 -0
  142. /package/dist/{chunk-BH6JWIKI.mjs.map → chunk-QIEF7HIL.mjs.map} +0 -0
  143. /package/dist/{chunk-P445QGOC.mjs.map → chunk-RFKSJIQB.mjs.map} +0 -0
  144. /package/dist/{chunk-6JUCIWDC.mjs.map → chunk-UOTHRJD2.mjs.map} +0 -0
  145. /package/dist/{chunk-N4RZZKL5.mjs.map → chunk-YKNULEHB.mjs.map} +0 -0
  146. /package/dist/{lifecycle-LKGPRBS7.mjs.map → lifecycle-KI3PCJSE.mjs.map} +0 -0
  147. /package/dist/{local-transport-QYFKA6AN.mjs.map → local-transport-C7YTA4I7.mjs.map} +0 -0
  148. /package/dist/{request-SQOUNWZ4.mjs.map → request-66FGJIHB.mjs.map} +0 -0
  149. /package/dist/{write-through-R25VQMH6.mjs.map → write-through-IWQKXYHE.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-PXDUJ47N.js","../src/offline/sync/artwork-cache.ts","../src/offline/store/schema.ts","../src/offline/store/sql-device-store.ts","../src/offline/store/shared-database.ts","../src/offline/store/worker-client.ts","../src/offline/store/lifecycle.ts"],"names":["newId"],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-PXDUJ47N.js","sourcesContent":[null,"// Artwork images for the device copy, kept in Cache Storage under the exact URL the stored\n// artwork record holds (`attributes.imageUrl`). The service worker answers those URLs from the\n// same cache when the network fails. The image downloads are plain GETs of the URLs the API\n// already returned (decision recorded in the plan).\n\n// The service worker names its caches `${appId}-images` and `${appId}-offline-${revision}`\n// (pwa/sw.js). The page does not load sw-config.js, so it finds the app id from those names;\n// Cache Storage is per origin, so only this app's caches are listed.\nconst IMAGES_CACHE_SUFFIX = \"-images\";\nconst OFFLINE_CACHE_MARKER = \"-offline-\";\n\nasync function imagesCacheName(): Promise<string | undefined> {\n if (typeof caches === \"undefined\") return undefined;\n const keys = await caches.keys();\n const existing = keys.find((key) => key.endsWith(IMAGES_CACHE_SUFFIX));\n if (existing) return existing;\n\n const offline = keys.find((key) => key.includes(OFFLINE_CACHE_MARKER));\n if (!offline) return undefined;\n return `${offline.slice(0, offline.indexOf(OFFLINE_CACHE_MARKER))}${IMAGES_CACHE_SUFFIX}`;\n}\n\n/** The image URL a stored resource carries, if any. */\nexport function imageUrlOf(resource: any): string | undefined {\n const url = resource?.attributes?.imageUrl;\n return typeof url === \"string\" && url.length > 0 ? url : undefined;\n}\n\n/**\n * Makes sure `url` is in the images cache: downloaded when missing. When `previousUrl` is a\n * different URL of the same artwork, its entry is removed. A failed download is logged and\n * retried at the next sync.\n */\nexport async function cacheArtworkImage(url: string, previousUrl?: string): Promise<void> {\n const name = await imagesCacheName();\n if (!name) return;\n const cache = await caches.open(name);\n\n if (previousUrl && previousUrl !== url) await cache.delete(previousUrl);\n if (await cache.match(url)) return;\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n console.warn(\"[offline] could not download an artwork image\", url, response.status);\n return;\n }\n await cache.put(url, response);\n } catch (error) {\n console.warn(\"[offline] could not download an artwork image\", url, error);\n }\n}\n\n/** Removes the cached copy of `url`. */\nexport async function removeArtworkImage(url: string): Promise<void> {\n const name = await imagesCacheName();\n if (!name) return;\n const cache = await caches.open(name);\n await cache.delete(url);\n}\n\n/** Deletes this app's whole images cache (logout, or another user signing in). */\nexport async function deleteImagesCaches(): Promise<void> {\n if (typeof caches === \"undefined\") return;\n const keys = await caches.keys();\n await Promise.all(keys.filter((key) => key.endsWith(IMAGES_CACHE_SUFFIX)).map((key) => caches.delete(key)));\n}\n","/**\n * Device-copy schema (spec § 1). Every statement is idempotent so it runs on\n * every open; `user_version` records the layout. A file of an older layout is\n * recreated (pre-release: no migration), see `DEVICE_STORE_RESET`.\n *\n * 2: `record_campaigns` (a shared record, e.g. an artwork or a template, belongs\n * to every root record that uses it) and `queue.package_id`.\n * `skipped_records` was added later inside layout 2: `CREATE TABLE IF NOT EXISTS` adds it to\n * an existing file without a reset (a reset would drop unsent changes).\n * 3: campaign columns and tables renamed to root (`campaign_id` -> `root_id`,\n * `record_campaigns` -> `record_roots`).\n */\nexport const DEVICE_STORE_SCHEMA_VERSION = 3;\n\n/**\n * Drops every table of an older layout before the schema is created again. Layout 2's\n * `record_campaigns` is dropped too, so a layout-2 file is fully recreated.\n */\nexport const DEVICE_STORE_RESET: string[] = [\n `DROP TABLE IF EXISTS records`,\n `DROP TABLE IF EXISTS record_campaigns`,\n `DROP TABLE IF EXISTS record_roots`,\n `DROP TABLE IF EXISTS links`,\n `DROP TABLE IF EXISTS queue`,\n `DROP TABLE IF EXISTS conflicts`,\n `DROP TABLE IF EXISTS sync_state`,\n `DROP TABLE IF EXISTS skipped_records`,\n];\n\nexport const DEVICE_STORE_SCHEMA: string[] = [\n `CREATE TABLE IF NOT EXISTS records (\n type TEXT NOT NULL,\n id TEXT NOT NULL,\n root_id TEXT NOT NULL,\n resource TEXT NOT NULL,\n updated_at TEXT,\n base TEXT,\n local_only INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (type, id)\n )`,\n `CREATE INDEX IF NOT EXISTS records_type_root ON records (type, root_id)`,\n `CREATE TABLE IF NOT EXISTS record_roots (\n type TEXT NOT NULL,\n id TEXT NOT NULL,\n root_id TEXT NOT NULL,\n PRIMARY KEY (type, id, root_id)\n )`,\n `CREATE INDEX IF NOT EXISTS record_roots_root ON record_roots (root_id, type)`,\n `CREATE TABLE IF NOT EXISTS links (\n from_id TEXT NOT NULL,\n from_type TEXT NOT NULL,\n to_id TEXT NOT NULL,\n to_type TEXT NOT NULL,\n kind TEXT NOT NULL,\n props TEXT NOT NULL DEFAULT '{}',\n root_id TEXT,\n PRIMARY KEY (from_id, to_id, kind)\n )`,\n `CREATE INDEX IF NOT EXISTS links_from ON links (from_id)`,\n `CREATE INDEX IF NOT EXISTS links_to ON links (to_id)`,\n `CREATE INDEX IF NOT EXISTS links_root ON links (root_id)`,\n `CREATE TABLE IF NOT EXISTS queue (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n root_id TEXT NOT NULL,\n request TEXT NOT NULL,\n base TEXT,\n created_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n package_id TEXT\n )`,\n `CREATE INDEX IF NOT EXISTS queue_status_seq ON queue (status, seq)`,\n `CREATE TABLE IF NOT EXISTS conflicts (\n id TEXT PRIMARY KEY,\n queue_seq INTEGER NOT NULL,\n root_id TEXT,\n kind TEXT NOT NULL,\n record_type TEXT,\n record_id TEXT,\n mine TEXT,\n theirs TEXT,\n base TEXT,\n merged TEXT,\n fields TEXT NOT NULL DEFAULT '[]',\n blocks TEXT,\n reason TEXT\n )`,\n `CREATE TABLE IF NOT EXISTS sync_state (\n root_id TEXT PRIMARY KEY,\n server_time TEXT,\n locked INTEGER NOT NULL DEFAULT 0,\n locked_from TEXT\n )`,\n `CREATE TABLE IF NOT EXISTS skipped_records (\n root_id TEXT NOT NULL,\n type TEXT NOT NULL,\n id TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (root_id, type, id)\n )`,\n `PRAGMA user_version = ${DEVICE_STORE_SCHEMA_VERSION}`,\n];\n","import type {\n Conflict,\n DeviceStore,\n QueueEntry,\n QueueStatus,\n SkippedRecord,\n StoredLink,\n StoredRecord,\n SyncState,\n} from \"./DeviceStore\";\nimport { DEVICE_STORE_RESET, DEVICE_STORE_SCHEMA, DEVICE_STORE_SCHEMA_VERSION } from \"./schema\";\n\n/** Runs one SQL statement with positional `?` binds and answers its rows as objects. */\nexport type ExecFn = (sql: string, bind?: unknown[]) => Promise<Record<string, unknown>[]>;\n\nexport type SqlDeviceStoreHooks = {\n /** Runs before a queue entry is written (the lifecycle marks the file as holding unsent changes). */\n beforeEnqueue?: () => Promise<void>;\n /** Runs after a committed change that can shrink the queue, with the number of entries left. */\n afterQueueChange?: (entries: number) => Promise<void>;\n /**\n * Runs each operation with exclusive use of the database across pages (the shared device copy:\n * several pages send statements to one database, and a transaction must not interleave).\n */\n exclusive?: <T>(fn: () => Promise<T>) => Promise<T>;\n};\n\nconst toJson = (value: unknown): string | null =>\n value === undefined || value === null ? null : JSON.stringify(value);\nconst fromJson = (value: unknown): any => (value === null || value === undefined ? null : JSON.parse(String(value)));\nconst toIso = (value: Date | null | undefined): string | null => (value ? value.toISOString() : null);\nconst fromIso = (value: unknown): Date | null =>\n value === null || value === undefined ? null : new Date(String(value));\n\nconst newId = (): string =>\n typeof globalThis.crypto?.randomUUID === \"function\"\n ? globalThis.crypto.randomUUID()\n : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n\nconst rowToRecord = (row: Record<string, unknown>): StoredRecord => ({\n type: String(row.type),\n id: String(row.id),\n rootId: String(row.root_id),\n resource: fromJson(row.resource),\n updatedAt: fromIso(row.updated_at),\n base: fromJson(row.base),\n localOnly: Number(row.local_only) === 1,\n});\n\nconst rowToLink = (row: Record<string, unknown>): StoredLink => ({\n fromType: String(row.from_type),\n fromId: String(row.from_id),\n toType: String(row.to_type),\n toId: String(row.to_id),\n kind: String(row.kind),\n props: fromJson(row.props) ?? {},\n});\n\nconst rowToQueueEntry = (row: Record<string, unknown>): QueueEntry => {\n const request = fromJson(row.request) ?? {};\n const entry: QueueEntry = {\n seq: Number(row.seq),\n rootId: String(row.root_id),\n method: request.method,\n path: request.path,\n base: fromJson(row.base),\n createdAt: new Date(String(row.created_at)),\n status: String(row.status) as QueueStatus,\n };\n if (request.body !== undefined) entry.body = request.body;\n if (request.recordType !== undefined) entry.recordType = request.recordType;\n if (request.recordId !== undefined) entry.recordId = request.recordId;\n if (request.resolution !== undefined) entry.resolution = request.resolution;\n if (row.package_id !== null && row.package_id !== undefined) entry.packageId = String(row.package_id);\n return entry;\n};\n\nconst rowToConflict = (row: Record<string, unknown>): Conflict => {\n const conflict: Conflict = {\n id: String(row.id),\n queueSeq: Number(row.queue_seq),\n kind: String(row.kind) as Conflict[\"kind\"],\n mine: fromJson(row.mine),\n theirs: fromJson(row.theirs),\n base: fromJson(row.base),\n fields: fromJson(row.fields) ?? [],\n };\n if (row.record_type !== null && row.record_type !== undefined) conflict.recordType = String(row.record_type);\n if (row.record_id !== null && row.record_id !== undefined) conflict.recordId = String(row.record_id);\n const merged = fromJson(row.merged);\n if (merged !== null) conflict.merged = merged;\n const blocks = fromJson(row.blocks);\n if (blocks !== null) conflict.blocks = blocks;\n if (row.reason !== null && row.reason !== undefined) conflict.reason = String(row.reason);\n return conflict;\n};\n\n/** The statements of every store operation, run over an exec that is already serialised. */\nconst createOperations = (exec: ExecFn, hooks: SqlDeviceStoreHooks) => {\n const insertLink = async (link: StoredLink, rootId: string | null) => {\n await exec(\n `INSERT OR REPLACE INTO links (from_id, from_type, to_id, to_type, kind, props, root_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [link.fromId, link.fromType, link.toId, link.toType, link.kind, JSON.stringify(link.props ?? {}), rootId],\n );\n };\n\n const deleteRecordEverywhere = async (type: string, id: string) => {\n await exec(`DELETE FROM records WHERE type = ? AND id = ?`, [type, id]);\n await exec(`DELETE FROM record_roots WHERE type = ? AND id = ?`, [type, id]);\n await exec(`DELETE FROM links WHERE from_id = ? OR to_id = ?`, [id, id]);\n };\n\n return {\n async getRecord(type: string, id: string): Promise<StoredRecord | undefined> {\n const rows = await exec(`SELECT * FROM records WHERE type = ? AND id = ?`, [type, id]);\n return rows[0] ? rowToRecord(rows[0]) : undefined;\n },\n\n async listRecords(filter: { type: string; rootId?: string; ids?: string[] }): Promise<StoredRecord[]> {\n if (filter.ids && filter.ids.length === 0) return [];\n const where = [\"type = ?\"];\n const bind: unknown[] = [filter.type];\n if (filter.rootId !== undefined) {\n where.push(\n \"EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id AND rc.root_id = ?)\",\n );\n bind.push(filter.rootId);\n }\n if (filter.ids) {\n where.push(`id IN (${filter.ids.map(() => \"?\").join(\", \")})`);\n bind.push(...filter.ids);\n }\n const rows = await exec(`SELECT * FROM records WHERE ${where.join(\" AND \")} ORDER BY rowid`, bind);\n return rows.map(rowToRecord);\n },\n\n async putRecord(\n record: Omit<StoredRecord, \"base\" | \"localOnly\"> & { base?: any | null; localOnly?: boolean },\n ): Promise<void> {\n // A record already stored keeps its first root record; the new one is added to its memberships.\n await exec(\n `INSERT INTO records (type, id, root_id, resource, updated_at, base, local_only) VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (type, id) DO UPDATE SET resource = excluded.resource, updated_at = excluded.updated_at,\n base = excluded.base, local_only = excluded.local_only`,\n [\n record.type,\n record.id,\n record.rootId,\n JSON.stringify(record.resource),\n toIso(record.updatedAt),\n toJson(record.base),\n record.localOnly ? 1 : 0,\n ],\n );\n await exec(`INSERT OR IGNORE INTO record_roots (type, id, root_id) VALUES (?, ?, ?)`, [\n record.type,\n record.id,\n record.rootId,\n ]);\n },\n\n async deleteRecord(type: string, id: string): Promise<void> {\n await deleteRecordEverywhere(type, id);\n },\n\n async removeRecordFromRoot(type: string, id: string, rootId: string): Promise<void> {\n await exec(`DELETE FROM record_roots WHERE type = ? AND id = ? AND root_id = ?`, [type, id, rootId]);\n const left = await exec(`SELECT root_id FROM record_roots WHERE type = ? AND id = ? ORDER BY root_id`, [\n type,\n id,\n ]);\n if (left.length === 0) {\n await deleteRecordEverywhere(type, id);\n return;\n }\n await exec(`UPDATE records SET root_id = ? WHERE type = ? AND id = ? AND root_id = ?`, [\n String(left[0].root_id),\n type,\n id,\n rootId,\n ]);\n },\n\n async linksFrom(fromId: string, kind?: string): Promise<StoredLink[]> {\n const rows =\n kind === undefined\n ? await exec(`SELECT * FROM links WHERE from_id = ? ORDER BY rowid`, [fromId])\n : await exec(`SELECT * FROM links WHERE from_id = ? AND kind = ? ORDER BY rowid`, [fromId, kind]);\n return rows.map(rowToLink);\n },\n\n async linksTo(toId: string, kind?: string): Promise<StoredLink[]> {\n const rows =\n kind === undefined\n ? await exec(`SELECT * FROM links WHERE to_id = ? ORDER BY rowid`, [toId])\n : await exec(`SELECT * FROM links WHERE to_id = ? AND kind = ? ORDER BY rowid`, [toId, kind]);\n return rows.map(rowToLink);\n },\n\n async replaceRootLinks(rootId: string, links: StoredLink[]): Promise<void> {\n await exec(`DELETE FROM links WHERE root_id = ?`, [rootId]);\n for (const link of links) await insertLink(link, rootId);\n },\n\n async putLink(link: StoredLink): Promise<void> {\n // A link belongs to the root record of the record at either end, so a later\n // replaceRootLinks / removeRoot of that root record reaches it.\n const owner = await exec(`SELECT root_id FROM records WHERE id IN (?, ?) ORDER BY id = ? DESC LIMIT 1`, [\n link.fromId,\n link.toId,\n link.fromId,\n ]);\n await insertLink(link, owner[0] ? String(owner[0].root_id) : null);\n },\n\n async deleteLink(link: Pick<StoredLink, \"fromId\" | \"toId\" | \"kind\">): Promise<void> {\n await exec(`DELETE FROM links WHERE from_id = ? AND to_id = ? AND kind = ?`, [link.fromId, link.toId, link.kind]);\n },\n\n async enqueue(entry: Omit<QueueEntry, \"seq\" | \"createdAt\" | \"status\">): Promise<number> {\n if (hooks.beforeEnqueue) await hooks.beforeEnqueue();\n const request = {\n method: entry.method,\n path: entry.path,\n body: entry.body,\n recordType: entry.recordType,\n recordId: entry.recordId,\n resolution: entry.resolution,\n };\n const rows = await exec(\n `INSERT INTO queue (root_id, request, base, created_at, status) VALUES (?, ?, ?, ?, 'pending') RETURNING seq`,\n [entry.rootId, JSON.stringify(request), toJson(entry.base), new Date().toISOString()],\n );\n return Number(rows[0].seq);\n },\n\n async queue(filter?: { status?: QueueStatus }): Promise<QueueEntry[]> {\n const rows = filter?.status\n ? await exec(`SELECT * FROM queue WHERE status = ? ORDER BY seq`, [filter.status])\n : await exec(`SELECT * FROM queue ORDER BY seq`);\n return rows.map(rowToQueueEntry);\n },\n\n async setQueueStatus(seq: number, status: QueueStatus): Promise<void> {\n await exec(`UPDATE queue SET status = ? WHERE seq = ?`, [status, seq]);\n },\n\n async setQueuePackage(seqs: number[], packageId: string | null): Promise<void> {\n if (seqs.length === 0) return;\n await exec(`UPDATE queue SET package_id = ? WHERE seq IN (${seqs.map(() => \"?\").join(\", \")})`, [\n packageId,\n ...seqs,\n ]);\n },\n\n async removeQueueEntry(seq: number): Promise<void> {\n await exec(`DELETE FROM queue WHERE seq = ?`, [seq]);\n },\n\n async addConflict(c: Omit<Conflict, \"id\">): Promise<string> {\n const id = newId();\n const owner = await exec(`SELECT root_id FROM queue WHERE seq = ?`, [c.queueSeq]);\n await exec(\n `INSERT INTO conflicts (id, queue_seq, root_id, kind, record_type, record_id, mine, theirs, base, merged, fields, blocks, reason)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id,\n c.queueSeq,\n owner[0] ? String(owner[0].root_id) : null,\n c.kind,\n c.recordType ?? null,\n c.recordId ?? null,\n toJson(c.mine),\n toJson(c.theirs),\n toJson(c.base),\n toJson(c.merged),\n JSON.stringify(c.fields ?? []),\n toJson(c.blocks),\n c.reason ?? null,\n ],\n );\n return id;\n },\n\n async conflicts(): Promise<Conflict[]> {\n const rows = await exec(`SELECT * FROM conflicts ORDER BY queue_seq, rowid`);\n return rows.map(rowToConflict);\n },\n\n async removeConflict(id: string): Promise<void> {\n await exec(`DELETE FROM conflicts WHERE id = ?`, [id]);\n },\n\n async getSyncState(rootId: string): Promise<SyncState> {\n const rows = await exec(`SELECT * FROM sync_state WHERE root_id = ?`, [rootId]);\n if (!rows[0]) return { serverTime: null, locked: false, lockedFrom: null };\n return {\n serverTime: fromIso(rows[0].server_time),\n locked: Number(rows[0].locked) === 1,\n lockedFrom: fromIso(rows[0].locked_from),\n };\n },\n\n async setSyncState(rootId: string, s: SyncState): Promise<void> {\n await exec(`INSERT OR REPLACE INTO sync_state (root_id, server_time, locked, locked_from) VALUES (?, ?, ?, ?)`, [\n rootId,\n toIso(s.serverTime),\n s.locked ? 1 : 0,\n toIso(s.lockedFrom),\n ]);\n },\n\n async skippedRecords(rootId: string): Promise<SkippedRecord[]> {\n const rows = await exec(`SELECT * FROM skipped_records WHERE root_id = ? ORDER BY type, id`, [rootId]);\n return rows.map((row) => ({\n type: String(row.type),\n id: String(row.id),\n updatedAt: new Date(String(row.updated_at)),\n }));\n },\n\n async setSkippedRecords(rootId: string, records: SkippedRecord[]): Promise<void> {\n await exec(`DELETE FROM skipped_records WHERE root_id = ?`, [rootId]);\n for (const record of records) {\n await exec(`INSERT OR REPLACE INTO skipped_records (root_id, type, id, updated_at) VALUES (?, ?, ?, ?)`, [\n rootId,\n record.type,\n record.id,\n record.updatedAt.toISOString(),\n ]);\n }\n },\n\n async removeRoot(rootId: string): Promise<void> {\n // Waiting changes are never dropped: a root record with queue entries is kept whole.\n const waiting = await exec(`SELECT COUNT(*) AS n FROM queue WHERE root_id = ?`, [rootId]);\n if (Number(waiting[0]?.n ?? 0) > 0) return;\n // A shared record another root record still uses stays for that root record; every other\n // record of this root record goes, with its links.\n await exec(`DELETE FROM links WHERE root_id = ?`, [rootId]);\n await exec(`DELETE FROM record_roots WHERE root_id = ?`, [rootId]);\n const orphans = `SELECT id FROM records r WHERE NOT EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = r.type AND rc.id = r.id)`;\n await exec(`DELETE FROM links WHERE from_id IN (${orphans}) OR to_id IN (${orphans})`);\n await exec(\n `DELETE FROM records WHERE NOT EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id)`,\n );\n await exec(\n `UPDATE records SET root_id = (SELECT MIN(rc.root_id) FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id) WHERE root_id = ?`,\n [rootId],\n );\n await exec(`DELETE FROM conflicts WHERE root_id = ?`, [rootId]);\n await exec(`DELETE FROM sync_state WHERE root_id = ?`, [rootId]);\n await exec(`DELETE FROM skipped_records WHERE root_id = ?`, [rootId]);\n },\n\n async queueSize(): Promise<number> {\n const rows = await exec(`SELECT COUNT(*) AS n FROM queue`);\n return Number(rows[0]?.n ?? 0);\n },\n };\n};\n\ntype Operations = ReturnType<typeof createOperations>;\n\n/** A store whose `transaction` runs `fn` directly: used inside an open transaction. */\nconst nestedStore = (ops: Operations): DeviceStore => {\n const { queueSize: _queueSize, ...rest } = ops;\n const store: DeviceStore = { ...rest, transaction: (fn) => fn(store) };\n return store;\n};\n\n/**\n * The device store over any SQLite `exec` (the worker's OPFS file, or an\n * in-memory database in tests). Every operation is serialised, so a\n * transaction never interleaves with another caller's statements.\n */\nexport async function createSqlDeviceStore(exec: ExecFn, hooks: SqlDeviceStoreHooks = {}): Promise<DeviceStore> {\n const exclusive = hooks.exclusive ?? (<T>(fn: () => Promise<T>) => fn());\n\n // A file of an older layout is recreated (pre-release: no migration of device files).\n await exclusive(async () => {\n const version = Number((await exec(`PRAGMA user_version`))[0]?.user_version ?? 0);\n if (version > 0 && version < DEVICE_STORE_SCHEMA_VERSION) {\n for (const statement of DEVICE_STORE_RESET) await exec(statement);\n }\n for (const statement of DEVICE_STORE_SCHEMA) await exec(statement);\n });\n\n const ops = createOperations(exec, hooks);\n let tail: Promise<unknown> = Promise.resolve();\n const serialised = <T>(fn: () => Promise<T>): Promise<T> => {\n const run = tail.then(\n () => exclusive(fn),\n () => exclusive(fn),\n );\n tail = run.catch(() => undefined);\n return run;\n };\n const settleQueue = async () => {\n if (hooks.afterQueueChange) await hooks.afterQueueChange(await ops.queueSize());\n };\n\n const store: DeviceStore = {\n getRecord: (type, id) => serialised(() => ops.getRecord(type, id)),\n listRecords: (filter) => serialised(() => ops.listRecords(filter)),\n putRecord: (record) => serialised(() => ops.putRecord(record)),\n deleteRecord: (type, id) => serialised(() => ops.deleteRecord(type, id)),\n removeRecordFromRoot: (type, id, rootId) => serialised(() => ops.removeRecordFromRoot(type, id, rootId)),\n linksFrom: (fromId, kind) => serialised(() => ops.linksFrom(fromId, kind)),\n linksTo: (toId, kind) => serialised(() => ops.linksTo(toId, kind)),\n replaceRootLinks: (rootId, links) =>\n serialised(async () => {\n await exec(\"BEGIN\");\n try {\n await ops.replaceRootLinks(rootId, links);\n await exec(\"COMMIT\");\n } catch (error) {\n await exec(\"ROLLBACK\");\n throw error;\n }\n }),\n putLink: (link) => serialised(() => ops.putLink(link)),\n deleteLink: (link) => serialised(() => ops.deleteLink(link)),\n enqueue: (entry) => serialised(() => ops.enqueue(entry)),\n queue: (filter) => serialised(() => ops.queue(filter)),\n setQueueStatus: (seq, status) => serialised(() => ops.setQueueStatus(seq, status)),\n setQueuePackage: (seqs, packageId) => serialised(() => ops.setQueuePackage(seqs, packageId)),\n removeQueueEntry: (seq) =>\n serialised(async () => {\n await ops.removeQueueEntry(seq);\n await settleQueue();\n }),\n addConflict: (c) => serialised(() => ops.addConflict(c)),\n conflicts: () => serialised(() => ops.conflicts()),\n removeConflict: (id) => serialised(() => ops.removeConflict(id)),\n getSyncState: (rootId) => serialised(() => ops.getSyncState(rootId)),\n setSyncState: (rootId, s) => serialised(() => ops.setSyncState(rootId, s)),\n skippedRecords: (rootId) => serialised(() => ops.skippedRecords(rootId)),\n setSkippedRecords: (rootId, records) => serialised(() => ops.setSkippedRecords(rootId, records)),\n removeRoot: (rootId) =>\n serialised(async () => {\n await exec(\"BEGIN\");\n try {\n await ops.removeRoot(rootId);\n await exec(\"COMMIT\");\n } catch (error) {\n await exec(\"ROLLBACK\");\n throw error;\n }\n await settleQueue();\n }),\n transaction: <T>(fn: (tx: DeviceStore) => Promise<T>) =>\n serialised(async () => {\n await exec(\"BEGIN\");\n let result: T;\n try {\n result = await fn(nestedStore(ops));\n await exec(\"COMMIT\");\n } catch (error) {\n await exec(\"ROLLBACK\");\n await settleQueue();\n throw error;\n }\n await settleQueue();\n return result;\n }),\n };\n return store;\n}\n","import type { ExecFn } from \"./sql-device-store\";\nimport type { OpenedDatabase } from \"./worker-client\";\n\n/**\n * One device copy shared by every page of the origin (tabs, the installed app, and the page that\n * is still unloading while the next one loads in the same tab).\n *\n * The browser lets only one page at a time hold the copy's files (opfs-sahpool: one set of access\n * handles per origin). So one page, the holder, opens the database; the others send their\n * statements to it over a BroadcastChannel. The holder is whoever holds the Web Lock\n * `nja-offline-db:<userId>`; every other page waits in that lock's queue and takes over when the\n * holder closes or goes away, retrying the open while the previous holder's files are released.\n *\n * Each store operation runs with exclusive use of the database across all pages (`exclusive`), so\n * a transaction of one page never interleaves with another page's statements.\n */\nexport type SharedDatabase = OpenedDatabase & {\n /** Runs `fn` with exclusive use of the database across every page. */\n exclusive: <T>(fn: () => Promise<T>) => Promise<T>;\n};\n\ntype Message =\n | { kind: \"holder\" }\n | { kind: \"lease\"; id: string }\n | { kind: \"granted\"; id: string }\n | { kind: \"release\"; id: string }\n | { kind: \"exec\"; id: string; lease: string; sql: string; bind?: unknown[] }\n | { kind: \"result\"; id: string; ok: true; rows: Record<string, unknown>[] }\n | { kind: \"result\"; id: string; ok: false; error: string }\n | { kind: \"shutdown\" };\n\n/** How long the holder keeps a lease for a page that never releases it (the page closed). */\nconst LEASE_TIMEOUT_MS = 30_000;\n/** How long a page waits for an answer before giving up on a statement. */\nconst EXEC_TIMEOUT_MS = 30_000;\n/** How long the next holder keeps retrying the open while the previous holder's files are released. */\nconst OPEN_RETRY_MS = 10_000;\n\nconst lockName = (userId: string) => `nja-offline-db:${userId}`;\nconst channelName = (userId: string) => `nja-offline-db:${userId}`;\n\nconst newId = (): string => globalThis.crypto.randomUUID();\n\n/** Whether this browser offers what sharing needs; without it the page opens the copy directly. */\nexport function canShareDatabase(): boolean {\n return (\n typeof BroadcastChannel !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n typeof navigator.locks?.request === \"function\"\n );\n}\n\n/** The files are still held by the page that had them (it is closing): worth retrying. */\nconst isHeldElsewhere = (error: unknown): boolean =>\n /NoModificationAllowedError|Access Handles cannot be created|createSyncAccessHandle/i.test(\n String((error as Error)?.message ?? error),\n );\n\nasync function openWithRetry(openLocal: () => Promise<OpenedDatabase>): Promise<OpenedDatabase> {\n const deadline = Date.now() + OPEN_RETRY_MS;\n for (let wait = 100; ; wait = Math.min(wait * 2, 1000)) {\n try {\n return await openLocal();\n } catch (error) {\n if (!isHeldElsewhere(error) || Date.now() >= deadline) throw error;\n await new Promise((resolve) => setTimeout(resolve, wait));\n }\n }\n}\n\n/** A promise chain: one `fn` at a time. */\nfunction createMutex() {\n let tail: Promise<unknown> = Promise.resolve();\n return <T>(fn: () => Promise<T>): Promise<T> => {\n const run = tail.then(fn, fn);\n tail = run.catch(() => undefined);\n return run;\n };\n}\n\n/**\n * Opens the shared device copy of `userId`. `openLocal` opens the database files in this page\n * (the worker); it runs only in the page that holds the lock.\n */\nexport async function openSharedDatabase(\n userId: string,\n openLocal: () => Promise<OpenedDatabase>,\n /** Called when another page asks every page to close the copy (logout, offline mode off). */\n onShutdown?: () => void,\n): Promise<SharedDatabase> {\n const channel = new BroadcastChannel(channelName(userId));\n const post = (message: Message) => channel.postMessage(message);\n const mutex = createMutex();\n\n let local: OpenedDatabase | undefined;\n let created = false;\n let closed = false;\n let releaseLock: (() => void) | undefined;\n const lockAbort = new AbortController();\n\n // Holder side: leases granted to other pages, released by them (or after a timeout).\n const leases = new Map<string, () => void>();\n // Page side: leases asked for and not yet granted, and statements waiting for an answer.\n const waitingLeases = new Map<\n string,\n { run: () => Promise<unknown>; resolve: (v: unknown) => void; reject: (e: Error) => void }\n >();\n const waitingResults = new Map<\n string,\n { resolve: (rows: Record<string, unknown>[]) => void; reject: (e: Error) => void }\n >();\n let currentLease: string | undefined;\n\n const isHolder = () => local !== undefined;\n\n /** Runs a lease granted by the holder: `run` sends its statements under that lease. */\n const runGranted = async (id: string) => {\n const waiting = waitingLeases.get(id);\n if (!waiting) {\n post({ kind: \"release\", id });\n return;\n }\n waitingLeases.delete(id);\n currentLease = id;\n try {\n waiting.resolve(await waiting.run());\n } catch (error) {\n waiting.reject(error as Error);\n } finally {\n currentLease = undefined;\n post({ kind: \"release\", id });\n }\n };\n\n channel.onmessage = (event: MessageEvent<Message>) => {\n const message = event.data;\n if (closed) return;\n switch (message.kind) {\n case \"holder\":\n // A new holder: the lease requests the old one never answered are asked again.\n if (!isHolder()) for (const id of waitingLeases.keys()) post({ kind: \"lease\", id });\n return;\n case \"lease\":\n if (!isHolder() || leases.has(message.id)) return;\n void mutex(\n () =>\n new Promise<void>((done) => {\n const timer = setTimeout(() => finish(), LEASE_TIMEOUT_MS);\n const finish = () => {\n clearTimeout(timer);\n leases.delete(message.id);\n done();\n };\n leases.set(message.id, finish);\n post({ kind: \"granted\", id: message.id });\n }),\n );\n return;\n case \"granted\":\n if (!isHolder()) void runGranted(message.id);\n return;\n case \"release\":\n leases.get(message.id)?.();\n return;\n case \"exec\":\n if (!isHolder() || !leases.has(message.lease)) return;\n local!\n .exec(message.sql, message.bind)\n .then((rows) => post({ kind: \"result\", id: message.id, ok: true, rows }))\n .catch((error) =>\n post({ kind: \"result\", id: message.id, ok: false, error: String(error?.message ?? error) }),\n );\n return;\n case \"result\": {\n const waiting = waitingResults.get(message.id);\n if (!waiting) return;\n waitingResults.delete(message.id);\n if (message.ok) waiting.resolve(message.rows);\n else waiting.reject(new Error(message.error));\n return;\n }\n case \"shutdown\":\n void close().finally(() => onShutdown?.());\n return;\n }\n };\n\n const becomeHolder = async () => {\n local = await openWithRetry(openLocal);\n created = local.created;\n // Leases this page asked for and nobody granted: they run here now.\n for (const [id, waiting] of waitingLeases) {\n waitingLeases.delete(id);\n mutex(waiting.run).then(waiting.resolve, waiting.reject);\n }\n post({ kind: \"holder\" });\n };\n\n /** Holds the lock as the holder until `close`; the lock passes to the next page when this ends. */\n const holdLock = async (): Promise<void> => {\n if (closed) return;\n await becomeHolder();\n await new Promise<void>((resolve) => (releaseLock = resolve));\n };\n\n // Free right now: this page is the holder. Held by another page: this page starts as its client\n // and queues for the lock, to take over when the holder leaves.\n let opened: Promise<void> | undefined;\n const isFree = await new Promise<boolean>((answer) => {\n navigator.locks\n .request(lockName(userId), { ifAvailable: true }, async (lock) => {\n if (!lock) {\n answer(false);\n return;\n }\n answer(true);\n opened = becomeHolder();\n try {\n await opened;\n } catch {\n return; // Could not open: the lock is released for the next page.\n }\n await new Promise<void>((resolve) => (releaseLock = resolve));\n })\n .catch(() => answer(false));\n });\n if (isFree) {\n try {\n await opened;\n } catch (error) {\n closed = true;\n channel.close();\n throw error;\n }\n } else {\n navigator.locks.request(lockName(userId), { signal: lockAbort.signal }, holdLock).catch((error) => {\n if ((error as { name?: string })?.name !== \"AbortError\")\n console.warn(\"[offline] taking over the device copy failed\", error);\n });\n }\n\n const remoteExec: ExecFn = (sql, bind) => {\n if (closed) return Promise.reject(new Error(\"The device database is closed.\"));\n const lease = currentLease;\n if (!lease) return exclusive(() => remoteExec(sql, bind));\n const id = newId();\n return new Promise<Record<string, unknown>[]>((resolve, reject) => {\n const timer = setTimeout(() => {\n waitingResults.delete(id);\n reject(new Error(\"The page holding the device copy did not answer.\"));\n }, EXEC_TIMEOUT_MS);\n waitingResults.set(id, {\n resolve: (rows) => {\n clearTimeout(timer);\n resolve(rows);\n },\n reject: (error) => {\n clearTimeout(timer);\n reject(error);\n },\n });\n post({ kind: \"exec\", id, lease, sql, bind });\n });\n };\n\n const exclusive = <T>(fn: () => Promise<T>): Promise<T> => {\n if (closed) return Promise.reject(new Error(\"The device database is closed.\"));\n if (isHolder()) return mutex(fn);\n const id = newId();\n return new Promise<T>((resolve, reject) => {\n waitingLeases.set(id, {\n run: fn as () => Promise<unknown>,\n resolve: resolve as (v: unknown) => void,\n reject,\n });\n post({ kind: \"lease\", id });\n });\n };\n\n const exec: ExecFn = (sql, bind) => (isHolder() ? local!.exec(sql, bind) : remoteExec(sql, bind));\n\n async function close(): Promise<void> {\n if (closed) return;\n closed = true;\n lockAbort.abort();\n const error = new Error(\"The device database is closed.\");\n for (const waiting of waitingLeases.values()) waiting.reject(error);\n for (const waiting of waitingResults.values()) waiting.reject(error);\n waitingLeases.clear();\n waitingResults.clear();\n for (const finish of [...leases.values()]) finish();\n try {\n if (local) await local.close();\n } finally {\n local = undefined;\n releaseLock?.();\n channel.close();\n }\n }\n\n return {\n get created() {\n return created;\n },\n exec,\n exclusive,\n close,\n };\n}\n\n/**\n * Asks every page to close the shared copy of `userId`, then runs `fn` once no page holds it\n * (logout, offline mode turned off: the files can only be deleted when nobody has them open).\n */\nexport async function withDatabaseReleased<T>(userId: string, fn: () => Promise<T>): Promise<T> {\n if (!canShareDatabase()) return fn();\n const channel = new BroadcastChannel(channelName(userId));\n channel.postMessage({ kind: \"shutdown\" } satisfies Message);\n channel.close();\n return navigator.locks.request(lockName(userId), () => fn()) as Promise<T>;\n}\n","import type { ExecFn } from \"./sql-device-store\";\nimport type { WorkerRequest, WorkerResponse } from \"./worker\";\n\nexport type OpenedDatabase = {\n exec: ExecFn;\n close: () => Promise<void>;\n /** True when the file did not exist before this open. */\n created: boolean;\n};\n\ntype Pending = { resolve: (value: unknown) => void; reject: (error: Error) => void };\n\n/** Starts the device-copy worker and opens `fileName` in it for `userId`. */\nexport async function openWorkerDatabase(\n userId: string,\n fileName: string,\n urls: { workerUrl: string; sqliteModuleUrl: string },\n): Promise<OpenedDatabase> {\n // A plain URL, so the app's bundler leaves it alone: the worker is a standalone file the app\n // serves (OfflineConfig.deviceWorkerUrl), precached by the service worker for offline use.\n const worker = new Worker(urls.workerUrl, { type: \"module\" });\n const { sqliteModuleUrl } = urls;\n const pending = new Map<number, Pending>();\n let nextId = 1;\n let closed = false;\n\n worker.onmessage = (event: MessageEvent<WorkerResponse>) => {\n const response = event.data;\n const waiting = pending.get(response.id);\n if (!waiting) return;\n pending.delete(response.id);\n if (response.ok) waiting.resolve(response.result);\n else waiting.reject(new Error(response.error));\n };\n worker.onerror = (event: ErrorEvent) => {\n const error = new Error(event.message || \"The device database worker failed.\");\n for (const waiting of pending.values()) waiting.reject(error);\n pending.clear();\n };\n\n const send = <T>(request: Omit<WorkerRequest, \"id\"> & { op: WorkerRequest[\"op\"] }): Promise<T> => {\n if (closed) return Promise.reject(new Error(\"The device database is closed.\"));\n const id = nextId++;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (value: unknown) => void, reject });\n worker.postMessage({ ...request, id } as WorkerRequest);\n });\n };\n\n let opened: { created: boolean };\n try {\n opened = await send<{ created: boolean }>({ op: \"open\", userId, fileName, sqliteModuleUrl } as WorkerRequest);\n } catch (error) {\n worker.terminate();\n throw error;\n }\n\n return {\n created: opened.created,\n exec: (sql, bind) => send<Record<string, unknown>[]>({ op: \"exec\", sql, bind } as WorkerRequest),\n close: async () => {\n if (closed) return;\n try {\n await send({ op: \"close\" } as WorkerRequest);\n } finally {\n closed = true;\n worker.terminate();\n }\n },\n };\n}\n","import type { Sqlite3Static } from \"@sqlite.org/sqlite-wasm\";\nimport {\n DeviceStoreUnavailableError,\n forgetOfflineStore,\n getDeviceWorkerUrl,\n getSqliteModuleUrl,\n isOfflineAvailable,\n isOfflineStoreBlocked,\n markOfflineUnavailable,\n OfflineUnavailableError,\n refreshOfflineDeviceState,\n unblockOfflineStore,\n} from \"../config\";\nimport { deleteImagesCaches } from \"../sync/artwork-cache\";\nimport type { DeviceStore } from \"./DeviceStore\";\nimport { createSqlDeviceStore } from \"./sql-device-store\";\nimport { canShareDatabase, openSharedDatabase, withDatabaseReleased, type SharedDatabase } from \"./shared-database\";\nimport { openWorkerDatabase, type OpenedDatabase } from \"./worker-client\";\n\n/** The part of an OPFS `FileSystemDirectoryHandle` the lifecycle uses (faked in tests). */\nexport type OfflineDirectory = {\n keys(): AsyncIterable<string>;\n getFileHandle(name: string, options?: { create?: boolean }): Promise<unknown>;\n removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>;\n};\n\nexport type DeviceStoreBackend = {\n /** The OPFS root that holds every user's device file. */\n getDirectory: () => Promise<OfflineDirectory>;\n /** Opens `fileName` for `userId` (the worker in the browser). */\n openDatabase: (userId: string, fileName: string) => Promise<OpenedDatabase>;\n /** Asks the browser to keep the device copy (`navigator.storage.persist()`). */\n requestPersistence: () => Promise<void>;\n};\n\nconst FILE_PREFIX = \"nja-offline-\";\nconst deviceFileName = (userId: string) => `${FILE_PREFIX}${userId}.sqlite3`;\n/** Present while the user's file holds unsent changes; read without opening the file. */\nconst pendingMarkerName = (userId: string) => `${FILE_PREFIX}${userId}.pending`;\n/** Names the user whose artwork the images cache holds (the images cache is not per user). */\nconst IMAGES_OWNER_PREFIX = \"nja-offline-images-owner-\";\nconst imagesOwnerName = (userId: string) => `${IMAGES_OWNER_PREFIX}${userId}`;\nconst FILE_PATTERN = /^nja-offline-(.+)\\.(?:sqlite3(?:-journal|-wal|-shm)?|pending)$/;\nconst POOL_PATTERN = /^\\.nja-offline-(.+)$/;\n\n/**\n * Why this browser cannot keep the device copy, or null when it may: browsers expose the private\n * file storage only in a secure context (https, or `localhost`). Checked before any worker starts.\n */\nconst missingDeviceStorage = (): string | null => {\n if (typeof window !== \"undefined\" && window.isSecureContext === false) {\n return \"the page is not served from a secure origin (https or localhost)\";\n }\n if (typeof navigator === \"undefined\" || typeof navigator.storage?.getDirectory !== \"function\") {\n return \"the browser offers no private file storage\";\n }\n return null;\n};\n\nconst assertDeviceStorage = (): void => {\n const reason = missingDeviceStorage();\n if (reason) throw new OfflineUnavailableError(reason);\n};\n\n/** What the SQLite worker answers when the browser lacks the OPFS sync-access handles. */\nconst OPFS_MISSING = /Missing required OPFS APIs/i;\n\nconst defaultBackend: DeviceStoreBackend = {\n getDirectory: async () => {\n assertDeviceStorage();\n try {\n return (await navigator.storage.getDirectory()) as unknown as OfflineDirectory;\n } catch (error) {\n // Storage refused outright (some private windows).\n if ((error as { name?: string })?.name === \"SecurityError\") {\n throw new OfflineUnavailableError(\"the browser refused its private file storage\");\n }\n throw error;\n }\n },\n openDatabase: async (userId, fileName) => {\n assertDeviceStorage();\n const openLocal = () =>\n openWorkerDatabase(userId, fileName, {\n workerUrl: getDeviceWorkerUrl(),\n sqliteModuleUrl: getSqliteModuleUrl(),\n });\n try {\n // Shared by every page of the origin: only one page at a time can hold the files.\n if (!canShareDatabase()) return await openLocal();\n return await openSharedDatabase(userId, openLocal, () => forgetClosedStore(userId));\n } catch (error) {\n if (OPFS_MISSING.test(String((error as Error)?.message ?? error))) {\n throw new OfflineUnavailableError(\"the browser lacks the file access the device copy needs (OPFS)\");\n }\n throw error;\n }\n },\n requestPersistence: async () => {\n if (typeof navigator !== \"undefined\" && navigator.storage?.persist) await navigator.storage.persist();\n },\n};\n\nlet backend: DeviceStoreBackend = defaultBackend;\n\ntype OpenStore = { userId: string; store: Promise<DeviceStore>; database: Promise<OpenedDatabase> };\nlet current: OpenStore | undefined;\n\n/** Replaces the OPFS directory / database opener (tests); `null` restores the browser one. */\nexport function setDeviceStoreBackend(next: Partial<DeviceStoreBackend> | null): void {\n backend = next ? { ...defaultBackend, ...next } : defaultBackend;\n current = undefined;\n}\n\nconst entryExists = async (directory: OfflineDirectory, name: string): Promise<boolean> => {\n for await (const key of directory.keys()) if (key === name) return true;\n return false;\n};\n\nconst removeIfPresent = async (directory: OfflineDirectory, name: string): Promise<void> => {\n try {\n await directory.removeEntry(name, { recursive: true });\n } catch (error) {\n if ((error as { name?: string })?.name !== \"NotFoundError\") throw error;\n }\n};\n\nconst entriesOf = async (directory: OfflineDirectory, userId: string): Promise<string[]> => {\n const names: string[] = [];\n for await (const key of directory.keys()) if (userIdOf(key) === userId) names.push(key);\n return names;\n};\n\n/**\n * The OPFS root, or undefined when this browser cannot keep the device copy: offline mode is then\n * switched off (`markOfflineUnavailable`). Any other failure is thrown as before.\n */\nconst deviceDirectory = async (): Promise<OfflineDirectory | undefined> => {\n if (!isOfflineAvailable()) return undefined;\n try {\n return await backend.getDirectory();\n } catch (error) {\n if (!(error instanceof OfflineUnavailableError)) throw error;\n markOfflineUnavailable(error);\n return undefined;\n }\n};\n\nconst userIdOf = (name: string): string | undefined => FILE_PATTERN.exec(name)?.[1] ?? POOL_PATTERN.exec(name)?.[1];\n\nconst closeCurrent = async (): Promise<void> => {\n const open = current;\n current = undefined;\n if (!open) return;\n try {\n await (await open.database).close();\n } catch {\n // A database that never opened has nothing to close.\n }\n};\n\n/**\n * Opens the device copy of `userId` (`nja-offline-<userId>.sqlite3` in the OPFS,\n * through the worker). Only one user's file is open at a time: opening another\n * user's file closes the previous one first.\n */\nexport function openDeviceStore(userId: string): Promise<DeviceStore> {\n if (!userId) return Promise.reject(new Error(\"openDeviceStore needs the signed-in user id.\"));\n if (isOfflineStoreBlocked(userId)) {\n return Promise.reject(new DeviceStoreUnavailableError(\"The device copy was deleted at logout\"));\n }\n if (!isOfflineAvailable()) {\n return Promise.reject(new OfflineUnavailableError(\"it was found unable to keep the device copy\"));\n }\n if (current?.userId === userId) return current.store;\n\n const previous = closeCurrent();\n const database = previous\n .then(() => backend.openDatabase(userId, deviceFileName(userId)))\n .catch((error) => {\n if (error instanceof OfflineUnavailableError) markOfflineUnavailable(error);\n throw error;\n });\n const store = database.then(async (opened) => {\n if (opened.created) await backend.requestPersistence();\n return createSqlDeviceStore(opened.exec, {\n exclusive: (opened as Partial<SharedDatabase>).exclusive,\n beforeEnqueue: async () => {\n const directory = await backend.getDirectory();\n await directory.getFileHandle(pendingMarkerName(userId), { create: true });\n },\n afterQueueChange: async (entries) => {\n if (entries > 0) return;\n await removeIfPresent(await backend.getDirectory(), pendingMarkerName(userId));\n },\n });\n });\n const open: OpenStore = { userId, store, database };\n current = open;\n store.catch(() => {\n if (current === open) current = undefined;\n });\n return store;\n}\n\nlet sqlite3Promise: Promise<Sqlite3Static> | undefined;\n\n/**\n * An in-memory SQLite database (no worker, no OPFS), for tests in Node. The package is imported\n * here at run time and hidden from app bundlers (see `OfflineConfig.sqliteModuleUrl`).\n */\nexport async function openMemoryDatabase(): Promise<OpenedDatabase> {\n sqlite3Promise ??= import(/* webpackIgnore: true */ /* turbopackIgnore: true */ \"@sqlite.org/sqlite-wasm\").then(\n (module) => module.default(),\n );\n const sqlite3 = await sqlite3Promise;\n const db = new sqlite3.oo1.DB(\":memory:\", \"c\");\n return {\n created: true,\n exec: async (sql, bind) =>\n db.exec({\n sql,\n bind: bind && bind.length ? (bind as any) : undefined,\n rowMode: \"object\",\n returnValue: \"resultRows\",\n }) as Record<string, unknown>[],\n close: async () => {\n db.close();\n },\n };\n}\n\n/** The same store and schema over an in-memory database, for tests. */\nexport async function openMemoryDeviceStore(): Promise<DeviceStore> {\n const database = await openMemoryDatabase();\n return createSqlDeviceStore(database.exec);\n}\n\n/**\n * Another page closed every page's copy of `userId` (logout, offline mode off): this page forgets\n * its store, and reads the device switch again before anything opens the copy.\n */\nfunction forgetClosedStore(userId: string): void {\n if (current?.userId === userId) current = undefined;\n forgetOfflineStore(userId);\n refreshOfflineDeviceState();\n}\n\n/**\n * Closes `userId`'s device copy in every page and removes every OPFS entry of it. The files can\n * only be removed once no page holds them, so the removal waits for every page to let go.\n */\nexport async function deleteDeviceStore(userId: string): Promise<void> {\n if (current?.userId === userId) await closeCurrent();\n const directory = await deviceDirectory();\n if (!directory) return;\n const remove = async () => {\n const names = await entriesOf(directory, userId);\n const marker = pendingMarkerName(userId);\n for (const name of names) if (name !== marker) await removeIfPresent(directory, name);\n await removeIfPresent(directory, marker);\n };\n if (backend === defaultBackend) await withDatabaseReleased(userId, remove);\n else await remove();\n}\n\n/** Whether `userId`'s device copy holds unsent changes. Never opens another user's file. */\nexport async function hasUnsentChanges(userId: string): Promise<boolean> {\n if (current?.userId === userId) {\n const store = await current.store;\n if ((await store.queue()).length > 0) return true;\n }\n const directory = await deviceDirectory();\n if (!directory) return false;\n return entryExists(directory, pendingMarkerName(userId));\n}\n\n/**\n * A user signed in on this browser: their device copy may be opened again (it is blocked after\n * a logout), and every other user's device copy is deleted when it has nothing unsent, and kept\n * untouched (never opened) when it has. When the images cache was not this user's (another user\n * signed in before, or no owner is known), it is deleted: the user's sync downloads their artwork.\n */\nexport async function onUserSignedIn(userId: string): Promise<void> {\n unblockOfflineStore(userId);\n if (current && current.userId !== userId) await closeCurrent();\n // A browser that cannot keep the device copy: offline mode switches off here, before the runtime starts.\n const directory = await deviceDirectory();\n if (!directory) return;\n const others = new Set<string>();\n const imageOwners: string[] = [];\n for await (const key of directory.keys()) {\n if (key.startsWith(IMAGES_OWNER_PREFIX)) {\n imageOwners.push(key);\n continue;\n }\n const owner = userIdOf(key);\n if (owner && owner !== userId) others.add(owner);\n }\n if (!imageOwners.includes(imagesOwnerName(userId))) {\n await deleteImagesCaches();\n for (const name of imageOwners) await removeIfPresent(directory, name);\n await directory.getFileHandle(imagesOwnerName(userId), { create: true });\n }\n for (const other of others) {\n if (await entryExists(directory, pendingMarkerName(other))) continue;\n await deleteDeviceStore(other);\n }\n}\n\n/** Locked now: the server said so, or the lock date it gave has passed. */\nexport function isRootLocked(s: { locked: boolean; lockedFrom: Date | null }, now: Date): boolean {\n if (s.locked) return true;\n return s.lockedFrom !== null && s.lockedFrom.getTime() <= now.getTime();\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-6RCZ64BQ.js","../src/offline/sync/artwork-cache.ts","../src/offline/store/schema.ts","../src/offline/store/sql-device-store.ts","../src/offline/store/shared-database.ts","../src/offline/store/worker-client.ts","../src/offline/store/lifecycle.ts"],"names":["newId"],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-6RCZ64BQ.js","sourcesContent":[null,"// Artwork images for the device copy, kept in Cache Storage under the exact URL the stored\n// artwork record holds (`attributes.imageUrl`). The service worker answers those URLs from the\n// same cache when the network fails. The image downloads are plain GETs of the URLs the API\n// already returned (decision recorded in the plan).\n\n// The service worker names its caches `${appId}-images` and `${appId}-offline-${revision}`\n// (pwa/sw.js). The page does not load sw-config.js, so it finds the app id from those names;\n// Cache Storage is per origin, so only this app's caches are listed.\nconst IMAGES_CACHE_SUFFIX = \"-images\";\nconst OFFLINE_CACHE_MARKER = \"-offline-\";\n\nasync function imagesCacheName(): Promise<string | undefined> {\n if (typeof caches === \"undefined\") return undefined;\n const keys = await caches.keys();\n const existing = keys.find((key) => key.endsWith(IMAGES_CACHE_SUFFIX));\n if (existing) return existing;\n\n const offline = keys.find((key) => key.includes(OFFLINE_CACHE_MARKER));\n if (!offline) return undefined;\n return `${offline.slice(0, offline.indexOf(OFFLINE_CACHE_MARKER))}${IMAGES_CACHE_SUFFIX}`;\n}\n\n/** The image URL a stored resource carries, if any. */\nexport function imageUrlOf(resource: any): string | undefined {\n const url = resource?.attributes?.imageUrl;\n return typeof url === \"string\" && url.length > 0 ? url : undefined;\n}\n\n/**\n * Makes sure `url` is in the images cache: downloaded when missing. When `previousUrl` is a\n * different URL of the same artwork, its entry is removed. A failed download is logged and\n * retried at the next sync.\n */\nexport async function cacheArtworkImage(url: string, previousUrl?: string): Promise<void> {\n const name = await imagesCacheName();\n if (!name) return;\n const cache = await caches.open(name);\n\n if (previousUrl && previousUrl !== url) await cache.delete(previousUrl);\n if (await cache.match(url)) return;\n\n try {\n const response = await fetch(url);\n if (!response.ok) {\n console.warn(\"[offline] could not download an artwork image\", url, response.status);\n return;\n }\n await cache.put(url, response);\n } catch (error) {\n console.warn(\"[offline] could not download an artwork image\", url, error);\n }\n}\n\n/** Removes the cached copy of `url`. */\nexport async function removeArtworkImage(url: string): Promise<void> {\n const name = await imagesCacheName();\n if (!name) return;\n const cache = await caches.open(name);\n await cache.delete(url);\n}\n\n/** Deletes this app's whole images cache (logout, or another user signing in). */\nexport async function deleteImagesCaches(): Promise<void> {\n if (typeof caches === \"undefined\") return;\n const keys = await caches.keys();\n await Promise.all(keys.filter((key) => key.endsWith(IMAGES_CACHE_SUFFIX)).map((key) => caches.delete(key)));\n}\n","/**\n * Device-copy schema (spec § 1). Every statement is idempotent so it runs on\n * every open; `user_version` records the layout. A file of an older layout is\n * recreated (pre-release: no migration), see `DEVICE_STORE_RESET`.\n *\n * 2: `record_campaigns` (a shared record, e.g. an artwork or a template, belongs\n * to every root record that uses it) and `queue.package_id`.\n * `skipped_records` was added later inside layout 2: `CREATE TABLE IF NOT EXISTS` adds it to\n * an existing file without a reset (a reset would drop unsent changes).\n * 3: campaign columns and tables renamed to root (`campaign_id` -> `root_id`,\n * `record_campaigns` -> `record_roots`).\n */\nexport const DEVICE_STORE_SCHEMA_VERSION = 3;\n\n/**\n * Drops every table of an older layout before the schema is created again. Layout 2's\n * `record_campaigns` is dropped too, so a layout-2 file is fully recreated.\n */\nexport const DEVICE_STORE_RESET: string[] = [\n `DROP TABLE IF EXISTS records`,\n `DROP TABLE IF EXISTS record_campaigns`,\n `DROP TABLE IF EXISTS record_roots`,\n `DROP TABLE IF EXISTS links`,\n `DROP TABLE IF EXISTS queue`,\n `DROP TABLE IF EXISTS conflicts`,\n `DROP TABLE IF EXISTS sync_state`,\n `DROP TABLE IF EXISTS skipped_records`,\n];\n\nexport const DEVICE_STORE_SCHEMA: string[] = [\n `CREATE TABLE IF NOT EXISTS records (\n type TEXT NOT NULL,\n id TEXT NOT NULL,\n root_id TEXT NOT NULL,\n resource TEXT NOT NULL,\n updated_at TEXT,\n base TEXT,\n local_only INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (type, id)\n )`,\n `CREATE INDEX IF NOT EXISTS records_type_root ON records (type, root_id)`,\n `CREATE TABLE IF NOT EXISTS record_roots (\n type TEXT NOT NULL,\n id TEXT NOT NULL,\n root_id TEXT NOT NULL,\n PRIMARY KEY (type, id, root_id)\n )`,\n `CREATE INDEX IF NOT EXISTS record_roots_root ON record_roots (root_id, type)`,\n `CREATE TABLE IF NOT EXISTS links (\n from_id TEXT NOT NULL,\n from_type TEXT NOT NULL,\n to_id TEXT NOT NULL,\n to_type TEXT NOT NULL,\n kind TEXT NOT NULL,\n props TEXT NOT NULL DEFAULT '{}',\n root_id TEXT,\n PRIMARY KEY (from_id, to_id, kind)\n )`,\n `CREATE INDEX IF NOT EXISTS links_from ON links (from_id)`,\n `CREATE INDEX IF NOT EXISTS links_to ON links (to_id)`,\n `CREATE INDEX IF NOT EXISTS links_root ON links (root_id)`,\n `CREATE TABLE IF NOT EXISTS queue (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n root_id TEXT NOT NULL,\n request TEXT NOT NULL,\n base TEXT,\n created_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n package_id TEXT\n )`,\n `CREATE INDEX IF NOT EXISTS queue_status_seq ON queue (status, seq)`,\n `CREATE TABLE IF NOT EXISTS conflicts (\n id TEXT PRIMARY KEY,\n queue_seq INTEGER NOT NULL,\n root_id TEXT,\n kind TEXT NOT NULL,\n record_type TEXT,\n record_id TEXT,\n mine TEXT,\n theirs TEXT,\n base TEXT,\n merged TEXT,\n fields TEXT NOT NULL DEFAULT '[]',\n blocks TEXT,\n reason TEXT\n )`,\n `CREATE TABLE IF NOT EXISTS sync_state (\n root_id TEXT PRIMARY KEY,\n server_time TEXT,\n locked INTEGER NOT NULL DEFAULT 0,\n locked_from TEXT\n )`,\n `CREATE TABLE IF NOT EXISTS skipped_records (\n root_id TEXT NOT NULL,\n type TEXT NOT NULL,\n id TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (root_id, type, id)\n )`,\n `PRAGMA user_version = ${DEVICE_STORE_SCHEMA_VERSION}`,\n];\n","import type {\n Conflict,\n DeviceStore,\n QueueEntry,\n QueueStatus,\n SkippedRecord,\n StoredLink,\n StoredRecord,\n SyncState,\n} from \"./DeviceStore\";\nimport { DEVICE_STORE_RESET, DEVICE_STORE_SCHEMA, DEVICE_STORE_SCHEMA_VERSION } from \"./schema\";\n\n/** Runs one SQL statement with positional `?` binds and answers its rows as objects. */\nexport type ExecFn = (sql: string, bind?: unknown[]) => Promise<Record<string, unknown>[]>;\n\nexport type SqlDeviceStoreHooks = {\n /** Runs before a queue entry is written (the lifecycle marks the file as holding unsent changes). */\n beforeEnqueue?: () => Promise<void>;\n /** Runs after a committed change that can shrink the queue, with the number of entries left. */\n afterQueueChange?: (entries: number) => Promise<void>;\n /**\n * Runs each operation with exclusive use of the database across pages (the shared device copy:\n * several pages send statements to one database, and a transaction must not interleave).\n */\n exclusive?: <T>(fn: () => Promise<T>) => Promise<T>;\n};\n\nconst toJson = (value: unknown): string | null =>\n value === undefined || value === null ? null : JSON.stringify(value);\nconst fromJson = (value: unknown): any => (value === null || value === undefined ? null : JSON.parse(String(value)));\nconst toIso = (value: Date | null | undefined): string | null => (value ? value.toISOString() : null);\nconst fromIso = (value: unknown): Date | null =>\n value === null || value === undefined ? null : new Date(String(value));\n\nconst newId = (): string =>\n typeof globalThis.crypto?.randomUUID === \"function\"\n ? globalThis.crypto.randomUUID()\n : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n\nconst rowToRecord = (row: Record<string, unknown>): StoredRecord => ({\n type: String(row.type),\n id: String(row.id),\n rootId: String(row.root_id),\n resource: fromJson(row.resource),\n updatedAt: fromIso(row.updated_at),\n base: fromJson(row.base),\n localOnly: Number(row.local_only) === 1,\n});\n\nconst rowToLink = (row: Record<string, unknown>): StoredLink => ({\n fromType: String(row.from_type),\n fromId: String(row.from_id),\n toType: String(row.to_type),\n toId: String(row.to_id),\n kind: String(row.kind),\n props: fromJson(row.props) ?? {},\n});\n\nconst rowToQueueEntry = (row: Record<string, unknown>): QueueEntry => {\n const request = fromJson(row.request) ?? {};\n const entry: QueueEntry = {\n seq: Number(row.seq),\n rootId: String(row.root_id),\n method: request.method,\n path: request.path,\n base: fromJson(row.base),\n createdAt: new Date(String(row.created_at)),\n status: String(row.status) as QueueStatus,\n };\n if (request.body !== undefined) entry.body = request.body;\n if (request.recordType !== undefined) entry.recordType = request.recordType;\n if (request.recordId !== undefined) entry.recordId = request.recordId;\n if (request.resolution !== undefined) entry.resolution = request.resolution;\n if (row.package_id !== null && row.package_id !== undefined) entry.packageId = String(row.package_id);\n return entry;\n};\n\nconst rowToConflict = (row: Record<string, unknown>): Conflict => {\n const conflict: Conflict = {\n id: String(row.id),\n queueSeq: Number(row.queue_seq),\n kind: String(row.kind) as Conflict[\"kind\"],\n mine: fromJson(row.mine),\n theirs: fromJson(row.theirs),\n base: fromJson(row.base),\n fields: fromJson(row.fields) ?? [],\n };\n if (row.record_type !== null && row.record_type !== undefined) conflict.recordType = String(row.record_type);\n if (row.record_id !== null && row.record_id !== undefined) conflict.recordId = String(row.record_id);\n const merged = fromJson(row.merged);\n if (merged !== null) conflict.merged = merged;\n const blocks = fromJson(row.blocks);\n if (blocks !== null) conflict.blocks = blocks;\n if (row.reason !== null && row.reason !== undefined) conflict.reason = String(row.reason);\n return conflict;\n};\n\n/** The statements of every store operation, run over an exec that is already serialised. */\nconst createOperations = (exec: ExecFn, hooks: SqlDeviceStoreHooks) => {\n const insertLink = async (link: StoredLink, rootId: string | null) => {\n await exec(\n `INSERT OR REPLACE INTO links (from_id, from_type, to_id, to_type, kind, props, root_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [link.fromId, link.fromType, link.toId, link.toType, link.kind, JSON.stringify(link.props ?? {}), rootId],\n );\n };\n\n const deleteRecordEverywhere = async (type: string, id: string) => {\n await exec(`DELETE FROM records WHERE type = ? AND id = ?`, [type, id]);\n await exec(`DELETE FROM record_roots WHERE type = ? AND id = ?`, [type, id]);\n await exec(`DELETE FROM links WHERE from_id = ? OR to_id = ?`, [id, id]);\n };\n\n return {\n async getRecord(type: string, id: string): Promise<StoredRecord | undefined> {\n const rows = await exec(`SELECT * FROM records WHERE type = ? AND id = ?`, [type, id]);\n return rows[0] ? rowToRecord(rows[0]) : undefined;\n },\n\n async listRecords(filter: { type: string; rootId?: string; ids?: string[] }): Promise<StoredRecord[]> {\n if (filter.ids && filter.ids.length === 0) return [];\n const where = [\"type = ?\"];\n const bind: unknown[] = [filter.type];\n if (filter.rootId !== undefined) {\n where.push(\n \"EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id AND rc.root_id = ?)\",\n );\n bind.push(filter.rootId);\n }\n if (filter.ids) {\n where.push(`id IN (${filter.ids.map(() => \"?\").join(\", \")})`);\n bind.push(...filter.ids);\n }\n const rows = await exec(`SELECT * FROM records WHERE ${where.join(\" AND \")} ORDER BY rowid`, bind);\n return rows.map(rowToRecord);\n },\n\n async putRecord(\n record: Omit<StoredRecord, \"base\" | \"localOnly\"> & { base?: any | null; localOnly?: boolean },\n ): Promise<void> {\n // A record already stored keeps its first root record; the new one is added to its memberships.\n await exec(\n `INSERT INTO records (type, id, root_id, resource, updated_at, base, local_only) VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (type, id) DO UPDATE SET resource = excluded.resource, updated_at = excluded.updated_at,\n base = excluded.base, local_only = excluded.local_only`,\n [\n record.type,\n record.id,\n record.rootId,\n JSON.stringify(record.resource),\n toIso(record.updatedAt),\n toJson(record.base),\n record.localOnly ? 1 : 0,\n ],\n );\n await exec(`INSERT OR IGNORE INTO record_roots (type, id, root_id) VALUES (?, ?, ?)`, [\n record.type,\n record.id,\n record.rootId,\n ]);\n },\n\n async deleteRecord(type: string, id: string): Promise<void> {\n await deleteRecordEverywhere(type, id);\n },\n\n async removeRecordFromRoot(type: string, id: string, rootId: string): Promise<void> {\n await exec(`DELETE FROM record_roots WHERE type = ? AND id = ? AND root_id = ?`, [type, id, rootId]);\n const left = await exec(`SELECT root_id FROM record_roots WHERE type = ? AND id = ? ORDER BY root_id`, [\n type,\n id,\n ]);\n if (left.length === 0) {\n await deleteRecordEverywhere(type, id);\n return;\n }\n await exec(`UPDATE records SET root_id = ? WHERE type = ? AND id = ? AND root_id = ?`, [\n String(left[0].root_id),\n type,\n id,\n rootId,\n ]);\n },\n\n async linksFrom(fromId: string, kind?: string): Promise<StoredLink[]> {\n const rows =\n kind === undefined\n ? await exec(`SELECT * FROM links WHERE from_id = ? ORDER BY rowid`, [fromId])\n : await exec(`SELECT * FROM links WHERE from_id = ? AND kind = ? ORDER BY rowid`, [fromId, kind]);\n return rows.map(rowToLink);\n },\n\n async linksTo(toId: string, kind?: string): Promise<StoredLink[]> {\n const rows =\n kind === undefined\n ? await exec(`SELECT * FROM links WHERE to_id = ? ORDER BY rowid`, [toId])\n : await exec(`SELECT * FROM links WHERE to_id = ? AND kind = ? ORDER BY rowid`, [toId, kind]);\n return rows.map(rowToLink);\n },\n\n async replaceRootLinks(rootId: string, links: StoredLink[]): Promise<void> {\n await exec(`DELETE FROM links WHERE root_id = ?`, [rootId]);\n for (const link of links) await insertLink(link, rootId);\n },\n\n async putLink(link: StoredLink): Promise<void> {\n // A link belongs to the root record of the record at either end, so a later\n // replaceRootLinks / removeRoot of that root record reaches it.\n const owner = await exec(`SELECT root_id FROM records WHERE id IN (?, ?) ORDER BY id = ? DESC LIMIT 1`, [\n link.fromId,\n link.toId,\n link.fromId,\n ]);\n await insertLink(link, owner[0] ? String(owner[0].root_id) : null);\n },\n\n async deleteLink(link: Pick<StoredLink, \"fromId\" | \"toId\" | \"kind\">): Promise<void> {\n await exec(`DELETE FROM links WHERE from_id = ? AND to_id = ? AND kind = ?`, [link.fromId, link.toId, link.kind]);\n },\n\n async enqueue(entry: Omit<QueueEntry, \"seq\" | \"createdAt\" | \"status\">): Promise<number> {\n if (hooks.beforeEnqueue) await hooks.beforeEnqueue();\n const request = {\n method: entry.method,\n path: entry.path,\n body: entry.body,\n recordType: entry.recordType,\n recordId: entry.recordId,\n resolution: entry.resolution,\n };\n const rows = await exec(\n `INSERT INTO queue (root_id, request, base, created_at, status) VALUES (?, ?, ?, ?, 'pending') RETURNING seq`,\n [entry.rootId, JSON.stringify(request), toJson(entry.base), new Date().toISOString()],\n );\n return Number(rows[0].seq);\n },\n\n async queue(filter?: { status?: QueueStatus }): Promise<QueueEntry[]> {\n const rows = filter?.status\n ? await exec(`SELECT * FROM queue WHERE status = ? ORDER BY seq`, [filter.status])\n : await exec(`SELECT * FROM queue ORDER BY seq`);\n return rows.map(rowToQueueEntry);\n },\n\n async setQueueStatus(seq: number, status: QueueStatus): Promise<void> {\n await exec(`UPDATE queue SET status = ? WHERE seq = ?`, [status, seq]);\n },\n\n async setQueuePackage(seqs: number[], packageId: string | null): Promise<void> {\n if (seqs.length === 0) return;\n await exec(`UPDATE queue SET package_id = ? WHERE seq IN (${seqs.map(() => \"?\").join(\", \")})`, [\n packageId,\n ...seqs,\n ]);\n },\n\n async removeQueueEntry(seq: number): Promise<void> {\n await exec(`DELETE FROM queue WHERE seq = ?`, [seq]);\n },\n\n async addConflict(c: Omit<Conflict, \"id\">): Promise<string> {\n const id = newId();\n const owner = await exec(`SELECT root_id FROM queue WHERE seq = ?`, [c.queueSeq]);\n await exec(\n `INSERT INTO conflicts (id, queue_seq, root_id, kind, record_type, record_id, mine, theirs, base, merged, fields, blocks, reason)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id,\n c.queueSeq,\n owner[0] ? String(owner[0].root_id) : null,\n c.kind,\n c.recordType ?? null,\n c.recordId ?? null,\n toJson(c.mine),\n toJson(c.theirs),\n toJson(c.base),\n toJson(c.merged),\n JSON.stringify(c.fields ?? []),\n toJson(c.blocks),\n c.reason ?? null,\n ],\n );\n return id;\n },\n\n async conflicts(): Promise<Conflict[]> {\n const rows = await exec(`SELECT * FROM conflicts ORDER BY queue_seq, rowid`);\n return rows.map(rowToConflict);\n },\n\n async removeConflict(id: string): Promise<void> {\n await exec(`DELETE FROM conflicts WHERE id = ?`, [id]);\n },\n\n async getSyncState(rootId: string): Promise<SyncState> {\n const rows = await exec(`SELECT * FROM sync_state WHERE root_id = ?`, [rootId]);\n if (!rows[0]) return { serverTime: null, locked: false, lockedFrom: null };\n return {\n serverTime: fromIso(rows[0].server_time),\n locked: Number(rows[0].locked) === 1,\n lockedFrom: fromIso(rows[0].locked_from),\n };\n },\n\n async setSyncState(rootId: string, s: SyncState): Promise<void> {\n await exec(`INSERT OR REPLACE INTO sync_state (root_id, server_time, locked, locked_from) VALUES (?, ?, ?, ?)`, [\n rootId,\n toIso(s.serverTime),\n s.locked ? 1 : 0,\n toIso(s.lockedFrom),\n ]);\n },\n\n async skippedRecords(rootId: string): Promise<SkippedRecord[]> {\n const rows = await exec(`SELECT * FROM skipped_records WHERE root_id = ? ORDER BY type, id`, [rootId]);\n return rows.map((row) => ({\n type: String(row.type),\n id: String(row.id),\n updatedAt: new Date(String(row.updated_at)),\n }));\n },\n\n async setSkippedRecords(rootId: string, records: SkippedRecord[]): Promise<void> {\n await exec(`DELETE FROM skipped_records WHERE root_id = ?`, [rootId]);\n for (const record of records) {\n await exec(`INSERT OR REPLACE INTO skipped_records (root_id, type, id, updated_at) VALUES (?, ?, ?, ?)`, [\n rootId,\n record.type,\n record.id,\n record.updatedAt.toISOString(),\n ]);\n }\n },\n\n async removeRoot(rootId: string): Promise<void> {\n // Waiting changes are never dropped: a root record with queue entries is kept whole.\n const waiting = await exec(`SELECT COUNT(*) AS n FROM queue WHERE root_id = ?`, [rootId]);\n if (Number(waiting[0]?.n ?? 0) > 0) return;\n // A shared record another root record still uses stays for that root record; every other\n // record of this root record goes, with its links.\n await exec(`DELETE FROM links WHERE root_id = ?`, [rootId]);\n await exec(`DELETE FROM record_roots WHERE root_id = ?`, [rootId]);\n const orphans = `SELECT id FROM records r WHERE NOT EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = r.type AND rc.id = r.id)`;\n await exec(`DELETE FROM links WHERE from_id IN (${orphans}) OR to_id IN (${orphans})`);\n await exec(\n `DELETE FROM records WHERE NOT EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id)`,\n );\n await exec(\n `UPDATE records SET root_id = (SELECT MIN(rc.root_id) FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id) WHERE root_id = ?`,\n [rootId],\n );\n await exec(`DELETE FROM conflicts WHERE root_id = ?`, [rootId]);\n await exec(`DELETE FROM sync_state WHERE root_id = ?`, [rootId]);\n await exec(`DELETE FROM skipped_records WHERE root_id = ?`, [rootId]);\n },\n\n async queueSize(): Promise<number> {\n const rows = await exec(`SELECT COUNT(*) AS n FROM queue`);\n return Number(rows[0]?.n ?? 0);\n },\n };\n};\n\ntype Operations = ReturnType<typeof createOperations>;\n\n/** A store whose `transaction` runs `fn` directly: used inside an open transaction. */\nconst nestedStore = (ops: Operations): DeviceStore => {\n const { queueSize: _queueSize, ...rest } = ops;\n const store: DeviceStore = { ...rest, transaction: (fn) => fn(store) };\n return store;\n};\n\n/**\n * The device store over any SQLite `exec` (the worker's OPFS file, or an\n * in-memory database in tests). Every operation is serialised, so a\n * transaction never interleaves with another caller's statements.\n */\nexport async function createSqlDeviceStore(exec: ExecFn, hooks: SqlDeviceStoreHooks = {}): Promise<DeviceStore> {\n const exclusive = hooks.exclusive ?? (<T>(fn: () => Promise<T>) => fn());\n\n // A file of an older layout is recreated (pre-release: no migration of device files).\n await exclusive(async () => {\n const version = Number((await exec(`PRAGMA user_version`))[0]?.user_version ?? 0);\n if (version > 0 && version < DEVICE_STORE_SCHEMA_VERSION) {\n for (const statement of DEVICE_STORE_RESET) await exec(statement);\n }\n for (const statement of DEVICE_STORE_SCHEMA) await exec(statement);\n });\n\n const ops = createOperations(exec, hooks);\n let tail: Promise<unknown> = Promise.resolve();\n const serialised = <T>(fn: () => Promise<T>): Promise<T> => {\n const run = tail.then(\n () => exclusive(fn),\n () => exclusive(fn),\n );\n tail = run.catch(() => undefined);\n return run;\n };\n const settleQueue = async () => {\n if (hooks.afterQueueChange) await hooks.afterQueueChange(await ops.queueSize());\n };\n\n const store: DeviceStore = {\n getRecord: (type, id) => serialised(() => ops.getRecord(type, id)),\n listRecords: (filter) => serialised(() => ops.listRecords(filter)),\n putRecord: (record) => serialised(() => ops.putRecord(record)),\n deleteRecord: (type, id) => serialised(() => ops.deleteRecord(type, id)),\n removeRecordFromRoot: (type, id, rootId) => serialised(() => ops.removeRecordFromRoot(type, id, rootId)),\n linksFrom: (fromId, kind) => serialised(() => ops.linksFrom(fromId, kind)),\n linksTo: (toId, kind) => serialised(() => ops.linksTo(toId, kind)),\n replaceRootLinks: (rootId, links) =>\n serialised(async () => {\n await exec(\"BEGIN\");\n try {\n await ops.replaceRootLinks(rootId, links);\n await exec(\"COMMIT\");\n } catch (error) {\n await exec(\"ROLLBACK\");\n throw error;\n }\n }),\n putLink: (link) => serialised(() => ops.putLink(link)),\n deleteLink: (link) => serialised(() => ops.deleteLink(link)),\n enqueue: (entry) => serialised(() => ops.enqueue(entry)),\n queue: (filter) => serialised(() => ops.queue(filter)),\n setQueueStatus: (seq, status) => serialised(() => ops.setQueueStatus(seq, status)),\n setQueuePackage: (seqs, packageId) => serialised(() => ops.setQueuePackage(seqs, packageId)),\n removeQueueEntry: (seq) =>\n serialised(async () => {\n await ops.removeQueueEntry(seq);\n await settleQueue();\n }),\n addConflict: (c) => serialised(() => ops.addConflict(c)),\n conflicts: () => serialised(() => ops.conflicts()),\n removeConflict: (id) => serialised(() => ops.removeConflict(id)),\n getSyncState: (rootId) => serialised(() => ops.getSyncState(rootId)),\n setSyncState: (rootId, s) => serialised(() => ops.setSyncState(rootId, s)),\n skippedRecords: (rootId) => serialised(() => ops.skippedRecords(rootId)),\n setSkippedRecords: (rootId, records) => serialised(() => ops.setSkippedRecords(rootId, records)),\n removeRoot: (rootId) =>\n serialised(async () => {\n await exec(\"BEGIN\");\n try {\n await ops.removeRoot(rootId);\n await exec(\"COMMIT\");\n } catch (error) {\n await exec(\"ROLLBACK\");\n throw error;\n }\n await settleQueue();\n }),\n transaction: <T>(fn: (tx: DeviceStore) => Promise<T>) =>\n serialised(async () => {\n await exec(\"BEGIN\");\n let result: T;\n try {\n result = await fn(nestedStore(ops));\n await exec(\"COMMIT\");\n } catch (error) {\n await exec(\"ROLLBACK\");\n await settleQueue();\n throw error;\n }\n await settleQueue();\n return result;\n }),\n };\n return store;\n}\n","import type { ExecFn } from \"./sql-device-store\";\nimport type { OpenedDatabase } from \"./worker-client\";\n\n/**\n * One device copy shared by every page of the origin (tabs, the installed app, and the page that\n * is still unloading while the next one loads in the same tab).\n *\n * The browser lets only one page at a time hold the copy's files (opfs-sahpool: one set of access\n * handles per origin). So one page, the holder, opens the database; the others send their\n * statements to it over a BroadcastChannel. The holder is whoever holds the Web Lock\n * `nja-offline-db:<userId>`; every other page waits in that lock's queue and takes over when the\n * holder closes or goes away, retrying the open while the previous holder's files are released.\n *\n * Each store operation runs with exclusive use of the database across all pages (`exclusive`), so\n * a transaction of one page never interleaves with another page's statements.\n */\nexport type SharedDatabase = OpenedDatabase & {\n /** Runs `fn` with exclusive use of the database across every page. */\n exclusive: <T>(fn: () => Promise<T>) => Promise<T>;\n};\n\ntype Message =\n | { kind: \"holder\" }\n | { kind: \"lease\"; id: string }\n | { kind: \"granted\"; id: string }\n | { kind: \"release\"; id: string }\n | { kind: \"exec\"; id: string; lease: string; sql: string; bind?: unknown[] }\n | { kind: \"result\"; id: string; ok: true; rows: Record<string, unknown>[] }\n | { kind: \"result\"; id: string; ok: false; error: string }\n | { kind: \"shutdown\" };\n\n/** How long the holder keeps a lease for a page that never releases it (the page closed). */\nconst LEASE_TIMEOUT_MS = 30_000;\n/** How long a page waits for an answer before giving up on a statement. */\nconst EXEC_TIMEOUT_MS = 30_000;\n/** How long the next holder keeps retrying the open while the previous holder's files are released. */\nconst OPEN_RETRY_MS = 10_000;\n\nconst lockName = (userId: string) => `nja-offline-db:${userId}`;\nconst channelName = (userId: string) => `nja-offline-db:${userId}`;\n\nconst newId = (): string => globalThis.crypto.randomUUID();\n\n/** Whether this browser offers what sharing needs; without it the page opens the copy directly. */\nexport function canShareDatabase(): boolean {\n return (\n typeof BroadcastChannel !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n typeof navigator.locks?.request === \"function\"\n );\n}\n\n/** The files are still held by the page that had them (it is closing): worth retrying. */\nconst isHeldElsewhere = (error: unknown): boolean =>\n /NoModificationAllowedError|Access Handles cannot be created|createSyncAccessHandle/i.test(\n String((error as Error)?.message ?? error),\n );\n\nasync function openWithRetry(openLocal: () => Promise<OpenedDatabase>): Promise<OpenedDatabase> {\n const deadline = Date.now() + OPEN_RETRY_MS;\n for (let wait = 100; ; wait = Math.min(wait * 2, 1000)) {\n try {\n return await openLocal();\n } catch (error) {\n if (!isHeldElsewhere(error) || Date.now() >= deadline) throw error;\n await new Promise((resolve) => setTimeout(resolve, wait));\n }\n }\n}\n\n/** A promise chain: one `fn` at a time. */\nfunction createMutex() {\n let tail: Promise<unknown> = Promise.resolve();\n return <T>(fn: () => Promise<T>): Promise<T> => {\n const run = tail.then(fn, fn);\n tail = run.catch(() => undefined);\n return run;\n };\n}\n\n/**\n * Opens the shared device copy of `userId`. `openLocal` opens the database files in this page\n * (the worker); it runs only in the page that holds the lock.\n */\nexport async function openSharedDatabase(\n userId: string,\n openLocal: () => Promise<OpenedDatabase>,\n /** Called when another page asks every page to close the copy (logout, offline mode off). */\n onShutdown?: () => void,\n): Promise<SharedDatabase> {\n const channel = new BroadcastChannel(channelName(userId));\n const post = (message: Message) => channel.postMessage(message);\n const mutex = createMutex();\n\n let local: OpenedDatabase | undefined;\n let created = false;\n let closed = false;\n let releaseLock: (() => void) | undefined;\n const lockAbort = new AbortController();\n\n // Holder side: leases granted to other pages, released by them (or after a timeout).\n const leases = new Map<string, () => void>();\n // Page side: leases asked for and not yet granted, and statements waiting for an answer.\n const waitingLeases = new Map<\n string,\n { run: () => Promise<unknown>; resolve: (v: unknown) => void; reject: (e: Error) => void }\n >();\n const waitingResults = new Map<\n string,\n { resolve: (rows: Record<string, unknown>[]) => void; reject: (e: Error) => void }\n >();\n let currentLease: string | undefined;\n\n const isHolder = () => local !== undefined;\n\n /** Runs a lease granted by the holder: `run` sends its statements under that lease. */\n const runGranted = async (id: string) => {\n const waiting = waitingLeases.get(id);\n if (!waiting) {\n post({ kind: \"release\", id });\n return;\n }\n waitingLeases.delete(id);\n currentLease = id;\n try {\n waiting.resolve(await waiting.run());\n } catch (error) {\n waiting.reject(error as Error);\n } finally {\n currentLease = undefined;\n post({ kind: \"release\", id });\n }\n };\n\n channel.onmessage = (event: MessageEvent<Message>) => {\n const message = event.data;\n if (closed) return;\n switch (message.kind) {\n case \"holder\":\n // A new holder: the lease requests the old one never answered are asked again.\n if (!isHolder()) for (const id of waitingLeases.keys()) post({ kind: \"lease\", id });\n return;\n case \"lease\":\n if (!isHolder() || leases.has(message.id)) return;\n void mutex(\n () =>\n new Promise<void>((done) => {\n const timer = setTimeout(() => finish(), LEASE_TIMEOUT_MS);\n const finish = () => {\n clearTimeout(timer);\n leases.delete(message.id);\n done();\n };\n leases.set(message.id, finish);\n post({ kind: \"granted\", id: message.id });\n }),\n );\n return;\n case \"granted\":\n if (!isHolder()) void runGranted(message.id);\n return;\n case \"release\":\n leases.get(message.id)?.();\n return;\n case \"exec\":\n if (!isHolder() || !leases.has(message.lease)) return;\n local!\n .exec(message.sql, message.bind)\n .then((rows) => post({ kind: \"result\", id: message.id, ok: true, rows }))\n .catch((error) =>\n post({ kind: \"result\", id: message.id, ok: false, error: String(error?.message ?? error) }),\n );\n return;\n case \"result\": {\n const waiting = waitingResults.get(message.id);\n if (!waiting) return;\n waitingResults.delete(message.id);\n if (message.ok) waiting.resolve(message.rows);\n else waiting.reject(new Error(message.error));\n return;\n }\n case \"shutdown\":\n void close().finally(() => onShutdown?.());\n return;\n }\n };\n\n const becomeHolder = async () => {\n local = await openWithRetry(openLocal);\n created = local.created;\n // Leases this page asked for and nobody granted: they run here now.\n for (const [id, waiting] of waitingLeases) {\n waitingLeases.delete(id);\n mutex(waiting.run).then(waiting.resolve, waiting.reject);\n }\n post({ kind: \"holder\" });\n };\n\n /** Holds the lock as the holder until `close`; the lock passes to the next page when this ends. */\n const holdLock = async (): Promise<void> => {\n if (closed) return;\n await becomeHolder();\n await new Promise<void>((resolve) => (releaseLock = resolve));\n };\n\n // Free right now: this page is the holder. Held by another page: this page starts as its client\n // and queues for the lock, to take over when the holder leaves.\n let opened: Promise<void> | undefined;\n const isFree = await new Promise<boolean>((answer) => {\n navigator.locks\n .request(lockName(userId), { ifAvailable: true }, async (lock) => {\n if (!lock) {\n answer(false);\n return;\n }\n answer(true);\n opened = becomeHolder();\n try {\n await opened;\n } catch {\n return; // Could not open: the lock is released for the next page.\n }\n await new Promise<void>((resolve) => (releaseLock = resolve));\n })\n .catch(() => answer(false));\n });\n if (isFree) {\n try {\n await opened;\n } catch (error) {\n closed = true;\n channel.close();\n throw error;\n }\n } else {\n navigator.locks.request(lockName(userId), { signal: lockAbort.signal }, holdLock).catch((error) => {\n if ((error as { name?: string })?.name !== \"AbortError\")\n console.warn(\"[offline] taking over the device copy failed\", error);\n });\n }\n\n const remoteExec: ExecFn = (sql, bind) => {\n if (closed) return Promise.reject(new Error(\"The device database is closed.\"));\n const lease = currentLease;\n if (!lease) return exclusive(() => remoteExec(sql, bind));\n const id = newId();\n return new Promise<Record<string, unknown>[]>((resolve, reject) => {\n const timer = setTimeout(() => {\n waitingResults.delete(id);\n reject(new Error(\"The page holding the device copy did not answer.\"));\n }, EXEC_TIMEOUT_MS);\n waitingResults.set(id, {\n resolve: (rows) => {\n clearTimeout(timer);\n resolve(rows);\n },\n reject: (error) => {\n clearTimeout(timer);\n reject(error);\n },\n });\n post({ kind: \"exec\", id, lease, sql, bind });\n });\n };\n\n const exclusive = <T>(fn: () => Promise<T>): Promise<T> => {\n if (closed) return Promise.reject(new Error(\"The device database is closed.\"));\n if (isHolder()) return mutex(fn);\n const id = newId();\n return new Promise<T>((resolve, reject) => {\n waitingLeases.set(id, {\n run: fn as () => Promise<unknown>,\n resolve: resolve as (v: unknown) => void,\n reject,\n });\n post({ kind: \"lease\", id });\n });\n };\n\n const exec: ExecFn = (sql, bind) => (isHolder() ? local!.exec(sql, bind) : remoteExec(sql, bind));\n\n async function close(): Promise<void> {\n if (closed) return;\n closed = true;\n lockAbort.abort();\n const error = new Error(\"The device database is closed.\");\n for (const waiting of waitingLeases.values()) waiting.reject(error);\n for (const waiting of waitingResults.values()) waiting.reject(error);\n waitingLeases.clear();\n waitingResults.clear();\n for (const finish of [...leases.values()]) finish();\n try {\n if (local) await local.close();\n } finally {\n local = undefined;\n releaseLock?.();\n channel.close();\n }\n }\n\n return {\n get created() {\n return created;\n },\n exec,\n exclusive,\n close,\n };\n}\n\n/**\n * Asks every page to close the shared copy of `userId`, then runs `fn` once no page holds it\n * (logout, offline mode turned off: the files can only be deleted when nobody has them open).\n */\nexport async function withDatabaseReleased<T>(userId: string, fn: () => Promise<T>): Promise<T> {\n if (!canShareDatabase()) return fn();\n const channel = new BroadcastChannel(channelName(userId));\n channel.postMessage({ kind: \"shutdown\" } satisfies Message);\n channel.close();\n return navigator.locks.request(lockName(userId), () => fn()) as Promise<T>;\n}\n","import type { ExecFn } from \"./sql-device-store\";\nimport type { WorkerRequest, WorkerResponse } from \"./worker\";\n\nexport type OpenedDatabase = {\n exec: ExecFn;\n close: () => Promise<void>;\n /** True when the file did not exist before this open. */\n created: boolean;\n};\n\ntype Pending = { resolve: (value: unknown) => void; reject: (error: Error) => void };\n\n/** Starts the device-copy worker and opens `fileName` in it for `userId`. */\nexport async function openWorkerDatabase(\n userId: string,\n fileName: string,\n urls: { workerUrl: string; sqliteModuleUrl: string },\n): Promise<OpenedDatabase> {\n // A plain URL, so the app's bundler leaves it alone: the worker is a standalone file the app\n // serves (OfflineConfig.deviceWorkerUrl), precached by the service worker for offline use.\n const worker = new Worker(urls.workerUrl, { type: \"module\" });\n const { sqliteModuleUrl } = urls;\n const pending = new Map<number, Pending>();\n let nextId = 1;\n let closed = false;\n\n worker.onmessage = (event: MessageEvent<WorkerResponse>) => {\n const response = event.data;\n const waiting = pending.get(response.id);\n if (!waiting) return;\n pending.delete(response.id);\n if (response.ok) waiting.resolve(response.result);\n else waiting.reject(new Error(response.error));\n };\n worker.onerror = (event: ErrorEvent) => {\n const error = new Error(event.message || \"The device database worker failed.\");\n for (const waiting of pending.values()) waiting.reject(error);\n pending.clear();\n };\n\n const send = <T>(request: Omit<WorkerRequest, \"id\"> & { op: WorkerRequest[\"op\"] }): Promise<T> => {\n if (closed) return Promise.reject(new Error(\"The device database is closed.\"));\n const id = nextId++;\n return new Promise<T>((resolve, reject) => {\n pending.set(id, { resolve: resolve as (value: unknown) => void, reject });\n worker.postMessage({ ...request, id } as WorkerRequest);\n });\n };\n\n let opened: { created: boolean };\n try {\n opened = await send<{ created: boolean }>({ op: \"open\", userId, fileName, sqliteModuleUrl } as WorkerRequest);\n } catch (error) {\n worker.terminate();\n throw error;\n }\n\n return {\n created: opened.created,\n exec: (sql, bind) => send<Record<string, unknown>[]>({ op: \"exec\", sql, bind } as WorkerRequest),\n close: async () => {\n if (closed) return;\n try {\n await send({ op: \"close\" } as WorkerRequest);\n } finally {\n closed = true;\n worker.terminate();\n }\n },\n };\n}\n","import type { Sqlite3Static } from \"@sqlite.org/sqlite-wasm\";\nimport {\n DeviceStoreUnavailableError,\n forgetOfflineStore,\n getDeviceWorkerUrl,\n getSqliteModuleUrl,\n isOfflineAvailable,\n isOfflineStoreBlocked,\n markOfflineUnavailable,\n OfflineUnavailableError,\n refreshOfflineDeviceState,\n unblockOfflineStore,\n} from \"../config\";\nimport { deleteImagesCaches } from \"../sync/artwork-cache\";\nimport type { DeviceStore } from \"./DeviceStore\";\nimport { createSqlDeviceStore } from \"./sql-device-store\";\nimport { canShareDatabase, openSharedDatabase, withDatabaseReleased, type SharedDatabase } from \"./shared-database\";\nimport { openWorkerDatabase, type OpenedDatabase } from \"./worker-client\";\n\n/** The part of an OPFS `FileSystemDirectoryHandle` the lifecycle uses (faked in tests). */\nexport type OfflineDirectory = {\n keys(): AsyncIterable<string>;\n getFileHandle(name: string, options?: { create?: boolean }): Promise<unknown>;\n removeEntry(name: string, options?: { recursive?: boolean }): Promise<void>;\n};\n\nexport type DeviceStoreBackend = {\n /** The OPFS root that holds every user's device file. */\n getDirectory: () => Promise<OfflineDirectory>;\n /** Opens `fileName` for `userId` (the worker in the browser). */\n openDatabase: (userId: string, fileName: string) => Promise<OpenedDatabase>;\n /** Asks the browser to keep the device copy (`navigator.storage.persist()`). */\n requestPersistence: () => Promise<void>;\n};\n\nconst FILE_PREFIX = \"nja-offline-\";\nconst deviceFileName = (userId: string) => `${FILE_PREFIX}${userId}.sqlite3`;\n/** Present while the user's file holds unsent changes; read without opening the file. */\nconst pendingMarkerName = (userId: string) => `${FILE_PREFIX}${userId}.pending`;\n/** Names the user whose artwork the images cache holds (the images cache is not per user). */\nconst IMAGES_OWNER_PREFIX = \"nja-offline-images-owner-\";\nconst imagesOwnerName = (userId: string) => `${IMAGES_OWNER_PREFIX}${userId}`;\nconst FILE_PATTERN = /^nja-offline-(.+)\\.(?:sqlite3(?:-journal|-wal|-shm)?|pending)$/;\nconst POOL_PATTERN = /^\\.nja-offline-(.+)$/;\n\n/**\n * Why this browser cannot keep the device copy, or null when it may: browsers expose the private\n * file storage only in a secure context (https, or `localhost`). Checked before any worker starts.\n */\nconst missingDeviceStorage = (): string | null => {\n if (typeof window !== \"undefined\" && window.isSecureContext === false) {\n return \"the page is not served from a secure origin (https or localhost)\";\n }\n if (typeof navigator === \"undefined\" || typeof navigator.storage?.getDirectory !== \"function\") {\n return \"the browser offers no private file storage\";\n }\n return null;\n};\n\nconst assertDeviceStorage = (): void => {\n const reason = missingDeviceStorage();\n if (reason) throw new OfflineUnavailableError(reason);\n};\n\n/** What the SQLite worker answers when the browser lacks the OPFS sync-access handles. */\nconst OPFS_MISSING = /Missing required OPFS APIs/i;\n\nconst defaultBackend: DeviceStoreBackend = {\n getDirectory: async () => {\n assertDeviceStorage();\n try {\n return (await navigator.storage.getDirectory()) as unknown as OfflineDirectory;\n } catch (error) {\n // Storage refused outright (some private windows).\n if ((error as { name?: string })?.name === \"SecurityError\") {\n throw new OfflineUnavailableError(\"the browser refused its private file storage\");\n }\n throw error;\n }\n },\n openDatabase: async (userId, fileName) => {\n assertDeviceStorage();\n const openLocal = () =>\n openWorkerDatabase(userId, fileName, {\n workerUrl: getDeviceWorkerUrl(),\n sqliteModuleUrl: getSqliteModuleUrl(),\n });\n try {\n // Shared by every page of the origin: only one page at a time can hold the files.\n if (!canShareDatabase()) return await openLocal();\n return await openSharedDatabase(userId, openLocal, () => forgetClosedStore(userId));\n } catch (error) {\n if (OPFS_MISSING.test(String((error as Error)?.message ?? error))) {\n throw new OfflineUnavailableError(\"the browser lacks the file access the device copy needs (OPFS)\");\n }\n throw error;\n }\n },\n requestPersistence: async () => {\n if (typeof navigator !== \"undefined\" && navigator.storage?.persist) await navigator.storage.persist();\n },\n};\n\nlet backend: DeviceStoreBackend = defaultBackend;\n\ntype OpenStore = { userId: string; store: Promise<DeviceStore>; database: Promise<OpenedDatabase> };\nlet current: OpenStore | undefined;\n\n/** Replaces the OPFS directory / database opener (tests); `null` restores the browser one. */\nexport function setDeviceStoreBackend(next: Partial<DeviceStoreBackend> | null): void {\n backend = next ? { ...defaultBackend, ...next } : defaultBackend;\n current = undefined;\n}\n\nconst entryExists = async (directory: OfflineDirectory, name: string): Promise<boolean> => {\n for await (const key of directory.keys()) if (key === name) return true;\n return false;\n};\n\nconst removeIfPresent = async (directory: OfflineDirectory, name: string): Promise<void> => {\n try {\n await directory.removeEntry(name, { recursive: true });\n } catch (error) {\n if ((error as { name?: string })?.name !== \"NotFoundError\") throw error;\n }\n};\n\nconst entriesOf = async (directory: OfflineDirectory, userId: string): Promise<string[]> => {\n const names: string[] = [];\n for await (const key of directory.keys()) if (userIdOf(key) === userId) names.push(key);\n return names;\n};\n\n/**\n * The OPFS root, or undefined when this browser cannot keep the device copy: offline mode is then\n * switched off (`markOfflineUnavailable`). Any other failure is thrown as before.\n */\nconst deviceDirectory = async (): Promise<OfflineDirectory | undefined> => {\n if (!isOfflineAvailable()) return undefined;\n try {\n return await backend.getDirectory();\n } catch (error) {\n if (!(error instanceof OfflineUnavailableError)) throw error;\n markOfflineUnavailable(error);\n return undefined;\n }\n};\n\nconst userIdOf = (name: string): string | undefined => FILE_PATTERN.exec(name)?.[1] ?? POOL_PATTERN.exec(name)?.[1];\n\nconst closeCurrent = async (): Promise<void> => {\n const open = current;\n current = undefined;\n if (!open) return;\n try {\n await (await open.database).close();\n } catch {\n // A database that never opened has nothing to close.\n }\n};\n\n/**\n * Opens the device copy of `userId` (`nja-offline-<userId>.sqlite3` in the OPFS,\n * through the worker). Only one user's file is open at a time: opening another\n * user's file closes the previous one first.\n */\nexport function openDeviceStore(userId: string): Promise<DeviceStore> {\n if (!userId) return Promise.reject(new Error(\"openDeviceStore needs the signed-in user id.\"));\n if (isOfflineStoreBlocked(userId)) {\n return Promise.reject(new DeviceStoreUnavailableError(\"The device copy was deleted at logout\"));\n }\n if (!isOfflineAvailable()) {\n return Promise.reject(new OfflineUnavailableError(\"it was found unable to keep the device copy\"));\n }\n if (current?.userId === userId) return current.store;\n\n const previous = closeCurrent();\n const database = previous\n .then(() => backend.openDatabase(userId, deviceFileName(userId)))\n .catch((error) => {\n if (error instanceof OfflineUnavailableError) markOfflineUnavailable(error);\n throw error;\n });\n const store = database.then(async (opened) => {\n if (opened.created) await backend.requestPersistence();\n return createSqlDeviceStore(opened.exec, {\n exclusive: (opened as Partial<SharedDatabase>).exclusive,\n beforeEnqueue: async () => {\n const directory = await backend.getDirectory();\n await directory.getFileHandle(pendingMarkerName(userId), { create: true });\n },\n afterQueueChange: async (entries) => {\n if (entries > 0) return;\n await removeIfPresent(await backend.getDirectory(), pendingMarkerName(userId));\n },\n });\n });\n const open: OpenStore = { userId, store, database };\n current = open;\n store.catch(() => {\n if (current === open) current = undefined;\n });\n return store;\n}\n\nlet sqlite3Promise: Promise<Sqlite3Static> | undefined;\n\n/**\n * An in-memory SQLite database (no worker, no OPFS), for tests in Node. The package is imported\n * here at run time and hidden from app bundlers (see `OfflineConfig.sqliteModuleUrl`).\n */\nexport async function openMemoryDatabase(): Promise<OpenedDatabase> {\n sqlite3Promise ??= import(/* webpackIgnore: true */ /* turbopackIgnore: true */ \"@sqlite.org/sqlite-wasm\").then(\n (module) => module.default(),\n );\n const sqlite3 = await sqlite3Promise;\n const db = new sqlite3.oo1.DB(\":memory:\", \"c\");\n return {\n created: true,\n exec: async (sql, bind) =>\n db.exec({\n sql,\n bind: bind && bind.length ? (bind as any) : undefined,\n rowMode: \"object\",\n returnValue: \"resultRows\",\n }) as Record<string, unknown>[],\n close: async () => {\n db.close();\n },\n };\n}\n\n/** The same store and schema over an in-memory database, for tests. */\nexport async function openMemoryDeviceStore(): Promise<DeviceStore> {\n const database = await openMemoryDatabase();\n return createSqlDeviceStore(database.exec);\n}\n\n/**\n * Another page closed every page's copy of `userId` (logout, offline mode off): this page forgets\n * its store, and reads the device switch again before anything opens the copy.\n */\nfunction forgetClosedStore(userId: string): void {\n if (current?.userId === userId) current = undefined;\n forgetOfflineStore(userId);\n refreshOfflineDeviceState();\n}\n\n/**\n * Closes `userId`'s device copy in every page and removes every OPFS entry of it. The files can\n * only be removed once no page holds them, so the removal waits for every page to let go.\n */\nexport async function deleteDeviceStore(userId: string): Promise<void> {\n if (current?.userId === userId) await closeCurrent();\n const directory = await deviceDirectory();\n if (!directory) return;\n const remove = async () => {\n const names = await entriesOf(directory, userId);\n const marker = pendingMarkerName(userId);\n for (const name of names) if (name !== marker) await removeIfPresent(directory, name);\n await removeIfPresent(directory, marker);\n };\n if (backend === defaultBackend) await withDatabaseReleased(userId, remove);\n else await remove();\n}\n\n/** Whether `userId`'s device copy holds unsent changes. Never opens another user's file. */\nexport async function hasUnsentChanges(userId: string): Promise<boolean> {\n if (current?.userId === userId) {\n const store = await current.store;\n if ((await store.queue()).length > 0) return true;\n }\n const directory = await deviceDirectory();\n if (!directory) return false;\n return entryExists(directory, pendingMarkerName(userId));\n}\n\n/**\n * A user signed in on this browser: their device copy may be opened again (it is blocked after\n * a logout), and every other user's device copy is deleted when it has nothing unsent, and kept\n * untouched (never opened) when it has. When the images cache was not this user's (another user\n * signed in before, or no owner is known), it is deleted: the user's sync downloads their artwork.\n */\nexport async function onUserSignedIn(userId: string): Promise<void> {\n unblockOfflineStore(userId);\n if (current && current.userId !== userId) await closeCurrent();\n // A browser that cannot keep the device copy: offline mode switches off here, before the runtime starts.\n const directory = await deviceDirectory();\n if (!directory) return;\n const others = new Set<string>();\n const imageOwners: string[] = [];\n for await (const key of directory.keys()) {\n if (key.startsWith(IMAGES_OWNER_PREFIX)) {\n imageOwners.push(key);\n continue;\n }\n const owner = userIdOf(key);\n if (owner && owner !== userId) others.add(owner);\n }\n if (!imageOwners.includes(imagesOwnerName(userId))) {\n await deleteImagesCaches();\n for (const name of imageOwners) await removeIfPresent(directory, name);\n await directory.getFileHandle(imagesOwnerName(userId), { create: true });\n }\n for (const other of others) {\n if (await entryExists(directory, pendingMarkerName(other))) continue;\n await deleteDeviceStore(other);\n }\n}\n\n/** Locked now: the server said so, or the lock date it gave has passed. */\nexport function isRootLocked(s: { locked: boolean; lockedFrom: Date | null }, now: Date): boolean {\n if (s.locked) return true;\n return s.lockedFrom !== null && s.lockedFrom.getTime() <= now.getTime();\n}\n"]}
@@ -1,17 +1,18 @@
1
1
  import {
2
2
  CURRENT_USER_STORAGE_KEY,
3
3
  getClientApiUrl
4
- } from "./chunk-DOCIFE6A.mjs";
4
+ } from "./chunk-AUD6ZLBB.mjs";
5
5
  import {
6
6
  NeedsConnectionError,
7
7
  getOfflineConfig,
8
8
  getOfflineState,
9
9
  getOfflineStore,
10
+ rootKinds,
10
11
  setOfflineState
11
- } from "./chunk-27LQDGNP.mjs";
12
+ } from "./chunk-4ZJIUTIV.mjs";
12
13
  import {
13
14
  getApiUrl
14
- } from "./chunk-DGWOVYEC.mjs";
15
+ } from "./chunk-MQ7TXALH.mjs";
15
16
  import {
16
17
  __name
17
18
  } from "./chunk-PAWJFY3S.mjs";
@@ -277,8 +278,8 @@ async function getOne(ctx, type) {
277
278
  return singleAnswer(ctx.store, record);
278
279
  }
279
280
  __name(getOne, "getOne");
280
- async function rootList(ctx, config, typeConfig) {
281
- const rootType = config.rootModule.name;
281
+ async function rootList(ctx, kind, typeConfig) {
282
+ const rootType = kind.module.name;
282
283
  const rootId = ctx.params.rootId;
283
284
  if (!await ctx.store.getRecord(rootType, rootId)) return notOnDeviceAnswer(rootType, rootId);
284
285
  const records = await ctx.store.listRecords({ type: typeConfig.module.name, rootId });
@@ -320,10 +321,10 @@ async function relatedList(ctx, config, typeConfig) {
320
321
  }
321
322
  __name(relatedList, "relatedList");
322
323
  async function rootIdForResource(store, config, resource) {
323
- const rootType = config.rootModule.name;
324
- if (resource?.type === rootType && resource.id) return resource.id;
324
+ const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));
325
+ if (rootTypes.has(resource?.type) && resource.id) return resource.id;
325
326
  const identifiers = Object.values(resource?.relationships ?? {}).flatMap(identifiersOf);
326
- const root = identifiers.find((identifier) => identifier.type === rootType);
327
+ const root = identifiers.find((identifier) => rootTypes.has(identifier.type));
327
328
  if (root) return root.id;
328
329
  for (const identifier of identifiers) {
329
330
  const stored = await store.getRecord(identifier.type, identifier.id);
@@ -405,6 +406,16 @@ async function create(ctx, config, type) {
405
406
  body: ctx.request.body,
406
407
  base: null
407
408
  });
409
+ if (typeConfig?.afterLocalWrite) {
410
+ await typeConfig.afterLocalWrite({
411
+ store,
412
+ module: typeConfig.module,
413
+ id,
414
+ method: "POST",
415
+ resource,
416
+ previous: null
417
+ });
418
+ }
408
419
  });
409
420
  return singleAnswer(ctx.store, record, 201);
410
421
  }
@@ -418,16 +429,12 @@ async function update(ctx, config, type) {
418
429
  const before = existing.resource ?? { type, id };
419
430
  const resource = mergeResource(before, { type, id }, data);
420
431
  const base = existing.localOnly ? null : existing.base ?? before;
432
+ const typeConfig = typeConfigOf(config, type);
433
+ const method = ctx.request.method === "PATCH" ? "PATCH" : "PUT";
421
434
  const record = { ...existing, resource, base, localOnly: existing.localOnly };
422
435
  await ctx.store.transaction(async (store) => {
423
436
  await store.putRecord(record);
424
- await linkRelationships(
425
- store,
426
- typeConfigOf(config, type),
427
- { type, id },
428
- before.relationships ?? {},
429
- data.relationships ?? {}
430
- );
437
+ await linkRelationships(store, typeConfig, { type, id }, before.relationships ?? {}, data.relationships ?? {});
431
438
  await store.enqueue({
432
439
  rootId: existing.rootId,
433
440
  method: ctx.request.method,
@@ -437,11 +444,14 @@ async function update(ctx, config, type) {
437
444
  recordId: id,
438
445
  base
439
446
  });
447
+ if (typeConfig?.afterLocalWrite) {
448
+ await typeConfig.afterLocalWrite({ store, module: typeConfig.module, id, method, resource, previous: before });
449
+ }
440
450
  });
441
451
  return singleAnswer(ctx.store, record);
442
452
  }
443
453
  __name(update, "update");
444
- async function remove(ctx, type) {
454
+ async function remove(ctx, config, type) {
445
455
  const id = ctx.params.id;
446
456
  const existing = await ctx.store.getRecord(type, id);
447
457
  if (!existing) return notOnDeviceAnswer(type, id);
@@ -459,6 +469,17 @@ async function remove(ctx, type) {
459
469
  recordId: id,
460
470
  base
461
471
  });
472
+ const typeConfig = typeConfigOf(config, type);
473
+ if (typeConfig?.afterLocalWrite) {
474
+ await typeConfig.afterLocalWrite({
475
+ store,
476
+ module: typeConfig.module,
477
+ id,
478
+ method: "DELETE",
479
+ resource: null,
480
+ previous: existing.resource
481
+ });
482
+ }
462
483
  });
463
484
  return { status: 204, body: void 0 };
464
485
  }
@@ -478,7 +499,9 @@ function buildGenericRoutes(config) {
478
499
  for (const typeConfig of config.types) {
479
500
  const type = typeConfig.module.name;
480
501
  route("GET", [typeConfig.module, ":id"], (ctx) => getOne(ctx, type));
481
- route("GET", [config.rootModule, ":rootId", typeConfig.module], (ctx) => rootList(ctx, config, typeConfig));
502
+ for (const kind of rootKinds(config)) {
503
+ route("GET", [kind.module, ":rootId", typeConfig.module], (ctx) => rootList(ctx, kind, typeConfig));
504
+ }
482
505
  route(
483
506
  "POST",
484
507
  [typeConfig.module],
@@ -497,7 +520,7 @@ function buildGenericRoutes(config) {
497
520
  route(
498
521
  "DELETE",
499
522
  [typeConfig.module, ":id"],
500
- readOnlyGuard(typeConfig, (ctx) => remove(ctx, type))
523
+ readOnlyGuard(typeConfig, (ctx) => remove(ctx, config, type))
501
524
  );
502
525
  }
503
526
  for (const typeConfig of config.types) {
@@ -628,9 +651,10 @@ async function localTransport(request) {
628
651
  return toApiData(needsConnectionAnswer("The device copy is not available in this tab"));
629
652
  }
630
653
  const ctx = { request: offlineRequest, params: match.params, store };
654
+ const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));
631
655
  const customRootId = /* @__PURE__ */ __name(async (route) => {
632
656
  const rootIndex = route.pattern.findIndex(
633
- (segment) => typeof segment !== "string" && segment.name === config.rootModule.name
657
+ (segment) => typeof segment !== "string" && rootTypes.has(segment.name)
634
658
  );
635
659
  const next = rootIndex >= 0 ? route.pattern[rootIndex + 1] : void 0;
636
660
  const rootParam = typeof next === "string" && next.startsWith(":") ? next.slice(1) : void 0;
@@ -701,4 +725,4 @@ export {
701
725
  splitApiUrl,
702
726
  localTransport
703
727
  };
704
- //# sourceMappingURL=chunk-7JZHPMGW.mjs.map
728
+ //# sourceMappingURL=chunk-A3RB47X4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/offline/generic-answers.ts","../src/offline/router.ts","../src/offline/local-transport.ts"],"sourcesContent":["import { CURRENT_USER_STORAGE_KEY } from \"../features/user/data/user.storage\";\nimport { getOfflineConfig, rootKinds } from \"./config\";\nimport { getOfflineState } from \"./state\";\nimport type { DeviceStore, StoredLink, StoredRecord } from \"./store/DeviceStore\";\nimport type {\n LocalAnswerContext,\n OfflineAnswer,\n OfflineConfig,\n OfflineMethod,\n OfflineRootKind,\n OfflineRoute,\n OfflineTypeConfig,\n} from \"./types\";\n\nconst DEFAULT_PAGE_SIZE = 25;\n\n// ---------------------------------------------------------------------------------------------\n// Answers shared by every route\n// ---------------------------------------------------------------------------------------------\n\n/** A JSON:API errors document with one error, answered with `status`. */\nexport function errorAnswer(status: number, title: string, detail: string, code?: string): OfflineAnswer {\n return {\n status,\n body: { errors: [{ status: String(status), ...(code ? { code } : {}), title, detail }] },\n };\n}\n\nexport function needsConnectionAnswer(detail: string = \"Needs a connection\"): OfflineAnswer {\n return errorAnswer(503, \"Needs a connection\", detail, \"needs-connection\");\n}\n\nexport function notFoundAnswer(detail: string): OfflineAnswer {\n return errorAnswer(404, \"Not found\", detail);\n}\n\n/** A single document of one stored record, with `included` (`includedFor`). */\nexport async function singleAnswer(\n store: DeviceStore,\n record: StoredRecord,\n status: number = 200,\n): Promise<OfflineAnswer> {\n return { status, body: { data: record.resource, included: await includedFor(store, [record]) } };\n}\n\nexport function isRootWriteLocked(rootId: string | undefined): boolean {\n return !!rootId && getOfflineState().lockedRootIds.includes(rootId);\n}\n\n// ---------------------------------------------------------------------------------------------\n// Resource helpers\n// ---------------------------------------------------------------------------------------------\n\ntype ResourceIdentifier = { type: string; id: string };\n\nfunction identifiersOf(relationship: any): ResourceIdentifier[] {\n const data = relationship?.data;\n if (!data) return [];\n return (Array.isArray(data) ? data : [data]).filter((item: any) => item && item.type && item.id);\n}\n\nfunction linkedIds(links: StoredLink[], id: string): ResourceIdentifier[] {\n return links.map((link) =>\n link.fromId === id ? { type: link.toType, id: link.toId } : { type: link.fromType, id: link.fromId },\n );\n}\n\n/** Every stored resource linked to one of `ids`, in either direction, without duplicates or the records themselves. */\n/** The `{ type, id }` every relationship of a JSON:API resource points to. */\nfunction relationshipTargets(resource: any): { type: string; id: string }[] {\n const targets: { type: string; id: string }[] = [];\n for (const relationship of Object.values<any>(resource?.relationships ?? {})) {\n const data = relationship?.data;\n for (const ref of Array.isArray(data) ? data : data ? [data] : []) {\n if (ref?.type && ref?.id) targets.push({ type: ref.type, id: ref.id });\n }\n }\n return targets;\n}\n\n/**\n * The `included` of a local answer: the stored records linked to `records` (the synced links),\n * then the stored records their relationships point to, and one level further the stored records\n * the relationships of those included records point to (an NPC's root record, then the root record's\n * owner), so a model reads the same nested relationships it reads from the server.\n *\n * Stored resources point at each other (a root record at its owner, the owner at the root record's\n * company, the company back at the owner); `RehydrationFactory` skips a resource met again inside\n * its own chain, so those circles never recurse.\n */\nexport async function includedFor(store: DeviceStore, records: StoredRecord[]): Promise<any[]> {\n // 0: the answer's own records; 1: what they link to; 2: what those link to.\n const level = new Map<string, number>(records.map((record) => [`${record.type}:${record.id}`, 0]));\n const included: { resource: any; level: number }[] = [];\n\n const add = async (target: { type: string; id: string }, atLevel: number): Promise<void> => {\n const key = `${target.type}:${target.id}`;\n if (level.has(key)) return;\n level.set(key, atLevel);\n const stored = await store.getRecord(target.type, target.id);\n const resource = stored?.resource ?? signedInUserResource(target);\n if (resource) included.push({ resource, level: atLevel });\n };\n\n for (const record of records) {\n const links = [...(await store.linksFrom(record.id)), ...(await store.linksTo(record.id))];\n for (const target of linkedIds(links, record.id)) await add(target, 1);\n }\n for (const record of records) {\n for (const target of relationshipTargets(record.resource)) await add(target, 1);\n }\n for (const entry of included.filter((candidate) => candidate.level === 1)) {\n for (const target of relationshipTargets(entry.resource)) await add(target, 2);\n }\n return included.map((entry) => entry.resource);\n}\n\n/**\n * The signed-in user as a resource, when `target` is that user. Users are not kept on the device,\n * but a relationship to the signed-in user (every downloaded root record's owner) must still resolve:\n * the app tells a GM from a player by comparing the root record's owner with the signed-in user. The\n * resource comes from the copy CurrentUserProvider keeps in localStorage, without relationships.\n */\nfunction signedInUserResource(target: { type: string; id: string }): any | undefined {\n try {\n if (target.id !== getOfflineConfig().getUserId()) return undefined;\n const stored = JSON.parse(window.localStorage.getItem(CURRENT_USER_STORAGE_KEY) ?? \"null\");\n const user = stored?.jsonApi;\n if (user?.type !== target.type || user?.id !== target.id) return undefined;\n return { type: user.type, id: user.id, attributes: user.attributes ?? {}, meta: user.meta };\n } catch {\n return undefined;\n }\n}\n\nfunction attributeOf(record: StoredRecord, name: string): unknown {\n const value = record.resource?.attributes?.[name];\n if (value !== undefined && value !== null) return value;\n // The server value the sync keeps beside the resource.\n if (name === \"updatedAt\") return record.updatedAt ?? null;\n return value;\n}\n\n/** The server's default list order (`defineEntity` sets `defaultOrderBy: \"updatedAt DESC\"`). */\nexport const SERVER_DEFAULT_ORDER_BY = \"updatedAt DESC\";\n\n/** The only `orderBy` the server accepts (`buildLegacyOrderByClause`): one property, optional ASC/DESC. */\nconst ORDER_BY = /^([A-Za-z_][A-Za-z0-9_]*)(?:\\s+(ASC|DESC))?$/i;\n\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}(T|$)/;\n\nfunction comparable(value: unknown): unknown {\n if (value instanceof Date) return value.getTime();\n if (typeof value === \"string\" && ISO_DATE.test(value)) {\n const time = Date.parse(value);\n if (!Number.isNaN(time)) return time;\n }\n return value;\n}\n\n/** Cypher ORDER BY on one property, ascending: null sorts after every value. */\nfunction compareAscending(left: unknown, right: unknown): number {\n const a = comparable(left);\n const b = comparable(right);\n const aNull = a === undefined || a === null;\n const bNull = b === undefined || b === null;\n if (aNull || bNull) return aNull === bNull ? 0 : aNull ? 1 : -1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") return Number(a) - Number(b);\n const x = String(a);\n const y = String(b);\n return x < y ? -1 : x > y ? 1 : 0;\n}\n\nexport type ListOptions = {\n /** The string attributes the server's fulltext index covers (default: every string attribute). */\n searchFields?: string[];\n /** The order with no `orderBy` (default `updatedAt DESC`, the server's). */\n defaultOrderBy?: string;\n};\n\n/**\n * `search` as the server runs it: the term is split into letter/digit tokens and a record\n * matches when every token appears, case-insensitive, inside one of its indexed string\n * attributes (the fulltext query `*token* AND ...`).\n */\nexport function searchRecords(\n records: StoredRecord[],\n query: URLSearchParams,\n options: ListOptions = {},\n): StoredRecord[] {\n const term = query.get(\"search\");\n if (term === null || term === undefined || term.trim() === \"\") return records;\n const tokens = term\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n if (tokens.length === 0) return records;\n\n return records.filter((record) => {\n const attributes = record.resource?.attributes ?? {};\n const fields = options.searchFields ?? Object.keys(attributes);\n const texts = fields\n .map((field) => attributes[field])\n .filter((value): value is string => typeof value === \"string\")\n .map((value) => value.toLowerCase());\n return tokens.every((token) => texts.some((text) => text.includes(token)));\n });\n}\n\n/**\n * Orders records as the server's `ORDER BY` does for `orderBy` (`field` or `field ASC|DESC`),\n * or for the default order when there is none. `null` when the server would refuse the\n * `orderBy` (400).\n */\nexport function orderRecords(\n records: StoredRecord[],\n query: URLSearchParams,\n options: ListOptions = {},\n): StoredRecord[] | null {\n const requested = query.get(\"orderBy\")?.trim();\n const value = requested || options.defaultOrderBy?.trim() || SERVER_DEFAULT_ORDER_BY;\n const match = ORDER_BY.exec(value);\n if (!match) return null;\n const field = match[1];\n const desc = match[2]?.toUpperCase() === \"DESC\";\n return [...records].sort((left, right) => {\n const order = compareAscending(attributeOf(left, field), attributeOf(right, field));\n return desc ? -order : order;\n });\n}\n\nfunction linkUrl(base: string, query: URLSearchParams, changes: Record<string, string | undefined>): string {\n const params = new URLSearchParams(query);\n for (const [key, value] of Object.entries(changes)) {\n if (value === undefined) params.delete(key);\n else params.set(key, value);\n }\n const search = params.toString().replace(/%5B/g, \"[\").replace(/%5D/g, \"]\").replace(/%2C/g, \",\");\n return search ? `${base}?${search}` : base;\n}\n\n/**\n * One list answer for a set of stored records, as the server answers a list: `search`, then\n * `orderBy` (or the default order), then paged the way `JsonApiPaginator` pages (`fetchAll`,\n * `page[size]`, `page[offset]`), with `included`. App answers pass the records they selected.\n */\nexport async function listAnswer(\n ctx: LocalAnswerContext,\n records: StoredRecord[],\n options: ListOptions = {},\n): Promise<OfflineAnswer> {\n const { request, store } = ctx;\n const base = request.url ? request.url.split(\"?\")[0] : request.path;\n const sorted = orderRecords(searchRecords(records, request.query, options), request.query, options);\n if (!sorted) {\n return errorAnswer(400, \"Bad Request\", \"Invalid orderBy\");\n }\n\n if (request.query.get(\"fetchAll\") === \"true\") {\n return {\n status: 200,\n body: {\n data: sorted.map((record) => record.resource),\n included: await includedFor(store, sorted),\n links: { self: linkUrl(base, request.query, {}) },\n meta: { total: sorted.length },\n },\n };\n }\n\n const size = Number(request.query.get(\"page[size]\")) || DEFAULT_PAGE_SIZE;\n const offset = Number(request.query.get(\"page[offset]\")) || 0;\n const page = sorted.slice(offset, offset + size);\n\n const links: Record<string, string> = {\n self: linkUrl(base, request.query, { \"page[size]\": String(size), \"page[offset]\": undefined }),\n };\n if (offset + size < sorted.length) {\n links.next = linkUrl(base, request.query, { \"page[size]\": String(size), \"page[offset]\": String(offset + size) });\n }\n if (offset > 0) {\n links.prev = linkUrl(base, request.query, {\n \"page[size]\": String(size),\n \"page[offset]\": String(Math.max(0, offset - size)),\n });\n // The server names it `previous`; the client reads `prev`.\n links.previous = links.prev;\n }\n\n return {\n status: 200,\n body: {\n data: page.map((record) => record.resource),\n included: await includedFor(store, page),\n links,\n meta: { total: sorted.length },\n },\n };\n}\n\n/**\n * A list document of stored records in the order the caller settled, every record in one page,\n * with `included`: for an app's custom list query that has its own `ORDER BY` and no paging.\n */\nexport async function orderedListAnswer(store: DeviceStore, records: StoredRecord[]): Promise<OfflineAnswer> {\n return {\n status: 200,\n body: {\n data: records.map((record) => record.resource),\n included: await includedFor(store, records),\n meta: { total: records.length },\n },\n };\n}\n\n/**\n * A list document of resources the device computed rather than stored (a query-result\n * projection such as a search hit, built by its model), answered as they are, nothing included.\n */\nexport function resourceListAnswer(resources: any[]): OfflineAnswer {\n return { status: 200, body: { data: resources, included: [] } };\n}\n\nfunction listOptionsOf(typeConfig: OfflineTypeConfig): ListOptions {\n return { searchFields: typeConfig.searchFields, defaultOrderBy: typeConfig.defaultOrderBy };\n}\n\n// ---------------------------------------------------------------------------------------------\n// Link kinds\n// ---------------------------------------------------------------------------------------------\n\n/**\n * The kind a link written on the device carries for relationship `name`: the Neo4j type the\n * type config maps it to (the kind synced links carry), otherwise the name itself.\n */\nexport function linkKindFor(typeConfig: OfflineTypeConfig | undefined, name: string): string {\n return typeConfig?.relationshipKinds?.[name] ?? name;\n}\n\n/**\n * Every kind a link of relationship `name` may carry on the device: the name (written on the\n * device before a mapping existed) and the mapped Neo4j type (synced). `undefined` when the\n * Neo4j type is not known: then a link of any kind may be the one.\n */\nexport function linkKindsOf(typeConfig: OfflineTypeConfig | undefined, name: string): string[] | undefined {\n const mapped = typeConfig?.relationshipKinds?.[name];\n return mapped ? [name, mapped] : undefined;\n}\n\n/**\n * The stored links of `id` whose kind is one of `kinds` (Neo4j types and/or relationship\n * names). `out`: links from `id`; `in`: links to `id`; `both`: either.\n */\nexport async function linksOfKind(\n store: DeviceStore,\n id: string,\n kinds: string[],\n direction: \"out\" | \"in\" | \"both\" = \"both\",\n): Promise<StoredLink[]> {\n const result: StoredLink[] = [];\n const seen = new Set<string>();\n for (const kind of new Set(kinds)) {\n const links = [\n ...(direction !== \"in\" ? await store.linksFrom(id, kind) : []),\n ...(direction !== \"out\" ? await store.linksTo(id, kind) : []),\n ];\n for (const link of links) {\n const key = `${link.fromId}|${link.toId}|${link.kind}`;\n if (seen.has(key)) continue;\n seen.add(key);\n result.push(link);\n }\n }\n return result;\n}\n\nfunction typeConfigOf(config: OfflineConfig, type: string): OfflineTypeConfig | undefined {\n return config.types.find((typeConfig) => typeConfig.module.name === type);\n}\n\n/** The relationship names of `resource` that point to a record of `type`. */\nfunction relationshipNamesToward(resource: any, type: string): string[] {\n return Object.entries<any>(resource?.relationships ?? {})\n .filter(([, relationship]) => identifiersOf(relationship).some((identifier) => identifier.type === type))\n .map(([name]) => name);\n}\n\n// ---------------------------------------------------------------------------------------------\n// Reads\n// ---------------------------------------------------------------------------------------------\n\n/** A record that is not on the device: only the server can answer for it. */\nfunction notOnDeviceAnswer(type: string, id: string): OfflineAnswer {\n return needsConnectionAnswer(`${type} ${id} is not on this device`);\n}\n\nasync function getOne(ctx: LocalAnswerContext, type: string): Promise<OfflineAnswer> {\n const record = await ctx.store.getRecord(type, ctx.params.id);\n if (!record) return notOnDeviceAnswer(type, ctx.params.id);\n return singleAnswer(ctx.store, record);\n}\n\nasync function rootList(\n ctx: LocalAnswerContext,\n kind: OfflineRootKind,\n typeConfig: OfflineTypeConfig,\n): Promise<OfflineAnswer> {\n const rootType = kind.module.name;\n const rootId = ctx.params.rootId;\n if (!(await ctx.store.getRecord(rootType, rootId))) return notOnDeviceAnswer(rootType, rootId);\n const records = await ctx.store.listRecords({ type: typeConfig.module.name, rootId });\n return listAnswer(ctx, records, listOptionsOf(typeConfig));\n}\n\n/**\n * `GET <parent>/:parentId/<type>`: the stored records of `type` linked to the parent by the\n * relationship between the two types. That relationship is the one whose name, on either\n * record, points to the other type; its links are matched by name and by mapped Neo4j type.\n * When its Neo4j type is not known, links of any kind count.\n */\nasync function relatedList(\n ctx: LocalAnswerContext,\n config: OfflineConfig,\n typeConfig: OfflineTypeConfig,\n): Promise<OfflineAnswer> {\n const type = typeConfig.module.name;\n const { parentType, parentId } = ctx.params;\n const parent = await ctx.store.getRecord(parentType, parentId);\n if (!parent) return notOnDeviceAnswer(parentType, parentId);\n\n const links = [...(await ctx.store.linksFrom(parentId)), ...(await ctx.store.linksTo(parentId))].filter((link) =>\n link.fromId === parentId ? link.toType === type : link.fromType === type,\n );\n const candidates = await ctx.store.listRecords({\n type,\n ids: [...new Set(links.map((link) => (link.fromId === parentId ? link.toId : link.fromId)))],\n });\n\n const kinds = new Set<string>();\n let anyKind = false;\n const collect = (owner: OfflineTypeConfig | undefined, names: string[]) => {\n for (const name of names) {\n const known = linkKindsOf(owner, name);\n if (known) known.forEach((kind) => kinds.add(kind));\n else anyKind = true;\n }\n };\n collect(typeConfig, [\n ...new Set(candidates.flatMap((record) => relationshipNamesToward(record.resource, parentType))),\n ]);\n collect(typeConfigOf(config, parentType), relationshipNamesToward(parent.resource, type));\n\n const accepted = anyKind || kinds.size === 0 ? links : links.filter((link) => kinds.has(link.kind));\n const ids = new Set(accepted.map((link) => (link.fromId === parentId ? link.toId : link.fromId)));\n return listAnswer(\n ctx,\n candidates.filter((record) => ids.has(record.id)),\n listOptionsOf(typeConfig),\n );\n}\n\n// ---------------------------------------------------------------------------------------------\n// Writes\n// ---------------------------------------------------------------------------------------------\n\n/**\n * The root record a record belongs to: itself (the type of any root kind), a relationship to a\n * root of any kind, or a related stored record.\n */\nexport async function rootIdForResource(\n store: DeviceStore,\n config: OfflineConfig,\n resource: any,\n): Promise<string | undefined> {\n const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));\n if (rootTypes.has(resource?.type) && resource.id) return resource.id;\n\n const identifiers = Object.values(resource?.relationships ?? {}).flatMap(identifiersOf);\n const root = identifiers.find((identifier) => rootTypes.has(identifier.type));\n if (root) return root.id;\n\n for (const identifier of identifiers) {\n const stored = await store.getRecord(identifier.type, identifier.id);\n if (stored?.rootId) return stored.rootId;\n }\n return undefined;\n}\n\nasync function linkRelationships(\n store: DeviceStore,\n typeConfig: OfflineTypeConfig | undefined,\n record: { type: string; id: string },\n before: Record<string, any>,\n after: Record<string, any>,\n): Promise<void> {\n for (const [name, relationship] of Object.entries(after)) {\n const previous = new Map(identifiersOf(before[name]).map((identifier) => [identifier.id, identifier]));\n const next = new Map(identifiersOf(relationship).map((identifier) => [identifier.id, identifier]));\n const kinds = linkKindsOf(typeConfig, name);\n\n for (const [id] of previous) {\n if (next.has(id)) continue;\n // Only the links of this relationship (by name or Neo4j type); any kind when it is not known.\n const links = kinds\n ? await linksOfKind(store, record.id, kinds, \"both\")\n : [...(await store.linksFrom(record.id)), ...(await store.linksTo(record.id))];\n for (const link of links) {\n if (link.fromId === id || link.toId === id) {\n await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });\n }\n }\n }\n for (const [id, identifier] of next) {\n if (previous.has(id)) continue;\n await store.putLink({\n fromType: record.type,\n fromId: record.id,\n toType: identifier.type,\n toId: id,\n kind: linkKindFor(typeConfig, name),\n props: {},\n });\n }\n }\n}\n\n/** The `data` of a JSON:API document a model built (`createJsonApi()` or a dedicated method). */\nexport type ResourceDocumentData = {\n attributes?: Record<string, unknown>;\n relationships?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n};\n\n/**\n * The resource a create document stands for, under `identity`: the document's attributes,\n * relationships and meta, as the device stores a record it creates.\n */\nexport function resourceFromDocument(document: any, identity: { type: string; id: string }): any {\n const data: ResourceDocumentData = document?.data ?? {};\n const resource: any = { type: identity.type, id: identity.id };\n if (data.attributes) resource.attributes = { ...data.attributes };\n if (data.relationships) resource.relationships = { ...data.relationships };\n if (data.meta) resource.meta = { ...data.meta };\n return resource;\n}\n\n/**\n * A stored resource with a document's `data` written over it: attributes and relationships merged\n * by name, and meta merged when the document carries one, as the device applies an edit.\n */\nexport function mergeResource(before: any, identity: { type: string; id: string }, data: ResourceDocumentData): any {\n const previous = before ?? identity;\n const resource: any = {\n ...previous,\n type: identity.type,\n id: identity.id,\n attributes: { ...(previous.attributes ?? {}), ...(data?.attributes ?? {}) },\n relationships: { ...(previous.relationships ?? {}), ...(data?.relationships ?? {}) },\n };\n if (data?.meta) resource.meta = { ...(previous.meta ?? {}), ...data.meta };\n return resource;\n}\n\nfunction newId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") return crypto.randomUUID();\n return `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n}\n\nasync function create(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const typeConfig = typeConfigOf(config, type);\n const id: string = ctx.request.body?.data?.id ?? newId();\n const resource = resourceFromDocument(ctx.request.body, { type, id });\n const rootId = await rootIdForResource(ctx.store, config, resource);\n if (!rootId) return needsConnectionAnswer(`A new ${type} that belongs to no root record needs a connection`);\n if (isRootWriteLocked(rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const record: StoredRecord = { type, id, rootId, resource, updatedAt: null, base: null, localOnly: true };\n await ctx.store.transaction(async (store) => {\n await store.putRecord(record);\n await linkRelationships(store, typeConfig, { type, id }, {}, resource.relationships ?? {});\n await store.enqueue({\n rootId,\n method: ctx.request.method,\n path: queuePath(ctx),\n body: ctx.request.body,\n base: null,\n });\n // The type's hook, inside the write's transaction: a throw rolls the write and the enqueue back.\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({\n store,\n module: typeConfig.module,\n id,\n method: \"POST\",\n resource,\n previous: null,\n });\n }\n });\n\n return singleAnswer(ctx.store, record, 201);\n}\n\nasync function update(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const id = ctx.params.id;\n const existing = await ctx.store.getRecord(type, id);\n if (!existing) return notOnDeviceAnswer(type, id);\n if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const data = ctx.request.body?.data ?? {};\n const before = existing.resource ?? { type, id };\n const resource = mergeResource(before, { type, id }, data);\n // A record created on this device has no server version to merge against.\n const base = existing.localOnly ? null : (existing.base ?? before);\n\n const typeConfig = typeConfigOf(config, type);\n const method = ctx.request.method === \"PATCH\" ? \"PATCH\" : \"PUT\";\n const record: StoredRecord = { ...existing, resource, base, localOnly: existing.localOnly };\n await ctx.store.transaction(async (store) => {\n await store.putRecord(record);\n await linkRelationships(store, typeConfig, { type, id }, before.relationships ?? {}, data.relationships ?? {});\n await store.enqueue({\n rootId: existing.rootId,\n method: ctx.request.method,\n path: queuePath(ctx),\n body: ctx.request.body,\n recordType: type,\n recordId: id,\n base,\n });\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({ store, module: typeConfig.module, id, method, resource, previous: before });\n }\n });\n\n return singleAnswer(ctx.store, record);\n}\n\nasync function remove(ctx: LocalAnswerContext, config: OfflineConfig, type: string): Promise<OfflineAnswer> {\n const id = ctx.params.id;\n const existing = await ctx.store.getRecord(type, id);\n if (!existing) return notOnDeviceAnswer(type, id);\n if (isRootWriteLocked(existing.rootId)) return needsConnectionAnswer(\"The root record is locked\");\n\n const base = existing.localOnly ? null : (existing.base ?? existing.resource);\n\n await ctx.store.transaction(async (store) => {\n const links = [...(await store.linksFrom(id)), ...(await store.linksTo(id))];\n for (const link of links) await store.deleteLink({ fromId: link.fromId, toId: link.toId, kind: link.kind });\n await store.deleteRecord(type, id);\n await store.enqueue({\n rootId: existing.rootId,\n method: \"DELETE\",\n path: queuePath(ctx),\n recordType: type,\n recordId: id,\n base,\n });\n const typeConfig = typeConfigOf(config, type);\n if (typeConfig?.afterLocalWrite) {\n await typeConfig.afterLocalWrite({\n store,\n module: typeConfig.module,\n id,\n method: \"DELETE\",\n resource: null,\n previous: existing.resource,\n });\n }\n });\n\n return { status: 204, body: undefined };\n}\n\n/** The request path as `EndpointCreator` built it, query string included. */\nexport function queuePath(ctx: Pick<LocalAnswerContext, \"request\">): string {\n const query = ctx.request.query.toString();\n return query ? `${ctx.request.path}?${query}` : ctx.request.path;\n}\n\n// ---------------------------------------------------------------------------------------------\n// The generic route list\n// ---------------------------------------------------------------------------------------------\n\nfunction readOnlyGuard(typeConfig: OfflineTypeConfig, answer: (ctx: LocalAnswerContext) => Promise<OfflineAnswer>) {\n return async (ctx: LocalAnswerContext): Promise<OfflineAnswer> =>\n typeConfig.readOnly ? needsConnectionAnswer(`${typeConfig.module.name} is read-only offline`) : answer(ctx);\n}\n\n/**\n * One set of routes per configured type, in matching order: the root record list before the\n * related list (both are `x/:id/<type>`), the related list last. These generic routes are not\n * queued by the layer: each write answer queues itself, with its base.\n */\nexport function buildGenericRoutes(config: OfflineConfig): OfflineRoute[] {\n const routes: OfflineRoute[] = [];\n const route = (method: OfflineMethod, pattern: OfflineRoute[\"pattern\"], answer: OfflineRoute[\"answer\"]) =>\n routes.push({ method, pattern, answer, queue: false });\n\n for (const typeConfig of config.types) {\n const type = typeConfig.module.name;\n route(\"GET\", [typeConfig.module, \":id\"], (ctx) => getOne(ctx, type));\n for (const kind of rootKinds(config)) {\n route(\"GET\", [kind.module, \":rootId\", typeConfig.module], (ctx) => rootList(ctx, kind, typeConfig));\n }\n route(\n \"POST\",\n [typeConfig.module],\n readOnlyGuard(typeConfig, (ctx) => create(ctx, config, type)),\n );\n route(\n \"PUT\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type)),\n );\n route(\n \"PATCH\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => update(ctx, config, type)),\n );\n route(\n \"DELETE\",\n [typeConfig.module, \":id\"],\n readOnlyGuard(typeConfig, (ctx) => remove(ctx, config, type)),\n );\n }\n for (const typeConfig of config.types) {\n route(\"GET\", [\":parentType\", \":parentId\", typeConfig.module], (ctx) => relatedList(ctx, config, typeConfig));\n }\n\n return routes;\n}\n","import type { OfflineMethod, OfflineRoute, PatternSegment } from \"./types\";\n\nexport type RouteMatch = { route: OfflineRoute; params: Record<string, string> };\n\nfunction splitPath(path: string): string[] {\n const withoutQuery = path.split(\"?\")[0].split(\"#\")[0];\n return withoutQuery.split(\"/\").filter((segment) => segment.length > 0);\n}\n\n/** A Module segment may itself hold slashes (\"roots/played\"), so each one is expanded. */\nfunction expandPattern(pattern: PatternSegment[]): string[] {\n return pattern.flatMap((segment) => splitPath(typeof segment === \"string\" ? segment : segment.name));\n}\n\nexport function matchPattern(pattern: PatternSegment[], path: string): Record<string, string> | null {\n const expected = expandPattern(pattern);\n const actual = splitPath(path);\n if (expected.length !== actual.length) return null;\n\n const params: Record<string, string> = {};\n for (let index = 0; index < expected.length; index++) {\n const segment = expected[index];\n const value = decodeURIComponent(actual[index]);\n if (segment.startsWith(\":\")) {\n params[segment.slice(1)] = value;\n } else if (segment !== value) {\n return null;\n }\n }\n return params;\n}\n\n/** The first route, in the order given, whose method and pattern match. The query string is ignored. */\nexport function matchRoute(routes: OfflineRoute[], method: OfflineMethod, path: string): RouteMatch | null {\n for (const route of routes) {\n if (route.method !== method) continue;\n const params = matchPattern(route.pattern, path);\n if (params) return { route, params };\n }\n return null;\n}\n","import { getClientApiUrl } from \"../client/JsonApiClient\";\nimport type { ApiData } from \"../core/interfaces/ApiData\";\nimport { getApiUrl } from \"../unified/JsonApiRequest\";\nimport { getOfflineConfig, getOfflineStore, rootKinds } from \"./config\";\nimport {\n buildGenericRoutes,\n rootIdForResource,\n isRootWriteLocked,\n needsConnectionAnswer,\n queuePath,\n} from \"./generic-answers\";\nimport { matchRoute } from \"./router\";\nimport { setOfflineState } from \"./state\";\nimport type { DeviceStore } from \"./store/DeviceStore\";\nimport {\n NeedsConnectionError,\n type OfflineAnswer,\n type OfflineMethod,\n type OfflineRequest,\n type OfflineRoute,\n} from \"./types\";\n\nexport type LocalTransportRequest = {\n method: string;\n url: string;\n body?: any;\n files?: unknown;\n};\n\nconst METHODS: OfflineMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\nfunction apiBases(): string[] {\n const bases: string[] = [];\n for (const read of [getApiUrl, getClientApiUrl]) {\n try {\n const base = read();\n if (base) bases.push(base);\n } catch {\n // not configured on this side\n }\n }\n return bases;\n}\n\n/** The path relative to the API base, as `EndpointCreator` built it, and its query. */\nexport function splitApiUrl(url: string): { path: string; query: URLSearchParams } {\n let relative: string | undefined;\n for (const base of apiBases()) {\n if (url.startsWith(base)) {\n relative = url.slice(base.length);\n break;\n }\n }\n if (relative === undefined) {\n try {\n const parsed = new URL(url);\n relative = `${parsed.pathname}${parsed.search}`;\n } catch {\n relative = url;\n }\n }\n\n const [pathPart, ...queryParts] = relative.split(\"?\");\n return {\n path: pathPart.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\"),\n query: new URLSearchParams(queryParts.join(\"?\")),\n };\n}\n\nfunction toApiData(answer: OfflineAnswer): ApiData {\n const ok = answer.status >= 200 && answer.status < 300;\n return {\n ok,\n status: answer.status,\n statusText: ok ? \"OK\" : answer.status === 503 ? \"Service Unavailable\" : \"Error\",\n data: answer.body,\n };\n}\n\n/** The root record a custom write belongs to: a `<root>/:param` capture, a stored record named by a param, or the body. */\nasync function rootIdForCustomWrite(\n store: DeviceStore,\n pattern: OfflineRequest,\n params: Record<string, string>,\n rootSegmentParam: string | undefined,\n): Promise<string | undefined> {\n const config = getOfflineConfig();\n if (rootSegmentParam && params[rootSegmentParam]) return params[rootSegmentParam];\n\n for (const value of Object.values(params)) {\n for (const typeConfig of config.types) {\n const stored = await store.getRecord(typeConfig.module.name, value);\n if (stored?.rootId) return stored.rootId;\n }\n }\n return rootIdForResource(store, config, pattern.body?.data);\n}\n\nasync function refreshPendingCount(store: DeviceStore): Promise<void> {\n const entries = await store.queue();\n setOfflineState({ pending: entries.filter((entry) => entry.status !== \"conflict\").length });\n}\n\n/**\n * Answers a request from the device copy: the app's custom routes first, then the generic ones.\n * The result has the `ApiData` shape `directFetch` returns, so `translateResponse` and\n * `rehydrate()` run unchanged.\n */\nexport async function localTransport(request: LocalTransportRequest): Promise<ApiData> {\n try {\n const method = request.method.toUpperCase() as OfflineMethod;\n if (!METHODS.includes(method)) return toApiData(needsConnectionAnswer());\n if (request.files) return toApiData(needsConnectionAnswer(\"Uploading files needs a connection\"));\n\n const config = getOfflineConfig();\n const { path, query } = splitApiUrl(request.url);\n const offlineRequest: OfflineRequest = { method, path, query, body: request.body, url: request.url };\n\n const custom = matchRoute(config.routes, method, path);\n const match = custom ?? matchRoute(buildGenericRoutes(config), method, path);\n if (!match) return toApiData(needsConnectionAnswer(`${method} ${path} has no offline answer`));\n\n let store: DeviceStore;\n try {\n store = await getOfflineStore();\n } catch (error) {\n // The device copy cannot be opened here (another tab holds the file, storage refused,\n // no signed-in user): only the server can answer.\n if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));\n console.warn(\"[offline] the device copy could not be opened\", error);\n return toApiData(needsConnectionAnswer(\"The device copy is not available in this tab\"));\n }\n const ctx = { request: offlineRequest, params: match.params, store };\n\n // The root record of a custom write (a `<root>/:param` capture of any root kind's module, a\n // stored record, or the body).\n const rootTypes = new Set(rootKinds(config).map((kind) => kind.module.name));\n const customRootId = async (route: OfflineRoute): Promise<string | undefined> => {\n const rootIndex = route.pattern.findIndex(\n (segment) => typeof segment !== \"string\" && rootTypes.has(segment.name),\n );\n const next = rootIndex >= 0 ? route.pattern[rootIndex + 1] : undefined;\n const rootParam = typeof next === \"string\" && next.startsWith(\":\") ? next.slice(1) : undefined;\n return rootIdForCustomWrite(store, offlineRequest, match.params, rootParam);\n };\n\n if (!custom || !custom.route.queue) {\n // A custom write that is not queued is refused on a locked root record too (GETs stay allowed).\n if (custom && method !== \"GET\") {\n const rootId = await customRootId(custom.route);\n if (rootId && isRootWriteLocked(rootId)) {\n return toApiData(needsConnectionAnswer(\"The root record is locked\"));\n }\n }\n const answer = await match.route.answer(ctx);\n if (method !== \"GET\") await refreshPendingCount(store);\n return toApiData(answer);\n }\n\n // A custom write the server must run too: refused on a locked root record, otherwise the answer\n // updates the device copy and the original request is queued in the same transaction.\n const rootId = await customRootId(custom.route);\n if (!rootId) return toApiData(needsConnectionAnswer(`${method} ${path} belongs to no root record on this device`));\n if (isRootWriteLocked(rootId)) return toApiData(needsConnectionAnswer(\"The root record is locked\"));\n\n const answer = await store.transaction(async (transactionStore) => {\n const result = await custom.route.answer({ ...ctx, store: transactionStore });\n if (result.status >= 200 && result.status < 300) {\n await transactionStore.enqueue({\n rootId,\n method,\n path: queuePath(ctx),\n body: request.body,\n base: null,\n });\n }\n return result;\n });\n await refreshPendingCount(store);\n return toApiData(answer);\n } catch (error) {\n if (error instanceof NeedsConnectionError) return toApiData(needsConnectionAnswer(error.message));\n console.error(\"[offline] local answer failed\", error);\n return toApiData({\n status: 500,\n body: {\n errors: [{ status: \"500\", title: \"Offline answer failed\", detail: String((error as Error)?.message ?? error) }],\n },\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAcA,IAAM,oBAAoB;AAOnB,SAAS,YAAY,QAAgB,OAAe,QAAgB,MAA8B;AACvG,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,OAAO,MAAM,GAAG,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC,GAAI,OAAO,OAAO,CAAC,EAAE;AAAA,EACzF;AACF;AALgB;AAOT,SAAS,sBAAsB,SAAiB,sBAAqC;AAC1F,SAAO,YAAY,KAAK,sBAAsB,QAAQ,kBAAkB;AAC1E;AAFgB;AAIT,SAAS,eAAe,QAA+B;AAC5D,SAAO,YAAY,KAAK,aAAa,MAAM;AAC7C;AAFgB;AAKhB,eAAsB,aACpB,OACA,QACA,SAAiB,KACO;AACxB,SAAO,EAAE,QAAQ,MAAM,EAAE,MAAM,OAAO,UAAU,UAAU,MAAM,YAAY,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE;AACjG;AANsB;AAQf,SAAS,kBAAkB,QAAqC;AACrE,SAAO,CAAC,CAAC,UAAU,gBAAgB,EAAE,cAAc,SAAS,MAAM;AACpE;AAFgB;AAUhB,SAAS,cAAc,cAAyC;AAC9D,QAAM,OAAO,cAAc;AAC3B,MAAI,CAAC,KAAM,QAAO,CAAC;AACnB,UAAQ,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,SAAc,QAAQ,KAAK,QAAQ,KAAK,EAAE;AACjG;AAJS;AAMT,SAAS,UAAU,OAAqB,IAAkC;AACxE,SAAO,MAAM;AAAA,IAAI,CAAC,SAChB,KAAK,WAAW,KAAK,EAAE,MAAM,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,MAAM,KAAK,UAAU,IAAI,KAAK,OAAO;AAAA,EACrG;AACF;AAJS;AAQT,SAAS,oBAAoB,UAA+C;AAC1E,QAAM,UAA0C,CAAC;AACjD,aAAW,gBAAgB,OAAO,OAAY,UAAU,iBAAiB,CAAC,CAAC,GAAG;AAC5E,UAAM,OAAO,cAAc;AAC3B,eAAW,OAAO,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,CAAC,IAAI,IAAI,CAAC,GAAG;AACjE,UAAI,KAAK,QAAQ,KAAK,GAAI,SAAQ,KAAK,EAAE,MAAM,IAAI,MAAM,IAAI,IAAI,GAAG,CAAC;AAAA,IACvE;AAAA,EACF;AACA,SAAO;AACT;AATS;AAqBT,eAAsB,YAAY,OAAoB,SAAyC;AAE7F,QAAM,QAAQ,IAAI,IAAoB,QAAQ,IAAI,CAAC,WAAW,CAAC,GAAG,OAAO,IAAI,IAAI,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;AACjG,QAAM,WAA+C,CAAC;AAEtD,QAAM,MAAM,8BAAO,QAAsC,YAAmC;AAC1F,UAAM,MAAM,GAAG,OAAO,IAAI,IAAI,OAAO,EAAE;AACvC,QAAI,MAAM,IAAI,GAAG,EAAG;AACpB,UAAM,IAAI,KAAK,OAAO;AACtB,UAAM,SAAS,MAAM,MAAM,UAAU,OAAO,MAAM,OAAO,EAAE;AAC3D,UAAM,WAAW,QAAQ,YAAY,qBAAqB,MAAM;AAChE,QAAI,SAAU,UAAS,KAAK,EAAE,UAAU,OAAO,QAAQ,CAAC;AAAA,EAC1D,GAPY;AASZ,aAAW,UAAU,SAAS;AAC5B,UAAM,QAAQ,CAAC,GAAI,MAAM,MAAM,UAAU,OAAO,EAAE,GAAI,GAAI,MAAM,MAAM,QAAQ,OAAO,EAAE,CAAE;AACzF,eAAW,UAAU,UAAU,OAAO,OAAO,EAAE,EAAG,OAAM,IAAI,QAAQ,CAAC;AAAA,EACvE;AACA,aAAW,UAAU,SAAS;AAC5B,eAAW,UAAU,oBAAoB,OAAO,QAAQ,EAAG,OAAM,IAAI,QAAQ,CAAC;AAAA,EAChF;AACA,aAAW,SAAS,SAAS,OAAO,CAAC,cAAc,UAAU,UAAU,CAAC,GAAG;AACzE,eAAW,UAAU,oBAAoB,MAAM,QAAQ,EAAG,OAAM,IAAI,QAAQ,CAAC;AAAA,EAC/E;AACA,SAAO,SAAS,IAAI,CAAC,UAAU,MAAM,QAAQ;AAC/C;AAzBsB;AAiCtB,SAAS,qBAAqB,QAAuD;AACnF,MAAI;AACF,QAAI,OAAO,OAAO,iBAAiB,EAAE,UAAU,EAAG,QAAO;AACzD,UAAM,SAAS,KAAK,MAAM,OAAO,aAAa,QAAQ,wBAAwB,KAAK,MAAM;AACzF,UAAM,OAAO,QAAQ;AACrB,QAAI,MAAM,SAAS,OAAO,QAAQ,MAAM,OAAO,OAAO,GAAI,QAAO;AACjE,WAAO,EAAE,MAAM,KAAK,MAAM,IAAI,KAAK,IAAI,YAAY,KAAK,cAAc,CAAC,GAAG,MAAM,KAAK,KAAK;AAAA,EAC5F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAVS;AAYT,SAAS,YAAY,QAAsB,MAAuB;AAChE,QAAM,QAAQ,OAAO,UAAU,aAAa,IAAI;AAChD,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAElD,MAAI,SAAS,YAAa,QAAO,OAAO,aAAa;AACrD,SAAO;AACT;AANS;AASF,IAAM,0BAA0B;AAGvC,IAAM,WAAW;AAEjB,IAAM,WAAW;AAEjB,SAAS,WAAW,OAAyB;AAC3C,MAAI,iBAAiB,KAAM,QAAO,MAAM,QAAQ;AAChD,MAAI,OAAO,UAAU,YAAY,SAAS,KAAK,KAAK,GAAG;AACrD,UAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,QAAI,CAAC,OAAO,MAAM,IAAI,EAAG,QAAO;AAAA,EAClC;AACA,SAAO;AACT;AAPS;AAUT,SAAS,iBAAiB,MAAe,OAAwB;AAC/D,QAAM,IAAI,WAAW,IAAI;AACzB,QAAM,IAAI,WAAW,KAAK;AAC1B,QAAM,QAAQ,MAAM,UAAa,MAAM;AACvC,QAAM,QAAQ,MAAM,UAAa,MAAM;AACvC,MAAI,SAAS,MAAO,QAAO,UAAU,QAAQ,IAAI,QAAQ,IAAI;AAC7D,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO,IAAI;AAC/D,MAAI,OAAO,MAAM,aAAa,OAAO,MAAM,UAAW,QAAO,OAAO,CAAC,IAAI,OAAO,CAAC;AACjF,QAAM,IAAI,OAAO,CAAC;AAClB,QAAM,IAAI,OAAO,CAAC;AAClB,SAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;AAClC;AAXS;AAyBF,SAAS,cACd,SACA,OACA,UAAuB,CAAC,GACR;AAChB,QAAM,OAAO,MAAM,IAAI,QAAQ;AAC/B,MAAI,SAAS,QAAQ,SAAS,UAAa,KAAK,KAAK,MAAM,GAAI,QAAO;AACtE,QAAM,SAAS,KACZ,YAAY,EACZ,MAAM,iBAAiB,EACvB,OAAO,OAAO;AACjB,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,SAAO,QAAQ,OAAO,CAAC,WAAW;AAChC,UAAM,aAAa,OAAO,UAAU,cAAc,CAAC;AACnD,UAAM,SAAS,QAAQ,gBAAgB,OAAO,KAAK,UAAU;AAC7D,UAAM,QAAQ,OACX,IAAI,CAAC,UAAU,WAAW,KAAK,CAAC,EAChC,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,EAC5D,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC;AACrC,WAAO,OAAO,MAAM,CAAC,UAAU,MAAM,KAAK,CAAC,SAAS,KAAK,SAAS,KAAK,CAAC,CAAC;AAAA,EAC3E,CAAC;AACH;AAtBgB;AA6BT,SAAS,aACd,SACA,OACA,UAAuB,CAAC,GACD;AACvB,QAAM,YAAY,MAAM,IAAI,SAAS,GAAG,KAAK;AAC7C,QAAM,QAAQ,aAAa,QAAQ,gBAAgB,KAAK,KAAK;AAC7D,QAAM,QAAQ,SAAS,KAAK,KAAK;AACjC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,OAAO,MAAM,CAAC,GAAG,YAAY,MAAM;AACzC,SAAO,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,MAAM,UAAU;AACxC,UAAM,QAAQ,iBAAiB,YAAY,MAAM,KAAK,GAAG,YAAY,OAAO,KAAK,CAAC;AAClF,WAAO,OAAO,CAAC,QAAQ;AAAA,EACzB,CAAC;AACH;AAfgB;AAiBhB,SAAS,QAAQ,MAAc,OAAwB,SAAqD;AAC1G,QAAM,SAAS,IAAI,gBAAgB,KAAK;AACxC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,QAAI,UAAU,OAAW,QAAO,OAAO,GAAG;AAAA,QACrC,QAAO,IAAI,KAAK,KAAK;AAAA,EAC5B;AACA,QAAM,SAAS,OAAO,SAAS,EAAE,QAAQ,QAAQ,GAAG,EAAE,QAAQ,QAAQ,GAAG,EAAE,QAAQ,QAAQ,GAAG;AAC9F,SAAO,SAAS,GAAG,IAAI,IAAI,MAAM,KAAK;AACxC;AARS;AAeT,eAAsB,WACpB,KACA,SACA,UAAuB,CAAC,GACA;AACxB,QAAM,EAAE,SAAS,MAAM,IAAI;AAC3B,QAAM,OAAO,QAAQ,MAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,QAAQ;AAC/D,QAAM,SAAS,aAAa,cAAc,SAAS,QAAQ,OAAO,OAAO,GAAG,QAAQ,OAAO,OAAO;AAClG,MAAI,CAAC,QAAQ;AACX,WAAO,YAAY,KAAK,eAAe,iBAAiB;AAAA,EAC1D;AAEA,MAAI,QAAQ,MAAM,IAAI,UAAU,MAAM,QAAQ;AAC5C,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,MAAM,OAAO,IAAI,CAAC,WAAW,OAAO,QAAQ;AAAA,QAC5C,UAAU,MAAM,YAAY,OAAO,MAAM;AAAA,QACzC,OAAO,EAAE,MAAM,QAAQ,MAAM,QAAQ,OAAO,CAAC,CAAC,EAAE;AAAA,QAChD,MAAM,EAAE,OAAO,OAAO,OAAO;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,OAAO,QAAQ,MAAM,IAAI,YAAY,CAAC,KAAK;AACxD,QAAM,SAAS,OAAO,QAAQ,MAAM,IAAI,cAAc,CAAC,KAAK;AAC5D,QAAM,OAAO,OAAO,MAAM,QAAQ,SAAS,IAAI;AAE/C,QAAM,QAAgC;AAAA,IACpC,MAAM,QAAQ,MAAM,QAAQ,OAAO,EAAE,cAAc,OAAO,IAAI,GAAG,gBAAgB,OAAU,CAAC;AAAA,EAC9F;AACA,MAAI,SAAS,OAAO,OAAO,QAAQ;AACjC,UAAM,OAAO,QAAQ,MAAM,QAAQ,OAAO,EAAE,cAAc,OAAO,IAAI,GAAG,gBAAgB,OAAO,SAAS,IAAI,EAAE,CAAC;AAAA,EACjH;AACA,MAAI,SAAS,GAAG;AACd,UAAM,OAAO,QAAQ,MAAM,QAAQ,OAAO;AAAA,MACxC,cAAc,OAAO,IAAI;AAAA,MACzB,gBAAgB,OAAO,KAAK,IAAI,GAAG,SAAS,IAAI,CAAC;AAAA,IACnD,CAAC;AAED,UAAM,WAAW,MAAM;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,MAAM,KAAK,IAAI,CAAC,WAAW,OAAO,QAAQ;AAAA,MAC1C,UAAU,MAAM,YAAY,OAAO,IAAI;AAAA,MACvC;AAAA,MACA,MAAM,EAAE,OAAO,OAAO,OAAO;AAAA,IAC/B;AAAA,EACF;AACF;AApDsB;AA0DtB,eAAsB,kBAAkB,OAAoB,SAAiD;AAC3G,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,QAAQ;AAAA,MAC7C,UAAU,MAAM,YAAY,OAAO,OAAO;AAAA,MAC1C,MAAM,EAAE,OAAO,QAAQ,OAAO;AAAA,IAChC;AAAA,EACF;AACF;AATsB;AAef,SAAS,mBAAmB,WAAiC;AAClE,SAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,MAAM,WAAW,UAAU,CAAC,EAAE,EAAE;AAChE;AAFgB;AAIhB,SAAS,cAAc,YAA4C;AACjE,SAAO,EAAE,cAAc,WAAW,cAAc,gBAAgB,WAAW,eAAe;AAC5F;AAFS;AAYF,SAAS,YAAY,YAA2C,MAAsB;AAC3F,SAAO,YAAY,oBAAoB,IAAI,KAAK;AAClD;AAFgB;AAST,SAAS,YAAY,YAA2C,MAAoC;AACzG,QAAM,SAAS,YAAY,oBAAoB,IAAI;AACnD,SAAO,SAAS,CAAC,MAAM,MAAM,IAAI;AACnC;AAHgB;AAShB,eAAsB,YACpB,OACA,IACA,OACA,YAAmC,QACZ;AACvB,QAAM,SAAuB,CAAC;AAC9B,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,QAAQ,IAAI,IAAI,KAAK,GAAG;AACjC,UAAM,QAAQ;AAAA,MACZ,GAAI,cAAc,OAAO,MAAM,MAAM,UAAU,IAAI,IAAI,IAAI,CAAC;AAAA,MAC5D,GAAI,cAAc,QAAQ,MAAM,MAAM,QAAQ,IAAI,IAAI,IAAI,CAAC;AAAA,IAC7D;AACA,eAAW,QAAQ,OAAO;AACxB,YAAM,MAAM,GAAG,KAAK,MAAM,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI;AACpD,UAAI,KAAK,IAAI,GAAG,EAAG;AACnB,WAAK,IAAI,GAAG;AACZ,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AArBsB;AAuBtB,SAAS,aAAa,QAAuB,MAA6C;AACxF,SAAO,OAAO,MAAM,KAAK,CAAC,eAAe,WAAW,OAAO,SAAS,IAAI;AAC1E;AAFS;AAKT,SAAS,wBAAwB,UAAe,MAAwB;AACtE,SAAO,OAAO,QAAa,UAAU,iBAAiB,CAAC,CAAC,EACrD,OAAO,CAAC,CAAC,EAAE,YAAY,MAAM,cAAc,YAAY,EAAE,KAAK,CAAC,eAAe,WAAW,SAAS,IAAI,CAAC,EACvG,IAAI,CAAC,CAAC,IAAI,MAAM,IAAI;AACzB;AAJS;AAWT,SAAS,kBAAkB,MAAc,IAA2B;AAClE,SAAO,sBAAsB,GAAG,IAAI,IAAI,EAAE,wBAAwB;AACpE;AAFS;AAIT,eAAe,OAAO,KAAyB,MAAsC;AACnF,QAAM,SAAS,MAAM,IAAI,MAAM,UAAU,MAAM,IAAI,OAAO,EAAE;AAC5D,MAAI,CAAC,OAAQ,QAAO,kBAAkB,MAAM,IAAI,OAAO,EAAE;AACzD,SAAO,aAAa,IAAI,OAAO,MAAM;AACvC;AAJe;AAMf,eAAe,SACb,KACA,MACA,YACwB;AACxB,QAAM,WAAW,KAAK,OAAO;AAC7B,QAAM,SAAS,IAAI,OAAO;AAC1B,MAAI,CAAE,MAAM,IAAI,MAAM,UAAU,UAAU,MAAM,EAAI,QAAO,kBAAkB,UAAU,MAAM;AAC7F,QAAM,UAAU,MAAM,IAAI,MAAM,YAAY,EAAE,MAAM,WAAW,OAAO,MAAM,OAAO,CAAC;AACpF,SAAO,WAAW,KAAK,SAAS,cAAc,UAAU,CAAC;AAC3D;AAVe;AAkBf,eAAe,YACb,KACA,QACA,YACwB;AACxB,QAAM,OAAO,WAAW,OAAO;AAC/B,QAAM,EAAE,YAAY,SAAS,IAAI,IAAI;AACrC,QAAM,SAAS,MAAM,IAAI,MAAM,UAAU,YAAY,QAAQ;AAC7D,MAAI,CAAC,OAAQ,QAAO,kBAAkB,YAAY,QAAQ;AAE1D,QAAM,QAAQ,CAAC,GAAI,MAAM,IAAI,MAAM,UAAU,QAAQ,GAAI,GAAI,MAAM,IAAI,MAAM,QAAQ,QAAQ,CAAE,EAAE;AAAA,IAAO,CAAC,SACvG,KAAK,WAAW,WAAW,KAAK,WAAW,OAAO,KAAK,aAAa;AAAA,EACtE;AACA,QAAM,aAAa,MAAM,IAAI,MAAM,YAAY;AAAA,IAC7C;AAAA,IACA,KAAK,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,CAAC,SAAU,KAAK,WAAW,WAAW,KAAK,OAAO,KAAK,MAAO,CAAC,CAAC;AAAA,EAC7F,CAAC;AAED,QAAM,QAAQ,oBAAI,IAAY;AAC9B,MAAI,UAAU;AACd,QAAM,UAAU,wBAAC,OAAsC,UAAoB;AACzE,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,YAAY,OAAO,IAAI;AACrC,UAAI,MAAO,OAAM,QAAQ,CAAC,SAAS,MAAM,IAAI,IAAI,CAAC;AAAA,UAC7C,WAAU;AAAA,IACjB;AAAA,EACF,GANgB;AAOhB,UAAQ,YAAY;AAAA,IAClB,GAAG,IAAI,IAAI,WAAW,QAAQ,CAAC,WAAW,wBAAwB,OAAO,UAAU,UAAU,CAAC,CAAC;AAAA,EACjG,CAAC;AACD,UAAQ,aAAa,QAAQ,UAAU,GAAG,wBAAwB,OAAO,UAAU,IAAI,CAAC;AAExF,QAAM,WAAW,WAAW,MAAM,SAAS,IAAI,QAAQ,MAAM,OAAO,CAAC,SAAS,MAAM,IAAI,KAAK,IAAI,CAAC;AAClG,QAAM,MAAM,IAAI,IAAI,SAAS,IAAI,CAAC,SAAU,KAAK,WAAW,WAAW,KAAK,OAAO,KAAK,MAAO,CAAC;AAChG,SAAO;AAAA,IACL;AAAA,IACA,WAAW,OAAO,CAAC,WAAW,IAAI,IAAI,OAAO,EAAE,CAAC;AAAA,IAChD,cAAc,UAAU;AAAA,EAC1B;AACF;AAvCe;AAiDf,eAAsB,kBACpB,OACA,QACA,UAC6B;AAC7B,QAAM,YAAY,IAAI,IAAI,UAAU,MAAM,EAAE,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC;AAC3E,MAAI,UAAU,IAAI,UAAU,IAAI,KAAK,SAAS,GAAI,QAAO,SAAS;AAElE,QAAM,cAAc,OAAO,OAAO,UAAU,iBAAiB,CAAC,CAAC,EAAE,QAAQ,aAAa;AACtF,QAAM,OAAO,YAAY,KAAK,CAAC,eAAe,UAAU,IAAI,WAAW,IAAI,CAAC;AAC5E,MAAI,KAAM,QAAO,KAAK;AAEtB,aAAW,cAAc,aAAa;AACpC,UAAM,SAAS,MAAM,MAAM,UAAU,WAAW,MAAM,WAAW,EAAE;AACnE,QAAI,QAAQ,OAAQ,QAAO,OAAO;AAAA,EACpC;AACA,SAAO;AACT;AAjBsB;AAmBtB,eAAe,kBACb,OACA,YACA,QACA,QACA,OACe;AACf,aAAW,CAAC,MAAM,YAAY,KAAK,OAAO,QAAQ,KAAK,GAAG;AACxD,UAAM,WAAW,IAAI,IAAI,cAAc,OAAO,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC,WAAW,IAAI,UAAU,CAAC,CAAC;AACrG,UAAM,OAAO,IAAI,IAAI,cAAc,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC,WAAW,IAAI,UAAU,CAAC,CAAC;AACjG,UAAM,QAAQ,YAAY,YAAY,IAAI;AAE1C,eAAW,CAAC,EAAE,KAAK,UAAU;AAC3B,UAAI,KAAK,IAAI,EAAE,EAAG;AAElB,YAAM,QAAQ,QACV,MAAM,YAAY,OAAO,OAAO,IAAI,OAAO,MAAM,IACjD,CAAC,GAAI,MAAM,MAAM,UAAU,OAAO,EAAE,GAAI,GAAI,MAAM,MAAM,QAAQ,OAAO,EAAE,CAAE;AAC/E,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,WAAW,MAAM,KAAK,SAAS,IAAI;AAC1C,gBAAM,MAAM,WAAW,EAAE,QAAQ,KAAK,QAAQ,MAAM,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;AAAA,QAClF;AAAA,MACF;AAAA,IACF;AACA,eAAW,CAAC,IAAI,UAAU,KAAK,MAAM;AACnC,UAAI,SAAS,IAAI,EAAE,EAAG;AACtB,YAAM,MAAM,QAAQ;AAAA,QAClB,UAAU,OAAO;AAAA,QACjB,QAAQ,OAAO;AAAA,QACf,QAAQ,WAAW;AAAA,QACnB,MAAM;AAAA,QACN,MAAM,YAAY,YAAY,IAAI;AAAA,QAClC,OAAO,CAAC;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF;AACF;AApCe;AAiDR,SAAS,qBAAqB,UAAe,UAA6C;AAC/F,QAAM,OAA6B,UAAU,QAAQ,CAAC;AACtD,QAAM,WAAgB,EAAE,MAAM,SAAS,MAAM,IAAI,SAAS,GAAG;AAC7D,MAAI,KAAK,WAAY,UAAS,aAAa,EAAE,GAAG,KAAK,WAAW;AAChE,MAAI,KAAK,cAAe,UAAS,gBAAgB,EAAE,GAAG,KAAK,cAAc;AACzE,MAAI,KAAK,KAAM,UAAS,OAAO,EAAE,GAAG,KAAK,KAAK;AAC9C,SAAO;AACT;AAPgB;AAaT,SAAS,cAAc,QAAa,UAAwC,MAAiC;AAClH,QAAM,WAAW,UAAU;AAC3B,QAAM,WAAgB;AAAA,IACpB,GAAG;AAAA,IACH,MAAM,SAAS;AAAA,IACf,IAAI,SAAS;AAAA,IACb,YAAY,EAAE,GAAI,SAAS,cAAc,CAAC,GAAI,GAAI,MAAM,cAAc,CAAC,EAAG;AAAA,IAC1E,eAAe,EAAE,GAAI,SAAS,iBAAiB,CAAC,GAAI,GAAI,MAAM,iBAAiB,CAAC,EAAG;AAAA,EACrF;AACA,MAAI,MAAM,KAAM,UAAS,OAAO,EAAE,GAAI,SAAS,QAAQ,CAAC,GAAI,GAAG,KAAK,KAAK;AACzE,SAAO;AACT;AAXgB;AAahB,SAAS,QAAgB;AACvB,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,WAAY,QAAO,OAAO,WAAW;AACvG,SAAO,SAAS,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AACnE;AAHS;AAKT,eAAe,OAAO,KAAyB,QAAuB,MAAsC;AAC1G,QAAM,aAAa,aAAa,QAAQ,IAAI;AAC5C,QAAM,KAAa,IAAI,QAAQ,MAAM,MAAM,MAAM,MAAM;AACvD,QAAM,WAAW,qBAAqB,IAAI,QAAQ,MAAM,EAAE,MAAM,GAAG,CAAC;AACpE,QAAM,SAAS,MAAM,kBAAkB,IAAI,OAAO,QAAQ,QAAQ;AAClE,MAAI,CAAC,OAAQ,QAAO,sBAAsB,SAAS,IAAI,oDAAoD;AAC3G,MAAI,kBAAkB,MAAM,EAAG,QAAO,sBAAsB,2BAA2B;AAEvF,QAAM,SAAuB,EAAE,MAAM,IAAI,QAAQ,UAAU,WAAW,MAAM,MAAM,MAAM,WAAW,KAAK;AACxG,QAAM,IAAI,MAAM,YAAY,OAAO,UAAU;AAC3C,UAAM,MAAM,UAAU,MAAM;AAC5B,UAAM,kBAAkB,OAAO,YAAY,EAAE,MAAM,GAAG,GAAG,CAAC,GAAG,SAAS,iBAAiB,CAAC,CAAC;AACzF,UAAM,MAAM,QAAQ;AAAA,MAClB;AAAA,MACA,QAAQ,IAAI,QAAQ;AAAA,MACpB,MAAM,UAAU,GAAG;AAAA,MACnB,MAAM,IAAI,QAAQ;AAAA,MAClB,MAAM;AAAA,IACR,CAAC;AAED,QAAI,YAAY,iBAAiB;AAC/B,YAAM,WAAW,gBAAgB;AAAA,QAC/B;AAAA,QACA,QAAQ,WAAW;AAAA,QACnB;AAAA,QACA,QAAQ;AAAA,QACR;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,SAAO,aAAa,IAAI,OAAO,QAAQ,GAAG;AAC5C;AAjCe;AAmCf,eAAe,OAAO,KAAyB,QAAuB,MAAsC;AAC1G,QAAM,KAAK,IAAI,OAAO;AACtB,QAAM,WAAW,MAAM,IAAI,MAAM,UAAU,MAAM,EAAE;AACnD,MAAI,CAAC,SAAU,QAAO,kBAAkB,MAAM,EAAE;AAChD,MAAI,kBAAkB,SAAS,MAAM,EAAG,QAAO,sBAAsB,2BAA2B;AAEhG,QAAM,OAAO,IAAI,QAAQ,MAAM,QAAQ,CAAC;AACxC,QAAM,SAAS,SAAS,YAAY,EAAE,MAAM,GAAG;AAC/C,QAAM,WAAW,cAAc,QAAQ,EAAE,MAAM,GAAG,GAAG,IAAI;AAEzD,QAAM,OAAO,SAAS,YAAY,OAAQ,SAAS,QAAQ;AAE3D,QAAM,aAAa,aAAa,QAAQ,IAAI;AAC5C,QAAM,SAAS,IAAI,QAAQ,WAAW,UAAU,UAAU;AAC1D,QAAM,SAAuB,EAAE,GAAG,UAAU,UAAU,MAAM,WAAW,SAAS,UAAU;AAC1F,QAAM,IAAI,MAAM,YAAY,OAAO,UAAU;AAC3C,UAAM,MAAM,UAAU,MAAM;AAC5B,UAAM,kBAAkB,OAAO,YAAY,EAAE,MAAM,GAAG,GAAG,OAAO,iBAAiB,CAAC,GAAG,KAAK,iBAAiB,CAAC,CAAC;AAC7G,UAAM,MAAM,QAAQ;AAAA,MAClB,QAAQ,SAAS;AAAA,MACjB,QAAQ,IAAI,QAAQ;AAAA,MACpB,MAAM,UAAU,GAAG;AAAA,MACnB,MAAM,IAAI,QAAQ;AAAA,MAClB,YAAY;AAAA,MACZ,UAAU;AAAA,MACV;AAAA,IACF,CAAC;AACD,QAAI,YAAY,iBAAiB;AAC/B,YAAM,WAAW,gBAAgB,EAAE,OAAO,QAAQ,WAAW,QAAQ,IAAI,QAAQ,UAAU,UAAU,OAAO,CAAC;AAAA,IAC/G;AAAA,EACF,CAAC;AAED,SAAO,aAAa,IAAI,OAAO,MAAM;AACvC;AAjCe;AAmCf,eAAe,OAAO,KAAyB,QAAuB,MAAsC;AAC1G,QAAM,KAAK,IAAI,OAAO;AACtB,QAAM,WAAW,MAAM,IAAI,MAAM,UAAU,MAAM,EAAE;AACnD,MAAI,CAAC,SAAU,QAAO,kBAAkB,MAAM,EAAE;AAChD,MAAI,kBAAkB,SAAS,MAAM,EAAG,QAAO,sBAAsB,2BAA2B;AAEhG,QAAM,OAAO,SAAS,YAAY,OAAQ,SAAS,QAAQ,SAAS;AAEpE,QAAM,IAAI,MAAM,YAAY,OAAO,UAAU;AAC3C,UAAM,QAAQ,CAAC,GAAI,MAAM,MAAM,UAAU,EAAE,GAAI,GAAI,MAAM,MAAM,QAAQ,EAAE,CAAE;AAC3E,eAAW,QAAQ,MAAO,OAAM,MAAM,WAAW,EAAE,QAAQ,KAAK,QAAQ,MAAM,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC;AAC1G,UAAM,MAAM,aAAa,MAAM,EAAE;AACjC,UAAM,MAAM,QAAQ;AAAA,MAClB,QAAQ,SAAS;AAAA,MACjB,QAAQ;AAAA,MACR,MAAM,UAAU,GAAG;AAAA,MACnB,YAAY;AAAA,MACZ,UAAU;AAAA,MACV;AAAA,IACF,CAAC;AACD,UAAM,aAAa,aAAa,QAAQ,IAAI;AAC5C,QAAI,YAAY,iBAAiB;AAC/B,YAAM,WAAW,gBAAgB;AAAA,QAC/B;AAAA,QACA,QAAQ,WAAW;AAAA,QACnB;AAAA,QACA,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,UAAU,SAAS;AAAA,MACrB,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,SAAO,EAAE,QAAQ,KAAK,MAAM,OAAU;AACxC;AAlCe;AAqCR,SAAS,UAAU,KAAkD;AAC1E,QAAM,QAAQ,IAAI,QAAQ,MAAM,SAAS;AACzC,SAAO,QAAQ,GAAG,IAAI,QAAQ,IAAI,IAAI,KAAK,KAAK,IAAI,QAAQ;AAC9D;AAHgB;AAShB,SAAS,cAAc,YAA+B,QAA6D;AACjH,SAAO,OAAO,QACZ,WAAW,WAAW,sBAAsB,GAAG,WAAW,OAAO,IAAI,uBAAuB,IAAI,OAAO,GAAG;AAC9G;AAHS;AAUF,SAAS,mBAAmB,QAAuC;AACxE,QAAM,SAAyB,CAAC;AAChC,QAAM,QAAQ,wBAAC,QAAuB,SAAkC,WACtE,OAAO,KAAK,EAAE,QAAQ,SAAS,QAAQ,OAAO,MAAM,CAAC,GADzC;AAGd,aAAW,cAAc,OAAO,OAAO;AACrC,UAAM,OAAO,WAAW,OAAO;AAC/B,UAAM,OAAO,CAAC,WAAW,QAAQ,KAAK,GAAG,CAAC,QAAQ,OAAO,KAAK,IAAI,CAAC;AACnE,eAAW,QAAQ,UAAU,MAAM,GAAG;AACpC,YAAM,OAAO,CAAC,KAAK,QAAQ,WAAW,WAAW,MAAM,GAAG,CAAC,QAAQ,SAAS,KAAK,MAAM,UAAU,CAAC;AAAA,IACpG;AACA;AAAA,MACE;AAAA,MACA,CAAC,WAAW,MAAM;AAAA,MAClB,cAAc,YAAY,CAAC,QAAQ,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AACA;AAAA,MACE;AAAA,MACA,CAAC,WAAW,QAAQ,KAAK;AAAA,MACzB,cAAc,YAAY,CAAC,QAAQ,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AACA;AAAA,MACE;AAAA,MACA,CAAC,WAAW,QAAQ,KAAK;AAAA,MACzB,cAAc,YAAY,CAAC,QAAQ,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AACA;AAAA,MACE;AAAA,MACA,CAAC,WAAW,QAAQ,KAAK;AAAA,MACzB,cAAc,YAAY,CAAC,QAAQ,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,aAAW,cAAc,OAAO,OAAO;AACrC,UAAM,OAAO,CAAC,eAAe,aAAa,WAAW,MAAM,GAAG,CAAC,QAAQ,YAAY,KAAK,QAAQ,UAAU,CAAC;AAAA,EAC7G;AAEA,SAAO;AACT;AArCgB;;;ACnrBhB,SAAS,UAAU,MAAwB;AACzC,QAAM,eAAe,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AACpD,SAAO,aAAa,MAAM,GAAG,EAAE,OAAO,CAAC,YAAY,QAAQ,SAAS,CAAC;AACvE;AAHS;AAMT,SAAS,cAAc,SAAqC;AAC1D,SAAO,QAAQ,QAAQ,CAAC,YAAY,UAAU,OAAO,YAAY,WAAW,UAAU,QAAQ,IAAI,CAAC;AACrG;AAFS;AAIF,SAAS,aAAa,SAA2B,MAA6C;AACnG,QAAM,WAAW,cAAc,OAAO;AACtC,QAAM,SAAS,UAAU,IAAI;AAC7B,MAAI,SAAS,WAAW,OAAO,OAAQ,QAAO;AAE9C,QAAM,SAAiC,CAAC;AACxC,WAAS,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS;AACpD,UAAM,UAAU,SAAS,KAAK;AAC9B,UAAM,QAAQ,mBAAmB,OAAO,KAAK,CAAC;AAC9C,QAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,aAAO,QAAQ,MAAM,CAAC,CAAC,IAAI;AAAA,IAC7B,WAAW,YAAY,OAAO;AAC5B,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAhBgB;AAmBT,SAAS,WAAW,QAAwB,QAAuB,MAAiC;AACzG,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,WAAW,OAAQ;AAC7B,UAAM,SAAS,aAAa,MAAM,SAAS,IAAI;AAC/C,QAAI,OAAQ,QAAO,EAAE,OAAO,OAAO;AAAA,EACrC;AACA,SAAO;AACT;AAPgB;;;ACJhB,IAAM,UAA2B,CAAC,OAAO,QAAQ,OAAO,SAAS,QAAQ;AAEzE,SAAS,WAAqB;AAC5B,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,CAAC,WAAW,eAAe,GAAG;AAC/C,QAAI;AACF,YAAM,OAAO,KAAK;AAClB,UAAI,KAAM,OAAM,KAAK,IAAI;AAAA,IAC3B,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAXS;AAcF,SAAS,YAAY,KAAuD;AACjF,MAAI;AACJ,aAAW,QAAQ,SAAS,GAAG;AAC7B,QAAI,IAAI,WAAW,IAAI,GAAG;AACxB,iBAAW,IAAI,MAAM,KAAK,MAAM;AAChC;AAAA,IACF;AAAA,EACF;AACA,MAAI,aAAa,QAAW;AAC1B,QAAI;AACF,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,iBAAW,GAAG,OAAO,QAAQ,GAAG,OAAO,MAAM;AAAA,IAC/C,QAAQ;AACN,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,CAAC,UAAU,GAAG,UAAU,IAAI,SAAS,MAAM,GAAG;AACpD,SAAO;AAAA,IACL,MAAM,SAAS,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAAA,IACrD,OAAO,IAAI,gBAAgB,WAAW,KAAK,GAAG,CAAC;AAAA,EACjD;AACF;AAtBgB;AAwBhB,SAAS,UAAU,QAAgC;AACjD,QAAM,KAAK,OAAO,UAAU,OAAO,OAAO,SAAS;AACnD,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,OAAO;AAAA,IACf,YAAY,KAAK,OAAO,OAAO,WAAW,MAAM,wBAAwB;AAAA,IACxE,MAAM,OAAO;AAAA,EACf;AACF;AARS;AAWT,eAAe,qBACb,OACA,SACA,QACA,kBAC6B;AAC7B,QAAM,SAAS,iBAAiB;AAChC,MAAI,oBAAoB,OAAO,gBAAgB,EAAG,QAAO,OAAO,gBAAgB;AAEhF,aAAW,SAAS,OAAO,OAAO,MAAM,GAAG;AACzC,eAAW,cAAc,OAAO,OAAO;AACrC,YAAM,SAAS,MAAM,MAAM,UAAU,WAAW,OAAO,MAAM,KAAK;AAClE,UAAI,QAAQ,OAAQ,QAAO,OAAO;AAAA,IACpC;AAAA,EACF;AACA,SAAO,kBAAkB,OAAO,QAAQ,QAAQ,MAAM,IAAI;AAC5D;AAhBe;AAkBf,eAAe,oBAAoB,OAAmC;AACpE,QAAM,UAAU,MAAM,MAAM,MAAM;AAClC,kBAAgB,EAAE,SAAS,QAAQ,OAAO,CAAC,UAAU,MAAM,WAAW,UAAU,EAAE,OAAO,CAAC;AAC5F;AAHe;AAUf,eAAsB,eAAe,SAAkD;AACrF,MAAI;AACF,UAAM,SAAS,QAAQ,OAAO,YAAY;AAC1C,QAAI,CAAC,QAAQ,SAAS,MAAM,EAAG,QAAO,UAAU,sBAAsB,CAAC;AACvE,QAAI,QAAQ,MAAO,QAAO,UAAU,sBAAsB,oCAAoC,CAAC;AAE/F,UAAM,SAAS,iBAAiB;AAChC,UAAM,EAAE,MAAM,MAAM,IAAI,YAAY,QAAQ,GAAG;AAC/C,UAAM,iBAAiC,EAAE,QAAQ,MAAM,OAAO,MAAM,QAAQ,MAAM,KAAK,QAAQ,IAAI;AAEnG,UAAM,SAAS,WAAW,OAAO,QAAQ,QAAQ,IAAI;AACrD,UAAM,QAAQ,UAAU,WAAW,mBAAmB,MAAM,GAAG,QAAQ,IAAI;AAC3E,QAAI,CAAC,MAAO,QAAO,UAAU,sBAAsB,GAAG,MAAM,IAAI,IAAI,wBAAwB,CAAC;AAE7F,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,gBAAgB;AAAA,IAChC,SAAS,OAAO;AAGd,UAAI,iBAAiB,qBAAsB,QAAO,UAAU,sBAAsB,MAAM,OAAO,CAAC;AAChG,cAAQ,KAAK,iDAAiD,KAAK;AACnE,aAAO,UAAU,sBAAsB,8CAA8C,CAAC;AAAA,IACxF;AACA,UAAM,MAAM,EAAE,SAAS,gBAAgB,QAAQ,MAAM,QAAQ,MAAM;AAInE,UAAM,YAAY,IAAI,IAAI,UAAU,MAAM,EAAE,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC;AAC3E,UAAM,eAAe,8BAAO,UAAqD;AAC/E,YAAM,YAAY,MAAM,QAAQ;AAAA,QAC9B,CAAC,YAAY,OAAO,YAAY,YAAY,UAAU,IAAI,QAAQ,IAAI;AAAA,MACxE;AACA,YAAM,OAAO,aAAa,IAAI,MAAM,QAAQ,YAAY,CAAC,IAAI;AAC7D,YAAM,YAAY,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG,IAAI,KAAK,MAAM,CAAC,IAAI;AACrF,aAAO,qBAAqB,OAAO,gBAAgB,MAAM,QAAQ,SAAS;AAAA,IAC5E,GAPqB;AASrB,QAAI,CAAC,UAAU,CAAC,OAAO,MAAM,OAAO;AAElC,UAAI,UAAU,WAAW,OAAO;AAC9B,cAAMA,UAAS,MAAM,aAAa,OAAO,KAAK;AAC9C,YAAIA,WAAU,kBAAkBA,OAAM,GAAG;AACvC,iBAAO,UAAU,sBAAsB,2BAA2B,CAAC;AAAA,QACrE;AAAA,MACF;AACA,YAAMC,UAAS,MAAM,MAAM,MAAM,OAAO,GAAG;AAC3C,UAAI,WAAW,MAAO,OAAM,oBAAoB,KAAK;AACrD,aAAO,UAAUA,OAAM;AAAA,IACzB;AAIA,UAAM,SAAS,MAAM,aAAa,OAAO,KAAK;AAC9C,QAAI,CAAC,OAAQ,QAAO,UAAU,sBAAsB,GAAG,MAAM,IAAI,IAAI,2CAA2C,CAAC;AACjH,QAAI,kBAAkB,MAAM,EAAG,QAAO,UAAU,sBAAsB,2BAA2B,CAAC;AAElG,UAAM,SAAS,MAAM,MAAM,YAAY,OAAO,qBAAqB;AACjE,YAAM,SAAS,MAAM,OAAO,MAAM,OAAO,EAAE,GAAG,KAAK,OAAO,iBAAiB,CAAC;AAC5E,UAAI,OAAO,UAAU,OAAO,OAAO,SAAS,KAAK;AAC/C,cAAM,iBAAiB,QAAQ;AAAA,UAC7B;AAAA,UACA;AAAA,UACA,MAAM,UAAU,GAAG;AAAA,UACnB,MAAM,QAAQ;AAAA,UACd,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AACA,aAAO;AAAA,IACT,CAAC;AACD,UAAM,oBAAoB,KAAK;AAC/B,WAAO,UAAU,MAAM;AAAA,EACzB,SAAS,OAAO;AACd,QAAI,iBAAiB,qBAAsB,QAAO,UAAU,sBAAsB,MAAM,OAAO,CAAC;AAChG,YAAQ,MAAM,iCAAiC,KAAK;AACpD,WAAO,UAAU;AAAA,MACf,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,QAAQ,CAAC,EAAE,QAAQ,OAAO,OAAO,yBAAyB,QAAQ,OAAQ,OAAiB,WAAW,KAAK,EAAE,CAAC;AAAA,MAChH;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAlFsB;","names":["rootId","answer"]}