mongo-realtime 1.0.3 → 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 +194 -84
- package/index.js +154 -17
- package/package.json +26 -26
package/README.md
CHANGED
|
@@ -27,16 +27,16 @@ npm install mongo-realtime
|
|
|
27
27
|
### Example setup
|
|
28
28
|
|
|
29
29
|
```javascript
|
|
30
|
-
const express = require(
|
|
31
|
-
const http = require(
|
|
32
|
-
const mongoose = require(
|
|
33
|
-
const MongoRealtime = require(
|
|
30
|
+
const express = require("express");
|
|
31
|
+
const http = require("http");
|
|
32
|
+
const mongoose = require("mongoose");
|
|
33
|
+
const MongoRealtime = require("mongo-realtime");
|
|
34
34
|
|
|
35
35
|
const app = express();
|
|
36
36
|
const server = http.createServer(app);
|
|
37
37
|
|
|
38
|
-
mongoose.connect(
|
|
39
|
-
|
|
38
|
+
mongoose.connect("mongodb://localhost:27017/mydb").then((c) => {
|
|
39
|
+
console.log("Connected to db", c.connection.name);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
MongoRealtime.init({
|
|
@@ -45,15 +45,15 @@ MongoRealtime.init({
|
|
|
45
45
|
ignore: ["posts"], // ignore 'posts' collection
|
|
46
46
|
onSocket: (socket) => {
|
|
47
47
|
console.log(`Client connected: ${socket.id}`);
|
|
48
|
-
socket.emit(
|
|
48
|
+
socket.emit("welcome", { message: "Connection successful!" });
|
|
49
49
|
},
|
|
50
50
|
offSocket: (socket, reason) => {
|
|
51
51
|
console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
|
|
52
|
-
}
|
|
52
|
+
},
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
server.listen(3000, () => {
|
|
56
|
-
console.log(
|
|
56
|
+
console.log("Server started on port 3000");
|
|
57
57
|
});
|
|
58
58
|
```
|
|
59
59
|
|
|
@@ -67,22 +67,31 @@ Initializes the socket system and MongoDB Change Streams.
|
|
|
67
67
|
|
|
68
68
|
\* means required
|
|
69
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
|
-
|
|
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
|
|
84
85
|
- `MongoRealtime.connection`: MongoDB connection
|
|
85
|
-
- `MongoRealtime.
|
|
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
|
|
86
95
|
|
|
87
96
|
## 🎯 Emitted Events
|
|
88
97
|
|
|
@@ -90,42 +99,42 @@ The package automatically emits six types of events for each database change:
|
|
|
90
99
|
|
|
91
100
|
### Event Types
|
|
92
101
|
|
|
93
|
-
| Event
|
|
94
|
-
|
|
95
|
-
| `db:change`
|
|
96
|
-
| `db:{type}`
|
|
97
|
-
| `db:change:{collection}`
|
|
98
|
-
| `db:{type}:{collection}`
|
|
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` |
|
|
99
108
|
| `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
|
|
100
|
-
| `db:{type}:{collection}:{id}` | Type + document
|
|
109
|
+
| `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
|
|
110
|
+
| `db:stream:{streamId}` | By stream | `db:stream:myStreamId` |
|
|
101
111
|
|
|
102
112
|
### Event listeners
|
|
103
113
|
|
|
104
114
|
You can add serverside listeners to those db events to trigger specific actions on the server:
|
|
105
115
|
|
|
106
116
|
```js
|
|
107
|
-
function sendNotification(change){
|
|
117
|
+
function sendNotification(change) {
|
|
108
118
|
const userId = change.docId; // or change.documentKey._id
|
|
109
|
-
NotificationService.send(userId,"Welcome to DB");
|
|
119
|
+
NotificationService.send(userId, "Welcome to DB");
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
MongoRealtime.listen("db:insert:users",sendNotification);
|
|
122
|
+
MongoRealtime.listen("db:insert:users", sendNotification);
|
|
113
123
|
```
|
|
114
124
|
|
|
115
125
|
#### Adding many callback to one event
|
|
116
126
|
|
|
117
127
|
```js
|
|
118
|
-
MongoRealtime.listen("db:insert:users",anotherAction);
|
|
119
|
-
MongoRealtime.listen("db:insert:users",anotherAction2);
|
|
128
|
+
MongoRealtime.listen("db:insert:users", anotherAction);
|
|
129
|
+
MongoRealtime.listen("db:insert:users", anotherAction2);
|
|
120
130
|
```
|
|
121
131
|
|
|
122
132
|
#### Removing event listeners
|
|
123
133
|
|
|
124
134
|
```js
|
|
125
|
-
MongoRealtime.removeListener("db:insert:users",sendNotification); // remove this specific action from this event
|
|
135
|
+
MongoRealtime.removeListener("db:insert:users", sendNotification); // remove this specific action from this event
|
|
126
136
|
MongoRealtime.removeListener("db:insert:users"); // remove all actions from this event
|
|
127
137
|
MongoRealtime.removeAllListeners(); // remove all listeners
|
|
128
|
-
|
|
129
138
|
```
|
|
130
139
|
|
|
131
140
|
### Event Payload Structure
|
|
@@ -136,7 +145,7 @@ Each event contains the full MongoDB change object:
|
|
|
136
145
|
{
|
|
137
146
|
"_id": {...},
|
|
138
147
|
"col":"users", // same as ns.coll
|
|
139
|
-
"docId":"...", // same as documentKey._id
|
|
148
|
+
"docId":"...", // same as documentKey._id
|
|
140
149
|
"operationType": "insert|update|delete|replace",
|
|
141
150
|
"documentKey": { "_id": "..." },
|
|
142
151
|
"ns": { "db": "mydb", "coll": "users" },
|
|
@@ -154,14 +163,13 @@ MongoRealtime.init({
|
|
|
154
163
|
connection: connection,
|
|
155
164
|
server: server,
|
|
156
165
|
onSocket: (socket) => {
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
socket.on("subscribe:users", () => {
|
|
167
|
+
socket.join("users-room");
|
|
159
168
|
});
|
|
160
169
|
},
|
|
161
|
-
|
|
162
170
|
});
|
|
163
171
|
|
|
164
|
-
MongoRealtime.io.to(
|
|
172
|
+
MongoRealtime.io.to("users-room").emit("custom-event", data);
|
|
165
173
|
```
|
|
166
174
|
|
|
167
175
|
### Client-side - Receiving updates
|
|
@@ -169,31 +177,31 @@ MongoRealtime.io.to('users-room').emit('custom-event', data);
|
|
|
169
177
|
```html
|
|
170
178
|
<!DOCTYPE html>
|
|
171
179
|
<html>
|
|
172
|
-
<head>
|
|
173
|
-
|
|
174
|
-
</head>
|
|
175
|
-
<body>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</body>
|
|
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>
|
|
197
205
|
</html>
|
|
198
206
|
```
|
|
199
207
|
|
|
@@ -204,15 +212,15 @@ MongoRealtime.init({
|
|
|
204
212
|
connection: mongoose.connection,
|
|
205
213
|
server: server,
|
|
206
214
|
onSocket: (socket) => {
|
|
207
|
-
socket.on(
|
|
208
|
-
console.error(
|
|
215
|
+
socket.on("error", (error) => {
|
|
216
|
+
console.error("Socket error:", error);
|
|
209
217
|
});
|
|
210
218
|
},
|
|
211
219
|
offSocket: (socket, reason) => {
|
|
212
|
-
if (reason ===
|
|
213
|
-
console.log(
|
|
220
|
+
if (reason === "transport error") {
|
|
221
|
+
console.log("Transport error detected");
|
|
214
222
|
}
|
|
215
|
-
}
|
|
223
|
+
},
|
|
216
224
|
});
|
|
217
225
|
```
|
|
218
226
|
|
|
@@ -221,13 +229,13 @@ MongoRealtime.init({
|
|
|
221
229
|
### Socket Authentication
|
|
222
230
|
|
|
223
231
|
```javascript
|
|
224
|
-
function authenticateSocket(token, socket){
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
MongoRealtime.init({
|
|
@@ -236,14 +244,116 @@ MongoRealtime.init({
|
|
|
236
244
|
authentify: authenticateSocket,
|
|
237
245
|
middlewares: [
|
|
238
246
|
(socket, next) => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
247
|
+
console.log(`User is authenticated: ${socket.user.email}`);
|
|
248
|
+
next();
|
|
249
|
+
},
|
|
242
250
|
],
|
|
243
251
|
offSocket: (socket, reason) => {
|
|
244
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
|
|
245
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");
|
|
246
350
|
});
|
|
351
|
+
|
|
352
|
+
MongoRealtime.addListStream(
|
|
353
|
+
"bestPosts",
|
|
354
|
+
"posts",
|
|
355
|
+
async (doc) => doc.likes > (await PostService.getLikesThreshold())
|
|
356
|
+
);
|
|
247
357
|
```
|
|
248
358
|
|
|
249
359
|
## 📚 Dependencies
|
|
@@ -267,4 +377,4 @@ MIT
|
|
|
267
377
|
|
|
268
378
|
## 🤝 Contributing
|
|
269
379
|
|
|
270
|
-
Contributions are welcome! Feel free to open an issue or submit a pull request
|
|
380
|
+
Contributions are welcome! Feel free to open an issue or submit a pull request.
|
package/index.js
CHANGED
|
@@ -1,11 +1,55 @@
|
|
|
1
1
|
const { Server } = require("socket.io");
|
|
2
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
|
+
|
|
3
39
|
class MongoRealtime {
|
|
4
40
|
/** @type {import("socket.io").Server} */ static io;
|
|
5
41
|
/** @type {import("mongoose").Connection} */ static connection;
|
|
6
|
-
/** @type {[import("socket.io").Socket]} */ static sockets = [];
|
|
7
42
|
/** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
|
|
8
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 = [];
|
|
9
53
|
|
|
10
54
|
/**
|
|
11
55
|
* Initializes the socket system.
|
|
@@ -17,6 +61,7 @@ class MongoRealtime {
|
|
|
17
61
|
* @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
|
|
18
62
|
* @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
|
|
19
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.
|
|
20
65
|
* @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
|
|
21
66
|
* @param {[String]} options.ignore - Collections to ignore. Can override `watch`
|
|
22
67
|
*
|
|
@@ -25,16 +70,14 @@ class MongoRealtime {
|
|
|
25
70
|
connection,
|
|
26
71
|
server,
|
|
27
72
|
authentify,
|
|
28
|
-
middlewares=[],
|
|
73
|
+
middlewares = [],
|
|
74
|
+
autoListStream,
|
|
29
75
|
onSocket,
|
|
30
76
|
offSocket,
|
|
31
77
|
watch = [],
|
|
32
78
|
ignore = [],
|
|
33
79
|
}) {
|
|
34
|
-
if (this.io)
|
|
35
|
-
this.io.close(() => {
|
|
36
|
-
this.sockets = [];
|
|
37
|
-
});
|
|
80
|
+
if (this.io) this.io.close();
|
|
38
81
|
this.io = new Server(server);
|
|
39
82
|
this.connection = connection;
|
|
40
83
|
|
|
@@ -44,20 +87,21 @@ class MongoRealtime {
|
|
|
44
87
|
this.io.use(async (socket, next) => {
|
|
45
88
|
if (!!authentify) {
|
|
46
89
|
try {
|
|
47
|
-
const token =
|
|
48
|
-
|
|
90
|
+
const token =
|
|
91
|
+
socket.handshake.auth.token ||
|
|
92
|
+
socket.handshake.headers.authorization;
|
|
93
|
+
if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
|
|
49
94
|
|
|
50
|
-
const authorized =await authentify(token, socket);
|
|
51
|
-
if (authorized===true) return next(); // exactly returns true
|
|
95
|
+
const authorized = await authentify(token, socket);
|
|
96
|
+
if (authorized === true) return next(); // exactly returns true
|
|
52
97
|
|
|
53
|
-
return next(new Error("
|
|
98
|
+
return next(new Error("UNAUTHORIZED"));
|
|
54
99
|
} catch (error) {
|
|
55
|
-
return next(new Error("
|
|
100
|
+
return next(new Error("AUTH_ERROR"));
|
|
56
101
|
}
|
|
57
102
|
} else {
|
|
58
103
|
return next();
|
|
59
104
|
}
|
|
60
|
-
|
|
61
105
|
});
|
|
62
106
|
|
|
63
107
|
for (let middleware of middlewares) {
|
|
@@ -65,16 +109,18 @@ class MongoRealtime {
|
|
|
65
109
|
}
|
|
66
110
|
|
|
67
111
|
this.io.on("connection", (socket) => {
|
|
68
|
-
this.sockets = [...this.io.sockets.sockets.values()];
|
|
69
112
|
if (onSocket) onSocket(socket);
|
|
70
113
|
|
|
71
114
|
socket.on("disconnect", (r) => {
|
|
72
|
-
this.sockets = [...this.io.sockets.sockets.values()];
|
|
73
115
|
if (offSocket) offSocket(socket, r);
|
|
74
116
|
});
|
|
75
117
|
});
|
|
76
118
|
|
|
77
|
-
connection.once("open", () => {
|
|
119
|
+
connection.once("open", async () => {
|
|
120
|
+
this.collections = (await connection.listCollections()).map(
|
|
121
|
+
(c) => c.name
|
|
122
|
+
);
|
|
123
|
+
|
|
78
124
|
let pipeline = [];
|
|
79
125
|
if (watch.length !== 0 && ignore.length === 0) {
|
|
80
126
|
pipeline = [{ $match: { "ns.coll": { $in: watch } } }];
|
|
@@ -98,7 +144,65 @@ class MongoRealtime {
|
|
|
98
144
|
fullDocumentBeforeChange: "whenAvailable",
|
|
99
145
|
});
|
|
100
146
|
|
|
101
|
-
|
|
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) => {
|
|
102
206
|
const colName = change.ns.coll.toLowerCase();
|
|
103
207
|
change.col = colName;
|
|
104
208
|
|
|
@@ -156,6 +260,39 @@ class MongoRealtime {
|
|
|
156
260
|
this.#listeners[key].push(cb);
|
|
157
261
|
}
|
|
158
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
|
+
|
|
159
296
|
/**
|
|
160
297
|
* Remove one or all listeners of an event
|
|
161
298
|
*
|
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": "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
|
+
}
|