@eleven-am/pondsocket-nest 0.0.129 → 0.0.130
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 +431 -443
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,514 +1,502 @@
|
|
|
1
|
-
# PondSocket
|
|
1
|
+
# PondSocket NestJS Integration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a NestJS integration layer for PondSocket, making it easy to use PondSocket's real-time WebSocket functionality within NestJS applications.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
To integrate PondSocket into your Node.js project, simply install it via npm:
|
|
8
|
-
|
|
9
7
|
```bash
|
|
10
|
-
npm install @eleven-am/pondsocket-
|
|
8
|
+
npm install @eleven-am/pondsocket-nest
|
|
11
9
|
```
|
|
12
10
|
|
|
13
11
|
## Overview
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
This package integrates PondSocket's powerful WebSocket capabilities with NestJS's architecture and dependency injection system. It provides decorators and services that make it natural to use WebSocket functionality in a NestJS application while maintaining all of PondSocket's features.
|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## Key Features
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
- **NestJS Integration**: Seamless integration with NestJS's module system and dependency injection
|
|
18
|
+
- **Decorator-based API**: Use familiar NestJS-style decorators for WebSocket endpoints and channels
|
|
19
|
+
- **Guard Support**: Full integration with NestJS guards for authentication and authorization
|
|
20
|
+
- **Pipe Support**: Use NestJS pipes for data transformation and validation
|
|
21
|
+
- **Type Safety**: Complete TypeScript support with proper type definitions
|
|
22
|
+
- **Distributed Support**: Maintains PondSocket's distributed backend capabilities
|
|
23
|
+
- **Automatic Discovery**: Uses NestJS's discovery service to automatically find and manage WebSocket endpoints
|
|
24
|
+
|
|
25
|
+
## Basic Usage
|
|
26
|
+
|
|
27
|
+
### Module Setup
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Module } from '@nestjs/common';
|
|
31
|
+
import { PondSocketModule } from '@eleven-am/pondsocket-nest';
|
|
32
|
+
|
|
33
|
+
@Module({
|
|
34
|
+
imports: [
|
|
35
|
+
PondSocketModule.forRoot({
|
|
36
|
+
guards: [AuthGuard], // Optional: Global guards
|
|
37
|
+
pipes: [ValidationPipe], // Optional: Global pipes
|
|
38
|
+
isGlobal: true, // Optional: Make the module global
|
|
39
|
+
})
|
|
40
|
+
]
|
|
41
|
+
})
|
|
42
|
+
export class AppModule {}
|
|
43
|
+
```
|
|
20
44
|
|
|
21
|
-
|
|
22
|
-
import PondSocket from "@eleven-am/pondsocket";
|
|
45
|
+
### Creating WebSocket Endpoints
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
```typescript
|
|
48
|
+
import { Controller } from '@nestjs/common';
|
|
49
|
+
import { PondSocketEndpoint, PondSocketConnection, Context } from '@eleven-am/pondsocket-nest';
|
|
25
50
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
@PondSocketEndpoint('/api/socket')
|
|
52
|
+
export class SocketController {
|
|
53
|
+
@PondSocketConnection()
|
|
54
|
+
async handleConnection(ctx: Context) {
|
|
55
|
+
const token = ctx.request.query.token;
|
|
56
|
+
|
|
57
|
+
if (isValidToken(token)) {
|
|
58
|
+
const role = getRoleFromToken(token);
|
|
59
|
+
ctx.accept({ role });
|
|
60
|
+
} else {
|
|
61
|
+
ctx.reject('Invalid token', 401);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
33
66
|
|
|
34
|
-
|
|
35
|
-
import pondSocket from "@eleven-am/pondsocket/express";
|
|
36
|
-
import express from "express";
|
|
67
|
+
### Creating Channels
|
|
37
68
|
|
|
38
|
-
|
|
69
|
+
```typescript
|
|
70
|
+
import { Controller } from '@nestjs/common';
|
|
71
|
+
import { PondSocketChannel, PondSocketJoin, Context } from '@eleven-am/pondsocket-nest';
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
@PondSocketChannel('/channel/:id')
|
|
74
|
+
export class ChannelController {
|
|
75
|
+
@PondSocketJoin()
|
|
76
|
+
async handleJoin(ctx: Context) {
|
|
77
|
+
const { role } = ctx.user.assigns;
|
|
78
|
+
const { username } = ctx.joinParams;
|
|
79
|
+
const { id } = ctx.event.params;
|
|
43
80
|
|
|
44
|
-
|
|
81
|
+
if (role === 'admin') {
|
|
82
|
+
ctx.accept({ username })
|
|
83
|
+
.trackPresence({
|
|
84
|
+
username,
|
|
85
|
+
role,
|
|
86
|
+
status: 'online',
|
|
87
|
+
onlineSince: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
ctx.decline('Insufficient permissions', 403);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
45
94
|
```
|
|
46
95
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
96
|
+
### Handling Channel Events
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { Controller } from '@nestjs/common';
|
|
100
|
+
import { PondSocketEvent, Context } from '@eleven-am/pondsocket-nest';
|
|
101
|
+
|
|
102
|
+
@PondSocketChannel('/channel/:id')
|
|
103
|
+
export class ChannelController {
|
|
104
|
+
@PondSocketEvent('message')
|
|
105
|
+
async handleMessage(ctx: Context) {
|
|
106
|
+
const { text } = ctx.event.payload;
|
|
107
|
+
|
|
108
|
+
// Broadcast to all users in the channel
|
|
109
|
+
ctx.broadcast('message', { text });
|
|
110
|
+
|
|
111
|
+
// Broadcast to specific users
|
|
112
|
+
ctx.broadcastTo(['user1', 'user2'], 'message', { text });
|
|
113
|
+
|
|
114
|
+
// Broadcast to all except sender
|
|
115
|
+
ctx.broadcastFrom('message', { text });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
53
118
|
```
|
|
54
119
|
|
|
55
|
-
##
|
|
120
|
+
## Advanced Features
|
|
56
121
|
|
|
57
|
-
|
|
122
|
+
### Presence Management
|
|
58
123
|
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
124
|
+
```typescript
|
|
125
|
+
@PondSocketChannel('/channel/:id')
|
|
126
|
+
export class ChannelController {
|
|
127
|
+
@PondSocketEvent('presence')
|
|
128
|
+
async handlePresence(ctx: Context) {
|
|
129
|
+
ctx.trackPresence({
|
|
130
|
+
username: ctx.user.assigns.username,
|
|
131
|
+
status: 'online',
|
|
132
|
+
lastSeen: Date.now()
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
64
136
|
```
|
|
65
137
|
|
|
66
|
-
|
|
138
|
+
### User Assigns
|
|
67
139
|
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
|
|
140
|
+
```typescript
|
|
141
|
+
@PondSocketChannel('/channel/:id')
|
|
142
|
+
export class ChannelController {
|
|
143
|
+
@PondSocketEvent('update-profile')
|
|
144
|
+
async handleProfileUpdate(ctx: Context) {
|
|
145
|
+
ctx.assign({
|
|
146
|
+
...ctx.user.assigns,
|
|
147
|
+
profile: ctx.event.payload
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
71
151
|
```
|
|
72
152
|
|
|
73
|
-
###
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
153
|
+
### Error Handling
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
@PondSocketChannel('/channel/:id')
|
|
157
|
+
export class ChannelController {
|
|
158
|
+
@PondSocketEvent('message')
|
|
159
|
+
async handleMessage(ctx: Context) {
|
|
160
|
+
try {
|
|
161
|
+
// Your logic here
|
|
162
|
+
ctx.accept();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
ctx.decline(error.message, 400);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
79
168
|
```
|
|
80
169
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
170
|
+
## Distributed Deployment
|
|
171
|
+
|
|
172
|
+
The package maintains PondSocket's distributed deployment capabilities:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Module } from '@nestjs/common';
|
|
176
|
+
import { PondSocketModule } from '@eleven-am/pondsocket-nest';
|
|
177
|
+
import { RedisBackend } from '@eleven-am/pondsocket';
|
|
178
|
+
|
|
179
|
+
@Module({
|
|
180
|
+
imports: [
|
|
181
|
+
PondSocketModule.forRoot({
|
|
182
|
+
backend: new RedisBackend({
|
|
183
|
+
host: 'localhost',
|
|
184
|
+
port: 6379
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
]
|
|
188
|
+
})
|
|
189
|
+
export class AppModule {}
|
|
190
|
+
```
|
|
91
191
|
|
|
92
|
-
|
|
192
|
+
### Distributed Mode Features
|
|
193
|
+
|
|
194
|
+
The distributed mode enables you to scale your WebSocket application across multiple server instances while maintaining state synchronization. Here are the key features:
|
|
195
|
+
|
|
196
|
+
1. **State Synchronization**
|
|
197
|
+
- Channel presence is synchronized across all instances
|
|
198
|
+
- User assigns are shared between instances
|
|
199
|
+
- Channel events are broadcasted to all instances
|
|
200
|
+
|
|
201
|
+
2. **Load Balancing**
|
|
202
|
+
- Multiple server instances can handle WebSocket connections
|
|
203
|
+
- Connections are distributed across available instances
|
|
204
|
+
- Automatic failover if an instance goes down
|
|
205
|
+
|
|
206
|
+
3. **Backend Options**
|
|
207
|
+
```typescript
|
|
208
|
+
// Redis Backend (Recommended for production)
|
|
209
|
+
import { RedisBackend } from '@eleven-am/pondsocket';
|
|
210
|
+
|
|
211
|
+
PondSocketModule.forRoot({
|
|
212
|
+
backend: new RedisBackend({
|
|
213
|
+
host: 'localhost',
|
|
214
|
+
port: 6379,
|
|
215
|
+
password: 'optional-password',
|
|
216
|
+
db: 0,
|
|
217
|
+
keyPrefix: 'pondsocket:', // Optional prefix for Redis keys
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// Memory Backend (For development/testing)
|
|
222
|
+
import { MemoryBackend } from '@eleven-am/pondsocket';
|
|
223
|
+
|
|
224
|
+
PondSocketModule.forRoot({
|
|
225
|
+
backend: new MemoryBackend()
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
4. **Configuration Options**
|
|
230
|
+
```typescript
|
|
231
|
+
interface DistributedBackendOptions {
|
|
232
|
+
// Redis specific options
|
|
233
|
+
host?: string;
|
|
234
|
+
port?: number;
|
|
235
|
+
password?: string;
|
|
236
|
+
db?: number;
|
|
237
|
+
keyPrefix?: string;
|
|
238
|
+
|
|
239
|
+
// General options
|
|
240
|
+
reconnectInterval?: number; // Time between reconnection attempts
|
|
241
|
+
maxRetries?: number; // Maximum number of reconnection attempts
|
|
242
|
+
timeout?: number; // Operation timeout in milliseconds
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
5. **Error Handling**
|
|
247
|
+
```typescript
|
|
248
|
+
@PondSocketChannel('/channel/:id')
|
|
249
|
+
export class ChannelController {
|
|
250
|
+
@PondSocketEvent('message')
|
|
251
|
+
async handleMessage(ctx: Context) {
|
|
252
|
+
try {
|
|
253
|
+
// Your logic here
|
|
254
|
+
ctx.accept();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// Handle distributed backend errors
|
|
257
|
+
if (error instanceof DistributedBackendError) {
|
|
258
|
+
// Handle specific distributed backend errors
|
|
259
|
+
ctx.decline('Backend error occurred', 500);
|
|
260
|
+
} else {
|
|
261
|
+
ctx.decline(error.message, 400);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
6. **Health Checks**
|
|
269
|
+
```typescript
|
|
270
|
+
import { PondSocketService } from '@eleven-am/pondsocket-nest';
|
|
271
|
+
|
|
272
|
+
@Controller('health')
|
|
273
|
+
export class HealthController {
|
|
274
|
+
constructor(private readonly pondSocketService: PondSocketService) {}
|
|
275
|
+
|
|
276
|
+
@Get('websocket')
|
|
277
|
+
async checkWebSocketHealth() {
|
|
278
|
+
const isHealthy = await this.pondSocketService.isHealthy();
|
|
279
|
+
return {
|
|
280
|
+
status: isHealthy ? 'healthy' : 'unhealthy',
|
|
281
|
+
timestamp: new Date().toISOString()
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
7. **Best Practices**
|
|
288
|
+
- Use Redis backend in production environments
|
|
289
|
+
- Implement proper error handling for distributed operations
|
|
290
|
+
- Monitor backend connection health
|
|
291
|
+
- Use appropriate Redis configuration for your scale
|
|
292
|
+
- Consider using Redis Cluster for high availability
|
|
293
|
+
- Implement proper logging for distributed operations
|
|
294
|
+
|
|
295
|
+
8. **Scaling Considerations**
|
|
296
|
+
- Monitor Redis memory usage
|
|
297
|
+
- Implement proper cleanup of stale data
|
|
298
|
+
- Consider using Redis Cluster for larger deployments
|
|
299
|
+
- Implement proper error handling and retry mechanisms
|
|
300
|
+
- Monitor network latency between instances
|
|
301
|
+
- Implement proper logging and monitoring
|
|
302
|
+
|
|
303
|
+
## Configuration Options
|
|
304
|
+
|
|
305
|
+
The `PondSocketModule.forRoot()` method accepts the following options:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
interface PondSocketOptions {
|
|
309
|
+
guards?: any[]; // Global guards
|
|
310
|
+
pipes?: any[]; // Global pipes
|
|
311
|
+
providers?: any[]; // Additional providers
|
|
312
|
+
imports?: any[]; // Additional imports
|
|
313
|
+
exports?: any[]; // Additional exports
|
|
314
|
+
isGlobal?: boolean; // Make the module global
|
|
315
|
+
isExclusiveSocketServer?: boolean; // Use exclusive socket server
|
|
316
|
+
backend?: IDistributedBackend; // Distributed backend
|
|
317
|
+
}
|
|
318
|
+
```
|
|
93
319
|
|
|
94
|
-
|
|
320
|
+
## Client Usage
|
|
95
321
|
|
|
96
|
-
|
|
322
|
+
The client-side usage remains the same as the core PondSocket package:
|
|
97
323
|
|
|
98
|
-
```
|
|
324
|
+
```typescript
|
|
99
325
|
import PondClient from "@eleven-am/pondsocket-client";
|
|
100
326
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Your authenticated user's token (replace with actual token)
|
|
105
|
-
const authToken = 'your-auth-token';
|
|
106
|
-
|
|
107
|
-
// Your username (replace with actual username)
|
|
108
|
-
const username = 'user123';
|
|
109
|
-
|
|
110
|
-
// Create a new PondClient instance
|
|
111
|
-
const socket = new PondClient(serverUrl, { token: authToken });
|
|
327
|
+
const socket = new PondClient('ws://your-server/api/socket', {
|
|
328
|
+
token: 'your-auth-token'
|
|
329
|
+
});
|
|
112
330
|
|
|
113
|
-
// Connect to the server
|
|
114
331
|
socket.connect();
|
|
115
332
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (connected) {
|
|
119
|
-
console.log('Connected to the server.');
|
|
120
|
-
} else {
|
|
121
|
-
console.log('Disconnected from the server.');
|
|
122
|
-
}
|
|
333
|
+
const channel = socket.createChannel('/channel/123', {
|
|
334
|
+
username: 'user123'
|
|
123
335
|
});
|
|
124
336
|
|
|
125
|
-
// Create a channel and join it
|
|
126
|
-
const channel = socket.createChannel('/channel/123', { username });
|
|
127
337
|
channel.join();
|
|
128
338
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
channel.broadcast('message', { text: message });
|
|
132
|
-
|
|
133
|
-
// Handle received messages
|
|
134
|
-
// Certain methods in the channel instance returns a subscription function, which can be used to unsubscribe from the event
|
|
135
|
-
const subscription = channel.onMessage((event, message) => {
|
|
136
|
-
console.log(`Received message from server: ${message.text}`);
|
|
339
|
+
channel.onMessage((event, message) => {
|
|
340
|
+
console.log(`Received message: ${message.text}`);
|
|
137
341
|
});
|
|
138
342
|
|
|
139
|
-
|
|
140
|
-
subscription();
|
|
343
|
+
channel.broadcast('message', { text: 'Hello, PondSocket!' });
|
|
141
344
|
```
|
|
142
345
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
### Server-side Example with Authentication and check for profanity before broadcasting
|
|
146
|
-
|
|
147
|
-
To create a PondSocket server that accepts authenticated connections and checks for profanity before broadcasting messages, follow the steps below:
|
|
148
|
-
|
|
149
|
-
```javascript
|
|
150
|
-
import PondSocket from "@eleven-am/pondsocket";
|
|
151
|
-
|
|
152
|
-
// Helper functions for token validation
|
|
153
|
-
function isValidToken(token) {
|
|
154
|
-
// Implement your token validation logic here
|
|
155
|
-
// Return true if the token is valid, false otherwise
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getRoleFromToken(token) {
|
|
160
|
-
// Implement the logic to extract the user's role from the token
|
|
161
|
-
// Return the user's role
|
|
162
|
-
return 'user';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isTextProfane(text) {
|
|
166
|
-
// Implement your profanity check logic here
|
|
167
|
-
// Return true if the text is profane, false otherwise
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getMessagesFromDatabase(channelId) {
|
|
172
|
-
// Implement your logic to retrieve messages from the database
|
|
173
|
-
// Return an array of messages
|
|
174
|
-
return [];
|
|
175
|
-
}
|
|
346
|
+
## Contributing
|
|
176
347
|
|
|
177
|
-
|
|
348
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
178
349
|
|
|
179
|
-
|
|
180
|
-
const endpoint = pond.createEndpoint('/api/socket', (req, res) => {
|
|
181
|
-
// Depending if the user already has cookies set, they can be accessed from the request headers or the request address
|
|
182
|
-
const token = req.query.token; // If the token is passed as a query parameter
|
|
183
|
-
|
|
184
|
-
// Perform token validation here
|
|
185
|
-
if (isValidToken(token)) {
|
|
186
|
-
// Extract the authenticated user's username
|
|
187
|
-
const role = getRoleFromToken(token);
|
|
188
|
-
|
|
189
|
-
// Handle socket connection and authentication for valid users
|
|
190
|
-
res.accept({role}); // Assign the user's role to the socket
|
|
191
|
-
} else {
|
|
192
|
-
// Reject the connection for invalid users or without a token
|
|
193
|
-
res.decline('Invalid token', 401);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Create a channel, providing a callback that is called when a user attempts to join the channel
|
|
198
|
-
const profanityChannel = endpoint.createChannel('/channel/:id', async (req, res) => {
|
|
199
|
-
// When joining the channel, any joinParams passed from the client will be available in the request payload
|
|
200
|
-
// Also any previous assigns on the socket will be available in the request payload as well
|
|
201
|
-
const {role} = req.user.assigns;
|
|
202
|
-
const {username} = req.joinParams;
|
|
203
|
-
const {id} = req.event.params;
|
|
350
|
+
## License
|
|
204
351
|
|
|
205
|
-
|
|
206
|
-
|
|
352
|
+
This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
|
|
353
|
+
|
|
354
|
+
## Return Type Functionality
|
|
355
|
+
|
|
356
|
+
The NestJS integration provides a powerful return type system that allows you to declaratively specify actions to be taken when handling WebSocket events. Instead of using the context object directly, you can return an object with specific properties to trigger various actions.
|
|
357
|
+
|
|
358
|
+
### Return Type Interface
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
type NestFuncType<Event extends string, Payload extends PondMessage, Presence extends PondPresence, Assigns extends PondAssigns = PondAssigns> = {
|
|
362
|
+
// Send an event to the user
|
|
363
|
+
event?: Event;
|
|
364
|
+
|
|
365
|
+
// Broadcast to all users in the channel
|
|
366
|
+
broadcast?: Event;
|
|
367
|
+
|
|
368
|
+
// Broadcast to all users except the sender
|
|
369
|
+
broadcastFrom?: Event;
|
|
370
|
+
|
|
371
|
+
// Update user assigns
|
|
372
|
+
assigns?: Partial<Assigns>;
|
|
373
|
+
|
|
374
|
+
// Update user presence
|
|
375
|
+
presence?: Presence;
|
|
376
|
+
} & Payload;
|
|
377
|
+
```
|
|
207
378
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
379
|
+
### Usage Examples
|
|
380
|
+
|
|
381
|
+
#### Channel Join with Multiple Actions
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
@PondSocketChannel('/chat/:roomId')
|
|
385
|
+
export class ChatController {
|
|
386
|
+
@PondSocketJoin()
|
|
387
|
+
async handleJoin(ctx: Context) {
|
|
388
|
+
const { username } = ctx.joinParams;
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
// Send welcome message to the joining user
|
|
392
|
+
event: 'welcome',
|
|
393
|
+
message: 'Welcome to the chat!',
|
|
394
|
+
|
|
395
|
+
// Broadcast join notification to all users
|
|
396
|
+
broadcast: 'user_joined',
|
|
397
|
+
username,
|
|
398
|
+
timestamp: Date.now(),
|
|
399
|
+
|
|
400
|
+
// Update user's assigns
|
|
401
|
+
assigns: {
|
|
402
|
+
username,
|
|
403
|
+
joinedAt: Date.now(),
|
|
404
|
+
role: 'member'
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
// Update user's presence
|
|
408
|
+
presence: {
|
|
214
409
|
username,
|
|
215
|
-
role,
|
|
216
410
|
status: 'online',
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.reply('history', {messages});
|
|
221
|
-
} else {
|
|
222
|
-
// Reject the join request
|
|
223
|
-
res.decline('You do not have the required role to join this channel', 403);
|
|
411
|
+
lastSeen: Date.now()
|
|
412
|
+
}
|
|
413
|
+
};
|
|
224
414
|
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Attach message event listener to the profanityChannel
|
|
228
|
-
profanityChannel.onEvent('message', (req, res) => {
|
|
229
|
-
const {text} = req.event.payload;
|
|
230
|
-
|
|
231
|
-
// Check for profanity
|
|
232
|
-
if (isTextProfane(text)) {
|
|
233
|
-
// Reject the message if it contains profanity
|
|
234
|
-
res.decline('Profanity is not allowed', 400, {
|
|
235
|
-
profanityCount: req.user.assigns.profanityCount + 1
|
|
236
|
-
});
|
|
415
|
+
}
|
|
416
|
+
```
|
|
237
417
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
418
|
+
#### Message Handling
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
@PondSocketChannel('/chat/:roomId')
|
|
422
|
+
export class ChatController {
|
|
423
|
+
@PondSocketEvent('message')
|
|
424
|
+
async handleMessage(ctx: Context) {
|
|
425
|
+
const { text } = ctx.event.payload;
|
|
426
|
+
const { username } = ctx.user.assigns;
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
// Broadcast the message to all users
|
|
430
|
+
broadcast: 'message',
|
|
431
|
+
text,
|
|
432
|
+
username,
|
|
433
|
+
timestamp: Date.now(),
|
|
434
|
+
|
|
435
|
+
// Update user's last message timestamp
|
|
436
|
+
assigns: {
|
|
437
|
+
lastMessageAt: Date.now()
|
|
438
|
+
}
|
|
439
|
+
};
|
|
251
440
|
}
|
|
252
|
-
|
|
253
|
-
// for more complete access to the channel, you can use the channel instance
|
|
254
|
-
// const channel = req.channel;
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
profanityChannel.onEvent('presence/:presence', (req, res) => {
|
|
258
|
-
const {presence} = req.event.params;
|
|
259
|
-
const {username} = req.user.assigns;
|
|
260
|
-
|
|
261
|
-
// Handle presence events
|
|
262
|
-
res.updatePresence({
|
|
263
|
-
username,
|
|
264
|
-
role,
|
|
265
|
-
onlineSince: Date.now(),
|
|
266
|
-
status: presence,
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
profanityChannel.onLeave((event) => {
|
|
271
|
-
const {username} = event.assigns;
|
|
272
|
-
|
|
273
|
-
// When a user leaves the channel, PondSocket will automatically remove the user from the presence list and inform other users in the channel
|
|
274
|
-
|
|
275
|
-
// perform a cleanup operation here
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// Start the server
|
|
279
|
-
pond.listen(3000, () => {
|
|
280
|
-
console.log('PondSocket server listening on port 3000');
|
|
281
|
-
});
|
|
441
|
+
}
|
|
282
442
|
```
|
|
283
443
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
- `reject(message?: string, errorCode?: number): void`: Rejects the request with the given error message and optional error code.
|
|
311
|
-
|
|
312
|
-
- `send(event: string, payload: PondMessage, assigns?: PondAssigns): void`: Emits a direct message to the client with the specified event and payload.
|
|
313
|
-
|
|
314
|
-
### Endpoint
|
|
315
|
-
|
|
316
|
-
The `Endpoint` class represents an endpoint in the PondSocket server where channels can be created.
|
|
317
|
-
|
|
318
|
-
**Methods:**
|
|
319
|
-
|
|
320
|
-
- `createChannel<Path extends string>(path: PondPath<Path>, handler: (request: JoinRequest<Path>, response: JoinResponse) => void | Promise<void>): PondChannel`: Adds a new PondChannel to this path on this endpoint with the provided handler function to authenticate the client.
|
|
321
|
-
|
|
322
|
-
- `broadcast(event: string, payload: PondMessage): void`: Broadcasts a message to all clients connected to this endpoint with the specified event and payload.
|
|
323
|
-
|
|
324
|
-
- `closeConnection(clientIds: string | string[]): void`: Closes specific clients connected to this endpoint identified by the provided clientIds.
|
|
325
|
-
|
|
326
|
-
### JoinRequest
|
|
327
|
-
|
|
328
|
-
The `JoinRequest` class represents the request object when a client joins a channel.
|
|
329
|
-
|
|
330
|
-
**Properties:**
|
|
331
|
-
|
|
332
|
-
- `event: PondEvent<Path>`: The event associated with the request.
|
|
333
|
-
|
|
334
|
-
- `channelName: string`: The name of the channel.
|
|
335
|
-
|
|
336
|
-
- `assigns: UserAssigns`: The assigns data for the client.
|
|
337
|
-
|
|
338
|
-
- `presence: UserPresences`: The presence data for the client.
|
|
339
|
-
|
|
340
|
-
- `joinParams: JoinParams`: The join parameters for the client.
|
|
341
|
-
|
|
342
|
-
- `user: UserData`: The user data associated with the client.
|
|
343
|
-
|
|
344
|
-
- `channel: Channel`: The Channel instance associated with the request.
|
|
345
|
-
|
|
346
|
-
### JoinResponse
|
|
347
|
-
|
|
348
|
-
The `JoinResponse` class represents the response object for the join request.
|
|
349
|
-
|
|
350
|
-
**Methods:**
|
|
351
|
-
|
|
352
|
-
- `accept(assigns?: PondAssigns): JoinResponse`: Accepts the join request and optionally assigns data to the client.
|
|
353
|
-
|
|
354
|
-
- `reject(message?: string, errorCode?: number): JoinResponse`: Rejects the join request with the given error message and optional error code.
|
|
355
|
-
|
|
356
|
-
- `send(event: string, payload: PondMessage, assigns?: PondAssigns): JoinResponse`: Emits a direct message to the client with the specified event, payload, and optional assigns data.
|
|
357
|
-
|
|
358
|
-
- `broadcast(event: string, payload: PondMessage): JoinResponse`: Emits a message to all clients in the channel with the specified event and payload.
|
|
359
|
-
|
|
360
|
-
- `broadcastFromUser(event: string, payload: PondMessage): JoinResponse`: Emits a message to all clients in the channel except the sender with the specified event and payload.
|
|
361
|
-
|
|
362
|
-
- `sendToUsers(event: string, payload: PondMessage, userIds: string[]): JoinResponse`: Emits a message to a specific set of clients identified by the provided userIds with the specified event and payload.
|
|
363
|
-
|
|
364
|
-
- `trackPresence(presence: PondPresence): JoinResponse`: Tracks the presence of the client in the channel.
|
|
365
|
-
|
|
366
|
-
### PondChannel
|
|
367
|
-
|
|
368
|
-
The `PondChannel` class represents a Generic channel in the PondSocket server. It is used to create a channel whose path matches the provided PondPath.
|
|
369
|
-
|
|
370
|
-
**Methods:**
|
|
371
|
-
|
|
372
|
-
- `onEvent<Event extends string>(event: PondPath<Event>, handler: (request: EventRequest<Event>, response: EventResponse) => void | Promise<void>): void`: Handles an event request made by a user for the specified event with the provided handler function.
|
|
373
|
-
|
|
374
|
-
- `broadcast(event: string, payload: PondMessage, channelName?: string): void`: Broadcasts a message to all users in the channel with the specified event and payload. Optionally, a specific channel name can be provided to broadcast the message only to users in that channel.
|
|
375
|
-
|
|
376
|
-
- `onLeave(handler: (event: LeaveEvent) => void | Promise<void>): void`: Handles a leave event for the channel with the provided handler function when a user leaves the channel.
|
|
377
|
-
|
|
378
|
-
### EventRequest
|
|
379
|
-
|
|
380
|
-
The `EventRequest` class represents the request object when an event is received from a client.
|
|
381
|
-
|
|
382
|
-
**Properties:**
|
|
383
|
-
|
|
384
|
-
- `event: PondEvent<Path>`: The event associated with the request.
|
|
385
|
-
|
|
386
|
-
- `channelName: string`: The name of the channel.
|
|
387
|
-
|
|
388
|
-
- `assigns: UserAssigns`: The assigns data for the client.
|
|
389
|
-
|
|
390
|
-
- `presence: UserPresences`: The presence data for the client.
|
|
391
|
-
|
|
392
|
-
- `user: UserData`: The user data associated with the client.
|
|
393
|
-
|
|
394
|
-
- `channel: Channel`: The Channel instance associated with the request.
|
|
395
|
-
|
|
396
|
-
### EventResponse
|
|
397
|
-
|
|
398
|
-
The `EventResponse` class represents the response object for handling events from clients.
|
|
399
|
-
|
|
400
|
-
**Methods:**
|
|
401
|
-
|
|
402
|
-
- `accept(assigns?: PondAssigns): EventResponse`: Accepts the request and optionally assigns data to the client.
|
|
403
|
-
|
|
404
|
-
- `reject(message?: string, errorCode?: number, assigns?: PondAssigns): EventResponse`: Rejects the request with the given error message, optional error code, and optional assigns data.
|
|
405
|
-
|
|
406
|
-
- `send(event: string, payload: PondMessage, assigns?: PondAssigns): void`: Emits a direct message to the client with the specified event, payload, and optional assigns data.
|
|
407
|
-
|
|
408
|
-
- `broadcast(event: string, payload: PondMessage): EventResponse`: Sends a message to all clients in the channel with the specified event and payload.
|
|
409
|
-
|
|
410
|
-
- `broadcastFromUser(event: string, payload: PondMessage): EventResponse`: Sends a message to all clients in the channel except the sender with the specified event and payload.
|
|
411
|
-
|
|
412
|
-
- `sendToUsers(event: string, payload: PondMessage, userIds: string[]): EventResponse`: Sends a message to a specific set of clients identified by the provided userIds with the specified event and payload.
|
|
413
|
-
|
|
414
|
-
- `trackPresence(presence: PondPresence, userId?: string): EventResponse`: Tracks a user's presence in the channel.
|
|
415
|
-
|
|
416
|
-
- `updatePresence(presence: PondPresence, userId?: string): EventResponse`: Updates a user's presence in the channel.
|
|
417
|
-
|
|
418
|
-
- `unTrackPresence(userId?: string): EventResponse`: Removes a user's presence from the channel.
|
|
419
|
-
|
|
420
|
-
- `evictUser(reason: string, userId?: string): void`: Evicts a user from the channel.
|
|
421
|
-
|
|
422
|
-
- `closeChannel(reason: string): void`: Closes the channel from the server-side for all clients.
|
|
423
|
-
|
|
424
|
-
### Channel
|
|
425
|
-
|
|
426
|
-
The `Channel` class represents a single Channel created by the PondSocket server. Note that a PondChannel can have multiple channels associated with it.
|
|
427
|
-
|
|
428
|
-
**Methods:**
|
|
429
|
-
|
|
430
|
-
- `name: string`: The name of the channel.
|
|
431
|
-
|
|
432
|
-
- `getAssigns: UserAssigns`: Gets the current assign data for the client.
|
|
433
|
-
|
|
434
|
-
- `getUserData(userId: string): UserData`: Gets the assign data for a specific user identified by the provided `userId`.
|
|
435
|
-
|
|
436
|
-
- `broadcastMessage(event: string, payload: PondMessage): void`: Broadcasts a message to every client in the channel with the specified event and payload.
|
|
437
|
-
|
|
438
|
-
- `sendToUser(userId: string, event: string, payload: PondMessage): void`: Sends a message to a specific client in the channel identified by the provided `userId`, with the specified event and payload.
|
|
439
|
-
|
|
440
|
-
- `sendToUsers(userIdS: string[], event: string, payload: PondMessage): void`: Sends a message to a specific set of clients identified by the provided `userIdS`, with the specified event and payload.
|
|
441
|
-
|
|
442
|
-
- `evictUser(userId: string, reason?: string): void`: Bans a user from the channel identified by the provided `userId`. Optionally, you can provide a `reason` for the ban.
|
|
443
|
-
|
|
444
|
-
- `trackPresence(userId: string, presence: PondPresence): void`: Tracks a user's presence in the channel identified by the provided `userId`.
|
|
445
|
-
|
|
446
|
-
- `removePresence(userId: string): void`: Removes a user's presence from the channel identified by the provided `userId`.
|
|
447
|
-
|
|
448
|
-
- `updatePresence(userId: string, presence: PondPresence): void`: Updates a user's presence in the channel identified by the provided `userId`.
|
|
449
|
-
|
|
450
|
-
### PondClient
|
|
451
|
-
|
|
452
|
-
The `PondClient` class represents a client that connects to the PondSocket server.
|
|
453
|
-
|
|
454
|
-
**Constructor:**
|
|
455
|
-
|
|
456
|
-
- `constructor(endpoint: string, params?: Record<string, any>)`: Creates a new instance of the PondClient with the provided endpoint URL and optional parameters.
|
|
457
|
-
|
|
458
|
-
**Methods:**
|
|
459
|
-
|
|
460
|
-
- `connect(backoff?: number): void`: Connects to the server with an optional backoff time.
|
|
461
|
-
|
|
462
|
-
- `getState(): boolean`: Returns the current state of the socket.
|
|
463
|
-
|
|
464
|
-
- `disconnect(): void`: Disconnects the socket.
|
|
465
|
-
|
|
466
|
-
- `createChannel(name: string, params?: JoinParams): ClientChannel`: Creates a channel with the given name and optional join parameters.
|
|
467
|
-
|
|
468
|
-
- `onConnectionChange(callback: (state: boolean) => void): Unsubscribe`: Subscribes to the connection state changes and calls the provided callback when the state changes.
|
|
469
|
-
|
|
470
|
-
### ClientChannel
|
|
471
|
-
|
|
472
|
-
The `ClientChannel` class represents a channel in the PondClient.
|
|
473
|
-
|
|
474
|
-
**Methods:**
|
|
475
|
-
|
|
476
|
-
- `join(): void`: Connects to the channel.
|
|
477
|
-
|
|
478
|
-
- `leave(): void`: Disconnects from the channel.
|
|
479
|
-
|
|
480
|
-
- `onMessage(callback: (event: string, message: PondMessage) => void): Unsubscribe`: Monitors the channel for messages and calls the provided callback when a message is received.
|
|
481
|
-
|
|
482
|
-
- `onMessageEvent(event: string, callback: (message: PondMessage) => void): Unsubscribe`: Monitors the channel for messages with the specified event and calls the provided callback when a message is received.
|
|
483
|
-
|
|
484
|
-
- `onChannelStateChange(callback: (connected: ChannelState) => void): Unsubscribe`: Monitors the channel state of the channel and calls the provided callback when the connection state changes.
|
|
485
|
-
|
|
486
|
-
- `onJoin(callback: (presence: PondPresence) => void): Unsubscribe`: Detects when clients join the channel and calls the provided callback when a client joins the channel.
|
|
487
|
-
|
|
488
|
-
- `onLeave(callback: (presence: PondPresence) => void): Unsubscribe`: Detects when clients leave the channel and calls the provided callback when a client leaves the channel.
|
|
489
|
-
|
|
490
|
-
- `onPresenceChange(callback: (presence: PresencePayload) => void): Unsubscribe`: Detects when clients change their presence in the channel and calls the provided callback when a client changes their presence in the channel.
|
|
444
|
+
#### Presence Updates
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
@PondSocketChannel('/chat/:roomId')
|
|
448
|
+
export class ChatController {
|
|
449
|
+
@PondSocketEvent('status')
|
|
450
|
+
async handleStatus(ctx: Context) {
|
|
451
|
+
const { status } = ctx.event.payload;
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
// Update user's presence
|
|
455
|
+
presence: {
|
|
456
|
+
status,
|
|
457
|
+
lastSeen: Date.now()
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
// Notify others about the status change
|
|
461
|
+
broadcastFrom: 'status_change',
|
|
462
|
+
username: ctx.user.assigns.username,
|
|
463
|
+
status,
|
|
464
|
+
timestamp: Date.now()
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
491
469
|
|
|
492
|
-
|
|
470
|
+
### Benefits
|
|
493
471
|
|
|
494
|
-
|
|
472
|
+
1. **Declarative Code**: Actions are clearly specified in the return object, making the code more readable and maintainable.
|
|
495
473
|
|
|
496
|
-
|
|
474
|
+
2. **Type Safety**: The return type is fully typed, providing excellent TypeScript support and IDE autocompletion.
|
|
497
475
|
|
|
498
|
-
|
|
476
|
+
3. **Reduced Boilerplate**: No need to call multiple context methods; all actions are specified in a single return statement.
|
|
499
477
|
|
|
500
|
-
|
|
478
|
+
4. **Flexible Combinations**: You can combine multiple actions in a single return statement, making it easy to handle complex scenarios.
|
|
501
479
|
|
|
502
|
-
|
|
480
|
+
5. **Automatic Handling**: The framework automatically processes the returned object and executes the specified actions in the correct order.
|
|
503
481
|
|
|
504
|
-
|
|
482
|
+
### Best Practices
|
|
505
483
|
|
|
506
|
-
|
|
484
|
+
1. **Type Your Returns**: Use TypeScript interfaces to define the shape of your return objects:
|
|
485
|
+
```typescript
|
|
486
|
+
interface ChatMessage {
|
|
487
|
+
text: string;
|
|
488
|
+
username: string;
|
|
489
|
+
timestamp: number;
|
|
490
|
+
}
|
|
507
491
|
|
|
508
|
-
|
|
492
|
+
@PondSocketEvent('message')
|
|
493
|
+
async handleMessage(ctx: Context): Promise<NestFuncType<'message', ChatMessage, UserPresence>> {
|
|
494
|
+
// Your implementation
|
|
495
|
+
}
|
|
496
|
+
```
|
|
509
497
|
|
|
510
|
-
|
|
498
|
+
2. **Keep It Simple**: While you can combine multiple actions, try to keep the return object focused on a single responsibility when possible.
|
|
511
499
|
|
|
512
|
-
|
|
500
|
+
3. **Use TypeScript**: Take advantage of TypeScript's type system to ensure your return objects are correctly structured.
|
|
513
501
|
|
|
514
|
-
|
|
502
|
+
4. **Handle Errors**: Remember that you can still use `ctx.decline()` for error cases where returning an object isn't appropriate.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eleven-am/pondsocket-nest",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.130",
|
|
4
4
|
"description": "PondSocket is a fast simple socket server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"socket",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"pipeline": "npm run build && npm run push"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@eleven-am/pondsocket": "^0.1.
|
|
31
|
+
"@eleven-am/pondsocket": "^0.1.212",
|
|
32
32
|
"@golevelup/nestjs-discovery": "^5.0.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|