@ooneex/pub-sub 0.0.4

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 Ooneex
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,648 @@
1
+ # @ooneex/pub-sub
2
+
3
+ A publish-subscribe messaging pattern implementation for TypeScript applications with Redis support. This package provides an abstract base class for creating pub/sub handlers and a Redis client for distributed messaging across application components.
4
+
5
+ ![Bun](https://img.shields.io/badge/Bun-Compatible-orange?style=flat-square&logo=bun)
6
+ ![Deno](https://img.shields.io/badge/Deno-Compatible-blue?style=flat-square&logo=deno)
7
+ ![Node.js](https://img.shields.io/badge/Node.js-Compatible-green?style=flat-square&logo=node.js)
8
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript)
9
+ ![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)
10
+
11
+ ## Features
12
+
13
+ ✅ **Redis Integration** - Built-in Redis pub/sub client for distributed messaging
14
+
15
+ ✅ **Abstract Base Class** - Create custom pub/sub handlers with consistent interface
16
+
17
+ ✅ **Channel Management** - Subscribe, unsubscribe, and publish to channels
18
+
19
+ ✅ **Type-Safe** - Full TypeScript support with generic data types
20
+
21
+ ✅ **Container Integration** - Works seamlessly with dependency injection
22
+
23
+ ✅ **Auto Reconnection** - Redis client with automatic reconnection support
24
+
25
+ ✅ **Key Support** - Optional message keys for routing and filtering
26
+
27
+ ## Installation
28
+
29
+ ### Bun
30
+ ```bash
31
+ bun add @ooneex/pub-sub
32
+ ```
33
+
34
+ ### pnpm
35
+ ```bash
36
+ pnpm add @ooneex/pub-sub
37
+ ```
38
+
39
+ ### Yarn
40
+ ```bash
41
+ yarn add @ooneex/pub-sub
42
+ ```
43
+
44
+ ### npm
45
+ ```bash
46
+ npm install @ooneex/pub-sub
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Basic Pub/Sub Handler
52
+
53
+ ```typescript
54
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
55
+ import type { ScalarType } from '@ooneex/types';
56
+
57
+ interface NotificationData extends Record<string, ScalarType> {
58
+ userId: string;
59
+ title: string;
60
+ body: string;
61
+ }
62
+
63
+ class NotificationPubSub extends PubSub<NotificationData> {
64
+ public getChannel(): string {
65
+ return 'notifications';
66
+ }
67
+
68
+ public async handler(context: {
69
+ data: NotificationData;
70
+ channel: string;
71
+ key?: string
72
+ }): Promise<void> {
73
+ const { data, channel, key } = context;
74
+
75
+ console.log(`Received on ${channel}:`, data);
76
+
77
+ // Handle the notification
78
+ await this.sendPushNotification(data.userId, data.title, data.body);
79
+ }
80
+
81
+ private async sendPushNotification(
82
+ userId: string,
83
+ title: string,
84
+ body: string
85
+ ): Promise<void> {
86
+ // Push notification logic
87
+ }
88
+ }
89
+
90
+ // Create Redis client
91
+ const redisClient = new RedisPubSub({
92
+ connectionString: 'redis://localhost:6379'
93
+ });
94
+
95
+ // Create pub/sub handler
96
+ const notificationPubSub = new NotificationPubSub(redisClient);
97
+
98
+ // Subscribe to channel
99
+ await notificationPubSub.subscribe();
100
+
101
+ // Publish a message
102
+ await notificationPubSub.publish({
103
+ userId: 'user-123',
104
+ title: 'New Message',
105
+ body: 'You have a new message!'
106
+ });
107
+ ```
108
+
109
+ ### Using Environment Variables
110
+
111
+ ```typescript
112
+ import { RedisPubSub } from '@ooneex/pub-sub';
113
+
114
+ // Automatically uses CACHE_REDIS_URL environment variable
115
+ const client = new RedisPubSub();
116
+ ```
117
+
118
+ **Environment Variables:**
119
+ - `CACHE_REDIS_URL` - Redis connection string
120
+
121
+ ### Publishing with Keys
122
+
123
+ ```typescript
124
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
125
+
126
+ interface OrderEvent extends Record<string, ScalarType> {
127
+ orderId: string;
128
+ status: string;
129
+ amount: number;
130
+ }
131
+
132
+ class OrderPubSub extends PubSub<OrderEvent> {
133
+ public getChannel(): string {
134
+ return 'orders';
135
+ }
136
+
137
+ public async handler(context: {
138
+ data: OrderEvent;
139
+ channel: string;
140
+ key?: string
141
+ }): Promise<void> {
142
+ const { data, key } = context;
143
+
144
+ // Route based on key
145
+ switch (key) {
146
+ case 'created':
147
+ await this.handleOrderCreated(data);
148
+ break;
149
+ case 'completed':
150
+ await this.handleOrderCompleted(data);
151
+ break;
152
+ case 'cancelled':
153
+ await this.handleOrderCancelled(data);
154
+ break;
155
+ }
156
+ }
157
+ }
158
+
159
+ const orderPubSub = new OrderPubSub(new RedisPubSub());
160
+
161
+ // Subscribe
162
+ await orderPubSub.subscribe();
163
+
164
+ // Publish with keys
165
+ await orderPubSub.publish({
166
+ orderId: 'order-456',
167
+ status: 'created',
168
+ amount: 99.99
169
+ }, 'created');
170
+
171
+ await orderPubSub.publish({
172
+ orderId: 'order-456',
173
+ status: 'completed',
174
+ amount: 99.99
175
+ }, 'completed');
176
+ ```
177
+
178
+ ### Dynamic Channels
179
+
180
+ ```typescript
181
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
182
+
183
+ interface ChatMessage extends Record<string, ScalarType> {
184
+ userId: string;
185
+ message: string;
186
+ timestamp: number;
187
+ }
188
+
189
+ class ChatRoomPubSub extends PubSub<ChatMessage> {
190
+ constructor(
191
+ client: RedisPubSub,
192
+ private readonly roomId: string
193
+ ) {
194
+ super(client);
195
+ }
196
+
197
+ public getChannel(): string {
198
+ return `chat:room:${this.roomId}`;
199
+ }
200
+
201
+ public async handler(context: {
202
+ data: ChatMessage;
203
+ channel: string
204
+ }): Promise<void> {
205
+ const { data, channel } = context;
206
+ console.log(`[${channel}] ${data.userId}: ${data.message}`);
207
+ }
208
+ }
209
+
210
+ // Create room-specific pub/sub
211
+ const room1 = new ChatRoomPubSub(new RedisPubSub(), 'room-1');
212
+ const room2 = new ChatRoomPubSub(new RedisPubSub(), 'room-2');
213
+
214
+ await room1.subscribe();
215
+ await room2.subscribe();
216
+
217
+ // Publish to specific room
218
+ await room1.publish({
219
+ userId: 'user-123',
220
+ message: 'Hello Room 1!',
221
+ timestamp: Date.now()
222
+ });
223
+ ```
224
+
225
+ ### Async Channel Names
226
+
227
+ ```typescript
228
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
229
+
230
+ interface UserEvent extends Record<string, ScalarType> {
231
+ action: string;
232
+ data: string;
233
+ }
234
+
235
+ class UserEventPubSub extends PubSub<UserEvent> {
236
+ constructor(
237
+ client: RedisPubSub,
238
+ private readonly getUserId: () => Promise<string>
239
+ ) {
240
+ super(client);
241
+ }
242
+
243
+ public async getChannel(): Promise<string> {
244
+ const userId = await this.getUserId();
245
+ return `user:${userId}:events`;
246
+ }
247
+
248
+ public async handler(context: {
249
+ data: UserEvent;
250
+ channel: string
251
+ }): Promise<void> {
252
+ console.log('User event:', context.data);
253
+ }
254
+ }
255
+ ```
256
+
257
+ ## API Reference
258
+
259
+ ### Classes
260
+
261
+ #### `PubSub<Data>` (Abstract)
262
+
263
+ Abstract base class for creating pub/sub handlers.
264
+
265
+ **Type Parameter:**
266
+ - `Data` - Data type extending `Record<string, ScalarType>`
267
+
268
+ **Constructor:**
269
+ ```typescript
270
+ new PubSub(client: IPubSubClient<Data>)
271
+ ```
272
+
273
+ **Abstract Methods:**
274
+
275
+ ##### `getChannel(): string | Promise<string>`
276
+
277
+ Returns the channel name to subscribe/publish to.
278
+
279
+ **Returns:** Channel name (sync or async)
280
+
281
+ ##### `handler(context: { data: Data; channel: string; key?: string }): Promise<void> | void`
282
+
283
+ Handle incoming messages on the channel.
284
+
285
+ **Parameters:**
286
+ - `context.data` - The message data
287
+ - `context.channel` - The channel name
288
+ - `context.key` - Optional message key
289
+
290
+ **Concrete Methods:**
291
+
292
+ ##### `publish(data: Data, key?: string): Promise<void>`
293
+
294
+ Publish a message to the channel.
295
+
296
+ **Parameters:**
297
+ - `data` - The data to publish
298
+ - `key` - Optional key for message routing
299
+
300
+ ##### `subscribe(): Promise<void>`
301
+
302
+ Subscribe to the channel and start receiving messages.
303
+
304
+ ##### `unsubscribe(): Promise<void>`
305
+
306
+ Unsubscribe from the channel.
307
+
308
+ ##### `unsubscribeAll(): Promise<void>`
309
+
310
+ Unsubscribe from all channels.
311
+
312
+ ---
313
+
314
+ #### `RedisPubSub`
315
+
316
+ Redis-based pub/sub client implementation.
317
+
318
+ **Constructor:**
319
+ ```typescript
320
+ new RedisPubSub(options?: RedisPubSubOptionsType)
321
+ ```
322
+
323
+ **Parameters:**
324
+ - `options.connectionString` - Redis URL (default: `CACHE_REDIS_URL` env var)
325
+ - `options.connectionTimeout` - Connection timeout in ms (default: 10000)
326
+ - `options.idleTimeout` - Idle timeout in ms (default: 30000)
327
+ - `options.autoReconnect` - Enable auto reconnection (default: true)
328
+ - `options.maxRetries` - Maximum retry attempts (default: 3)
329
+ - `options.enableOfflineQueue` - Queue commands when offline (default: true)
330
+ - `options.enableAutoPipelining` - Enable auto pipelining (default: true)
331
+ - `options.tls` - TLS configuration (optional)
332
+
333
+ **Methods:**
334
+
335
+ ##### `publish(config: { channel: string; data: Data; key?: string }): Promise<void>`
336
+
337
+ Publish a message to a channel.
338
+
339
+ ##### `subscribe(channel: string, handler: PubSubMessageHandlerType<Data>): Promise<void>`
340
+
341
+ Subscribe to a channel with a message handler.
342
+
343
+ ##### `unsubscribe(channel: string): Promise<void>`
344
+
345
+ Unsubscribe from a specific channel.
346
+
347
+ ##### `unsubscribeAll(): Promise<void>`
348
+
349
+ Unsubscribe from all channels.
350
+
351
+ ### Types
352
+
353
+ #### `PubSubMessageHandlerType<Data>`
354
+
355
+ Handler function type for incoming messages.
356
+
357
+ ```typescript
358
+ type PubSubMessageHandlerType<Data> = (context: {
359
+ data: Data;
360
+ channel: string;
361
+ key?: string;
362
+ }) => Promise<void> | void;
363
+ ```
364
+
365
+ #### `RedisPubSubOptionsType`
366
+
367
+ Configuration options for Redis pub/sub client.
368
+
369
+ ```typescript
370
+ type RedisPubSubOptionsType = {
371
+ connectionString?: string;
372
+ connectionTimeout?: number;
373
+ idleTimeout?: number;
374
+ autoReconnect?: boolean;
375
+ maxRetries?: number;
376
+ enableOfflineQueue?: boolean;
377
+ enableAutoPipelining?: boolean;
378
+ tls?: boolean | object;
379
+ };
380
+ ```
381
+
382
+ #### `IPubSubClient<Data>`
383
+
384
+ Interface for pub/sub client implementations.
385
+
386
+ ```typescript
387
+ interface IPubSubClient<Data> {
388
+ publish: (config: { channel: string; data: Data; key?: string }) => Promise<void>;
389
+ subscribe: (channel: string, handler: PubSubMessageHandlerType<Data>) => Promise<void>;
390
+ unsubscribe: (channel: string) => Promise<void>;
391
+ unsubscribeAll: () => Promise<void>;
392
+ }
393
+ ```
394
+
395
+ #### `IPubSub<Data>`
396
+
397
+ Interface for pub/sub handler implementations.
398
+
399
+ ```typescript
400
+ interface IPubSub<Data> {
401
+ getChannel: () => Promise<string> | string;
402
+ handler: (context: { data: Data; channel: string; key?: string }) => Promise<void> | void;
403
+ publish: (data: Data, key?: string) => Promise<void> | void;
404
+ subscribe: () => Promise<void> | void;
405
+ unsubscribe: () => Promise<void> | void;
406
+ unsubscribeAll: () => Promise<void> | void;
407
+ }
408
+ ```
409
+
410
+ #### `PubSubClassType`
411
+
412
+ Type for pub/sub class constructors.
413
+
414
+ ```typescript
415
+ type PubSubClassType = new (...args: any[]) => IPubSub;
416
+ ```
417
+
418
+ ### Exceptions
419
+
420
+ #### `PubSubException`
421
+
422
+ Thrown when pub/sub operations fail.
423
+
424
+ ```typescript
425
+ import { PubSub, PubSubException, RedisPubSub } from '@ooneex/pub-sub';
426
+
427
+ try {
428
+ const client = new RedisPubSub();
429
+ await client.publish({ channel: 'test', data: { message: 'hello' } });
430
+ } catch (error) {
431
+ if (error instanceof PubSubException) {
432
+ console.error('PubSub error:', error.message);
433
+ }
434
+ }
435
+ ```
436
+
437
+ ### Decorators
438
+
439
+ #### `@decorator.pubsub()`
440
+
441
+ Decorator to register pub/sub classes with the DI container.
442
+
443
+ ```typescript
444
+ import { PubSub, decorator, RedisPubSub } from '@ooneex/pub-sub';
445
+
446
+ @decorator.pubsub()
447
+ class MyEventPubSub extends PubSub {
448
+ // Implementation
449
+ }
450
+ ```
451
+
452
+ ## Advanced Usage
453
+
454
+ ### Multiple Subscribers
455
+
456
+ ```typescript
457
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
458
+
459
+ interface EventData extends Record<string, ScalarType> {
460
+ type: string;
461
+ payload: string;
462
+ }
463
+
464
+ // Subscriber 1: Log events
465
+ class EventLoggerPubSub extends PubSub<EventData> {
466
+ public getChannel(): string {
467
+ return 'events';
468
+ }
469
+
470
+ public async handler(context: { data: EventData }): Promise<void> {
471
+ console.log('Event logged:', context.data);
472
+ }
473
+ }
474
+
475
+ // Subscriber 2: Process events
476
+ class EventProcessorPubSub extends PubSub<EventData> {
477
+ public getChannel(): string {
478
+ return 'events';
479
+ }
480
+
481
+ public async handler(context: { data: EventData }): Promise<void> {
482
+ await this.processEvent(context.data);
483
+ }
484
+ }
485
+
486
+ // Both subscribe to the same channel
487
+ const client = new RedisPubSub();
488
+ const logger = new EventLoggerPubSub(client);
489
+ const processor = new EventProcessorPubSub(client);
490
+
491
+ await logger.subscribe();
492
+ await processor.subscribe();
493
+
494
+ // Both receive this message
495
+ await logger.publish({ type: 'user.created', payload: '{"id":"123"}' });
496
+ ```
497
+
498
+ ### Error Handling
499
+
500
+ ```typescript
501
+ import { PubSub, RedisPubSub, PubSubException } from '@ooneex/pub-sub';
502
+
503
+ class ResilientPubSub extends PubSub<Record<string, ScalarType>> {
504
+ public getChannel(): string {
505
+ return 'resilient';
506
+ }
507
+
508
+ public async handler(context: { data: Record<string, ScalarType> }): Promise<void> {
509
+ try {
510
+ await this.processMessage(context.data);
511
+ } catch (error) {
512
+ console.error('Failed to process message:', error);
513
+ // Optionally republish to dead letter channel
514
+ await this.publishToDeadLetter(context.data, error);
515
+ }
516
+ }
517
+
518
+ private async publishToDeadLetter(
519
+ data: Record<string, ScalarType>,
520
+ error: unknown
521
+ ): Promise<void> {
522
+ // Dead letter queue logic
523
+ }
524
+ }
525
+ ```
526
+
527
+ ### Integration with WebSocket
528
+
529
+ ```typescript
530
+ import { Route } from '@ooneex/routing';
531
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
532
+ import type { IController, ContextType } from '@ooneex/socket';
533
+
534
+ interface ChatData extends Record<string, ScalarType> {
535
+ roomId: string;
536
+ userId: string;
537
+ message: string;
538
+ }
539
+
540
+ // Pub/Sub for distributing messages across server instances
541
+ class ChatPubSub extends PubSub<ChatData> {
542
+ public getChannel(): string {
543
+ return 'chat:messages';
544
+ }
545
+
546
+ public async handler(context: { data: ChatData }): Promise<void> {
547
+ // Broadcast to all connected WebSocket clients
548
+ await this.broadcastToRoom(context.data.roomId, context.data);
549
+ }
550
+ }
551
+
552
+ // WebSocket controller uses pub/sub for distributed messaging
553
+ @Route.socket({
554
+ name: 'api.chat.send',
555
+ path: '/ws/chat/:roomId',
556
+ description: 'Send chat message'
557
+ })
558
+ class ChatController implements IController {
559
+ private readonly chatPubSub = new ChatPubSub(new RedisPubSub());
560
+
561
+ public async index(context: ContextType): Promise<IResponse> {
562
+ const { roomId } = context.params;
563
+ const { message } = context.payload;
564
+
565
+ // Publish through Redis for distribution to all server instances
566
+ await this.chatPubSub.publish({
567
+ roomId,
568
+ userId: context.user?.id ?? 'anonymous',
569
+ message
570
+ });
571
+
572
+ return context.response.json({ sent: true });
573
+ }
574
+ }
575
+ ```
576
+
577
+ ### Pattern-Based Subscriptions
578
+
579
+ ```typescript
580
+ import { PubSub, RedisPubSub } from '@ooneex/pub-sub';
581
+
582
+ interface SystemEvent extends Record<string, ScalarType> {
583
+ service: string;
584
+ event: string;
585
+ severity: string;
586
+ }
587
+
588
+ class SystemMonitorPubSub extends PubSub<SystemEvent> {
589
+ constructor(
590
+ client: RedisPubSub,
591
+ private readonly servicePattern: string
592
+ ) {
593
+ super(client);
594
+ }
595
+
596
+ public getChannel(): string {
597
+ return `system:${this.servicePattern}:events`;
598
+ }
599
+
600
+ public async handler(context: {
601
+ data: SystemEvent;
602
+ channel: string
603
+ }): Promise<void> {
604
+ const { data, channel } = context;
605
+
606
+ if (data.severity === 'critical') {
607
+ await this.alertOncall(channel, data);
608
+ }
609
+
610
+ await this.logToMonitoring(data);
611
+ }
612
+ }
613
+
614
+ // Monitor specific services
615
+ const apiMonitor = new SystemMonitorPubSub(new RedisPubSub(), 'api');
616
+ const dbMonitor = new SystemMonitorPubSub(new RedisPubSub(), 'database');
617
+ const cacheMonitor = new SystemMonitorPubSub(new RedisPubSub(), 'cache');
618
+
619
+ await apiMonitor.subscribe();
620
+ await dbMonitor.subscribe();
621
+ await cacheMonitor.subscribe();
622
+ ```
623
+
624
+ ## License
625
+
626
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
627
+
628
+ ## Contributing
629
+
630
+ Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
631
+
632
+ ### Development Setup
633
+
634
+ 1. Clone the repository
635
+ 2. Install dependencies: `bun install`
636
+ 3. Run tests: `bun run test`
637
+ 4. Build the project: `bun run build`
638
+
639
+ ### Guidelines
640
+
641
+ - Write tests for new features
642
+ - Follow the existing code style
643
+ - Update documentation for API changes
644
+ - Ensure all tests pass before submitting PR
645
+
646
+ ---
647
+
648
+ Made with ❤️ by the Ooneex team
@@ -0,0 +1,78 @@
1
+ import { EContainerScope } from "@ooneex/container";
2
+ import { ScalarType } from "@ooneex/types";
3
+ type PubSubMessageHandlerType<Data extends Record<string, ScalarType> = Record<string, ScalarType>> = (context: {
4
+ data: Data;
5
+ channel: string;
6
+ key?: string;
7
+ }) => Promise<void> | void;
8
+ type RedisPubSubOptionsType = {
9
+ connectionString?: string;
10
+ connectionTimeout?: number;
11
+ idleTimeout?: number;
12
+ autoReconnect?: boolean;
13
+ maxRetries?: number;
14
+ enableOfflineQueue?: boolean;
15
+ enableAutoPipelining?: boolean;
16
+ tls?: boolean | object;
17
+ };
18
+ interface IPubSubClient<Data extends Record<string, ScalarType> = Record<string, ScalarType>> {
19
+ publish: (config: {
20
+ channel: string;
21
+ data: Data;
22
+ key?: string;
23
+ }) => Promise<void>;
24
+ subscribe: (channel: string, handler: PubSubMessageHandlerType<Data>) => Promise<void>;
25
+ unsubscribe: (channel: string) => Promise<void>;
26
+ unsubscribeAll: () => Promise<void>;
27
+ }
28
+ interface IPubSub<Data extends Record<string, ScalarType> = Record<string, ScalarType>> {
29
+ getChannel: () => Promise<string> | string;
30
+ handler: (context: {
31
+ data: Data;
32
+ channel: string;
33
+ key?: string;
34
+ }) => Promise<void> | void;
35
+ publish: (data: Data, key?: string) => Promise<void> | void;
36
+ subscribe: () => Promise<void> | void;
37
+ unsubscribe: () => Promise<void> | void;
38
+ unsubscribeAll: () => Promise<void> | void;
39
+ }
40
+ type PubSubClassType = new (...args: any[]) => IPubSub;
41
+ declare const decorator: {
42
+ pubSub: (scope?: EContainerScope) => (target: PubSubClassType) => void;
43
+ };
44
+ import { ScalarType as ScalarType2 } from "@ooneex/types";
45
+ declare abstract class PubSub<Data extends Record<string, ScalarType2> = Record<string, ScalarType2>> implements IPubSub<Data> {
46
+ protected readonly client: IPubSubClient<Data>;
47
+ constructor(client: IPubSubClient<Data>);
48
+ abstract handler(context: {
49
+ data: Data;
50
+ channel: string;
51
+ key?: string;
52
+ }): Promise<void> | void;
53
+ publish(data: Data, key?: string): Promise<void>;
54
+ subscribe(): Promise<void>;
55
+ unsubscribe(): Promise<void>;
56
+ unsubscribeAll(): Promise<void>;
57
+ }
58
+ import { Exception } from "@ooneex/exception";
59
+ declare class PubSubException extends Exception {
60
+ constructor(message: string, data?: Record<string, unknown>);
61
+ }
62
+ import { ScalarType as ScalarType3 } from "@ooneex/types";
63
+ declare class RedisPubSubClient<Data extends Record<string, ScalarType3>> implements IPubSubClient<Data> {
64
+ private client;
65
+ private subscriber;
66
+ constructor(options?: RedisPubSubOptionsType);
67
+ private connect;
68
+ private connectSubscriber;
69
+ publish(config: {
70
+ channel: string;
71
+ data: Data;
72
+ key?: string;
73
+ }): Promise<void>;
74
+ subscribe(channel: string, handler: PubSubMessageHandlerType<Data>): Promise<void>;
75
+ unsubscribe(channel: string): Promise<void>;
76
+ unsubscribeAll(): Promise<void>;
77
+ }
78
+ export { decorator, RedisPubSubOptionsType, RedisPubSubClient as RedisPubSub, PubSubMessageHandlerType, PubSubException, PubSubClassType, PubSub, IPubSubClient, IPubSub };
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // @bun
2
+ var b=function(e,t,i,n){var o=arguments.length,s=o<3?t:n===null?n=Object.getOwnPropertyDescriptor(t,i):n,a;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")s=Reflect.decorate(e,t,i,n);else for(var u=e.length-1;u>=0;u--)if(a=e[u])s=(o<3?a(s):o>3?a(t,i,s):a(t,i))||s;return o>3&&s&&Object.defineProperty(t,i,s),s};var l=(e,t)=>{if(typeof Reflect==="object"&&typeof Reflect.metadata==="function")return Reflect.metadata(e,t)};import{container as p,EContainerScope as d}from"@ooneex/container";var m={pubSub:(e=d.Singleton)=>{return(t)=>{p.add(t,e),p.get(t).subscribe()}}};class h{client;constructor(e){this.client=e}async publish(e,t){await this.client.publish({channel:await this.getChannel(),data:e,...t!==void 0&&{key:t}})}async subscribe(){await this.client.subscribe(await this.getChannel(),this.handler.bind(this))}async unsubscribe(){await this.client.unsubscribe(await this.getChannel())}async unsubscribeAll(){await this.client.unsubscribeAll()}}import{Exception as S}from"@ooneex/exception";import{HttpStatus as y}from"@ooneex/http-status";class r extends S{constructor(e,t={}){super(e,{status:y.Code.InternalServerError,data:t});this.name="PubSubException"}}import{injectable as P}from"@ooneex/container";class c{client;subscriber=null;constructor(e={}){let t=e.connectionString||Bun.env.PUBSUB_REDIS_URL;if(!t)throw new r("Redis connection string is required. Please provide a connection string either through the constructor options or set the PUBSUB_REDIS_URL environment variable.");let{connectionString:i,...n}=e,s={...{connectionTimeout:1e4,idleTimeout:30000,autoReconnect:!0,maxRetries:3,enableOfflineQueue:!0,enableAutoPipelining:!0},...n};this.client=new Bun.RedisClient(t,s)}async connect(){if(!this.client.connected)await this.client.connect()}async connectSubscriber(){if(!this.subscriber)this.subscriber=await this.client.duplicate();if(!this.subscriber.connected)await this.subscriber.connect()}async publish(e){try{await this.connect();let t=JSON.stringify(e.data);await this.client.publish(e.channel,t)}catch(t){throw new r(`Failed to publish message to channel "${e.channel}": ${t}`)}finally{this.client.close()}}async subscribe(e,t){try{await this.connectSubscriber(),await this.subscriber?.subscribe(e,(i,n)=>{let o=JSON.parse(i);t({data:o,channel:n})})}catch(i){throw new r(`Failed to subscribe to channel "${e}": ${i}`)}}async unsubscribe(e){try{if(!this.subscriber)return;await this.subscriber.unsubscribe(e)}catch(t){throw new r(`Failed to unsubscribe from channel "${e}": ${t}`)}}async unsubscribeAll(){try{if(!this.subscriber)return;await this.subscriber.unsubscribe()}catch(e){throw new r(`Failed to unsubscribe from all channels: ${e}`)}}}c=b([P(),l("design:paramtypes",[typeof RedisPubSubOptionsType==="undefined"?Object:RedisPubSubOptionsType])],c);export{m as decorator,c as RedisPubSub,r as PubSubException,h as PubSub};
3
+
4
+ //# debugId=71EA4C698ED7AF9C64756E2164756E21
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["src/decorators.ts", "src/PubSub.ts", "src/PubSubException.ts", "src/RedisPubSubClient.ts"],
4
+ "sourcesContent": [
5
+ "import { container, EContainerScope } from \"@ooneex/container\";\nimport type { IPubSub, PubSubClassType } from \"./types\";\n\nexport const decorator = {\n pubSub: (scope: EContainerScope = EContainerScope.Singleton) => {\n return (target: PubSubClassType): void => {\n container.add(target, scope);\n const pubsub = container.get<IPubSub>(target);\n pubsub.subscribe();\n };\n },\n};\n",
6
+ "import type { ScalarType } from \"@ooneex/types\";\nimport type { IPubSub, IPubSubClient } from \"./types\";\n\nexport abstract class PubSub<Data extends Record<string, ScalarType> = Record<string, ScalarType>>\n implements IPubSub<Data>\n{\n constructor(protected readonly client: IPubSubClient<Data>) {}\n\n public abstract getChannel(): string | Promise<string>;\n public abstract handler(context: { data: Data; channel: string; key?: string }): Promise<void> | void;\n\n public async publish(data: Data, key?: string): Promise<void> {\n await this.client.publish({\n channel: await this.getChannel(),\n data,\n ...(key !== undefined && { key }),\n });\n }\n\n public async subscribe(): Promise<void> {\n await this.client.subscribe(await this.getChannel(), this.handler.bind(this));\n }\n\n public async unsubscribe(): Promise<void> {\n await this.client.unsubscribe(await this.getChannel());\n }\n\n public async unsubscribeAll(): Promise<void> {\n await this.client.unsubscribeAll();\n }\n}\n",
7
+ "import { Exception } from \"@ooneex/exception\";\nimport { HttpStatus } from \"@ooneex/http-status\";\n\nexport class PubSubException extends Exception {\n constructor(message: string, data: Record<string, unknown> = {}) {\n super(message, {\n status: HttpStatus.Code.InternalServerError,\n data,\n });\n this.name = \"PubSubException\";\n }\n}\n",
8
+ "import { injectable } from \"@ooneex/container\";\nimport type { ScalarType } from \"@ooneex/types\";\nimport { PubSubException } from \"./PubSubException\";\nimport type { IPubSubClient, PubSubMessageHandlerType, RedisPubSubOptionsType } from \"./types\";\n\n@injectable()\nexport class RedisPubSubClient<Data extends Record<string, ScalarType>> implements IPubSubClient<Data> {\n private client: Bun.RedisClient;\n private subscriber: Bun.RedisClient | null = null;\n\n constructor(options: RedisPubSubOptionsType = {}) {\n const connectionString = options.connectionString || Bun.env.PUBSUB_REDIS_URL;\n\n if (!connectionString) {\n throw new PubSubException(\n \"Redis connection string is required. Please provide a connection string either through the constructor options or set the PUBSUB_REDIS_URL environment variable.\",\n );\n }\n\n const { connectionString: _, ...userOptions } = options;\n\n const defaultOptions = {\n connectionTimeout: 10_000,\n idleTimeout: 30_000,\n autoReconnect: true,\n maxRetries: 3,\n enableOfflineQueue: true,\n enableAutoPipelining: true,\n };\n\n const clientOptions = { ...defaultOptions, ...userOptions };\n\n this.client = new Bun.RedisClient(connectionString, clientOptions);\n }\n\n private async connect(): Promise<void> {\n if (!this.client.connected) {\n await this.client.connect();\n }\n }\n\n private async connectSubscriber(): Promise<void> {\n if (!this.subscriber) {\n this.subscriber = await this.client.duplicate();\n }\n\n if (!this.subscriber.connected) {\n await this.subscriber.connect();\n }\n }\n\n public async publish(config: { channel: string; data: Data; key?: string }): Promise<void> {\n try {\n await this.connect();\n const message = JSON.stringify(config.data);\n await this.client.publish(config.channel, message);\n } catch (error) {\n throw new PubSubException(`Failed to publish message to channel \"${config.channel}\": ${error}`);\n } finally {\n this.client.close();\n }\n }\n\n public async subscribe(channel: string, handler: PubSubMessageHandlerType<Data>): Promise<void> {\n try {\n await this.connectSubscriber();\n await this.subscriber?.subscribe(channel, (message: string, ch: string) => {\n const data = JSON.parse(message) as Data;\n handler({ data, channel: ch });\n });\n } catch (error) {\n throw new PubSubException(`Failed to subscribe to channel \"${channel}\": ${error}`);\n }\n }\n\n public async unsubscribe(channel: string): Promise<void> {\n try {\n if (!this.subscriber) {\n return;\n }\n\n await this.subscriber.unsubscribe(channel);\n } catch (error) {\n throw new PubSubException(`Failed to unsubscribe from channel \"${channel}\": ${error}`);\n }\n }\n\n public async unsubscribeAll(): Promise<void> {\n try {\n if (!this.subscriber) {\n return;\n }\n\n await this.subscriber.unsubscribe();\n } catch (error) {\n throw new PubSubException(`Failed to unsubscribe from all channels: ${error}`);\n }\n }\n}\n"
9
+ ],
10
+ "mappings": ";ybAAA,oBAAS,qBAAW,0BAGb,IAAM,EAAY,CACvB,OAAQ,CAAC,EAAyB,EAAgB,YAAc,CAC9D,MAAO,CAAC,IAAkC,CACxC,EAAU,IAAI,EAAQ,CAAK,EACZ,EAAU,IAAa,CAAM,EACrC,UAAU,GAGvB,ECRO,MAAe,CAEtB,CACiC,OAA/B,WAAW,CAAoB,EAA6B,CAA7B,mBAKlB,QAAO,CAAC,EAAY,EAA6B,CAC5D,MAAM,KAAK,OAAO,QAAQ,CACxB,QAAS,MAAM,KAAK,WAAW,EAC/B,UACI,IAAQ,QAAa,CAAE,KAAI,CACjC,CAAC,OAGU,UAAS,EAAkB,CACtC,MAAM,KAAK,OAAO,UAAU,MAAM,KAAK,WAAW,EAAG,KAAK,QAAQ,KAAK,IAAI,CAAC,OAGjE,YAAW,EAAkB,CACxC,MAAM,KAAK,OAAO,YAAY,MAAM,KAAK,WAAW,CAAC,OAG1C,eAAc,EAAkB,CAC3C,MAAM,KAAK,OAAO,eAAe,EAErC,CC9BA,oBAAS,0BACT,qBAAS,4BAEF,MAAM,UAAwB,CAAU,CAC7C,WAAW,CAAC,EAAiB,EAAgC,CAAC,EAAG,CAC/D,MAAM,EAAS,CACb,OAAQ,EAAW,KAAK,oBACxB,MACF,CAAC,EACD,KAAK,KAAO,kBAEhB,CCXA,qBAAS,0BAMF,MAAM,CAA0F,CAC7F,OACA,WAAqC,KAE7C,WAAW,CAAC,EAAkC,CAAC,EAAG,CAChD,IAAM,EAAmB,EAAQ,kBAAoB,IAAI,IAAI,iBAE7D,GAAI,CAAC,EACH,MAAM,IAAI,EACR,kKACF,EAGF,IAAQ,iBAAkB,KAAM,GAAgB,EAW1C,EAAgB,IATC,CACrB,kBAAmB,IACnB,YAAa,MACb,cAAe,GACf,WAAY,EACZ,mBAAoB,GACpB,qBAAsB,EACxB,KAE8C,CAAY,EAE1D,KAAK,OAAS,IAAI,IAAI,YAAY,EAAkB,CAAa,OAGrD,QAAO,EAAkB,CACrC,GAAI,CAAC,KAAK,OAAO,UACf,MAAM,KAAK,OAAO,QAAQ,OAIhB,kBAAiB,EAAkB,CAC/C,GAAI,CAAC,KAAK,WACR,KAAK,WAAa,MAAM,KAAK,OAAO,UAAU,EAGhD,GAAI,CAAC,KAAK,WAAW,UACnB,MAAM,KAAK,WAAW,QAAQ,OAIrB,QAAO,CAAC,EAAsE,CACzF,GAAI,CACF,MAAM,KAAK,QAAQ,EACnB,IAAM,EAAU,KAAK,UAAU,EAAO,IAAI,EAC1C,MAAM,KAAK,OAAO,QAAQ,EAAO,QAAS,CAAO,EACjD,MAAO,EAAO,CACd,MAAM,IAAI,EAAgB,yCAAyC,EAAO,aAAa,GAAO,SAC9F,CACA,KAAK,OAAO,MAAM,QAIT,UAAS,CAAC,EAAiB,EAAwD,CAC9F,GAAI,CACF,MAAM,KAAK,kBAAkB,EAC7B,MAAM,KAAK,YAAY,UAAU,EAAS,CAAC,EAAiB,IAAe,CACzE,IAAM,EAAO,KAAK,MAAM,CAAO,EAC/B,EAAQ,CAAE,OAAM,QAAS,CAAG,CAAC,EAC9B,EACD,MAAO,EAAO,CACd,MAAM,IAAI,EAAgB,mCAAmC,OAAa,GAAO,QAIxE,YAAW,CAAC,EAAgC,CACvD,GAAI,CACF,GAAI,CAAC,KAAK,WACR,OAGF,MAAM,KAAK,WAAW,YAAY,CAAO,EACzC,MAAO,EAAO,CACd,MAAM,IAAI,EAAgB,uCAAuC,OAAa,GAAO,QAI5E,eAAc,EAAkB,CAC3C,GAAI,CACF,GAAI,CAAC,KAAK,WACR,OAGF,MAAM,KAAK,WAAW,YAAY,EAClC,MAAO,EAAO,CACd,MAAM,IAAI,EAAgB,4CAA4C,GAAO,GAGnF,CA5Fa,EAAN,GADN,EAAW,EACL,oGAAM",
11
+ "debugId": "71EA4C698ED7AF9C64756E2164756E21",
12
+ "names": []
13
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@ooneex/pub-sub",
3
+ "description": "Publish-subscribe messaging pattern implementation for event-driven communication between application components",
4
+ "version": "0.0.4",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "LICENSE",
9
+ "README.md",
10
+ "package.json"
11
+ ],
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "license": "MIT",
24
+ "scripts": {
25
+ "build": "bunup",
26
+ "lint": "tsgo --noEmit && bunx biome lint",
27
+ "publish": "bun publish --access public || true",
28
+ "test": "bun test tests"
29
+ },
30
+ "dependencies": {
31
+ "@ooneex/container": "0.0.2",
32
+ "@ooneex/exception": "0.0.1",
33
+ "@ooneex/http-status": "0.0.1"
34
+ },
35
+ "devDependencies": {
36
+ "@ooneex/types": "0.0.1"
37
+ },
38
+ "keywords": [
39
+ "bun",
40
+ "events",
41
+ "messaging",
42
+ "ooneex",
43
+ "pub-sub",
44
+ "pubsub",
45
+ "typescript"
46
+ ]
47
+ }