@evolu/common 8.7.0 → 8.9.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/Array.d.ts +21 -21
- package/dist/src/Array.js +5 -5
- package/dist/src/Assert.d.ts +498 -141
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +648 -99
- package/dist/src/{Buffer.d.ts → Binary.d.ts} +85 -12
- package/dist/src/Binary.d.ts.map +1 -0
- package/dist/src/{Buffer.js → Binary.js} +206 -24
- package/dist/src/Console.d.ts +2 -10
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +6 -12
- package/dist/src/Crypto.d.ts +5 -5
- package/dist/src/Crypto.js +1 -1
- package/dist/src/Eq.d.ts +35 -40
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +51 -87
- package/dist/src/Function.d.ts +9 -14
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +7 -9
- package/dist/src/Http.d.ts +6 -7
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +1 -0
- package/dist/src/LockManager.d.ts +1 -1
- package/dist/src/LockManager.js +1 -1
- package/dist/src/Lookup.d.ts +3 -2
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +4 -3
- package/dist/src/Number.d.ts +1 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +1 -1
- package/dist/src/Object.d.ts +4 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -4
- package/dist/src/Option.d.ts +1 -2
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +1 -2
- package/dist/src/Platform.js +2 -0
- package/dist/src/Polyfills.d.ts +6 -0
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +10 -1
- package/dist/src/Random.d.ts +1 -1
- package/dist/src/Result.d.ts +49 -54
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +17 -22
- package/dist/src/Set.d.ts +6 -6
- package/dist/src/Set.js +2 -2
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/String.js +3 -2
- package/dist/src/Task.d.ts +32 -32
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +20 -22
- package/dist/src/Test.d.ts +31 -3
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +46 -3
- package/dist/src/Time.d.ts +3 -11
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +10 -18
- package/dist/src/Type.d.ts +87 -85
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +70 -63
- package/dist/src/Types.d.ts +12 -12
- package/dist/src/Types.js +1 -1
- package/dist/src/WebSocket.d.ts +4 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts +1 -1
- package/dist/src/Worker.js +7 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +1 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +1 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +1 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +1 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +1 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +1 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +1 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +1 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +1 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +1 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +1 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +1 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +1 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +1 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +1 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +1 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +1 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +1 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +1 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +1 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +1 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +1 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +1 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +1 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +1 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +1 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +1 -1
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +1 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +1 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +1 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +1 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +1 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +1 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +1 -1
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +1 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +1 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +1 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +1 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +1 -1
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +1 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +1 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +1 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +1 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +1 -1
- package/dist/src/local-first/Evolu.d.ts +12 -12
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +2 -2
- package/dist/src/local-first/Protocol.d.ts +14 -76
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +29 -206
- package/dist/src/local-first/Query.d.ts +11 -11
- package/dist/src/local-first/Query.js +6 -6
- package/dist/src/local-first/Relay.d.ts +2 -2
- package/dist/src/local-first/Schema.d.ts +3 -5
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +2 -4
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +5 -5
- package/dist/src/local-first/Storage.js +4 -4
- package/dist/src/local-first/Timestamp.d.ts +9 -0
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +13 -10
- package/package.json +7 -4
- package/src/Array.test.ts +809 -0
- package/src/Array.ts +21 -21
- package/src/Assert.test.ts +1232 -0
- package/src/Assert.ts +1023 -227
- package/src/BigInt.test.ts +28 -0
- package/src/Binary.test.ts +1128 -0
- package/src/{Buffer.ts → Binary.ts} +273 -46
- package/src/Brand.test.ts +90 -0
- package/src/Cache.test.ts +183 -0
- package/src/Callbacks.test.ts +66 -0
- package/src/Console.test.ts +809 -0
- package/src/Console.ts +9 -17
- package/src/Crypto.test.ts +138 -0
- package/src/Crypto.ts +5 -5
- package/src/Eq.test.ts +333 -0
- package/src/Eq.ts +110 -176
- package/src/Error.test.ts +118 -0
- package/src/Function.test.ts +204 -0
- package/src/Function.ts +10 -15
- package/src/Http.test.ts +816 -0
- package/src/Http.ts +6 -7
- package/src/LeakDetector.test.ts +210 -0
- package/src/LeakDetector.ts +1 -0
- package/src/LockManager.ts +2 -2
- package/src/Lookup.test.ts +418 -0
- package/src/Lookup.ts +4 -3
- package/src/Microtask.test.ts +140 -0
- package/src/Number.test.ts +218 -0
- package/src/Number.ts +2 -2
- package/src/Object.test.ts +293 -0
- package/src/Object.ts +5 -4
- package/src/Option.test.ts +104 -0
- package/src/Option.ts +1 -2
- package/src/Order.test.ts +56 -0
- package/src/Platform.ts +2 -0
- package/src/Polyfills.ts +9 -4
- package/src/Random.test.ts +89 -0
- package/src/Random.ts +1 -1
- package/src/Ref.test.ts +139 -0
- package/src/RefCount.test.ts +184 -0
- package/src/Relation.test.ts +505 -0
- package/src/Resource.test.ts +4623 -0
- package/src/Result.test.ts +1829 -0
- package/src/Result.ts +49 -54
- package/src/Schedule.test.ts +1554 -0
- package/src/Set.test.ts +279 -0
- package/src/Set.ts +6 -6
- package/src/Sqlite.ts +1 -1
- package/src/Store.test.ts +288 -0
- package/src/String.test.ts +54 -0
- package/src/String.ts +3 -3
- package/src/Task.test.ts +10820 -0
- package/src/Task.ts +38 -43
- package/src/Test.test.ts +88 -0
- package/src/Test.ts +49 -3
- package/src/Time.test.ts +926 -0
- package/src/Time.ts +13 -21
- package/src/Type.base64.test.ts +88 -0
- package/src/Type.test.ts +20465 -0
- package/src/Type.ts +114 -111
- package/src/Types.test.ts +324 -0
- package/src/Types.ts +13 -13
- package/src/WebSocket.ts +4 -7
- package/src/Worker.test.ts +755 -0
- package/src/Worker.ts +13 -18
- package/src/index.ts +1 -1
- package/src/intl/_en.ts +1 -1
- package/src/intl/ar.ts +1 -1
- package/src/intl/bn.ts +1 -1
- package/src/intl/ca.ts +1 -1
- package/src/intl/cs.ts +1 -1
- package/src/intl/da.ts +1 -1
- package/src/intl/de.ts +1 -1
- package/src/intl/el.ts +1 -1
- package/src/intl/es.ts +1 -1
- package/src/intl/fa.ts +1 -1
- package/src/intl/fi.ts +1 -1
- package/src/intl/fil.ts +1 -1
- package/src/intl/fr.ts +1 -1
- package/src/intl/he.ts +1 -1
- package/src/intl/hi.ts +1 -1
- package/src/intl/hr.ts +1 -1
- package/src/intl/hu.ts +1 -1
- package/src/intl/id.ts +1 -1
- package/src/intl/intl.test.ts +3106 -0
- package/src/intl/it.ts +1 -1
- package/src/intl/ja.ts +1 -1
- package/src/intl/ko.ts +1 -1
- package/src/intl/ml.ts +1 -1
- package/src/intl/mr.ts +1 -1
- package/src/intl/ms.ts +1 -1
- package/src/intl/nb.ts +1 -1
- package/src/intl/nl.ts +1 -1
- package/src/intl/pa.ts +1 -1
- package/src/intl/pl.ts +1 -1
- package/src/intl/pt-BR.ts +1 -1
- package/src/intl/pt.ts +1 -1
- package/src/intl/ro.ts +1 -1
- package/src/intl/sk.ts +1 -1
- package/src/intl/sl.ts +1 -1
- package/src/intl/sv.ts +1 -1
- package/src/intl/sw.ts +1 -1
- package/src/intl/ta.ts +1 -1
- package/src/intl/te.ts +1 -1
- package/src/intl/th.ts +1 -1
- package/src/intl/tr.ts +1 -1
- package/src/intl/uk.ts +1 -1
- package/src/intl/ur.ts +1 -1
- package/src/intl/vi.ts +1 -1
- package/src/intl/zh-CN.ts +1 -1
- package/src/intl/zh-TW.ts +1 -1
- package/src/local-first/Evolu.test.ts +1921 -0
- package/src/local-first/Evolu.ts +18 -14
- package/src/local-first/Owner.test.ts +83 -0
- package/src/local-first/Protocol.test.ts +1037 -0
- package/src/local-first/Protocol.ts +58 -261
- package/src/local-first/Query.test.ts +349 -0
- package/src/local-first/Query.ts +11 -11
- package/src/local-first/Relay.ts +2 -2
- package/src/local-first/Schema.ts +3 -5
- package/src/local-first/Shared.test.ts +1828 -0
- package/src/local-first/Shared.ts +9 -8
- package/src/local-first/Storage.ts +4 -4
- package/src/local-first/Timestamp.test.ts +374 -0
- package/src/local-first/Timestamp.ts +22 -10
- package/dist/src/Buffer.d.ts.map +0 -1
|
@@ -0,0 +1,1921 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assert,
|
|
3
|
+
assertEqual,
|
|
4
|
+
assertEqualBytes,
|
|
5
|
+
assertFalse,
|
|
6
|
+
assertLength,
|
|
7
|
+
assertNonNullable,
|
|
8
|
+
assertNotNull,
|
|
9
|
+
assertNotSame,
|
|
10
|
+
assertNotUndefined,
|
|
11
|
+
assertRejects,
|
|
12
|
+
assertSame,
|
|
13
|
+
assertThrowsInstanceOf,
|
|
14
|
+
assertTrue,
|
|
15
|
+
} from "../Assert.ts";
|
|
16
|
+
import { describe, it } from "node:test";
|
|
17
|
+
import type { Brand } from "../Brand.ts";
|
|
18
|
+
import type { ConsoleEntry, TestConsole } from "../Console.ts";
|
|
19
|
+
import { testCreateConsole } from "../Console.ts";
|
|
20
|
+
import { constVoid } from "../Function.ts";
|
|
21
|
+
import type { DbWorkerInit } from "./Db.ts";
|
|
22
|
+
import {
|
|
23
|
+
AppName,
|
|
24
|
+
createEvolu,
|
|
25
|
+
createEvoluDeps,
|
|
26
|
+
testAppName,
|
|
27
|
+
type EvoluPlatformDeps,
|
|
28
|
+
} from "./Evolu.ts";
|
|
29
|
+
import { createOwnerWebSocketTransport, testAppOwner } from "./Owner.ts";
|
|
30
|
+
import { createQueryBuilder } from "./Schema.ts";
|
|
31
|
+
import {
|
|
32
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
33
|
+
type ConsoleEntryOrError,
|
|
34
|
+
type DbWorkerInput,
|
|
35
|
+
type DbWorkerOutput,
|
|
36
|
+
type EvoluInput,
|
|
37
|
+
type EvoluOutput,
|
|
38
|
+
type SharedWorker,
|
|
39
|
+
type SharedWorkerInput,
|
|
40
|
+
type SharedWorkerOutput,
|
|
41
|
+
} from "./Shared.ts";
|
|
42
|
+
import { testCreateLockManager } from "../LockManager.ts";
|
|
43
|
+
import { installPolyfills } from "../Polyfills.ts";
|
|
44
|
+
import { err, ok } from "../Result.ts";
|
|
45
|
+
import { SqliteBoolean } from "../Sqlite.ts";
|
|
46
|
+
import { explicitAbortReason, testCreateRun } from "../Task.ts";
|
|
47
|
+
import {
|
|
48
|
+
assertType,
|
|
49
|
+
createIdFromString,
|
|
50
|
+
id,
|
|
51
|
+
NonEmptyTrimmedString100,
|
|
52
|
+
nullOr,
|
|
53
|
+
testName,
|
|
54
|
+
} from "../Type.ts";
|
|
55
|
+
import type { ExtractTyped } from "../Type.ts";
|
|
56
|
+
import {
|
|
57
|
+
testCreateBroadcastChannel,
|
|
58
|
+
testCreateMessageChannel,
|
|
59
|
+
testCreateMessagePort,
|
|
60
|
+
testCreateSharedWorker,
|
|
61
|
+
testCreateWorker,
|
|
62
|
+
testWaitForWorkerMessage,
|
|
63
|
+
type MessagePort,
|
|
64
|
+
} from "../Worker.ts";
|
|
65
|
+
|
|
66
|
+
installPolyfills();
|
|
67
|
+
|
|
68
|
+
const TodoId = id("Todo");
|
|
69
|
+
type TodoId = typeof TodoId.Output;
|
|
70
|
+
|
|
71
|
+
const Schema = {
|
|
72
|
+
todo: {
|
|
73
|
+
id: TodoId,
|
|
74
|
+
title: NonEmptyTrimmedString100,
|
|
75
|
+
isCompleted: nullOr(SqliteBoolean),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const testCreateEvolu = createEvolu(Schema, {
|
|
80
|
+
appName: testAppName,
|
|
81
|
+
appOwner: testAppOwner,
|
|
82
|
+
transports: [],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const testOwnerTransport = createOwnerWebSocketTransport({
|
|
86
|
+
url: "wss://example.com",
|
|
87
|
+
ownerId: testAppOwner.id,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const createQuery = createQueryBuilder(Schema);
|
|
91
|
+
|
|
92
|
+
const todoTitleQuery = createQuery((db) =>
|
|
93
|
+
db.selectFrom("todo").select(["title"]).orderBy("createdAt"),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const todoTitleDescQuery = createQuery((db) =>
|
|
97
|
+
db.selectFrom("todo").select(["title"]).orderBy("createdAt", "desc"),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
describe("Evolu", () => {
|
|
101
|
+
const setupRunWithEvoluDeps = async ({
|
|
102
|
+
onSharedWorkerPostMessage,
|
|
103
|
+
...overrides
|
|
104
|
+
}: Partial<EvoluPlatformDeps> & {
|
|
105
|
+
readonly onSharedWorkerPostMessage?: (message: SharedWorkerInput) => void;
|
|
106
|
+
} = {}) => {
|
|
107
|
+
await using disposer = new AsyncDisposableStack();
|
|
108
|
+
|
|
109
|
+
const sharedWorker = disposer.use(
|
|
110
|
+
testCreateSharedWorker<SharedWorkerInput, SharedWorkerOutput>(),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const evoluInputs: Array<EvoluInput> = [];
|
|
114
|
+
let evoluPort: {
|
|
115
|
+
onMessage: ((input: EvoluInput) => void) | null;
|
|
116
|
+
readonly postMessage: (output: EvoluOutput) => void;
|
|
117
|
+
} | null = null;
|
|
118
|
+
const queuedEvoluOutputs: Array<EvoluOutput> = [];
|
|
119
|
+
|
|
120
|
+
sharedWorker.self.onConnect = (port) => {
|
|
121
|
+
port.onMessage = (message) => {
|
|
122
|
+
if (message.type !== "CreateEvolu") return;
|
|
123
|
+
evoluPort = testCreateMessagePort<EvoluOutput, EvoluInput>(
|
|
124
|
+
message.evoluPort,
|
|
125
|
+
);
|
|
126
|
+
evoluPort.onMessage = (input) => {
|
|
127
|
+
evoluInputs.push(input);
|
|
128
|
+
};
|
|
129
|
+
for (const output of queuedEvoluOutputs) {
|
|
130
|
+
evoluPort.postMessage(output);
|
|
131
|
+
}
|
|
132
|
+
queuedEvoluOutputs.length = 0;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
sharedWorker.connect();
|
|
136
|
+
|
|
137
|
+
const postEvoluOutput = (output: EvoluOutput) => {
|
|
138
|
+
if (evoluPort) {
|
|
139
|
+
evoluPort.postMessage(output);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
queuedEvoluOutputs.push(output);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const sharedWorkerDep: EvoluPlatformDeps["sharedWorker"] =
|
|
146
|
+
onSharedWorkerPostMessage
|
|
147
|
+
? {
|
|
148
|
+
port: {
|
|
149
|
+
postMessage: (message, transfer) => {
|
|
150
|
+
sharedWorker.port.postMessage(message, transfer);
|
|
151
|
+
onSharedWorkerPostMessage(message);
|
|
152
|
+
},
|
|
153
|
+
get onMessage() {
|
|
154
|
+
return sharedWorker.port.onMessage;
|
|
155
|
+
},
|
|
156
|
+
set onMessage(value) {
|
|
157
|
+
sharedWorker.port.onMessage = value;
|
|
158
|
+
},
|
|
159
|
+
native: sharedWorker.port.native,
|
|
160
|
+
[Symbol.dispose]: constVoid,
|
|
161
|
+
},
|
|
162
|
+
[Symbol.dispose]: constVoid,
|
|
163
|
+
}
|
|
164
|
+
: sharedWorker;
|
|
165
|
+
|
|
166
|
+
const evoluDeps: EvoluPlatformDeps = {
|
|
167
|
+
createDbWorker: testCreateWorker,
|
|
168
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
169
|
+
createMessageChannel: testCreateMessageChannel,
|
|
170
|
+
lockManager: testCreateLockManager(),
|
|
171
|
+
reloadApp: constVoid,
|
|
172
|
+
sharedWorker: sharedWorkerDep,
|
|
173
|
+
...overrides,
|
|
174
|
+
};
|
|
175
|
+
const run = disposer.use(testCreateRun(evoluDeps));
|
|
176
|
+
const disposables = disposer.move();
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
run,
|
|
180
|
+
evoluInputs,
|
|
181
|
+
postEvoluOutput,
|
|
182
|
+
[Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
it("AppName", () => {
|
|
187
|
+
assertEqual(AppName.fromUnknown("my-app"), ok("my-app"));
|
|
188
|
+
assertEqual(
|
|
189
|
+
AppName.fromUnknown(""),
|
|
190
|
+
err({
|
|
191
|
+
type: "UrlSafeString",
|
|
192
|
+
source: "^[A-Za-z0-9_-]+$",
|
|
193
|
+
flags: "u",
|
|
194
|
+
value: "",
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
assertEqual(AppName.fromUnknown("a".repeat(41)), ok("a".repeat(41)));
|
|
198
|
+
assertEqual(
|
|
199
|
+
AppName.fromUnknown("a".repeat(42)),
|
|
200
|
+
err({
|
|
201
|
+
type: "AppName",
|
|
202
|
+
value: "a".repeat(42),
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const appName = AppName.orThrow("my-app");
|
|
207
|
+
assertType<typeof appName, AppName>();
|
|
208
|
+
assertType<typeof AppName.Input, string>();
|
|
209
|
+
assertType<typeof AppName.parent.Output, string & Brand<"UrlSafeString">>();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("createEvoluDeps", () => {
|
|
213
|
+
const setupCreateEvoluDeps = async (
|
|
214
|
+
console: TestConsole = testCreateConsole(),
|
|
215
|
+
): Promise<{
|
|
216
|
+
readonly deps: ReturnType<typeof createEvoluDeps>;
|
|
217
|
+
readonly messages: Array<SharedWorkerInput>;
|
|
218
|
+
readonly consoleEntryOrErrorBroadcastChannel: {
|
|
219
|
+
readonly postMessage: (message: ConsoleEntryOrError) => void;
|
|
220
|
+
};
|
|
221
|
+
readonly [Symbol.dispose]: () => void;
|
|
222
|
+
}> => {
|
|
223
|
+
using disposer = new DisposableStack();
|
|
224
|
+
const worker = disposer.use(
|
|
225
|
+
testCreateSharedWorker<SharedWorkerInput, SharedWorkerOutput>(),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const messages: Array<SharedWorkerInput> = [];
|
|
229
|
+
worker.self.onConnect = (port) => {
|
|
230
|
+
port.onMessage = (message) => {
|
|
231
|
+
messages.push(message);
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
worker.connect();
|
|
235
|
+
|
|
236
|
+
const deps = disposer.use(
|
|
237
|
+
createEvoluDeps({
|
|
238
|
+
createDbWorker: testCreateWorker,
|
|
239
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
240
|
+
createMessageChannel: testCreateMessageChannel,
|
|
241
|
+
lockManager: testCreateLockManager(),
|
|
242
|
+
sharedWorker: worker,
|
|
243
|
+
reloadApp: constVoid,
|
|
244
|
+
console,
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
await testWaitForWorkerMessage();
|
|
249
|
+
|
|
250
|
+
assertLength(messages, 1);
|
|
251
|
+
assertSame(messages[0].type, "AnnounceTabLeader");
|
|
252
|
+
const consoleEntryOrErrorBroadcastChannel = disposer.use(
|
|
253
|
+
testCreateBroadcastChannel<ConsoleEntryOrError>(
|
|
254
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
const disposables = disposer.move();
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
deps,
|
|
261
|
+
messages,
|
|
262
|
+
consoleEntryOrErrorBroadcastChannel,
|
|
263
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
it("posts AnnounceTabLeader with console level to worker", async () => {
|
|
268
|
+
const testConsole = testCreateConsole();
|
|
269
|
+
using setup = await setupCreateEvoluDeps(testConsole);
|
|
270
|
+
const { messages } = setup;
|
|
271
|
+
|
|
272
|
+
assertLength(messages, 1);
|
|
273
|
+
assertEqual(messages[0], {
|
|
274
|
+
type: "AnnounceTabLeader",
|
|
275
|
+
consoleLevel: testConsole.getLevel(),
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("initializes db worker from shared worker init output", async () => {
|
|
280
|
+
const dbWorkerMessages: Array<DbWorkerInit> = [];
|
|
281
|
+
const dbWorkerChannel = testCreateMessageChannel<
|
|
282
|
+
DbWorkerOutput,
|
|
283
|
+
DbWorkerInput
|
|
284
|
+
>();
|
|
285
|
+
const sqliteSchema = {
|
|
286
|
+
tables: { todo: new Set(["title"]) },
|
|
287
|
+
indexes: [],
|
|
288
|
+
};
|
|
289
|
+
const sharedWorkerPort: {
|
|
290
|
+
value: MessagePort<SharedWorkerOutput, SharedWorkerInput> | null;
|
|
291
|
+
} = { value: null };
|
|
292
|
+
|
|
293
|
+
const worker = testCreateSharedWorker<
|
|
294
|
+
SharedWorkerInput,
|
|
295
|
+
SharedWorkerOutput
|
|
296
|
+
>();
|
|
297
|
+
worker.self.onConnect = (port) => {
|
|
298
|
+
sharedWorkerPort.value = port;
|
|
299
|
+
port.onMessage = constVoid;
|
|
300
|
+
};
|
|
301
|
+
worker.connect();
|
|
302
|
+
|
|
303
|
+
using _deps = createEvoluDeps({
|
|
304
|
+
createDbWorker: () => {
|
|
305
|
+
const worker = testCreateWorker<DbWorkerInit>();
|
|
306
|
+
worker.self.onMessage = (message) => {
|
|
307
|
+
dbWorkerMessages.push(message);
|
|
308
|
+
};
|
|
309
|
+
return worker;
|
|
310
|
+
},
|
|
311
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
312
|
+
createMessageChannel: testCreateMessageChannel,
|
|
313
|
+
lockManager: testCreateLockManager(),
|
|
314
|
+
sharedWorker: worker,
|
|
315
|
+
reloadApp: constVoid,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await testWaitForWorkerMessage();
|
|
319
|
+
assertNotNull(sharedWorkerPort.value);
|
|
320
|
+
|
|
321
|
+
sharedWorkerPort.value.postMessage(
|
|
322
|
+
{
|
|
323
|
+
type: "DbWorkerInit",
|
|
324
|
+
name: testName,
|
|
325
|
+
consoleLevel: "debug",
|
|
326
|
+
sqliteSchema,
|
|
327
|
+
encryptionKey: testAppOwner.encryptionKey,
|
|
328
|
+
memoryOnly: true,
|
|
329
|
+
port: dbWorkerChannel.port1.native,
|
|
330
|
+
},
|
|
331
|
+
[dbWorkerChannel.port1.native],
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
await testWaitForWorkerMessage();
|
|
335
|
+
|
|
336
|
+
assertLength(dbWorkerMessages, 1);
|
|
337
|
+
assertEqual(dbWorkerMessages[0], {
|
|
338
|
+
type: "DbWorkerInit",
|
|
339
|
+
name: testName,
|
|
340
|
+
consoleLevel: "debug",
|
|
341
|
+
sqliteSchema,
|
|
342
|
+
encryptionKey: testAppOwner.encryptionKey,
|
|
343
|
+
memoryOnly: true,
|
|
344
|
+
port: dbWorkerChannel.port1.native,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
dbWorkerChannel[Symbol.dispose]();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("falls back to default console when not provided", async () => {
|
|
351
|
+
// oxlint-disable-next-line evolu/no-unnecessary-global-this -- This test temporarily observes the default global object console.
|
|
352
|
+
const nativeConsole = globalThis.console;
|
|
353
|
+
const originalConsoleError = nativeConsole.error;
|
|
354
|
+
const consoleErrors: Array<ReadonlyArray<unknown>> = [];
|
|
355
|
+
nativeConsole.error = ((...args: ReadonlyArray<unknown>) => {
|
|
356
|
+
consoleErrors.push(args);
|
|
357
|
+
}) as typeof nativeConsole.error;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const worker = testCreateSharedWorker<
|
|
361
|
+
SharedWorkerInput,
|
|
362
|
+
SharedWorkerOutput
|
|
363
|
+
>();
|
|
364
|
+
const messages: Array<SharedWorkerInput> = [];
|
|
365
|
+
|
|
366
|
+
worker.self.onConnect = (port) => {
|
|
367
|
+
port.onMessage = (message) => {
|
|
368
|
+
messages.push(message);
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
worker.connect();
|
|
372
|
+
|
|
373
|
+
using deps = createEvoluDeps({
|
|
374
|
+
createDbWorker: testCreateWorker,
|
|
375
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
376
|
+
createMessageChannel: testCreateMessageChannel,
|
|
377
|
+
lockManager: testCreateLockManager(),
|
|
378
|
+
sharedWorker: worker,
|
|
379
|
+
reloadApp: constVoid,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await testWaitForWorkerMessage();
|
|
383
|
+
|
|
384
|
+
assertNotUndefined(deps.console);
|
|
385
|
+
assertLength(messages, 1);
|
|
386
|
+
assertSame(messages[0].type, "AnnounceTabLeader");
|
|
387
|
+
using consoleEntryOrErrorBroadcastChannel =
|
|
388
|
+
testCreateBroadcastChannel<ConsoleEntryOrError>(
|
|
389
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
393
|
+
type: "ConsoleEntry",
|
|
394
|
+
entry: { method: "error", path: ["global"], args: ["boom"] },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await testWaitForWorkerMessage();
|
|
398
|
+
|
|
399
|
+
assertLength(messages, 1);
|
|
400
|
+
assertSame(messages[0].type, "AnnounceTabLeader");
|
|
401
|
+
assertEqual(deps.evoluError.get(), {
|
|
402
|
+
type: "UnknownError",
|
|
403
|
+
error: ["boom"],
|
|
404
|
+
});
|
|
405
|
+
assertEqual(consoleErrors, [["boom"]]);
|
|
406
|
+
} finally {
|
|
407
|
+
nativeConsole.error = originalConsoleError;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("wires console channel to console.write", async () => {
|
|
412
|
+
const testConsole = testCreateConsole();
|
|
413
|
+
using setup = await setupCreateEvoluDeps(testConsole);
|
|
414
|
+
const { consoleEntryOrErrorBroadcastChannel } = setup;
|
|
415
|
+
|
|
416
|
+
const entry: ConsoleEntry = {
|
|
417
|
+
method: "info",
|
|
418
|
+
path: ["test"],
|
|
419
|
+
args: ["hello"],
|
|
420
|
+
};
|
|
421
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
422
|
+
type: "ConsoleEntry",
|
|
423
|
+
entry,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await testWaitForWorkerMessage();
|
|
427
|
+
|
|
428
|
+
assertEqual(testConsole.getEntriesSnapshot(), [entry]);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("maps ConsoleEntry error output to deps.evoluError store", async () => {
|
|
432
|
+
using setup = await setupCreateEvoluDeps();
|
|
433
|
+
const { deps, consoleEntryOrErrorBroadcastChannel } = setup;
|
|
434
|
+
|
|
435
|
+
const entry: ConsoleEntry = {
|
|
436
|
+
method: "error",
|
|
437
|
+
path: ["global"],
|
|
438
|
+
args: ["error", { type: "UnknownError", error: "boom" }],
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
442
|
+
type: "ConsoleEntry",
|
|
443
|
+
entry,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await testWaitForWorkerMessage();
|
|
447
|
+
|
|
448
|
+
assertEqual(deps.evoluError.get(), {
|
|
449
|
+
type: "UnknownError",
|
|
450
|
+
error: ["error", { type: "UnknownError", error: "boom" }],
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("wraps single-arg ConsoleEntry error output to UnknownError", async () => {
|
|
455
|
+
using setup = await setupCreateEvoluDeps();
|
|
456
|
+
const { deps, consoleEntryOrErrorBroadcastChannel } = setup;
|
|
457
|
+
|
|
458
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
459
|
+
type: "ConsoleEntry",
|
|
460
|
+
entry: { method: "error", path: ["global"], args: ["boom"] },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await testWaitForWorkerMessage();
|
|
464
|
+
|
|
465
|
+
assertEqual(deps.evoluError.get(), {
|
|
466
|
+
type: "UnknownError",
|
|
467
|
+
error: ["boom"],
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("wraps multi-arg ConsoleEntry error output to UnknownError", async () => {
|
|
472
|
+
using setup = await setupCreateEvoluDeps();
|
|
473
|
+
const { deps, consoleEntryOrErrorBroadcastChannel } = setup;
|
|
474
|
+
|
|
475
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
476
|
+
type: "ConsoleEntry",
|
|
477
|
+
entry: { method: "error", path: ["global"], args: ["error", "boom"] },
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await testWaitForWorkerMessage();
|
|
481
|
+
|
|
482
|
+
assertEqual(deps.evoluError.get(), {
|
|
483
|
+
type: "UnknownError",
|
|
484
|
+
error: ["error", "boom"],
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("wires EvoluError output to deps.evoluError store", async () => {
|
|
489
|
+
using setup = await setupCreateEvoluDeps();
|
|
490
|
+
const { deps, consoleEntryOrErrorBroadcastChannel } = setup;
|
|
491
|
+
|
|
492
|
+
const error = { type: "UnknownError", error: "boom" } as const;
|
|
493
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({ type: "Error", error });
|
|
494
|
+
|
|
495
|
+
await testWaitForWorkerMessage();
|
|
496
|
+
|
|
497
|
+
assertEqual(deps.evoluError.get(), error);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("throws for unknown tab output type", () => {
|
|
501
|
+
const consoleEntryOrErrorBroadcastChannel: {
|
|
502
|
+
value: {
|
|
503
|
+
readonly onMessage: ((message: ConsoleEntryOrError) => void) | null;
|
|
504
|
+
} | null;
|
|
505
|
+
} = { value: null };
|
|
506
|
+
|
|
507
|
+
using _deps = createEvoluDeps({
|
|
508
|
+
createDbWorker: testCreateWorker,
|
|
509
|
+
createBroadcastChannel: <Input, Output = Input>(name: string) => {
|
|
510
|
+
const channel = testCreateBroadcastChannel<Input, Output>(name);
|
|
511
|
+
consoleEntryOrErrorBroadcastChannel.value = channel as NonNullable<
|
|
512
|
+
typeof consoleEntryOrErrorBroadcastChannel.value
|
|
513
|
+
>;
|
|
514
|
+
return channel;
|
|
515
|
+
},
|
|
516
|
+
createMessageChannel: testCreateMessageChannel,
|
|
517
|
+
lockManager: testCreateLockManager(),
|
|
518
|
+
sharedWorker: testCreateSharedWorker<
|
|
519
|
+
SharedWorkerInput,
|
|
520
|
+
SharedWorkerOutput
|
|
521
|
+
>(),
|
|
522
|
+
reloadApp: constVoid,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const onMessage = consoleEntryOrErrorBroadcastChannel.value?.onMessage;
|
|
526
|
+
assertNonNullable(onMessage);
|
|
527
|
+
|
|
528
|
+
assertThrowsInstanceOf(() => {
|
|
529
|
+
onMessage({ type: "Unknown" } as never);
|
|
530
|
+
}, Error);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("dispose cleans up resources", () => {
|
|
534
|
+
const worker = testCreateSharedWorker<
|
|
535
|
+
SharedWorkerInput,
|
|
536
|
+
SharedWorkerOutput
|
|
537
|
+
>();
|
|
538
|
+
worker.self.onConnect = (port) => {
|
|
539
|
+
port.onMessage = constVoid;
|
|
540
|
+
};
|
|
541
|
+
worker.connect();
|
|
542
|
+
|
|
543
|
+
const broadcastChannels: Array<{ readonly isDisposed: () => boolean }> =
|
|
544
|
+
[];
|
|
545
|
+
let workerDisposed = false;
|
|
546
|
+
const sharedWorker: SharedWorker = {
|
|
547
|
+
port: worker.port,
|
|
548
|
+
[Symbol.dispose]: () => {
|
|
549
|
+
workerDisposed = true;
|
|
550
|
+
worker[Symbol.dispose]();
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const deps = createEvoluDeps({
|
|
555
|
+
createDbWorker: testCreateWorker,
|
|
556
|
+
createBroadcastChannel: <Input, Output = Input>(name: string) => {
|
|
557
|
+
const channel = testCreateBroadcastChannel<Input, Output>(name);
|
|
558
|
+
broadcastChannels.push(channel);
|
|
559
|
+
return channel;
|
|
560
|
+
},
|
|
561
|
+
createMessageChannel: testCreateMessageChannel,
|
|
562
|
+
lockManager: testCreateLockManager(),
|
|
563
|
+
sharedWorker,
|
|
564
|
+
reloadApp: constVoid,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
assertLength(broadcastChannels, 1);
|
|
568
|
+
assertFalse(broadcastChannels[0].isDisposed());
|
|
569
|
+
assertFalse(workerDisposed);
|
|
570
|
+
deps[Symbol.dispose]();
|
|
571
|
+
assertTrue(broadcastChannels[0].isDisposed());
|
|
572
|
+
assertTrue(workerDisposed);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe("createEvolu", () => {
|
|
577
|
+
it("resolves name from appName and appOwner hash", async () => {
|
|
578
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
579
|
+
const { run } = setup;
|
|
580
|
+
|
|
581
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
582
|
+
const expectedSuffix = createIdFromString(testAppOwner.id);
|
|
583
|
+
assertEqual(evolu.name, `AppName-${expectedSuffix}`);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("appOwner from config is exposed as evolu.appOwner", async () => {
|
|
587
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
588
|
+
const { run } = setup;
|
|
589
|
+
|
|
590
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
591
|
+
|
|
592
|
+
assertSame(evolu.appOwner, testAppOwner);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("posts CreateEvolu with db worker init config", async () => {
|
|
596
|
+
const sharedWorkerMessages: Array<SharedWorkerInput> = [];
|
|
597
|
+
const sharedWorker = testCreateSharedWorker<
|
|
598
|
+
SharedWorkerInput,
|
|
599
|
+
SharedWorkerOutput
|
|
600
|
+
>();
|
|
601
|
+
|
|
602
|
+
sharedWorker.self.onConnect = (port) => {
|
|
603
|
+
port.onMessage = (message) => {
|
|
604
|
+
sharedWorkerMessages.push(message);
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
sharedWorker.connect();
|
|
608
|
+
|
|
609
|
+
await using run = testCreateRun({
|
|
610
|
+
createDbWorker: testCreateWorker,
|
|
611
|
+
createBroadcastChannel: testCreateBroadcastChannel,
|
|
612
|
+
createMessageChannel: testCreateMessageChannel,
|
|
613
|
+
lockManager: testCreateLockManager(),
|
|
614
|
+
reloadApp: constVoid,
|
|
615
|
+
sharedWorker,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
619
|
+
|
|
620
|
+
await testWaitForWorkerMessage();
|
|
621
|
+
|
|
622
|
+
assertLength(sharedWorkerMessages, 1);
|
|
623
|
+
const message = sharedWorkerMessages[0];
|
|
624
|
+
assertSame(message.type, "CreateEvolu");
|
|
625
|
+
assertEqual(Object.keys(message).toSorted(), [
|
|
626
|
+
"consoleLevel",
|
|
627
|
+
"encryptionKey",
|
|
628
|
+
"evoluPort",
|
|
629
|
+
"id",
|
|
630
|
+
"memoryOnly",
|
|
631
|
+
"name",
|
|
632
|
+
"sqliteSchema",
|
|
633
|
+
"type",
|
|
634
|
+
]);
|
|
635
|
+
assertEqual(message.name, evolu.name);
|
|
636
|
+
assertEqual(message.id, "in2khoBFZNo9ESZlzuacxA");
|
|
637
|
+
assertTrue(typeof message.consoleLevel === "string");
|
|
638
|
+
assertTrue(
|
|
639
|
+
typeof message.sqliteSchema === "object" &&
|
|
640
|
+
message.sqliteSchema !== null,
|
|
641
|
+
);
|
|
642
|
+
assertEqualBytes(message.encryptionKey, testAppOwner.encryptionKey);
|
|
643
|
+
assertFalse(message.memoryOnly);
|
|
644
|
+
assertNonNullable(message.evoluPort);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
describe("useOwner", () => {
|
|
648
|
+
it("auto-uses appOwner in a microtask when transports are configured", async () => {
|
|
649
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
650
|
+
const { run, evoluInputs } = setup;
|
|
651
|
+
await run.ok(
|
|
652
|
+
createEvolu(Schema, {
|
|
653
|
+
appName: testAppName,
|
|
654
|
+
appOwner: testAppOwner,
|
|
655
|
+
transports: [testOwnerTransport],
|
|
656
|
+
}),
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
assertEqual(evoluInputs, []);
|
|
660
|
+
|
|
661
|
+
await testWaitForWorkerMessage();
|
|
662
|
+
|
|
663
|
+
assertEqual(evoluInputs, [
|
|
664
|
+
{
|
|
665
|
+
type: "UseOwner",
|
|
666
|
+
actions: [
|
|
667
|
+
{
|
|
668
|
+
owner: {
|
|
669
|
+
owner: testAppOwner,
|
|
670
|
+
transports: [testOwnerTransport],
|
|
671
|
+
},
|
|
672
|
+
action: "add",
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
]);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("posts in a microtask with fallback transports", async () => {
|
|
680
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
681
|
+
const { run, evoluInputs } = setup;
|
|
682
|
+
const evolu = await run.ok(
|
|
683
|
+
createEvolu(Schema, {
|
|
684
|
+
appName: testAppName,
|
|
685
|
+
appOwner: testAppOwner,
|
|
686
|
+
transports: [testOwnerTransport],
|
|
687
|
+
}),
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
await testWaitForWorkerMessage();
|
|
691
|
+
evoluInputs.length = 0;
|
|
692
|
+
|
|
693
|
+
evolu.useOwner(testAppOwner);
|
|
694
|
+
|
|
695
|
+
assertEqual(evoluInputs, []);
|
|
696
|
+
|
|
697
|
+
await testWaitForWorkerMessage();
|
|
698
|
+
|
|
699
|
+
assertEqual(evoluInputs, [
|
|
700
|
+
{
|
|
701
|
+
type: "UseOwner",
|
|
702
|
+
actions: [
|
|
703
|
+
{
|
|
704
|
+
owner: {
|
|
705
|
+
owner: testAppOwner,
|
|
706
|
+
transports: [testOwnerTransport],
|
|
707
|
+
},
|
|
708
|
+
action: "add",
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
},
|
|
712
|
+
]);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("preserves same-tick add and remove order", async () => {
|
|
716
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
717
|
+
const { run, evoluInputs } = setup;
|
|
718
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
719
|
+
|
|
720
|
+
const unuseOwner = evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
721
|
+
unuseOwner();
|
|
722
|
+
|
|
723
|
+
assertEqual(evoluInputs, []);
|
|
724
|
+
|
|
725
|
+
await testWaitForWorkerMessage();
|
|
726
|
+
|
|
727
|
+
assertEqual(evoluInputs, [
|
|
728
|
+
{
|
|
729
|
+
type: "UseOwner",
|
|
730
|
+
actions: [
|
|
731
|
+
{
|
|
732
|
+
owner: {
|
|
733
|
+
owner: testAppOwner,
|
|
734
|
+
transports: [testOwnerTransport],
|
|
735
|
+
},
|
|
736
|
+
action: "add",
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
owner: {
|
|
740
|
+
owner: testAppOwner,
|
|
741
|
+
transports: [testOwnerTransport],
|
|
742
|
+
},
|
|
743
|
+
action: "remove",
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
},
|
|
747
|
+
]);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("throws when unuseOwner is called twice", async () => {
|
|
751
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
752
|
+
const { run, evoluInputs } = setup;
|
|
753
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
754
|
+
|
|
755
|
+
const unuseOwner = evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
756
|
+
await testWaitForWorkerMessage();
|
|
757
|
+
evoluInputs.length = 0;
|
|
758
|
+
|
|
759
|
+
unuseOwner();
|
|
760
|
+
|
|
761
|
+
assertEqual(
|
|
762
|
+
assertThrowsInstanceOf(() => {
|
|
763
|
+
unuseOwner();
|
|
764
|
+
}, Error).message,
|
|
765
|
+
"UnuseOwner can be called only once.",
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
await testWaitForWorkerMessage();
|
|
769
|
+
|
|
770
|
+
assertEqual(evoluInputs, [
|
|
771
|
+
{
|
|
772
|
+
type: "UseOwner",
|
|
773
|
+
actions: [
|
|
774
|
+
{
|
|
775
|
+
owner: {
|
|
776
|
+
owner: testAppOwner,
|
|
777
|
+
transports: [testOwnerTransport],
|
|
778
|
+
},
|
|
779
|
+
action: "remove",
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
},
|
|
783
|
+
]);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it("flush keeps call order before mutate batch", async () => {
|
|
787
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
788
|
+
const { run, evoluInputs } = setup;
|
|
789
|
+
const evolu = await run.ok(
|
|
790
|
+
createEvolu(Schema, {
|
|
791
|
+
appName: testAppName,
|
|
792
|
+
appOwner: testAppOwner,
|
|
793
|
+
transports: [],
|
|
794
|
+
}),
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
798
|
+
evolu.insert("todo", {
|
|
799
|
+
title: NonEmptyTrimmedString100.orThrow("Queued after useOwner"),
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
await testWaitForWorkerMessage();
|
|
803
|
+
|
|
804
|
+
assertEqual(evoluInputs[0], {
|
|
805
|
+
type: "UseOwner",
|
|
806
|
+
actions: [
|
|
807
|
+
{
|
|
808
|
+
owner: {
|
|
809
|
+
owner: testAppOwner,
|
|
810
|
+
transports: [testOwnerTransport],
|
|
811
|
+
},
|
|
812
|
+
action: "add",
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
});
|
|
816
|
+
assertSame(evoluInputs[1]?.type, "Mutate");
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
describe("dispose evolu", () => {
|
|
822
|
+
it("logs disposeEvolu once", async () => {
|
|
823
|
+
const testConsole = testCreateConsole();
|
|
824
|
+
await using setup = await setupRunWithEvoluDeps({ console: testConsole });
|
|
825
|
+
const { run } = setup;
|
|
826
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
827
|
+
|
|
828
|
+
testConsole.clearEntries();
|
|
829
|
+
|
|
830
|
+
await evolu[Symbol.asyncDispose]();
|
|
831
|
+
await evolu[Symbol.asyncDispose]();
|
|
832
|
+
|
|
833
|
+
const entries = testConsole
|
|
834
|
+
.getEntriesSnapshot()
|
|
835
|
+
.filter((entry) => entry.args[0] === "disposeEvolu");
|
|
836
|
+
|
|
837
|
+
assertEqual(entries, [
|
|
838
|
+
{
|
|
839
|
+
method: "info",
|
|
840
|
+
path: [evolu.name, "Evolu"],
|
|
841
|
+
args: ["disposeEvolu"],
|
|
842
|
+
},
|
|
843
|
+
]);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it("disposes resources when createEvolu is aborted after setup", async () => {
|
|
847
|
+
const channels: Array<{ readonly isDisposed: () => boolean }> = [];
|
|
848
|
+
let abortCreateEvolu: () => void = () => {
|
|
849
|
+
assert(
|
|
850
|
+
false,
|
|
851
|
+
"Expected createEvolu fiber to exist before postMessage.",
|
|
852
|
+
);
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
856
|
+
createMessageChannel: <Input, Output = never>() => {
|
|
857
|
+
const channel = testCreateMessageChannel<Input, Output>();
|
|
858
|
+
channels.push(channel);
|
|
859
|
+
return channel;
|
|
860
|
+
},
|
|
861
|
+
onSharedWorkerPostMessage: (message) => {
|
|
862
|
+
if (message.type !== "CreateEvolu") return;
|
|
863
|
+
abortCreateEvolu();
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
const { run } = setup;
|
|
867
|
+
|
|
868
|
+
const fiber = run.abortable(testCreateEvolu);
|
|
869
|
+
abortCreateEvolu = () => fiber.abort({ type: "LateAbort" });
|
|
870
|
+
|
|
871
|
+
assertEqual(
|
|
872
|
+
await fiber,
|
|
873
|
+
err({ type: "AbortError", reason: { type: "LateAbort" } }),
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
assertLength(channels, 1);
|
|
877
|
+
assertTrue(channels[0].isDisposed());
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("rejects pending export", async () => {
|
|
881
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
882
|
+
const { run, evoluInputs } = setup;
|
|
883
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
884
|
+
|
|
885
|
+
const exportPromise = evolu.exportDatabase();
|
|
886
|
+
|
|
887
|
+
await testWaitForWorkerMessage();
|
|
888
|
+
|
|
889
|
+
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
890
|
+
|
|
891
|
+
await evolu[Symbol.asyncDispose]();
|
|
892
|
+
|
|
893
|
+
await assertRejects(exportPromise, {
|
|
894
|
+
type: "EvoluDisposedError",
|
|
895
|
+
});
|
|
896
|
+
await testWaitForWorkerMessage();
|
|
897
|
+
|
|
898
|
+
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("throws from sync methods after dispose", async () => {
|
|
902
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
903
|
+
const { run } = setup;
|
|
904
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
905
|
+
evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
906
|
+
|
|
907
|
+
await evolu[Symbol.asyncDispose]();
|
|
908
|
+
|
|
909
|
+
const disposedMessage = "Cannot use a disposed object.";
|
|
910
|
+
|
|
911
|
+
assertEqual(
|
|
912
|
+
assertThrowsInstanceOf(() => {
|
|
913
|
+
evolu.insert("todo", {
|
|
914
|
+
title: NonEmptyTrimmedString100.orThrow("Inserted after dispose"),
|
|
915
|
+
});
|
|
916
|
+
}, Error).message,
|
|
917
|
+
disposedMessage,
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
assertEqual(
|
|
921
|
+
assertThrowsInstanceOf(() => {
|
|
922
|
+
evolu.update("todo", {
|
|
923
|
+
id: TodoId.orThrow(createIdFromString("todo-update-after-dispose")),
|
|
924
|
+
title: NonEmptyTrimmedString100.orThrow("Updated after dispose"),
|
|
925
|
+
});
|
|
926
|
+
}, Error).message,
|
|
927
|
+
disposedMessage,
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
assertEqual(
|
|
931
|
+
assertThrowsInstanceOf(() => {
|
|
932
|
+
evolu.upsert("todo", {
|
|
933
|
+
id: TodoId.orThrow(createIdFromString("todo-upsert-after-dispose")),
|
|
934
|
+
title: NonEmptyTrimmedString100.orThrow("Upserted after dispose"),
|
|
935
|
+
});
|
|
936
|
+
}, Error).message,
|
|
937
|
+
disposedMessage,
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
assertEqual(
|
|
941
|
+
assertThrowsInstanceOf(() => {
|
|
942
|
+
void evolu.loadQuery(todoTitleQuery);
|
|
943
|
+
}, Error).message,
|
|
944
|
+
disposedMessage,
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
assertEqual(
|
|
948
|
+
assertThrowsInstanceOf(() => {
|
|
949
|
+
void evolu.loadQueries([todoTitleQuery, todoTitleDescQuery]);
|
|
950
|
+
}, Error).message,
|
|
951
|
+
disposedMessage,
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
assertEqual(
|
|
955
|
+
assertThrowsInstanceOf(() => {
|
|
956
|
+
evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
957
|
+
}, Error).message,
|
|
958
|
+
disposedMessage,
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
assertEqual(
|
|
962
|
+
assertThrowsInstanceOf(() => {
|
|
963
|
+
evolu.getQueryRows(todoTitleQuery);
|
|
964
|
+
}, Error).message,
|
|
965
|
+
disposedMessage,
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
assertEqual(
|
|
969
|
+
assertThrowsInstanceOf(() => {
|
|
970
|
+
void evolu.exportDatabase();
|
|
971
|
+
}, Error).message,
|
|
972
|
+
disposedMessage,
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
assertEqual(
|
|
976
|
+
assertThrowsInstanceOf(() => {
|
|
977
|
+
evolu.deleteDatabase();
|
|
978
|
+
}, Error).message,
|
|
979
|
+
disposedMessage,
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
assertEqual(
|
|
983
|
+
assertThrowsInstanceOf(() => {
|
|
984
|
+
evolu.deleteOwner(testAppOwner);
|
|
985
|
+
}, Error).message,
|
|
986
|
+
disposedMessage,
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
assertEqual(
|
|
990
|
+
assertThrowsInstanceOf(() => {
|
|
991
|
+
evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
992
|
+
}, Error).message,
|
|
993
|
+
disposedMessage,
|
|
994
|
+
);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it("delete methods are placeholders", async () => {
|
|
998
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
999
|
+
const { run } = setup;
|
|
1000
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1001
|
+
|
|
1002
|
+
assertEqual(
|
|
1003
|
+
assertThrowsInstanceOf(() => {
|
|
1004
|
+
evolu.deleteDatabase();
|
|
1005
|
+
}, Error).message,
|
|
1006
|
+
"not yet implemented",
|
|
1007
|
+
);
|
|
1008
|
+
|
|
1009
|
+
assertEqual(
|
|
1010
|
+
assertThrowsInstanceOf(() => {
|
|
1011
|
+
evolu.deleteOwner(testAppOwner);
|
|
1012
|
+
}, Error).message,
|
|
1013
|
+
"not yet implemented",
|
|
1014
|
+
);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it("unuseOwner is a no-op after dispose", async () => {
|
|
1018
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1019
|
+
const { run } = setup;
|
|
1020
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1021
|
+
|
|
1022
|
+
const unuseOwner = evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
1023
|
+
|
|
1024
|
+
await evolu[Symbol.asyncDispose]();
|
|
1025
|
+
|
|
1026
|
+
// Parent teardown can dispose Evolu while child cleanup still holds
|
|
1027
|
+
// UnuseOwner callbacks. React is one example, regardless of its exact
|
|
1028
|
+
// unmount ordering. Late UnuseOwner calls must not throw, and they do not
|
|
1029
|
+
// leak because tenant-side instance disposal releases all used owners.
|
|
1030
|
+
(() => {
|
|
1031
|
+
unuseOwner();
|
|
1032
|
+
})();
|
|
1033
|
+
|
|
1034
|
+
(() => {
|
|
1035
|
+
unuseOwner();
|
|
1036
|
+
})();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it("resolves pending loadQuery with empty rows on dispose", async () => {
|
|
1040
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1041
|
+
const { run, evoluInputs } = setup;
|
|
1042
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1043
|
+
|
|
1044
|
+
const loadPromise = evolu.loadQuery(todoTitleQuery);
|
|
1045
|
+
|
|
1046
|
+
await testWaitForWorkerMessage();
|
|
1047
|
+
|
|
1048
|
+
assertEqual(evoluInputs, [
|
|
1049
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1050
|
+
]);
|
|
1051
|
+
|
|
1052
|
+
await evolu[Symbol.asyncDispose]();
|
|
1053
|
+
|
|
1054
|
+
assertEqual(await loadPromise, []);
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it("dispose keeps fulfilled subscribed loadQuery settled", async () => {
|
|
1058
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1059
|
+
const { run, postEvoluOutput } = setup;
|
|
1060
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1061
|
+
|
|
1062
|
+
evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1063
|
+
const loadPromise = evolu.loadQuery(todoTitleQuery);
|
|
1064
|
+
|
|
1065
|
+
await testWaitForWorkerMessage();
|
|
1066
|
+
|
|
1067
|
+
postEvoluOutput({
|
|
1068
|
+
type: "OnPatchesByQuery",
|
|
1069
|
+
patchesByQuery: new Map([
|
|
1070
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "A" }] }]],
|
|
1071
|
+
]),
|
|
1072
|
+
onCompleteIds: [],
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
assertEqual(await loadPromise, [{ title: "A" }]);
|
|
1076
|
+
|
|
1077
|
+
await evolu[Symbol.asyncDispose]();
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it("drops pending mutation microtask batch on dispose", async () => {
|
|
1081
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1082
|
+
const { evoluInputs, run } = setup;
|
|
1083
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1084
|
+
|
|
1085
|
+
evolu.insert("todo", {
|
|
1086
|
+
title: NonEmptyTrimmedString100.orThrow("Queued then disposed"),
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
await evolu[Symbol.asyncDispose]();
|
|
1090
|
+
|
|
1091
|
+
await testWaitForWorkerMessage();
|
|
1092
|
+
|
|
1093
|
+
assertEqual(evoluInputs, []);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
it("drops pending query microtask batch on dispose", async () => {
|
|
1097
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1098
|
+
const { evoluInputs, run } = setup;
|
|
1099
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1100
|
+
|
|
1101
|
+
const loadPromise = evolu.loadQuery(todoTitleQuery);
|
|
1102
|
+
|
|
1103
|
+
await evolu[Symbol.asyncDispose]();
|
|
1104
|
+
|
|
1105
|
+
assertEqual(await loadPromise, []);
|
|
1106
|
+
|
|
1107
|
+
await testWaitForWorkerMessage();
|
|
1108
|
+
|
|
1109
|
+
assertEqual(evoluInputs, []);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it("drops pending useOwner microtask batch on dispose", async () => {
|
|
1113
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1114
|
+
const { evoluInputs, run } = setup;
|
|
1115
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1116
|
+
|
|
1117
|
+
evolu.useOwner(testAppOwner, [testOwnerTransport]);
|
|
1118
|
+
|
|
1119
|
+
await evolu[Symbol.asyncDispose]();
|
|
1120
|
+
|
|
1121
|
+
await testWaitForWorkerMessage();
|
|
1122
|
+
|
|
1123
|
+
assertEqual(evoluInputs, []);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it("disposes internal message channels", async () => {
|
|
1127
|
+
const channels: Array<{ readonly isDisposed: () => boolean }> = [];
|
|
1128
|
+
|
|
1129
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
1130
|
+
createMessageChannel: <Input, Output = never>() => {
|
|
1131
|
+
const channel = testCreateMessageChannel<Input, Output>();
|
|
1132
|
+
channels.push(channel);
|
|
1133
|
+
return channel;
|
|
1134
|
+
},
|
|
1135
|
+
});
|
|
1136
|
+
const { run } = setup;
|
|
1137
|
+
|
|
1138
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1139
|
+
|
|
1140
|
+
assertLength(channels, 1);
|
|
1141
|
+
assertFalse(channels[0].isDisposed());
|
|
1142
|
+
|
|
1143
|
+
await evolu[Symbol.asyncDispose]();
|
|
1144
|
+
|
|
1145
|
+
assertTrue(channels[0].isDisposed());
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it("does not execute mutate onComplete callback after dispose", async () => {
|
|
1149
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1150
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1151
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1152
|
+
|
|
1153
|
+
let called = 0;
|
|
1154
|
+
evolu.insert(
|
|
1155
|
+
"todo",
|
|
1156
|
+
{ title: NonEmptyTrimmedString100.orThrow("With completion") },
|
|
1157
|
+
{
|
|
1158
|
+
onComplete: () => {
|
|
1159
|
+
called += 1;
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
await testWaitForWorkerMessage();
|
|
1165
|
+
|
|
1166
|
+
const mutate = evoluInputs[0] as ExtractTyped<EvoluInput, "Mutate">;
|
|
1167
|
+
const [onCompleteId] = mutate.onCompleteIds;
|
|
1168
|
+
|
|
1169
|
+
await evolu[Symbol.asyncDispose]();
|
|
1170
|
+
|
|
1171
|
+
postEvoluOutput({
|
|
1172
|
+
type: "OnPatchesByQuery",
|
|
1173
|
+
patchesByQuery: new Map(),
|
|
1174
|
+
onCompleteIds: [onCompleteId],
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
await testWaitForWorkerMessage();
|
|
1178
|
+
|
|
1179
|
+
assertEqual(called, 0);
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
it("executes mutate onComplete callback when query patches are received", async () => {
|
|
1183
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1184
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1185
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1186
|
+
|
|
1187
|
+
let called = 0;
|
|
1188
|
+
evolu.insert(
|
|
1189
|
+
"todo",
|
|
1190
|
+
{ title: NonEmptyTrimmedString100.orThrow("With completion") },
|
|
1191
|
+
{
|
|
1192
|
+
onComplete: () => {
|
|
1193
|
+
called += 1;
|
|
1194
|
+
},
|
|
1195
|
+
},
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
await testWaitForWorkerMessage();
|
|
1199
|
+
|
|
1200
|
+
const mutate = evoluInputs[0] as ExtractTyped<EvoluInput, "Mutate">;
|
|
1201
|
+
const [onCompleteId] = mutate.onCompleteIds;
|
|
1202
|
+
|
|
1203
|
+
postEvoluOutput({
|
|
1204
|
+
type: "OnPatchesByQuery",
|
|
1205
|
+
patchesByQuery: new Map(),
|
|
1206
|
+
onCompleteIds: [onCompleteId],
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
await testWaitForWorkerMessage();
|
|
1210
|
+
|
|
1211
|
+
assertEqual(called, 1);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
it("uses flushSync for query patches with onComplete callbacks", async () => {
|
|
1215
|
+
let flushSyncCalls = 0;
|
|
1216
|
+
|
|
1217
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
1218
|
+
flushSync: (callback: () => void) => {
|
|
1219
|
+
flushSyncCalls += 1;
|
|
1220
|
+
callback();
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1224
|
+
|
|
1225
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1226
|
+
|
|
1227
|
+
let called = 0;
|
|
1228
|
+
evolu.insert(
|
|
1229
|
+
"todo",
|
|
1230
|
+
{ title: NonEmptyTrimmedString100.orThrow("With completion") },
|
|
1231
|
+
{
|
|
1232
|
+
onComplete: () => {
|
|
1233
|
+
called += 1;
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
);
|
|
1237
|
+
|
|
1238
|
+
await testWaitForWorkerMessage();
|
|
1239
|
+
|
|
1240
|
+
const mutate = evoluInputs[0] as ExtractTyped<EvoluInput, "Mutate">;
|
|
1241
|
+
const [onCompleteId] = mutate.onCompleteIds;
|
|
1242
|
+
|
|
1243
|
+
postEvoluOutput({
|
|
1244
|
+
type: "OnPatchesByQuery",
|
|
1245
|
+
patchesByQuery: new Map(),
|
|
1246
|
+
onCompleteIds: [onCompleteId],
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
await testWaitForWorkerMessage();
|
|
1250
|
+
|
|
1251
|
+
assertEqual(flushSyncCalls, 1);
|
|
1252
|
+
assertEqual(called, 1);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it("does not use flushSync when query patches have no onComplete callbacks", async () => {
|
|
1256
|
+
let flushSyncCalls = 0;
|
|
1257
|
+
|
|
1258
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
1259
|
+
flushSync: (callback: () => void) => {
|
|
1260
|
+
flushSyncCalls += 1;
|
|
1261
|
+
callback();
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
1264
|
+
const { run, postEvoluOutput } = setup;
|
|
1265
|
+
|
|
1266
|
+
await run.ok(testCreateEvolu);
|
|
1267
|
+
|
|
1268
|
+
postEvoluOutput({
|
|
1269
|
+
type: "OnPatchesByQuery",
|
|
1270
|
+
patchesByQuery: new Map(),
|
|
1271
|
+
onCompleteIds: [],
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
assertEqual(flushSyncCalls, 0);
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
describe("worker outputs", () => {
|
|
1279
|
+
it("ignores RefreshQueries when there are no subscribed queries", async () => {
|
|
1280
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1281
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1282
|
+
await run.ok(testCreateEvolu);
|
|
1283
|
+
|
|
1284
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1285
|
+
|
|
1286
|
+
await testWaitForWorkerMessage();
|
|
1287
|
+
|
|
1288
|
+
assertEqual(evoluInputs, []);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
it("throws for unknown evolu output type", async () => {
|
|
1292
|
+
const channels: Array<{
|
|
1293
|
+
readonly port1: {
|
|
1294
|
+
onMessage: ((message: EvoluOutput) => void) | null;
|
|
1295
|
+
};
|
|
1296
|
+
}> = [];
|
|
1297
|
+
|
|
1298
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
1299
|
+
createMessageChannel: <Input, Output = never>() => {
|
|
1300
|
+
const channel = testCreateMessageChannel<Input, Output>();
|
|
1301
|
+
channels.push(channel as never);
|
|
1302
|
+
return channel;
|
|
1303
|
+
},
|
|
1304
|
+
});
|
|
1305
|
+
const { run } = setup;
|
|
1306
|
+
await run.ok(testCreateEvolu);
|
|
1307
|
+
|
|
1308
|
+
const evoluChannel = channels.find((channel) => channel.port1.onMessage);
|
|
1309
|
+
assertNonNullable(evoluChannel?.port1.onMessage);
|
|
1310
|
+
|
|
1311
|
+
assertThrowsInstanceOf(() => {
|
|
1312
|
+
evoluChannel.port1.onMessage?.({ type: "Unknown" } as never);
|
|
1313
|
+
}, Error);
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
describe("query behavior", () => {
|
|
1318
|
+
it("loadQuery reuses pending promise and sends one Query message", async () => {
|
|
1319
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1320
|
+
const { run, evoluInputs } = setup;
|
|
1321
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1322
|
+
|
|
1323
|
+
const firstLoad = evolu.loadQuery(todoTitleQuery);
|
|
1324
|
+
const secondLoad = evolu.loadQuery(todoTitleQuery);
|
|
1325
|
+
|
|
1326
|
+
assertSame(firstLoad, secondLoad);
|
|
1327
|
+
|
|
1328
|
+
await testWaitForWorkerMessage();
|
|
1329
|
+
|
|
1330
|
+
await testWaitForWorkerMessage();
|
|
1331
|
+
|
|
1332
|
+
assertEqual(evoluInputs, [
|
|
1333
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1334
|
+
]);
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
it("loadQueries delegates to loadQuery for each query", async () => {
|
|
1338
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1339
|
+
const { run, evoluInputs } = setup;
|
|
1340
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1341
|
+
const loads = evolu.loadQueries([todoTitleQuery, todoTitleDescQuery]);
|
|
1342
|
+
|
|
1343
|
+
assertLength(loads, 2);
|
|
1344
|
+
|
|
1345
|
+
await testWaitForWorkerMessage();
|
|
1346
|
+
|
|
1347
|
+
await testWaitForWorkerMessage();
|
|
1348
|
+
|
|
1349
|
+
assertEqual(evoluInputs, [
|
|
1350
|
+
{
|
|
1351
|
+
type: "Query",
|
|
1352
|
+
queries: new Set([todoTitleQuery, todoTitleDescQuery]),
|
|
1353
|
+
},
|
|
1354
|
+
]);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
it("getQueryRows returns empty array for unknown query", async () => {
|
|
1358
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1359
|
+
const { run } = setup;
|
|
1360
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1361
|
+
|
|
1362
|
+
assertEqual(evolu.getQueryRows(todoTitleQuery), []);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it("subscribeQuery does not trigger Query by itself", async () => {
|
|
1366
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1367
|
+
const { run, evoluInputs } = setup;
|
|
1368
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1369
|
+
|
|
1370
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1371
|
+
|
|
1372
|
+
await testWaitForWorkerMessage();
|
|
1373
|
+
|
|
1374
|
+
await testWaitForWorkerMessage();
|
|
1375
|
+
|
|
1376
|
+
assertEqual(evoluInputs, []);
|
|
1377
|
+
|
|
1378
|
+
unsubscribe();
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it("allows subscribeQuery unsubscribe after dispose", async () => {
|
|
1382
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1383
|
+
const { run } = setup;
|
|
1384
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1385
|
+
|
|
1386
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1387
|
+
|
|
1388
|
+
await evolu[Symbol.asyncDispose]();
|
|
1389
|
+
|
|
1390
|
+
(() => {
|
|
1391
|
+
unsubscribe();
|
|
1392
|
+
})();
|
|
1393
|
+
|
|
1394
|
+
assertEqual(
|
|
1395
|
+
assertThrowsInstanceOf(() => {
|
|
1396
|
+
unsubscribe();
|
|
1397
|
+
}, Error).message,
|
|
1398
|
+
"subscribeQuery unsubscribe can be called only once.",
|
|
1399
|
+
);
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
it("RefreshQueries re-queries pending unsubscribed loadQuery", async () => {
|
|
1403
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1404
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1405
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1406
|
+
|
|
1407
|
+
void evolu.loadQuery(todoTitleQuery);
|
|
1408
|
+
await testWaitForWorkerMessage();
|
|
1409
|
+
|
|
1410
|
+
evoluInputs.length = 0;
|
|
1411
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1412
|
+
|
|
1413
|
+
await testWaitForWorkerMessage();
|
|
1414
|
+
|
|
1415
|
+
assertEqual(evoluInputs, [
|
|
1416
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1417
|
+
]);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it("RefreshQueries re-queries subscribed query without loadQuery", async () => {
|
|
1421
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1422
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1423
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1424
|
+
|
|
1425
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1426
|
+
|
|
1427
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1428
|
+
|
|
1429
|
+
await testWaitForWorkerMessage();
|
|
1430
|
+
|
|
1431
|
+
assertEqual(evoluInputs, [
|
|
1432
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1433
|
+
]);
|
|
1434
|
+
|
|
1435
|
+
unsubscribe();
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
it("mutation releases pending unsubscribed loading promise on resolve", async () => {
|
|
1439
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1440
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1441
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1442
|
+
|
|
1443
|
+
const loadFiber = evolu.loadQuery(todoTitleQuery);
|
|
1444
|
+
await testWaitForWorkerMessage();
|
|
1445
|
+
|
|
1446
|
+
evoluInputs.length = 0;
|
|
1447
|
+
|
|
1448
|
+
evolu.insert("todo", { title: NonEmptyTrimmedString100.orThrow("M") });
|
|
1449
|
+
await testWaitForWorkerMessage();
|
|
1450
|
+
|
|
1451
|
+
postEvoluOutput({
|
|
1452
|
+
type: "OnPatchesByQuery",
|
|
1453
|
+
patchesByQuery: new Map([
|
|
1454
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "R" }] }]],
|
|
1455
|
+
]),
|
|
1456
|
+
onCompleteIds: [],
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
assertEqual(await loadFiber, [{ title: "R" }]);
|
|
1460
|
+
|
|
1461
|
+
evoluInputs.length = 0;
|
|
1462
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1463
|
+
|
|
1464
|
+
await testWaitForWorkerMessage();
|
|
1465
|
+
|
|
1466
|
+
assertEqual(evoluInputs, []);
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
it("RefreshQueries drops fulfilled unsubscribed loading promises", async () => {
|
|
1470
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1471
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1472
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1473
|
+
|
|
1474
|
+
void evolu.loadQuery(todoTitleQuery);
|
|
1475
|
+
await testWaitForWorkerMessage();
|
|
1476
|
+
|
|
1477
|
+
postEvoluOutput({
|
|
1478
|
+
type: "OnPatchesByQuery",
|
|
1479
|
+
patchesByQuery: new Map([
|
|
1480
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "A" }] }]],
|
|
1481
|
+
]),
|
|
1482
|
+
onCompleteIds: [],
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
evoluInputs.length = 0;
|
|
1486
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1487
|
+
|
|
1488
|
+
await testWaitForWorkerMessage();
|
|
1489
|
+
|
|
1490
|
+
assertEqual(evoluInputs, []);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
it("RefreshQueries keeps loading promise for subscribed query", async () => {
|
|
1494
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1495
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1496
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1497
|
+
|
|
1498
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1499
|
+
void evolu.loadQuery(todoTitleQuery);
|
|
1500
|
+
await testWaitForWorkerMessage();
|
|
1501
|
+
|
|
1502
|
+
evoluInputs.length = 0;
|
|
1503
|
+
postEvoluOutput({ type: "RefreshQueries" });
|
|
1504
|
+
|
|
1505
|
+
await testWaitForWorkerMessage();
|
|
1506
|
+
|
|
1507
|
+
assertEqual(evoluInputs, [
|
|
1508
|
+
{ type: "Query", queries: new Set([todoTitleQuery]) },
|
|
1509
|
+
]);
|
|
1510
|
+
|
|
1511
|
+
unsubscribe();
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
it("OnPatchesByQuery replaces fulfilled loading promise for subscribed query", async () => {
|
|
1515
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1516
|
+
const { run, postEvoluOutput } = setup;
|
|
1517
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1518
|
+
|
|
1519
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(constVoid);
|
|
1520
|
+
|
|
1521
|
+
const firstLoad = evolu.loadQuery(todoTitleQuery);
|
|
1522
|
+
await testWaitForWorkerMessage();
|
|
1523
|
+
|
|
1524
|
+
postEvoluOutput({
|
|
1525
|
+
type: "OnPatchesByQuery",
|
|
1526
|
+
patchesByQuery: new Map([
|
|
1527
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "A" }] }]],
|
|
1528
|
+
]),
|
|
1529
|
+
onCompleteIds: [],
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
assertEqual(await firstLoad, [{ title: "A" }]);
|
|
1533
|
+
|
|
1534
|
+
const fulfilledLoad = evolu.loadQuery(todoTitleQuery);
|
|
1535
|
+
|
|
1536
|
+
postEvoluOutput({
|
|
1537
|
+
type: "OnPatchesByQuery",
|
|
1538
|
+
patchesByQuery: new Map([
|
|
1539
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "B" }] }]],
|
|
1540
|
+
]),
|
|
1541
|
+
onCompleteIds: [],
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
await testWaitForWorkerMessage();
|
|
1545
|
+
|
|
1546
|
+
const replacedLoad = evolu.loadQuery(todoTitleQuery);
|
|
1547
|
+
|
|
1548
|
+
assertNotSame(replacedLoad, fulfilledLoad);
|
|
1549
|
+
assertEqual(await replacedLoad, [{ title: "B" }]);
|
|
1550
|
+
|
|
1551
|
+
unsubscribe();
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
it("OnPatchesByQuery ignores queries without loading promises", async () => {
|
|
1555
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1556
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1557
|
+
await run.ok(testCreateEvolu);
|
|
1558
|
+
|
|
1559
|
+
postEvoluOutput({
|
|
1560
|
+
type: "OnPatchesByQuery",
|
|
1561
|
+
patchesByQuery: new Map([
|
|
1562
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "X" }] }]],
|
|
1563
|
+
]),
|
|
1564
|
+
onCompleteIds: [],
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
await testWaitForWorkerMessage();
|
|
1568
|
+
|
|
1569
|
+
assertEqual(evoluInputs, []);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
it("subscribeQuery notifies only when query rows reference changes", async () => {
|
|
1573
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1574
|
+
const { run, postEvoluOutput } = setup;
|
|
1575
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1576
|
+
|
|
1577
|
+
let calls = 0;
|
|
1578
|
+
const unsubscribe = evolu.subscribeQuery(todoTitleQuery)(() => {
|
|
1579
|
+
calls += 1;
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
postEvoluOutput({
|
|
1583
|
+
type: "OnPatchesByQuery",
|
|
1584
|
+
patchesByQuery: new Map([
|
|
1585
|
+
[todoTitleQuery, [{ op: "replaceAll", value: [{ title: "A" }] }]],
|
|
1586
|
+
]),
|
|
1587
|
+
onCompleteIds: [],
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
postEvoluOutput({
|
|
1591
|
+
type: "OnPatchesByQuery",
|
|
1592
|
+
patchesByQuery: new Map(),
|
|
1593
|
+
onCompleteIds: [],
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
await testWaitForWorkerMessage();
|
|
1597
|
+
|
|
1598
|
+
assertEqual(calls, 1);
|
|
1599
|
+
|
|
1600
|
+
unsubscribe();
|
|
1601
|
+
});
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
describe("mutations", () => {
|
|
1605
|
+
it("insert posts mutate with generated id and stripped values", async () => {
|
|
1606
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1607
|
+
const { run, evoluInputs } = setup;
|
|
1608
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1609
|
+
|
|
1610
|
+
const insertedId = evolu.insert("todo", {
|
|
1611
|
+
title: NonEmptyTrimmedString100.orThrow("Todo 1"),
|
|
1612
|
+
}).id;
|
|
1613
|
+
|
|
1614
|
+
await testWaitForWorkerMessage();
|
|
1615
|
+
|
|
1616
|
+
await testWaitForWorkerMessage();
|
|
1617
|
+
|
|
1618
|
+
assertEqual(evoluInputs, [
|
|
1619
|
+
{
|
|
1620
|
+
changes: [
|
|
1621
|
+
{
|
|
1622
|
+
id: insertedId,
|
|
1623
|
+
isDelete: null,
|
|
1624
|
+
isInsert: true,
|
|
1625
|
+
ownerId: testAppOwner.id,
|
|
1626
|
+
table: "todo",
|
|
1627
|
+
values: { title: "Todo 1" },
|
|
1628
|
+
},
|
|
1629
|
+
],
|
|
1630
|
+
onCompleteIds: [],
|
|
1631
|
+
subscribedQueries: new Set(),
|
|
1632
|
+
type: "Mutate",
|
|
1633
|
+
},
|
|
1634
|
+
]);
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
it("update and upsert preserve passed id and set isInsert correctly", async () => {
|
|
1638
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1639
|
+
const { run, evoluInputs } = setup;
|
|
1640
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1641
|
+
|
|
1642
|
+
const updateId = TodoId.orThrow(createIdFromString("todo-update"));
|
|
1643
|
+
const upsertId = TodoId.orThrow(createIdFromString("todo-upsert"));
|
|
1644
|
+
|
|
1645
|
+
evolu.update("todo", {
|
|
1646
|
+
id: updateId,
|
|
1647
|
+
title: NonEmptyTrimmedString100.orThrow("Updated"),
|
|
1648
|
+
isDeleted: 1,
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
evolu.upsert("todo", {
|
|
1652
|
+
id: upsertId,
|
|
1653
|
+
title: NonEmptyTrimmedString100.orThrow("Upserted"),
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
await testWaitForWorkerMessage();
|
|
1657
|
+
|
|
1658
|
+
await testWaitForWorkerMessage();
|
|
1659
|
+
|
|
1660
|
+
assertEqual(evoluInputs, [
|
|
1661
|
+
{
|
|
1662
|
+
changes: [
|
|
1663
|
+
{
|
|
1664
|
+
id: updateId,
|
|
1665
|
+
isDelete: true,
|
|
1666
|
+
isInsert: false,
|
|
1667
|
+
ownerId: testAppOwner.id,
|
|
1668
|
+
table: "todo",
|
|
1669
|
+
values: { title: "Updated" },
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
id: upsertId,
|
|
1673
|
+
isDelete: null,
|
|
1674
|
+
isInsert: true,
|
|
1675
|
+
ownerId: testAppOwner.id,
|
|
1676
|
+
table: "todo",
|
|
1677
|
+
values: { title: "Upserted" },
|
|
1678
|
+
},
|
|
1679
|
+
],
|
|
1680
|
+
onCompleteIds: [],
|
|
1681
|
+
subscribedQueries: new Set(),
|
|
1682
|
+
type: "Mutate",
|
|
1683
|
+
},
|
|
1684
|
+
]);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
it("coalesces insert, update, and upsert in one microtask", async () => {
|
|
1688
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1689
|
+
const { run, evoluInputs } = setup;
|
|
1690
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1691
|
+
|
|
1692
|
+
const updateId = TodoId.orThrow(createIdFromString("todo-batch-update"));
|
|
1693
|
+
const upsertId = TodoId.orThrow(createIdFromString("todo-batch-upsert"));
|
|
1694
|
+
|
|
1695
|
+
const insertedId = evolu.insert("todo", {
|
|
1696
|
+
title: NonEmptyTrimmedString100.orThrow("A"),
|
|
1697
|
+
}).id;
|
|
1698
|
+
evolu.update("todo", {
|
|
1699
|
+
id: updateId,
|
|
1700
|
+
title: NonEmptyTrimmedString100.orThrow("B"),
|
|
1701
|
+
});
|
|
1702
|
+
evolu.upsert("todo", {
|
|
1703
|
+
id: upsertId,
|
|
1704
|
+
title: NonEmptyTrimmedString100.orThrow("C"),
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
await testWaitForWorkerMessage();
|
|
1708
|
+
|
|
1709
|
+
await testWaitForWorkerMessage();
|
|
1710
|
+
|
|
1711
|
+
assertEqual(evoluInputs, [
|
|
1712
|
+
{
|
|
1713
|
+
changes: [
|
|
1714
|
+
{
|
|
1715
|
+
id: insertedId,
|
|
1716
|
+
isDelete: null,
|
|
1717
|
+
isInsert: true,
|
|
1718
|
+
ownerId: testAppOwner.id,
|
|
1719
|
+
table: "todo",
|
|
1720
|
+
values: { title: "A" },
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
id: updateId,
|
|
1724
|
+
isDelete: null,
|
|
1725
|
+
isInsert: false,
|
|
1726
|
+
ownerId: testAppOwner.id,
|
|
1727
|
+
table: "todo",
|
|
1728
|
+
values: { title: "B" },
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
id: upsertId,
|
|
1732
|
+
isDelete: null,
|
|
1733
|
+
isInsert: true,
|
|
1734
|
+
ownerId: testAppOwner.id,
|
|
1735
|
+
table: "todo",
|
|
1736
|
+
values: { title: "C" },
|
|
1737
|
+
},
|
|
1738
|
+
],
|
|
1739
|
+
onCompleteIds: [],
|
|
1740
|
+
subscribedQueries: new Set(),
|
|
1741
|
+
type: "Mutate",
|
|
1742
|
+
},
|
|
1743
|
+
]);
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
it("includes ownerId and onComplete callback ids", async () => {
|
|
1747
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1748
|
+
const { run, evoluInputs } = setup;
|
|
1749
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1750
|
+
|
|
1751
|
+
const insertedId = evolu.insert(
|
|
1752
|
+
"todo",
|
|
1753
|
+
{ title: NonEmptyTrimmedString100.orThrow("With callback") },
|
|
1754
|
+
{ ownerId: testAppOwner.id, onComplete: constVoid },
|
|
1755
|
+
).id;
|
|
1756
|
+
|
|
1757
|
+
await testWaitForWorkerMessage();
|
|
1758
|
+
|
|
1759
|
+
await testWaitForWorkerMessage();
|
|
1760
|
+
|
|
1761
|
+
const input = evoluInputs[0];
|
|
1762
|
+
assertSame(input?.type, "Mutate");
|
|
1763
|
+
const onCompleteId = input.onCompleteIds[0];
|
|
1764
|
+
assertNotUndefined(onCompleteId);
|
|
1765
|
+
assertEqual(evoluInputs, [
|
|
1766
|
+
{
|
|
1767
|
+
changes: [
|
|
1768
|
+
{
|
|
1769
|
+
id: insertedId,
|
|
1770
|
+
isDelete: null,
|
|
1771
|
+
isInsert: true,
|
|
1772
|
+
ownerId: testAppOwner.id,
|
|
1773
|
+
table: "todo",
|
|
1774
|
+
values: { title: "With callback" },
|
|
1775
|
+
},
|
|
1776
|
+
],
|
|
1777
|
+
onCompleteIds: [onCompleteId],
|
|
1778
|
+
subscribedQueries: new Set(),
|
|
1779
|
+
type: "Mutate",
|
|
1780
|
+
},
|
|
1781
|
+
]);
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
describe("exportDatabase", () => {
|
|
1786
|
+
it("throws when OnExport arrives without pending export", async () => {
|
|
1787
|
+
const channels: Array<{
|
|
1788
|
+
readonly port1: {
|
|
1789
|
+
onMessage: ((message: EvoluOutput) => void) | null;
|
|
1790
|
+
};
|
|
1791
|
+
}> = [];
|
|
1792
|
+
|
|
1793
|
+
await using setup = await setupRunWithEvoluDeps({
|
|
1794
|
+
createMessageChannel: <Input, Output = never>() => {
|
|
1795
|
+
const channel = testCreateMessageChannel<Input, Output>();
|
|
1796
|
+
channels.push(channel as never);
|
|
1797
|
+
return channel;
|
|
1798
|
+
},
|
|
1799
|
+
});
|
|
1800
|
+
const { run } = setup;
|
|
1801
|
+
await run.ok(testCreateEvolu);
|
|
1802
|
+
|
|
1803
|
+
const evoluChannel = channels.find((channel) => channel.port1.onMessage);
|
|
1804
|
+
assertNonNullable(evoluChannel?.port1.onMessage);
|
|
1805
|
+
|
|
1806
|
+
assertEqual(
|
|
1807
|
+
assertThrowsInstanceOf(() => {
|
|
1808
|
+
evoluChannel.port1.onMessage?.({
|
|
1809
|
+
type: "OnExport",
|
|
1810
|
+
file: new Uint8Array(),
|
|
1811
|
+
});
|
|
1812
|
+
}, Error).message,
|
|
1813
|
+
"OnExport received without pending export.",
|
|
1814
|
+
);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
it("exports database for one caller", async () => {
|
|
1818
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1819
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1820
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1821
|
+
|
|
1822
|
+
const exportPromise = evolu.exportDatabase();
|
|
1823
|
+
|
|
1824
|
+
await testWaitForWorkerMessage();
|
|
1825
|
+
|
|
1826
|
+
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
1827
|
+
|
|
1828
|
+
const file = new Uint8Array([1, 2, 3]);
|
|
1829
|
+
postEvoluOutput({ type: "OnExport", file });
|
|
1830
|
+
|
|
1831
|
+
assertEqualBytes(await exportPromise, file);
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
it("shares pending export and resolves both callers", async () => {
|
|
1835
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1836
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1837
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1838
|
+
|
|
1839
|
+
const firstExport = evolu.exportDatabase();
|
|
1840
|
+
const secondExport = evolu.exportDatabase();
|
|
1841
|
+
|
|
1842
|
+
await testWaitForWorkerMessage();
|
|
1843
|
+
|
|
1844
|
+
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
1845
|
+
|
|
1846
|
+
const firstFile = new Uint8Array([1, 2, 3]);
|
|
1847
|
+
postEvoluOutput({ type: "OnExport", file: firstFile });
|
|
1848
|
+
|
|
1849
|
+
assertEqualBytes(await firstExport, firstFile);
|
|
1850
|
+
assertEqualBytes(await secondExport, firstFile);
|
|
1851
|
+
|
|
1852
|
+
const thirdExport = evolu.exportDatabase();
|
|
1853
|
+
await testWaitForWorkerMessage();
|
|
1854
|
+
|
|
1855
|
+
assertEqual(evoluInputs, [{ type: "Export" }, { type: "Export" }]);
|
|
1856
|
+
|
|
1857
|
+
const secondFile = new Uint8Array([4, 5, 6]);
|
|
1858
|
+
postEvoluOutput({ type: "OnExport", file: secondFile });
|
|
1859
|
+
|
|
1860
|
+
assertEqualBytes(await thirdExport, secondFile);
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
it("returns a new promise after previous export resolves", async () => {
|
|
1864
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1865
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1866
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1867
|
+
|
|
1868
|
+
const firstExport = evolu.exportDatabase();
|
|
1869
|
+
const secondExport = evolu.exportDatabase();
|
|
1870
|
+
|
|
1871
|
+
await testWaitForWorkerMessage();
|
|
1872
|
+
|
|
1873
|
+
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
1874
|
+
|
|
1875
|
+
const file = new Uint8Array([7, 8, 9]);
|
|
1876
|
+
postEvoluOutput({ type: "OnExport", file });
|
|
1877
|
+
|
|
1878
|
+
assertEqualBytes(await firstExport, file);
|
|
1879
|
+
assertEqualBytes(await secondExport, file);
|
|
1880
|
+
|
|
1881
|
+
const thirdExport = evolu.exportDatabase();
|
|
1882
|
+
|
|
1883
|
+
await testWaitForWorkerMessage();
|
|
1884
|
+
|
|
1885
|
+
assertEqual(evoluInputs, [{ type: "Export" }, { type: "Export" }]);
|
|
1886
|
+
|
|
1887
|
+
const secondFile = new Uint8Array([13, 14, 15]);
|
|
1888
|
+
postEvoluOutput({ type: "OnExport", file: secondFile });
|
|
1889
|
+
|
|
1890
|
+
assertEqualBytes(await thirdExport, secondFile);
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
it("aborting run-wrapped export does not cancel shared export", async () => {
|
|
1894
|
+
await using setup = await setupRunWithEvoluDeps();
|
|
1895
|
+
const { run, evoluInputs, postEvoluOutput } = setup;
|
|
1896
|
+
const evolu = await run.ok(testCreateEvolu);
|
|
1897
|
+
|
|
1898
|
+
const sharedExport = evolu.exportDatabase();
|
|
1899
|
+
const wrappedExport = run.abortable(async (run) => {
|
|
1900
|
+
const file = await evolu.exportDatabase();
|
|
1901
|
+
run.signal.throwIfAborted();
|
|
1902
|
+
return ok(file);
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1905
|
+
await testWaitForWorkerMessage();
|
|
1906
|
+
|
|
1907
|
+
assertEqual(evoluInputs, [{ type: "Export" }]);
|
|
1908
|
+
|
|
1909
|
+
wrappedExport.abort();
|
|
1910
|
+
|
|
1911
|
+
const file = new Uint8Array([16, 17, 18]);
|
|
1912
|
+
postEvoluOutput({ type: "OnExport", file });
|
|
1913
|
+
|
|
1914
|
+
assertEqual(
|
|
1915
|
+
await wrappedExport,
|
|
1916
|
+
err({ type: "AbortError", reason: explicitAbortReason }),
|
|
1917
|
+
);
|
|
1918
|
+
assertEqualBytes(await sharedExport, file);
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
});
|