# @spearwolf/eventize

> A tiny, clever, and dependency-free library for synchronous event-driven programming in JavaScript and TypeScript.

Latest version **6.2.0** (published 2026-08-30) · Apache-2.0 license · 0 weekly downloads

## Install

```sh
npm install @spearwolf/eventize
pnpm add @spearwolf/eventize
yarn add @spearwolf/eventize
bun add @spearwolf/eventize
```

## Health

**Score 75/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 6.2.0 |
| Published | 2026-08-30 |
| First published | 2017-12-07 |
| Weekly downloads | 0 |
| License | Apache-2.0 |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=18.16 |
| Dependencies | 0 |
| Unpacked size | 1.3 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 3 |
| Author | Wolfger Schramm |
| Maintainers | spearwolf |

## Links

- npm: https://www.npmjs.com/package/@spearwolf/eventize
- Repository: https://github.com/spearwolf/eventize
- Homepage: https://github.com/spearwolf/eventize/
- Issues: https://github.com/spearwolf/eventize/issues
- npm.io page: https://npm.io/package/@spearwolf/eventize

## Recent versions

- 6.2.0 (latest) — 2026-08-30
- 6.1.0 — 2026-08-30
- 6.0.0 — 2026-08-05
- 5.1.0 — 2026-07-25
- 5.0.0 — 2026-05-13
- 4.3.1 — 2026-05-08
- 4.3.0 — 2026-05-08
- 4.2.1 — 2026-05-08
- 4.2.0 — 2026-05-08
- 4.1.0 — 2026-05-08
- 4.0.3 — 2026-05-07
- 4.0.2 — 2025-08-07
- 4.0.1 — 2024-08-04
- 4.0.0 — 2024-07-22
- 3.4.2 — 2024-06-01
- … 27 more at https://npm.io/package/@spearwolf/eventize/versions

## README

# @spearwolf/eventize

A tiny, clever, and dependency-free library for synchronous event-driven programming in JavaScript and TypeScript.

![npm (scoped)](https://img.shields.io/npm/v/%40spearwolf/eventize)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/spearwolf/eventize/main.yml)
![GitHub](https://img.shields.io/github/license/spearwolf/eventize)

## Introduction 👀

`@spearwolf/eventize` provides a powerful and intuitive API for building event-based systems. This library invokes event listeners _synchronously_.
That design choice gives you precise control over your execution flow, which is critical in scenarios like game loops (`requestAnimationFrame`),
real-time applications, or anywhere immediate, predictable execution is necessary.

Written entirely in TypeScript and targeting modern `ES2022`, it offers a type-safe developer experience
without sacrificing performance or adding bloat.

### Features

- 🚀 **Developer-Focused API**: Clean, modern, and functional.
- ✨ **Wildcards & Priorities**: Subscribe to all events and control listener execution order.
- 🔷 **Full TypeScript Support**: Optional generic event maps narrow `emit`, `on`, retained-event names and listener arguments — without losing first-class duck-typing for code that doesn't opt in.
- 📦 **Zero Runtime Dependencies**: Lightweight with a minimal footprint (~6.5 kB gzipped, practically indivisible — see above).
- ESM & CommonJS Support.
- Apache 2.0 Licensed.

## ⚙️ Installation

```sh
$ npm install @spearwolf/eventize
```

The library is distributed in both ES Module (`import`) and CommonJS (`require`) formats.

> [!NOTE]
> Since version 3.0.0 there is also a [CHANGELOG](./CHANGELOG.md)

### 🤖 For AI coding agents

This repo ships a quick-reference skill for AI coding assistants (Claude Code & co.) at [`skills/using-eventize/`](./skills/using-eventize/SKILL.md).
`SKILL.md` carries the mental model, the API surface, the four behavior families and the pitfalls;
deeper material sits in `references/` and is loaded only when a task needs it:


| Reference | Covers |
| --- | --- |
| [`api-details.md`](./skills/using-eventize/references/api-details.md) | every `on()` / `off()` shape, per-event priorities, retain semantics in full |
| [`lifecycle.md`](./skills/using-eventize/references/lifecycle.md) | what an emitter holds, what each `off()` form releases, handle lifetime |
| [`typed-events.md`](./skills/using-eventize/references/typed-events.md) | generic event maps, the `EventMap` trap, symbol escape hatch |
| [`migration.md`](./skills/using-eventize/references/migration.md) | v5 → v6 breaking changes, the v4 → v5 emit change, the v4.3 type-brand migration for classes |

The skill folder is self-contained: everything it references lives inside it, so it works when symlinked out of the repo.

To use it, copy or symlink the folder into your agent's skills directory, e.g. for Claude Code:

```sh
ln -s "$(pwd)/skills/using-eventize" ~/.claude/skills/using-eventize
```

Skills are auto-discovered — no extra registration step.

### 📚 Further documentation

The deep material behind the summaries below:

- [Unsubscribing in depth](./docs/off.md) — every `off()` signature, the interaction with `retain()`, and reference counting
- [Dispatch in depth](./docs/emit.md) — all six dispatch functions, and what each one does with a listener that throws
- [Retained events in depth](./docs/retain.md) — `retain()`, `retainClear()`, `unretain()`, symbol names, and the wildcard bulk forms
- [Typed event maps](./docs/typed-events.md) — generic event maps, the inject and class forms, symbol events as an escape hatch
- [Lifecycle & cleanup](./docs/lifecycle.md) — what an emitter holds and what releases it
- [Migration guide](./docs/migration.md) — upgrading from v5, and the older jumps

## 📖 Getting Started

The core idea is simple: an object, called an **emitter**, can be "eventized" to emit named events.
Other parts of your application, called **listeners**, subscribe to those events and run immediately when the event is emitted.

![Emitter emits named event to listeners](https://raw.githubusercontent.com/spearwolf/eventize/main/docs-assets/emitter-emits-named-events-listeners.svg)

```javascript
import {eventize, on, emit} from '@spearwolf/eventize';

// 1. Create an eventized object (the emitter)
const bus = eventize({});

// 2. Subscribe to a 'data' event
on(bus, 'data', (message, code) => {
  console.log(`Received message: ${message} with code ${code}`);
});

// 3. Emit the 'data' event with some arguments
emit(bus, 'data', 'Hello World!', 42);

// Output: Received message: Hello World! with code 42
```

## The Event-Driven Model

### Emitters

An emitter is any object that has been enhanced with event capabilities. The recommended way to create one is the `eventize()` function.

> [!TIP]
> We often use `ε` (epsilon) as a variable name to denote an _eventized_ object.

```javascript
import {eventize} from '@spearwolf/eventize';

const ε = eventize();          // from a new empty object

const myApp = {name: 'MyApp'};
eventize(myApp);               // myApp is now an emitter
```

### Listeners

A listener can be a function or a method on an object.

```js
on(ε, 'foo', (a) => {
  console.log('(1) Hello', a);
});

on(ε, 'foo', {
  foo(a, b) {
    console.log('(2)', b, a);
  },
});

on(ε, {
  foo(a, b) {
    console.log('(3) Hi', a);
  },
  bar() {
    console.log('(4) hej');
  },
});

emit(ε, 'foo', 'eventize', 'Greetings from');
// => "(1) Hello eventize"
// => "(2) Greetings from eventize"
// => "(3) Hi eventize"

emit(ε, 'bar');
// => "(4) hej"
```

### Events

Events are identified by a name, which can be a `string` or a `symbol`. Anywhere a name is accepted, an array of names works too.

```javascript
emit(ε, 'user-login');                          // no data
emit(ε, 'update', {id: 1, payload: 'new data'}); // one argument
emit(ε, 'hello', 'hi', 'hej', 'hallo');          // several arguments
```

## 📚 API Reference

The API is designed to be used functionally, with named exports like `on(ε, …)` and `emit(ε, …)`.
For class-based patterns you can inject the same API as methods.

| API           | Description                                                          |
| ------------- | -------------------------------------------------------------------- |
| `on`          | subscribe to events                                                  |
| `once`        | subscribe to the next event only                                     |
| `onceAsync`   | the async version of subscribe only to the next event                |
| `emit`        | dispatch an event                                                    |
| `emitAsync`   | dispatch an event and wait for any promises returned by subscribers  |
| `emitSafe`    | dispatch an event; a throwing listener does not stop the others       |
| `emitSafeAsync` | the async version of `emitSafe`                                    |
| `emitStrict`  | dispatch an event; every listener runs and every failure is raised    |
| `emitStrictAsync` | the async version of `emitStrict`                                 |
| `off`         | unsubscribe                                                          |
| `retain`      | hold the last event until it is received by a subscriber             |
| `retainClear` | clear the last event                                                 |
| `unretain`    | remove the retain policy entirely (clears value and disables retain) |

### Creating Emitters

| Method                      | Is a `EventizedObject`? | Has API Methods Injected? | Recommended For                             |
| --------------------------- | ----------------------- | ------------------------- | ------------------------------------------- |
| `eventize(obj)`             | ✅                      | ❌                        | Functional programming, general use.        |
| `eventize.inject(obj)`      | ✅                      | ✅                        | Object-oriented or class-based composition. |
| `class extends Eventize {}` | ✅                      | ✅                        | Class-based inheritance.                    |

#### `eventize(obj)`

The primary and recommended approach — it prepares an object for the functional API.

```typescript
import {eventize, on, emit} from '@spearwolf/eventize';

const ε = eventize(); // creates an emitter from {}

on(ε, 'foo', () => console.log('foo called'));

emit(ε, 'foo'); // => "foo called"
```

> [!NOTE]
> The target must be extensible: `eventize(Object.freeze(obj))` throws a `TypeError` naming the cause — frozen, sealed and `preventExtensions()`ed alike.
> An object frozen *after* it was eventized keeps working.

#### `eventize.inject(obj)`

Modifies the object, attaching the entire API as methods.

```typescript
const myApp = {name: 'MyApp'};
const obj = eventize.inject(myApp);

obj.on('foo', () => console.log('foo called'));

obj.emit('foo'); // => "foo called"
```

#### `class extends Eventize`

```typescript
import {Eventize} from '@spearwolf/eventize';

class MyEmitter extends Eventize {}

const obj = new MyEmitter();

obj.on('foo', () => console.log('foo called'));

obj.emit('foo'); // => "foo called"
```

#### Class-based, but without inheritance

Call `eventize.inject` in the constructor instead:

```ts
import {eventize, Eventize} from '@spearwolf/eventize';

interface Foo extends Eventize {}

class Foo {
  constructor() {
    eventize.inject(this);
  }
}
```

---

### The four behavior families

Eventize splits its API into four families by how each function treats a target that was never eventized. This is **by design**:

| Function                                    | On a non-eventized object                    |
| ------------------------------------------- | -------------------------------------------- |
| `on()`, `once()`, `onceAsync()`, `retain()` | Auto-eventizes the object                    |
| `emit()`, `emitAsync()` (v5+), `emitSafe()`, `emitSafeAsync()` (v6.1.0), `emitStrict()`, `emitStrictAsync()` (v6.2.0) | Duck-types: calls `obj[eventName](...args)` — a function target too (v6.0.0) |
| `off()`, `getSubscriptionCount()`, `getSubscribedEventNames()`, `getRetainedCount()`, `getRetainedEventNames()` | Silently does nothing / returns `0` / `[]` |
| `retainClear()`, `unretain()`               | Throws `TypeError`                           |

**Why the split?**

`on` / `once` / `retain` _install_ behavior. Requiring an explicit `eventize(obj)` before every `on(obj, …)` would be pure ceremony,
so they auto-eventize: `on({}, 'foo', fn)` is a perfectly meaningful intent.

`emit` / `emitAsync` fire events. On an eventized target they dispatch to subscribed listeners.
On a non-eventized object (v5+) they fall back to duck-typing — the same pattern that already powers listener-object dispatch:

1. If `obj[eventName]` is a function the object actually provides → call it with the args (with `this === obj`).
2. Else if `obj.emit` is a function → call `obj.emit(eventName, ...args)`.
3. Otherwise → silently no-op.

`retainClear` / `unretain` _operate on retain state_ that only exists on eventized objects. There is no meaningful duck-typed equivalent,
so they keep throwing — pointing them at a plain `{}` is almost always a bug.

`off` is permissive because cleanup code routinely runs against objects whose lifecycle isn't fully under the caller's control.
`getSubscriptionCount` follows the same reasoning and returns `0` for any non-eventized input.

```javascript
// ✅ Auto-eventize: convenient, intent is clear
const obj = {};
on(obj, 'foo', () => console.log('foo')); // obj is now eventized
emit(obj, 'foo'); // works (dispatches to listener)

// ✅ Duck-typing (v5+): point emit() at a plain method-bag
const sink = {
  foo(msg) {
    console.log('foo:', msg);
  },
};
emit(sink, 'foo', 'hello'); // => "foo: hello"
emit(sink, 'missing'); // no-op (no method, no .emit fallback)

// ✅ Duck-typing (v6.0.0): a function or a class is a target too
class Registry {
  static reset(reason) {
    console.log('reset:', reason);
  }
}
emit(Registry, 'reset', 'shutdown'); // => "reset: shutdown"
emit(Registry, 'bind', globalThis); // no-op — inherited from Function.prototype

// ✅ off() is permissive — safe in cleanup paths
off({}); // no-op, no throw

// ❌ Strict: retain-state mutators still surface typos
retainClear({}, 'foo'); // throws TypeError: retainClear() cannot operate on a non-eventized object — ...
unretain({}, 'foo'); // throws TypeError: unretain() cannot operate on a non-eventized object — ...
```

The type guard `isEventized(obj)` (see _Utilities_) lets you check defensively when you need to.

> **Migration from v4 → v5:** Previously `emit()` / `emitAsync()` also threw `"object is not eventized"` on a non-eventized target.
> If you relied on that as a typo-safety net, either gate the call with `isEventized()` or use a typed emitter (`eventize<TEvents>()`)
> — typed emitters still reject unknown event names at compile time. See the [migration guide](./docs/migration.md).

---

### Subscribing to Events

#### `on(emitter, ...args)`

Subscribes a listener to one or more events and returns an `unsubscribe` function.

```typescript
on(ε, eventName(s), [priority], listener, [context]);
on(ε, [priority], listener, [context]); // wildcard subscription
```

```javascript
const ε = eventize();
const listener = (val) => console.log(val);

const unsubscribe = on(ε, 'my-event', listener);
emit(ε, 'my-event', 'Hello!'); // => "Hello!"

unsubscribe();
emit(ε, 'my-event', 'Silent?'); // (nothing happens)
```

The full set of call shapes — including the method-name form `on(ε, 'foo', 'methodName', obj)` — is listed
in [`skills/using-eventize/references/api-details.md`](./skills/using-eventize/references/api-details.md).

The listener slot takes only what can be dispatched to: a function, a method name (string or symbol), or a listener object.
Anything else throws — `on(ε, 'foo', 5)` fails instead of registering a subscription no `emit()` could ever reach.

A method name is resolved off its listener object at dispatch time, so the method may appear later — that is late binding, and it still works.

##### Multiple Event Names

```javascript
on(ε, ['foo', 'bar'], listener);

emit(ε, 'foo', 1); // => 1
emit(ε, 'bar', 2); // => 2
```

> [!CAUTION]
> An **empty** array throws (since v6.0.0).
> Where the name list is assembled at runtime, check its `.length` before subscribing.

> [!TIP]
> Every **entry** has to be an event name — a string or a symbol (since v6.0.0).
>
> `on(ε, [123], listener)` used to file a bucket under `123` that no `emit()` could reach and no `off(ε, 123)` could remove;
> the same for `null`, `undefined` and a nested array. Filter a runtime-assembled list, or check the first slot of each `[name, priority]` tuple.
>
> A single name is checked the same way wherever a priority follows it — `on(ε, {}, 10, listener)` throws too.

##### Wildcards (`*`)

Listen to _all_ events of an object using `'*'` or by omitting the event name entirely.

```javascript
const wildcardListener = (...args) => {
  console.log('event fired with args:', args);
};

on(ε, '*', wildcardListener); // or just on(ε, wildcardListener)

emit(ε, 'foo', 1, 2); // => event fired with args: [1, 2]
emit(ε, 'bar', 'A'); // => event fired with args: ['A']
```

> [!IMPORTANT]
> A function-form wildcard listener receives **only the `emit()` arguments**. The event name is _not_ passed in.
> If you need to know which event fired, register a listener-object with an `.emit()` method instead
> — eventize falls back to it for events without a matching named method, and passes `eventName` as the first argument:
>
> ```javascript
> on(ε, {
>   emit(eventName, ...args) {
>     console.log(`Event '${eventName}' fired with:`, args);
>   },
> });
>
> emit(ε, 'foo', 1, 2); // => Event 'foo' fired with: [1, 2]
> emit(ε, 'bar', 'A'); // => Event 'bar' fired with: ['A']
> ```

> [!NOTE]
> The `.emit()` fallback also applies to **named** subscriptions. `on(ε, 'foo', listenerObj)` calls `listenerObj.emit('foo', ...args)` when
`listenerObj.foo` is not a function. A matching named method always wins over `.emit()`.

##### Forwarding events between emitters

Because the `.emit()` fallback matches the signature of the `emit` method that `eventize.inject()` (and `class extends Eventize`) install,
you can subscribe one eventized object directly as a catch-all listener of another to **forward all events**:

```javascript
const upstream = eventize.inject();
const downstream = eventize.inject();

on(downstream, 'data', (x) => console.log('downstream got', x));

on(upstream, downstream); // forward every event from upstream

emit(upstream, 'data', 42); // => downstream got 42
```

Caveats:

- The target must have an `.emit(eventName, ...args)` method. `eventize.inject(obj)` and `class extends Eventize` install one; **plain `eventize(obj)` does not** — forwarding to such a target silently does nothing.
- A target method whose name matches the event takes precedence over `.emit()`.
- **Forwarding cycles are not detected.** `A → B → A` (or same-emitter same-event re-emission from inside a listener) recurses without bound and overflows the stack. Eventize threw on this in v4.2, but the guard forbade valid scenarios; breaking cycles is now the caller's job (set a flag, gate the forward, or emit a different event).

##### Priorities

Listeners with higher priority numbers run first. The default is `0`.

```javascript
import {eventize, on, emit, Priority} from '@spearwolf/eventize';

const ε = eventize();
const calls = [];

on(ε, 'test', () => calls.push('Normal'));
on(ε, 'test', Priority.Low, () => calls.push('Low'));           // runs later
on(ε, 'test', Priority.Critical, () => calls.push('Critical')); // runs sooner

emit(ε, 'test');
console.log(calls); // => ["Critical", "Normal", "Low"]
```

`Priority` provides `Max`, `Critical`, `High`, `Medium`, `Normal`, `Low`, and `Min`.
The legacy aliases `AAA` (= `Critical`), `BB` (= `High`), `C` (= `Medium`), and `Default` (= `Normal`) are `@deprecated` on `EventizePriority`
and slated for removal in a future major — they keep working, but editors now strike them through.

A priority must be an actual number: since v6.0.0 `NaN` throws (`subscribeTo() called with a NaN priority`), in every position a priority can occupy
— up to v5.1.0 it passed and left the listener wherever the size of the bucket put it.

`Priority.Max` and `Priority.Min` are `±Infinity` and are perfectly valid — this is not a finiteness test.

To give each event of a multi-event subscription its own priority, pass `[eventName, priority]` tuples.
Tuples and bare names may be mixed freely; a tuple's priority overrides the call-level one for that event:

```javascript
on(ε, [['foo', Priority.Critical], 'bar'], listener);
// 'foo' subscribed at Critical, 'bar' at the default priority

on(ε, [['foo', Priority.Critical], ['bar', Priority.Low]], Priority.High, listener);
// both tuples win over the call-level Priority.High
```

Since v5.1 this works on typed emitters too, and tuples may be mixed with bare names;
event names inside tuples are still checked against the event map.
Earlier versions required a homogeneous array of tuples and rejected the form on typed emitters.

> [!WARNING]
> The `NaN` rule reaches into the tuples, and it rejects the whole call: a single `NaN` in one tuple leaves none of the listed names subscribed,
> and a `NaN` at call level throws even when every tuple carries its own priority to override it.

##### Listener Objects

Subscribe an object whose method names match the event names.

```javascript
const service = {
  onSave(data) {
    console.log('Saving:', data);
  },
  onDelete(id) {
    console.log('Deleting:', id);
  },
};

on(ε, service); // methods are matched to event names

emit(ε, 'onSave', {user: 'test'}); // => "Saving: { user: 'test' }"
emit(ε, 'onDelete', 123); // => "Deleting: 123"
```

Subscribing the **same listener-object** twice for the same event with `on()` does _not_ register two listeners — eventize collapses
the second call into the existing entry and increments an internal reference count, so the listener still fires once per `emit()`:

```javascript
const listener = {foo: () => console.log('foo')};

on(ε, 'foo', listener);
on(ε, 'foo', listener); // same (event, priority, listener, context) → refCount = 2

emit(ε, 'foo'); // => "foo"  (called once, not twice)
```

> [!IMPORTANT]
> De-duplication applies **only to listener-object forms** — and, since v6.0.0, `once()` shares it with `on()`: subscribing
> the same listener object through any mixture of the two calls, in any order, still yields one registration.
> Plain function listeners are **not** deduplicated: registering the same function twice produces two independent listeners that will both run.
> See [_Reference counting_](./docs/off.md#reference-counting) for details.

---

#### `once(emitter, ...args)`

Subscribes a listener that is removed automatically after its first call. Arguments are the same as for `on()`.

```javascript
once(ε, 'my-event', () => console.log('This runs only once.'));

emit(ε, 'my-event'); // => "This runs only once."
emit(ε, 'my-event'); // (nothing happens)
```

> [!NOTE]
> With multiple event names, the listener is removed after the _first_ of those events fires.

> [!NOTE]
> `once()` aggregates onto the same listener-object identity `on()` does (since v6.0.0).
> Two `once(ε, 'foo', listenerObject)` calls land on one listener: the next `emit()` calls it once and discharges both.
> An `on(ε, 'foo', listenerObject)` on the same identity keeps it subscribed after that. See [_Reference counting_](./docs/off.md#reference-counting).

#### `onceAsync(emitter, eventName | eventName[], options?)`

Returns a `Promise` that resolves with the event's first argument.

```javascript
async function waitForLoad() {
  console.log('Waiting for data...');
  const data = await onceAsync(ε, 'loaded');
  console.log('Data loaded:', data);
}

waitForLoad();

setTimeout(() => emit(ε, 'loaded', {content: '...'}), 100);
// => Waiting for data...
// => Data loaded: { content: '...' }
```

`onceAsync()` takes an optional `{signal}`. Without it, an event that never
fires keeps the listener, the resolve closure and the caller's `await`
continuation attached to the emitter for as long as the emitter lives —
there is no other handle to release them with.

```javascript
const controller = new AbortController();
try {
  const value = await onceAsync(ε, 'ready', {signal: controller.signal});
} catch (err) {
  if (err.name === 'AbortError') {
    /* cancelled */
  }
}
// somewhere in teardown:
controller.abort();
```

Aborting unsubscribes the internal `once()` and rejects with the signal's
`reason` whenever it has one, and with an `AbortError` `DOMException`
otherwise — including the `abort(null)` case, where `fetch()` would hand you a
`null` to catch. A signal that is already aborted rejects without ever
subscribing.

---

### Unsubscribing

#### `off(emitter, ...args)`

Removes listeners from an emitter — the counterpart to `on()`, for cleanup where you no longer hold the `unsubscribe` function.

```javascript
off(ε);                       // all listeners — and all retained state
off(ε, undefined);            // ⚠️ the same branch — wipes everything
off(ε, 'foo');                // all listeners for 'foo' (also unretains 'foo')
off(ε, ['foo', 'bar']);       // several events
off(ε, listenerFunc);         // that function, under every context it was registered with
off(ε, listenerFunc, ctx);    // only where it was registered with exactly that context
off(ε, listenerObject);       // every subscription of that object
off(ε, 'foo', listenerObject); // that object, on 'foo' only — but still unretains 'foo'
off(ε, '*', listenerObject);   // that object's wildcard subscription only
```

A nullish second argument is **not** a no-op. `off(ε, handlers[name])` empties the whole emitter when the lookup misses,
and one `null` or `undefined` element turns an array form into the same total wipe.
Guard the lookup, or keep the handle `on()` returned and call that.

Calling `off()` on a non-eventized object (or on `null`/`undefined` as the *emitter*) is a no-op,
which makes it safe in cleanup paths without an `isEventized()` check — safe against anything
except an object eventized by an incompatible copy of the library, which throws the same protocol `TypeError` every other call does.

Since v6.0.0 the bulk forms `off(ε)` and `off(ε, '*')` also empty the retained-events keeper
— every retained value and every retain policy goes with the listeners.
Before that they cleared only the listeners, so a subscriber arriving afterwards was still handed the old payload.

📖 **[Full `off()` reference →](./docs/off.md)** — every signature, the interaction with `retain()`, behavior during an active `emit()`, and reference counting.

---

### Emitting Events

#### `emit(emitter, eventName | eventName[], ...args)`

Dispatches an event synchronously, immediately invoking all subscribed listeners.

```javascript
on(ε, 'update', (id, data) => console.log(`Item ${id}:`, data));

emit(ε, 'update', 42, {status: 'complete'});
// => "Item 42: { status: 'complete' }"

emit(ε, ['update', 'log'], 100, {status: 'multi-event'});
```

> [!IMPORTANT]
> `'*'` is reserved for **subscribing** to all events and cannot be emitted. `emit(ε, '*', …)` throws — emit a concrete event name instead.
> (In an array form, events listed before the `'*'` element still dispatch before the throw, consistent with mid-dispatch error semantics.)
>
> Calling `emit()` from inside a listener is fine, including re-emitting the same event.
> Eventize does **not** detect forwarding cycles or same-event self-recursion — `A → B → A` (or `on(ε, 'foo', () => emit(ε, 'foo'))`)
> will overflow the stack. If you build a forwarding chain, break cycles yourself.

#### `emitAsync(emitter, ...)`

Emits an event and returns a `Promise` that resolves once all promises returned by listeners have resolved.
Non-`null` and non-`undefined` return values are collected into an array.

```javascript
on(ε, 'load', () => Promise.resolve('Data from source 1'));
on(ε, 'load', () => 'Simple data');
on(ε, 'load', () => null); // ignored

const results = await emitAsync(ε, 'load');
console.log(results); // => ["Data from source 1", "Simple data"]
```

A listener returning an array has its elements awaited with `Promise.all`, but the array stays one entry of the result:
a lone listener returning `[1, Promise.resolve(2)]` gives `[[1, 2]]`, not `[1, 2]`. Flatten yourself if you want the elements inline.

> [!NOTE]
> When nothing was collected — no listeners, or every listener returned `null`/`undefined`
> — the promise resolves to **`undefined`**, not to an empty array.

---

### Error Handling in Listeners

Listeners are dispatched **synchronously**. If a listener throws, the exception propagates out of the `emit()` call
(or out of the synchronous portion of `emitAsync()`) — eventize does **not** catch it for you.

Consequences worth knowing:

- **Dispatch is aborted.** Listeners that haven't run yet for the same `emit()` will _not_ be called. Listeners that already ran are unaffected.
- **The throwing listener stays subscribed.** It is not auto-removed; the next `emit()` calls it again.
- **`retain()` is not updated for that emit.** The retained value is written _after_ all listeners run,
  so a thrown exception leaves the previously retained value untouched.

```javascript
const calls = [];

on(ε, 'foo', () => calls.push('first'));
on(ε, 'foo', () => {
  throw new Error('boom');
});
on(ε, 'foo', () => calls.push('third')); // not reached

try {
  emit(ε, 'foo');
} catch (err) {
  console.error(err.message); // => "boom"
}

console.log(calls); // => ["first"]
```

**Three ways out.** Since v6.1.0, `emitSafe()` and `emitSafeAsync()` dispatch the same event with each listener isolated: a throw is reported through `console.warn` and the listeners behind it still run.

```javascript
const calls = [];

on(ε, 'foo', () => calls.push('first'));
on(ε, 'foo', () => {
  throw new Error('boom');
});
on(ε, 'foo', () => calls.push('third'));

emitSafe(ε, 'foo'); // no throw; console.warn reports the failure

console.log(calls); // => ["first", "third"]
```

What they guarantee is **execution, not completeness**: no listener can prevent the others from running. They do not promise that nothing went wrong, they hand you no error object, and `emitSafeAsync()` still rejects if a listener returns a rejected promise — by then every listener has already run. `emit(ε, '*')` still throws from every dispatch function. The third bullet above flips with them: nothing unwinds, so the retained value *is* written.

Two behaviours differ from `emit()`, both on purpose: the retained value **is** written, because the event was delivered; and a `once()` queued behind a throwing listener is spent, because it now runs. The throwing listener itself keeps its subscription either way.

**When you need both halves**, `emitStrict()` and `emitStrictAsync()` (v6.2.0) run every listener and then raise what failed:

```javascript
on(ε, 'foo', () => calls.push('first'));
on(ε, 'foo', () => {
  throw new Error('boom');
});
on(ε, 'foo', () => calls.push('third'));

emitStrict(ε, 'foo'); // throws "boom" — after both other listeners ran
```

One failure is rethrown unchanged, so an existing `toThrow('boom')` assertion survives the swap; two or more arrive as an `AggregateError` in dispatch order, which is a shape `emit()` could never produce. `emitStrictAsync()` collects rejected listener promises the same way and reports everything through its promise — it never throws synchronously, not even for `'*'`. [`docs/emit.md`](./docs/emit.md) has the rest, the cost included.

The third way out is unchanged and still the right one where a single listener needs its own policy: wrap that listener's body in `try/catch`. Eventize deliberately keeps no global error handler, so `emit()` stays the default and error policy stays explicit at the call site.

See [`docs/emit.md`](./docs/emit.md) for the full dispatch semantics.

> [!NOTE]
> `emitAsync()` aggregates listener return values into a single `Promise.all`. A listener returning a **rejected promise** rejects the awaited result,
> but the other listeners — being dispatched synchronously — have already run by then.
> A listener that throws synchronously still aborts dispatch in the same way as with `emit()`.

> [!NOTE]
> Mixing the two in one dispatch costs the collected values. The aggregation is built *after* the dispatch returns,
> so a synchronous throw — from a listener, or from `'*'` inside a name array — never reaches it: the caller gets the throw and nothing else.
> Every value collected up to that point is dropped, and any rejection among them is discarded along with it, so no `unhandledrejection` fires
> and Node's default `--unhandled-rejections=throw` has nothing to terminate on. What a listener already returned is unrecoverable,
> though — wrap a throwing listener in `try/catch` if the values before it matter.

---

### State Management

#### `retain(emitter, eventName | eventName[])`

Tells an emitter to hold onto the last-emitted event and its data. A new listener is immediately called with the retained data
— comparable to a `ReplaySubject(1)` in RxJS.

```javascript
import {eventize, retain, emit, on} from '@spearwolf/eventize';

const ε = eventize();

retain(ε, 'status');

emit(ε, 'status', 'ready'); // nobody is listening yet

on(ε, 'status', (currentStatus) => {
  console.log(`Status is: ${currentStatus}`);
});
// the new listener fires immediately => "Status is: ready"

emit(ε, 'status', 'running'); // => "Status is: running"
```

Only the **last** emission is kept, events emitted _before_ the `retain()` call are not stored, and `retain()` on a plain object auto-eventizes it.

`retainClear(ε, name)` discards the stored value but keeps retaining future emissions.
`unretain(ε, name)` drops the value **and** the policy. Both throw on a non-eventized object.

📖 **[Full retain reference →](./docs/retain.md)** — multiple events, symbol names, interaction with `once()`/`onceAsync()`,
and the exact difference between `retainClear` and `unretain`.

---

### Utilities

#### `isEventized(obj)`

A type guard returning `true` if an object has been processed by `eventize()`. Also available as `eventize.is(obj)`.
On a typed emitter it preserves the event map it narrows, so a wrong event name stays a compile error *inside* the `if`
— up to v5.1.0 the narrowing widened the map back to the permissive default and silently reopened every loose overload.

The marker it probes for is a property, so it is inherited like any other: `eventize(SomeClass.prototype)` makes
every instance answer `true` and share that one prototype's emitter — `on()` on one instance is reachable from `emit()` on another.
Useful for a single emitter shared by a whole class, surprising when each instance was expected to keep independent subscriptions.

```javascript
import {eventize, isEventized} from '@spearwolf/eventize';

console.log(isEventized(eventize())); // => true
console.log(isEventized({}));         // => false
console.log(eventize.is({}));         // => false
```

#### `asEventized(obj)`

The low-level primitive behind `eventize(obj)`: attaches the hidden emitter slot and returns the object, without injecting any API methods.
Idempotent — an already-eventized object is returned untouched, unless *another copy* of the library eventized it,
in which case it throws the protocol-mismatch `TypeError` rather than handing back an emitter it cannot drive.
Like `eventize()`, it refuses a non-extensible target. Reach for `eventize()` unless you specifically need the primitive.

#### `getEventizeProtocol(obj)`

Which copy of eventize eventized this object. The marker is keyed by `Symbol.for('eventize')` — realm-wide, so two majors installed
side by side share one slot and each reads the other's payload as its own. Since v6.0.0 the payload carries a protocol number,
and this is how you read it.

```javascript
import {eventize, getEventizeProtocol, isEventized} from '@spearwolf/eventize';

console.log(getEventizeProtocol(eventize())); // => 6
console.log(getEventizeProtocol({}));         // => undefined
console.log(getEventizeProtocol(null));       // => undefined
```

It never throws — it is the tool for diagnosing the situation *before* something else does. Two kinds of `undefined` come back,
and `isEventized()` separates them: `false` means the object was never eventized,
`true` means a copy from before the field existed (up to v5.1.0) got there first.

Any other number means two incompatible copies are live on the same object, and every `on()` / `emit()` / `off()` against it
throws a `TypeError` naming both protocols and the remedy — dedupe `@spearwolf/eventize` in your dependency tree.
See the [migration guide](./docs/migration.md).

#### `getSubscriptionCount(emitter)`

Returns the number of active subscriptions (named + wildcard listeners). Useful for debugging, testing, or verifying that cleanup actually happened.

```javascript
const ε = eventize();

on(ε, 'foo', () => {});
on(ε, 'bar', () => {});
on(ε, '*', () => {}); // wildcard listeners are counted too

console.log(getSubscriptionCount(ε)); // => 3

off(ε);

console.log(getSubscriptionCount(ε)); // => 0
```

Edge cases worth knowing:

- A **non-eventized** object returns `0` rather than throwing — safe to call on any input.

  ```javascript
  getSubscriptionCount({}); // => 0
  getSubscriptionCount(new Date()); // => 0
  ```

- A **wildcard listener-object** counts as a _single_ subscription, no matter how many event-named methods it exposes —
  dispatch resolves `listener[eventName]` at `emit()` time.

  ```javascript
  on(ε, {foo() {}, bar() {}, baz() {}});
  getSubscriptionCount(ε); // => 1, not 3
  ```

- Subscriptions sharing an entry through reference counting count as **one**, not as the number of `on()` calls.
- A `once()` listener counts as a normal subscription until it fires.

### Inspecting emitter state

- `getSubscriptionCount(ε)` — how many listeners are registered.
- `getSubscribedEventNames(ε)` — every event name with an active listener, named
  plus `EVENT_CATCH_EM_ALL` if a wildcard listener is registered, in
  unspecified order. `getSubscribedEventNames(ε).length === 0` agrees with
  `getSubscriptionCount(ε) === 0` exactly, and says which names when it
  doesn't.
- `getRetainedCount(ε)` — how many events hold a retained value.
- `getRetainedEventNames(ε)` — every name carrying a retain policy, fired or not.
- `getEventizeProtocol(ε)` — which copy of eventize owns the marker.

They exist for debugging, testing, and verifying that cleanup actually happened.

#### `EVENT_CATCH_EM_ALL`

The wildcard event name (`'*'`) as a named export, so you don't have to write the magic string.

---

## TypeScript: Typed Event Maps

Eventize ships an _opt-in_ generic event map for `eventize<TEvents>()`, `eventize.inject<TEvents>()`, and `class extends Eventize<TEvents>`.
The map describes each event's argument tuple, and all three surfaces pick the types up automatically — the standalone functions since v4.1,
the injected methods and the class since v6.0.0.

```ts
import {eventize, emit, on} from '@spearwolf/eventize';

interface ChatEvents {
  message: [from: string, text: string];
  joined: [user: string];
  closed: [];
}

const ε = eventize<ChatEvents>();

on(ε, 'message', (from, text) => {
  // from: string, text: string — inferred from the map
});

emit(ε, 'message', 'alice', 'hello'); // ✅
// emit(ε, 'unknown', 1);             // ❌ unknown event name
// emit(ε, 'message', 'alice');       // ❌ missing 'text'
```

Define the map as a **plain interface**. `extends EventMap` buys nothing — `EventMap` is `object`, so nothing is inherited and the narrowing survives
— but it costs an import for no effect. What actually reopens the map is an index signature written into it (`[key: string]: any[]`),
which is the deliberate escape hatch for dynamic names.

Every value has to be an argument tuple — `[]` for an event carrying none; a `readonly` tuple and an optional key are both fine
and both checked positionally. A value that is not an array makes `emit(ε, 'name', …)`, `on(ε, 'name', fn)` and the typed listener-object form
compile errors for that key since v6.

Without a generic, every API behaves exactly like v4.0.x:
arbitrary event names, arbitrary arguments, listener-objects with whatever method names you like.

#### Exported types

Everything below is exported from the package root, and each exists to be written by a consumer rather than merely inferred:

| Type | What it is for |
| --- | --- |
| `EventName` | `string \| symbol` — what an event may be called, and the element type `getSubscribedEventNames()` / `getRetainedEventNames()` hand back |
| `EventMap`, `DefaultEventMap` | the constraint on an event map, and the permissive default |
| `EventizedObject<TEvents>` | the branded emitter type. A class that calls `eventize(this)` declaration-merges with it — `export interface MyClass extends EventizedObject {}` — so that `emit(this, …)` inside the class type-checks; see the [migration guide](./docs/migration.md#v40x--v43x-the-type-brand-on-classes). Extend this, never `EventizeApi`, which carries the method signatures and collides with same-named methods on the host class |
| `UnsubscribeFunc` | what `on()` and `once()` hand back — `() => void`, nothing else |
| `OnceAsyncOptions` | the `{signal?: AbortSignal}` bag `onceAsync()` takes |
| `EventNameWithPriority` | the `[eventName, priority]` tuple the array form of `on()` accepts |
| `StandaloneSubscribeFunc` | the exported overload set of the standalone `on()` / `once()`, for a wrapper that needs to match it exactly rather than widen to `SubscribeImpl` |
| `ListenerObjectSlot<L>` | the listener-object constraint that rejects an array, a function and nullish — appears in the published `on()` / `once()` signatures |
| `MultiArgsFor<T, K>` | the merged argument list for a listener serving several event keys at once |
| `AnyEventNames` | the event-name argument of `emit()` / `retain()`: a single name or an array of names, with no notion of priority |
| `EventArgs` | `any[]`, the loose argument-list type a subclass override of `emit()` forwards through untyped |
| `SubscribeImpl` | the implementation signature to cast to when writing a forwarding wrapper around `on()` / `once()` |
| `SubscribeArgs` | the union of every accepted argument shape, plus eleven named arms so a wrapper can name the one it handles |

The last two are the ones worth knowing about: TypeScript refuses to spread a union of tuples into a fixed-arity call,
so `on(target, ...args)` never compiles against the public overloads.
`const rawOn = on as SubscribeImpl` is the sanctioned escape — see [`docs/typed-events.md` → Wrapping `on()` / `once()`](./docs/typed-events.md#wrapping-on--once).

📖 **[Full typed-events reference →](./docs/typed-events.md)** — typed listener-objects, the inject and class forms, symbol events as an escape hatch,
and the caveats around `off()` and multi-event calls.

---
_Source: https://npm.io/package/@spearwolf/eventize · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
