@hedwigjs/broker 0.1.0

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,1507 @@
1
+ /**
2
+ * Backpressure strategies to control message processing rate
3
+ *
4
+ * Control how messages are processed to prevent UI freezing and optimize performance.
5
+ * All strategies are mutually exclusive - only one can be used per subscription.
6
+ */
7
+ interface BackpressureOptions {
8
+ /**
9
+ * Throttle: Limit handler calls to once per `throttle` milliseconds
10
+ *
11
+ * First call executes immediately, subsequent calls are delayed.
12
+ * Guarantees maximum call rate without losing the last message.
13
+ *
14
+ * Use case: Real-time charts, high-frequency updates
15
+ */
16
+ throttle?: number;
17
+ /**
18
+ * Debounce: Delay handler execution until `debounce` milliseconds of silence
19
+ *
20
+ * Resets timer on each new message. Only the last message is processed.
21
+ *
22
+ * Use case: Search autocomplete, input validation
23
+ */
24
+ debounce?: number;
25
+ /**
26
+ * Rate limiting: Drop messages exceeding rate limit
27
+ *
28
+ * Allows maximum `max` messages per `window` milliseconds.
29
+ * Messages exceeding the limit are dropped (lost permanently).
30
+ *
31
+ * Use case: Prevent flooding, protect from bursts, spam protection
32
+ */
33
+ rateLimit?: {
34
+ /** Maximum number of messages allowed */
35
+ max: number;
36
+ /** Time window in milliseconds */
37
+ window: number;
38
+ };
39
+ /**
40
+ * Callback when messages are dropped (rate limiting only)
41
+ *
42
+ * @param droppedCount - Number of messages dropped so far
43
+ */
44
+ onDrop?: (droppedCount: number) => void;
45
+ }
46
+
47
+ /**
48
+ * All structured log events emitted by the broker infrastructure.
49
+ *
50
+ * These are machine-readable codes — stable across versions.
51
+ * Use them as filter keys in Sentry / Datadog / Grafana.
52
+ */
53
+ type BrokerLogEvent = 'broker.subscribe.after_destroy' | 'broker.bridge.add.after_destroy' | 'broker.bridge.replaced' | 'broker.client.register.after_destroy' | 'broker.replay.history_disabled' | 'facade.createClient.reset' | 'handler.failed' | 'hook.after_send.failed' | 'hook.failed' | 'bridge.message.parse_failed' | 'backpressure.handler.failed' | 'backpressure.on_drop.failed' | 'replay.handler.failed' | 'replay.query.failed' | 'system_events.listener.failed';
54
+ /**
55
+ * Pluggable logger interface for broker infrastructure events.
56
+ *
57
+ * Implement this to redirect broker warnings and errors to your
58
+ * observability stack (Sentry, Datadog, pino, etc.).
59
+ *
60
+ * @example
61
+ * initBroker({
62
+ * logger: {
63
+ * warn: (event, meta) => myLogger.warn(event, meta),
64
+ * error: (event, meta) => Sentry.captureMessage(event, { extra: meta }),
65
+ * }
66
+ * });
67
+ */
68
+ interface BrokerLogger {
69
+ warn(event: BrokerLogEvent, meta?: Record<string, unknown>): void;
70
+ error(event: BrokerLogEvent, meta?: Record<string, unknown>): void;
71
+ }
72
+ /**
73
+ * Default logger — forwards to console.
74
+ * Used when no logger is provided in BrokerConfig.
75
+ */
76
+ declare const defaultLogger: BrokerLogger;
77
+
78
+ /** Unique client identifier in the system */
79
+ type ClientID = string;
80
+ /**
81
+ * Internal message format for inter-module communication
82
+ */
83
+ interface Message<T extends string = string, P = any> {
84
+ /** Unique message identifier (e.g. "abc-42") for debugging and DevTools */
85
+ id: string;
86
+ /** Message topic (e.g. 'user.login.v1') */
87
+ topic: T;
88
+ /** Message source - client ID that emitted the message */
89
+ source: string;
90
+ /** Target client ID or '*' for broadcast */
91
+ target: string;
92
+ /** Message payload data */
93
+ data: P;
94
+ /** Unix timestamp in milliseconds */
95
+ timestamp: number;
96
+ /** Indicates if this is a replayed historical message */
97
+ replayed?: boolean;
98
+ /** Indicates if this message was received from external source (bridge) */
99
+ fromExternal?: boolean;
100
+ /**
101
+ * Marks a debug/test message injected via `broker.$debug.send(...)`.
102
+ * Routing, hooks, history and bridge forwarding all treat it as a
103
+ * real message — the flag is purely metadata so DevTools and integration
104
+ * tests can distinguish spoofed traffic from production events.
105
+ */
106
+ synthetic?: boolean;
107
+ }
108
+ /**
109
+ * Message handler function
110
+ * Can return data for Request-Reply pattern
111
+ */
112
+ type HandlerFn<T extends string, P = unknown> = (message: Message<T, P>) => void | any | Promise<void | any>;
113
+ /**
114
+ * Type-erased message handler for internal core usage
115
+ *
116
+ * Same contract as HandlerFn but without generic type parameters.
117
+ * Used in Broker, Subscriptions, Router, BackpressureHandler
118
+ * where specific message/payload types are already erased.
119
+ */
120
+ type MessageHandler = (message: Message) => void | any | Promise<void | any>;
121
+ /**
122
+ * Options for message emission and requests
123
+ */
124
+ interface MessageOptions {
125
+ /**
126
+ * Record this message to history for replay
127
+ *
128
+ * Important: History has limited capacity (default 1000 messages).
129
+ * Only mark truly important messages that late subscribers need to replay.
130
+ *
131
+ * @default false
132
+ */
133
+ history?: boolean;
134
+ }
135
+ /**
136
+ * Options for replaying historical messages
137
+ */
138
+ interface ReplayOptions {
139
+ /**
140
+ * Maximum number of historical messages to replay
141
+ * If not specified, replays all matching messages
142
+ */
143
+ limit?: number;
144
+ /**
145
+ * Replay messages starting from this timestamp (Unix ms)
146
+ */
147
+ since?: number;
148
+ /**
149
+ * Replay messages until this timestamp (Unix ms)
150
+ */
151
+ until?: number;
152
+ }
153
+ /**
154
+ * Options for message subscription
155
+ *
156
+ * Controls how messages are processed and delivered to handlers.
157
+ * All options are opt-in and can be combined.
158
+ */
159
+ interface SubscriptionOptions {
160
+ /**
161
+ * Backpressure control strategies for incoming messages
162
+ *
163
+ * Controls the rate and manner of message processing to prevent
164
+ * UI freezing and optimize performance.
165
+ */
166
+ backpressure?: BackpressureOptions;
167
+ /**
168
+ * Replay historical messages when subscribing
169
+ *
170
+ * Allows late subscribers to catch up on missed messages.
171
+ * Messages are replayed asynchronously after subscription is established.
172
+ */
173
+ replay?: ReplayOptions;
174
+ }
175
+ interface ClientSubscriptionInfo {
176
+ topic: string;
177
+ /**
178
+ * Options of the first handler registered on this (client, topic) pair.
179
+ * A pair may hold multiple handlers, each with distinct options — this
180
+ * field surfaces one representative set for observability tools that
181
+ * predate the multi-handler model.
182
+ */
183
+ options?: SubscriptionOptions;
184
+ /** Total number of handlers this client has attached to the topic. */
185
+ handlerCount: number;
186
+ }
187
+ /** Point-in-time snapshot of a single registered client. */
188
+ interface ClientInfo {
189
+ id: ClientID;
190
+ /** Unix timestamp (ms) when the client registered. */
191
+ connectedAt: number;
192
+ subscriptions: ClientSubscriptionInfo[];
193
+ }
194
+ /**
195
+ * Configuration for Broker
196
+ */
197
+ interface BrokerConfig {
198
+ /** Message history configuration */
199
+ history?: {
200
+ /** Enable message history */
201
+ enabled: boolean;
202
+ /** Maximum number of messages to keep in memory (default: 1000) */
203
+ maxSize?: number;
204
+ /** Time to live for messages (ms). undefined = no expiration */
205
+ ttl?: number;
206
+ };
207
+ /**
208
+ * Pluggable logger for broker infrastructure events.
209
+ *
210
+ * Receives structured event codes (e.g. `'handler.failed'`) and a metadata
211
+ * object. Use this to redirect broker warnings and errors to your
212
+ * observability stack (Sentry, Datadog, pino, etc.).
213
+ *
214
+ * Defaults to `console.warn` / `console.error` when not provided.
215
+ */
216
+ logger?: BrokerLogger;
217
+ }
218
+
219
+ declare const RoutingReason: {
220
+ readonly DELIVERED: "DELIVERED";
221
+ readonly DISPATCHED: "DISPATCHED";
222
+ readonly REPLAY_DELIVERED: "REPLAY_DELIVERED";
223
+ readonly HOOK_REJECTED: "HOOK_REJECTED";
224
+ readonly NO_SUBSCRIBERS: "NO_SUBSCRIBERS";
225
+ readonly NOT_SUBSCRIBED: "NOT_SUBSCRIBED";
226
+ readonly HANDLER_FAILED: "HANDLER_FAILED";
227
+ readonly BROKER_DESTROYED: "BROKER_DESTROYED";
228
+ };
229
+ type RoutingReasonType = (typeof RoutingReason)[keyof typeof RoutingReason];
230
+ /**
231
+ * RoutingResult - Message delivery result (Value Object)
232
+ *
233
+ * Immutable object representing the result of a message dispatch operation.
234
+ * Can only be created through the static factory method create().
235
+ */
236
+ declare class RoutingResult<TResponse = unknown> {
237
+ readonly status: 'ACK' | 'NACK';
238
+ readonly reason: RoutingReasonType;
239
+ readonly message: string;
240
+ readonly timestamp: number;
241
+ /** Recipient client ID — set for unicast, undefined for multicast. */
242
+ readonly recipientId?: ClientID;
243
+ /** All recipient client IDs — set for multicast, undefined for unicast. */
244
+ readonly recipientIds?: ClientID[];
245
+ readonly data?: TResponse;
246
+ private constructor();
247
+ /**
248
+ * @param status - ACK for success, NACK for failure
249
+ * @param reason - Machine-readable reason code (use RoutingReason constants)
250
+ * @param message - Human-readable result description
251
+ * @param recipientId - Recipient client ID (unicast only)
252
+ * @param data - Response data from handler (Request-Reply pattern only)
253
+ * @param recipientIds - All recipient client IDs (multicast only)
254
+ */
255
+ static create<T = unknown>(status: 'ACK' | 'NACK', reason: RoutingReasonType, message: string, recipientId?: ClientID, data?: T, recipientIds?: ClientID[]): RoutingResult<T>;
256
+ }
257
+
258
+ /**
259
+ * Pluggable wire used by {@link BridgeConfig} / {@link MessageBroker.addBridge}.
260
+ *
261
+ * This is the extension point for cross-context transports. Built-in
262
+ * implementations (`PostMessageTransport`, `BroadcastChannelTransport`,
263
+ * `WebSocketTransport`, `SSETransport`) are exported from `@hedwigjs/broker`;
264
+ * custom transports (WebRTC, Service Worker, Electron IPC, etc.) plug in by
265
+ * implementing this interface — no other broker internals need to be touched.
266
+ *
267
+ * ## Contract
268
+ *
269
+ * ### Outbound — `send(data)`
270
+ * Called by the {@link Bridge} whenever a local broker message matches the
271
+ * bridge's forward patterns. Implementation must serialize (if needed) and
272
+ * hand the payload to the underlying wire. Errors should be caught and
273
+ * logged, not thrown — a failing wire must not crash the broker pipeline.
274
+ *
275
+ * ### Inbound — `onMessage(callback)`
276
+ * Called once by the {@link Bridge} at construction time to subscribe to
277
+ * incoming messages. Implementation must invoke `callback` for every valid
278
+ * inbound payload after any transport-level validation (e.g. origin checks
279
+ * for postMessage, channel filtering for BroadcastChannel). Returns an
280
+ * unsubscribe function; the {@link Bridge} calls it in `destroy()`.
281
+ *
282
+ * ### Cleanup — `destroy()`
283
+ * Called when the bridge is removed or the broker is destroyed. Must release
284
+ * all resources (event listeners, sockets, channels). Must be idempotent.
285
+ *
286
+ * ## Security note
287
+ *
288
+ * Transports are the trust boundary between the broker and the outside
289
+ * world. If your wire crosses origins (postMessage, WebSocket, SSE),
290
+ * validate the source before invoking the inbound callback — the broker
291
+ * will otherwise route whatever it receives.
292
+ *
293
+ * @example Custom transport skeleton
294
+ * ```ts
295
+ * class MyTransport implements BridgeTransport {
296
+ * #cb: ((data: unknown) => void) | null = null;
297
+ *
298
+ * send(data: unknown): void {
299
+ * try { myWire.publish(data); }
300
+ * catch (e) { console.error('[MyTransport] send failed:', e); }
301
+ * }
302
+ *
303
+ * onMessage(cb: (data: unknown) => void): () => void {
304
+ * this.#cb = cb;
305
+ * const off = myWire.subscribe((payload) => {
306
+ * if (!isTrusted(payload)) return;
307
+ * this.#cb?.(payload);
308
+ * });
309
+ * return () => { off(); this.#cb = null; };
310
+ * }
311
+ *
312
+ * destroy(): void { this.#cb = null; myWire.close(); }
313
+ * }
314
+ * ```
315
+ */
316
+ interface BridgeTransport {
317
+ /** Outbound: send a payload to the wire. See interface docs for contract. */
318
+ send(data: unknown): void;
319
+ /** Inbound: subscribe to payloads from the wire. Returns unsubscribe. */
320
+ onMessage(callback: (data: unknown) => void): () => void;
321
+ /** Release resources. Must be idempotent. */
322
+ destroy(): void;
323
+ }
324
+ /**
325
+ * Bridge configuration
326
+ */
327
+ interface BridgeConfig {
328
+ /** Low-level duplex link (see {@link BridgeTransport}) */
329
+ transport: BridgeTransport;
330
+ /** Message patterns to forward (e.g. ['user.*', 'theme.*']) */
331
+ forward: string[];
332
+ }
333
+ /**
334
+ * Bridge interface - transport layer for cross-context communication
335
+ *
336
+ * Bridge forwards messages
337
+ * between broker and external contexts (iframes, tabs, servers).
338
+ */
339
+ interface Bridge {
340
+ /** Topic patterns this bridge forwards to its transport. */
341
+ readonly forwardPatterns: ReadonlyArray<string>;
342
+ /**
343
+ * Transport class name with the `Transport` suffix stripped
344
+ * (e.g. `WebSocket`, `SSE`). Used only by the Inspector to label
345
+ * bridges in DevTools. `undefined` for transports whose constructor
346
+ * is an anonymous class.
347
+ */
348
+ readonly transportKind?: string;
349
+ /**
350
+ * Check if topic should be forwarded to transport
351
+ */
352
+ shouldForward(topic: string): boolean;
353
+ /**
354
+ * Send message to transport
355
+ */
356
+ send(message: Message): void;
357
+ /**
358
+ * Cleanup resources
359
+ */
360
+ destroy(): void;
361
+ }
362
+
363
+ /**
364
+ * System events - internal signals about broker state transitions.
365
+ *
366
+ * These are NOT user messages. System events expose infrastructure-level
367
+ * state changes (client/subscription/bridge lifecycle) that are not
368
+ * observable through the hook pipeline.
369
+ *
370
+ * Intended consumers:
371
+ * - DevTools (message-broker-devtools)
372
+ * - Tracing / metrics integrations
373
+ *
374
+ * These events are FIRE-AND-FORGET. Listeners cannot influence the pipeline;
375
+ * exceptions thrown from a listener are caught and logged, not propagated.
376
+ *
377
+ * Exposed on the broker under `$systemEvents` (the `$` prefix signals that
378
+ * this is a broker-internal channel, distinct from user message pub/sub).
379
+ */
380
+ interface SystemEventMap<T extends string, P extends Record<T, any>> {
381
+ 'client.registered': {
382
+ clientId: ClientID;
383
+ /** Unix ms when the client was registered. */
384
+ at: number;
385
+ };
386
+ 'client.unregistered': {
387
+ clientId: ClientID;
388
+ /** Unix ms when the client was unregistered. */
389
+ at: number;
390
+ };
391
+ 'subscription.added': {
392
+ clientId: ClientID;
393
+ topic: T;
394
+ options?: SubscriptionOptions;
395
+ };
396
+ 'subscription.removed': {
397
+ clientId: ClientID;
398
+ topic: T;
399
+ };
400
+ /**
401
+ * Fired when an `onSubscribe` hook denied a subscription attempt. The
402
+ * broker still throws on the caller so the subscription is NOT registered;
403
+ * this event exists so observability tools (DevTools, ACL audit) can pick
404
+ * up the denial without racing the exception.
405
+ */
406
+ 'subscription.rejected': {
407
+ clientId: ClientID;
408
+ topic: T;
409
+ reason: string;
410
+ };
411
+ /**
412
+ * Fired when a `beforeSend` hook denied an outgoing message. The message
413
+ * also surfaces in the delivery result as `NACK HOOK_REJECTED`, but this
414
+ * event lets pure-observability consumers listen for security signals on a
415
+ * dedicated channel without inspecting every RoutingResult.
416
+ */
417
+ 'message.rejected': {
418
+ source: ClientID;
419
+ target: ClientID | '*';
420
+ topic: T;
421
+ reason: string;
422
+ };
423
+ 'bridge.added': {
424
+ bridgeId: string;
425
+ };
426
+ 'bridge.removed': {
427
+ bridgeId: string;
428
+ };
429
+ }
430
+ type SystemEventName<T extends string, P extends Record<T, any>> = keyof SystemEventMap<T, P>;
431
+ type SystemEventPayload<T extends string, P extends Record<T, any>, K extends SystemEventName<T, P>> = SystemEventMap<T, P>[K];
432
+ /**
433
+ * Listener for a specific system event. Receives the event payload.
434
+ * Exceptions are caught by the dispatcher.
435
+ */
436
+ type SystemEventListener<T extends string, P extends Record<T, any>, K extends SystemEventName<T, P>> = (payload: SystemEventPayload<T, P, K>) => void;
437
+ /**
438
+ * Listener that receives every system event with its name.
439
+ * Useful for universal recorders (e.g. DevTools event log).
440
+ */
441
+ type SystemAnyEventListener<T extends string, P extends Record<T, any>> = <K extends SystemEventName<T, P>>(event: K, payload: SystemEventPayload<T, P, K>) => void;
442
+ /**
443
+ * Subscriber contract for the system event channel.
444
+ *
445
+ * Consumers (DevTools, tracing, metrics) use this to subscribe. Listeners
446
+ * are fire-and-forget — they cannot influence the pipeline, and exceptions
447
+ * they throw are caught and logged, never propagated.
448
+ *
449
+ * `emit()` and `clear()` are intentionally absent: only `BrokerCore`
450
+ * (which holds the concrete `SystemEvents` instance) can publish events
451
+ * or tear down listeners.
452
+ */
453
+ interface SystemEventsEmitter<T extends string, P extends Record<T, any>> {
454
+ /**
455
+ * Subscribe to a specific system event.
456
+ * @returns Unsubscribe function.
457
+ */
458
+ on<K extends SystemEventName<T, P>>(event: K, listener: SystemEventListener<T, P, K>): () => void;
459
+ /**
460
+ * Subscribe once — listener is automatically removed after first invocation.
461
+ * @returns Unsubscribe function.
462
+ */
463
+ once<K extends SystemEventName<T, P>>(event: K, listener: SystemEventListener<T, P, K>): () => void;
464
+ /**
465
+ * Remove all listeners for a given event. Omit `event` to clear everything.
466
+ */
467
+ off<K extends SystemEventName<T, P>>(event?: K): void;
468
+ /**
469
+ * Subscribe to every system event with one listener.
470
+ * Useful for DevTools panels that need a unified event feed.
471
+ * @returns Unsubscribe function.
472
+ */
473
+ onAny(listener: SystemAnyEventListener<T, P>): () => void;
474
+ /**
475
+ * Number of registered listeners for a given event, or total across all
476
+ * events when called without arguments. Useful for tests and fast-path checks.
477
+ */
478
+ listenerCount<K extends SystemEventName<T, P>>(event?: K): number;
479
+ }
480
+
481
+ /**
482
+ * Result of hook execution
483
+ */
484
+ type HookResult = {
485
+ allowed: true;
486
+ } | {
487
+ allowed: false;
488
+ message: string;
489
+ };
490
+ /**
491
+ * Hook function types
492
+ */
493
+ type OnSubscribeHook<T> = (topic: T, clientId: ClientID) => HookResult;
494
+ /** Called before each message is sent. Return { allowed: false } to block. */
495
+ type BeforeSendHook<T extends string, P extends Record<T, any>> = (message: Readonly<Message<T, P[T]>>) => HookResult;
496
+ /** Called after each message is sent. Receives delivery result. */
497
+ type AfterSendHook<T extends string, P extends Record<T, any>> = (message: Readonly<Message<T, P[T]>>, messageResult: RoutingResult) => void;
498
+
499
+ /**
500
+ * BrokerCore — low-level message broker engine, internal implementation
501
+ * of the public {@link MessageBroker} interface.
502
+ *
503
+ * Responsibilities:
504
+ * - Message routing and delivery pipeline (hooks → routing → history → bridges)
505
+ * - Subscription management (delegates to Subscriptions)
506
+ * - Coordinate Router, HooksRegistry, ClientRegistry
507
+ * - Bridge management for cross-context communication
508
+ * - Message history & replay
509
+ * - Emit system events on the internal system events channel
510
+ * - Expose state snapshots via the inspect facade
511
+ * - Lifecycle management
512
+ *
513
+ * The class is exported within the package for unit tests. Consumers
514
+ * receive only the `MessageBroker<T, P>` contract from `initBroker()` /
515
+ * `getBroker()`. Methods tagged `@internal` (subscribe, unsubscribe,
516
+ * processMessage, registerClient, unregisterClient, resetClient,
517
+ * getClient) form the internal protocol between
518
+ * BrokerClient, Bridge and the facade — they are stable only inside the
519
+ * package and may change without notice.
520
+ */
521
+ declare class BrokerCore<T extends string, P extends Record<T, any>> implements MessageBroker<T, P> {
522
+ #private;
523
+ /**
524
+ * Infrastructure logger configured via {@link BrokerConfig.logger}.
525
+ *
526
+ * @internal Used by the facade layer.
527
+ */
528
+ readonly logger: BrokerLogger;
529
+ constructor(config?: BrokerConfig);
530
+ /**
531
+ * Broker-internal system event channel (push model).
532
+ *
533
+ * The `$` prefix marks this as a broker-internal API. Intended for tooling:
534
+ * DevTools, tracing collectors, metrics integrations.
535
+ *
536
+ * This is NOT for extending broker behaviour — extension hooks are exposed
537
+ * via `useBeforeSendHook`, `useAfterSendHook`, `useOnSubscribeHook`.
538
+ *
539
+ * @example
540
+ * broker.$systemEvents.on('client.registered', ({ clientId }) => { ... });
541
+ * broker.$systemEvents.on('subscription.added', ({ clientId, topic }) => { ... });
542
+ */
543
+ get $systemEvents(): SystemEventsEmitter<T, P>;
544
+ /**
545
+ * Point-in-time state snapshots (pull model).
546
+ *
547
+ * Read-only view over broker state for DevTools and debugging tools.
548
+ *
549
+ * @example
550
+ * const clients = broker.inspect.getClients();
551
+ * const history = broker.inspect.getHistory();
552
+ */
553
+ get inspect(): Inspector<T, P>;
554
+ /**
555
+ * Subscribe a client to a topic.
556
+ *
557
+ * Multiple handlers may be attached to the same `(clientId, topic)` pair —
558
+ * each call returns a distinct subscription id that identifies THIS
559
+ * handler for later removal via {@link unsubscribeOne}.
560
+ *
561
+ * @returns Subscription id, or `0` when the call was a no-op (broker
562
+ * destroyed). Zero is never a valid id.
563
+ *
564
+ * @throws Error if subscription is blocked on onSubscribe hook
565
+ *
566
+ * @internal Called by {@link BrokerClient.on}. Not part of the public
567
+ * `MessageBroker` contract.
568
+ */
569
+ subscribe(clientId: ClientID, topic: T, handler: MessageHandler, options?: SubscriptionOptions): number;
570
+ /**
571
+ * Unsubscribe a client from a topic — removes every handler this client
572
+ * has attached to the topic.
573
+ *
574
+ * @internal Called by {@link BrokerClient.off}. Not part of the public
575
+ * `MessageBroker` contract.
576
+ */
577
+ unsubscribe(clientId: ClientID, topic: T): void;
578
+ /**
579
+ * Unsubscribe a single handler by its subscription id.
580
+ *
581
+ * Fires `subscription.removed` only if this was the last handler that
582
+ * client had on the topic — otherwise the client is still subscribed.
583
+ *
584
+ * @internal Called by the unsubscribe closure returned from {@link BrokerClient.on}.
585
+ */
586
+ unsubscribeOne(subscriptionId: number): void;
587
+ /**
588
+ * Process a message originating from a local client.
589
+ *
590
+ * Runs the full lifecycle pipeline: beforeSend → history → routing →
591
+ * afterSend → forward to bridges.
592
+ *
593
+ * @param topic - Type of message
594
+ * @param sender - Client ID of sender
595
+ * @param recipient - Target recipient: specific ClientID (unicast) or '*' (multicast)
596
+ * @param data - Message payload
597
+ * @param options - Message options (history)
598
+ * @returns Promise resolving to RoutingResult with delivery status
599
+ *
600
+ * @internal Called by {@link BrokerClient.emit} / {@link BrokerClient.request}.
601
+ * Not part of the public `MessageBroker` contract.
602
+ */
603
+ processMessage<K extends T, R = unknown>(topic: K, sender: ClientID, recipient: ClientID | '*', data: P[K], options?: MessageOptions): Promise<RoutingResult<R>>;
604
+ /**
605
+ * Broker-internal debug channel.
606
+ *
607
+ * `send()` runs the full message pipeline exactly like a normal
608
+ * `Client.emit()` / `Client.request()` — routing, hooks, history and
609
+ * bridge forwarding all apply — but with two differences:
610
+ *
611
+ * 1. `source` is an arbitrary string, not tied to a registered client.
612
+ * Nothing gets reset in the client registry: safe to «impersonate»
613
+ * any client id for testing subscribers without breaking that
614
+ * client's own subscriptions.
615
+ * 2. `message.synthetic === true` on the resulting Message, so
616
+ * DevTools and integration tests can distinguish spoofed traffic
617
+ * from production events (e.g. render a `SYNTHETIC` badge).
618
+ *
619
+ * Multicast vs unicast is picked by `target`: `'*'` fans out to all
620
+ * subscribers, a specific `ClientID` targets one recipient and captures
621
+ * that handler's return value in `RoutingResult.data`.
622
+ *
623
+ * The `$` prefix marks this as a broker-internal API — for DevTools
624
+ * and integration tests, not for business code.
625
+ */
626
+ get $debug(): {
627
+ send<K extends T, R = unknown>(source: ClientID, topic: K, target: ClientID | '*', data: P[K], options?: MessageOptions): Promise<RoutingResult<R>>;
628
+ };
629
+ /**
630
+ * Add a bridge for cross-context communication (idempotent)
631
+ *
632
+ * If a bridge with the given ID already exists, the old bridge is destroyed
633
+ * and replaced with the new one. This prevents duplicate bridges during HMR.
634
+ *
635
+ * @param id - Unique identifier for the bridge (e.g. 'cross-tab', 'iframe-checkout')
636
+ * @param config - Bridge configuration (transport + forward patterns)
637
+ * @returns Function to remove the bridge
638
+ */
639
+ addBridge(id: string, config: BridgeConfig): () => void;
640
+ /**
641
+ * Register a client instance.
642
+ *
643
+ * @internal Called by the `BrokerClient` constructor.
644
+ */
645
+ registerClient(client: BrokerClient<T, P>): void;
646
+ /**
647
+ * Unregister a client and remove all its subscriptions.
648
+ *
649
+ * @internal Called by {@link BrokerClient.destroy}.
650
+ */
651
+ unregisterClient(clientId: ClientID): void;
652
+ /**
653
+ * Get a registered client by ID.
654
+ *
655
+ * @param clientId - Unique client identifier
656
+ * @returns Client instance or undefined if not found
657
+ *
658
+ * @internal Used by the `createClient` facade for idempotency checks.
659
+ */
660
+ getClient(clientId: ClientID): BrokerClient<T, P> | undefined;
661
+ /**
662
+ * Reset a client: clear all its subscriptions and backpressure strategies
663
+ * while keeping the client registered.
664
+ *
665
+ * Used for idempotent client creation (HMR, re-mounting).
666
+ * Iterates the client's subscriptions and calls unsubscribe() for each,
667
+ * which correctly flushes/destroys backpressure strategies.
668
+ *
669
+ * @param clientId - Unique client identifier
670
+ *
671
+ * @internal Called by {@link BrokerClient.reset} and by the
672
+ * `createClient` facade on idempotent re-creation.
673
+ */
674
+ resetClient(clientId: ClientID): void;
675
+ /**
676
+ * Register a beforeSend hook
677
+ *
678
+ * Called before routing for ALL messages, including those from bridges.
679
+ * Use message.fromExternal to distinguish local vs external if needed.
680
+ */
681
+ useBeforeSendHook(hook: BeforeSendHook<T, P>): () => void;
682
+ /**
683
+ * Register an afterSend hook
684
+ * Note: afterSend hooks are called for ALL messages (check message.fromExternal if needed)
685
+ */
686
+ useAfterSendHook(hook: AfterSendHook<T, P>): () => void;
687
+ /**
688
+ * Register an onSubscribe hook
689
+ */
690
+ useOnSubscribeHook(hook: OnSubscribeHook<T>): () => void;
691
+ /**
692
+ * Destroy the broker and clean up all resources
693
+ */
694
+ destroy(): void;
695
+ }
696
+
697
+ /**
698
+ * Client — the public contract of a broker-connected client.
699
+ *
700
+ * Instances are obtained via `createClient(id)`. Consumers should treat
701
+ * this as the stable surface — the underlying class is an internal
702
+ * implementation detail and may change in minor releases.
703
+ *
704
+ * A client provides three things:
705
+ * - subscription (`on` / `off`) to a topic,
706
+ * - message emission: broadcast (`emit`) or targeted (`request`),
707
+ * - lifecycle control (`reset`, `destroy`).
708
+ */
709
+ interface Client<T extends string, P extends Record<T, any>> {
710
+ /** Unique client identifier passed to `createClient(id)`. */
711
+ readonly id: ClientID;
712
+ /**
713
+ * Subscribe to a topic.
714
+ *
715
+ * @param topic - Topic name (e.g. `'user.login.v1'`).
716
+ * @param handler - Handler invoked for every matching message.
717
+ * @param options - Subscription options (backpressure strategy, replay).
718
+ * @returns Unsubscribe function. Equivalent to `client.off(topic)`.
719
+ * @throws If an `onSubscribe` hook rejects the subscription.
720
+ */
721
+ on<K extends T>(topic: K, handler: HandlerFn<K, P[K]>, options?: SubscriptionOptions): () => void;
722
+ /**
723
+ * Unsubscribe from a topic. No-op if the client was not subscribed.
724
+ */
725
+ off<K extends T>(topic: K): void;
726
+ /**
727
+ * Broadcast a message to every subscriber of `topic` (multicast).
728
+ *
729
+ * @returns Promise resolving to the aggregated {@link RoutingResult}.
730
+ */
731
+ emit<K extends T>(topic: K, data: P[K], options?: MessageOptions): Promise<RoutingResult>;
732
+ /**
733
+ * Send a targeted message to a specific recipient (unicast).
734
+ *
735
+ * @typeParam R - Expected shape of the handler's return value, surfaced
736
+ * on `RoutingResult.data`. Defaults to `unknown` — caller must specify
737
+ * to get a typed response (e.g. `client.request<'user.fetch', User>(…)`).
738
+ * Not enforced against the handler signature; treated as a boundary cast.
739
+ * @returns Promise resolving to the {@link RoutingResult} for that one
740
+ * recipient.
741
+ */
742
+ request<K extends T, R = unknown>(recipient: ClientID, topic: K, data: P[K], options?: MessageOptions): Promise<RoutingResult<R>>;
743
+ /**
744
+ * Reset the client: flush and drop every subscription and its
745
+ * backpressure strategy while keeping the client registered.
746
+ *
747
+ * Used by `createClient(id)` for idempotent creation (HMR, re-mount).
748
+ */
749
+ reset(): void;
750
+ /**
751
+ * Destroy the client: unregister it and remove all its subscriptions.
752
+ * After `destroy()` the client instance becomes inert.
753
+ */
754
+ destroy(): void;
755
+ }
756
+
757
+ /**
758
+ * BrokerClient — concrete implementation of the {@link Client} contract.
759
+ *
760
+ * Created by the facade function `createClient(id)`. Use cases:
761
+ * - Communication between microfrontends in the same browser context
762
+ * - Fastest possible message delivery (no serialization)
763
+ * - Default choice for most applications
764
+ *
765
+ * Consumers should treat `Client<T, P>` as the stable surface — this
766
+ * class is an internal implementation detail. It is exported within the
767
+ * package so unit tests can construct instances directly with a
768
+ * `BrokerCore` stub, but it is NOT part of the public API: transport of
769
+ * `BrokerClient` through the package exports map is disabled.
770
+ */
771
+ declare class BrokerClient<T extends string, P extends Record<T, any>> implements Client<T, P> {
772
+ #private;
773
+ readonly id: ClientID;
774
+ constructor(id: ClientID, core: BrokerCore<T, P>);
775
+ /**
776
+ * Subscribe to a topic with handler
777
+ *
778
+ * @param topic - Topic to subscribe to (e.g. 'user.login.v1')
779
+ * @param handler - Message handler function
780
+ * @param options - Subscription options (backpressure, replay)
781
+ * @returns Unsubscribe function
782
+ */
783
+ on<K extends T>(topic: K, handler: HandlerFn<K, P[K]>, options?: SubscriptionOptions): () => void;
784
+ /**
785
+ * Emit message to all subscribers (multicast)
786
+ */
787
+ emit<K extends T>(topic: K, data: P[K], options?: MessageOptions): Promise<RoutingResult>;
788
+ /**
789
+ * Send request to specific client (unicast).
790
+ *
791
+ * The recipient's handler return value (if any) is captured in
792
+ * `RoutingResult.data`. Caller specifies `R` to type that payload.
793
+ * The broker does not enforce that the handler actually returns `R` —
794
+ * the cast happens at the boundary, same trust level as `as R`.
795
+ */
796
+ request<K extends T, R = unknown>(recipient: ClientID, topic: K, data: P[K], options?: MessageOptions): Promise<RoutingResult<R>>;
797
+ /**
798
+ * Unsubscribe from a topic
799
+ */
800
+ off<K extends T>(topic: K): void;
801
+ /**
802
+ * Reset client: clear all subscriptions and backpressure strategies
803
+ * while keeping the client registered in the broker.
804
+ *
805
+ * After reset, the client can subscribe to messages again with fresh handlers.
806
+ * Existing backpressure strategies are flushed and destroyed.
807
+ */
808
+ reset(): void;
809
+ /**
810
+ * Destroy client and cleanup resources
811
+ */
812
+ destroy(): void;
813
+ }
814
+
815
+ /**
816
+ * ClientRegistry - Manages registered clients
817
+ *
818
+ * Responsibilities:
819
+ * - Register/unregister clients
820
+ * - Track active clients
821
+ * - Provide client lookup
822
+ *
823
+ * Used for observability and DevTools integration
824
+ */
825
+ declare class ClientRegistry<T extends string, P extends Record<T, any>> {
826
+ #private;
827
+ /**
828
+ * Register a client
829
+ */
830
+ register(client: BrokerClient<T, P>): void;
831
+ /**
832
+ * Unregister a client
833
+ */
834
+ unregister(clientId: ClientID): void;
835
+ /**
836
+ * Get the timestamp when a client registered (Unix ms)
837
+ */
838
+ getConnectedAt(clientId: ClientID): number | undefined;
839
+ /**
840
+ * Get client by ID
841
+ */
842
+ get(clientId: ClientID): BrokerClient<T, P> | undefined;
843
+ /**
844
+ * Check if client is registered
845
+ */
846
+ has(clientId: ClientID): boolean;
847
+ /**
848
+ * Get all registered clients
849
+ */
850
+ getAll(): BrokerClient<T, P>[];
851
+ /**
852
+ * Get all client IDs
853
+ */
854
+ getAllIds(): ClientID[];
855
+ /**
856
+ * Clear all clients
857
+ */
858
+ clear(): void;
859
+ /**
860
+ * Get number of registered clients
861
+ */
862
+ get size(): number;
863
+ }
864
+
865
+ /**
866
+ * Subscriptions - Efficient subscription and handler management
867
+ *
868
+ * A single `(clientId, topic)` pair may hold MANY handlers. Each individual
869
+ * subscription is identified by a monotonic numeric id returned from
870
+ * {@link subscribe}, so callers can remove one handler without touching the
871
+ * others. Bulk operations (`unsubscribe(clientId, topic)`,
872
+ * `unsubscribeAll(clientId)`) still nuke every handler in scope.
873
+ *
874
+ * Bidirectional indexes for O(1) lookups:
875
+ * - Topic → Clients mapping (multicast recipient lookup — a client appears
876
+ * once no matter how many handlers it registered)
877
+ * - Client → Topics mapping (unsubscribe fan-out)
878
+ * - Composite key → ordered handler entries
879
+ * - Subscription id → location (for O(1) per-handler removal)
880
+ *
881
+ * @internal This class is used internally by BrokerCore and Router
882
+ */
883
+ /** Opaque handle returned by {@link Subscriptions.subscribe}. */
884
+ type SubscriptionId = number;
885
+ type SubscriptionEntry = {
886
+ readonly id: SubscriptionId;
887
+ readonly handler: MessageHandler;
888
+ readonly options?: SubscriptionOptions;
889
+ };
890
+ declare class Subscriptions<T extends string> {
891
+ #private;
892
+ /**
893
+ * Reserve a subscription id ahead of {@link subscribe}.
894
+ *
895
+ * Callers that need the id BEFORE the handler is finalized (e.g. to key
896
+ * a backpressure strategy by that id) can pre-allocate here and then
897
+ * pass the reserved id to {@link subscribe}.
898
+ */
899
+ reserveId(): SubscriptionId;
900
+ /**
901
+ * Subscribe a handler to a (client, topic) pair.
902
+ *
903
+ * Appends a new entry — previously registered handlers on the same pair
904
+ * are preserved. Returns the subscription id so the caller can remove
905
+ * this specific handler later via {@link unsubscribeOne}.
906
+ *
907
+ * If `preReservedId` is provided (from {@link reserveId}), that id is
908
+ * used instead of generating a new one.
909
+ */
910
+ subscribe(clientId: ClientID, topic: T, handler: MessageHandler, options?: SubscriptionOptions, preReservedId?: SubscriptionId): SubscriptionId;
911
+ /**
912
+ * Remove a single handler by its subscription id.
913
+ *
914
+ * If this was the last handler for the pair, the pair is fully removed
915
+ * from the bidirectional indexes (mirroring `unsubscribe` semantics).
916
+ *
917
+ * @returns Removal outcome: the removed entry, the pair it belonged to,
918
+ * and whether it was the last handler on that pair. `undefined`
919
+ * when no such id existed.
920
+ */
921
+ unsubscribeOne(id: SubscriptionId): {
922
+ entry: SubscriptionEntry;
923
+ clientId: ClientID;
924
+ topic: T;
925
+ wasLast: boolean;
926
+ } | undefined;
927
+ /**
928
+ * Unsubscribe every handler a client holds on a topic.
929
+ *
930
+ * @returns Entries that were actually removed. Empty when the client had
931
+ * no handlers on the topic. Callers use this to release
932
+ * per-handler resources (e.g. backpressure strategies).
933
+ */
934
+ unsubscribe(clientId: ClientID, topic: T): readonly SubscriptionEntry[];
935
+ /**
936
+ * Remove every subscription held by a given client.
937
+ *
938
+ * @returns Per-topic entry buckets that were removed, in iteration order.
939
+ * Empty when the client had no active subscriptions. Callers use
940
+ * this to release per-handler resources and emit per-topic
941
+ * `subscription.removed` events.
942
+ */
943
+ unsubscribeAll(clientId: ClientID): ReadonlyArray<{
944
+ topic: T;
945
+ entries: readonly SubscriptionEntry[];
946
+ }>;
947
+ /**
948
+ * Get all topics a client is subscribed to
949
+ */
950
+ getClientTopics(clientId: ClientID): ReadonlySet<T> | undefined;
951
+ /**
952
+ * Check if a client has at least one handler on a topic.
953
+ */
954
+ isSubscribed(clientId: ClientID, topic: T): boolean;
955
+ /**
956
+ * All handler entries a client has on a topic, in registration order.
957
+ */
958
+ getEntries(clientId: ClientID, topic: T): readonly SubscriptionEntry[];
959
+ /**
960
+ * Options of the first handler registered on `(clientId, topic)`.
961
+ *
962
+ * Convenience for read-only observers (e.g. Inspector) that predate the
963
+ * multi-handler model and expect a single options blob per pair.
964
+ */
965
+ getFirstOptions(clientId: ClientID, topic: T): SubscriptionOptions | undefined;
966
+ /**
967
+ * Number of handlers a client holds on a topic (0 = not subscribed).
968
+ */
969
+ getHandlerCount(clientId: ClientID, topic: T): number;
970
+ /**
971
+ * Get all subscribers for a topic (read-only)
972
+ */
973
+ getSubscribers(topic: T): ReadonlySet<ClientID>;
974
+ /**
975
+ * Get list of all clients that have active subscriptions
976
+ */
977
+ getAllSubscribedClients(): ClientID[];
978
+ /**
979
+ * Get detailed subscription map for all clients
980
+ */
981
+ getAllSubscriptions(): Record<string, string[]>;
982
+ /**
983
+ * Clear all subscriptions and handlers.
984
+ *
985
+ * @returns All entries that were held, so the caller can release
986
+ * per-handler resources (e.g. backpressure strategies).
987
+ */
988
+ clear(): readonly SubscriptionEntry[];
989
+ }
990
+
991
+ /**
992
+ * Entry in message history with metadata
993
+ */
994
+ interface HistoryEntry<T extends string = string, P = any> {
995
+ /** The message itself (immutable) */
996
+ message: Readonly<Message<T, P>>;
997
+ /** Unix timestamp (ms) when message was recorded */
998
+ timestamp: number;
999
+ /** Sequence number for guaranteed ordering */
1000
+ sequence: number;
1001
+ }
1002
+ /**
1003
+ * Filter for querying message history
1004
+ */
1005
+ interface HistoryFilter<T extends string = string> {
1006
+ /** Topics to filter (supports glob patterns like 'user.*') */
1007
+ topics?: T[];
1008
+ /** Filter by event sources */
1009
+ sources?: ClientID[];
1010
+ /** Start timestamp (inclusive) */
1011
+ since?: number;
1012
+ /** End timestamp (inclusive) */
1013
+ until?: number;
1014
+ /** Maximum number of messages to return */
1015
+ limit?: number;
1016
+ }
1017
+ /**
1018
+ * Configuration for message history
1019
+ */
1020
+ interface HistoryConfig {
1021
+ /** Enable message history */
1022
+ enabled: boolean;
1023
+ /** Maximum number of messages to keep in memory (default: 1000) */
1024
+ maxSize?: number;
1025
+ /** Time to live for messages (ms). undefined = no expiration */
1026
+ ttl?: number;
1027
+ }
1028
+ /**
1029
+ * Statistics about message history
1030
+ */
1031
+ interface HistoryStats {
1032
+ /** Total number of messages in history */
1033
+ count: number;
1034
+ /** Unix timestamp (ms) of oldest message */
1035
+ oldestTimestamp?: number;
1036
+ /** Unix timestamp (ms) of newest message */
1037
+ newestTimestamp?: number;
1038
+ /** Memory usage estimate (bytes) */
1039
+ memoryUsage?: number;
1040
+ }
1041
+
1042
+ /**
1043
+ * MessageHistory - In-memory message history
1044
+ *
1045
+ * Features:
1046
+ * - FIFO eviction when maxSize is reached
1047
+ * - TTL-based automatic cleanup
1048
+ * - Glob pattern matching for topics
1049
+ * - Immutable messages (deepFreeze)
1050
+ * - Efficient filtering
1051
+ */
1052
+ declare class MessageHistory<T extends string, P extends Record<T, any>> {
1053
+ #private;
1054
+ constructor(config: HistoryConfig);
1055
+ /**
1056
+ * Record a message to history
1057
+ */
1058
+ record(message: Message<T, P[T]>): void;
1059
+ /**
1060
+ * Query messages from history
1061
+ */
1062
+ query(filter?: HistoryFilter<T>): Promise<HistoryEntry<T, P[T]>[]>;
1063
+ /**
1064
+ * Clear messages from history
1065
+ */
1066
+ clear(filter?: HistoryFilter<T>): Promise<void>;
1067
+ /**
1068
+ * Return a point-in-time snapshot of all entries (oldest → newest).
1069
+ */
1070
+ getSnapshot(): ReadonlyArray<HistoryEntry<T, P[T]>>;
1071
+ /**
1072
+ * Get history statistics
1073
+ */
1074
+ getStats(): HistoryStats;
1075
+ /**
1076
+ * Cleanup and destroy
1077
+ */
1078
+ destroy(): void;
1079
+ }
1080
+
1081
+ /**
1082
+ * Read-only view of a registered bridge.
1083
+ *
1084
+ * Produced by `broker.inspect.getBridges()`. Does NOT leak the internal
1085
+ * `Bridge` instance (`transport`, lifecycle methods, etc.) — only the
1086
+ * information useful for DevTools / debugging.
1087
+ */
1088
+ interface BridgeInfo {
1089
+ /** Unique bridge identifier passed to `addBridge(id, ...)`. */
1090
+ id: string;
1091
+ /** Topic patterns this bridge forwards to its transport. */
1092
+ forwardPatterns: ReadonlyArray<string>;
1093
+ /**
1094
+ * Transport class name with the `Transport` suffix stripped —
1095
+ * e.g. `WebSocket`, `PostMessage`, `SSE`, `BroadcastChannel`, or the
1096
+ * bare constructor name for custom implementations. `undefined` when
1097
+ * the transport was created from an anonymous class expression.
1098
+ */
1099
+ transportKind?: string;
1100
+ }
1101
+
1102
+ /**
1103
+ * Inspector - read-only view over broker state.
1104
+ *
1105
+ * Exposed via `broker.inspect`. Intended for DevTools, debugging tools, and
1106
+ * diagnostic integrations that need point-in-time state snapshots (pull model).
1107
+ * Pair with `broker.$systemEvents` (push model) for incremental updates.
1108
+ *
1109
+ * This class is a pure facade over internal registries — it does NOT own data,
1110
+ * it only aggregates and projects it. Dependencies are injected as references,
1111
+ * so adding new snapshot methods does not require changing the constructor
1112
+ * shape or threading callbacks through `BrokerCore`.
1113
+ *
1114
+ * All collection-returning methods return `ReadonlyArray<T>` to prevent
1115
+ * accidental mutation of broker state by external callers.
1116
+ */
1117
+ declare class Inspector<T extends string, P extends Record<T, any>> {
1118
+ #private;
1119
+ constructor(clients: ClientRegistry<T, P>, subscriptions: Subscriptions<T>, bridges: ReadonlyMap<string, Bridge>, getHistory: () => MessageHistory<T, P> | undefined);
1120
+ /**
1121
+ * Snapshot of every registered client together with its active subscriptions.
1122
+ *
1123
+ * Use together with `$systemEvents.on('client.*' | 'subscription.*')` to
1124
+ * build an accurate initial state without race conditions: read the snapshot
1125
+ * first, then subscribe to events for incremental updates.
1126
+ */
1127
+ getClients(): ReadonlyArray<ClientInfo>;
1128
+ /**
1129
+ * IDs of clients that have at least one active subscription.
1130
+ */
1131
+ getSubscribedClientIds(): ReadonlyArray<ClientID>;
1132
+ /**
1133
+ * Lifecycle info for every registered bridge. Does NOT expose internal
1134
+ * `Bridge` instances (see `BridgeInfo`).
1135
+ */
1136
+ getBridges(): ReadonlyArray<BridgeInfo>;
1137
+ /**
1138
+ * All messages currently stored in the replay buffer (oldest → newest).
1139
+ * Returns an empty array when history is not enabled.
1140
+ */
1141
+ getHistory(): ReadonlyArray<HistoryEntry>;
1142
+ /**
1143
+ * Replay buffer statistics. Always returns `{ enabled: false, count: 0 }`
1144
+ * when history is not enabled.
1145
+ */
1146
+ getHistoryStats(): HistoryStats & {
1147
+ enabled: boolean;
1148
+ };
1149
+ }
1150
+
1151
+ /**
1152
+ * Broker-internal debug channel — see {@link MessageBroker.$debug}.
1153
+ */
1154
+ interface DebugChannel<T extends string, P extends Record<T, any>> {
1155
+ /**
1156
+ * Inject a message into the pipeline with an arbitrary `source`.
1157
+ *
1158
+ * Runs full routing/hooks/history/bridge-forward like a normal emit;
1159
+ * only difference is `message.synthetic === true` and `source` is not
1160
+ * validated against the client registry. Multicast when `target === '*'`,
1161
+ * unicast otherwise (return value from the handler is captured in
1162
+ * `RoutingResult.data`).
1163
+ */
1164
+ send<K extends T, R = unknown>(source: ClientID, topic: K, target: ClientID | '*', data: P[K], options?: MessageOptions): Promise<RoutingResult<R>>;
1165
+ }
1166
+ /**
1167
+ * MessageBroker — the public contract of the broker instance.
1168
+ *
1169
+ * This is the type returned by `initBroker()` / `getBroker()`. It exposes
1170
+ * the stable, user-facing surface of the broker and intentionally HIDES
1171
+ * low-level internals (message pipeline, subscription management, client
1172
+ * registry plumbing) which are accessed via the {@link Client} instances
1173
+ * returned by `createClient()`.
1174
+ *
1175
+ * The broker surface is organized in three groups:
1176
+ *
1177
+ * 1. **Tooling APIs** — stable channels for DevTools / observers.
1178
+ * - `$systemEvents` (push) for lifecycle events.
1179
+ * - `inspect` (pull) for point-in-time state snapshots.
1180
+ *
1181
+ * 2. **Extensibility** — hooks that let adapters and plugins alter or
1182
+ * observe broker behaviour without reaching into internals.
1183
+ * - `useBeforeSendHook`, `useAfterSendHook`, `useOnSubscribeHook`.
1184
+ *
1185
+ * 3. **Infrastructure wiring** — bridges for cross-context delivery.
1186
+ * - `addBridge(id, { transport, forward })`. Pass a {@link BridgeTransport}
1187
+ * instance; built-in implementations are used internally by framework
1188
+ * adapters and are not part of the public surface.
1189
+ *
1190
+ * 4. **Lifecycle**
1191
+ * - `destroy()` for clean shutdown.
1192
+ */
1193
+ interface MessageBroker<T extends string, P extends Record<T, any>> {
1194
+ /**
1195
+ * Broker-internal system event channel (push model).
1196
+ *
1197
+ * The `$` prefix marks this as a broker-internal API. Intended for
1198
+ * tooling: DevTools, tracing collectors, metrics integrations.
1199
+ *
1200
+ * Not for extending broker behaviour — use the `use*Hook` methods
1201
+ * for that.
1202
+ *
1203
+ * @example
1204
+ * broker.$systemEvents.on('client.registered', ({ clientId }) => { ... });
1205
+ * broker.$systemEvents.on('subscription.added', ({ clientId, topic }) => { ... });
1206
+ */
1207
+ readonly $systemEvents: SystemEventsEmitter<T, P>;
1208
+ /**
1209
+ * Point-in-time state snapshots (pull model).
1210
+ *
1211
+ * Read-only view over broker state for DevTools and debugging tools.
1212
+ *
1213
+ * @example
1214
+ * const clients = broker.inspect.getClients();
1215
+ * const history = broker.inspect.getHistory();
1216
+ */
1217
+ readonly inspect: Inspector<T, P>;
1218
+ /**
1219
+ * Broker-internal debug channel (test / DevTools).
1220
+ *
1221
+ * `$debug.send(source, topic, target, data)` runs the full pipeline
1222
+ * with an arbitrary source id and no client-registry side effects.
1223
+ * Messages are tagged `synthetic: true` so tools can distinguish
1224
+ * spoofed traffic from production events. See {@link DebugChannel}.
1225
+ *
1226
+ * The `$` prefix marks this as broker-internal — for DevTools and
1227
+ * integration tests, not for business code.
1228
+ */
1229
+ readonly $debug: DebugChannel<T, P>;
1230
+ /**
1231
+ * Register a bridge for cross-context communication (idempotent).
1232
+ *
1233
+ * A bridge forwards messages whose topic matches `forward` patterns to
1234
+ * the given {@link BridgeTransport}, and injects messages coming back from
1235
+ * the transport into this broker. Framework adapters supply the transport;
1236
+ * this package does not export concrete transport classes.
1237
+ *
1238
+ * If a bridge with the given `id` already exists, the old one is
1239
+ * destroyed and replaced. This keeps the operation HMR-safe.
1240
+ *
1241
+ * @param id - Unique bridge identifier (e.g. `'cross-tab'`, `'iframe-checkout'`).
1242
+ * @param config - Bridge configuration: `transport` + `forward` patterns.
1243
+ * @returns Function that removes the bridge and tears down its listeners.
1244
+ */
1245
+ addBridge(id: string, config: BridgeConfig): () => void;
1246
+ /**
1247
+ * Register a `beforeSend` hook.
1248
+ *
1249
+ * Invoked synchronously before every message enters the routing stage,
1250
+ * for both locally-emitted AND externally-injected (bridge) messages.
1251
+ * Use `message.fromExternal` to distinguish.
1252
+ *
1253
+ * A hook returning `{ allowed: false, message }` short-circuits the
1254
+ * pipeline with a `NACK(HOOK_REJECTED)`.
1255
+ */
1256
+ useBeforeSendHook(hook: BeforeSendHook<T, P>): () => void;
1257
+ /**
1258
+ * Register an `afterSend` hook.
1259
+ *
1260
+ * Invoked after routing completes, regardless of success. Receives the
1261
+ * frozen message and the final {@link RoutingResult}. Runs for both
1262
+ * local and external messages.
1263
+ */
1264
+ useAfterSendHook(hook: AfterSendHook<T, P>): () => void;
1265
+ /**
1266
+ * Register an `onSubscribe` hook.
1267
+ *
1268
+ * Invoked synchronously when a client subscribes to a topic. A hook
1269
+ * returning `{ allowed: false, message }` prevents the subscription —
1270
+ * `BrokerClient.on()` will throw with `message`.
1271
+ */
1272
+ useOnSubscribeHook(hook: OnSubscribeHook<T>): () => void;
1273
+ /**
1274
+ * Shut the broker down and release all resources.
1275
+ *
1276
+ * Destroys every bridge, clears subscriptions, history, hooks and the
1277
+ * client registry. After `destroy()` the broker becomes inert: further
1278
+ * calls are no-ops with console warnings.
1279
+ */
1280
+ destroy(): void;
1281
+ }
1282
+
1283
+ /**
1284
+ * Initialize the message broker (idempotent).
1285
+ *
1286
+ * Called once by the host application (e.g. the shell / app bootstrap).
1287
+ * If the broker is already initialized, the existing instance is returned
1288
+ * without changes. To reinitialize, call {@link destroyBroker} first.
1289
+ *
1290
+ * @param config - Broker configuration (history, etc.).
1291
+ * @returns The broker instance typed against the caller's Topics/Payloads.
1292
+ */
1293
+ declare function initBroker<T extends string = string, P extends Record<T, any> = any>(config?: BrokerConfig): MessageBroker<T, P>;
1294
+ /**
1295
+ * Create or retrieve a client for message communication (idempotent).
1296
+ *
1297
+ * If a client with the given ID already exists, its subscriptions and
1298
+ * backpressure strategies are reset and the existing instance is returned.
1299
+ * Safe for HMR and component re-mounting scenarios.
1300
+ *
1301
+ * Pass explicit type parameters to get a fully typed client without a cast:
1302
+ *
1303
+ * @example
1304
+ * import type { Topic, TopicPayloads } from '@hedwigjs/registry';
1305
+ * const client = createClient<Topic, TopicPayloads>('cart');
1306
+ *
1307
+ * @param id - Unique identifier for the client.
1308
+ * @throws Error if the broker has not been initialized.
1309
+ */
1310
+ declare function createClient<T extends string = string, P extends Record<T, any> = any>(id: string): Client<T, P>;
1311
+ /**
1312
+ * Get the current broker instance.
1313
+ *
1314
+ * Use when you need access to broker methods (hooks, `$systemEvents`,
1315
+ * `inspect`, `addBridge`) without holding the reference returned by
1316
+ * {@link initBroker}.
1317
+ *
1318
+ * Types can be passed explicitly: `getBroker<MyTopics, MyPayloads>()`.
1319
+ *
1320
+ * @throws Error if the broker has not been initialized.
1321
+ */
1322
+ declare function getBroker<T extends string = string, P extends Record<T, any> = any>(): MessageBroker<T, P>;
1323
+ /**
1324
+ * Destroy the broker and release all resources.
1325
+ *
1326
+ * Destroys bridges, clears subscriptions, history, and the client registry.
1327
+ */
1328
+ declare function destroyBroker(): void;
1329
+
1330
+ /**
1331
+ * Configuration for PostMessageTransport
1332
+ */
1333
+ interface PostMessageTransportConfig {
1334
+ /** Target window to communicate with (iframe.contentWindow, window.parent, etc.) */
1335
+ target: Window;
1336
+ /**
1337
+ * Target origin for outbound `postMessage` calls.
1338
+ * Use `'*'` only for trusted contexts — the browser will otherwise refuse
1339
+ * to deliver the message if the target's origin doesn't match.
1340
+ * @default '*'
1341
+ */
1342
+ origin?: string;
1343
+ /**
1344
+ * Explicit allowlist for **inbound** message origins.
1345
+ *
1346
+ * When set, only messages whose `e.origin` is included in this list are
1347
+ * forwarded to the broker; others are dropped with a `console.warn`. This
1348
+ * is the trust boundary between the broker and cross-origin iframes.
1349
+ *
1350
+ * When omitted, inbound validation falls back to `origin`:
1351
+ * - If `origin` is an explicit URL, it acts as a single-item allowlist.
1352
+ * - If `origin` is `'*'`, all origins are accepted (a `console.warn` is
1353
+ * emitted at construction time — this mode is intended only for
1354
+ * trusted contexts and should not be used in production against
1355
+ * untrusted iframes).
1356
+ *
1357
+ * Prefer setting `allowedOrigins` explicitly for cross-origin scenarios.
1358
+ */
1359
+ allowedOrigins?: string[];
1360
+ }
1361
+ /**
1362
+ * PostMessageTransport - Transport for cross-window communication
1363
+ *
1364
+ * Uses `window.postMessage` for iframe/popup communication.
1365
+ *
1366
+ * Security:
1367
+ * - Validates message source window (must match configured `target`).
1368
+ * - Validates message origin against `allowedOrigins` (explicit allowlist)
1369
+ * or `origin` (fallback single-item allowlist).
1370
+ */
1371
+ declare class PostMessageTransport implements BridgeTransport {
1372
+ #private;
1373
+ constructor(config: PostMessageTransportConfig);
1374
+ /**
1375
+ * Send data to target window via postMessage
1376
+ */
1377
+ send(data: unknown): void;
1378
+ /**
1379
+ * Subscribe to incoming messages from target window
1380
+ */
1381
+ onMessage(callback: (data: unknown) => void): () => void;
1382
+ /**
1383
+ * Cleanup: remove event listener
1384
+ */
1385
+ destroy(): void;
1386
+ }
1387
+
1388
+ /**
1389
+ * BroadcastChannelTransport - Transport for cross-tab communication
1390
+ *
1391
+ * Uses BroadcastChannel API for communication between browser tabs
1392
+ * of the same origin.
1393
+ *
1394
+ * Use cases:
1395
+ * - Sync user session across tabs
1396
+ * - Sync theme/locale preferences
1397
+ * - Broadcast notifications to all tabs
1398
+ */
1399
+ declare class BroadcastChannelTransport implements BridgeTransport {
1400
+ #private;
1401
+ /**
1402
+ * @param channelName - Unique channel name for this application
1403
+ */
1404
+ constructor(channelName: string);
1405
+ /**
1406
+ * Broadcast data to all other tabs
1407
+ */
1408
+ send(data: unknown): void;
1409
+ /**
1410
+ * Subscribe to messages from other tabs
1411
+ */
1412
+ onMessage(callback: (data: unknown) => void): () => void;
1413
+ /**
1414
+ * Cleanup: close the channel
1415
+ */
1416
+ destroy(): void;
1417
+ }
1418
+
1419
+ /**
1420
+ * WebSocketTransport - Transport wrapper for WebSocket
1421
+ *
1422
+ * Simple wrapper that forwards messages to/from an existing WebSocket.
1423
+ * All connection management (connect, reconnect, etc.) is handled externally.
1424
+ */
1425
+ declare class WebSocketTransport implements BridgeTransport {
1426
+ #private;
1427
+ /**
1428
+ * @param socket - WebSocket instance (managed externally)
1429
+ */
1430
+ constructor(socket: WebSocket);
1431
+ /**
1432
+ * Send data to server via WebSocket
1433
+ */
1434
+ send(data: unknown): void;
1435
+ /**
1436
+ * Subscribe to messages from server
1437
+ */
1438
+ onMessage(callback: (data: unknown) => void): () => void;
1439
+ /**
1440
+ * Cleanup: remove listener (does NOT close socket)
1441
+ */
1442
+ destroy(): void;
1443
+ }
1444
+
1445
+ /**
1446
+ * Configuration for {@link SSETransport}.
1447
+ */
1448
+ interface SSETransportConfig {
1449
+ /**
1450
+ * URL of the Server-Sent Events endpoint. `EventSource` is created
1451
+ * internally and the browser handles reconnection.
1452
+ */
1453
+ url: string;
1454
+ /**
1455
+ * If set, subscribes to a named SSE event (`event: <name>`) instead of
1456
+ * the default unnamed `message` stream. Broker Messages already carry
1457
+ * their own `topic` field, so most integrations leave this unset and
1458
+ * multiplex on the topic.
1459
+ */
1460
+ eventName?: string;
1461
+ /**
1462
+ * Passed to `new EventSource(url, { withCredentials })`. Enables sending
1463
+ * cookies for same-origin auth on cross-origin SSE endpoints.
1464
+ */
1465
+ withCredentials?: boolean;
1466
+ }
1467
+ /**
1468
+ * SSETransport — inbound-only transport backed by `EventSource`.
1469
+ *
1470
+ * SSE is server → client by design. `send()` is a no-op with a warning;
1471
+ * bridges built on top of this transport are effectively receive-only.
1472
+ * If your integration needs client → server frames, use
1473
+ * {@link WebSocketTransport} or pair SSE with a separate POST endpoint.
1474
+ *
1475
+ * Reconnect handling is delegated to the browser's built-in EventSource
1476
+ * behavior — no external backoff required, unlike WebSocket where the
1477
+ * transport wraps an already-connected socket.
1478
+ *
1479
+ * Expects incoming payloads to be JSON-encoded broker Messages
1480
+ * (`{id, topic, source, target, data, timestamp}`) — the same wire
1481
+ * format all the other bridges use.
1482
+ */
1483
+ declare class SSETransport implements BridgeTransport {
1484
+ #private;
1485
+ constructor(config: SSETransportConfig);
1486
+ /**
1487
+ * SSE has no upstream channel from the browser. This method exists to
1488
+ * satisfy the {@link BridgeTransport} contract but never actually
1489
+ * transmits — it logs a warning so misconfigurations surface early.
1490
+ *
1491
+ * Practical guidance: keep the bridge's `forward` list to topics that
1492
+ * are ONLY emitted by the server (never by local clients), so this
1493
+ * warning never fires in normal operation.
1494
+ */
1495
+ send(_data: unknown): void;
1496
+ /**
1497
+ * Subscribe to incoming SSE messages. Parses JSON payloads before
1498
+ * forwarding to the bridge.
1499
+ */
1500
+ onMessage(callback: (data: unknown) => void): () => void;
1501
+ /**
1502
+ * Cleanup: remove listener and close the underlying EventSource.
1503
+ */
1504
+ destroy(): void;
1505
+ }
1506
+
1507
+ export { type AfterSendHook, type BackpressureOptions, type BeforeSendHook, type BridgeConfig, type BridgeInfo, type BridgeTransport, BroadcastChannelTransport, type BrokerConfig, type BrokerLogEvent, type BrokerLogger, type Client, type ClientID, type ClientInfo, type ClientSubscriptionInfo, type HandlerFn, type HistoryEntry, type HistoryStats, type HookResult, Inspector, type Message, type MessageBroker, type MessageHandler, type MessageOptions, type OnSubscribeHook, PostMessageTransport, type PostMessageTransportConfig, type ReplayOptions, RoutingReason, type RoutingReasonType, RoutingResult, SSETransport, type SSETransportConfig, type SubscriptionOptions, type SystemAnyEventListener, type SystemEventListener, type SystemEventMap, type SystemEventName, type SystemEventPayload, type SystemEventsEmitter, WebSocketTransport, createClient, defaultLogger, destroyBroker, getBroker, initBroker };