@evolu/common 8.10.0 → 8.11.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/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 +336 -211
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +102 -15
- 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 +94 -16
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +118 -38
- 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/Schema.d.ts +335 -21
- 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 +192 -14
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +81 -20
- 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/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 +15 -6
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +686 -21
- package/src/local-first/Evolu.ts +450 -228
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +617 -10
- package/src/local-first/Protocol.ts +196 -72
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +363 -24
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +218 -32
- 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
|
@@ -18,15 +18,22 @@ import type { Brand } from "../Brand.ts";
|
|
|
18
18
|
import type { ConsoleEntry, TestConsole } from "../Console.ts";
|
|
19
19
|
import { testCreateConsole } from "../Console.ts";
|
|
20
20
|
import { constVoid } from "../Function.ts";
|
|
21
|
-
import type { DbWorkerInit } from "./Db.ts";
|
|
21
|
+
import type { DbWorkerInit, UnsupportedDbVersionError } from "./Db.ts";
|
|
22
22
|
import {
|
|
23
23
|
AppName,
|
|
24
24
|
createEvolu,
|
|
25
25
|
createEvoluDeps,
|
|
26
|
+
type Evolu,
|
|
27
|
+
type EvoluConfig,
|
|
26
28
|
testAppName,
|
|
27
29
|
type EvoluPlatformDeps,
|
|
28
30
|
} from "./Evolu.ts";
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createOwnerSecret,
|
|
33
|
+
createOwnerWebSocketTransport,
|
|
34
|
+
createSharedOwner,
|
|
35
|
+
testAppOwner,
|
|
36
|
+
} from "./Owner.ts";
|
|
30
37
|
import { createQueryBuilder } from "./Schema.ts";
|
|
31
38
|
import {
|
|
32
39
|
consoleEntryOrErrorBroadcastChannelName,
|
|
@@ -36,21 +43,30 @@ import {
|
|
|
36
43
|
type EvoluInput,
|
|
37
44
|
type EvoluOutput,
|
|
38
45
|
type SharedWorker,
|
|
46
|
+
type SharedWorkerId,
|
|
39
47
|
type SharedWorkerInput,
|
|
40
48
|
type SharedWorkerOutput,
|
|
49
|
+
type SyncState,
|
|
41
50
|
} from "./Shared.ts";
|
|
42
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
acquireLeaderLock,
|
|
53
|
+
type LockManagerDep,
|
|
54
|
+
testCreateLockManager,
|
|
55
|
+
} from "../LockManager.ts";
|
|
43
56
|
import { installPolyfills } from "../Polyfills.ts";
|
|
44
57
|
import { err, ok } from "../Result.ts";
|
|
45
58
|
import { SqliteBoolean } from "../Sqlite.ts";
|
|
46
59
|
import { explicitAbortReason, testCreateRun } from "../Task.ts";
|
|
60
|
+
import { testCreateId } from "../Test.ts";
|
|
47
61
|
import {
|
|
48
62
|
assertType,
|
|
49
63
|
createIdFromString,
|
|
50
64
|
id,
|
|
51
65
|
NonEmptyTrimmedString100,
|
|
52
66
|
nullOr,
|
|
67
|
+
PositiveInt,
|
|
53
68
|
testName,
|
|
69
|
+
Name,
|
|
54
70
|
} from "../Type.ts";
|
|
55
71
|
import type { ExtractTyped } from "../Type.ts";
|
|
56
72
|
import {
|
|
@@ -203,6 +219,8 @@ describe("Evolu", () => {
|
|
|
203
219
|
}),
|
|
204
220
|
);
|
|
205
221
|
|
|
222
|
+
assertThrowsInstanceOf(() => AppName.orThrow("a".repeat(42)), Error);
|
|
223
|
+
|
|
206
224
|
const appName = AppName.orThrow("my-app");
|
|
207
225
|
assertType<typeof appName, AppName>();
|
|
208
226
|
assertType<typeof AppName.Input, string>();
|
|
@@ -212,12 +230,22 @@ describe("Evolu", () => {
|
|
|
212
230
|
describe("createEvoluDeps", () => {
|
|
213
231
|
const setupCreateEvoluDeps = async (
|
|
214
232
|
console: TestConsole = testCreateConsole(),
|
|
233
|
+
lockManager: LockManagerDep["lockManager"] = testCreateLockManager(),
|
|
215
234
|
): Promise<{
|
|
216
235
|
readonly deps: ReturnType<typeof createEvoluDeps>;
|
|
217
236
|
readonly messages: Array<SharedWorkerInput>;
|
|
237
|
+
readonly sharedWorkerPort: MessagePort<
|
|
238
|
+
SharedWorkerOutput,
|
|
239
|
+
SharedWorkerInput
|
|
240
|
+
>;
|
|
218
241
|
readonly consoleEntryOrErrorBroadcastChannel: {
|
|
219
242
|
readonly postMessage: (message: ConsoleEntryOrError) => void;
|
|
220
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* Sends the worker's Connected message, as a worker holding its lock
|
|
246
|
+
* does.
|
|
247
|
+
*/
|
|
248
|
+
readonly connect: (workerId: SharedWorkerId) => Promise<void>;
|
|
221
249
|
readonly [Symbol.dispose]: () => void;
|
|
222
250
|
}> => {
|
|
223
251
|
using disposer = new DisposableStack();
|
|
@@ -226,7 +254,11 @@ describe("Evolu", () => {
|
|
|
226
254
|
);
|
|
227
255
|
|
|
228
256
|
const messages: Array<SharedWorkerInput> = [];
|
|
257
|
+
const sharedWorkerPort: {
|
|
258
|
+
value: MessagePort<SharedWorkerOutput, SharedWorkerInput> | null;
|
|
259
|
+
} = { value: null };
|
|
229
260
|
worker.self.onConnect = (port) => {
|
|
261
|
+
sharedWorkerPort.value = port;
|
|
230
262
|
port.onMessage = (message) => {
|
|
231
263
|
messages.push(message);
|
|
232
264
|
};
|
|
@@ -238,7 +270,7 @@ describe("Evolu", () => {
|
|
|
238
270
|
createDbWorker: testCreateWorker,
|
|
239
271
|
createBroadcastChannel: testCreateBroadcastChannel,
|
|
240
272
|
createMessageChannel: testCreateMessageChannel,
|
|
241
|
-
lockManager
|
|
273
|
+
lockManager,
|
|
242
274
|
sharedWorker: worker,
|
|
243
275
|
reloadApp: constVoid,
|
|
244
276
|
console,
|
|
@@ -247,8 +279,10 @@ describe("Evolu", () => {
|
|
|
247
279
|
|
|
248
280
|
await testWaitForWorkerMessage();
|
|
249
281
|
|
|
250
|
-
|
|
251
|
-
|
|
282
|
+
// A tab says nothing until its worker connects.
|
|
283
|
+
assertEqual(messages, []);
|
|
284
|
+
const port = sharedWorkerPort.value;
|
|
285
|
+
assertNotNull(port);
|
|
252
286
|
const consoleEntryOrErrorBroadcastChannel = disposer.use(
|
|
253
287
|
testCreateBroadcastChannel<ConsoleEntryOrError>(
|
|
254
288
|
consoleEntryOrErrorBroadcastChannelName,
|
|
@@ -259,21 +293,110 @@ describe("Evolu", () => {
|
|
|
259
293
|
return {
|
|
260
294
|
deps,
|
|
261
295
|
messages,
|
|
296
|
+
sharedWorkerPort: port,
|
|
262
297
|
consoleEntryOrErrorBroadcastChannel,
|
|
298
|
+
connect: async (workerId) => {
|
|
299
|
+
port.postMessage({
|
|
300
|
+
type: "Connected",
|
|
301
|
+
workerId,
|
|
302
|
+
syncStateChannelName: `evolu:sync-state:${workerId}`,
|
|
303
|
+
});
|
|
304
|
+
await testWaitForWorkerMessage();
|
|
305
|
+
},
|
|
263
306
|
[Symbol.dispose]: () => disposables.dispose(),
|
|
264
307
|
};
|
|
265
308
|
};
|
|
266
309
|
|
|
267
|
-
it("
|
|
310
|
+
it("reports an OtherBuildRunningError its worker sends and clears it once the worker connects", async () => {
|
|
311
|
+
using setup = await setupCreateEvoluDeps();
|
|
312
|
+
|
|
313
|
+
setup.sharedWorkerPort.postMessage({
|
|
314
|
+
type: "Error",
|
|
315
|
+
error: { type: "OtherBuildRunningError" },
|
|
316
|
+
});
|
|
317
|
+
await testWaitForWorkerMessage();
|
|
318
|
+
assertEqual(setup.deps.evoluError.get(), {
|
|
319
|
+
type: "OtherBuildRunningError",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await setup.connect(testCreateId()<"SharedWorker">());
|
|
323
|
+
assertSame(setup.deps.evoluError.get(), null);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("keeps another error when its worker connects", async () => {
|
|
327
|
+
using setup = await setupCreateEvoluDeps();
|
|
328
|
+
const error: UnsupportedDbVersionError = {
|
|
329
|
+
type: "UnsupportedDbVersionError",
|
|
330
|
+
storedVersion: PositiveInt.orThrow(3),
|
|
331
|
+
supportedVersion: PositiveInt.orThrow(2),
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
setup.sharedWorkerPort.postMessage({ type: "Error", error });
|
|
335
|
+
await testWaitForWorkerMessage();
|
|
336
|
+
await setup.connect(testCreateId()<"SharedWorker">());
|
|
337
|
+
|
|
338
|
+
assertEqual(setup.deps.evoluError.get(), error);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("says nothing to a worker that reports it waits", async () => {
|
|
342
|
+
using setup = await setupCreateEvoluDeps();
|
|
343
|
+
|
|
344
|
+
setup.sharedWorkerPort.postMessage({
|
|
345
|
+
type: "Waiting",
|
|
346
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
347
|
+
});
|
|
348
|
+
await testWaitForWorkerMessage();
|
|
349
|
+
|
|
350
|
+
assertEqual(setup.messages, []);
|
|
351
|
+
assertSame(setup.deps.evoluError.get(), null);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("says nothing to a worker that reports it keeps databases in memory", async () => {
|
|
355
|
+
using setup = await setupCreateEvoluDeps();
|
|
356
|
+
|
|
357
|
+
setup.sharedWorkerPort.postMessage({ type: "StorageUnavailable" });
|
|
358
|
+
await testWaitForWorkerMessage();
|
|
359
|
+
|
|
360
|
+
assertEqual(setup.messages, []);
|
|
361
|
+
assertSame(setup.deps.evoluError.get(), null);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("announces itself as tab leader with its console level once its worker connects", async () => {
|
|
268
365
|
const testConsole = testCreateConsole();
|
|
269
366
|
using setup = await setupCreateEvoluDeps(testConsole);
|
|
270
|
-
const { messages } = setup;
|
|
367
|
+
const { messages, connect } = setup;
|
|
271
368
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
369
|
+
await connect(testCreateId()<"SharedWorker">());
|
|
370
|
+
|
|
371
|
+
assertEqual(messages, [
|
|
372
|
+
{ type: "RequestSyncState" },
|
|
373
|
+
{ type: "AnnounceTabLeader", consoleLevel: testConsole.getLevel() },
|
|
374
|
+
]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("competes for tab leadership only with tabs of its worker", async () => {
|
|
378
|
+
const lockManager = testCreateLockManager();
|
|
379
|
+
await using run = testCreateRun({ lockManager });
|
|
380
|
+
const createWorkerId = testCreateId();
|
|
381
|
+
const worker = createWorkerId<"SharedWorker">();
|
|
382
|
+
// The tab's worker holds this lock for its lifetime.
|
|
383
|
+
await using _workerBuildLock = await run.ok(acquireLeaderLock("tab"));
|
|
384
|
+
using first = await setupCreateEvoluDeps(undefined, lockManager);
|
|
385
|
+
using second = await setupCreateEvoluDeps(undefined, lockManager);
|
|
386
|
+
using ofOtherWorker = await setupCreateEvoluDeps(undefined, lockManager);
|
|
387
|
+
await first.connect(worker);
|
|
388
|
+
await second.connect(worker);
|
|
389
|
+
await ofOtherWorker.connect(createWorkerId<"SharedWorker">());
|
|
390
|
+
const isLeader = (setup: typeof first): boolean =>
|
|
391
|
+
setup.messages.some((message) => message.type === "AnnounceTabLeader");
|
|
392
|
+
|
|
393
|
+
assertTrue(isLeader(first));
|
|
394
|
+
assertFalse(isLeader(second));
|
|
395
|
+
assertTrue(isLeader(ofOtherWorker));
|
|
396
|
+
|
|
397
|
+
first[Symbol.dispose]();
|
|
398
|
+
await testWaitForWorkerMessage();
|
|
399
|
+
assertTrue(isLeader(second));
|
|
277
400
|
});
|
|
278
401
|
|
|
279
402
|
it("initializes db worker from shared worker init output", async () => {
|
|
@@ -367,6 +490,11 @@ describe("Evolu", () => {
|
|
|
367
490
|
port.onMessage = (message) => {
|
|
368
491
|
messages.push(message);
|
|
369
492
|
};
|
|
493
|
+
port.postMessage({
|
|
494
|
+
type: "Connected",
|
|
495
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
496
|
+
syncStateChannelName: "evolu:sync-state:worker",
|
|
497
|
+
});
|
|
370
498
|
};
|
|
371
499
|
worker.connect();
|
|
372
500
|
|
|
@@ -382,8 +510,11 @@ describe("Evolu", () => {
|
|
|
382
510
|
await testWaitForWorkerMessage();
|
|
383
511
|
|
|
384
512
|
assertNotUndefined(deps.console);
|
|
385
|
-
|
|
386
|
-
|
|
513
|
+
const messageTypes = ["RequestSyncState", "AnnounceTabLeader"];
|
|
514
|
+
assertEqual(
|
|
515
|
+
messages.map((message) => message.type),
|
|
516
|
+
messageTypes,
|
|
517
|
+
);
|
|
387
518
|
using consoleEntryOrErrorBroadcastChannel =
|
|
388
519
|
testCreateBroadcastChannel<ConsoleEntryOrError>(
|
|
389
520
|
consoleEntryOrErrorBroadcastChannelName,
|
|
@@ -396,8 +527,10 @@ describe("Evolu", () => {
|
|
|
396
527
|
|
|
397
528
|
await testWaitForWorkerMessage();
|
|
398
529
|
|
|
399
|
-
|
|
400
|
-
|
|
530
|
+
assertEqual(
|
|
531
|
+
messages.map((message) => message.type),
|
|
532
|
+
messageTypes,
|
|
533
|
+
);
|
|
401
534
|
assertEqual(deps.evoluError.get(), {
|
|
402
535
|
type: "UnknownError",
|
|
403
536
|
error: ["boom"],
|
|
@@ -428,6 +561,84 @@ describe("Evolu", () => {
|
|
|
428
561
|
assertEqual(testConsole.getEntriesSnapshot(), [entry]);
|
|
429
562
|
});
|
|
430
563
|
|
|
564
|
+
it("follows the sync state channel its worker names", async () => {
|
|
565
|
+
using setup = await setupCreateEvoluDeps();
|
|
566
|
+
const { deps, messages, connect } = setup;
|
|
567
|
+
const workerId = testCreateId()<"SharedWorker">();
|
|
568
|
+
using channel = testCreateBroadcastChannel<SyncState>(
|
|
569
|
+
`evolu:sync-state:${workerId}`,
|
|
570
|
+
);
|
|
571
|
+
using otherChannel = testCreateBroadcastChannel<SyncState>(
|
|
572
|
+
"evolu:sync-state:other-worker",
|
|
573
|
+
);
|
|
574
|
+
const createState = (label: string): SyncState => ({
|
|
575
|
+
transports: [],
|
|
576
|
+
tenants: [{ name: Name.orThrow(label), refused: false, owners: [] }],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Nothing reaches the tab before its worker names the channel.
|
|
580
|
+
channel.postMessage(createState("early"));
|
|
581
|
+
await testWaitForWorkerMessage();
|
|
582
|
+
assertSame(deps.syncState.get(), null);
|
|
583
|
+
|
|
584
|
+
await connect(workerId);
|
|
585
|
+
assertEqual(messages.at(0), { type: "RequestSyncState" });
|
|
586
|
+
channel.postMessage(createState("requested"));
|
|
587
|
+
await testWaitForWorkerMessage();
|
|
588
|
+
assertEqual(deps.syncState.get(), createState("requested"));
|
|
589
|
+
|
|
590
|
+
channel.postMessage(createState("changed"));
|
|
591
|
+
await testWaitForWorkerMessage();
|
|
592
|
+
assertEqual(deps.syncState.get(), createState("changed"));
|
|
593
|
+
|
|
594
|
+
// Another worker's channel is never heard.
|
|
595
|
+
otherChannel.postMessage(createState("other"));
|
|
596
|
+
await testWaitForWorkerMessage();
|
|
597
|
+
assertEqual(deps.syncState.get(), createState("changed"));
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("listens on its worker's channel before asking for a snapshot", async () => {
|
|
601
|
+
const name = "evolu:sync-state:worker";
|
|
602
|
+
using worker = testCreateSharedWorker<
|
|
603
|
+
SharedWorkerInput,
|
|
604
|
+
SharedWorkerOutput
|
|
605
|
+
>();
|
|
606
|
+
worker.self.onConnect = (port) => {
|
|
607
|
+
port.onMessage = constVoid;
|
|
608
|
+
port.postMessage({
|
|
609
|
+
type: "Connected",
|
|
610
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
611
|
+
syncStateChannelName: name,
|
|
612
|
+
});
|
|
613
|
+
};
|
|
614
|
+
worker.connect();
|
|
615
|
+
using channel = testCreateBroadcastChannel<SyncState>(name);
|
|
616
|
+
const requested: SyncState = { transports: [], tenants: [] };
|
|
617
|
+
// A worker on another thread can broadcast its answer before the tab's
|
|
618
|
+
// postMessage returns.
|
|
619
|
+
const port = new Proxy(worker.port, {
|
|
620
|
+
get: (target, property) =>
|
|
621
|
+
property === "postMessage"
|
|
622
|
+
? (message: SharedWorkerInput) => {
|
|
623
|
+
if (message.type === "RequestSyncState")
|
|
624
|
+
channel.postMessage(requested);
|
|
625
|
+
target.postMessage(message);
|
|
626
|
+
}
|
|
627
|
+
: (Reflect.get(target, property) as unknown),
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
using deps = createEvoluDeps({
|
|
631
|
+
createDbWorker: testCreateWorker,
|
|
632
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
633
|
+
createMessageChannel: testCreateMessageChannel,
|
|
634
|
+
lockManager: testCreateLockManager(),
|
|
635
|
+
sharedWorker: { port, [Symbol.dispose]: constVoid },
|
|
636
|
+
reloadApp: constVoid,
|
|
637
|
+
});
|
|
638
|
+
await testWaitForWorkerMessage();
|
|
639
|
+
assertEqual(deps.syncState.get(), requested);
|
|
640
|
+
});
|
|
641
|
+
|
|
431
642
|
it("maps ConsoleEntry error output to deps.evoluError store", async () => {
|
|
432
643
|
using setup = await setupCreateEvoluDeps();
|
|
433
644
|
const { deps, consoleEntryOrErrorBroadcastChannel } = setup;
|
|
@@ -497,6 +708,78 @@ describe("Evolu", () => {
|
|
|
497
708
|
assertEqual(deps.evoluError.get(), error);
|
|
498
709
|
});
|
|
499
710
|
|
|
711
|
+
for (const source of ["SharedWorker", "Error", "ConsoleEntry"] as const) {
|
|
712
|
+
it(`preserves the first startup refusal while logging a later ${source} message`, async () => {
|
|
713
|
+
const testConsole = testCreateConsole();
|
|
714
|
+
using setup = await setupCreateEvoluDeps(testConsole);
|
|
715
|
+
const { deps, sharedWorkerPort, consoleEntryOrErrorBroadcastChannel } =
|
|
716
|
+
setup;
|
|
717
|
+
assertSame(deps.evoluError.get(), null);
|
|
718
|
+
|
|
719
|
+
const initialError = {
|
|
720
|
+
type: "UnknownError",
|
|
721
|
+
error: "before refusal",
|
|
722
|
+
} as const;
|
|
723
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
724
|
+
type: "Error",
|
|
725
|
+
error: initialError,
|
|
726
|
+
});
|
|
727
|
+
await testWaitForWorkerMessage();
|
|
728
|
+
assertEqual(deps.evoluError.get(), initialError);
|
|
729
|
+
|
|
730
|
+
let notifications = 0;
|
|
731
|
+
deps.evoluError.subscribe(() => {
|
|
732
|
+
notifications++;
|
|
733
|
+
});
|
|
734
|
+
const refusal: UnsupportedDbVersionError = {
|
|
735
|
+
type: "UnsupportedDbVersionError",
|
|
736
|
+
storedVersion: PositiveInt.orThrow(2),
|
|
737
|
+
supportedVersion: PositiveInt.orThrow(1),
|
|
738
|
+
};
|
|
739
|
+
sharedWorkerPort.postMessage({ type: "Error", error: refusal });
|
|
740
|
+
await testWaitForWorkerMessage();
|
|
741
|
+
const storedRefusal = deps.evoluError.get();
|
|
742
|
+
assertEqual(storedRefusal, refusal);
|
|
743
|
+
assertSame(notifications, 1);
|
|
744
|
+
|
|
745
|
+
const laterRefusal: UnsupportedDbVersionError = {
|
|
746
|
+
...refusal,
|
|
747
|
+
storedVersion: PositiveInt.orThrow(3),
|
|
748
|
+
};
|
|
749
|
+
const laterError = {
|
|
750
|
+
type: "UnknownError",
|
|
751
|
+
error: "after refusal",
|
|
752
|
+
} as const;
|
|
753
|
+
const laterEntry: ConsoleEntry = {
|
|
754
|
+
method: "error",
|
|
755
|
+
path: source === "ConsoleEntry" ? ["worker"] : [],
|
|
756
|
+
args: [source === "SharedWorker" ? laterRefusal : laterError],
|
|
757
|
+
};
|
|
758
|
+
if (source === "SharedWorker") {
|
|
759
|
+
sharedWorkerPort.postMessage({ type: "Error", error: laterRefusal });
|
|
760
|
+
} else if (source === "Error") {
|
|
761
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
762
|
+
type: "Error",
|
|
763
|
+
error: laterError,
|
|
764
|
+
});
|
|
765
|
+
} else {
|
|
766
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
767
|
+
type: "ConsoleEntry",
|
|
768
|
+
entry: laterEntry,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
await testWaitForWorkerMessage();
|
|
772
|
+
|
|
773
|
+
assertSame(deps.evoluError.get(), storedRefusal);
|
|
774
|
+
assertSame(notifications, 1);
|
|
775
|
+
assertEqual(testConsole.getEntriesSnapshot(), [
|
|
776
|
+
{ method: "error", path: [], args: [initialError] },
|
|
777
|
+
{ method: "error", path: [], args: [refusal] },
|
|
778
|
+
laterEntry,
|
|
779
|
+
]);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
500
783
|
it("throws for unknown tab output type", () => {
|
|
501
784
|
const consoleEntryOrErrorBroadcastChannel: {
|
|
502
785
|
value: {
|
|
@@ -530,13 +813,18 @@ describe("Evolu", () => {
|
|
|
530
813
|
}, Error);
|
|
531
814
|
});
|
|
532
815
|
|
|
533
|
-
it("dispose cleans up resources", () => {
|
|
816
|
+
it("dispose cleans up resources", async () => {
|
|
534
817
|
const worker = testCreateSharedWorker<
|
|
535
818
|
SharedWorkerInput,
|
|
536
819
|
SharedWorkerOutput
|
|
537
820
|
>();
|
|
538
821
|
worker.self.onConnect = (port) => {
|
|
539
822
|
port.onMessage = constVoid;
|
|
823
|
+
port.postMessage({
|
|
824
|
+
type: "Connected",
|
|
825
|
+
workerId: testCreateId()<"SharedWorker">(),
|
|
826
|
+
syncStateChannelName: "evolu:sync-state:worker",
|
|
827
|
+
});
|
|
540
828
|
};
|
|
541
829
|
worker.connect();
|
|
542
830
|
|
|
@@ -564,16 +852,93 @@ describe("Evolu", () => {
|
|
|
564
852
|
reloadApp: constVoid,
|
|
565
853
|
});
|
|
566
854
|
|
|
567
|
-
|
|
855
|
+
// The console/error channel, then the sync state channel its worker names.
|
|
856
|
+
assertSame(broadcastChannels.length, 1);
|
|
857
|
+
await testWaitForWorkerMessage();
|
|
858
|
+
assertLength(broadcastChannels, 2);
|
|
568
859
|
assertFalse(broadcastChannels[0].isDisposed());
|
|
860
|
+
assertFalse(broadcastChannels[1].isDisposed());
|
|
569
861
|
assertFalse(workerDisposed);
|
|
570
862
|
deps[Symbol.dispose]();
|
|
571
863
|
assertTrue(broadcastChannels[0].isDisposed());
|
|
864
|
+
assertTrue(broadcastChannels[1].isDisposed());
|
|
572
865
|
assertTrue(workerDisposed);
|
|
573
866
|
});
|
|
574
867
|
});
|
|
575
868
|
|
|
869
|
+
describe("local tables", () => {
|
|
870
|
+
const LocalSchema = { _todo: Schema.todo };
|
|
871
|
+
const config: EvoluConfig = {
|
|
872
|
+
appName: testAppName,
|
|
873
|
+
appOwner: testAppOwner,
|
|
874
|
+
transports: [],
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
it("keeps useOwner available for an instance with only local tables", async () => {
|
|
878
|
+
const messages: Array<SharedWorkerInput> = [];
|
|
879
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
880
|
+
onSharedWorkerPostMessage: (message) => {
|
|
881
|
+
messages.push(message);
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
const evolu = await setup.run.ok(createEvolu(LocalSchema, config));
|
|
885
|
+
assertType<typeof evolu, Evolu<typeof LocalSchema>>();
|
|
886
|
+
assertSame(evolu.appOwner, testAppOwner);
|
|
887
|
+
assertEqual(evolu.name, `AppName-${createIdFromString(testAppOwner.id)}`);
|
|
888
|
+
await testWaitForWorkerMessage();
|
|
889
|
+
assertEqual(setup.evoluInputs, []);
|
|
890
|
+
const message = messages.find(
|
|
891
|
+
(message) => message.type === "CreateEvolu",
|
|
892
|
+
);
|
|
893
|
+
assertNotUndefined(message);
|
|
894
|
+
assertSame(message.encryptionKey, testAppOwner.encryptionKey);
|
|
895
|
+
assertFalse(message.memoryOnly);
|
|
896
|
+
|
|
897
|
+
evolu.insert("_todo", {
|
|
898
|
+
title: NonEmptyTrimmedString100.orThrow("Local"),
|
|
899
|
+
});
|
|
900
|
+
await testWaitForWorkerMessage();
|
|
901
|
+
assertTrue(setup.evoluInputs.some((input) => input.type === "Mutate"));
|
|
902
|
+
assertFalse(setup.evoluInputs.some((input) => input.type === "UseOwner"));
|
|
903
|
+
const unuseOwner = evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
904
|
+
await testWaitForWorkerMessage();
|
|
905
|
+
assertTrue(setup.evoluInputs.some((input) => input.type === "UseOwner"));
|
|
906
|
+
unuseOwner();
|
|
907
|
+
await evolu[Symbol.asyncDispose]();
|
|
908
|
+
assertThrowsInstanceOf(
|
|
909
|
+
() =>
|
|
910
|
+
evolu.insert("_todo", {
|
|
911
|
+
title: NonEmptyTrimmedString100.orThrow("Disposed"),
|
|
912
|
+
}),
|
|
913
|
+
Error,
|
|
914
|
+
);
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
576
918
|
describe("createEvolu", () => {
|
|
919
|
+
it("uses the default relay when transports are omitted", async () => {
|
|
920
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
921
|
+
await setup.run.ok(
|
|
922
|
+
createEvolu(Schema, {
|
|
923
|
+
appName: testAppName,
|
|
924
|
+
appOwner: testAppOwner,
|
|
925
|
+
}),
|
|
926
|
+
);
|
|
927
|
+
await testWaitForWorkerMessage();
|
|
928
|
+
const input = setup.evoluInputs.find(
|
|
929
|
+
(input) => input.type === "UseOwner",
|
|
930
|
+
);
|
|
931
|
+
assertNotUndefined(input);
|
|
932
|
+
assertEqual(input.actions, [
|
|
933
|
+
{
|
|
934
|
+
action: "add",
|
|
935
|
+
owner: {
|
|
936
|
+
owner: testAppOwner,
|
|
937
|
+
transports: [{ type: "WebSocket", url: "wss://free.evoluhq.com" }],
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
]);
|
|
941
|
+
});
|
|
577
942
|
it("resolves name from appName and appOwner hash", async () => {
|
|
578
943
|
await using setup = await setupRunWithEvoluDeps();
|
|
579
944
|
const { run } = setup;
|
|
@@ -644,6 +1009,70 @@ describe("Evolu", () => {
|
|
|
644
1009
|
assertNonNullable(message.evoluPort);
|
|
645
1010
|
});
|
|
646
1011
|
|
|
1012
|
+
describe("requestSync", () => {
|
|
1013
|
+
it("preserves pending owner operations and returns immediately", async () => {
|
|
1014
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1015
|
+
const { run, evoluInputs } = setup;
|
|
1016
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1017
|
+
const sharedOwner = createSharedOwner(createOwnerSecret(run.deps));
|
|
1018
|
+
const owner = { owner: sharedOwner, transports: [testOwnerTransport] };
|
|
1019
|
+
assertNotSame(sharedOwner.id, evolu.appOwner.id);
|
|
1020
|
+
|
|
1021
|
+
const unuse = evolu.useOwner(sharedOwner, [testOwnerTransport]);
|
|
1022
|
+
const result = evolu.requestSync(sharedOwner.id);
|
|
1023
|
+
assertType<typeof result, void>();
|
|
1024
|
+
assertSame(result, undefined);
|
|
1025
|
+
unuse();
|
|
1026
|
+
evolu.requestSync(sharedOwner.id);
|
|
1027
|
+
assertEqual(evoluInputs, []);
|
|
1028
|
+
|
|
1029
|
+
await testWaitForWorkerMessage();
|
|
1030
|
+
assertEqual(evoluInputs, [
|
|
1031
|
+
{
|
|
1032
|
+
type: "UseOwner",
|
|
1033
|
+
actions: [{ action: "add", owner }],
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
type: "UseOwner",
|
|
1037
|
+
actions: [
|
|
1038
|
+
{ action: "sync", ownerId: sharedOwner.id },
|
|
1039
|
+
{ action: "remove", owner },
|
|
1040
|
+
],
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
type: "UseOwner",
|
|
1044
|
+
actions: [{ action: "sync", ownerId: sharedOwner.id }],
|
|
1045
|
+
},
|
|
1046
|
+
]);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("drops pending requests on disposal", async () => {
|
|
1050
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1051
|
+
const { run, evoluInputs } = setup;
|
|
1052
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1053
|
+
evolu.requestSync(testAppOwner.id);
|
|
1054
|
+
await evolu[Symbol.asyncDispose]();
|
|
1055
|
+
await testWaitForWorkerMessage();
|
|
1056
|
+
assertEqual(evoluInputs, []);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it("preserves registration, mutation, and sync request order", async () => {
|
|
1060
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1061
|
+
const { run, evoluInputs } = setup;
|
|
1062
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1063
|
+
evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
1064
|
+
evolu.insert("todo", {
|
|
1065
|
+
title: NonEmptyTrimmedString100.orThrow("Before sync"),
|
|
1066
|
+
});
|
|
1067
|
+
evolu.requestSync(testAppOwner.id);
|
|
1068
|
+
await testWaitForWorkerMessage();
|
|
1069
|
+
assertEqual(
|
|
1070
|
+
evoluInputs.map(({ type }) => type),
|
|
1071
|
+
["UseOwner", "Mutate", "UseOwner"],
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
647
1076
|
describe("useOwner", () => {
|
|
648
1077
|
it("auto-uses appOwner in a microtask when transports are configured", async () => {
|
|
649
1078
|
await using setup = await setupRunWithEvoluDeps();
|
|
@@ -898,7 +1327,7 @@ describe("Evolu", () => {
|
|
|
898
1327
|
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
899
1328
|
});
|
|
900
1329
|
|
|
901
|
-
it("throws from
|
|
1330
|
+
it("throws from synchronous methods after disposal", async () => {
|
|
902
1331
|
await using setup = await setupRunWithEvoluDeps();
|
|
903
1332
|
const { run } = setup;
|
|
904
1333
|
const evolu = await run.ok(testCreateEvolu);
|
|
@@ -992,6 +1421,13 @@ describe("Evolu", () => {
|
|
|
992
1421
|
}, Error).message,
|
|
993
1422
|
disposedMessage,
|
|
994
1423
|
);
|
|
1424
|
+
|
|
1425
|
+
assertEqual(
|
|
1426
|
+
assertThrowsInstanceOf(() => {
|
|
1427
|
+
evolu.requestSync(testAppOwner.id);
|
|
1428
|
+
}, Error).message,
|
|
1429
|
+
disposedMessage,
|
|
1430
|
+
);
|
|
995
1431
|
});
|
|
996
1432
|
|
|
997
1433
|
it("delete methods are placeholders", async () => {
|
|
@@ -1315,6 +1751,229 @@ describe("Evolu", () => {
|
|
|
1315
1751
|
});
|
|
1316
1752
|
|
|
1317
1753
|
describe("query behavior", () => {
|
|
1754
|
+
it("subscribeQuery catches a refresh between loading and subscribing", async () => {
|
|
1755
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1756
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1757
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1758
|
+
|
|
1759
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1760
|
+
await testWaitForWorkerMessage();
|
|
1761
|
+
postEvoluOutput({
|
|
1762
|
+
type: "OnPatchesByQuery",
|
|
1763
|
+
patchesByQuery: new Map([
|
|
1764
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1765
|
+
]),
|
|
1766
|
+
onCompleteIds: [],
|
|
1767
|
+
});
|
|
1768
|
+
const initialRows = await initialLoad;
|
|
1769
|
+
assertEqual(initialRows, []);
|
|
1770
|
+
|
|
1771
|
+
evoluInputs.length = 0;
|
|
1772
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1773
|
+
await testWaitForWorkerMessage();
|
|
1774
|
+
assertEqual(evoluInputs, []);
|
|
1775
|
+
|
|
1776
|
+
let notifications = 0;
|
|
1777
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(() => {
|
|
1778
|
+
notifications += 1;
|
|
1779
|
+
});
|
|
1780
|
+
// React use() must still see fulfilled rows if another render happens
|
|
1781
|
+
// before the worker answers the refresh queued by subscription.
|
|
1782
|
+
const cachedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1783
|
+
assert("status" in cachedLoad, "Cached promises expose React status");
|
|
1784
|
+
assertSame(cachedLoad.status, "fulfilled");
|
|
1785
|
+
assert("value" in cachedLoad, "Fulfilled promises expose cached rows");
|
|
1786
|
+
assertSame(cachedLoad.value, initialRows);
|
|
1787
|
+
assertSame(await cachedLoad, initialRows);
|
|
1788
|
+
assertSame(notifications, 0);
|
|
1789
|
+
await testWaitForWorkerMessage();
|
|
1790
|
+
assertEqual(evoluInputs, [
|
|
1791
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1792
|
+
]);
|
|
1793
|
+
|
|
1794
|
+
postEvoluOutput({
|
|
1795
|
+
type: "OnPatchesByQuery",
|
|
1796
|
+
patchesByQuery: new Map([
|
|
1797
|
+
[
|
|
1798
|
+
todoTitleQuery,
|
|
1799
|
+
[{ op: "replaceAll", value: [{ title: "Synced" }] }],
|
|
1800
|
+
],
|
|
1801
|
+
]),
|
|
1802
|
+
onCompleteIds: [],
|
|
1803
|
+
});
|
|
1804
|
+
await testWaitForWorkerMessage();
|
|
1805
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), [{ title: "Synced" }]);
|
|
1806
|
+
assertEqual(notifications, 1);
|
|
1807
|
+
// React use() sees a new fulfilled promise after the refresh.
|
|
1808
|
+
const refreshedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1809
|
+
assertNotSame(refreshedLoad, cachedLoad);
|
|
1810
|
+
assertEqual(await refreshedLoad, [{ title: "Synced" }]);
|
|
1811
|
+
unsubscribe();
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
it("subscribeQuery catches a mutation while the initial read is pending", async () => {
|
|
1815
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1816
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1817
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1818
|
+
|
|
1819
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1820
|
+
await testWaitForWorkerMessage();
|
|
1821
|
+
evolu.insert("todo", { title: NonEmptyTrimmedString100.orThrow("New") });
|
|
1822
|
+
await testWaitForWorkerMessage();
|
|
1823
|
+
evoluInputs.length = 0;
|
|
1824
|
+
|
|
1825
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1826
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1827
|
+
await testWaitForWorkerMessage();
|
|
1828
|
+
assertEqual(evoluInputs, [
|
|
1829
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1830
|
+
]);
|
|
1831
|
+
|
|
1832
|
+
// The old read completes first. Its promise must still settle, and the
|
|
1833
|
+
// new subscription must retain the loading entry for the fresh response.
|
|
1834
|
+
postEvoluOutput({
|
|
1835
|
+
type: "OnPatchesByQuery",
|
|
1836
|
+
patchesByQuery: new Map([
|
|
1837
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1838
|
+
]),
|
|
1839
|
+
onCompleteIds: [],
|
|
1840
|
+
});
|
|
1841
|
+
assertEqual(await initialLoad, []);
|
|
1842
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1843
|
+
|
|
1844
|
+
postEvoluOutput({
|
|
1845
|
+
type: "OnPatchesByQuery",
|
|
1846
|
+
patchesByQuery: new Map([
|
|
1847
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "New" }] }]],
|
|
1848
|
+
]),
|
|
1849
|
+
onCompleteIds: [],
|
|
1850
|
+
});
|
|
1851
|
+
await testWaitForWorkerMessage();
|
|
1852
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), [{ title: "New" }]);
|
|
1853
|
+
assertEqual(await evolu.loadQuery(todoTitleQuery), [{ title: "New" }]);
|
|
1854
|
+
unsubscribe();
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
it("subscribeQuery reuses valid pending and completed reads", async () => {
|
|
1858
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1859
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1860
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1861
|
+
|
|
1862
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1863
|
+
await testWaitForWorkerMessage();
|
|
1864
|
+
evoluInputs.length = 0;
|
|
1865
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1866
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1867
|
+
await testWaitForWorkerMessage();
|
|
1868
|
+
assertEqual(evoluInputs, []);
|
|
1869
|
+
|
|
1870
|
+
postEvoluOutput({
|
|
1871
|
+
type: "OnPatchesByQuery",
|
|
1872
|
+
patchesByQuery: new Map([
|
|
1873
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1874
|
+
]),
|
|
1875
|
+
onCompleteIds: [],
|
|
1876
|
+
});
|
|
1877
|
+
await initialLoad;
|
|
1878
|
+
unsubscribe();
|
|
1879
|
+
|
|
1880
|
+
const unsubscribeAgain = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1881
|
+
assertSame(evolu.loadQuery(todoTitleQuery), initialLoad);
|
|
1882
|
+
await testWaitForWorkerMessage();
|
|
1883
|
+
assertEqual(evoluInputs, []);
|
|
1884
|
+
unsubscribeAgain();
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
it("subscribeQuery keeps refreshed rows cached after unsubscribe until the next invalidation", async () => {
|
|
1888
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1889
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1890
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1891
|
+
|
|
1892
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1893
|
+
await testWaitForWorkerMessage();
|
|
1894
|
+
postEvoluOutput({
|
|
1895
|
+
type: "OnPatchesByQuery",
|
|
1896
|
+
patchesByQuery: new Map([
|
|
1897
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1898
|
+
]),
|
|
1899
|
+
onCompleteIds: [],
|
|
1900
|
+
});
|
|
1901
|
+
await initialLoad;
|
|
1902
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1903
|
+
await testWaitForWorkerMessage();
|
|
1904
|
+
evoluInputs.length = 0;
|
|
1905
|
+
|
|
1906
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1907
|
+
await testWaitForWorkerMessage();
|
|
1908
|
+
assertEqual(evoluInputs, [
|
|
1909
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1910
|
+
]);
|
|
1911
|
+
unsubscribe();
|
|
1912
|
+
evoluInputs.length = 0;
|
|
1913
|
+
|
|
1914
|
+
// The refresh response still replaces the cached rows.
|
|
1915
|
+
postEvoluOutput({
|
|
1916
|
+
type: "OnPatchesByQuery",
|
|
1917
|
+
patchesByQuery: new Map([
|
|
1918
|
+
[
|
|
1919
|
+
todoTitleQuery,
|
|
1920
|
+
[{ op: "replaceAll", value: [{ title: "Synced" }] }],
|
|
1921
|
+
],
|
|
1922
|
+
]),
|
|
1923
|
+
onCompleteIds: [],
|
|
1924
|
+
});
|
|
1925
|
+
await testWaitForWorkerMessage();
|
|
1926
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), [{ title: "Synced" }]);
|
|
1927
|
+
const cachedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1928
|
+
assertEqual(await cachedLoad, [{ title: "Synced" }]);
|
|
1929
|
+
await testWaitForWorkerMessage();
|
|
1930
|
+
assertEqual(evoluInputs, []);
|
|
1931
|
+
|
|
1932
|
+
// The next invalidation drops the unsubscribed cache without a read.
|
|
1933
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1934
|
+
await testWaitForWorkerMessage();
|
|
1935
|
+
assertEqual(evoluInputs, []);
|
|
1936
|
+
const reload = evolu.loadQuery(todoTitleQuery);
|
|
1937
|
+
assertNotSame(reload, cachedLoad);
|
|
1938
|
+
await testWaitForWorkerMessage();
|
|
1939
|
+
assertEqual(evoluInputs, [
|
|
1940
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1941
|
+
]);
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it("subscribeQuery reuses the refreshing entry for repeated subscriptions in one tick", async () => {
|
|
1945
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1946
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1947
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1948
|
+
|
|
1949
|
+
const initialLoad = evolu.loadQuery(todoTitleQuery);
|
|
1950
|
+
await testWaitForWorkerMessage();
|
|
1951
|
+
postEvoluOutput({
|
|
1952
|
+
type: "OnPatchesByQuery",
|
|
1953
|
+
patchesByQuery: new Map([
|
|
1954
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [] }]],
|
|
1955
|
+
]),
|
|
1956
|
+
onCompleteIds: [],
|
|
1957
|
+
});
|
|
1958
|
+
await initialLoad;
|
|
1959
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1960
|
+
await testWaitForWorkerMessage();
|
|
1961
|
+
evoluInputs.length = 0;
|
|
1962
|
+
|
|
1963
|
+
// React StrictMode subscribes, unsubscribes, and subscribes again
|
|
1964
|
+
// synchronously.
|
|
1965
|
+
const unsubscribeFirst = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1966
|
+
const refreshingLoad = evolu.loadQuery(todoTitleQuery);
|
|
1967
|
+
unsubscribeFirst();
|
|
1968
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1969
|
+
assertSame(evolu.loadQuery(todoTitleQuery), refreshingLoad);
|
|
1970
|
+
await testWaitForWorkerMessage();
|
|
1971
|
+
assertEqual(evoluInputs, [
|
|
1972
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1973
|
+
]);
|
|
1974
|
+
unsubscribe();
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1318
1977
|
it("loadQuery reuses pending promise and sends one Query message", async () => {
|
|
1319
1978
|
await using setup = await setupRunWithEvoluDeps();
|
|
1320
1979
|
const { run, evoluInputs } = setup;
|
|
@@ -1399,7 +2058,7 @@ describe("Evolu", () => {
|
|
|
1399
2058
|
);
|
|
1400
2059
|
});
|
|
1401
2060
|
|
|
1402
|
-
it("RefreshQueries
|
|
2061
|
+
it("RefreshQueries reads a pending unsubscribed loadQuery once, when it is subscribed", async () => {
|
|
1403
2062
|
await using setup = await setupRunWithEvoluDeps();
|
|
1404
2063
|
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1405
2064
|
const evolu = await run.ok(testCreateEvolu);
|
|
@@ -1409,12 +2068,18 @@ describe("Evolu", () => {
|
|
|
1409
2068
|
|
|
1410
2069
|
evoluInputs.length = 0;
|
|
1411
2070
|
postEvoluOutput({ type: "RefreshQueries" });
|
|
2071
|
+
await testWaitForWorkerMessage();
|
|
1412
2072
|
|
|
2073
|
+
assertEqual(evoluInputs, []);
|
|
2074
|
+
|
|
2075
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1413
2076
|
await testWaitForWorkerMessage();
|
|
1414
2077
|
|
|
1415
2078
|
assertEqual(evoluInputs, [
|
|
1416
2079
|
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1417
2080
|
]);
|
|
2081
|
+
|
|
2082
|
+
unsubscribe();
|
|
1418
2083
|
});
|
|
1419
2084
|
|
|
1420
2085
|
it("RefreshQueries re-queries subscribed query without loadQuery", async () => {
|