Files
lcbp3.np-dms.work/frontend/node_modules/rettime/README.md
2025-09-21 20:29:15 +07:00

311 lines
10 KiB
Markdown

# Rettime
A type-safe marriage of `EventTarget` and `EventEmitter`.
## Features
- 🎯 **Event-based**. Control event flow: prevent defaults, stop propagation, cancel events. Something your common `Emitter` can't do.
- 🗼 **Emitter-inspired**. Emit event types and data, don't bother with creating `Event` instances. A bit less verbosity than a common `EventTarget`.
- ⛑️ **Type-safe**. Describe the exact event types and payloads accepted by the emitter. Never emit or listen to unknown events.
- 🧰 **Convenience methods** like `.emitAsPromise()` and `.emitAsGenerator()` to build more complex event-driven systems.
- 🐙 **Tiny**. 700B gzipped.
> [!WARNING]
> This library **does not** have performance as the end goal. In fact, since it operates on events and supports event cancellation, it will likely be _slower_ than the emitters that don't do that.
## Motivation
### Why not just `EventTarget`?
The `EventTarget` API is fantastic. It works in the browser and in Node.js, dispatches actual events, supports cancellation, etc. At the same time, it has a number of flaws that prevent me from using it for anything serious:
- Complete lack of type safety. The `type` in `new Event(type)` is not a type argument in `lib.dom.ts`. It's always `string`. It means it's impossible to narrow it down to a literal string type to achieve type safety.
- No concept of `.prependListener()`. There is no way to add a listener to run _first_, before other existing listeners.
- No concept of `.removeAllListeners()`. You have to remove each individual listener by hand. Good if you own the listeners, not so good if you don't.
- No concept of `.listenerCount()` or knowing if a dispatched event had any listeners (the `boolean` returned from `.dispatch()` indicates if the event has been prevented, not whether it had any listeners).
- (Opinionated) Verbose. I prefer `.on()` over `.addEventListener()`. I prefer passing data than constructing `new MessageEvent()` all the time.
### Why not just `Emitter` (in Node.js)?
The `Emitter` API in Node.js is great, but it has its own downsides:
- Node.js-specific. `Emitter` does not work in the browser.
- Lacks any type safety.
- No concept of `.stopPropagation()` and `.stopImmediatePropagation()`. Those methods are defined but literally do nothing.
## Install
```sh
npm install rettime
```
## API
### `TypedEvent`
`TypedEvent` is a subset of `MessageEvent` that allows for type-safe event declaration.
```ts
new TypedEvent<DataType, ReturnType, EventType>(type: EventType, { data: DataType })
```
> The `data` argument depends on the `DataType` of your event. Use `void` if the event must not send any data.
#### Custom events
You can implement custom events by extending the default `TypedEvent` class and forwarding the type arguments that it expects:
```ts
class GreetingEvent<
DataType = void,
ReturnType = any,
EventType extends string = string,
> extends TypedEvent<DataType, ReturnType, EventType> {
public id: string
}
const emitter = new Emitter<{ greeting: GreetingEvent<'john'> }>()
emitter.on('greeting', (event) => {
console.log(event instanceof GreetingEvent) // true
console.log(event instanceof TypedEvent) // true
console.log(event instanceof MessageEvent) // true
console.log(event.type) // "greeting"
console.log(event.data) // "john"
console.log(event.id) // string
})
```
### `Emitter`
```ts
new Emitter<EventMap>()
```
The `EventMap` type argument allows you describe the supported event types, their payload, and the return type of their event listeners.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ hello: TypedEvent<string, number> }>()
emitter.on('hello', () => 1) // ✅
emitter.on('hello', () => 'oops') // ❌ string not assignable to type number
emitter.emit(new TypedEvent('hello', { data: 'John' })) // ✅
emitter.emit(new TypedEvent('hello', { data: 123 })) // ❌ number is not assignable to type string
emitter.emit(new TypedEvent('hello')) // ❌ missing data argument of type string
emitter.emit(new TypedEvent('unknown')) // ❌ "unknown" does not satisfy "hello"
```
#### Describing events
The `Emitter` class requires a type argument that describes the event map. If you do not provide that argument, adding listeners or emitting events will produce a type error as your emitter doesn't have an event map defined.
An event map is an object of the following shape:
```ts
{
[type: string]: TypedEvent
}
```
The `type` is a string indicating the event type (e.g. `greet` or `ping`). The array it accepts has two members: `args` describes the arguments accepted by this event (can also be `never` for events without arguments) and `returnValue` is an optional type for the data returned from the listeners for this event.
Let's say you want to define a `greet` event that expects a user name as data and returns a greeting string:
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ greet: TypedEvent<string, string> }>()
emitter.on('greet', (event) => {
console.log(`Hello, ${event.data}!`)
})
emitter.emit(new TypedEvent('greet', { data: 'John' }))
// "Hello, John!"
```
Here's another example where we define a `ping` event that has no arguments but returns a timestamp for each ping:
```ts
const emitter = new Emitter<{ ping: TypedEvent<void, number> }>()
emitter.on('ping', () => Date.now())
const results = await emitter.emitAsPromise(new TypedEvent('ping'))
// [1745658424732]
```
> [!IMPORTANT]
> When providing type arguments to your `TypedEvents`, you **do not** need to provide the `EventType` argument—it will be inferred from your event map.
### `.on(type, listener[, options])`
Adds an event listener for the given event type.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ hello: TypedEvent<string> }>()
emitter.on('hello', (event) => {
// `event` is a `TypedEvent` instance derived from `MessageEvent`.
console.log(event.data)
})
```
All methods that add new listeners return an `AbortController` instance bound to that listener. You can use that controller to cancel the event handling, including mid-air:
```ts
const controller = emitter.on('hello', listener)
controller.abort(reason)
```
All methods that add new listeners also accept an optional `options` argument. You can use it to configure event handling behavior. For example, you can provide an existing `AbortController` signal as the `options.signal` value so the attached listener abides by your controller:
```ts
emitter.on('hello', listener, { signal: controller.signal })
```
> Both the public controller of the event and your custom controller are combined using `AbortSignal.any()`.
### `.once(type, listener[, options])`
Adds a one-time event listener for the given event type.
### `.earlyOn(type, listener[, options])`
Prepends a listener for the given event type.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ hello: TypedEvent<void, number> }>()
emitter.on('hello', () => 1)
emitter.earlyOn('hello', () => 2)
const results = await emitter.emitAsPromise(new TypedEvent('hello'))
// [2, 1]
```
### `.earlyOnce(type, listener[, options])`
Prepends a one-time listener for the given event type.
### `.emit(type[, data])`
Emits the given event with optional data.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ hello: TypedEvent<string> }>()
emitter.on('hello', (event) => console.log(event.data))
emitter.emit(new TypedEvent('hello', 'John'))
```
### `.emitAsPromise(type[, data])`
Emits the given event and returns a Promise that resolves with the returned data of all matching event listeners, or rejects whenever any of the matching event listeners throws an error.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ hello: TypedEvent<number, Promise<number>> }>()
emitter.on('hello', async (event) => {
await sleep(100)
return event.data + 1
})
emitter.on('hello', async (event) => event.data + 2)
const values = await emitter.emitAsPromise(new TypedEvent('hello', { data: 1 }))
// [2, 3]
```
> Unlike `.emit()`, the `.emitAsPromise()` method _awaits asynchronous listeners_.
### `.emitAsGenerator(type[, data])`
Emits the given event and returns a generator function that exhausts all matching event listeners. Using a generator gives you granular control over what listeners are called.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ hello: TypedEvent<string, number> }>()
emitter.on('hello', () => 1)
emitter.on('hello', () => 2)
for (const listenerResult of emitter.emitAsGenerator(
new TypedEvent('hello', { data: 'John' }),
)) {
// Stop event emission if a listener returns a particular value.
if (listenerResult === 1) {
break
}
}
```
### `.listeners([type])`
Returns the list of all event listeners matching the given event type. If no event `type` is provided, returns the list of all existing event listeners.
### `.listenerCount([type])`
Returns the number of the event listeners matching the given event type. If no event `type` is provided, returns the total number of existing listeners.
### `.removeListener(type, listener)`
Removes the event listener for the given event type.
### `.removeAllListeners([type])`
Removes all event listeners for the given event type. If no event `type` is provided, removes all existing event listeners.
## Types
This library also comes with a set of helper types to make your life easier.
### `Emitter.EventType`
Returns the `Event` type (or its subtype) representing the given listener.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ greeting: TypedEvent<'john'> }>()
type GreetingEvent = Emitter.EventType<typeof emitter, 'greeting'>
// TypedEvent<'john'>
```
### `Emitter.ListenerType`
Returns the type of the given event's listener.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ greeting: TypedEvent<string, number[]> }>()
type GreetingListener = Emitter.ListenerType<typeof emitter, 'greeting'>
// (event: TypedEvent<string>) => number[]
```
> The `ListenerType` helper is in itself type-safe, allowing only known event types as the second argument.
### `Emitter.ListenerReturnType`
Returns the return type of the given event's listener.
```ts
import { Emitter, TypedEvent } from 'rettime'
const emitter = new Emitter<{ getTotalPrice: TypedEvent<Cart, number> }>()
type CartTotal = Emitter.ListenerReturnType<typeof emitter, 'getTotalPrice'>
// number
```