@aionbuilders/pulse 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Killian Di Vincenzo
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,538 @@
1
+ # @aionbuilders/pulse
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@aionbuilders/pulse.svg)](https://www.npmjs.com/package/@aionbuilders/pulse)
4
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@aionbuilders/pulse)](https://bundlephobia.com/package/@aionbuilders/pulse)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ > A powerful, lightweight event system with pattern matching, middleware, and timeout support for JavaScript applications.
8
+
9
+ Pulse is a sophisticated event system that extends beyond basic pub/sub patterns. It provides hierarchical topic structures with support for wildcards, middleware chains, response handling, and automatic timeout management.
10
+
11
+ ## Features
12
+
13
+ - ๐Ÿ” **Hierarchical topic matching** with single (`*`), zero-or-more (`**`), and one-or-more (`++`) wildcards
14
+ - โšก **Middleware support** for transforming and filtering events
15
+ - โฑ๏ธ **Built-in timeout handling** for asynchronous operations (default 5s)
16
+ - ๐Ÿ”„ **Promise-based API** for modern JavaScript applications
17
+ - ๐Ÿง  **Smart event routing** based on pattern matching
18
+ - ๐Ÿ’พ **Context management** for storing data within events
19
+ - ๐Ÿ’ช **Typed with JSDoc** for excellent IDE integration
20
+ - ๐Ÿชถ **Lightweight** with zero dependencies
21
+ - ๐Ÿงช **Thoroughly tested** with extensive unit tests
22
+ - ๐Ÿ›ก๏ธ **Unified error handling** across middlewares and listeners
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # Using npm
28
+ npm install @aionbuilders/pulse
29
+
30
+ # Using yarn
31
+ yarn add @aionbuilders/pulse
32
+
33
+ # Using pnpm
34
+ pnpm add @aionbuilders/pulse
35
+
36
+ # Using bun
37
+ bun add @aionbuilders/pulse
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```javascript
43
+ import { Pulse } from '@aionbuilders/pulse';
44
+
45
+ // Create a new Pulse instance
46
+ const pulse = new Pulse();
47
+
48
+ // Subscribe to events
49
+ pulse.on('user:created', ({ event }) => {
50
+ console.log(`New user created: ${event.data.username}`);
51
+ event.respond({ success: true });
52
+ });
53
+
54
+ // Subscribe with wildcards
55
+ pulse.on('user:*:updated', ({ event }) => {
56
+ console.log(`User property updated: ${event.topic}`);
57
+ });
58
+
59
+ // Add middleware
60
+ pulse.use('user:**', async ({ event }, next) => {
61
+ console.log(`[${new Date().toISOString()}] User event: ${event.topic}`);
62
+ // Call next to continue the middleware chain
63
+ await next();
64
+ console.log('Event processing completed');
65
+ });
66
+
67
+ // Emit events
68
+ const event = await pulse.emit('user:created', {
69
+ username: 'john_doe',
70
+ email: 'john@example.com'
71
+ });
72
+
73
+ console.log(event.responses[0]); // { success: true }
74
+ ```
75
+
76
+ ## Core Concepts
77
+
78
+ ### Topics and Patterns
79
+
80
+ Topics in Pulse are hierarchical, colon-separated strings:
81
+
82
+ ```
83
+ namespace:entity:action
84
+ ```
85
+
86
+ For example:
87
+ - `user:login`
88
+ - `chat:message:sent`
89
+ - `system:database:connected`
90
+
91
+ Listeners can use patterns to match multiple topics:
92
+
93
+ - `*`: Matches exactly one section
94
+ - `user:*` matches `user:login` and `user:logout` but not `user:profile:view`
95
+
96
+ - `**`: Matches zero or more sections
97
+ - `user:**` matches `user` (0 sections), `user:login` (1 section), and `user:profile:view` (2 sections)
98
+ - `**:login` matches `login` (0 sections before), `user:login`, `app:user:login`
99
+ - `user:**:deleted` matches `user:deleted` (0 sections between), `user:account:deleted`, etc.
100
+
101
+ - `++`: Matches one or more sections (new in v2.1.3)
102
+ - `user:++` matches `user:login` and `user:profile:view` but NOT `user`
103
+ - `++:login` matches `user:login` and `app:user:login` but NOT `login`
104
+ - `user:++:deleted` matches `user:account:deleted` but NOT `user:deleted`
105
+
106
+ ### Events
107
+
108
+ When you emit a topic, Pulse creates an Event object containing:
109
+
110
+ - `topic`: The emitted topic
111
+ - `data`: The payload data
112
+ - `responses`: Array of response data from listeners
113
+ - `errors`: Array of errors that occurred during processing
114
+ - `timestamp`: When the event was created
115
+ - `id`: Unique event identifier
116
+ - `options`: Event options (silent, source, timeout)
117
+
118
+ ### Middleware
119
+
120
+ Middleware functions allow for pre/post-processing of events:
121
+
122
+ ```javascript
123
+ pulse.use('pattern', async ({ event, pulse, listener }, next) => {
124
+ // Pre-processing
125
+ console.log(`Processing ${event.topic}`);
126
+
127
+ // Continue chain (required to reach listeners)
128
+ await next();
129
+
130
+ // Post-processing (happens after listener execution)
131
+ console.log(`Responses: ${event.responses}`);
132
+ });
133
+ ```
134
+
135
+ ## Detailed Usage
136
+
137
+ ### Basic Event Subscription
138
+
139
+ ```javascript
140
+ // Basic subscription
141
+ pulse.on('topic', ({ event, pulse, listener }) => {
142
+ // Handle event
143
+ // event: the event object
144
+ // pulse: the pulse instance
145
+ // listener: the listener object
146
+ });
147
+
148
+ // Subscribe once (auto-remove after first call)
149
+ pulse.once('topic', ({ event }) => {
150
+ // This will only be called once
151
+ });
152
+
153
+ // With options
154
+ pulse.on('topic', ({ event }) => {
155
+ // Handle event
156
+ }, {
157
+ once: true, // Auto-remove after being called once
158
+ autodestroy: {
159
+ calls: 5, // Remove after 5 calls
160
+ timeout: 60000 // Remove after 60 seconds
161
+ }
162
+ });
163
+
164
+ // Return a value to automatically add it to responses
165
+ pulse.on('greeting', ({ event }) => {
166
+ return `Hello, ${event.data.name}!`; // Automatically added to responses
167
+ });
168
+ ```
169
+
170
+ ### Event Emission with Timeout
171
+
172
+ ```javascript
173
+ // Basic emission
174
+ const event = await pulse.emit('topic', { message: 'Hello World' });
175
+
176
+ // With custom timeout (default is 5000ms)
177
+ const event = await pulse.emit('topic', data, { timeout: 10000 });
178
+
179
+ // Silent mode (no responses or errors collected)
180
+ const event = await pulse.emit('topic', data, { silent: true });
181
+ ```
182
+
183
+ ### Handling Responses
184
+
185
+ ```javascript
186
+ // In listener: set a response
187
+ pulse.on('greeting', ({ event }) => {
188
+ return `Hello, ${event.data.name}!`; // Automatically added to responses
189
+ // Or explicitly:
190
+ // event.respond(`Hello, ${event.data.name}!`);
191
+ });
192
+
193
+ // In emitter: get the responses
194
+ const event = await pulse.emit('greeting', { name: 'John' });
195
+ console.log(event.responses[0]); // "Hello, John!"
196
+
197
+ // Multiple listeners can add multiple responses
198
+ pulse.on('greeting', ({ event }) => event.respond('Response 1'));
199
+ pulse.on('greeting', ({ event }) => event.respond('Response 2'));
200
+ const event = await pulse.emit('greeting', {});
201
+ console.log(event.responses); // ['Response 1', 'Response 2']
202
+ ```
203
+
204
+ ### Error Handling
205
+
206
+ ```javascript
207
+ // In listener: handle errors
208
+ pulse.on('process', ({ event }) => {
209
+ try {
210
+ // Do something that might fail
211
+ throw new Error('Something went wrong');
212
+ } catch (err) {
213
+ event.error(err);
214
+ }
215
+ });
216
+
217
+ // Errors thrown in listeners are automatically caught
218
+ pulse.on('process', ({ event }) => {
219
+ throw new Error('Auto-caught error'); // Automatically added to errors
220
+ });
221
+
222
+ // In emitter: check for errors
223
+ const event = await pulse.emit('process', data);
224
+ if (event.errors.length > 0) {
225
+ console.error('Errors occurred:', event.errors);
226
+ }
227
+ ```
228
+
229
+ ### Middleware Chains
230
+
231
+ ```javascript
232
+ // Authentication middleware
233
+ pulse.use('secure:**', async ({ event, pulse, listener }, next) => {
234
+ if (!event.data.token) {
235
+ event.error(new Error('Authentication required'));
236
+ return; // Don't call next() to stop the chain
237
+ }
238
+
239
+ // Validate token
240
+ const user = validateToken(event.data.token);
241
+ if (!user) {
242
+ event.error(new Error('Invalid token'));
243
+ return;
244
+ }
245
+
246
+ // Add user to event data for downstream handlers
247
+ event.data.user = user;
248
+
249
+ // Continue the middleware chain
250
+ await next();
251
+ });
252
+
253
+ // Logging middleware
254
+ pulse.use('**', async ({ event }, next) => {
255
+ console.time(`event:${event.id}`);
256
+ await next();
257
+ console.timeEnd(`event:${event.id}`);
258
+ });
259
+
260
+ // Error handling in middleware (v2.1.3+)
261
+ // Errors thrown in middleware are automatically caught and collected
262
+ pulse.use('user:**', async ({ event }, next) => {
263
+ throw new Error('Middleware error'); // Auto-caught and added to event.errors
264
+ // The next middleware and listener will still execute
265
+ });
266
+ ```
267
+
268
+ ### Cleaning Up
269
+
270
+ ```javascript
271
+ // Remove a specific listener
272
+ const listener = pulse.on('topic', callback);
273
+ listener.destroy();
274
+
275
+ // Remove all listeners for a topic
276
+ pulse.off('topic');
277
+
278
+ // Remove all listeners
279
+ pulse.removeAllListeners();
280
+
281
+ // Clear pattern cache (v2.1.3+)
282
+ // Useful for long-running applications with dynamic patterns
283
+ pulse.clearPatternCache();
284
+ ```
285
+
286
+ ## API Reference
287
+
288
+ ### Pulse Class
289
+
290
+ #### Constructor
291
+
292
+ ```javascript
293
+ const pulse = new Pulse(options);
294
+ ```
295
+
296
+ **Options:**
297
+ - `EventClass` (optional): Custom event class that extends PulseEvent
298
+
299
+ #### Methods
300
+
301
+ | Method | Parameters | Returns | Description |
302
+ |--------|------------|---------|-------------|
303
+ | `on` | `pattern: string`, `callback: Function`, `options?: Object` | `Listener` | Subscribe to events matching the pattern |
304
+ | `once` | `pattern: string`, `callback: Function`, `options?: Object` | `Listener` | Subscribe to events matching the pattern (auto-remove after first call) |
305
+ | `use` | `pattern: string`, `callback: Function` | `Middleware` | Add middleware for events matching the pattern |
306
+ | `emit` | `topic: string`, `data: any`, `options?: Object` | `Promise<PulseEvent>` | Emit an event with the specified topic and data |
307
+ | `off` | `pattern: string` | `void` | Remove all listeners for a pattern |
308
+ | `removeAllListeners` | | `void` | Remove all listeners |
309
+ | `clearPatternCache` | | `void` | Clear the pattern cache (useful for memory management) |
310
+ | `matchesPattern` | `topic: string`, `pattern: string` | `boolean` | Check if a topic matches a pattern |
311
+
312
+ **Callback signature for listeners:**
313
+ ```javascript
314
+ ({ event, pulse, listener }) => { /* ... */ }
315
+ ```
316
+
317
+ **Callback signature for middleware:**
318
+ ```javascript
319
+ async ({ event, pulse, listener }, next) => { /* ... */ }
320
+ ```
321
+
322
+ #### Listener Options
323
+
324
+ | Option | Type | Default | Description |
325
+ |--------|------|---------|-------------|
326
+ | `once` | `boolean` | `false` | If true, the listener will be removed after it is called once |
327
+ | `autodestroy.calls` | `number` | `undefined` | Number of calls after which the listener is removed |
328
+ | `autodestroy.timeout` | `number` | `undefined` | Time in milliseconds after which the listener is removed |
329
+
330
+ #### Emit Options
331
+
332
+ | Option | Type | Default | Description |
333
+ |--------|------|---------|-------------|
334
+ | `timeout` | `number` | `5000` | Time in milliseconds to wait for listener responses before timing out |
335
+ | `silent` | `boolean` | `false` | If true, the event will not collect responses or errors |
336
+ | `source` | `string\|null` | `null` | Optional source identifier for the event |
337
+
338
+ ### Event Class
339
+
340
+ #### Properties
341
+
342
+ | Property | Type | Description |
343
+ |----------|------|-------------|
344
+ | `topic` | `string` | The topic of the event |
345
+ | `data` | `any` | The payload data of the event |
346
+ | `responses` | `any[]` | Array of response data from listeners |
347
+ | `errors` | `Error[]` | Array of errors that occurred during processing |
348
+ | `timestamp` | `number` | When the event was created (milliseconds since epoch) |
349
+ | `id` | `string` | Unique event identifier |
350
+ | `options` | `Object` | Event options (silent, source, timeout) |
351
+
352
+ #### Methods
353
+
354
+ | Method | Parameters | Returns | Description |
355
+ |--------|------------|---------|-------------|
356
+ | `respond` | `data: any` | `PulseEvent` | Add response data to the event |
357
+ | `error` | `error: Error` | `PulseEvent` | Add an error to the event |
358
+ | `set` | `key: any`, `value: any` | `PulseEvent` | Set a context value (chainable) |
359
+ | `get` | `key: any` | `any` | Get a context value |
360
+ | `has` | `key: any` | `boolean` | Check if a context key exists |
361
+ | `delete` | `key: any` | `boolean` | Delete a context entry |
362
+ | `clearContext` | | `PulseEvent` | Clear all context data (chainable) |
363
+
364
+ ## Advanced Examples
365
+
366
+ ### Using Event Context
367
+
368
+ Event context allows you to store metadata and share data between middlewares and listeners:
369
+
370
+ ```javascript
371
+ // Middleware can enrich the event with context data
372
+ pulse.use('user:**', async ({ event }, next) => {
373
+ // Add metadata to event context
374
+ event.set('processingStarted', Date.now());
375
+ event.set('correlationId', generateId());
376
+
377
+ await next();
378
+
379
+ // Access context after processing
380
+ const duration = Date.now() - event.get('processingStarted');
381
+ console.log(`Event ${event.get('correlationId')} took ${duration}ms`);
382
+ });
383
+
384
+ // Listeners can access context data
385
+ pulse.on('user:created', ({ event }) => {
386
+ console.log(`Correlation ID: ${event.get('correlationId')}`);
387
+
388
+ // Store additional metadata
389
+ event.set('handler', 'user-creation-handler');
390
+ event.set('version', '1.0');
391
+
392
+ // Check if context exists
393
+ if (event.has('correlationId')) {
394
+ // Use it for logging/tracking
395
+ }
396
+ });
397
+
398
+ // Clear context when needed
399
+ pulse.on('cleanup', ({ event }) => {
400
+ event.clearContext();
401
+ });
402
+ ```
403
+
404
+ ### Creating a Request-Response Pattern
405
+
406
+ ```javascript
407
+ // Create an RPC-like system
408
+ async function callRemote(method, params) {
409
+ const responseTopic = `response:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`;
410
+
411
+ // Create a promise that will resolve when we get a response
412
+ const responsePromise = new Promise((resolve) => {
413
+ pulse.once(responseTopic, ({ event }) => {
414
+ resolve(event.data);
415
+ });
416
+ });
417
+
418
+ // Add the response topic to the request
419
+ await pulse.emit(`rpc:${method}`, {
420
+ params,
421
+ responseTopic
422
+ });
423
+
424
+ // Wait for the response
425
+ return responsePromise;
426
+ }
427
+
428
+ // Handle RPC requests
429
+ pulse.on('rpc:**', ({ event }) => {
430
+ const method = event.topic.split(':')[1];
431
+ const { params, responseTopic } = event.data;
432
+
433
+ // Process the request
434
+ const result = processMethod(method, params);
435
+
436
+ // Send the response back
437
+ pulse.emit(responseTopic, result);
438
+ });
439
+
440
+ // Usage
441
+ const result = await callRemote('getUserProfile', { id: 123 });
442
+ ```
443
+
444
+ ### Creating a State Management System
445
+
446
+ ```javascript
447
+ class Store {
448
+ constructor(pulse, initialState = {}) {
449
+ this.pulse = pulse;
450
+ this.state = initialState;
451
+
452
+ // Listen for state change requests
453
+ this.pulse.on('store:set:**', ({ event }) => {
454
+ const path = event.topic.replace('store:set:', '').split(':');
455
+ this.setState(path, event.data);
456
+ });
457
+ }
458
+
459
+ setState(path, value) {
460
+ let current = this.state;
461
+ const pathArr = Array.isArray(path) ? path : [path];
462
+
463
+ // Navigate to the nested property
464
+ for (let i = 0; i < pathArr.length - 1; i++) {
465
+ if (!current[pathArr[i]]) {
466
+ current[pathArr[i]] = {};
467
+ }
468
+ current = current[pathArr[i]];
469
+ }
470
+
471
+ // Set the value
472
+ const lastKey = pathArr[pathArr.length - 1];
473
+ const oldValue = current[lastKey];
474
+ current[lastKey] = value;
475
+
476
+ // Emit state change event
477
+ this.pulse.emit(`store:changed:${path.join(':')}`, {
478
+ oldValue,
479
+ newValue: value,
480
+ path
481
+ });
482
+ }
483
+
484
+ getState(path) {
485
+ if (!path) return this.state;
486
+
487
+ let current = this.state;
488
+ const pathArr = Array.isArray(path) ? path : path.split(':');
489
+
490
+ for (const key of pathArr) {
491
+ if (current[key] === undefined) return undefined;
492
+ current = current[key];
493
+ }
494
+
495
+ return current;
496
+ }
497
+ }
498
+
499
+ // Usage
500
+ const store = new Store(pulse, {
501
+ user: { name: 'John', age: 30 },
502
+ settings: { theme: 'dark' }
503
+ });
504
+
505
+ // Listen for changes
506
+ pulse.on('store:changed:user:name', ({ event }) => {
507
+ console.log(`User's name changed from ${event.data.oldValue} to ${event.data.newValue}`);
508
+ });
509
+
510
+ // Update state
511
+ pulse.emit('store:set:user:name', 'Jane');
512
+ ```
513
+
514
+ ## Performance Considerations
515
+
516
+ - **Wildcards**: Using many `**` wildcards can impact performance as they require more complex pattern matching
517
+ - **Listener Count**: High numbers of listeners on frequently emitted topics can affect performance
518
+ - **Middleware Complexity**: Complex middleware chains can add overhead to event processing
519
+
520
+ ## Browser Support
521
+
522
+ Pulse works in all modern browsers and Node.js environments. It uses ES6 features like Promises, Maps, and arrow functions.
523
+
524
+ ## License
525
+
526
+ MIT ยฉ Killian Di Vincenzo
527
+
528
+ ---
529
+
530
+ ## Contributing
531
+
532
+ Contributions are welcome! Feel free to submit issues and pull requests.
533
+
534
+ 1. Fork the repository
535
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
536
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
537
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
538
+ 5. Open a Pull Request
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Base event class for Pulse event system.
3
+ * Can be extended to create custom event types.
4
+ */
5
+ export class PulseEvent {
6
+ /**
7
+ * @param {string} topic
8
+ * @param {any} data
9
+ * @param {Object} [options]
10
+ * @param {boolean} [options.silent=false] - If true, the event will not collect responses or errors.
11
+ * @param {string|null} [options.source=null] - The source of the event.
12
+ * @param {number} [options.timeout=5000] - The timeout for the event in milliseconds.
13
+ */
14
+ constructor(topic: string, data: any, options?: {
15
+ silent?: boolean | undefined;
16
+ source?: string | null | undefined;
17
+ timeout?: number | undefined;
18
+ });
19
+ topic: string;
20
+ data: any;
21
+ options: {
22
+ silent: boolean;
23
+ source: string | null;
24
+ timeout: number;
25
+ };
26
+ timestamp: number;
27
+ id: string;
28
+ /**
29
+ * @type {any[]}
30
+ */
31
+ responses: any[];
32
+ /**
33
+ * @type {Error[]}
34
+ */
35
+ errors: Error[];
36
+ /**
37
+ * Add a response to the event
38
+ * @template {PulseEvent} T
39
+ * @this {T}
40
+ * @param {any} data
41
+ * @returns {T} Returns this for chaining
42
+ */
43
+ respond<T extends PulseEvent>(this: T, data: any): T;
44
+ /**
45
+ * Add an error to the event
46
+ * @template {PulseEvent} T
47
+ * @this {T}
48
+ * @param {Error} err
49
+ * @returns {T} Returns this for chaining
50
+ */
51
+ error<T extends PulseEvent>(this: T, err: Error): T;
52
+ /**
53
+ * Set a context value
54
+ * @template {PulseEvent} T
55
+ * @this {T}
56
+ * @param {any} key - The key to set
57
+ * @param {any} value - The value to associate with the key
58
+ * @returns {T} Returns this for chaining
59
+ */
60
+ set<T extends PulseEvent>(this: T, key: any, value: any): T;
61
+ /**
62
+ * Get a context value
63
+ * @param {any} key - The key to retrieve
64
+ * @returns {any} The value associated with the key, or undefined if not found
65
+ */
66
+ get(key: any): any;
67
+ /**
68
+ * Check if a context key exists
69
+ * @param {any} key - The key to check
70
+ * @returns {boolean} True if the key exists in the context
71
+ */
72
+ has(key: any): boolean;
73
+ /**
74
+ * Delete a context entry
75
+ * @param {any} key - The key to delete
76
+ * @returns {boolean} True if the key existed and was deleted, false otherwise
77
+ */
78
+ delete(key: any): boolean;
79
+ /**
80
+ * Clear all context data
81
+ * @template {PulseEvent} T
82
+ * @this {T}
83
+ * @returns {T} Returns this for chaining
84
+ */
85
+ clearContext<T extends PulseEvent>(this: T): T;
86
+ #private;
87
+ }
88
+ //# sourceMappingURL=event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/core/event.js"],"names":[],"mappings":"AAGA;;;GAGG;AACH;IAOI;;;;;;;OAOG;IACH,mBAPW,MAAM,QACN,GAAG,YAEX;QAA0B,MAAM;QACF,MAAM;QACX,OAAO;KAClC,EAuBA;IArBG,cAAkB;IAClB,UAAgB;IAChB;gBAPO,OAAO;gBACP,MAAM,GAAC,IAAI;iBACX,MAAM;MAUZ;IAED,kBAA2B;IAC3B,WAA6D;IAE7D;;OAEG;IACH,WAFU,GAAG,EAAE,CAEI;IAEnB;;OAEG;IACH,QAFU,KAAK,EAAE,CAED;IAGpB;;;;;;OAMG;IACH,QAL0B,CAAC,SAAb,UAAW,iBAEd,GAAG,GACD,CAAC,CAMb;IAED;;;;;;OAMG;IACH,MAL0B,CAAC,SAAb,UAAW,gBAEd,KAAK,GACH,CAAC,CAMb;IAED;;;;;;;OAOG;IACH,IAN0B,CAAC,SAAb,UAAW,gBAEd,GAAG,SACH,GAAG,GACD,CAAC,CAKb;IAED;;;;OAIG;IACH,SAHW,GAAG,GACD,GAAG,CAIf;IAED;;;;OAIG;IACH,SAHW,GAAG,GACD,OAAO,CAInB;IAED;;;;OAIG;IACH,YAHW,GAAG,GACD,OAAO,CAInB;IAED;;;;;OAKG;IACH,aAJ0B,CAAC,SAAb,UAAW,YAEZ,CAAC,CAKb;;CACJ"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @typedef {Object} ListenerOptions
3
+ * @property {boolean} [once] - If true, the listener will be removed after it is called once.
4
+ * @property {Object} [autodestroy]
5
+ * @property {Number} [autodestroy.timeout] - The time in milliseconds to wait before the listener is removed.
6
+ * @property {Number} [autodestroy.calls] - The number of calls to the listener before it is removed.
7
+ */
8
+ /**
9
+ * @template {import('./event').PulseEvent} [TEvent=import('./event').PulseEvent]
10
+ * @typedef {Object} ListenerContext
11
+ * @property {import('./pulse').Pulse} pulse
12
+ * @property {TEvent} event
13
+ * @property {Listener<TEvent>} listener
14
+ */
15
+ /**
16
+ * @template {import('./event').PulseEvent} [TEvent=import('./event').PulseEvent]
17
+ */
18
+ export class Listener<TEvent extends import("./event").PulseEvent = import("./event").PulseEvent> {
19
+ /**
20
+ * @param {import('./pulse').Pulse} pulse
21
+ * @param {string} pattern
22
+ * @param {(context: ListenerContext<TEvent>) => void} callback
23
+ * @param {ListenerOptions} options
24
+ */
25
+ constructor(pulse: import("./pulse").Pulse, pattern: string, callback: (context: ListenerContext<TEvent>) => void, options: ListenerOptions);
26
+ pulse: import("./pulse").Pulse<typeof import("./event").PulseEvent>;
27
+ pattern: string;
28
+ options: ListenerOptions;
29
+ calls: number;
30
+ timeout: number | null;
31
+ /** @param {TEvent} event */
32
+ call: (event: TEvent) => Promise<void>;
33
+ destroy: () => void;
34
+ #private;
35
+ }
36
+ export type ListenerOptions = {
37
+ /**
38
+ * - If true, the listener will be removed after it is called once.
39
+ */
40
+ once?: boolean | undefined;
41
+ autodestroy?: {
42
+ /**
43
+ * - The time in milliseconds to wait before the listener is removed.
44
+ */
45
+ timeout?: number | undefined;
46
+ /**
47
+ * - The number of calls to the listener before it is removed.
48
+ */
49
+ calls?: number | undefined;
50
+ } | undefined;
51
+ };
52
+ export type ListenerContext<TEvent extends import("./event").PulseEvent = import("./event").PulseEvent> = {
53
+ pulse: import("./pulse").Pulse;
54
+ event: TEvent;
55
+ listener: Listener<TEvent>;
56
+ };
57
+ //# sourceMappingURL=listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/core/listener.js"],"names":[],"mappings":"AAAA;;;;;;EAME;AAEF;;;;;;GAMG;AAEH;;GAEG;AACH,sBAF6C,MAAM,SAAtC,OAAQ,SAAS,EAAE,UAAW;IAGvC;;;;;OAKG;IACH,mBALW,OAAO,SAAS,EAAE,KAAK,WACvB,MAAM,YACN,CAAC,OAAO,EAAE,eAAe,CAAC,MAAM,CAAC,KAAK,IAAI,WAC1C,eAAe,EAqBzB;IAlBG,oEAAkB;IAClB,gBAAsB;IAEtB,yBAAsB;IAEtB,cAAc;IACd,uBAAmB;IAiBvB,4BAA4B;IAC5B,OAAc,OADF,MACO,mBAajB;IAEF,oBAWC;;CACJ;;;;;;;;;;;;;;;;;4BArE4C,MAAM,SAAtC,OAAQ,SAAS,EAAE,UAAW;WAE7B,OAAO,SAAS,EAAE,KAAK;WACvB,MAAM;cACN,QAAQ,CAAC,MAAM,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @typedef {import('./pulse').Pulse} Pulse
3
+ * @typedef {import('./event').PulseEvent} PulseEvent
4
+ */
5
+ /**
6
+ * @template {PulseEvent} [TEvent=PulseEvent]
7
+ * @callback NextCallback
8
+ * @returns {Promise<void>}
9
+ */
10
+ /**
11
+ * @template {PulseEvent} [TEvent=PulseEvent]
12
+ * @callback MiddlewareCallback
13
+ * @param {import('./listener').ListenerContext<TEvent>} context
14
+ * @param {NextCallback<TEvent>} next
15
+ * @returns {Promise<any>}
16
+ */
17
+ /**
18
+ * @template {PulseEvent} [TEvent=PulseEvent]
19
+ */
20
+ export class Middleware<TEvent extends PulseEvent = import("./event").PulseEvent> {
21
+ /**
22
+ * @param {import('./pulse').Pulse} pulse
23
+ * @param {String} pattern
24
+ * @param {MiddlewareCallback<TEvent>} callback
25
+ */
26
+ constructor(pulse: import("./pulse").Pulse, pattern: string, callback: MiddlewareCallback<TEvent>);
27
+ pulse: import("./pulse").Pulse<typeof import("./event").PulseEvent>;
28
+ pattern: string;
29
+ callback: MiddlewareCallback<TEvent>;
30
+ /**
31
+ * @param {string} topic
32
+ * @returns {boolean}
33
+ */
34
+ matches(topic: string): boolean;
35
+ destroy(): void;
36
+ }
37
+ export type Pulse = import("./pulse").Pulse;
38
+ export type PulseEvent = import("./event").PulseEvent;
39
+ export type NextCallback<TEvent extends PulseEvent = import("./event").PulseEvent> = () => Promise<void>;
40
+ export type MiddlewareCallback<TEvent extends PulseEvent = import("./event").PulseEvent> = (context: import("./listener").ListenerContext<TEvent>, next: NextCallback<TEvent>) => Promise<any>;
41
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/core/middleware.js"],"names":[],"mappings":"AAAA;;;EAGE;AAEF;;;;EAIE;AAEF;;;;;;EAME;AAEF;;EAEE;AACF,wBAF0B,MAAM,SAAnB,UAAW;IAGpB;;;;MAIE;IACF,mBAJU,OAAO,SAAS,EAAE,KAAK,6BAEvB,kBAAkB,CAAC,MAAM,CAAC,EAMnC;IAHG,oEAAkB;IAClB,gBAAsB;IACtB,qCAAwB;IAG5B;;;MAGE;IACF,eAHU,MAAM,GACJ,OAAO,CAIlB;IAED,gBAKC;CACJ;oBA/CW,OAAO,SAAS,EAAE,KAAK;yBACvB,OAAO,SAAS,EAAE,UAAU;yBAId,MAAM,SAAnB,UAAW,yCAEZ,OAAO,CAAC,IAAI,CAAC;+BAIC,MAAM,SAAnB,UAAW,6CAEd,OAAO,YAAY,EAAE,eAAe,CAAC,MAAM,CAAC,QAC5C,YAAY,CAAC,MAAM,CAAC,KAClB,OAAO,CAAC,GAAG,CAAC"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @template {typeof PulseEvent} [TEventClass=typeof PulseEvent]
3
+ * @typedef {Object} PulseOptions
4
+ * @property {TEventClass} [EventClass] - Custom event class to use (must extend PulseEvent)
5
+ */
6
+ /**
7
+ * @template {typeof PulseEvent} [TEventClass=typeof PulseEvent]
8
+ */
9
+ export class Pulse<TEventClass extends typeof PulseEvent = typeof PulseEvent> {
10
+ /**
11
+ * @param {PulseOptions<TEventClass>} [options]
12
+ */
13
+ constructor(options?: PulseOptions<TEventClass>);
14
+ /** @type {Map<string, Set<import('./listener').Listener<InstanceType<TEventClass>>>>} */
15
+ listeners: Map<string, Set<import("./listener").Listener<InstanceType<TEventClass>>>>;
16
+ /** @type {import('./middleware').Middleware<InstanceType<TEventClass>>[]} */
17
+ middlewares: import("./middleware").Middleware<InstanceType<TEventClass>>[];
18
+ EventClass: typeof PulseEvent;
19
+ /**
20
+ * @param {String} pattern
21
+ * @param {(context: import('./listener').ListenerContext<InstanceType<TEventClass>>) => any} callback
22
+ * @param {import('./listener').ListenerOptions} options
23
+ * @returns {import('./listener').Listener<InstanceType<TEventClass>>}
24
+ */
25
+ on: (pattern: string, callback: (context: import("./listener").ListenerContext<InstanceType<TEventClass>>) => any, options?: import("./listener").ListenerOptions) => import("./listener").Listener<InstanceType<TEventClass>>;
26
+ /**
27
+ * @param {String} pattern
28
+ * @param {(context: import('./listener').ListenerContext<InstanceType<TEventClass>>) => any} callback
29
+ * @param {import('./listener').ListenerOptions} options
30
+ * @returns {import('./listener').Listener<InstanceType<TEventClass>>}
31
+ */
32
+ once: (pattern: string, callback: (context: import("./listener").ListenerContext<InstanceType<TEventClass>>) => any, options?: import("./listener").ListenerOptions) => import("./listener").Listener<InstanceType<TEventClass>>;
33
+ /**
34
+ * @param {string} pattern
35
+ * @param {import('./middleware').MiddlewareCallback<InstanceType<TEventClass>>} callback
36
+ * @returns {import('./middleware').Middleware<InstanceType<TEventClass>>}
37
+ */
38
+ use: (pattern: string, callback: import("./middleware").MiddlewareCallback<InstanceType<TEventClass>>) => import("./middleware").Middleware<InstanceType<TEventClass>>;
39
+ /**
40
+ * @param {InstanceType<TEventClass>} event
41
+ * @param {import('./listener').Listener<InstanceType<TEventClass>>} listener
42
+ */
43
+ applyMiddlewaresToListener(event: InstanceType<TEventClass>, listener: import("./listener").Listener<InstanceType<TEventClass>>): Promise<any>;
44
+ /**
45
+ * @param {string} topic
46
+ * @param {any} data
47
+ * @param {{
48
+ * timeout?: number,
49
+ * }} options
50
+ * @returns {Promise<InstanceType<TEventClass>>}
51
+ */
52
+ emit: (topic: string, data: any, options?: {
53
+ timeout?: number;
54
+ }) => Promise<InstanceType<TEventClass>>;
55
+ /**
56
+ * @param {string} topic
57
+ * @returns {boolean}
58
+ */
59
+ isValidTopic(topic: string): boolean;
60
+ /**
61
+ * @param {string} topic
62
+ * @param {string} pattern
63
+ * @returns {boolean}
64
+ */
65
+ matchesPattern(topic: string, pattern: string): boolean;
66
+ /**
67
+ * Remove all listeners for a specific pattern
68
+ * @param {string} pattern
69
+ */
70
+ off(pattern: string): void;
71
+ /**
72
+ * Remove all listeners
73
+ */
74
+ removeAllListeners(): void;
75
+ /**
76
+ * Clear the pattern cache
77
+ * Useful for memory management in long-running applications
78
+ */
79
+ clearPatternCache(): void;
80
+ #private;
81
+ }
82
+ export type PulseOptions<TEventClass extends typeof PulseEvent = typeof PulseEvent> = {
83
+ /**
84
+ * - Custom event class to use (must extend PulseEvent)
85
+ */
86
+ EventClass?: TEventClass | undefined;
87
+ };
88
+ import { PulseEvent } from './event.js';
89
+ //# sourceMappingURL=pulse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pulse.d.ts","sourceRoot":"","sources":["../../src/core/pulse.js"],"names":[],"mappings":"AAIA;;;;GAIG;AAEH;;GAEG;AACH,mBAFkC,WAAW,SAAhC,OAAQ,UAAW;IAG5B;;OAEG;IACH,sBAFW,YAAY,CAAC,WAAW,CAAC,EAiBnC;IAdG,yFAAyF;IACzF,WADW,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAC3D;IAC1B,6EAA6E;IAC7E,aADW,OAAO,cAAc,EAAE,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,EAAE,CACpD;IAKrB,8BAAkD;IActD;;;;;OAKG;IACH,KAAM,eAAO,EAAE,UAJJ,CAAC,OAAO,EAAE,OAAO,YAAY,EAAE,eAAe,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,KAAK,GAIlE,EAAE,UAHd,OAAO,YAAY,EAAE,eAGK,KAFxB,OAAO,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CASpE;IAED;;;;;OAKG;IACH,OAAQ,eAAO,EAAE,UAJN,CAAC,OAAO,EAAE,OAAO,YAAY,EAAE,eAAe,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,KAAK,GAIhE,EAAE,UAHhB,OAAO,YAAY,EAAE,eAGO,KAF1B,OAAO,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAE6B;IAElG;;;;OAIG;IACH,MAAO,SAJI,MAIG,EAAE,UAHL,OAAO,cAAc,EAAE,kBAAkB,CAAC,YAAY,CAAC,WAAW,CAAC,CAGtD,KAFX,OAAO,cAAc,EAAE,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAMxE;IAED;;;OAGG;IACH,kCAHW,YAAY,CAAC,WAAW,CAAC,YACzB,OAAO,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,gBA2BlE;IAED;;;;;;;OAOG;IACH,OAAc,OAPH,MAOQ,EAAE,MANV,GAMc,EAAE,UALhB;QACR,OAAO,CAAC,EAAE,MAAM,CAAC;KAImB,KAF1B,OAAO,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CA+C9C;IA0HD;;;OAGG;IACH,oBAHW,MAAM,GACJ,OAAO,CAKnB;IAED;;;;OAIG;IACH,sBAJW,MAAM,WACN,MAAM,GACJ,OAAO,CAInB;IAED;;;OAGG;IACH,aAFW,MAAM,QAIhB;IAED;;OAEG;IACH,2BAEC;IAED;;;OAGG;IACH,0BAEC;;CAEJ;yBA5TiC,WAAW,SAAhC,OAAQ,UAAW;;;;;;2BALL,YAAY"}
@@ -0,0 +1,5 @@
1
+ export { Pulse } from "./core/pulse.js";
2
+ export { PulseEvent } from "./core/event.js";
3
+ export { Listener } from "./core/listener.js";
4
+ export { Middleware } from "./core/middleware.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ var R=0;class G{#f=new Map;constructor(f,q,z={}){this.topic=f,this.data=q,this.options={silent:!1,source:null,timeout:5000,...z},this.timestamp=Date.now(),this.id=`${this.topic}-${this.timestamp}-${++R}`,this.responses=[],this.errors=[]}respond(f){if(this.options.silent)return this;return this.responses.push(f),this}error(f){if(this.options.silent)return this;return this.errors.push(f),this}set(f,q){return this.#f.set(f,q),this}get(f){return this.#f.get(f)}has(f){return this.#f.has(f)}delete(f){return this.#f.delete(f)}clearContext(){return this.#f.clear(),this}}class Y{constructor(f,q,z,B){if(this.pulse=f,this.pattern=q,this.#f=z,this.options=B,this.calls=0,this.timeout=null,this.options?.autodestroy?.timeout)this.timeout=setTimeout(()=>{this.destroy()},this.options.autodestroy.timeout);if(this.options?.once)this.options.autodestroy??={},this.options.autodestroy.calls=1}#f;call=async(f)=>{if(this.calls++,this.options?.autodestroy?.calls&&this.calls>=this.options.autodestroy.calls)this.destroy();return Promise.resolve(this.#f({event:f,pulse:this.pulse,listener:this})).then((q)=>{if(q!==void 0&&q!==null)f.respond(q)}).catch((q)=>{f.error(q)})};destroy=()=>{if(this.timeout)clearTimeout(this.timeout);let f=this.pulse.listeners.get(this.pattern);if(f){if(f.delete(this),f.size===0)this.pulse.listeners.delete(this.pattern)}}}class ${constructor(f,q,z){this.pulse=f,this.pattern=q,this.callback=z}matches(f){return this.pulse.matchesPattern(f,this.pattern)}destroy(){this.pulse.middlewares=this.pulse.middlewares.filter((f)=>f!==this),this.destroy=()=>{throw Error("Middleware already destroyed")}}}class K{constructor(f={}){if(this.listeners=new Map,this.middlewares=[],this.#f=new Map,this.EventClass=f.EventClass||G,this.EventClass!==G&&!(this.EventClass.prototype instanceof G))throw Error("EventClass must extend PulseEvent")}#f;#z=1000;on=(f,q,z={})=>{let B=new Y(this,f,q,z),D=f.replace(/\*+|\++/g,"placeholder");if(!this.isValidTopic(D))throw Error(`Invalid pattern: ${f}`);if(!this.listeners.has(f))this.listeners.set(f,new Set);return this.listeners.get(f)?.add(B),B};once=(f,q,z={})=>this.on(f,q,{...z,once:!0});use=(f,q)=>{let z=new $(this,f,q);return this.middlewares.push(z),z};async applyMiddlewaresToListener(f,q){let z=this.middlewares.filter((F)=>F.matches(f.topic));if(z.length===0)return q.call(f);let B=0,D=async()=>{if(B>=z.length)return q.call(f);let F=z[B++];if(!F)return q.call(f);try{return await F.callback({event:f,pulse:this,listener:q},D)}catch(J){let N=J instanceof Error?J:Error(String(J));f.error(N)}};return D()}emit=async(f,q,z={})=>{if(!this.isValidTopic(f))throw Error(`Invalid topic: ${f}`);let B=[];for(let[N,V]of this.listeners.entries())if(this.#q(f,N))B.push(...V);let D=new this.EventClass(f,q,z);if(B.length===0)return D;let F=z?.timeout||5000,J=B.map(async(N)=>{let V;try{let Q=new Promise((H,L)=>{V=setTimeout(()=>{L(Error(`Listener timed out after ${F}ms for topic: ${f}`))},F)});return await Promise.race([this.applyMiddlewaresToListener(D,N),Q])}catch(Q){let H=Q instanceof Error?Q:Error(String(Q));D.error(H)}finally{clearTimeout(V)}});return await Promise.allSettled(J),D};#B(f){if(this.#f.has(f))return this.#f.get(f)||/.*/;let q="^",z=0;while(z<f.length)if(f[z]==="*"&&f[z+1]==="*"){if(z===0){if(z+2>=f.length)q+=".*";else if(f[z+2]===":"){q+="(?:.*:)?",z+=3;continue}}else{if(q.endsWith("\\:"))q=q.slice(0,-2);if(z+2>=f.length)q+="(?::[^:]+(?::[^:]+)*)?";else if(f[z+2]===":"){q+="(?::[^:]+(?::[^:]+)*)?\\:",z+=3;continue}}z+=2}else if(f[z]==="+"&&f[z+1]==="+"){if(z===0){if(z+2>=f.length)q+="[^:]+(?::[^:]+)*";else if(f[z+2]===":"){q+="[^:]+(?::[^:]+)*\\:",z+=3;continue}}else{if(q.endsWith("\\:"))q=q.slice(0,-2);if(z+2>=f.length)q+=":[^:]+(?::[^:]+)*";else if(f[z+2]===":"){q+=":[^:]+(?::[^:]+)*\\:",z+=3;continue}}z+=2}else if(f[z]==="*")q+="[^:]+",z++;else if(f[z]===":"||f[z]===".")q+="\\"+f[z],z++;else q+=f[z],z++;q+="$";let B=new RegExp(q);if(this.#f.size>=this.#z){let D=this.#f.keys().next().value;this.#f.delete(D)}return this.#f.set(f,B),B}#q(f,q){return this.#B(q).test(f)}isValidTopic(f){return/^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*$/.test(f)}matchesPattern(f,q){return this.#q(f,q)}off(f){this.listeners.delete(f)}removeAllListeners(){this.listeners.clear()}clearPatternCache(){this.#f.clear()}}export{G as PulseEvent,K as Pulse,$ as Middleware,Y as Listener};
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@aionbuilders/pulse",
3
+ "version": "1.0.0",
4
+ "description": "A powerful, lightweight event system with pattern matching, middleware, and timeout support",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "files": [
8
+ "dist/",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "keywords": [
13
+ "events",
14
+ "pubsub",
15
+ "middleware",
16
+ "timeout",
17
+ "pattern-matching"
18
+ ],
19
+ "scripts": {
20
+ "demo": "bun --watch test/demo.js",
21
+ "clean": "rm -rf dist",
22
+ "build": "bun build src/index.js --outdir dist --target node --minify",
23
+ "generate-types": "tsc --project ./tsc/tsconfig.json",
24
+ "prepublishOnly": "npm run clean && npm run build && npm run generate-types",
25
+ "release:alpha": "npm version prerelease && npm publish --tag alpha",
26
+ "release:stable": "npm version major && npm publish",
27
+ "release:patch": "npm version patch && npm publish",
28
+ "release:minor": "npm version minor && npm publish --tag latest"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "author": "Killian Di Vincenzo",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/aionbuilders/pulse.git"
38
+ },
39
+ "type": "module",
40
+ "exports": {
41
+ ".": "./dist/index.js"
42
+ }
43
+ }