# React to a saved note

> Use typed, in-process events for small reactions while keeping delivery and failure behavior visible.

- Package: `@db3.ai/app/events`
- Canonical page: [https://db3.ai/docs/events](https://db3.ai/docs/events)
- Markdown: [https://db3.ai/docs/events.md](https://db3.ai/docs/events.md)
- Framework source of truth: `packages/app/src/events/README.md`

<a id="setup"></a>

## Start with one event

Complete Installation and copy the example. It needs no database or worker. The trace represents reactions; it does not actually save or index a note.

In an app, register listeners during boot on `app().events`. Dispatch only once the operation the event describes has succeeded.

- [Installation](https://db3.ai/docs/installation.md)

### Copy the shipped example

```bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/events/examples/. examples/
```

<a id="run"></a>

## Run, fail and repair a dispatch

Expect the trace `index:one`, `first:one`, `index:two`, `after:four`, with `rejected: true` and `hasListeners: false`.

The failed third dispatch never reaches the later listener. Removing the failing registration allows the fourth dispatch to complete. All remaining listeners are cleared in `finally`.

### Run the lab

```bash
npx tsx examples/runNoteEvents.ts
```

<a id="event"></a>

## Use a class as the event identity

Name the event after something that happened and carry stable IDs. Event instances are not cloned. Readonly fields help communicate intent, but do not deeply freeze nested values.

The exact class constructor is the subscription key. A subclass is a different event type. Plain objects are rejected rather than sharing one ambiguous Object key.

### examples/NoteSaved.ts

```typescript
/** Immutable notification that one application note has already been saved. */
export class NoteSaved {
	/**
	 * Carries identifiers rather than a request, model connection or mutable service.
	 * @param noteId - Saved note identity.
	 * @param ownerId - Authorized owner identity determined by the producer.
	 */
	constructor(readonly noteId: string, readonly ownerId: string) {}
}
```

<a id="listeners"></a>

## Subscribe and await delivery

`listen(EventClass, listener)` returns an idempotent unsubscribe function. `once()` removes its registration before invocation so overlapping dispatches cannot run that listener twice.

`dispatch(new Event(...))` snapshots current listeners and awaits them sequentially in registration order. Removing a registration does not cancel a callback already captured by an in-progress dispatch.

### examples/runNoteEvents.ts

```typescript
import { pathToFileURL } from 'node:url';
import { Events } from '@db3.ai/app/events';
import { NoteSaved } from './NoteSaved';

/**
 * Exercises ordered listeners, one-shot observation, failure and explicit repair.
 * @returns The dispatch trace and teardown state; no durable side effect is made.
 */
export async function runNoteEvents() {
	const events = new Events();
	const trace: string[] = [];
	try {
		const unsubscribe = events.listen(NoteSaved, async event => { await Promise.resolve(); trace.push(`index:${event.noteId}`); });
		events.once(NoteSaved, event => { trace.push(`first:${event.noteId}`); });
		await events.dispatch(new NoteSaved('one', 'ada'));
		await events.dispatch(new NoteSaved('two', 'ada'));
		unsubscribe();
		const removeFailure = events.listen(NoteSaved, () => { throw new Error('Reaction failed'); });
		events.listen(NoteSaved, event => { trace.push(`after:${event.noteId}`); });
		let rejected = false;
		try { await events.dispatch(new NoteSaved('three', 'ada')); } catch { rejected = true; }
		removeFailure();
		await events.dispatch(new NoteSaved('four', 'ada'));
		events.forget(NoteSaved);
		return { trace, rejected, hasListeners: events.hasListeners(NoteSaved) };
	} finally {
		events.clear();
	}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) console.log(JSON.stringify(await runNoteEvents(), null, 2));
```

<a id="errors"></a>

## Decide whether a reaction must succeed

The first thrown/rejected listener stops dispatch and propagates to the caller. Earlier listeners may already have changed state; there is no rollback of their effects.

Catch only when you intentionally make a reaction non-fatal. Do not replay a whole event blindly: earlier successful side effects might run twice. This lab repairs registration and dispatches a new event; it does not claim automatic retry or duplicate protection.

<a id="durability"></a>

## Use Queue for durable work

Events are in memory, process-local and not persisted. They do not cross API, worker or scheduler processes, survive a crash or retry failed listeners.

A listener can enqueue a job, but a database commit followed by dispatch is not an atomic outbox. If losing the handoff matters, design that transaction/reconciliation explicitly.

Queue lifecycle observers are a different stream: their errors are isolated from already-completed queue transitions. Application Events intentionally propagates listener failures. Logs are observability, not a way to trigger product behavior.

- [Durable queued reports](https://db3.ai/docs/queue-overview.md)
- [Structured logging](https://db3.ai/docs/logging.md)

<a id="cleanup"></a>

## Remove listeners at their owner boundary

Use the returned unsubscribe function for one registration, `forget(EventClass)` for one event type, and `clear()` for the whole dispatcher. Do not clear a shared app dispatcher after each request.

`App.close()` clears its dispatcher. A standalone dispatcher owns no network resources, but you should still remove listeners when its owner finishes.

<a id="testing"></a>

## Testing

Create a `tests/events` directory and save the test below as `runNoteEvents.test.ts`. It imports the example you copied into `examples/`. Keep the same folder layout so the relative import resolves.

Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout.

### tests/events/runNoteEvents.test.ts

```typescript
import { expect, it } from 'vitest';
import { Events } from '@db3.ai/app/events';
import { NoteSaved } from '../../examples/NoteSaved';
import { runNoteEvents } from '../../examples/runNoteEvents';

it('awaits ordered listeners, invokes once once, propagates failure and recovers after removal', async () => {
	expect(await runNoteEvents()).toEqual({ trace: ['index:one', 'first:one', 'index:two', 'after:four'], rejected: true, hasListeners: false });
});

it('dispatches exact classes only and rejects anonymous payloads', async () => {
	const events = new Events();
	/** Distinct event identity, not an implicit subscription to the base event. */
	class ImportedNoteSaved extends NoteSaved {}
	let calls = 0;
	const stop = events.listen(NoteSaved, () => { calls++; });
	try {
		await events.dispatch(new ImportedNoteSaved('one', 'ada'));
		expect(calls).toBe(0);
		await expect(events.dispatch({ noteId: 'one' })).rejects.toThrow('class instances');
		await events.dispatch(new NoteSaved('one', 'ada'));
		expect(calls).toBe(1);
		stop();
		stop();
		expect(events.hasListeners(NoteSaved)).toBe(false);
	} finally { events.clear(); }
});
```

<a id="run-tests"></a>

## Test ordering and failure

The copied tests use the real dispatcher and assert the complete trace, failure propagation, one-shot observation, exact-class matching, plain-object rejection and idempotent unsubscribe.

Add a second application listener and prove its position in the trace. If it creates durable work, test the Queue handoff separately.

### Run your copied test and check types

```bash
npx vitest run tests/events/runNoteEvents.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

<a id="coverage"></a>

## Coverage and limits

Taught/tested: every Events method, async order, listener failure/recovery, exact-class identity and cleanup. App shutdown and overlapping one-shot dispatch also have existing service tests.

Cross-process delivery, replay, retries, cancellation and transaction coupling are not Events features. The reference contains the service and every public event contract.

- [Full Events API](https://db3.ai/docs/events-api.md)

## Behavioural verification
Runs exact-class event listeners in order, unsubscribes, rejects a failing listener and resumes after explicit removal.
- Behaviour test: `packages/app/src/events/tests/examples/runNoteEvents.test.ts`
- Repository test command (framework checkout only): `npm run test:service --workspace @platform/app -- events --maxWorkers=1`
- In an installed application, use the walkthrough commands instead of this repository test.
- Expected outcome: The guide test passes against the real framework components.
- Environment: Node.js 24. In-process Events; no database or queued/durable delivery claim.

## Related documentation
- [Events API reference](https://db3.ai/docs/events-api.md): Current emitted signatures and options for @db3.ai/app/events.
- [App](https://db3.ai/docs/app.md): Create one application at boot. Configure its services, use request-local state and close the resources you own.
- [Queue](https://db3.ai/docs/queue-overview.md): Create durable background jobs, run named workers, understand every attempt, compose chains and batches, and recover failures through one complete service guide.
- [Write useful, safe logs](https://db3.ai/docs/logging.md): Record what happened with enough context to investigate it, without copying secrets into your logs.

## Framework-owned source: `packages/app/src/events/README.md`

This is the exact source document captured by the documentation build. Use it for detailed API and workflow guidance, subject to the public package exports and behavioural evidence identified above.

````markdown
# Events

`@db3.ai/app/events` provides the typed, in-process event dispatcher exposed
through `app().events`. It is intended for application events and small
cross-cutting reactions without coupling the producer to each listener.

## Run a note event

Complete [Installation](https://db3.ai/docs/installation), then run this from your independent application:

```sh
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/events/examples/. examples/
npx tsx examples/runNoteEvents.ts
```

Expect the trace `index:one`, `first:one`, `index:two`, `after:four`, with `rejected: true` and `hasListeners: false`. The third dispatch fails before its later listener. The example removes the failing listener and dispatches a new event. It has no database, side-effecting indexer or durable worker.

Copy the exact test from [Events](https://db3.ai/docs/events#testing) into `tests/events/runNoteEvents.test.ts` and run:

```sh
npx vitest run tests/events/runNoteEvents.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts
```

The tests use the real dispatcher and cover every service method, exact-class identity, async ordering, failure propagation and cleanup. See the [complete contract reference](https://db3.ai/docs/events-api) for listener and constructor types.

## Defining And Dispatching Events

Events are normal classes containing application data. Prefer past-tense names
that describe something that has already happened:

```ts
class WebsiteCrawled {
	constructor(
		readonly websiteId: string,
		readonly crawlId: string,
	) {}
}
```

Register listeners during application boot, then dispatch an instance:

```ts
const unsubscribe = app().events.listen(WebsiteCrawled, async event => {
	await updateSearchIndex(event.websiteId);
});

await app().events.dispatch(new WebsiteCrawled(websiteId, crawlId));

unsubscribe();
```

The event class is the runtime listener key and gives the callback its inferred
event type. Listeners registered for a parent class do not receive subclass
instances.

Use `once(...)` for a one-shot listener:

```ts
app().events.once(WebsiteCrawled, event => {
	app().log.info({
		websiteId: event.websiteId,
	}, 'Observed the first completed crawl');
});
```

`hasListeners(EventClass)`, `forget(EventClass)`, and `clear()` are available
for bootstrapping, tests, and explicit teardown. `App.close()` automatically
clears listeners created through `app().events`.

## Execution And Errors

`dispatch(...)` invokes the listeners present when dispatch begins, in
registration order. It awaits each asynchronous listener before starting the
next listener.

If a listener throws or rejects, dispatch rejects and later listeners are not
run. This makes failure visible to the producer and avoids silently losing
required application work. Catch the error at the producer only when that
failure is intentionally non-fatal.

Earlier listeners may already have made changes; there is no rollback. Do not replay a whole dispatch without considering duplicate side effects. The listener list is captured when dispatch begins, so unsubscribing does not cancel callbacks already in that snapshot.

Event data is not cloned. Every listener receives the same instance, so event
classes should normally be immutable data objects.

## Process And Durability Boundary

Framework events are process-local and in-memory:

- They are not persisted.
- They do not cross API, queue-worker, or scheduler processes.
- They are lost if the process exits before dispatch completes.
- They do not retry failed listeners.

Use the queue for durable, retryable, delayed, or cross-process work. A simple
event listener can dispatch a queued job when an event should trigger that kind
of work.

A database commit followed by event dispatch is not an atomic outbox. Design a transaction/reconciliation boundary explicitly when a durable handoff cannot be lost.

Queue lifecycle events remain a separate typed stream because they describe
queue-driver transitions and intentionally isolate observer failures from
already-completed queue operations. Logs also remain observability records;
they must not be used to trigger application behavior.
````

## Guidance for AI tools
Use the documented public import `@db3.ai/app/events` and its exported types. Prefer the source-backed examples and behavioural outcomes above over invented APIs or source-relative internal imports.
