@evolu/web 3.0.2 → 3.1.1
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/README.md +7 -1
- package/dist/src/Platform.d.ts +2 -0
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +6 -1
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +10 -4
- package/dist/src/Task.js +1 -1
- package/dist/src/Worker.d.ts +3 -3
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +11 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/local-first/Db.worker.js +1 -1
- 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/LocalAuth.js +6 -3
- package/dist/src/local-first/Shared.worker.js +2 -2
- package/package.json +6 -5
- package/src/Platform.test.ts +45 -0
- package/src/Platform.ts +8 -1
- package/src/Sqlite.test.ts +356 -0
- package/src/Sqlite.ts +16 -4
- package/src/Task.test.ts +51 -0
- package/src/Task.ts +1 -1
- package/src/Worker.test.ts +473 -0
- package/src/Worker.ts +15 -6
- package/src/index.ts +1 -1
- package/src/local-first/Db.worker.ts +1 -1
- package/src/local-first/Evolu.test.ts +50 -0
- package/src/local-first/Evolu.ts +2 -3
- package/src/local-first/LocalAuth.ts +6 -3
- package/src/local-first/Shared.worker.ts +2 -2
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEqual,
|
|
3
|
+
assertNonNullable,
|
|
4
|
+
assertSame,
|
|
5
|
+
assertThrowsInstanceOf,
|
|
6
|
+
testStubGlobal,
|
|
7
|
+
type MessagePort,
|
|
8
|
+
type NativeMessagePort,
|
|
9
|
+
} from "@evolu/common";
|
|
10
|
+
import { describe, it, mock, test } from "node:test";
|
|
11
|
+
import {
|
|
12
|
+
createBroadcastChannel,
|
|
13
|
+
createOneTabSharedWorkerSelfPolyfill,
|
|
14
|
+
createMessageChannel,
|
|
15
|
+
createMessagePort,
|
|
16
|
+
createSharedWorker,
|
|
17
|
+
createSharedWorkerSelf,
|
|
18
|
+
createWorker,
|
|
19
|
+
createWorkerDeps,
|
|
20
|
+
createWorkerSelf,
|
|
21
|
+
installOneTabSharedWorkerPolyfill,
|
|
22
|
+
} from "./Worker.ts";
|
|
23
|
+
|
|
24
|
+
test("createWorker wraps a native worker and disposes via terminate", () => {
|
|
25
|
+
const nativeWorker = {
|
|
26
|
+
onmessage: null as ((event: MessageEvent<string>) => void) | null,
|
|
27
|
+
postMessage:
|
|
28
|
+
mock.fn<
|
|
29
|
+
(message: unknown, transfer?: ReadonlyArray<Transferable>) => void
|
|
30
|
+
>(),
|
|
31
|
+
terminate: mock.fn(),
|
|
32
|
+
};
|
|
33
|
+
const worker = createWorker<string, string>(
|
|
34
|
+
nativeWorker as unknown as Worker,
|
|
35
|
+
);
|
|
36
|
+
const received: Array<string> = [];
|
|
37
|
+
|
|
38
|
+
worker.onMessage = (message) => {
|
|
39
|
+
received.push(message);
|
|
40
|
+
};
|
|
41
|
+
nativeWorker.onmessage?.({ data: "response" } as MessageEvent<string>);
|
|
42
|
+
worker.postMessage("request");
|
|
43
|
+
worker[Symbol.dispose]();
|
|
44
|
+
|
|
45
|
+
assertEqual(received, ["response"]);
|
|
46
|
+
assertSame(worker.native, nativeWorker);
|
|
47
|
+
assertEqual(
|
|
48
|
+
nativeWorker.postMessage.mock.calls.map(({ arguments: args }) => args),
|
|
49
|
+
[["request"]],
|
|
50
|
+
);
|
|
51
|
+
assertEqual(nativeWorker.terminate.mock.callCount(), 1);
|
|
52
|
+
assertEqual(nativeWorker.onmessage, null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("createMessageChannel queues messages until onMessage is assigned", async () => {
|
|
56
|
+
using channel = createMessageChannel<string>();
|
|
57
|
+
const received: Array<string> = [];
|
|
58
|
+
const delivered = Promise.withResolvers<void>();
|
|
59
|
+
|
|
60
|
+
channel.port1.postMessage("queued");
|
|
61
|
+
channel.port2.onMessage = (message) => {
|
|
62
|
+
received.push(message);
|
|
63
|
+
delivered.resolve();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await delivered.promise;
|
|
67
|
+
assertEqual(received, ["queued"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("createMessageChannel supports bidirectional communication and disposal", async () => {
|
|
71
|
+
using channel = createMessageChannel<string, number>();
|
|
72
|
+
const strings: Array<string> = [];
|
|
73
|
+
const numbers: Array<number> = [];
|
|
74
|
+
const stringDelivered = Promise.withResolvers<void>();
|
|
75
|
+
const numberDelivered = Promise.withResolvers<void>();
|
|
76
|
+
|
|
77
|
+
channel.port2.onMessage = (message) => {
|
|
78
|
+
strings.push(message);
|
|
79
|
+
stringDelivered.resolve();
|
|
80
|
+
};
|
|
81
|
+
channel.port1.onMessage = (message) => {
|
|
82
|
+
numbers.push(message);
|
|
83
|
+
numberDelivered.resolve();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
channel.port1.postMessage("hello");
|
|
87
|
+
channel.port2.postMessage(42);
|
|
88
|
+
|
|
89
|
+
await Promise.all([stringDelivered.promise, numberDelivered.promise]);
|
|
90
|
+
assertEqual(strings, ["hello"]);
|
|
91
|
+
assertEqual(numbers, [42]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("createMessagePort wraps a native port received from MessageChannel", async () => {
|
|
95
|
+
using disposer = new DisposableStack();
|
|
96
|
+
const nativeChannel = new MessageChannel();
|
|
97
|
+
disposer.defer(() => {
|
|
98
|
+
nativeChannel.port2.close();
|
|
99
|
+
});
|
|
100
|
+
const wrappedPort = disposer.use(
|
|
101
|
+
createMessagePort<number, string>(
|
|
102
|
+
nativeChannel.port1 as unknown as NativeMessagePort<number, string>,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
const received: Array<string> = [];
|
|
106
|
+
const delivered = Promise.withResolvers<void>();
|
|
107
|
+
|
|
108
|
+
wrappedPort.onMessage = (message) => {
|
|
109
|
+
received.push(message);
|
|
110
|
+
delivered.resolve();
|
|
111
|
+
};
|
|
112
|
+
nativeChannel.port2.postMessage("hello");
|
|
113
|
+
|
|
114
|
+
await delivered.promise;
|
|
115
|
+
assertEqual(received, ["hello"]);
|
|
116
|
+
|
|
117
|
+
const nativeReceived = new Promise<number>((resolve) => {
|
|
118
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- Assigning onmessage also starts this MessagePort automatically.
|
|
119
|
+
nativeChannel.port2.onmessage = (event) => {
|
|
120
|
+
resolve(event.data as number);
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
wrappedPort.postMessage(42);
|
|
125
|
+
|
|
126
|
+
assertEqual(await nativeReceived, 42);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("createMessagePort assigns and clears the native onmessage handler", () => {
|
|
130
|
+
const nativePort = createClosableNativePort<string>();
|
|
131
|
+
const wrappedPort = createMessagePort<string, string>(
|
|
132
|
+
nativePort as unknown as NativeMessagePort<string, string>,
|
|
133
|
+
);
|
|
134
|
+
const transferable = new ArrayBuffer(1);
|
|
135
|
+
const received: Array<string> = [];
|
|
136
|
+
|
|
137
|
+
wrappedPort.onMessage = (message) => {
|
|
138
|
+
received.push(message);
|
|
139
|
+
};
|
|
140
|
+
nativePort.onmessage?.({ data: "response" } as MessageEvent<string>);
|
|
141
|
+
wrappedPort.postMessage("without transfer");
|
|
142
|
+
wrappedPort.postMessage("with transfer", [transferable]);
|
|
143
|
+
wrappedPort.onMessage = null;
|
|
144
|
+
|
|
145
|
+
assertEqual(received, ["response"]);
|
|
146
|
+
assertEqual(nativePort.onmessage, null);
|
|
147
|
+
assertEqual(wrappedPort.onMessage, null);
|
|
148
|
+
assertEqual(
|
|
149
|
+
nativePort.postMessage.mock.calls.map(({ arguments: args }) => args),
|
|
150
|
+
[["without transfer"], ["with transfer", [transferable]]],
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("createBroadcastChannel wraps native BroadcastChannel", async () => {
|
|
155
|
+
const channelName = `test-channel-${crypto.randomUUID()}`;
|
|
156
|
+
const channel1 = createBroadcastChannel<string>(channelName);
|
|
157
|
+
const received1: Array<string> = [];
|
|
158
|
+
const received2: Array<string> = [];
|
|
159
|
+
const delivered = Promise.withResolvers<void>();
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
using _channel1 = channel1;
|
|
163
|
+
using channel2 = createBroadcastChannel<string>(channelName);
|
|
164
|
+
|
|
165
|
+
channel1.onMessage = (message) => {
|
|
166
|
+
received1.push(message);
|
|
167
|
+
};
|
|
168
|
+
channel2.onMessage = (message) => {
|
|
169
|
+
received2.push(message);
|
|
170
|
+
};
|
|
171
|
+
assertNonNullable(channel2.onMessage);
|
|
172
|
+
channel2.onMessage = null;
|
|
173
|
+
assertEqual(channel2.onMessage, null);
|
|
174
|
+
channel2.onMessage = (message) => {
|
|
175
|
+
received2.push(message);
|
|
176
|
+
delivered.resolve();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
channel1.postMessage("hello");
|
|
180
|
+
|
|
181
|
+
await delivered.promise;
|
|
182
|
+
assertEqual(received2, ["hello"]);
|
|
183
|
+
|
|
184
|
+
assertEqual(received1, []);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
channel1.onMessage = (message) => {
|
|
188
|
+
received1.push(message);
|
|
189
|
+
};
|
|
190
|
+
assertEqual(channel1.onMessage, null);
|
|
191
|
+
const error = assertThrowsInstanceOf(
|
|
192
|
+
() => channel1.postMessage("closed"),
|
|
193
|
+
Error,
|
|
194
|
+
);
|
|
195
|
+
assertEqual(error.message, "Cannot use a disposed object.");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("createMessagePort dispose uses terminate when available", () => {
|
|
199
|
+
const nativePort = {
|
|
200
|
+
onmessage: null as ((event: MessageEvent<string>) => void) | null,
|
|
201
|
+
postMessage:
|
|
202
|
+
mock.fn<
|
|
203
|
+
(message: unknown, transfer?: ReadonlyArray<Transferable>) => void
|
|
204
|
+
>(),
|
|
205
|
+
terminate: mock.fn(),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const wrappedPort = createMessagePort(
|
|
209
|
+
nativePort as unknown as NativeMessagePort<string>,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
wrappedPort[Symbol.dispose]();
|
|
213
|
+
|
|
214
|
+
assertEqual(nativePort.onmessage, null);
|
|
215
|
+
assertEqual(nativePort.terminate.mock.callCount(), 1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("createSharedWorker wraps a shared worker port and disposes via close", () => {
|
|
219
|
+
const nativePort = createClosableNativePort<string>();
|
|
220
|
+
const nativeSharedWorker = { port: nativePort };
|
|
221
|
+
const worker = createSharedWorker<string, string>(
|
|
222
|
+
nativeSharedWorker as unknown as SharedWorker,
|
|
223
|
+
);
|
|
224
|
+
const received: Array<string> = [];
|
|
225
|
+
|
|
226
|
+
worker.port.onMessage = (message) => {
|
|
227
|
+
received.push(message);
|
|
228
|
+
};
|
|
229
|
+
nativePort.onmessage?.({ data: "response" } as MessageEvent<string>);
|
|
230
|
+
worker.port.postMessage("request");
|
|
231
|
+
worker[Symbol.dispose]();
|
|
232
|
+
|
|
233
|
+
assertEqual(received, ["response"]);
|
|
234
|
+
assertSame(worker.port.native, nativePort);
|
|
235
|
+
assertEqual(
|
|
236
|
+
nativePort.postMessage.mock.calls.map(({ arguments: args }) => args),
|
|
237
|
+
[["request"]],
|
|
238
|
+
);
|
|
239
|
+
assertEqual(nativePort.close.mock.callCount(), 1);
|
|
240
|
+
assertEqual(nativePort.onmessage, null);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("one-tab SharedWorker polyfill", () => {
|
|
244
|
+
it("installOneTabSharedWorkerPolyfill installs a Worker-backed SharedWorker", () => {
|
|
245
|
+
const nativeWorker = createClosableNativePort<string>();
|
|
246
|
+
const calls: Array<{
|
|
247
|
+
readonly scriptURL: string | URL;
|
|
248
|
+
readonly options: WorkerOptions | undefined;
|
|
249
|
+
}> = [];
|
|
250
|
+
const Worker = function (scriptURL: string | URL, options?: WorkerOptions) {
|
|
251
|
+
calls.push({ scriptURL, options });
|
|
252
|
+
return nativeWorker;
|
|
253
|
+
} as unknown as typeof globalThis.Worker;
|
|
254
|
+
const scriptURL = new URL("https://example.com/Shared.worker.js");
|
|
255
|
+
const options = { type: "module" } as const;
|
|
256
|
+
|
|
257
|
+
using _sharedWorker = testStubGlobal("SharedWorker", undefined);
|
|
258
|
+
using _worker = testStubGlobal("Worker", Worker);
|
|
259
|
+
|
|
260
|
+
installOneTabSharedWorkerPolyfill();
|
|
261
|
+
const nativeSharedWorker = new SharedWorker(scriptURL, options);
|
|
262
|
+
|
|
263
|
+
assertEqual(calls, [{ scriptURL, options }]);
|
|
264
|
+
assertSame(nativeSharedWorker.port, nativeWorker);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("installOneTabSharedWorkerPolyfill keeps native SharedWorker", () => {
|
|
268
|
+
const nativePort = createClosableNativePort<string>();
|
|
269
|
+
const NativeSharedWorker = class {
|
|
270
|
+
readonly port = nativePort;
|
|
271
|
+
} as unknown as typeof SharedWorker;
|
|
272
|
+
|
|
273
|
+
using _sharedWorker = testStubGlobal("SharedWorker", NativeSharedWorker);
|
|
274
|
+
|
|
275
|
+
installOneTabSharedWorkerPolyfill();
|
|
276
|
+
assertSame(SharedWorker, NativeSharedWorker);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("createOneTabSharedWorkerSelfPolyfill creates one queued synthetic connection", () => {
|
|
280
|
+
const nativeSelf = createClosableNativePort<string>();
|
|
281
|
+
const workerSelf = createOneTabSharedWorkerSelfPolyfill<string, string>(
|
|
282
|
+
nativeSelf as unknown as DedicatedWorkerGlobalScope,
|
|
283
|
+
);
|
|
284
|
+
const received: Array<string> = [];
|
|
285
|
+
let connectedPort!: MessagePort<string, string>;
|
|
286
|
+
|
|
287
|
+
assertEqual(workerSelf.onConnect, null);
|
|
288
|
+
workerSelf.onConnect = null;
|
|
289
|
+
workerSelf.onConnect = (port) => {
|
|
290
|
+
connectedPort = port;
|
|
291
|
+
};
|
|
292
|
+
assertNonNullable(workerSelf.onConnect);
|
|
293
|
+
|
|
294
|
+
nativeSelf.onmessage?.({ data: "queued" } as MessageEvent<string>);
|
|
295
|
+
connectedPort.onMessage = (message) => {
|
|
296
|
+
received.push(message);
|
|
297
|
+
};
|
|
298
|
+
assertNonNullable(connectedPort.onMessage);
|
|
299
|
+
|
|
300
|
+
nativeSelf.onmessage?.({ data: "immediate" } as MessageEvent<string>);
|
|
301
|
+
connectedPort.onMessage = null;
|
|
302
|
+
assertEqual(connectedPort.onMessage, null);
|
|
303
|
+
connectedPort.onMessage = (message) => {
|
|
304
|
+
received.push(message);
|
|
305
|
+
};
|
|
306
|
+
workerSelf.onConnect = null;
|
|
307
|
+
|
|
308
|
+
connectedPort.postMessage("response");
|
|
309
|
+
const transferable = new ArrayBuffer(1);
|
|
310
|
+
connectedPort.postMessage("response with transfer", [transferable]);
|
|
311
|
+
const nativeSelfOnMessage = nativeSelf.onmessage;
|
|
312
|
+
workerSelf[Symbol.dispose]();
|
|
313
|
+
nativeSelfOnMessage?.({ data: "ignored" } as MessageEvent<string>);
|
|
314
|
+
connectedPort.postMessage("ignored");
|
|
315
|
+
connectedPort.onMessage = () => {
|
|
316
|
+
received.push("ignored");
|
|
317
|
+
};
|
|
318
|
+
workerSelf[Symbol.dispose]();
|
|
319
|
+
|
|
320
|
+
assertEqual(received, ["queued", "immediate"]);
|
|
321
|
+
assertSame(connectedPort.native, nativeSelf);
|
|
322
|
+
assertEqual(connectedPort.onMessage, null);
|
|
323
|
+
assertEqual(
|
|
324
|
+
nativeSelf.postMessage.mock.calls.map(({ arguments: args }) => args),
|
|
325
|
+
[["response"], ["response with transfer", [transferable]]],
|
|
326
|
+
);
|
|
327
|
+
assertEqual(nativeSelf.close.mock.callCount(), 1);
|
|
328
|
+
assertEqual(nativeSelf.onmessage, null);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("createOneTabSharedWorkerSelfPolyfill stops flushing when onMessage is cleared", () => {
|
|
332
|
+
const nativeSelf = createClosableNativePort<string>();
|
|
333
|
+
const workerSelf = createOneTabSharedWorkerSelfPolyfill<string, string>(
|
|
334
|
+
nativeSelf as unknown as DedicatedWorkerGlobalScope,
|
|
335
|
+
);
|
|
336
|
+
const received: Array<string> = [];
|
|
337
|
+
let connectedPort!: MessagePort<string, string>;
|
|
338
|
+
|
|
339
|
+
workerSelf.onConnect = (port) => {
|
|
340
|
+
connectedPort = port;
|
|
341
|
+
};
|
|
342
|
+
nativeSelf.onmessage?.({ data: "first" } as MessageEvent<string>);
|
|
343
|
+
nativeSelf.onmessage?.({ data: "second" } as MessageEvent<string>);
|
|
344
|
+
connectedPort.onMessage = (message) => {
|
|
345
|
+
received.push(message);
|
|
346
|
+
connectedPort.onMessage = null;
|
|
347
|
+
};
|
|
348
|
+
workerSelf[Symbol.dispose]();
|
|
349
|
+
|
|
350
|
+
assertEqual(received, ["first"]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("createOneTabSharedWorkerSelfPolyfill disposes from connected port", () => {
|
|
354
|
+
const nativeSelf = createClosableNativePort<string>();
|
|
355
|
+
const workerSelf = createOneTabSharedWorkerSelfPolyfill<string, string>(
|
|
356
|
+
nativeSelf as unknown as DedicatedWorkerGlobalScope,
|
|
357
|
+
);
|
|
358
|
+
let connectedPort!: MessagePort<string, string>;
|
|
359
|
+
|
|
360
|
+
workerSelf.onConnect = (port) => {
|
|
361
|
+
connectedPort = port;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
connectedPort[Symbol.dispose]();
|
|
365
|
+
workerSelf.onConnect = () => {
|
|
366
|
+
throw new Error("Disposed worker self must ignore onConnect setter.");
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
assertEqual(workerSelf.onConnect, null);
|
|
370
|
+
assertEqual(connectedPort.onMessage, null);
|
|
371
|
+
assertEqual(nativeSelf.close.mock.callCount(), 1);
|
|
372
|
+
assertEqual(nativeSelf.onmessage, null);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("createWorkerSelf wraps dedicated worker self and disposes via close", () => {
|
|
377
|
+
const nativeSelf = createClosableNativePort<string>();
|
|
378
|
+
const workerSelf = createWorkerSelf<string, string>(
|
|
379
|
+
nativeSelf as unknown as DedicatedWorkerGlobalScope,
|
|
380
|
+
);
|
|
381
|
+
const received: Array<string> = [];
|
|
382
|
+
|
|
383
|
+
workerSelf.onMessage = (message) => {
|
|
384
|
+
received.push(message);
|
|
385
|
+
};
|
|
386
|
+
nativeSelf.onmessage?.({ data: "request" } as MessageEvent<string>);
|
|
387
|
+
workerSelf.postMessage("response");
|
|
388
|
+
workerSelf[Symbol.dispose]();
|
|
389
|
+
|
|
390
|
+
assertEqual(received, ["request"]);
|
|
391
|
+
assertEqual(
|
|
392
|
+
nativeSelf.postMessage.mock.calls.map(({ arguments: args }) => args),
|
|
393
|
+
[["response"]],
|
|
394
|
+
);
|
|
395
|
+
assertEqual(nativeSelf.close.mock.callCount(), 1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("createSharedWorkerSelf wraps connected ports and disposes the worker scope", () => {
|
|
399
|
+
const nativePort = createClosableNativePort<string>();
|
|
400
|
+
const nativeSelf = {
|
|
401
|
+
close: mock.fn(),
|
|
402
|
+
onconnect: null as ((event: MessageEvent) => void) | null,
|
|
403
|
+
};
|
|
404
|
+
const workerSelf = createSharedWorkerSelf<string, string>(
|
|
405
|
+
nativeSelf as unknown as SharedWorkerGlobalScope,
|
|
406
|
+
);
|
|
407
|
+
const received: Array<string> = [];
|
|
408
|
+
let connectedPort!: MessagePort<string, string>;
|
|
409
|
+
|
|
410
|
+
workerSelf.onConnect = (port) => {
|
|
411
|
+
connectedPort = port;
|
|
412
|
+
};
|
|
413
|
+
nativeSelf.onconnect?.({ ports: [nativePort] } as unknown as MessageEvent);
|
|
414
|
+
|
|
415
|
+
connectedPort.onMessage = (message) => {
|
|
416
|
+
received.push(message);
|
|
417
|
+
};
|
|
418
|
+
nativePort.onmessage?.({ data: "request" } as MessageEvent<string>);
|
|
419
|
+
connectedPort.postMessage("response");
|
|
420
|
+
connectedPort[Symbol.dispose]();
|
|
421
|
+
workerSelf[Symbol.dispose]();
|
|
422
|
+
|
|
423
|
+
assertEqual(received, ["request"]);
|
|
424
|
+
assertSame(connectedPort.native, nativePort);
|
|
425
|
+
assertEqual(
|
|
426
|
+
nativePort.postMessage.mock.calls.map(({ arguments: args }) => args),
|
|
427
|
+
[["response"]],
|
|
428
|
+
);
|
|
429
|
+
assertEqual(nativePort.close.mock.callCount(), 1);
|
|
430
|
+
assertEqual(nativeSelf.onconnect, null);
|
|
431
|
+
assertEqual(nativeSelf.close.mock.callCount(), 1);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("createSharedWorkerSelf asserts when a connection arrives before onConnect is set", () => {
|
|
435
|
+
const nativeSelf = {
|
|
436
|
+
close: mock.fn(),
|
|
437
|
+
onconnect: null as ((event: MessageEvent) => void) | null,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
createSharedWorkerSelf<string, string>(
|
|
441
|
+
nativeSelf as unknown as SharedWorkerGlobalScope,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const error = assertThrowsInstanceOf(() => {
|
|
445
|
+
nativeSelf.onconnect?.({ ports: [] } as unknown as MessageEvent);
|
|
446
|
+
}, Error);
|
|
447
|
+
assertEqual(
|
|
448
|
+
error.message,
|
|
449
|
+
"onConnect must be set before receiving connections",
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("createWorkerDeps stores console output entries and exposes createMessagePort", () => {
|
|
454
|
+
const deps = createWorkerDeps();
|
|
455
|
+
|
|
456
|
+
deps.console.warn("worker-warning");
|
|
457
|
+
|
|
458
|
+
assertSame(deps.createMessagePort, createMessagePort);
|
|
459
|
+
assertEqual(deps.consoleStoreOutputEntry.get(), {
|
|
460
|
+
args: ["worker-warning"],
|
|
461
|
+
method: "warn",
|
|
462
|
+
path: [],
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const createClosableNativePort = <Output = never>() => ({
|
|
467
|
+
close: mock.fn(),
|
|
468
|
+
onmessage: null as ((event: MessageEvent<Output>) => void) | null,
|
|
469
|
+
postMessage:
|
|
470
|
+
mock.fn<
|
|
471
|
+
(message: unknown, transfer?: ReadonlyArray<Transferable>) => void
|
|
472
|
+
>(),
|
|
473
|
+
});
|
package/src/Worker.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
WorkerSelf,
|
|
16
16
|
} from "@evolu/common";
|
|
17
17
|
import {
|
|
18
|
-
|
|
18
|
+
assertNonNullable,
|
|
19
19
|
createConsole,
|
|
20
20
|
createConsoleStoreOutput,
|
|
21
21
|
disposable,
|
|
@@ -117,6 +117,7 @@ export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
|
117
117
|
|
|
118
118
|
disposer.defer(() => {
|
|
119
119
|
disposed = true;
|
|
120
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
120
121
|
nativeBroadcastChannel.onmessage = null;
|
|
121
122
|
nativeBroadcastChannel.close();
|
|
122
123
|
});
|
|
@@ -134,6 +135,7 @@ export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
|
134
135
|
set onMessage(fn) {
|
|
135
136
|
if (disposed) return;
|
|
136
137
|
onMessageHandler = fn;
|
|
138
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
137
139
|
nativeBroadcastChannel.onmessage = fn
|
|
138
140
|
? (event: MessageEvent<Output>) => {
|
|
139
141
|
fn(event.data);
|
|
@@ -150,7 +152,7 @@ export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
|
150
152
|
* (`self` inside a dedicated worker).
|
|
151
153
|
*/
|
|
152
154
|
export const createWorkerSelf = <Input, Output = never>(
|
|
153
|
-
nativeSelf:
|
|
155
|
+
nativeSelf: DedicatedWorkerGlobalScope,
|
|
154
156
|
): WorkerSelf<Input, Output> => wrap<Output, Input>(nativeSelf);
|
|
155
157
|
|
|
156
158
|
/**
|
|
@@ -160,19 +162,21 @@ export const createWorkerSelf = <Input, Output = never>(
|
|
|
160
162
|
* Disposing closes the shared worker scope for all connected clients.
|
|
161
163
|
*/
|
|
162
164
|
export const createSharedWorkerSelf = <Input, Output = never>(
|
|
163
|
-
nativeSelf:
|
|
165
|
+
nativeSelf: SharedWorkerGlobalScope,
|
|
164
166
|
): SharedWorkerSelf<Input, Output> => {
|
|
165
167
|
const self: SharedWorkerSelf<Input, Output> = {
|
|
166
168
|
onConnect: null,
|
|
167
169
|
[Symbol.dispose]: () => {
|
|
170
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
168
171
|
nativeSelf.onconnect = null;
|
|
169
172
|
nativeSelf.close();
|
|
170
173
|
},
|
|
171
174
|
};
|
|
172
175
|
|
|
176
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
173
177
|
nativeSelf.onconnect = (e) => {
|
|
174
|
-
|
|
175
|
-
self.onConnect
|
|
178
|
+
assertNonNullable(
|
|
179
|
+
self.onConnect,
|
|
176
180
|
"onConnect must be set before receiving connections",
|
|
177
181
|
);
|
|
178
182
|
self.onConnect(wrap<Output, Input>(e.ports[0]));
|
|
@@ -189,7 +193,7 @@ export const createSharedWorkerSelf = <Input, Output = never>(
|
|
|
189
193
|
* `onMessage` is set, and does not share state across tabs.
|
|
190
194
|
*/
|
|
191
195
|
export const createOneTabSharedWorkerSelfPolyfill = <Input, Output = never>(
|
|
192
|
-
nativeSelf:
|
|
196
|
+
nativeSelf: DedicatedWorkerGlobalScope,
|
|
193
197
|
): SharedWorkerSelf<Input, Output> => {
|
|
194
198
|
using disposer = new DisposableStack();
|
|
195
199
|
|
|
@@ -203,12 +207,14 @@ export const createOneTabSharedWorkerSelfPolyfill = <Input, Output = never>(
|
|
|
203
207
|
messages.length = 0;
|
|
204
208
|
onConnectHandler = null;
|
|
205
209
|
onMessageHandler = null;
|
|
210
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
206
211
|
nativeSelf.onmessage = null;
|
|
207
212
|
nativeSelf.close();
|
|
208
213
|
});
|
|
209
214
|
|
|
210
215
|
const disposables = disposer.move();
|
|
211
216
|
|
|
217
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
212
218
|
nativeSelf.onmessage = (event: MessageEvent<Input>) => {
|
|
213
219
|
if (disposables.disposed) return;
|
|
214
220
|
|
|
@@ -300,10 +306,12 @@ const wrap = <Input, Output>(
|
|
|
300
306
|
onMessageHandler = fn;
|
|
301
307
|
if (fn) {
|
|
302
308
|
// Messages are queued until onMessage is assigned.
|
|
309
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
303
310
|
native.onmessage = (event: MessageEvent<Output>) => {
|
|
304
311
|
fn(event.data);
|
|
305
312
|
};
|
|
306
313
|
} else {
|
|
314
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
307
315
|
native.onmessage = null;
|
|
308
316
|
}
|
|
309
317
|
},
|
|
@@ -311,6 +319,7 @@ const wrap = <Input, Output>(
|
|
|
311
319
|
native: native as unknown as NativeMessagePort<Input, Output>,
|
|
312
320
|
|
|
313
321
|
[Symbol.dispose]: () => {
|
|
322
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener -- This adapter owns and clears one handler.
|
|
314
323
|
native.onmessage = null;
|
|
315
324
|
if ("terminate" in native) native.terminate();
|
|
316
325
|
else native.close();
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { createWorkerDeps, createWorkerSelf } from "../Worker.ts";
|
|
|
13
13
|
const run = createRun({
|
|
14
14
|
...createWorkerDeps(),
|
|
15
15
|
createSqliteDriver: createWasmSqliteDriver,
|
|
16
|
-
lockManager:
|
|
16
|
+
lockManager: navigator.locks,
|
|
17
17
|
randomBytes: createRandomBytes(),
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEqual,
|
|
3
|
+
assertNonNullable,
|
|
4
|
+
testStubGlobal,
|
|
5
|
+
type NativeMessagePort,
|
|
6
|
+
} from "@evolu/common";
|
|
7
|
+
import { describe, it, mock } from "node:test";
|
|
8
|
+
import { createEvoluDeps } from "./Evolu.ts";
|
|
9
|
+
|
|
10
|
+
describe("createEvoluDeps", () => {
|
|
11
|
+
it("createEvoluDeps calls callback when one-tab SharedWorker polyfill is already open", () => {
|
|
12
|
+
const nativeSharedWorkerPort = createClosableNativePort<unknown>();
|
|
13
|
+
const nativeDbWorker = createClosableNativePort();
|
|
14
|
+
const onSharedWorkerUnsupported = mock.fn<() => void>();
|
|
15
|
+
|
|
16
|
+
using _sharedWorker = testStubGlobal(
|
|
17
|
+
"SharedWorker",
|
|
18
|
+
class {
|
|
19
|
+
readonly port = nativeSharedWorkerPort as unknown as NativeMessagePort<
|
|
20
|
+
never,
|
|
21
|
+
unknown
|
|
22
|
+
>;
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
const Worker = mock.fn(function () {
|
|
26
|
+
return nativeDbWorker;
|
|
27
|
+
});
|
|
28
|
+
using _worker = testStubGlobal("Worker", Worker);
|
|
29
|
+
|
|
30
|
+
using deps = createEvoluDeps({
|
|
31
|
+
onSharedWorkerUnsupported,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
nativeSharedWorkerPort.onmessage?.(
|
|
35
|
+
new MessageEvent("message", {
|
|
36
|
+
data: { type: "SharedWorkerUnsupported" },
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
assertEqual(onSharedWorkerUnsupported.mock.callCount(), 1);
|
|
41
|
+
assertEqual(Worker.mock.callCount(), 0);
|
|
42
|
+
assertNonNullable(deps);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const createClosableNativePort = <Output = never>() => ({
|
|
47
|
+
close: mock.fn(),
|
|
48
|
+
onmessage: null as ((event: MessageEvent<Output>) => void) | null,
|
|
49
|
+
postMessage: mock.fn(),
|
|
50
|
+
});
|
package/src/local-first/Evolu.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
} from "../Worker.ts";
|
|
19
19
|
|
|
20
20
|
// // TODO: Redesign.
|
|
21
|
-
// // eslint-disable-next-line evolu/require-pure-annotation
|
|
22
21
|
// export const localAuth = createLocalAuth({
|
|
23
22
|
// randomBytes: createRandomBytes(),
|
|
24
23
|
// secureStorage: createWebAuthnStore({ randomBytes: createRandomBytes() }),
|
|
@@ -76,7 +75,7 @@ export const createEvoluDeps = (
|
|
|
76
75
|
if (deps.onSharedWorkerUnsupported) {
|
|
77
76
|
deps.onSharedWorkerUnsupported();
|
|
78
77
|
} else {
|
|
79
|
-
|
|
78
|
+
alert(
|
|
80
79
|
"This browser supports Evolu in one tab only. Close this tab and use the already open tab.",
|
|
81
80
|
);
|
|
82
81
|
}
|
|
@@ -97,7 +96,7 @@ export const createEvoluDeps = (
|
|
|
97
96
|
createDbWorker,
|
|
98
97
|
createBroadcastChannel,
|
|
99
98
|
createMessageChannel,
|
|
100
|
-
lockManager:
|
|
99
|
+
lockManager: navigator.locks,
|
|
101
100
|
reloadApp,
|
|
102
101
|
sharedWorker,
|
|
103
102
|
});
|
|
@@ -226,9 +226,12 @@ const createCredentialCreationOptions =
|
|
|
226
226
|
displayName: username,
|
|
227
227
|
},
|
|
228
228
|
pubKeyCredParams: [
|
|
229
|
-
|
|
230
|
-
{ type: "public-key", alg: -
|
|
231
|
-
|
|
229
|
+
// Ed25519
|
|
230
|
+
{ type: "public-key", alg: -8 },
|
|
231
|
+
// ES256
|
|
232
|
+
{ type: "public-key", alg: -7 },
|
|
233
|
+
// RS256
|
|
234
|
+
{ type: "public-key", alg: -257 },
|
|
232
235
|
],
|
|
233
236
|
attestation: "none",
|
|
234
237
|
authenticatorSelection: {
|