> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Event

> Publish and subscribe to typed event channels over Redis for decoupled, event-driven communication between components.

The `@ooneex/event` component is a Redis-backed publish/subscribe layer. You declare an event as a class that extends `RedisPubSub`, give it a channel name, and implement a `handler` for incoming messages. Publishers call `publish(data)`; subscribers receive the same typed payload on the channel — so components communicate without knowing about each other, even across separate processes or server instances.

## Why this component

* **Decoupled communication.** Publishers and subscribers only share a channel name and a data shape — never a direct reference.
* **Typed payloads.** Each event class is generic over its `Data` type, carried through publish and into the handler.
* **One class per event.** `getChannel()`, `handler()`, `publish()`, and `subscribe()` live together in a single `RedisPubSub` subclass.
* **Distributed by default.** Messages travel through Redis, so every server instance subscribed to a channel receives them.
* **Container-managed.** Register an event class with a decorator and resolve it (with its injected client) from the container.

## How it works

You extend the abstract `RedisPubSub` base class, declare a channel with `getChannel()`, and implement `handler()` for messages arriving on that channel. The base class wires `publish`, `subscribe`, `unsubscribe`, and `unsubscribeAll` to an injected `RedisPubSubClient`, which talks to Redis and JSON-serializes payloads on the way out and parses them on the way in.

| Member             | Purpose                                                          |
| ------------------ | ---------------------------------------------------------------- |
| `getChannel()`     | Return the channel name to publish/subscribe on (sync or async). |
| `handler(context)` | Handle an incoming message; receives `{ data, channel }`.        |
| `publish(data)`    | Serialize and publish `data` to the channel.                     |
| `subscribe()`      | Start receiving messages on the channel, routed to `handler`.    |
| `unsubscribe()`    | Stop receiving on this event's channel.                          |
| `unsubscribeAll()` | Stop receiving on every subscribed channel.                      |

`getChannel()` and `handler()` are the two abstract members you implement; the rest are provided by `RedisPubSub`. Data types must extend `Record<string, ScalarType>` so they round-trip cleanly through JSON. Malformed messages that fail to parse are silently ignored by the client.

## Environment variables

| Variable           | Required                       | Purpose                                                                                                        |
| ------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `PUBSUB_REDIS_URL` | Yes (unless passed in options) | Redis connection string, e.g. `redis://localhost:6379`. Missing throws `EventException` (`CONNECTION_FAILED`). |

The client reads `PUBSUB_REDIS_URL` from the app environment when no `connectionString` is given in its options.

```bash theme={null}
PUBSUB_REDIS_URL=redis://localhost:6379
```

## Decorator and usage

### `@decorator.event()`

Registers an event class with the container. It accepts an optional scope (defaults to singleton). The decorated class gets its `RedisPubSubClient` injected, so you resolve the fully wired event from the container.

```typescript theme={null}
import { inject } from "@ooneex/container";
import type { ScalarType } from "@ooneex/types";
import { decorator, RedisPubSub, RedisPubSubClient } from "@ooneex/event";

interface UserSignedUpData extends Record<string, ScalarType> {
  userId: string;
  email: string;
}

@decorator.event()
export class UserSignedUpEvent extends RedisPubSub<UserSignedUpData> {
  constructor(
    @inject(RedisPubSubClient)
    client: RedisPubSubClient<UserSignedUpData>,
  ) {
    super(client);
  }

  public getChannel(): string {
    return "user-signed-up";
  }

  public async handler(context: { data: UserSignedUpData; channel: string }): Promise<void> {
    const { data } = context;
    await sendWelcomeEmail(data.email);
  }
}
```

Resolve the event from the container, subscribe once at startup, and publish whenever the event occurs:

```typescript theme={null}
import { container } from "@ooneex/container";

const event = container.get(UserSignedUpEvent);

// Start receiving messages on the channel
await event.subscribe();

// Publish from anywhere — every subscribed instance gets it
await event.publish({ userId: "123", email: "john@example.com" });
```

The publisher and the subscriber can live in different processes. As long as both resolve an event with the same channel, the message published by one reaches the other's `handler` through Redis.

A channel can be dynamic. Compute it in `getChannel()` to scope subscriptions per tenant, room, or user:

```typescript theme={null}
public async getChannel(): Promise<string> {
  const userId = await this.resolveUserId();
  return `user:${userId}:events`;
}
```

## Exceptions

The component throws `EventException` when the client cannot connect or an operation fails. It carries a machine-readable `key`, a human-readable `message`, and a `data` object.

| Key                      | When                                                                              |
| ------------------------ | --------------------------------------------------------------------------------- |
| `CONNECTION_FAILED`      | `RedisPubSubClient` is created without a connection string or `PUBSUB_REDIS_URL`. |
| `PUBLISH_FAILED`         | Publishing a message to a channel fails.                                          |
| `SUBSCRIBE_FAILED`       | Subscribing to a channel fails.                                                   |
| `UNSUBSCRIBE_FAILED`     | Unsubscribing from a channel fails.                                               |
| `UNSUBSCRIBE_ALL_FAILED` | Unsubscribing from all channels fails.                                            |

```typescript theme={null}
import { EventException } from "@ooneex/event";

try {
  await event.publish({ userId: "123", email: "john@example.com" });
} catch (error) {
  if (error instanceof EventException) {
    logger.error(`Event error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}
```

## Best practices

* **One channel, one shape.** Keep each channel tied to a single `Data` type so publishers and subscribers stay in agreement.
* **Subscribe once at startup.** Call `subscribe()` during bootstrap, not per request, and `unsubscribe()` on shutdown.
* **Keep payloads serializable.** Data round-trips through JSON; use scalar fields and avoid class instances, functions, and circular references.
* **Handle errors inside `handler`.** A throwing handler should catch and route its own failures — don't let one bad message stop processing.
* **Name channels consistently.** Use a stable scheme like `user-signed-up` or `user:123:events` so dynamic channels stay predictable.
* **Inject the client.** Let the container provide `RedisPubSubClient` rather than constructing connections by hand.

## CLI command

Scaffold an event class and its test file with the generator. It writes the class under `modules/<module>/src/events/<Name>Event.ts`, registers it in the module, and installs `@ooneex/event` if it is missing.

```bash theme={null}
# Interactive: prompts for the name
ooneex event:create

# Provide the name
ooneex event:create --name=UserSignedUp

# Target a module, set a channel, and overwrite
ooneex event:create --name=UserSignedUp --module=auth --channel=user-signed-up --override
```

| Option       | Description                                                     | Default                |
| ------------ | --------------------------------------------------------------- | ---------------------- |
| `--name`     | Event class name. The `Event` suffix is appended automatically. | Prompted if omitted    |
| `--module`   | Target module the class is generated into.                      | `shared`               |
| `--channel`  | Channel name the event publishes/subscribes on.                 | Kebab-case of the name |
| `--override` | Overwrite an existing class without prompting.                  | `false`                |

The generated class extends `RedisPubSub` with the client injected, ready for you to type the payload and fill in the handler:

```typescript theme={null}
import { inject } from "@ooneex/container";
import type { ScalarType } from "@ooneex/types";
import { decorator, RedisPubSub, RedisPubSubClient } from "@ooneex/event";

@decorator.event()
export class UserSignedUpEvent<Data extends Record<string, ScalarType> = Record<string, ScalarType>> extends RedisPubSub<Data> {
  constructor(
    @inject(RedisPubSubClient)
    client: RedisPubSubClient<Data>,
  ) {
    super(client);
  }

  public getChannel(): string {
    return "user-signed-up";
  }

  public async handler(context: { data: Data; channel: string }): Promise<void> {
    console.log(context);
    // TODO: Implement handler logic here
  }
}
```

See [event:create](/cli/commands/event-create) for the full command reference.

## Use with Claude and Codex

The generator ships a matching `event:create` skill. It runs the scaffold and then guides your AI agent through completing the event — defining a real `Data` type, setting the channel in `getChannel()`, and implementing `handler()`. Initialize the skills once for your agent:

<Tabs>
  <Tab title="Claude">
    ```bash theme={null}
    ooneex claude:init
    ```

    Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create an event that fires when a user signs up.
    ```
  </Tab>

  <Tab title="Codex">
    ```bash theme={null}
    ooneex codex:init
    ```

    Then ask Codex in natural language — it maps the request to the generator, runs it, and fills in the implementation:

    ```text Prompt icon="terminal" wrap theme={null}
    Create an event that fires when a user signs up.
    ```
  </Tab>
</Tabs>

For example, the prompt above maps to `event:create --name=UserSignedUp --channel=user-signed-up`, then types the payload and implements `handler()`.
