@d1g1tal/subscribr 4.0.4 โ†’ 4.1.8

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/README.md CHANGED
@@ -1,75 +1,433 @@
1
1
  # subscribr
2
- JavaScript Publish/Subscribe
3
2
 
4
- This simple class enables you to subscribe and publish events using the Observer pattern.
3
+ [![CI](https://github.com/D1g1talEntr0py/subscribr/actions/workflows/ci.yml/badge.svg)](https://github.com/D1g1talEntr0py/subscribr/actions/workflows/ci.yml)
4
+ [![Codecov](https://codecov.io/gh/D1g1talEntr0py/subscribr/branch/main/graph/badge.svg)](https://codecov.io/gh/D1g1talEntr0py/subscribr)
5
+ [![npm version](https://img.shields.io/npm/v/@d1g1tal/subscribr.svg)](https://www.npmjs.com/package/@d1g1tal/subscribr)
6
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
5
8
 
6
- Basically, you subscribe your event handler using the `Subscribr` instance and are returned a `Subscription` instance.
9
+ A lightweight TypeScript publish/subscribe library implementing the Observer pattern.
7
10
 
8
- When the event is published, your event handler will be called.
11
+ ## Features
12
+
13
+ - ๐ŸŽฏ **Simple API** - Subscribe, publish, and unsubscribe with ease
14
+ - ๐Ÿ”’ **Type-safe** - Full TypeScript support with strict typing
15
+ - ๐ŸŽช **Context binding** - Maintain proper `this` context in handlers
16
+ - ๐Ÿ”„ **Once subscriptions** - Auto-unsubscribe after first event via options
17
+ - ๐Ÿ›ก๏ธ **Error handling** - Built-in error boundary prevents cascading failures
18
+ - โœ… **Validation** - Event name validation for reliable behavior
19
+ - ๐Ÿ“ฆ **Lightweight** - Minimal dependencies (@d1g1tal/collections)
20
+ - ๐Ÿงช **Well-tested** - Comprehensive test coverage
21
+
22
+ ## Installation
9
23
 
10
- ### Installation:
11
24
  ```bash
12
25
  # Install with pnpm
13
- pnpm install @d1g1tal/subscribr
26
+ pnpm add @d1g1tal/subscribr
14
27
 
15
- # Install with NPM
28
+ # Install with npm
16
29
  npm install @d1g1tal/subscribr
17
- ```
18
- Or Script tags from downloaded script or from a NPM CDN like jsDelivr
19
-
20
- ```html
21
- <!-- Load as global script -->
22
- <script src="/subscribr/dist/iife/subscribr.min.js"></script>
23
30
 
24
- <!-- Load from CDN -->
25
- <script src="https://cdn.jsdelivr.net/npm/@d1g1tal/subscribr@3/dist/iife/subscribr.min.js"></script>
31
+ # Install with yarn
32
+ yarn add @d1g1tal/subscribr
33
+ ```
26
34
 
27
- <!-- Load as ES Module -->
28
- <script type="module">
29
- import Subscribr from '/app/js/subscribr.min.js';
30
- </script>
35
+ ### CDN Usage
31
36
 
32
- <!-- Load from CDN -->
37
+ ```html
38
+ <!-- Load as ES Module from CDN -->
33
39
  <script type="module">
34
- import Subscribr from 'https://cdn.jsdelivr.net/npm/@d1g1tal/subscribr@3/dist/subscribr.min.js';
40
+ import Subscribr from 'https://cdn.jsdelivr.net/npm/@d1g1tal/subscribr@4/dist/subscribr.js';
35
41
  </script>
36
42
  ```
37
43
 
38
- ### Usage:
44
+ ## Quick Start
45
+
39
46
  ```javascript
40
- // ES Module
41
47
  import Subscribr from '@d1g1tal/subscribr';
42
48
 
49
+ // Create a new instance
50
+ const subscribr = new Subscribr();
51
+
52
+ // Subscribe to an event
53
+ const subscription = subscribr.subscribe('user-login', (event, data) => {
54
+ console.log('User logged in:', data.userId);
55
+ });
56
+
57
+ // Publish an event
58
+ subscribr.publish('user-login', undefined, { userId: 123 });
59
+ // Output: User logged in: 123
60
+
61
+ // Check if subscribed
62
+ console.log(subscribr.isSubscribed(subscription)); // true
63
+
64
+ // Unsubscribe
65
+ subscribr.unsubscribe(subscription);
66
+ console.log(subscribr.isSubscribed(subscription)); // false
67
+ ```
68
+
69
+ ## API Reference
70
+
71
+ ### Constructor
72
+
73
+ #### `new Subscribr()`
74
+
75
+ Creates a new Subscribr instance.
76
+
77
+ ```javascript
78
+ const subscribr = new Subscribr();
79
+ ```
80
+
81
+ ---
82
+
83
+ ### Methods
84
+
85
+ #### `subscribe(eventName, eventHandler, context?, options?)`
86
+
87
+ Subscribe to an event.
88
+
89
+ **Parameters:**
90
+ - `eventName` (string) - The name of the event to subscribe to
91
+ - `eventHandler` (function) - The handler function `(event: Event, data?: unknown) => void`
92
+ - `context` (any, optional) - The `this` context for the handler (defaults to the handler itself)
93
+ - `options` (object, optional) - Subscription options
94
+ - `once` (boolean) - If true, automatically unsubscribe after first event
95
+
96
+ **Returns:** `Subscription` - A subscription object for managing the subscription
97
+
98
+ **Throws:**
99
+ - `TypeError` - If event name is not a non-empty string
100
+ - `Error` - If event name has leading or trailing whitespace
101
+
102
+ ```javascript
103
+ // Basic subscription
104
+ const subscription = subscribr.subscribe('my-event', (event, data) => {
105
+ console.log('Event received:', event.type, data);
106
+ });
107
+
108
+ // With custom context
109
+ const myObject = {
110
+ name: 'MyObject',
111
+ handleEvent(event, data) {
112
+ console.log(this.name, 'received:', data);
113
+ }
114
+ };
115
+ subscribr.subscribe('my-event', myObject.handleEvent, myObject);
116
+
117
+ // One-time subscription using options
118
+ subscribr.subscribe('init', (event, data) => {
119
+ console.log('Initialization complete:', data);
120
+ }, undefined, { once: true });
121
+
122
+ subscribr.publish('init', undefined, { status: 'ready' });
123
+ // Output: Initialization complete: { status: 'ready' }
124
+
125
+ subscribr.publish('init', undefined, { status: 'ready' });
126
+ // No output - handler was auto-unsubscribed
127
+ ```
128
+
129
+ ---
130
+
131
+ #### `publish(eventName, event?, data?)`
132
+
133
+ Publish an event to all subscribed handlers.
134
+
135
+ **Parameters:**
136
+ - `eventName` (string) - The name of the event to publish
137
+ - `event` (Event, optional) - A custom event object (defaults to `new CustomEvent(eventName)`)
138
+ - `data` (any, optional) - Data to pass to event handlers
139
+
140
+ **Returns:** `void`
141
+
142
+ **Throws:**
143
+ - `TypeError` - If event name is not a non-empty string
144
+ - `Error` - If event name has leading or trailing whitespace
145
+
146
+ ```javascript
147
+ // Publish with just event name and data
148
+ subscribr.publish('user-action', undefined, { action: 'click', target: 'button' });
149
+
150
+ // Publish with custom event
151
+ const customEvent = new CustomEvent('custom-event', { detail: 'info' });
152
+ subscribr.publish('custom-event', customEvent, { extra: 'data' });
153
+ ```
154
+
155
+ ---
156
+
157
+ #### `unsubscribe(subscription)`
158
+
159
+ Unsubscribe from an event.
160
+
161
+ **Parameters:**
162
+ - `subscription` (Subscription) - The subscription object returned from `subscribe()` or `once()`
163
+
164
+ **Returns:** `boolean` - `true` if successfully unsubscribed, `false` if subscription wasn't found
165
+
166
+ ```javascript
167
+ const subscription = subscribr.subscribe('my-event', handler);
168
+ const result = subscribr.unsubscribe(subscription); // true
169
+ const result2 = subscribr.unsubscribe(subscription); // false (already unsubscribed)
170
+ ```
171
+
172
+ ---
173
+
174
+ #### `isSubscribed(subscription)`
175
+
176
+ Check if a subscription is still active.
177
+
178
+ **Parameters:**
179
+ - `subscription` (Subscription) - The subscription object to check
180
+
181
+ **Returns:** `boolean` - `true` if the subscription is active, `false` otherwise
182
+
183
+ ```javascript
184
+ const subscription = subscribr.subscribe('my-event', handler);
185
+ subscribr.isSubscribed(subscription); // true
186
+ subscribr.unsubscribe(subscription);
187
+ subscribr.isSubscribed(subscription); // false
188
+ ```
189
+
190
+ ---
191
+
192
+ #### `setErrorHandler(errorHandler)`
193
+
194
+ Set a custom error handler for handling errors that occur in event listeners. By default, errors are logged to the console.
195
+
196
+ **Parameters:**
197
+ - `errorHandler` (function) - The error handler function `(error: Error, eventName: string, event: Event, data?: unknown) => void`
198
+
199
+ **Returns:** `void`
200
+
201
+ ```javascript
202
+ // Set a custom error handler
203
+ subscribr.setErrorHandler((error, eventName, event, data) => {
204
+ console.error(`Error in handler for '${eventName}':`, error.message);
205
+ // Send to error tracking service
206
+ errorTracker.report(error, { eventName, data });
207
+ });
208
+
209
+ // Now if a handler throws an error, it won't break other handlers
210
+ subscribr.subscribe('my-event', () => {
211
+ throw new Error('Oops!');
212
+ });
213
+
214
+ subscribr.subscribe('my-event', () => {
215
+ console.log('This still runs!'); // โœ… Executes despite the error above
216
+ });
217
+
218
+ subscribr.publish('my-event');
219
+ // Output: Error in handler for 'my-event': Oops!
220
+ // Output: This still runs!
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Usage Examples
226
+
227
+ ### Multiple Handlers for the Same Event
228
+
229
+ ```javascript
230
+ const subscribr = new Subscribr();
231
+
232
+ subscribr.subscribe('data-update', (event, data) => {
233
+ console.log('Handler 1:', data);
234
+ });
235
+
236
+ subscribr.subscribe('data-update', (event, data) => {
237
+ console.log('Handler 2:', data);
238
+ });
239
+
240
+ subscribr.publish('data-update', undefined, { value: 42 });
241
+ // Output:
242
+ // Handler 1: { value: 42 }
243
+ // Handler 2: { value: 42 }
244
+ ```
245
+
246
+ ### Using with DOM Events
247
+
248
+ ```javascript
249
+ const subscribr = new Subscribr();
250
+
251
+ subscribr.subscribe('user-click', (event, data) => {
252
+ console.log('User clicked:', data.elementId);
253
+ });
254
+
255
+ document.getElementById('myButton').addEventListener('click', (event) => {
256
+ subscribr.publish('user-click', event, { elementId: event.target.id });
257
+ });
258
+ ```
259
+
260
+ ### Class-based Event Handling
261
+
262
+ ```javascript
263
+ class UserManager {
264
+ constructor(subscribr) {
265
+ this.subscribr = subscribr;
266
+ this.users = [];
267
+
268
+ // Subscribe with proper context binding
269
+ this.subscribr.subscribe('user-added', this.onUserAdded, this);
270
+ }
271
+
272
+ onUserAdded(event, data) {
273
+ // 'this' correctly refers to UserManager instance
274
+ this.users.push(data.user);
275
+ console.log(`Total users: ${this.users.length}`);
276
+ }
277
+
278
+ addUser(user) {
279
+ this.subscribr.publish('user-added', undefined, { user });
280
+ }
281
+ }
282
+
283
+ const subscribr = new Subscribr();
284
+ const manager = new UserManager(subscribr);
285
+ manager.addUser({ id: 1, name: 'Alice' });
286
+ // Output: Total users: 1
287
+ ```
288
+
289
+ ### Error-Resistant Event System
290
+
291
+ ```javascript
43
292
  const subscribr = new Subscribr();
44
- const eventName = 'myEvent';
45
293
 
46
- // Subscribr.prototype.subscribe returns a Subscription object.
47
- const mySubscription = subscribr.subscribe(eventName, (event, data) => console.log(`Event: '${event.type}' published with data: ${data}`));
294
+ // Set up error tracking
295
+ subscribr.setErrorHandler((error, eventName, event, data) => {
296
+ console.error(`Error in handler for '${eventName}':`, error.message);
297
+ // Send to error tracking service (if available)
298
+ });
299
+
300
+ // Even if one handler fails, others continue
301
+ subscribr.subscribe('process-data', (event, data) => {
302
+ if (!data.valid) throw new Error('Invalid data');
303
+ processData(data);
304
+ });
48
305
 
49
- // Publish the event with just the name and a CustomEvent will be created with the eventName. All handlers that have subscribed are called
50
- subscribr.publish(eventName, { foo: 'bar', zip: 'fizz' }); // Event: 'myEvent' published with data: { foo: 'bar', zip: 'fizz' }
306
+ subscribr.subscribe('process-data', (event, data) => {
307
+ // This still executes even if the previous handler throws
308
+ updateUI(data);
309
+ });
310
+ ```
51
311
 
52
- // Is the event subscribed?
53
- const isSubscribed = subscribr.isSubscribed(mySubscription); // true
312
+ ### One-Time Subscriptions
54
313
 
55
- const isUnsubscribed = subscribr.unsubscribe(mySubscription); // true
314
+ ```javascript
315
+ const subscribr = new Subscribr();
56
316
 
57
- const isSubscribed = subscribr.isSubscribed(mySubscription); // false
317
+ // One-time subscription using options
318
+ subscribr.subscribe('app-ready', (event, data) => {
319
+ console.log('App initialized with config:', data.config);
320
+ startApplication(data.config);
321
+ }, undefined, { once: true });
58
322
 
323
+ // This will only trigger the handler once
324
+ subscribr.publish('app-ready', undefined, { config: { theme: 'dark' } });
325
+ subscribr.publish('app-ready', undefined, { config: { theme: 'light' } }); // Ignored
326
+ ```
59
327
 
60
- // Subscribe to a DOM event
61
- const eventTargetSubscription = subsribr.subscribe('eventTargetChanged', (event, data) => console.log(`Event target changed with data: ${data}`));
328
+ ## TypeScript Support
62
329
 
63
- // Add some event to an event target (DOMElement)
64
- new EventTarget().addEventListener('change', function(event) {
65
- // Publish the event and all handlers that have subscribed are called
66
- subscribr.publish('eventTargetChanged', event, { value: this.value }); // Event target changed with data: {value: 'new value'}
330
+ Full TypeScript support with strict typing:
331
+
332
+ ```typescript
333
+ import Subscribr from '@d1g1tal/subscribr';
334
+
335
+ interface UserData {
336
+ userId: number;
337
+ userName: string;
338
+ }
339
+
340
+ const subscribr = new Subscribr();
341
+
342
+ subscribr.subscribe('user-login', (event: Event, data?: UserData) => {
343
+ if (data) {
344
+ console.log(`User ${data.userName} (ID: ${data.userId}) logged in`);
345
+ }
346
+ });
347
+
348
+ subscribr.publish<UserData>('user-login', undefined, {
349
+ userId: 123,
350
+ userName: 'Alice'
67
351
  });
352
+ ```
353
+
354
+ ## Event Name Validation
355
+
356
+ Event names are validated to prevent common mistakes:
357
+
358
+ ```javascript
359
+ // โŒ These will throw errors:
360
+ subscribr.subscribe('', handler); // TypeError: Event name must be a non-empty string
361
+ subscribr.subscribe(' event', handler); // Error: Event name cannot have leading or trailing whitespace
362
+ subscribr.subscribe('event ', handler); // Error: Event name cannot have leading or trailing whitespace
363
+ subscribr.subscribe(null, handler); // TypeError: Event name must be a non-empty string
364
+
365
+ // โœ… These are valid:
366
+ subscribr.subscribe('my-event', handler);
367
+ subscribr.subscribe('user:login', handler);
368
+ subscribr.subscribe('data_update', handler);
369
+ ```
370
+
371
+ ## Best Practices
372
+
373
+ 1. **Always unsubscribe** when done to prevent memory leaks:
374
+ ```javascript
375
+ const subscription = subscribr.subscribe('event', handler);
376
+ // ... later
377
+ subscribr.unsubscribe(subscription);
378
+ ```
379
+
380
+ 2. **Use `{ once: true }` option for one-time events** to avoid manual cleanup:
381
+ ```javascript
382
+ subscribr.subscribe('init', handler, undefined, { once: true }); // Auto-unsubscribes
383
+ ```
384
+
385
+ 3. **Set up error handling** for production applications:
386
+ ```javascript
387
+ subscribr.setErrorHandler((error, eventName) => {
388
+ console.error(`Event '${eventName}' error:`, error);
389
+ // Report to error tracking service if available
390
+ });
391
+ ```
392
+
393
+ 4. **Use meaningful event names** with consistent naming conventions:
394
+ ```javascript
395
+ // Good
396
+ subscribr.subscribe('user-login', handler);
397
+ subscribr.subscribe('data-updated', handler);
398
+
399
+ // Avoid
400
+ subscribr.subscribe('event1', handler);
401
+ subscribr.subscribe('stuff', handler);
402
+ ```
403
+
404
+ 5. **Bind context properly** for class methods:
405
+ ```javascript
406
+ class MyClass {
407
+ constructor() {
408
+ subscribr.subscribe('event', this.handleEvent, this);
409
+ }
410
+ handleEvent(event, data) {
411
+ // 'this' refers to MyClass instance
412
+ }
413
+ }
414
+ ```
415
+
416
+ ## Browser Support
417
+
418
+ Supports all modern browsers with ES6 module support and Node.js โ‰ฅ 20.15.1
419
+
420
+ ## License
421
+
422
+ ISC
423
+
424
+ ## Contributing
68
425
 
69
- // Is the event subscribed?
70
- const isSubscribed = subscribr.isSubscribed(eventTargetSubscription); // true
426
+ Contributions are welcome! Please feel free to submit a Pull Request.
71
427
 
72
- const isUnsubscribed = subscribr.unsubscribe(eventTargetSubscription); // true
428
+ ## Links
73
429
 
74
- const isSubscribed = subscribr.isSubscribed(eventTargetSubscription); // false
75
- ```
430
+ - [GitHub Repository](https://github.com/D1g1talEntr0py/subscribr)
431
+ - [npm Package](https://www.npmjs.com/package/@d1g1tal/subscribr)
432
+ - [Issue Tracker](https://github.com/D1g1talEntr0py/subscribr/issues)
433
+ - [Changelog](./CHANGELOG.md)
@@ -1,15 +1,22 @@
1
1
  /** Context event listener function. */
2
- type ContextEventListener = (event: Event, data?: unknown) => void;
2
+ type EventHandler = (event: Event, data?: unknown) => void;
3
+ /** Error handler function for handling errors in event listeners. */
4
+ type ErrorHandler = (error: Error, eventName: string, event: Event, data?: unknown) => void;
5
+ /** Subscription options. */
6
+ interface SubscriptionOptions {
7
+ /** If true, the subscription will automatically unsubscribe after the first event. */
8
+ once?: boolean;
9
+ }
3
10
 
4
11
  /** A wrapper for an event handler that binds a context to the event handler. */
5
12
  declare class ContextEventHandler {
6
13
  private readonly context;
7
- private readonly eventListener;
14
+ private readonly eventHandler;
8
15
  /**
9
16
  * @param context The context to bind to the event handler.
10
- * @param eventListener The event handler to call when the event is published.
17
+ * @param eventHandler The event handler to call when the event is published.
11
18
  */
12
- constructor(context: unknown, eventListener: ContextEventListener);
19
+ constructor(context: unknown, eventHandler: EventHandler);
13
20
  /**
14
21
  * Call the event handler for the provided event.
15
22
  *
@@ -59,16 +66,24 @@ declare class Subscription {
59
66
  /** A class that allows objects to subscribe to events and be notified when the event is published. */
60
67
  declare class Subscribr {
61
68
  private readonly subscribers;
62
- constructor();
69
+ private errorHandler?;
70
+ /**
71
+ * Set a custom error handler for handling errors that occur in event listeners.
72
+ * If not set, errors will be logged to the console.
73
+ *
74
+ * @param errorHandler The error handler function to call when an error occurs in an event listener.
75
+ */
76
+ setErrorHandler(errorHandler: ErrorHandler): void;
63
77
  /**
64
78
  * Subscribe to an event
65
79
  *
66
80
  * @param eventName The event name to subscribe to.
67
81
  * @param eventHandler The event handler to call when the event is published.
68
82
  * @param context The context to bind to the event handler.
83
+ * @param options Subscription options.
69
84
  * @returns An object used to check if the subscription still exists and to unsubscribe from the event.
70
85
  */
71
- subscribe(eventName: string, eventHandler: ContextEventListener, context?: unknown): Subscription;
86
+ subscribe(eventName: string, eventHandler: EventHandler, context?: unknown, options?: SubscriptionOptions): Subscription;
72
87
  /**
73
88
  * Unsubscribe from the event
74
89
  *
@@ -92,6 +107,18 @@ declare class Subscribr {
92
107
  * @returns true if the event name and handler are subscribed, false otherwise.
93
108
  */
94
109
  isSubscribed({ eventName, contextEventHandler }: Subscription): boolean;
110
+ /**
111
+ * Validate the event name
112
+ *
113
+ * @param eventName The event name to validate.
114
+ * @throws {TypeError} If the event name is not a non-empty string.
115
+ * @throws {Error} If the event name has leading or trailing whitespace.
116
+ */
117
+ private validateEventName;
118
+ /**
119
+ * Clears all subscriptions. The instance should not be used after calling this method.
120
+ */
121
+ destroy(): void;
95
122
  /**
96
123
  * A String value that is used in the creation of the default string
97
124
  * description of an object. Called by the built-in method {@link Object.prototype.toString}.
@@ -101,4 +128,5 @@ declare class Subscribr {
101
128
  get [Symbol.toStringTag](): string;
102
129
  }
103
130
 
104
- export { ContextEventHandler, type ContextEventListener, Subscribr, Subscription };
131
+ export { ContextEventHandler, Subscribr, Subscription };
132
+ export type { ErrorHandler, EventHandler, SubscriptionOptions };
package/dist/subscribr.js CHANGED
@@ -1,2 +1,257 @@
1
- var s=class extends Map{set(e,t){return super.set(e,(super.get(e)??new Set).add(t)),this}hasValue(e,t){let n=super.get(e);return n?n.has(t):!1}find(e,t){let n=this.get(e);if(n!==void 0)return Array.from(n).find(t)}deleteValue(e,t){if(t===void 0)return this.delete(e);let n=super.get(e);if(n){let i=n.delete(t);return n.size===0&&super.delete(e),i}return!1}get[Symbol.toStringTag](){return"SetMultiMap"}};var r=class{context;eventListener;constructor(e,t){this.context=e,this.eventListener=t}handle(e,t){this.eventListener.call(this.context,e,t)}get[Symbol.toStringTag](){return"ContextEventHandler"}};var l=class{_eventName;_contextEventHandler;constructor(e,t){this._eventName=e,this._contextEventHandler=t}get eventName(){return this._eventName}get contextEventHandler(){return this._contextEventHandler}get[Symbol.toStringTag](){return"Subscription"}};var u=class{subscribers;constructor(){this.subscribers=new s}subscribe(e,t,n=t){let i=new r(n,t);return this.subscribers.set(e,i),new l(e,i)}unsubscribe({eventName:e,contextEventHandler:t}){let n=this.subscribers.get(e)??new Set,i=n.delete(t);return i&&n.size===0&&this.subscribers.delete(e),i}publish(e,t=new CustomEvent(e),n){this.subscribers.get(e)?.forEach(i=>i.handle(t,n))}isSubscribed({eventName:e,contextEventHandler:t}){return this.subscribers.get(e)?.has(t)??!1}get[Symbol.toStringTag](){return"Subscribr"}};export{r as ContextEventHandler,u as Subscribr,l as Subscription};
1
+ // node_modules/.pnpm/@d1g1tal+collections@2.1.1_typescript@5.9.3/node_modules/@d1g1tal/collections/src/set-multi-map.ts
2
+ var SetMultiMap = class extends Map {
3
+ /**
4
+ * Adds a new element with a specified key and value to the SetMultiMap.
5
+ * If an element with the same key already exists, the value will be added to the underlying {@link Set}.
6
+ * If the value already exists in the {@link Set}, it will not be added again.
7
+ *
8
+ * @param key The key to set.
9
+ * @param value The value to add to the SetMultiMap
10
+ * @returns The SetMultiMap with the updated key and value.
11
+ */
12
+ set(key, value) {
13
+ super.set(key, value instanceof Set ? value : (super.get(key) ?? /* @__PURE__ */ new Set()).add(value));
14
+ return this;
15
+ }
16
+ /**
17
+ * Finds a specific value for a specific key using an iterator function.
18
+ * @param key The key to find the value for.
19
+ * @param iterator The iterator function to use to find the value.
20
+ * @returns The value for the specified key
21
+ */
22
+ find(key, iterator) {
23
+ const values = this.get(key);
24
+ if (values !== void 0) {
25
+ return Array.from(values).find(iterator);
26
+ }
27
+ return void 0;
28
+ }
29
+ /**
30
+ * Checks if a specific key has a specific value.
31
+ *
32
+ * @param key The key to check.
33
+ * @param value The value to check.
34
+ * @returns True if the key has the value, false otherwise.
35
+ */
36
+ hasValue(key, value) {
37
+ const values = super.get(key);
38
+ return values ? values.has(value) : false;
39
+ }
40
+ /**
41
+ * Removes a specific value from a specific key.
42
+ * @param key The key to remove the value from.
43
+ * @param value The value to remove.
44
+ * @returns True if the value was removed, false otherwise.
45
+ */
46
+ deleteValue(key, value) {
47
+ if (value === void 0) {
48
+ return this.delete(key);
49
+ }
50
+ const values = super.get(key);
51
+ if (values) {
52
+ const deleted = values.delete(value);
53
+ if (values.size === 0) {
54
+ super.delete(key);
55
+ }
56
+ return deleted;
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * The string tag of the SetMultiMap.
62
+ * @returns The string tag of the SetMultiMap.
63
+ */
64
+ get [Symbol.toStringTag]() {
65
+ return "SetMultiMap";
66
+ }
67
+ };
68
+
69
+ // src/context-event-handler.ts
70
+ var ContextEventHandler = class {
71
+ context;
72
+ eventHandler;
73
+ /**
74
+ * @param context The context to bind to the event handler.
75
+ * @param eventHandler The event handler to call when the event is published.
76
+ */
77
+ constructor(context, eventHandler) {
78
+ this.context = context;
79
+ this.eventHandler = eventHandler;
80
+ }
81
+ /**
82
+ * Call the event handler for the provided event.
83
+ *
84
+ * @param event The event to handle
85
+ * @param data The value to be passed to the event handler as a parameter.
86
+ */
87
+ handle(event, data) {
88
+ this.eventHandler.call(this.context, event, data);
89
+ }
90
+ /**
91
+ * A String value that is used in the creation of the default string
92
+ * description of an object. Called by the built-in method {@link Object.prototype.toString}.
93
+ *
94
+ * @returns The default string description of this object.
95
+ */
96
+ get [Symbol.toStringTag]() {
97
+ return "ContextEventHandler";
98
+ }
99
+ };
100
+
101
+ // src/subscription.ts
102
+ var Subscription = class {
103
+ _eventName;
104
+ _contextEventHandler;
105
+ /**
106
+ * @param eventName The event name.
107
+ * @param contextEventHandler The context event handler.
108
+ */
109
+ constructor(eventName, contextEventHandler) {
110
+ this._eventName = eventName;
111
+ this._contextEventHandler = contextEventHandler;
112
+ }
113
+ /**
114
+ * Gets the event name for the subscription.
115
+ *
116
+ * @returns The event name.
117
+ */
118
+ get eventName() {
119
+ return this._eventName;
120
+ }
121
+ /**
122
+ * Gets the context event handler.
123
+ *
124
+ * @returns The context event handler
125
+ */
126
+ get contextEventHandler() {
127
+ return this._contextEventHandler;
128
+ }
129
+ /**
130
+ * A String value that is used in the creation of the default string
131
+ * description of an object. Called by the built-in method {@link Object.prototype.toString}.
132
+ *
133
+ * @returns The default string description of this object.
134
+ */
135
+ get [Symbol.toStringTag]() {
136
+ return "Subscription";
137
+ }
138
+ };
139
+
140
+ // src/subscribr.ts
141
+ var Subscribr = class {
142
+ subscribers = new SetMultiMap();
143
+ errorHandler;
144
+ /**
145
+ * Set a custom error handler for handling errors that occur in event listeners.
146
+ * If not set, errors will be logged to the console.
147
+ *
148
+ * @param errorHandler The error handler function to call when an error occurs in an event listener.
149
+ */
150
+ setErrorHandler(errorHandler) {
151
+ this.errorHandler = errorHandler;
152
+ }
153
+ /**
154
+ * Subscribe to an event
155
+ *
156
+ * @param eventName The event name to subscribe to.
157
+ * @param eventHandler The event handler to call when the event is published.
158
+ * @param context The context to bind to the event handler.
159
+ * @param options Subscription options.
160
+ * @returns An object used to check if the subscription still exists and to unsubscribe from the event.
161
+ */
162
+ subscribe(eventName, eventHandler, context = eventHandler, options) {
163
+ this.validateEventName(eventName);
164
+ if (options?.once) {
165
+ const originalHandler = eventHandler;
166
+ eventHandler = (event, data) => {
167
+ originalHandler.call(context, event, data);
168
+ this.unsubscribe(subscription);
169
+ };
170
+ }
171
+ const contextEventHandler = new ContextEventHandler(context, eventHandler);
172
+ this.subscribers.set(eventName, contextEventHandler);
173
+ const subscription = new Subscription(eventName, contextEventHandler);
174
+ return subscription;
175
+ }
176
+ /**
177
+ * Unsubscribe from the event
178
+ *
179
+ * @param subscription The subscription to unsubscribe.
180
+ * @returns true if eventListener has been removed successfully. false if the value is not found or if the value is not an object.
181
+ */
182
+ unsubscribe({ eventName, contextEventHandler }) {
183
+ const contextEventHandlers = this.subscribers.get(eventName) ?? /* @__PURE__ */ new Set();
184
+ const removed = contextEventHandlers.delete(contextEventHandler);
185
+ if (removed && contextEventHandlers.size === 0) {
186
+ this.subscribers.delete(eventName);
187
+ }
188
+ return removed;
189
+ }
190
+ /**
191
+ * Publish an event
192
+ *
193
+ * @template T
194
+ * @param eventName The name of the event.
195
+ * @param event The event to be handled.
196
+ * @param data The value to be passed to the event handler as a parameter.
197
+ */
198
+ publish(eventName, event = new CustomEvent(eventName), data) {
199
+ this.validateEventName(eventName);
200
+ this.subscribers.get(eventName)?.forEach((contextEventHandler) => {
201
+ try {
202
+ contextEventHandler.handle(event, data);
203
+ } catch (error) {
204
+ if (this.errorHandler) {
205
+ this.errorHandler(error, eventName, event, data);
206
+ } else {
207
+ console.error(`Error in event handler for '${eventName}':`, error);
208
+ }
209
+ }
210
+ });
211
+ }
212
+ /**
213
+ * Check if the event and handler are subscribed.
214
+ *
215
+ * @param subscription The subscription object.
216
+ * @returns true if the event name and handler are subscribed, false otherwise.
217
+ */
218
+ isSubscribed({ eventName, contextEventHandler }) {
219
+ return this.subscribers.get(eventName)?.has(contextEventHandler) ?? false;
220
+ }
221
+ /**
222
+ * Validate the event name
223
+ *
224
+ * @param eventName The event name to validate.
225
+ * @throws {TypeError} If the event name is not a non-empty string.
226
+ * @throws {Error} If the event name has leading or trailing whitespace.
227
+ */
228
+ validateEventName(eventName) {
229
+ if (!eventName || typeof eventName !== "string") {
230
+ throw new TypeError("Event name must be a non-empty string");
231
+ }
232
+ if (eventName.trim() !== eventName) {
233
+ throw new Error("Event name cannot have leading or trailing whitespace");
234
+ }
235
+ }
236
+ /**
237
+ * Clears all subscriptions. The instance should not be used after calling this method.
238
+ */
239
+ destroy() {
240
+ this.subscribers.clear();
241
+ }
242
+ /**
243
+ * A String value that is used in the creation of the default string
244
+ * description of an object. Called by the built-in method {@link Object.prototype.toString}.
245
+ *
246
+ * @returns The default string description of this object.
247
+ */
248
+ get [Symbol.toStringTag]() {
249
+ return "Subscribr";
250
+ }
251
+ };
252
+ export {
253
+ ContextEventHandler,
254
+ Subscribr,
255
+ Subscription
256
+ };
2
257
  //# sourceMappingURL=subscribr.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../node_modules/.pnpm/@d1g1tal+collections@2.1.0_typescript@5.9.3/node_modules/@d1g1tal/collections/src/set-multi-map.ts", "../src/context-event-handler.ts", "../src/subscription.ts", "../src/subscribr.ts"],
4
- "sourcesContent": ["/** A {@link Map} that can contain multiple, unique, values for the same key. */\nexport class SetMultiMap<K, V> extends Map<K, Set<V>>{\n\t/**\n\t * Adds a new element with a specified key and value to the SetMultiMap.\n\t * If an element with the same key already exists, the value will be added to the underlying {@link Set}.\n\t * If the value already exists in the {@link Set}, it will not be added again.\n\t *\n\t * @param key The key to set.\n\t * @param value The value to add to the SetMultiMap\n\t * @returns The SetMultiMap with the updated key and value.\n\t */\n\t// @ts-expect-error I am overriding the set method from the Map class\n\toverride set(key: K, value: V): SetMultiMap<K, V> {\n\t\tsuper.set(key, (super.get(key) ?? new Set()).add(value));\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Checks if a specific key has a specific value.\n\t *\n\t * @param key The key to check.\n\t * @param value The value to check.\n\t * @returns True if the key has the value, false otherwise.\n\t */\n\thasValue(key: K, value: V): boolean {\n\t\tconst values = super.get(key);\n\n\t\treturn values ? values.has(value) : false;\n\t}\n\n\t/**\n\t * Finds a specific value for a specific key using an iterator function.\n\t * @param key The key to find the value for.\n\t * @param iterator The iterator function to use to find the value.\n\t * @returns The value for the specified key\n\t */\n\tfind(key: K, iterator: (value: V) => boolean): V | undefined {\n\t\tconst values = this.get(key);\n\n\t\tif (values !== undefined) {\n\t\t\treturn Array.from(values).find(iterator);\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Removes a specific value from a specific key.\n\t * @param key The key to remove the value from.\n\t * @param value The value to remove.\n\t * @returns True if the value was removed, false otherwise.\n\t */\n\tdeleteValue(key: K, value: V | undefined): boolean {\n\t\tif (value === undefined) { return this.delete(key) }\n\n\t\tconst values = super.get(key);\n\t\tif (values) {\n\t\t\tconst deleted = values.delete(value);\n\n\t\t\tif (values.size === 0) {\n\t\t\t\tsuper.delete(key);\n\t\t\t}\n\n\t\t\treturn deleted;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * The string tag of the SetMultiMap.\n\t * @returns The string tag of the SetMultiMap.\n\t */\n\toverride get [Symbol.toStringTag](): string {\n\t\treturn 'SetMultiMap';\n\t}\n}", "import type { ContextEventListener } from './@types';\n\n/** A wrapper for an event handler that binds a context to the event handler. */\nexport class ContextEventHandler {\n\tprivate readonly context: unknown;\n\tprivate readonly eventListener: ContextEventListener;\n\n\t/**\n\t * @param context The context to bind to the event handler.\n\t * @param eventListener The event handler to call when the event is published.\n\t */\n\tconstructor(context: unknown, eventListener: ContextEventListener) {\n\t\tthis.context = context;\n\t\tthis.eventListener = eventListener;\n\t}\n\n\t/**\n\t * Call the event handler for the provided event.\n\t *\n\t * @param event The event to handle\n\t * @param data The value to be passed to the event handler as a parameter.\n\t */\n\thandle(event: Event, data?: unknown): void {\n\t\tthis.eventListener.call(this.context, event, data);\n\t}\n\n\t/**\n\t * A String value that is used in the creation of the default string\n\t * description of an object. Called by the built-in method {@link Object.prototype.toString}.\n\t *\n\t * @returns The default string description of this object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'ContextEventHandler';\n\t}\n}", "import type { ContextEventHandler } from './context-event-handler';\n\n/** Represents a subscription to an event. */\nexport class Subscription {\n\tprivate readonly _eventName: string;\n\tprivate readonly _contextEventHandler: ContextEventHandler;\n\n\t/**\n\t * @param eventName The event name.\n\t * @param contextEventHandler The context event handler.\n\t */\n\tconstructor(eventName: string, contextEventHandler: ContextEventHandler) {\n\t\tthis._eventName = eventName;\n\t\tthis._contextEventHandler = contextEventHandler;\n\t}\n\n\t/**\n\t * Gets the event name for the subscription.\n\t *\n\t * @returns The event name.\n\t */\n\tget eventName(): string {\n\t\treturn this._eventName;\n\t}\n\n\t/**\n\t * Gets the context event handler.\n\t *\n\t * @returns The context event handler\n\t */\n\tget contextEventHandler(): ContextEventHandler {\n\t\treturn this._contextEventHandler;\n\t}\n\n\t/**\n\t * A String value that is used in the creation of the default string\n\t * description of an object. Called by the built-in method {@link Object.prototype.toString}.\n\t *\n\t * @returns The default string description of this object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'Subscription';\n\t}\n}", "import { SetMultiMap } from '@d1g1tal/collections/src';\nimport { ContextEventHandler } from './context-event-handler';\nimport { Subscription } from './subscription';\nimport type { ContextEventListener } from './@types';\n\n/** A class that allows objects to subscribe to events and be notified when the event is published. */\nexport class Subscribr {\n\tprivate readonly subscribers: SetMultiMap<string, ContextEventHandler>;\n\n\tconstructor() {\n\t\tthis.subscribers = new SetMultiMap();\n\t}\n\n\t/**\n\t * Subscribe to an event\n\t *\n\t * @param eventName The event name to subscribe to.\n\t * @param eventHandler The event handler to call when the event is published.\n\t * @param context The context to bind to the event handler.\n\t * @returns An object used to check if the subscription still exists and to unsubscribe from the event.\n\t */\n\tsubscribe(eventName: string, eventHandler: ContextEventListener, context: unknown = eventHandler): Subscription {\n\t\tconst contextEventHandler = new ContextEventHandler(context, eventHandler);\n\t\tthis.subscribers.set(eventName, contextEventHandler);\n\n\t\treturn new Subscription(eventName, contextEventHandler);\n\t}\n\n\t/**\n\t * Unsubscribe from the event\n\t *\n\t * @param subscription The subscription to unsubscribe.\n\t * @returns true if eventListener has been removed successfully. false if the value is not found or if the value is not an object.\n\t */\n\tunsubscribe({ eventName, contextEventHandler }: Subscription): boolean {\n\t\tconst contextEventHandlers = this.subscribers.get(eventName) ?? new Set();\n\t\tconst removed = contextEventHandlers.delete(contextEventHandler);\n\n\t\tif (removed && contextEventHandlers.size === 0) {\tthis.subscribers.delete(eventName) }\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Publish an event\n\t *\n\t * @template T\n\t * @param eventName The name of the event.\n\t * @param event The event to be handled.\n\t * @param data The value to be passed to the event handler as a parameter.\n\t */\n\tpublish<T>(eventName: string, event: Event = new CustomEvent(eventName), data?: T): void {\n\t\tthis.subscribers.get(eventName)?.forEach((contextEventHandler: ContextEventHandler) => contextEventHandler.handle(event, data));\n\t}\n\n\t/**\n\t * Check if the event and handler are subscribed.\n\t *\n\t * @param subscription The subscription object.\n\t * @returns true if the event name and handler are subscribed, false otherwise.\n\t */\n\tisSubscribed({ eventName, contextEventHandler }: Subscription): boolean {\n\t\treturn this.subscribers.get(eventName)?.has(contextEventHandler) ?? false;\n\t}\n\n\t/**\n\t * A String value that is used in the creation of the default string\n\t * description of an object. Called by the built-in method {@link Object.prototype.toString}.\n\t *\n\t * @returns The default string description of this object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'Subscribr';\n\t}\n}"],
5
- "mappings": "AACO,IAAMA,EAAN,cAAgC,GAAc,CAW3C,IAAIC,EAAQC,EAA6B,CACjD,aAAM,IAAID,GAAM,MAAM,IAAIA,CAAG,GAAK,IAAI,KAAO,IAAIC,CAAK,CAAC,EAEhD,IACR,CASA,SAASD,EAAQC,EAAmB,CACnC,IAAMC,EAAS,MAAM,IAAIF,CAAG,EAE5B,OAAOE,EAASA,EAAO,IAAID,CAAK,EAAI,EACrC,CAQA,KAAKD,EAAQG,EAAgD,CAC5D,IAAMD,EAAS,KAAK,IAAIF,CAAG,EAE3B,GAAIE,IAAW,OACd,OAAO,MAAM,KAAKA,CAAM,EAAE,KAAKC,CAAQ,CAIzC,CAQA,YAAYH,EAAQC,EAA+B,CAClD,GAAIA,IAAU,OAAa,OAAO,KAAK,OAAOD,CAAG,EAEjD,IAAME,EAAS,MAAM,IAAIF,CAAG,EAC5B,GAAIE,EAAQ,CACX,IAAME,EAAUF,EAAO,OAAOD,CAAK,EAEnC,OAAIC,EAAO,OAAS,GACnB,MAAM,OAAOF,CAAG,EAGVI,CACR,CAEA,MAAO,EACR,CAMA,IAAc,OAAO,WAAW,GAAY,CAC3C,MAAO,aACR,CACD,EC1EO,IAAMC,EAAN,KAA0B,CACf,QACA,cAMjB,YAAYC,EAAkBC,EAAqC,CAClE,KAAK,QAAUD,EACf,KAAK,cAAgBC,CACtB,CAQA,OAAOC,EAAcC,EAAsB,CAC1C,KAAK,cAAc,KAAK,KAAK,QAASD,EAAOC,CAAI,CAClD,CAQA,IAAK,OAAO,WAAW,GAAY,CAClC,MAAO,qBACR,CACD,EChCO,IAAMC,EAAN,KAAmB,CACR,WACA,qBAMjB,YAAYC,EAAmBC,EAA0C,CACxE,KAAK,WAAaD,EAClB,KAAK,qBAAuBC,CAC7B,CAOA,IAAI,WAAoB,CACvB,OAAO,KAAK,UACb,CAOA,IAAI,qBAA2C,CAC9C,OAAO,KAAK,oBACb,CAQA,IAAK,OAAO,WAAW,GAAY,CAClC,MAAO,cACR,CACD,ECrCO,IAAMC,EAAN,KAAgB,CACL,YAEjB,aAAc,CACb,KAAK,YAAc,IAAIC,CACxB,CAUA,UAAUC,EAAmBC,EAAoCC,EAAmBD,EAA4B,CAC/G,IAAME,EAAsB,IAAIC,EAAoBF,EAASD,CAAY,EACzE,YAAK,YAAY,IAAID,EAAWG,CAAmB,EAE5C,IAAIE,EAAaL,EAAWG,CAAmB,CACvD,CAQA,YAAY,CAAE,UAAAH,EAAW,oBAAAG,CAAoB,EAA0B,CACtE,IAAMG,EAAuB,KAAK,YAAY,IAAIN,CAAS,GAAK,IAAI,IAC9DO,EAAUD,EAAqB,OAAOH,CAAmB,EAE/D,OAAII,GAAWD,EAAqB,OAAS,GAAK,KAAK,YAAY,OAAON,CAAS,EAE5EO,CACR,CAUA,QAAWP,EAAmBQ,EAAe,IAAI,YAAYR,CAAS,EAAGS,EAAgB,CACxF,KAAK,YAAY,IAAIT,CAAS,GAAG,QAASG,GAA6CA,EAAoB,OAAOK,EAAOC,CAAI,CAAC,CAC/H,CAQA,aAAa,CAAE,UAAAT,EAAW,oBAAAG,CAAoB,EAA0B,CACvE,OAAO,KAAK,YAAY,IAAIH,CAAS,GAAG,IAAIG,CAAmB,GAAK,EACrE,CAQA,IAAK,OAAO,WAAW,GAAY,CAClC,MAAO,WACR,CACD",
6
- "names": ["SetMultiMap", "key", "value", "values", "iterator", "deleted", "ContextEventHandler", "context", "eventListener", "event", "data", "Subscription", "eventName", "contextEventHandler", "Subscribr", "SetMultiMap", "eventName", "eventHandler", "context", "contextEventHandler", "ContextEventHandler", "Subscription", "contextEventHandlers", "removed", "event", "data"]
3
+ "sources": ["../node_modules/.pnpm/@d1g1tal+collections@2.1.1_typescript@5.9.3/node_modules/@d1g1tal/collections/src/set-multi-map.ts", "../src/context-event-handler.ts", "../src/subscription.ts", "../src/subscribr.ts"],
4
+ "sourcesContent": ["/** A {@link Map} that can contain multiple, unique, values for the same key. */\nexport class SetMultiMap<K, V> extends Map<K, Set<V>>{\n\t/**\n\t * Adds a new element with a specified key and value to the SetMultiMap.\n\t * If an element with the same key already exists, the value will be added to the underlying {@link Set}.\n\t * If the value already exists in the {@link Set}, it will not be added again.\n\t *\n\t * @param key - The key to set.\n\t * @param value - The value to add to the SetMultiMap.\n\t * @returns The SetMultiMap with the updated key and value.\n\t */\n\toverride set(key: K, value: V): this;\n\t/**\n\t * Adds a new Set with a specified key and value to the SetMultiMap.\n\t * If an element with the same key already exists, the value will be added to the underlying {@link Set}.\n\t * If the value already exists in the {@link Set}, it will not be added again.\n\t *\n\t * @param key - The key to set.\n\t * @param value - The set of values to add to the SetMultiMap.\n\t * @returns The SetMultiMap with the updated key and value.\n\t */\n\toverride set(key: K, value: Set<V>): this;\n\t/**\n\t * Adds a new element with a specified key and value to the SetMultiMap.\n\t * If an element with the same key already exists, the value will be added to the underlying {@link Set}.\n\t * If the value already exists in the {@link Set}, it will not be added again.\n\t *\n\t * @param key The key to set.\n\t * @param value The value to add to the SetMultiMap\n\t * @returns The SetMultiMap with the updated key and value.\n\t */\n\toverride set(key: K, value: V | Set<V>): SetMultiMap<K, V> {\n\t\tsuper.set(key, value instanceof Set ? value : (super.get(key) ?? new Set<V>()).add(value));\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Finds a specific value for a specific key using an iterator function.\n\t * @param key The key to find the value for.\n\t * @param iterator The iterator function to use to find the value.\n\t * @returns The value for the specified key\n\t */\n\tfind(key: K, iterator: (value: V) => boolean): V | undefined {\n\t\tconst values = this.get(key);\n\n\t\tif (values !== undefined) {\n\t\t\treturn Array.from(values).find(iterator);\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Checks if a specific key has a specific value.\n\t *\n\t * @param key The key to check.\n\t * @param value The value to check.\n\t * @returns True if the key has the value, false otherwise.\n\t */\n\thasValue(key: K, value: V): boolean {\n\t\tconst values = super.get(key);\n\n\t\treturn values ? values.has(value) : false;\n\t}\n\n\t/**\n\t * Removes a specific value from a specific key.\n\t * @param key The key to remove the value from.\n\t * @param value The value to remove.\n\t * @returns True if the value was removed, false otherwise.\n\t */\n\tdeleteValue(key: K, value: V | undefined): boolean {\n\t\tif (value === undefined) { return this.delete(key) }\n\n\t\tconst values = super.get(key);\n\t\tif (values) {\n\t\t\tconst deleted = values.delete(value);\n\n\t\t\tif (values.size === 0) {\n\t\t\t\tsuper.delete(key);\n\t\t\t}\n\n\t\t\treturn deleted;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * The string tag of the SetMultiMap.\n\t * @returns The string tag of the SetMultiMap.\n\t */\n\toverride get [Symbol.toStringTag](): string {\n\t\treturn 'SetMultiMap';\n\t}\n}", "import type { EventHandler } from './@types';\n\n/** A wrapper for an event handler that binds a context to the event handler. */\nexport class ContextEventHandler {\n\tprivate readonly context: unknown;\n\tprivate readonly eventHandler: EventHandler;\n\n\t/**\n\t * @param context The context to bind to the event handler.\n\t * @param eventHandler The event handler to call when the event is published.\n\t */\n\tconstructor(context: unknown, eventHandler: EventHandler) {\n\t\tthis.context = context;\n\t\tthis.eventHandler = eventHandler;\n\t}\n\n\t/**\n\t * Call the event handler for the provided event.\n\t *\n\t * @param event The event to handle\n\t * @param data The value to be passed to the event handler as a parameter.\n\t */\n\thandle(event: Event, data?: unknown): void {\n\t\tthis.eventHandler.call(this.context, event, data);\n\t}\n\n\t/**\n\t * A String value that is used in the creation of the default string\n\t * description of an object. Called by the built-in method {@link Object.prototype.toString}.\n\t *\n\t * @returns The default string description of this object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'ContextEventHandler';\n\t}\n}", "import type { ContextEventHandler } from './context-event-handler';\n\n/** Represents a subscription to an event. */\nexport class Subscription {\n\tprivate readonly _eventName: string;\n\tprivate readonly _contextEventHandler: ContextEventHandler;\n\n\t/**\n\t * @param eventName The event name.\n\t * @param contextEventHandler The context event handler.\n\t */\n\tconstructor(eventName: string, contextEventHandler: ContextEventHandler) {\n\t\tthis._eventName = eventName;\n\t\tthis._contextEventHandler = contextEventHandler;\n\t}\n\n\t/**\n\t * Gets the event name for the subscription.\n\t *\n\t * @returns The event name.\n\t */\n\tget eventName(): string {\n\t\treturn this._eventName;\n\t}\n\n\t/**\n\t * Gets the context event handler.\n\t *\n\t * @returns The context event handler\n\t */\n\tget contextEventHandler(): ContextEventHandler {\n\t\treturn this._contextEventHandler;\n\t}\n\n\t/**\n\t * A String value that is used in the creation of the default string\n\t * description of an object. Called by the built-in method {@link Object.prototype.toString}.\n\t *\n\t * @returns The default string description of this object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'Subscription';\n\t}\n}", "import { SetMultiMap } from '@d1g1tal/collections/src';\nimport { ContextEventHandler } from './context-event-handler';\nimport { Subscription } from './subscription';\nimport type { EventHandler, ErrorHandler, SubscriptionOptions } from './@types';\n\n/** A class that allows objects to subscribe to events and be notified when the event is published. */\nexport class Subscribr {\n\tprivate readonly subscribers: SetMultiMap<string, ContextEventHandler> = new SetMultiMap();\n\tprivate errorHandler?: ErrorHandler;\n\n\t/**\n\t * Set a custom error handler for handling errors that occur in event listeners.\n\t * If not set, errors will be logged to the console.\n\t *\n\t * @param errorHandler The error handler function to call when an error occurs in an event listener.\n\t */\n\tsetErrorHandler(errorHandler: ErrorHandler): void {\n\t\tthis.errorHandler = errorHandler;\n\t}\n\n\t/**\n\t * Subscribe to an event\n\t *\n\t * @param eventName The event name to subscribe to.\n\t * @param eventHandler The event handler to call when the event is published.\n\t * @param context The context to bind to the event handler.\n\t * @param options Subscription options.\n\t * @returns An object used to check if the subscription still exists and to unsubscribe from the event.\n\t */\n\tsubscribe(eventName: string, eventHandler: EventHandler, context: unknown = eventHandler, options?: SubscriptionOptions): Subscription {\n\t\tthis.validateEventName(eventName);\n\n\t\t// If once option is set, wrap the handler to auto-unsubscribe\n\t\tif (options?.once) {\n\t\t\tconst originalHandler = eventHandler;\n\t\t\teventHandler = (event: Event, data?: unknown) => {\n\t\t\t\toriginalHandler.call(context, event, data);\n\t\t\t\tthis.unsubscribe(subscription);\n\t\t\t};\n\t\t}\n\n\t\tconst contextEventHandler = new ContextEventHandler(context, eventHandler);\n\t\tthis.subscribers.set(eventName, contextEventHandler);\n\n\t\tconst subscription = new Subscription(eventName, contextEventHandler);\n\n\t\treturn subscription;\n\t}\n\n\t/**\n\t * Unsubscribe from the event\n\t *\n\t * @param subscription The subscription to unsubscribe.\n\t * @returns true if eventListener has been removed successfully. false if the value is not found or if the value is not an object.\n\t */\n\tunsubscribe({ eventName, contextEventHandler }: Subscription): boolean {\n\t\tconst contextEventHandlers = this.subscribers.get(eventName) ?? new Set();\n\t\tconst removed = contextEventHandlers.delete(contextEventHandler);\n\n\t\tif (removed && contextEventHandlers.size === 0) {\tthis.subscribers.delete(eventName) }\n\n\t\treturn removed;\n\t}\n\n\t/**\n\t * Publish an event\n\t *\n\t * @template T\n\t * @param eventName The name of the event.\n\t * @param event The event to be handled.\n\t * @param data The value to be passed to the event handler as a parameter.\n\t */\n\tpublish<T>(eventName: string, event: Event = new CustomEvent(eventName), data?: T): void {\n\t\tthis.validateEventName(eventName);\n\t\tthis.subscribers.get(eventName)?.forEach((contextEventHandler: ContextEventHandler) => {\n\t\t\ttry {\n\t\t\t\tcontextEventHandler.handle(event, data);\n\t\t\t} catch (error) {\n\t\t\t\tif (this.errorHandler) {\n\t\t\t\t\tthis.errorHandler(error as Error, eventName, event, data);\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error(`Error in event handler for '${eventName}':`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Check if the event and handler are subscribed.\n\t *\n\t * @param subscription The subscription object.\n\t * @returns true if the event name and handler are subscribed, false otherwise.\n\t */\n\tisSubscribed({ eventName, contextEventHandler }: Subscription): boolean {\n\t\treturn this.subscribers.get(eventName)?.has(contextEventHandler) ?? false;\n\t}\n\n\t/**\n\t * Validate the event name\n\t *\n\t * @param eventName The event name to validate.\n\t * @throws {TypeError} If the event name is not a non-empty string.\n\t * @throws {Error} If the event name has leading or trailing whitespace.\n\t */\n\tprivate validateEventName(eventName: string): void {\n\t\tif (!eventName || typeof eventName !== 'string') {\n\t\t\tthrow new TypeError('Event name must be a non-empty string');\n\t\t}\n\n\t\tif (eventName.trim() !== eventName) {\n\t\t\tthrow new Error('Event name cannot have leading or trailing whitespace');\n\t\t}\n\t}\n\n\t/**\n\t * Clears all subscriptions. The instance should not be used after calling this method.\n\t */\n\tdestroy(): void {\n\t\tthis.subscribers.clear();\n\t}\n\n\t/**\n\t * A String value that is used in the creation of the default string\n\t * description of an object. Called by the built-in method {@link Object.prototype.toString}.\n\t *\n\t * @returns The default string description of this object.\n\t */\n\tget [Symbol.toStringTag](): string {\n\t\treturn 'Subscribr';\n\t}\n}"],
5
+ "mappings": ";AACO,IAAM,cAAN,cAAgC,IAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8B3C,IAAI,KAAQ,OAAsC;AAC1D,UAAM,IAAI,KAAK,iBAAiB,MAAM,SAAS,MAAM,IAAI,GAAG,KAAK,oBAAI,IAAO,GAAG,IAAI,KAAK,CAAC;AAEzF,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAK,KAAQ,UAAgD;AAC5D,UAAM,SAAS,KAAK,IAAI,GAAG;AAE3B,QAAI,WAAW,QAAW;AACzB,aAAO,MAAM,KAAK,MAAM,EAAE,KAAK,QAAQ;AAAA,IACxC;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,SAAS,KAAQ,OAAmB;AACnC,UAAM,SAAS,MAAM,IAAI,GAAG;AAE5B,WAAO,SAAS,OAAO,IAAI,KAAK,IAAI;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAY,KAAQ,OAA+B;AAClD,QAAI,UAAU,QAAW;AAAE,aAAO,KAAK,OAAO,GAAG;AAAA,IAAE;AAEnD,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,QAAQ;AACX,YAAM,UAAU,OAAO,OAAO,KAAK;AAEnC,UAAI,OAAO,SAAS,GAAG;AACtB,cAAM,OAAO,GAAG;AAAA,MACjB;AAEA,aAAO;AAAA,IACR;AAEA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,KAAc,OAAO,WAAW,IAAY;AAC3C,WAAO;AAAA,EACR;AACD;;;AC7FO,IAAM,sBAAN,MAA0B;AAAA,EACf;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,YAAY,SAAkB,cAA4B;AACzD,SAAK,UAAU;AACf,SAAK,eAAe;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,OAAc,MAAsB;AAC1C,SAAK,aAAa,KAAK,KAAK,SAAS,OAAO,IAAI;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAK,OAAO,WAAW,IAAY;AAClC,WAAO;AAAA,EACR;AACD;;;AChCO,IAAM,eAAN,MAAmB;AAAA,EACR;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,YAAY,WAAmB,qBAA0C;AACxE,SAAK,aAAa;AAClB,SAAK,uBAAuB;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,YAAoB;AACvB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,sBAA2C;AAC9C,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAK,OAAO,WAAW,IAAY;AAClC,WAAO;AAAA,EACR;AACD;;;ACrCO,IAAM,YAAN,MAAgB;AAAA,EACL,cAAwD,IAAI,YAAY;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQR,gBAAgB,cAAkC;AACjD,SAAK,eAAe;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,UAAU,WAAmB,cAA4B,UAAmB,cAAc,SAA6C;AACtI,SAAK,kBAAkB,SAAS;AAGhC,QAAI,SAAS,MAAM;AAClB,YAAM,kBAAkB;AACxB,qBAAe,CAAC,OAAc,SAAmB;AAChD,wBAAgB,KAAK,SAAS,OAAO,IAAI;AACzC,aAAK,YAAY,YAAY;AAAA,MAC9B;AAAA,IACD;AAEA,UAAM,sBAAsB,IAAI,oBAAoB,SAAS,YAAY;AACzE,SAAK,YAAY,IAAI,WAAW,mBAAmB;AAEnD,UAAM,eAAe,IAAI,aAAa,WAAW,mBAAmB;AAEpE,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAY,EAAE,WAAW,oBAAoB,GAA0B;AACtE,UAAM,uBAAuB,KAAK,YAAY,IAAI,SAAS,KAAK,oBAAI,IAAI;AACxE,UAAM,UAAU,qBAAqB,OAAO,mBAAmB;AAE/D,QAAI,WAAW,qBAAqB,SAAS,GAAG;AAAE,WAAK,YAAY,OAAO,SAAS;AAAA,IAAE;AAErF,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAW,WAAmB,QAAe,IAAI,YAAY,SAAS,GAAG,MAAgB;AACxF,SAAK,kBAAkB,SAAS;AAChC,SAAK,YAAY,IAAI,SAAS,GAAG,QAAQ,CAAC,wBAA6C;AACtF,UAAI;AACH,4BAAoB,OAAO,OAAO,IAAI;AAAA,MACvC,SAAS,OAAO;AACf,YAAI,KAAK,cAAc;AACtB,eAAK,aAAa,OAAgB,WAAW,OAAO,IAAI;AAAA,QACzD,OAAO;AACN,kBAAQ,MAAM,+BAA+B,SAAS,MAAM,KAAK;AAAA,QAClE;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,EAAE,WAAW,oBAAoB,GAA0B;AACvE,WAAO,KAAK,YAAY,IAAI,SAAS,GAAG,IAAI,mBAAmB,KAAK;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,kBAAkB,WAAyB;AAClD,QAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAChD,YAAM,IAAI,UAAU,uCAAuC;AAAA,IAC5D;AAEA,QAAI,UAAU,KAAK,MAAM,WAAW;AACnC,YAAM,IAAI,MAAM,uDAAuD;AAAA,IACxE;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AACf,SAAK,YAAY,MAAM;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAK,OAAO,WAAW,IAAY;AAClC,WAAO;AAAA,EACR;AACD;",
6
+ "names": []
7
7
  }
package/package.json CHANGED
@@ -1,74 +1,72 @@
1
1
  {
2
- "name": "@d1g1tal/subscribr",
3
- "version": "4.0.4",
4
- "description": "JavaScript Publish/Subscribe Library",
5
- "author": "Jason DiMeo",
6
- "license": "ISC",
7
- "homepage": "https://github.com/D1g1talEntr0py/subscribr#readme",
8
- "bugs": {
9
- "url": "https://github.com/D1g1talEntr0py/subscribr/issues"
10
- },
11
- "keywords": [
12
- "JavaScript",
13
- "ObserverPattern",
14
- "Publish/Subscribe"
15
- ],
16
- "publishConfig": {
17
- "access": "public"
18
- },
19
- "type": "module",
20
- "exports": {
21
- ".": {
22
- "types": "./dist/subscribr.d.ts",
23
- "import": "./dist/subscribr.js"
24
- },
25
- "./src": {
26
- "types": "./dist/subscribr.d.ts",
27
- "import": "./src/index.ts"
28
- }
29
- },
30
- "files": [
31
- "/src",
32
- "/dist"
33
- ],
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/D1g1talEntr0py/subscribr.git"
37
- },
38
- "dependencies": {
39
- "@d1g1tal/collections": "^2.1.0"
40
- },
41
- "devDependencies": {
42
- "@d1g1tal/chrysalis": "^2.5.0",
43
- "@eslint/compat": "^1.4.0",
44
- "@eslint/js": "^9.37.0",
45
- "@types/node": "^24.7.1",
46
- "@typescript-eslint/eslint-plugin": "^8.46.0",
47
- "@typescript-eslint/parser": "^8.46.0",
48
- "@vitest/coverage-v8": "^3.2.4",
49
- "eslint": "^9.37.0",
50
- "eslint-plugin-compat": "^6.0.2",
51
- "eslint-plugin-jsdoc": "^61.1.0",
52
- "tsbuild": "link:../tsbuild",
53
- "typescript-eslint": "^8.46.0",
54
- "vitest": "^3.2.4"
55
- },
56
- "peerDependencies": {
57
- "typescript": "^5.0.0"
58
- },
59
- "browserslist": [
60
- "defaults and fully supports es6-module",
61
- "node >= 20.15.1"
62
- ],
63
- "scripts": {
64
- "build": "tsbuild",
65
- "build:minify": "tsbuild --minify --force",
66
- "build:watch": "tsbuild --watch",
67
- "type-check": "tsbuild --typeCheck",
68
- "lint": "eslint ./src",
69
- "test": "vitest run",
70
- "test:coverage": "vitest run --coverage",
71
- "test:watch": "vitest",
72
- "prepublish": "pnpm lint && pnpm test && pnpm -s build"
73
- }
74
- }
2
+ "name": "@d1g1tal/subscribr",
3
+ "version": "4.1.8",
4
+ "packageManager": "pnpm@10.30.1",
5
+ "description": "JavaScript Publish/Subscribe Library",
6
+ "author": "Jason DiMeo",
7
+ "license": "ISC",
8
+ "homepage": "https://github.com/D1g1talEntr0py/subscribr#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/D1g1talEntr0py/subscribr/issues"
11
+ },
12
+ "keywords": [
13
+ "JavaScript",
14
+ "ObserverPattern",
15
+ "Publish/Subscribe"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/subscribr.d.ts",
24
+ "import": "./dist/subscribr.js"
25
+ },
26
+ "./src": {
27
+ "types": "./dist/subscribr.d.ts",
28
+ "import": "./src/index.ts"
29
+ }
30
+ },
31
+ "files": [
32
+ "/src",
33
+ "/dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsbuild",
37
+ "build:minify": "tsbuild --minify --force",
38
+ "build:watch": "tsbuild --watch",
39
+ "type-check": "tsbuild --noEmit",
40
+ "lint": "eslint ./src",
41
+ "test": "vitest run",
42
+ "test:coverage": "vitest run --coverage",
43
+ "test:watch": "vitest",
44
+ "prepublish": "pnpm lint && pnpm test && pnpm -s build --force"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/D1g1talEntr0py/subscribr.git"
49
+ },
50
+ "dependencies": {
51
+ "@d1g1tal/collections": "^2.1.1"
52
+ },
53
+ "devDependencies": {
54
+ "@d1g1tal/tsbuild": "1.0.0",
55
+ "@eslint/compat": "^2.0.2",
56
+ "@eslint/js": "^10.0.1",
57
+ "@types/node": "^25.3.0",
58
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
59
+ "@typescript-eslint/parser": "^8.56.0",
60
+ "@vitest/coverage-v8": "^4.0.18",
61
+ "eslint": "^10.0.1",
62
+ "eslint-plugin-compat": "^6.2.0",
63
+ "eslint-plugin-jsdoc": "^62.7.0",
64
+ "typescript": "^5.9.3",
65
+ "typescript-eslint": "^8.56.0",
66
+ "vitest": "^4.0.18"
67
+ },
68
+ "browserslist": [
69
+ "defaults and fully supports es6-module",
70
+ "node >= 20.15.1"
71
+ ]
72
+ }
@@ -1,4 +1,13 @@
1
1
  /** Context event listener function. */
2
- type ContextEventListener = (event: Event, data?: unknown) => void;
2
+ type EventHandler = (event: Event, data?: unknown) => void;
3
3
 
4
- export type { ContextEventListener };
4
+ /** Error handler function for handling errors in event listeners. */
5
+ type ErrorHandler = (error: Error, eventName: string, event: Event, data?: unknown) => void;
6
+
7
+ /** Subscription options. */
8
+ interface SubscriptionOptions {
9
+ /** If true, the subscription will automatically unsubscribe after the first event. */
10
+ once?: boolean;
11
+ }
12
+
13
+ export type { EventHandler, ErrorHandler, SubscriptionOptions };
@@ -1,17 +1,17 @@
1
- import type { ContextEventListener } from './@types';
1
+ import type { EventHandler } from './@types';
2
2
 
3
3
  /** A wrapper for an event handler that binds a context to the event handler. */
4
4
  export class ContextEventHandler {
5
5
  private readonly context: unknown;
6
- private readonly eventListener: ContextEventListener;
6
+ private readonly eventHandler: EventHandler;
7
7
 
8
8
  /**
9
9
  * @param context The context to bind to the event handler.
10
- * @param eventListener The event handler to call when the event is published.
10
+ * @param eventHandler The event handler to call when the event is published.
11
11
  */
12
- constructor(context: unknown, eventListener: ContextEventListener) {
12
+ constructor(context: unknown, eventHandler: EventHandler) {
13
13
  this.context = context;
14
- this.eventListener = eventListener;
14
+ this.eventHandler = eventHandler;
15
15
  }
16
16
 
17
17
  /**
@@ -21,7 +21,7 @@ export class ContextEventHandler {
21
21
  * @param data The value to be passed to the event handler as a parameter.
22
22
  */
23
23
  handle(event: Event, data?: unknown): void {
24
- this.eventListener.call(this.context, event, data);
24
+ this.eventHandler.call(this.context, event, data);
25
25
  }
26
26
 
27
27
  /**
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { Subscribr } from './subscribr';
2
2
  export { Subscription } from './subscription';
3
3
  export { ContextEventHandler } from './context-event-handler';
4
- export type { ContextEventListener } from './@types';
4
+ export type { EventHandler } from './@types';
package/src/subscribr.ts CHANGED
@@ -1,14 +1,21 @@
1
1
  import { SetMultiMap } from '@d1g1tal/collections/src';
2
2
  import { ContextEventHandler } from './context-event-handler';
3
3
  import { Subscription } from './subscription';
4
- import type { ContextEventListener } from './@types';
4
+ import type { EventHandler, ErrorHandler, SubscriptionOptions } from './@types';
5
5
 
6
6
  /** A class that allows objects to subscribe to events and be notified when the event is published. */
7
7
  export class Subscribr {
8
- private readonly subscribers: SetMultiMap<string, ContextEventHandler>;
8
+ private readonly subscribers: SetMultiMap<string, ContextEventHandler> = new SetMultiMap();
9
+ private errorHandler?: ErrorHandler;
9
10
 
10
- constructor() {
11
- this.subscribers = new SetMultiMap();
11
+ /**
12
+ * Set a custom error handler for handling errors that occur in event listeners.
13
+ * If not set, errors will be logged to the console.
14
+ *
15
+ * @param errorHandler The error handler function to call when an error occurs in an event listener.
16
+ */
17
+ setErrorHandler(errorHandler: ErrorHandler): void {
18
+ this.errorHandler = errorHandler;
12
19
  }
13
20
 
14
21
  /**
@@ -17,13 +24,27 @@ export class Subscribr {
17
24
  * @param eventName The event name to subscribe to.
18
25
  * @param eventHandler The event handler to call when the event is published.
19
26
  * @param context The context to bind to the event handler.
27
+ * @param options Subscription options.
20
28
  * @returns An object used to check if the subscription still exists and to unsubscribe from the event.
21
29
  */
22
- subscribe(eventName: string, eventHandler: ContextEventListener, context: unknown = eventHandler): Subscription {
30
+ subscribe(eventName: string, eventHandler: EventHandler, context: unknown = eventHandler, options?: SubscriptionOptions): Subscription {
31
+ this.validateEventName(eventName);
32
+
33
+ // If once option is set, wrap the handler to auto-unsubscribe
34
+ if (options?.once) {
35
+ const originalHandler = eventHandler;
36
+ eventHandler = (event: Event, data?: unknown) => {
37
+ originalHandler.call(context, event, data);
38
+ this.unsubscribe(subscription);
39
+ };
40
+ }
41
+
23
42
  const contextEventHandler = new ContextEventHandler(context, eventHandler);
24
43
  this.subscribers.set(eventName, contextEventHandler);
25
44
 
26
- return new Subscription(eventName, contextEventHandler);
45
+ const subscription = new Subscription(eventName, contextEventHandler);
46
+
47
+ return subscription;
27
48
  }
28
49
 
29
50
  /**
@@ -50,7 +71,18 @@ export class Subscribr {
50
71
  * @param data The value to be passed to the event handler as a parameter.
51
72
  */
52
73
  publish<T>(eventName: string, event: Event = new CustomEvent(eventName), data?: T): void {
53
- this.subscribers.get(eventName)?.forEach((contextEventHandler: ContextEventHandler) => contextEventHandler.handle(event, data));
74
+ this.validateEventName(eventName);
75
+ this.subscribers.get(eventName)?.forEach((contextEventHandler: ContextEventHandler) => {
76
+ try {
77
+ contextEventHandler.handle(event, data);
78
+ } catch (error) {
79
+ if (this.errorHandler) {
80
+ this.errorHandler(error as Error, eventName, event, data);
81
+ } else {
82
+ console.error(`Error in event handler for '${eventName}':`, error);
83
+ }
84
+ }
85
+ });
54
86
  }
55
87
 
56
88
  /**
@@ -63,6 +95,30 @@ export class Subscribr {
63
95
  return this.subscribers.get(eventName)?.has(contextEventHandler) ?? false;
64
96
  }
65
97
 
98
+ /**
99
+ * Validate the event name
100
+ *
101
+ * @param eventName The event name to validate.
102
+ * @throws {TypeError} If the event name is not a non-empty string.
103
+ * @throws {Error} If the event name has leading or trailing whitespace.
104
+ */
105
+ private validateEventName(eventName: string): void {
106
+ if (!eventName || typeof eventName !== 'string') {
107
+ throw new TypeError('Event name must be a non-empty string');
108
+ }
109
+
110
+ if (eventName.trim() !== eventName) {
111
+ throw new Error('Event name cannot have leading or trailing whitespace');
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Clears all subscriptions. The instance should not be used after calling this method.
117
+ */
118
+ destroy(): void {
119
+ this.subscribers.clear();
120
+ }
121
+
66
122
  /**
67
123
  * A String value that is used in the creation of the default string
68
124
  * description of an object. Called by the built-in method {@link Object.prototype.toString}.