payload-socket-plugin 1.0.0 → 1.1.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 +75 -0
- package/dist/socketManager.js +10 -155
- package/dist/types.d.ts +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,6 +144,7 @@ socket.on("payload:event", (event) => {
|
|
|
144
144
|
| `authorize` | `object` | - | Per-collection authorization handlers |
|
|
145
145
|
| `shouldEmit` | `function` | - | Filter function to determine if event should be emitted |
|
|
146
146
|
| `transformEvent` | `function` | - | Transform events before emitting |
|
|
147
|
+
| `onSocketConnection` | `function` | - | Custom event handlers for each socket connection |
|
|
147
148
|
|
|
148
149
|
## Authorization
|
|
149
150
|
|
|
@@ -211,6 +212,80 @@ socket.on("payload:event:all", (event) => {
|
|
|
211
212
|
|
|
212
213
|
## Advanced Usage
|
|
213
214
|
|
|
215
|
+
### Custom Socket Event Handlers
|
|
216
|
+
|
|
217
|
+
You can register your own custom event handlers that will be attached to each authenticated socket.
|
|
218
|
+
|
|
219
|
+
**Simple inline handlers:**
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
socketPlugin({
|
|
223
|
+
onSocketConnection: (socket, io, payload) => {
|
|
224
|
+
// Custom event handler
|
|
225
|
+
socket.on("send-message", async (data) => {
|
|
226
|
+
const { roomId, message } = data;
|
|
227
|
+
|
|
228
|
+
// Broadcast to room
|
|
229
|
+
io.to(`room:${roomId}`).emit("new-message", {
|
|
230
|
+
user: socket.user,
|
|
231
|
+
message,
|
|
232
|
+
timestamp: new Date().toISOString(),
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Custom room management
|
|
237
|
+
socket.on("join-custom-room", (roomId) => {
|
|
238
|
+
socket.join(`room:${roomId}`);
|
|
239
|
+
socket.emit("joined-room", { roomId });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Access Payload CMS from your handlers
|
|
243
|
+
socket.on("get-user-data", async () => {
|
|
244
|
+
const user = await payload.findByID({
|
|
245
|
+
collection: "users",
|
|
246
|
+
id: socket.user!.id as string,
|
|
247
|
+
});
|
|
248
|
+
socket.emit("user-data", user);
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Organized in separate files (recommended):**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Import from examples directory
|
|
258
|
+
import { projectHandlers } from "./examples/projectHandlers";
|
|
259
|
+
import { chatHandlers } from "./examples/chatHandlers";
|
|
260
|
+
import { notificationHandlers } from "./examples/notificationHandlers";
|
|
261
|
+
|
|
262
|
+
// Or import all at once
|
|
263
|
+
import {
|
|
264
|
+
projectHandlers,
|
|
265
|
+
chatHandlers,
|
|
266
|
+
notificationHandlers,
|
|
267
|
+
} from "./examples";
|
|
268
|
+
|
|
269
|
+
socketPlugin({
|
|
270
|
+
onSocketConnection: (socket, io, payload) => {
|
|
271
|
+
// Use one or more pre-built handlers
|
|
272
|
+
projectHandlers(socket, io, payload);
|
|
273
|
+
chatHandlers(socket, io, payload);
|
|
274
|
+
notificationHandlers(socket, io, payload);
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**See the [examples directory](./examples) for complete implementations** including:
|
|
280
|
+
|
|
281
|
+
| Handler | Features |
|
|
282
|
+
| ------------------------- | -------------------------------------------------------------------- |
|
|
283
|
+
| **Project Collaboration** | Join/leave rooms, permission checking, presence tracking, kick users |
|
|
284
|
+
| **Chat/Messaging** | Send messages, typing indicators, read receipts |
|
|
285
|
+
| **Notifications** | User notifications, broadcast announcements (admin only) |
|
|
286
|
+
|
|
287
|
+
Each example includes full client-side and server-side code with error handling and best practices.
|
|
288
|
+
|
|
214
289
|
### Custom Event Filtering
|
|
215
290
|
|
|
216
291
|
```typescript
|
package/dist/socketManager.js
CHANGED
|
@@ -123,7 +123,7 @@ class SocketIOManager {
|
|
|
123
123
|
* Setup connection event handlers
|
|
124
124
|
*/
|
|
125
125
|
setupConnectionHandlers() {
|
|
126
|
-
this.io.on("connection", (socket) => {
|
|
126
|
+
this.io.on("connection", async (socket) => {
|
|
127
127
|
payload_1.default.logger.info(`Client connected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
128
128
|
// Allow clients to subscribe to specific collections
|
|
129
129
|
socket.on("subscribe", (collections) => {
|
|
@@ -135,7 +135,7 @@ class SocketIOManager {
|
|
|
135
135
|
payload_1.default.logger.info(`Client ${socket.id} subscribed to collection: ${collection}`);
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
|
-
//
|
|
138
|
+
// Allow clients to unsubscribe from collections
|
|
139
139
|
socket.on("unsubscribe", (collections) => {
|
|
140
140
|
const collectionList = Array.isArray(collections)
|
|
141
141
|
? collections
|
|
@@ -145,169 +145,24 @@ class SocketIOManager {
|
|
|
145
145
|
payload_1.default.logger.info(`Client ${socket.id} unsubscribed from collection: ${collection}`);
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
|
-
// Allow clients to join collection rooms
|
|
148
|
+
// Allow clients to join collection rooms (alias for subscribe)
|
|
149
149
|
socket.on("join-collection", (collection) => {
|
|
150
150
|
const roomName = `collection:${collection}`;
|
|
151
151
|
socket.join(roomName);
|
|
152
152
|
payload_1.default.logger.info(`Client ${socket.id} (${socket.user?.email}) joined collection room: ${roomName}`);
|
|
153
153
|
});
|
|
154
|
-
//
|
|
155
|
-
socket.on("
|
|
156
|
-
|
|
157
|
-
payload_1.default.logger.warn(`Client ${socket.id} tried to join project without ID`);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
// Check if user has permission to join this project
|
|
161
|
-
try {
|
|
162
|
-
const project = await payload_1.default.findByID({
|
|
163
|
-
collection: "projects",
|
|
164
|
-
id: projectId,
|
|
165
|
-
depth: 0,
|
|
166
|
-
});
|
|
167
|
-
const projectOwnerId = typeof project.user === "string"
|
|
168
|
-
? project.user
|
|
169
|
-
: project.user?.id;
|
|
170
|
-
const isOwner = socket.user.id === projectOwnerId;
|
|
171
|
-
// Check if user has editor invitation
|
|
172
|
-
const invitation = await payload_1.default.find({
|
|
173
|
-
collection: "projectInvitations",
|
|
174
|
-
depth: 0,
|
|
175
|
-
where: {
|
|
176
|
-
user: { equals: socket.user.id },
|
|
177
|
-
status: { equals: "accepted" },
|
|
178
|
-
project: { equals: projectId },
|
|
179
|
-
role: { equals: "editor" },
|
|
180
|
-
},
|
|
181
|
-
limit: 1,
|
|
182
|
-
});
|
|
183
|
-
const hasEditorInvite = invitation.docs.length > 0;
|
|
184
|
-
// Only allow owner, users with editor invitation
|
|
185
|
-
if (!isOwner && !hasEditorInvite) {
|
|
186
|
-
payload_1.default.logger.warn(`Client ${socket.id} (${socket.user?.email}) denied access to project ${projectId} - no editor permission`);
|
|
187
|
-
socket.emit("join-project-error", {
|
|
188
|
-
message: "You need editor access to join this project room",
|
|
189
|
-
});
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
payload_1.default.logger.error("Error checking project permissions:", error);
|
|
195
|
-
socket.emit("join-project-error", {
|
|
196
|
-
message: "Failed to verify project permissions",
|
|
197
|
-
});
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const roomName = `project:${projectId}:room`;
|
|
201
|
-
await socket.join(roomName);
|
|
202
|
-
payload_1.default.logger.info(`Client ${socket.id} (${socket.user?.email || socket.user?.id}) joined project: ${projectId}, room: ${roomName}`);
|
|
203
|
-
// Debug: Log all rooms this socket is in
|
|
204
|
-
payload_1.default.logger.info(`Socket ${socket.id} is now in rooms: ${Array.from(socket.rooms).join(", ")}`);
|
|
205
|
-
// Get all sockets in this project room
|
|
206
|
-
const socketsInRoom = await this.io.in(roomName).fetchSockets();
|
|
207
|
-
// Build list of active users
|
|
208
|
-
const activeUsers = socketsInRoom
|
|
209
|
-
.map((s) => {
|
|
210
|
-
if (s.user) {
|
|
211
|
-
return {
|
|
212
|
-
id: s.user.id,
|
|
213
|
-
email: s.user.email || undefined,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
})
|
|
218
|
-
.filter((u) => u !== null);
|
|
219
|
-
// Remove duplicates (same user, multiple tabs)
|
|
220
|
-
const uniqueUsers = Array.from(new Map(activeUsers.map((u) => [u.id, u])).values());
|
|
221
|
-
// Send current active users to the joining client
|
|
222
|
-
socket.emit("project:active-users", uniqueUsers);
|
|
223
|
-
// Notify others in the room that a new user joined
|
|
224
|
-
socket.to(roomName).emit("project:user-joined", {
|
|
225
|
-
id: socket.user.id,
|
|
226
|
-
email: socket.user.email || undefined,
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
socket.on("leave-project", (projectId) => {
|
|
230
|
-
if (!projectId)
|
|
231
|
-
return;
|
|
232
|
-
const roomName = `project:${projectId}:room`;
|
|
233
|
-
socket.leave(roomName);
|
|
234
|
-
payload_1.default.logger.info(`Client ${socket.id} left project: ${projectId}`);
|
|
235
|
-
// Notify others that user left
|
|
236
|
-
socket.to(roomName).emit("project:user-left", socket.user.id);
|
|
154
|
+
// Handle disconnection
|
|
155
|
+
socket.on("disconnect", () => {
|
|
156
|
+
payload_1.default.logger.info(`Client disconnected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
237
157
|
});
|
|
238
|
-
|
|
239
|
-
const { projectId, userId } = data;
|
|
240
|
-
if (!projectId || !userId) {
|
|
241
|
-
payload_1.default.logger.warn(`Client ${socket.id} tried to kick user without projectId or userId`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
// Verify the kicker is the project owner
|
|
158
|
+
if (this.options.onSocketConnection) {
|
|
245
159
|
try {
|
|
246
|
-
|
|
247
|
-
collection: "projects",
|
|
248
|
-
id: projectId,
|
|
249
|
-
});
|
|
250
|
-
const projectOwnerId = typeof project.user === "string"
|
|
251
|
-
? project.user
|
|
252
|
-
: project.user?.id;
|
|
253
|
-
if (socket.user.id !== projectOwnerId) {
|
|
254
|
-
payload_1.default.logger.warn(`Client ${socket.id} (${socket.user?.email}) tried to kick user but is not the owner`);
|
|
255
|
-
socket.emit("kick-error", {
|
|
256
|
-
message: "Only the project owner can kick users",
|
|
257
|
-
});
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Find all sockets for the user to kick
|
|
261
|
-
const roomName = `project:${projectId}:room`;
|
|
262
|
-
const socketsInRoom = await this.io.in(roomName).fetchSockets();
|
|
263
|
-
let kicked = false;
|
|
264
|
-
for (const s of socketsInRoom) {
|
|
265
|
-
const socketUser = s.user;
|
|
266
|
-
if (socketUser && socketUser.id === userId) {
|
|
267
|
-
// Emit kick event to the user being kicked
|
|
268
|
-
s.emit("kicked-from-project", {
|
|
269
|
-
projectId,
|
|
270
|
-
message: "You have been removed from this project by the owner",
|
|
271
|
-
});
|
|
272
|
-
// Remove them from the room
|
|
273
|
-
s.leave(roomName);
|
|
274
|
-
kicked = true;
|
|
275
|
-
payload_1.default.logger.info(`User ${userId} (${socketUser.email}) was kicked from project ${projectId} by ${socket.user?.email}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (kicked) {
|
|
279
|
-
// Notify others that user was kicked
|
|
280
|
-
socket.to(roomName).emit("project:user-left", userId);
|
|
281
|
-
// Confirm to the kicker
|
|
282
|
-
socket.emit("kick-success", { userId });
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
socket.emit("kick-error", {
|
|
286
|
-
message: "User not found in project",
|
|
287
|
-
});
|
|
288
|
-
}
|
|
160
|
+
await this.options.onSocketConnection(socket, this.io, payload_1.default);
|
|
289
161
|
}
|
|
290
162
|
catch (error) {
|
|
291
|
-
payload_1.default.logger.error(
|
|
292
|
-
socket.emit("kick-error", {
|
|
293
|
-
message: "Failed to kick user",
|
|
294
|
-
});
|
|
163
|
+
payload_1.default.logger.error(`Error in custom socket connection handler: ${error}`);
|
|
295
164
|
}
|
|
296
|
-
}
|
|
297
|
-
// Handle disconnection - clean up project presence
|
|
298
|
-
socket.on("disconnect", async () => {
|
|
299
|
-
payload_1.default.logger.info(`Client disconnected: ${socket.id}, User: ${socket.user?.email || socket.user?.id}`);
|
|
300
|
-
// Notify all project rooms that this user left
|
|
301
|
-
// Socket.IO automatically tracks which rooms the socket was in
|
|
302
|
-
const rooms = Array.from(socket.rooms);
|
|
303
|
-
for (const room of rooms) {
|
|
304
|
-
// Only process project rooms (skip the socket's own room)
|
|
305
|
-
if (room.startsWith("project:") && socket.user) {
|
|
306
|
-
socket.to(room).emit("project:user-left", socket.user.id);
|
|
307
|
-
payload_1.default.logger.info(`Notified room ${room} that user ${socket.user.id} left`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
});
|
|
165
|
+
}
|
|
311
166
|
});
|
|
312
167
|
}
|
|
313
168
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -110,4 +110,23 @@ export interface RealtimeEventsPluginOptions {
|
|
|
110
110
|
* Custom event transformer
|
|
111
111
|
*/
|
|
112
112
|
transformEvent?: (event: RealtimeEventPayload) => RealtimeEventPayload;
|
|
113
|
+
/**
|
|
114
|
+
* Custom socket event handlers
|
|
115
|
+
* Register your own event handlers that will be attached to each authenticated socket
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* onSocketConnection: (socket, io, payload) => {
|
|
120
|
+
* socket.on('custom-event', (data) => {
|
|
121
|
+
* // Handle custom event
|
|
122
|
+
* socket.emit('custom-response', { success: true });
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* socket.on('join-room', (roomId) => {
|
|
126
|
+
* socket.join(`custom:${roomId}`);
|
|
127
|
+
* });
|
|
128
|
+
* }
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
onSocketConnection?: (socket: AuthenticatedSocket, io: any, payload: any) => void | Promise<void>;
|
|
113
132
|
}
|