@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "The portable meta-framework built on web standards.",
5
5
  "keywords": [
6
6
  "service-worker",
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 implements ServiceWorkerGlobalScope {
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
  };