@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.
@@ -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
- assert,
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: globalThis.DedicatedWorkerGlobalScope,
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: globalThis.SharedWorkerGlobalScope,
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
- assert(
175
- self.onConnect != null,
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: globalThis.DedicatedWorkerGlobalScope,
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
@@ -1,5 +1,5 @@
1
1
  export * from "./local-first/index.ts";
2
- export { availableParallelism } from "./Platform.ts";
2
+ export { availableParallelism, isApplePlatform } from "./Platform.ts";
3
3
  export * from "./Sqlite.ts";
4
4
  export * from "./Task.ts";
5
5
  export * from "./Worker.ts";
@@ -13,7 +13,7 @@ import { createWorkerDeps, createWorkerSelf } from "../Worker.ts";
13
13
  const run = createRun({
14
14
  ...createWorkerDeps(),
15
15
  createSqliteDriver: createWasmSqliteDriver,
16
- lockManager: globalThis.navigator.locks,
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
+ });
@@ -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
- globalThis.alert(
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: globalThis.navigator.locks,
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
- { type: "public-key", alg: -8 }, // Ed25519
230
- { type: "public-key", alg: -7 }, // ES256
231
- { type: "public-key", alg: -257 }, // RS256
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: {