Topic-typed application pub/sub with sticky values, wildcards, and scopes.
Module eventBus | Source packages/front/fw/src/io/utils/eventBus.js | Deps none | Worker-safe yes
Each thread instantiates its own bus via eventBus.create(). Cross-tab / cross-worker routing is delegated to broadcastChannel / processMessage. Distinct from fw.events (DOM events): this event bus is purely application-level.
Resolve
const eventBus = runtime.resolve('eventBus');
const bus = eventBus.create();
// Returns: { on, off, emit, sticky, set, clear, scope, topics }
API
| Method | Signature | Returns |
|---|---|---|
create |
() => Bus |
New isolated bus instance |
on |
(topic: string, fn: Function, opts?: {replay?: boolean}) => off |
Subscribes. Returns an unsubscribe function. Topic may be a wildcard 'a:*'. With {replay: true}, equivalent to sticky(topic, fn). |
off |
(topic: string, fn: Function) => void |
Unsubscribes (alternative to the unsubscribe returned by on). |
emit |
(topic: string, data: any) => void |
Synchronous dispatch to all subscribers (including matching wildcards). |
sticky |
(topic: string, fn: Function) => off |
Subscribes AND immediately replays the last value published via set if it exists. |
set |
(topic: string, value: any) => void |
Publishes AND stores the value; future sticky(topic) calls will receive it. |
clear |
(topic?: string) => void |
Clears the sticky value of a topic, or all if topic is omitted. |
scope |
() => { on, sticky, dispose } |
Subscription group; dispose() unsubscribes all on/sticky in the scope. |
topics |
() => string[] |
List of active topics (subscribers + sticky). Debug use. |
bus.on(topic, fn)
Subscribes fn to the topic. The topic may be a wildcard pattern 'namespace:*'. Returns an off() function.
Wildcard rules:
'a:*'matches'a:b','a:b:c'; not'a'nor'b:a'.- Only one star allowed, required at the end of the pattern (
'a:*:b'→ throws). - No global
'*'pattern (throws).
bus.emit(topic, data)
Synchronous dispatch. If a handler throws, the error is captured via console.error and the other handlers still run. Calling emit on a topic with no subscriber is a no-op.
bus.sticky(topic, fn), bus.on(topic, fn, {replay:true}) and bus.set(topic, value)
Three primitives that form the sticky / replay model:
set(topic, value)— stores the latest value of the topic AND publishes it to all current subscribers (exact + wildcards). The sticky store is internal and persists untilclear(topic)orclear().sticky(topic, fn)— subscribes to the topic AND immediately replays the last value published viasetif it exists (otherwise only delivered on the nextset/emit).on(topic, fn, {replay: true})— additive alias ofsticky(topic, fn)(N.41). Unifies the subscription API: a singleon()whose sticky replay is controlled by options.{replay: false}or omitted option = classic behavior without replay.
sticky() remains exposed for backward compatibility; both forms are strictly equivalent from the observable side.
bus.scope()
Returns an object { on, sticky, dispose } with the same signature as the bus methods. All subscriptions created via this scope are atomically unsubscribed by dispose(), without affecting subscriptions outside the scope.
Examples
Log streaming
const eventBus = runtime.resolve('eventBus');
const bus = eventBus.create();
// Producer
function logError(msg) {
bus.emit('log:error', { msg, ts: Date.now() });
}
// Consumer
const off = bus.on('log:*', (entry) => {
console.error('[LOG]', entry.msg);
});
logError('something went wrong');
// → [LOG] something went wrong
off(); // unsubscribe
Sticky — shared configuration
const bus = eventBus.create();
// Publish config as soon as it's available
bus.set('app:config', { theme: 'dark', lang: 'fr' });
// Module loaded later receives the value immediately
bus.sticky('app:config', (cfg) => {
applyTheme(cfg.theme);
});
// Equivalent form via `on` + option (J2 #00 / N.41)
bus.on('app:config', (cfg) => applyTheme(cfg.theme), { replay: true });
Scope — automatic cleanup on module teardown
const bus = eventBus.create();
function mountWidget(el) {
const s = bus.scope();
s.on('window:resize', () => layoutWidget(el));
s.sticky('app:config', (cfg) => applyConfig(el, cfg));
return {
destroy() {
s.dispose(); // all subscriptions unsubscribed in one line
}
};
}
const widget = mountWidget(document.getElementById('my-widget'));
// ... later
widget.destroy();
Worker Usage
const worker = fw.createWorker(
function ({ libs }) {
const bus = libs.eventBus.create();
bus.on('task:progress', (pct) => {
self.postMessage({ type: 'progress', pct });
});
// Run a long task and emit progress
for (let i = 0; i <= 100; i++) {
bus.emit('task:progress', i);
}
},
{ dependencies: ['eventBus'] }
);
Notes
- Dispatch is synchronous — no microtask, no
Promise. This guarantees ordered cleanup when anemitis immediately followed by an assertion in unit tests. - Wildcards are indexed separately from exact topics to avoid scanning all subscribers on every
emit. - Each
create()instance is completely isolated: no shared state between instances, compatible with multi-Worker usage. topics()is a debug API; do not use in critical application logic (may include phantom patterns ifoffhas not yet been called).
See also
- events — named DOM event handler
- broadcastChannel — cross-tab / cross-worker communication
- scope pattern — fw module pattern