@engjts/nexus 0.1.8 → 0.1.9
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/package.json +1 -1
- package/BENCHMARK_REPORT.md +0 -343
- package/documentation/01-getting-started.md +0 -240
- package/documentation/02-context.md +0 -335
- package/documentation/03-routing.md +0 -397
- package/documentation/04-middleware.md +0 -483
- package/documentation/05-validation.md +0 -514
- package/documentation/06-error-handling.md +0 -465
- package/documentation/07-performance.md +0 -364
- package/documentation/08-adapters.md +0 -470
- package/documentation/09-api-reference.md +0 -548
- package/documentation/10-examples.md +0 -582
- package/documentation/11-deployment.md +0 -477
- package/documentation/12-sentry.md +0 -620
- package/documentation/13-sentry-data-storage.md +0 -996
- package/documentation/14-sentry-data-reference.md +0 -457
- package/documentation/15-sentry-summary.md +0 -409
- package/documentation/16-alerts-system.md +0 -745
- package/documentation/17-alert-adapters.md +0 -696
- package/documentation/18-alerts-implementation-summary.md +0 -385
- package/documentation/19-class-based-routing.md +0 -840
- package/documentation/20-websocket-realtime.md +0 -813
- package/documentation/21-cache-system.md +0 -510
- package/documentation/22-job-queue.md +0 -772
- package/documentation/23-sentry-plugin.md +0 -551
- package/documentation/24-testing-utilities.md +0 -1287
- package/documentation/25-api-versioning.md +0 -533
- package/documentation/26-context-store.md +0 -607
- package/documentation/27-dependency-injection.md +0 -329
- package/documentation/28-lifecycle-hooks.md +0 -521
- package/documentation/29-package-structure.md +0 -196
- package/documentation/30-plugin-system.md +0 -414
- package/documentation/31-jwt-authentication.md +0 -597
- package/documentation/32-cli.md +0 -268
- package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
- package/documentation/ALERTS-INDEX.md +0 -330
- package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
- package/documentation/README.md +0 -178
- package/documentation/index.html +0 -34
- package/modern_framework_paper.md +0 -1870
- package/public/css/style.css +0 -87
- package/public/index.html +0 -34
- package/public/js/app.js +0 -27
- package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
- package/src/advanced/cache/MultiTierCache.ts +0 -194
- package/src/advanced/cache/RedisCacheStore.ts +0 -341
- package/src/advanced/cache/index.ts +0 -5
- package/src/advanced/cache/types.ts +0 -40
- package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
- package/src/advanced/graphql/index.ts +0 -22
- package/src/advanced/graphql/server.ts +0 -252
- package/src/advanced/graphql/types.ts +0 -42
- package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
- package/src/advanced/jobs/JobQueue.ts +0 -556
- package/src/advanced/jobs/RedisQueueStore.ts +0 -367
- package/src/advanced/jobs/index.ts +0 -5
- package/src/advanced/jobs/types.ts +0 -70
- package/src/advanced/observability/APMManager.ts +0 -163
- package/src/advanced/observability/AlertManager.ts +0 -109
- package/src/advanced/observability/MetricRegistry.ts +0 -151
- package/src/advanced/observability/ObservabilityCenter.ts +0 -304
- package/src/advanced/observability/StructuredLogger.ts +0 -154
- package/src/advanced/observability/TracingManager.ts +0 -117
- package/src/advanced/observability/adapters.ts +0 -304
- package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
- package/src/advanced/observability/index.ts +0 -11
- package/src/advanced/observability/types.ts +0 -174
- package/src/advanced/playground/extractPathParams.ts +0 -6
- package/src/advanced/playground/generateFieldExample.ts +0 -31
- package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
- package/src/advanced/playground/generateSummary.ts +0 -19
- package/src/advanced/playground/getTagFromPath.ts +0 -9
- package/src/advanced/playground/index.ts +0 -8
- package/src/advanced/playground/playground.ts +0 -250
- package/src/advanced/playground/types.ts +0 -49
- package/src/advanced/playground/zodToExample.ts +0 -16
- package/src/advanced/playground/zodToParams.ts +0 -15
- package/src/advanced/postman/buildAuth.ts +0 -31
- package/src/advanced/postman/buildBody.ts +0 -15
- package/src/advanced/postman/buildQueryParams.ts +0 -27
- package/src/advanced/postman/buildRequestItem.ts +0 -36
- package/src/advanced/postman/buildResponses.ts +0 -11
- package/src/advanced/postman/buildUrl.ts +0 -33
- package/src/advanced/postman/capitalize.ts +0 -4
- package/src/advanced/postman/generateCollection.ts +0 -59
- package/src/advanced/postman/generateEnvironment.ts +0 -34
- package/src/advanced/postman/generateExampleFromZod.ts +0 -21
- package/src/advanced/postman/generateFieldExample.ts +0 -45
- package/src/advanced/postman/generateName.ts +0 -20
- package/src/advanced/postman/generateUUID.ts +0 -11
- package/src/advanced/postman/getTagFromPath.ts +0 -10
- package/src/advanced/postman/index.ts +0 -28
- package/src/advanced/postman/postman.ts +0 -156
- package/src/advanced/postman/slugify.ts +0 -7
- package/src/advanced/postman/types.ts +0 -140
- package/src/advanced/realtime/index.ts +0 -18
- package/src/advanced/realtime/websocket.ts +0 -231
- package/src/advanced/sentry/index.ts +0 -1236
- package/src/advanced/sentry/types.ts +0 -355
- package/src/advanced/static/generateDirectoryListing.ts +0 -47
- package/src/advanced/static/generateETag.ts +0 -7
- package/src/advanced/static/getMimeType.ts +0 -9
- package/src/advanced/static/index.ts +0 -32
- package/src/advanced/static/isSafePath.ts +0 -13
- package/src/advanced/static/publicDir.ts +0 -21
- package/src/advanced/static/serveStatic.ts +0 -225
- package/src/advanced/static/spa.ts +0 -24
- package/src/advanced/static/types.ts +0 -159
- package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
- package/src/advanced/swagger/buildOperation.ts +0 -61
- package/src/advanced/swagger/buildParameters.ts +0 -61
- package/src/advanced/swagger/buildRequestBody.ts +0 -21
- package/src/advanced/swagger/buildResponses.ts +0 -54
- package/src/advanced/swagger/capitalize.ts +0 -5
- package/src/advanced/swagger/convertPath.ts +0 -9
- package/src/advanced/swagger/createSwagger.ts +0 -12
- package/src/advanced/swagger/generateOperationId.ts +0 -21
- package/src/advanced/swagger/generateSpec.ts +0 -105
- package/src/advanced/swagger/generateSummary.ts +0 -24
- package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
- package/src/advanced/swagger/generateThemeCss.ts +0 -53
- package/src/advanced/swagger/index.ts +0 -25
- package/src/advanced/swagger/swagger.ts +0 -237
- package/src/advanced/swagger/types.ts +0 -206
- package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
- package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
- package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
- package/src/advanced/testing/factory.ts +0 -509
- package/src/advanced/testing/harness.ts +0 -612
- package/src/advanced/testing/index.ts +0 -430
- package/src/advanced/testing/load-test.ts +0 -618
- package/src/advanced/testing/mock-server.ts +0 -498
- package/src/advanced/testing/mock.ts +0 -670
- package/src/cli/bin.ts +0 -9
- package/src/cli/cli.ts +0 -158
- package/src/cli/commands/add.ts +0 -178
- package/src/cli/commands/build.ts +0 -73
- package/src/cli/commands/create.ts +0 -166
- package/src/cli/commands/dev.ts +0 -85
- package/src/cli/commands/generate.ts +0 -99
- package/src/cli/commands/help.ts +0 -95
- package/src/cli/commands/init.ts +0 -91
- package/src/cli/commands/version.ts +0 -38
- package/src/cli/index.ts +0 -6
- package/src/cli/templates/generators.ts +0 -359
- package/src/cli/templates/index.ts +0 -680
- package/src/cli/utils/exec.ts +0 -52
- package/src/cli/utils/file-system.ts +0 -78
- package/src/cli/utils/logger.ts +0 -111
- package/src/core/adapter.ts +0 -88
- package/src/core/application.ts +0 -1453
- package/src/core/context-pool.ts +0 -79
- package/src/core/context.ts +0 -856
- package/src/core/index.ts +0 -94
- package/src/core/middleware.ts +0 -272
- package/src/core/performance/buffer-pool.ts +0 -108
- package/src/core/performance/middleware-optimizer.ts +0 -162
- package/src/core/plugin/PluginManager.ts +0 -435
- package/src/core/plugin/builder.ts +0 -358
- package/src/core/plugin/index.ts +0 -50
- package/src/core/plugin/types.ts +0 -214
- package/src/core/router/file-router.ts +0 -623
- package/src/core/router/index.ts +0 -260
- package/src/core/router/radix-tree.ts +0 -242
- package/src/core/serializer.ts +0 -397
- package/src/core/store/index.ts +0 -30
- package/src/core/store/registry.ts +0 -178
- package/src/core/store/request-store.ts +0 -240
- package/src/core/store/types.ts +0 -233
- package/src/core/types.ts +0 -616
- package/src/database/adapter.ts +0 -35
- package/src/database/adapters/index.ts +0 -1
- package/src/database/adapters/mysql.ts +0 -669
- package/src/database/database.ts +0 -70
- package/src/database/dialect.ts +0 -388
- package/src/database/index.ts +0 -12
- package/src/database/migrations.ts +0 -86
- package/src/database/optimizer.ts +0 -125
- package/src/database/query-builder.ts +0 -404
- package/src/database/realtime.ts +0 -53
- package/src/database/schema.ts +0 -71
- package/src/database/transactions.ts +0 -56
- package/src/database/types.ts +0 -87
- package/src/deployment/cluster.ts +0 -471
- package/src/deployment/config.ts +0 -454
- package/src/deployment/docker.ts +0 -599
- package/src/deployment/graceful-shutdown.ts +0 -373
- package/src/deployment/index.ts +0 -56
- package/src/index.ts +0 -281
- package/src/security/adapter.ts +0 -318
- package/src/security/auth/JWTPlugin.ts +0 -234
- package/src/security/auth/JWTProvider.ts +0 -316
- package/src/security/auth/adapter.ts +0 -12
- package/src/security/auth/jwt.ts +0 -234
- package/src/security/auth/middleware.ts +0 -188
- package/src/security/csrf.ts +0 -220
- package/src/security/headers.ts +0 -108
- package/src/security/index.ts +0 -60
- package/src/security/rate-limit/adapter.ts +0 -7
- package/src/security/rate-limit/memory.ts +0 -108
- package/src/security/rate-limit/middleware.ts +0 -181
- package/src/security/sanitization.ts +0 -75
- package/src/security/types.ts +0 -240
- package/src/security/utils.ts +0 -52
- package/tsconfig.json +0 -39
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
# WebSocket Realtime
|
|
2
|
-
|
|
3
|
-
Nexus Framework menyediakan **WebSocket support** built-in untuk membangun aplikasi realtime seperti chat, notifications, live updates, dan lainnya.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { createApp } from 'nexus';
|
|
9
|
-
|
|
10
|
-
const app = createApp();
|
|
11
|
-
|
|
12
|
-
// Define WebSocket route
|
|
13
|
-
app.ws('/ws/chat', {
|
|
14
|
-
onConnect: async (socket, ctx) => {
|
|
15
|
-
console.log('Client connected');
|
|
16
|
-
socket.send(JSON.stringify({ type: 'welcome', message: 'Hello!' }));
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
onMessage: async (socket, message, ctx) => {
|
|
20
|
-
console.log('Received:', message);
|
|
21
|
-
socket.send(JSON.stringify({ echo: message }));
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
onClose: async (socket, ctx) => {
|
|
25
|
-
console.log('Client disconnected');
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
app.listen(3000, () => {
|
|
30
|
-
console.log('Server running at http://localhost:3000');
|
|
31
|
-
console.log('WebSocket at ws://localhost:3000/ws/chat');
|
|
32
|
-
});
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## WebSocketRouteConfig Interface
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
interface WebSocketRouteConfig {
|
|
39
|
-
/** Authentication handler - validate token and return user object */
|
|
40
|
-
auth?: (ctx: WebSocketContext) => Promise<any>;
|
|
41
|
-
|
|
42
|
-
/** Called before onConnect - can be used for additional setup */
|
|
43
|
-
beforeConnect?: (socket: WebSocket, ctx: WebSocketContext) => Promise<void>;
|
|
44
|
-
|
|
45
|
-
/** Called when client connects */
|
|
46
|
-
onConnect?: (socket: WebSocket, ctx: WebSocketContext) => Promise<void>;
|
|
47
|
-
|
|
48
|
-
/** Called when client sends a message */
|
|
49
|
-
onMessage?: (socket: WebSocket, message: any, ctx: WebSocketContext) => Promise<void>;
|
|
50
|
-
|
|
51
|
-
/** Called when client disconnects */
|
|
52
|
-
onClose?: (socket: WebSocket, ctx: WebSocketContext, code: number, reason?: Buffer) => Promise<void>;
|
|
53
|
-
|
|
54
|
-
/** Called on WebSocket error */
|
|
55
|
-
onError?: (socket: WebSocket, error: Error, ctx: WebSocketContext) => Promise<void>;
|
|
56
|
-
|
|
57
|
-
/** Enable room support (default: true) */
|
|
58
|
-
rooms?: boolean;
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## WebSocketContext
|
|
63
|
-
|
|
64
|
-
Context object yang tersedia di setiap handler:
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
interface WebSocketContext {
|
|
68
|
-
/** WebSocket path (e.g., '/ws/chat') */
|
|
69
|
-
path: string;
|
|
70
|
-
|
|
71
|
-
/** Query parameters from URL */
|
|
72
|
-
query: Record<string, string | string[]>;
|
|
73
|
-
|
|
74
|
-
/** Request headers */
|
|
75
|
-
headers: IncomingMessage['headers'];
|
|
76
|
-
|
|
77
|
-
/** User object (set by auth handler) */
|
|
78
|
-
user?: any;
|
|
79
|
-
|
|
80
|
-
/** Custom metadata */
|
|
81
|
-
metadata?: Record<string, any>;
|
|
82
|
-
|
|
83
|
-
/** Raw Node.js request */
|
|
84
|
-
raw: {
|
|
85
|
-
req: IncomingMessage;
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Fitur-Fitur
|
|
91
|
-
|
|
92
|
-
### 1. Authentication
|
|
93
|
-
|
|
94
|
-
Validasi token/credentials sebelum koneksi diterima:
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
app.ws('/ws/protected', {
|
|
98
|
-
auth: async (ctx) => {
|
|
99
|
-
const token = ctx.query.token as string;
|
|
100
|
-
|
|
101
|
-
if (!token) {
|
|
102
|
-
throw new Error('Token required');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Verify JWT token
|
|
106
|
-
const user = await verifyToken(token);
|
|
107
|
-
|
|
108
|
-
if (!user) {
|
|
109
|
-
throw new Error('Invalid token');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Return user object - will be available as ctx.user
|
|
113
|
-
return user;
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
onConnect: async (socket, ctx) => {
|
|
117
|
-
// ctx.user is now available
|
|
118
|
-
console.log(`User ${ctx.user.name} connected`);
|
|
119
|
-
|
|
120
|
-
socket.send(JSON.stringify({
|
|
121
|
-
type: 'authenticated',
|
|
122
|
-
user: ctx.user
|
|
123
|
-
}));
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
onMessage: async (socket, message, ctx) => {
|
|
127
|
-
console.log(`Message from ${ctx.user.name}:`, message);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**Client connection:**
|
|
133
|
-
```javascript
|
|
134
|
-
const ws = new WebSocket('ws://localhost:3000/ws/protected?token=your-jwt-token');
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### 2. Room Management
|
|
138
|
-
|
|
139
|
-
Kelompokkan koneksi ke dalam "rooms" untuk broadcast targeted:
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
app.ws('/ws/chat', {
|
|
143
|
-
onConnect: async (socket, ctx) => {
|
|
144
|
-
const ws = app.getWebSocket()!;
|
|
145
|
-
const room = ctx.query.room as string || 'general';
|
|
146
|
-
|
|
147
|
-
// Join a room
|
|
148
|
-
ws.joinRoom(room, socket);
|
|
149
|
-
|
|
150
|
-
// Notify others in the room
|
|
151
|
-
ws.broadcast(room, {
|
|
152
|
-
type: 'user_joined',
|
|
153
|
-
message: `New user joined ${room}`
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
socket.send(JSON.stringify({
|
|
157
|
-
type: 'joined',
|
|
158
|
-
room
|
|
159
|
-
}));
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
onMessage: async (socket, message, ctx) => {
|
|
163
|
-
const ws = app.getWebSocket()!;
|
|
164
|
-
|
|
165
|
-
if (message.type === 'chat') {
|
|
166
|
-
// Broadcast to current room
|
|
167
|
-
ws.broadcast(message.room, {
|
|
168
|
-
type: 'chat',
|
|
169
|
-
user: ctx.user?.name,
|
|
170
|
-
text: message.text,
|
|
171
|
-
timestamp: new Date().toISOString()
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (message.type === 'join_room') {
|
|
176
|
-
ws.joinRoom(message.room, socket);
|
|
177
|
-
socket.send(JSON.stringify({ type: 'room_joined', room: message.room }));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (message.type === 'leave_room') {
|
|
181
|
-
ws.leaveRoom(message.room, socket);
|
|
182
|
-
socket.send(JSON.stringify({ type: 'room_left', room: message.room }));
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
onClose: async (socket, ctx) => {
|
|
187
|
-
const ws = app.getWebSocket()!;
|
|
188
|
-
|
|
189
|
-
// Broadcast user left (rooms are auto-cleaned)
|
|
190
|
-
ws.broadcast('general', {
|
|
191
|
-
type: 'user_left',
|
|
192
|
-
user: ctx.user?.name
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### 3. Broadcasting
|
|
199
|
-
|
|
200
|
-
Kirim pesan ke semua client dalam room:
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
const ws = app.getWebSocket()!;
|
|
204
|
-
|
|
205
|
-
// Broadcast to specific room
|
|
206
|
-
ws.broadcast('notifications', {
|
|
207
|
-
type: 'alert',
|
|
208
|
-
message: 'System maintenance in 5 minutes'
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Broadcast to multiple rooms
|
|
212
|
-
['room1', 'room2', 'room3'].forEach(room => {
|
|
213
|
-
ws.broadcast(room, { type: 'announcement', text: 'Hello everyone!' });
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### 4. Room Manager API
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
const ws = app.getWebSocket()!;
|
|
221
|
-
|
|
222
|
-
// Create room explicitly
|
|
223
|
-
ws.createRoom('vip-lounge');
|
|
224
|
-
|
|
225
|
-
// Join room
|
|
226
|
-
ws.joinRoom('vip-lounge', socket);
|
|
227
|
-
|
|
228
|
-
// Leave room
|
|
229
|
-
ws.leaveRoom('vip-lounge', socket);
|
|
230
|
-
|
|
231
|
-
// Broadcast to room
|
|
232
|
-
ws.broadcast('vip-lounge', { message: 'VIP announcement' });
|
|
233
|
-
|
|
234
|
-
// Using roomManager helper
|
|
235
|
-
ws.roomManager.create('new-room');
|
|
236
|
-
ws.roomManager.join('new-room', socket);
|
|
237
|
-
ws.roomManager.leave('new-room', socket);
|
|
238
|
-
ws.roomManager.broadcast('new-room', { data: 'hello' });
|
|
239
|
-
ws.roomManager.list(); // ['new-room', 'vip-lounge', ...]
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## Contoh Penggunaan
|
|
243
|
-
|
|
244
|
-
### Chat Application
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
app.ws('/ws/chat', {
|
|
248
|
-
auth: async (ctx) => {
|
|
249
|
-
const token = ctx.query.token as string;
|
|
250
|
-
return { id: generateId(), name: token || 'Anonymous' };
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
onConnect: async (socket, ctx) => {
|
|
254
|
-
const ws = app.getWebSocket()!;
|
|
255
|
-
|
|
256
|
-
// Join default room
|
|
257
|
-
ws.joinRoom('general', socket);
|
|
258
|
-
|
|
259
|
-
// Welcome message
|
|
260
|
-
socket.send(JSON.stringify({
|
|
261
|
-
type: 'welcome',
|
|
262
|
-
message: `Welcome ${ctx.user.name}!`,
|
|
263
|
-
room: 'general'
|
|
264
|
-
}));
|
|
265
|
-
|
|
266
|
-
// Notify others
|
|
267
|
-
ws.broadcast('general', {
|
|
268
|
-
type: 'system',
|
|
269
|
-
message: `${ctx.user.name} joined the chat`
|
|
270
|
-
});
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
onMessage: async (socket, message, ctx) => {
|
|
274
|
-
const ws = app.getWebSocket()!;
|
|
275
|
-
|
|
276
|
-
switch (message.type) {
|
|
277
|
-
case 'chat':
|
|
278
|
-
ws.broadcast(message.room || 'general', {
|
|
279
|
-
type: 'chat',
|
|
280
|
-
user: ctx.user.name,
|
|
281
|
-
text: message.text,
|
|
282
|
-
timestamp: new Date().toISOString()
|
|
283
|
-
});
|
|
284
|
-
break;
|
|
285
|
-
|
|
286
|
-
case 'private':
|
|
287
|
-
// Private message implementation
|
|
288
|
-
// Find target socket and send directly
|
|
289
|
-
break;
|
|
290
|
-
|
|
291
|
-
case 'typing':
|
|
292
|
-
ws.broadcast(message.room || 'general', {
|
|
293
|
-
type: 'typing',
|
|
294
|
-
user: ctx.user.name
|
|
295
|
-
});
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
onClose: async (socket, ctx) => {
|
|
301
|
-
const ws = app.getWebSocket()!;
|
|
302
|
-
ws.broadcast('general', {
|
|
303
|
-
type: 'system',
|
|
304
|
-
message: `${ctx.user.name} left the chat`
|
|
305
|
-
});
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
onError: async (socket, error, ctx) => {
|
|
309
|
-
console.error(`WebSocket error for ${ctx.user?.name}:`, error);
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Notifications Service
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
app.ws('/ws/notifications', {
|
|
318
|
-
auth: async (ctx) => {
|
|
319
|
-
const token = ctx.query.token as string;
|
|
320
|
-
return await verifyToken(token);
|
|
321
|
-
},
|
|
322
|
-
|
|
323
|
-
onConnect: async (socket, ctx) => {
|
|
324
|
-
const ws = app.getWebSocket()!;
|
|
325
|
-
|
|
326
|
-
// Join user-specific room
|
|
327
|
-
ws.joinRoom(`user:${ctx.user.id}`, socket);
|
|
328
|
-
|
|
329
|
-
// Join role-based rooms
|
|
330
|
-
ctx.user.roles.forEach((role: string) => {
|
|
331
|
-
ws.joinRoom(`role:${role}`, socket);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Send unread notifications
|
|
335
|
-
const unread = await getUnreadNotifications(ctx.user.id);
|
|
336
|
-
socket.send(JSON.stringify({
|
|
337
|
-
type: 'unread',
|
|
338
|
-
count: unread.length,
|
|
339
|
-
notifications: unread
|
|
340
|
-
}));
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
onMessage: async (socket, message, ctx) => {
|
|
344
|
-
if (message.type === 'mark_read') {
|
|
345
|
-
await markNotificationRead(message.notificationId, ctx.user.id);
|
|
346
|
-
socket.send(JSON.stringify({
|
|
347
|
-
type: 'marked_read',
|
|
348
|
-
notificationId: message.notificationId
|
|
349
|
-
}));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Send notification from anywhere in your app
|
|
355
|
-
function sendNotification(userId: string, notification: any) {
|
|
356
|
-
const ws = app.getWebSocket();
|
|
357
|
-
ws?.broadcast(`user:${userId}`, {
|
|
358
|
-
type: 'notification',
|
|
359
|
-
...notification
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Send to all admins
|
|
364
|
-
function notifyAdmins(message: string) {
|
|
365
|
-
const ws = app.getWebSocket();
|
|
366
|
-
ws?.broadcast('role:admin', {
|
|
367
|
-
type: 'admin_alert',
|
|
368
|
-
message
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### Live Updates / Real-time Data
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
app.ws('/ws/stocks', {
|
|
377
|
-
onConnect: async (socket, ctx) => {
|
|
378
|
-
const ws = app.getWebSocket()!;
|
|
379
|
-
const symbols = (ctx.query.symbols as string)?.split(',') || ['AAPL', 'GOOGL'];
|
|
380
|
-
|
|
381
|
-
// Subscribe to stock symbols
|
|
382
|
-
symbols.forEach(symbol => {
|
|
383
|
-
ws.joinRoom(`stock:${symbol}`, socket);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
socket.send(JSON.stringify({
|
|
387
|
-
type: 'subscribed',
|
|
388
|
-
symbols
|
|
389
|
-
}));
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
onMessage: async (socket, message, ctx) => {
|
|
393
|
-
const ws = app.getWebSocket()!;
|
|
394
|
-
|
|
395
|
-
if (message.type === 'subscribe') {
|
|
396
|
-
ws.joinRoom(`stock:${message.symbol}`, socket);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (message.type === 'unsubscribe') {
|
|
400
|
-
ws.leaveRoom(`stock:${message.symbol}`, socket);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// Price update service (simulated)
|
|
406
|
-
setInterval(() => {
|
|
407
|
-
const ws = app.getWebSocket();
|
|
408
|
-
const stocks = ['AAPL', 'GOOGL', 'MSFT', 'AMZN'];
|
|
409
|
-
|
|
410
|
-
stocks.forEach(symbol => {
|
|
411
|
-
ws?.broadcast(`stock:${symbol}`, {
|
|
412
|
-
type: 'price_update',
|
|
413
|
-
symbol,
|
|
414
|
-
price: (Math.random() * 1000).toFixed(2),
|
|
415
|
-
timestamp: new Date().toISOString()
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
}, 1000);
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Multiplayer Game
|
|
422
|
-
|
|
423
|
-
```typescript
|
|
424
|
-
app.ws('/ws/game', {
|
|
425
|
-
auth: async (ctx) => {
|
|
426
|
-
return {
|
|
427
|
-
id: generatePlayerId(),
|
|
428
|
-
name: ctx.query.name as string || 'Player'
|
|
429
|
-
};
|
|
430
|
-
},
|
|
431
|
-
|
|
432
|
-
onConnect: async (socket, ctx) => {
|
|
433
|
-
const ws = app.getWebSocket()!;
|
|
434
|
-
const gameId = ctx.query.game as string;
|
|
435
|
-
|
|
436
|
-
if (!gameId) {
|
|
437
|
-
socket.send(JSON.stringify({ type: 'error', message: 'Game ID required' }));
|
|
438
|
-
socket.close();
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Join game room
|
|
443
|
-
ws.joinRoom(`game:${gameId}`, socket);
|
|
444
|
-
|
|
445
|
-
// Notify other players
|
|
446
|
-
ws.broadcast(`game:${gameId}`, {
|
|
447
|
-
type: 'player_joined',
|
|
448
|
-
player: ctx.user
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// Send current game state
|
|
452
|
-
const gameState = await getGameState(gameId);
|
|
453
|
-
socket.send(JSON.stringify({
|
|
454
|
-
type: 'game_state',
|
|
455
|
-
state: gameState
|
|
456
|
-
}));
|
|
457
|
-
},
|
|
458
|
-
|
|
459
|
-
onMessage: async (socket, message, ctx) => {
|
|
460
|
-
const ws = app.getWebSocket()!;
|
|
461
|
-
const gameId = ctx.query.game as string;
|
|
462
|
-
|
|
463
|
-
switch (message.type) {
|
|
464
|
-
case 'move':
|
|
465
|
-
const result = await processMove(gameId, ctx.user.id, message.move);
|
|
466
|
-
ws.broadcast(`game:${gameId}`, {
|
|
467
|
-
type: 'move_made',
|
|
468
|
-
player: ctx.user.id,
|
|
469
|
-
move: message.move,
|
|
470
|
-
result
|
|
471
|
-
});
|
|
472
|
-
break;
|
|
473
|
-
|
|
474
|
-
case 'chat':
|
|
475
|
-
ws.broadcast(`game:${gameId}`, {
|
|
476
|
-
type: 'game_chat',
|
|
477
|
-
player: ctx.user.name,
|
|
478
|
-
text: message.text
|
|
479
|
-
});
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
},
|
|
483
|
-
|
|
484
|
-
onClose: async (socket, ctx) => {
|
|
485
|
-
const ws = app.getWebSocket()!;
|
|
486
|
-
const gameId = ctx.query.game as string;
|
|
487
|
-
|
|
488
|
-
ws.broadcast(`game:${gameId}`, {
|
|
489
|
-
type: 'player_left',
|
|
490
|
-
player: ctx.user
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
## Client-Side Examples
|
|
497
|
-
|
|
498
|
-
### Browser (Vanilla JavaScript)
|
|
499
|
-
|
|
500
|
-
```javascript
|
|
501
|
-
// Basic connection
|
|
502
|
-
const ws = new WebSocket('ws://localhost:3000/ws/chat?token=myname');
|
|
503
|
-
|
|
504
|
-
ws.onopen = () => {
|
|
505
|
-
console.log('Connected!');
|
|
506
|
-
ws.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
ws.onmessage = (event) => {
|
|
510
|
-
const data = JSON.parse(event.data);
|
|
511
|
-
console.log('Received:', data);
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
ws.onclose = () => {
|
|
515
|
-
console.log('Disconnected');
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
ws.onerror = (error) => {
|
|
519
|
-
console.error('WebSocket error:', error);
|
|
520
|
-
};
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
### React Hook
|
|
524
|
-
|
|
525
|
-
```typescript
|
|
526
|
-
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
527
|
-
|
|
528
|
-
function useWebSocket(url: string) {
|
|
529
|
-
const [messages, setMessages] = useState<any[]>([]);
|
|
530
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
531
|
-
const wsRef = useRef<WebSocket | null>(null);
|
|
532
|
-
|
|
533
|
-
useEffect(() => {
|
|
534
|
-
const ws = new WebSocket(url);
|
|
535
|
-
wsRef.current = ws;
|
|
536
|
-
|
|
537
|
-
ws.onopen = () => setIsConnected(true);
|
|
538
|
-
ws.onclose = () => setIsConnected(false);
|
|
539
|
-
ws.onmessage = (event) => {
|
|
540
|
-
const data = JSON.parse(event.data);
|
|
541
|
-
setMessages(prev => [...prev, data]);
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
return () => ws.close();
|
|
545
|
-
}, [url]);
|
|
546
|
-
|
|
547
|
-
const send = useCallback((data: any) => {
|
|
548
|
-
wsRef.current?.send(JSON.stringify(data));
|
|
549
|
-
}, []);
|
|
550
|
-
|
|
551
|
-
return { messages, isConnected, send };
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Usage
|
|
555
|
-
function ChatComponent() {
|
|
556
|
-
const { messages, isConnected, send } = useWebSocket('ws://localhost:3000/ws/chat?token=user1');
|
|
557
|
-
|
|
558
|
-
return (
|
|
559
|
-
<div>
|
|
560
|
-
<p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
|
|
561
|
-
<button onClick={() => send({ type: 'chat', text: 'Hello!' })}>
|
|
562
|
-
Send
|
|
563
|
-
</button>
|
|
564
|
-
<ul>
|
|
565
|
-
{messages.map((msg, i) => (
|
|
566
|
-
<li key={i}>{JSON.stringify(msg)}</li>
|
|
567
|
-
))}
|
|
568
|
-
</ul>
|
|
569
|
-
</div>
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
### Node.js Client
|
|
575
|
-
|
|
576
|
-
```typescript
|
|
577
|
-
import WebSocket from 'ws';
|
|
578
|
-
|
|
579
|
-
const ws = new WebSocket('ws://localhost:3000/ws/chat?token=bot');
|
|
580
|
-
|
|
581
|
-
ws.on('open', () => {
|
|
582
|
-
console.log('Connected to server');
|
|
583
|
-
ws.send(JSON.stringify({ type: 'chat', text: 'Bot is online!' }));
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
ws.on('message', (data) => {
|
|
587
|
-
const message = JSON.parse(data.toString());
|
|
588
|
-
console.log('Received:', message);
|
|
589
|
-
|
|
590
|
-
// Auto-reply to messages
|
|
591
|
-
if (message.type === 'chat' && message.user !== 'bot') {
|
|
592
|
-
ws.send(JSON.stringify({
|
|
593
|
-
type: 'chat',
|
|
594
|
-
text: `You said: ${message.text}`
|
|
595
|
-
}));
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
ws.on('close', () => {
|
|
600
|
-
console.log('Disconnected');
|
|
601
|
-
});
|
|
602
|
-
```
|
|
603
|
-
|
|
604
|
-
## Error Handling
|
|
605
|
-
|
|
606
|
-
```typescript
|
|
607
|
-
app.ws('/ws/robust', {
|
|
608
|
-
auth: async (ctx) => {
|
|
609
|
-
try {
|
|
610
|
-
const token = ctx.query.token as string;
|
|
611
|
-
if (!token) {
|
|
612
|
-
throw new Error('AUTH_REQUIRED');
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const user = await verifyToken(token);
|
|
616
|
-
if (!user) {
|
|
617
|
-
throw new Error('INVALID_TOKEN');
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return user;
|
|
621
|
-
} catch (error) {
|
|
622
|
-
// Throwing here will reject the connection
|
|
623
|
-
throw error;
|
|
624
|
-
}
|
|
625
|
-
},
|
|
626
|
-
|
|
627
|
-
onConnect: async (socket, ctx) => {
|
|
628
|
-
try {
|
|
629
|
-
// Setup logic
|
|
630
|
-
const ws = app.getWebSocket()!;
|
|
631
|
-
ws.joinRoom('main', socket);
|
|
632
|
-
} catch (error) {
|
|
633
|
-
socket.send(JSON.stringify({
|
|
634
|
-
type: 'error',
|
|
635
|
-
message: 'Failed to setup connection'
|
|
636
|
-
}));
|
|
637
|
-
socket.close(1011, 'Setup failed');
|
|
638
|
-
}
|
|
639
|
-
},
|
|
640
|
-
|
|
641
|
-
onMessage: async (socket, message, ctx) => {
|
|
642
|
-
try {
|
|
643
|
-
// Validate message
|
|
644
|
-
if (!message.type) {
|
|
645
|
-
throw new Error('Message type required');
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Process message
|
|
649
|
-
await processMessage(message, ctx);
|
|
650
|
-
|
|
651
|
-
} catch (error) {
|
|
652
|
-
socket.send(JSON.stringify({
|
|
653
|
-
type: 'error',
|
|
654
|
-
message: error.message
|
|
655
|
-
}));
|
|
656
|
-
}
|
|
657
|
-
},
|
|
658
|
-
|
|
659
|
-
onError: async (socket, error, ctx) => {
|
|
660
|
-
console.error('WebSocket Error:', {
|
|
661
|
-
user: ctx.user?.id,
|
|
662
|
-
error: error.message,
|
|
663
|
-
stack: error.stack
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
// Optionally send error to client
|
|
667
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
668
|
-
socket.send(JSON.stringify({
|
|
669
|
-
type: 'error',
|
|
670
|
-
message: 'Internal server error'
|
|
671
|
-
}));
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
});
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
## Best Practices
|
|
678
|
-
|
|
679
|
-
### 1. Heartbeat/Ping-Pong
|
|
680
|
-
|
|
681
|
-
```typescript
|
|
682
|
-
app.ws('/ws/with-heartbeat', {
|
|
683
|
-
onConnect: async (socket, ctx) => {
|
|
684
|
-
// Send ping every 30 seconds
|
|
685
|
-
const pingInterval = setInterval(() => {
|
|
686
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
687
|
-
socket.ping();
|
|
688
|
-
}
|
|
689
|
-
}, 30000);
|
|
690
|
-
|
|
691
|
-
// Store interval in context for cleanup
|
|
692
|
-
ctx.metadata = { pingInterval };
|
|
693
|
-
},
|
|
694
|
-
|
|
695
|
-
onClose: async (socket, ctx) => {
|
|
696
|
-
// Clear ping interval
|
|
697
|
-
clearInterval(ctx.metadata?.pingInterval);
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### 2. Rate Limiting
|
|
703
|
-
|
|
704
|
-
```typescript
|
|
705
|
-
const messageCount = new Map<string, number>();
|
|
706
|
-
|
|
707
|
-
app.ws('/ws/rate-limited', {
|
|
708
|
-
auth: async (ctx) => {
|
|
709
|
-
return { id: ctx.query.userId as string };
|
|
710
|
-
},
|
|
711
|
-
|
|
712
|
-
onMessage: async (socket, message, ctx) => {
|
|
713
|
-
const userId = ctx.user.id;
|
|
714
|
-
const count = (messageCount.get(userId) || 0) + 1;
|
|
715
|
-
messageCount.set(userId, count);
|
|
716
|
-
|
|
717
|
-
// Reset count every minute
|
|
718
|
-
setTimeout(() => {
|
|
719
|
-
messageCount.set(userId, Math.max(0, (messageCount.get(userId) || 0) - 1));
|
|
720
|
-
}, 60000);
|
|
721
|
-
|
|
722
|
-
// Limit: 100 messages per minute
|
|
723
|
-
if (count > 100) {
|
|
724
|
-
socket.send(JSON.stringify({
|
|
725
|
-
type: 'error',
|
|
726
|
-
message: 'Rate limit exceeded'
|
|
727
|
-
}));
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Process message...
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
### 3. Message Validation
|
|
737
|
-
|
|
738
|
-
```typescript
|
|
739
|
-
import { z } from 'zod';
|
|
740
|
-
|
|
741
|
-
const ChatMessageSchema = z.object({
|
|
742
|
-
type: z.literal('chat'),
|
|
743
|
-
room: z.string().optional(),
|
|
744
|
-
text: z.string().min(1).max(1000)
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
const JoinRoomSchema = z.object({
|
|
748
|
-
type: z.literal('join_room'),
|
|
749
|
-
room: z.string().min(1).max(50)
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
const MessageSchema = z.discriminatedUnion('type', [
|
|
753
|
-
ChatMessageSchema,
|
|
754
|
-
JoinRoomSchema
|
|
755
|
-
]);
|
|
756
|
-
|
|
757
|
-
app.ws('/ws/validated', {
|
|
758
|
-
onMessage: async (socket, message, ctx) => {
|
|
759
|
-
const result = MessageSchema.safeParse(message);
|
|
760
|
-
|
|
761
|
-
if (!result.success) {
|
|
762
|
-
socket.send(JSON.stringify({
|
|
763
|
-
type: 'error',
|
|
764
|
-
message: 'Invalid message format',
|
|
765
|
-
errors: result.error.errors
|
|
766
|
-
}));
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const validatedMessage = result.data;
|
|
771
|
-
|
|
772
|
-
// Now TypeScript knows the exact type
|
|
773
|
-
switch (validatedMessage.type) {
|
|
774
|
-
case 'chat':
|
|
775
|
-
// validatedMessage.text is guaranteed to exist
|
|
776
|
-
break;
|
|
777
|
-
case 'join_room':
|
|
778
|
-
// validatedMessage.room is guaranteed to exist
|
|
779
|
-
break;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
});
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
## WebSocket vs HTTP
|
|
786
|
-
|
|
787
|
-
| Feature | WebSocket | HTTP |
|
|
788
|
-
|---------|-----------|------|
|
|
789
|
-
| Connection | Persistent | Request/Response |
|
|
790
|
-
| Direction | Bidirectional | Client → Server |
|
|
791
|
-
| Overhead | Low (after handshake) | High (headers per request) |
|
|
792
|
-
| Use Case | Real-time, streaming | CRUD operations |
|
|
793
|
-
| Scaling | More complex | Easier |
|
|
794
|
-
|
|
795
|
-
**Gunakan WebSocket untuk:**
|
|
796
|
-
- Chat applications
|
|
797
|
-
- Live notifications
|
|
798
|
-
- Real-time dashboards
|
|
799
|
-
- Multiplayer games
|
|
800
|
-
- Collaborative editing
|
|
801
|
-
- Live streaming data
|
|
802
|
-
|
|
803
|
-
**Gunakan HTTP untuk:**
|
|
804
|
-
- CRUD operations
|
|
805
|
-
- File uploads
|
|
806
|
-
- Authentication endpoints
|
|
807
|
-
- One-time requests
|
|
808
|
-
|
|
809
|
-
---
|
|
810
|
-
|
|
811
|
-
**Related:**
|
|
812
|
-
- [03-routing.md](./03-routing.md) - HTTP Routing
|
|
813
|
-
- [19-class-based-routing.md](./19-class-based-routing.md) - Class-based Routes
|