@asaidimu/utils-events 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +303 -0
- package/index.d.mts +172 -0
- package/index.d.ts +172 -0
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/package.json +63 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Saidimu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# @asaidimu/utils-events
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@asaidimu/utils-events)
|
|
4
|
+
[](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/asaidimu/erp-utils/actions)
|
|
6
|
+
|
|
7
|
+
A lightweight, type-safe event bus for TypeScript applications with batching, cross-instance broadcast, and built-in metrics.
|
|
8
|
+
|
|
9
|
+
> **Why another event bus?**
|
|
10
|
+
> Many existing solutions either lack type safety or force a heavy dependency. This bus provides full TypeScript inference, optional batching to reduce synchronous dispatch overhead, and automatic cross-instance synchronisation โ all in a tiny, zero-dependency package.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ๐ Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Overview & Features](#overview--features)
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [`createEventBus`](#createeventbus)
|
|
21
|
+
- [`.subscribe()`](#subscribe)
|
|
22
|
+
- [`.once()`](#once)
|
|
23
|
+
- [`.emit()`](#emit)
|
|
24
|
+
- [`.metrics()`](#metrics)
|
|
25
|
+
- [`.clear()`](#clear)
|
|
26
|
+
- [Advanced Usage](#advanced-usage)
|
|
27
|
+
- [Batching / Deferred Mode](#batching--deferred-mode)
|
|
28
|
+
- [Cross-instance Broadcast](#cross-instance-broadcast)
|
|
29
|
+
- [Custom Error Handling](#custom-error-handling)
|
|
30
|
+
- [Architecture Notes](#architecture-notes)
|
|
31
|
+
- [Development & Contributing](#development--contributing)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Overview & Features
|
|
37
|
+
|
|
38
|
+
`@asaidimu/utils-events` is a typed event bus designed for modern frontend and Node.js applications. It lets you define a strongly-typed event map once, then enjoy full autocompletion and compile-time checks for every `emit`, `subscribe`, and `once` call.
|
|
39
|
+
|
|
40
|
+
### Key Features
|
|
41
|
+
|
|
42
|
+
- ๐ **Type-safe** โ infer payload types from a single `EventMap` interface.
|
|
43
|
+
- โก **Batching (deferred mode)** โ coalesce rapid-fire events into a single flush to reduce layout thrashing and improve performance.
|
|
44
|
+
- ๐ก **Cross-instance broadcast** โ synchronise events across browser tabs using `BroadcastChannel` (with graceful fallback).
|
|
45
|
+
- ๐ฉบ **Built-in metrics** โ track total events, active subscriptions, per-event counts, and average dispatch duration.
|
|
46
|
+
- ๐งน **Cleanup utilities** โ unsubscribe handles, `.clear()` to fully reset the bus.
|
|
47
|
+
- ๐ชถ **Zero runtime dependencies** โ small footprint, easy to audit.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @asaidimu/utils-events
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
yarn add @asaidimu/utils-events
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm add @asaidimu/utils-events
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> **Requirements**: TypeScript 4.7+ (for `Record` generic inference) and a modern runtime that supports `performance.now()` and optionally `BroadcastChannel`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createEventBus } from '@asaidimu/utils-events';
|
|
73
|
+
|
|
74
|
+
// 1. Define your event map
|
|
75
|
+
interface AppEvents {
|
|
76
|
+
userLogin: { userId: string; name: string };
|
|
77
|
+
dataUpdate: { records: number };
|
|
78
|
+
error: { message: string; code?: number };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Create the bus
|
|
82
|
+
const bus = createEventBus<AppEvents>();
|
|
83
|
+
|
|
84
|
+
// 3. Subscribe
|
|
85
|
+
const unsubscribe = bus.subscribe('userLogin', (payload) => {
|
|
86
|
+
console.log(`Welcome ${payload.name}!`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 4. Emit an event
|
|
90
|
+
bus.emit({ name: 'userLogin', payload: { userId: '123', name: 'Alice' } });
|
|
91
|
+
// Logs: "Welcome Alice!"
|
|
92
|
+
|
|
93
|
+
// 5. Unsubscribe when done
|
|
94
|
+
unsubscribe();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## API Reference
|
|
100
|
+
|
|
101
|
+
### `createEventBus`
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
function createEventBus<TEventMap extends Record<string, any>>(
|
|
105
|
+
options?: EventBusOptions
|
|
106
|
+
): EventBus<TEventMap>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Creates a new event bus instance.
|
|
110
|
+
|
|
111
|
+
#### Options
|
|
112
|
+
|
|
113
|
+
| Option | Type | Default | Description |
|
|
114
|
+
| -------------------- | ----------------------------- | --------------------------- | --------------------------------------------------------------------------- |
|
|
115
|
+
| `batch.size` | `number` | `undefined` | Enables **deferred mode**; flush when queue reaches this size. |
|
|
116
|
+
| `batch.delay` | `number` | `1000` (if batching) | Quiet period (ms) before flushing a batch. |
|
|
117
|
+
| `errorHandler` | `(error: EventError) => void` | `console.error` | Custom error handler for subscriber callbacks. |
|
|
118
|
+
| `broadcast.channel` | `string` | `"event-bus-channel"` | Enables cross-instance broadcast using the given `BroadcastChannel` name. |
|
|
119
|
+
|
|
120
|
+
> If `batch.size` is provided, the bus runs in **deferred mode** (events are queued and flushed asynchronously). Otherwise it runs in **synchronous mode** (events are dispatched immediately).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### `.subscribe()`
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
subscribe<TEventName extends keyof TEventMap>(
|
|
128
|
+
eventName: TEventName,
|
|
129
|
+
callback: (payload: TEventMap[TEventName]) => void
|
|
130
|
+
): () => void
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Registers a permanent listener. Returns an **unsubscribe function**.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const off = bus.subscribe('dataUpdate', ({ records }) => {
|
|
137
|
+
updateUI(records);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Later
|
|
141
|
+
off();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### `.once()`
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
once<TEventName extends keyof TEventMap>(
|
|
150
|
+
eventName: TEventName,
|
|
151
|
+
callback: (payload: TEventMap[TEventName]) => void
|
|
152
|
+
): () => void
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Registers a one-time listener that automatically unsubscribes after the first emission. Returns a **cancel function** (to unsubscribe before it fires).
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
bus.once('userLogin', (payload) => {
|
|
159
|
+
console.log('First login only');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// The callback will run at most once.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `.emit()`
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
emit<TEventName extends keyof TEventMap>(
|
|
171
|
+
event: {
|
|
172
|
+
name: TEventName;
|
|
173
|
+
payload: TEventMap[TEventName];
|
|
174
|
+
}
|
|
175
|
+
): void
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Dispatches an event. In **synchronous mode** all subscribers run immediately. In **deferred mode** the event is queued and flushed according to `batch.size` and `batch.delay`. Cross-instance messages are sent **immediately** even in deferred mode to avoid latency.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
bus.emit({ name: 'dataUpdate', payload: { records: 42 } });
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### `.metrics()`
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
metrics(): EventMetrics
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Returns performance and usage statistics.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
console.log(bus.metrics());
|
|
196
|
+
// {
|
|
197
|
+
// totalEvents: 127,
|
|
198
|
+
// activeSubscriptions: 5,
|
|
199
|
+
// eventCounts: Map { 'userLogin' => 45, 'dataUpdate' => 82 },
|
|
200
|
+
// averageEmitDuration: 0.32 // ms
|
|
201
|
+
// }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### `.clear()`
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
clear(): void
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Removes all subscriptions, clears the event queue (if in deferred mode), resets all metrics, and **re-opens** the `BroadcastChannel` (if enabled). After calling `clear()` the bus is fully reusable.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
bus.clear(); // fresh start
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Advanced Usage
|
|
221
|
+
|
|
222
|
+
### Batching / Deferred Mode
|
|
223
|
+
|
|
224
|
+
When many events are fired in rapid succession (e.g., keystrokes, scroll handlers), synchronous dispatch can cause performance issues. Batching coalesces them into a single microtask / timer flush.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const batchedBus = createEventBus<MyEvents>({
|
|
228
|
+
batch: {
|
|
229
|
+
size: 20, // flush after 20 queued events
|
|
230
|
+
delay: 100 // or after 100ms of inactivity
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- If the queue reaches `batch.size` before the timer expires, it flushes immediately.
|
|
236
|
+
- The timer resets on every new event (quiet period).
|
|
237
|
+
- Metrics are still collected per event, so you can measure the real dispatch cost.
|
|
238
|
+
|
|
239
|
+
### Cross-instance Broadcast
|
|
240
|
+
|
|
241
|
+
Enable the `broadcast` option to automatically send every emitted event to other instances that share the same `channel` name. Events received from another instances are dispatched to **local subscribers** exactly as if they were emitted locally.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const bus = createEventBus<MyEvents>({
|
|
245
|
+
broadcast: { channel: 'my-app-events' }
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// In tab A
|
|
249
|
+
bus.emit({ name: 'userLogin', payload: { userId: '1' } });
|
|
250
|
+
|
|
251
|
+
// In tab B (same origin)
|
|
252
|
+
bus.subscribe('userLogin', (payload) => {
|
|
253
|
+
console.log('Another tab logged in:', payload.userId);
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
> โ ๏ธ `BroadcastChannel` is **not supported** in Node.js. The bus will log a warning and disable cross-instance functionality gracefully. For Node.js, simply omit the `broadcast` option.
|
|
258
|
+
|
|
259
|
+
### Custom Error Handling
|
|
260
|
+
|
|
261
|
+
By default, any error thrown inside a subscriber callback is caught and logged to `console.error`. Override this to send errors to a monitoring service.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const bus = createEventBus<MyEvents>({
|
|
265
|
+
errorHandler: (err) => {
|
|
266
|
+
myErrorTracker.capture(err, {
|
|
267
|
+
eventName: err.eventName,
|
|
268
|
+
payload: err.payload
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The `EventError` interface extends `Error` and adds optional `eventName` and `payload` fields.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Architecture Notes
|
|
279
|
+
|
|
280
|
+
- **Subscriber snapshotting**: When an event is dispatched, the bus iterates over a **snapshot** of the current subscribers. This prevents bugs where a callback calls `unsubscribe()` on itself and stops other listeners from running.
|
|
281
|
+
- **Metrics overhead**: `performance.now()` is called twice per synchronous event (or per event inside a batch). This overhead is negligible (< 0.01ms) but can be ignored if not needed.
|
|
282
|
+
- **Cross-instance isolation**: `BroadcastChannel` does **not** send messages to the originating tab. Therefore there is no risk of infinite loops when broadcasting.
|
|
283
|
+
- **Debouncer**: The batching mechanism uses an internal `Debouncer` utility that guarantees a final flush after the quiet period, even if no new events arrive.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Reporting Issues
|
|
288
|
+
|
|
289
|
+
Please use the [GitHub issue tracker](https://github.com/asaidimu/erp-utils/issues) and include:
|
|
290
|
+
|
|
291
|
+
- A minimal reproduction (code snippet)
|
|
292
|
+
- Expected vs actual behaviour
|
|
293
|
+
- Environment (browser / Node, version)
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT ยฉ [Saidimu](https://github.com/asaidimu). See [LICENSE](https://github.com/asaidimu/erp-utils/blob/main/LICENSE) for details.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
**Built with โค๏ธ for type-safe event-driven architectures.**
|
package/index.d.mts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result when the debounced function successfully executed.
|
|
3
|
+
*/
|
|
4
|
+
type DebouncerOk<T> = {
|
|
5
|
+
status: "ok";
|
|
6
|
+
value: T;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Result when the debounced function threw an error.
|
|
10
|
+
*/
|
|
11
|
+
type DebouncerError = {
|
|
12
|
+
status: "error";
|
|
13
|
+
error: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Result when the debounced call was cancelled before execution.
|
|
17
|
+
*/
|
|
18
|
+
type DebouncerCancelled = {
|
|
19
|
+
status: "cancelled";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Discriminated union of all possible Debouncer outcomes.
|
|
23
|
+
*
|
|
24
|
+
* - `DebouncerOk<T>`: function ran and returned a value.
|
|
25
|
+
* - `DebouncerError`: function ran and threw an error.
|
|
26
|
+
* - `DebouncerCancelled`: call was cancelled before it ran.
|
|
27
|
+
*/
|
|
28
|
+
type DebouncerResult<T> = DebouncerOk<T> | DebouncerError | DebouncerCancelled;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for configuring the EventBus.
|
|
32
|
+
* Each field has an independent default โ passing a partial object is safe.
|
|
33
|
+
*/
|
|
34
|
+
interface EventBusOptions {
|
|
35
|
+
batch?: {
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of events to accumulate before forcing a synchronous
|
|
38
|
+
* flush, regardless of the batch delay timer.
|
|
39
|
+
*/
|
|
40
|
+
size: number;
|
|
41
|
+
/**
|
|
42
|
+
* Milliseconds to wait (quiet period) before flushing a batch.
|
|
43
|
+
* Only applies when `deferred: true`.
|
|
44
|
+
* @default 16
|
|
45
|
+
*/
|
|
46
|
+
delay?: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Called when a subscriber callback throws. Defaults to console.error.
|
|
50
|
+
* Errors in one subscriber do not prevent other subscribers from running.
|
|
51
|
+
*/
|
|
52
|
+
errorHandler?: (error: EventError) => void;
|
|
53
|
+
/**
|
|
54
|
+
* When set, events are broadcast to other instances via BroadcastChannel.
|
|
55
|
+
* Only applies in environments that support BroadcastChannel.
|
|
56
|
+
*/
|
|
57
|
+
broadcast?: {
|
|
58
|
+
/**
|
|
59
|
+
* The BroadcastChannel name used for cross-tab communication.
|
|
60
|
+
* Must be unique per logical bus if you run multiple buses.
|
|
61
|
+
* Only applies when `crossTab: true`.
|
|
62
|
+
*/
|
|
63
|
+
channel: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Interface defining the shape of the EventBus.
|
|
68
|
+
* @template TEventMap - A record mapping event names to their respective payload types.
|
|
69
|
+
*/
|
|
70
|
+
interface EventBus<TEventMap extends Record<string, any>> {
|
|
71
|
+
/**
|
|
72
|
+
* Subscribes to a specific event by name.
|
|
73
|
+
* @param eventName - The name of the event to subscribe to.
|
|
74
|
+
* @param callback - The function to call when the event is emitted.
|
|
75
|
+
* @returns A function to unsubscribe from the event.
|
|
76
|
+
*/
|
|
77
|
+
subscribe: <TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void) => () => void;
|
|
78
|
+
/**
|
|
79
|
+
* Subscribes to an event and automatically unsubscribes after it fires once.
|
|
80
|
+
* @param eventName - The name of the event to subscribe to.
|
|
81
|
+
* @param callback - The function to call when the event is emitted.
|
|
82
|
+
* @returns A function to cancel the one-shot subscription before it fires.
|
|
83
|
+
*/
|
|
84
|
+
once: <TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void) => () => void;
|
|
85
|
+
/**
|
|
86
|
+
* Emits an event with a payload to all subscribed listeners.
|
|
87
|
+
* @param event - An object containing the event name and payload.
|
|
88
|
+
*/
|
|
89
|
+
emit: <TEventName extends keyof TEventMap>(event: {
|
|
90
|
+
name: TEventName;
|
|
91
|
+
payload: TEventMap[TEventName];
|
|
92
|
+
}) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Retrieves metrics about event bus usage.
|
|
95
|
+
* @returns An object containing various metrics.
|
|
96
|
+
*/
|
|
97
|
+
metrics: () => EventMetrics;
|
|
98
|
+
/**
|
|
99
|
+
* Clears all subscriptions and resets metrics.
|
|
100
|
+
* After calling clear(), the bus is fully reset and can be reused โ
|
|
101
|
+
* cross-tab communication is re-established if it was previously enabled.
|
|
102
|
+
*/
|
|
103
|
+
clear: () => void;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Interface defining the metrics tracked by the EventBus.
|
|
107
|
+
*/
|
|
108
|
+
interface EventMetrics {
|
|
109
|
+
/** Total number of events emitted (both sync and deferred paths). */
|
|
110
|
+
totalEvents: number;
|
|
111
|
+
/** Number of active subscriptions across all event names. */
|
|
112
|
+
activeSubscriptions: number;
|
|
113
|
+
/** Map of event names to their emission counts. */
|
|
114
|
+
eventCounts: Map<string, number>;
|
|
115
|
+
/** Average duration of event dispatch in milliseconds. */
|
|
116
|
+
averageEmitDuration: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Custom error interface for event bus errors.
|
|
120
|
+
* @extends Error
|
|
121
|
+
*/
|
|
122
|
+
interface EventError extends Error {
|
|
123
|
+
/** Optional name of the event that caused the error. */
|
|
124
|
+
eventName?: string;
|
|
125
|
+
/** Optional payload that caused the error. */
|
|
126
|
+
payload?: unknown;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Creates a typed event bus.
|
|
131
|
+
*/
|
|
132
|
+
declare function createEventBus<TEventMap extends Record<string, any>>(options?: EventBusOptions): EventBus<TEventMap>;
|
|
133
|
+
|
|
134
|
+
declare class Events<TEventMap extends Record<string, any>> implements EventBus<TEventMap> {
|
|
135
|
+
private bus;
|
|
136
|
+
constructor(options?: EventBusOptions);
|
|
137
|
+
/**
|
|
138
|
+
* Subscribes to a specific event by name.
|
|
139
|
+
* @param eventName - The name of the event to subscribe to.
|
|
140
|
+
* @param callback - The function to call when the event is emitted.
|
|
141
|
+
* @returns A function to unsubscribe from the event.
|
|
142
|
+
*/
|
|
143
|
+
subscribe<TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void): () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Subscribes to an event and automatically unsubscribes after it fires once.
|
|
146
|
+
* @param eventName - The name of the event to subscribe to.
|
|
147
|
+
* @param callback - The function to call when the event is emitted.
|
|
148
|
+
* @returns A function to cancel the one-shot subscription before it fires.
|
|
149
|
+
*/
|
|
150
|
+
once<TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void): () => void;
|
|
151
|
+
/**
|
|
152
|
+
* Emits an event with a payload to all subscribed listeners.
|
|
153
|
+
* @param event - An object containing the event name and payload.
|
|
154
|
+
*/
|
|
155
|
+
emit<TEventName extends keyof TEventMap>(event: {
|
|
156
|
+
name: TEventName;
|
|
157
|
+
payload: TEventMap[TEventName];
|
|
158
|
+
}): void;
|
|
159
|
+
/**
|
|
160
|
+
* Retrieves metrics about event bus usage.
|
|
161
|
+
* @returns An object containing various metrics.
|
|
162
|
+
*/
|
|
163
|
+
metrics(): EventMetrics;
|
|
164
|
+
/**
|
|
165
|
+
* Clears all subscriptions and resets metrics.
|
|
166
|
+
* After calling clear(), the bus is fully reset and can be reused โ
|
|
167
|
+
* cross-tab communication is re-established if it was previously enabled.
|
|
168
|
+
*/
|
|
169
|
+
clear(): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { type DebouncerResult, type EventBus, type EventBusOptions, type EventError, type EventMetrics, Events, createEventBus };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result when the debounced function successfully executed.
|
|
3
|
+
*/
|
|
4
|
+
type DebouncerOk<T> = {
|
|
5
|
+
status: "ok";
|
|
6
|
+
value: T;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Result when the debounced function threw an error.
|
|
10
|
+
*/
|
|
11
|
+
type DebouncerError = {
|
|
12
|
+
status: "error";
|
|
13
|
+
error: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Result when the debounced call was cancelled before execution.
|
|
17
|
+
*/
|
|
18
|
+
type DebouncerCancelled = {
|
|
19
|
+
status: "cancelled";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Discriminated union of all possible Debouncer outcomes.
|
|
23
|
+
*
|
|
24
|
+
* - `DebouncerOk<T>`: function ran and returned a value.
|
|
25
|
+
* - `DebouncerError`: function ran and threw an error.
|
|
26
|
+
* - `DebouncerCancelled`: call was cancelled before it ran.
|
|
27
|
+
*/
|
|
28
|
+
type DebouncerResult<T> = DebouncerOk<T> | DebouncerError | DebouncerCancelled;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for configuring the EventBus.
|
|
32
|
+
* Each field has an independent default โ passing a partial object is safe.
|
|
33
|
+
*/
|
|
34
|
+
interface EventBusOptions {
|
|
35
|
+
batch?: {
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of events to accumulate before forcing a synchronous
|
|
38
|
+
* flush, regardless of the batch delay timer.
|
|
39
|
+
*/
|
|
40
|
+
size: number;
|
|
41
|
+
/**
|
|
42
|
+
* Milliseconds to wait (quiet period) before flushing a batch.
|
|
43
|
+
* Only applies when `deferred: true`.
|
|
44
|
+
* @default 16
|
|
45
|
+
*/
|
|
46
|
+
delay?: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Called when a subscriber callback throws. Defaults to console.error.
|
|
50
|
+
* Errors in one subscriber do not prevent other subscribers from running.
|
|
51
|
+
*/
|
|
52
|
+
errorHandler?: (error: EventError) => void;
|
|
53
|
+
/**
|
|
54
|
+
* When set, events are broadcast to other instances via BroadcastChannel.
|
|
55
|
+
* Only applies in environments that support BroadcastChannel.
|
|
56
|
+
*/
|
|
57
|
+
broadcast?: {
|
|
58
|
+
/**
|
|
59
|
+
* The BroadcastChannel name used for cross-tab communication.
|
|
60
|
+
* Must be unique per logical bus if you run multiple buses.
|
|
61
|
+
* Only applies when `crossTab: true`.
|
|
62
|
+
*/
|
|
63
|
+
channel: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Interface defining the shape of the EventBus.
|
|
68
|
+
* @template TEventMap - A record mapping event names to their respective payload types.
|
|
69
|
+
*/
|
|
70
|
+
interface EventBus<TEventMap extends Record<string, any>> {
|
|
71
|
+
/**
|
|
72
|
+
* Subscribes to a specific event by name.
|
|
73
|
+
* @param eventName - The name of the event to subscribe to.
|
|
74
|
+
* @param callback - The function to call when the event is emitted.
|
|
75
|
+
* @returns A function to unsubscribe from the event.
|
|
76
|
+
*/
|
|
77
|
+
subscribe: <TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void) => () => void;
|
|
78
|
+
/**
|
|
79
|
+
* Subscribes to an event and automatically unsubscribes after it fires once.
|
|
80
|
+
* @param eventName - The name of the event to subscribe to.
|
|
81
|
+
* @param callback - The function to call when the event is emitted.
|
|
82
|
+
* @returns A function to cancel the one-shot subscription before it fires.
|
|
83
|
+
*/
|
|
84
|
+
once: <TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void) => () => void;
|
|
85
|
+
/**
|
|
86
|
+
* Emits an event with a payload to all subscribed listeners.
|
|
87
|
+
* @param event - An object containing the event name and payload.
|
|
88
|
+
*/
|
|
89
|
+
emit: <TEventName extends keyof TEventMap>(event: {
|
|
90
|
+
name: TEventName;
|
|
91
|
+
payload: TEventMap[TEventName];
|
|
92
|
+
}) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Retrieves metrics about event bus usage.
|
|
95
|
+
* @returns An object containing various metrics.
|
|
96
|
+
*/
|
|
97
|
+
metrics: () => EventMetrics;
|
|
98
|
+
/**
|
|
99
|
+
* Clears all subscriptions and resets metrics.
|
|
100
|
+
* After calling clear(), the bus is fully reset and can be reused โ
|
|
101
|
+
* cross-tab communication is re-established if it was previously enabled.
|
|
102
|
+
*/
|
|
103
|
+
clear: () => void;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Interface defining the metrics tracked by the EventBus.
|
|
107
|
+
*/
|
|
108
|
+
interface EventMetrics {
|
|
109
|
+
/** Total number of events emitted (both sync and deferred paths). */
|
|
110
|
+
totalEvents: number;
|
|
111
|
+
/** Number of active subscriptions across all event names. */
|
|
112
|
+
activeSubscriptions: number;
|
|
113
|
+
/** Map of event names to their emission counts. */
|
|
114
|
+
eventCounts: Map<string, number>;
|
|
115
|
+
/** Average duration of event dispatch in milliseconds. */
|
|
116
|
+
averageEmitDuration: number;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Custom error interface for event bus errors.
|
|
120
|
+
* @extends Error
|
|
121
|
+
*/
|
|
122
|
+
interface EventError extends Error {
|
|
123
|
+
/** Optional name of the event that caused the error. */
|
|
124
|
+
eventName?: string;
|
|
125
|
+
/** Optional payload that caused the error. */
|
|
126
|
+
payload?: unknown;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Creates a typed event bus.
|
|
131
|
+
*/
|
|
132
|
+
declare function createEventBus<TEventMap extends Record<string, any>>(options?: EventBusOptions): EventBus<TEventMap>;
|
|
133
|
+
|
|
134
|
+
declare class Events<TEventMap extends Record<string, any>> implements EventBus<TEventMap> {
|
|
135
|
+
private bus;
|
|
136
|
+
constructor(options?: EventBusOptions);
|
|
137
|
+
/**
|
|
138
|
+
* Subscribes to a specific event by name.
|
|
139
|
+
* @param eventName - The name of the event to subscribe to.
|
|
140
|
+
* @param callback - The function to call when the event is emitted.
|
|
141
|
+
* @returns A function to unsubscribe from the event.
|
|
142
|
+
*/
|
|
143
|
+
subscribe<TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void): () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Subscribes to an event and automatically unsubscribes after it fires once.
|
|
146
|
+
* @param eventName - The name of the event to subscribe to.
|
|
147
|
+
* @param callback - The function to call when the event is emitted.
|
|
148
|
+
* @returns A function to cancel the one-shot subscription before it fires.
|
|
149
|
+
*/
|
|
150
|
+
once<TEventName extends keyof TEventMap>(eventName: TEventName, callback: (payload: TEventMap[TEventName]) => void): () => void;
|
|
151
|
+
/**
|
|
152
|
+
* Emits an event with a payload to all subscribed listeners.
|
|
153
|
+
* @param event - An object containing the event name and payload.
|
|
154
|
+
*/
|
|
155
|
+
emit<TEventName extends keyof TEventMap>(event: {
|
|
156
|
+
name: TEventName;
|
|
157
|
+
payload: TEventMap[TEventName];
|
|
158
|
+
}): void;
|
|
159
|
+
/**
|
|
160
|
+
* Retrieves metrics about event bus usage.
|
|
161
|
+
* @returns An object containing various metrics.
|
|
162
|
+
*/
|
|
163
|
+
metrics(): EventMetrics;
|
|
164
|
+
/**
|
|
165
|
+
* Clears all subscriptions and resets metrics.
|
|
166
|
+
* After calling clear(), the bus is fully reset and can be reused โ
|
|
167
|
+
* cross-tab communication is re-established if it was previously enabled.
|
|
168
|
+
*/
|
|
169
|
+
clear(): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { type DebouncerResult, type EventBus, type EventBusOptions, type EventError, type EventMetrics, Events, createEventBus };
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=class{_delay;_leading;_timer;_pendingFn;_pendingResolvers=[];_leadingFired=!1;constructor(e){this._delay=e?.delay??300,this._leading=e?.leading??!1}do(e){return new Promise((t=>{this._enqueue(e,t)}))}fire(e){this._enqueue(e,void 0)}_enqueue(e,t){if(this._pendingFn=e,t&&this._pendingResolvers.push(t),this._leading&&!this._leadingFired)return this._leadingFired=!0,this._fire(),clearTimeout(this._timer),void(this._timer=setTimeout((()=>{this._leadingFired=!1,void 0!==this._pendingFn&&this._fire()}),this._delay));clearTimeout(this._timer),this._timer=setTimeout((()=>{this._leadingFired=!1,this._fire()}),this._delay)}cancel(){clearTimeout(this._timer),this._timer=void 0,this._pendingFn=void 0,this._leadingFired=!1;const e=this._pendingResolvers.splice(0);for(const t of e)t({status:"cancelled"})}async flush(){return this._pendingFn?(clearTimeout(this._timer),this._timer=void 0,this._leadingFired=!1,this._fire()):null}pending(){return void 0!==this._pendingFn}async _fire(){const e=this._pendingFn,t=this._pendingResolvers.splice(0);let n;this._pendingFn=void 0;try{n={status:"ok",value:await e()}}catch(e){n={status:"error",error:e}}for(const e of t)e(n);return n}};function t(t){const n=t??{},{deferred:s,batchSize:r,batchDelay:i,errorHandler:a,broadcast:o,channel:c}={deferred:null!=n.batch?.size,batchSize:n.batch?.size,batchDelay:n.batch?.delay||16,errorHandler:n.errorHandler||(e=>console.error("EventBus Error:",e)),broadcast:null!=n.broadcast?.channel&&n.broadcast.channel.length>0,channel:null!=n.broadcast?.channel&&n.broadcast.channel.length>0?n.broadcast?.channel:"event-bus-channel"},l=new Map;let d=[],h=0,u=0;const _=new Map,g=()=>{if(!o)return null;if("undefined"==typeof BroadcastChannel)return console.warn("EventBus: BroadcastChannel is not supported in this environment. Cross-tab notifications are disabled."),null;const e=new BroadcastChannel(c);return e.onmessage=e=>{const{name:t,payload:n}=e.data;f(t,n)},e};let m=g();const p=(e,t)=>{h++,u+=t,_.set(e,(_.get(e)??0)+1)},f=(e,t)=>{const n=l.get(e);if(n&&0!==n.size)for(const s of Array.from(n))try{s(t)}catch(n){const s=n instanceof Error?n:Object.assign(new Error(String(n)),{cause:n}),r=Object.assign(s,{eventName:e,payload:t});a(r)}},b=()=>{const e=d;d=[];for(const{name:t,payload:n}of e){const e=performance.now();f(t,n),p(t,performance.now()-e)}},v=new e({delay:i});return{subscribe:(e,t)=>{l.has(e)||l.set(e,new Set);const n=l.get(e);return n.add(t),()=>{n.delete(t),0===n.size&&l.delete(e)}},once:(e,t)=>{let n;const s=e=>{n(),t(e)};return n=(()=>{l.has(e)||l.set(e,new Set);const t=l.get(e);return t.add(s),()=>{t.delete(s),0===t.size&&l.delete(e)}})(),n},emit:({name:e,payload:t})=>{if(s)return d.push({name:e,payload:t}),d.length>=r?(v.cancel(),void b()):(v.fire((()=>b())),void m?.postMessage({name:e,payload:t}));const n=performance.now();f(e,t),p(e,performance.now()-n),m?.postMessage({name:e,payload:t})},metrics:()=>({totalEvents:h,activeSubscriptions:Array.from(l.values()).reduce(((e,t)=>e+t.size),0),eventCounts:_,averageEmitDuration:h>0?u/h:0}),clear:()=>{l.clear(),d=[],v.cancel(),h=0,u=0,_.clear(),m?.close(),m=g()}}}exports.Events=class{bus;constructor(e){this.bus=t(e)}subscribe(e,t){return this.bus.subscribe(e,t)}once(e,t){return this.bus.once(e,t)}emit(e){return this.bus.emit(e)}metrics(){return this.bus.metrics()}clear(){return this.bus.clear()}},exports.createEventBus=t;
|
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class{_delay;_leading;_timer;_pendingFn;_pendingResolvers=[];_leadingFired=!1;constructor(e){this._delay=e?.delay??300,this._leading=e?.leading??!1}do(e){return new Promise((t=>{this._enqueue(e,t)}))}fire(e){this._enqueue(e,void 0)}_enqueue(e,t){if(this._pendingFn=e,t&&this._pendingResolvers.push(t),this._leading&&!this._leadingFired)return this._leadingFired=!0,this._fire(),clearTimeout(this._timer),void(this._timer=setTimeout((()=>{this._leadingFired=!1,void 0!==this._pendingFn&&this._fire()}),this._delay));clearTimeout(this._timer),this._timer=setTimeout((()=>{this._leadingFired=!1,this._fire()}),this._delay)}cancel(){clearTimeout(this._timer),this._timer=void 0,this._pendingFn=void 0,this._leadingFired=!1;const e=this._pendingResolvers.splice(0);for(const t of e)t({status:"cancelled"})}async flush(){return this._pendingFn?(clearTimeout(this._timer),this._timer=void 0,this._leadingFired=!1,this._fire()):null}pending(){return void 0!==this._pendingFn}async _fire(){const e=this._pendingFn,t=this._pendingResolvers.splice(0);let n;this._pendingFn=void 0;try{n={status:"ok",value:await e()}}catch(e){n={status:"error",error:e}}for(const e of t)e(n);return n}};function t(t){const n=t??{},{deferred:r,batchSize:s,batchDelay:i,errorHandler:a,broadcast:o,channel:c}={deferred:null!=n.batch?.size,batchSize:n.batch?.size,batchDelay:n.batch?.delay||16,errorHandler:n.errorHandler||(e=>console.error("EventBus Error:",e)),broadcast:null!=n.broadcast?.channel&&n.broadcast.channel.length>0,channel:null!=n.broadcast?.channel&&n.broadcast.channel.length>0?n.broadcast?.channel:"event-bus-channel"},l=new Map;let d=[],h=0,u=0;const _=new Map,g=()=>{if(!o)return null;if("undefined"==typeof BroadcastChannel)return console.warn("EventBus: BroadcastChannel is not supported in this environment. Cross-tab notifications are disabled."),null;const e=new BroadcastChannel(c);return e.onmessage=e=>{const{name:t,payload:n}=e.data;f(t,n)},e};let m=g();const p=(e,t)=>{h++,u+=t,_.set(e,(_.get(e)??0)+1)},f=(e,t)=>{const n=l.get(e);if(n&&0!==n.size)for(const r of Array.from(n))try{r(t)}catch(n){const r=n instanceof Error?n:Object.assign(new Error(String(n)),{cause:n}),s=Object.assign(r,{eventName:e,payload:t});a(s)}},b=()=>{const e=d;d=[];for(const{name:t,payload:n}of e){const e=performance.now();f(t,n),p(t,performance.now()-e)}},v=new e({delay:i});return{subscribe:(e,t)=>{l.has(e)||l.set(e,new Set);const n=l.get(e);return n.add(t),()=>{n.delete(t),0===n.size&&l.delete(e)}},once:(e,t)=>{let n;const r=e=>{n(),t(e)};return n=(()=>{l.has(e)||l.set(e,new Set);const t=l.get(e);return t.add(r),()=>{t.delete(r),0===t.size&&l.delete(e)}})(),n},emit:({name:e,payload:t})=>{if(r)return d.push({name:e,payload:t}),d.length>=s?(v.cancel(),void b()):(v.fire((()=>b())),void m?.postMessage({name:e,payload:t}));const n=performance.now();f(e,t),p(e,performance.now()-n),m?.postMessage({name:e,payload:t})},metrics:()=>({totalEvents:h,activeSubscriptions:Array.from(l.values()).reduce(((e,t)=>e+t.size),0),eventCounts:_,averageEmitDuration:h>0?u/h:0}),clear:()=>{l.clear(),d=[],v.cancel(),h=0,u=0,_.clear(),m?.close(),m=g()}}}var n=class{bus;constructor(e){this.bus=t(e)}subscribe(e,t){return this.bus.subscribe(e,t)}once(e,t){return this.bus.once(e,t)}emit(e){return this.bus.emit(e)}metrics(){return this.bus.metrics()}clear(){return this.bus.clear()}};export{n as Events,t as createEventBus};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asaidimu/utils-events",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight, type-safe event bus implementation for TypeScript applications.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"typescript",
|
|
10
|
+
"utility"
|
|
11
|
+
],
|
|
12
|
+
"author": "Saidimu <47994458+asaidimu@users.noreply.github.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/asaidimu/erp-utils.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/asaidimu/erp-utils/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/asaidimu/erp-utils/tree/main/src/events#readme",
|
|
22
|
+
"files": [
|
|
23
|
+
"./*"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"default": "./index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./index.d.ts",
|
|
33
|
+
"default": "./index.js"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"registry": "https://registry.npmjs.org/",
|
|
40
|
+
"tag": "latest",
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"release": {
|
|
44
|
+
"plugins": [
|
|
45
|
+
[
|
|
46
|
+
"@semantic-release/npm",
|
|
47
|
+
{
|
|
48
|
+
"pkgRoot": "./dist"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
"@semantic-release/git",
|
|
53
|
+
{
|
|
54
|
+
"assets": [
|
|
55
|
+
"CHANGELOG.md",
|
|
56
|
+
"package.json"
|
|
57
|
+
],
|
|
58
|
+
"message": "chore(release): Release @asaidimu/utils-events v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|