@evolu/common 8.0.0-next.3 → 8.0.0-next.5
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 +10 -5
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +10 -5
- package/dist/src/Error.d.ts +5 -3
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +16 -0
- 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 +66 -41
- package/dist/src/Task.d.ts +99 -123
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +273 -256
- package/dist/src/Test.d.ts +14 -15
- package/dist/src/Test.d.ts.map +1 -1
- 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/Types.d.ts +0 -8
- package/dist/src/Types.d.ts.map +1 -1
- 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 +292 -112
- 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 +43 -37
- 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 +390 -376
- package/package.json +1 -1
- package/src/Array.ts +8 -8
- package/src/Assert.ts +11 -6
- package/src/Error.ts +5 -4
- 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 +43 -31
- package/src/Task.ts +392 -430
- package/src/Test.ts +14 -15
- package/src/Time.ts +7 -0
- package/src/Type.ts +9 -0
- package/src/Types.ts +0 -9
- package/src/WebSocket.ts +7 -6
- package/src/Worker.ts +195 -53
- package/src/index.ts +1 -0
- package/src/local-first/Db.ts +86 -61
- 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 +485 -476
package/src/Test.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type Random,
|
|
18
18
|
type RandomLibDep,
|
|
19
19
|
} from "./Random.js";
|
|
20
|
-
import { createRun, type Run, type
|
|
20
|
+
import { createRun, type Run, type RunDefaultDeps } from "./Task.js";
|
|
21
21
|
import {
|
|
22
22
|
minMillis,
|
|
23
23
|
setTimeout,
|
|
@@ -35,14 +35,14 @@ export type TestCreateId = <B extends string = never>() => [B] extends [never]
|
|
|
35
35
|
/**
|
|
36
36
|
* Cheap deterministic baseline deps for tests.
|
|
37
37
|
*
|
|
38
|
-
* `TestDeps` includes test-friendly replacements for {@link
|
|
39
|
-
* {@link TestConsole} and {@link TestTime}, plus extra helpers commonly
|
|
40
|
-
* tests and fixtures, such as `randomLib` (only that for now).
|
|
38
|
+
* `TestDeps` includes test-friendly replacements for {@link RunDefaultDeps},
|
|
39
|
+
* such as {@link TestConsole} and {@link TestTime}, plus extra helpers commonly
|
|
40
|
+
* useful in tests and fixtures, such as `randomLib` (only that for now).
|
|
41
41
|
*
|
|
42
42
|
* Use it directly for synchronous test setup, fixtures, and helpers. It is also
|
|
43
43
|
* intentionally the base deps used by {@link testCreateRun}.
|
|
44
44
|
*/
|
|
45
|
-
export type TestDeps = Omit<
|
|
45
|
+
export type TestDeps = Omit<RunDefaultDeps, "console" | "time"> &
|
|
46
46
|
TestConsoleDep &
|
|
47
47
|
TestTimeDep &
|
|
48
48
|
RandomLibDep;
|
|
@@ -103,7 +103,7 @@ export const testCreateDeps = (options?: {
|
|
|
103
103
|
* ### Example
|
|
104
104
|
*
|
|
105
105
|
* ```ts
|
|
106
|
-
* //
|
|
106
|
+
* // Default TestTime
|
|
107
107
|
* await using run = testCreateRun();
|
|
108
108
|
* const fiber = run(sleep("1s"));
|
|
109
109
|
* run.deps.time.advance("1s");
|
|
@@ -112,24 +112,23 @@ export const testCreateDeps = (options?: {
|
|
|
112
112
|
* // For a single test, create the dependency using the Run.
|
|
113
113
|
* await using run = testCreateRun();
|
|
114
114
|
* await using foo = await run.orThrow(createFoo());
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* expect(runWithFoo.deps.foo).toBe(foo);
|
|
115
|
+
* await run.orThrow(taskThatNeedsFoo, { foo });
|
|
118
116
|
*
|
|
119
117
|
* // If multiple tests need the same setup, create a disposable helper.
|
|
120
118
|
* // Then `await using setup` disposes everything the helper owns.
|
|
121
119
|
* const setupFoo = async () => {
|
|
122
|
-
* await using
|
|
123
|
-
* const run =
|
|
124
|
-
* const foo =
|
|
125
|
-
* const
|
|
120
|
+
* await using disposer = new AsyncDisposableStack();
|
|
121
|
+
* const run = disposer.use(testCreateRun());
|
|
122
|
+
* const foo = disposer.use(await run.orThrow(createFoo()));
|
|
123
|
+
* const runWithFoo = disposer.use(run.create({ ...run.deps, foo }));
|
|
124
|
+
* const disposables = disposer.move();
|
|
126
125
|
*
|
|
127
126
|
* // Return whatever the tests need: a Run with the dependency,
|
|
128
127
|
* // the dependency itself, or both.
|
|
129
128
|
* return {
|
|
130
|
-
* run:
|
|
129
|
+
* run: runWithFoo,
|
|
131
130
|
* foo,
|
|
132
|
-
* [Symbol.asyncDispose]: () =>
|
|
131
|
+
* [Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
133
132
|
* };
|
|
134
133
|
* };
|
|
135
134
|
*
|
package/src/Time.ts
CHANGED
|
@@ -177,6 +177,13 @@ export const minMillis = 0 as Millis;
|
|
|
177
177
|
/** Maximum {@link Millis} value. */
|
|
178
178
|
export const maxMillis = (maxMillisWithInfinity - 1) as Millis;
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Converts a number to {@link Millis}, rounding to the nearest millisecond and
|
|
182
|
+
* saturating overflow at {@link maxMillis}.
|
|
183
|
+
*/
|
|
184
|
+
export const saturateMillis = (value: number): Millis =>
|
|
185
|
+
Millis.orNull(Math.max(0, Math.round(value))) ?? maxMillis;
|
|
186
|
+
|
|
180
187
|
/**
|
|
181
188
|
* Converts {@link Millis} to {@link DateIso}.
|
|
182
189
|
*
|
package/src/Type.ts
CHANGED
|
@@ -2334,6 +2334,14 @@ export const formatFiniteError =
|
|
|
2334
2334
|
export const FiniteNumber = /*#__PURE__*/ finite(Number);
|
|
2335
2335
|
export type FiniteNumber = typeof FiniteNumber.Type;
|
|
2336
2336
|
|
|
2337
|
+
/**
|
|
2338
|
+
* Non-negative finite number.
|
|
2339
|
+
*
|
|
2340
|
+
* @group Number
|
|
2341
|
+
*/
|
|
2342
|
+
export const NonNegativeFiniteNumber = /*#__PURE__*/ nonNegative(FiniteNumber);
|
|
2343
|
+
export type NonNegativeFiniteNumber = typeof NonNegativeFiniteNumber.Type;
|
|
2344
|
+
|
|
2337
2345
|
/**
|
|
2338
2346
|
* Number that is a multiple of a divisor.
|
|
2339
2347
|
*
|
|
@@ -4651,6 +4659,7 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
4651
4659
|
|
|
4652
4660
|
if (extraMessage != null) return extraMessage;
|
|
4653
4661
|
|
|
4662
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
4654
4663
|
error = error as TypeErrors;
|
|
4655
4664
|
|
|
4656
4665
|
switch (error.type) {
|
package/src/Types.ts
CHANGED
|
@@ -359,12 +359,3 @@ export type ExtractType<
|
|
|
359
359
|
TUnion extends { readonly type: TypeName },
|
|
360
360
|
TType extends TUnion["type"],
|
|
361
361
|
> = Extract<TUnion, { readonly type: TType }>;
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Constrains `T` to only contain keys not present in `Existing`.
|
|
365
|
-
*
|
|
366
|
-
* Use as a generic constraint to prevent overriding existing properties.
|
|
367
|
-
*/
|
|
368
|
-
export type NewKeys<T, Existing> = {
|
|
369
|
-
[K in keyof T]: K extends keyof Existing ? never : T[K];
|
|
370
|
-
};
|
package/src/WebSocket.ts
CHANGED
|
@@ -200,7 +200,7 @@ export const createWebSocket: CreateWebSocket =
|
|
|
200
200
|
} = {},
|
|
201
201
|
) =>
|
|
202
202
|
async (run) => {
|
|
203
|
-
await using
|
|
203
|
+
await using disposer = new AsyncDisposableStack();
|
|
204
204
|
|
|
205
205
|
let socket: globalThis.WebSocket | null = null;
|
|
206
206
|
|
|
@@ -268,7 +268,7 @@ export const createWebSocket: CreateWebSocket =
|
|
|
268
268
|
return closeSocket;
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
-
const retryFiber =
|
|
271
|
+
const retryFiber = disposer.use(run.daemon(retry(connect, schedule)));
|
|
272
272
|
|
|
273
273
|
// Report RetryError (schedule exhausted) via onError callback
|
|
274
274
|
void retryFiber.then((result) => {
|
|
@@ -277,7 +277,7 @@ export const createWebSocket: CreateWebSocket =
|
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
const
|
|
280
|
+
const disposables = disposer.move();
|
|
281
281
|
|
|
282
282
|
return ok<WebSocket>({
|
|
283
283
|
send: (data) => {
|
|
@@ -290,14 +290,15 @@ export const createWebSocket: CreateWebSocket =
|
|
|
290
290
|
},
|
|
291
291
|
|
|
292
292
|
getReadyState: () => {
|
|
293
|
-
if (
|
|
293
|
+
if (disposables.disposed) return "closed";
|
|
294
294
|
return socket ? nativeToStringState[socket.readyState] : "connecting";
|
|
295
295
|
},
|
|
296
296
|
|
|
297
297
|
isOpen: () =>
|
|
298
|
-
!
|
|
298
|
+
!disposables.disposed &&
|
|
299
|
+
socket?.readyState === globalThis.WebSocket.OPEN,
|
|
299
300
|
|
|
300
|
-
[Symbol.asyncDispose]: () =>
|
|
301
|
+
[Symbol.asyncDispose]: () => disposables.disposeAsync(),
|
|
301
302
|
});
|
|
302
303
|
};
|
|
303
304
|
|
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,17 +275,17 @@ export const createMessageChannel: CreateMessageChannel = <
|
|
|
250
275
|
handler: null,
|
|
251
276
|
queue: [],
|
|
252
277
|
flushScheduled: false,
|
|
253
|
-
|
|
278
|
+
flushTimeoutId: null,
|
|
279
|
+
ownerCount: 0,
|
|
254
280
|
pendingTransferCount: 0,
|
|
255
|
-
closed: false,
|
|
256
281
|
};
|
|
257
282
|
const state2: PortState<Input> = {
|
|
258
283
|
handler: null,
|
|
259
284
|
queue: [],
|
|
260
285
|
flushScheduled: false,
|
|
261
|
-
|
|
286
|
+
flushTimeoutId: null,
|
|
287
|
+
ownerCount: 0,
|
|
262
288
|
pendingTransferCount: 0,
|
|
263
|
-
closed: false,
|
|
264
289
|
};
|
|
265
290
|
|
|
266
291
|
const native1 = createNativeMessagePortToken<Input, Output>();
|
|
@@ -277,8 +302,11 @@ export const createMessageChannel: CreateMessageChannel = <
|
|
|
277
302
|
native: native2,
|
|
278
303
|
};
|
|
279
304
|
|
|
280
|
-
|
|
281
|
-
|
|
305
|
+
using disposer = new DisposableStack();
|
|
306
|
+
|
|
307
|
+
const port1 = disposer.use(createTestPort(port1Registration));
|
|
308
|
+
const port2 = disposer.use(createTestPort(port2Registration));
|
|
309
|
+
const disposables = disposer.move();
|
|
282
310
|
|
|
283
311
|
nativePortRegistry.set(native1, port1Registration);
|
|
284
312
|
nativePortRegistry.set(native2, port2Registration);
|
|
@@ -286,10 +314,7 @@ export const createMessageChannel: CreateMessageChannel = <
|
|
|
286
314
|
return {
|
|
287
315
|
port1,
|
|
288
316
|
port2,
|
|
289
|
-
[Symbol.dispose]: () =>
|
|
290
|
-
port1[Symbol.dispose]();
|
|
291
|
-
port2[Symbol.dispose]();
|
|
292
|
-
},
|
|
317
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
293
318
|
};
|
|
294
319
|
};
|
|
295
320
|
|
|
@@ -307,6 +332,75 @@ export const createMessagePort: CreateMessagePort = <Input, Output = never>(
|
|
|
307
332
|
return createTestPort(registration as PortRegistration<Input, Output>);
|
|
308
333
|
};
|
|
309
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
|
+
};
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
type BroadcastChannelDispatch<T> = (message: T) => void;
|
|
398
|
+
|
|
399
|
+
const broadcastChannelDispatchesByName = new Map<
|
|
400
|
+
string,
|
|
401
|
+
Set<BroadcastChannelDispatch<any>>
|
|
402
|
+
>();
|
|
403
|
+
|
|
310
404
|
/**
|
|
311
405
|
* Test {@link Worker} with access to its paired worker-side `self`.
|
|
312
406
|
*
|
|
@@ -342,6 +436,14 @@ export interface TestMessageChannel<
|
|
|
342
436
|
readonly isDisposed: () => boolean;
|
|
343
437
|
}
|
|
344
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
|
+
|
|
345
447
|
/**
|
|
346
448
|
* Creates a connected {@link TestWorker} for testing.
|
|
347
449
|
*
|
|
@@ -358,7 +460,7 @@ export const testCreateWorker = <Input, Output = never>(): TestWorker<
|
|
|
358
460
|
self = nextSelf;
|
|
359
461
|
});
|
|
360
462
|
|
|
361
|
-
return Object.assign(worker, { self })
|
|
463
|
+
return Object.assign(worker, { self });
|
|
362
464
|
};
|
|
363
465
|
|
|
364
466
|
/**
|
|
@@ -378,7 +480,7 @@ export const testCreateSharedWorker = <
|
|
|
378
480
|
return Object.assign(worker, {
|
|
379
481
|
self,
|
|
380
482
|
connect,
|
|
381
|
-
})
|
|
483
|
+
});
|
|
382
484
|
};
|
|
383
485
|
|
|
384
486
|
/**
|
|
@@ -394,18 +496,16 @@ export const testCreateMessageChannel = <
|
|
|
394
496
|
Input,
|
|
395
497
|
Output = never,
|
|
396
498
|
>(): TestMessageChannel<Input, Output> => {
|
|
397
|
-
|
|
499
|
+
using disposer = new DisposableStack();
|
|
398
500
|
|
|
399
|
-
|
|
501
|
+
const channel = disposer.use(createMessageChannel<Input, Output>());
|
|
502
|
+
const disposables = disposer.move();
|
|
400
503
|
|
|
401
504
|
return {
|
|
402
505
|
port1: channel.port1,
|
|
403
506
|
port2: channel.port2,
|
|
404
|
-
isDisposed: () => disposed,
|
|
405
|
-
[Symbol.dispose]: () =>
|
|
406
|
-
disposed = true;
|
|
407
|
-
channel[Symbol.dispose]();
|
|
408
|
-
},
|
|
507
|
+
isDisposed: () => disposables.disposed,
|
|
508
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
409
509
|
};
|
|
410
510
|
};
|
|
411
511
|
|
|
@@ -414,11 +514,35 @@ export const testCreateMessagePort: CreateMessagePort = <Input, Output = never>(
|
|
|
414
514
|
nativePort: NativeMessagePort<Input, Output>,
|
|
415
515
|
): MessagePort<Input, Output> => createMessagePort(nativePort);
|
|
416
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
|
+
|
|
417
539
|
/**
|
|
418
|
-
* Waits
|
|
540
|
+
* Waits the minimal time necessary for in-memory worker message delivery in
|
|
541
|
+
* tests.
|
|
419
542
|
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
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.
|
|
422
546
|
*/
|
|
423
547
|
export const testWaitForWorkerMessage = async (): Promise<void> => {
|
|
424
548
|
await testWaitForMacrotask();
|
|
@@ -498,9 +622,9 @@ interface PortState<T> {
|
|
|
498
622
|
handler: ((message: T) => void) | null;
|
|
499
623
|
readonly queue: Array<T>;
|
|
500
624
|
flushScheduled: boolean;
|
|
501
|
-
|
|
625
|
+
flushTimeoutId: ReturnType<typeof globalThis.setTimeout> | null;
|
|
626
|
+
ownerCount: number;
|
|
502
627
|
pendingTransferCount: number;
|
|
503
|
-
closed: boolean;
|
|
504
628
|
}
|
|
505
629
|
|
|
506
630
|
interface PortRegistration<Input, Output> {
|
|
@@ -514,23 +638,46 @@ const createPort = <Input, Output>({
|
|
|
514
638
|
peerReceive,
|
|
515
639
|
native,
|
|
516
640
|
}: PortRegistration<Input, Output>): MessagePort<Input, Output> => {
|
|
641
|
+
using disposer = new DisposableStack();
|
|
642
|
+
|
|
517
643
|
if (receive.pendingTransferCount > 0) receive.pendingTransferCount -= 1;
|
|
518
|
-
receive.
|
|
519
|
-
|
|
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();
|
|
520
663
|
|
|
521
664
|
const scheduleFlush = (state: PortState<any>): void => {
|
|
522
|
-
if (state.flushScheduled || state.
|
|
665
|
+
if (state.flushScheduled || state.ownerCount === 0) return;
|
|
523
666
|
state.flushScheduled = true;
|
|
524
667
|
|
|
525
668
|
// Native worker messages are task-queued; use macrotask timing.
|
|
526
|
-
setTimeout(() => {
|
|
669
|
+
state.flushTimeoutId = globalThis.setTimeout(() => {
|
|
670
|
+
state.flushTimeoutId = null;
|
|
527
671
|
state.flushScheduled = false;
|
|
528
|
-
if (state.
|
|
672
|
+
if (state.ownerCount === 0) return;
|
|
529
673
|
|
|
530
|
-
const
|
|
531
|
-
if (!
|
|
674
|
+
const firstHandler = state.handler;
|
|
675
|
+
if (!firstHandler) return;
|
|
532
676
|
|
|
533
677
|
for (const message of state.queue.splice(0)) {
|
|
678
|
+
const handler = state.handler;
|
|
679
|
+
if (!handler) return;
|
|
680
|
+
|
|
534
681
|
handler(message);
|
|
535
682
|
}
|
|
536
683
|
}, 0);
|
|
@@ -538,41 +685,36 @@ const createPort = <Input, Output>({
|
|
|
538
685
|
|
|
539
686
|
return {
|
|
540
687
|
postMessage: (message, transfer) => {
|
|
688
|
+
if (
|
|
689
|
+
disposables.disposed ||
|
|
690
|
+
receive.ownerCount === 0 ||
|
|
691
|
+
peerReceive.ownerCount === 0
|
|
692
|
+
)
|
|
693
|
+
return;
|
|
694
|
+
|
|
541
695
|
for (const transferable of transfer ?? []) {
|
|
542
696
|
if (transferable instanceof globalThis.ArrayBuffer) continue;
|
|
543
697
|
|
|
544
698
|
const registration = nativePortRegistry.get(transferable);
|
|
545
699
|
if (!registration) continue;
|
|
546
700
|
|
|
701
|
+
registration.receive.ownerCount += 1;
|
|
547
702
|
registration.receive.pendingTransferCount += 1;
|
|
548
|
-
registration.receive.closed = false;
|
|
549
703
|
}
|
|
550
704
|
|
|
551
|
-
if (peerReceive.closed) return;
|
|
552
705
|
peerReceive.queue.push(message);
|
|
553
706
|
scheduleFlush(peerReceive);
|
|
554
707
|
},
|
|
555
708
|
get onMessage() {
|
|
556
|
-
return
|
|
709
|
+
return disposables.disposed ? null : receive.handler;
|
|
557
710
|
},
|
|
558
711
|
set onMessage(fn) {
|
|
559
|
-
if (
|
|
712
|
+
if (disposables.disposed || receive.ownerCount === 0) return;
|
|
560
713
|
receive.handler = fn;
|
|
561
714
|
if (fn) scheduleFlush(receive);
|
|
562
715
|
},
|
|
563
716
|
native,
|
|
564
|
-
[Symbol.dispose]: () =>
|
|
565
|
-
if (isDisposed) return;
|
|
566
|
-
isDisposed = true;
|
|
567
|
-
|
|
568
|
-
receive.refCount -= 1;
|
|
569
|
-
if (receive.refCount > 0 || receive.pendingTransferCount > 0) return;
|
|
570
|
-
|
|
571
|
-
receive.closed = true;
|
|
572
|
-
receive.handler = null;
|
|
573
|
-
receive.flushScheduled = false;
|
|
574
|
-
receive.queue.length = 0;
|
|
575
|
-
},
|
|
717
|
+
[Symbol.dispose]: () => disposables.dispose(),
|
|
576
718
|
};
|
|
577
719
|
};
|
|
578
720
|
|
package/src/index.ts
CHANGED