@carlonicora/nextjs-jsonapi 6.13.0 → 6.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/{BlockNoteEditor-2VLPSEKS.js → BlockNoteEditor-CUNH4LSQ.js} +25 -25
  2. package/dist/{BlockNoteEditor-2VLPSEKS.js.map → BlockNoteEditor-CUNH4LSQ.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-22Q4DJBU.mjs → BlockNoteEditor-QIVV2OB4.mjs} +10 -10
  4. package/dist/{BlockNoteViewer-2XYASA5Y.js → BlockNoteViewer-APBOPT4B.js} +8 -8
  5. package/dist/{BlockNoteViewer-2XYASA5Y.js.map → BlockNoteViewer-APBOPT4B.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-NBW2H6LC.mjs → BlockNoteViewer-HIUAJQRI.mjs} +7 -7
  7. package/dist/JsonApiRequest-G4J2WTSU.js +26 -0
  8. package/dist/{JsonApiRequest-WISJZSDE.js.map → JsonApiRequest-G4J2WTSU.js.map} +1 -1
  9. package/dist/{JsonApiRequest-F6TMJ42G.mjs → JsonApiRequest-IKKHWCPB.mjs} +2 -2
  10. package/dist/billing/index.js +497 -497
  11. package/dist/billing/index.mjs +9 -9
  12. package/dist/{chunk-7DBJTNSU.mjs → chunk-4RPGFQBA.mjs} +2 -2
  13. package/dist/{chunk-BZ5J2FIV.js → chunk-4XLDBOSE.js} +11 -11
  14. package/dist/{chunk-BZ5J2FIV.js.map → chunk-4XLDBOSE.js.map} +1 -1
  15. package/dist/{chunk-27LQDGNP.mjs → chunk-4ZJIUTIV.mjs} +6 -1
  16. package/dist/chunk-4ZJIUTIV.mjs.map +1 -0
  17. package/dist/{chunk-PXDUJ47N.js → chunk-6RCZ64BQ.js} +18 -18
  18. package/dist/{chunk-PXDUJ47N.js.map → chunk-6RCZ64BQ.js.map} +1 -1
  19. package/dist/{chunk-7JZHPMGW.mjs → chunk-A3RB47X4.mjs} +44 -20
  20. package/dist/chunk-A3RB47X4.mjs.map +1 -0
  21. package/dist/{chunk-DOCIFE6A.mjs → chunk-AUD6ZLBB.mjs} +3 -3
  22. package/dist/{chunk-GB6DBFYY.js → chunk-DGN3PR2F.js} +3 -3
  23. package/dist/{chunk-GB6DBFYY.js.map → chunk-DGN3PR2F.js.map} +1 -1
  24. package/dist/{chunk-7ZHTPFFT.js → chunk-DJXSCARG.js} +15 -15
  25. package/dist/{chunk-7ZHTPFFT.js.map → chunk-DJXSCARG.js.map} +1 -1
  26. package/dist/{chunk-QKNGZDSQ.js → chunk-GM4VZTWL.js} +2 -2
  27. package/dist/{chunk-QKNGZDSQ.js.map → chunk-GM4VZTWL.js.map} +1 -1
  28. package/dist/{chunk-2H342HNN.js → chunk-GSRTVIIK.js} +1184 -1178
  29. package/dist/chunk-GSRTVIIK.js.map +1 -0
  30. package/dist/{chunk-47BBY5KA.mjs → chunk-HAW5PNJG.mjs} +4 -4
  31. package/dist/{chunk-YRKIFYOE.mjs → chunk-KTGKW72G.mjs} +27 -8
  32. package/dist/chunk-KTGKW72G.mjs.map +1 -0
  33. package/dist/{chunk-PV4Z5W6V.js → chunk-KV4N7DEG.js} +19 -19
  34. package/dist/{chunk-PV4Z5W6V.js.map → chunk-KV4N7DEG.js.map} +1 -1
  35. package/dist/{chunk-ZLTOUZN2.mjs → chunk-M3N7LPHX.mjs} +4 -4
  36. package/dist/{chunk-DGWOVYEC.mjs → chunk-MQ7TXALH.mjs} +2 -2
  37. package/dist/{chunk-W62MHDWM.js → chunk-N5ZE2Z6I.js} +58 -34
  38. package/dist/chunk-N5ZE2Z6I.js.map +1 -0
  39. package/dist/{chunk-2SV4SS6N.js → chunk-OQQ7CZWT.js} +12 -12
  40. package/dist/{chunk-2SV4SS6N.js.map → chunk-OQQ7CZWT.js.map} +1 -1
  41. package/dist/{chunk-JJMMT24D.mjs → chunk-OUEYZOLB.mjs} +2 -2
  42. package/dist/{chunk-XXP3TQHF.mjs → chunk-OY65RYD3.mjs} +2 -2
  43. package/dist/{chunk-MOGEGWXW.js → chunk-PMESKNKL.js} +868 -868
  44. package/dist/{chunk-MOGEGWXW.js.map → chunk-PMESKNKL.js.map} +1 -1
  45. package/dist/{chunk-B6JGKDP2.js → chunk-QGVVF77L.js} +48 -29
  46. package/dist/chunk-QGVVF77L.js.map +1 -0
  47. package/dist/{chunk-BH6JWIKI.mjs → chunk-QIEF7HIL.mjs} +2 -2
  48. package/dist/{chunk-P445QGOC.mjs → chunk-RFKSJIQB.mjs} +7 -7
  49. package/dist/{chunk-6JUCIWDC.mjs → chunk-UOTHRJD2.mjs} +2 -2
  50. package/dist/{chunk-KMW7UNL2.js → chunk-WCEYT3GG.js} +8 -8
  51. package/dist/{chunk-KMW7UNL2.js.map → chunk-WCEYT3GG.js.map} +1 -1
  52. package/dist/{chunk-MHB36MYE.js → chunk-WUNSG3IN.js} +7 -2
  53. package/dist/chunk-WUNSG3IN.js.map +1 -0
  54. package/dist/{chunk-H4NLDX5G.mjs → chunk-YKNULEHB.mjs} +19 -13
  55. package/dist/chunk-YKNULEHB.mjs.map +1 -0
  56. package/dist/{chunk-Z4POQWFG.js → chunk-ZYSNJKOP.js} +4 -4
  57. package/dist/{chunk-Z4POQWFG.js.map → chunk-ZYSNJKOP.js.map} +1 -1
  58. package/dist/client/index.js +10 -10
  59. package/dist/client/index.mjs +9 -9
  60. package/dist/components/index.js +10 -10
  61. package/dist/components/index.mjs +9 -9
  62. package/dist/contexts/index.js +10 -10
  63. package/dist/contexts/index.mjs +9 -9
  64. package/dist/core/index.js +7 -7
  65. package/dist/core/index.mjs +6 -6
  66. package/dist/email-automations/index.js +99 -99
  67. package/dist/email-automations/index.mjs +9 -9
  68. package/dist/features/analytics/index.js +126 -126
  69. package/dist/features/analytics/index.mjs +9 -9
  70. package/dist/features/analytics/server.js +10 -10
  71. package/dist/features/analytics/server.mjs +8 -8
  72. package/dist/features/blog/index.js +156 -156
  73. package/dist/features/blog/index.mjs +9 -9
  74. package/dist/features/blog/server.js +28 -28
  75. package/dist/features/blog/server.mjs +7 -7
  76. package/dist/features/help/index.js +43 -43
  77. package/dist/features/help/index.mjs +9 -9
  78. package/dist/features/tokenusage/index.js +132 -132
  79. package/dist/features/tokenusage/index.mjs +9 -9
  80. package/dist/index.js +8 -8
  81. package/dist/index.mjs +7 -7
  82. package/dist/{lifecycle-LKGPRBS7.mjs → lifecycle-KI3PCJSE.mjs} +3 -3
  83. package/dist/lifecycle-TO65I7TO.js +23 -0
  84. package/dist/{lifecycle-4GUOTB46.js.map → lifecycle-TO65I7TO.js.map} +1 -1
  85. package/dist/local-transport-4YDJ6FQG.js +15 -0
  86. package/dist/{local-transport-7H3SPZSA.js.map → local-transport-4YDJ6FQG.js.map} +1 -1
  87. package/dist/local-transport-C7YTA4I7.mjs +15 -0
  88. package/dist/offline/index.d.mts +160 -4
  89. package/dist/offline/index.d.ts +160 -4
  90. package/dist/offline/index.js +210 -163
  91. package/dist/offline/index.js.map +1 -1
  92. package/dist/offline/index.mjs +76 -29
  93. package/dist/offline/index.mjs.map +1 -1
  94. package/dist/request-66FGJIHB.mjs +10 -0
  95. package/dist/request-6W6UGKJV.js +10 -0
  96. package/dist/{request-KTBV6HHH.js.map → request-6W6UGKJV.js.map} +1 -1
  97. package/dist/server/index.js +9 -9
  98. package/dist/server/index.mjs +8 -8
  99. package/dist/write-through-BAPS24GL.js +15 -0
  100. package/dist/{write-through-FDQ7N4YI.js.map → write-through-BAPS24GL.js.map} +1 -1
  101. package/dist/write-through-IWQKXYHE.mjs +15 -0
  102. package/package.json +1 -1
  103. package/src/features/onboarding/contexts/OnboardingContext.spec.tsx +42 -1
  104. package/src/features/onboarding/contexts/OnboardingContext.tsx +11 -1
  105. package/src/offline/__tests__/generic-answers.spec.ts +146 -3
  106. package/src/offline/__tests__/local-transport.spec.ts +15 -1
  107. package/src/offline/__tests__/offline-fixtures.ts +29 -2
  108. package/src/offline/__tests__/write-through.spec.ts +27 -1
  109. package/src/offline/config.ts +9 -0
  110. package/src/offline/generic-answers.ts +44 -17
  111. package/src/offline/index.ts +2 -0
  112. package/src/offline/local-transport.ts +5 -3
  113. package/src/offline/sync/__tests__/runtime.spec.ts +30 -1
  114. package/src/offline/sync/__tests__/sync-fixtures.ts +22 -0
  115. package/src/offline/sync/__tests__/sync-root.spec.ts +184 -1
  116. package/src/offline/sync/known-roots.ts +29 -7
  117. package/src/offline/sync/runtime.ts +3 -2
  118. package/src/offline/sync/sync-root.ts +70 -19
  119. package/src/offline/types.ts +39 -1
  120. package/src/offline/write-through.ts +6 -3
  121. package/dist/JsonApiRequest-WISJZSDE.js +0 -26
  122. package/dist/chunk-27LQDGNP.mjs.map +0 -1
  123. package/dist/chunk-2H342HNN.js.map +0 -1
  124. package/dist/chunk-7JZHPMGW.mjs.map +0 -1
  125. package/dist/chunk-B6JGKDP2.js.map +0 -1
  126. package/dist/chunk-H4NLDX5G.mjs.map +0 -1
  127. package/dist/chunk-MHB36MYE.js.map +0 -1
  128. package/dist/chunk-W62MHDWM.js.map +0 -1
  129. package/dist/chunk-YRKIFYOE.mjs.map +0 -1
  130. package/dist/lifecycle-4GUOTB46.js +0 -23
  131. package/dist/local-transport-7H3SPZSA.js +0 -15
  132. package/dist/local-transport-QYFKA6AN.mjs +0 -15
  133. package/dist/request-KTBV6HHH.js +0 -10
  134. package/dist/request-SQOUNWZ4.mjs +0 -10
  135. package/dist/write-through-FDQ7N4YI.js +0 -15
  136. package/dist/write-through-R25VQMH6.mjs +0 -15
  137. /package/dist/{BlockNoteEditor-22Q4DJBU.mjs.map → BlockNoteEditor-QIVV2OB4.mjs.map} +0 -0
  138. /package/dist/{BlockNoteViewer-NBW2H6LC.mjs.map → BlockNoteViewer-HIUAJQRI.mjs.map} +0 -0
  139. /package/dist/{JsonApiRequest-F6TMJ42G.mjs.map → JsonApiRequest-IKKHWCPB.mjs.map} +0 -0
  140. /package/dist/{chunk-7DBJTNSU.mjs.map → chunk-4RPGFQBA.mjs.map} +0 -0
  141. /package/dist/{chunk-DOCIFE6A.mjs.map → chunk-AUD6ZLBB.mjs.map} +0 -0
  142. /package/dist/{chunk-47BBY5KA.mjs.map → chunk-HAW5PNJG.mjs.map} +0 -0
  143. /package/dist/{chunk-ZLTOUZN2.mjs.map → chunk-M3N7LPHX.mjs.map} +0 -0
  144. /package/dist/{chunk-DGWOVYEC.mjs.map → chunk-MQ7TXALH.mjs.map} +0 -0
  145. /package/dist/{chunk-JJMMT24D.mjs.map → chunk-OUEYZOLB.mjs.map} +0 -0
  146. /package/dist/{chunk-XXP3TQHF.mjs.map → chunk-OY65RYD3.mjs.map} +0 -0
  147. /package/dist/{chunk-BH6JWIKI.mjs.map → chunk-QIEF7HIL.mjs.map} +0 -0
  148. /package/dist/{chunk-P445QGOC.mjs.map → chunk-RFKSJIQB.mjs.map} +0 -0
  149. /package/dist/{chunk-6JUCIWDC.mjs.map → chunk-UOTHRJD2.mjs.map} +0 -0
  150. /package/dist/{lifecycle-LKGPRBS7.mjs.map → lifecycle-KI3PCJSE.mjs.map} +0 -0
  151. /package/dist/{local-transport-QYFKA6AN.mjs.map → local-transport-C7YTA4I7.mjs.map} +0 -0
  152. /package/dist/{request-SQOUNWZ4.mjs.map → request-66FGJIHB.mjs.map} +0 -0
  153. /package/dist/{write-through-R25VQMH6.mjs.map → write-through-IWQKXYHE.mjs.map} +0 -0
@@ -1,7 +1,19 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import type { DeviceStore } from "../../store/DeviceStore";
3
3
  import { RootSync } from "../models/RootSync";
4
- import { artwork, fakeCacheStorage, npc, setUpSync, syncTestConfig, tearDownSync, TestModel } from "./sync-fixtures";
4
+ import {
5
+ artwork,
6
+ book,
7
+ bookTypes,
8
+ Books,
9
+ fakeCacheStorage,
10
+ npc,
11
+ page,
12
+ setUpSync,
13
+ syncTestConfig,
14
+ tearDownSync,
15
+ TestModel,
16
+ } from "./sync-fixtures";
5
17
 
6
18
  const { JsonApiGet } = vi.hoisted(() => ({ JsonApiGet: vi.fn() }));
7
19
  vi.mock("../../../unified/JsonApiRequest", () => ({
@@ -724,4 +736,175 @@ describe("syncAll", () => {
724
736
  now.mockRestore();
725
737
  localStorage.clear();
726
738
  });
739
+
740
+ describe("extra root kinds", () => {
741
+ const bookSetUp = (
742
+ kind: Partial<{ listSyncRoots: () => Promise<string[]>; typeEndpoints: any; syncModule: any }> = {},
743
+ primaryRoots: () => Promise<string[]> = async () => [],
744
+ ) =>
745
+ setUpSync({
746
+ listSyncRoots: primaryRoots,
747
+ types: [...syncTestConfig().types, ...bookTypes],
748
+ extraRootKinds: [{ module: Books, listSyncRoots: async () => ["b1"], ...kind }],
749
+ });
750
+
751
+ it("syncAll syncs extra-kind roots from {module}/:rootId/sync", async () => {
752
+ tearDownSync();
753
+ store = await bookSetUp();
754
+ serve({
755
+ "books/b1/sync": syncAnswer({ entries: [entry("books", "b1"), entry("pages", "p1")] }, "b1"),
756
+ "books/b1": oneAnswer(book("b1")),
757
+ "books/b1/pages?fetchAll=true": listAnswer([page("p1", "First")]),
758
+ });
759
+
760
+ await syncAll();
761
+
762
+ expect(JsonApiGet.mock.calls.map((call) => call[0].endpoint)).toEqual([
763
+ "books/b1/sync",
764
+ "books/b1",
765
+ "books/b1/pages?fetchAll=true",
766
+ ]);
767
+ expect(await store.getRecord("pages", "p1")).toMatchObject({ rootId: "b1" });
768
+ expect(await store.getRecord("books", "b1")).toMatchObject({ rootId: "b1" });
769
+ });
770
+
771
+ it("syncAll keeps an extra-kind root when a campaign root is removed", async () => {
772
+ tearDownSync();
773
+ let primary = ["C"];
774
+ store = await bookSetUp({}, async () => primary);
775
+ const at = new Date("2026-10-31T10:00:00.000Z");
776
+ await store.putRecord({
777
+ type: "roots",
778
+ id: "C",
779
+ rootId: "C",
780
+ resource: { type: "roots", id: "C" },
781
+ updatedAt: at,
782
+ });
783
+ await store.putRecord({ type: "npcs", id: "n1", rootId: "C", resource: npc("n1", "N"), updatedAt: at });
784
+ await store.putRecord({ type: "books", id: "b1", rootId: "b1", resource: book("b1"), updatedAt: at });
785
+ await store.putRecord({ type: "pages", id: "p1", rootId: "b1", resource: page("p1", "P"), updatedAt: at });
786
+ await store.setSyncState("b1", {
787
+ serverTime: new Date("2026-11-01T00:00:00.000Z"),
788
+ locked: false,
789
+ lockedFrom: null,
790
+ });
791
+ serve({
792
+ "books/b1/sync": syncAnswer({ entries: [entry("books", "b1"), entry("pages", "p1")] }, "b1"),
793
+ });
794
+
795
+ primary = [];
796
+ await syncAll();
797
+
798
+ expect(await store.getRecord("roots", "C")).toBeUndefined();
799
+ expect(await store.getRecord("npcs", "n1")).toBeUndefined();
800
+ expect(await store.getRecord("books", "b1")).toBeDefined();
801
+ expect(await store.getRecord("pages", "p1")).toBeDefined();
802
+ });
803
+
804
+ it("typeEndpoints replace the type's fetch under that kind and get the stored root record", async () => {
805
+ tearDownSync();
806
+ store = await bookSetUp({
807
+ typeEndpoints: [
808
+ {
809
+ module: { name: "npcs", model: TestModel },
810
+ endpoint: (root: { id: string; resource: any }) =>
811
+ `roots/${root.resource.attributes.parent}/npcs/as-participant`,
812
+ },
813
+ ],
814
+ });
815
+ await store.setSyncState("b1", {
816
+ serverTime: new Date("2026-10-30T00:00:00.000Z"),
817
+ locked: false,
818
+ lockedFrom: null,
819
+ });
820
+ serve({
821
+ "books/b1/sync": syncAnswer({ entries: [entry("books", "b1"), entry("npcs", "n1")] }, "b1"),
822
+ "books/b1": oneAnswer(book("b1", { parent: "R9" })),
823
+ "roots/R9/npcs/as-participant": listAnswer([npc("n1", "Alda", "R9"), npc("other", "Other", "R9")]),
824
+ });
825
+
826
+ await syncAll();
827
+
828
+ expect(JsonApiGet.mock.calls.map((call) => call[0].endpoint)).toEqual([
829
+ "books/b1/sync",
830
+ "books/b1",
831
+ "roots/R9/npcs/as-participant",
832
+ ]);
833
+ expect(await store.getRecord("npcs", "n1")).toMatchObject({ rootId: "b1" });
834
+ expect(await store.getRecord("npcs", "other")).toBeUndefined();
835
+ });
836
+
837
+ it("typeEndpoints build from the stored root record when the root record did not change", async () => {
838
+ tearDownSync();
839
+ store = await bookSetUp({
840
+ typeEndpoints: [
841
+ {
842
+ module: { name: "npcs", model: TestModel },
843
+ endpoint: (root: { id: string; resource: any }) =>
844
+ `roots/${root.resource.attributes.parent}/npcs/as-participant`,
845
+ },
846
+ ],
847
+ });
848
+ await store.putRecord({
849
+ type: "books",
850
+ id: "b1",
851
+ rootId: "b1",
852
+ resource: book("b1", { parent: "R7" }),
853
+ updatedAt: new Date("2026-10-20T00:00:00.000Z"),
854
+ });
855
+ await store.setSyncState("b1", {
856
+ serverTime: new Date("2026-10-30T00:00:00.000Z"),
857
+ locked: false,
858
+ lockedFrom: null,
859
+ });
860
+ serve({
861
+ "books/b1/sync": syncAnswer(
862
+ { entries: [entry("books", "b1", "2026-10-20T00:00:00.000Z"), entry("npcs", "n1")] },
863
+ "b1",
864
+ ),
865
+ "roots/R7/npcs/as-participant": listAnswer([npc("n1", "Alda", "R7")]),
866
+ });
867
+
868
+ await syncAll();
869
+
870
+ expect(JsonApiGet.mock.calls.map((call) => call[0].endpoint)).toEqual([
871
+ "books/b1/sync",
872
+ "roots/R7/npcs/as-participant",
873
+ ]);
874
+ expect(await store.getRecord("npcs", "n1")).toMatchObject({ rootId: "b1" });
875
+ });
876
+
877
+ it("rootCount counts primary roots only; progress total counts all", async () => {
878
+ tearDownSync();
879
+ store = await bookSetUp({ listSyncRoots: async () => ["b1", "b2"] }, async () => ["C"]);
880
+ serve({
881
+ "roots/C/sync": syncAnswer({ entries: [] }),
882
+ "books/b1/sync": syncAnswer({ entries: [] }, "b1"),
883
+ "books/b2/sync": syncAnswer({ entries: [] }, "b2"),
884
+ });
885
+ const totals = new Set<number>();
886
+ const unsubscribe = subscribeOfflineState((state) => {
887
+ if (state.syncProgress) totals.add(state.syncProgress.total);
888
+ });
889
+
890
+ await syncAll();
891
+ unsubscribe();
892
+
893
+ expect([...totals]).toEqual([3]);
894
+ expect(getOfflineState().rootCount).toBe(1);
895
+ localStorage.clear();
896
+ });
897
+
898
+ it("an extra kind with syncModule reads its sync answer through that module", async () => {
899
+ tearDownSync();
900
+ const BookSyncs = { name: "book-syncs", model: RootSync };
901
+ store = await bookSetUp({ syncModule: BookSyncs });
902
+ serve({ "books/b1/sync": syncAnswer({ entries: [] }, "b1") });
903
+
904
+ await syncAll();
905
+
906
+ expect(JsonApiGet.mock.calls[0][0]).toMatchObject({ endpoint: "books/b1/sync", classKey: BookSyncs });
907
+ localStorage.clear();
908
+ });
909
+ });
727
910
  });
@@ -1,28 +1,50 @@
1
- import { getOfflineConfig, getOfflineStore } from "../config";
1
+ import { getOfflineConfig, getOfflineStore, rootKinds } from "../config";
2
2
  import { setOfflineState } from "../state";
3
3
  import type { DeviceStore } from "../store/DeviceStore";
4
4
  import { isRootLocked } from "../store/lifecycle";
5
5
 
6
6
  let _lastSyncRoots: string[] = [];
7
+ let _lastExtraSyncRoots: string[] = [];
7
8
 
8
- /** Remembers the root records the last online `listSyncRoots()` returned. */
9
- export function rememberSyncRoots(ids: string[]): void {
9
+ /**
10
+ * Remembers the root records the last online sync listed: `ids` of every kind, `extraIds` those
11
+ * of the `extraRootKinds` among them.
12
+ */
13
+ export function rememberSyncRoots(ids: string[], extraIds: string[] = []): void {
10
14
  _lastSyncRoots = [...ids];
15
+ _lastExtraSyncRoots = [...extraIds];
11
16
  }
12
17
 
13
18
  /**
14
- * Every root record the device knows of: its stored root records, the root records with waiting
15
- * changes, and the last sync roots (a locked root record is never downloaded, so it may have
16
- * no stored record).
19
+ * Every root record the device knows of, of every kind: its stored root records, the root records
20
+ * with waiting changes, and the last sync roots (a locked root record is never downloaded, so it
21
+ * may have no stored record).
17
22
  */
18
23
  export async function knownRootIds(store: DeviceStore): Promise<string[]> {
19
24
  const config = getOfflineConfig();
20
25
  const ids = new Set<string>(_lastSyncRoots);
21
- for (const record of await store.listRecords({ type: config.rootModule.name })) ids.add(record.id);
26
+ for (const kind of rootKinds(config)) {
27
+ for (const record of await store.listRecords({ type: kind.module.name })) ids.add(record.id);
28
+ }
22
29
  for (const entry of await store.queue()) ids.add(entry.rootId);
23
30
  return [...ids].sort();
24
31
  }
25
32
 
33
+ /** The known root records of the `extraRootKinds`: stored, or listed by the last sync. */
34
+ async function extraRootIds(store: DeviceStore): Promise<Set<string>> {
35
+ const ids = new Set<string>(_lastExtraSyncRoots);
36
+ for (const kind of getOfflineConfig().extraRootKinds ?? []) {
37
+ for (const record of await store.listRecords({ type: kind.module.name })) ids.add(record.id);
38
+ }
39
+ return ids;
40
+ }
41
+
42
+ /** `knownRootIds` without the root records of the `extraRootKinds` (the primary kind only). */
43
+ export async function knownPrimaryRootIds(store: DeviceStore): Promise<string[]> {
44
+ const extra = await extraRootIds(store);
45
+ return (await knownRootIds(store)).filter((id) => !extra.has(id));
46
+ }
47
+
26
48
  /** Recomputes `lockedRootIds` from the stored sync states, now. */
27
49
  export async function refreshLockedRoots(): Promise<void> {
28
50
  const config = getOfflineConfig();
@@ -14,7 +14,7 @@ import { getOfflineState, isReconnecting, setOfflineState, setReconnecting, subs
14
14
  import { OFFLINE_REPLAY_HEADER } from "../types";
15
15
  import { refreshOfflineCounts } from "./counts";
16
16
  import { drainQueue } from "./drain-queue";
17
- import { knownRootIds, refreshLockedRoots } from "./known-roots";
17
+ import { knownPrimaryRootIds, refreshLockedRoots } from "./known-roots";
18
18
  import { ROOT_SYNC_ENDPOINT } from "./models/RootSync";
19
19
  import { currentLocale } from "./session-refresh";
20
20
  import { syncAll } from "./sync-root";
@@ -32,7 +32,8 @@ const PROBE_INTERVAL_MS = 30_000;
32
32
  async function probeApi(): Promise<boolean> {
33
33
  const config = getOfflineConfig();
34
34
  const store = await getOfflineStore();
35
- const [rootId] = await knownRootIds(store);
35
+ // A root record of the primary kind only (the extra kinds' roots answer elsewhere).
36
+ const [rootId] = await knownPrimaryRootIds(store);
36
37
  const endpoint = rootId
37
38
  ? new EndpointCreator({ endpoint: config.rootModule, id: rootId, childEndpoint: ROOT_SYNC_ENDPOINT })
38
39
  : new EndpointCreator({ endpoint: config.rootModule }).addAdditionalParam("page[size]", "1");
@@ -2,9 +2,10 @@ import { EndpointCreator } from "../../core/endpoint/EndpointCreator";
2
2
  import { ApiRequestDataTypeInterface } from "../../core/interfaces/ApiRequestDataTypeInterface";
3
3
  import { Modules } from "../../core/registry/ModuleRegistry";
4
4
  import { JsonApiGet } from "../../unified/JsonApiRequest";
5
- import { getOfflineConfig, getOfflineStore, isOfflineConfigured, writeOfflineLastSync } from "../config";
5
+ import { getOfflineConfig, getOfflineStore, isOfflineConfigured, rootKinds, writeOfflineLastSync } from "../config";
6
6
  import { canReachApi, setOfflineState } from "../state";
7
7
  import type { DeviceStore, SkippedRecord, StoredRecord } from "../store/DeviceStore";
8
+ import type { OfflineRootKind } from "../types";
8
9
  import { cacheArtworkImage, imageUrlOf, removeArtworkImage } from "./artwork-cache";
9
10
  import { knownRootIds, refreshLockedRoots, rememberSyncRoots } from "./known-roots";
10
11
  import { ROOT_SYNC_ENDPOINT, RootSync } from "./models/RootSync";
@@ -75,18 +76,19 @@ async function removeRootIfIdle(store: DeviceStore, rootId: string): Promise<voi
75
76
  * are replaced, and artwork images are downloaded. The sync state is stored only once every
76
77
  * fetch succeeded (or answered 403/404). A record with a waiting change is never overwritten.
77
78
  */
78
- export async function syncRoot(rootId: string): Promise<void> {
79
+ export async function syncRoot(rootId: string, kind?: OfflineRootKind): Promise<void> {
79
80
  const config = getOfflineConfig();
81
+ const rootKind = kind ?? rootKinds(config)[0];
80
82
  const store = await getOfflineStore();
81
83
  const previous = await store.getSyncState(rootId);
82
84
 
83
85
  const syncEndpoint = new EndpointCreator({
84
- endpoint: config.rootModule,
86
+ endpoint: rootKind.module,
85
87
  id: rootId,
86
88
  childEndpoint: ROOT_SYNC_ENDPOINT,
87
89
  }).generate();
88
90
  const syncResponse = await JsonApiGet({
89
- classKey: Modules.RootSync,
91
+ classKey: rootKind.syncModule ?? Modules.RootSync,
90
92
  endpoint: syncEndpoint,
91
93
  language: currentLocale(),
92
94
  });
@@ -146,16 +148,35 @@ export async function syncRoot(rootId: string): Promise<void> {
146
148
  // Records the answers include whose type the device does not sync itself (the root record's
147
149
  // owner): kept for display only, so a local answer can include them as the server does.
148
150
  const included: any[] = [];
149
- for (const typeConfig of config.types) {
151
+ // The root record of the kind is fetched before the other types: a kind's `typeEndpoints` are
152
+ // built from it.
153
+ const rootTypeName = rootKind.module.name;
154
+ const orderedTypes = [
155
+ ...config.types.filter((typeConfig) => typeConfig.module.name === rootTypeName),
156
+ ...config.types.filter((typeConfig) => typeConfig.module.name !== rootTypeName),
157
+ ];
158
+ const rootResource = async (): Promise<StoredRecord["resource"]> => {
159
+ const fresh = fetched.find((resource) => resource?.type === rootTypeName && resource?.id === rootId);
160
+ if (fresh) return fresh;
161
+ const storedRoot = await store.getRecord(rootTypeName, rootId);
162
+ if (storedRoot) return storedRoot.resource;
163
+ throw new Error(`[offline] ${rootTypeName} ${rootId}: the root record is needed to build the type endpoints`);
164
+ };
165
+ for (const typeConfig of orderedTypes) {
150
166
  const changedOfType = changed.filter((entry) => entry.type === typeConfig.module.name);
151
167
  if (changedOfType.length === 0) continue;
152
168
 
153
- if (typeConfig.fetch === "root-list") {
169
+ const typeEndpoint = rootKind.typeEndpoints?.find((candidate) => candidate.module.name === typeConfig.module.name);
170
+ if (typeEndpoint) {
171
+ // One full list read under this kind, never with `updatedSince`.
172
+ const endpoint = typeEndpoint.endpoint({ id: rootId, resource: await rootResource() });
173
+ fetched.push(...(await fetchResources(typeConfig.module, endpoint, skipped, included)));
174
+ } else if (typeConfig.fetch === "root-list") {
154
175
  // One list read per type. With `updatedSince` when every changed record is inside the
155
176
  // window; without it when a record older than the window is missing or stale on the
156
177
  // device, so the whole list comes in the same single request (never one read per record).
157
178
  const endpoint = new EndpointCreator({
158
- endpoint: config.rootModule,
179
+ endpoint: rootKind.module,
159
180
  id: rootId,
160
181
  childEndpoint: typeConfig.module,
161
182
  }).addAdditionalParam("fetchAll", "true");
@@ -284,31 +305,61 @@ export async function syncRoot(rootId: string): Promise<void> {
284
305
  }
285
306
 
286
307
  /**
287
- * Syncs every root record `listSyncRoots()` returns, then removes from the device the root records
288
- * that are no longer roots (unless they still have waiting changes). Does nothing offline.
308
+ * Syncs every root record of every kind (`listSyncRoots()` of the primary kind and of each
309
+ * `extraRootKinds` entry), then removes from the device the known root records no kind lists
310
+ * (unless they still have waiting changes). A kind whose list throws keeps its device copy
311
+ * unchanged; the primary kind's list throwing stops the sync. Does nothing offline.
289
312
  */
290
313
  export async function syncAll(): Promise<void> {
291
314
  if (!isOfflineConfigured()) return;
292
315
  const config = getOfflineConfig();
293
316
  if (!config.getUserId() || !canReachApi()) return;
294
317
 
295
- let roots: string[];
318
+ const [primaryKind, ...extraKinds] = rootKinds(config);
319
+ let primaryRoots: string[];
296
320
  try {
297
- roots = await config.listSyncRoots();
321
+ primaryRoots = await primaryKind.listSyncRoots();
298
322
  } catch (error) {
299
323
  console.warn("[offline] could not list the root records to sync", error);
300
324
  return;
301
325
  }
326
+ const work: { rootId: string; kind: OfflineRootKind }[] = primaryRoots.map((rootId) => ({
327
+ rootId,
328
+ kind: primaryKind,
329
+ }));
330
+ const extraRoots: string[] = [];
331
+ // Kinds whose list failed: their stored roots are kept as they are.
332
+ const unlistedKinds: OfflineRootKind[] = [];
333
+ for (const kind of extraKinds) {
334
+ try {
335
+ const ids = await kind.listSyncRoots();
336
+ extraRoots.push(...ids);
337
+ work.push(...ids.map((rootId) => ({ rootId, kind })));
338
+ } catch (error) {
339
+ console.warn(`[offline] could not list the ${kind.module.name} root records to sync`, error);
340
+ unlistedKinds.push(kind);
341
+ }
342
+ }
302
343
  if (!canReachApi()) return;
303
- rememberSyncRoots(roots);
344
+ const kept = new Set(work.map((item) => item.rootId));
345
+ if (unlistedKinds.length > 0) {
346
+ const store = await getOfflineStore();
347
+ for (const kind of unlistedKinds) {
348
+ for (const record of await store.listRecords({ type: kind.module.name })) {
349
+ kept.add(record.id);
350
+ extraRoots.push(record.id);
351
+ }
352
+ }
353
+ }
354
+ rememberSyncRoots([...kept], extraRoots);
304
355
 
305
- const total = roots.length;
356
+ const total = work.length;
306
357
  setOfflineState({ syncProgress: { done: 0, total } });
307
358
  try {
308
359
  let done = 0;
309
- for (const rootId of roots) {
360
+ for (const { rootId, kind } of work) {
310
361
  try {
311
- await syncRoot(rootId);
362
+ await syncRoot(rootId, kind);
312
363
  } catch (error) {
313
364
  console.warn(`[offline] sync of root record ${rootId} failed`, error);
314
365
  if (!canReachApi()) return;
@@ -318,16 +369,16 @@ export async function syncAll(): Promise<void> {
318
369
  }
319
370
  }
320
371
 
321
- // Every root record was tried: this counts as the device's last sync.
372
+ // Every root record was tried: this counts as the device's last sync. The root count is the
373
+ // primary kind's (shown as the number of root records, e.g. campaigns).
322
374
  const finishedAt = Date.now();
323
375
  const userId = config.getUserId();
324
376
  if (userId) writeOfflineLastSync(userId, finishedAt);
325
- setOfflineState({ lastSyncedAt: finishedAt, rootCount: total });
377
+ setOfflineState({ lastSyncedAt: finishedAt, rootCount: primaryRoots.length });
326
378
 
327
379
  const store = await getOfflineStore();
328
- const rootSet = new Set(roots);
329
380
  for (const rootId of await knownRootIds(store)) {
330
- if (!rootSet.has(rootId)) await removeRootIfIdle(store, rootId);
381
+ if (!kept.has(rootId)) await removeRootIfIdle(store, rootId);
331
382
  }
332
383
  await refreshLockedRoots();
333
384
  } finally {
@@ -1,5 +1,5 @@
1
1
  import type { ApiRequestDataTypeInterface } from "../core/interfaces/ApiRequestDataTypeInterface";
2
- import type { DeviceStore } from "./store/DeviceStore";
2
+ import type { DeviceStore, StoredRecord } from "./store/DeviceStore";
3
3
 
4
4
  /** Header of a request the server runs as an offline replay. Never answered locally. */
5
5
  export const OFFLINE_REPLAY_HEADER = "x-offline-replay";
@@ -31,6 +31,37 @@ export type OfflineLinkedListFetch = {
31
31
  endpoint: (rootId: string, target: { type: string; id: string }) => string;
32
32
  };
33
33
 
34
+ /** One more kind of root record beside `rootModule` (e.g. a notebook). Optional. */
35
+ export type OfflineRootKind = {
36
+ /** The module of the root records of this kind. Sync reads `{module}/:rootId/sync`. */
37
+ module: ApiRequestDataTypeInterface;
38
+ /** The ids of the roots of this kind to keep on the device. Throw to keep the device copy unchanged. */
39
+ listSyncRoots: () => Promise<string[]>;
40
+ /**
41
+ * Per record type, the full list endpoint (query string included) that downloads every record of
42
+ * that type under one root of this kind, built from the stored root record. Replaces the type's
43
+ * `fetch` mode under this kind only. Never given `updatedSince`.
44
+ */
45
+ typeEndpoints?: {
46
+ module: ApiRequestDataTypeInterface;
47
+ endpoint: (root: { id: string; resource: StoredRecord["resource"] }) => string;
48
+ }[];
49
+ /** The module whose model reads this kind's sync answer. Default: the library's `RootSync` module (`root-syncs`). */
50
+ syncModule?: ApiRequestDataTypeInterface;
51
+ };
52
+
53
+ type LocalWriteBase = { store: DeviceStore; module: ApiRequestDataTypeInterface; id: string }; // store = the write's transaction
54
+
55
+ /** What a generic local write (`afterLocalWrite`) did, inside its transaction. */
56
+ export type OfflineLocalWriteContext =
57
+ | (LocalWriteBase & { method: "POST"; resource: StoredRecord["resource"]; previous: null })
58
+ | (LocalWriteBase & {
59
+ method: "PUT" | "PATCH";
60
+ resource: StoredRecord["resource"];
61
+ previous: StoredRecord["resource"];
62
+ })
63
+ | (LocalWriteBase & { method: "DELETE"; resource: null; previous: StoredRecord["resource"] });
64
+
34
65
  export type OfflineTypeConfig = {
35
66
  module: ApiRequestDataTypeInterface;
36
67
  /**
@@ -53,6 +84,11 @@ export type OfflineTypeConfig = {
53
84
  searchFields?: string[];
54
85
  /** The server's order with no `orderBy` (default `updatedAt DESC`). */
55
86
  defaultOrderBy?: string;
87
+ /**
88
+ * Called by the generic POST/PUT/PATCH/DELETE local answers inside the same transaction as the
89
+ * write and the enqueue (a throw rolls both back). Never called by sync or write-through.
90
+ */
91
+ afterLocalWrite?: (ctx: OfflineLocalWriteContext) => Promise<void>;
56
92
  };
57
93
 
58
94
  export type OfflineConfig = {
@@ -68,6 +104,8 @@ export type OfflineConfig = {
68
104
  routes: OfflineRoute[];
69
105
  /** Ids of the root records to keep on the device. */
70
106
  listSyncRoots: () => Promise<string[]>;
107
+ /** More kinds of root records beside `rootModule`, each synced and kept on its own (optional). */
108
+ extraRootKinds?: OfflineRootKind[];
71
109
  shellPath: (locale: string) => string;
72
110
  syncIntervalMs: number;
73
111
  /**
@@ -1,5 +1,5 @@
1
1
  import type { ApiData } from "../core/interfaces/ApiData";
2
- import { getOfflineConfig, getOfflineStore, OfflineUnavailableError } from "./config";
2
+ import { getOfflineConfig, getOfflineStore, OfflineUnavailableError, rootKinds } from "./config";
3
3
  import { rootIdForResource, linkKindFor, linkKindsOf, linksOfKind } from "./generic-answers";
4
4
  import { splitApiUrl } from "./local-transport";
5
5
  import { matchPattern } from "./router";
@@ -158,8 +158,11 @@ export async function writeThrough(request: WriteThroughRequest, response: ApiDa
158
158
  return;
159
159
  }
160
160
 
161
- const rootParams = matchPattern([config.rootModule, ":rootId"], path.split("/").slice(0, 2).join("/"));
162
- const pathRootId = rootParams?.rootId;
161
+ // A path under a root record of any kind (`<root module>/:rootId/...`).
162
+ const rootSegments = path.split("/").slice(0, 2).join("/");
163
+ const pathRootId = rootKinds(config)
164
+ .map((kind) => matchPattern([kind.module, ":rootId"], rootSegments)?.rootId)
165
+ .find((rootId) => rootId !== undefined);
163
166
 
164
167
  const document = response.data;
165
168
  const primary = Array.isArray(document?.data) ? document.data : document?.data ? [document.data] : [];
@@ -1,26 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
- var _chunkQKNGZDSQjs = require('./chunk-QKNGZDSQ.js');
13
- require('./chunk-7QVYU63E.js');
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
- exports.JsonApiDelete = _chunkQKNGZDSQjs.JsonApiDelete; exports.JsonApiGet = _chunkQKNGZDSQjs.JsonApiGet; exports.JsonApiPatch = _chunkQKNGZDSQjs.JsonApiPatch; exports.JsonApiPost = _chunkQKNGZDSQjs.JsonApiPost; exports.JsonApiPut = _chunkQKNGZDSQjs.JsonApiPut; exports.buildUrl = _chunkQKNGZDSQjs.buildUrl; exports.configureJsonApi = _chunkQKNGZDSQjs.configureJsonApi; exports.getApiUrl = _chunkQKNGZDSQjs.getApiUrl; exports.getAppUrl = _chunkQKNGZDSQjs.getAppUrl; exports.getTrackablePages = _chunkQKNGZDSQjs.getTrackablePages;
26
- //# sourceMappingURL=JsonApiRequest-WISJZSDE.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/offline/state.ts","../src/offline/types.ts","../src/offline/config.ts"],"sourcesContent":["// Kept free of React: `directFetch` (every entry, server ones included) imports this module.\n// The hooks live in state-hooks.ts, exported only from the `./offline` entry.\n\nexport type OfflineState = {\n online: boolean;\n pending: number;\n conflicts: number;\n draining: boolean;\n lockedRootIds: string[];\n /**\n * False once this browser was found unable to keep the device copy (no secure context, no\n * private file storage, or the worker reports the OPFS APIs missing). Offline mode is then off\n * for the rest of the page: `isOfflineConfigured()` answers false and nothing offline runs.\n */\n available: boolean;\n /**\n * Offline mode is on on this device for the signed-in user. Always true once configured\n * without `OfflineConfig.requireOptIn`; otherwise the GM's per-device switch.\n */\n enabled: boolean;\n /** The running full sync: root records done out of the total, null when no sync runs. */\n syncProgress: { done: number; total: number } | null;\n /** When the last full sync on this device finished (epoch ms), null when never. */\n lastSyncedAt: number | null;\n /** How many root records the last full sync kept on this device, null when unknown. */\n rootCount: number | null;\n};\n\nexport const SERVER_SNAPSHOT: OfflineState = {\n online: true,\n pending: 0,\n conflicts: 0,\n draining: false,\n lockedRootIds: [],\n available: true,\n enabled: false,\n syncProgress: null,\n lastSyncedAt: null,\n rootCount: null,\n};\n\nlet _state: OfflineState = { ...SERVER_SNAPSHOT };\nconst _listeners = new Set<(s: OfflineState) => void>();\nlet _detachBrowserEvents: (() => void) | null = null;\n\nexport function getOfflineState(): OfflineState {\n return _state;\n}\n\nexport function subscribeOfflineState(listener: (s: OfflineState) => void): () => void {\n _listeners.add(listener);\n return () => {\n _listeners.delete(listener);\n };\n}\n\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return a === b;\n}\n\n// A reconnect attempt: a probe reached the API while the session is offline. The session stays\n// offline (spec § 4: offline \"until a sync succeeds\") while the drain and the sync run; their\n// reads and the change-set request go to the network meanwhile (`reachesNetworkWhileOffline`).\n// Any network failure (`online: false` set again) ends the attempt.\nlet _reconnecting = false;\nconst _passThroughUrls = new Set<string>();\n\n/** Whether a reconnect attempt (drain then sync, after a probe reached the API) is running. */\nexport function isReconnecting(): boolean {\n return _reconnecting;\n}\n\n/** Starts or ends a reconnect attempt (the offline runtime). */\nexport function setReconnecting(value: boolean): void {\n _reconnecting = value;\n if (!value) _passThroughUrls.clear();\n}\n\n/** Lets one write (the drain's change-set request) reach the network during a reconnect attempt. */\nexport function allowWhileReconnecting(url: string): () => void {\n _passThroughUrls.add(url);\n return () => {\n _passThroughUrls.delete(url);\n };\n}\n\n/**\n * While the session is offline, whether a request still goes to the network: only during a\n * reconnect attempt, for a GET or a write the attempt let through. Any other write stays local\n * (queued), so it is sent after the waiting changes, in order.\n */\nexport function reachesNetworkWhileOffline(method: string, url: string): boolean {\n if (!_reconnecting) return false;\n return method.toUpperCase() === \"GET\" || _passThroughUrls.has(url);\n}\n\n/** The sync and the drain may talk to the API: online, or during a reconnect attempt. */\nexport function canReachApi(): boolean {\n return _state.online || _reconnecting;\n}\n\n/** Applies a partial change. Listeners run once, and only when a value actually changed. */\nexport function setOfflineState(patch: Partial<OfflineState>): void {\n if (patch.online === false) setReconnecting(false);\n const changed = (Object.keys(patch) as (keyof OfflineState)[]).some(\n (key) => patch[key] !== undefined && !sameValue(patch[key], _state[key]),\n );\n if (!changed) return;\n\n _state = { ..._state, ...patch };\n for (const listener of [..._listeners]) listener(_state);\n}\n\n/**\n * Follows the browser's offline event. Called by `configureOffline`, never at import, so an app\n * that does not opt in gets no listener. The browser's online event alone never brings the\n * session back online (spec § 4: offline \"until a sync succeeds\"): the runtime probes the API on\n * that event and goes online only when the probe reaches it.\n */\nexport function attachBrowserOnlineEvents(): void {\n if (_detachBrowserEvents || typeof window === \"undefined\") return;\n\n if (typeof navigator !== \"undefined\" && typeof navigator.onLine === \"boolean\") {\n setOfflineState({ online: navigator.onLine });\n }\n\n const onOffline = () => setOfflineState({ online: false });\n window.addEventListener(\"offline\", onOffline);\n _detachBrowserEvents = () => {\n window.removeEventListener(\"offline\", onOffline);\n };\n}\n\n/** Test helper: back to the initial state, browser events detached, listeners kept. */\nexport function resetOfflineState(): void {\n _detachBrowserEvents?.();\n _detachBrowserEvents = null;\n setReconnecting(false);\n _state = { ...SERVER_SNAPSHOT };\n}\n","import type { ApiRequestDataTypeInterface } from \"../core/interfaces/ApiRequestDataTypeInterface\";\nimport type { DeviceStore } from \"./store/DeviceStore\";\n\n/** Header of a request the server runs as an offline replay. Never answered locally. */\nexport const OFFLINE_REPLAY_HEADER = \"x-offline-replay\";\n\nexport type OfflineMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\n/** A Module (matched by its endpoint), a \":param\" capture, or a literal segment (\"playing\", \"merge\"). */\nexport type PatternSegment = ApiRequestDataTypeInterface | string;\n\n/** `url` (beyond the contract, optional): the absolute URL the request was made to, for the `links` of a list. */\nexport type OfflineRequest = { method: OfflineMethod; path: string; query: URLSearchParams; body?: any; url?: string };\n\n/** `body` is a JSON:API document, `undefined` for a 204. */\nexport type OfflineAnswer = { status: number; body: any };\n\nexport type LocalAnswerContext = { request: OfflineRequest; params: Record<string, string>; store: DeviceStore };\n\nexport type LocalAnswer = (ctx: LocalAnswerContext) => Promise<OfflineAnswer>;\n\nexport type OfflineRoute = { method: OfflineMethod; pattern: PatternSegment[]; answer: LocalAnswer; queue: boolean };\n\n/**\n * A type the API reads only as a list per target record: each changed record's target is the\n * other end of its sync link of `linkKind` (from the record to the target), and `endpoint`\n * builds that target's list path with `EndpointCreator`.\n */\nexport type OfflineLinkedListFetch = {\n linkKind: string;\n endpoint: (rootId: string, target: { type: string; id: string }) => string;\n};\n\nexport type OfflineTypeConfig = {\n module: ApiRequestDataTypeInterface;\n /**\n * How the sync fetches the type's changed records: the root record's list (`root-list`), one\n * GET per record (`by-id`), or (`linked-list`) the list of each record's target, found through\n * the sync answer's links of `linkedList.linkKind` (e.g. player notes, read per target only).\n */\n fetch: \"root-list\" | \"by-id\" | \"linked-list\";\n /** Required with `fetch: \"linked-list\"`. */\n linkedList?: OfflineLinkedListFetch;\n readOnly: boolean;\n serverOwnedFields: string[];\n /**\n * Relationship name → Neo4j relationship type (`faction` → `MEMBER_OF`). A link the device\n * writes from a request's relationships carries the mapped type, the kind synced links carry;\n * an unmapped name is stored as the name itself.\n */\n relationshipKinds?: Record<string, string>;\n /** The string attributes the server's fulltext index covers (default: every string attribute). */\n searchFields?: string[];\n /** The server's order with no `orderBy` (default `updatedAt DESC`). */\n defaultOrderBy?: string;\n};\n\nexport type OfflineConfig = {\n getUserId: () => string | undefined;\n /**\n * Offline mode is off on each device until the user turns it on (`turnOnOffline`), stored per\n * user per device. Default (undefined/false): always on, every device keeps the copy.\n */\n requireOptIn?: boolean;\n /** The module whose records own every other record (in Wyrdli, Modules.Campaign). */\n rootModule: ApiRequestDataTypeInterface;\n types: OfflineTypeConfig[];\n routes: OfflineRoute[];\n /** Ids of the root records to keep on the device. */\n listSyncRoots: () => Promise<string[]>;\n shellPath: (locale: string) => string;\n syncIntervalMs: number;\n /**\n * Where the device worker loads SQLite from at run time (default `/offline-sqlite/index.mjs`).\n * SQLite is never bundled: its own helper worker uses a computed URL that app bundlers refuse.\n * The app serves the files of `dist/offline/sqlite/` at this URL's folder.\n */\n sqliteModuleUrl?: string;\n /**\n * Where the device-copy worker is started from (default `/offline-sqlite/device-worker.mjs`).\n * It is a standalone file in `dist/offline/sqlite/`, never bundled by the app.\n */\n deviceWorkerUrl?: string;\n};\n\nexport const DEFAULT_SQLITE_MODULE_URL = \"/offline-sqlite/index.mjs\";\nexport const DEVICE_WORKER_FILE = \"device-worker.mjs\";\nexport const DEFAULT_DEVICE_WORKER_URL = `/offline-sqlite/${DEVICE_WORKER_FILE}`;\n\n/**\n * The files of `dist/offline/sqlite/` the app serves: the device worker and the SQLite files\n * copied from @sqlite.org/sqlite-wasm.\n */\nexport const OFFLINE_SQLITE_FILES = [\n DEVICE_WORKER_FILE,\n \"index.mjs\",\n \"sqlite3.wasm\",\n \"sqlite3-opfs-async-proxy.js\",\n] as const;\n\n/** Thrown by a local answer for something that only the server can do. Answered as a 503 \"needs-connection\". */\nexport class NeedsConnectionError extends Error {\n readonly status = 503;\n readonly code = \"needs-connection\";\n\n constructor(message: string = \"Needs a connection\") {\n super(message);\n this.name = \"NeedsConnectionError\";\n }\n}\n\n/** Segments joined with \"/\", modules by their endpoint. */\nexport function patternToString(pattern: PatternSegment[]): string {\n return pattern.map((segment) => (typeof segment === \"string\" ? segment : segment.name)).join(\"/\");\n}\n","import { attachBrowserOnlineEvents, getOfflineState, setOfflineState } from \"./state\";\nimport type { DeviceStore } from \"./store/DeviceStore\";\nimport {\n DEFAULT_DEVICE_WORKER_URL,\n DEFAULT_SQLITE_MODULE_URL,\n NeedsConnectionError,\n type OfflineConfig,\n} from \"./types\";\n\n// Kept free of store, worker and React imports: `directFetch` reads `isOfflineConfigured()` on\n// every request, so this module is the only offline code an app that never opts in loads.\n\nlet _config: OfflineConfig | null = null;\nlet _opener: ((userId: string) => Promise<DeviceStore>) | null = null;\n// Registered by the `./offline` entry (configure.ts), so the store code is reachable only from it.\nlet _defaultOpener: ((userId: string) => Promise<DeviceStore>) | null = null;\nconst _stores = new Map<string, Promise<DeviceStore>>();\n// Users whose device store must not be opened again (logged out) until they sign in again.\nconst _blockedUsers = new Set<string>();\n// Per user: whether offline mode is on on this device (`requireOptIn`). Read from localStorage\n// once, since `isOfflineConfigured()` runs on every request.\nconst _enabledCache = new Map<string, boolean>();\n\nconst ENABLED_KEY_PREFIX = \"nja-offline-enabled:\";\nconst LAST_SYNC_KEY_PREFIX = \"nja-offline-last-sync:\";\n\nfunction readStorage(key: string): string | null {\n try {\n return typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeStorage(key: string, value: string | null): void {\n try {\n if (typeof localStorage === \"undefined\") return;\n if (value === null) localStorage.removeItem(key);\n else localStorage.setItem(key, value);\n } catch {\n // Storage can be blocked or full: the switch then lasts for this page only.\n }\n}\n\n/**\n * The signed-in user's device store cannot be used: it could not be opened (opfs-sahpool holds\n * one connection per origin, so a second tab cannot open it) or the user logged out. A request\n * answered from the device copy then needs a connection (503 \"needs-connection\").\n */\nexport class DeviceStoreUnavailableError extends NeedsConnectionError {\n constructor(message: string = \"The device copy is not available in this tab\") {\n super(message);\n this.name = \"DeviceStoreUnavailableError\";\n }\n}\n\n/**\n * This browser cannot keep the device copy at all: not a secure context (https or `localhost`),\n * no private file storage (`navigator.storage.getDirectory`), or the worker reports the OPFS\n * APIs missing. Offline mode switches off for the page (`markOfflineUnavailable`). A failure on\n * a browser that can keep the copy (the file held by another tab) is a `DeviceStoreUnavailableError`.\n */\nexport class OfflineUnavailableError extends DeviceStoreUnavailableError {\n constructor(reason: string) {\n super(`Offline mode isn't available in this browser: ${reason}`);\n this.name = \"OfflineUnavailableError\";\n }\n}\n\n/**\n * Switches offline mode off for this page, once: one console warning, `available: false` in the\n * offline state. From then on `isOfflineConfigured()` answers false, so no request, sync, drain,\n * write-through or lock check touches the device copy again.\n */\nexport function markOfflineUnavailable(error: OfflineUnavailableError): void {\n if (!getOfflineState().available) return;\n console.warn(`[offline] ${error.message}. Offline mode is off in this browser.`);\n setOfflineState({ available: false });\n}\n\n/** Whether this browser can keep the device copy (true until found otherwise). */\nexport function isOfflineAvailable(): boolean {\n return getOfflineState().available;\n}\n\n/** Logout: the user's store is never opened again in this page until `unblockOfflineStore`. */\nexport function blockOfflineStore(userId: string): void {\n _blockedUsers.add(userId);\n _stores.delete(userId);\n}\n\n/** A sign-in (`onUserSignedIn`): the user's store may be opened again. */\nexport function unblockOfflineStore(userId: string): void {\n _blockedUsers.delete(userId);\n}\n\nexport function isOfflineStoreBlocked(userId: string): boolean {\n return _blockedUsers.has(userId);\n}\n\n/** Opts the app into offline mode. Without this call nothing offline runs. */\nexport function configureOffline(config: OfflineConfig): void {\n _config = config;\n attachBrowserOnlineEvents();\n refreshOfflineDeviceState();\n}\n\n/**\n * Configured, the browser can keep the device copy (see `markOfflineUnavailable`), and offline\n * mode is on on this device for the signed-in user (see `requireOptIn`).\n */\nexport function isOfflineConfigured(): boolean {\n return _config !== null && isOfflineAvailable() && isOfflineEnabledOnDevice(_config.getUserId());\n}\n\n/**\n * Whether offline mode is on on this device for `userId`. Always true without\n * `OfflineConfig.requireOptIn`; otherwise the stored switch, false with no user.\n */\nexport function isOfflineEnabledOnDevice(userId?: string | null): boolean {\n if (!_config?.requireOptIn) return true;\n if (!userId) return false;\n let enabled = _enabledCache.get(userId);\n if (enabled === undefined) {\n enabled = readStorage(ENABLED_KEY_PREFIX + userId) === \"1\";\n _enabledCache.set(userId, enabled);\n }\n return enabled;\n}\n\n/** Stores the device switch for `userId` (`turnOnOffline` / `turnOffOffline`). */\nexport function setOfflineEnabledOnDevice(userId: string, enabled: boolean): void {\n writeStorage(ENABLED_KEY_PREFIX + userId, enabled ? \"1\" : null);\n _enabledCache.set(userId, enabled);\n}\n\n/** When `userId`'s last full sync on this device finished (epoch ms), or null. */\nexport function readOfflineLastSync(userId: string): number | null {\n const value = Number(readStorage(LAST_SYNC_KEY_PREFIX + userId));\n return Number.isFinite(value) && value > 0 ? value : null;\n}\n\n/** Stores, or with null removes, when `userId`'s last full sync finished. */\nexport function writeOfflineLastSync(userId: string, at: number | null): void {\n writeStorage(LAST_SYNC_KEY_PREFIX + userId, at === null ? null : String(at));\n}\n\n/**\n * Reads the device switch and the last sync time of the signed-in user into the offline state\n * (`enabled`, `lastSyncedAt`). Does nothing before `configureOffline`.\n */\nexport function refreshOfflineDeviceState(): void {\n if (!_config) return;\n const userId = _config.getUserId();\n if (userId) _enabledCache.delete(userId);\n setOfflineState({\n enabled: isOfflineEnabledOnDevice(userId),\n lastSyncedAt: userId ? readOfflineLastSync(userId) : null,\n });\n}\n\nexport function getOfflineConfig(): OfflineConfig {\n if (!_config) throw new Error(\"Offline mode is not configured. Call configureOffline() first.\");\n return _config;\n}\n\n/** The browser opener, set by the `./offline` entry's `configureOffline`. */\nexport function setDefaultDeviceStoreOpener(opener: (userId: string) => Promise<DeviceStore>): void {\n _defaultOpener = opener;\n}\n\n/** The URL the device worker loads SQLite from (`OfflineConfig.sqliteModuleUrl`). */\nexport function getSqliteModuleUrl(): string {\n return _config?.sqliteModuleUrl ?? DEFAULT_SQLITE_MODULE_URL;\n}\n\n/** The URL the device worker is started from (`OfflineConfig.deviceWorkerUrl`). */\nexport function getDeviceWorkerUrl(): string {\n return _config?.deviceWorkerUrl ?? DEFAULT_DEVICE_WORKER_URL;\n}\n\n/** Replaces how a user's device store is opened (tests pass `openMemoryDeviceStore`). */\nexport function setDeviceStoreOpener(opener: ((userId: string) => Promise<DeviceStore>) | null): void {\n _opener = opener;\n _stores.clear();\n}\n\n/**\n * The signed-in user's device store, opened once per user id. Only the signed-in user's file is\n * ever opened; with no signed-in user the request needs a connection.\n */\nexport async function getOfflineStore(): Promise<DeviceStore> {\n const userId = getOfflineConfig().getUserId();\n if (!userId) throw new NeedsConnectionError(\"No signed-in user\");\n if (!isOfflineEnabledOnDevice(userId)) throw new DeviceStoreUnavailableError(\"Offline mode is off on this device\");\n if (_blockedUsers.has(userId)) throw new DeviceStoreUnavailableError(\"The device copy was deleted at logout\");\n if (!isOfflineAvailable()) throw new OfflineUnavailableError(\"it was found unable to keep the device copy\");\n\n let store = _stores.get(userId);\n if (!store) {\n const opener = _opener ?? _defaultOpener;\n if (!opener) throw new Error(\"No device store opener: configure offline mode through the ./offline entry.\");\n store = opener(userId).catch((error) => {\n if (error instanceof OfflineUnavailableError) markOfflineUnavailable(error);\n if (error instanceof NeedsConnectionError) throw error;\n console.warn(\"[offline] the device copy could not be opened\", error);\n throw new DeviceStoreUnavailableError(\n `The device copy could not be opened: ${String((error as Error)?.message ?? error)}`,\n );\n });\n _stores.set(userId, store);\n store.catch(() => _stores.delete(userId));\n }\n return store;\n}\n\n/** Forgets the opened store of a user (logout, deleted file). */\nexport function forgetOfflineStore(userId: string): void {\n _stores.delete(userId);\n}\n\n/** Test helper: back to \"not configured\". */\nexport function resetOfflineConfig(): void {\n _config = null;\n _opener = null;\n _stores.clear();\n _blockedUsers.clear();\n _enabledCache.clear();\n}\n"],"mappings":"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","names":[]}