stacksjs

stacks-events

Use when working with the event system in a Stacks application - dispatching events, listening for events, model events, wildcard listeners, the event emitter, or event-driven architecture. Covers @stacksjs/events, app/Events.ts, app/Listener.ts, and app/Listeners/.

stacksjs 636 20 Updated 1w ago
GitHub

Install

npx skillscat add stacksjs/stacks/stacks-events

Install via the SkillsCat registry.

SKILL.md

Stacks Events

Use the native event bus for process-local application and model events. Use
stacks-realtime for network broadcasting and stacks-queue for durable work.

Dispatch and completion

@stacksjs/events exports dispatch, dispatchAsync,
dispatchAndCollect, listen, once, off and emitter.

  • dispatch(name, payload) invokes handlers immediately and returns void.
    Async handlers continue without blocking the caller; errors are logged.
  • await dispatchAsync(name, payload) awaits matching handlers in sequence.
    Rejections are logged and appear as undefined results, so this does not prove
    that every handler succeeded.
  • await dispatchAndCollect(name, payload) returns a result for each handler:
    { ok: true, value } or { ok: false, error }. Inspect failures when
    downstream work determines the response.

Payloadless events are supported by the emitter. Exact handlers receive the
payload; wildcard and glob handlers receive (eventName, payload).

emitter.on(name, handler, { priority }) sorts higher priorities first
within each handler bucket. Equal priority preserves registration order.
Exact handlers run before glob buckets, then * handlers. Snapshotting keeps
subscription changes during a dispatch from corrupting iteration.

Typed application events

StacksEvents combines built-in auth events and the augmentable
AppEvents interface. Declare an application event before dispatching it:

import { dispatch, listen } from '@stacksjs/events'

declare module '@stacksjs/events' {
  interface AppEvents {
    'invoice:settled': { id: number, total: number }
  }
}

listen('invoice:settled', invoice => {
  console.log(invoice.id, invoice.total)
})
dispatch('invoice:settled', { id: 1, total: 1200 })

Built-in auth names include user:registered, user:logged-in,
user:logged-out, user:password-reset and user:password-changed.
Read their payload types from core/events/src/index.ts rather than guessing.

Application listeners

app/Events.ts default-exports defineEvents({ event: [listenerName] }).
Names resolve against application listeners, application actions, then framework
defaults. A standalone module in app/Listeners/ may instead default-export
defineListener({ listensTo, handle }). Its handler is (payload, eventName),
including for a glob subscription, because the discovery layer adapts the bus.

registerAppListeners() loads both conventions at boot and deduplicates each
event/module pair. injectGlobalAutoImports() calls it for HTTP, CLI, scheduler
and seeding entrypoints. app/Listener.ts may call the same registration hook.
Adding a listener requires a process restart. See stacks-listeners for files.

Model lifecycle

traits.observe opts a model into lifecycle events. Before events are
saving, creating, updating and deleting; after events are
created, updated, deleted and saved. Names lowercase the model
name, for example teammember:saved.

Before handlers receive the model object, with its row under .attributes;
returning exactly false cancels the write. After handlers receive the row.
The ORM awaits the before-event result; that is separate from ordinary
fire-and-forget application dispatch. See stacks-models and stacks-orm
for which writes emit hooks, bulk operations and transaction boundaries.
Model event types derive from the models barrel in
storage/framework/types/model-events.d.ts.

Isolation and cleanup

createEmitter<EventMap>() creates an isolated emitter; the compatibility
mitt/default export is its alias. The application singleton uses a global
symbol so duplicate installed copies share the same bus in one process.
It is not cross-process messaging.

once removes its listener after the first invocation. off(name, handler)
removes by identity; off(name) clears that bucket.
emitter.removeAllListeners(name?) removes a bucket or all listeners;
listenerCount(name) counts only the exact bucket, not matching patterns.
scopedEvents(prefix) and scope(emitter, prefix) provide prefixed names
without creating another bus. Read their prefix composition in the source
before combining them with an application's named events.

Source and verification

  • Public API and async behavior: storage/framework/core/events/src/index.ts.
  • Discovery and typed listener adapters: core/events/src/discover.ts.
  • Retained tests: native-emitter.test.ts, priority-and-collect.test.ts,
    scope.test.ts, register.test.ts, shared-emitter.test.ts under
    storage/framework/core/events/tests/.