@ooneex/socket 0.0.1 → 0.3.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/README.md CHANGED
@@ -1 +1,546 @@
1
- # @ooneex/router
1
+ # @ooneex/socket
2
+
3
+ A WebSocket server and client implementation for real-time bidirectional communication in TypeScript applications. This package provides types and utilities for creating WebSocket controllers with pub/sub capabilities, channel management, and seamless integration with the Ooneex framework.
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
+ ✅ **WebSocket Controllers** - Define socket endpoints using decorators
14
+
15
+ ✅ **Channel Management** - Subscribe, publish, and manage WebSocket channels
16
+
17
+ ✅ **Pub/Sub Support** - Built-in publish/subscribe pattern for real-time messaging
18
+
19
+ ✅ **Type-Safe** - Full TypeScript support with proper type definitions
20
+
21
+ ✅ **Framework Integration** - Works seamlessly with Ooneex routing and controllers
22
+
23
+ ✅ **Bidirectional Communication** - Send and receive messages in real-time
24
+
25
+ ✅ **Client Library** - Separate client export for frontend applications
26
+
27
+ ## Installation
28
+
29
+ ### Bun
30
+ ```bash
31
+ bun add @ooneex/socket
32
+ ```
33
+
34
+ ### pnpm
35
+ ```bash
36
+ pnpm add @ooneex/socket
37
+ ```
38
+
39
+ ### Yarn
40
+ ```bash
41
+ yarn add @ooneex/socket
42
+ ```
43
+
44
+ ### npm
45
+ ```bash
46
+ npm install @ooneex/socket
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Basic WebSocket Controller
52
+
53
+ ```typescript
54
+ import { Route } from '@ooneex/routing';
55
+ import type { IController, ContextType } from '@ooneex/socket';
56
+ import type { IResponse } from '@ooneex/http-response';
57
+
58
+ @Route.socket({
59
+ name: 'api.chat.connect',
60
+ path: '/ws/chat',
61
+ description: 'Connect to chat WebSocket'
62
+ })
63
+ class ChatController implements IController {
64
+ public async index(context: ContextType): Promise<IResponse> {
65
+ // Subscribe to the channel
66
+ await context.channel.subscribe();
67
+
68
+ return context.response.json({
69
+ connected: true,
70
+ message: 'Welcome to the chat!'
71
+ });
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### WebSocket with Room Support
77
+
78
+ ```typescript
79
+ import { Route } from '@ooneex/routing';
80
+ import { type } from '@ooneex/validation';
81
+ import type { IController, ContextType } from '@ooneex/socket';
82
+
83
+ @Route.socket({
84
+ name: 'api.rooms.join',
85
+ path: '/ws/rooms/:roomId',
86
+ description: 'Join a specific room',
87
+ params: {
88
+ roomId: type('string')
89
+ }
90
+ })
91
+ class RoomController implements IController {
92
+ public async index(context: ContextType): Promise<IResponse> {
93
+ const { roomId } = context.params;
94
+
95
+ // Subscribe to the room channel
96
+ await context.channel.subscribe();
97
+
98
+ // Notify other users in the room
99
+ await context.channel.publish(
100
+ context.response.json({
101
+ event: 'user_joined',
102
+ roomId,
103
+ userId: context.user?.id
104
+ })
105
+ );
106
+
107
+ return context.response.json({
108
+ joined: true,
109
+ roomId
110
+ });
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Sending Messages
116
+
117
+ ```typescript
118
+ import { Route } from '@ooneex/routing';
119
+ import type { IController, ContextType } from '@ooneex/socket';
120
+
121
+ @Route.socket({
122
+ name: 'api.messages.send',
123
+ path: '/ws/messages',
124
+ description: 'Send a message'
125
+ })
126
+ class MessageController implements IController {
127
+ public async index(context: ContextType): Promise<IResponse> {
128
+ const { message, recipientId } = context.payload;
129
+
130
+ // Send response back to the sender
131
+ await context.channel.send(
132
+ context.response.json({
133
+ event: 'message_sent',
134
+ message,
135
+ timestamp: new Date().toISOString()
136
+ })
137
+ );
138
+
139
+ return context.response.json({ success: true });
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Broadcasting to Channel
145
+
146
+ ```typescript
147
+ import { Route } from '@ooneex/routing';
148
+ import type { IController, ContextType } from '@ooneex/socket';
149
+
150
+ @Route.socket({
151
+ name: 'api.notifications.broadcast',
152
+ path: '/ws/notifications',
153
+ description: 'Broadcast notifications'
154
+ })
155
+ class NotificationController implements IController {
156
+ public async index(context: ContextType): Promise<IResponse> {
157
+ const { title, body } = context.payload;
158
+
159
+ // Subscribe first
160
+ await context.channel.subscribe();
161
+
162
+ // Publish to all subscribers
163
+ await context.channel.publish(
164
+ context.response.json({
165
+ event: 'notification',
166
+ title,
167
+ body,
168
+ timestamp: new Date().toISOString()
169
+ })
170
+ );
171
+
172
+ return context.response.json({ broadcasted: true });
173
+ }
174
+ }
175
+ ```
176
+
177
+ ### Managing Subscriptions
178
+
179
+ ```typescript
180
+ import { Route } from '@ooneex/routing';
181
+ import type { IController, ContextType } from '@ooneex/socket';
182
+
183
+ @Route.socket({
184
+ name: 'api.presence.toggle',
185
+ path: '/ws/presence',
186
+ description: 'Toggle presence subscription'
187
+ })
188
+ class PresenceController implements IController {
189
+ public async index(context: ContextType): Promise<IResponse> {
190
+ const { channel } = context;
191
+
192
+ // Check subscription status
193
+ if (channel.isSubscribed()) {
194
+ // Unsubscribe from channel
195
+ await channel.unsubscribe();
196
+
197
+ return context.response.json({
198
+ subscribed: false,
199
+ message: 'Unsubscribed from presence updates'
200
+ });
201
+ }
202
+
203
+ // Subscribe to channel
204
+ await channel.subscribe();
205
+
206
+ return context.response.json({
207
+ subscribed: true,
208
+ message: 'Subscribed to presence updates'
209
+ });
210
+ }
211
+ }
212
+ ```
213
+
214
+ ### Closing Connections
215
+
216
+ ```typescript
217
+ import { Route } from '@ooneex/routing';
218
+ import type { IController, ContextType } from '@ooneex/socket';
219
+
220
+ @Route.socket({
221
+ name: 'api.connection.close',
222
+ path: '/ws/disconnect',
223
+ description: 'Close WebSocket connection'
224
+ })
225
+ class DisconnectController implements IController {
226
+ public async index(context: ContextType): Promise<IResponse> {
227
+ const { channel } = context;
228
+
229
+ // Clean up subscription
230
+ if (channel.isSubscribed()) {
231
+ await channel.unsubscribe();
232
+ }
233
+
234
+ // Close the connection with a code and reason
235
+ channel.close(1000, 'User disconnected');
236
+
237
+ return context.response.json({ disconnected: true });
238
+ }
239
+ }
240
+ ```
241
+
242
+ ## API Reference
243
+
244
+ ### Interfaces
245
+
246
+ #### `IController<T>`
247
+
248
+ Interface for WebSocket controllers.
249
+
250
+ ```typescript
251
+ interface IController<T extends ContextConfigType = ContextConfigType> {
252
+ index: (context: ContextType<T>) => Promise<IResponse<T["response"]>> | IResponse<T["response"]>;
253
+ }
254
+ ```
255
+
256
+ ### Types
257
+
258
+ #### `ContextType<T>`
259
+
260
+ Extended context type for WebSocket controllers with channel management.
261
+
262
+ ```typescript
263
+ type ContextType<T extends ContextConfigType = ContextConfigType> = ControllerContextType<T> & {
264
+ channel: {
265
+ send: (response: IResponse<T["response"]>) => Promise<void>;
266
+ close(code?: number, reason?: string): void;
267
+ subscribe: () => Promise<void>;
268
+ isSubscribed(): boolean;
269
+ unsubscribe: () => Promise<void>;
270
+ publish: (response: IResponse<T["response"]>) => Promise<void>;
271
+ };
272
+ };
273
+ ```
274
+
275
+ **Channel Methods:**
276
+
277
+ | Method | Returns | Description |
278
+ |--------|---------|-------------|
279
+ | `send(response)` | `Promise<void>` | Send a message to the connected client |
280
+ | `close(code?, reason?)` | `void` | Close the WebSocket connection |
281
+ | `subscribe()` | `Promise<void>` | Subscribe to the channel |
282
+ | `isSubscribed()` | `boolean` | Check if currently subscribed |
283
+ | `unsubscribe()` | `Promise<void>` | Unsubscribe from the channel |
284
+ | `publish(response)` | `Promise<void>` | Publish message to all channel subscribers |
285
+
286
+ #### `ContextConfigType`
287
+
288
+ Configuration type for socket context.
289
+
290
+ ```typescript
291
+ type ContextConfigType = {
292
+ response: Record<string, unknown>;
293
+ } & RequestConfigType;
294
+ ```
295
+
296
+ #### `ControllerClassType`
297
+
298
+ Type for socket controller class constructors.
299
+
300
+ ```typescript
301
+ type ControllerClassType = new (...args: any[]) => IController;
302
+ ```
303
+
304
+ ## Client Usage
305
+
306
+ Import the client for frontend applications:
307
+
308
+ ```typescript
309
+ import { /* client exports */ } from '@ooneex/socket/client';
310
+ ```
311
+
312
+ ### Basic Client Connection
313
+
314
+ ```typescript
315
+ // Connect to WebSocket endpoint
316
+ const ws = new WebSocket('wss://api.example.com/ws/chat');
317
+
318
+ ws.onopen = () => {
319
+ console.log('Connected to WebSocket');
320
+
321
+ // Send a message
322
+ ws.send(JSON.stringify({
323
+ action: 'subscribe',
324
+ channel: 'general'
325
+ }));
326
+ };
327
+
328
+ ws.onmessage = (event) => {
329
+ const data = JSON.parse(event.data);
330
+ console.log('Received:', data);
331
+ };
332
+
333
+ ws.onclose = (event) => {
334
+ console.log('Disconnected:', event.code, event.reason);
335
+ };
336
+
337
+ ws.onerror = (error) => {
338
+ console.error('WebSocket error:', error);
339
+ };
340
+ ```
341
+
342
+ ## Advanced Usage
343
+
344
+ ### Typed Socket Controller
345
+
346
+ ```typescript
347
+ import { Route } from '@ooneex/routing';
348
+ import { type } from '@ooneex/validation';
349
+ import type { IController, ContextType, ContextConfigType } from '@ooneex/socket';
350
+
351
+ interface ChatConfig extends ContextConfigType {
352
+ params: { roomId: string };
353
+ payload: { message: string; type: 'text' | 'image' };
354
+ queries: Record<string, never>;
355
+ response: {
356
+ event: string;
357
+ data: unknown;
358
+ timestamp: string;
359
+ };
360
+ }
361
+
362
+ @Route.socket({
363
+ name: 'api.chat.message',
364
+ path: '/ws/chat/:roomId',
365
+ description: 'Send chat message',
366
+ params: { roomId: type('string') },
367
+ payload: type({
368
+ message: 'string',
369
+ type: "'text' | 'image'"
370
+ })
371
+ })
372
+ class ChatMessageController implements IController<ChatConfig> {
373
+ public async index(context: ContextType<ChatConfig>): Promise<IResponse<ChatConfig['response']>> {
374
+ const { roomId } = context.params;
375
+ const { message, type } = context.payload;
376
+
377
+ // TypeScript knows the exact types
378
+ await context.channel.publish(
379
+ context.response.json({
380
+ event: 'new_message',
381
+ data: { roomId, message, type, userId: context.user?.id },
382
+ timestamp: new Date().toISOString()
383
+ })
384
+ );
385
+
386
+ return context.response.json({
387
+ event: 'message_sent',
388
+ data: { messageId: crypto.randomUUID() },
389
+ timestamp: new Date().toISOString()
390
+ });
391
+ }
392
+ }
393
+ ```
394
+
395
+ ### Real-Time Notifications
396
+
397
+ ```typescript
398
+ import { Route } from '@ooneex/routing';
399
+ import type { IController, ContextType } from '@ooneex/socket';
400
+
401
+ @Route.socket({
402
+ name: 'api.notifications.stream',
403
+ path: '/ws/notifications/:userId',
404
+ description: 'Stream user notifications'
405
+ })
406
+ class NotificationStreamController implements IController {
407
+ public async index(context: ContextType): Promise<IResponse> {
408
+ const { userId } = context.params;
409
+
410
+ // Only allow users to subscribe to their own notifications
411
+ if (context.user?.id !== userId) {
412
+ return context.response.exception('Unauthorized', { status: 403 });
413
+ }
414
+
415
+ await context.channel.subscribe();
416
+
417
+ // Send initial notification count
418
+ const unreadCount = await this.notificationService.getUnreadCount(userId);
419
+
420
+ return context.response.json({
421
+ subscribed: true,
422
+ unreadCount
423
+ });
424
+ }
425
+ }
426
+ ```
427
+
428
+ ### Live Collaboration
429
+
430
+ ```typescript
431
+ import { Route } from '@ooneex/routing';
432
+ import type { IController, ContextType } from '@ooneex/socket';
433
+
434
+ @Route.socket({
435
+ name: 'api.docs.collaborate',
436
+ path: '/ws/docs/:documentId',
437
+ description: 'Real-time document collaboration'
438
+ })
439
+ class DocumentCollaborationController implements IController {
440
+ public async index(context: ContextType): Promise<IResponse> {
441
+ const { documentId } = context.params;
442
+ const { action, content, cursor } = context.payload;
443
+
444
+ await context.channel.subscribe();
445
+
446
+ switch (action) {
447
+ case 'edit':
448
+ await context.channel.publish(
449
+ context.response.json({
450
+ event: 'content_update',
451
+ documentId,
452
+ userId: context.user?.id,
453
+ content
454
+ })
455
+ );
456
+ break;
457
+
458
+ case 'cursor':
459
+ await context.channel.publish(
460
+ context.response.json({
461
+ event: 'cursor_move',
462
+ documentId,
463
+ userId: context.user?.id,
464
+ cursor
465
+ })
466
+ );
467
+ break;
468
+ }
469
+
470
+ return context.response.json({ success: true });
471
+ }
472
+ }
473
+ ```
474
+
475
+ ### WebSocket with Authentication
476
+
477
+ ```typescript
478
+ import { Route } from '@ooneex/routing';
479
+ import { ERole } from '@ooneex/role';
480
+ import type { IController, ContextType } from '@ooneex/socket';
481
+
482
+ @Route.socket({
483
+ name: 'api.admin.dashboard',
484
+ path: '/ws/admin/dashboard',
485
+ description: 'Admin real-time dashboard',
486
+ roles: [ERole.ADMIN, ERole.SUPER_ADMIN]
487
+ })
488
+ class AdminDashboardController implements IController {
489
+ public async index(context: ContextType): Promise<IResponse> {
490
+ // User is guaranteed to have ADMIN or SUPER_ADMIN role
491
+ await context.channel.subscribe();
492
+
493
+ // Stream real-time metrics
494
+ return context.response.json({
495
+ connected: true,
496
+ role: context.user?.role,
497
+ subscribedAt: new Date().toISOString()
498
+ });
499
+ }
500
+ }
501
+ ```
502
+
503
+ ## WebSocket Close Codes
504
+
505
+ Common WebSocket close codes you can use:
506
+
507
+ | Code | Name | Description |
508
+ |------|------|-------------|
509
+ | `1000` | Normal Closure | Normal connection closure |
510
+ | `1001` | Going Away | Endpoint is going away |
511
+ | `1002` | Protocol Error | Protocol error |
512
+ | `1003` | Unsupported Data | Received unsupported data type |
513
+ | `1008` | Policy Violation | Message violates policy |
514
+ | `1011` | Internal Error | Server encountered an error |
515
+
516
+ ```typescript
517
+ // Close with specific code and reason
518
+ context.channel.close(1000, 'Session ended');
519
+ context.channel.close(1008, 'Message too large');
520
+ ```
521
+
522
+ ## License
523
+
524
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
525
+
526
+ ## Contributing
527
+
528
+ 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.
529
+
530
+ ### Development Setup
531
+
532
+ 1. Clone the repository
533
+ 2. Install dependencies: `bun install`
534
+ 3. Run tests: `bun run test`
535
+ 4. Build the project: `bun run build`
536
+
537
+ ### Guidelines
538
+
539
+ - Write tests for new features
540
+ - Follow the existing code style
541
+ - Update documentation for API changes
542
+ - Ensure all tests pass before submitting PR
543
+
544
+ ---
545
+
546
+ Made with ❤️ by the Ooneex team
@@ -3,7 +3,7 @@ import { ResponseDataType } from "@ooneex/http-response";
3
3
  import { LocaleInfoType } from "@ooneex/translation";
4
4
  type RequestDataType = {
5
5
  payload?: Record<string, unknown>;
6
- queries?: Record<string, string | number>;
6
+ queries?: Record<string, boolean | number | bigint | string>;
7
7
  language?: LocaleInfoType;
8
8
  };
9
9
  interface ISocket<
@@ -15,7 +15,7 @@ interface ISocket<
15
15
  onMessage: (handler: (response: ResponseDataType<Response>) => void) => void;
16
16
  onOpen: (handler: (event: Event) => void) => void;
17
17
  onClose: (handler: (event: CloseEvent) => void) => void;
18
- onError: (handler: (event: Event) => void) => void;
18
+ onError: (handler: (event: Event, response?: ResponseDataType<Response>) => void) => void;
19
19
  }
20
20
  declare class Socket<
21
21
  SendData extends RequestDataType = RequestDataType,
@@ -35,7 +35,7 @@ declare class Socket<
35
35
  onMessage(handler: (response: ResponseDataType2<Response>) => void): void;
36
36
  onOpen(handler: (event: Event) => void): void;
37
37
  onClose(handler: (event: CloseEvent) => void): void;
38
- onError(handler: (event: Event) => void): void;
38
+ onError(handler: (event: Event, response?: ResponseDataType2<Response>) => void): void;
39
39
  private buildURL;
40
40
  private setupEventHandlers;
41
41
  private flushQueuedMessages;
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- class n{url;ws;messageHandler;openHandler;errorHandler;closeHandler;queuedMessages=[];constructor(o){this.url=o;let e=this.buildURL(this.url);this.ws=new WebSocket(e),this.setupEventHandlers()}close(o,e){this.ws.close(o,e)}send(o){let e=JSON.stringify(o);this.sendRaw(e)}sendRaw(o){if(this.ws&&this.ws.readyState===WebSocket.OPEN)this.ws.send(o);else this.queuedMessages.push(o)}onMessage(o){this.messageHandler=o}onOpen(o){this.openHandler=o}onClose(o){this.closeHandler=o}onError(o){this.errorHandler=o}buildURL(o){if(o.startsWith("ws://")||o.startsWith("wss://"))return o;if(o.startsWith("http://"))o=o.replace("http://","ws://");else if(o.startsWith("https://"))o=o.replace("https://","wss://");else if(!o.startsWith("ws://")&&!o.startsWith("wss://"))o=`wss://${o}`;return o}setupEventHandlers(){this.ws.onmessage=(o)=>{if(this.messageHandler){let e=JSON.parse(o.data);if(e.done)this.ws.close();if(e.success){this.messageHandler(e);return}if(this.errorHandler)this.errorHandler(o,e)}},this.ws.onopen=(o)=>{if(this.flushQueuedMessages(),this.openHandler)this.openHandler(o)},this.ws.onerror=(o)=>{if(this.errorHandler)this.errorHandler(o)},this.ws.onclose=(o)=>{if(this.closeHandler)this.closeHandler(o)}}flushQueuedMessages(){while(this.queuedMessages.length>0){let o=this.queuedMessages.shift();if(o!==void 0&&this.ws.readyState===WebSocket.OPEN)this.ws.send(o)}}}export{n as Socket};
2
+ class n{url;ws;messageHandler;openHandler;errorHandler;closeHandler;queuedMessages=[];constructor(e){this.url=e;let o=this.buildURL(this.url);this.ws=new WebSocket(o),this.setupEventHandlers()}close(e,o){this.ws.close(e,o)}send(e){let o=JSON.stringify(e);this.sendRaw(o)}sendRaw(e){if(this.ws&&this.ws.readyState===WebSocket.OPEN)this.ws.send(e);else this.queuedMessages.push(e)}onMessage(e){this.messageHandler=e}onOpen(e){this.openHandler=e}onClose(e){this.closeHandler=e}onError(e){this.errorHandler=e}buildURL(e){if(e.startsWith("ws://")||e.startsWith("wss://"))return e;if(e.startsWith("http://"))e=e.replace("http://","ws://");else if(e.startsWith("https://"))e=e.replace("https://","wss://");else if(!e.startsWith("ws://")&&!e.startsWith("wss://"))e=`wss://${e}`;return e}setupEventHandlers(){this.ws.onmessage=(e)=>{if(this.messageHandler){let o=JSON.parse(e.data);if(o.done)this.ws.close();if(o.success){this.messageHandler(o);return}if(this.errorHandler)this.errorHandler(e,o)}},this.ws.onopen=(e)=>{if(this.flushQueuedMessages(),this.openHandler)this.openHandler(e)},this.ws.onerror=(e)=>{if(this.errorHandler)this.errorHandler(e)},this.ws.onclose=(e)=>{if(this.closeHandler)this.closeHandler(e)}}flushQueuedMessages(){while(this.queuedMessages.length>0){let e=this.queuedMessages.shift();if(e!==void 0&&this.ws.readyState===WebSocket.OPEN)this.ws.send(e)}}}export{n as Socket};
3
3
 
4
- //# debugId=B1BC1332EB24344564756E2164756E21
4
+ //# debugId=DDC4BBAA6B121D9B64756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["src/client/Socket.ts"],
4
4
  "sourcesContent": [
5
- "import type { ResponseDataType } from \"@ooneex/http-response\";\nimport type { ISocket, RequestDataType } from \"./types\";\n\nexport class Socket<\n SendData extends RequestDataType = RequestDataType,\n Response extends Record<string, unknown> = Record<string, unknown>,\n> implements ISocket<SendData, Response>\n{\n private ws: WebSocket;\n private messageHandler?: (response: ResponseDataType<Response>) => void;\n private openHandler?: (event: Event) => void;\n private errorHandler?: (event: Event, response?: ResponseDataType<Response>) => void;\n private closeHandler?: (event: CloseEvent) => void;\n private queuedMessages: (string | ArrayBufferLike | Blob | ArrayBufferView)[] = [];\n\n constructor(private readonly url: string) {\n const fullURL = this.buildURL(this.url);\n this.ws = new WebSocket(fullURL);\n this.setupEventHandlers();\n }\n\n public close(code?: number, reason?: string): void {\n this.ws.close(code, reason);\n }\n\n public send(data: SendData): void {\n const text = JSON.stringify(data);\n this.sendRaw(text);\n }\n\n private sendRaw(payload: string | ArrayBufferLike | Blob | ArrayBufferView): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(payload);\n } else {\n this.queuedMessages.push(payload);\n }\n }\n\n public onMessage(handler: (response: ResponseDataType<Response>) => void): void {\n this.messageHandler = handler;\n }\n\n public onOpen(handler: (event: Event) => void): void {\n this.openHandler = handler;\n }\n\n public onClose(handler: (event: CloseEvent) => void): void {\n this.closeHandler = handler;\n }\n\n public onError(handler: (event: Event) => void): void {\n this.errorHandler = handler;\n }\n\n private buildURL(url: string): string {\n if (url.startsWith(\"ws://\") || url.startsWith(\"wss://\")) {\n return url;\n }\n\n // Convert HTTP(S) to WebSocket protocol\n if (url.startsWith(\"http://\")) {\n url = url.replace(\"http://\", \"ws://\");\n } else if (url.startsWith(\"https://\")) {\n url = url.replace(\"https://\", \"wss://\");\n } else if (!url.startsWith(\"ws://\") && !url.startsWith(\"wss://\")) {\n url = `wss://${url}`;\n }\n\n return url;\n }\n\n private setupEventHandlers(): void {\n this.ws.onmessage = (event: MessageEvent) => {\n if (this.messageHandler) {\n const data = JSON.parse(event.data) as ResponseDataType<Response>;\n\n if (data.done) {\n this.ws.close();\n }\n\n if (data.success) {\n this.messageHandler(data);\n\n return;\n }\n\n if (this.errorHandler) {\n this.errorHandler(event, data);\n }\n }\n };\n\n this.ws.onopen = (event: Event) => {\n this.flushQueuedMessages();\n if (this.openHandler) {\n this.openHandler(event);\n }\n };\n\n this.ws.onerror = (event: Event) => {\n if (this.errorHandler) {\n this.errorHandler(event);\n }\n };\n\n this.ws.onclose = (event: CloseEvent) => {\n if (this.closeHandler) {\n this.closeHandler(event);\n }\n };\n }\n\n private flushQueuedMessages(): void {\n while (this.queuedMessages.length > 0) {\n const message = this.queuedMessages.shift();\n if (message !== undefined && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(message);\n }\n }\n }\n}\n"
5
+ "import type { ResponseDataType } from \"@ooneex/http-response\";\nimport type { ISocket, RequestDataType } from \"./types\";\n\nexport class Socket<\n SendData extends RequestDataType = RequestDataType,\n Response extends Record<string, unknown> = Record<string, unknown>,\n> implements ISocket<SendData, Response>\n{\n private ws: WebSocket;\n private messageHandler?: (response: ResponseDataType<Response>) => void;\n private openHandler?: (event: Event) => void;\n private errorHandler?: (event: Event, response?: ResponseDataType<Response>) => void;\n private closeHandler?: (event: CloseEvent) => void;\n private queuedMessages: (string | ArrayBufferLike | Blob | ArrayBufferView)[] = [];\n\n constructor(private readonly url: string) {\n const fullURL = this.buildURL(this.url);\n this.ws = new WebSocket(fullURL);\n this.setupEventHandlers();\n }\n\n public close(code?: number, reason?: string): void {\n this.ws.close(code, reason);\n }\n\n public send(data: SendData): void {\n const text = JSON.stringify(data);\n this.sendRaw(text);\n }\n\n private sendRaw(payload: string | ArrayBufferLike | Blob | ArrayBufferView): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(payload);\n } else {\n this.queuedMessages.push(payload);\n }\n }\n\n public onMessage(handler: (response: ResponseDataType<Response>) => void): void {\n this.messageHandler = handler;\n }\n\n public onOpen(handler: (event: Event) => void): void {\n this.openHandler = handler;\n }\n\n public onClose(handler: (event: CloseEvent) => void): void {\n this.closeHandler = handler;\n }\n\n public onError(handler: (event: Event, response?: ResponseDataType<Response>) => void): void {\n this.errorHandler = handler;\n }\n\n private buildURL(url: string): string {\n if (url.startsWith(\"ws://\") || url.startsWith(\"wss://\")) {\n return url;\n }\n\n // Convert HTTP(S) to WebSocket protocol\n if (url.startsWith(\"http://\")) {\n url = url.replace(\"http://\", \"ws://\");\n } else if (url.startsWith(\"https://\")) {\n url = url.replace(\"https://\", \"wss://\");\n } else if (!url.startsWith(\"ws://\") && !url.startsWith(\"wss://\")) {\n url = `wss://${url}`;\n }\n\n return url;\n }\n\n private setupEventHandlers(): void {\n this.ws.onmessage = (event: MessageEvent) => {\n if (this.messageHandler) {\n const data = JSON.parse(event.data) as ResponseDataType<Response>;\n\n if (data.done) {\n this.ws.close();\n }\n\n if (data.success) {\n this.messageHandler(data);\n\n return;\n }\n\n if (this.errorHandler) {\n this.errorHandler(event, data);\n }\n }\n };\n\n this.ws.onopen = (event: Event) => {\n this.flushQueuedMessages();\n if (this.openHandler) {\n this.openHandler(event);\n }\n };\n\n this.ws.onerror = (event: Event) => {\n if (this.errorHandler) {\n this.errorHandler(event);\n }\n };\n\n this.ws.onclose = (event: CloseEvent) => {\n if (this.closeHandler) {\n this.closeHandler(event);\n }\n };\n }\n\n private flushQueuedMessages(): void {\n while (this.queuedMessages.length > 0) {\n const message = this.queuedMessages.shift();\n if (message !== undefined && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(message);\n }\n }\n }\n}\n"
6
6
  ],
7
- "mappings": ";AAGO,MAAM,CAIb,CAQ+B,IAPrB,GACA,eACA,YACA,aACA,aACA,eAAwE,CAAC,EAEjF,WAAW,CAAkB,EAAa,CAAb,WAC3B,IAAM,EAAU,KAAK,SAAS,KAAK,GAAG,EACtC,KAAK,GAAK,IAAI,UAAU,CAAO,EAC/B,KAAK,mBAAmB,EAGnB,KAAK,CAAC,EAAe,EAAuB,CACjD,KAAK,GAAG,MAAM,EAAM,CAAM,EAGrB,IAAI,CAAC,EAAsB,CAChC,IAAM,EAAO,KAAK,UAAU,CAAI,EAChC,KAAK,QAAQ,CAAI,EAGX,OAAO,CAAC,EAAkE,CAChF,GAAI,KAAK,IAAM,KAAK,GAAG,aAAe,UAAU,KAC9C,KAAK,GAAG,KAAK,CAAO,EAEpB,UAAK,eAAe,KAAK,CAAO,EAI7B,SAAS,CAAC,EAA+D,CAC9E,KAAK,eAAiB,EAGjB,MAAM,CAAC,EAAuC,CACnD,KAAK,YAAc,EAGd,OAAO,CAAC,EAA4C,CACzD,KAAK,aAAe,EAGf,OAAO,CAAC,EAAuC,CACpD,KAAK,aAAe,EAGd,QAAQ,CAAC,EAAqB,CACpC,GAAI,EAAI,WAAW,OAAO,GAAK,EAAI,WAAW,QAAQ,EACpD,OAAO,EAIT,GAAI,EAAI,WAAW,SAAS,EAC1B,EAAM,EAAI,QAAQ,UAAW,OAAO,EAC/B,QAAI,EAAI,WAAW,UAAU,EAClC,EAAM,EAAI,QAAQ,WAAY,QAAQ,EACjC,QAAI,CAAC,EAAI,WAAW,OAAO,GAAK,CAAC,EAAI,WAAW,QAAQ,EAC7D,EAAM,SAAS,IAGjB,OAAO,EAGD,kBAAkB,EAAS,CACjC,KAAK,GAAG,UAAY,CAAC,IAAwB,CAC3C,GAAI,KAAK,eAAgB,CACvB,IAAM,EAAO,KAAK,MAAM,EAAM,IAAI,EAElC,GAAI,EAAK,KACP,KAAK,GAAG,MAAM,EAGhB,GAAI,EAAK,QAAS,CAChB,KAAK,eAAe,CAAI,EAExB,OAGF,GAAI,KAAK,aACP,KAAK,aAAa,EAAO,CAAI,IAKnC,KAAK,GAAG,OAAS,CAAC,IAAiB,CAEjC,GADA,KAAK,oBAAoB,EACrB,KAAK,YACP,KAAK,YAAY,CAAK,GAI1B,KAAK,GAAG,QAAU,CAAC,IAAiB,CAClC,GAAI,KAAK,aACP,KAAK,aAAa,CAAK,GAI3B,KAAK,GAAG,QAAU,CAAC,IAAsB,CACvC,GAAI,KAAK,aACP,KAAK,aAAa,CAAK,GAKrB,mBAAmB,EAAS,CAClC,MAAO,KAAK,eAAe,OAAS,EAAG,CACrC,IAAM,EAAU,KAAK,eAAe,MAAM,EAC1C,GAAI,IAAY,QAAa,KAAK,GAAG,aAAe,UAAU,KAC5D,KAAK,GAAG,KAAK,CAAO,GAI5B",
8
- "debugId": "B1BC1332EB24344564756E2164756E21",
7
+ "mappings": ";AAGO,MAAM,CAIb,CAQ+B,IAPrB,GACA,eACA,YACA,aACA,aACA,eAAwE,CAAC,EAEjF,WAAW,CAAkB,EAAa,CAAb,WAC3B,IAAM,EAAU,KAAK,SAAS,KAAK,GAAG,EACtC,KAAK,GAAK,IAAI,UAAU,CAAO,EAC/B,KAAK,mBAAmB,EAGnB,KAAK,CAAC,EAAe,EAAuB,CACjD,KAAK,GAAG,MAAM,EAAM,CAAM,EAGrB,IAAI,CAAC,EAAsB,CAChC,IAAM,EAAO,KAAK,UAAU,CAAI,EAChC,KAAK,QAAQ,CAAI,EAGX,OAAO,CAAC,EAAkE,CAChF,GAAI,KAAK,IAAM,KAAK,GAAG,aAAe,UAAU,KAC9C,KAAK,GAAG,KAAK,CAAO,EAEpB,UAAK,eAAe,KAAK,CAAO,EAI7B,SAAS,CAAC,EAA+D,CAC9E,KAAK,eAAiB,EAGjB,MAAM,CAAC,EAAuC,CACnD,KAAK,YAAc,EAGd,OAAO,CAAC,EAA4C,CACzD,KAAK,aAAe,EAGf,OAAO,CAAC,EAA8E,CAC3F,KAAK,aAAe,EAGd,QAAQ,CAAC,EAAqB,CACpC,GAAI,EAAI,WAAW,OAAO,GAAK,EAAI,WAAW,QAAQ,EACpD,OAAO,EAIT,GAAI,EAAI,WAAW,SAAS,EAC1B,EAAM,EAAI,QAAQ,UAAW,OAAO,EAC/B,QAAI,EAAI,WAAW,UAAU,EAClC,EAAM,EAAI,QAAQ,WAAY,QAAQ,EACjC,QAAI,CAAC,EAAI,WAAW,OAAO,GAAK,CAAC,EAAI,WAAW,QAAQ,EAC7D,EAAM,SAAS,IAGjB,OAAO,EAGD,kBAAkB,EAAS,CACjC,KAAK,GAAG,UAAY,CAAC,IAAwB,CAC3C,GAAI,KAAK,eAAgB,CACvB,IAAM,EAAO,KAAK,MAAM,EAAM,IAAI,EAElC,GAAI,EAAK,KACP,KAAK,GAAG,MAAM,EAGhB,GAAI,EAAK,QAAS,CAChB,KAAK,eAAe,CAAI,EAExB,OAGF,GAAI,KAAK,aACP,KAAK,aAAa,EAAO,CAAI,IAKnC,KAAK,GAAG,OAAS,CAAC,IAAiB,CAEjC,GADA,KAAK,oBAAoB,EACrB,KAAK,YACP,KAAK,YAAY,CAAK,GAI1B,KAAK,GAAG,QAAU,CAAC,IAAiB,CAClC,GAAI,KAAK,aACP,KAAK,aAAa,CAAK,GAI3B,KAAK,GAAG,QAAU,CAAC,IAAsB,CACvC,GAAI,KAAK,aACP,KAAK,aAAa,CAAK,GAKrB,mBAAmB,EAAS,CAClC,MAAO,KAAK,eAAe,OAAS,EAAG,CACrC,IAAM,EAAU,KAAK,eAAe,MAAM,EAC1C,GAAI,IAAY,QAAa,KAAK,GAAG,aAAe,UAAU,KAC5D,KAAK,GAAG,KAAK,CAAO,GAI5B",
8
+ "debugId": "DDC4BBAA6B121D9B64756E2164756E21",
9
9
  "names": []
10
10
  }
package/dist/index.d.ts CHANGED
@@ -1,19 +1,6 @@
1
- import { IAnalytics } from "@ooneex/analytics";
2
- import { IAppEnv } from "@ooneex/app-env";
3
- import { ICache } from "@ooneex/cache";
4
- import { IDatabase, IRedisDatabaseAdapter } from "@ooneex/database";
5
- import { Header } from "@ooneex/http-header";
1
+ import { ContextType as ControllerContextType } from "@ooneex/controller";
6
2
  import { RequestConfigType } from "@ooneex/http-request";
7
- import { IRequestFile } from "@ooneex/http-request-file";
8
3
  import { IResponse } from "@ooneex/http-response";
9
- import { ILogger, LogsEntity } from "@ooneex/logger";
10
- import { IMailer } from "@ooneex/mailer";
11
- import { IPermission } from "@ooneex/permission";
12
- import { IStorage } from "@ooneex/storage";
13
- import { LocaleInfoType } from "@ooneex/translation";
14
- import { ScalarType } from "@ooneex/types";
15
- import { IUrl } from "@ooneex/url";
16
- import { IUser } from "@ooneex/user";
17
4
  type ControllerClassType = new (...args: any[]) => IController;
18
5
  interface IController<T extends ContextConfigType = ContextConfigType> {
19
6
  index: (context: ContextType<T>) => Promise<IResponse<T["response"]>> | IResponse<T["response"]>;
@@ -21,19 +8,7 @@ interface IController<T extends ContextConfigType = ContextConfigType> {
21
8
  type ContextConfigType = {
22
9
  response: Record<string, unknown>;
23
10
  } & RequestConfigType;
24
- type ContextType<T extends ContextConfigType = ContextConfigType> = {
25
- logger: ILogger<Record<string, ScalarType>> | ILogger<LogsEntity>;
26
- analytics?: IAnalytics;
27
- cache?: ICache;
28
- permission?: IPermission;
29
- storage?: IStorage;
30
- database?: IDatabase;
31
- redis?: IRedisDatabaseAdapter;
32
- mailer?: IMailer;
33
- app: {
34
- url: IAppEnv;
35
- env: IAppEnv;
36
- };
11
+ type ContextType<T extends ContextConfigType = ContextConfigType> = ControllerContextType<T> & {
37
12
  channel: {
38
13
  send: (response: IResponse<T["response"]>) => Promise<void>;
39
14
  close(code?: number, reason?: string): void;
@@ -42,17 +17,5 @@ type ContextType<T extends ContextConfigType = ContextConfigType> = {
42
17
  unsubscribe: () => Promise<void>;
43
18
  publish: (response: IResponse<T["response"]>) => Promise<void>;
44
19
  };
45
- response: IResponse<T["response"]>;
46
- path: string;
47
- url: IUrl;
48
- header: Header;
49
- params: T["params"];
50
- payload: T["payload"];
51
- queries: T["queries"];
52
- files: Record<string, IRequestFile>;
53
- ip: string | null;
54
- host: string;
55
- language: LocaleInfoType;
56
- user: IUser | null;
57
20
  };
58
21
  export { IController, ControllerClassType, ContextType, ContextConfigType };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ooneex/socket",
3
- "description": "",
4
- "version": "0.0.1",
3
+ "description": "WebSocket server and client implementation for real-time bidirectional communication",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -31,29 +31,21 @@
31
31
  "test": "bun test tests",
32
32
  "build": "bunup",
33
33
  "lint": "tsgo --noEmit && bunx biome lint",
34
- "publish:prod": "bun publish --tolerate-republish --access public",
35
- "publish:pack": "bun pm pack --destination ./dist",
36
- "publish:dry": "bun publish --dry-run"
34
+ "publish": "bun publish --access public || true"
37
35
  },
38
36
  "devDependencies": {
39
- "@ooneex/analytics": "0.0.1",
40
- "@ooneex/cache": "0.0.1",
41
- "@ooneex/database": "0.0.1",
42
- "@ooneex/http-header": "0.0.1",
37
+ "@ooneex/controller": "0.0.1",
43
38
  "@ooneex/http-request": "0.0.1",
44
- "@ooneex/http-request-file": "0.0.1",
45
39
  "@ooneex/http-response": "0.0.1",
46
- "@ooneex/logger": "0.0.1",
47
- "@ooneex/mailer": "0.0.1",
48
- "@ooneex/permission": "0.0.1",
49
- "@ooneex/storage": "0.0.1",
50
- "@ooneex/translation": "0.0.1",
51
- "@ooneex/types": "0.0.1",
52
- "@ooneex/url": "0.0.1",
53
- "@ooneex/user": "0.0.1"
40
+ "@ooneex/translation": "0.0.1"
54
41
  },
55
- "peerDependencies": {},
56
- "dependencies": {
57
- "@ooneex/app-env": "0.0.1"
58
- }
42
+ "keywords": [
43
+ "bun",
44
+ "ooneex",
45
+ "realtime",
46
+ "socket",
47
+ "typescript",
48
+ "websocket",
49
+ "ws"
50
+ ]
59
51
  }
Binary file