@carlonicora/nextjs-jsonapi 6.12.1 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/dist/{ApiData-DPKNfY-9.d.mts → ApiData-Be8yxCOX.d.mts} +2 -0
  2. package/dist/{ApiData-DPKNfY-9.d.ts → ApiData-Be8yxCOX.d.ts} +2 -0
  3. package/dist/{BlockNoteEditor-N2NBYCPC.mjs → BlockNoteEditor-22Q4DJBU.mjs} +20 -8
  4. package/dist/BlockNoteEditor-22Q4DJBU.mjs.map +1 -0
  5. package/dist/{BlockNoteEditor-CBYKEXAQ.js → BlockNoteEditor-2VLPSEKS.js} +34 -22
  6. package/dist/BlockNoteEditor-2VLPSEKS.js.map +1 -0
  7. package/dist/{BlockNoteViewer-WGCDB7IJ.js → BlockNoteViewer-2XYASA5Y.js} +8 -5
  8. package/dist/BlockNoteViewer-2XYASA5Y.js.map +1 -0
  9. package/dist/{BlockNoteViewer-MTGS6YJV.mjs → BlockNoteViewer-NBW2H6LC.mjs} +7 -4
  10. package/dist/{BlockNoteViewer-MTGS6YJV.mjs.map → BlockNoteViewer-NBW2H6LC.mjs.map} +1 -1
  11. package/dist/{JsonApiRequest-A66XK5WT.mjs → JsonApiRequest-F6TMJ42G.mjs} +2 -2
  12. package/dist/JsonApiRequest-WISJZSDE.js +26 -0
  13. package/dist/{JsonApiRequest-FZGPLNUF.js.map → JsonApiRequest-WISJZSDE.js.map} +1 -1
  14. package/dist/billing/index.js +499 -494
  15. package/dist/billing/index.js.map +1 -1
  16. package/dist/billing/index.mjs +13 -8
  17. package/dist/billing/index.mjs.map +1 -1
  18. package/dist/chunk-27LQDGNP.mjs +317 -0
  19. package/dist/chunk-27LQDGNP.mjs.map +1 -0
  20. package/dist/{chunk-6NZKWIOR.js → chunk-2H342HNN.js} +956 -929
  21. package/dist/chunk-2H342HNN.js.map +1 -0
  22. package/dist/{chunk-KSYQEKJX.js → chunk-2SV4SS6N.js} +14 -14
  23. package/dist/{chunk-KSYQEKJX.js.map → chunk-2SV4SS6N.js.map} +1 -1
  24. package/dist/{chunk-4FNGWALA.mjs → chunk-47BBY5KA.mjs} +6 -4
  25. package/dist/{chunk-4FNGWALA.mjs.map → chunk-47BBY5KA.mjs.map} +1 -1
  26. package/dist/chunk-6JUCIWDC.mjs +570 -0
  27. package/dist/chunk-6JUCIWDC.mjs.map +1 -0
  28. package/dist/chunk-7DBJTNSU.mjs +1049 -0
  29. package/dist/chunk-7DBJTNSU.mjs.map +1 -0
  30. package/dist/chunk-7JZHPMGW.mjs +704 -0
  31. package/dist/chunk-7JZHPMGW.mjs.map +1 -0
  32. package/dist/{chunk-L6PYFQTO.js → chunk-7ZHTPFFT.js} +16 -14
  33. package/dist/chunk-7ZHTPFFT.js.map +1 -0
  34. package/dist/chunk-B6JGKDP2.js +214 -0
  35. package/dist/chunk-B6JGKDP2.js.map +1 -0
  36. package/dist/{chunk-3YW35YHQ.mjs → chunk-BH6JWIKI.mjs} +5 -5
  37. package/dist/chunk-BZ5J2FIV.js +570 -0
  38. package/dist/chunk-BZ5J2FIV.js.map +1 -0
  39. package/dist/{chunk-V5MI7RNB.mjs → chunk-DGWOVYEC.mjs} +2 -2
  40. package/dist/chunk-DOCIFE6A.mjs +212 -0
  41. package/dist/chunk-DOCIFE6A.mjs.map +1 -0
  42. package/dist/chunk-GB6DBFYY.js +25 -0
  43. package/dist/chunk-GB6DBFYY.js.map +1 -0
  44. package/dist/{chunk-6XJOQFCP.mjs → chunk-H4NLDX5G.mjs} +59 -32
  45. package/dist/chunk-H4NLDX5G.mjs.map +1 -0
  46. package/dist/{chunk-EWCY6QJA.mjs → chunk-JJMMT24D.mjs} +37 -3
  47. package/dist/chunk-JJMMT24D.mjs.map +1 -0
  48. package/dist/{chunk-JTPRJNT2.js → chunk-KMW7UNL2.js} +38 -3
  49. package/dist/chunk-KMW7UNL2.js.map +1 -0
  50. package/dist/chunk-MHB36MYE.js +317 -0
  51. package/dist/chunk-MHB36MYE.js.map +1 -0
  52. package/dist/{chunk-FF4EIQ6H.js → chunk-MOGEGWXW.js} +1039 -1680
  53. package/dist/chunk-MOGEGWXW.js.map +1 -0
  54. package/dist/{chunk-AA52ORW2.mjs → chunk-P445QGOC.mjs} +61 -702
  55. package/dist/chunk-P445QGOC.mjs.map +1 -0
  56. package/dist/chunk-PV4Z5W6V.js +212 -0
  57. package/dist/chunk-PV4Z5W6V.js.map +1 -0
  58. package/dist/chunk-PXDUJ47N.js +1049 -0
  59. package/dist/chunk-PXDUJ47N.js.map +1 -0
  60. package/dist/{chunk-GHE4VT2R.js → chunk-QKNGZDSQ.js} +2 -2
  61. package/dist/{chunk-GHE4VT2R.js.map → chunk-QKNGZDSQ.js.map} +1 -1
  62. package/dist/chunk-W62MHDWM.js +704 -0
  63. package/dist/chunk-W62MHDWM.js.map +1 -0
  64. package/dist/chunk-XXP3TQHF.mjs +25 -0
  65. package/dist/chunk-XXP3TQHF.mjs.map +1 -0
  66. package/dist/chunk-YRKIFYOE.mjs +214 -0
  67. package/dist/chunk-YRKIFYOE.mjs.map +1 -0
  68. package/dist/{chunk-O3PDL2CV.js → chunk-Z4POQWFG.js} +40 -6
  69. package/dist/chunk-Z4POQWFG.js.map +1 -0
  70. package/dist/{chunk-PI56A4DG.mjs → chunk-ZLTOUZN2.mjs} +37 -2
  71. package/dist/chunk-ZLTOUZN2.mjs.map +1 -0
  72. package/dist/client/index.d.mts +2 -2
  73. package/dist/client/index.d.ts +2 -2
  74. package/dist/client/index.js +14 -9
  75. package/dist/client/index.js.map +1 -1
  76. package/dist/client/index.mjs +24 -19
  77. package/dist/components/index.d.mts +7 -1
  78. package/dist/components/index.d.ts +7 -1
  79. package/dist/components/index.js +11 -7
  80. package/dist/components/index.js.map +1 -1
  81. package/dist/components/index.mjs +10 -6
  82. package/dist/contexts/index.d.mts +1 -1
  83. package/dist/contexts/index.d.ts +1 -1
  84. package/dist/contexts/index.js +11 -7
  85. package/dist/contexts/index.js.map +1 -1
  86. package/dist/contexts/index.mjs +10 -6
  87. package/dist/core/index.d.mts +128 -2
  88. package/dist/core/index.d.ts +128 -2
  89. package/dist/core/index.js +17 -4
  90. package/dist/core/index.js.map +1 -1
  91. package/dist/core/index.mjs +22 -9
  92. package/dist/email-automations/index.js +101 -96
  93. package/dist/email-automations/index.js.map +1 -1
  94. package/dist/email-automations/index.mjs +13 -8
  95. package/dist/email-automations/index.mjs.map +1 -1
  96. package/dist/features/analytics/index.js +128 -123
  97. package/dist/features/analytics/index.js.map +1 -1
  98. package/dist/features/analytics/index.mjs +13 -8
  99. package/dist/features/analytics/index.mjs.map +1 -1
  100. package/dist/features/analytics/server.js +11 -8
  101. package/dist/features/analytics/server.js.map +1 -1
  102. package/dist/features/analytics/server.mjs +9 -6
  103. package/dist/features/analytics/server.mjs.map +1 -1
  104. package/dist/features/blog/index.js +158 -153
  105. package/dist/features/blog/index.js.map +1 -1
  106. package/dist/features/blog/index.mjs +14 -9
  107. package/dist/features/blog/index.mjs.map +1 -1
  108. package/dist/features/blog/server.js +30 -26
  109. package/dist/features/blog/server.js.map +1 -1
  110. package/dist/features/blog/server.mjs +10 -6
  111. package/dist/features/blog/server.mjs.map +1 -1
  112. package/dist/features/help/index.js +44 -40
  113. package/dist/features/help/index.js.map +1 -1
  114. package/dist/features/help/index.mjs +10 -6
  115. package/dist/features/help/index.mjs.map +1 -1
  116. package/dist/features/tokenusage/index.js +134 -129
  117. package/dist/features/tokenusage/index.js.map +1 -1
  118. package/dist/features/tokenusage/index.mjs +13 -8
  119. package/dist/features/tokenusage/index.mjs.map +1 -1
  120. package/dist/index.d.mts +2 -2
  121. package/dist/index.d.ts +2 -2
  122. package/dist/index.js +18 -5
  123. package/dist/index.js.map +1 -1
  124. package/dist/index.mjs +24 -11
  125. package/dist/lifecycle-4GUOTB46.js +23 -0
  126. package/dist/lifecycle-4GUOTB46.js.map +1 -0
  127. package/dist/lifecycle-LKGPRBS7.mjs +23 -0
  128. package/dist/local-transport-7H3SPZSA.js +15 -0
  129. package/dist/local-transport-7H3SPZSA.js.map +1 -0
  130. package/dist/local-transport-QYFKA6AN.mjs +15 -0
  131. package/dist/local-transport-QYFKA6AN.mjs.map +1 -0
  132. package/dist/offline/index.d.mts +607 -0
  133. package/dist/offline/index.d.ts +607 -0
  134. package/dist/offline/index.js +1052 -0
  135. package/dist/offline/index.js.map +1 -0
  136. package/dist/offline/index.mjs +1052 -0
  137. package/dist/offline/index.mjs.map +1 -0
  138. package/dist/offline/sqlite/device-worker.mjs +62 -0
  139. package/dist/offline/sqlite/index.mjs +17392 -0
  140. package/dist/offline/sqlite/sqlite3-opfs-async-proxy.js +881 -0
  141. package/dist/offline/sqlite/sqlite3.wasm +0 -0
  142. package/dist/request-KTBV6HHH.js +10 -0
  143. package/dist/request-KTBV6HHH.js.map +1 -0
  144. package/dist/request-SQOUNWZ4.mjs +10 -0
  145. package/dist/request-SQOUNWZ4.mjs.map +1 -0
  146. package/dist/server/cachedGet.d.mts +1 -1
  147. package/dist/server/cachedGet.d.ts +1 -1
  148. package/dist/server/cachedGet.stub.d.mts +1 -1
  149. package/dist/server/cachedGet.stub.d.ts +1 -1
  150. package/dist/server/index.d.mts +28 -2
  151. package/dist/server/index.d.ts +28 -2
  152. package/dist/server/index.js +18 -8
  153. package/dist/server/index.js.map +1 -1
  154. package/dist/server/index.mjs +18 -8
  155. package/dist/{useSocket-IRHtk2xc.d.ts → useSocket-Bo9M0t0J.d.ts} +3 -1
  156. package/dist/{useSocket-C8fqU4od.d.mts → useSocket-J5qanQPN.d.mts} +3 -1
  157. package/dist/write-through-FDQ7N4YI.js +15 -0
  158. package/dist/write-through-FDQ7N4YI.js.map +1 -0
  159. package/dist/write-through-R25VQMH6.mjs +15 -0
  160. package/dist/write-through-R25VQMH6.mjs.map +1 -0
  161. package/package.json +7 -1
  162. package/pwa/sw.js +277 -26
  163. package/src/client/request.ts +37 -0
  164. package/src/components/containers/RoundPageContainer.tsx +8 -8
  165. package/src/components/editors/BlockNoteEditor.tsx +14 -0
  166. package/src/components/editors/__tests__/BlockNoteEditor.empty-change.spec.tsx +205 -0
  167. package/src/contexts/SocketContext.tsx +7 -1
  168. package/src/contexts/__tests__/SocketContext.session.spec.tsx +52 -5
  169. package/src/core/factories/RehydrationFactory.ts +28 -8
  170. package/src/core/factories/__tests__/RehydrationFactory.spec.ts +33 -0
  171. package/src/core/index.ts +3 -0
  172. package/src/core/interfaces/ApiData.ts +2 -0
  173. package/src/core/registry/ModuleRegistry.ts +3 -0
  174. package/src/features/auth/components/forms/Logout.tsx +8 -1
  175. package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +62 -0
  176. package/src/hooks/__tests__/useSocket.pause.spec.ts +39 -0
  177. package/src/hooks/useSocket.ts +20 -1
  178. package/src/offline/__tests__/answer-builders.spec.ts +176 -0
  179. package/src/offline/__tests__/device-switch.spec.ts +179 -0
  180. package/src/offline/__tests__/generic-answers.spec.ts +380 -0
  181. package/src/offline/__tests__/local-transport.spec.ts +150 -0
  182. package/src/offline/__tests__/logout.spec.ts +122 -0
  183. package/src/offline/__tests__/offline-fixtures.ts +89 -0
  184. package/src/offline/__tests__/router.spec.ts +56 -0
  185. package/src/offline/__tests__/state.spec.ts +62 -0
  186. package/src/offline/__tests__/write-through.spec.ts +137 -0
  187. package/src/offline/config.ts +229 -0
  188. package/src/offline/configure.ts +14 -0
  189. package/src/offline/device-switch.ts +75 -0
  190. package/src/offline/generic-answers.ts +706 -0
  191. package/src/offline/index.ts +67 -0
  192. package/src/offline/local-transport.ts +189 -0
  193. package/src/offline/logout.ts +41 -0
  194. package/src/offline/router.ts +41 -0
  195. package/src/offline/server/__tests__/build-offline-manifest.spec.ts +76 -0
  196. package/src/offline/server/build-offline-manifest.ts +49 -0
  197. package/src/offline/state-hooks.ts +10 -0
  198. package/src/offline/state.ts +143 -0
  199. package/src/offline/store/DeviceStore.ts +112 -0
  200. package/src/offline/store/__tests__/device-store-lifecycle.spec.ts +343 -0
  201. package/src/offline/store/__tests__/shared-database.spec.ts +151 -0
  202. package/src/offline/store/__tests__/sql-device-store.spec.ts +399 -0
  203. package/src/offline/store/lifecycle.ts +315 -0
  204. package/src/offline/store/schema.ts +101 -0
  205. package/src/offline/store/shared-database.ts +321 -0
  206. package/src/offline/store/sql-device-store.ts +469 -0
  207. package/src/offline/store/worker-client.ts +71 -0
  208. package/src/offline/store/worker.ts +87 -0
  209. package/src/offline/sync/__tests__/conflicts.spec.ts +185 -0
  210. package/src/offline/sync/__tests__/queue-drain.spec.ts +364 -0
  211. package/src/offline/sync/__tests__/runtime.spec.ts +337 -0
  212. package/src/offline/sync/__tests__/sync-fixtures.ts +103 -0
  213. package/src/offline/sync/__tests__/sync-root.spec.ts +727 -0
  214. package/src/offline/sync/artwork-cache.ts +67 -0
  215. package/src/offline/sync/conflicts.ts +211 -0
  216. package/src/offline/sync/counts.ts +39 -0
  217. package/src/offline/sync/drain-queue.ts +246 -0
  218. package/src/offline/sync/known-roots.ts +37 -0
  219. package/src/offline/sync/models/OfflineChangeSet.ts +34 -0
  220. package/src/offline/sync/models/OfflineChangeSetInterface.ts +54 -0
  221. package/src/offline/sync/models/RootSync.ts +68 -0
  222. package/src/offline/sync/models/RootSyncInterface.ts +20 -0
  223. package/src/offline/sync/models/__tests__/OfflineChangeSet.spec.ts +47 -0
  224. package/src/offline/sync/models/__tests__/RootSync.spec.ts +54 -0
  225. package/src/offline/sync/models/index.ts +5 -0
  226. package/src/offline/sync/models/offline.modules.ts +14 -0
  227. package/src/offline/sync/runtime.ts +226 -0
  228. package/src/offline/sync/session-refresh.ts +32 -0
  229. package/src/offline/sync/sync-root.ts +336 -0
  230. package/src/offline/sync/waiting.ts +40 -0
  231. package/src/offline/types.ts +115 -0
  232. package/src/offline/write-through.ts +189 -0
  233. package/src/pwa/__tests__/sw.spec.ts +358 -1
  234. package/src/server/index.ts +5 -0
  235. package/src/unified/__tests__/JsonApiRequest.offline.spec.ts +243 -0
  236. package/dist/BlockNoteEditor-CBYKEXAQ.js.map +0 -1
  237. package/dist/BlockNoteEditor-N2NBYCPC.mjs.map +0 -1
  238. package/dist/BlockNoteViewer-WGCDB7IJ.js.map +0 -1
  239. package/dist/JsonApiRequest-FZGPLNUF.js +0 -26
  240. package/dist/chunk-6NZKWIOR.js.map +0 -1
  241. package/dist/chunk-6XJOQFCP.mjs.map +0 -1
  242. package/dist/chunk-AA52ORW2.mjs.map +0 -1
  243. package/dist/chunk-EWCY6QJA.mjs.map +0 -1
  244. package/dist/chunk-FF4EIQ6H.js.map +0 -1
  245. package/dist/chunk-JTPRJNT2.js.map +0 -1
  246. package/dist/chunk-L6PYFQTO.js.map +0 -1
  247. package/dist/chunk-O3PDL2CV.js.map +0 -1
  248. package/dist/chunk-PI56A4DG.mjs.map +0 -1
  249. package/dist/request-5RLK2AM3.mjs +0 -9
  250. package/dist/request-EUGTI3BE.js +0 -9
  251. package/dist/request-EUGTI3BE.js.map +0 -1
  252. /package/dist/{JsonApiRequest-A66XK5WT.mjs.map → JsonApiRequest-F6TMJ42G.mjs.map} +0 -0
  253. /package/dist/{chunk-3YW35YHQ.mjs.map → chunk-BH6JWIKI.mjs.map} +0 -0
  254. /package/dist/{chunk-V5MI7RNB.mjs.map → chunk-DGWOVYEC.mjs.map} +0 -0
  255. /package/dist/{request-5RLK2AM3.mjs.map → lifecycle-LKGPRBS7.mjs.map} +0 -0
@@ -0,0 +1,1049 @@
1
+ import {
2
+ DeviceStoreUnavailableError,
3
+ OfflineUnavailableError,
4
+ forgetOfflineStore,
5
+ getDeviceWorkerUrl,
6
+ getSqliteModuleUrl,
7
+ isOfflineAvailable,
8
+ isOfflineStoreBlocked,
9
+ markOfflineUnavailable,
10
+ refreshOfflineDeviceState,
11
+ unblockOfflineStore
12
+ } from "./chunk-27LQDGNP.mjs";
13
+ import {
14
+ __name
15
+ } from "./chunk-PAWJFY3S.mjs";
16
+
17
+ // src/offline/sync/artwork-cache.ts
18
+ var IMAGES_CACHE_SUFFIX = "-images";
19
+ var OFFLINE_CACHE_MARKER = "-offline-";
20
+ async function imagesCacheName() {
21
+ if (typeof caches === "undefined") return void 0;
22
+ const keys = await caches.keys();
23
+ const existing = keys.find((key) => key.endsWith(IMAGES_CACHE_SUFFIX));
24
+ if (existing) return existing;
25
+ const offline = keys.find((key) => key.includes(OFFLINE_CACHE_MARKER));
26
+ if (!offline) return void 0;
27
+ return `${offline.slice(0, offline.indexOf(OFFLINE_CACHE_MARKER))}${IMAGES_CACHE_SUFFIX}`;
28
+ }
29
+ __name(imagesCacheName, "imagesCacheName");
30
+ function imageUrlOf(resource) {
31
+ const url = resource?.attributes?.imageUrl;
32
+ return typeof url === "string" && url.length > 0 ? url : void 0;
33
+ }
34
+ __name(imageUrlOf, "imageUrlOf");
35
+ async function cacheArtworkImage(url, previousUrl) {
36
+ const name = await imagesCacheName();
37
+ if (!name) return;
38
+ const cache = await caches.open(name);
39
+ if (previousUrl && previousUrl !== url) await cache.delete(previousUrl);
40
+ if (await cache.match(url)) return;
41
+ try {
42
+ const response = await fetch(url);
43
+ if (!response.ok) {
44
+ console.warn("[offline] could not download an artwork image", url, response.status);
45
+ return;
46
+ }
47
+ await cache.put(url, response);
48
+ } catch (error) {
49
+ console.warn("[offline] could not download an artwork image", url, error);
50
+ }
51
+ }
52
+ __name(cacheArtworkImage, "cacheArtworkImage");
53
+ async function removeArtworkImage(url) {
54
+ const name = await imagesCacheName();
55
+ if (!name) return;
56
+ const cache = await caches.open(name);
57
+ await cache.delete(url);
58
+ }
59
+ __name(removeArtworkImage, "removeArtworkImage");
60
+ async function deleteImagesCaches() {
61
+ if (typeof caches === "undefined") return;
62
+ const keys = await caches.keys();
63
+ await Promise.all(keys.filter((key) => key.endsWith(IMAGES_CACHE_SUFFIX)).map((key) => caches.delete(key)));
64
+ }
65
+ __name(deleteImagesCaches, "deleteImagesCaches");
66
+
67
+ // src/offline/store/schema.ts
68
+ var DEVICE_STORE_SCHEMA_VERSION = 3;
69
+ var DEVICE_STORE_RESET = [
70
+ `DROP TABLE IF EXISTS records`,
71
+ `DROP TABLE IF EXISTS record_campaigns`,
72
+ `DROP TABLE IF EXISTS record_roots`,
73
+ `DROP TABLE IF EXISTS links`,
74
+ `DROP TABLE IF EXISTS queue`,
75
+ `DROP TABLE IF EXISTS conflicts`,
76
+ `DROP TABLE IF EXISTS sync_state`,
77
+ `DROP TABLE IF EXISTS skipped_records`
78
+ ];
79
+ var DEVICE_STORE_SCHEMA = [
80
+ `CREATE TABLE IF NOT EXISTS records (
81
+ type TEXT NOT NULL,
82
+ id TEXT NOT NULL,
83
+ root_id TEXT NOT NULL,
84
+ resource TEXT NOT NULL,
85
+ updated_at TEXT,
86
+ base TEXT,
87
+ local_only INTEGER NOT NULL DEFAULT 0,
88
+ PRIMARY KEY (type, id)
89
+ )`,
90
+ `CREATE INDEX IF NOT EXISTS records_type_root ON records (type, root_id)`,
91
+ `CREATE TABLE IF NOT EXISTS record_roots (
92
+ type TEXT NOT NULL,
93
+ id TEXT NOT NULL,
94
+ root_id TEXT NOT NULL,
95
+ PRIMARY KEY (type, id, root_id)
96
+ )`,
97
+ `CREATE INDEX IF NOT EXISTS record_roots_root ON record_roots (root_id, type)`,
98
+ `CREATE TABLE IF NOT EXISTS links (
99
+ from_id TEXT NOT NULL,
100
+ from_type TEXT NOT NULL,
101
+ to_id TEXT NOT NULL,
102
+ to_type TEXT NOT NULL,
103
+ kind TEXT NOT NULL,
104
+ props TEXT NOT NULL DEFAULT '{}',
105
+ root_id TEXT,
106
+ PRIMARY KEY (from_id, to_id, kind)
107
+ )`,
108
+ `CREATE INDEX IF NOT EXISTS links_from ON links (from_id)`,
109
+ `CREATE INDEX IF NOT EXISTS links_to ON links (to_id)`,
110
+ `CREATE INDEX IF NOT EXISTS links_root ON links (root_id)`,
111
+ `CREATE TABLE IF NOT EXISTS queue (
112
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
113
+ root_id TEXT NOT NULL,
114
+ request TEXT NOT NULL,
115
+ base TEXT,
116
+ created_at TEXT NOT NULL,
117
+ status TEXT NOT NULL DEFAULT 'pending',
118
+ package_id TEXT
119
+ )`,
120
+ `CREATE INDEX IF NOT EXISTS queue_status_seq ON queue (status, seq)`,
121
+ `CREATE TABLE IF NOT EXISTS conflicts (
122
+ id TEXT PRIMARY KEY,
123
+ queue_seq INTEGER NOT NULL,
124
+ root_id TEXT,
125
+ kind TEXT NOT NULL,
126
+ record_type TEXT,
127
+ record_id TEXT,
128
+ mine TEXT,
129
+ theirs TEXT,
130
+ base TEXT,
131
+ merged TEXT,
132
+ fields TEXT NOT NULL DEFAULT '[]',
133
+ blocks TEXT,
134
+ reason TEXT
135
+ )`,
136
+ `CREATE TABLE IF NOT EXISTS sync_state (
137
+ root_id TEXT PRIMARY KEY,
138
+ server_time TEXT,
139
+ locked INTEGER NOT NULL DEFAULT 0,
140
+ locked_from TEXT
141
+ )`,
142
+ `CREATE TABLE IF NOT EXISTS skipped_records (
143
+ root_id TEXT NOT NULL,
144
+ type TEXT NOT NULL,
145
+ id TEXT NOT NULL,
146
+ updated_at TEXT NOT NULL,
147
+ PRIMARY KEY (root_id, type, id)
148
+ )`,
149
+ `PRAGMA user_version = ${DEVICE_STORE_SCHEMA_VERSION}`
150
+ ];
151
+
152
+ // src/offline/store/sql-device-store.ts
153
+ var toJson = /* @__PURE__ */ __name((value) => value === void 0 || value === null ? null : JSON.stringify(value), "toJson");
154
+ var fromJson = /* @__PURE__ */ __name((value) => value === null || value === void 0 ? null : JSON.parse(String(value)), "fromJson");
155
+ var toIso = /* @__PURE__ */ __name((value) => value ? value.toISOString() : null, "toIso");
156
+ var fromIso = /* @__PURE__ */ __name((value) => value === null || value === void 0 ? null : new Date(String(value)), "fromIso");
157
+ var newId = /* @__PURE__ */ __name(() => typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, "newId");
158
+ var rowToRecord = /* @__PURE__ */ __name((row) => ({
159
+ type: String(row.type),
160
+ id: String(row.id),
161
+ rootId: String(row.root_id),
162
+ resource: fromJson(row.resource),
163
+ updatedAt: fromIso(row.updated_at),
164
+ base: fromJson(row.base),
165
+ localOnly: Number(row.local_only) === 1
166
+ }), "rowToRecord");
167
+ var rowToLink = /* @__PURE__ */ __name((row) => ({
168
+ fromType: String(row.from_type),
169
+ fromId: String(row.from_id),
170
+ toType: String(row.to_type),
171
+ toId: String(row.to_id),
172
+ kind: String(row.kind),
173
+ props: fromJson(row.props) ?? {}
174
+ }), "rowToLink");
175
+ var rowToQueueEntry = /* @__PURE__ */ __name((row) => {
176
+ const request = fromJson(row.request) ?? {};
177
+ const entry = {
178
+ seq: Number(row.seq),
179
+ rootId: String(row.root_id),
180
+ method: request.method,
181
+ path: request.path,
182
+ base: fromJson(row.base),
183
+ createdAt: new Date(String(row.created_at)),
184
+ status: String(row.status)
185
+ };
186
+ if (request.body !== void 0) entry.body = request.body;
187
+ if (request.recordType !== void 0) entry.recordType = request.recordType;
188
+ if (request.recordId !== void 0) entry.recordId = request.recordId;
189
+ if (request.resolution !== void 0) entry.resolution = request.resolution;
190
+ if (row.package_id !== null && row.package_id !== void 0) entry.packageId = String(row.package_id);
191
+ return entry;
192
+ }, "rowToQueueEntry");
193
+ var rowToConflict = /* @__PURE__ */ __name((row) => {
194
+ const conflict = {
195
+ id: String(row.id),
196
+ queueSeq: Number(row.queue_seq),
197
+ kind: String(row.kind),
198
+ mine: fromJson(row.mine),
199
+ theirs: fromJson(row.theirs),
200
+ base: fromJson(row.base),
201
+ fields: fromJson(row.fields) ?? []
202
+ };
203
+ if (row.record_type !== null && row.record_type !== void 0) conflict.recordType = String(row.record_type);
204
+ if (row.record_id !== null && row.record_id !== void 0) conflict.recordId = String(row.record_id);
205
+ const merged = fromJson(row.merged);
206
+ if (merged !== null) conflict.merged = merged;
207
+ const blocks = fromJson(row.blocks);
208
+ if (blocks !== null) conflict.blocks = blocks;
209
+ if (row.reason !== null && row.reason !== void 0) conflict.reason = String(row.reason);
210
+ return conflict;
211
+ }, "rowToConflict");
212
+ var createOperations = /* @__PURE__ */ __name((exec, hooks) => {
213
+ const insertLink = /* @__PURE__ */ __name(async (link, rootId) => {
214
+ await exec(
215
+ `INSERT OR REPLACE INTO links (from_id, from_type, to_id, to_type, kind, props, root_id) VALUES (?, ?, ?, ?, ?, ?, ?)`,
216
+ [link.fromId, link.fromType, link.toId, link.toType, link.kind, JSON.stringify(link.props ?? {}), rootId]
217
+ );
218
+ }, "insertLink");
219
+ const deleteRecordEverywhere = /* @__PURE__ */ __name(async (type, id) => {
220
+ await exec(`DELETE FROM records WHERE type = ? AND id = ?`, [type, id]);
221
+ await exec(`DELETE FROM record_roots WHERE type = ? AND id = ?`, [type, id]);
222
+ await exec(`DELETE FROM links WHERE from_id = ? OR to_id = ?`, [id, id]);
223
+ }, "deleteRecordEverywhere");
224
+ return {
225
+ async getRecord(type, id) {
226
+ const rows = await exec(`SELECT * FROM records WHERE type = ? AND id = ?`, [type, id]);
227
+ return rows[0] ? rowToRecord(rows[0]) : void 0;
228
+ },
229
+ async listRecords(filter) {
230
+ if (filter.ids && filter.ids.length === 0) return [];
231
+ const where = ["type = ?"];
232
+ const bind = [filter.type];
233
+ if (filter.rootId !== void 0) {
234
+ where.push(
235
+ "EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id AND rc.root_id = ?)"
236
+ );
237
+ bind.push(filter.rootId);
238
+ }
239
+ if (filter.ids) {
240
+ where.push(`id IN (${filter.ids.map(() => "?").join(", ")})`);
241
+ bind.push(...filter.ids);
242
+ }
243
+ const rows = await exec(`SELECT * FROM records WHERE ${where.join(" AND ")} ORDER BY rowid`, bind);
244
+ return rows.map(rowToRecord);
245
+ },
246
+ async putRecord(record) {
247
+ await exec(
248
+ `INSERT INTO records (type, id, root_id, resource, updated_at, base, local_only) VALUES (?, ?, ?, ?, ?, ?, ?)
249
+ ON CONFLICT (type, id) DO UPDATE SET resource = excluded.resource, updated_at = excluded.updated_at,
250
+ base = excluded.base, local_only = excluded.local_only`,
251
+ [
252
+ record.type,
253
+ record.id,
254
+ record.rootId,
255
+ JSON.stringify(record.resource),
256
+ toIso(record.updatedAt),
257
+ toJson(record.base),
258
+ record.localOnly ? 1 : 0
259
+ ]
260
+ );
261
+ await exec(`INSERT OR IGNORE INTO record_roots (type, id, root_id) VALUES (?, ?, ?)`, [
262
+ record.type,
263
+ record.id,
264
+ record.rootId
265
+ ]);
266
+ },
267
+ async deleteRecord(type, id) {
268
+ await deleteRecordEverywhere(type, id);
269
+ },
270
+ async removeRecordFromRoot(type, id, rootId) {
271
+ await exec(`DELETE FROM record_roots WHERE type = ? AND id = ? AND root_id = ?`, [type, id, rootId]);
272
+ const left = await exec(`SELECT root_id FROM record_roots WHERE type = ? AND id = ? ORDER BY root_id`, [
273
+ type,
274
+ id
275
+ ]);
276
+ if (left.length === 0) {
277
+ await deleteRecordEverywhere(type, id);
278
+ return;
279
+ }
280
+ await exec(`UPDATE records SET root_id = ? WHERE type = ? AND id = ? AND root_id = ?`, [
281
+ String(left[0].root_id),
282
+ type,
283
+ id,
284
+ rootId
285
+ ]);
286
+ },
287
+ async linksFrom(fromId, kind) {
288
+ const rows = kind === void 0 ? await exec(`SELECT * FROM links WHERE from_id = ? ORDER BY rowid`, [fromId]) : await exec(`SELECT * FROM links WHERE from_id = ? AND kind = ? ORDER BY rowid`, [fromId, kind]);
289
+ return rows.map(rowToLink);
290
+ },
291
+ async linksTo(toId, kind) {
292
+ const rows = kind === void 0 ? await exec(`SELECT * FROM links WHERE to_id = ? ORDER BY rowid`, [toId]) : await exec(`SELECT * FROM links WHERE to_id = ? AND kind = ? ORDER BY rowid`, [toId, kind]);
293
+ return rows.map(rowToLink);
294
+ },
295
+ async replaceRootLinks(rootId, links) {
296
+ await exec(`DELETE FROM links WHERE root_id = ?`, [rootId]);
297
+ for (const link of links) await insertLink(link, rootId);
298
+ },
299
+ async putLink(link) {
300
+ const owner = await exec(`SELECT root_id FROM records WHERE id IN (?, ?) ORDER BY id = ? DESC LIMIT 1`, [
301
+ link.fromId,
302
+ link.toId,
303
+ link.fromId
304
+ ]);
305
+ await insertLink(link, owner[0] ? String(owner[0].root_id) : null);
306
+ },
307
+ async deleteLink(link) {
308
+ await exec(`DELETE FROM links WHERE from_id = ? AND to_id = ? AND kind = ?`, [link.fromId, link.toId, link.kind]);
309
+ },
310
+ async enqueue(entry) {
311
+ if (hooks.beforeEnqueue) await hooks.beforeEnqueue();
312
+ const request = {
313
+ method: entry.method,
314
+ path: entry.path,
315
+ body: entry.body,
316
+ recordType: entry.recordType,
317
+ recordId: entry.recordId,
318
+ resolution: entry.resolution
319
+ };
320
+ const rows = await exec(
321
+ `INSERT INTO queue (root_id, request, base, created_at, status) VALUES (?, ?, ?, ?, 'pending') RETURNING seq`,
322
+ [entry.rootId, JSON.stringify(request), toJson(entry.base), (/* @__PURE__ */ new Date()).toISOString()]
323
+ );
324
+ return Number(rows[0].seq);
325
+ },
326
+ async queue(filter) {
327
+ const rows = filter?.status ? await exec(`SELECT * FROM queue WHERE status = ? ORDER BY seq`, [filter.status]) : await exec(`SELECT * FROM queue ORDER BY seq`);
328
+ return rows.map(rowToQueueEntry);
329
+ },
330
+ async setQueueStatus(seq, status) {
331
+ await exec(`UPDATE queue SET status = ? WHERE seq = ?`, [status, seq]);
332
+ },
333
+ async setQueuePackage(seqs, packageId) {
334
+ if (seqs.length === 0) return;
335
+ await exec(`UPDATE queue SET package_id = ? WHERE seq IN (${seqs.map(() => "?").join(", ")})`, [
336
+ packageId,
337
+ ...seqs
338
+ ]);
339
+ },
340
+ async removeQueueEntry(seq) {
341
+ await exec(`DELETE FROM queue WHERE seq = ?`, [seq]);
342
+ },
343
+ async addConflict(c) {
344
+ const id = newId();
345
+ const owner = await exec(`SELECT root_id FROM queue WHERE seq = ?`, [c.queueSeq]);
346
+ await exec(
347
+ `INSERT INTO conflicts (id, queue_seq, root_id, kind, record_type, record_id, mine, theirs, base, merged, fields, blocks, reason)
348
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
349
+ [
350
+ id,
351
+ c.queueSeq,
352
+ owner[0] ? String(owner[0].root_id) : null,
353
+ c.kind,
354
+ c.recordType ?? null,
355
+ c.recordId ?? null,
356
+ toJson(c.mine),
357
+ toJson(c.theirs),
358
+ toJson(c.base),
359
+ toJson(c.merged),
360
+ JSON.stringify(c.fields ?? []),
361
+ toJson(c.blocks),
362
+ c.reason ?? null
363
+ ]
364
+ );
365
+ return id;
366
+ },
367
+ async conflicts() {
368
+ const rows = await exec(`SELECT * FROM conflicts ORDER BY queue_seq, rowid`);
369
+ return rows.map(rowToConflict);
370
+ },
371
+ async removeConflict(id) {
372
+ await exec(`DELETE FROM conflicts WHERE id = ?`, [id]);
373
+ },
374
+ async getSyncState(rootId) {
375
+ const rows = await exec(`SELECT * FROM sync_state WHERE root_id = ?`, [rootId]);
376
+ if (!rows[0]) return { serverTime: null, locked: false, lockedFrom: null };
377
+ return {
378
+ serverTime: fromIso(rows[0].server_time),
379
+ locked: Number(rows[0].locked) === 1,
380
+ lockedFrom: fromIso(rows[0].locked_from)
381
+ };
382
+ },
383
+ async setSyncState(rootId, s) {
384
+ await exec(`INSERT OR REPLACE INTO sync_state (root_id, server_time, locked, locked_from) VALUES (?, ?, ?, ?)`, [
385
+ rootId,
386
+ toIso(s.serverTime),
387
+ s.locked ? 1 : 0,
388
+ toIso(s.lockedFrom)
389
+ ]);
390
+ },
391
+ async skippedRecords(rootId) {
392
+ const rows = await exec(`SELECT * FROM skipped_records WHERE root_id = ? ORDER BY type, id`, [rootId]);
393
+ return rows.map((row) => ({
394
+ type: String(row.type),
395
+ id: String(row.id),
396
+ updatedAt: new Date(String(row.updated_at))
397
+ }));
398
+ },
399
+ async setSkippedRecords(rootId, records) {
400
+ await exec(`DELETE FROM skipped_records WHERE root_id = ?`, [rootId]);
401
+ for (const record of records) {
402
+ await exec(`INSERT OR REPLACE INTO skipped_records (root_id, type, id, updated_at) VALUES (?, ?, ?, ?)`, [
403
+ rootId,
404
+ record.type,
405
+ record.id,
406
+ record.updatedAt.toISOString()
407
+ ]);
408
+ }
409
+ },
410
+ async removeRoot(rootId) {
411
+ const waiting = await exec(`SELECT COUNT(*) AS n FROM queue WHERE root_id = ?`, [rootId]);
412
+ if (Number(waiting[0]?.n ?? 0) > 0) return;
413
+ await exec(`DELETE FROM links WHERE root_id = ?`, [rootId]);
414
+ await exec(`DELETE FROM record_roots WHERE root_id = ?`, [rootId]);
415
+ 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)`;
416
+ await exec(`DELETE FROM links WHERE from_id IN (${orphans}) OR to_id IN (${orphans})`);
417
+ await exec(
418
+ `DELETE FROM records WHERE NOT EXISTS (SELECT 1 FROM record_roots rc WHERE rc.type = records.type AND rc.id = records.id)`
419
+ );
420
+ await exec(
421
+ `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 = ?`,
422
+ [rootId]
423
+ );
424
+ await exec(`DELETE FROM conflicts WHERE root_id = ?`, [rootId]);
425
+ await exec(`DELETE FROM sync_state WHERE root_id = ?`, [rootId]);
426
+ await exec(`DELETE FROM skipped_records WHERE root_id = ?`, [rootId]);
427
+ },
428
+ async queueSize() {
429
+ const rows = await exec(`SELECT COUNT(*) AS n FROM queue`);
430
+ return Number(rows[0]?.n ?? 0);
431
+ }
432
+ };
433
+ }, "createOperations");
434
+ var nestedStore = /* @__PURE__ */ __name((ops) => {
435
+ const { queueSize: _queueSize, ...rest } = ops;
436
+ const store = { ...rest, transaction: /* @__PURE__ */ __name((fn) => fn(store), "transaction") };
437
+ return store;
438
+ }, "nestedStore");
439
+ async function createSqlDeviceStore(exec, hooks = {}) {
440
+ const exclusive = hooks.exclusive ?? ((fn) => fn());
441
+ await exclusive(async () => {
442
+ const version = Number((await exec(`PRAGMA user_version`))[0]?.user_version ?? 0);
443
+ if (version > 0 && version < DEVICE_STORE_SCHEMA_VERSION) {
444
+ for (const statement of DEVICE_STORE_RESET) await exec(statement);
445
+ }
446
+ for (const statement of DEVICE_STORE_SCHEMA) await exec(statement);
447
+ });
448
+ const ops = createOperations(exec, hooks);
449
+ let tail = Promise.resolve();
450
+ const serialised = /* @__PURE__ */ __name((fn) => {
451
+ const run = tail.then(
452
+ () => exclusive(fn),
453
+ () => exclusive(fn)
454
+ );
455
+ tail = run.catch(() => void 0);
456
+ return run;
457
+ }, "serialised");
458
+ const settleQueue = /* @__PURE__ */ __name(async () => {
459
+ if (hooks.afterQueueChange) await hooks.afterQueueChange(await ops.queueSize());
460
+ }, "settleQueue");
461
+ const store = {
462
+ getRecord: /* @__PURE__ */ __name((type, id) => serialised(() => ops.getRecord(type, id)), "getRecord"),
463
+ listRecords: /* @__PURE__ */ __name((filter) => serialised(() => ops.listRecords(filter)), "listRecords"),
464
+ putRecord: /* @__PURE__ */ __name((record) => serialised(() => ops.putRecord(record)), "putRecord"),
465
+ deleteRecord: /* @__PURE__ */ __name((type, id) => serialised(() => ops.deleteRecord(type, id)), "deleteRecord"),
466
+ removeRecordFromRoot: /* @__PURE__ */ __name((type, id, rootId) => serialised(() => ops.removeRecordFromRoot(type, id, rootId)), "removeRecordFromRoot"),
467
+ linksFrom: /* @__PURE__ */ __name((fromId, kind) => serialised(() => ops.linksFrom(fromId, kind)), "linksFrom"),
468
+ linksTo: /* @__PURE__ */ __name((toId, kind) => serialised(() => ops.linksTo(toId, kind)), "linksTo"),
469
+ replaceRootLinks: /* @__PURE__ */ __name((rootId, links) => serialised(async () => {
470
+ await exec("BEGIN");
471
+ try {
472
+ await ops.replaceRootLinks(rootId, links);
473
+ await exec("COMMIT");
474
+ } catch (error) {
475
+ await exec("ROLLBACK");
476
+ throw error;
477
+ }
478
+ }), "replaceRootLinks"),
479
+ putLink: /* @__PURE__ */ __name((link) => serialised(() => ops.putLink(link)), "putLink"),
480
+ deleteLink: /* @__PURE__ */ __name((link) => serialised(() => ops.deleteLink(link)), "deleteLink"),
481
+ enqueue: /* @__PURE__ */ __name((entry) => serialised(() => ops.enqueue(entry)), "enqueue"),
482
+ queue: /* @__PURE__ */ __name((filter) => serialised(() => ops.queue(filter)), "queue"),
483
+ setQueueStatus: /* @__PURE__ */ __name((seq, status) => serialised(() => ops.setQueueStatus(seq, status)), "setQueueStatus"),
484
+ setQueuePackage: /* @__PURE__ */ __name((seqs, packageId) => serialised(() => ops.setQueuePackage(seqs, packageId)), "setQueuePackage"),
485
+ removeQueueEntry: /* @__PURE__ */ __name((seq) => serialised(async () => {
486
+ await ops.removeQueueEntry(seq);
487
+ await settleQueue();
488
+ }), "removeQueueEntry"),
489
+ addConflict: /* @__PURE__ */ __name((c) => serialised(() => ops.addConflict(c)), "addConflict"),
490
+ conflicts: /* @__PURE__ */ __name(() => serialised(() => ops.conflicts()), "conflicts"),
491
+ removeConflict: /* @__PURE__ */ __name((id) => serialised(() => ops.removeConflict(id)), "removeConflict"),
492
+ getSyncState: /* @__PURE__ */ __name((rootId) => serialised(() => ops.getSyncState(rootId)), "getSyncState"),
493
+ setSyncState: /* @__PURE__ */ __name((rootId, s) => serialised(() => ops.setSyncState(rootId, s)), "setSyncState"),
494
+ skippedRecords: /* @__PURE__ */ __name((rootId) => serialised(() => ops.skippedRecords(rootId)), "skippedRecords"),
495
+ setSkippedRecords: /* @__PURE__ */ __name((rootId, records) => serialised(() => ops.setSkippedRecords(rootId, records)), "setSkippedRecords"),
496
+ removeRoot: /* @__PURE__ */ __name((rootId) => serialised(async () => {
497
+ await exec("BEGIN");
498
+ try {
499
+ await ops.removeRoot(rootId);
500
+ await exec("COMMIT");
501
+ } catch (error) {
502
+ await exec("ROLLBACK");
503
+ throw error;
504
+ }
505
+ await settleQueue();
506
+ }), "removeRoot"),
507
+ transaction: /* @__PURE__ */ __name((fn) => serialised(async () => {
508
+ await exec("BEGIN");
509
+ let result;
510
+ try {
511
+ result = await fn(nestedStore(ops));
512
+ await exec("COMMIT");
513
+ } catch (error) {
514
+ await exec("ROLLBACK");
515
+ await settleQueue();
516
+ throw error;
517
+ }
518
+ await settleQueue();
519
+ return result;
520
+ }), "transaction")
521
+ };
522
+ return store;
523
+ }
524
+ __name(createSqlDeviceStore, "createSqlDeviceStore");
525
+
526
+ // src/offline/store/shared-database.ts
527
+ var LEASE_TIMEOUT_MS = 3e4;
528
+ var EXEC_TIMEOUT_MS = 3e4;
529
+ var OPEN_RETRY_MS = 1e4;
530
+ var lockName = /* @__PURE__ */ __name((userId) => `nja-offline-db:${userId}`, "lockName");
531
+ var channelName = /* @__PURE__ */ __name((userId) => `nja-offline-db:${userId}`, "channelName");
532
+ var newId2 = /* @__PURE__ */ __name(() => globalThis.crypto.randomUUID(), "newId");
533
+ function canShareDatabase() {
534
+ return typeof BroadcastChannel !== "undefined" && typeof navigator !== "undefined" && typeof navigator.locks?.request === "function";
535
+ }
536
+ __name(canShareDatabase, "canShareDatabase");
537
+ var isHeldElsewhere = /* @__PURE__ */ __name((error) => /NoModificationAllowedError|Access Handles cannot be created|createSyncAccessHandle/i.test(
538
+ String(error?.message ?? error)
539
+ ), "isHeldElsewhere");
540
+ async function openWithRetry(openLocal) {
541
+ const deadline = Date.now() + OPEN_RETRY_MS;
542
+ for (let wait = 100; ; wait = Math.min(wait * 2, 1e3)) {
543
+ try {
544
+ return await openLocal();
545
+ } catch (error) {
546
+ if (!isHeldElsewhere(error) || Date.now() >= deadline) throw error;
547
+ await new Promise((resolve) => setTimeout(resolve, wait));
548
+ }
549
+ }
550
+ }
551
+ __name(openWithRetry, "openWithRetry");
552
+ function createMutex() {
553
+ let tail = Promise.resolve();
554
+ return (fn) => {
555
+ const run = tail.then(fn, fn);
556
+ tail = run.catch(() => void 0);
557
+ return run;
558
+ };
559
+ }
560
+ __name(createMutex, "createMutex");
561
+ async function openSharedDatabase(userId, openLocal, onShutdown) {
562
+ const channel = new BroadcastChannel(channelName(userId));
563
+ const post = /* @__PURE__ */ __name((message) => channel.postMessage(message), "post");
564
+ const mutex = createMutex();
565
+ let local;
566
+ let created = false;
567
+ let closed = false;
568
+ let releaseLock;
569
+ const lockAbort = new AbortController();
570
+ const leases = /* @__PURE__ */ new Map();
571
+ const waitingLeases = /* @__PURE__ */ new Map();
572
+ const waitingResults = /* @__PURE__ */ new Map();
573
+ let currentLease;
574
+ const isHolder = /* @__PURE__ */ __name(() => local !== void 0, "isHolder");
575
+ const runGranted = /* @__PURE__ */ __name(async (id) => {
576
+ const waiting = waitingLeases.get(id);
577
+ if (!waiting) {
578
+ post({ kind: "release", id });
579
+ return;
580
+ }
581
+ waitingLeases.delete(id);
582
+ currentLease = id;
583
+ try {
584
+ waiting.resolve(await waiting.run());
585
+ } catch (error) {
586
+ waiting.reject(error);
587
+ } finally {
588
+ currentLease = void 0;
589
+ post({ kind: "release", id });
590
+ }
591
+ }, "runGranted");
592
+ channel.onmessage = (event) => {
593
+ const message = event.data;
594
+ if (closed) return;
595
+ switch (message.kind) {
596
+ case "holder":
597
+ if (!isHolder()) for (const id of waitingLeases.keys()) post({ kind: "lease", id });
598
+ return;
599
+ case "lease":
600
+ if (!isHolder() || leases.has(message.id)) return;
601
+ void mutex(
602
+ () => new Promise((done) => {
603
+ const timer = setTimeout(() => finish(), LEASE_TIMEOUT_MS);
604
+ const finish = /* @__PURE__ */ __name(() => {
605
+ clearTimeout(timer);
606
+ leases.delete(message.id);
607
+ done();
608
+ }, "finish");
609
+ leases.set(message.id, finish);
610
+ post({ kind: "granted", id: message.id });
611
+ })
612
+ );
613
+ return;
614
+ case "granted":
615
+ if (!isHolder()) void runGranted(message.id);
616
+ return;
617
+ case "release":
618
+ leases.get(message.id)?.();
619
+ return;
620
+ case "exec":
621
+ if (!isHolder() || !leases.has(message.lease)) return;
622
+ local.exec(message.sql, message.bind).then((rows) => post({ kind: "result", id: message.id, ok: true, rows })).catch(
623
+ (error) => post({ kind: "result", id: message.id, ok: false, error: String(error?.message ?? error) })
624
+ );
625
+ return;
626
+ case "result": {
627
+ const waiting = waitingResults.get(message.id);
628
+ if (!waiting) return;
629
+ waitingResults.delete(message.id);
630
+ if (message.ok) waiting.resolve(message.rows);
631
+ else waiting.reject(new Error(message.error));
632
+ return;
633
+ }
634
+ case "shutdown":
635
+ void close().finally(() => onShutdown?.());
636
+ return;
637
+ }
638
+ };
639
+ const becomeHolder = /* @__PURE__ */ __name(async () => {
640
+ local = await openWithRetry(openLocal);
641
+ created = local.created;
642
+ for (const [id, waiting] of waitingLeases) {
643
+ waitingLeases.delete(id);
644
+ mutex(waiting.run).then(waiting.resolve, waiting.reject);
645
+ }
646
+ post({ kind: "holder" });
647
+ }, "becomeHolder");
648
+ const holdLock = /* @__PURE__ */ __name(async () => {
649
+ if (closed) return;
650
+ await becomeHolder();
651
+ await new Promise((resolve) => releaseLock = resolve);
652
+ }, "holdLock");
653
+ let opened;
654
+ const isFree = await new Promise((answer) => {
655
+ navigator.locks.request(lockName(userId), { ifAvailable: true }, async (lock) => {
656
+ if (!lock) {
657
+ answer(false);
658
+ return;
659
+ }
660
+ answer(true);
661
+ opened = becomeHolder();
662
+ try {
663
+ await opened;
664
+ } catch {
665
+ return;
666
+ }
667
+ await new Promise((resolve) => releaseLock = resolve);
668
+ }).catch(() => answer(false));
669
+ });
670
+ if (isFree) {
671
+ try {
672
+ await opened;
673
+ } catch (error) {
674
+ closed = true;
675
+ channel.close();
676
+ throw error;
677
+ }
678
+ } else {
679
+ navigator.locks.request(lockName(userId), { signal: lockAbort.signal }, holdLock).catch((error) => {
680
+ if (error?.name !== "AbortError")
681
+ console.warn("[offline] taking over the device copy failed", error);
682
+ });
683
+ }
684
+ const remoteExec = /* @__PURE__ */ __name((sql, bind) => {
685
+ if (closed) return Promise.reject(new Error("The device database is closed."));
686
+ const lease = currentLease;
687
+ if (!lease) return exclusive(() => remoteExec(sql, bind));
688
+ const id = newId2();
689
+ return new Promise((resolve, reject) => {
690
+ const timer = setTimeout(() => {
691
+ waitingResults.delete(id);
692
+ reject(new Error("The page holding the device copy did not answer."));
693
+ }, EXEC_TIMEOUT_MS);
694
+ waitingResults.set(id, {
695
+ resolve: /* @__PURE__ */ __name((rows) => {
696
+ clearTimeout(timer);
697
+ resolve(rows);
698
+ }, "resolve"),
699
+ reject: /* @__PURE__ */ __name((error) => {
700
+ clearTimeout(timer);
701
+ reject(error);
702
+ }, "reject")
703
+ });
704
+ post({ kind: "exec", id, lease, sql, bind });
705
+ });
706
+ }, "remoteExec");
707
+ const exclusive = /* @__PURE__ */ __name((fn) => {
708
+ if (closed) return Promise.reject(new Error("The device database is closed."));
709
+ if (isHolder()) return mutex(fn);
710
+ const id = newId2();
711
+ return new Promise((resolve, reject) => {
712
+ waitingLeases.set(id, {
713
+ run: fn,
714
+ resolve,
715
+ reject
716
+ });
717
+ post({ kind: "lease", id });
718
+ });
719
+ }, "exclusive");
720
+ const exec = /* @__PURE__ */ __name((sql, bind) => isHolder() ? local.exec(sql, bind) : remoteExec(sql, bind), "exec");
721
+ async function close() {
722
+ if (closed) return;
723
+ closed = true;
724
+ lockAbort.abort();
725
+ const error = new Error("The device database is closed.");
726
+ for (const waiting of waitingLeases.values()) waiting.reject(error);
727
+ for (const waiting of waitingResults.values()) waiting.reject(error);
728
+ waitingLeases.clear();
729
+ waitingResults.clear();
730
+ for (const finish of [...leases.values()]) finish();
731
+ try {
732
+ if (local) await local.close();
733
+ } finally {
734
+ local = void 0;
735
+ releaseLock?.();
736
+ channel.close();
737
+ }
738
+ }
739
+ __name(close, "close");
740
+ return {
741
+ get created() {
742
+ return created;
743
+ },
744
+ exec,
745
+ exclusive,
746
+ close
747
+ };
748
+ }
749
+ __name(openSharedDatabase, "openSharedDatabase");
750
+ async function withDatabaseReleased(userId, fn) {
751
+ if (!canShareDatabase()) return fn();
752
+ const channel = new BroadcastChannel(channelName(userId));
753
+ channel.postMessage({ kind: "shutdown" });
754
+ channel.close();
755
+ return navigator.locks.request(lockName(userId), () => fn());
756
+ }
757
+ __name(withDatabaseReleased, "withDatabaseReleased");
758
+
759
+ // src/offline/store/worker-client.ts
760
+ async function openWorkerDatabase(userId, fileName, urls) {
761
+ const worker = new Worker(urls.workerUrl, { type: "module" });
762
+ const { sqliteModuleUrl } = urls;
763
+ const pending = /* @__PURE__ */ new Map();
764
+ let nextId = 1;
765
+ let closed = false;
766
+ worker.onmessage = (event) => {
767
+ const response = event.data;
768
+ const waiting = pending.get(response.id);
769
+ if (!waiting) return;
770
+ pending.delete(response.id);
771
+ if (response.ok) waiting.resolve(response.result);
772
+ else waiting.reject(new Error(response.error));
773
+ };
774
+ worker.onerror = (event) => {
775
+ const error = new Error(event.message || "The device database worker failed.");
776
+ for (const waiting of pending.values()) waiting.reject(error);
777
+ pending.clear();
778
+ };
779
+ const send = /* @__PURE__ */ __name((request) => {
780
+ if (closed) return Promise.reject(new Error("The device database is closed."));
781
+ const id = nextId++;
782
+ return new Promise((resolve, reject) => {
783
+ pending.set(id, { resolve, reject });
784
+ worker.postMessage({ ...request, id });
785
+ });
786
+ }, "send");
787
+ let opened;
788
+ try {
789
+ opened = await send({ op: "open", userId, fileName, sqliteModuleUrl });
790
+ } catch (error) {
791
+ worker.terminate();
792
+ throw error;
793
+ }
794
+ return {
795
+ created: opened.created,
796
+ exec: /* @__PURE__ */ __name((sql, bind) => send({ op: "exec", sql, bind }), "exec"),
797
+ close: /* @__PURE__ */ __name(async () => {
798
+ if (closed) return;
799
+ try {
800
+ await send({ op: "close" });
801
+ } finally {
802
+ closed = true;
803
+ worker.terminate();
804
+ }
805
+ }, "close")
806
+ };
807
+ }
808
+ __name(openWorkerDatabase, "openWorkerDatabase");
809
+
810
+ // src/offline/store/lifecycle.ts
811
+ var FILE_PREFIX = "nja-offline-";
812
+ var deviceFileName = /* @__PURE__ */ __name((userId) => `${FILE_PREFIX}${userId}.sqlite3`, "deviceFileName");
813
+ var pendingMarkerName = /* @__PURE__ */ __name((userId) => `${FILE_PREFIX}${userId}.pending`, "pendingMarkerName");
814
+ var IMAGES_OWNER_PREFIX = "nja-offline-images-owner-";
815
+ var imagesOwnerName = /* @__PURE__ */ __name((userId) => `${IMAGES_OWNER_PREFIX}${userId}`, "imagesOwnerName");
816
+ var FILE_PATTERN = /^nja-offline-(.+)\.(?:sqlite3(?:-journal|-wal|-shm)?|pending)$/;
817
+ var POOL_PATTERN = /^\.nja-offline-(.+)$/;
818
+ var missingDeviceStorage = /* @__PURE__ */ __name(() => {
819
+ if (typeof window !== "undefined" && window.isSecureContext === false) {
820
+ return "the page is not served from a secure origin (https or localhost)";
821
+ }
822
+ if (typeof navigator === "undefined" || typeof navigator.storage?.getDirectory !== "function") {
823
+ return "the browser offers no private file storage";
824
+ }
825
+ return null;
826
+ }, "missingDeviceStorage");
827
+ var assertDeviceStorage = /* @__PURE__ */ __name(() => {
828
+ const reason = missingDeviceStorage();
829
+ if (reason) throw new OfflineUnavailableError(reason);
830
+ }, "assertDeviceStorage");
831
+ var OPFS_MISSING = /Missing required OPFS APIs/i;
832
+ var defaultBackend = {
833
+ getDirectory: /* @__PURE__ */ __name(async () => {
834
+ assertDeviceStorage();
835
+ try {
836
+ return await navigator.storage.getDirectory();
837
+ } catch (error) {
838
+ if (error?.name === "SecurityError") {
839
+ throw new OfflineUnavailableError("the browser refused its private file storage");
840
+ }
841
+ throw error;
842
+ }
843
+ }, "getDirectory"),
844
+ openDatabase: /* @__PURE__ */ __name(async (userId, fileName) => {
845
+ assertDeviceStorage();
846
+ const openLocal = /* @__PURE__ */ __name(() => openWorkerDatabase(userId, fileName, {
847
+ workerUrl: getDeviceWorkerUrl(),
848
+ sqliteModuleUrl: getSqliteModuleUrl()
849
+ }), "openLocal");
850
+ try {
851
+ if (!canShareDatabase()) return await openLocal();
852
+ return await openSharedDatabase(userId, openLocal, () => forgetClosedStore(userId));
853
+ } catch (error) {
854
+ if (OPFS_MISSING.test(String(error?.message ?? error))) {
855
+ throw new OfflineUnavailableError("the browser lacks the file access the device copy needs (OPFS)");
856
+ }
857
+ throw error;
858
+ }
859
+ }, "openDatabase"),
860
+ requestPersistence: /* @__PURE__ */ __name(async () => {
861
+ if (typeof navigator !== "undefined" && navigator.storage?.persist) await navigator.storage.persist();
862
+ }, "requestPersistence")
863
+ };
864
+ var backend = defaultBackend;
865
+ var current;
866
+ function setDeviceStoreBackend(next) {
867
+ backend = next ? { ...defaultBackend, ...next } : defaultBackend;
868
+ current = void 0;
869
+ }
870
+ __name(setDeviceStoreBackend, "setDeviceStoreBackend");
871
+ var entryExists = /* @__PURE__ */ __name(async (directory, name) => {
872
+ for await (const key of directory.keys()) if (key === name) return true;
873
+ return false;
874
+ }, "entryExists");
875
+ var removeIfPresent = /* @__PURE__ */ __name(async (directory, name) => {
876
+ try {
877
+ await directory.removeEntry(name, { recursive: true });
878
+ } catch (error) {
879
+ if (error?.name !== "NotFoundError") throw error;
880
+ }
881
+ }, "removeIfPresent");
882
+ var entriesOf = /* @__PURE__ */ __name(async (directory, userId) => {
883
+ const names = [];
884
+ for await (const key of directory.keys()) if (userIdOf(key) === userId) names.push(key);
885
+ return names;
886
+ }, "entriesOf");
887
+ var deviceDirectory = /* @__PURE__ */ __name(async () => {
888
+ if (!isOfflineAvailable()) return void 0;
889
+ try {
890
+ return await backend.getDirectory();
891
+ } catch (error) {
892
+ if (!(error instanceof OfflineUnavailableError)) throw error;
893
+ markOfflineUnavailable(error);
894
+ return void 0;
895
+ }
896
+ }, "deviceDirectory");
897
+ var userIdOf = /* @__PURE__ */ __name((name) => FILE_PATTERN.exec(name)?.[1] ?? POOL_PATTERN.exec(name)?.[1], "userIdOf");
898
+ var closeCurrent = /* @__PURE__ */ __name(async () => {
899
+ const open = current;
900
+ current = void 0;
901
+ if (!open) return;
902
+ try {
903
+ await (await open.database).close();
904
+ } catch {
905
+ }
906
+ }, "closeCurrent");
907
+ function openDeviceStore(userId) {
908
+ if (!userId) return Promise.reject(new Error("openDeviceStore needs the signed-in user id."));
909
+ if (isOfflineStoreBlocked(userId)) {
910
+ return Promise.reject(new DeviceStoreUnavailableError("The device copy was deleted at logout"));
911
+ }
912
+ if (!isOfflineAvailable()) {
913
+ return Promise.reject(new OfflineUnavailableError("it was found unable to keep the device copy"));
914
+ }
915
+ if (current?.userId === userId) return current.store;
916
+ const previous = closeCurrent();
917
+ const database = previous.then(() => backend.openDatabase(userId, deviceFileName(userId))).catch((error) => {
918
+ if (error instanceof OfflineUnavailableError) markOfflineUnavailable(error);
919
+ throw error;
920
+ });
921
+ const store = database.then(async (opened) => {
922
+ if (opened.created) await backend.requestPersistence();
923
+ return createSqlDeviceStore(opened.exec, {
924
+ exclusive: opened.exclusive,
925
+ beforeEnqueue: /* @__PURE__ */ __name(async () => {
926
+ const directory = await backend.getDirectory();
927
+ await directory.getFileHandle(pendingMarkerName(userId), { create: true });
928
+ }, "beforeEnqueue"),
929
+ afterQueueChange: /* @__PURE__ */ __name(async (entries) => {
930
+ if (entries > 0) return;
931
+ await removeIfPresent(await backend.getDirectory(), pendingMarkerName(userId));
932
+ }, "afterQueueChange")
933
+ });
934
+ });
935
+ const open = { userId, store, database };
936
+ current = open;
937
+ store.catch(() => {
938
+ if (current === open) current = void 0;
939
+ });
940
+ return store;
941
+ }
942
+ __name(openDeviceStore, "openDeviceStore");
943
+ var sqlite3Promise;
944
+ async function openMemoryDatabase() {
945
+ sqlite3Promise ??= import(
946
+ /* webpackIgnore: true */
947
+ /* turbopackIgnore: true */
948
+ "@sqlite.org/sqlite-wasm"
949
+ ).then(
950
+ (module) => module.default()
951
+ );
952
+ const sqlite3 = await sqlite3Promise;
953
+ const db = new sqlite3.oo1.DB(":memory:", "c");
954
+ return {
955
+ created: true,
956
+ exec: /* @__PURE__ */ __name(async (sql, bind) => db.exec({
957
+ sql,
958
+ bind: bind && bind.length ? bind : void 0,
959
+ rowMode: "object",
960
+ returnValue: "resultRows"
961
+ }), "exec"),
962
+ close: /* @__PURE__ */ __name(async () => {
963
+ db.close();
964
+ }, "close")
965
+ };
966
+ }
967
+ __name(openMemoryDatabase, "openMemoryDatabase");
968
+ async function openMemoryDeviceStore() {
969
+ const database = await openMemoryDatabase();
970
+ return createSqlDeviceStore(database.exec);
971
+ }
972
+ __name(openMemoryDeviceStore, "openMemoryDeviceStore");
973
+ function forgetClosedStore(userId) {
974
+ if (current?.userId === userId) current = void 0;
975
+ forgetOfflineStore(userId);
976
+ refreshOfflineDeviceState();
977
+ }
978
+ __name(forgetClosedStore, "forgetClosedStore");
979
+ async function deleteDeviceStore(userId) {
980
+ if (current?.userId === userId) await closeCurrent();
981
+ const directory = await deviceDirectory();
982
+ if (!directory) return;
983
+ const remove = /* @__PURE__ */ __name(async () => {
984
+ const names = await entriesOf(directory, userId);
985
+ const marker = pendingMarkerName(userId);
986
+ for (const name of names) if (name !== marker) await removeIfPresent(directory, name);
987
+ await removeIfPresent(directory, marker);
988
+ }, "remove");
989
+ if (backend === defaultBackend) await withDatabaseReleased(userId, remove);
990
+ else await remove();
991
+ }
992
+ __name(deleteDeviceStore, "deleteDeviceStore");
993
+ async function hasUnsentChanges(userId) {
994
+ if (current?.userId === userId) {
995
+ const store = await current.store;
996
+ if ((await store.queue()).length > 0) return true;
997
+ }
998
+ const directory = await deviceDirectory();
999
+ if (!directory) return false;
1000
+ return entryExists(directory, pendingMarkerName(userId));
1001
+ }
1002
+ __name(hasUnsentChanges, "hasUnsentChanges");
1003
+ async function onUserSignedIn(userId) {
1004
+ unblockOfflineStore(userId);
1005
+ if (current && current.userId !== userId) await closeCurrent();
1006
+ const directory = await deviceDirectory();
1007
+ if (!directory) return;
1008
+ const others = /* @__PURE__ */ new Set();
1009
+ const imageOwners = [];
1010
+ for await (const key of directory.keys()) {
1011
+ if (key.startsWith(IMAGES_OWNER_PREFIX)) {
1012
+ imageOwners.push(key);
1013
+ continue;
1014
+ }
1015
+ const owner = userIdOf(key);
1016
+ if (owner && owner !== userId) others.add(owner);
1017
+ }
1018
+ if (!imageOwners.includes(imagesOwnerName(userId))) {
1019
+ await deleteImagesCaches();
1020
+ for (const name of imageOwners) await removeIfPresent(directory, name);
1021
+ await directory.getFileHandle(imagesOwnerName(userId), { create: true });
1022
+ }
1023
+ for (const other of others) {
1024
+ if (await entryExists(directory, pendingMarkerName(other))) continue;
1025
+ await deleteDeviceStore(other);
1026
+ }
1027
+ }
1028
+ __name(onUserSignedIn, "onUserSignedIn");
1029
+ function isRootLocked(s, now) {
1030
+ if (s.locked) return true;
1031
+ return s.lockedFrom !== null && s.lockedFrom.getTime() <= now.getTime();
1032
+ }
1033
+ __name(isRootLocked, "isRootLocked");
1034
+
1035
+ export {
1036
+ imageUrlOf,
1037
+ cacheArtworkImage,
1038
+ removeArtworkImage,
1039
+ deleteImagesCaches,
1040
+ setDeviceStoreBackend,
1041
+ openDeviceStore,
1042
+ openMemoryDatabase,
1043
+ openMemoryDeviceStore,
1044
+ deleteDeviceStore,
1045
+ hasUnsentChanges,
1046
+ onUserSignedIn,
1047
+ isRootLocked
1048
+ };
1049
+ //# sourceMappingURL=chunk-7DBJTNSU.mjs.map