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 CHANGED
@@ -108,26 +108,18 @@ await initSocketIO(server);
108
108
 
109
109
  ```typescript
110
110
  // client.ts
111
- import { io, Socket } from "socket.io-client";
112
- import type {
113
- ServerToClientEvents,
114
- ClientToServerEvents,
115
- } from "payload-socket-plugin";
111
+ import { io } from "socket.io-client";
116
112
 
117
- // Create a typed socket for full TypeScript support
118
- const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
119
- "http://localhost:3000",
120
- {
121
- auth: {
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 (fully typed!)
119
+ // Subscribe to collection events
128
120
  socket.emit("join-collection", "posts");
129
121
 
130
- // Listen for events (event parameter is fully typed!)
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
- # Required for Redis multi-instance support
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
- ## TypeScript Types
370
+ Then pass them in your plugin configuration:
377
371
 
378
- The plugin provides full TypeScript support with typed Socket.IO events following [Socket.IO v4 TypeScript best practices](https://socket.io/docs/v4/typescript/).
372
+ ```typescript
373
+ socketPlugin({
374
+ redis: {
375
+ url: process.env.REDIS_URL,
376
+ },
377
+ });
378
+ ```
379
379
 
380
- ### Available Types
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 `REDIS_URL` environment variable is set correctly
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
@@ -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, ServerToClientEvents, ClientToServerEvents, InterServerEvents, SocketData } from "./types";
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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>>;
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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData> | null;
42
+ getIO(): SocketIOServer | null;
43
43
  /**
44
44
  * Cleanup and close connections
45
45
  */
@@ -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 with typed events
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 = process.env.REDIS_URL;
52
+ const redisUrl = this.options.redis?.url;
52
53
  if (!redisUrl) {
53
- payload_1.default.logger.warn("REDIS_URL not configured. Skipping Redis adapter setup.");
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
- // Attach user info to socket.data
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 = socket.data.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
- // Cleanup happens automatically
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 user = socket.data.user || socket.user;
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<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData> {
30
+ export interface AuthenticatedSocket extends Socket {
69
31
  user?: {
70
32
  id: string | number;
71
33
  email?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-socket-plugin",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Real-time Socket.IO plugin for Payload CMS with Redis support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",