@pylonsync/sync 0.20.0 → 0.22.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/index.d.ts +108 -7
- package/dist/local-store.d.ts +7 -0
- package/dist/multi-tab-orchestrator.d.ts +14 -0
- package/dist/mutation-queue.d.ts +57 -2
- package/dist/persistence.d.ts +12 -0
- package/dist/room-subscriptions.d.ts +5 -0
- package/dist/test-harness/server.d.ts +7 -0
- package/package.json +1 -1
- package/src/bootstrap-speedup.test.ts +9 -6
- package/src/idb-warm-load.test.ts +5 -11
- package/src/identity-reset-safety.test.ts +174 -0
- package/src/identity-resync.test.ts +120 -0
- package/src/index.ts +701 -152
- package/src/live-batch-persist.test.ts +180 -0
- package/src/local-store.ts +14 -0
- package/src/multi-tab-orchestrator.test.ts +3 -0
- package/src/multi-tab-orchestrator.ts +39 -3
- package/src/mutation-outcome.test.ts +203 -0
- package/src/mutation-owner-token.test.ts +131 -0
- package/src/mutation-owner.test.ts +217 -0
- package/src/mutation-queue.ts +177 -3
- package/src/persistence.ts +31 -0
- package/src/reconcile.test.ts +6 -3
- package/src/room-subscriptions.test.ts +32 -0
- package/src/room-subscriptions.ts +11 -0
- package/src/scenarios.test.ts +9 -3
- package/src/synced-flag.test.ts +117 -0
- package/src/test-harness/server.ts +15 -0
- package/src/test-harness/transport.ts +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Sign-in without a page reload must re-sync EVERY entity.
|
|
2
|
+
//
|
|
3
|
+
// Repro (crm template): sign in to an existing account in a tab that
|
|
4
|
+
// started anonymous. The dashboard showed 24 companies and 0 deals until
|
|
5
|
+
// a reload. Two faults combined:
|
|
6
|
+
// 1. The session refresh after sign-in (`persistSession` →
|
|
7
|
+
// `notifySessionChanged`) saw a user flip with no tenant flip and
|
|
8
|
+
// did nothing but notify. No reset, no pull, and the live socket
|
|
9
|
+
// stayed bound to the anonymous identity.
|
|
10
|
+
// 2. The only rows that arrived came from `observeEntity`'s scoped
|
|
11
|
+
// reconcile, and concurrent scoped reconciles coalesced onto the
|
|
12
|
+
// FIRST caller's entity list, so the second entity never fetched.
|
|
13
|
+
|
|
14
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import { createTestEnv, type TestEnv } from "./test-harness";
|
|
17
|
+
|
|
18
|
+
// Rows are visible only to a signed-in user.
|
|
19
|
+
const signedInOnly = (
|
|
20
|
+
_e: string,
|
|
21
|
+
rows: Array<Record<string, unknown>>,
|
|
22
|
+
auth: { userId: string | null },
|
|
23
|
+
) => (auth.userId ? rows : []);
|
|
24
|
+
|
|
25
|
+
function seedCrm(env: TestEnv) {
|
|
26
|
+
env.server.seed("Company", [
|
|
27
|
+
{ id: "c1", name: "Acme" },
|
|
28
|
+
{ id: "c2", name: "Globex" },
|
|
29
|
+
]);
|
|
30
|
+
env.server.seed("Deal", [
|
|
31
|
+
{ id: "d1", companyId: "c1", value: 10 },
|
|
32
|
+
{ id: "d2", companyId: "c2", value: 20 },
|
|
33
|
+
{ id: "d3", companyId: "c2", value: 30 },
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("identity change re-syncs every entity", () => {
|
|
38
|
+
let env: TestEnv | null = null;
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
if (env) await env.dispose();
|
|
41
|
+
env = null;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("anonymous → signed-in via notifySessionChanged pulls all entities", async () => {
|
|
45
|
+
env = createTestEnv({ visible: signedInOnly });
|
|
46
|
+
seedCrm(env);
|
|
47
|
+
await env.start();
|
|
48
|
+
expect(env.engine.store.list("Company")).toHaveLength(0);
|
|
49
|
+
expect(env.engine.store.list("Deal")).toHaveLength(0);
|
|
50
|
+
|
|
51
|
+
// What persistSession() does: store the token, then refresh.
|
|
52
|
+
env.signIn({ userId: "u1", tenantId: null });
|
|
53
|
+
await env.engine.notifySessionChanged();
|
|
54
|
+
await env.flush(50);
|
|
55
|
+
|
|
56
|
+
expect(env.engine.resolvedSession().userId).toBe("u1");
|
|
57
|
+
expect(env.engine.store.list("Company")).toHaveLength(2);
|
|
58
|
+
expect(env.engine.store.list("Deal")).toHaveLength(3);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the live socket reconnects as the signed-in user", async () => {
|
|
62
|
+
env = createTestEnv({ visible: signedInOnly, reconnectDelay: 1 });
|
|
63
|
+
seedCrm(env);
|
|
64
|
+
await env.start();
|
|
65
|
+
await env.flush(50);
|
|
66
|
+
const before = env.transport.wsConnectCount();
|
|
67
|
+
|
|
68
|
+
env.signIn({ userId: "u1", tenantId: null });
|
|
69
|
+
await env.engine.notifySessionChanged();
|
|
70
|
+
await env.flush(50);
|
|
71
|
+
expect(env.transport.wsConnectCount()).toBeGreaterThan(before);
|
|
72
|
+
|
|
73
|
+
env.server.pushToUser("u1", {
|
|
74
|
+
seq: env.server.nextSeqValue(),
|
|
75
|
+
entity: "Deal",
|
|
76
|
+
row_id: "d-live",
|
|
77
|
+
kind: "insert",
|
|
78
|
+
data: { id: "d-live", value: 99 },
|
|
79
|
+
timestamp: "",
|
|
80
|
+
});
|
|
81
|
+
await env.flush(50);
|
|
82
|
+
expect(env.engine.store.get("Deal", "d-live")).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("user A → user B through the session refresh wipes A's rows", async () => {
|
|
86
|
+
env = createTestEnv({
|
|
87
|
+
visible: (_e, rows, auth) =>
|
|
88
|
+
rows.filter((r) => (r as { ownerId?: string }).ownerId === auth.userId),
|
|
89
|
+
});
|
|
90
|
+
env.server.seed("Note", [
|
|
91
|
+
{ id: "a1", ownerId: "uA" },
|
|
92
|
+
{ id: "b1", ownerId: "uB" },
|
|
93
|
+
]);
|
|
94
|
+
env.signIn({ userId: "uA", tenantId: null });
|
|
95
|
+
await env.start();
|
|
96
|
+
expect(env.engine.store.list("Note").map((r) => r.id)).toEqual(["a1"]);
|
|
97
|
+
|
|
98
|
+
env.signIn({ userId: "uB", tenantId: null });
|
|
99
|
+
await env.engine.notifySessionChanged();
|
|
100
|
+
await env.flush(50);
|
|
101
|
+
expect(env.engine.store.list("Note").map((r) => r.id)).toEqual(["b1"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("concurrent scoped reconciles fetch every requested entity", async () => {
|
|
105
|
+
env = createTestEnv();
|
|
106
|
+
env.signIn({ userId: "u1" });
|
|
107
|
+
await env.start();
|
|
108
|
+
// Rows the engine never saw (created before its cursor).
|
|
109
|
+
seedCrm(env);
|
|
110
|
+
|
|
111
|
+
// Two components mount in the same tick and observe their entity.
|
|
112
|
+
const p1 = env.engine.reconcile(["Company"]);
|
|
113
|
+
const p2 = env.engine.reconcile(["Deal"]);
|
|
114
|
+
await Promise.all([p1, p2]);
|
|
115
|
+
await env.flush();
|
|
116
|
+
|
|
117
|
+
expect(env.engine.store.list("Company")).toHaveLength(2);
|
|
118
|
+
expect(env.engine.store.list("Deal")).toHaveLength(3);
|
|
119
|
+
});
|
|
120
|
+
});
|