payload-socket-plugin 1.1.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -22
- package/dist/index.js +13 -13
- package/dist/initSocketIO.d.ts +7 -2
- package/dist/initSocketIO.js +12 -11
- package/dist/socketManager.d.ts +3 -1
- package/dist/socketManager.js +36 -23
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
Real-time event broadcasting plugin for Payload CMS using Socket.IO with Redis support for multi-instance deployments.
|
|
8
8
|
|
|
9
|
+
> **Version 2.0.0+** supports Payload CMS v3.72.0+
|
|
10
|
+
> For Payload v2, use [v1.x](https://github.com/beewhoo/payload-socket-plugin/tree/v1.1.5)
|
|
11
|
+
|
|
9
12
|
## Features
|
|
10
13
|
|
|
11
14
|
- ✅ **Real-time Events**: Broadcast collection changes (create, update, delete) to connected clients
|
|
@@ -18,17 +21,21 @@ Real-time event broadcasting plugin for Payload CMS using Socket.IO with Redis s
|
|
|
18
21
|
## Prerequisites
|
|
19
22
|
|
|
20
23
|
- **Node.js**: >= 20.0.0
|
|
21
|
-
- **Payload CMS**: ^
|
|
24
|
+
- **Payload CMS**: ^3.72.0 (for v2.x of this plugin)
|
|
22
25
|
- **Redis** (optional): Required for multi-instance deployments
|
|
23
26
|
|
|
24
27
|
## Installation
|
|
25
28
|
|
|
29
|
+
### For Payload v3 (Current)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install payload-socket-plugin@^2.0.0
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### For Payload v2 (Legacy)
|
|
36
|
+
|
|
26
37
|
```bash
|
|
27
|
-
npm install payload-socket-plugin
|
|
28
|
-
# or
|
|
29
|
-
yarn add payload-socket-plugin
|
|
30
|
-
# or
|
|
31
|
-
pnpm add payload-socket-plugin
|
|
38
|
+
npm install payload-socket-plugin@^1.0.0
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
### Install Socket.IO Client (for frontend)
|
|
@@ -37,13 +44,13 @@ pnpm add payload-socket-plugin
|
|
|
37
44
|
npm install socket.io-client
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
## Quick Start
|
|
47
|
+
## Quick Start (Payload v3)
|
|
41
48
|
|
|
42
49
|
### 1. Configure the Plugin
|
|
43
50
|
|
|
44
51
|
```typescript
|
|
45
52
|
// payload.config.ts
|
|
46
|
-
import { buildConfig } from "payload
|
|
53
|
+
import { buildConfig } from "payload";
|
|
47
54
|
import { socketPlugin } from "payload-socket-plugin";
|
|
48
55
|
|
|
49
56
|
export default buildConfig({
|
|
@@ -79,29 +86,42 @@ export default buildConfig({
|
|
|
79
86
|
});
|
|
80
87
|
```
|
|
81
88
|
|
|
82
|
-
### 2. Initialize Socket.IO Server
|
|
89
|
+
### 2. Initialize Socket.IO Server (Next.js Custom Server)
|
|
83
90
|
|
|
84
91
|
```typescript
|
|
85
92
|
// server.ts
|
|
86
93
|
import express from "express";
|
|
87
|
-
import
|
|
94
|
+
import { createServer } from "http";
|
|
95
|
+
import next from "next";
|
|
96
|
+
import { getPayload } from "payload";
|
|
88
97
|
import { initSocketIO } from "payload-socket-plugin";
|
|
98
|
+
import config from "@payload-config";
|
|
89
99
|
|
|
90
|
-
const
|
|
100
|
+
const port = parseInt(process.env.PORT || "3000", 10);
|
|
101
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
102
|
+
const app = next({ dev });
|
|
103
|
+
const handle = app.getRequestHandler();
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
secret: process.env.PAYLOAD_SECRET,
|
|
95
|
-
express: app,
|
|
96
|
-
});
|
|
105
|
+
app.prepare().then(async () => {
|
|
106
|
+
const server = express();
|
|
97
107
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
console.log("Server running on port 3000");
|
|
101
|
-
});
|
|
108
|
+
// Let Next.js handle all routes
|
|
109
|
+
server.all("*", (req, res) => handle(req, res));
|
|
102
110
|
|
|
103
|
-
//
|
|
104
|
-
|
|
111
|
+
// Create HTTP server
|
|
112
|
+
const httpServer = createServer(server);
|
|
113
|
+
|
|
114
|
+
// Get Payload instance
|
|
115
|
+
const payload = await getPayload({ config });
|
|
116
|
+
|
|
117
|
+
// Initialize Socket.IO with the payload instance
|
|
118
|
+
await initSocketIO(httpServer, payload);
|
|
119
|
+
|
|
120
|
+
// Start server
|
|
121
|
+
httpServer.listen(port, () => {
|
|
122
|
+
console.log(`Server listening on port ${port}`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
105
125
|
```
|
|
106
126
|
|
|
107
127
|
### 3. Connect from Client
|
|
@@ -434,6 +454,43 @@ import type {
|
|
|
434
454
|
- Check that your `tsconfig.json` includes the plugin's types
|
|
435
455
|
- Verify Payload CMS version compatibility (>= 2.0.0)
|
|
436
456
|
|
|
457
|
+
## Multi-Instance Deployments with Redis
|
|
458
|
+
|
|
459
|
+
When using Redis adapter for multi-instance deployments, user data is automatically synchronized across all server instances:
|
|
460
|
+
|
|
461
|
+
- **`socket.data.user`**: Automatically synchronized across servers via Redis adapter
|
|
462
|
+
- **`socket.user`**: Only available on the local server where the socket connected (backward compatibility)
|
|
463
|
+
|
|
464
|
+
### Accessing User Data in Custom Handlers
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
socketPlugin({
|
|
468
|
+
onSocketConnection: (socket, io, payload) => {
|
|
469
|
+
socket.on("get-active-users", async (roomName) => {
|
|
470
|
+
const sockets = await io.in(roomName).fetchSockets();
|
|
471
|
+
|
|
472
|
+
const users = sockets.map((s) => {
|
|
473
|
+
// Use socket.data.user for Redis compatibility (works across all servers)
|
|
474
|
+
// Fallback to socket.user for local connections
|
|
475
|
+
const user = s.data.user || (s as any).user;
|
|
476
|
+
return {
|
|
477
|
+
id: user?.id,
|
|
478
|
+
email: user?.email,
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
socket.emit("active-users", users);
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Important**: When using `io.in(room).fetchSockets()` with Redis adapter:
|
|
489
|
+
|
|
490
|
+
- Remote sockets (from other servers) will have `socket.data.user` populated
|
|
491
|
+
- Local sockets will have both `socket.data.user` and `socket.user` populated
|
|
492
|
+
- Always check `socket.data.user` first for Redis compatibility
|
|
493
|
+
|
|
437
494
|
## Performance Considerations
|
|
438
495
|
|
|
439
496
|
- **Redis**: Highly recommended for production multi-instance deployments
|
|
@@ -454,6 +511,99 @@ import type {
|
|
|
454
511
|
- No built-in event replay or history mechanism
|
|
455
512
|
- Redis is required for multi-instance deployments
|
|
456
513
|
|
|
514
|
+
## Migration Guide
|
|
515
|
+
|
|
516
|
+
### Migrating from v1.x (Payload v2) to v2.x (Payload v3)
|
|
517
|
+
|
|
518
|
+
Version 2.0.0 introduces breaking changes to support Payload CMS v3:
|
|
519
|
+
|
|
520
|
+
#### 1. Update Dependencies
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
npm install payload@^3.72.0 payload-socket-plugin@^2.0.0
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
#### 2. Update Import Paths
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// ❌ Old (v1.x)
|
|
530
|
+
import { buildConfig } from "payload/config";
|
|
531
|
+
|
|
532
|
+
// ✅ New (v2.x)
|
|
533
|
+
import { buildConfig } from "payload";
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### 3. Update Server Initialization
|
|
537
|
+
|
|
538
|
+
The `initSocketIO` function now requires the Payload instance as a parameter:
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// ❌ Old (v1.x)
|
|
542
|
+
import payload from "payload";
|
|
543
|
+
import { initSocketIO } from "payload-socket-plugin";
|
|
544
|
+
|
|
545
|
+
await payload.init({ secret, express: app });
|
|
546
|
+
const server = app.listen(3000);
|
|
547
|
+
await initSocketIO(server);
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
// ✅ New (v2.x)
|
|
552
|
+
import { getPayload } from "payload";
|
|
553
|
+
import { initSocketIO } from "payload-socket-plugin";
|
|
554
|
+
import config from "@payload-config";
|
|
555
|
+
|
|
556
|
+
const payload = await getPayload({ config });
|
|
557
|
+
const httpServer = createServer(app);
|
|
558
|
+
await initSocketIO(httpServer, payload);
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### 4. Update Next.js Integration
|
|
562
|
+
|
|
563
|
+
Payload v3 uses Next.js by default. Update your server setup:
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
// server.ts (Payload v3 with Next.js)
|
|
567
|
+
import express from "express";
|
|
568
|
+
import { createServer } from "http";
|
|
569
|
+
import next from "next";
|
|
570
|
+
import { getPayload } from "payload";
|
|
571
|
+
import { initSocketIO } from "payload-socket-plugin";
|
|
572
|
+
import config from "@payload-config";
|
|
573
|
+
|
|
574
|
+
const app = next({ dev: process.env.NODE_ENV !== "production" });
|
|
575
|
+
const handle = app.getRequestHandler();
|
|
576
|
+
|
|
577
|
+
app.prepare().then(async () => {
|
|
578
|
+
const server = express();
|
|
579
|
+
server.all("*", (req, res) => handle(req, res));
|
|
580
|
+
|
|
581
|
+
const httpServer = createServer(server);
|
|
582
|
+
const payload = await getPayload({ config });
|
|
583
|
+
|
|
584
|
+
await initSocketIO(httpServer, payload);
|
|
585
|
+
|
|
586
|
+
httpServer.listen(3000);
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### 5. Environment Variables
|
|
591
|
+
|
|
592
|
+
Make sure to set `SOCKET_ENABLED=true` in your `.env` file if you're using the `enabled` option:
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
# .env
|
|
596
|
+
SOCKET_ENABLED=true
|
|
597
|
+
REDIS_URL=redis://localhost:6379
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### Breaking Changes Summary
|
|
601
|
+
|
|
602
|
+
- **Minimum Payload version**: Now requires Payload v3.72.0+
|
|
603
|
+
- **`initSocketIO` signature**: Now requires `(httpServer, payloadInstance)` instead of just `(httpServer)`
|
|
604
|
+
- **Import paths**: Changed from `payload/config` to `payload`
|
|
605
|
+
- **Payload initialization**: Use `getPayload({ config })` instead of `payload.init()`
|
|
606
|
+
|
|
457
607
|
## Changelog
|
|
458
608
|
|
|
459
609
|
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ const socketManager_1 = require("./socketManager");
|
|
|
53
53
|
*/
|
|
54
54
|
const socketPlugin = (pluginOptions = {}) => {
|
|
55
55
|
return (incomingConfig) => {
|
|
56
|
+
var _a;
|
|
56
57
|
// Default options
|
|
57
58
|
const options = {
|
|
58
59
|
enabled: true,
|
|
@@ -79,12 +80,13 @@ const socketPlugin = (pluginOptions = {}) => {
|
|
|
79
80
|
* Create event payload from hook arguments
|
|
80
81
|
*/
|
|
81
82
|
const createEventPayload = (type, collection, args) => {
|
|
83
|
+
var _a, _b;
|
|
82
84
|
return {
|
|
83
85
|
type,
|
|
84
86
|
collection,
|
|
85
|
-
id: args.doc
|
|
87
|
+
id: ((_a = args.doc) === null || _a === void 0 ? void 0 : _a.id) || args.id,
|
|
86
88
|
doc: type === "delete" ? undefined : args.doc,
|
|
87
|
-
user: args.req
|
|
89
|
+
user: ((_b = args.req) === null || _b === void 0 ? void 0 : _b.user)
|
|
88
90
|
? {
|
|
89
91
|
id: args.req.user.id,
|
|
90
92
|
email: args.req.user.email,
|
|
@@ -97,7 +99,8 @@ const socketPlugin = (pluginOptions = {}) => {
|
|
|
97
99
|
/**
|
|
98
100
|
* Add hooks to collections
|
|
99
101
|
*/
|
|
100
|
-
const collectionsWithHooks = incomingConfig.collections
|
|
102
|
+
const collectionsWithHooks = ((_a = incomingConfig.collections) === null || _a === void 0 ? void 0 : _a.map((collection) => {
|
|
103
|
+
var _a, _b;
|
|
101
104
|
// Skip if events should not be emitted for this collection
|
|
102
105
|
if (!shouldEmitForCollection(collection.slug)) {
|
|
103
106
|
return collection;
|
|
@@ -108,7 +111,7 @@ const socketPlugin = (pluginOptions = {}) => {
|
|
|
108
111
|
...collection.hooks,
|
|
109
112
|
// After change hook - only emit for updates
|
|
110
113
|
afterChange: [
|
|
111
|
-
...(collection.hooks
|
|
114
|
+
...(((_a = collection.hooks) === null || _a === void 0 ? void 0 : _a.afterChange) || []),
|
|
112
115
|
async (args) => {
|
|
113
116
|
try {
|
|
114
117
|
// Only emit events for updates, not creates
|
|
@@ -125,7 +128,7 @@ const socketPlugin = (pluginOptions = {}) => {
|
|
|
125
128
|
],
|
|
126
129
|
// After delete hook
|
|
127
130
|
afterDelete: [
|
|
128
|
-
...(collection.hooks
|
|
131
|
+
...(((_b = collection.hooks) === null || _b === void 0 ? void 0 : _b.afterDelete) || []),
|
|
129
132
|
async (args) => {
|
|
130
133
|
try {
|
|
131
134
|
const event = createEventPayload("delete", collection.slug, args);
|
|
@@ -138,7 +141,7 @@ const socketPlugin = (pluginOptions = {}) => {
|
|
|
138
141
|
],
|
|
139
142
|
},
|
|
140
143
|
};
|
|
141
|
-
}) || [];
|
|
144
|
+
})) || [];
|
|
142
145
|
/**
|
|
143
146
|
* Add onInit hook to initialize Socket.IO server
|
|
144
147
|
*/
|
|
@@ -147,13 +150,10 @@ const socketPlugin = (pluginOptions = {}) => {
|
|
|
147
150
|
if (incomingConfig.onInit) {
|
|
148
151
|
await incomingConfig.onInit(payload);
|
|
149
152
|
}
|
|
150
|
-
//
|
|
151
|
-
// The server
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// The HTTP server will be initialized in server.ts using initSocketIO()
|
|
155
|
-
payload.__socketManager = socketManager;
|
|
156
|
-
}
|
|
153
|
+
// Store the socket manager for later initialization
|
|
154
|
+
// The HTTP server will be initialized in server.ts using initSocketIO()
|
|
155
|
+
// This works for both standard Payload server and custom Express servers
|
|
156
|
+
payload.__socketManager = socketManager;
|
|
157
157
|
};
|
|
158
158
|
return {
|
|
159
159
|
...incomingConfig,
|
package/dist/initSocketIO.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Server as HTTPServer } from "http";
|
|
2
|
+
import type { Payload } from "payload";
|
|
2
3
|
/**
|
|
3
4
|
* Initialize Socket.IO server with the HTTP server instance
|
|
4
5
|
* This should be called after the HTTP server is created
|
|
@@ -6,13 +7,17 @@ import { Server as HTTPServer } from "http";
|
|
|
6
7
|
* @example
|
|
7
8
|
* ```ts
|
|
8
9
|
* import { initSocketIO } from 'payload-socket-plugin';
|
|
10
|
+
* import { getPayload } from 'payload';
|
|
11
|
+
* import config from '@payload-config';
|
|
12
|
+
*
|
|
13
|
+
* const payload = await getPayload({ config });
|
|
9
14
|
*
|
|
10
15
|
* const server = app.listen(PORT, () => {
|
|
11
16
|
* console.log(`Server is running on port ${PORT}`);
|
|
12
17
|
* });
|
|
13
18
|
*
|
|
14
19
|
* // Initialize Socket.IO
|
|
15
|
-
* await initSocketIO(server);
|
|
20
|
+
* await initSocketIO(server, payload);
|
|
16
21
|
* ```
|
|
17
22
|
*/
|
|
18
|
-
export declare function initSocketIO(httpServer: HTTPServer): Promise<void>;
|
|
23
|
+
export declare function initSocketIO(httpServer: HTTPServer, payloadInstance: Payload): Promise<void>;
|
package/dist/initSocketIO.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.initSocketIO = initSocketIO;
|
|
7
|
-
const payload_1 = __importDefault(require("payload"));
|
|
8
4
|
/**
|
|
9
5
|
* Initialize Socket.IO server with the HTTP server instance
|
|
10
6
|
* This should be called after the HTTP server is created
|
|
@@ -12,29 +8,34 @@ const payload_1 = __importDefault(require("payload"));
|
|
|
12
8
|
* @example
|
|
13
9
|
* ```ts
|
|
14
10
|
* import { initSocketIO } from 'payload-socket-plugin';
|
|
11
|
+
* import { getPayload } from 'payload';
|
|
12
|
+
* import config from '@payload-config';
|
|
13
|
+
*
|
|
14
|
+
* const payload = await getPayload({ config });
|
|
15
15
|
*
|
|
16
16
|
* const server = app.listen(PORT, () => {
|
|
17
17
|
* console.log(`Server is running on port ${PORT}`);
|
|
18
18
|
* });
|
|
19
19
|
*
|
|
20
20
|
* // Initialize Socket.IO
|
|
21
|
-
* await initSocketIO(server);
|
|
21
|
+
* await initSocketIO(server, payload);
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
async function initSocketIO(httpServer) {
|
|
24
|
+
async function initSocketIO(httpServer, payloadInstance) {
|
|
25
25
|
try {
|
|
26
26
|
// Get the socket manager from payload instance
|
|
27
|
-
const socketManager =
|
|
27
|
+
const socketManager = payloadInstance
|
|
28
|
+
.__socketManager;
|
|
28
29
|
if (!socketManager) {
|
|
29
|
-
|
|
30
|
+
payloadInstance.logger.warn("Socket.IO manager not found. Make sure socketPlugin is configured.");
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
33
|
// Initialize Socket.IO with the HTTP server
|
|
33
|
-
await socketManager.init(httpServer);
|
|
34
|
-
|
|
34
|
+
await socketManager.init(httpServer, payloadInstance);
|
|
35
|
+
payloadInstance.logger.info("Socket.IO initialized successfully");
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
|
-
|
|
38
|
+
payloadInstance.logger.error("Failed to initialize Socket.IO:", error);
|
|
38
39
|
throw error;
|
|
39
40
|
}
|
|
40
41
|
}
|
package/dist/socketManager.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Server as SocketIOServer } from "socket.io";
|
|
2
2
|
import { Server as HTTPServer } from "http";
|
|
3
3
|
import { RealtimeEventsPluginOptions, RealtimeEventPayload } from "./types";
|
|
4
|
+
import type { Payload } from "payload";
|
|
4
5
|
/**
|
|
5
6
|
* Socket.IO Manager for handling real-time events with Redis adapter
|
|
6
7
|
* Supports multiple Payload instances for production environments
|
|
@@ -10,11 +11,12 @@ export declare class SocketIOManager {
|
|
|
10
11
|
private pubClient;
|
|
11
12
|
private subClient;
|
|
12
13
|
private options;
|
|
14
|
+
private payload;
|
|
13
15
|
constructor(options: RealtimeEventsPluginOptions);
|
|
14
16
|
/**
|
|
15
17
|
* Initialize Socket.IO server with Redis adapter
|
|
16
18
|
*/
|
|
17
|
-
init(server: HTTPServer): Promise<SocketIOServer>;
|
|
19
|
+
init(server: HTTPServer, payloadInstance: Payload): Promise<SocketIOServer>;
|
|
18
20
|
/**
|
|
19
21
|
* Setup Redis adapter for multi-instance synchronization
|
|
20
22
|
*/
|
package/dist/socketManager.js
CHANGED
|
@@ -8,7 +8,6 @@ const socket_io_1 = require("socket.io");
|
|
|
8
8
|
const redis_adapter_1 = require("@socket.io/redis-adapter");
|
|
9
9
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
10
10
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
11
|
-
const payload_1 = __importDefault(require("payload"));
|
|
12
11
|
/**
|
|
13
12
|
* Socket.IO Manager for handling real-time events with Redis adapter
|
|
14
13
|
* Supports multiple Payload instances for production environments
|
|
@@ -18,12 +17,14 @@ class SocketIOManager {
|
|
|
18
17
|
this.io = null;
|
|
19
18
|
this.pubClient = null;
|
|
20
19
|
this.subClient = null;
|
|
20
|
+
this.payload = null;
|
|
21
21
|
this.options = options;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Initialize Socket.IO server with Redis adapter
|
|
25
25
|
*/
|
|
26
|
-
async init(server) {
|
|
26
|
+
async init(server, payloadInstance) {
|
|
27
|
+
this.payload = payloadInstance;
|
|
27
28
|
const { redis, socketIO = {} } = this.options;
|
|
28
29
|
// Create Socket.IO server
|
|
29
30
|
this.io = new socket_io_1.Server(server, {
|
|
@@ -42,16 +43,17 @@ class SocketIOManager {
|
|
|
42
43
|
this.setupAuthentication();
|
|
43
44
|
// Setup connection handlers
|
|
44
45
|
this.setupConnectionHandlers();
|
|
45
|
-
|
|
46
|
+
this.payload.logger.info("Socket.IO server initialized with real-time events plugin");
|
|
46
47
|
return this.io;
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Setup Redis adapter for multi-instance synchronization
|
|
50
51
|
*/
|
|
51
52
|
async setupRedisAdapter() {
|
|
52
|
-
|
|
53
|
+
var _a;
|
|
54
|
+
const redisUrl = (_a = this.options.redis) === null || _a === void 0 ? void 0 : _a.url;
|
|
53
55
|
if (!redisUrl) {
|
|
54
|
-
|
|
56
|
+
this.payload.logger.warn("Redis URL not configured. Skipping Redis adapter setup. Set redis.url in plugin options.");
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
59
|
try {
|
|
@@ -68,10 +70,10 @@ class SocketIOManager {
|
|
|
68
70
|
new Promise((resolve) => this.subClient.once("ready", resolve)),
|
|
69
71
|
]);
|
|
70
72
|
this.io.adapter((0, redis_adapter_1.createAdapter)(this.pubClient, this.subClient));
|
|
71
|
-
|
|
73
|
+
this.payload.logger.info("Redis adapter configured for Socket.IO multi-instance support");
|
|
72
74
|
}
|
|
73
75
|
catch (error) {
|
|
74
|
-
|
|
76
|
+
this.payload.logger.error("Failed to setup Redis adapter:", error);
|
|
75
77
|
throw error;
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -91,22 +93,26 @@ class SocketIOManager {
|
|
|
91
93
|
return next(new Error("Authentication token required"));
|
|
92
94
|
}
|
|
93
95
|
try {
|
|
94
|
-
const decoded = jsonwebtoken_1.default.verify(token,
|
|
96
|
+
const decoded = jsonwebtoken_1.default.verify(token, this.payload.secret);
|
|
95
97
|
// Fetch full user document from Payload
|
|
96
|
-
const userDoc = await
|
|
98
|
+
const userDoc = await this.payload.findByID({
|
|
97
99
|
collection: decoded.collection || "users",
|
|
98
100
|
id: decoded.id,
|
|
99
101
|
});
|
|
100
102
|
if (!userDoc) {
|
|
101
103
|
return next(new Error("User not found"));
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
socket.user = {
|
|
105
|
+
const userInfo = {
|
|
105
106
|
id: userDoc.id,
|
|
106
107
|
email: userDoc.email,
|
|
107
108
|
collection: decoded.collection || "users",
|
|
108
109
|
role: userDoc.role,
|
|
109
110
|
};
|
|
111
|
+
// Store in socket.data for Redis adapter compatibility
|
|
112
|
+
// socket.data is automatically synchronized across servers via Redis
|
|
113
|
+
socket.data.user = userInfo;
|
|
114
|
+
// Also attach to socket.user for backward compatibility
|
|
115
|
+
socket.user = userInfo;
|
|
110
116
|
next();
|
|
111
117
|
}
|
|
112
118
|
catch (jwtError) {
|
|
@@ -114,7 +120,7 @@ class SocketIOManager {
|
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
catch (error) {
|
|
117
|
-
|
|
123
|
+
this.payload.logger.error("Socket authentication error:", error);
|
|
118
124
|
next(new Error("Authentication failed"));
|
|
119
125
|
}
|
|
120
126
|
});
|
|
@@ -124,7 +130,8 @@ class SocketIOManager {
|
|
|
124
130
|
*/
|
|
125
131
|
setupConnectionHandlers() {
|
|
126
132
|
this.io.on("connection", async (socket) => {
|
|
127
|
-
|
|
133
|
+
var _a, _b;
|
|
134
|
+
this.payload.logger.info(`Client connected: ${socket.id}, User: ${((_a = socket.user) === null || _a === void 0 ? void 0 : _a.email) || ((_b = socket.user) === null || _b === void 0 ? void 0 : _b.id)}`);
|
|
128
135
|
// Allow clients to subscribe to specific collections
|
|
129
136
|
socket.on("subscribe", (collections) => {
|
|
130
137
|
const collectionList = Array.isArray(collections)
|
|
@@ -132,7 +139,7 @@ class SocketIOManager {
|
|
|
132
139
|
: [collections];
|
|
133
140
|
collectionList.forEach((collection) => {
|
|
134
141
|
socket.join(`collection:${collection}`);
|
|
135
|
-
|
|
142
|
+
this.payload.logger.info(`Client ${socket.id} subscribed to collection: ${collection}`);
|
|
136
143
|
});
|
|
137
144
|
});
|
|
138
145
|
// Allow clients to unsubscribe from collections
|
|
@@ -142,25 +149,27 @@ class SocketIOManager {
|
|
|
142
149
|
: [collections];
|
|
143
150
|
collectionList.forEach((collection) => {
|
|
144
151
|
socket.leave(`collection:${collection}`);
|
|
145
|
-
|
|
152
|
+
this.payload.logger.info(`Client ${socket.id} unsubscribed from collection: ${collection}`);
|
|
146
153
|
});
|
|
147
154
|
});
|
|
148
155
|
// Allow clients to join collection rooms (alias for subscribe)
|
|
149
156
|
socket.on("join-collection", (collection) => {
|
|
157
|
+
var _a;
|
|
150
158
|
const roomName = `collection:${collection}`;
|
|
151
159
|
socket.join(roomName);
|
|
152
|
-
|
|
160
|
+
this.payload.logger.info(`Client ${socket.id} (${(_a = socket.user) === null || _a === void 0 ? void 0 : _a.email}) joined collection room: ${roomName}`);
|
|
153
161
|
});
|
|
154
162
|
// Handle disconnection
|
|
155
163
|
socket.on("disconnect", () => {
|
|
156
|
-
|
|
164
|
+
var _a, _b;
|
|
165
|
+
this.payload.logger.info(`Client disconnected: ${socket.id}, User: ${((_a = socket.user) === null || _a === void 0 ? void 0 : _a.email) || ((_b = socket.user) === null || _b === void 0 ? void 0 : _b.id)}`);
|
|
157
166
|
});
|
|
158
167
|
if (this.options.onSocketConnection) {
|
|
159
168
|
try {
|
|
160
|
-
await this.options.onSocketConnection(socket, this.io,
|
|
169
|
+
await this.options.onSocketConnection(socket, this.io, this.payload);
|
|
161
170
|
}
|
|
162
171
|
catch (error) {
|
|
163
|
-
|
|
172
|
+
this.payload.logger.error(`Error in custom socket connection handler: ${error}`);
|
|
164
173
|
}
|
|
165
174
|
}
|
|
166
175
|
});
|
|
@@ -170,7 +179,7 @@ class SocketIOManager {
|
|
|
170
179
|
*/
|
|
171
180
|
async emitEvent(event) {
|
|
172
181
|
if (!this.io) {
|
|
173
|
-
|
|
182
|
+
this.payload.logger.warn("Socket.IO server not initialized, cannot emit event");
|
|
174
183
|
return;
|
|
175
184
|
}
|
|
176
185
|
const { authorize, shouldEmit, transformEvent } = this.options;
|
|
@@ -190,8 +199,10 @@ class SocketIOManager {
|
|
|
190
199
|
const sockets = await this.io.in(room).fetchSockets();
|
|
191
200
|
for (const socket of sockets) {
|
|
192
201
|
const authSocket = socket;
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
// Use socket.data.user for remote sockets (Redis adapter), fallback to socket.user for local
|
|
203
|
+
const user = socket.data.user || authSocket.user;
|
|
204
|
+
if (user) {
|
|
205
|
+
const isAuthorized = await collectionHandler(user, finalEvent);
|
|
195
206
|
if (isAuthorized) {
|
|
196
207
|
socket.emit("payload:event", finalEvent);
|
|
197
208
|
}
|
|
@@ -226,7 +237,9 @@ class SocketIOManager {
|
|
|
226
237
|
if (this.subClient) {
|
|
227
238
|
await this.subClient.quit();
|
|
228
239
|
}
|
|
229
|
-
|
|
240
|
+
if (this.payload) {
|
|
241
|
+
this.payload.logger.info("Socket.IO server closed");
|
|
242
|
+
}
|
|
230
243
|
}
|
|
231
244
|
}
|
|
232
245
|
exports.SocketIOManager = SocketIOManager;
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-socket-plugin",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Real-time Socket.IO plugin for Payload CMS with Redis support",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Real-time Socket.IO plugin for Payload CMS v3 with Redis support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./browser": {
|
|
14
|
+
"import": "./dist/browser.js",
|
|
15
|
+
"require": "./dist/browser.js",
|
|
16
|
+
"types": "./dist/browser.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
8
19
|
"files": [
|
|
9
20
|
"dist",
|
|
10
21
|
"README.md"
|
|
@@ -30,6 +41,9 @@
|
|
|
30
41
|
"engines": {
|
|
31
42
|
"node": ">=20.0.0"
|
|
32
43
|
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"payload": "^3.72.0"
|
|
46
|
+
},
|
|
33
47
|
"dependencies": {
|
|
34
48
|
"@socket.io/redis-adapter": "^8.0.0",
|
|
35
49
|
"ioredis": "^5.3.0",
|