Skip to content

Events

SemitraEvents is the event subsystem in Semitra.

Use events when you want to publish that something happened, not necessarily perform the work itself.

Events support:

  • named listeners through on()
  • one-shot listeners through once()
  • synchronous emission through emit()
  • asynchronous emission through emitAsync()
  • queued emission through enqueue()
  • draining queued events through drain()
  • tenant propagation across event handling

That makes events useful for fan-out and cross-cutting reactions.

Events are emitted as envelopes with:

  • an event name
  • a payload
  • a queued timestamp
  • a trace ID
  • a tenant value
  • optional scheduling metadata

The envelope format keeps observability and delayed processing consistent.

The record subsystem emits record lifecycle events through SemitraEvents. That lets you react to data changes without coupling the model layer directly to every side effect.

Example reactions:

  • log that a record was created
  • increment an analytics counter
  • invalidate a cache entry
  • enqueue a job after a successful save

A common pattern is to publish one domain signal and let multiple listeners react independently:

import {
InMemoryEventQueue,
SemitraCache,
SemitraEvents
} from "@semitra/cli";
type PostPublishedEvent = {
postId: string;
authorId: string;
tenant: string | null;
};
const queue = new InMemoryEventQueue<PostPublishedEvent>();
export function registerPostListeners(cache: SemitraCache) {
SemitraEvents.on<PostPublishedEvent>("posts:published", async ({ postId }) => {
await cache.delete("posts:index", { namespace: "api" });
await cache.delete(`posts:${postId}`, { namespace: "api" });
});
SemitraEvents.on<PostPublishedEvent>(
"posts:published",
async ({ authorId, postId }) => {
console.log("analytics", {
type: "post_published",
authorId,
postId
});
}
);
}
export async function publishPostEvent(
postId: string,
authorId: string,
tenant: string | null
) {
await SemitraEvents.enqueue(
"posts:published",
{ postId, authorId, tenant },
queue,
{ tenant }
);
}
export async function flushPostEvents() {
await SemitraEvents.drain(queue);
}

That is a good fit for a publishing flow where the controller or record only needs to say “a post was published” and should not own cache invalidation, analytics, or notification fan-out itself.

  • invalidate or warm caches after a write
  • record analytics events after a domain action
  • fan out a signal to multiple internal listeners
  • bridge record lifecycle hooks to downstream processes
  • emit “something happened” notifications without hard-coding the consumer

Do not use events when:

  • the work must run exactly once and be durable
  • the work belongs behind a queue consumer
  • you need the side effect to be part of the controller response path

Events are the publishing layer. Jobs are the durable execution layer.