Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[strictDestroy]
Added
destroy(reason?)anddestroyed—destroyaccepts an optional reason, kept from the first call.Bus#destroyedbecomestrueonce teardown completes (stillfalseinsidewillDestroyhooks). It is optional onIntrospectionSurfacefor now, so hand-rolled implementations need not add it.clear(options?)— onBusandControlSurface. Resets a bus without destroying it: lifecycle hooks stay registered and see the removals, anddestroyedstaysfalse. TakesIntrospectionOptions, defaulting toscope: ListenerScope.ANYandincludeIncognito: true.OWNcancels pendingnext/scanpromises and releases this bus’s subscribers;DOWNSTREAMsevers edges to buses it pipes into;ANYdoes both and also detaches from upstreams.includeIncognito: falseleaves incognito subscriptions, promises, and edges in place. UnderonUseAfterDestroy,clear()on a destroyed bus is a silent no-op. Hand-rolledControlSurfaceimplementations must add it.options.onUseAfterDestroy(defaultfalse) —false|'debug'|'info'|'warn'|'error'|'throw'. When set, calling anyMonitoringSurfaceorSubscriptionSurfacemethod on a destroyed bus throws, or logsStrongbusLogCode.UseAfterDestroy(1016) at that level, naming the method and the destroy reason. Thrown errors carry the samecode;next/scanreject with that error instead of throwing synchronously. When not throwing, the call is skipped and returns an inert stub: a no-opSubscriptionfrom subscribe/hook methods, the downstream frompipe, and a never-settlingCancelablePromisefromnext/scan.emitanddestroy(ControlSurface/EmittingSurface) become silent no-ops under any setting other thanfalse;emitreturnsfalse. Introspection is unaffected. Piping a live bus into a destroyed one is reported under the destroyed bus’s setting and creates no edge. See Teardown.
Removed
- Runtime
Scannerexport —Scanneris now exported as a type only (Scanner.Evaluator,Scanner.Resolver, and friends still work in type positions). Usebus.scan(...)instead of constructing aScanner.
Fixed
- Upstreams of a destroyed bus —
destroy()released only the bus’s own downstream edges. An upstream that piped into a destroyed bus kept forwarding events to it, butdestroy()had cleared the hooks that report the bus’s listener changes upstream. Listeners added afterwards received events without updating the upstream’sactivestate or lifecycle hooks, and its listener counts went stale until something else refreshed them.destroy()now detaches the bus from every upstream, so upstreams stop forwarding to it. - Calls made during
destroy()—destroy()released subscribers before firingwillDestroy, so a hook (or any lifecycle hook fired by the teardown) that subscribed or callednext/scanleft listeners behind on the destroyed bus, and those promises never settled. A hook that calleddestroy()re-entered teardown and recursed. While teardown runs,destroyandclearare now ignored, andMonitoringSurface/SubscriptionSurfacemethods are skipped under anyonUseAfterDestroysetting, loggingStrongbusLogCode.UseDuringDestroy(1015) atdebug. They return the same inert stubs, except thatnext/scanreturn an already-canceled promise.emitstill runs.
[3.0.0]
v3 tightens the subscription API around a smaller, more explicit set of methods
and makes next and scan easier to type. The behavioral core (emitting,
piping, scanning, memory-leak detection) is largely unchanged; lifecycle hook
ordering and downstream piping behavior are tightened (see Fixed and Changed
below).
See the Migration guide for step-by-step changes.
Added
- Surface brand helpers — each surface (
SubscriptionSurface,MonitoringSurface,ControlSurface,EmittingSurface,IntrospectionSurface) is now a callable + namespace withBRAND/Branded. Domain objects brand only the surfaces they expose (obj[SubscriptionSurface.BRAND] = bus); consumers unwrap withSubscriptionSurface(obj).on(...)instead of flattened method aliases. EmittingSurface<TEventMap>—emitonly. Use when a dependency may raise events but must not destroy the bus.ControlSurfaceextends it withdestroy.duplicateSubscriptionStrategy— bus option controlling duplicate listenable+handler registrations along four axes (observability,invocation,disposal,logLevel), eachcollapse|stackexceptlogLevel(never|debug|info|warn|error). Defaults are allcollapsewithlogLevel: 'warn'(warns on duplicates; emit/count still collapsed). Applies fully toon,any, andtap.oncehonors observability, invocation, and logLevel with kind-isolated disposal (off/ disposingonnever clearsoncefor the same handler, and vice versa). Named presets:DuplicateSubscriptionStrategy.EventEmitter,.EventTarget,.SharedHandler.Logger.debugis now required on custom loggers (used whenduplicateSubscriptionStrategy.logLevelis'debug').once(event, handler)— subscribe to a single event and automatically unsubscribe after the first emission.off(event, handler)— remove a handler previously registered withonby the same function reference. Returnsvoid(not the bus). Prefer theSubscriptionfromonwhen available;offdoes not remove wrappers fromonce,any, ortap.SubscribeOptions/{incognito: true}— optional trailing options onon,once,any,tap,pipe(bus),next, andscan. An incognito registration still receives or forwards events but does not count toward this bus’s monitoring (active/idle,monitor, listener lifecycle hooks, or default introspection).pipe(bus, {incognito: true})forwards events without coupling the target’s listener tree into the source’s monitoring (the target’s own monitoring is unchanged). Duplicateonwith the same handler under defaultduplicateSubscriptionStrategykeeps the first registration’s mode. Memory-leak logger thresholds still count own incognito handlers.ScanOptionsextendsSubscribeOptions; pooled scans never share a scanner across differentincognitomodes. See Incognito subscriptions.-
tap(handler)— observe every raised event as a correlated{event, payload}message without creating a graph edge. Replaces the oldpipe(sink)observer role (and removesforward). Discriminating onmessage.eventcorrelatively narrowsmessage.payload.TapHandler/PipedMessageare the exported types. DuplicatetapfollowsduplicateSubscriptionStrategy.bus.tap((piped) => { if (piped.event === 'foo') { piped.payload.toUpperCase(); } }); pipe(predicate).pipe(dest)— call-site filter for multi-hop relay. Unfiltered outbound edges from a bus that already has inbound pipes warn once per unique unsound path and block passthrough; local raises still deliver. See docs/pipe_limitations.md.ASSUMED_SOUND_EDGE— exported() => truepipe predicate forbus.pipe(ASSUMED_SOUND_EDGE).pipe(dest), signaling that the author of the calling code trusts the multi-hop path is sound.EventSink<TEventMap>handler type — the(event, payload)handler shape used byany.StrongbusLogRecord/StrongbusLogCode— Strongbus-authored log lines are{code, message, context?}records with stable numeric codes. Customoptions.loggerimplementations receivelevel(record)and can discriminate onrecord.code; structured extras (e.g. error-handler failure details) live on optionalrecord.context. Both are exported from the package root. See Changed / migration for theLoggermethod signature break.ControlSurface<TEventMap>—emitanddestroy(extendsEmittingSurface).EmittingSurface<TEventMap>—emitonly.SubscriptionSurface<TEventMap>— subscribe, await, scan, and pipe (on,once,off,any,next,scan,tap,pipe,unpipe), including optionalSubscribeOptionson subscribe/pipe/await methods. Use when a component should listen but must not raise events.IntrospectionSurface<TEventMap>— scoped listener introspection (hasListeners,getListenerCount,getListeners,getEventCount,hasListenersFor,getListenerCountFor,getListenersFor,forEach).MonitoringSurface<TEventMap>— lifecycle observation (monitor,hook,active).ListenerScope— selects own, downstream (piped bus), or combined (ANY) handlers for listener introspection.ANYis equivalent toListenerScope.OWN | ListenerScope.DOWNSTREAM.DOWNSTREAMcovers listeners on buses attached withpipe(bus)only, nottaphandlers.IntrospectionOptions—{scope?: ListenerScope; includeIncognito?: boolean}accepted by the listener-introspection methods.includeIncognitodefaults tofalse(incognito own handlers and incognito-piped trees are omitted). Lifecycle /active/monitornever consult this flag. Exported from the package root along withSubscribeOptions.Bus.configure(options)— merge partialOptions(exceptname) onto static defaults for all subsequently constructed instances. Nestedthresholdsare merged recursively. Use the constructor for per-instancename.Merge<Base, Ext>— a flattening event-map merge (overlapping keys takeBase) for composing event maps. UnlikeBase & Ext,Mergekeeps indexed access concrete, so a generic base class (new Bus<Merge<Fixed, TGeneric>>()) canemitits fixed events with literal payloads without casting toany. See Composing event maps.options.coalesceDownstreamLifecycleEvents(defaulttrue) — whentrue, a source bus emits at most onewillAddListener/didAddListener(and matching remove hooks) per event key per downstream lifecycle episode duringpipe()/unpipe()reconcile when the downstream bus already has listeners, instead of once per downstream handler. Listener counts and introspection are unchanged; only hook emissions are coalesced for that reconcile. Incremental downstream listener changes after the link is established still emit hooks per listener. Set tofalsefor one hook pair per listener during reconcile (see Lifecycle hook ordering).
Changed
Loggermethod arguments areStrongbusLogRecord— each ofinfo/warn/error/debugis now(record: StrongbusLogRecord) => void, not(...args: any[]) => void. Strongbus always invokeslevel(record)with a single{code, message, context?}value. Custom loggers that forwarded rest args toconsoleor another sink must unpackrecord.message/record.context(or handlerecordas a unit).debugis required (invoked whenoptions.duplicateSubscriptionStrategy.logLevelis'debug'). Passing bareconsoleasoptions.loggeris no longer appropriate — omitloggerto use the built-in console adapter, or pass your own record-shapedLogger.-
Listener introspection — scoped methods on
Bus/IntrospectionSurface:hasListeners,getListenerCount,getListeners,getEventCount,hasListenersFor,getListenerCountFor,getListenersFor, andforEach(event-first callback). Each takes optionalIntrospectionOptions(scopeand/orincludeIncognito);scopedefaults toListenerScope.ANYandincludeIncognitodefaults tofalse.getListenersForreturns an empty set when none are registered.forEachcallback event keys are compile-time narrowed only; at runtime all registered keys are visited. on(event, handler)is reference-idempotent with defaultoptions.duplicateSubscriptionStrategy— a secondonwith the same event and handler returns the existingSubscription(one emit invocation, one remove lifecycle).emit(event, payload)takes a single correlated payload instead of a rest-spread (emit(event, ...payload)). The payload type is nowTEventMap[T]directly, so a call type-checks even when the event map is a generic type parameter (e.g.new Bus<TEvents>()inside a generic base class) — previously the rest tupleEventPayload<TEventMap, T>failed to resolve against an abstract map. Payload is required for non-void events; void events may still be emitted asemit(event)oremit(event, null). Anything the type system already accepted keeps working; only genuine multi-arg spreads (which were never typeable) are gone. Generic-key forwarding (emit<K extends keyof M>(event: K, payload: M[K])) works over concrete maps.ControlSurface.emitadopts the same correlated shape. A correlated-tuple overload remains for call sites that discriminated oneventfirst. Observers usetapand receive{event, payload}as one value; graph edges usepipe(bus)rather than splitting the pair.options.onUnhandledEventreplacesallowUnhandledEvents—'ignore'(default),'throw', or a(event, payload) => voidcallback. SubclassinghandleUnexpectedEventis no longer the customization path; pass a callback (or'throw') instead.Bus.defaultAllowUnhandledEvents = falsestill maps toconfigure({onUnhandledEvent: 'throw'}).-
PipePayloadOverlapuses tuple equality ([A] extends [B]) so narrow-to-widepipe/forwardtargets type-check when the source map is an open generic and the downstream map is a concrete superset (e.g._incomingPushBus.pipe(_bus)). Shared keys also allow a one-way widen within the same primitive family (string/boolean/number, including literal unions such as'a'|'b' → stringor1|2 → number); object and other structured payloads still require an exact match. The unsafe reverse (string → 'a'|'b') remains a type error. pipe(bus)returns the concrete downstream type (TDownstream), preserving subclasses for chaining (e.g.head.pipe(mid).pipe(tail)).on(event, handler)only accepts a single event key. It no longer forwards arrays toanyor'*'toproxy.next(...)resolves with{event, payload}instead of the bare payload (single event) orundefined(array/wildcard). Wildcard ('*') triggers are not accepted; usescanwithtrigger: '*'when you need any-event listening with evaluator-side discrimination (see migration guide).scan<T>(...)— the type parameter is now the resolved value type rather than the evaluator type. Inference from a typedevaluatoris unchanged.- Public handler types reduced to
EventHandlerandEventSink. generateSubscriptionis renamed tosubscriptionWrapper(exported from the package root).- Lifecycle hook ordering — transition-centric bracketing on every bus. See Lifecycle hook ordering in the README.
options.verbosedefault is nowfalse— memory-leak threshold messages log at boundaries and multiples only. v2 defaulted totrue(every listener above a threshold). Setverbose: truein constructor or globally withBus.configure({verbose: true}to restore v2-style logging.
Deprecated
SingleEventHandler— deprecated alias forEventHandler.Bus.defaultAllowUnhandledEvents,Bus.defaultThresholds,Bus.defaultLogger, andBus.verbosestatic setters — useBus.configure()instead.
Removed
-
options.allowUnhandledEvents— useoptions.onUnhandledEvent('ignore''throw'callback). Bus#handleUnexpectedEvent— override path removed; useonUnhandledEvent: 'throw'or a callback.proxy(handler)andevery(handler)— usetap(handler).on('*', handler)andon([...], handler)overloads — usetap(handler)andany([...], handler)respectively.- Handler types
MultiEventHandler,WildcardEventHandler— useEventSink(forany) orTapHandler(fortap). GenericHandleris no longer exported — it was an internal type.- Per-event listener helpers (v2) —
hasListenersFor,getListenerCountFor, etc. without a scope parameter. listenersandownListenersReadonlyMapgetters (v2).hasListeners/hasOwnListeners/hasDownstreamListenersproperties,listenerCount, and related per-scope method triplets — use the scoped introspection API withListenerScope.
Fixed
- Downstream listeners before
pipe()— when a downstream already has listeners (including via nestedpipelinks) at the momentpipe(bus)runs, the upstream bus now bubbles the corresponding add-listener lifecycle hooks, updates listener counts, and transitions toactiveas if those handlers had been present from the start.unpipesymmetrically bubbles remove hooks, clears downstream-scoped introspection, and marks the upstream bus idle when that downstream was its only remaining listener demand.willIdle/idleare not emitted when another downstream (or own listener) still has demand. scanwithoptions.timeoutno longer schedules a timer when the evaluator settles eagerly — if the evaluator resolves or rejects during eager evaluation (eager: true, the default), the scan is already settled before any timer would be needed, so none is created. Previously a timeout scan always armed its timer, leaving a pending timer for the full duration even though it could no longer affect the result — which kept Node processes and test runners alive until it fired. Behavior for scans that do not settle eagerly is unchanged: the timeout still cancels the scan after N milliseconds.- Variance: a
Bus<Wide>is assignable toSubscriptionSurface<Narrow>,IntrospectionSurface<Narrow>, and other narrower views, so consumers can declare a narrower event map while still preventing subscription to events outside it. The same Wide→Narrow assignability holds surface-to-surface.scan,any,next,pipe, and listener introspection methods participate in this narrowing.
Internal
- Extracted lifecycle orchestration into
LifecycleManager(lifecycleManager.ts). - Extracted scanner pooling into a dedicated
ScannerPoolsclass. - Expanded test coverage: logger thresholds and memory-pressure messages,
error-handler failures,
subscriptionWrapper,emitreturn value, and compile-time type-safety/variance assertions.
Migrating from v2 to v3
| v2 (removed or changed) | v3 equivalent |
|---|---|
bus.on('*', handler) |
bus.tap(handler) |
allowUnhandledEvents: false |
options.onUnhandledEvent: 'throw' |
allowUnhandledEvents: true (default) |
options.onUnhandledEvent: 'ignore' (default) or omit |
subclass handleUnexpectedEvent |
options.onUnhandledEvent: (event, payload) => { ... } |
feeder.on('*', hub.emit) |
feeder.pipe(hub) |
bus.on([...events], handler) |
bus.any([...events], handler) |
bus.proxy(handler) |
bus.tap(handler) |
bus.every(handler) |
bus.tap(handler) |
const payload = await bus.next('foo') → payload |
const {payload} = await bus.next('foo') |
await bus.next([...]) → undefined |
const {event, payload} = await bus.next([...]) |
bus.next('*', ...) |
bus.next([...events]) or bus.scan('*', evaluator, options?) — see Wildcard ('*') triggers on next |
bus.scan({evaluator, trigger, ...}) |
bus.scan(trigger, evaluator, options?) — object form deprecated |
bus.scan<typeof evaluator>(...) |
bus.scan<ResolvedType>(...) |
generateSubscription(dispose) |
subscriptionWrapper(dispose) |
EventHandler<Map, 'foo'> |
EventHandler<Map, 'foo'> (unchanged) |
MultiEventHandler<Map> |
EventSink<Map> |
WildcardEventHandler<Map> |
TapHandler<Map> |
GenericHandler (exported) |
(internal; not part of the public API) |
bus.listeners |
bus.forEach((event, handlers) => ...) or bus.forEach((event, handlers) => ..., {scope: ListenerScope.ANY}) |
bus.listeners.get('foo') |
bus.getListenersFor('foo') or bus.getListenersFor('foo', {scope: ListenerScope.ANY}) |
bus.ownListeners |
bus.forEach((event, handlers) => ..., {scope: ListenerScope.OWN}) |
bus.hasListeners (property) |
bus.hasListeners() or bus.hasListeners({scope: ListenerScope.ANY}) |
bus.hasOwnListeners |
bus.hasListeners({scope: ListenerScope.OWN}) |
bus.hasDownstreamListeners |
bus.hasListeners({scope: ListenerScope.DOWNSTREAM}) |
bus.listenerCount |
bus.getListenerCount() or bus.getListenerCount({scope: ListenerScope.ANY}) |
bus.listenerEventCount |
bus.getEventCount() or bus.getEventCount({scope: ListenerScope.ANY}) |
bus.ownListenerEventCount |
bus.getEventCount({scope: ListenerScope.OWN}) |
bus.downstreamListenerEventCount |
bus.getEventCount({scope: ListenerScope.DOWNSTREAM}) |
bus.hasListenersFor('foo') |
bus.hasListenersFor('foo') or bus.hasListenersFor('foo', {scope: ListenerScope.ANY}) |
bus.hasOwnListenersFor('foo') |
bus.hasListenersFor('foo', {scope: ListenerScope.OWN}) |
bus.hasDownstreamListenersFor('foo') |
bus.hasListenersFor('foo', {scope: ListenerScope.DOWNSTREAM}) |
bus.getListenerCountFor('foo') |
bus.getListenerCountFor('foo') or bus.getListenerCountFor('foo', {scope: ListenerScope.ANY}) |
bus.getOwnListenerCountFor('foo') |
bus.getListenerCountFor('foo', {scope: ListenerScope.OWN}) |
bus.getDownstreamListenerCountFor('foo') |
bus.getListenerCountFor('foo', {scope: ListenerScope.DOWNSTREAM}) |
bus.getListener('foo') |
bus.getListenersFor('foo') or bus.getListenersFor('foo', {scope: ListenerScope.ANY}) |
bus.getOwnListener('foo') |
bus.getListenersFor('foo', {scope: ListenerScope.OWN}) |
bus.getDownstreamListener('foo') |
bus.getListenersFor('foo', {scope: ListenerScope.DOWNSTREAM}) |
bus.getListenerCount('foo') |
bus.getListenerCountFor('foo') or bus.getListenerCountFor('foo', {scope: ListenerScope.ANY}) |
bus.getOwnListenerCount('foo') |
bus.getListenerCountFor('foo', {scope: ListenerScope.OWN}) |
bus.getDownstreamListenerCount('foo') |
bus.getListenerCountFor('foo', {scope: ListenerScope.DOWNSTREAM}) |
bus.forEachListener((handlers, event) => ...) |
bus.forEach((event, handlers) => ...) or bus.forEach((event, handlers) => ..., {scope: ListenerScope.ANY}) |
bus.forEachOwnListener((handlers, event) => ...) |
bus.forEach((event, handlers) => ..., {scope: ListenerScope.OWN}) |
bus.forEachDownstreamListener((handlers, event) => ...) |
bus.forEach((event, handlers) => ..., {scope: ListenerScope.DOWNSTREAM}) |
custom Logger with info/warn/error/debug as (...args: any[]) => void |
accept (record: StrongbusLogRecord) => void; add debug if missing |
options.logger: console (or console-shaped rest-arg sink) |
omit logger (falls back to built-in console adapter), or wrap record.message / record.context |
Import ListenerScope from 'strongbus' wherever the v3 column uses it. ListenerScope.ANY is equivalent to ListenerScope.OWN | ListenerScope.DOWNSTREAM. ListenerScope.DOWNSTREAM covers listeners on buses attached with pipe(bus) only, not tap handlers (those are ListenerScope.OWN).
Custom Logger: record argument + required debug
Logger methods no longer take rest args. Each level receives one
StrongbusLogRecord:
// v2
const logger: Logger = {
info: (...args) => console.info(...args),
warn: (...args) => console.warn(...args),
error: (...args) => console.error(...args),
// debug often omitted
};
// v3
import {
StrongbusLogCode,
type Logger,
type StrongbusLogRecord
} from 'strongbus';
const logger: Logger = {
info(record) {
console.info(record.message, record.context);
},
warn(record) {
console.warn(record.message, record.context);
},
error(record) {
console.error(record.message, record.context);
},
debug(record) {
console.debug(record.message, record.context);
}
};
// Discriminate without parsing message text:
function onLog(record: StrongbusLogRecord): void {
if (record.code === StrongbusLogCode.DuplicateSubscription) {
// ...
}
}
// Omit `logger` to use the built-in console adapter.
new Bus();
debug is required alongside info / warn / error. Strongbus calls it when
options.duplicateSubscriptionStrategy.logLevel is 'debug'. A no-op debug
is fine if you do not care about that level.
on with arrays or the wildcard
on is now single-event only. Move array and wildcard subscriptions to any
and tap.
// v2
bus.on('foo', onFoo);
bus.on(['foo', 'bar'], (event, payload) => { /* ... */ });
bus.on('*', (event, payload) => { /* ... */ });
// v3
bus.on('foo', onFoo); // unchanged
bus.any(['foo', 'bar'], (event, payload) => { /* ... */ }); // arrays -> any
bus.tap(({event, payload}) => { /* ... */ }); // '*' -> tap
proxy / every → tap
Both are removed; tap covers them.
// v2
const sub = bus.proxy((event, payload) => { /* ... */ });
const sub2 = bus.every((event, payload) => { /* ... */ });
// v3 — the handler receives one correlated { event, payload } message
const sub = bus.tap(({event, payload}) => { /* ... */ });
Hubs: feeder.pipe(hub)
v2 often used feeder.on('*', hub.emit). v3 removes the '*' subscription; pipe feeders
into the hub directly:
feeder.pipe(hub);
For multi-hop bridges with disagreeing maps, use mid.pipe(pred).pipe(leaf) — see
docs/pipe_limitations.md.
next resolves with {event, payload}
// v2
const payload = await bus.next('foo'); // payload: TEventMap['foo']
await bus.next(['foo', 'bar']); // resolved with undefined
// v3
const {payload} = await bus.next('foo'); // payload: TEventMap['foo']
const {event, payload} = await bus.next(['foo', 'bar']);
// event is narrowed to 'foo' | 'bar'; payload is the matching payload type
Wildcard ('*') triggers on next
next no longer accepts '*' as a resolution or rejection trigger. next always resolves on the first
matching event. In the type system, the wider events are transparent to the narrower bus, however at runtime they do get piped into the narrower bus, so a wildcard handler could unintentionally resolve/reject a next on an event the narrow bus is oblivious to.
scan still accepts trigger: '*' because the evaluator can discriminate on resolve.trigger before
resolving.
// before — compile-time allowed on next, payload typing was unsound
await bus.next('*');
// v3 — list events when using next
await bus.next(['foo', 'bar', 'baz']);
// v3 — wildcard with conditional resolve via scan
await bus.scan('*', (resolve) => {
if (resolve.trigger.type === 'event' && shouldAccept(resolve.trigger)) {
resolve(resolve.trigger);
}
});
scan signature and type argument
The positional form is preferred. The object form
scan({evaluator, trigger, ...options}) is deprecated but still supported. The
type argument is the resolved value type (not the evaluator type). Inference from
a typed evaluator is unchanged.
// v2
bus.scan<typeof myEvaluator>({evaluator: myEvaluator, trigger: 'foo'});
// v3 (preferred)
bus.scan<boolean>('foo', myEvaluator);
bus.scan<boolean>('foo', myEvaluator, {eager: false, pool: false, timeout: 1000});
// v3 (deprecated object form — still supported)
bus.scan<boolean>({evaluator: myEvaluator, trigger: 'foo'});
// inference is unchanged in both versions
const ready = await bus.scan('foo', myEvaluator);
Renamed handler types
// v2
import type {EventHandler, MultiEventHandler, WildcardEventHandler} from 'strongbus';
// v3 — single-event handlers, any sinks, and tap observers
import type {EventHandler, EventSink, TapHandler, PipedMessage} from 'strongbus';
MultiEventHandler maps to EventSink (the (event, payload) handler used by
any). WildcardEventHandler maps to TapHandler, which receives a single
correlated {event, payload} PipedMessage (tap((message) => …)). A
single-event handler that was typed via EventHandler<Map, 'foo'> in v2 is still
EventHandler<Map, 'foo'> in v3.
Renamed subscription helper
// v2
import {generateSubscription} from 'strongbus';
// v3
import {subscriptionWrapper} from 'strongbus';
Narrower consumer views
Prefer SubscriptionSurface<Narrow> when a component should subscribe but not emit, or IntrospectionSurface<Narrow> when it only inspects listeners. A Bus<Wide> remains assignable to either narrower view.
// v3
import type {SubscriptionSurface, IntrospectionSurface} from 'strongbus';
function consume(source: SubscriptionSurface<Pick<MyEvents, 'foo' | 'bar'>>) {
source.on('foo', handler);
}
function inspect(source: IntrospectionSurface<Pick<MyEvents, 'foo' | 'bar'>>) {
source.getListenerCountFor('foo');
}
Listener introspection
import {ListenerScope} from 'strongbus';
// v2
if (bus.hasListenersFor('foo')) { /* ... */ }
const count = bus.getListenerCountFor('foo');
const handlers = bus.listeners.get('foo');
for (const [event, set] of bus.listeners) { /* ... */ }
// v3 (scope / includeIncognito are optional)
if (bus.hasListenersFor('foo')) { /* ... */ }
const count = bus.getListenerCountFor('foo');
const handlers = bus.getListenersFor('foo');
bus.forEach((event, set) => { /* ... */ });
bus.forEach((event, set) => { /* ... */ }, {scope: ListenerScope.OWN});
bus.forEach((event, set) => { /* ... */ }, {scope: ListenerScope.DOWNSTREAM});
bus.hasListeners({includeIncognito: true});