@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
package/pwa/sw.js CHANGED
@@ -6,12 +6,147 @@
6
6
  //
7
7
  // Per-app values (appId, offlineRevision, notificationIcon) come from the app's
8
8
  // own public/sw-config.js, which sets self.NJA_SW_CONFIG.
9
+ //
10
+ // Offline mode (opt-in; without these keys the worker behaves exactly as before):
11
+ // - offlineShell: { default: "/offline-shell", locales: { it: "/it/offline-shell" } }
12
+ // At install the worker reads /offline-manifest.json ({ buildId, assets }) and
13
+ // caches every asset plus each shell URL in `${appId}-shell-${buildId}`, then
14
+ // a completion marker last; one failed file fails the install and deletes the
15
+ // half-filled cache, so the previous worker stays in charge and no worker ever
16
+ // answers from a cache without the marker. A
17
+ // failed navigation answers the shell of its locale (the first path segment
18
+ // matched against `locales`, otherwise `default`), then offline.html. GETs
19
+ // under /_next/static/ are served cache-first from the shell cache; after an
20
+ // online navigation the worker re-reads the manifest (at most every few
21
+ // minutes) and precaches a new build's shell in the background, so a deploy
22
+ // refreshes the shell without a new worker; any other
23
+ // same-origin GET goes to the network first and, when it fails, is answered
24
+ // from the shell cache (the device worker and SQLite files). Activate deletes
25
+ // shell caches of other builds.
26
+ // - offlineImageHosts: ["api.example.com"] (a host or an origin)
27
+ // A GET to one of these hosts goes to the network; when the network fails it
28
+ // is answered from `${appId}-images`, where the app stores each artwork image
29
+ // under its exact URL. That cache is kept across activations.
9
30
 
10
31
  importScripts("/sw-config.js");
11
32
 
12
33
  const OFFLINE_URL = "/offline.html";
13
34
  const CACHE_NAME = `${self.NJA_SW_CONFIG.appId}-offline-${self.NJA_SW_CONFIG.offlineRevision}`;
14
35
 
36
+ const OFFLINE_SHELL = self.NJA_SW_CONFIG.offlineShell || null;
37
+ const OFFLINE_IMAGE_HOSTS = self.NJA_SW_CONFIG.offlineImageHosts || [];
38
+ const MANIFEST_URL = "/offline-manifest.json";
39
+ const SHELL_CACHE_PREFIX = `${self.NJA_SW_CONFIG.appId}-shell-`;
40
+ const IMAGES_CACHE_NAME = `${self.NJA_SW_CONFIG.appId}-images`;
41
+ // Written last into a shell cache: a shell cache without it is incomplete and never used.
42
+ const SHELL_COMPLETE_URL = "/__nja-offline-shell-complete__";
43
+
44
+ // The shell cache this worker installed. A worker can be stopped and restarted
45
+ // between events, losing this value; it then falls back to the newest complete
46
+ // shell cache (caches.keys() lists caches in creation order).
47
+ let shellCacheName = null;
48
+
49
+ async function currentShellCacheName() {
50
+ if (shellCacheName) return shellCacheName;
51
+ const keys = await caches.keys();
52
+ const shells = keys.filter(function (key) {
53
+ return key.startsWith(SHELL_CACHE_PREFIX);
54
+ });
55
+ for (let index = shells.length - 1; index >= 0; index--) {
56
+ const cache = await caches.open(shells[index]);
57
+ if (await cache.match(SHELL_COMPLETE_URL)) return shells[index];
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function shellUrls() {
63
+ const urls = [OFFLINE_SHELL.default];
64
+ const locales = OFFLINE_SHELL.locales || {};
65
+ for (const locale of Object.keys(locales)) urls.push(locales[locale]);
66
+ return urls;
67
+ }
68
+
69
+ function shellUrlFor(url) {
70
+ const segment = new URL(url).pathname.split("/")[1] || "";
71
+ const locales = OFFLINE_SHELL.locales || {};
72
+ return Object.prototype.hasOwnProperty.call(locales, segment) ? locales[segment] : OFFLINE_SHELL.default;
73
+ }
74
+
75
+ async function installShell() {
76
+ const response = await fetch(new Request(MANIFEST_URL, { cache: "no-store" }));
77
+ if (!response.ok) throw new Error(`${MANIFEST_URL} answered ${response.status}`);
78
+ const manifest = await response.json();
79
+ const name = `${SHELL_CACHE_PREFIX}${manifest.buildId}`;
80
+ try {
81
+ const cache = await caches.open(name);
82
+ // cache.addAll rejects when any file fails, so the install fails with it.
83
+ await cache.addAll(
84
+ manifest.assets.concat(shellUrls()).map(function (url) {
85
+ return new Request(url, { cache: "reload" });
86
+ }),
87
+ );
88
+ await cache.put(SHELL_COMPLETE_URL, new Response("", { status: 200 }));
89
+ } catch (error) {
90
+ // Never leave a half-filled shell cache behind for any worker to pick.
91
+ await caches.delete(name);
92
+ throw error;
93
+ }
94
+ shellCacheName = name;
95
+ }
96
+
97
+ // A deploy changes the app's build, not this file: sw.js is byte-identical across builds, so no
98
+ // new worker is installed. After an online navigation the worker reads the manifest and, when
99
+ // the server runs a build it has no complete shell cache for, precaches that build's shell in
100
+ // the background, then drops the other shell caches. At most one check per few minutes.
101
+ const SHELL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
102
+ let lastShellRefresh = 0;
103
+ let shellRefresh = null;
104
+
105
+ async function refreshShellIfStale() {
106
+ const response = await fetch(new Request(MANIFEST_URL, { cache: "no-store" }));
107
+ if (!response.ok) return;
108
+ const manifest = await response.json();
109
+ const name = `${SHELL_CACHE_PREFIX}${manifest.buildId}`;
110
+ const keys = await caches.keys();
111
+ if (keys.indexOf(name) !== -1) {
112
+ const existing = await caches.open(name);
113
+ if (await existing.match(SHELL_COMPLETE_URL)) {
114
+ shellCacheName = name;
115
+ return;
116
+ }
117
+ }
118
+ await installShell();
119
+ const others = (await caches.keys()).filter(function (key) {
120
+ return key.startsWith(SHELL_CACHE_PREFIX) && key !== shellCacheName;
121
+ });
122
+ await Promise.all(
123
+ others.map(function (key) {
124
+ return caches.delete(key);
125
+ }),
126
+ );
127
+ }
128
+
129
+ function maybeRefreshShell() {
130
+ const now = Date.now();
131
+ if (shellRefresh || now - lastShellRefresh < SHELL_REFRESH_INTERVAL_MS) return shellRefresh || Promise.resolve();
132
+ lastShellRefresh = now;
133
+ shellRefresh = refreshShellIfStale()
134
+ .catch(function (error) {
135
+ // The current shell stays in charge; the next navigation tries again later.
136
+ console.warn("[sw] refreshing the offline shell failed", error);
137
+ })
138
+ .finally(function () {
139
+ shellRefresh = null;
140
+ });
141
+ return shellRefresh;
142
+ }
143
+
144
+ function isImageHost(url) {
145
+ if (!OFFLINE_IMAGE_HOSTS.length) return false;
146
+ const parsed = new URL(url);
147
+ return OFFLINE_IMAGE_HOSTS.indexOf(parsed.host) !== -1 || OFFLINE_IMAGE_HOSTS.indexOf(parsed.origin) !== -1;
148
+ }
149
+
15
150
  // Served when a navigation fails and the cached offline page is missing too
16
151
  // (evicted or cleared). Never answer a navigation with a bare network error.
17
152
  const OFFLINE_FALLBACK_HTML = `<!doctype html>
@@ -58,11 +193,16 @@ const OFFLINE_FALLBACK_HTML = `<!doctype html>
58
193
 
59
194
  self.addEventListener("install", function (event) {
60
195
  event.waitUntil(
61
- caches.open(CACHE_NAME).then(function (cache) {
62
- // cache.add rejects on a network error or a non-2xx response, so the
63
- // install fails and the previous worker stays in charge.
64
- return cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
65
- }),
196
+ caches
197
+ .open(CACHE_NAME)
198
+ .then(function (cache) {
199
+ // cache.add rejects on a network error or a non-2xx response, so the
200
+ // install fails and the previous worker stays in charge.
201
+ return cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
202
+ })
203
+ .then(function () {
204
+ if (OFFLINE_SHELL) return installShell();
205
+ }),
66
206
  );
67
207
  });
68
208
 
@@ -70,10 +210,16 @@ self.addEventListener("activate", function (event) {
70
210
  event.waitUntil(
71
211
  (async function () {
72
212
  const keys = await caches.keys();
213
+ const keep = [CACHE_NAME];
214
+ if (OFFLINE_SHELL) {
215
+ const shell = await currentShellCacheName();
216
+ if (shell) keep.push(shell);
217
+ }
218
+ if (OFFLINE_SHELL || OFFLINE_IMAGE_HOSTS.length) keep.push(IMAGES_CACHE_NAME);
73
219
  await Promise.all(
74
220
  keys
75
221
  .filter(function (key) {
76
- return key !== CACHE_NAME;
222
+ return keep.indexOf(key) === -1;
77
223
  })
78
224
  .map(function (key) {
79
225
  return caches.delete(key);
@@ -88,28 +234,133 @@ self.addEventListener("activate", function (event) {
88
234
  });
89
235
 
90
236
  self.addEventListener("fetch", function (event) {
91
- // Only navigations are intercepted. Everything else goes straight to the
92
- // network without respondWith.
93
- if (event.request.mode !== "navigate") return;
237
+ // Only navigations are intercepted, plus (offline mode only) build assets and
238
+ // artwork images. Everything else goes straight to the network without
239
+ // respondWith.
240
+ if (event.request.mode !== "navigate") {
241
+ if (!OFFLINE_SHELL && !OFFLINE_IMAGE_HOSTS.length) return;
242
+ const method = event.request.method || "GET";
243
+ if (method !== "GET") return;
244
+ // A no-store request (the page's session refresh of the shell) is never answered from a
245
+ // cache: when the network fails it must fail, so the page knows the app is unreachable.
246
+ if (event.request.cache === "no-store") return;
247
+ const url = new URL(event.request.url);
94
248
 
95
- event.respondWith(
96
- (async function () {
97
- try {
98
- const preload = await event.preloadResponse;
99
- if (preload) return preload;
100
- // A 4xx/5xx or an opaque redirect is passed through as is.
101
- return await fetch(event.request);
102
- } catch (error) {
103
- const cache = await caches.open(CACHE_NAME);
104
- const cached = await cache.match(OFFLINE_URL);
105
- if (cached) return cached;
106
- return new Response(OFFLINE_FALLBACK_HTML, {
107
- status: 200,
108
- headers: { "content-type": "text/html; charset=utf-8" },
109
- });
249
+ if (OFFLINE_SHELL && url.origin === self.location.origin && url.pathname.startsWith("/_next/static/")) {
250
+ event.respondWith(
251
+ (async function () {
252
+ const name = await currentShellCacheName();
253
+ if (name) {
254
+ const cache = await caches.open(name);
255
+ const cached = await cache.match(event.request.url);
256
+ if (cached) return cached;
257
+ }
258
+ try {
259
+ return await fetch(event.request);
260
+ } catch {
261
+ // Offline and not in the shell: a plain network error for the page, no uncaught rejection here.
262
+ return Response.error();
263
+ }
264
+ })(),
265
+ );
266
+ return;
267
+ }
268
+
269
+ // An image of an offline image host through Next's image optimizer (`/_next/image?url=...`):
270
+ // network first, then the downloaded original (the images cache keeps it under its own URL).
271
+ const optimizedSource = url.origin === self.location.origin && url.pathname === "/_next/image" ? url.searchParams.get("url") : null;
272
+ if (optimizedSource && /^https?:/.test(optimizedSource) && isImageHost(optimizedSource)) {
273
+ event.respondWith(
274
+ (async function () {
275
+ try {
276
+ return await fetch(event.request);
277
+ } catch {
278
+ const cache = await caches.open(IMAGES_CACHE_NAME);
279
+ const cached = await cache.match(optimizedSource);
280
+ if (cached) return cached;
281
+ return Response.error();
282
+ }
283
+ })(),
284
+ );
285
+ return;
286
+ }
287
+
288
+ // Any other same-origin file the offline manifest precached (the device worker and the
289
+ // SQLite files it loads): network first, the shell cache when the network fails.
290
+ if (OFFLINE_SHELL && url.origin === self.location.origin) {
291
+ event.respondWith(
292
+ (async function () {
293
+ try {
294
+ return await fetch(event.request);
295
+ } catch {
296
+ const name = await currentShellCacheName();
297
+ if (name) {
298
+ const cache = await caches.open(name);
299
+ const cached = await cache.match(event.request.url);
300
+ if (cached) return cached;
301
+ }
302
+ return Response.error();
303
+ }
304
+ })(),
305
+ );
306
+ return;
307
+ }
308
+
309
+ if (isImageHost(event.request.url)) {
310
+ event.respondWith(
311
+ (async function () {
312
+ try {
313
+ return await fetch(event.request);
314
+ } catch {
315
+ const cache = await caches.open(IMAGES_CACHE_NAME);
316
+ const cached = await cache.match(event.request.url);
317
+ if (cached) return cached;
318
+ return Response.error();
319
+ }
320
+ })(),
321
+ );
322
+ }
323
+ return;
324
+ }
325
+
326
+ let reachedNetwork = false;
327
+ const answer = (async function () {
328
+ try {
329
+ const preload = await event.preloadResponse;
330
+ if (preload) {
331
+ reachedNetwork = true;
332
+ return preload;
110
333
  }
111
- })(),
112
- );
334
+ // A 4xx/5xx or an opaque redirect is passed through as is.
335
+ const response = await fetch(event.request);
336
+ reachedNetwork = true;
337
+ return response;
338
+ } catch (error) {
339
+ if (OFFLINE_SHELL) {
340
+ const name = await currentShellCacheName();
341
+ if (name) {
342
+ const shellCache = await caches.open(name);
343
+ const shell = await shellCache.match(shellUrlFor(event.request.url));
344
+ if (shell) return shell;
345
+ }
346
+ }
347
+ const cache = await caches.open(CACHE_NAME);
348
+ const cached = await cache.match(OFFLINE_URL);
349
+ if (cached) return cached;
350
+ return new Response(OFFLINE_FALLBACK_HTML, {
351
+ status: 200,
352
+ headers: { "content-type": "text/html; charset=utf-8" },
353
+ });
354
+ }
355
+ })();
356
+ event.respondWith(answer);
357
+ if (OFFLINE_SHELL) {
358
+ event.waitUntil(
359
+ answer.then(function () {
360
+ if (reachedNetwork) return maybeRefreshShell();
361
+ }),
362
+ );
363
+ }
113
364
  });
114
365
 
115
366
  self.addEventListener("message", function (event) {
@@ -1,6 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  import { ApiData } from "../core/interfaces/ApiData";
4
+ import { isOfflineConfigured } from "../offline/config";
5
+ import { getOfflineState, reachesNetworkWhileOffline, setOfflineState } from "../offline/state";
6
+ import { OFFLINE_REPLAY_HEADER } from "../offline/types";
4
7
 
5
8
  export interface DirectFetchParams {
6
9
  method: string;
@@ -18,6 +21,39 @@ export interface DirectFetchParams {
18
21
  * Use this for client-side API calls.
19
22
  */
20
23
  export async function directFetch(params: DirectFetchParams): Promise<ApiData> {
24
+ // Offline mode is opt-in (`configureOffline`). Without it nothing below loads or runs. A request
25
+ // the server runs as an offline replay always goes to the network, and so do the reads and the
26
+ // change-set request of a reconnect attempt (the session stays offline until its sync succeeds).
27
+ const offline = isOfflineConfigured() && !hasReplayHeader(params.additionalHeaders);
28
+ if (offline && !getOfflineState().online && !reachesNetworkWhileOffline(params.method, params.url)) {
29
+ return answerLocally(params);
30
+ }
31
+
32
+ const response = await fetchFromApi(params);
33
+
34
+ if (offline && response.networkError) {
35
+ setOfflineState({ online: false });
36
+ return answerLocally(params);
37
+ }
38
+ if (offline && response.ok && params.method.toUpperCase() !== "GET") {
39
+ const { writeThrough } = await import("../offline/write-through");
40
+ await writeThrough({ method: params.method, url: params.url }, response);
41
+ }
42
+
43
+ return response;
44
+ }
45
+
46
+ function hasReplayHeader(headers?: Record<string, string>): boolean {
47
+ if (!headers) return false;
48
+ return Object.keys(headers).some((key) => key.toLowerCase() === OFFLINE_REPLAY_HEADER);
49
+ }
50
+
51
+ async function answerLocally(params: DirectFetchParams): Promise<ApiData> {
52
+ const { localTransport } = await import("../offline/local-transport");
53
+ return localTransport({ method: params.method, url: params.url, body: params.body, files: params.files });
54
+ }
55
+
56
+ async function fetchFromApi(params: DirectFetchParams): Promise<ApiData> {
21
57
  const response: ApiData = {
22
58
  data: undefined,
23
59
  ok: false,
@@ -95,6 +131,7 @@ export async function directFetch(params: DirectFetchParams): Promise<ApiData> {
95
131
  response.status = 503;
96
132
  response.statusText = "Service Unavailable";
97
133
  response.data = undefined;
134
+ response.networkError = true;
98
135
  }
99
136
 
100
137
  return response;
@@ -486,7 +486,7 @@ export function RoundPageContainer({
486
486
  of label length. */}
487
487
  <TabsList className="flex h-auto w-full flex-col items-stretch gap-0.5 bg-transparent p-0">
488
488
  {ungrouped.map((tab) => (
489
- <TabsTrigger key={tab.label} value={tabValue(tab)} className={railTriggerClass}>
489
+ <TabsTrigger key={tabValue(tab)} value={tabValue(tab)} className={railTriggerClass}>
490
490
  {tab.contentLabel ?? tab.label}
491
491
  </TabsTrigger>
492
492
  ))}
@@ -496,7 +496,7 @@ export function RoundPageContainer({
496
496
  {group.label}
497
497
  </MicroLabel>
498
498
  {group.items.map((tab) => (
499
- <TabsTrigger key={tab.label} value={tabValue(tab)} className={railTriggerClass}>
499
+ <TabsTrigger key={tabValue(tab)} value={tabValue(tab)} className={railTriggerClass}>
500
500
  {tab.contentLabel ?? tab.label}
501
501
  </TabsTrigger>
502
502
  ))}
@@ -529,7 +529,7 @@ export function RoundPageContainer({
529
529
  </SelectTrigger>
530
530
  <SelectContent>
531
531
  {ungrouped.map((tab) => (
532
- <SelectItem key={tab.label} value={tabValue(tab)}>
532
+ <SelectItem key={tabValue(tab)} value={tabValue(tab)}>
533
533
  {tab.contentLabel ?? tab.label}
534
534
  </SelectItem>
535
535
  ))}
@@ -537,7 +537,7 @@ export function RoundPageContainer({
537
537
  <SelectGroup key={group.label}>
538
538
  <SelectLabel>{group.label}</SelectLabel>
539
539
  {group.items.map((tab) => (
540
- <SelectItem key={tab.label} value={tabValue(tab)}>
540
+ <SelectItem key={tabValue(tab)} value={tabValue(tab)}>
541
541
  {tab.contentLabel ?? tab.label}
542
542
  </SelectItem>
543
543
  ))}
@@ -565,7 +565,7 @@ export function RoundPageContainer({
565
565
  {header}
566
566
  {tabs.map((tab) => (
567
567
  <TabsContent
568
- key={tab.label}
568
+ key={tabValue(tab)}
569
569
  value={tabValue(tab)}
570
570
  className={tab.fillHeight ? `flex min-h-0 w-full flex-1 flex-col` : ``}
571
571
  >
@@ -615,7 +615,7 @@ export function RoundPageContainer({
615
615
  </SelectTrigger>
616
616
  <SelectContent>
617
617
  {tabs.map((tab) => (
618
- <SelectItem key={tab.label} value={tabValue(tab)}>
618
+ <SelectItem key={tabValue(tab)} value={tabValue(tab)}>
619
619
  {tab.contentLabel ?? tab.label}
620
620
  </SelectItem>
621
621
  ))}
@@ -626,7 +626,7 @@ export function RoundPageContainer({
626
626
  <div className="p-4">
627
627
  <TabsList data-help="page.tabs">
628
628
  {tabs.map((tab) => (
629
- <TabsTrigger key={tab.label} value={tabValue(tab)} className="px-4">
629
+ <TabsTrigger key={tabValue(tab)} value={tabValue(tab)} className="px-4">
630
630
  {tab.contentLabel ?? tab.label}
631
631
  </TabsTrigger>
632
632
  ))}
@@ -642,7 +642,7 @@ export function RoundPageContainer({
642
642
  >
643
643
  {tabs.map((tab) => (
644
644
  <TabsContent
645
- key={tab.label}
645
+ key={tabValue(tab)}
646
646
  value={tabValue(tab)}
647
647
  className={tab.fillHeight ? `flex flex-1 min-h-0 w-full flex-col` : `pb-20`}
648
648
  >
@@ -821,11 +821,21 @@ export default function BlockNoteEditor({
821
821
  // initialContent is just an echo of our own emission (form-controlled flow).
822
822
  const lastEmittedHashRef = useRef<string | null>(null);
823
823
 
824
+ // Whether the parent's copy of the text is empty: the content it handed in, then each change
825
+ // reported to it. A change from empty to still empty is not reported: BlockNote emits one when an
826
+ // empty editor mounts (its trailing empty paragraph), and reported it became a save of nothing on
827
+ // every view of a page with an empty field.
828
+ const processedContentRef = useRef<any[]>([]);
829
+ const parentKnowsEmptyRef = useRef<boolean | null>(null);
830
+
824
831
  const handleChange = useCallback(async () => {
825
832
  if (!onChange) return;
826
833
  const newBlocks = editor.document;
827
834
  lastEmittedHashRef.current = JSON.stringify(newBlocks);
828
835
  const isEmpty = isDocumentEmpty(newBlocks);
836
+ const wasEmpty = parentKnowsEmptyRef.current ?? isDocumentEmpty(processedContentRef.current);
837
+ if (isEmpty && wasEmpty) return;
838
+ parentKnowsEmptyRef.current = isEmpty;
829
839
 
830
840
  function hasUnresolvedDiffsRecursive(block: any): boolean {
831
841
  if (!block || typeof block !== "object") return false;
@@ -880,6 +890,8 @@ export default function BlockNoteEditor({
880
890
  onChange(newBlocks, isEmpty, hasUnresolvedDiff);
881
891
  }, [editor, onChange, id, acceptedChanges, rejectedChanges]);
882
892
 
893
+ processedContentRef.current = processedContent as any[];
894
+
883
895
  // Utility: deep equality for arrays of blocks
884
896
  const areBlocksEqual = (a: any[], b: any[]): boolean => {
885
897
  return JSON.stringify(a) === JSON.stringify(b);
@@ -912,6 +924,8 @@ export default function BlockNoteEditor({
912
924
  if (collaboration) return;
913
925
  const hash = JSON.stringify(processedContent);
914
926
  if (previousContentHashRef.current === hash) return; // no changes
927
+ // New content from the parent: that is now the text it knows.
928
+ parentKnowsEmptyRef.current = isDocumentEmpty(processedContent as any[]);
915
929
  // Skip replaceBlocks when the new initialContent is just an echo of what
916
930
  // the editor itself emitted. Without this, a form-controlled parent
917
931
  // (FormBlockNote) round-trips field.value back as initialContent on every