payload-socket-plugin 1.1.3 → 1.1.5
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 +59 -99
- package/dist/socketManager.d.ts +3 -3
- package/dist/socketManager.js +20 -9
- package/dist/types.d.ts +1 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,26 +108,18 @@ await initSocketIO(server);
|
|
|
108
108
|
|
|
109
109
|
```typescript
|
|
110
110
|
// client.ts
|
|
111
|
-
import { io
|
|
112
|
-
import type {
|
|
113
|
-
ServerToClientEvents,
|
|
114
|
-
ClientToServerEvents,
|
|
115
|
-
} from "payload-socket-plugin";
|
|
111
|
+
import { io } from "socket.io-client";
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
token: "your-jwt-token", // Get from Payload login
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
);
|
|
113
|
+
const socket = io("http://localhost:3000", {
|
|
114
|
+
auth: {
|
|
115
|
+
token: "your-jwt-token", // Get from Payload login
|
|
116
|
+
},
|
|
117
|
+
});
|
|
126
118
|
|
|
127
|
-
// Subscribe to collection events
|
|
119
|
+
// Subscribe to collection events
|
|
128
120
|
socket.emit("join-collection", "posts");
|
|
129
121
|
|
|
130
|
-
// Listen for events
|
|
122
|
+
// Listen for events
|
|
131
123
|
socket.on("payload:event", (event) => {
|
|
132
124
|
console.log("Event received:", event);
|
|
133
125
|
// {
|
|
@@ -365,105 +357,35 @@ This is handled automatically via the `"browser"` field in `package.json`, so yo
|
|
|
365
357
|
|
|
366
358
|
## Environment Variables
|
|
367
359
|
|
|
360
|
+
The plugin does not read environment variables directly. You can use environment variables in your configuration:
|
|
361
|
+
|
|
368
362
|
```bash
|
|
369
|
-
#
|
|
363
|
+
# Example: Redis URL for multi-instance support
|
|
370
364
|
REDIS_URL=redis://localhost:6379
|
|
371
365
|
|
|
372
366
|
# Optional: Payload configuration
|
|
373
367
|
PAYLOAD_SECRET=your-secret-key
|
|
374
368
|
```
|
|
375
369
|
|
|
376
|
-
|
|
370
|
+
Then pass them in your plugin configuration:
|
|
377
371
|
|
|
378
|
-
|
|
372
|
+
```typescript
|
|
373
|
+
socketPlugin({
|
|
374
|
+
redis: {
|
|
375
|
+
url: process.env.REDIS_URL,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
379
|
|
|
380
|
-
|
|
380
|
+
## TypeScript Types
|
|
381
381
|
|
|
382
382
|
```typescript
|
|
383
383
|
import type {
|
|
384
|
-
// Event interfaces for Socket.IO
|
|
385
|
-
ServerToClientEvents,
|
|
386
|
-
ClientToServerEvents,
|
|
387
|
-
InterServerEvents,
|
|
388
|
-
SocketData,
|
|
389
|
-
|
|
390
|
-
// Plugin types
|
|
391
384
|
CollectionAuthorizationHandler,
|
|
392
385
|
RealtimeEventPayload,
|
|
393
386
|
AuthenticatedSocket,
|
|
394
387
|
EventType,
|
|
395
|
-
RealtimeEventsPluginOptions,
|
|
396
|
-
} from "payload-socket-plugin";
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### Typed Socket.IO Client
|
|
400
|
-
|
|
401
|
-
Use the event interfaces for full type safety on the client side:
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
import { io, Socket } from "socket.io-client";
|
|
405
|
-
import type {
|
|
406
|
-
ServerToClientEvents,
|
|
407
|
-
ClientToServerEvents,
|
|
408
388
|
} from "payload-socket-plugin";
|
|
409
|
-
|
|
410
|
-
// Create a typed socket client
|
|
411
|
-
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
|
|
412
|
-
"http://localhost:3000",
|
|
413
|
-
{
|
|
414
|
-
auth: {
|
|
415
|
-
token: "your-jwt-token",
|
|
416
|
-
},
|
|
417
|
-
}
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
// Now you get full autocomplete and type checking!
|
|
421
|
-
socket.emit("subscribe", ["posts", "users"]); // ✅ Type-safe
|
|
422
|
-
socket.emit("join-collection", "posts"); // ✅ Type-safe
|
|
423
|
-
|
|
424
|
-
socket.on("payload:event", (event) => {
|
|
425
|
-
// event is typed as RealtimeEventPayload
|
|
426
|
-
console.log(event.collection, event.type, event.doc);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
socket.on("payload:event:all", (event) => {
|
|
430
|
-
// event is typed as RealtimeEventPayload
|
|
431
|
-
console.log("Global event:", event);
|
|
432
|
-
});
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### Event Interfaces
|
|
436
|
-
|
|
437
|
-
**ServerToClientEvents** - Events the server emits to clients:
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
440
|
-
interface ServerToClientEvents {
|
|
441
|
-
"payload:event": (event: RealtimeEventPayload) => void;
|
|
442
|
-
"payload:event:all": (event: RealtimeEventPayload) => void;
|
|
443
|
-
}
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
**ClientToServerEvents** - Events clients can send to the server:
|
|
447
|
-
|
|
448
|
-
```typescript
|
|
449
|
-
interface ClientToServerEvents {
|
|
450
|
-
subscribe: (collections: string | string[]) => void;
|
|
451
|
-
unsubscribe: (collections: string | string[]) => void;
|
|
452
|
-
"join-collection": (collection: string) => void;
|
|
453
|
-
}
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**SocketData** - Data stored on each socket:
|
|
457
|
-
|
|
458
|
-
```typescript
|
|
459
|
-
interface SocketData {
|
|
460
|
-
user?: {
|
|
461
|
-
id: string | number;
|
|
462
|
-
email?: string;
|
|
463
|
-
collection?: string;
|
|
464
|
-
role?: string;
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
389
|
```
|
|
468
390
|
|
|
469
391
|
## Troubleshooting
|
|
@@ -496,10 +418,11 @@ interface SocketData {
|
|
|
496
418
|
|
|
497
419
|
**Solutions**:
|
|
498
420
|
|
|
499
|
-
- Verify `
|
|
421
|
+
- Verify `redis.url` is set correctly in plugin options
|
|
500
422
|
- Check Redis server is running and accessible
|
|
501
423
|
- Ensure both server instances use the same Redis URL
|
|
502
424
|
- Check Redis logs for connection errors
|
|
425
|
+
- Make sure you're passing the Redis URL in the plugin configuration, not relying on environment variables
|
|
503
426
|
|
|
504
427
|
### TypeScript Errors
|
|
505
428
|
|
|
@@ -511,6 +434,43 @@ interface SocketData {
|
|
|
511
434
|
- Check that your `tsconfig.json` includes the plugin's types
|
|
512
435
|
- Verify Payload CMS version compatibility (>= 2.0.0)
|
|
513
436
|
|
|
437
|
+
## Multi-Instance Deployments with Redis
|
|
438
|
+
|
|
439
|
+
When using Redis adapter for multi-instance deployments, user data is automatically synchronized across all server instances:
|
|
440
|
+
|
|
441
|
+
- **`socket.data.user`**: Automatically synchronized across servers via Redis adapter
|
|
442
|
+
- **`socket.user`**: Only available on the local server where the socket connected (backward compatibility)
|
|
443
|
+
|
|
444
|
+
### Accessing User Data in Custom Handlers
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
socketPlugin({
|
|
448
|
+
onSocketConnection: (socket, io, payload) => {
|
|
449
|
+
socket.on("get-active-users", async (roomName) => {
|
|
450
|
+
const sockets = await io.in(roomName).fetchSockets();
|
|
451
|
+
|
|
452
|
+
const users = sockets.map((s) => {
|
|
453
|
+
// Use socket.data.user for Redis compatibility (works across all servers)
|
|
454
|
+
// Fallback to socket.user for local connections
|
|
455
|
+
const user = s.data.user || (s as any).user;
|
|
456
|
+
return {
|
|
457
|
+
id: user?.id,
|
|
458
|
+
email: user?.email,
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
socket.emit("active-users", users);
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Important**: When using `io.in(room).fetchSockets()` with Redis adapter:
|
|
469
|
+
|
|
470
|
+
- Remote sockets (from other servers) will have `socket.data.user` populated
|
|
471
|
+
- Local sockets will have both `socket.data.user` and `socket.user` populated
|
|
472
|
+
- Always check `socket.data.user` first for Redis compatibility
|
|
473
|
+
|
|
514
474
|
## Performance Considerations
|
|
515
475
|
|
|
516
476
|
- **Redis**: Highly recommended for production multi-instance deployments
|
package/dist/socketManager.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Server as SocketIOServer } from "socket.io";
|
|
2
2
|
import { Server as HTTPServer } from "http";
|
|
3
|
-
import { RealtimeEventsPluginOptions, RealtimeEventPayload
|
|
3
|
+
import { RealtimeEventsPluginOptions, RealtimeEventPayload } from "./types";
|
|
4
4
|
/**
|
|
5
5
|
* Socket.IO Manager for handling real-time events with Redis adapter
|
|
6
6
|
* Supports multiple Payload instances for production environments
|
|
@@ -14,7 +14,7 @@ export declare class SocketIOManager {
|
|
|
14
14
|
/**
|
|
15
15
|
* Initialize Socket.IO server with Redis adapter
|
|
16
16
|
*/
|
|
17
|
-
init(server: HTTPServer): Promise<SocketIOServer
|
|
17
|
+
init(server: HTTPServer): Promise<SocketIOServer>;
|
|
18
18
|
/**
|
|
19
19
|
* Setup Redis adapter for multi-instance synchronization
|
|
20
20
|
*/
|
|
@@ -39,7 +39,7 @@ export declare class SocketIOManager {
|
|
|
39
39
|
/**
|
|
40
40
|
* Get Socket.IO server instance
|
|
41
41
|
*/
|
|
42
|
-
getIO(): SocketIOServer
|
|
42
|
+
getIO(): SocketIOServer | null;
|
|
43
43
|
/**
|
|
44
44
|
* Cleanup and close connections
|
|
45
45
|
*/
|
package/dist/socketManager.js
CHANGED
|
@@ -25,7 +25,7 @@ class SocketIOManager {
|
|
|
25
25
|
*/
|
|
26
26
|
async init(server) {
|
|
27
27
|
const { redis, socketIO = {} } = this.options;
|
|
28
|
-
// Create Socket.IO server
|
|
28
|
+
// Create Socket.IO server
|
|
29
29
|
this.io = new socket_io_1.Server(server, {
|
|
30
30
|
path: socketIO.path || "/socket.io",
|
|
31
31
|
cors: socketIO.cors || {
|
|
@@ -42,15 +42,16 @@ class SocketIOManager {
|
|
|
42
42
|
this.setupAuthentication();
|
|
43
43
|
// Setup connection handlers
|
|
44
44
|
this.setupConnectionHandlers();
|
|
45
|
+
payload_1.default.logger.info("Socket.IO server initialized with real-time events plugin");
|
|
45
46
|
return this.io;
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
48
49
|
* Setup Redis adapter for multi-instance synchronization
|
|
49
50
|
*/
|
|
50
51
|
async setupRedisAdapter() {
|
|
51
|
-
const redisUrl =
|
|
52
|
+
const redisUrl = this.options.redis?.url;
|
|
52
53
|
if (!redisUrl) {
|
|
53
|
-
payload_1.default.logger.warn("
|
|
54
|
+
payload_1.default.logger.warn("Redis URL not configured. Skipping Redis adapter setup. Set redis.url in plugin options.");
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
try {
|
|
@@ -67,6 +68,7 @@ class SocketIOManager {
|
|
|
67
68
|
new Promise((resolve) => this.subClient.once("ready", resolve)),
|
|
68
69
|
]);
|
|
69
70
|
this.io.adapter((0, redis_adapter_1.createAdapter)(this.pubClient, this.subClient));
|
|
71
|
+
payload_1.default.logger.info("Redis adapter configured for Socket.IO multi-instance support");
|
|
70
72
|
}
|
|
71
73
|
catch (error) {
|
|
72
74
|
payload_1.default.logger.error("Failed to setup Redis adapter:", error);
|
|
@@ -98,15 +100,17 @@ class SocketIOManager {
|
|
|
98
100
|
if (!userDoc) {
|
|
99
101
|
return next(new Error("User not found"));
|
|
100
102
|
}
|
|
101
|
-
|
|
102
|
-
socket.data.user = {
|
|
103
|
+
const userInfo = {
|
|
103
104
|
id: userDoc.id,
|
|
104
105
|
email: userDoc.email,
|
|
105
106
|
collection: decoded.collection || "users",
|
|
106
107
|
role: userDoc.role,
|
|
107
108
|
};
|
|
109
|
+
// Store in socket.data for Redis adapter compatibility
|
|
110
|
+
// socket.data is automatically synchronized across servers via Redis
|
|
111
|
+
socket.data.user = userInfo;
|
|
108
112
|
// Also attach to socket.user for backward compatibility
|
|
109
|
-
socket.user =
|
|
113
|
+
socket.user = userInfo;
|
|
110
114
|
next();
|
|
111
115
|
}
|
|
112
116
|
catch (jwtError) {
|
|
@@ -124,6 +128,7 @@ class SocketIOManager {
|
|
|
124
128
|
*/
|
|
125
129
|
setupConnectionHandlers() {
|
|
126
130
|
this.io.on("connection", async (socket) => {
|
|
131
|
+
payload_1.default.logger.info(`Client connected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
127
132
|
// Allow clients to subscribe to specific collections
|
|
128
133
|
socket.on("subscribe", (collections) => {
|
|
129
134
|
const collectionList = Array.isArray(collections)
|
|
@@ -131,6 +136,7 @@ class SocketIOManager {
|
|
|
131
136
|
: [collections];
|
|
132
137
|
collectionList.forEach((collection) => {
|
|
133
138
|
socket.join(`collection:${collection}`);
|
|
139
|
+
payload_1.default.logger.info(`Client ${socket.id} subscribed to collection: ${collection}`);
|
|
134
140
|
});
|
|
135
141
|
});
|
|
136
142
|
// Allow clients to unsubscribe from collections
|
|
@@ -140,20 +146,21 @@ class SocketIOManager {
|
|
|
140
146
|
: [collections];
|
|
141
147
|
collectionList.forEach((collection) => {
|
|
142
148
|
socket.leave(`collection:${collection}`);
|
|
149
|
+
payload_1.default.logger.info(`Client ${socket.id} unsubscribed from collection: ${collection}`);
|
|
143
150
|
});
|
|
144
151
|
});
|
|
145
152
|
// Allow clients to join collection rooms (alias for subscribe)
|
|
146
153
|
socket.on("join-collection", (collection) => {
|
|
147
154
|
const roomName = `collection:${collection}`;
|
|
148
155
|
socket.join(roomName);
|
|
156
|
+
payload_1.default.logger.info(`Client ${socket.id} (${socket.user?.email}) joined collection room: ${roomName}`);
|
|
149
157
|
});
|
|
150
158
|
// Handle disconnection
|
|
151
159
|
socket.on("disconnect", () => {
|
|
152
|
-
|
|
160
|
+
payload_1.default.logger.info(`Client disconnected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
153
161
|
});
|
|
154
162
|
if (this.options.onSocketConnection) {
|
|
155
163
|
try {
|
|
156
|
-
// Cast to AuthenticatedSocket for backward compatibility
|
|
157
164
|
await this.options.onSocketConnection(socket, this.io, payload_1.default);
|
|
158
165
|
}
|
|
159
166
|
catch (error) {
|
|
@@ -167,6 +174,7 @@ class SocketIOManager {
|
|
|
167
174
|
*/
|
|
168
175
|
async emitEvent(event) {
|
|
169
176
|
if (!this.io) {
|
|
177
|
+
payload_1.default.logger.warn("Socket.IO server not initialized, cannot emit event");
|
|
170
178
|
return;
|
|
171
179
|
}
|
|
172
180
|
const { authorize, shouldEmit, transformEvent } = this.options;
|
|
@@ -185,7 +193,9 @@ class SocketIOManager {
|
|
|
185
193
|
if (collectionHandler) {
|
|
186
194
|
const sockets = await this.io.in(room).fetchSockets();
|
|
187
195
|
for (const socket of sockets) {
|
|
188
|
-
const
|
|
196
|
+
const authSocket = socket;
|
|
197
|
+
// Use socket.data.user for remote sockets (Redis adapter), fallback to socket.user for local
|
|
198
|
+
const user = socket.data.user || authSocket.user;
|
|
189
199
|
if (user) {
|
|
190
200
|
const isAuthorized = await collectionHandler(user, finalEvent);
|
|
191
201
|
if (isAuthorized) {
|
|
@@ -222,6 +232,7 @@ class SocketIOManager {
|
|
|
222
232
|
if (this.subClient) {
|
|
223
233
|
await this.subClient.quit();
|
|
224
234
|
}
|
|
235
|
+
payload_1.default.logger.info("Socket.IO server closed");
|
|
225
236
|
}
|
|
226
237
|
}
|
|
227
238
|
exports.SocketIOManager = SocketIOManager;
|
package/dist/types.d.ts
CHANGED
|
@@ -24,48 +24,10 @@ export interface RealtimeEventPayload {
|
|
|
24
24
|
/** Timestamp of the event */
|
|
25
25
|
timestamp: string;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Events that the server can emit to clients
|
|
29
|
-
*/
|
|
30
|
-
export interface ServerToClientEvents {
|
|
31
|
-
/** Real-time event for a specific collection */
|
|
32
|
-
"payload:event": (event: RealtimeEventPayload) => void;
|
|
33
|
-
/** Real-time event broadcast to all listeners */
|
|
34
|
-
"payload:event:all": (event: RealtimeEventPayload) => void;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Events that clients can send to the server
|
|
38
|
-
*/
|
|
39
|
-
export interface ClientToServerEvents {
|
|
40
|
-
/** Subscribe to one or more collections */
|
|
41
|
-
subscribe: (collections: string | string[]) => void;
|
|
42
|
-
/** Unsubscribe from one or more collections */
|
|
43
|
-
unsubscribe: (collections: string | string[]) => void;
|
|
44
|
-
/** Join a collection room (alias for subscribe) */
|
|
45
|
-
"join-collection": (collection: string) => void;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Events for inter-server communication (when using Redis adapter)
|
|
49
|
-
*/
|
|
50
|
-
export interface InterServerEvents {
|
|
51
|
-
ping: () => void;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Data stored on each socket instance
|
|
55
|
-
*/
|
|
56
|
-
export interface SocketData {
|
|
57
|
-
user?: {
|
|
58
|
-
id: string | number;
|
|
59
|
-
email?: string;
|
|
60
|
-
collection?: string;
|
|
61
|
-
role?: string;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
27
|
/**
|
|
65
28
|
* Socket.IO server instance with authentication
|
|
66
|
-
* @deprecated Use Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData> instead
|
|
67
29
|
*/
|
|
68
|
-
export interface AuthenticatedSocket extends Socket
|
|
30
|
+
export interface AuthenticatedSocket extends Socket {
|
|
69
31
|
user?: {
|
|
70
32
|
id: string | number;
|
|
71
33
|
email?: string;
|