@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 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
+ [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-events.svg)](https://www.npmjs.com/package/@asaidimu/utils-events)
4
+ [![license](https://img.shields.io/npm/l/@asaidimu/utils-events.svg)](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
5
+ [![build status](https://img.shields.io/github/actions/workflow/status/asaidimu/erp-utils/ci.yml?branch=main)](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
+ }