@multiplatform.one/frappe 8.2.2 → 8.4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/frappe",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
4
4
|
"description": "frappe client for multiplatform.one ecosystem",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@multiplatform.one/store": "^8.
|
|
64
|
+
"@multiplatform.one/store": "^8.4.0",
|
|
65
65
|
"@tanstack/db": "0.11.3",
|
|
66
66
|
"@tanstack/pacer": "0.21.1",
|
|
67
67
|
"@tanstack/react-db": "0.5.3",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"socket.io-client": "^4.8.4"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@multiplatform.one/config": "8.
|
|
74
|
-
"@multiplatform.one/platform": "8.
|
|
75
|
-
"@multiplatform.one/test-utils": "8.
|
|
76
|
-
"@multiplatform.one/theme": "8.
|
|
73
|
+
"@multiplatform.one/config": "8.4.0",
|
|
74
|
+
"@multiplatform.one/platform": "8.4.0",
|
|
75
|
+
"@multiplatform.one/test-utils": "8.4.0",
|
|
76
|
+
"@multiplatform.one/theme": "8.4.0",
|
|
77
77
|
"@tamagui/build": "2.7.6",
|
|
78
78
|
"@testing-library/jest-dom": "^7.0.1",
|
|
79
79
|
"@testing-library/react": "^16.3.3",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vitest": "^4.1.12"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@multiplatform.one/platform": "^8.
|
|
89
|
+
"@multiplatform.one/platform": "^8.4.0",
|
|
90
90
|
"react": "^19.1.0"
|
|
91
91
|
},
|
|
92
92
|
"engines": {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
2
|
+
import { createFrappeCollection } from "./collection";
|
|
3
|
+
import { SyncModule } from "./sync";
|
|
4
|
+
|
|
5
|
+
vi.mock("@multiplatform.one/platform", () => ({
|
|
6
|
+
isNative: false,
|
|
7
|
+
isServer: true,
|
|
8
|
+
isWindowDefined: false,
|
|
9
|
+
onAppFocus: () => () => {},
|
|
10
|
+
onOnline: () => () => {},
|
|
11
|
+
onVisibilityChange: () => () => {},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const instances: SyncModule[] = [];
|
|
15
|
+
const cleanups: (() => unknown)[] = [];
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
try {
|
|
18
|
+
for (const cleanup of cleanups.splice(0)) await cleanup();
|
|
19
|
+
} finally {
|
|
20
|
+
for (const sync of instances.splice(0)) await sync.dispose();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const row = (name: string, title: string) => ({
|
|
25
|
+
name,
|
|
26
|
+
title,
|
|
27
|
+
doctype: "Item",
|
|
28
|
+
modified: "2026-09-09T01:00:00Z",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function deferred() {
|
|
32
|
+
let resolve!: () => void;
|
|
33
|
+
const promise = new Promise<void>((done) => {
|
|
34
|
+
resolve = done;
|
|
35
|
+
});
|
|
36
|
+
return { promise, resolve };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function mountedCollection() {
|
|
40
|
+
const ack = deferred();
|
|
41
|
+
const db = {
|
|
42
|
+
getDocList: vi.fn().mockResolvedValue([row("a", "old"), row("b", "other")]),
|
|
43
|
+
updateDoc: vi.fn().mockImplementation(() => ack.promise),
|
|
44
|
+
getDoc: vi.fn().mockResolvedValue({ ...row("a", "new"), modified: "2026-09-09T02:00:00Z" }),
|
|
45
|
+
};
|
|
46
|
+
const sync = new SyncModule(db as never, undefined);
|
|
47
|
+
instances.push(sync);
|
|
48
|
+
const collection = createFrappeCollection<ReturnType<typeof row>>("Item", { sync });
|
|
49
|
+
cleanups.push(() => collection.cleanup());
|
|
50
|
+
await collection.preload();
|
|
51
|
+
const seen: string[] = [];
|
|
52
|
+
const subscription = collection.subscribeChanges((changes) => {
|
|
53
|
+
for (const change of changes) {
|
|
54
|
+
if (change.key === "a" && change.type !== "delete") seen.push(change.value.title);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
cleanups.push(() => subscription.unsubscribe());
|
|
58
|
+
return { ack, collection, db, seen, sync };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
it("an edit never shows the previous synced value while its save is in flight or after it settles", async () => {
|
|
62
|
+
const { ack, collection, db, seen } = await mountedCollection();
|
|
63
|
+
expect(collection.get("a")?.title).toBe("old");
|
|
64
|
+
|
|
65
|
+
const tx = collection.update("a", (draft) => {
|
|
66
|
+
draft.title = "new";
|
|
67
|
+
});
|
|
68
|
+
expect(collection.get("a")?.title).toBe("new");
|
|
69
|
+
|
|
70
|
+
await vi.waitFor(() => expect(db.updateDoc).toHaveBeenCalledTimes(1));
|
|
71
|
+
expect(db.updateDoc.mock.calls[0][0]).toMatchObject({ name: "a", title: "new" });
|
|
72
|
+
expect(collection.get("a")?.title).toBe("new");
|
|
73
|
+
|
|
74
|
+
ack.resolve();
|
|
75
|
+
await tx.isPersisted.promise;
|
|
76
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
77
|
+
|
|
78
|
+
expect(collection.get("a")?.title).toBe("new");
|
|
79
|
+
expect(collection.get("b")?.title).toBe("other");
|
|
80
|
+
expect(seen).toContain("new");
|
|
81
|
+
expect(seen.slice(seen.indexOf("new"))).not.toContain("old");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("the synced row already carries the edit when the save resolves, so dropping the optimistic layer changes nothing", async () => {
|
|
85
|
+
const { ack, collection, sync } = await mountedCollection();
|
|
86
|
+
|
|
87
|
+
const tx = collection.update("a", (draft) => {
|
|
88
|
+
draft.title = "new";
|
|
89
|
+
});
|
|
90
|
+
await vi.waitFor(() => expect(sync.getDocFromCache("Item", "a")?.title).toBe("new"));
|
|
91
|
+
|
|
92
|
+
ack.resolve();
|
|
93
|
+
await tx.isPersisted.promise;
|
|
94
|
+
|
|
95
|
+
expect(sync.getDocFromCache("Item", "a")).toMatchObject({
|
|
96
|
+
title: "new",
|
|
97
|
+
modified: "2026-09-09T02:00:00Z",
|
|
98
|
+
});
|
|
99
|
+
expect(collection.state.get("a")?.title).toBe("new");
|
|
100
|
+
});
|