Broadcast buses and messengers
BroadcastTypedEventBus<EVENT> decorates a local TypedEventBus<EVENT> with cross-context delivery. The transport is notification-only: there is no acknowledgment, remote completion wait, replay, or delivery guarantee.
Keep the delegate when constructing a broadcast bus so its handlers can be cleaned up separately. destroy() closes only a messenger the bus created. A messenger passed in options.messenger belongs to the caller: destroy() detaches the bus from it (its onmessage is replaced by a no-op) and leaves it open, so close it yourself once nothing else uses it. Because a messenger has one onmessage, still give each independently owned broadcast bus its own messenger.
Broadcast options and flow
new BroadcastTypedEventBus(options: BroadcastTypedEventBusOptions<EVENT>) requires delegate. Its type and handlers come from the delegate; on/off forward to it. Default messenger is createCrossTabMessenger('_broadcast_:' + delegate.type); construction throws Error('Messenger setup failed') if none is available.
| Option/member | Default | Contract |
|---|---|---|
messenger?: CrossTabMessenger | Runtime-selected transport | Inject to control environment/channel ownership. |
messageTransformer.serialize(event) | No transformer | Convert outbound event into wire data. |
messageTransformer.deserialize(message) | No transformer | Decode incoming wire data into EVENT. |
serializeBeforeDispatch? | false | True snapshots before local handlers; false serializes after local delivery. |
fallbackSerialize?(message, error) | None | On a postMessage throw, transform once and retry postMessage once. |
destroy() | — | Closes the messenger it created, or only detaches a supplied one (left open for its owner); does not destroy delegate or remove its handlers. Later emits run local handlers and post nothing. |
emit awaits the local delegate first, then posts the message (with optional pre-serialization). Outbound serialization/post failures reject emit; local delivery may already have happened. A post that throws (DataCloneError for a non-cloneable event on a BroadcastChannel, say) rejects unless fallbackSerialize is set and its retry succeeds. After destroy(), emit skips serialization and posting and resolves once local delivery completes. A pre-serialization throw prevents local delivery. Incoming messages deserialize and emit only on the delegate, so they are not rebroadcast; decode/delegate rejections are caught and warned. The mutable messageTransformer affects later operations.
CrossTabMessenger
The contract is postMessage(message: any): void, a setter onmessage: CrossTabMessageHandler, and close(): void; handler type is (message: any) => void. Setting onmessage replaces the callback. A messenger never receives its own posts, but other messengers on the same channel in the same tab differ by transport: BroadcastChannelMessenger delivers to them (BroadcastChannel reaches every other channel object, same document included), while StorageMessenger does not, because a storage event never fires in the document that wrote it. With the storage fallback, two buses in one tab do not hear each other.
isBroadcastChannelSupported() checks the global and prototype postMessage. isStorageEventSupported() checks StorageEvent, window.addEventListener, and localStorage or sessionStorage availability. These are feature probes, not permission tests. createCrossTabMessenger(channelName) prefers BroadcastChannelMessenger, then StorageMessenger, otherwise returns undefined. Construction errors are not silently converted to fallback.
new BroadcastChannelMessenger(channelName) wraps native BroadcastChannel, forwards MessageEvent.data, and uses structured cloning; unsupported data can throw DataCloneError. Where the channel has unref() (Node), it is unrefed, so an open messenger does not keep the process alive. close() closes its channel.
StorageMessenger
new StorageMessenger(options: StorageMessengerOptions) requires a browser with window and localStorage even when injecting a backend. Options are required channelName, optional storage (localStorage), ttl (1000 ms), and cleanupInterval (60000 ms).
Each post JSON-encodes StorageMessage {data: any, timestamp: number} under a unique channel-prefixed storage key, then schedules key deletion after ttl. Periodic cleanup removes expired/invalid messages matching that channel. Receiving filters by storageArea and key format; invalid JSON warns. TTL controls cleanup, not reliable replay or a receive-age filter. Native storage events do not notify their originating document.
close() removes the storage listener, clears the interval and pending deletion timers, and removes the message keys this messenger wrote that were still waiting for deletion. JSON stringify, quota, or access failures can throw on posting. A sessionStorage backend has that platform's restricted sharing scope. The sender's local delegate still handles local delivery independently.
Complete example
import {
BroadcastTypedEventBus,
SerialTypedEventBus,
createCrossTabMessenger,
} from '@ahoo-wang/fetcher-eventbus';
const messenger = createCrossTabMessenger('settings-demo');
if (messenger) {
const delegate = new SerialTypedEventBus<{ theme: string }>('settings');
const bus = new BroadcastTypedEventBus({ delegate, messenger });
bus.on({
name: 'ui',
handle: event => {
console.log(event.theme);
},
});
try {
await bus.emit({ theme: 'dark' });
} finally {
bus.destroy();
delegate.destroy();
messenger.close();
}
}Public symbols and source
| Symbol | Implementation |
|---|---|
BroadcastTypedEventBusOptions | broadcastTypedEventBus.ts:30 |
BroadcastTypedEventBus | broadcastTypedEventBus.ts:127 |
BroadcastChannelMessenger | broadcastChannelMessenger.ts:19 |
CrossTabMessageHandler | crossTabMessenger.ts:17 |
CrossTabMessenger | crossTabMessenger.ts:32 |
isBroadcastChannelSupported | crossTabMessenger.ts:53 |
isStorageEventSupported | crossTabMessenger.ts:60 |
createCrossTabMessenger | crossTabMessenger.ts:70 |
StorageMessengerOptions | storageMessenger.ts:19 |
StorageMessage | storageMessenger.ts:27 |
StorageMessenger | storageMessenger.ts:35 |
