@evolu/common 8.0.0-next.2 → 8.0.0-next.4
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/Assert.d.ts +5 -5
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +4 -4
- package/dist/src/Identicon.js +4 -4
- package/dist/src/LockManager.d.ts +74 -0
- package/dist/src/LockManager.d.ts.map +1 -0
- package/dist/src/LockManager.js +103 -0
- package/dist/src/Microtask.d.ts.map +1 -1
- package/dist/src/Microtask.js +90 -26
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/RefCount.js +153 -68
- package/dist/src/Resource.d.ts +6 -6
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +211 -162
- package/dist/src/Schedule.d.ts +5 -5
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +53 -44
- package/dist/src/Sqlite.d.ts +2 -0
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +64 -40
- package/dist/src/Task.d.ts +6 -33
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +213 -204
- package/dist/src/Test.d.ts +4 -4
- package/dist/src/Time.d.ts +5 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +5 -0
- package/dist/src/Type.d.ts +7 -0
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +7 -0
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +7 -6
- package/dist/src/Worker.d.ts +42 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +298 -75
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/local-first/Db.d.ts +5 -4
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +44 -38
- package/dist/src/local-first/Evolu.d.ts +3 -2
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +90 -80
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +65 -54
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +387 -373
- package/package.json +1 -1
- package/src/Array.ts +8 -8
- package/src/Assert.ts +5 -5
- package/src/Identicon.ts +4 -4
- package/src/LockManager.ts +181 -0
- package/src/Microtask.ts +17 -11
- package/src/Object.ts +3 -3
- package/src/RefCount.ts +25 -21
- package/src/Resource.ts +63 -56
- package/src/Schedule.ts +70 -48
- package/src/Set.ts +4 -4
- package/src/Sqlite.ts +41 -30
- package/src/Task.ts +77 -120
- package/src/Test.ts +4 -4
- package/src/Time.ts +7 -0
- package/src/Type.ts +9 -0
- package/src/WebSocket.ts +7 -6
- package/src/Worker.ts +240 -49
- package/src/index.ts +1 -0
- package/src/local-first/Db.ts +69 -46
- package/src/local-first/Evolu.ts +74 -68
- package/src/local-first/Owner.ts +1 -1
- package/src/local-first/Query.ts +1 -1
- package/src/local-first/Shared.ts +472 -464
package/src/Worker.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Typed, disposable worker and messaging primitives.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { assert } from "./Assert.js";
|
|
7
|
+
import { assert, assertNotDisposed } from "./Assert.js";
|
|
8
8
|
import type { Brand } from "./Brand.js";
|
|
9
9
|
import type { ConsoleDep, ConsoleStoreOutputEntryDep } from "./Console.js";
|
|
10
10
|
import { testWaitForMacrotask } from "./Test.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Typed, disposable Worker.
|
|
14
14
|
*
|
|
15
15
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Worker
|
|
16
16
|
*/
|
|
@@ -20,7 +20,7 @@ export interface Worker<Input, Output = never> extends MessagePort<
|
|
|
20
20
|
> {}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Typed, disposable SharedWorker.
|
|
24
24
|
*
|
|
25
25
|
* A shared worker is shared across multiple clients (tabs, windows, iframes)
|
|
26
26
|
* and provides a port for bidirectional communication with each client.
|
|
@@ -33,7 +33,7 @@ export interface SharedWorker<Input, Output = never> extends Disposable {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Typed, disposable MessagePort.
|
|
37
37
|
*
|
|
38
38
|
* Note: There is no reliable way to detect when a port is closed or
|
|
39
39
|
* disconnected. Calling `postMessage` on a disposed port does not throw — it
|
|
@@ -114,7 +114,7 @@ export type WorkerDeps = ConsoleDep &
|
|
|
114
114
|
CreateMessagePortDep;
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* Typed, disposable MessageChannel.
|
|
118
118
|
*
|
|
119
119
|
* Creates two entangled ports: keep one and transfer the other (e.g., to a
|
|
120
120
|
* SharedWorker via `postMessage` with `transfer`). Messages sent to one port
|
|
@@ -178,6 +178,31 @@ export interface CreateMessageChannelDep {
|
|
|
178
178
|
readonly createMessageChannel: CreateMessageChannel;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Typed, disposable BroadcastChannel.
|
|
183
|
+
*
|
|
184
|
+
* Broadcast channels deliver messages to other channels opened with the same
|
|
185
|
+
* name. The sending channel does not receive its own messages.
|
|
186
|
+
*
|
|
187
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
|
|
188
|
+
*/
|
|
189
|
+
export interface BroadcastChannel<Input, Output = Input> extends Disposable {
|
|
190
|
+
/** Posts a message to other channels with the same name. */
|
|
191
|
+
readonly postMessage: (message: Input) => void;
|
|
192
|
+
|
|
193
|
+
/** Handler for incoming broadcast messages. */
|
|
194
|
+
onMessage: ((message: Output) => void) | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Factory function to create a {@link BroadcastChannel}. */
|
|
198
|
+
export type CreateBroadcastChannel = <Input, Output = Input>(
|
|
199
|
+
name: string,
|
|
200
|
+
) => BroadcastChannel<Input, Output>;
|
|
201
|
+
|
|
202
|
+
export interface CreateBroadcastChannelDep {
|
|
203
|
+
readonly createBroadcastChannel: CreateBroadcastChannel;
|
|
204
|
+
}
|
|
205
|
+
|
|
181
206
|
/**
|
|
182
207
|
* Typed `self` for code running inside a dedicated worker.
|
|
183
208
|
*
|
|
@@ -250,29 +275,46 @@ export const createMessageChannel: CreateMessageChannel = <
|
|
|
250
275
|
handler: null,
|
|
251
276
|
queue: [],
|
|
252
277
|
flushScheduled: false,
|
|
278
|
+
flushTimeoutId: null,
|
|
279
|
+
ownerCount: 0,
|
|
280
|
+
pendingTransferCount: 0,
|
|
253
281
|
};
|
|
254
282
|
const state2: PortState<Input> = {
|
|
255
283
|
handler: null,
|
|
256
284
|
queue: [],
|
|
257
285
|
flushScheduled: false,
|
|
286
|
+
flushTimeoutId: null,
|
|
287
|
+
ownerCount: 0,
|
|
288
|
+
pendingTransferCount: 0,
|
|
258
289
|
};
|
|
259
290
|
|
|
260
291
|
const native1 = createNativeMessagePortToken<Input, Output>();
|
|
261
292
|
const native2 = createNativeMessagePortToken<Output, Input>();
|
|
262
293
|
|
|
263
|
-
const
|
|
264
|
-
|
|
294
|
+
const port1Registration: PortRegistration<Input, Output> = {
|
|
295
|
+
receive: state1,
|
|
296
|
+
peerReceive: state2,
|
|
297
|
+
native: native1,
|
|
298
|
+
};
|
|
299
|
+
const port2Registration: PortRegistration<Output, Input> = {
|
|
300
|
+
receive: state2,
|
|
301
|
+
peerReceive: state1,
|
|
302
|
+
native: native2,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
using disposer = new DisposableStack();
|
|
265
306
|
|
|
266
|
-
|
|
267
|
-
|
|
307
|
+
const port1 = disposer.use(createTestPort(port1Registration));
|
|
308
|
+
const port2 = disposer.use(createTestPort(port2Registration));
|
|
309
|
+
const disposables = disposer.move();
|
|
310
|
+
|
|
311
|
+
nativePortRegistry.set(native1, port1Registration);
|
|
312
|
+
nativePortRegistry.set(native2, port2Registration);
|
|
268
313
|
|
|
269
314
|
return {
|
|
270
315
|
port1,
|
|
271
316
|
port2,
|
|
272
|
-
[Symbol.dispose]: () =>
|
|
273
|
-
port1[Symbol.dispose]();
|
|
274
|
-
port2[Symbol.dispose]();
|
|
275
|
-
},
|
|
317
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
276
318
|
};
|
|
277
319
|
};
|
|
278
320
|
|
|
@@ -285,11 +327,80 @@ export const createMessageChannel: CreateMessageChannel = <
|
|
|
285
327
|
export const createMessagePort: CreateMessagePort = <Input, Output = never>(
|
|
286
328
|
nativePort: NativeMessagePort<Input, Output>,
|
|
287
329
|
): MessagePort<Input, Output> => {
|
|
288
|
-
const
|
|
289
|
-
assert(
|
|
290
|
-
return
|
|
330
|
+
const registration = nativePortRegistry.get(nativePort);
|
|
331
|
+
assert(registration, "Unknown native port — did you transfer it?");
|
|
332
|
+
return createTestPort(registration as PortRegistration<Input, Output>);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Creates an in-memory {@link BroadcastChannel}.
|
|
337
|
+
*
|
|
338
|
+
* This is a memory-only fallback for platforms without native BroadcastChannel
|
|
339
|
+
* support. Message delivery is asynchronous in-process.
|
|
340
|
+
*/
|
|
341
|
+
export const createBroadcastChannel: CreateBroadcastChannel = <
|
|
342
|
+
Input,
|
|
343
|
+
Output = Input,
|
|
344
|
+
>(
|
|
345
|
+
name: string,
|
|
346
|
+
): BroadcastChannel<Input, Output> => {
|
|
347
|
+
let onMessageHandler: ((message: Output) => void) | null = null;
|
|
348
|
+
const dispatch: BroadcastChannelDispatch<Output> = (message) => {
|
|
349
|
+
onMessageHandler?.(message);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
let dispatches = broadcastChannelDispatchesByName.get(name);
|
|
353
|
+
if (!dispatches) {
|
|
354
|
+
dispatches = new Set();
|
|
355
|
+
broadcastChannelDispatchesByName.set(name, dispatches);
|
|
356
|
+
}
|
|
357
|
+
dispatches.add(dispatch as BroadcastChannelDispatch<any>);
|
|
358
|
+
|
|
359
|
+
using disposer = new DisposableStack();
|
|
360
|
+
|
|
361
|
+
disposer.defer(() => {
|
|
362
|
+
onMessageHandler = null;
|
|
363
|
+
|
|
364
|
+
dispatches.delete(dispatch as BroadcastChannelDispatch<any>);
|
|
365
|
+
if (dispatches.size === 0) broadcastChannelDispatchesByName.delete(name);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const disposables = disposer.move();
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
postMessage: (message) => {
|
|
372
|
+
assertNotDisposed(disposables);
|
|
373
|
+
|
|
374
|
+
const dispatches = broadcastChannelDispatchesByName.get(name);
|
|
375
|
+
assert(dispatches, "Expected broadcast channel dispatches");
|
|
376
|
+
|
|
377
|
+
for (const otherDispatch of [...dispatches]) {
|
|
378
|
+
if (otherDispatch === dispatch) continue;
|
|
379
|
+
|
|
380
|
+
globalThis.setTimeout(() => {
|
|
381
|
+
if (!dispatches.has(otherDispatch)) return;
|
|
382
|
+
otherDispatch(message);
|
|
383
|
+
}, 0);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
get onMessage() {
|
|
387
|
+
return disposables.disposed ? null : onMessageHandler;
|
|
388
|
+
},
|
|
389
|
+
set onMessage(fn) {
|
|
390
|
+
if (disposables.disposed) return;
|
|
391
|
+
onMessageHandler = fn;
|
|
392
|
+
},
|
|
393
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
394
|
+
};
|
|
291
395
|
};
|
|
292
396
|
|
|
397
|
+
type BroadcastChannelDispatch<T> = (message: T) => void;
|
|
398
|
+
|
|
399
|
+
const broadcastChannelDispatchesByName = new Map<
|
|
400
|
+
string,
|
|
401
|
+
Set<BroadcastChannelDispatch<any>>
|
|
402
|
+
>();
|
|
403
|
+
|
|
293
404
|
/**
|
|
294
405
|
* Test {@link Worker} with access to its paired worker-side `self`.
|
|
295
406
|
*
|
|
@@ -325,6 +436,14 @@ export interface TestMessageChannel<
|
|
|
325
436
|
readonly isDisposed: () => boolean;
|
|
326
437
|
}
|
|
327
438
|
|
|
439
|
+
/** {@link BroadcastChannel} with disposal tracking for testing. */
|
|
440
|
+
export interface TestBroadcastChannel<
|
|
441
|
+
Input,
|
|
442
|
+
Output = Input,
|
|
443
|
+
> extends BroadcastChannel<Input, Output> {
|
|
444
|
+
readonly isDisposed: () => boolean;
|
|
445
|
+
}
|
|
446
|
+
|
|
328
447
|
/**
|
|
329
448
|
* Creates a connected {@link TestWorker} for testing.
|
|
330
449
|
*
|
|
@@ -341,7 +460,7 @@ export const testCreateWorker = <Input, Output = never>(): TestWorker<
|
|
|
341
460
|
self = nextSelf;
|
|
342
461
|
});
|
|
343
462
|
|
|
344
|
-
return Object.assign(worker, { self })
|
|
463
|
+
return Object.assign(worker, { self });
|
|
345
464
|
};
|
|
346
465
|
|
|
347
466
|
/**
|
|
@@ -361,7 +480,7 @@ export const testCreateSharedWorker = <
|
|
|
361
480
|
return Object.assign(worker, {
|
|
362
481
|
self,
|
|
363
482
|
connect,
|
|
364
|
-
})
|
|
483
|
+
});
|
|
365
484
|
};
|
|
366
485
|
|
|
367
486
|
/**
|
|
@@ -377,18 +496,16 @@ export const testCreateMessageChannel = <
|
|
|
377
496
|
Input,
|
|
378
497
|
Output = never,
|
|
379
498
|
>(): TestMessageChannel<Input, Output> => {
|
|
380
|
-
|
|
499
|
+
using disposer = new DisposableStack();
|
|
381
500
|
|
|
382
|
-
|
|
501
|
+
const channel = disposer.use(createMessageChannel<Input, Output>());
|
|
502
|
+
const disposables = disposer.move();
|
|
383
503
|
|
|
384
504
|
return {
|
|
385
505
|
port1: channel.port1,
|
|
386
506
|
port2: channel.port2,
|
|
387
|
-
isDisposed: () => disposed,
|
|
388
|
-
[Symbol.dispose]: () =>
|
|
389
|
-
disposed = true;
|
|
390
|
-
channel[Symbol.dispose]();
|
|
391
|
-
},
|
|
507
|
+
isDisposed: () => disposables.disposed,
|
|
508
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
392
509
|
};
|
|
393
510
|
};
|
|
394
511
|
|
|
@@ -397,11 +514,35 @@ export const testCreateMessagePort: CreateMessagePort = <Input, Output = never>(
|
|
|
397
514
|
nativePort: NativeMessagePort<Input, Output>,
|
|
398
515
|
): MessagePort<Input, Output> => createMessagePort(nativePort);
|
|
399
516
|
|
|
517
|
+
/** Creates an in-memory {@link BroadcastChannel} for testing. */
|
|
518
|
+
export const testCreateBroadcastChannel = <Input, Output = Input>(
|
|
519
|
+
name: string,
|
|
520
|
+
): TestBroadcastChannel<Input, Output> => {
|
|
521
|
+
using disposer = new DisposableStack();
|
|
522
|
+
|
|
523
|
+
const channel = disposer.use(createBroadcastChannel<Input, Output>(name));
|
|
524
|
+
const disposables = disposer.move();
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
postMessage: channel.postMessage,
|
|
528
|
+
get onMessage() {
|
|
529
|
+
return channel.onMessage;
|
|
530
|
+
},
|
|
531
|
+
set onMessage(value) {
|
|
532
|
+
channel.onMessage = value;
|
|
533
|
+
},
|
|
534
|
+
isDisposed: () => disposables.disposed,
|
|
535
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
|
|
400
539
|
/**
|
|
401
|
-
* Waits
|
|
540
|
+
* Waits the minimal time necessary for in-memory worker message delivery in
|
|
541
|
+
* tests.
|
|
402
542
|
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
543
|
+
* This is better than `vi.waitFor` when a test only needs to flush the known
|
|
544
|
+
* in-memory worker/message-port pipeline. It encodes that transport boundary
|
|
545
|
+
* once, avoids polling, and keeps worker-focused tests fast and explicit.
|
|
405
546
|
*/
|
|
406
547
|
export const testWaitForWorkerMessage = async (): Promise<void> => {
|
|
407
548
|
await testWaitForMacrotask();
|
|
@@ -481,55 +622,105 @@ interface PortState<T> {
|
|
|
481
622
|
handler: ((message: T) => void) | null;
|
|
482
623
|
readonly queue: Array<T>;
|
|
483
624
|
flushScheduled: boolean;
|
|
625
|
+
flushTimeoutId: ReturnType<typeof globalThis.setTimeout> | null;
|
|
626
|
+
ownerCount: number;
|
|
627
|
+
pendingTransferCount: number;
|
|
484
628
|
}
|
|
485
629
|
|
|
486
|
-
|
|
487
|
-
receive: PortState<Output
|
|
488
|
-
peerReceive: PortState<Input
|
|
489
|
-
native: NativeMessagePort<Input, Output
|
|
490
|
-
|
|
630
|
+
interface PortRegistration<Input, Output> {
|
|
631
|
+
readonly receive: PortState<Output>;
|
|
632
|
+
readonly peerReceive: PortState<Input>;
|
|
633
|
+
readonly native: NativeMessagePort<Input, Output>;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const createPort = <Input, Output>({
|
|
637
|
+
receive,
|
|
638
|
+
peerReceive,
|
|
639
|
+
native,
|
|
640
|
+
}: PortRegistration<Input, Output>): MessagePort<Input, Output> => {
|
|
641
|
+
using disposer = new DisposableStack();
|
|
642
|
+
|
|
643
|
+
if (receive.pendingTransferCount > 0) receive.pendingTransferCount -= 1;
|
|
644
|
+
else receive.ownerCount += 1;
|
|
645
|
+
|
|
646
|
+
disposer.defer(() => {
|
|
647
|
+
receive.ownerCount -= 1;
|
|
648
|
+
if (receive.ownerCount > 0) return;
|
|
649
|
+
|
|
650
|
+
nativePortRegistry.delete(native);
|
|
651
|
+
|
|
652
|
+
if (receive.flushTimeoutId != null) {
|
|
653
|
+
globalThis.clearTimeout(receive.flushTimeoutId);
|
|
654
|
+
receive.flushTimeoutId = null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
receive.handler = null;
|
|
658
|
+
receive.flushScheduled = false;
|
|
659
|
+
receive.queue.length = 0;
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const disposables = disposer.move();
|
|
663
|
+
|
|
491
664
|
const scheduleFlush = (state: PortState<any>): void => {
|
|
492
|
-
if (state.flushScheduled) return;
|
|
665
|
+
if (state.flushScheduled || state.ownerCount === 0) return;
|
|
493
666
|
state.flushScheduled = true;
|
|
494
667
|
|
|
495
668
|
// Native worker messages are task-queued; use macrotask timing.
|
|
496
|
-
setTimeout(() => {
|
|
669
|
+
state.flushTimeoutId = globalThis.setTimeout(() => {
|
|
670
|
+
state.flushTimeoutId = null;
|
|
497
671
|
state.flushScheduled = false;
|
|
672
|
+
if (state.ownerCount === 0) return;
|
|
498
673
|
|
|
499
|
-
const
|
|
500
|
-
if (!
|
|
674
|
+
const firstHandler = state.handler;
|
|
675
|
+
if (!firstHandler) return;
|
|
501
676
|
|
|
502
677
|
for (const message of state.queue.splice(0)) {
|
|
678
|
+
const handler = state.handler;
|
|
679
|
+
if (!handler) return;
|
|
680
|
+
|
|
503
681
|
handler(message);
|
|
504
682
|
}
|
|
505
683
|
}, 0);
|
|
506
684
|
};
|
|
507
685
|
|
|
508
686
|
return {
|
|
509
|
-
postMessage: (message) => {
|
|
687
|
+
postMessage: (message, transfer) => {
|
|
688
|
+
if (
|
|
689
|
+
disposables.disposed ||
|
|
690
|
+
receive.ownerCount === 0 ||
|
|
691
|
+
peerReceive.ownerCount === 0
|
|
692
|
+
)
|
|
693
|
+
return;
|
|
694
|
+
|
|
695
|
+
for (const transferable of transfer ?? []) {
|
|
696
|
+
if (transferable instanceof globalThis.ArrayBuffer) continue;
|
|
697
|
+
|
|
698
|
+
const registration = nativePortRegistry.get(transferable);
|
|
699
|
+
if (!registration) continue;
|
|
700
|
+
|
|
701
|
+
registration.receive.ownerCount += 1;
|
|
702
|
+
registration.receive.pendingTransferCount += 1;
|
|
703
|
+
}
|
|
704
|
+
|
|
510
705
|
peerReceive.queue.push(message);
|
|
511
706
|
scheduleFlush(peerReceive);
|
|
512
707
|
},
|
|
513
708
|
get onMessage() {
|
|
514
|
-
return receive.handler;
|
|
709
|
+
return disposables.disposed ? null : receive.handler;
|
|
515
710
|
},
|
|
516
711
|
set onMessage(fn) {
|
|
712
|
+
if (disposables.disposed || receive.ownerCount === 0) return;
|
|
517
713
|
receive.handler = fn;
|
|
518
714
|
if (fn) scheduleFlush(receive);
|
|
519
715
|
},
|
|
520
716
|
native,
|
|
521
|
-
[Symbol.dispose]: () =>
|
|
522
|
-
receive.handler = null;
|
|
523
|
-
receive.flushScheduled = false;
|
|
524
|
-
},
|
|
717
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
525
718
|
};
|
|
526
719
|
};
|
|
527
720
|
|
|
528
721
|
const createTestPort = <Input, Output>(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
native: NativeMessagePort<Input, Output>,
|
|
532
|
-
): MessagePort<Input, Output> => createPort(receive, peerReceive, native);
|
|
722
|
+
registration: PortRegistration<Input, Output>,
|
|
723
|
+
): MessagePort<Input, Output> => createPort(registration);
|
|
533
724
|
|
|
534
725
|
const createNativeMessagePortToken = <Input, Output>(): NativeMessagePort<
|
|
535
726
|
Input,
|
|
@@ -548,5 +739,5 @@ const createNativeMessagePortToken = <Input, Output>(): NativeMessagePort<
|
|
|
548
739
|
*/
|
|
549
740
|
const nativePortRegistry = new WeakMap<
|
|
550
741
|
NativeMessagePort<any, any>,
|
|
551
|
-
|
|
742
|
+
PortRegistration<any, any>
|
|
552
743
|
>();
|
package/src/index.ts
CHANGED
package/src/local-first/Db.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type RandomBytesDep,
|
|
19
19
|
} from "../Crypto.js";
|
|
20
20
|
import { exhaustiveCheck, lazyFalse, lazyVoid } from "../Function.js";
|
|
21
|
+
import type { LockManagerDep } from "../LockManager.js";
|
|
22
|
+
import { acquireLeaderLock } from "../LockManager.js";
|
|
21
23
|
import { createRecord, getProperty, objectToEntries } from "../Object.js";
|
|
22
24
|
import { ok, type Result } from "../Result.js";
|
|
23
25
|
import type {
|
|
@@ -35,7 +37,7 @@ import {
|
|
|
35
37
|
sqliteQueryStringToSqliteQuery,
|
|
36
38
|
SqliteValue,
|
|
37
39
|
} from "../Sqlite.js";
|
|
38
|
-
import { callback, type
|
|
40
|
+
import { callback, type Task } from "../Task.js";
|
|
39
41
|
import { Millis, millisToDateIso, type TimeDep } from "../Time.js";
|
|
40
42
|
import type { Name } from "../Type.js";
|
|
41
43
|
import {
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
} from "../Type.js";
|
|
48
50
|
import type { ExtractType } from "../Types.js";
|
|
49
51
|
import type {
|
|
52
|
+
CreateBroadcastChannelDep,
|
|
50
53
|
NativeMessagePort,
|
|
51
54
|
Worker,
|
|
52
55
|
WorkerDeps,
|
|
@@ -72,7 +75,14 @@ import {
|
|
|
72
75
|
getEvoluSqliteSchema,
|
|
73
76
|
systemColumns,
|
|
74
77
|
} from "./Schema.js";
|
|
75
|
-
import type {
|
|
78
|
+
import type {
|
|
79
|
+
ConsoleEntryOrError,
|
|
80
|
+
DbWorkerInput,
|
|
81
|
+
DbWorkerOutput,
|
|
82
|
+
DbWorkerQueuedResponse,
|
|
83
|
+
DbWorkerRequest,
|
|
84
|
+
} from "./Shared.js";
|
|
85
|
+
import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.js";
|
|
76
86
|
import {
|
|
77
87
|
createBaseSqliteStorage,
|
|
78
88
|
createBaseSqliteStorageTables,
|
|
@@ -105,7 +115,7 @@ import {
|
|
|
105
115
|
export type DbWorker = Worker<DbWorkerInit>;
|
|
106
116
|
|
|
107
117
|
export interface DbWorkerInit {
|
|
108
|
-
readonly type: "
|
|
118
|
+
readonly type: "DbWorkerInit";
|
|
109
119
|
readonly name: Name;
|
|
110
120
|
readonly consoleLevel: ConsoleLevel;
|
|
111
121
|
readonly sqliteSchema: SqliteSchema;
|
|
@@ -120,18 +130,22 @@ export interface CreateDbWorkerDep {
|
|
|
120
130
|
readonly createDbWorker: CreateDbWorker;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
export type DbWorkerDeps = WorkerDeps &
|
|
133
|
+
export type DbWorkerDeps = WorkerDeps &
|
|
134
|
+
CreateBroadcastChannelDep &
|
|
135
|
+
LockManagerDep &
|
|
136
|
+
CreateSqliteDriverDep;
|
|
124
137
|
|
|
125
138
|
export const startDbWorker =
|
|
126
139
|
(self: WorkerSelf<DbWorkerInit>): Task<void, never, DbWorkerDeps> =>
|
|
127
|
-
async (
|
|
128
|
-
await using
|
|
129
|
-
|
|
130
|
-
const
|
|
140
|
+
async (run) => {
|
|
141
|
+
await using disposer = new AsyncDisposableStack();
|
|
142
|
+
disposer.use(self);
|
|
143
|
+
const dbWorkerRun = disposer.use(run.create());
|
|
144
|
+
const { deps } = dbWorkerRun;
|
|
131
145
|
|
|
132
146
|
let initialized = false;
|
|
133
147
|
|
|
134
|
-
const initMessage = await
|
|
148
|
+
const initMessage = await dbWorkerRun.orThrow(
|
|
135
149
|
callback<DbWorkerInit>(({ ok }) => {
|
|
136
150
|
self.onMessage = (message) => {
|
|
137
151
|
assert(!initialized, "DbWorker must be initialized only once");
|
|
@@ -143,26 +157,36 @@ export const startDbWorker =
|
|
|
143
157
|
|
|
144
158
|
const console = deps.console.child(initMessage.name).child("DbWorker");
|
|
145
159
|
console.setLevel(initMessage.consoleLevel);
|
|
146
|
-
console.info("start DbWorker");
|
|
147
160
|
|
|
148
|
-
const port =
|
|
161
|
+
const port = disposer.use(
|
|
149
162
|
deps.createMessagePort<DbWorkerOutput, DbWorkerInput>(initMessage.port),
|
|
150
163
|
);
|
|
164
|
+
const consoleEntryOrErrorBroadcastChannel = disposer.use(
|
|
165
|
+
deps.createBroadcastChannel<ConsoleEntryOrError>(
|
|
166
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
151
169
|
|
|
152
|
-
|
|
170
|
+
disposer.defer(
|
|
153
171
|
deps.consoleStoreOutputEntry.subscribe(() => {
|
|
154
172
|
const entry = deps.consoleStoreOutputEntry.get();
|
|
155
|
-
if (entry)
|
|
173
|
+
if (entry)
|
|
174
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
175
|
+
type: "ConsoleEntry",
|
|
176
|
+
entry,
|
|
177
|
+
});
|
|
156
178
|
}),
|
|
157
179
|
);
|
|
180
|
+
console.debug("createDbWorker");
|
|
158
181
|
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
disposer.use(
|
|
183
|
+
await dbWorkerRun.orThrow(acquireLeaderLock(initMessage.name)),
|
|
184
|
+
);
|
|
161
185
|
|
|
162
186
|
port.postMessage({ type: "LeaderAcquired", name: initMessage.name });
|
|
163
187
|
|
|
164
|
-
const sqlite =
|
|
165
|
-
await
|
|
188
|
+
const sqlite = disposer.use(
|
|
189
|
+
await dbWorkerRun.orThrow(
|
|
166
190
|
createSqlite(
|
|
167
191
|
initMessage.name,
|
|
168
192
|
initMessage.memoryOnly
|
|
@@ -173,10 +197,13 @@ export const startDbWorker =
|
|
|
173
197
|
);
|
|
174
198
|
console.debug("SQLite created");
|
|
175
199
|
|
|
176
|
-
const baseSqliteStorage = createBaseSqliteStorage({
|
|
200
|
+
const baseSqliteStorage = createBaseSqliteStorage({
|
|
201
|
+
sqlite,
|
|
202
|
+
...dbWorkerRun.deps,
|
|
203
|
+
});
|
|
177
204
|
|
|
178
205
|
const dbDeps = {
|
|
179
|
-
...
|
|
206
|
+
...dbWorkerRun.deps,
|
|
180
207
|
sqlite,
|
|
181
208
|
sqliteSchema: initMessage.sqliteSchema,
|
|
182
209
|
baseSqliteStorage,
|
|
@@ -195,33 +222,26 @@ export const startDbWorker =
|
|
|
195
222
|
|
|
196
223
|
const storage = createClientStorage({ ...dbDeps, clock })({
|
|
197
224
|
onError: (error) => {
|
|
198
|
-
|
|
225
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
226
|
+
type: "Error",
|
|
227
|
+
error,
|
|
228
|
+
});
|
|
199
229
|
},
|
|
200
230
|
});
|
|
201
231
|
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
const runWithStorage = run.addDeps({ storage });
|
|
232
|
+
const disposables = disposer.move();
|
|
233
|
+
const dbWorkerRunWithStorage = dbWorkerRun.addDeps({ storage });
|
|
205
234
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
* memory pressure shows up; even millions of short callback IDs are
|
|
213
|
-
* acceptable here, and naive eviction could let a still-retried request run
|
|
214
|
-
* again.
|
|
215
|
-
*/
|
|
216
|
-
const processedRequestIds = new Set<Id>();
|
|
235
|
+
port.onMessage = (input) => {
|
|
236
|
+
if (input.type === "Dispose") {
|
|
237
|
+
if (!disposables.disposed) console.debug("disposeDbWorker");
|
|
238
|
+
void disposables.disposeAsync();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
217
241
|
|
|
218
|
-
|
|
219
|
-
if (processedRequestIds.has(callbackId)) return;
|
|
220
|
-
processedRequestIds.add(callbackId);
|
|
242
|
+
const { callbackId, request } = input;
|
|
221
243
|
|
|
222
|
-
const postQueuedResponse = (
|
|
223
|
-
response: ExtractType<DbWorkerOutput, "OnQueuedResponse">["response"],
|
|
224
|
-
): void => {
|
|
244
|
+
const postQueuedResponse = (response: DbWorkerQueuedResponse): void => {
|
|
225
245
|
port.postMessage(
|
|
226
246
|
{ type: "OnQueuedResponse", callbackId, response },
|
|
227
247
|
response.type === "ForEvolu" && response.message.type === "Export"
|
|
@@ -238,11 +258,14 @@ export const startDbWorker =
|
|
|
238
258
|
request.message,
|
|
239
259
|
);
|
|
240
260
|
if (!result.ok) {
|
|
241
|
-
|
|
261
|
+
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
262
|
+
type: "Error",
|
|
263
|
+
error: result.error,
|
|
264
|
+
});
|
|
242
265
|
} else {
|
|
243
266
|
postQueuedResponse({
|
|
244
267
|
type: "ForEvolu",
|
|
245
|
-
|
|
268
|
+
id: request.id,
|
|
246
269
|
message: result.value,
|
|
247
270
|
});
|
|
248
271
|
}
|
|
@@ -252,7 +275,7 @@ export const startDbWorker =
|
|
|
252
275
|
case "Query":
|
|
253
276
|
postQueuedResponse({
|
|
254
277
|
type: "ForEvolu",
|
|
255
|
-
|
|
278
|
+
id: request.id,
|
|
256
279
|
message: {
|
|
257
280
|
type: "Query",
|
|
258
281
|
rowsByQuery: loadQueries(dbDeps)(request.message.queries),
|
|
@@ -263,7 +286,7 @@ export const startDbWorker =
|
|
|
263
286
|
case "Export":
|
|
264
287
|
postQueuedResponse({
|
|
265
288
|
type: "ForEvolu",
|
|
266
|
-
|
|
289
|
+
id: request.id,
|
|
267
290
|
message: {
|
|
268
291
|
type: "Export",
|
|
269
292
|
file: dbDeps.sqlite.export(),
|
|
@@ -307,7 +330,7 @@ export const startDbWorker =
|
|
|
307
330
|
case "ApplySyncMessage": {
|
|
308
331
|
const { owner, inputMessage } = request.message;
|
|
309
332
|
|
|
310
|
-
|
|
333
|
+
dbWorkerRunWithStorage<void, never>(async (run) => {
|
|
311
334
|
storage.setRequestContext(owner.encryptionKey);
|
|
312
335
|
|
|
313
336
|
const result = await run(
|
|
@@ -777,7 +800,7 @@ const handleMutation =
|
|
|
777
800
|
) =>
|
|
778
801
|
(
|
|
779
802
|
message: ExtractType<
|
|
780
|
-
ExtractType<
|
|
803
|
+
ExtractType<DbWorkerRequest, "ForEvolu">["message"],
|
|
781
804
|
"Mutate"
|
|
782
805
|
>,
|
|
783
806
|
): Result<
|