mongo-realtime 1.0.4 → 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 +380 -270
- package/index.js +315 -177
- package/package.json +26 -26
package/README.md
CHANGED
|
@@ -1,270 +1,380 @@
|
|
|
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(
|
|
31
|
-
const http = require(
|
|
32
|
-
const mongoose = require(
|
|
33
|
-
const MongoRealtime = require(
|
|
34
|
-
|
|
35
|
-
const app = express();
|
|
36
|
-
const server = http.createServer(app);
|
|
37
|
-
|
|
38
|
-
mongoose.connect(
|
|
39
|
-
|
|
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(
|
|
49
|
-
},
|
|
50
|
-
offSocket: (socket, reason) => {
|
|
51
|
-
console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
server.listen(3000, () => {
|
|
56
|
-
console.log(
|
|
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
|
|
71
|
-
|
|
72
|
-
| `options.connection`
|
|
73
|
-
| `options.server`
|
|
74
|
-
| `options.authentify`
|
|
75
|
-
| `options.middlewares`
|
|
76
|
-
| `options.onSocket`
|
|
77
|
-
| `options.offSocket`
|
|
78
|
-
| `options.watch`
|
|
79
|
-
| `options.ignore`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- `MongoRealtime.
|
|
85
|
-
- `MongoRealtime.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
| `options.autoListStream` | `Array[String]` | Collections to automatically stream to clients. Default is all |
|
|
81
|
+
|
|
82
|
+
#### Static Properties and Methods
|
|
83
|
+
|
|
84
|
+
- `MongoRealtime.addListStream(streamId, collection, filter)`: Manually add a list stream for a specific collection and filter
|
|
85
|
+
- `MongoRealtime.connection`: MongoDB connection
|
|
86
|
+
- `MongoRealtime.collections`: Array of database collections
|
|
87
|
+
- `MongoRealtime.io`: Socket.IO server instance
|
|
88
|
+
- `MongoRealtime.init(options)`: Initialize the package with options
|
|
89
|
+
- `MongoRealtime.listen(event, callback)`: Add an event listener for database changes
|
|
90
|
+
- `MongoRealtime.notifyListeners(event, data)`: Manually emit an event to all listeners
|
|
91
|
+
- `MongoRealtime.removeStream(streamId)`: Remove a previously added stream by id
|
|
92
|
+
- `MongoRealtime.removeListener(event, callback)`: Remove a specific event listener or all listeners for an event
|
|
93
|
+
- `MongoRealtime.removeAllListeners()`: Remove all event listeners
|
|
94
|
+
- `MongoRealtime.sockets()`: Returns an array of connected sockets
|
|
95
|
+
|
|
96
|
+
## 🎯 Emitted Events
|
|
97
|
+
|
|
98
|
+
The package automatically emits six types of events for each database change:
|
|
99
|
+
|
|
100
|
+
### Event Types
|
|
101
|
+
|
|
102
|
+
| Event | Description | Example |
|
|
103
|
+
| ----------------------------- | ----------------- | ------------------------------------------ |
|
|
104
|
+
| `db:change` | All changes | Any collection change |
|
|
105
|
+
| `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
|
|
106
|
+
| `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
|
|
107
|
+
| `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
|
|
108
|
+
| `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
|
|
109
|
+
| `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
|
|
110
|
+
| `db:stream:{streamId}` | By stream | `db:stream:myStreamId` |
|
|
111
|
+
|
|
112
|
+
### Event listeners
|
|
113
|
+
|
|
114
|
+
You can add serverside listeners to those db events to trigger specific actions on the server:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
function sendNotification(change) {
|
|
118
|
+
const userId = change.docId; // or change.documentKey._id
|
|
119
|
+
NotificationService.send(userId, "Welcome to DB");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
MongoRealtime.listen("db:insert:users", sendNotification);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Adding many callback to one event
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
MongoRealtime.listen("db:insert:users", anotherAction);
|
|
129
|
+
MongoRealtime.listen("db:insert:users", anotherAction2);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Removing event listeners
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
MongoRealtime.removeListener("db:insert:users", sendNotification); // remove this specific action from this event
|
|
136
|
+
MongoRealtime.removeListener("db:insert:users"); // remove all actions from this event
|
|
137
|
+
MongoRealtime.removeAllListeners(); // remove all listeners
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Event Payload Structure
|
|
141
|
+
|
|
142
|
+
Each event contains the full MongoDB change object:
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
{
|
|
146
|
+
"_id": {...},
|
|
147
|
+
"col":"users", // same as ns.coll
|
|
148
|
+
"docId":"...", // same as documentKey._id
|
|
149
|
+
"operationType": "insert|update|delete|replace",
|
|
150
|
+
"documentKey": { "_id": "..." },
|
|
151
|
+
"ns": { "db": "mydb", "coll": "users" },
|
|
152
|
+
"fullDocument": {...},
|
|
153
|
+
"fullDocumentBeforeChange": {...}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 🔨 Usage Examples
|
|
158
|
+
|
|
159
|
+
### Server-side - Listening to specific events
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
MongoRealtime.init({
|
|
163
|
+
connection: connection,
|
|
164
|
+
server: server,
|
|
165
|
+
onSocket: (socket) => {
|
|
166
|
+
socket.on("subscribe:users", () => {
|
|
167
|
+
socket.join("users-room");
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
MongoRealtime.io.to("users-room").emit("custom-event", data);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Client-side - Receiving updates
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<!DOCTYPE html>
|
|
179
|
+
<html>
|
|
180
|
+
<head>
|
|
181
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<script>
|
|
185
|
+
const socket = io();
|
|
186
|
+
|
|
187
|
+
socket.on("db:change", (change) => {
|
|
188
|
+
console.log("Detected change:", change);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
socket.on("db:insert:users", (change) => {
|
|
192
|
+
console.log("New user:", change.fullDocument);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const userId = "507f1f77bcf86cd799439011";
|
|
196
|
+
socket.on(`db:update:users:${userId}`, (change) => {
|
|
197
|
+
console.log("Updated user:", change.fullDocument);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
socket.on("db:delete", (change) => {
|
|
201
|
+
console.log("Deleted document:", change.documentKey);
|
|
202
|
+
});
|
|
203
|
+
</script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Error Handling
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
MongoRealtime.init({
|
|
212
|
+
connection: mongoose.connection,
|
|
213
|
+
server: server,
|
|
214
|
+
onSocket: (socket) => {
|
|
215
|
+
socket.on("error", (error) => {
|
|
216
|
+
console.error("Socket error:", error);
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
offSocket: (socket, reason) => {
|
|
220
|
+
if (reason === "transport error") {
|
|
221
|
+
console.log("Transport error detected");
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 🔒 Security
|
|
228
|
+
|
|
229
|
+
### Socket Authentication
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
function authenticateSocket(token, socket) {
|
|
233
|
+
const verify = AuthService.verifyToken(token);
|
|
234
|
+
if (verify) {
|
|
235
|
+
socket.user = verify.user; // attach user info to socket
|
|
236
|
+
return true; // should return true to accept the connection
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
MongoRealtime.init({
|
|
242
|
+
connection: mongoose.connection,
|
|
243
|
+
server: server,
|
|
244
|
+
authentify: authenticateSocket,
|
|
245
|
+
middlewares: [
|
|
246
|
+
(socket, next) => {
|
|
247
|
+
console.log(`User is authenticated: ${socket.user.email}`);
|
|
248
|
+
next();
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
offSocket: (socket, reason) => {
|
|
252
|
+
console.log(`Socket ${socket.id} disconnected: ${reason}`);
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Setup list streams
|
|
258
|
+
|
|
259
|
+
The server will automatically emit a list of filtered documents from the specified collections after each change.\
|
|
260
|
+
Each list stream requires an unique `streamId`, the `collection` name, and an optional `filter` function that returns a boolean or a promise resolving to a boolean.
|
|
261
|
+
Clients receive the list on the event `db:stream:{streamId}`.\
|
|
262
|
+
For best practice, two list streams can't have the same `streamId` or else an error will be thrown. This will prevent accidental overrides. You can still remove a stream with `MongoRealtime.removeListStream(streamId)` and add it again or use inside a `try-catch` scope.
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
MongoRealtime.init({
|
|
266
|
+
connection: mongoose.connection,
|
|
267
|
+
server: server,
|
|
268
|
+
autoListStream: ["users"], // automatically stream users collection only
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // will throw an error as streamId 'users' already exists
|
|
272
|
+
|
|
273
|
+
MongoRealtime.removeListStream("users"); // remove the previous stream
|
|
274
|
+
MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // client can listen to db:stream:users
|
|
275
|
+
|
|
276
|
+
MongoRealtime.addListStream("usersWithEmail", "users", (doc) => !!doc.email); // client can listen to db:stream:usersWithEmail
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### ⚠️ NOTICE
|
|
280
|
+
|
|
281
|
+
When `autoListStream` is not set, all collections are automatically streamed and WITHOUT any filter.\
|
|
282
|
+
That means that if you have a `posts` collection, all documents from this collection will be sent to the clients on each change.\
|
|
283
|
+
And as two list streams can't have the same `streamId`, if you want to add a filtered list stream with id `posts`, you must set `autoListStream` to an array NOT containing `"posts"`or `MongoRealtime.removeListStream("posts")` after initialization.\
|
|
284
|
+
Therefore, if you want to add a filtered list stream for all collections, you must set `autoListStream` to an empty array.
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
MongoRealtime.init({
|
|
288
|
+
connection: mongoose.connection,
|
|
289
|
+
server: server,
|
|
290
|
+
autoListStream: [], // stream no collection automatically (you can add your own filtered streams later)
|
|
291
|
+
});
|
|
292
|
+
MongoRealtime.addListStream("posts", "posts", (doc) => !!doc.title); // client can listen to db:stream:posts
|
|
293
|
+
MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // will not throw an error
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Usecase for id based streams
|
|
297
|
+
|
|
298
|
+
```javascript
|
|
299
|
+
MongoRealtime.init({
|
|
300
|
+
connection: mongoose.connection,
|
|
301
|
+
server: server,
|
|
302
|
+
authentify: (token, socket) => {
|
|
303
|
+
try {
|
|
304
|
+
socket.uid = decodeToken(token).uid; // setup user id from token
|
|
305
|
+
return true;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
onSocket: (socket) => {
|
|
311
|
+
// setup a personal stream for each connected user
|
|
312
|
+
MongoRealtime.addListStream(
|
|
313
|
+
`userPost:${socket.uid}`,
|
|
314
|
+
"posts",
|
|
315
|
+
(doc) => doc._id == socket.uid
|
|
316
|
+
);
|
|
317
|
+
},
|
|
318
|
+
offSocket: (socket) => {
|
|
319
|
+
// clean up when user disconnects
|
|
320
|
+
MongoRealtime.removeListStream(`userPost:${socket.uid}`);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// ...
|
|
325
|
+
// or activate stream from a controller or middleware
|
|
326
|
+
app.get("/my-posts", (req, res) => {
|
|
327
|
+
const { user } = req;
|
|
328
|
+
try {
|
|
329
|
+
MongoRealtime.addListStream(
|
|
330
|
+
`userPosts:${user._id}`,
|
|
331
|
+
"posts",
|
|
332
|
+
(doc) => doc.authorId === user._id
|
|
333
|
+
);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
// stream already exists
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
res.send("Stream activated");
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### Usecase with async filter
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
// MongoRealtime.init({...});
|
|
346
|
+
|
|
347
|
+
MongoRealtime.addListStream("authorizedUsers", "users", async (doc) => {
|
|
348
|
+
const isAdmin = await UserService.isAdmin(doc._id);
|
|
349
|
+
return isAdmin && doc.email.endsWith("@mydomain.com");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
MongoRealtime.addListStream(
|
|
353
|
+
"bestPosts",
|
|
354
|
+
"posts",
|
|
355
|
+
async (doc) => doc.likes > (await PostService.getLikesThreshold())
|
|
356
|
+
);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## 📚 Dependencies
|
|
360
|
+
|
|
361
|
+
- `socket.io`: WebSocket management
|
|
362
|
+
- `mongoose`: MongoDB ODM with Change Streams support
|
|
363
|
+
|
|
364
|
+
## 🐛 Troubleshooting
|
|
365
|
+
|
|
366
|
+
### MongoDB must be in Replica Set mode
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
mongod --replSet rs0
|
|
370
|
+
|
|
371
|
+
rs.initiate()
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## 📄 License
|
|
375
|
+
|
|
376
|
+
MIT
|
|
377
|
+
|
|
378
|
+
## 🤝 Contributing
|
|
379
|
+
|
|
380
|
+
Contributions are welcome! Feel free to open an issue or submit a pull request.
|
package/index.js
CHANGED
|
@@ -1,177 +1,315 @@
|
|
|
1
|
-
const { Server } = require("socket.io");
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
const { Server } = require("socket.io");
|
|
2
|
+
|
|
3
|
+
function sortObj(obj = {}) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (let k of Object.keys(obj).sort()) {
|
|
6
|
+
const v = obj[k];
|
|
7
|
+
out[k] = typeof v == "object" && !Array.isArray(v) ? sortObj(v) : v;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ChangeStreamDocument
|
|
15
|
+
* @property {"insert"|"update"|"replace"|"delete"|"invalidate"|"drop"|"dropDatabase"|"rename"} operationType
|
|
16
|
+
* The type of operation that triggered the event.
|
|
17
|
+
*
|
|
18
|
+
* @property {Object} ns
|
|
19
|
+
* @property {string} ns.db - Database name
|
|
20
|
+
* @property {string} ns.coll - Collection name
|
|
21
|
+
*
|
|
22
|
+
* @property {Object} documentKey
|
|
23
|
+
* @property {import("bson").ObjectId|string} documentKey._id - The document’s identifier
|
|
24
|
+
*
|
|
25
|
+
* @property {Object} [fullDocument]
|
|
26
|
+
* The full document after the change (only present if `fullDocument: "updateLookup"` is enabled).
|
|
27
|
+
*
|
|
28
|
+
* @property {Object} [updateDescription]
|
|
29
|
+
* @property {Object.<string, any>} [updateDescription.updatedFields]
|
|
30
|
+
* Fields that were updated during an update operation.
|
|
31
|
+
* @property {string[]} [updateDescription.removedFields]
|
|
32
|
+
* Fields that were removed during an update operation.
|
|
33
|
+
*
|
|
34
|
+
* @property {Object} [rename] - Info about the collection rename (if operationType is "rename").
|
|
35
|
+
*
|
|
36
|
+
* @property {Date} [clusterTime] - Logical timestamp of the event.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
class MongoRealtime {
|
|
40
|
+
/** @type {import("socket.io").Server} */ static io;
|
|
41
|
+
/** @type {import("mongoose").Connection} */ static connection;
|
|
42
|
+
/** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
|
|
43
|
+
{};
|
|
44
|
+
/** @type {Record<String, [Object]>} */
|
|
45
|
+
static #cache = {};
|
|
46
|
+
static sockets = () => [...this.io.sockets.sockets.values()];
|
|
47
|
+
|
|
48
|
+
/**@type {Record<String, {collection:String,filter: (doc:Object)=>Promise<boolean>}>} */
|
|
49
|
+
static #streams = {};
|
|
50
|
+
|
|
51
|
+
/** @type {[String]} - All DB collections */
|
|
52
|
+
static collections = [];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initializes the socket system.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} options
|
|
58
|
+
* @param {import("mongoose").Connection} options.connection - Active Mongoose connection
|
|
59
|
+
* @param {(token:String, socket: import("socket.io").Socket) => boolean | Promise<boolean>} options.authentify - Auth function that should return true if `token` is valid
|
|
60
|
+
* @param {[( socket: import("socket.io").Socket, next: (err?: ExtendedError) => void) => void]} options.middlewares - Register mmiddlewares on incoming socket
|
|
61
|
+
* @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
|
|
62
|
+
* @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
|
|
63
|
+
* @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
|
|
64
|
+
* @param {[String]} options.autoListStream - Collections to stream automatically. If empty, will stream no collection. If null, will stream all collections.
|
|
65
|
+
* @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
|
|
66
|
+
* @param {[String]} options.ignore - Collections to ignore. Can override `watch`
|
|
67
|
+
*
|
|
68
|
+
*/
|
|
69
|
+
static init({
|
|
70
|
+
connection,
|
|
71
|
+
server,
|
|
72
|
+
authentify,
|
|
73
|
+
middlewares = [],
|
|
74
|
+
autoListStream,
|
|
75
|
+
onSocket,
|
|
76
|
+
offSocket,
|
|
77
|
+
watch = [],
|
|
78
|
+
ignore = [],
|
|
79
|
+
}) {
|
|
80
|
+
if (this.io) this.io.close();
|
|
81
|
+
this.io = new Server(server);
|
|
82
|
+
this.connection = connection;
|
|
83
|
+
|
|
84
|
+
watch = watch.map((s) => s.toLowerCase());
|
|
85
|
+
ignore = ignore.map((s) => s.toLowerCase());
|
|
86
|
+
|
|
87
|
+
this.io.use(async (socket, next) => {
|
|
88
|
+
if (!!authentify) {
|
|
89
|
+
try {
|
|
90
|
+
const token =
|
|
91
|
+
socket.handshake.auth.token ||
|
|
92
|
+
socket.handshake.headers.authorization;
|
|
93
|
+
if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
|
|
94
|
+
|
|
95
|
+
const authorized = await authentify(token, socket);
|
|
96
|
+
if (authorized === true) return next(); // exactly returns true
|
|
97
|
+
|
|
98
|
+
return next(new Error("UNAUTHORIZED"));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return next(new Error("AUTH_ERROR"));
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
return next();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
for (let middleware of middlewares) {
|
|
108
|
+
this.io.use(middleware);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.io.on("connection", (socket) => {
|
|
112
|
+
if (onSocket) onSocket(socket);
|
|
113
|
+
|
|
114
|
+
socket.on("disconnect", (r) => {
|
|
115
|
+
if (offSocket) offSocket(socket, r);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
connection.once("open", async () => {
|
|
120
|
+
this.collections = (await connection.listCollections()).map(
|
|
121
|
+
(c) => c.name
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
let pipeline = [];
|
|
125
|
+
if (watch.length !== 0 && ignore.length === 0) {
|
|
126
|
+
pipeline = [{ $match: { "ns.coll": { $in: watch } } }];
|
|
127
|
+
} else if (watch.length === 0 && ignore.length !== 0) {
|
|
128
|
+
pipeline = [{ $match: { "ns.coll": { $nin: ignore } } }];
|
|
129
|
+
} else if (watch.length !== 0 && ignore.length !== 0) {
|
|
130
|
+
pipeline = [
|
|
131
|
+
{
|
|
132
|
+
$match: {
|
|
133
|
+
$and: [
|
|
134
|
+
{ "ns.coll": { $in: watch } },
|
|
135
|
+
{ "ns.coll": { $nin: ignore } },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const changeStream = connection.watch(pipeline, {
|
|
143
|
+
fullDocument: "updateLookup",
|
|
144
|
+
fullDocumentBeforeChange: "whenAvailable",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** Setup main streams */
|
|
148
|
+
let collectionsToStream = [];
|
|
149
|
+
if (autoListStream == null) collectionsToStream = this.collections;
|
|
150
|
+
else
|
|
151
|
+
collectionsToStream = this.collections.filter((c) =>
|
|
152
|
+
autoListStream.includes(c)
|
|
153
|
+
);
|
|
154
|
+
for (let col of collectionsToStream) this.addListStream(col, col);
|
|
155
|
+
|
|
156
|
+
/** Emit streams on change */
|
|
157
|
+
changeStream.on("change", async (change) => {
|
|
158
|
+
const coll = change.ns.coll;
|
|
159
|
+
|
|
160
|
+
if (!this.#cache[coll]) {
|
|
161
|
+
this.#cache[coll] = await connection.db
|
|
162
|
+
.collection(coll)
|
|
163
|
+
.find({})
|
|
164
|
+
.toArray();
|
|
165
|
+
} else
|
|
166
|
+
switch (change.operationType) {
|
|
167
|
+
case "insert":
|
|
168
|
+
this.#cache[coll].push(change.fullDocument);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case "update":
|
|
172
|
+
case "replace":
|
|
173
|
+
this.#cache[coll] = this.#cache[coll].map((doc) =>
|
|
174
|
+
doc._id.toString() === change.documentKey._id.toString()
|
|
175
|
+
? change.fullDocument
|
|
176
|
+
: doc
|
|
177
|
+
);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case "delete":
|
|
181
|
+
this.#cache[coll] = this.#cache[coll].filter(
|
|
182
|
+
(doc) =>
|
|
183
|
+
doc._id.toString() !== change.documentKey._id.toString()
|
|
184
|
+
);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Object.entries(this.#streams).forEach(async (e) => {
|
|
189
|
+
const key = e[0];
|
|
190
|
+
const value = e[1];
|
|
191
|
+
if (value.collection != coll) return;
|
|
192
|
+
const filterResults = await Promise.allSettled(
|
|
193
|
+
this.#cache[coll].map((doc) => value.filter(doc))
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const filtered = this.#cache[coll].filter(
|
|
197
|
+
(_, i) => filterResults[i] && filterResults[i].value
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
this.io.emit(`db:stream:${key}`, filtered);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** Emit listen events on change */
|
|
205
|
+
changeStream.on("change", async (change) => {
|
|
206
|
+
const colName = change.ns.coll.toLowerCase();
|
|
207
|
+
change.col = colName;
|
|
208
|
+
|
|
209
|
+
const type = change.operationType;
|
|
210
|
+
const id = change.documentKey?._id;
|
|
211
|
+
|
|
212
|
+
const e_change = "db:change";
|
|
213
|
+
const e_change_type = `db:${type}`;
|
|
214
|
+
const e_change_col = `${e_change}:${colName}`;
|
|
215
|
+
const e_change_type_col = `${e_change_type}:${colName}`;
|
|
216
|
+
|
|
217
|
+
const events = [
|
|
218
|
+
e_change,
|
|
219
|
+
e_change_type,
|
|
220
|
+
e_change_col,
|
|
221
|
+
e_change_type_col,
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
if (id) {
|
|
225
|
+
change.docId = id;
|
|
226
|
+
const e_change_doc = `${e_change_col}:${id}`;
|
|
227
|
+
const e_change_type_doc = `${e_change_type_col}:${id}`;
|
|
228
|
+
events.push(e_change_doc, e_change_type_doc);
|
|
229
|
+
}
|
|
230
|
+
for (let e of events) {
|
|
231
|
+
this.io.emit(e, change);
|
|
232
|
+
this.notifyListeners(e, change);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Notify all event listeners
|
|
240
|
+
*
|
|
241
|
+
* @param {String} e - Name of the event
|
|
242
|
+
* @param {ChangeStreamDocument} change - Change Stream
|
|
243
|
+
*/
|
|
244
|
+
static notifyListeners(e, change) {
|
|
245
|
+
if (this.#listeners[e]) {
|
|
246
|
+
for (let c of this.#listeners[e]) {
|
|
247
|
+
c(change);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Subscribe to an event
|
|
254
|
+
*
|
|
255
|
+
* @param {String} key - Name of the event
|
|
256
|
+
* @param {(change:ChangeStreamDocument)=>void} cb - Callback
|
|
257
|
+
*/
|
|
258
|
+
static listen(key, cb) {
|
|
259
|
+
if (!this.#listeners[key]) this.#listeners[key] = [];
|
|
260
|
+
this.#listeners[key].push(cb);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
*
|
|
265
|
+
* @param {String} streamId - StreamId of the list stream
|
|
266
|
+
* @param {String} collection - Name of the collection to stream
|
|
267
|
+
* @param { (doc:Object )=>Promise<boolean>} filter - Collection filter
|
|
268
|
+
*
|
|
269
|
+
* Register a new list stream to listen
|
|
270
|
+
*/
|
|
271
|
+
static addListStream(streamId, collection, filter) {
|
|
272
|
+
if (!streamId) throw new Error("Stream id is required");
|
|
273
|
+
if (!collection) throw new Error("Collection is required");
|
|
274
|
+
|
|
275
|
+
filter ??= (_, __) => true;
|
|
276
|
+
if (this.#streams[streamId]) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Stream '${streamId}' already registered or is reserved.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
this.#streams[streamId] = {
|
|
282
|
+
collection,
|
|
283
|
+
filter,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @param {String} streamId - StreamId of the stream
|
|
289
|
+
*
|
|
290
|
+
* Delete a registered stream
|
|
291
|
+
*/
|
|
292
|
+
static removeListStream(streamId) {
|
|
293
|
+
delete this.#streams[streamId];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Remove one or all listeners of an event
|
|
298
|
+
*
|
|
299
|
+
* @param {String} key - Name of the event
|
|
300
|
+
* @param {(change:ChangeStreamDocument)=>void} cb - Callback
|
|
301
|
+
*/
|
|
302
|
+
static removeListener(key, cb) {
|
|
303
|
+
if (cb) this.#listeners[key] = this.#listeners[key].filter((c) => c != cb);
|
|
304
|
+
else this.#listeners[key] = [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Unsubscribe to all events
|
|
309
|
+
*/
|
|
310
|
+
static removeAllListeners() {
|
|
311
|
+
this.#listeners = {};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = MongoRealtime;
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mongo-realtime",
|
|
3
|
-
"version": "1.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": "git+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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mongo-realtime",
|
|
3
|
+
"version": "1.1.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": "git+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
|
+
}
|