@carlonicora/nextjs-jsonapi 6.12.2 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) 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-75ED2CN2.mjs → BlockNoteEditor-22Q4DJBU.mjs} +20 -8
  4. package/dist/BlockNoteEditor-22Q4DJBU.mjs.map +1 -0
  5. package/dist/{BlockNoteEditor-7M5QW2YQ.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-V3YDVJ63.js → chunk-2H342HNN.js} +948 -921
  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-RXQYEEKZ.mjs → chunk-H4NLDX5G.mjs} +51 -24
  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/editors/BlockNoteEditor.tsx +14 -0
  165. package/src/components/editors/__tests__/BlockNoteEditor.empty-change.spec.tsx +205 -0
  166. package/src/contexts/SocketContext.tsx +7 -1
  167. package/src/contexts/__tests__/SocketContext.session.spec.tsx +52 -5
  168. package/src/core/factories/RehydrationFactory.ts +28 -8
  169. package/src/core/factories/__tests__/RehydrationFactory.spec.ts +33 -0
  170. package/src/core/index.ts +3 -0
  171. package/src/core/interfaces/ApiData.ts +2 -0
  172. package/src/core/registry/ModuleRegistry.ts +3 -0
  173. package/src/features/auth/components/forms/Logout.tsx +8 -1
  174. package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +62 -0
  175. package/src/hooks/__tests__/useSocket.pause.spec.ts +39 -0
  176. package/src/hooks/useSocket.ts +20 -1
  177. package/src/offline/__tests__/answer-builders.spec.ts +176 -0
  178. package/src/offline/__tests__/device-switch.spec.ts +179 -0
  179. package/src/offline/__tests__/generic-answers.spec.ts +380 -0
  180. package/src/offline/__tests__/local-transport.spec.ts +150 -0
  181. package/src/offline/__tests__/logout.spec.ts +122 -0
  182. package/src/offline/__tests__/offline-fixtures.ts +89 -0
  183. package/src/offline/__tests__/router.spec.ts +56 -0
  184. package/src/offline/__tests__/state.spec.ts +62 -0
  185. package/src/offline/__tests__/write-through.spec.ts +137 -0
  186. package/src/offline/config.ts +229 -0
  187. package/src/offline/configure.ts +14 -0
  188. package/src/offline/device-switch.ts +75 -0
  189. package/src/offline/generic-answers.ts +706 -0
  190. package/src/offline/index.ts +67 -0
  191. package/src/offline/local-transport.ts +189 -0
  192. package/src/offline/logout.ts +41 -0
  193. package/src/offline/router.ts +41 -0
  194. package/src/offline/server/__tests__/build-offline-manifest.spec.ts +76 -0
  195. package/src/offline/server/build-offline-manifest.ts +49 -0
  196. package/src/offline/state-hooks.ts +10 -0
  197. package/src/offline/state.ts +143 -0
  198. package/src/offline/store/DeviceStore.ts +112 -0
  199. package/src/offline/store/__tests__/device-store-lifecycle.spec.ts +343 -0
  200. package/src/offline/store/__tests__/shared-database.spec.ts +151 -0
  201. package/src/offline/store/__tests__/sql-device-store.spec.ts +399 -0
  202. package/src/offline/store/lifecycle.ts +315 -0
  203. package/src/offline/store/schema.ts +101 -0
  204. package/src/offline/store/shared-database.ts +321 -0
  205. package/src/offline/store/sql-device-store.ts +469 -0
  206. package/src/offline/store/worker-client.ts +71 -0
  207. package/src/offline/store/worker.ts +87 -0
  208. package/src/offline/sync/__tests__/conflicts.spec.ts +185 -0
  209. package/src/offline/sync/__tests__/queue-drain.spec.ts +364 -0
  210. package/src/offline/sync/__tests__/runtime.spec.ts +337 -0
  211. package/src/offline/sync/__tests__/sync-fixtures.ts +103 -0
  212. package/src/offline/sync/__tests__/sync-root.spec.ts +727 -0
  213. package/src/offline/sync/artwork-cache.ts +67 -0
  214. package/src/offline/sync/conflicts.ts +211 -0
  215. package/src/offline/sync/counts.ts +39 -0
  216. package/src/offline/sync/drain-queue.ts +246 -0
  217. package/src/offline/sync/known-roots.ts +37 -0
  218. package/src/offline/sync/models/OfflineChangeSet.ts +34 -0
  219. package/src/offline/sync/models/OfflineChangeSetInterface.ts +54 -0
  220. package/src/offline/sync/models/RootSync.ts +68 -0
  221. package/src/offline/sync/models/RootSyncInterface.ts +20 -0
  222. package/src/offline/sync/models/__tests__/OfflineChangeSet.spec.ts +47 -0
  223. package/src/offline/sync/models/__tests__/RootSync.spec.ts +54 -0
  224. package/src/offline/sync/models/index.ts +5 -0
  225. package/src/offline/sync/models/offline.modules.ts +14 -0
  226. package/src/offline/sync/runtime.ts +226 -0
  227. package/src/offline/sync/session-refresh.ts +32 -0
  228. package/src/offline/sync/sync-root.ts +336 -0
  229. package/src/offline/sync/waiting.ts +40 -0
  230. package/src/offline/types.ts +115 -0
  231. package/src/offline/write-through.ts +189 -0
  232. package/src/pwa/__tests__/sw.spec.ts +358 -1
  233. package/src/server/index.ts +5 -0
  234. package/src/unified/__tests__/JsonApiRequest.offline.spec.ts +243 -0
  235. package/dist/BlockNoteEditor-75ED2CN2.mjs.map +0 -1
  236. package/dist/BlockNoteEditor-7M5QW2YQ.js.map +0 -1
  237. package/dist/BlockNoteViewer-WGCDB7IJ.js.map +0 -1
  238. package/dist/JsonApiRequest-FZGPLNUF.js +0 -26
  239. package/dist/chunk-AA52ORW2.mjs.map +0 -1
  240. package/dist/chunk-EWCY6QJA.mjs.map +0 -1
  241. package/dist/chunk-FF4EIQ6H.js.map +0 -1
  242. package/dist/chunk-JTPRJNT2.js.map +0 -1
  243. package/dist/chunk-L6PYFQTO.js.map +0 -1
  244. package/dist/chunk-O3PDL2CV.js.map +0 -1
  245. package/dist/chunk-PI56A4DG.mjs.map +0 -1
  246. package/dist/chunk-RXQYEEKZ.mjs.map +0 -1
  247. package/dist/chunk-V3YDVJ63.js.map +0 -1
  248. package/dist/request-5RLK2AM3.mjs +0 -9
  249. package/dist/request-EUGTI3BE.js +0 -9
  250. package/dist/request-EUGTI3BE.js.map +0 -1
  251. /package/dist/{JsonApiRequest-A66XK5WT.mjs.map → JsonApiRequest-F6TMJ42G.mjs.map} +0 -0
  252. /package/dist/{chunk-3YW35YHQ.mjs.map → chunk-BH6JWIKI.mjs.map} +0 -0
  253. /package/dist/{chunk-V5MI7RNB.mjs.map → chunk-DGWOVYEC.mjs.map} +0 -0
  254. /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;
@@ -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
@@ -0,0 +1,205 @@
1
+ import { act, render } from "@testing-library/react";
2
+ import React from "react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ /**
6
+ * BlockNote emits one change when an empty editor mounts (it inserts its trailing empty
7
+ * paragraph). Reported to the parent, that no-op became a save on every page view (an autosave
8
+ * of an empty field), and offline a queued change. The editor reports a change unless the text
9
+ * went from empty to still empty. Mocks as in BlockNoteEditor.ai.spec.tsx, plus the view's
10
+ * onChange and the editor's replaceBlocks.
11
+ */
12
+ const h = vi.hoisted(() => {
13
+ const AIExtension = vi.fn((options: unknown) => ({ aiFactory: true, options }));
14
+ return {
15
+ AIExtension,
16
+ editorsCreated: 0,
17
+ company: undefined as { id: string } | undefined,
18
+ editor: undefined as any,
19
+ ai: undefined as any,
20
+ transportConfig: undefined as any,
21
+ viewOnChange: undefined as undefined | (() => void),
22
+ };
23
+ });
24
+
25
+ vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
26
+ vi.mock("next-themes", () => ({ useTheme: () => ({ resolvedTheme: "light" }) }));
27
+ vi.mock("../../../i18n/config", () => ({ useI18nLocale: () => "en" }));
28
+ vi.mock("../../../client/config", () => ({ getPublicApiUrl: () => "http://api.test/" }));
29
+ vi.mock("../../../client/token", () => ({ getClientToken: async () => "token" }));
30
+ vi.mock("../../../contexts", () => ({
31
+ useCurrentUserContextOptional: () => (h.company ? { company: h.company } : undefined),
32
+ }));
33
+
34
+ vi.mock("@blocknote/react", async () => {
35
+ const React = await import("react");
36
+ const EditorContext = React.createContext<any>(null);
37
+ const useBlockNoteEditor = () => React.useContext(EditorContext);
38
+ const requireAI = (editor: any) => {
39
+ if (!editor?.getExtension(h.AIExtension)) throw new Error("Extension not found");
40
+ };
41
+ return {
42
+ EditorContext,
43
+ createReactInlineContentSpec: (config: unknown) => ({ config }),
44
+ getDefaultReactSlashMenuItems: () => [],
45
+ SuggestionMenuController: () => null,
46
+ useBlockNoteEditor,
47
+ useExtension: () => {
48
+ const editor = useBlockNoteEditor();
49
+ requireAI(editor);
50
+ return editor.getExtension(h.AIExtension);
51
+ },
52
+ useExtensionState: () => "closed",
53
+ useCreateBlockNote: (options: any, deps: React.DependencyList = []) =>
54
+ React.useMemo(() => {
55
+ h.editorsCreated += 1;
56
+ const extensions: any[] = options?.extensions ?? [];
57
+ // Mirrors xl-ai 0.55: a store holding aiMenuState, and the calls that
58
+ // move it back to "closed".
59
+ const store = { state: { aiMenuState: "closed" as any } };
60
+ const ai = {
61
+ store,
62
+ invokeAI: vi.fn(),
63
+ openAIMenuAtBlock: vi.fn((blockId: string) => {
64
+ store.state.aiMenuState = { blockId, status: "user-input" };
65
+ }),
66
+ abort: vi.fn(async () => {
67
+ const menu = store.state.aiMenuState;
68
+ if (menu !== "closed") store.state.aiMenuState = { ...menu, status: "user-reviewing" };
69
+ }),
70
+ closeAIMenu: vi.fn(() => {
71
+ store.state.aiMenuState = "closed";
72
+ }),
73
+ rejectChanges: vi.fn(() => {
74
+ store.state.aiMenuState = "closed";
75
+ }),
76
+ };
77
+ const hasAI = extensions.some((e) => e?.aiFactory);
78
+ const editor = {
79
+ document: [] as any[],
80
+ replaceBlocks: (_old: unknown, blocks: any[]) => {
81
+ editor.document = blocks;
82
+ },
83
+ focus: () => undefined,
84
+ getSelection: () => undefined,
85
+ getExtension: (factory: unknown) => (factory === h.AIExtension && hasAI ? ai : undefined),
86
+ };
87
+ h.editor = editor;
88
+ h.ai = hasAI ? ai : undefined;
89
+ return editor;
90
+ }, deps),
91
+ BasicTextStyleButton: () => null,
92
+ BlockTypeSelect: () => null,
93
+ CreateLinkButton: () => null,
94
+ FileCaptionButton: () => null,
95
+ FileReplaceButton: () => null,
96
+ TextAlignButton: () => null,
97
+ FormattingToolbar: ({ children }: { children: React.ReactNode }) => <>{children}</>,
98
+ FormattingToolbarController: ({ formattingToolbar }: { formattingToolbar: () => React.ReactNode }) => (
99
+ <>{formattingToolbar()}</>
100
+ ),
101
+ // Shared with the xl-ai stubs below.
102
+ __requireAI: requireAI,
103
+ };
104
+ });
105
+
106
+ vi.mock("@blocknote/shadcn", async () => {
107
+ const { EditorContext } = (await import("@blocknote/react")) as any;
108
+ return {
109
+ BlockNoteView: ({
110
+ editor,
111
+ children,
112
+ onChange,
113
+ }: {
114
+ editor: unknown;
115
+ children: React.ReactNode;
116
+ onChange?: () => void;
117
+ }) => {
118
+ h.viewOnChange = onChange;
119
+ return <EditorContext.Provider value={editor}>{children}</EditorContext.Provider>;
120
+ },
121
+ };
122
+ });
123
+
124
+ vi.mock("@blocknote/xl-ai", async () => {
125
+ const { useBlockNoteEditor, __requireAI } = (await import("@blocknote/react")) as any;
126
+ return {
127
+ AIExtension: h.AIExtension,
128
+ AIMenuController: () => {
129
+ __requireAI(useBlockNoteEditor());
130
+ return <div data-testid="ai-menu" />;
131
+ },
132
+ AIToolbarButton: () => {
133
+ __requireAI(useBlockNoteEditor());
134
+ return <div data-testid="ai-toolbar-button" />;
135
+ },
136
+ getAISlashMenuItems: () => [],
137
+ getDefaultAIMenuItems: () => [],
138
+ PromptSuggestionMenu: () => null,
139
+ useAIDictionary: () => ({}),
140
+ };
141
+ });
142
+
143
+ vi.mock("ai", () => ({
144
+ DefaultChatTransport: function (this: any, config: unknown) {
145
+ h.transportConfig = config;
146
+ },
147
+ }));
148
+ vi.mock("@blocknote/core", () => ({
149
+ BlockNoteSchema: { create: (config: unknown) => ({ config }) },
150
+ defaultInlineContentSpecs: {},
151
+ filterSuggestionItems: (items: unknown[]) => items,
152
+ }));
153
+ vi.mock("@blocknote/core/locales", () => ({ en: { core: "en" } }));
154
+ vi.mock("@blocknote/xl-ai/locales", () => ({ en: { ai: "en" } }));
155
+ vi.mock("@blocknote/shadcn/style.css", () => ({}));
156
+ vi.mock("@blocknote/xl-ai/style.css", () => ({}));
157
+ vi.mock("../BlockNoteEditorMentionInlineContent", () => ({ createMentionInlineContentSpec: () => ({}) }));
158
+
159
+ import BlockNoteEditor from "../BlockNoteEditor";
160
+
161
+ const emptyParagraph = () => [{ id: "p0", type: "paragraph", props: {}, content: [], children: [] }];
162
+ const textParagraph = (text: string) => [
163
+ { id: "p1", type: "paragraph", props: {}, content: [{ type: "text", text, styles: {} }], children: [] },
164
+ ];
165
+
166
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
167
+
168
+ async function emit(document: any[]): Promise<void> {
169
+ h.editor.document = document;
170
+ await act(async () => {
171
+ h.viewOnChange?.();
172
+ await flush();
173
+ });
174
+ }
175
+
176
+ describe("BlockNoteEditor change reports", () => {
177
+ const onChange = vi.fn();
178
+
179
+ beforeEach(() => {
180
+ onChange.mockReset();
181
+ h.viewOnChange = undefined;
182
+ });
183
+
184
+ it("does not report the change an empty editor emits when it mounts (empty to still empty)", async () => {
185
+ render(<BlockNoteEditor id="npc-1" type="npcs" onChange={onChange} />);
186
+ await emit(emptyParagraph());
187
+ expect(onChange).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it("reports typing into an empty editor, then clearing it again", async () => {
191
+ render(<BlockNoteEditor id="npc-1" type="npcs" onChange={onChange} />);
192
+ await emit(textParagraph("Hello"));
193
+ expect(onChange).toHaveBeenLastCalledWith(textParagraph("Hello"), false, false);
194
+ await emit(emptyParagraph());
195
+ expect(onChange).toHaveBeenLastCalledWith(emptyParagraph(), true, false);
196
+ expect(onChange).toHaveBeenCalledTimes(2);
197
+ });
198
+
199
+ it("reports clearing an editor that started with text, even as its first change", async () => {
200
+ render(<BlockNoteEditor id="npc-1" type="npcs" initialContent={textParagraph("Old") as any} onChange={onChange} />);
201
+ await emit(emptyParagraph());
202
+ expect(onChange).toHaveBeenCalledTimes(1);
203
+ expect(onChange).toHaveBeenLastCalledWith(emptyParagraph(), true, false);
204
+ });
205
+ });
@@ -3,6 +3,8 @@
3
3
  import React, { createContext, useContext, useEffect, useState } from "react";
4
4
  import { NotificationInterface } from "../features/notification/data";
5
5
  import { useSocket } from "../hooks";
6
+ import { isOfflineConfigured } from "../offline/config";
7
+ import { useOfflineState } from "../offline/state-hooks";
6
8
  import type { SessionSnapshot } from "../server/ServerSession";
7
9
 
8
10
  interface SocketContextProps {
@@ -62,8 +64,12 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ token, session,
62
64
  }, [session]);
63
65
 
64
66
  const resolvedToken = token ?? sessionToken;
67
+ // Offline mode (opt-in): the socket stops retrying while the session is offline and connects
68
+ // again once it is back online. Without offline mode the state stays online: never paused.
69
+ const offlineState = useOfflineState();
70
+ const paused = isOfflineConfigured() && !offlineState.online;
65
71
  // Called unconditionally: useSocket returns early while the token is falsy.
66
- const socketHookData = useSocket({ token: resolvedToken as string });
72
+ const socketHookData = useSocket({ token: resolvedToken as string, paused });
67
73
 
68
74
  const sendMessage = (event: string, data: any) => {
69
75
  const currentSocket = socketHookData ? socketHookData.socket : defaultContextValue.socket;