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