cojson 0.20.16 → 0.20.18
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/ClockOffset.d.ts +21 -0
- package/dist/ClockOffset.d.ts.map +1 -0
- package/dist/ClockOffset.js +55 -0
- package/dist/ClockOffset.js.map +1 -0
- package/dist/coValueCore/coValueCore.js +2 -2
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/localNode.d.ts +16 -3
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +36 -5
- package/dist/localNode.js.map +1 -1
- package/dist/tests/ClockOffset.test.d.ts +2 -0
- package/dist/tests/ClockOffset.test.d.ts.map +1 -0
- package/dist/tests/ClockOffset.test.js +146 -0
- package/dist/tests/ClockOffset.test.js.map +1 -0
- package/dist/tests/clockDrift.integration.test.d.ts +2 -0
- package/dist/tests/clockDrift.integration.test.d.ts.map +1 -0
- package/dist/tests/clockDrift.integration.test.js +177 -0
- package/dist/tests/clockDrift.integration.test.js.map +1 -0
- package/dist/tests/localNode.clockOffset.test.d.ts +2 -0
- package/dist/tests/localNode.clockOffset.test.d.ts.map +1 -0
- package/dist/tests/localNode.clockOffset.test.js +70 -0
- package/dist/tests/localNode.clockOffset.test.js.map +1 -0
- package/dist/tests/testUtils.d.ts +2 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +9 -2
- package/dist/tests/testUtils.js.map +1 -1
- package/package.json +4 -4
- package/src/ClockOffset.ts +83 -0
- package/src/coValueCore/coValueCore.ts +2 -2
- package/src/localNode.ts +31 -0
- package/src/tests/ClockOffset.test.ts +177 -0
- package/src/tests/clockDrift.integration.test.ts +261 -0
- package/src/tests/localNode.clockOffset.test.ts +92 -0
- package/src/tests/testUtils.ts +16 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import {
|
|
4
|
+
loadCoValueOrFail,
|
|
5
|
+
setupTestAccount,
|
|
6
|
+
setupTestNode,
|
|
7
|
+
waitFor,
|
|
8
|
+
} from "./testUtils.js";
|
|
9
|
+
|
|
10
|
+
const CLIENT_SKEW_MS = 20_000;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("clock drift across peers with clock sync enabled", () => {
|
|
17
|
+
test("worker can publish and self-remove after a skewed client with clock sync creates group, grant and chat", async () => {
|
|
18
|
+
const errorSpy = vi.spyOn(logger, "error");
|
|
19
|
+
|
|
20
|
+
const realNow = Math.floor(performance.timeOrigin + performance.now());
|
|
21
|
+
|
|
22
|
+
vi.spyOn(Date, "now").mockImplementation(() => {
|
|
23
|
+
const real = performance.timeOrigin + performance.now();
|
|
24
|
+
return Math.floor(real) + CLIENT_SKEW_MS;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const worker = await setupTestAccount({
|
|
28
|
+
isSyncServer: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const client = await setupTestAccount({
|
|
32
|
+
connected: true,
|
|
33
|
+
experimental_clockSyncFromServerPings: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const skewedNow = Date.now();
|
|
37
|
+
client.node.clockOffset.addSample({
|
|
38
|
+
serverTime: skewedNow - CLIENT_SKEW_MS,
|
|
39
|
+
localReceiveTime: skewedNow,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const group = client.node.createGroup();
|
|
43
|
+
|
|
44
|
+
const workerAccountOnClient = await loadCoValueOrFail(
|
|
45
|
+
client.node,
|
|
46
|
+
worker.accountID,
|
|
47
|
+
);
|
|
48
|
+
group.addMember(workerAccountOnClient, "admin");
|
|
49
|
+
|
|
50
|
+
const oneToOneChat = group.createMap();
|
|
51
|
+
oneToOneChat.set("kind", "OneToOneChat", "trusting");
|
|
52
|
+
oneToOneChat.set("published", false, "trusting");
|
|
53
|
+
|
|
54
|
+
await oneToOneChat.core.waitForSync();
|
|
55
|
+
await group.core.waitForSync();
|
|
56
|
+
|
|
57
|
+
const clientGroupTxs = group.core.getValidSortedTransactions();
|
|
58
|
+
const maxClientGroupMadeAt = Math.max(
|
|
59
|
+
...clientGroupTxs.map((tx) => tx.madeAt),
|
|
60
|
+
);
|
|
61
|
+
expect(maxClientGroupMadeAt).toBeLessThan(realNow + CLIENT_SKEW_MS - 5_000);
|
|
62
|
+
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
const errorSpyAfter = vi.spyOn(logger, "error");
|
|
65
|
+
|
|
66
|
+
const chatOnWorker = await loadCoValueOrFail(worker.node, oneToOneChat.id);
|
|
67
|
+
const groupOnWorker = await loadCoValueOrFail(worker.node, group.id);
|
|
68
|
+
const workerAccountOnWorker = await loadCoValueOrFail(
|
|
69
|
+
worker.node,
|
|
70
|
+
worker.accountID,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(chatOnWorker.get("kind")).toBe("OneToOneChat");
|
|
75
|
+
expect(groupOnWorker.roleOf(worker.accountID)).toBe("admin");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
chatOnWorker.set("published", true, "trusting");
|
|
79
|
+
groupOnWorker.removeMember(workerAccountOnWorker);
|
|
80
|
+
|
|
81
|
+
await chatOnWorker.core.waitForSync();
|
|
82
|
+
await groupOnWorker.core.waitForSync();
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(oneToOneChat.get("published")).toBe(true);
|
|
86
|
+
expect(group.roleOf(worker.accountID)).not.toBe("admin");
|
|
87
|
+
expect(chatOnWorker.get("published")).toBe(true);
|
|
88
|
+
expect(groupOnWorker.roleOf(worker.accountID)).not.toBe("admin");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(oneToOneChat.get("published")).toBe(true);
|
|
92
|
+
expect(chatOnWorker.get("published")).toBe(true);
|
|
93
|
+
expect(group.roleOf(worker.accountID)).not.toBe("admin");
|
|
94
|
+
expect(groupOnWorker.roleOf(worker.accountID)).not.toBe("admin");
|
|
95
|
+
|
|
96
|
+
const forbiddenFragments = [
|
|
97
|
+
"invalid transaction",
|
|
98
|
+
"permission",
|
|
99
|
+
"rejected",
|
|
100
|
+
"not authorized",
|
|
101
|
+
"not authorised",
|
|
102
|
+
];
|
|
103
|
+
const offendingCalls = [...errorSpy.mock.calls, ...errorSpyAfter.mock.calls]
|
|
104
|
+
.map((call) => call.map((arg) => String(arg)).join(" "))
|
|
105
|
+
.filter((line) =>
|
|
106
|
+
forbiddenFragments.some((f) => line.toLowerCase().includes(f)),
|
|
107
|
+
);
|
|
108
|
+
expect(offendingCalls).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("experimental_clockSyncFromServerPings flag wiring", () => {
|
|
113
|
+
function seedOffset(
|
|
114
|
+
clockOffset: {
|
|
115
|
+
addSample: (s: { serverTime: number; localReceiveTime: number }) => void;
|
|
116
|
+
},
|
|
117
|
+
offsetMs: number,
|
|
118
|
+
) {
|
|
119
|
+
const localReceiveTime = Date.now();
|
|
120
|
+
clockOffset.addSample({
|
|
121
|
+
serverTime: localReceiveTime + offsetMs,
|
|
122
|
+
localReceiveTime,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
test("with the flag on, a seeded +10_000 ms offset pulls locally-stamped madeAt forward", () => {
|
|
127
|
+
const { node } = setupTestNode({
|
|
128
|
+
experimental_clockSyncFromServerPings: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
seedOffset(node.clockOffset, 10_000);
|
|
132
|
+
|
|
133
|
+
const group = node.createGroup();
|
|
134
|
+
const map = group.createMap();
|
|
135
|
+
|
|
136
|
+
const before = Date.now();
|
|
137
|
+
map.set("k", "v", "trusting");
|
|
138
|
+
const after = Date.now();
|
|
139
|
+
|
|
140
|
+
const txs = map.core.getValidSortedTransactions();
|
|
141
|
+
const lastTx = txs.at(-1);
|
|
142
|
+
expect(lastTx).toBeDefined();
|
|
143
|
+
expect(lastTx!.madeAt).toBeGreaterThanOrEqual(before + 9_900);
|
|
144
|
+
expect(lastTx!.madeAt).toBeLessThanOrEqual(after + 10_100);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("without the flag, a seeded +10_000 ms offset does NOT shift locally-stamped madeAt", () => {
|
|
148
|
+
const { node } = setupTestNode({
|
|
149
|
+
experimental_clockSyncFromServerPings: false,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
seedOffset(node.clockOffset, 10_000);
|
|
153
|
+
|
|
154
|
+
const group = node.createGroup();
|
|
155
|
+
const map = group.createMap();
|
|
156
|
+
|
|
157
|
+
const before = Date.now();
|
|
158
|
+
map.set("k", "v", "trusting");
|
|
159
|
+
const after = Date.now();
|
|
160
|
+
|
|
161
|
+
const txs = map.core.getValidSortedTransactions();
|
|
162
|
+
const lastTx = txs.at(-1);
|
|
163
|
+
expect(lastTx).toBeDefined();
|
|
164
|
+
expect(lastTx!.madeAt).toBeGreaterThanOrEqual(before);
|
|
165
|
+
expect(lastTx!.madeAt).toBeLessThanOrEqual(after + 100);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("clock sync pulls skewed client stamps toward server time", () => {
|
|
170
|
+
test("with both nodes flag-on, a client whose wall clock is 20s ahead authors transactions that land near real time on the worker", async () => {
|
|
171
|
+
const errorSpy = vi.spyOn(logger, "error");
|
|
172
|
+
|
|
173
|
+
const worker = await setupTestAccount({
|
|
174
|
+
isSyncServer: true,
|
|
175
|
+
experimental_clockSyncFromServerPings: true,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const realNowBeforeClient = Date.now();
|
|
179
|
+
|
|
180
|
+
const SKEW_MS = 20_000;
|
|
181
|
+
vi.spyOn(Date, "now").mockImplementation(() => {
|
|
182
|
+
const real = Math.floor(performance.timeOrigin + performance.now());
|
|
183
|
+
return real + SKEW_MS;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const client = await setupTestAccount({
|
|
187
|
+
connected: true,
|
|
188
|
+
experimental_clockSyncFromServerPings: true,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const localReceiveTime = Date.now();
|
|
192
|
+
client.node.clockOffset.addSample({
|
|
193
|
+
serverTime: localReceiveTime - SKEW_MS,
|
|
194
|
+
localReceiveTime,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(client.node.clockOffset.currentOffset()).toBeLessThanOrEqual(
|
|
198
|
+
-SKEW_MS + 100,
|
|
199
|
+
);
|
|
200
|
+
expect(client.node.clockOffset.currentOffset()).toBeGreaterThanOrEqual(
|
|
201
|
+
-SKEW_MS - 100,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const group = client.node.createGroup();
|
|
205
|
+
group.addMember("everyone", "writer");
|
|
206
|
+
|
|
207
|
+
const map = group.createMap();
|
|
208
|
+
map.set("from", "skewed-client", "trusting");
|
|
209
|
+
|
|
210
|
+
await map.core.waitForSync();
|
|
211
|
+
await group.core.waitForSync();
|
|
212
|
+
|
|
213
|
+
vi.restoreAllMocks();
|
|
214
|
+
const errorSpyAfter = vi.spyOn(logger, "error");
|
|
215
|
+
|
|
216
|
+
const realNowAfter = Date.now();
|
|
217
|
+
|
|
218
|
+
const mapOnWorker = await loadCoValueOrFail(worker.node, map.id);
|
|
219
|
+
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(mapOnWorker.get("from")).toBe("skewed-client");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const clientTxsOnWorker = mapOnWorker.core.getValidSortedTransactions();
|
|
225
|
+
const setTx = clientTxsOnWorker.find((tx) => {
|
|
226
|
+
const changes = tx.changes as ReadonlyArray<{
|
|
227
|
+
op?: string;
|
|
228
|
+
key?: string;
|
|
229
|
+
value?: unknown;
|
|
230
|
+
}>;
|
|
231
|
+
return (
|
|
232
|
+
changes?.[0]?.key === "from" && changes[0]?.value === "skewed-client"
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(setTx).toBeDefined();
|
|
237
|
+
|
|
238
|
+
const TOLERANCE_MS = 2_000;
|
|
239
|
+
expect(setTx!.madeAt).toBeGreaterThanOrEqual(
|
|
240
|
+
realNowBeforeClient - TOLERANCE_MS,
|
|
241
|
+
);
|
|
242
|
+
expect(setTx!.madeAt).toBeLessThanOrEqual(realNowAfter + TOLERANCE_MS);
|
|
243
|
+
|
|
244
|
+
expect(setTx!.madeAt).toBeLessThan(realNowAfter + SKEW_MS - 5_000);
|
|
245
|
+
|
|
246
|
+
const forbiddenFragments = [
|
|
247
|
+
"invalid transaction",
|
|
248
|
+
"permission",
|
|
249
|
+
"rejected",
|
|
250
|
+
"not authorized",
|
|
251
|
+
"not authorised",
|
|
252
|
+
"out of order",
|
|
253
|
+
];
|
|
254
|
+
const offendingCalls = [...errorSpy.mock.calls, ...errorSpyAfter.mock.calls]
|
|
255
|
+
.map((call) => call.map((arg) => String(arg)).join(" "))
|
|
256
|
+
.filter((line) =>
|
|
257
|
+
forbiddenFragments.some((f) => line.toLowerCase().includes(f)),
|
|
258
|
+
);
|
|
259
|
+
expect(offendingCalls).toEqual([]);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { ClockOffset } from "../ClockOffset.js";
|
|
3
|
+
import { LocalNode } from "../localNode.js";
|
|
4
|
+
import { randomAgentAndSessionID } from "./testUtils.js";
|
|
5
|
+
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
6
|
+
|
|
7
|
+
const Crypto = await WasmCrypto.create();
|
|
8
|
+
|
|
9
|
+
function makeNode(opts: { experimental_clockSyncFromServerPings?: boolean }) {
|
|
10
|
+
const [admin, session] = randomAgentAndSessionID();
|
|
11
|
+
return new LocalNode(
|
|
12
|
+
admin.agentSecret,
|
|
13
|
+
session,
|
|
14
|
+
Crypto,
|
|
15
|
+
undefined,
|
|
16
|
+
undefined,
|
|
17
|
+
opts,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("LocalNode clock offset wiring", () => {
|
|
22
|
+
test("clockOffset is always present on the node, regardless of flag state", () => {
|
|
23
|
+
const withFlag = makeNode({ experimental_clockSyncFromServerPings: true });
|
|
24
|
+
const withoutFlag = makeNode({
|
|
25
|
+
experimental_clockSyncFromServerPings: false,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(withFlag.clockOffset).toBeInstanceOf(ClockOffset);
|
|
29
|
+
expect(withoutFlag.clockOffset).toBeInstanceOf(ClockOffset);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("with the flag enabled, seeding clockOffset with a +10_000 ms sample pulls stampNow() forward by ~10_000 ms", () => {
|
|
33
|
+
const flagged = makeNode({ experimental_clockSyncFromServerPings: true });
|
|
34
|
+
const control = makeNode({
|
|
35
|
+
experimental_clockSyncFromServerPings: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const localReceiveTime = Date.now();
|
|
39
|
+
flagged.clockOffset.addSample({
|
|
40
|
+
serverTime: localReceiveTime + 10_000,
|
|
41
|
+
localReceiveTime,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const controlStamp = control.stampNow();
|
|
45
|
+
const flaggedStamp = flagged.stampNow();
|
|
46
|
+
|
|
47
|
+
const delta = flaggedStamp - controlStamp;
|
|
48
|
+
expect(delta).toBeGreaterThanOrEqual(9_900);
|
|
49
|
+
expect(delta).toBeLessThanOrEqual(10_100);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("getClockOffsetDiagnostics() reflects current offset and sample count", () => {
|
|
53
|
+
const node = makeNode({ experimental_clockSyncFromServerPings: true });
|
|
54
|
+
|
|
55
|
+
expect(node.getClockOffsetDiagnostics()).toEqual({
|
|
56
|
+
currentOffset: 0,
|
|
57
|
+
sampleCount: 0,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const base = Date.now();
|
|
61
|
+
node.clockOffset.addSample({
|
|
62
|
+
serverTime: base + 400,
|
|
63
|
+
localReceiveTime: base,
|
|
64
|
+
});
|
|
65
|
+
node.clockOffset.addSample({
|
|
66
|
+
serverTime: base + 600,
|
|
67
|
+
localReceiveTime: base,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(node.getClockOffsetDiagnostics()).toEqual({
|
|
71
|
+
currentOffset: 500,
|
|
72
|
+
sampleCount: 2,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("without the flag, seeding clockOffset with a +10_000 ms sample does NOT affect stampNow()", () => {
|
|
77
|
+
const node = makeNode({ experimental_clockSyncFromServerPings: false });
|
|
78
|
+
|
|
79
|
+
const localReceiveTime = Date.now();
|
|
80
|
+
node.clockOffset.addSample({
|
|
81
|
+
serverTime: localReceiveTime + 10_000,
|
|
82
|
+
localReceiveTime,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const before = Date.now();
|
|
86
|
+
const stamp = node.stampNow();
|
|
87
|
+
const after = Date.now();
|
|
88
|
+
|
|
89
|
+
expect(stamp).toBeGreaterThanOrEqual(before);
|
|
90
|
+
expect(stamp).toBeLessThanOrEqual(after + 100);
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/tests/testUtils.ts
CHANGED
|
@@ -452,6 +452,7 @@ export function setupTestNode(
|
|
|
452
452
|
secret?: AgentSecret;
|
|
453
453
|
syncWhen?: SyncWhen;
|
|
454
454
|
enableFullStorageReconciliation?: boolean;
|
|
455
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
455
456
|
} = {},
|
|
456
457
|
) {
|
|
457
458
|
const [admin, session] = opts.secret
|
|
@@ -464,6 +465,10 @@ export function setupTestNode(
|
|
|
464
465
|
Crypto,
|
|
465
466
|
opts.syncWhen,
|
|
466
467
|
opts.enableFullStorageReconciliation,
|
|
468
|
+
{
|
|
469
|
+
experimental_clockSyncFromServerPings:
|
|
470
|
+
opts.experimental_clockSyncFromServerPings,
|
|
471
|
+
},
|
|
467
472
|
);
|
|
468
473
|
|
|
469
474
|
if (opts.isSyncServer) {
|
|
@@ -542,6 +547,10 @@ export function setupTestNode(
|
|
|
542
547
|
Crypto,
|
|
543
548
|
opts.syncWhen,
|
|
544
549
|
opts.enableFullStorageReconciliation,
|
|
550
|
+
{
|
|
551
|
+
experimental_clockSyncFromServerPings:
|
|
552
|
+
opts.experimental_clockSyncFromServerPings,
|
|
553
|
+
},
|
|
545
554
|
);
|
|
546
555
|
|
|
547
556
|
if (opts.isSyncServer) {
|
|
@@ -556,6 +565,8 @@ export function setupTestNode(
|
|
|
556
565
|
connected: opts.connected,
|
|
557
566
|
isSyncServer: opts.isSyncServer,
|
|
558
567
|
enableFullStorageReconciliation: opts.enableFullStorageReconciliation,
|
|
568
|
+
experimental_clockSyncFromServerPings:
|
|
569
|
+
opts.experimental_clockSyncFromServerPings,
|
|
559
570
|
});
|
|
560
571
|
},
|
|
561
572
|
disconnect: () => {
|
|
@@ -577,6 +588,7 @@ export async function setupTestAccount(
|
|
|
577
588
|
storage?: StorageAPI;
|
|
578
589
|
accountID?: RawAccountID;
|
|
579
590
|
accountSecret?: AgentSecret;
|
|
591
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
580
592
|
} = {},
|
|
581
593
|
) {
|
|
582
594
|
const ctx =
|
|
@@ -593,6 +605,8 @@ export async function setupTestAccount(
|
|
|
593
605
|
accountID: opts.accountID,
|
|
594
606
|
accountSecret: opts.accountSecret,
|
|
595
607
|
sessionID: Crypto.newRandomSessionID(opts.accountID),
|
|
608
|
+
experimental_clockSyncFromServerPings:
|
|
609
|
+
opts.experimental_clockSyncFromServerPings,
|
|
596
610
|
}),
|
|
597
611
|
accountID: opts.accountID,
|
|
598
612
|
accountSecret: opts.accountSecret,
|
|
@@ -602,6 +616,8 @@ export async function setupTestAccount(
|
|
|
602
616
|
crypto: Crypto,
|
|
603
617
|
creationProps: { name: "Client" },
|
|
604
618
|
storage: opts.storage,
|
|
619
|
+
experimental_clockSyncFromServerPings:
|
|
620
|
+
opts.experimental_clockSyncFromServerPings,
|
|
605
621
|
});
|
|
606
622
|
|
|
607
623
|
if (opts.isSyncServer) {
|