mongo-realtime 1.0.3 → 1.0.4
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 +270 -270
- package/index.js +177 -178
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
# Mongo Realtime
|
|
2
|
-
|
|
3
|
-
A Node.js package that combines Socket.IO and MongoDB Change Streams to deliver real-time database updates to your WebSocket clients.
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
## 🚀 Features
|
|
8
|
-
|
|
9
|
-
- **Real-time updates**: Automatically detects changes in MongoDB and broadcasts them via Socket.IO
|
|
10
|
-
- **Granular events**: Emits specific events by operation type, collection, and document
|
|
11
|
-
- **Connection management**: Customizable callbacks for socket connections/disconnections
|
|
12
|
-
- **TypeScript compatible**: JSDoc annotations for better development experience
|
|
13
|
-
|
|
14
|
-
## 📦 Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install mongo-realtime
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Setup
|
|
21
|
-
|
|
22
|
-
### Prerequisites
|
|
23
|
-
|
|
24
|
-
- MongoDB running as a replica set (required for Change Streams)
|
|
25
|
-
- Node.js HTTP server (See below how to configure an HTTP server with Express)
|
|
26
|
-
|
|
27
|
-
### Example setup
|
|
28
|
-
|
|
29
|
-
```javascript
|
|
30
|
-
const express = require('express');
|
|
31
|
-
const http = require('http');
|
|
32
|
-
const mongoose = require('mongoose');
|
|
33
|
-
const MongoRealtime = require('mongo-realtime');
|
|
34
|
-
|
|
35
|
-
const app = express();
|
|
36
|
-
const server = http.createServer(app);
|
|
37
|
-
|
|
38
|
-
mongoose.connect('mongodb://localhost:27017/mydb').then((c) => {
|
|
39
|
-
console.log("Connected to db",c.connection.name);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
MongoRealtime.init({
|
|
43
|
-
connection: mongoose.connection,
|
|
44
|
-
server: server,
|
|
45
|
-
ignore: ["posts"], // ignore 'posts' collection
|
|
46
|
-
onSocket: (socket) => {
|
|
47
|
-
console.log(`Client connected: ${socket.id}`);
|
|
48
|
-
socket.emit('welcome', { message: 'Connection successful!' });
|
|
49
|
-
},
|
|
50
|
-
offSocket: (socket, reason) => {
|
|
51
|
-
console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
server.listen(3000, () => {
|
|
56
|
-
console.log('Server started on port 3000');
|
|
57
|
-
});
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## 📋 API
|
|
61
|
-
|
|
62
|
-
### `MongoRealtime.init(options)`
|
|
63
|
-
|
|
64
|
-
Initializes the socket system and MongoDB Change Streams.
|
|
65
|
-
|
|
66
|
-
#### Parameters
|
|
67
|
-
|
|
68
|
-
\* means required
|
|
69
|
-
|
|
70
|
-
| Parameter | Type | Description |
|
|
71
|
-
|-----------|------|-------------|
|
|
72
|
-
| `options.connection` | `mongoose.Connection`* | Active Mongoose connection |
|
|
73
|
-
| `options.server` | `http.Server`* | HTTP server to attach Socket.IO |
|
|
74
|
-
| `options.authentify` | `Function` | Function to authenticate socket connections. Should return true if authenticated |
|
|
75
|
-
| `options.middlewares` | `Array[Function]` | Array of Socket.IO middlewares |
|
|
76
|
-
| `options.onSocket` | `Function` | Callback on socket connection |
|
|
77
|
-
| `options.offSocket` | `Function` | Callback on socket disconnection |
|
|
78
|
-
| `options.watch` | `Array[String]` | Collections to only watch. Listen to all when is empty |
|
|
79
|
-
| `options.ignore` | `Array[String]` | Collections to only ignore. Overrides watch array |
|
|
80
|
-
|
|
81
|
-
#### Static Properties
|
|
82
|
-
|
|
83
|
-
- `MongoRealtime.io`: Socket.IO server instance
|
|
84
|
-
- `MongoRealtime.connection`: MongoDB connection
|
|
85
|
-
- `MongoRealtime.sockets`: Array of connected sockets
|
|
86
|
-
|
|
87
|
-
## 🎯 Emitted Events
|
|
88
|
-
|
|
89
|
-
The package automatically emits six types of events for each database change:
|
|
90
|
-
|
|
91
|
-
### Event Types
|
|
92
|
-
|
|
93
|
-
| Event | Description | Example |
|
|
94
|
-
|-------|-------------|---------|
|
|
95
|
-
| `db:change` | All changes | Any collection change |
|
|
96
|
-
| `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
|
|
97
|
-
| `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
|
|
98
|
-
| `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
|
|
99
|
-
| `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
|
|
100
|
-
| `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
|
|
101
|
-
|
|
102
|
-
### Event listeners
|
|
103
|
-
|
|
104
|
-
You can add serverside listeners to those db events to trigger specific actions on the server:
|
|
105
|
-
|
|
106
|
-
```js
|
|
107
|
-
function sendNotification(change){
|
|
108
|
-
const userId = change.docId; // or change.documentKey._id
|
|
109
|
-
NotificationService.send(userId,"Welcome to DB");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
MongoRealtime.listen("db:insert:users",sendNotification);
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
#### Adding many callback to one event
|
|
116
|
-
|
|
117
|
-
```js
|
|
118
|
-
MongoRealtime.listen("db:insert:users",anotherAction);
|
|
119
|
-
MongoRealtime.listen("db:insert:users",anotherAction2);
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
#### Removing event listeners
|
|
123
|
-
|
|
124
|
-
```js
|
|
125
|
-
MongoRealtime.removeListener("db:insert:users",sendNotification); // remove this specific action from this event
|
|
126
|
-
MongoRealtime.removeListener("db:insert:users"); // remove all actions from this event
|
|
127
|
-
MongoRealtime.removeAllListeners(); // remove all listeners
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Event Payload Structure
|
|
132
|
-
|
|
133
|
-
Each event contains the full MongoDB change object:
|
|
134
|
-
|
|
135
|
-
```javascript
|
|
136
|
-
{
|
|
137
|
-
"_id": {...},
|
|
138
|
-
"col":"users", // same as ns.coll
|
|
139
|
-
"docId":"...", // same as documentKey._id
|
|
140
|
-
"operationType": "insert|update|delete|replace",
|
|
141
|
-
"documentKey": { "_id": "..." },
|
|
142
|
-
"ns": { "db": "mydb", "coll": "users" },
|
|
143
|
-
"fullDocument": {...},
|
|
144
|
-
"fullDocumentBeforeChange": {...}
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## 🔨 Usage Examples
|
|
149
|
-
|
|
150
|
-
### Server-side - Listening to specific events
|
|
151
|
-
|
|
152
|
-
```javascript
|
|
153
|
-
MongoRealtime.init({
|
|
154
|
-
connection: connection,
|
|
155
|
-
server: server,
|
|
156
|
-
onSocket: (socket) => {
|
|
157
|
-
socket.on('subscribe:users', () => {
|
|
158
|
-
socket.join('users-room');
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
MongoRealtime.io.to('users-room').emit('custom-event', data);
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### Client-side - Receiving updates
|
|
168
|
-
|
|
169
|
-
```html
|
|
170
|
-
<!DOCTYPE html>
|
|
171
|
-
<html>
|
|
172
|
-
<head>
|
|
173
|
-
<script src="/socket.io/socket.io.js"></script>
|
|
174
|
-
</head>
|
|
175
|
-
<body>
|
|
176
|
-
<script>
|
|
177
|
-
const socket = io();
|
|
178
|
-
|
|
179
|
-
socket.on('db:change', (change) => {
|
|
180
|
-
console.log('Detected change:', change);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
socket.on('db:insert:users', (change) => {
|
|
184
|
-
console.log('New user:', change.fullDocument);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const userId = '507f1f77bcf86cd799439011';
|
|
188
|
-
socket.on(`db:update:users:${userId}`, (change) => {
|
|
189
|
-
console.log('Updated user:', change.fullDocument);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
socket.on('db:delete', (change) => {
|
|
193
|
-
console.log('Deleted document:', change.documentKey);
|
|
194
|
-
});
|
|
195
|
-
</script>
|
|
196
|
-
</body>
|
|
197
|
-
</html>
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
## Error Handling
|
|
201
|
-
|
|
202
|
-
```javascript
|
|
203
|
-
MongoRealtime.init({
|
|
204
|
-
connection: mongoose.connection,
|
|
205
|
-
server: server,
|
|
206
|
-
onSocket: (socket) => {
|
|
207
|
-
socket.on('error', (error) => {
|
|
208
|
-
console.error('Socket error:', error);
|
|
209
|
-
});
|
|
210
|
-
},
|
|
211
|
-
offSocket: (socket, reason) => {
|
|
212
|
-
if (reason === 'transport error') {
|
|
213
|
-
console.log('Transport error detected');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## 🔒 Security
|
|
220
|
-
|
|
221
|
-
### Socket Authentication
|
|
222
|
-
|
|
223
|
-
```javascript
|
|
224
|
-
function authenticateSocket(token, socket){
|
|
225
|
-
const verify = AuthService.verifyToken(token);
|
|
226
|
-
if(verify){
|
|
227
|
-
socket.user = verify.user; // attach user info to socket
|
|
228
|
-
return true; // should return true to accept the connection
|
|
229
|
-
}
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
MongoRealtime.init({
|
|
234
|
-
connection: mongoose.connection,
|
|
235
|
-
server: server,
|
|
236
|
-
authentify: authenticateSocket,
|
|
237
|
-
middlewares: [
|
|
238
|
-
(socket, next) => {
|
|
239
|
-
console.log(`User is authenticated: ${socket.user.email}`);
|
|
240
|
-
next();
|
|
241
|
-
}
|
|
242
|
-
],
|
|
243
|
-
offSocket: (socket, reason) => {
|
|
244
|
-
console.log(`Socket ${socket.id} disconnected: ${reason}`);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## 📚 Dependencies
|
|
250
|
-
|
|
251
|
-
- `socket.io`: WebSocket management
|
|
252
|
-
- `mongoose`: MongoDB ODM with Change Streams support
|
|
253
|
-
|
|
254
|
-
## 🐛 Troubleshooting
|
|
255
|
-
|
|
256
|
-
### MongoDB must be in Replica Set mode
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
mongod --replSet rs0
|
|
260
|
-
|
|
261
|
-
rs.initiate()
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
## 📄 License
|
|
265
|
-
|
|
266
|
-
MIT
|
|
267
|
-
|
|
268
|
-
## 🤝 Contributing
|
|
269
|
-
|
|
270
|
-
Contributions are welcome! Feel free to open an issue or submit a pull request.****
|
|
1
|
+
# Mongo Realtime
|
|
2
|
+
|
|
3
|
+
A Node.js package that combines Socket.IO and MongoDB Change Streams to deliver real-time database updates to your WebSocket clients.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## 🚀 Features
|
|
8
|
+
|
|
9
|
+
- **Real-time updates**: Automatically detects changes in MongoDB and broadcasts them via Socket.IO
|
|
10
|
+
- **Granular events**: Emits specific events by operation type, collection, and document
|
|
11
|
+
- **Connection management**: Customizable callbacks for socket connections/disconnections
|
|
12
|
+
- **TypeScript compatible**: JSDoc annotations for better development experience
|
|
13
|
+
|
|
14
|
+
## 📦 Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install mongo-realtime
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### Prerequisites
|
|
23
|
+
|
|
24
|
+
- MongoDB running as a replica set (required for Change Streams)
|
|
25
|
+
- Node.js HTTP server (See below how to configure an HTTP server with Express)
|
|
26
|
+
|
|
27
|
+
### Example setup
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const express = require('express');
|
|
31
|
+
const http = require('http');
|
|
32
|
+
const mongoose = require('mongoose');
|
|
33
|
+
const MongoRealtime = require('mongo-realtime');
|
|
34
|
+
|
|
35
|
+
const app = express();
|
|
36
|
+
const server = http.createServer(app);
|
|
37
|
+
|
|
38
|
+
mongoose.connect('mongodb://localhost:27017/mydb').then((c) => {
|
|
39
|
+
console.log("Connected to db",c.connection.name);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
MongoRealtime.init({
|
|
43
|
+
connection: mongoose.connection,
|
|
44
|
+
server: server,
|
|
45
|
+
ignore: ["posts"], // ignore 'posts' collection
|
|
46
|
+
onSocket: (socket) => {
|
|
47
|
+
console.log(`Client connected: ${socket.id}`);
|
|
48
|
+
socket.emit('welcome', { message: 'Connection successful!' });
|
|
49
|
+
},
|
|
50
|
+
offSocket: (socket, reason) => {
|
|
51
|
+
console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
server.listen(3000, () => {
|
|
56
|
+
console.log('Server started on port 3000');
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 📋 API
|
|
61
|
+
|
|
62
|
+
### `MongoRealtime.init(options)`
|
|
63
|
+
|
|
64
|
+
Initializes the socket system and MongoDB Change Streams.
|
|
65
|
+
|
|
66
|
+
#### Parameters
|
|
67
|
+
|
|
68
|
+
\* means required
|
|
69
|
+
|
|
70
|
+
| Parameter | Type | Description |
|
|
71
|
+
|-----------|------|-------------|
|
|
72
|
+
| `options.connection` | `mongoose.Connection`* | Active Mongoose connection |
|
|
73
|
+
| `options.server` | `http.Server`* | HTTP server to attach Socket.IO |
|
|
74
|
+
| `options.authentify` | `Function` | Function to authenticate socket connections. Should return true if authenticated |
|
|
75
|
+
| `options.middlewares` | `Array[Function]` | Array of Socket.IO middlewares |
|
|
76
|
+
| `options.onSocket` | `Function` | Callback on socket connection |
|
|
77
|
+
| `options.offSocket` | `Function` | Callback on socket disconnection |
|
|
78
|
+
| `options.watch` | `Array[String]` | Collections to only watch. Listen to all when is empty |
|
|
79
|
+
| `options.ignore` | `Array[String]` | Collections to only ignore. Overrides watch array |
|
|
80
|
+
|
|
81
|
+
#### Static Properties
|
|
82
|
+
|
|
83
|
+
- `MongoRealtime.io`: Socket.IO server instance
|
|
84
|
+
- `MongoRealtime.connection`: MongoDB connection
|
|
85
|
+
- `MongoRealtime.sockets`: Array of connected sockets
|
|
86
|
+
|
|
87
|
+
## 🎯 Emitted Events
|
|
88
|
+
|
|
89
|
+
The package automatically emits six types of events for each database change:
|
|
90
|
+
|
|
91
|
+
### Event Types
|
|
92
|
+
|
|
93
|
+
| Event | Description | Example |
|
|
94
|
+
|-------|-------------|---------|
|
|
95
|
+
| `db:change` | All changes | Any collection change |
|
|
96
|
+
| `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
|
|
97
|
+
| `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
|
|
98
|
+
| `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
|
|
99
|
+
| `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
|
|
100
|
+
| `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
|
|
101
|
+
|
|
102
|
+
### Event listeners
|
|
103
|
+
|
|
104
|
+
You can add serverside listeners to those db events to trigger specific actions on the server:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
function sendNotification(change){
|
|
108
|
+
const userId = change.docId; // or change.documentKey._id
|
|
109
|
+
NotificationService.send(userId,"Welcome to DB");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
MongoRealtime.listen("db:insert:users",sendNotification);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Adding many callback to one event
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
MongoRealtime.listen("db:insert:users",anotherAction);
|
|
119
|
+
MongoRealtime.listen("db:insert:users",anotherAction2);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Removing event listeners
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
MongoRealtime.removeListener("db:insert:users",sendNotification); // remove this specific action from this event
|
|
126
|
+
MongoRealtime.removeListener("db:insert:users"); // remove all actions from this event
|
|
127
|
+
MongoRealtime.removeAllListeners(); // remove all listeners
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Event Payload Structure
|
|
132
|
+
|
|
133
|
+
Each event contains the full MongoDB change object:
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
{
|
|
137
|
+
"_id": {...},
|
|
138
|
+
"col":"users", // same as ns.coll
|
|
139
|
+
"docId":"...", // same as documentKey._id
|
|
140
|
+
"operationType": "insert|update|delete|replace",
|
|
141
|
+
"documentKey": { "_id": "..." },
|
|
142
|
+
"ns": { "db": "mydb", "coll": "users" },
|
|
143
|
+
"fullDocument": {...},
|
|
144
|
+
"fullDocumentBeforeChange": {...}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 🔨 Usage Examples
|
|
149
|
+
|
|
150
|
+
### Server-side - Listening to specific events
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
MongoRealtime.init({
|
|
154
|
+
connection: connection,
|
|
155
|
+
server: server,
|
|
156
|
+
onSocket: (socket) => {
|
|
157
|
+
socket.on('subscribe:users', () => {
|
|
158
|
+
socket.join('users-room');
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
MongoRealtime.io.to('users-room').emit('custom-event', data);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Client-side - Receiving updates
|
|
168
|
+
|
|
169
|
+
```html
|
|
170
|
+
<!DOCTYPE html>
|
|
171
|
+
<html>
|
|
172
|
+
<head>
|
|
173
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<script>
|
|
177
|
+
const socket = io();
|
|
178
|
+
|
|
179
|
+
socket.on('db:change', (change) => {
|
|
180
|
+
console.log('Detected change:', change);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
socket.on('db:insert:users', (change) => {
|
|
184
|
+
console.log('New user:', change.fullDocument);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const userId = '507f1f77bcf86cd799439011';
|
|
188
|
+
socket.on(`db:update:users:${userId}`, (change) => {
|
|
189
|
+
console.log('Updated user:', change.fullDocument);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
socket.on('db:delete', (change) => {
|
|
193
|
+
console.log('Deleted document:', change.documentKey);
|
|
194
|
+
});
|
|
195
|
+
</script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Error Handling
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
MongoRealtime.init({
|
|
204
|
+
connection: mongoose.connection,
|
|
205
|
+
server: server,
|
|
206
|
+
onSocket: (socket) => {
|
|
207
|
+
socket.on('error', (error) => {
|
|
208
|
+
console.error('Socket error:', error);
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
offSocket: (socket, reason) => {
|
|
212
|
+
if (reason === 'transport error') {
|
|
213
|
+
console.log('Transport error detected');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## 🔒 Security
|
|
220
|
+
|
|
221
|
+
### Socket Authentication
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
function authenticateSocket(token, socket){
|
|
225
|
+
const verify = AuthService.verifyToken(token);
|
|
226
|
+
if(verify){
|
|
227
|
+
socket.user = verify.user; // attach user info to socket
|
|
228
|
+
return true; // should return true to accept the connection
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
MongoRealtime.init({
|
|
234
|
+
connection: mongoose.connection,
|
|
235
|
+
server: server,
|
|
236
|
+
authentify: authenticateSocket,
|
|
237
|
+
middlewares: [
|
|
238
|
+
(socket, next) => {
|
|
239
|
+
console.log(`User is authenticated: ${socket.user.email}`);
|
|
240
|
+
next();
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
offSocket: (socket, reason) => {
|
|
244
|
+
console.log(`Socket ${socket.id} disconnected: ${reason}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## 📚 Dependencies
|
|
250
|
+
|
|
251
|
+
- `socket.io`: WebSocket management
|
|
252
|
+
- `mongoose`: MongoDB ODM with Change Streams support
|
|
253
|
+
|
|
254
|
+
## 🐛 Troubleshooting
|
|
255
|
+
|
|
256
|
+
### MongoDB must be in Replica Set mode
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
mongod --replSet rs0
|
|
260
|
+
|
|
261
|
+
rs.initiate()
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## 📄 License
|
|
265
|
+
|
|
266
|
+
MIT
|
|
267
|
+
|
|
268
|
+
## 🤝 Contributing
|
|
269
|
+
|
|
270
|
+
Contributions are welcome! Feel free to open an issue or submit a pull request.****
|
package/index.js
CHANGED
|
@@ -1,178 +1,177 @@
|
|
|
1
|
-
const { Server } = require("socket.io");
|
|
2
|
-
|
|
3
|
-
class MongoRealtime {
|
|
4
|
-
/** @type {import("socket.io").Server} */ static io;
|
|
5
|
-
/** @type {import("mongoose").Connection} */ static connection;
|
|
6
|
-
/** @type {[import("socket.io").Socket]} */ static sockets = [];
|
|
7
|
-
/** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
|
|
8
|
-
{};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Initializes the socket system.
|
|
12
|
-
*
|
|
13
|
-
* @param {Object} options
|
|
14
|
-
* @param {import("mongoose").Connection} options.connection - Active Mongoose connection
|
|
15
|
-
* @param {(token:String, socket: import("socket.io").Socket) => boolean | Promise<boolean>} options.authentify - Auth function that should return true if `token` is valid
|
|
16
|
-
* @param {[( socket: import("socket.io").Socket, next: (err?: ExtendedError) => void) => void]} options.middlewares - Register mmiddlewares on incoming socket
|
|
17
|
-
* @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
|
|
18
|
-
* @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
|
|
19
|
-
* @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
|
|
20
|
-
* @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
|
|
21
|
-
* @param {[String]} options.ignore - Collections to ignore. Can override `watch`
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
static init({
|
|
25
|
-
connection,
|
|
26
|
-
server,
|
|
27
|
-
authentify,
|
|
28
|
-
middlewares=[],
|
|
29
|
-
onSocket,
|
|
30
|
-
offSocket,
|
|
31
|
-
watch = [],
|
|
32
|
-
ignore = [],
|
|
33
|
-
}) {
|
|
34
|
-
if (this.io)
|
|
35
|
-
this.io.close(() => {
|
|
36
|
-
this.sockets = [];
|
|
37
|
-
});
|
|
38
|
-
this.io = new Server(server);
|
|
39
|
-
this.connection = connection;
|
|
40
|
-
|
|
41
|
-
watch = watch.map((s) => s.toLowerCase());
|
|
42
|
-
ignore = ignore.map((s) => s.toLowerCase());
|
|
43
|
-
|
|
44
|
-
this.io.use(async (socket, next) => {
|
|
45
|
-
if (!!authentify) {
|
|
46
|
-
try {
|
|
47
|
-
const token = socket.handshake.auth.token;
|
|
48
|
-
if (!token) return next(new Error("No token provided"));
|
|
49
|
-
|
|
50
|
-
const authorized =await authentify(token, socket);
|
|
51
|
-
if (authorized===true) return next(); // exactly returns true
|
|
52
|
-
|
|
53
|
-
return next(new Error("Unauthorized"));
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return next(new Error("Authentication error"));
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
return next();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{ "ns.coll": { $
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* @param {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* @param {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* @param {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
module.exports = MongoRealtime;
|
|
1
|
+
const { Server } = require("socket.io");
|
|
2
|
+
|
|
3
|
+
class MongoRealtime {
|
|
4
|
+
/** @type {import("socket.io").Server} */ static io;
|
|
5
|
+
/** @type {import("mongoose").Connection} */ static connection;
|
|
6
|
+
/** @type {[import("socket.io").Socket]} */ static sockets = [];
|
|
7
|
+
/** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
|
|
8
|
+
{};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the socket system.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {import("mongoose").Connection} options.connection - Active Mongoose connection
|
|
15
|
+
* @param {(token:String, socket: import("socket.io").Socket) => boolean | Promise<boolean>} options.authentify - Auth function that should return true if `token` is valid
|
|
16
|
+
* @param {[( socket: import("socket.io").Socket, next: (err?: ExtendedError) => void) => void]} options.middlewares - Register mmiddlewares on incoming socket
|
|
17
|
+
* @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
|
|
18
|
+
* @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
|
|
19
|
+
* @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
|
|
20
|
+
* @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
|
|
21
|
+
* @param {[String]} options.ignore - Collections to ignore. Can override `watch`
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
static init({
|
|
25
|
+
connection,
|
|
26
|
+
server,
|
|
27
|
+
authentify,
|
|
28
|
+
middlewares = [],
|
|
29
|
+
onSocket,
|
|
30
|
+
offSocket,
|
|
31
|
+
watch = [],
|
|
32
|
+
ignore = [],
|
|
33
|
+
}) {
|
|
34
|
+
if (this.io)
|
|
35
|
+
this.io.close(() => {
|
|
36
|
+
this.sockets = [];
|
|
37
|
+
});
|
|
38
|
+
this.io = new Server(server);
|
|
39
|
+
this.connection = connection;
|
|
40
|
+
|
|
41
|
+
watch = watch.map((s) => s.toLowerCase());
|
|
42
|
+
ignore = ignore.map((s) => s.toLowerCase());
|
|
43
|
+
|
|
44
|
+
this.io.use(async (socket, next) => {
|
|
45
|
+
if (!!authentify) {
|
|
46
|
+
try {
|
|
47
|
+
const token = socket.handshake.auth.token || socket.handshake.headers.authorization;
|
|
48
|
+
if (!token) return next(new Error("No token provided"));
|
|
49
|
+
|
|
50
|
+
const authorized =await authentify(token, socket);
|
|
51
|
+
if (authorized===true) return next(); // exactly returns true
|
|
52
|
+
|
|
53
|
+
return next(new Error("Unauthorized"));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return next(new Error("Authentication error"));
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
for (let middleware of middlewares) {
|
|
63
|
+
this.io.use(middleware);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.io.on("connection", (socket) => {
|
|
67
|
+
this.sockets = [...this.io.sockets.sockets.values()];
|
|
68
|
+
if (onSocket) onSocket(socket);
|
|
69
|
+
|
|
70
|
+
socket.on("disconnect", (r) => {
|
|
71
|
+
this.sockets = [...this.io.sockets.sockets.values()];
|
|
72
|
+
if (offSocket) offSocket(socket, r);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
connection.once("open", () => {
|
|
77
|
+
let pipeline = [];
|
|
78
|
+
if (watch.length !== 0 && ignore.length === 0) {
|
|
79
|
+
pipeline = [{ $match: { "ns.coll": { $in: watch } } }];
|
|
80
|
+
} else if (watch.length === 0 && ignore.length !== 0) {
|
|
81
|
+
pipeline = [{ $match: { "ns.coll": { $nin: ignore } } }];
|
|
82
|
+
} else if (watch.length !== 0 && ignore.length !== 0) {
|
|
83
|
+
pipeline = [
|
|
84
|
+
{
|
|
85
|
+
$match: {
|
|
86
|
+
$and: [
|
|
87
|
+
{ "ns.coll": { $in: watch } },
|
|
88
|
+
{ "ns.coll": { $nin: ignore } },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const changeStream = connection.watch(pipeline, {
|
|
96
|
+
fullDocument: "updateLookup",
|
|
97
|
+
fullDocumentBeforeChange: "whenAvailable",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
changeStream.on("change", (change) => {
|
|
101
|
+
const colName = change.ns.coll.toLowerCase();
|
|
102
|
+
change.col = colName;
|
|
103
|
+
|
|
104
|
+
const type = change.operationType;
|
|
105
|
+
const id = change.documentKey?._id;
|
|
106
|
+
|
|
107
|
+
const e_change = "db:change";
|
|
108
|
+
const e_change_type = `db:${type}`;
|
|
109
|
+
const e_change_col = `${e_change}:${colName}`;
|
|
110
|
+
const e_change_type_col = `${e_change_type}:${colName}`;
|
|
111
|
+
|
|
112
|
+
const events = [
|
|
113
|
+
e_change,
|
|
114
|
+
e_change_type,
|
|
115
|
+
e_change_col,
|
|
116
|
+
e_change_type_col,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
if (id) {
|
|
120
|
+
change.docId = id;
|
|
121
|
+
const e_change_doc = `${e_change_col}:${id}`;
|
|
122
|
+
const e_change_type_doc = `${e_change_type_col}:${id}`;
|
|
123
|
+
events.push(e_change_doc, e_change_type_doc);
|
|
124
|
+
}
|
|
125
|
+
for (let e of events) {
|
|
126
|
+
this.io.emit(e, change);
|
|
127
|
+
this.notifyListeners(e, change);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Notify all event listeners
|
|
135
|
+
*
|
|
136
|
+
* @param {String} e - Name of the event
|
|
137
|
+
* @param {ChangeStreamDocument} change - Change Stream
|
|
138
|
+
*/
|
|
139
|
+
static notifyListeners(e, change) {
|
|
140
|
+
if (this.#listeners[e]) {
|
|
141
|
+
for (let c of this.#listeners[e]) {
|
|
142
|
+
c(change);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Subscribe to an event
|
|
149
|
+
*
|
|
150
|
+
* @param {String} key - Name of the event
|
|
151
|
+
* @param {(change:ChangeStreamDocument)=>void} cb - Callback
|
|
152
|
+
*/
|
|
153
|
+
static listen(key, cb) {
|
|
154
|
+
if (!this.#listeners[key]) this.#listeners[key] = [];
|
|
155
|
+
this.#listeners[key].push(cb);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Remove one or all listeners of an event
|
|
160
|
+
*
|
|
161
|
+
* @param {String} key - Name of the event
|
|
162
|
+
* @param {(change:ChangeStreamDocument)=>void} cb - Callback
|
|
163
|
+
*/
|
|
164
|
+
static removeListener(key, cb) {
|
|
165
|
+
if (cb) this.#listeners[key] = this.#listeners[key].filter((c) => c != cb);
|
|
166
|
+
else this.#listeners[key] = [];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Unsubscribe to all events
|
|
171
|
+
*/
|
|
172
|
+
static removeAllListeners() {
|
|
173
|
+
this.#listeners = {};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = MongoRealtime;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mongo-realtime",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {},
|
|
6
6
|
"keywords": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"author": "D3R50N",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"repository": {
|
|
19
|
-
"url": "https://github.com/D3R50N/mongo-realtime.git"
|
|
19
|
+
"url": "git+https://github.com/D3R50N/mongo-realtime.git"
|
|
20
20
|
},
|
|
21
21
|
"description": "A Node.js package that combines Socket.IO and MongoDB Change Streams to deliver real-time database updates to your WebSocket clients.",
|
|
22
22
|
"dependencies": {
|