@evolu/common 8.10.0 → 8.12.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/src/Bytes.d.ts +39 -2
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +50 -2
- package/dist/src/Config.d.ts +22 -22
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +92 -18
- package/dist/src/Fs.d.ts.map +1 -1
- package/dist/src/Fs.js +2 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +63 -9
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +106 -9
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +55 -4
- package/dist/src/Type.d.ts +1455 -1310
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1274 -517
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +6 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +412 -213
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +181 -18
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +106 -19
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +162 -60
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +346 -23
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +195 -17
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +85 -22
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +403 -81
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- package/src/Config.test.ts +2 -6
- package/src/Config.ts +133 -133
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.ts +92 -18
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Object.test.ts +27 -12
- package/src/Object.ts +5 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +137 -17
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.ts +106 -9
- package/src/Type.test.ts +946 -1028
- package/src/Type.ts +4195 -3136
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +20 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +994 -22
- package/src/local-first/Evolu.ts +625 -232
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +634 -10
- package/src/local-first/Protocol.ts +255 -109
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +376 -26
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +224 -36
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +434 -118
- package/src/local-first/index.ts +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- package/src/local-first/LocalAuth.ts +0 -457
|
@@ -13,20 +13,41 @@ import {
|
|
|
13
13
|
assertThrowsInstanceOf,
|
|
14
14
|
assertTrue,
|
|
15
15
|
} from "../Assert.ts";
|
|
16
|
+
import * as fc from "fast-check";
|
|
16
17
|
import { describe, it } from "node:test";
|
|
17
18
|
import type { Brand } from "../Brand.ts";
|
|
19
|
+
import { createBuffer } from "../Bytes.ts";
|
|
18
20
|
import type { ConsoleEntry, TestConsole } from "../Console.ts";
|
|
19
21
|
import { testCreateConsole } from "../Console.ts";
|
|
20
22
|
import { constVoid } from "../Function.ts";
|
|
21
|
-
import type { DbWorkerInit } from "./Db.ts";
|
|
23
|
+
import type { DbWorkerInit, UnsupportedDbVersionError } from "./Db.ts";
|
|
22
24
|
import {
|
|
23
25
|
AppName,
|
|
24
26
|
createEvolu,
|
|
25
27
|
createEvoluDeps,
|
|
28
|
+
type Evolu,
|
|
29
|
+
type EvoluConfig,
|
|
30
|
+
maxMutationSize,
|
|
26
31
|
testAppName,
|
|
27
32
|
type EvoluPlatformDeps,
|
|
28
33
|
} from "./Evolu.ts";
|
|
29
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
createOwnerSecret,
|
|
36
|
+
createOwnerWebSocketTransport,
|
|
37
|
+
createSharedOwner,
|
|
38
|
+
testAppOwner,
|
|
39
|
+
} from "./Owner.ts";
|
|
40
|
+
import {
|
|
41
|
+
createProtocolBroadcastMessagesFromCrdtMessages,
|
|
42
|
+
createProtocolMessageBuffer,
|
|
43
|
+
createProtocolMessageFromCrdtMessages,
|
|
44
|
+
createTimestampsBuffer,
|
|
45
|
+
encodeAndEncryptDbChange,
|
|
46
|
+
encodeDbChange,
|
|
47
|
+
MessageType,
|
|
48
|
+
ProtocolErrorCode,
|
|
49
|
+
ProtocolMessageRangesMaxSize,
|
|
50
|
+
} from "./Protocol.ts";
|
|
30
51
|
import { createQueryBuilder } from "./Schema.ts";
|
|
31
52
|
import {
|
|
32
53
|
consoleEntryOrErrorBroadcastChannelName,
|
|
@@ -36,23 +57,37 @@ import {
|
|
|
36
57
|
type EvoluInput,
|
|
37
58
|
type EvoluOutput,
|
|
38
59
|
type SharedWorker,
|
|
60
|
+
type SharedWorkerId,
|
|
39
61
|
type SharedWorkerInput,
|
|
40
62
|
type SharedWorkerOutput,
|
|
63
|
+
type SyncState,
|
|
41
64
|
} from "./Shared.ts";
|
|
42
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
acquireLeaderLock,
|
|
67
|
+
type LockManagerDep,
|
|
68
|
+
testCreateLockManager,
|
|
69
|
+
} from "../LockManager.ts";
|
|
43
70
|
import { installPolyfills } from "../Polyfills.ts";
|
|
44
71
|
import { err, ok } from "../Result.ts";
|
|
45
72
|
import { SqliteBoolean } from "../Sqlite.ts";
|
|
46
|
-
import { explicitAbortReason, testCreateRun } from "../Task.ts";
|
|
73
|
+
import { explicitAbortReason, testCreateDeps, testCreateRun } from "../Task.ts";
|
|
74
|
+
import { testCreateId } from "../Test.ts";
|
|
47
75
|
import {
|
|
48
76
|
assertType,
|
|
49
77
|
createIdFromString,
|
|
78
|
+
FiniteNumber,
|
|
50
79
|
id,
|
|
51
80
|
NonEmptyTrimmedString100,
|
|
52
81
|
nullOr,
|
|
82
|
+
PositiveInt,
|
|
83
|
+
String as StringType,
|
|
53
84
|
testName,
|
|
85
|
+
Name,
|
|
86
|
+
Uint8Array as Uint8ArrayType,
|
|
54
87
|
} from "../Type.ts";
|
|
55
88
|
import type { ExtractTyped } from "../Type.ts";
|
|
89
|
+
import { DbChange } from "./Storage.ts";
|
|
90
|
+
import { createTimestamp, type NodeId } from "./Timestamp.ts";
|
|
56
91
|
import {
|
|
57
92
|
testCreateBroadcastChannel,
|
|
58
93
|
testCreateMessageChannel,
|
|
@@ -203,6 +238,8 @@ describe("Evolu", () => {
|
|
|
203
238
|
}),
|
|
204
239
|
);
|
|
205
240
|
|
|
241
|
+
assertThrowsInstanceOf(() => AppName.orThrow("a".repeat(42)), Error);
|
|
242
|
+
|
|
206
243
|
const appName = AppName.orThrow("my-app");
|
|
207
244
|
assertType<typeof appName, AppName>();
|
|
208
245
|
assertType<typeof AppName.Input, string>();
|
|
@@ -212,12 +249,22 @@ describe("Evolu", () => {
|
|
|
212
249
|
describe("createEvoluDeps", () => {
|
|
213
250
|
const setupCreateEvoluDeps = async (
|
|
214
251
|
console: TestConsole = testCreateConsole(),
|
|
252
|
+
lockManager: LockManagerDep["lockManager"] = testCreateLockManager(),
|
|
215
253
|
): Promise<{
|
|
216
254
|
readonly deps: ReturnType<typeof createEvoluDeps>;
|
|
217
255
|
readonly messages: Array<SharedWorkerInput>;
|
|
256
|
+
readonly sharedWorkerPort: MessagePort<
|
|
257
|
+
SharedWorkerOutput,
|
|
258
|
+
SharedWorkerInput
|
|
259
|
+
>;
|
|
218
260
|
readonly consoleEntryOrErrorBroadcastChannel: {
|
|
219
261
|
readonly postMessage: (message: ConsoleEntryOrError) => void;
|
|
220
262
|
};
|
|
263
|
+
/**
|
|
264
|
+
* Sends the worker's Connected message, as a worker holding its lock
|
|
265
|
+
* does.
|
|
266
|
+
*/
|
|
267
|
+
readonly connect: (workerId: SharedWorkerId) => Promise<void>;
|
|
221
268
|
readonly [Symbol.dispose]: () => void;
|
|
222
269
|
}> => {
|
|
223
270
|
using disposer = new DisposableStack();
|
|
@@ -226,7 +273,11 @@ describe("Evolu", () => {
|
|
|
226
273
|
);
|
|
227
274
|
|
|
228
275
|
const messages: Array<SharedWorkerInput> = [];
|
|
276
|
+
const sharedWorkerPort: {
|
|
277
|
+
value: MessagePort<SharedWorkerOutput, SharedWorkerInput> | null;
|
|
278
|
+
} = { value: null };
|
|
229
279
|
worker.self.onConnect = (port) => {
|
|
280
|
+
sharedWorkerPort.value = port;
|
|
230
281
|
port.onMessage = (message) => {
|
|
231
282
|
messages.push(message);
|
|
232
283
|
};
|
|
@@ -238,7 +289,7 @@ describe("Evolu", () => {
|
|
|
238
289
|
createDbWorker: testCreateWorker,
|
|
239
290
|
createBroadcastChannel: testCreateBroadcastChannel,
|
|
240
291
|
createMessageChannel: testCreateMessageChannel,
|
|
241
|
-
lockManager
|
|
292
|
+
lockManager,
|
|
242
293
|
sharedWorker: worker,
|
|
243
294
|
reloadApp: constVoid,
|
|
244
295
|
console,
|
|
@@ -247,8 +298,10 @@ describe("Evolu", () => {
|
|
|
247
298
|
|
|
248
299
|
await testWaitForWorkerMessage();
|
|
249
300
|
|
|
250
|
-
|
|
251
|
-
|
|
301
|
+
// A tab says nothing until its worker connects.
|
|
302
|
+
assertEqual(messages, []);
|
|
303
|
+
const port = sharedWorkerPort.value;
|
|
304
|
+
assertNotNull(port);
|
|
252
305
|
const consoleEntryOrErrorBroadcastChannel = disposer.use(
|
|
253
306
|
testCreateBroadcastChannel<ConsoleEntryOrError>(
|
|
254
307
|
consoleEntryOrErrorBroadcastChannelName,
|
|
@@ -259,21 +312,110 @@ describe("Evolu", () => {
|
|
|
259
312
|
return {
|
|
260
313
|
deps,
|
|
261
314
|
messages,
|
|
315
|
+
sharedWorkerPort: port,
|
|
262
316
|
consoleEntryOrErrorBroadcastChannel,
|
|
317
|
+
connect: async (workerId) => {
|
|
318
|
+
port.postMessage({
|
|
319
|
+
type: "Connected",
|
|
320
|
+
workerId,
|
|
321
|
+
syncStateChannelName: `evolu:sync-state:${workerId}`,
|
|
322
|
+
});
|
|
323
|
+
await testWaitForWorkerMessage();
|
|
324
|
+
},
|
|
263
325
|
[Symbol.dispose]: () => disposables.dispose(),
|
|
264
326
|
};
|
|
265
327
|
};
|
|
266
328
|
|
|
267
|
-
it("
|
|
329
|
+
it("reports an OtherBuildRunningError its worker sends and clears it once the worker connects", async () => {
|
|
330
|
+
using setup = await setupCreateEvoluDeps();
|
|
331
|
+
|
|
332
|
+
setup.sharedWorkerPort.postMessage({
|
|
333
|
+
type: "Error",
|
|
334
|
+
error: { type: "OtherBuildRunningError" },
|
|
335
|
+
});
|
|
336
|
+
await testWaitForWorkerMessage();
|
|
337
|
+
assertEqual(setup.deps.evoluError.get(), {
|
|
338
|
+
type: "OtherBuildRunningError",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await setup.connect(testCreateId()<"SharedWorker">());
|
|
342
|
+
assertSame(setup.deps.evoluError.get(), null);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("keeps another error when its worker connects", async () => {
|
|
346
|
+
using setup = await setupCreateEvoluDeps();
|
|
347
|
+
const error: UnsupportedDbVersionError = {
|
|
348
|
+
type: "UnsupportedDbVersionError",
|
|
349
|
+
storedVersion: PositiveInt.orThrow(3),
|
|
350
|
+
supportedVersion: PositiveInt.orThrow(2),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
setup.sharedWorkerPort.postMessage({ type: "Error", error });
|
|
354
|
+
await testWaitForWorkerMessage();
|
|
355
|
+
await setup.connect(testCreateId()<"SharedWorker">());
|
|
356
|
+
|
|
357
|
+
assertEqual(setup.deps.evoluError.get(), error);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("says nothing to a worker that reports it waits", async () => {
|
|
361
|
+
using setup = await setupCreateEvoluDeps();
|
|
362
|
+
|
|
363
|
+
setup.sharedWorkerPort.postMessage({
|
|
364
|
+
type: "Waiting",
|
|
365
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
366
|
+
});
|
|
367
|
+
await testWaitForWorkerMessage();
|
|
368
|
+
|
|
369
|
+
assertEqual(setup.messages, []);
|
|
370
|
+
assertSame(setup.deps.evoluError.get(), null);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("says nothing to a worker that reports it keeps databases in memory", async () => {
|
|
374
|
+
using setup = await setupCreateEvoluDeps();
|
|
375
|
+
|
|
376
|
+
setup.sharedWorkerPort.postMessage({ type: "StorageUnavailable" });
|
|
377
|
+
await testWaitForWorkerMessage();
|
|
378
|
+
|
|
379
|
+
assertEqual(setup.messages, []);
|
|
380
|
+
assertSame(setup.deps.evoluError.get(), null);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("announces itself as tab leader with its console level once its worker connects", async () => {
|
|
268
384
|
const testConsole = testCreateConsole();
|
|
269
385
|
using setup = await setupCreateEvoluDeps(testConsole);
|
|
270
|
-
const { messages } = setup;
|
|
386
|
+
const { messages, connect } = setup;
|
|
271
387
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
388
|
+
await connect(testCreateId()<"SharedWorker">());
|
|
389
|
+
|
|
390
|
+
assertEqual(messages, [
|
|
391
|
+
{ type: "RequestSyncState" },
|
|
392
|
+
{ type: "AnnounceTabLeader", consoleLevel: testConsole.getLevel() },
|
|
393
|
+
]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("competes for tab leadership only with tabs of its worker", async () => {
|
|
397
|
+
const lockManager = testCreateLockManager();
|
|
398
|
+
await using run = testCreateRun({ lockManager });
|
|
399
|
+
const createWorkerId = testCreateId();
|
|
400
|
+
const worker = createWorkerId<"SharedWorker">();
|
|
401
|
+
// The tab's worker holds this lock for its lifetime.
|
|
402
|
+
await using _workerBuildLock = await run.ok(acquireLeaderLock("tab"));
|
|
403
|
+
using first = await setupCreateEvoluDeps(undefined, lockManager);
|
|
404
|
+
using second = await setupCreateEvoluDeps(undefined, lockManager);
|
|
405
|
+
using ofOtherWorker = await setupCreateEvoluDeps(undefined, lockManager);
|
|
406
|
+
await first.connect(worker);
|
|
407
|
+
await second.connect(worker);
|
|
408
|
+
await ofOtherWorker.connect(createWorkerId<"SharedWorker">());
|
|
409
|
+
const isLeader = (setup: typeof first): boolean =>
|
|
410
|
+
setup.messages.some((message) => message.type === "AnnounceTabLeader");
|
|
411
|
+
|
|
412
|
+
assertTrue(isLeader(first));
|
|
413
|
+
assertFalse(isLeader(second));
|
|
414
|
+
assertTrue(isLeader(ofOtherWorker));
|
|
415
|
+
|
|
416
|
+
first[Symbol.dispose]();
|
|
417
|
+
await testWaitForWorkerMessage();
|
|
418
|
+
assertTrue(isLeader(second));
|
|
277
419
|
});
|
|
278
420
|
|
|
279
421
|
it("initializes db worker from shared worker init output", async () => {
|
|
@@ -367,6 +509,11 @@ describe("Evolu", () => {
|
|
|
367
509
|
port.onMessage = (message) => {
|
|
368
510
|
messages.push(message);
|
|
369
511
|
};
|
|
512
|
+
port.postMessage({
|
|
513
|
+
type: "Connected",
|
|
514
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
515
|
+
syncStateChannelName: "evolu:sync-state:worker",
|
|
516
|
+
});
|
|
370
517
|
};
|
|
371
518
|
worker.connect();
|
|
372
519
|
|
|
@@ -382,8 +529,11 @@ describe("Evolu", () => {
|
|
|
382
529
|
await testWaitForWorkerMessage();
|
|
383
530
|
|
|
384
531
|
assertNotUndefined(deps.console);
|
|
385
|
-
|
|
386
|
-
|
|
532
|
+
const messageTypes = ["RequestSyncState", "AnnounceTabLeader"];
|
|
533
|
+
assertEqual(
|
|
534
|
+
messages.map((message) => message.type),
|
|
535
|
+
messageTypes,
|
|
536
|
+
);
|
|
387
537
|
using consoleEntryOrErrorBroadcastChannel =
|
|
388
538
|
testCreateBroadcastChannel<ConsoleEntryOrError>(
|
|
389
539
|
consoleEntryOrErrorBroadcastChannelName,
|
|
@@ -396,8 +546,10 @@ describe("Evolu", () => {
|
|
|
396
546
|
|
|
397
547
|
await testWaitForWorkerMessage();
|
|
398
548
|
|
|
399
|
-
|
|
400
|
-
|
|
549
|
+
assertEqual(
|
|
550
|
+
messages.map((message) => message.type),
|
|
551
|
+
messageTypes,
|
|
552
|
+
);
|
|
401
553
|
assertEqual(deps.evoluError.get(), {
|
|
402
554
|
type: "UnknownError",
|
|
403
555
|
error: ["boom"],
|
|
@@ -428,6 +580,84 @@ describe("Evolu", () => {
|
|
|
428
580
|
assertEqual(testConsole.getEntriesSnapshot(), [entry]);
|
|
429
581
|
});
|
|
430
582
|
|
|
583
|
+
it("follows the sync state channel its worker names", async () => {
|
|
584
|
+
using setup = await setupCreateEvoluDeps();
|
|
585
|
+
const { deps, messages, connect } = setup;
|
|
586
|
+
const workerId = testCreateId()<"SharedWorker">();
|
|
587
|
+
using channel = testCreateBroadcastChannel<SyncState>(
|
|
588
|
+
`evolu:sync-state:${workerId}`,
|
|
589
|
+
);
|
|
590
|
+
using otherChannel = testCreateBroadcastChannel<SyncState>(
|
|
591
|
+
"evolu:sync-state:other-worker",
|
|
592
|
+
);
|
|
593
|
+
const createState = (label: string): SyncState => ({
|
|
594
|
+
transports: [],
|
|
595
|
+
tenants: [{ name: Name.orThrow(label), refused: false, owners: [] }],
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Nothing reaches the tab before its worker names the channel.
|
|
599
|
+
channel.postMessage(createState("early"));
|
|
600
|
+
await testWaitForWorkerMessage();
|
|
601
|
+
assertSame(deps.syncState.get(), null);
|
|
602
|
+
|
|
603
|
+
await connect(workerId);
|
|
604
|
+
assertEqual(messages.at(0), { type: "RequestSyncState" });
|
|
605
|
+
channel.postMessage(createState("requested"));
|
|
606
|
+
await testWaitForWorkerMessage();
|
|
607
|
+
assertEqual(deps.syncState.get(), createState("requested"));
|
|
608
|
+
|
|
609
|
+
channel.postMessage(createState("changed"));
|
|
610
|
+
await testWaitForWorkerMessage();
|
|
611
|
+
assertEqual(deps.syncState.get(), createState("changed"));
|
|
612
|
+
|
|
613
|
+
// Another worker's channel is never heard.
|
|
614
|
+
otherChannel.postMessage(createState("other"));
|
|
615
|
+
await testWaitForWorkerMessage();
|
|
616
|
+
assertEqual(deps.syncState.get(), createState("changed"));
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("listens on its worker's channel before asking for a snapshot", async () => {
|
|
620
|
+
const name = "evolu:sync-state:worker";
|
|
621
|
+
using worker = testCreateSharedWorker<
|
|
622
|
+
SharedWorkerInput,
|
|
623
|
+
SharedWorkerOutput
|
|
624
|
+
>();
|
|
625
|
+
worker.self.onConnect = (port) => {
|
|
626
|
+
port.onMessage = constVoid;
|
|
627
|
+
port.postMessage({
|
|
628
|
+
type: "Connected",
|
|
629
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
630
|
+
syncStateChannelName: name,
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
worker.connect();
|
|
634
|
+
using channel = testCreateBroadcastChannel<SyncState>(name);
|
|
635
|
+
const requested: SyncState = { transports: [], tenants: [] };
|
|
636
|
+
// A worker on another thread can broadcast its answer before the tab's
|
|
637
|
+
// postMessage returns.
|
|
638
|
+
const port = new Proxy(worker.port, {
|
|
639
|
+
get: (target, property) =>
|
|
640
|
+
property === "postMessage"
|
|
641
|
+
? (message: SharedWorkerInput) => {
|
|
642
|
+
if (message.type === "RequestSyncState")
|
|
643
|
+
channel.postMessage(requested);
|
|
644
|
+
target.postMessage(message);
|
|
645
|
+
}
|
|
646
|
+
: (Reflect.get(target, property) as unknown),
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
using deps = createEvoluDeps({
|
|
650
|
+
createDbWorker: testCreateWorker,
|
|
651
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
652
|
+
createMessageChannel: testCreateMessageChannel,
|
|
653
|
+
lockManager: testCreateLockManager(),
|
|
654
|
+
sharedWorker: { port, [Symbol.dispose]: constVoid },
|
|
655
|
+
reloadApp: constVoid,
|
|
656
|
+
});
|
|
657
|
+
await testWaitForWorkerMessage();
|
|
658
|
+
assertEqual(deps.syncState.get(), requested);
|
|
659
|
+
});
|
|
660
|
+
|
|
431
661
|
it("maps ConsoleEntry error output to deps.evoluError store", async () => {
|
|
432
662
|
using setup = await setupCreateEvoluDeps();
|
|
433
663
|
const { deps, consoleEntryOrErrorBroadcastChannel } = setup;
|
|
@@ -497,6 +727,78 @@ describe("Evolu", () => {
|
|
|
497
727
|
assertEqual(deps.evoluError.get(), error);
|
|
498
728
|
});
|
|
499
729
|
|
|
730
|
+
for (const source of ["SharedWorker", "Error", "ConsoleEntry"] as const) {
|
|
731
|
+
it(`preserves the first startup refusal while logging a later ${source} message`, async () => {
|
|
732
|
+
const testConsole = testCreateConsole();
|
|
733
|
+
using setup = await setupCreateEvoluDeps(testConsole);
|
|
734
|
+
const { deps, sharedWorkerPort, consoleEntryOrErrorBroadcastChannel } =
|
|
735
|
+
setup;
|
|
736
|
+
assertSame(deps.evoluError.get(), null);
|
|
737
|
+
|
|
738
|
+
const initialError = {
|
|
739
|
+
type: "UnknownError",
|
|
740
|
+
error: "before refusal",
|
|
741
|
+
} as const;
|
|
742
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
743
|
+
type: "Error",
|
|
744
|
+
error: initialError,
|
|
745
|
+
});
|
|
746
|
+
await testWaitForWorkerMessage();
|
|
747
|
+
assertEqual(deps.evoluError.get(), initialError);
|
|
748
|
+
|
|
749
|
+
let notifications = 0;
|
|
750
|
+
deps.evoluError.subscribe(() => {
|
|
751
|
+
notifications++;
|
|
752
|
+
});
|
|
753
|
+
const refusal: UnsupportedDbVersionError = {
|
|
754
|
+
type: "UnsupportedDbVersionError",
|
|
755
|
+
storedVersion: PositiveInt.orThrow(2),
|
|
756
|
+
supportedVersion: PositiveInt.orThrow(1),
|
|
757
|
+
};
|
|
758
|
+
sharedWorkerPort.postMessage({ type: "Error", error: refusal });
|
|
759
|
+
await testWaitForWorkerMessage();
|
|
760
|
+
const storedRefusal = deps.evoluError.get();
|
|
761
|
+
assertEqual(storedRefusal, refusal);
|
|
762
|
+
assertSame(notifications, 1);
|
|
763
|
+
|
|
764
|
+
const laterRefusal: UnsupportedDbVersionError = {
|
|
765
|
+
...refusal,
|
|
766
|
+
storedVersion: PositiveInt.orThrow(3),
|
|
767
|
+
};
|
|
768
|
+
const laterError = {
|
|
769
|
+
type: "UnknownError",
|
|
770
|
+
error: "after refusal",
|
|
771
|
+
} as const;
|
|
772
|
+
const laterEntry: ConsoleEntry = {
|
|
773
|
+
method: "error",
|
|
774
|
+
path: source === "ConsoleEntry" ? ["worker"] : [],
|
|
775
|
+
args: [source === "SharedWorker" ? laterRefusal : laterError],
|
|
776
|
+
};
|
|
777
|
+
if (source === "SharedWorker") {
|
|
778
|
+
sharedWorkerPort.postMessage({ type: "Error", error: laterRefusal });
|
|
779
|
+
} else if (source === "Error") {
|
|
780
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
781
|
+
type: "Error",
|
|
782
|
+
error: laterError,
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
786
|
+
type: "ConsoleEntry",
|
|
787
|
+
entry: laterEntry,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
await testWaitForWorkerMessage();
|
|
791
|
+
|
|
792
|
+
assertSame(deps.evoluError.get(), storedRefusal);
|
|
793
|
+
assertSame(notifications, 1);
|
|
794
|
+
assertEqual(testConsole.getEntriesSnapshot(), [
|
|
795
|
+
{ method: "error", path: [], args: [initialError] },
|
|
796
|
+
{ method: "error", path: [], args: [refusal] },
|
|
797
|
+
laterEntry,
|
|
798
|
+
]);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
500
802
|
it("throws for unknown tab output type", () => {
|
|
501
803
|
const consoleEntryOrErrorBroadcastChannel: {
|
|
502
804
|
value: {
|
|
@@ -530,13 +832,18 @@ describe("Evolu", () => {
|
|
|
530
832
|
}, Error);
|
|
531
833
|
});
|
|
532
834
|
|
|
533
|
-
it("dispose cleans up resources", () => {
|
|
835
|
+
it("dispose cleans up resources", async () => {
|
|
534
836
|
const worker = testCreateSharedWorker<
|
|
535
837
|
SharedWorkerInput,
|
|
536
838
|
SharedWorkerOutput
|
|
537
839
|
>();
|
|
538
840
|
worker.self.onConnect = (port) => {
|
|
539
841
|
port.onMessage = constVoid;
|
|
842
|
+
port.postMessage({
|
|
843
|
+
type: "Connected",
|
|
844
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
845
|
+
syncStateChannelName: "evolu:sync-state:worker",
|
|
846
|
+
});
|
|
540
847
|
};
|
|
541
848
|
worker.connect();
|
|
542
849
|
|
|
@@ -564,16 +871,93 @@ describe("Evolu", () => {
|
|
|
564
871
|
reloadApp: constVoid,
|
|
565
872
|
});
|
|
566
873
|
|
|
567
|
-
|
|
874
|
+
// The console/error channel, then the sync state channel its worker names.
|
|
875
|
+
assertSame(broadcastChannels.length, 1);
|
|
876
|
+
await testWaitForWorkerMessage();
|
|
877
|
+
assertLength(broadcastChannels, 2);
|
|
568
878
|
assertFalse(broadcastChannels[0].isDisposed());
|
|
879
|
+
assertFalse(broadcastChannels[1].isDisposed());
|
|
569
880
|
assertFalse(workerDisposed);
|
|
570
881
|
deps[Symbol.dispose]();
|
|
571
882
|
assertTrue(broadcastChannels[0].isDisposed());
|
|
883
|
+
assertTrue(broadcastChannels[1].isDisposed());
|
|
572
884
|
assertTrue(workerDisposed);
|
|
573
885
|
});
|
|
574
886
|
});
|
|
575
887
|
|
|
888
|
+
describe("local tables", () => {
|
|
889
|
+
const LocalSchema = { _todo: Schema.todo };
|
|
890
|
+
const config: EvoluConfig = {
|
|
891
|
+
appName: testAppName,
|
|
892
|
+
appOwner: testAppOwner,
|
|
893
|
+
transports: [],
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
it("keeps useOwner available for an instance with only local tables", async () => {
|
|
897
|
+
const messages: Array<SharedWorkerInput> = [];
|
|
898
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
899
|
+
onSharedWorkerPostMessage: (message) => {
|
|
900
|
+
messages.push(message);
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
const evolu = await setup.run.ok(createEvolu(LocalSchema, config));
|
|
904
|
+
assertType<typeof evolu, Evolu<typeof LocalSchema>>();
|
|
905
|
+
assertSame(evolu.appOwner, testAppOwner);
|
|
906
|
+
assertEqual(evolu.name, `AppName-${createIdFromString(testAppOwner.id)}`);
|
|
907
|
+
await testWaitForWorkerMessage();
|
|
908
|
+
assertEqual(setup.evoluInputs, []);
|
|
909
|
+
const message = messages.find(
|
|
910
|
+
(message) => message.type === "CreateEvolu",
|
|
911
|
+
);
|
|
912
|
+
assertNotUndefined(message);
|
|
913
|
+
assertSame(message.encryptionKey, testAppOwner.encryptionKey);
|
|
914
|
+
assertFalse(message.memoryOnly);
|
|
915
|
+
|
|
916
|
+
evolu.insert("_todo", {
|
|
917
|
+
title: NonEmptyTrimmedString100.orThrow("Local"),
|
|
918
|
+
});
|
|
919
|
+
await testWaitForWorkerMessage();
|
|
920
|
+
assertTrue(setup.evoluInputs.some((input) => input.type === "Mutate"));
|
|
921
|
+
assertFalse(setup.evoluInputs.some((input) => input.type === "UseOwner"));
|
|
922
|
+
const unuseOwner = evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
923
|
+
await testWaitForWorkerMessage();
|
|
924
|
+
assertTrue(setup.evoluInputs.some((input) => input.type === "UseOwner"));
|
|
925
|
+
unuseOwner();
|
|
926
|
+
await evolu[Symbol.asyncDispose]();
|
|
927
|
+
assertThrowsInstanceOf(
|
|
928
|
+
() =>
|
|
929
|
+
evolu.insert("_todo", {
|
|
930
|
+
title: NonEmptyTrimmedString100.orThrow("Disposed"),
|
|
931
|
+
}),
|
|
932
|
+
Error,
|
|
933
|
+
);
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
|
|
576
937
|
describe("createEvolu", () => {
|
|
938
|
+
it("uses the default relay when transports are omitted", async () => {
|
|
939
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
940
|
+
await setup.run.ok(
|
|
941
|
+
createEvolu(Schema, {
|
|
942
|
+
appName: testAppName,
|
|
943
|
+
appOwner: testAppOwner,
|
|
944
|
+
}),
|
|
945
|
+
);
|
|
946
|
+
await testWaitForWorkerMessage();
|
|
947
|
+
const input = setup.evoluInputs.find(
|
|
948
|
+
(input) => input.type === "UseOwner",
|
|
949
|
+
);
|
|
950
|
+
assertNotUndefined(input);
|
|
951
|
+
assertEqual(input.actions, [
|
|
952
|
+
{
|
|
953
|
+
action: "add",
|
|
954
|
+
owner: {
|
|
955
|
+
owner: testAppOwner,
|
|
956
|
+
transports: [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
]);
|
|
960
|
+
});
|
|
577
961
|
it("resolves name from appName and appOwner hash", async () => {
|
|
578
962
|
await using setup = await setupRunWithEvoluDeps();
|
|
579
963
|
const { run } = setup;
|
|
@@ -644,6 +1028,70 @@ describe("Evolu", () => {
|
|
|
644
1028
|
assertNonNullable(message.evoluPort);
|
|
645
1029
|
});
|
|
646
1030
|
|
|
1031
|
+
describe("requestSync", () => {
|
|
1032
|
+
it("preserves pending owner operations and returns immediately", async () => {
|
|
1033
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1034
|
+
const { run, evoluInputs } = setup;
|
|
1035
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1036
|
+
const sharedOwner = createSharedOwner(createOwnerSecret(run.deps));
|
|
1037
|
+
const owner = { owner: sharedOwner, transports: [testOwnerTransport] };
|
|
1038
|
+
assertNotSame(sharedOwner.id, evolu.appOwner.id);
|
|
1039
|
+
|
|
1040
|
+
const unuse = evolu.useOwner(sharedOwner, [testOwnerTransport]);
|
|
1041
|
+
const result = evolu.requestSync(sharedOwner.id);
|
|
1042
|
+
assertType<typeof result, void>();
|
|
1043
|
+
assertSame(result, undefined);
|
|
1044
|
+
unuse();
|
|
1045
|
+
evolu.requestSync(sharedOwner.id);
|
|
1046
|
+
assertEqual(evoluInputs, []);
|
|
1047
|
+
|
|
1048
|
+
await testWaitForWorkerMessage();
|
|
1049
|
+
assertEqual(evoluInputs, [
|
|
1050
|
+
{
|
|
1051
|
+
type: "UseOwner",
|
|
1052
|
+
actions: [{ action: "add", owner }],
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
type: "UseOwner",
|
|
1056
|
+
actions: [
|
|
1057
|
+
{ action: "sync", ownerId: sharedOwner.id },
|
|
1058
|
+
{ action: "remove", owner },
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
type: "UseOwner",
|
|
1063
|
+
actions: [{ action: "sync", ownerId: sharedOwner.id }],
|
|
1064
|
+
},
|
|
1065
|
+
]);
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("drops pending requests on disposal", async () => {
|
|
1069
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1070
|
+
const { run, evoluInputs } = setup;
|
|
1071
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1072
|
+
evolu.requestSync(testAppOwner.id);
|
|
1073
|
+
await evolu[Symbol.asyncDispose]();
|
|
1074
|
+
await testWaitForWorkerMessage();
|
|
1075
|
+
assertEqual(evoluInputs, []);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it("preserves registration, mutation, and sync request order", async () => {
|
|
1079
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1080
|
+
const { run, evoluInputs } = setup;
|
|
1081
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1082
|
+
evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
1083
|
+
evolu.insert("todo", {
|
|
1084
|
+
title: NonEmptyTrimmedString100.orThrow("Before sync"),
|
|
1085
|
+
});
|
|
1086
|
+
evolu.requestSync(testAppOwner.id);
|
|
1087
|
+
await testWaitForWorkerMessage();
|
|
1088
|
+
assertEqual(
|
|
1089
|
+
evoluInputs.map(({ type }) => type),
|
|
1090
|
+
["UseOwner", "Mutate", "UseOwner"],
|
|
1091
|
+
);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
647
1095
|
describe("useOwner", () => {
|
|
648
1096
|
it("auto-uses appOwner in a microtask when transports are configured", async () => {
|
|
649
1097
|
await using setup = await setupRunWithEvoluDeps();
|
|
@@ -898,7 +1346,7 @@ describe("Evolu", () => {
|
|
|
898
1346
|
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
899
1347
|
});
|
|
900
1348
|
|
|
901
|
-
it("throws from
|
|
1349
|
+
it("throws from synchronous methods after disposal", async () => {
|
|
902
1350
|
await using setup = await setupRunWithEvoluDeps();
|
|
903
1351
|
const { run } = setup;
|
|
904
1352
|
const evolu = await run.ok(testCreateEvolu);
|
|
@@ -992,6 +1440,13 @@ describe("Evolu", () => {
|
|
|
992
1440
|
}, Error).message,
|
|
993
1441
|
disposedMessage,
|
|
994
1442
|
);
|
|
1443
|
+
|
|
1444
|
+
assertEqual(
|
|
1445
|
+
assertThrowsInstanceOf(() => {
|
|
1446
|
+
evolu.requestSync(testAppOwner.id);
|
|
1447
|
+
}, Error).message,
|
|
1448
|
+
disposedMessage,
|
|
1449
|
+
);
|
|
995
1450
|
});
|
|
996
1451
|
|
|
997
1452
|
it("delete methods are placeholders", async () => {
|
|
@@ -1315,6 +1770,229 @@ describe("Evolu", () => {
|
|
|
1315
1770
|
});
|
|
1316
1771
|
|
|
1317
1772
|
describe("query behavior", () => {
|
|
1773
|
+
it("subscribeQuery catches a refresh between loading and subscribing", async () => {
|
|
1774
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1775
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1776
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1777
|
+
|
|
1778
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1779
|
+
await testWaitForWorkerMessage();
|
|
1780
|
+
postEvoluOutput({
|
|
1781
|
+
type: "OnPatchesByQuery",
|
|
1782
|
+
patchesByQuery: new Map([
|
|
1783
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1784
|
+
]),
|
|
1785
|
+
onCompleteIds: [],
|
|
1786
|
+
});
|
|
1787
|
+
const initialRows = await initialLoad;
|
|
1788
|
+
assertEqual(initialRows, []);
|
|
1789
|
+
|
|
1790
|
+
evoluInputs.length = 0;
|
|
1791
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1792
|
+
await testWaitForWorkerMessage();
|
|
1793
|
+
assertEqual(evoluInputs, []);
|
|
1794
|
+
|
|
1795
|
+
let notifications = 0;
|
|
1796
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(() => {
|
|
1797
|
+
notifications += 1;
|
|
1798
|
+
});
|
|
1799
|
+
// React use() must still see fulfilled rows if another render happens
|
|
1800
|
+
// before the worker answers the refresh queued by subscription.
|
|
1801
|
+
const cachedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1802
|
+
assert("status" in cachedLoad, "Cached promises expose React status");
|
|
1803
|
+
assertSame(cachedLoad.status, "fulfilled");
|
|
1804
|
+
assert("value" in cachedLoad, "Fulfilled promises expose cached rows");
|
|
1805
|
+
assertSame(cachedLoad.value, initialRows);
|
|
1806
|
+
assertSame(await cachedLoad, initialRows);
|
|
1807
|
+
assertSame(notifications, 0);
|
|
1808
|
+
await testWaitForWorkerMessage();
|
|
1809
|
+
assertEqual(evoluInputs, [
|
|
1810
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1811
|
+
]);
|
|
1812
|
+
|
|
1813
|
+
postEvoluOutput({
|
|
1814
|
+
type: "OnPatchesByQuery",
|
|
1815
|
+
patchesByQuery: new Map([
|
|
1816
|
+
[
|
|
1817
|
+
todoTitleQuery,
|
|
1818
|
+
[{ op: "replaceAll", value: [{ title: "Synced" }] }],
|
|
1819
|
+
],
|
|
1820
|
+
]),
|
|
1821
|
+
onCompleteIds: [],
|
|
1822
|
+
});
|
|
1823
|
+
await testWaitForWorkerMessage();
|
|
1824
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), [{ title: "Synced" }]);
|
|
1825
|
+
assertEqual(notifications, 1);
|
|
1826
|
+
// React use() sees a new fulfilled promise after the refresh.
|
|
1827
|
+
const refreshedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1828
|
+
assertNotSame(refreshedLoad, cachedLoad);
|
|
1829
|
+
assertEqual(await refreshedLoad, [{ title: "Synced" }]);
|
|
1830
|
+
unsubscribe();
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
it("subscribeQuery catches a mutation while the initial read is pending", async () => {
|
|
1834
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1835
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1836
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1837
|
+
|
|
1838
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1839
|
+
await testWaitForWorkerMessage();
|
|
1840
|
+
evolu.insert("todo", { title: NonEmptyTrimmedString100.orThrow("New") });
|
|
1841
|
+
await testWaitForWorkerMessage();
|
|
1842
|
+
evoluInputs.length = 0;
|
|
1843
|
+
|
|
1844
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1845
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1846
|
+
await testWaitForWorkerMessage();
|
|
1847
|
+
assertEqual(evoluInputs, [
|
|
1848
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1849
|
+
]);
|
|
1850
|
+
|
|
1851
|
+
// The old read completes first. Its promise must still settle, and the
|
|
1852
|
+
// new subscription must retain the loading entry for the fresh response.
|
|
1853
|
+
postEvoluOutput({
|
|
1854
|
+
type: "OnPatchesByQuery",
|
|
1855
|
+
patchesByQuery: new Map([
|
|
1856
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1857
|
+
]),
|
|
1858
|
+
onCompleteIds: [],
|
|
1859
|
+
});
|
|
1860
|
+
assertEqual(await initialLoad, []);
|
|
1861
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1862
|
+
|
|
1863
|
+
postEvoluOutput({
|
|
1864
|
+
type: "OnPatchesByQuery",
|
|
1865
|
+
patchesByQuery: new Map([
|
|
1866
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "New" }] }]],
|
|
1867
|
+
]),
|
|
1868
|
+
onCompleteIds: [],
|
|
1869
|
+
});
|
|
1870
|
+
await testWaitForWorkerMessage();
|
|
1871
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), [{ title: "New" }]);
|
|
1872
|
+
assertEqual(await evolu.loadQuery(todoTitleQuery), [{ title: "New" }]);
|
|
1873
|
+
unsubscribe();
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it("subscribeQuery reuses valid pending and completed reads", async () => {
|
|
1877
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1878
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1879
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1880
|
+
|
|
1881
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1882
|
+
await testWaitForWorkerMessage();
|
|
1883
|
+
evoluInputs.length = 0;
|
|
1884
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1885
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1886
|
+
await testWaitForWorkerMessage();
|
|
1887
|
+
assertEqual(evoluInputs, []);
|
|
1888
|
+
|
|
1889
|
+
postEvoluOutput({
|
|
1890
|
+
type: "OnPatchesByQuery",
|
|
1891
|
+
patchesByQuery: new Map([
|
|
1892
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1893
|
+
]),
|
|
1894
|
+
onCompleteIds: [],
|
|
1895
|
+
});
|
|
1896
|
+
await initialLoad;
|
|
1897
|
+
unsubscribe();
|
|
1898
|
+
|
|
1899
|
+
const unsubscribeAgain = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1900
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1901
|
+
await testWaitForWorkerMessage();
|
|
1902
|
+
assertEqual(evoluInputs, []);
|
|
1903
|
+
unsubscribeAgain();
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
it("subscribeQuery keeps refreshed rows cached after unsubscribe until the next invalidation", async () => {
|
|
1907
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1908
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1909
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1910
|
+
|
|
1911
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1912
|
+
await testWaitForWorkerMessage();
|
|
1913
|
+
postEvoluOutput({
|
|
1914
|
+
type: "OnPatchesByQuery",
|
|
1915
|
+
patchesByQuery: new Map([
|
|
1916
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1917
|
+
]),
|
|
1918
|
+
onCompleteIds: [],
|
|
1919
|
+
});
|
|
1920
|
+
await initialLoad;
|
|
1921
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1922
|
+
await testWaitForWorkerMessage();
|
|
1923
|
+
evoluInputs.length = 0;
|
|
1924
|
+
|
|
1925
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1926
|
+
await testWaitForWorkerMessage();
|
|
1927
|
+
assertEqual(evoluInputs, [
|
|
1928
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1929
|
+
]);
|
|
1930
|
+
unsubscribe();
|
|
1931
|
+
evoluInputs.length = 0;
|
|
1932
|
+
|
|
1933
|
+
// The refresh response still replaces the cached rows.
|
|
1934
|
+
postEvoluOutput({
|
|
1935
|
+
type: "OnPatchesByQuery",
|
|
1936
|
+
patchesByQuery: new Map([
|
|
1937
|
+
[
|
|
1938
|
+
todoTitleQuery,
|
|
1939
|
+
[{ op: "replaceAll", value: [{ title: "Synced" }] }],
|
|
1940
|
+
],
|
|
1941
|
+
]),
|
|
1942
|
+
onCompleteIds: [],
|
|
1943
|
+
});
|
|
1944
|
+
await testWaitForWorkerMessage();
|
|
1945
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), [{ title: "Synced" }]);
|
|
1946
|
+
const cachedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1947
|
+
assertEqual(await cachedLoad, [{ title: "Synced" }]);
|
|
1948
|
+
await testWaitForWorkerMessage();
|
|
1949
|
+
assertEqual(evoluInputs, []);
|
|
1950
|
+
|
|
1951
|
+
// The next invalidation drops the unsubscribed cache without a read.
|
|
1952
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1953
|
+
await testWaitForWorkerMessage();
|
|
1954
|
+
assertEqual(evoluInputs, []);
|
|
1955
|
+
const reload = evolu.loadQuery(todoTitleQuery);
|
|
1956
|
+
assertNotSame(reload, cachedLoad);
|
|
1957
|
+
await testWaitForWorkerMessage();
|
|
1958
|
+
assertEqual(evoluInputs, [
|
|
1959
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1960
|
+
]);
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
it("subscribeQuery reuses the refreshing entry for repeated subscriptions in one tick", async () => {
|
|
1964
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1965
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1966
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1967
|
+
|
|
1968
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1969
|
+
await testWaitForWorkerMessage();
|
|
1970
|
+
postEvoluOutput({
|
|
1971
|
+
type: "OnPatchesByQuery",
|
|
1972
|
+
patchesByQuery: new Map([
|
|
1973
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1974
|
+
]),
|
|
1975
|
+
onCompleteIds: [],
|
|
1976
|
+
});
|
|
1977
|
+
await initialLoad;
|
|
1978
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1979
|
+
await testWaitForWorkerMessage();
|
|
1980
|
+
evoluInputs.length = 0;
|
|
1981
|
+
|
|
1982
|
+
// React StrictMode subscribes, unsubscribes, and subscribes again
|
|
1983
|
+
// synchronously.
|
|
1984
|
+
const unsubscribeFirst = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1985
|
+
const refreshingLoad = evolu.loadQuery(todoTitleQuery);
|
|
1986
|
+
unsubscribeFirst();
|
|
1987
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1988
|
+
assertSame(evolu.loadQuery(todoTitleQuery), refreshingLoad);
|
|
1989
|
+
await testWaitForWorkerMessage();
|
|
1990
|
+
assertEqual(evoluInputs, [
|
|
1991
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1992
|
+
]);
|
|
1993
|
+
unsubscribe();
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1318
1996
|
it("loadQuery reuses pending promise and sends one Query message", async () => {
|
|
1319
1997
|
await using setup = await setupRunWithEvoluDeps();
|
|
1320
1998
|
const { run, evoluInputs } = setup;
|
|
@@ -1399,7 +2077,7 @@ describe("Evolu", () => {
|
|
|
1399
2077
|
);
|
|
1400
2078
|
});
|
|
1401
2079
|
|
|
1402
|
-
it("RefreshQueries
|
|
2080
|
+
it("RefreshQueries reads a pending unsubscribed loadQuery once, when it is subscribed", async () => {
|
|
1403
2081
|
await using setup = await setupRunWithEvoluDeps();
|
|
1404
2082
|
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1405
2083
|
const evolu = await run.ok(testCreateEvolu);
|
|
@@ -1409,12 +2087,18 @@ describe("Evolu", () => {
|
|
|
1409
2087
|
|
|
1410
2088
|
evoluInputs.length = 0;
|
|
1411
2089
|
postEvoluOutput({ type: "RefreshQueries" });
|
|
2090
|
+
await testWaitForWorkerMessage();
|
|
1412
2091
|
|
|
2092
|
+
assertEqual(evoluInputs, []);
|
|
2093
|
+
|
|
2094
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1413
2095
|
await testWaitForWorkerMessage();
|
|
1414
2096
|
|
|
1415
2097
|
assertEqual(evoluInputs, [
|
|
1416
2098
|
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1417
2099
|
]);
|
|
2100
|
+
|
|
2101
|
+
unsubscribe();
|
|
1418
2102
|
});
|
|
1419
2103
|
|
|
1420
2104
|
it("RefreshQueries re-queries subscribed query without loadQuery", async () => {
|
|
@@ -1782,6 +2466,294 @@ describe("Evolu", () => {
|
|
|
1782
2466
|
});
|
|
1783
2467
|
});
|
|
1784
2468
|
|
|
2469
|
+
describe("mutation size", () => {
|
|
2470
|
+
const NoteId = id("Note");
|
|
2471
|
+
const note = {
|
|
2472
|
+
id: NoteId,
|
|
2473
|
+
body: nullOr(StringType),
|
|
2474
|
+
data: nullOr(Uint8ArrayType),
|
|
2475
|
+
count: nullOr(FiniteNumber),
|
|
2476
|
+
};
|
|
2477
|
+
const SizeSchema = { note, _note: note };
|
|
2478
|
+
|
|
2479
|
+
const createSizeEvolu = createEvolu(SizeSchema, {
|
|
2480
|
+
appName: testAppName,
|
|
2481
|
+
appOwner: testAppOwner,
|
|
2482
|
+
transports: [],
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
const noteId = NoteId.orThrow(createIdFromString("note"));
|
|
2486
|
+
|
|
2487
|
+
const getChanges = (inputs: ReadonlyArray<EvoluInput>) =>
|
|
2488
|
+
inputs.flatMap((input) => (input.type === "Mutate" ? input.changes : []));
|
|
2489
|
+
|
|
2490
|
+
const tooLargeMessage = (size: number) =>
|
|
2491
|
+
`The mutation of table 'note' is ${size} bytes, over maxMutationSize (640000). Bound the Types of its columns or check it with evolu.getMutationSize.`;
|
|
2492
|
+
|
|
2493
|
+
// The length of a blob that makes a note exactly maxMutationSize. Its
|
|
2494
|
+
// length varint takes three bytes, two more than an empty blob's.
|
|
2495
|
+
const getDataAtLimitLength = (evolu: Evolu<typeof SizeSchema>) =>
|
|
2496
|
+
maxMutationSize -
|
|
2497
|
+
evolu.getMutationSize("note", { data: new Uint8Array() }) -
|
|
2498
|
+
2;
|
|
2499
|
+
|
|
2500
|
+
it("getMutationSize measures the change as it is encoded", async () => {
|
|
2501
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2502
|
+
const evolu = await setup.run.ok(createSizeEvolu);
|
|
2503
|
+
|
|
2504
|
+
const values = {
|
|
2505
|
+
body: "Milk",
|
|
2506
|
+
data: new Uint8Array(10),
|
|
2507
|
+
count: FiniteNumber.orThrow(0.1),
|
|
2508
|
+
};
|
|
2509
|
+
const buffer = createBuffer();
|
|
2510
|
+
encodeDbChange(buffer, {
|
|
2511
|
+
timestamp: createTimestamp(),
|
|
2512
|
+
change: DbChange.orThrow({
|
|
2513
|
+
table: "note",
|
|
2514
|
+
id: noteId,
|
|
2515
|
+
values,
|
|
2516
|
+
isInsert: false,
|
|
2517
|
+
isDelete: true,
|
|
2518
|
+
}),
|
|
2519
|
+
});
|
|
2520
|
+
assertEqual(evolu.getMutationSize("note", values), buffer.getLength());
|
|
2521
|
+
|
|
2522
|
+
// `id` and `isDeleted` are not columns.
|
|
2523
|
+
assertEqual(
|
|
2524
|
+
evolu.getMutationSize("note", { id: noteId, isDeleted: 1 }),
|
|
2525
|
+
evolu.getMutationSize("note", {}),
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
// @ts-expect-error Only the schema's tables can be measured.
|
|
2529
|
+
evolu.getMutationSize("unknown", {});
|
|
2530
|
+
// @ts-expect-error Values must match their column's Type.
|
|
2531
|
+
evolu.getMutationSize("note", { body: 1 });
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
it("encodes no string in more than three bytes per code unit", async () => {
|
|
2535
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2536
|
+
const evolu = await setup.run.ok(createSizeEvolu);
|
|
2537
|
+
|
|
2538
|
+
const assertAtMostThreeBytesPerCodeUnit = (value: string) => {
|
|
2539
|
+
// A string also takes a length varint of up to three bytes, besides
|
|
2540
|
+
// the type tag that null takes too.
|
|
2541
|
+
assertTrue(
|
|
2542
|
+
evolu.getMutationSize("note", { body: value }) -
|
|
2543
|
+
evolu.getMutationSize("note", { body: null }) <=
|
|
2544
|
+
3 * value.length + 3,
|
|
2545
|
+
);
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
for (const value of [
|
|
2549
|
+
"",
|
|
2550
|
+
"€".repeat(200_000),
|
|
2551
|
+
"😀".repeat(1_000),
|
|
2552
|
+
"\uD800".repeat(1_000),
|
|
2553
|
+
// JSON takes exactly three bytes per code unit here.
|
|
2554
|
+
"0.1",
|
|
2555
|
+
`[${"0.1,".repeat(999)}0.1]`,
|
|
2556
|
+
"[".repeat(1_001) + "]".repeat(1_001),
|
|
2557
|
+
noteId,
|
|
2558
|
+
"2024-01-01T00:00:00.000Z",
|
|
2559
|
+
]) {
|
|
2560
|
+
assertAtMostThreeBytesPerCodeUnit(value);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
fc.assert(
|
|
2564
|
+
fc.property(
|
|
2565
|
+
fc.oneof(fc.string(), fc.string({ unit: "binary" }), fc.json()),
|
|
2566
|
+
assertAtMostThreeBytesPerCodeUnit,
|
|
2567
|
+
),
|
|
2568
|
+
{ numRuns: 2_000 },
|
|
2569
|
+
);
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
it("rejects a mutation over maxMutationSize before batching it", async () => {
|
|
2573
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2574
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
2575
|
+
const evolu = await run.ok(createSizeEvolu);
|
|
2576
|
+
|
|
2577
|
+
const dataAtLimitLength = getDataAtLimitLength(evolu);
|
|
2578
|
+
const atLimit = { data: new Uint8Array(dataAtLimitLength) };
|
|
2579
|
+
assertEqual(evolu.getMutationSize("note", atLimit), maxMutationSize);
|
|
2580
|
+
const completed: Array<string> = [];
|
|
2581
|
+
|
|
2582
|
+
const { id } = evolu.insert("note", atLimit, {
|
|
2583
|
+
onComplete: () => {
|
|
2584
|
+
completed.push("at limit");
|
|
2585
|
+
},
|
|
2586
|
+
});
|
|
2587
|
+
const error = assertThrowsInstanceOf(
|
|
2588
|
+
() =>
|
|
2589
|
+
evolu.insert(
|
|
2590
|
+
"note",
|
|
2591
|
+
{ data: new Uint8Array(dataAtLimitLength + 1) },
|
|
2592
|
+
{
|
|
2593
|
+
onComplete: () => {
|
|
2594
|
+
completed.push("over limit");
|
|
2595
|
+
},
|
|
2596
|
+
},
|
|
2597
|
+
),
|
|
2598
|
+
Error,
|
|
2599
|
+
);
|
|
2600
|
+
assertEqual(error.message, tooLargeMessage(maxMutationSize + 1));
|
|
2601
|
+
|
|
2602
|
+
await testWaitForWorkerMessage();
|
|
2603
|
+
|
|
2604
|
+
// The mutation made before the rejected one is sent as usual.
|
|
2605
|
+
assertEqual(
|
|
2606
|
+
getChanges(evoluInputs).map((change) => change.id),
|
|
2607
|
+
[id],
|
|
2608
|
+
);
|
|
2609
|
+
const mutate = evoluInputs.find((input) => input.type === "Mutate");
|
|
2610
|
+
assertNotUndefined(mutate);
|
|
2611
|
+
postEvoluOutput({
|
|
2612
|
+
type: "OnPatchesByQuery",
|
|
2613
|
+
patchesByQuery: new Map(),
|
|
2614
|
+
onCompleteIds: mutate.onCompleteIds,
|
|
2615
|
+
});
|
|
2616
|
+
await testWaitForWorkerMessage();
|
|
2617
|
+
assertEqual(completed, ["at limit"]);
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
it("lets plain text use the whole limit at a byte per character", async () => {
|
|
2621
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2622
|
+
const { run, evoluInputs } = setup;
|
|
2623
|
+
const evolu = await run.ok(createSizeEvolu);
|
|
2624
|
+
|
|
2625
|
+
// Spaces keep the text from being encoded as Base64Url, which is smaller.
|
|
2626
|
+
// A long text's length varint takes two more bytes than a short one's.
|
|
2627
|
+
const length =
|
|
2628
|
+
maxMutationSize - evolu.getMutationSize("note", { body: " " }) - 1;
|
|
2629
|
+
assertEqual(
|
|
2630
|
+
evolu.getMutationSize("note", { body: " ".repeat(length) }),
|
|
2631
|
+
maxMutationSize,
|
|
2632
|
+
);
|
|
2633
|
+
assertTrue(length > 639_900);
|
|
2634
|
+
|
|
2635
|
+
evolu.update("note", { id: noteId, body: " ".repeat(length) });
|
|
2636
|
+
const error = assertThrowsInstanceOf(
|
|
2637
|
+
() =>
|
|
2638
|
+
evolu.update("note", { id: noteId, body: " ".repeat(length + 1) }),
|
|
2639
|
+
Error,
|
|
2640
|
+
);
|
|
2641
|
+
assertEqual(error.message, tooLargeMessage(maxMutationSize + 1));
|
|
2642
|
+
|
|
2643
|
+
await testWaitForWorkerMessage();
|
|
2644
|
+
assertLength(getChanges(evoluInputs), 1);
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
it("saves a copy of each binary value", async () => {
|
|
2648
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2649
|
+
const { run, evoluInputs } = setup;
|
|
2650
|
+
const evolu = await run.ok(createSizeEvolu);
|
|
2651
|
+
|
|
2652
|
+
const resizable = new ArrayBuffer(3, { maxByteLength: 8 });
|
|
2653
|
+
const growing = new Uint8Array(resizable);
|
|
2654
|
+
growing.set([1, 2, 3]);
|
|
2655
|
+
const shared = new Uint8Array(new SharedArrayBuffer(3));
|
|
2656
|
+
shared.set([4, 5, 6]);
|
|
2657
|
+
const nodeBuffer = Buffer.from([7, 8, 9]);
|
|
2658
|
+
|
|
2659
|
+
evolu.insert("note", { data: growing });
|
|
2660
|
+
evolu.insert("note", { data: shared });
|
|
2661
|
+
evolu.insert("note", { data: nodeBuffer });
|
|
2662
|
+
|
|
2663
|
+
// Changes made before the batch is sent do not reach it.
|
|
2664
|
+
resizable.resize(8);
|
|
2665
|
+
growing.fill(0);
|
|
2666
|
+
shared.fill(0);
|
|
2667
|
+
nodeBuffer.fill(0);
|
|
2668
|
+
|
|
2669
|
+
await testWaitForWorkerMessage();
|
|
2670
|
+
|
|
2671
|
+
const saved = getChanges(evoluInputs).map((change) => change.values.data);
|
|
2672
|
+
assertEqual(saved, [
|
|
2673
|
+
new Uint8Array([1, 2, 3]),
|
|
2674
|
+
new Uint8Array([4, 5, 6]),
|
|
2675
|
+
new Uint8Array([7, 8, 9]),
|
|
2676
|
+
]);
|
|
2677
|
+
for (const data of saved) {
|
|
2678
|
+
assert(data instanceof Uint8Array, "Expected a Uint8Array.");
|
|
2679
|
+
assertSame(Object.getPrototypeOf(data), Uint8Array.prototype);
|
|
2680
|
+
assertFalse(data.buffer instanceof SharedArrayBuffer);
|
|
2681
|
+
assertFalse((data.buffer as ArrayBuffer).resizable);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
it("does not limit local-only tables but copies their binary values", async () => {
|
|
2686
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2687
|
+
const { run, evoluInputs } = setup;
|
|
2688
|
+
const evolu = await run.ok(createSizeEvolu);
|
|
2689
|
+
|
|
2690
|
+
const data = new Uint8Array(maxMutationSize + 1);
|
|
2691
|
+
evolu.insert("_note", { data });
|
|
2692
|
+
data[0] = 1;
|
|
2693
|
+
|
|
2694
|
+
await testWaitForWorkerMessage();
|
|
2695
|
+
|
|
2696
|
+
const [change] = getChanges(evoluInputs);
|
|
2697
|
+
assertNotUndefined(change);
|
|
2698
|
+
const saved = change.values.data;
|
|
2699
|
+
assert(saved instanceof Uint8Array, "Expected a Uint8Array.");
|
|
2700
|
+
assertNotSame(saved, data);
|
|
2701
|
+
assertEqual(saved.byteLength, maxMutationSize + 1);
|
|
2702
|
+
assertEqual(saved[0], 0);
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
it("fits a change at the limit into one message next to the largest ranges section", async () => {
|
|
2706
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
2707
|
+
const evolu = await setup.run.ok(createSizeEvolu);
|
|
2708
|
+
const deps = testCreateDeps();
|
|
2709
|
+
const values = { data: new Uint8Array(getDataAtLimitLength(evolu)) };
|
|
2710
|
+
assertEqual(evolu.getMutationSize("note", values), maxMutationSize);
|
|
2711
|
+
|
|
2712
|
+
const message = {
|
|
2713
|
+
timestamp: createTimestamp(),
|
|
2714
|
+
change: DbChange.orThrow({
|
|
2715
|
+
table: "note",
|
|
2716
|
+
id: noteId,
|
|
2717
|
+
values,
|
|
2718
|
+
isInsert: true,
|
|
2719
|
+
isDelete: null,
|
|
2720
|
+
}),
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2723
|
+
// A request and a broadcast assert that the change fits.
|
|
2724
|
+
createProtocolMessageFromCrdtMessages(deps)(testAppOwner, [message]);
|
|
2725
|
+
createProtocolBroadcastMessagesFromCrdtMessages(deps)(testAppOwner, [
|
|
2726
|
+
message,
|
|
2727
|
+
]);
|
|
2728
|
+
|
|
2729
|
+
// A relay's response can pair it with the largest ranges section.
|
|
2730
|
+
const rangesMaxSize = ProtocolMessageRangesMaxSize.orThrow(100_000);
|
|
2731
|
+
const timestamps = createTimestampsBuffer();
|
|
2732
|
+
for (let index = 1; timestamps.getLength() < 99_900; index++) {
|
|
2733
|
+
timestamps.add(
|
|
2734
|
+
createTimestamp({
|
|
2735
|
+
millis: createTimestamp().millis,
|
|
2736
|
+
nodeId: index.toString(16).padStart(16, "0") as NodeId,
|
|
2737
|
+
}),
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
const response = createProtocolMessageBuffer(testAppOwner.id, {
|
|
2741
|
+
messageType: MessageType.Response,
|
|
2742
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
2743
|
+
rangesMaxSize,
|
|
2744
|
+
});
|
|
2745
|
+
assertTrue(
|
|
2746
|
+
response.canAddTimestampsRangeAndMessage(timestamps, {
|
|
2747
|
+
timestamp: message.timestamp,
|
|
2748
|
+
change: encodeAndEncryptDbChange(deps)(
|
|
2749
|
+
message,
|
|
2750
|
+
testAppOwner.encryptionKey,
|
|
2751
|
+
),
|
|
2752
|
+
}),
|
|
2753
|
+
);
|
|
2754
|
+
});
|
|
2755
|
+
});
|
|
2756
|
+
|
|
1785
2757
|
describe("exportDatabase", () => {
|
|
1786
2758
|
it("throws when OnExport arrives without pending export", async () => {
|
|
1787
2759
|
const channels: Array<{
|