The Event Bus
The event bus lets Slingshot packages communicate without coupling to each other. slingshot-auth doesn’t know about slingshot-community — but when a user logs in, auth emits an event and community reacts.
The interface
Section titled “The interface”interface SlingshotEventBus { emit<K extends keyof SlingshotEventMap>(event: K, payload: SlingshotEventMap[K]): void; on<K extends keyof SlingshotEventMap>( event: K, listener: (payload: SlingshotEventMap[K]) => void | Promise<void>, ): void; off<K extends keyof SlingshotEventMap>( event: K, listener: (payload: SlingshotEventMap[K]) => void, ): void; shutdown?(): Promise<void>;}InProcessAdapter
Section titled “InProcessAdapter”InProcessAdapter is the default — fast, zero-dependency, sufficient for single-instance deployments.
import { InProcessAdapter } from '@lastshotlabs/slingshot-core';
declare function hydrateUserProfile(userId: string): Promise<void>;
const bus = new InProcessAdapter();bus.on('auth:user.created', async payload => { await hydrateUserProfile(payload.userId);});bus.emit('auth:user.created', { userId: 'usr_123', email: 'new-user@example.com', tenantId: null,});The bus calls handlers and moves on — it doesn’t await them inline with request handling. Use drain() in tests to wait for pending handlers to settle.
Event namespacing
Section titled “Event namespacing”Event keys use a namespace:entity.action pattern. Reserved namespaces:
security.*— internal auth security events, never exposed to clientsauth:*— auth events, never exposed to clientscommunity:delivery.*— delivery status, internal onlypush:*— push notification internalsapp:*— application lifecycle events
Any other key can be exposed externally, but only when its event definition opts into exposure: ['client-safe'].
Client-safe events and SSE
Section titled “Client-safe events and SSE”SSE endpoints filter against registry-backed event definitions. Plugins or route configs declare which events flow through by setting exposure: ['client-safe']:
declare const sseClients: Array<{ send(event: string, payload: unknown): void }>;
events.register( defineEvent('community:thread.created', { owner: 'community', exposure: ['client-safe'], }),);
sseClients.forEach(client => client.send('community:thread.created', { id: 'thr_123' }));This keeps internal events — session invalidation, security events, delivery status — from leaking to browser clients via SSE.
drain() — test isolation
Section titled “drain() — test isolation”Handlers are fire-and-forget from the request handler’s perspective. In tests, wait for them explicitly:
// Emit eventawait app.request('/community/threads', { method: 'POST', ... });
// Wait for all bus handlers to completeawait bus.drain();
// Now assert on side effectsexpect(notificationStore.notifications).toHaveLength(1);drain() resolves when all currently-enqueued handlers have settled. You’ll only need it in tests — production code never awaits the bus.
Extending to multi-instance
Section titled “Extending to multi-instance”When events need to cross process boundaries, replace InProcessAdapter with a shared implementation by passing your own bus to defineApp:
import { defineApp } from '@lastshotlabs/slingshot';import { createBullMQAdapter } from '@lastshotlabs/slingshot-bullmq';
export default defineApp({ eventBus: createBullMQAdapter({ connection: { host: process.env.REDIS_HOST!, port: 6379 }, prefix: 'myapp:events', }), // ...});The bus interface stays identical. Only the implementation changes.
See also
Section titled “See also”- Context and Registrar — where the bus lives in
SlingshotContext - Plugin Lifecycle —
setupPost()is where event subscriptions belong