@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 +546 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +3 -3
- package/dist/index.d.ts +2 -39
- package/package.json +14 -22
- package/dist/ooneex-socket-0.0.1.tgz +0 -0
package/README.md
CHANGED
|
@@ -1 +1,546 @@
|
|
|
1
|
-
# @ooneex/
|
|
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
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
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
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
class n{url;ws;messageHandler;openHandler;errorHandler;closeHandler;queuedMessages=[];constructor(
|
|
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=
|
|
4
|
+
//# debugId=DDC4BBAA6B121D9B64756E2164756E21
|
package/dist/client/index.js.map
CHANGED
|
@@ -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,
|
|
8
|
-
"debugId": "
|
|
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 {
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
"
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
42
|
+
"keywords": [
|
|
43
|
+
"bun",
|
|
44
|
+
"ooneex",
|
|
45
|
+
"realtime",
|
|
46
|
+
"socket",
|
|
47
|
+
"typescript",
|
|
48
|
+
"websocket",
|
|
49
|
+
"ws"
|
|
50
|
+
]
|
|
59
51
|
}
|
|
Binary file
|