@b9g/platform 0.1.18 → 0.1.19
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/package.json +1 -1
- package/src/globals.d.ts +1 -4
- package/src/index.d.ts +1 -0
- package/src/index.js +10 -0
- package/src/internal/broadcast-channel-backend.d.ts +15 -0
- package/src/internal/broadcast-channel.d.ts +36 -0
- package/src/runtime.d.ts +8 -2
- package/src/runtime.js +133 -1
package/package.json
CHANGED
package/src/globals.d.ts
CHANGED
|
@@ -124,10 +124,7 @@ declare global {
|
|
|
124
124
|
interface Window {
|
|
125
125
|
addEventListener<K extends keyof WorkerGlobalScopeEventMap>(
|
|
126
126
|
type: K,
|
|
127
|
-
listener: (
|
|
128
|
-
this: Window,
|
|
129
|
-
ev: WorkerGlobalScopeEventMap[K],
|
|
130
|
-
) => any,
|
|
127
|
+
listener: (this: Window, ev: WorkerGlobalScopeEventMap[K]) => any,
|
|
131
128
|
options?: boolean | AddEventListenerOptions,
|
|
132
129
|
): void;
|
|
133
130
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -238,3 +238,4 @@ export declare class ServiceWorkerPool {
|
|
|
238
238
|
export { CustomLoggerStorage, type LoggerStorage };
|
|
239
239
|
export type { LoggerFactory } from "./runtime.js";
|
|
240
240
|
export { CustomDatabaseStorage, createDatabaseFactory, type DatabaseStorage, type DatabaseConfig, type DatabaseFactory, type DatabaseUpgradeEvent, } from "./runtime.js";
|
|
241
|
+
export type { BroadcastChannelBackend } from "./internal/broadcast-channel-backend.js";
|
package/src/index.js
CHANGED
|
@@ -230,6 +230,16 @@ var ServiceWorkerPool = class {
|
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
+
} else if (message.type === "broadcast:post") {
|
|
234
|
+
for (const w of this.#workers) {
|
|
235
|
+
if (w !== worker) {
|
|
236
|
+
w.postMessage({
|
|
237
|
+
type: "broadcast:deliver",
|
|
238
|
+
channel: message.channel,
|
|
239
|
+
data: message.data
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
233
243
|
}
|
|
234
244
|
break;
|
|
235
245
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BroadcastChannel Backend Interface
|
|
3
|
+
*
|
|
4
|
+
* Pluggable backend for cross-process/cross-machine BroadcastChannel relay.
|
|
5
|
+
* When configured, replaces the postMessage relay (the backend handles both
|
|
6
|
+
* cross-worker and cross-process pub/sub).
|
|
7
|
+
*/
|
|
8
|
+
export interface BroadcastChannelBackend {
|
|
9
|
+
/** Publish a message to a channel (called when local BC posts) */
|
|
10
|
+
publish(channelName: string, data: unknown): void;
|
|
11
|
+
/** Subscribe to a channel (called when first BC instance for a name is created) */
|
|
12
|
+
subscribe(channelName: string, callback: (data: unknown) => void): () => void;
|
|
13
|
+
/** Cleanup connections */
|
|
14
|
+
dispose(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/// <reference path="../globals.d.ts" />
|
|
2
|
+
/// <reference path="../shovel-config.d.ts" />
|
|
3
|
+
/**
|
|
4
|
+
* BroadcastChannel - WHATWG standard for cross-context pub/sub
|
|
5
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
|
|
6
|
+
*
|
|
7
|
+
* In-memory implementation with cross-worker relay and pluggable backend support.
|
|
8
|
+
* - Local fan-out: messages delivered to all same-name channels in this process
|
|
9
|
+
* - Relay: messages forwarded to other workers via postMessage (set by startWorkerMessageLoop)
|
|
10
|
+
* - Backend: messages published to external pub/sub (e.g. Redis) for cross-process delivery
|
|
11
|
+
*/
|
|
12
|
+
import type { BroadcastChannelBackend } from "./broadcast-channel-backend.js";
|
|
13
|
+
/**
|
|
14
|
+
* Set the relay function for cross-worker message forwarding.
|
|
15
|
+
* Called by startWorkerMessageLoop or Bun worker template.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setBroadcastChannelRelay(fn: (channelName: string, data: unknown) => void): void;
|
|
18
|
+
/**
|
|
19
|
+
* Deliver a message from relay/backend to all local instances on a channel.
|
|
20
|
+
* Does NOT re-relay — prevents infinite loops.
|
|
21
|
+
*/
|
|
22
|
+
export declare function deliverBroadcastMessage(channelName: string, data: unknown): void;
|
|
23
|
+
/**
|
|
24
|
+
* Set a pluggable backend for cross-process BroadcastChannel relay.
|
|
25
|
+
* When set, publish goes through the backend instead of postMessage relay.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setBroadcastChannelBackend(b: BroadcastChannelBackend): void;
|
|
28
|
+
export declare class ShovelBroadcastChannel extends EventTarget {
|
|
29
|
+
#private;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
onmessage: ((ev: MessageEvent) => any) | null;
|
|
32
|
+
onmessageerror: ((ev: MessageEvent) => any) | null;
|
|
33
|
+
constructor(name: string);
|
|
34
|
+
postMessage(message: unknown): void;
|
|
35
|
+
close(): void;
|
|
36
|
+
}
|
package/src/runtime.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
15
15
|
import { CustomCacheStorage, Cache } from "@b9g/cache";
|
|
16
|
+
import type { BroadcastChannelBackend } from "./internal/broadcast-channel-backend.js";
|
|
16
17
|
declare global {
|
|
17
18
|
interface CookieListItem {
|
|
18
19
|
domain?: string;
|
|
@@ -626,7 +627,7 @@ export declare class DedicatedWorkerGlobalScope extends WorkerGlobalScope {
|
|
|
626
627
|
*
|
|
627
628
|
* Use restore() to revert all patches (useful for testing).
|
|
628
629
|
*/
|
|
629
|
-
export declare class ServiceWorkerGlobals
|
|
630
|
+
export declare class ServiceWorkerGlobals {
|
|
630
631
|
#private;
|
|
631
632
|
readonly self: any;
|
|
632
633
|
readonly registration: ServiceWorkerRegistration;
|
|
@@ -725,6 +726,10 @@ export interface ShovelConfig {
|
|
|
725
726
|
caches?: Record<string, CacheConfig>;
|
|
726
727
|
directories?: Record<string, DirectoryConfig>;
|
|
727
728
|
databases?: Record<string, DatabaseConfig>;
|
|
729
|
+
broadcastChannel?: {
|
|
730
|
+
impl?: (new (options: Record<string, unknown>) => BroadcastChannelBackend) | ((options: Record<string, unknown>) => BroadcastChannelBackend);
|
|
731
|
+
[key: string]: unknown;
|
|
732
|
+
};
|
|
728
733
|
}
|
|
729
734
|
/**
|
|
730
735
|
* Creates a directory factory function for CustomDirectoryStorage.
|
|
@@ -882,4 +887,5 @@ export interface ProcessedLoggingConfig {
|
|
|
882
887
|
* @param loggingConfig - The logging configuration
|
|
883
888
|
*/
|
|
884
889
|
export declare function configureLogging(loggingConfig: LoggingConfig): Promise<void>;
|
|
885
|
-
export {};
|
|
890
|
+
export { ShovelBroadcastChannel, setBroadcastChannelRelay, deliverBroadcastMessage, setBroadcastChannelBackend, } from "./internal/broadcast-channel.js";
|
|
891
|
+
export type { BroadcastChannelBackend } from "./internal/broadcast-channel-backend.js";
|
package/src/runtime.js
CHANGED
|
@@ -5,6 +5,117 @@ import { getLogger, getConsoleSink } from "@logtape/logtape";
|
|
|
5
5
|
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
6
6
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
7
7
|
import { handleCacheResponse, PostMessageCache } from "@b9g/cache/postmessage";
|
|
8
|
+
|
|
9
|
+
// src/internal/broadcast-channel.ts
|
|
10
|
+
var channels = /* @__PURE__ */ new Map();
|
|
11
|
+
var relayFn = null;
|
|
12
|
+
var backend = null;
|
|
13
|
+
var backendSubscriptions = /* @__PURE__ */ new Map();
|
|
14
|
+
function setBroadcastChannelRelay(fn) {
|
|
15
|
+
relayFn = fn;
|
|
16
|
+
}
|
|
17
|
+
function deliverBroadcastMessage(channelName, data) {
|
|
18
|
+
const set = channels.get(channelName);
|
|
19
|
+
if (!set)
|
|
20
|
+
return;
|
|
21
|
+
for (const ch of set) {
|
|
22
|
+
queueMicrotask(() => {
|
|
23
|
+
const cloned = structuredClone(data);
|
|
24
|
+
const event = new MessageEvent("message", { data: cloned });
|
|
25
|
+
ch.dispatchEvent(event);
|
|
26
|
+
ch.onmessage?.call(ch, event);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function setBroadcastChannelBackend(b) {
|
|
31
|
+
backend = b;
|
|
32
|
+
}
|
|
33
|
+
var ShovelBroadcastChannel = class extends EventTarget {
|
|
34
|
+
name;
|
|
35
|
+
#closed;
|
|
36
|
+
// Event handler properties (Web API compat)
|
|
37
|
+
onmessage;
|
|
38
|
+
onmessageerror;
|
|
39
|
+
constructor(name) {
|
|
40
|
+
super();
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.#closed = false;
|
|
43
|
+
this.onmessage = null;
|
|
44
|
+
this.onmessageerror = null;
|
|
45
|
+
let set = channels.get(name);
|
|
46
|
+
if (!set) {
|
|
47
|
+
set = /* @__PURE__ */ new Set();
|
|
48
|
+
channels.set(name, set);
|
|
49
|
+
}
|
|
50
|
+
set.add(this);
|
|
51
|
+
if (backend && !backendSubscriptions.has(name)) {
|
|
52
|
+
const unsub = backend.subscribe(name, (data) => {
|
|
53
|
+
deliverBroadcastMessage(name, data);
|
|
54
|
+
});
|
|
55
|
+
backendSubscriptions.set(name, unsub);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
postMessage(message) {
|
|
59
|
+
if (this.#closed) {
|
|
60
|
+
throw new DOMException("BroadcastChannel is closed", "InvalidStateError");
|
|
61
|
+
}
|
|
62
|
+
let data;
|
|
63
|
+
try {
|
|
64
|
+
data = structuredClone(message);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const set2 = channels.get(this.name);
|
|
67
|
+
if (!set2)
|
|
68
|
+
return;
|
|
69
|
+
for (const ch of set2) {
|
|
70
|
+
if (ch !== this && !ch.#closed) {
|
|
71
|
+
queueMicrotask(() => {
|
|
72
|
+
const event = new MessageEvent("messageerror");
|
|
73
|
+
ch.dispatchEvent(event);
|
|
74
|
+
ch.onmessageerror?.call(ch, event);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const set = channels.get(this.name);
|
|
81
|
+
if (set) {
|
|
82
|
+
for (const ch of set) {
|
|
83
|
+
if (ch !== this && !ch.#closed) {
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
const cloned = structuredClone(data);
|
|
86
|
+
const event = new MessageEvent("message", { data: cloned });
|
|
87
|
+
ch.dispatchEvent(event);
|
|
88
|
+
ch.onmessage?.call(ch, event);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (backend) {
|
|
94
|
+
backend.publish(this.name, data);
|
|
95
|
+
} else if (relayFn) {
|
|
96
|
+
relayFn(this.name, data);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
close() {
|
|
100
|
+
if (this.#closed)
|
|
101
|
+
return;
|
|
102
|
+
this.#closed = true;
|
|
103
|
+
const set = channels.get(this.name);
|
|
104
|
+
if (set) {
|
|
105
|
+
set.delete(this);
|
|
106
|
+
if (set.size === 0) {
|
|
107
|
+
channels.delete(this.name);
|
|
108
|
+
const unsub = backendSubscriptions.get(this.name);
|
|
109
|
+
if (unsub) {
|
|
110
|
+
unsub();
|
|
111
|
+
backendSubscriptions.delete(this.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/runtime.ts
|
|
8
119
|
import {
|
|
9
120
|
configure
|
|
10
121
|
} from "@logtape/logtape";
|
|
@@ -368,7 +479,8 @@ var PATCHED_KEYS = [
|
|
|
368
479
|
"dispatchEvent",
|
|
369
480
|
"WorkerGlobalScope",
|
|
370
481
|
"DedicatedWorkerGlobalScope",
|
|
371
|
-
"cookieStore"
|
|
482
|
+
"cookieStore",
|
|
483
|
+
"BroadcastChannel"
|
|
372
484
|
];
|
|
373
485
|
function promiseWithTimeout(promise, timeoutMs, errorMessage) {
|
|
374
486
|
return Promise.race([
|
|
@@ -1202,6 +1314,7 @@ var ServiceWorkerGlobals = class {
|
|
|
1202
1314
|
get: () => cookieStoreStorage.get(),
|
|
1203
1315
|
configurable: true
|
|
1204
1316
|
});
|
|
1317
|
+
g.BroadcastChannel = ShovelBroadcastChannel;
|
|
1205
1318
|
}
|
|
1206
1319
|
/**
|
|
1207
1320
|
* Restore original globals (for testing)
|
|
@@ -1313,6 +1426,14 @@ async function initWorkerRuntime(options) {
|
|
|
1313
1426
|
loggers
|
|
1314
1427
|
});
|
|
1315
1428
|
scope.install();
|
|
1429
|
+
if (config?.broadcastChannel?.impl) {
|
|
1430
|
+
const { impl, ...bcOptions } = config.broadcastChannel;
|
|
1431
|
+
const opts = bcOptions;
|
|
1432
|
+
const bcBackend = isClass(impl) ? new impl(opts) : impl(
|
|
1433
|
+
opts
|
|
1434
|
+
);
|
|
1435
|
+
setBroadcastChannelBackend(bcBackend);
|
|
1436
|
+
}
|
|
1316
1437
|
runtimeLogger.debug("Worker runtime initialized");
|
|
1317
1438
|
return { registration, scope, caches, directories, databases, loggers };
|
|
1318
1439
|
}
|
|
@@ -1383,6 +1504,10 @@ function startWorkerMessageLoop(options) {
|
|
|
1383
1504
|
});
|
|
1384
1505
|
return;
|
|
1385
1506
|
}
|
|
1507
|
+
if (message?.type === "broadcast:deliver") {
|
|
1508
|
+
deliverBroadcastMessage(message.channel, message.data);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1386
1511
|
if (message?.type === "shutdown") {
|
|
1387
1512
|
messageLogger.debug(`[Worker-${workerId}] Received shutdown signal`);
|
|
1388
1513
|
(async () => {
|
|
@@ -1409,6 +1534,9 @@ function startWorkerMessageLoop(options) {
|
|
|
1409
1534
|
}
|
|
1410
1535
|
}
|
|
1411
1536
|
self.addEventListener("message", handleMessage);
|
|
1537
|
+
setBroadcastChannelRelay((channelName, data) => {
|
|
1538
|
+
sendMessage({ type: "broadcast:post", channel: channelName, data });
|
|
1539
|
+
});
|
|
1412
1540
|
sendMessage({ type: "ready" });
|
|
1413
1541
|
messageLogger.debug(`[Worker-${workerId}] Message loop started`);
|
|
1414
1542
|
}
|
|
@@ -1490,6 +1618,7 @@ export {
|
|
|
1490
1618
|
RequestCookieStore,
|
|
1491
1619
|
ServiceWorkerGlobals,
|
|
1492
1620
|
ShovelActivateEvent,
|
|
1621
|
+
ShovelBroadcastChannel,
|
|
1493
1622
|
ShovelClient,
|
|
1494
1623
|
ShovelClients,
|
|
1495
1624
|
ShovelExtendableEvent,
|
|
@@ -1507,6 +1636,7 @@ export {
|
|
|
1507
1636
|
createCacheFactory,
|
|
1508
1637
|
createDatabaseFactory,
|
|
1509
1638
|
createDirectoryFactory,
|
|
1639
|
+
deliverBroadcastMessage,
|
|
1510
1640
|
dispatchRequest,
|
|
1511
1641
|
initWorkerRuntime,
|
|
1512
1642
|
kDispatchActivate,
|
|
@@ -1516,5 +1646,7 @@ export {
|
|
|
1516
1646
|
parseSetCookieHeader,
|
|
1517
1647
|
runLifecycle,
|
|
1518
1648
|
serializeCookie,
|
|
1649
|
+
setBroadcastChannelBackend,
|
|
1650
|
+
setBroadcastChannelRelay,
|
|
1519
1651
|
startWorkerMessageLoop
|
|
1520
1652
|
};
|