@carlonicora/nextjs-jsonapi 6.12.1 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ApiData-DPKNfY-9.d.mts → ApiData-Be8yxCOX.d.mts} +2 -0
- package/dist/{ApiData-DPKNfY-9.d.ts → ApiData-Be8yxCOX.d.ts} +2 -0
- package/dist/{BlockNoteEditor-N2NBYCPC.mjs → BlockNoteEditor-22Q4DJBU.mjs} +20 -8
- package/dist/BlockNoteEditor-22Q4DJBU.mjs.map +1 -0
- package/dist/{BlockNoteEditor-CBYKEXAQ.js → BlockNoteEditor-2VLPSEKS.js} +34 -22
- package/dist/BlockNoteEditor-2VLPSEKS.js.map +1 -0
- package/dist/{BlockNoteViewer-WGCDB7IJ.js → BlockNoteViewer-2XYASA5Y.js} +8 -5
- package/dist/BlockNoteViewer-2XYASA5Y.js.map +1 -0
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs → BlockNoteViewer-NBW2H6LC.mjs} +7 -4
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs.map → BlockNoteViewer-NBW2H6LC.mjs.map} +1 -1
- package/dist/{JsonApiRequest-A66XK5WT.mjs → JsonApiRequest-F6TMJ42G.mjs} +2 -2
- package/dist/JsonApiRequest-WISJZSDE.js +26 -0
- package/dist/{JsonApiRequest-FZGPLNUF.js.map → JsonApiRequest-WISJZSDE.js.map} +1 -1
- package/dist/billing/index.js +499 -494
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +13 -8
- package/dist/billing/index.mjs.map +1 -1
- package/dist/chunk-27LQDGNP.mjs +317 -0
- package/dist/chunk-27LQDGNP.mjs.map +1 -0
- package/dist/{chunk-6NZKWIOR.js → chunk-2H342HNN.js} +956 -929
- package/dist/chunk-2H342HNN.js.map +1 -0
- package/dist/{chunk-KSYQEKJX.js → chunk-2SV4SS6N.js} +14 -14
- package/dist/{chunk-KSYQEKJX.js.map → chunk-2SV4SS6N.js.map} +1 -1
- package/dist/{chunk-4FNGWALA.mjs → chunk-47BBY5KA.mjs} +6 -4
- package/dist/{chunk-4FNGWALA.mjs.map → chunk-47BBY5KA.mjs.map} +1 -1
- package/dist/chunk-6JUCIWDC.mjs +570 -0
- package/dist/chunk-6JUCIWDC.mjs.map +1 -0
- package/dist/chunk-7DBJTNSU.mjs +1049 -0
- package/dist/chunk-7DBJTNSU.mjs.map +1 -0
- package/dist/chunk-7JZHPMGW.mjs +704 -0
- package/dist/chunk-7JZHPMGW.mjs.map +1 -0
- package/dist/{chunk-L6PYFQTO.js → chunk-7ZHTPFFT.js} +16 -14
- package/dist/chunk-7ZHTPFFT.js.map +1 -0
- package/dist/chunk-B6JGKDP2.js +214 -0
- package/dist/chunk-B6JGKDP2.js.map +1 -0
- package/dist/{chunk-3YW35YHQ.mjs → chunk-BH6JWIKI.mjs} +5 -5
- package/dist/chunk-BZ5J2FIV.js +570 -0
- package/dist/chunk-BZ5J2FIV.js.map +1 -0
- package/dist/{chunk-V5MI7RNB.mjs → chunk-DGWOVYEC.mjs} +2 -2
- package/dist/chunk-DOCIFE6A.mjs +212 -0
- package/dist/chunk-DOCIFE6A.mjs.map +1 -0
- package/dist/chunk-GB6DBFYY.js +25 -0
- package/dist/chunk-GB6DBFYY.js.map +1 -0
- package/dist/{chunk-6XJOQFCP.mjs → chunk-H4NLDX5G.mjs} +59 -32
- package/dist/chunk-H4NLDX5G.mjs.map +1 -0
- package/dist/{chunk-EWCY6QJA.mjs → chunk-JJMMT24D.mjs} +37 -3
- package/dist/chunk-JJMMT24D.mjs.map +1 -0
- package/dist/{chunk-JTPRJNT2.js → chunk-KMW7UNL2.js} +38 -3
- package/dist/chunk-KMW7UNL2.js.map +1 -0
- package/dist/chunk-MHB36MYE.js +317 -0
- package/dist/chunk-MHB36MYE.js.map +1 -0
- package/dist/{chunk-FF4EIQ6H.js → chunk-MOGEGWXW.js} +1039 -1680
- package/dist/chunk-MOGEGWXW.js.map +1 -0
- package/dist/{chunk-AA52ORW2.mjs → chunk-P445QGOC.mjs} +61 -702
- package/dist/chunk-P445QGOC.mjs.map +1 -0
- package/dist/chunk-PV4Z5W6V.js +212 -0
- package/dist/chunk-PV4Z5W6V.js.map +1 -0
- package/dist/chunk-PXDUJ47N.js +1049 -0
- package/dist/chunk-PXDUJ47N.js.map +1 -0
- package/dist/{chunk-GHE4VT2R.js → chunk-QKNGZDSQ.js} +2 -2
- package/dist/{chunk-GHE4VT2R.js.map → chunk-QKNGZDSQ.js.map} +1 -1
- package/dist/chunk-W62MHDWM.js +704 -0
- package/dist/chunk-W62MHDWM.js.map +1 -0
- package/dist/chunk-XXP3TQHF.mjs +25 -0
- package/dist/chunk-XXP3TQHF.mjs.map +1 -0
- package/dist/chunk-YRKIFYOE.mjs +214 -0
- package/dist/chunk-YRKIFYOE.mjs.map +1 -0
- package/dist/{chunk-O3PDL2CV.js → chunk-Z4POQWFG.js} +40 -6
- package/dist/chunk-Z4POQWFG.js.map +1 -0
- package/dist/{chunk-PI56A4DG.mjs → chunk-ZLTOUZN2.mjs} +37 -2
- package/dist/chunk-ZLTOUZN2.mjs.map +1 -0
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +14 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +24 -19
- package/dist/components/index.d.mts +7 -1
- package/dist/components/index.d.ts +7 -1
- package/dist/components/index.js +11 -7
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -6
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +11 -7
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +10 -6
- package/dist/core/index.d.mts +128 -2
- package/dist/core/index.d.ts +128 -2
- package/dist/core/index.js +17 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +22 -9
- package/dist/email-automations/index.js +101 -96
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +13 -8
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +128 -123
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +13 -8
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +11 -8
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +9 -6
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.js +158 -153
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +14 -9
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +30 -26
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +10 -6
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/help/index.js +44 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +10 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +134 -129
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +13 -8
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -11
- package/dist/lifecycle-4GUOTB46.js +23 -0
- package/dist/lifecycle-4GUOTB46.js.map +1 -0
- package/dist/lifecycle-LKGPRBS7.mjs +23 -0
- package/dist/local-transport-7H3SPZSA.js +15 -0
- package/dist/local-transport-7H3SPZSA.js.map +1 -0
- package/dist/local-transport-QYFKA6AN.mjs +15 -0
- package/dist/local-transport-QYFKA6AN.mjs.map +1 -0
- package/dist/offline/index.d.mts +607 -0
- package/dist/offline/index.d.ts +607 -0
- package/dist/offline/index.js +1052 -0
- package/dist/offline/index.js.map +1 -0
- package/dist/offline/index.mjs +1052 -0
- package/dist/offline/index.mjs.map +1 -0
- package/dist/offline/sqlite/device-worker.mjs +62 -0
- package/dist/offline/sqlite/index.mjs +17392 -0
- package/dist/offline/sqlite/sqlite3-opfs-async-proxy.js +881 -0
- package/dist/offline/sqlite/sqlite3.wasm +0 -0
- package/dist/request-KTBV6HHH.js +10 -0
- package/dist/request-KTBV6HHH.js.map +1 -0
- package/dist/request-SQOUNWZ4.mjs +10 -0
- package/dist/request-SQOUNWZ4.mjs.map +1 -0
- package/dist/server/cachedGet.d.mts +1 -1
- package/dist/server/cachedGet.d.ts +1 -1
- package/dist/server/cachedGet.stub.d.mts +1 -1
- package/dist/server/cachedGet.stub.d.ts +1 -1
- package/dist/server/index.d.mts +28 -2
- package/dist/server/index.d.ts +28 -2
- package/dist/server/index.js +18 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -8
- package/dist/{useSocket-IRHtk2xc.d.ts → useSocket-Bo9M0t0J.d.ts} +3 -1
- package/dist/{useSocket-C8fqU4od.d.mts → useSocket-J5qanQPN.d.mts} +3 -1
- package/dist/write-through-FDQ7N4YI.js +15 -0
- package/dist/write-through-FDQ7N4YI.js.map +1 -0
- package/dist/write-through-R25VQMH6.mjs +15 -0
- package/dist/write-through-R25VQMH6.mjs.map +1 -0
- package/package.json +7 -1
- package/pwa/sw.js +277 -26
- package/src/client/request.ts +37 -0
- package/src/components/containers/RoundPageContainer.tsx +8 -8
- package/src/components/editors/BlockNoteEditor.tsx +14 -0
- package/src/components/editors/__tests__/BlockNoteEditor.empty-change.spec.tsx +205 -0
- package/src/contexts/SocketContext.tsx +7 -1
- package/src/contexts/__tests__/SocketContext.session.spec.tsx +52 -5
- package/src/core/factories/RehydrationFactory.ts +28 -8
- package/src/core/factories/__tests__/RehydrationFactory.spec.ts +33 -0
- package/src/core/index.ts +3 -0
- package/src/core/interfaces/ApiData.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/auth/components/forms/Logout.tsx +8 -1
- package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +62 -0
- package/src/hooks/__tests__/useSocket.pause.spec.ts +39 -0
- package/src/hooks/useSocket.ts +20 -1
- package/src/offline/__tests__/answer-builders.spec.ts +176 -0
- package/src/offline/__tests__/device-switch.spec.ts +179 -0
- package/src/offline/__tests__/generic-answers.spec.ts +380 -0
- package/src/offline/__tests__/local-transport.spec.ts +150 -0
- package/src/offline/__tests__/logout.spec.ts +122 -0
- package/src/offline/__tests__/offline-fixtures.ts +89 -0
- package/src/offline/__tests__/router.spec.ts +56 -0
- package/src/offline/__tests__/state.spec.ts +62 -0
- package/src/offline/__tests__/write-through.spec.ts +137 -0
- package/src/offline/config.ts +229 -0
- package/src/offline/configure.ts +14 -0
- package/src/offline/device-switch.ts +75 -0
- package/src/offline/generic-answers.ts +706 -0
- package/src/offline/index.ts +67 -0
- package/src/offline/local-transport.ts +189 -0
- package/src/offline/logout.ts +41 -0
- package/src/offline/router.ts +41 -0
- package/src/offline/server/__tests__/build-offline-manifest.spec.ts +76 -0
- package/src/offline/server/build-offline-manifest.ts +49 -0
- package/src/offline/state-hooks.ts +10 -0
- package/src/offline/state.ts +143 -0
- package/src/offline/store/DeviceStore.ts +112 -0
- package/src/offline/store/__tests__/device-store-lifecycle.spec.ts +343 -0
- package/src/offline/store/__tests__/shared-database.spec.ts +151 -0
- package/src/offline/store/__tests__/sql-device-store.spec.ts +399 -0
- package/src/offline/store/lifecycle.ts +315 -0
- package/src/offline/store/schema.ts +101 -0
- package/src/offline/store/shared-database.ts +321 -0
- package/src/offline/store/sql-device-store.ts +469 -0
- package/src/offline/store/worker-client.ts +71 -0
- package/src/offline/store/worker.ts +87 -0
- package/src/offline/sync/__tests__/conflicts.spec.ts +185 -0
- package/src/offline/sync/__tests__/queue-drain.spec.ts +364 -0
- package/src/offline/sync/__tests__/runtime.spec.ts +337 -0
- package/src/offline/sync/__tests__/sync-fixtures.ts +103 -0
- package/src/offline/sync/__tests__/sync-root.spec.ts +727 -0
- package/src/offline/sync/artwork-cache.ts +67 -0
- package/src/offline/sync/conflicts.ts +211 -0
- package/src/offline/sync/counts.ts +39 -0
- package/src/offline/sync/drain-queue.ts +246 -0
- package/src/offline/sync/known-roots.ts +37 -0
- package/src/offline/sync/models/OfflineChangeSet.ts +34 -0
- package/src/offline/sync/models/OfflineChangeSetInterface.ts +54 -0
- package/src/offline/sync/models/RootSync.ts +68 -0
- package/src/offline/sync/models/RootSyncInterface.ts +20 -0
- package/src/offline/sync/models/__tests__/OfflineChangeSet.spec.ts +47 -0
- package/src/offline/sync/models/__tests__/RootSync.spec.ts +54 -0
- package/src/offline/sync/models/index.ts +5 -0
- package/src/offline/sync/models/offline.modules.ts +14 -0
- package/src/offline/sync/runtime.ts +226 -0
- package/src/offline/sync/session-refresh.ts +32 -0
- package/src/offline/sync/sync-root.ts +336 -0
- package/src/offline/sync/waiting.ts +40 -0
- package/src/offline/types.ts +115 -0
- package/src/offline/write-through.ts +189 -0
- package/src/pwa/__tests__/sw.spec.ts +358 -1
- package/src/server/index.ts +5 -0
- package/src/unified/__tests__/JsonApiRequest.offline.spec.ts +243 -0
- package/dist/BlockNoteEditor-CBYKEXAQ.js.map +0 -1
- package/dist/BlockNoteEditor-N2NBYCPC.mjs.map +0 -1
- package/dist/BlockNoteViewer-WGCDB7IJ.js.map +0 -1
- package/dist/JsonApiRequest-FZGPLNUF.js +0 -26
- package/dist/chunk-6NZKWIOR.js.map +0 -1
- package/dist/chunk-6XJOQFCP.mjs.map +0 -1
- package/dist/chunk-AA52ORW2.mjs.map +0 -1
- package/dist/chunk-EWCY6QJA.mjs.map +0 -1
- package/dist/chunk-FF4EIQ6H.js.map +0 -1
- package/dist/chunk-JTPRJNT2.js.map +0 -1
- package/dist/chunk-L6PYFQTO.js.map +0 -1
- package/dist/chunk-O3PDL2CV.js.map +0 -1
- package/dist/chunk-PI56A4DG.mjs.map +0 -1
- package/dist/request-5RLK2AM3.mjs +0 -9
- package/dist/request-EUGTI3BE.js +0 -9
- package/dist/request-EUGTI3BE.js.map +0 -1
- /package/dist/{JsonApiRequest-A66XK5WT.mjs.map → JsonApiRequest-F6TMJ42G.mjs.map} +0 -0
- /package/dist/{chunk-3YW35YHQ.mjs.map → chunk-BH6JWIKI.mjs.map} +0 -0
- /package/dist/{chunk-V5MI7RNB.mjs.map → chunk-DGWOVYEC.mjs.map} +0 -0
- /package/dist/{request-5RLK2AM3.mjs.map → lifecycle-LKGPRBS7.mjs.map} +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import * as offline from "../index";
|
|
3
|
+
import {
|
|
4
|
+
errorAnswer,
|
|
5
|
+
mergeResource,
|
|
6
|
+
orderedListAnswer,
|
|
7
|
+
resourceFromDocument,
|
|
8
|
+
resourceListAnswer,
|
|
9
|
+
singleAnswer,
|
|
10
|
+
} from "../generic-answers";
|
|
11
|
+
import type { DeviceStore } from "../store/DeviceStore";
|
|
12
|
+
import { configureOffline, resetOfflineConfig } from "../config";
|
|
13
|
+
import { offlineConfig, seededStore } from "./offline-fixtures";
|
|
14
|
+
|
|
15
|
+
describe("answer builders", () => {
|
|
16
|
+
let store: DeviceStore;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
store = await seededStore();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("the ./offline entry exports the builders an app's custom answers use", () => {
|
|
23
|
+
for (const builder of [
|
|
24
|
+
singleAnswer,
|
|
25
|
+
errorAnswer,
|
|
26
|
+
orderedListAnswer,
|
|
27
|
+
resourceListAnswer,
|
|
28
|
+
mergeResource,
|
|
29
|
+
resourceFromDocument,
|
|
30
|
+
]) {
|
|
31
|
+
expect(typeof builder).toBe("function");
|
|
32
|
+
}
|
|
33
|
+
expect(offline.singleAnswer).toBe(singleAnswer);
|
|
34
|
+
expect(offline.errorAnswer).toBe(errorAnswer);
|
|
35
|
+
expect(offline.orderedListAnswer).toBe(orderedListAnswer);
|
|
36
|
+
expect(offline.resourceListAnswer).toBe(resourceListAnswer);
|
|
37
|
+
expect(offline.mergeResource).toBe(mergeResource);
|
|
38
|
+
expect(offline.resourceFromDocument).toBe(resourceFromDocument);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("errorAnswer answers a JSON:API errors document with that status", () => {
|
|
42
|
+
expect(errorAnswer(409, "Conflict", "Already taken")).toEqual({
|
|
43
|
+
status: 409,
|
|
44
|
+
body: { errors: [{ status: "409", title: "Conflict", detail: "Already taken" }] },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("singleAnswer answers the stored resource with its included records", async () => {
|
|
49
|
+
const record = (await store.getRecord("npcs", "n1"))!;
|
|
50
|
+
const result = await singleAnswer(store, record, 201);
|
|
51
|
+
expect(result.status).toBe(201);
|
|
52
|
+
expect(result.body.data).toEqual(record.resource);
|
|
53
|
+
expect(Array.isArray(result.body.included)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("included records keep their relationships whole: a root record keeps its owner even when the owner is also linked to the record", async () => {
|
|
57
|
+
const put = (type: string, id: string, relationships: Record<string, any>) =>
|
|
58
|
+
store.putRecord({
|
|
59
|
+
type,
|
|
60
|
+
id,
|
|
61
|
+
rootId: "C",
|
|
62
|
+
resource: { type, id, attributes: {}, relationships },
|
|
63
|
+
updatedAt: null,
|
|
64
|
+
});
|
|
65
|
+
// The npc points at its root record and at the user; the root record points back at the npc and at the user.
|
|
66
|
+
await put("npcs", "cy-npc", {
|
|
67
|
+
root: { data: { type: "roots", id: "cy-camp" } },
|
|
68
|
+
author: { data: { type: "users", id: "cy-user" } },
|
|
69
|
+
});
|
|
70
|
+
await put("roots", "cy-camp", {
|
|
71
|
+
owner: { data: { type: "users", id: "cy-user" } },
|
|
72
|
+
npcs: { data: [{ type: "npcs", id: "cy-npc" }] },
|
|
73
|
+
});
|
|
74
|
+
await put("users", "cy-user", { company: { data: { type: "companies", id: "cy-co" } } });
|
|
75
|
+
await put("companies", "cy-co", { owner: { data: { type: "users", id: "cy-user" } } });
|
|
76
|
+
|
|
77
|
+
const record = (await store.getRecord("npcs", "cy-npc"))!;
|
|
78
|
+
const included = (await singleAnswer(store, record)).body.included as any[];
|
|
79
|
+
const byKey = new Map(included.map((resource) => [`${resource.type}:${resource.id}`, resource]));
|
|
80
|
+
|
|
81
|
+
expect(byKey.get("roots:cy-camp").relationships.owner.data).toEqual({ type: "users", id: "cy-user" });
|
|
82
|
+
expect(byKey.get("roots:cy-camp").relationships.npcs.data).toEqual([{ type: "npcs", id: "cy-npc" }]);
|
|
83
|
+
expect(byKey.get("companies:cy-co").relationships.owner.data).toEqual({ type: "users", id: "cy-user" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("a relationship to the signed-in user (a root record's owner) resolves from the stored sign-in, though users are not on the device", async () => {
|
|
87
|
+
configureOffline(offlineConfig());
|
|
88
|
+
localStorage.setItem(
|
|
89
|
+
"user",
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
jsonApi: {
|
|
92
|
+
type: "users",
|
|
93
|
+
id: "u1",
|
|
94
|
+
attributes: { name: "Carlo" },
|
|
95
|
+
relationships: { company: { data: { type: "companies", id: "co" } } },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
try {
|
|
100
|
+
await store.putRecord({
|
|
101
|
+
type: "roots",
|
|
102
|
+
id: "own-camp",
|
|
103
|
+
rootId: "own-camp",
|
|
104
|
+
resource: {
|
|
105
|
+
type: "roots",
|
|
106
|
+
id: "own-camp",
|
|
107
|
+
attributes: {},
|
|
108
|
+
relationships: { owner: { data: { type: "users", id: "u1" } } },
|
|
109
|
+
},
|
|
110
|
+
updatedAt: null,
|
|
111
|
+
});
|
|
112
|
+
await store.putRecord({
|
|
113
|
+
type: "npcs",
|
|
114
|
+
id: "own-npc",
|
|
115
|
+
rootId: "own-camp",
|
|
116
|
+
resource: {
|
|
117
|
+
type: "npcs",
|
|
118
|
+
id: "own-npc",
|
|
119
|
+
attributes: {},
|
|
120
|
+
relationships: { root: { data: { type: "roots", id: "own-camp" } } },
|
|
121
|
+
},
|
|
122
|
+
updatedAt: null,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const included = (await singleAnswer(store, (await store.getRecord("npcs", "own-npc"))!)).body.included as any[];
|
|
126
|
+
const owner = included.find((resource) => resource.type === "users");
|
|
127
|
+
// Only the signed-in user, with no relationships of its own.
|
|
128
|
+
expect(owner).toEqual({ type: "users", id: "u1", attributes: { name: "Carlo" }, meta: undefined });
|
|
129
|
+
} finally {
|
|
130
|
+
localStorage.removeItem("user");
|
|
131
|
+
resetOfflineConfig();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("orderedListAnswer answers the records in the given order, in one page", async () => {
|
|
136
|
+
const records = await store.listRecords({ type: "npcs" });
|
|
137
|
+
const reversed = [...records].reverse();
|
|
138
|
+
const result = await orderedListAnswer(store, reversed);
|
|
139
|
+
expect(result.status).toBe(200);
|
|
140
|
+
expect(result.body.data.map((resource: { id: string }) => resource.id)).toEqual(
|
|
141
|
+
reversed.map((record) => record.id),
|
|
142
|
+
);
|
|
143
|
+
expect(result.body.meta).toEqual({ total: reversed.length });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("resourceListAnswer answers resources the device computed, as they are, with nothing included", () => {
|
|
147
|
+
const resources = [{ type: "searches", id: "a", attributes: { name: "A" } }];
|
|
148
|
+
expect(resourceListAnswer(resources)).toEqual({ status: 200, body: { data: resources, included: [] } });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("mergeResource writes a document's attributes, relationships and meta over the resource", () => {
|
|
152
|
+
const before = {
|
|
153
|
+
type: "npcs",
|
|
154
|
+
id: "n1",
|
|
155
|
+
attributes: { name: "Old", age: 3 },
|
|
156
|
+
relationships: { faction: { data: { type: "factions", id: "f1" } } },
|
|
157
|
+
meta: { a: 1 },
|
|
158
|
+
};
|
|
159
|
+
expect(mergeResource(before, { type: "npcs", id: "n1" }, { attributes: { name: "New" }, meta: { b: 2 } })).toEqual({
|
|
160
|
+
type: "npcs",
|
|
161
|
+
id: "n1",
|
|
162
|
+
attributes: { name: "New", age: 3 },
|
|
163
|
+
relationships: { faction: { data: { type: "factions", id: "f1" } } },
|
|
164
|
+
meta: { a: 1, b: 2 },
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("resourceFromDocument keeps the document's attributes, relationships and meta under the given identity", () => {
|
|
169
|
+
expect(
|
|
170
|
+
resourceFromDocument(
|
|
171
|
+
{ data: { type: "npcs", id: "x", attributes: { name: "A" }, meta: { m: 1 } } },
|
|
172
|
+
{ type: "npcs", id: "n9" },
|
|
173
|
+
),
|
|
174
|
+
).toEqual({ type: "npcs", id: "n9", attributes: { name: "A" }, meta: { m: 1 } });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
configureOffline,
|
|
4
|
+
DeviceStoreUnavailableError,
|
|
5
|
+
getOfflineStore,
|
|
6
|
+
isOfflineConfigured,
|
|
7
|
+
isOfflineEnabledOnDevice,
|
|
8
|
+
resetOfflineConfig,
|
|
9
|
+
setDeviceStoreOpener,
|
|
10
|
+
} from "../config";
|
|
11
|
+
import { turnOffOffline, turnOnOffline } from "../device-switch";
|
|
12
|
+
import { getOfflineState, resetOfflineState, setOfflineState } from "../state";
|
|
13
|
+
import type { DeviceStore, QueueEntry } from "../store/DeviceStore";
|
|
14
|
+
import type { OfflineConfig } from "../types";
|
|
15
|
+
|
|
16
|
+
const lifecycle = vi.hoisted(() => ({ deleteDeviceStore: vi.fn(), hasUnsentChanges: vi.fn() }));
|
|
17
|
+
vi.mock("../store/lifecycle", () => lifecycle);
|
|
18
|
+
const runtime = vi.hoisted(() => ({ stopOfflineRuntime: vi.fn() }));
|
|
19
|
+
vi.mock("../sync/runtime", () => runtime);
|
|
20
|
+
|
|
21
|
+
const ENABLED_KEY = "nja-offline-enabled:u1";
|
|
22
|
+
const LAST_SYNC_KEY = "nja-offline-last-sync:u1";
|
|
23
|
+
|
|
24
|
+
function config(overrides: Partial<OfflineConfig> = {}): OfflineConfig {
|
|
25
|
+
return {
|
|
26
|
+
getUserId: () => "u1",
|
|
27
|
+
rootModule: { name: "roots" } as any,
|
|
28
|
+
types: [],
|
|
29
|
+
routes: [],
|
|
30
|
+
listSyncRoots: async () => [],
|
|
31
|
+
shellPath: () => "/offline-shell",
|
|
32
|
+
syncIntervalMs: 300000,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function storeWithQueue(entries: Partial<QueueEntry>[]): DeviceStore {
|
|
38
|
+
return { queue: vi.fn(async () => entries as QueueEntry[]) } as unknown as DeviceStore;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("offline device switch", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
localStorage.clear();
|
|
44
|
+
resetOfflineConfig();
|
|
45
|
+
resetOfflineState();
|
|
46
|
+
lifecycle.deleteDeviceStore.mockReset().mockResolvedValue(undefined);
|
|
47
|
+
lifecycle.hasUnsentChanges.mockReset().mockResolvedValue(false);
|
|
48
|
+
runtime.stopOfflineRuntime.mockReset();
|
|
49
|
+
vi.stubGlobal("caches", { keys: vi.fn(async () => ["app-images"]), delete: vi.fn(async () => true) });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
resetOfflineConfig();
|
|
54
|
+
resetOfflineState();
|
|
55
|
+
localStorage.clear();
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("without requireOptIn offline mode is on, as before", async () => {
|
|
60
|
+
configureOffline(config());
|
|
61
|
+
setDeviceStoreOpener(async () => storeWithQueue([]));
|
|
62
|
+
|
|
63
|
+
expect(isOfflineEnabledOnDevice("u1")).toBe(true);
|
|
64
|
+
expect(isOfflineConfigured()).toBe(true);
|
|
65
|
+
expect(getOfflineState().enabled).toBe(true);
|
|
66
|
+
await expect(getOfflineStore()).resolves.toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("with requireOptIn and no stored switch, offline mode is off and the store is refused", async () => {
|
|
70
|
+
configureOffline(config({ requireOptIn: true }));
|
|
71
|
+
const opener = vi.fn(async () => storeWithQueue([]));
|
|
72
|
+
setDeviceStoreOpener(opener);
|
|
73
|
+
|
|
74
|
+
expect(isOfflineConfigured()).toBe(false);
|
|
75
|
+
expect(getOfflineState().enabled).toBe(false);
|
|
76
|
+
await expect(getOfflineStore()).rejects.toBeInstanceOf(DeviceStoreUnavailableError);
|
|
77
|
+
expect(opener).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("with requireOptIn and no signed-in user, offline mode is off", () => {
|
|
81
|
+
configureOffline(config({ requireOptIn: true, getUserId: () => undefined }));
|
|
82
|
+
expect(isOfflineEnabledOnDevice(undefined)).toBe(false);
|
|
83
|
+
expect(isOfflineConfigured()).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("reads the stored switch and the last sync time at configure", () => {
|
|
87
|
+
localStorage.setItem(ENABLED_KEY, "1");
|
|
88
|
+
localStorage.setItem(LAST_SYNC_KEY, "1761990000000");
|
|
89
|
+
configureOffline(config({ requireOptIn: true }));
|
|
90
|
+
|
|
91
|
+
expect(isOfflineConfigured()).toBe(true);
|
|
92
|
+
expect(getOfflineState()).toEqual(expect.objectContaining({ enabled: true, lastSyncedAt: 1761990000000 }));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("turnOnOffline stores the switch and sets enabled", async () => {
|
|
96
|
+
const persist = vi.fn(async () => true);
|
|
97
|
+
vi.stubGlobal("navigator", { ...navigator, storage: { persist } });
|
|
98
|
+
configureOffline(config({ requireOptIn: true }));
|
|
99
|
+
|
|
100
|
+
await turnOnOffline();
|
|
101
|
+
|
|
102
|
+
expect(localStorage.getItem(ENABLED_KEY)).toBe("1");
|
|
103
|
+
expect(getOfflineState().enabled).toBe(true);
|
|
104
|
+
expect(isOfflineConfigured()).toBe(true);
|
|
105
|
+
expect(persist).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("turnOnOffline ignores a failing persist request", async () => {
|
|
109
|
+
const persist = vi.fn(async () => Promise.reject(new Error("no")));
|
|
110
|
+
vi.stubGlobal("navigator", { ...navigator, storage: { persist } });
|
|
111
|
+
configureOffline(config({ requireOptIn: true }));
|
|
112
|
+
|
|
113
|
+
await expect(turnOnOffline()).resolves.toBeUndefined();
|
|
114
|
+
expect(getOfflineState().enabled).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("turnOffOffline refuses, changing nothing, while changes wait to be sent", async () => {
|
|
118
|
+
localStorage.setItem(ENABLED_KEY, "1");
|
|
119
|
+
configureOffline(config({ requireOptIn: true }));
|
|
120
|
+
setDeviceStoreOpener(async () => storeWithQueue([{ status: "pending" }, { status: "failed" }]));
|
|
121
|
+
|
|
122
|
+
expect(await turnOffOffline()).toEqual({ turnedOff: false, pending: 2 });
|
|
123
|
+
|
|
124
|
+
expect(localStorage.getItem(ENABLED_KEY)).toBe("1");
|
|
125
|
+
expect(getOfflineState().enabled).toBe(true);
|
|
126
|
+
expect(runtime.stopOfflineRuntime).not.toHaveBeenCalled();
|
|
127
|
+
expect(lifecycle.deleteDeviceStore).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("turnOffOffline refuses when the copy is held by another tab and its marker says changes wait", async () => {
|
|
131
|
+
localStorage.setItem(ENABLED_KEY, "1");
|
|
132
|
+
configureOffline(config({ requireOptIn: true }));
|
|
133
|
+
setDeviceStoreOpener(async () => {
|
|
134
|
+
throw new Error("held by another tab");
|
|
135
|
+
});
|
|
136
|
+
lifecycle.hasUnsentChanges.mockResolvedValue(true);
|
|
137
|
+
|
|
138
|
+
expect(await turnOffOffline()).toEqual({ turnedOff: false, pending: 1 });
|
|
139
|
+
|
|
140
|
+
expect(lifecycle.hasUnsentChanges).toHaveBeenCalledWith("u1");
|
|
141
|
+
expect(localStorage.getItem(ENABLED_KEY)).toBe("1");
|
|
142
|
+
expect(lifecycle.deleteDeviceStore).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("turnOffOffline deletes the device copy and clears the switch and the state", async () => {
|
|
146
|
+
localStorage.setItem(ENABLED_KEY, "1");
|
|
147
|
+
localStorage.setItem(LAST_SYNC_KEY, "1761990000000");
|
|
148
|
+
configureOffline(config({ requireOptIn: true }));
|
|
149
|
+
const opener = vi.fn(async () => storeWithQueue([]));
|
|
150
|
+
setDeviceStoreOpener(opener);
|
|
151
|
+
setOfflineState({ rootCount: 3, syncProgress: { done: 1, total: 3 } });
|
|
152
|
+
|
|
153
|
+
expect(await turnOffOffline()).toEqual({ turnedOff: true });
|
|
154
|
+
|
|
155
|
+
expect(runtime.stopOfflineRuntime).toHaveBeenCalled();
|
|
156
|
+
expect(lifecycle.deleteDeviceStore).toHaveBeenCalledWith("u1");
|
|
157
|
+
expect(caches.delete).toHaveBeenCalledWith("app-images");
|
|
158
|
+
expect(localStorage.getItem(ENABLED_KEY)).toBeNull();
|
|
159
|
+
expect(localStorage.getItem(LAST_SYNC_KEY)).toBeNull();
|
|
160
|
+
expect(getOfflineState()).toEqual(
|
|
161
|
+
expect.objectContaining({ enabled: false, syncProgress: null, lastSyncedAt: null, rootCount: null }),
|
|
162
|
+
);
|
|
163
|
+
expect(isOfflineConfigured()).toBe(false);
|
|
164
|
+
await expect(getOfflineStore()).rejects.toBeInstanceOf(DeviceStoreUnavailableError);
|
|
165
|
+
expect(opener).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("turnOffOffline on a browser that cannot keep the copy only clears the switch and the state", async () => {
|
|
169
|
+
localStorage.setItem(ENABLED_KEY, "1");
|
|
170
|
+
configureOffline(config({ requireOptIn: true }));
|
|
171
|
+
setOfflineState({ available: false });
|
|
172
|
+
|
|
173
|
+
expect(await turnOffOffline()).toEqual({ turnedOff: true });
|
|
174
|
+
|
|
175
|
+
expect(lifecycle.deleteDeviceStore).not.toHaveBeenCalled();
|
|
176
|
+
expect(localStorage.getItem(ENABLED_KEY)).toBeNull();
|
|
177
|
+
expect(getOfflineState().enabled).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
});
|