mongo-realtime 1.1.3 → 2.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 +111 -69
- package/index.js +286 -142
- package/package.json +27 -26
package/README.md
CHANGED
|
@@ -29,18 +29,19 @@ npm install mongo-realtime
|
|
|
29
29
|
```javascript
|
|
30
30
|
const express = require("express");
|
|
31
31
|
const http = require("http");
|
|
32
|
-
const mongoose = require("mongoose");
|
|
33
32
|
const MongoRealtime = require("mongo-realtime");
|
|
34
33
|
|
|
35
34
|
const app = express();
|
|
36
35
|
const server = http.createServer(app);
|
|
37
36
|
|
|
38
|
-
mongoose.connect("mongodb://localhost:27017/mydb").then((c) => {
|
|
39
|
-
console.log("Connected to db", c.connection.name);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
37
|
MongoRealtime.init({
|
|
43
|
-
|
|
38
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
39
|
+
onDbConnect: (conn) => {
|
|
40
|
+
console.log("Connected to db", conn.name);
|
|
41
|
+
},
|
|
42
|
+
onDbError: (err) => {
|
|
43
|
+
console.log(err.message);
|
|
44
|
+
},
|
|
44
45
|
server: server,
|
|
45
46
|
ignore: ["posts"], // ignore 'posts' collection
|
|
46
47
|
onSocket: (socket) => {
|
|
@@ -57,6 +58,44 @@ server.listen(3000, () => {
|
|
|
57
58
|
});
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
## Breaking Changes since v2.x.x
|
|
62
|
+
|
|
63
|
+
In older versions, streams receive all documents and send them to sockets. This can lead to performance issues with large collections.\
|
|
64
|
+
From version 2.x.x, streams now require a limit (default 50 documents) from the client side to avoid overloading the server and network.\
|
|
65
|
+
Receiving streamed docs now works like this:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
socket.emit("realtime", {
|
|
69
|
+
limit: 100,
|
|
70
|
+
streamId: "users",
|
|
71
|
+
reverse: true,
|
|
72
|
+
registerId: "random-client-generated-id",
|
|
73
|
+
});
|
|
74
|
+
/*
|
|
75
|
+
* Request 100 documents from 'users' stream (streamId defined server-side or collection name if autoStream)
|
|
76
|
+
* 'reverse' to get the latest documents first
|
|
77
|
+
* 'registerId' is a unique id generated by the client to identify this request. It will be used to listen to the response event.
|
|
78
|
+
* The response return an object {results: [...], count: number, total:number, remaining:number, coll:string}
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
socket.on("realtime:users:random-client-generated-id", (data) => {
|
|
82
|
+
console.log("Received streamed documents:", data.results); // 100 max per
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
socket.emit("realtime", {
|
|
86
|
+
limit: 50,
|
|
87
|
+
streamId: "usersWithEmail", // make sure this stream is defined server-side
|
|
88
|
+
registerId: "specific-request-id",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
socket.on("realtime:usersWithEmail:specific-request-id", (data) => {
|
|
92
|
+
console.log("Received streamed documents with email:", data.results); // 50 max per
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Make sure to listen before emitting the request to avoid missing the response
|
|
96
|
+
// To stop receiving streamed documents for a specific request just use socket.off
|
|
97
|
+
```
|
|
98
|
+
|
|
60
99
|
## 📋 API
|
|
61
100
|
|
|
62
101
|
### `MongoRealtime.init(options)`
|
|
@@ -67,21 +106,25 @@ Initializes the socket system and MongoDB Change Streams.
|
|
|
67
106
|
|
|
68
107
|
\* means required
|
|
69
108
|
|
|
70
|
-
| Parameter
|
|
71
|
-
|
|
|
72
|
-
| `options.
|
|
73
|
-
| `options.
|
|
74
|
-
| `options.
|
|
75
|
-
| `options.
|
|
76
|
-
| `options.
|
|
77
|
-
| `options.
|
|
78
|
-
| `options.
|
|
79
|
-
| `options.
|
|
80
|
-
| `options.
|
|
109
|
+
| Parameter | Type | Description |
|
|
110
|
+
| --------------------- | ----------------- | -------------------------------------------------------------------------------- |
|
|
111
|
+
| `options.dbUri` | `String`\* | Database URI |
|
|
112
|
+
| `options.dbOptions` | `Object` | Mongoose connection options |
|
|
113
|
+
| `options.onDbConnect` | `Function` | Callback on successful database connection |
|
|
114
|
+
| `options.onDbError` | `Function` | Callback on database connection error |
|
|
115
|
+
| `options.server` | `http.Server`\* | HTTP server to attach Socket.IO |
|
|
116
|
+
| `options.authentify` | `Function` | Function to authenticate socket connections. Should return true if authenticated |
|
|
117
|
+
| `options.middlewares` | `Array[Function]` | Array of Socket.IO middlewares |
|
|
118
|
+
| `options.onSocket` | `Function` | Callback on socket connection |
|
|
119
|
+
| `options.offSocket` | `Function` | Callback on socket disconnection |
|
|
120
|
+
| `options.watch` | `Array[String]` | Collections to only watch. Listen to all when is empty |
|
|
121
|
+
| `options.ignore` | `Array[String]` | Collections to only ignore. Overrides watch array |
|
|
122
|
+
| `options.autoStream` | `Array[String]` | Collections to automatically stream to clients. Default is all |
|
|
123
|
+
| `options.debug` | `Boolean` | Enable debug logs |
|
|
81
124
|
|
|
82
125
|
#### Static Properties and Methods
|
|
83
126
|
|
|
84
|
-
- `MongoRealtime.
|
|
127
|
+
- `MongoRealtime.addStream(streamId, collection, filter)`: Manually add a list stream for a specific collection and filter
|
|
85
128
|
- `MongoRealtime.connection`: MongoDB connection
|
|
86
129
|
- `MongoRealtime.collections`: Array of database collections
|
|
87
130
|
- `MongoRealtime.io`: Socket.IO server instance
|
|
@@ -99,15 +142,15 @@ The package automatically emits six types of events for each database change:
|
|
|
99
142
|
|
|
100
143
|
### Event Types
|
|
101
144
|
|
|
102
|
-
| Event
|
|
103
|
-
|
|
|
104
|
-
| `db:change`
|
|
105
|
-
| `db:{type}`
|
|
106
|
-
| `db:change:{collection}`
|
|
107
|
-
| `db:{type}:{collection}`
|
|
108
|
-
| `db:change:{collection}:{id}`
|
|
109
|
-
| `db:{type}:{collection}:{id}`
|
|
110
|
-
| `
|
|
145
|
+
| Event | Description | Example |
|
|
146
|
+
| ---------------------------------- | ----------------- | ------------------------------------------ |
|
|
147
|
+
| `db:change` | All changes | Any collection change |
|
|
148
|
+
| `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
|
|
149
|
+
| `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
|
|
150
|
+
| `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
|
|
151
|
+
| `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
|
|
152
|
+
| `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
|
|
153
|
+
| `realtime:{streamId}:{registerId}` | By stream | `realtime:myStreamId:registerId` |
|
|
111
154
|
|
|
112
155
|
### Event listeners
|
|
113
156
|
|
|
@@ -160,7 +203,7 @@ Each event contains the full MongoDB change object:
|
|
|
160
203
|
|
|
161
204
|
```javascript
|
|
162
205
|
MongoRealtime.init({
|
|
163
|
-
|
|
206
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
164
207
|
server: server,
|
|
165
208
|
onSocket: (socket) => {
|
|
166
209
|
socket.on("subscribe:users", () => {
|
|
@@ -209,7 +252,7 @@ MongoRealtime.io.to("users-room").emit("custom-event", data);
|
|
|
209
252
|
|
|
210
253
|
```javascript
|
|
211
254
|
MongoRealtime.init({
|
|
212
|
-
|
|
255
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
213
256
|
server: server,
|
|
214
257
|
onSocket: (socket) => {
|
|
215
258
|
socket.on("error", (error) => {
|
|
@@ -228,6 +271,15 @@ MongoRealtime.init({
|
|
|
228
271
|
|
|
229
272
|
### Socket Authentication
|
|
230
273
|
|
|
274
|
+
You can provide an `authentify` function in the init options to authenticate socket connections.\
|
|
275
|
+
The function receives the token (from `socket.handshake.auth.token` or `socket.handshake.headers.authorization`) and the socket object.\
|
|
276
|
+
When setted, it rejects connections based on this logic:
|
|
277
|
+
|
|
278
|
+
- Token not provided -> error `NO_TOKEN_PROVIDED`
|
|
279
|
+
- Token invalid or returns `false` -> error `UNAUTHORIZED`
|
|
280
|
+
- Any other error -> error `AUTH_ERROR`
|
|
281
|
+
- Return `true` to accept the connection
|
|
282
|
+
|
|
231
283
|
```javascript
|
|
232
284
|
function authenticateSocket(token, socket) {
|
|
233
285
|
const verify = AuthService.verifyToken(token);
|
|
@@ -239,7 +291,7 @@ function authenticateSocket(token, socket) {
|
|
|
239
291
|
}
|
|
240
292
|
|
|
241
293
|
MongoRealtime.init({
|
|
242
|
-
|
|
294
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
243
295
|
server: server,
|
|
244
296
|
authentify: authenticateSocket,
|
|
245
297
|
middlewares: [
|
|
@@ -254,64 +306,54 @@ MongoRealtime.init({
|
|
|
254
306
|
});
|
|
255
307
|
```
|
|
256
308
|
|
|
257
|
-
### Setup
|
|
309
|
+
### Setup streams
|
|
258
310
|
|
|
259
311
|
The server will automatically emit a list of filtered documents from the specified collections after each change.\
|
|
260
312
|
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
313
|
Clients receive the list on the event `db:stream:{streamId}`.\
|
|
262
314
|
|
|
263
|
-
On init, when `safeListStream` is `true`(default), 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.
|
|
264
|
-
|
|
265
315
|
```javascript
|
|
266
316
|
MongoRealtime.init({
|
|
267
|
-
|
|
317
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
268
318
|
server: server,
|
|
269
|
-
|
|
319
|
+
autoStream: ["users"], // automatically stream users collection only
|
|
270
320
|
});
|
|
271
321
|
|
|
272
|
-
MongoRealtime.
|
|
322
|
+
MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // will throw an error as streamId 'users' already exists
|
|
273
323
|
|
|
274
|
-
MongoRealtime.
|
|
275
|
-
MongoRealtime.
|
|
324
|
+
MongoRealtime.removeStream("users"); // remove the previous stream
|
|
325
|
+
MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // client can listen to db:stream:users
|
|
276
326
|
|
|
277
|
-
MongoRealtime.
|
|
327
|
+
MongoRealtime.addStream("usersWithEmail", "users", (doc) => !!doc.email); // client can listen to db:stream:usersWithEmail
|
|
278
328
|
```
|
|
279
329
|
|
|
280
330
|
#### ⚠️ NOTICE
|
|
281
331
|
|
|
282
|
-
When `
|
|
332
|
+
When `autoStream` is not set, all collections are automatically streamed and WITHOUT any filter.\
|
|
283
333
|
That means that if you have a `posts` collection, all documents from this collection will be sent to the clients on each change.\
|
|
284
334
|
|
|
285
|
-
Also, `safeListStream` is enabled by default. So, you can't add a list stream with id `posts` if `autoListStream` contains `"posts"` or is not set.\
|
|
286
|
-
|
|
287
|
-
If you want to add a filtered list stream with id `posts`, you must set `autoListStream` to an array NOT containing `"posts"`or call `MongoRealtime.removeListStream("posts")` after initialization.\
|
|
288
|
-
Therefore, if you want to add a filtered list stream for all collections, you must set `autoListStream` to an empty array.
|
|
289
|
-
|
|
290
|
-
To avoid all these issues, you can set `safeListStream` to `false` in the init options but be careful as this will allow you to override existing streams.
|
|
291
|
-
|
|
292
335
|
```javascript
|
|
293
336
|
MongoRealtime.init({
|
|
294
|
-
|
|
337
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
295
338
|
server: server,
|
|
296
|
-
|
|
339
|
+
autoStream: [], // stream no collection automatically (you can add your own filtered streams later)
|
|
297
340
|
});
|
|
298
341
|
// or
|
|
299
342
|
MongoRealtime.init({
|
|
300
|
-
|
|
343
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
301
344
|
server: server,
|
|
302
|
-
|
|
303
|
-
// Still stream all collections automatically but you can override them
|
|
345
|
+
// Stream all collections automatically but you can override them
|
|
304
346
|
}):
|
|
305
347
|
|
|
306
|
-
MongoRealtime.
|
|
307
|
-
MongoRealtime.
|
|
348
|
+
MongoRealtime.addStream("postsWithTitle", "posts", (doc) => !!doc.title); // client can listen to db:stream:posts
|
|
349
|
+
MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // will not throw an error cause 'users' is already streamed but with no filter
|
|
308
350
|
```
|
|
309
351
|
|
|
310
352
|
#### Usecase for id based streams
|
|
311
353
|
|
|
312
354
|
```javascript
|
|
313
355
|
MongoRealtime.init({
|
|
314
|
-
|
|
356
|
+
dbUri: "mongodb://localhost:27017/mydb",
|
|
315
357
|
server: server,
|
|
316
358
|
authentify: (token, socket) => {
|
|
317
359
|
try {
|
|
@@ -323,7 +365,7 @@ MongoRealtime.init({
|
|
|
323
365
|
},
|
|
324
366
|
onSocket: (socket) => {
|
|
325
367
|
// setup a personal stream for each connected user
|
|
326
|
-
MongoRealtime.
|
|
368
|
+
MongoRealtime.addStream(
|
|
327
369
|
`userPost:${socket.uid}`,
|
|
328
370
|
"posts",
|
|
329
371
|
(doc) => doc._id == socket.uid
|
|
@@ -331,7 +373,7 @@ MongoRealtime.init({
|
|
|
331
373
|
},
|
|
332
374
|
offSocket: (socket) => {
|
|
333
375
|
// clean up when user disconnects
|
|
334
|
-
MongoRealtime.
|
|
376
|
+
MongoRealtime.removeStream(`userPost:${socket.uid}`);
|
|
335
377
|
},
|
|
336
378
|
});
|
|
337
379
|
|
|
@@ -339,31 +381,27 @@ MongoRealtime.init({
|
|
|
339
381
|
// or activate stream from a controller or middleware
|
|
340
382
|
app.get("/my-posts", (req, res) => {
|
|
341
383
|
const { user } = req;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
348
|
-
} catch (e) {
|
|
349
|
-
// stream already exists
|
|
350
|
-
}
|
|
384
|
+
MongoRealtime.addStream(
|
|
385
|
+
`userPosts:${user._id}`,
|
|
386
|
+
"posts",
|
|
387
|
+
(doc) => doc.authorId === user._id
|
|
388
|
+
);
|
|
351
389
|
|
|
352
390
|
res.send("Stream activated");
|
|
353
391
|
});
|
|
354
392
|
```
|
|
355
393
|
|
|
356
|
-
|
|
394
|
+
### Usecase with async filter
|
|
357
395
|
|
|
358
396
|
```javascript
|
|
359
397
|
// MongoRealtime.init({...});
|
|
360
398
|
|
|
361
|
-
MongoRealtime.
|
|
399
|
+
MongoRealtime.addStream("authorizedUsers", "users", async (doc) => {
|
|
362
400
|
const isAdmin = await UserService.isAdmin(doc._id);
|
|
363
401
|
return isAdmin && doc.email.endsWith("@mydomain.com");
|
|
364
402
|
});
|
|
365
403
|
|
|
366
|
-
MongoRealtime.
|
|
404
|
+
MongoRealtime.addStream(
|
|
367
405
|
"bestPosts",
|
|
368
406
|
"posts",
|
|
369
407
|
async (doc) => doc.likes > (await PostService.getLikesThreshold())
|
|
@@ -379,12 +417,16 @@ MongoRealtime.addListStream(
|
|
|
379
417
|
|
|
380
418
|
### MongoDB must be in Replica Set mode
|
|
381
419
|
|
|
420
|
+
To use Change Streams, MongoDB must be running as a replica set. For local development, you can initiate a single-node replica set:
|
|
421
|
+
|
|
382
422
|
```bash
|
|
383
423
|
mongod --replSet rs0
|
|
384
424
|
|
|
385
425
|
rs.initiate()
|
|
386
426
|
```
|
|
387
427
|
|
|
428
|
+
For any other issues, open an issue on the GitHub repository.
|
|
429
|
+
|
|
388
430
|
## 📄 License
|
|
389
431
|
|
|
390
432
|
MIT
|
package/index.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
1
2
|
const { Server } = require("socket.io");
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
}
|
|
3
|
+
const { version } = require("./package.json");
|
|
4
|
+
const chalk = require("chalk");
|
|
12
5
|
|
|
13
6
|
/**
|
|
14
7
|
* @typedef {Object} ChangeStreamDocument
|
|
@@ -38,114 +31,123 @@ function sortObj(obj = {}) {
|
|
|
38
31
|
|
|
39
32
|
class MongoRealtime {
|
|
40
33
|
/** @type {import("socket.io").Server} */ static io;
|
|
41
|
-
/** @type {import("mongoose").Connection} */ static connection
|
|
34
|
+
/** @type {import("mongoose").Connection} */ static connection =
|
|
35
|
+
mongoose.connection;
|
|
42
36
|
/** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
|
|
43
37
|
{};
|
|
44
|
-
|
|
45
|
-
static #cache = {};
|
|
38
|
+
|
|
46
39
|
static sockets = () => [...this.io.sockets.sockets.values()];
|
|
47
40
|
|
|
48
41
|
/**@type {Record<String, {collection:String,filter: (doc:Object)=>Promise<boolean>}>} */
|
|
49
42
|
static #streams = {};
|
|
50
43
|
|
|
44
|
+
/**@type {Record<String, {expiration:Date,result:Record<String,{}> }>} */
|
|
45
|
+
static #data = {};
|
|
46
|
+
|
|
51
47
|
/** @type {[String]} - All DB collections */
|
|
52
48
|
static collections = [];
|
|
53
49
|
|
|
54
|
-
static #
|
|
50
|
+
static #debug = false;
|
|
51
|
+
|
|
52
|
+
static version = version;
|
|
53
|
+
|
|
54
|
+
static #check(fn, err) {
|
|
55
|
+
const result = fn();
|
|
56
|
+
if (!result) {
|
|
57
|
+
let src = fn.toString().trim();
|
|
58
|
+
|
|
59
|
+
let match =
|
|
60
|
+
src.match(/=>\s*([^{};]+)$/) ||
|
|
61
|
+
src.match(/return\s+([^;}]*)/) ||
|
|
62
|
+
src.match(/{([^}]*)}/);
|
|
63
|
+
|
|
64
|
+
const expr = err ?? (match ? match[1].trim() : src);
|
|
65
|
+
|
|
66
|
+
throw new Error(`MongoRealtime failed to check "${expr}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static #log(message, type = 0) {
|
|
71
|
+
const text = `[REALTIME] ${message}`;
|
|
72
|
+
switch (type) {
|
|
73
|
+
case 1:
|
|
74
|
+
console.log(chalk.bold.hex("#11AA60FF")(text));
|
|
75
|
+
break;
|
|
76
|
+
case 2:
|
|
77
|
+
console.log(chalk.bold.bgHex("#257993")(text));
|
|
78
|
+
break;
|
|
79
|
+
case 3:
|
|
80
|
+
console.log(chalk.bold.yellow(text));
|
|
81
|
+
break;
|
|
82
|
+
case 4:
|
|
83
|
+
console.log(chalk.bold.red(text));
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case 5:
|
|
87
|
+
console.log(chalk.italic(text));
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
console.log(text);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static #debugLog(message) {
|
|
97
|
+
if (this.#debug) this.#log("[DEBUG] " + message, 5);
|
|
98
|
+
}
|
|
55
99
|
|
|
56
100
|
/**
|
|
57
101
|
* Initializes the socket system.
|
|
58
102
|
*
|
|
59
103
|
* @param {Object} options
|
|
60
|
-
* @param {
|
|
104
|
+
* @param {String} options.dbUri - Database URI
|
|
105
|
+
* @param {mongoose.ConnectOptions | undefined} options.dbOptions - Database connect options
|
|
61
106
|
* @param {(token:String, socket: import("socket.io").Socket) => boolean | Promise<boolean>} options.authentify - Auth function that should return true if `token` is valid
|
|
62
107
|
* @param {[( socket: import("socket.io").Socket, next: (err?: ExtendedError) => void) => void]} options.middlewares - Register mmiddlewares on incoming socket
|
|
108
|
+
* @param {(conn:mongoose.Connection) => void} options.onDbConnect - Callback triggered when a socket connects
|
|
109
|
+
* @param {(err:Error) => void} options.onDbError - Callback triggered when a socket connects
|
|
63
110
|
* @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
|
|
64
111
|
* @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
|
|
65
112
|
* @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
|
|
66
|
-
* @param {[String]} options.
|
|
113
|
+
* @param {[String]} options.autoStream - Collections to stream automatically. If empty, will stream no collection. If null, will stream all collections.
|
|
67
114
|
* @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
|
|
68
115
|
* @param {[String]} options.ignore - Collections to ignore. Can override `watch`
|
|
69
|
-
* @param {bool} options.
|
|
116
|
+
* @param {bool} options.debug - Enable debug mode
|
|
70
117
|
*
|
|
71
118
|
*/
|
|
72
|
-
static init({
|
|
73
|
-
|
|
119
|
+
static async init({
|
|
120
|
+
dbUri,
|
|
121
|
+
dbOptions,
|
|
74
122
|
server,
|
|
123
|
+
onDbConnect,
|
|
124
|
+
onDbError,
|
|
75
125
|
authentify,
|
|
76
|
-
middlewares = [],
|
|
77
|
-
autoListStream,
|
|
78
126
|
onSocket,
|
|
79
127
|
offSocket,
|
|
80
|
-
|
|
128
|
+
debug = false,
|
|
129
|
+
autoStream,
|
|
130
|
+
middlewares = [],
|
|
81
131
|
watch = [],
|
|
82
132
|
ignore = [],
|
|
83
133
|
}) {
|
|
84
|
-
|
|
85
|
-
this.io = new Server(server);
|
|
86
|
-
this.connection = connection;
|
|
87
|
-
this.#safeListStream = !!safeListStream;
|
|
88
|
-
|
|
89
|
-
watch = watch.map((s) => s.toLowerCase());
|
|
90
|
-
ignore = ignore.map((s) => s.toLowerCase());
|
|
91
|
-
|
|
92
|
-
this.io.use(async (socket, next) => {
|
|
93
|
-
if (!!authentify) {
|
|
94
|
-
try {
|
|
95
|
-
const token =
|
|
96
|
-
socket.handshake.auth.token ||
|
|
97
|
-
socket.handshake.headers.authorization;
|
|
98
|
-
if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
|
|
99
|
-
|
|
100
|
-
const authorized = await authentify(token, socket);
|
|
101
|
-
if (authorized === true) return next(); // exactly returns true
|
|
102
|
-
|
|
103
|
-
return next(new Error("UNAUTHORIZED"));
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return next(new Error("AUTH_ERROR"));
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
return next();
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
for (let middleware of middlewares) {
|
|
113
|
-
this.io.use(middleware);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
this.io.on("connection", (socket) => {
|
|
117
|
-
if (onSocket) onSocket(socket);
|
|
134
|
+
this.#log(`MongoRealtime version (${this.version})`, 2);
|
|
118
135
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!this.#cache[coll]) {
|
|
125
|
-
this.#cache[coll] = await connection.db
|
|
126
|
-
.collection(coll)
|
|
127
|
-
.find({})
|
|
128
|
-
.toArray();
|
|
129
|
-
}
|
|
130
|
-
const filterResults = await Promise.allSettled(
|
|
131
|
-
this.#cache[coll].map((doc) => stream.filter(doc))
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const filtered = this.#cache[coll].filter(
|
|
135
|
-
(_, i) => filterResults[i] && filterResults[i].value
|
|
136
|
-
);
|
|
137
|
-
this.io.emit(`db:stream[register][${registerId}]`, filtered);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
socket.on("disconnect", (r) => {
|
|
141
|
-
if (offSocket) offSocket(socket, r);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
136
|
+
if (this.io) this.io.close();
|
|
137
|
+
this.#check(() => dbUri);
|
|
138
|
+
this.#check(() => server);
|
|
139
|
+
this.#debug = debug;
|
|
144
140
|
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
this.io = new Server(server);
|
|
142
|
+
this.connection.once("open", async () => {
|
|
143
|
+
this.collections = (await this.connection.listCollections()).map(
|
|
147
144
|
(c) => c.name
|
|
148
145
|
);
|
|
146
|
+
this.#debugLog(
|
|
147
|
+
`${this.collections.length} collections found : ${this.collections.join(
|
|
148
|
+
", "
|
|
149
|
+
)}`
|
|
150
|
+
);
|
|
149
151
|
|
|
150
152
|
let pipeline = [];
|
|
151
153
|
if (watch.length !== 0 && ignore.length === 0) {
|
|
@@ -165,76 +167,63 @@ class MongoRealtime {
|
|
|
165
167
|
];
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
const changeStream = connection.watch(pipeline, {
|
|
170
|
+
const changeStream = this.connection.watch(pipeline, {
|
|
169
171
|
fullDocument: "updateLookup",
|
|
170
172
|
fullDocumentBeforeChange: "whenAvailable",
|
|
171
173
|
});
|
|
172
174
|
|
|
173
175
|
/** Setup main streams */
|
|
174
176
|
let collectionsToStream = [];
|
|
175
|
-
if (
|
|
177
|
+
if (autoStream == null) collectionsToStream = this.collections;
|
|
176
178
|
else
|
|
177
179
|
collectionsToStream = this.collections.filter((c) =>
|
|
178
|
-
|
|
180
|
+
autoStream.includes(c)
|
|
179
181
|
);
|
|
180
|
-
for (let col of collectionsToStream) this.
|
|
182
|
+
for (let col of collectionsToStream) this.addStream(col, col);
|
|
183
|
+
this.#debugLog(
|
|
184
|
+
`Auto stream on collections : ${collectionsToStream.join(", ")}`
|
|
185
|
+
);
|
|
181
186
|
|
|
182
|
-
/** Emit
|
|
187
|
+
/** Emit listen events on change */
|
|
183
188
|
changeStream.on("change", async (change) => {
|
|
184
189
|
const coll = change.ns.coll;
|
|
190
|
+
const colName = coll.toLowerCase();
|
|
191
|
+
const doc = change.fullDocument;
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
this.#cache[coll] = await connection.db
|
|
188
|
-
.collection(coll)
|
|
189
|
-
.find({})
|
|
190
|
-
.toArray();
|
|
191
|
-
} else
|
|
192
|
-
switch (change.operationType) {
|
|
193
|
-
case "insert":
|
|
194
|
-
this.#cache[coll].push(change.fullDocument);
|
|
195
|
-
break;
|
|
193
|
+
this.#debugLog(`Collection '${colName}' changed`);
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
case "replace":
|
|
199
|
-
this.#cache[coll] = this.#cache[coll].map((doc) =>
|
|
200
|
-
doc._id.toString() === change.documentKey._id.toString()
|
|
201
|
-
? change.fullDocument
|
|
202
|
-
: doc
|
|
203
|
-
);
|
|
204
|
-
break;
|
|
195
|
+
change.col = colName;
|
|
205
196
|
|
|
197
|
+
const type = change.operationType;
|
|
198
|
+
const id = change.documentKey?._id.toString();
|
|
199
|
+
|
|
200
|
+
for (const k in this.#streams) {
|
|
201
|
+
const stream = this.#streams[k];
|
|
202
|
+
if (stream.collection != coll) continue;
|
|
203
|
+
|
|
204
|
+
Promise.resolve(stream.filter(doc)).then((ok) => {
|
|
205
|
+
if (ok) {
|
|
206
|
+
this.io.emit(`realtime:${k}`, {
|
|
207
|
+
results: [doc],
|
|
208
|
+
coll: coll,
|
|
209
|
+
count: 1,
|
|
210
|
+
total: 1,
|
|
211
|
+
remaining: 0,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
for (let k in this.#data) {
|
|
217
|
+
if (!k.startsWith(`${coll}-`) || !this.#data[k].result[id]) continue;
|
|
218
|
+
doc._id = doc._id.toString();
|
|
219
|
+
switch (change.operationType) {
|
|
206
220
|
case "delete":
|
|
207
|
-
this.#
|
|
208
|
-
(doc) =>
|
|
209
|
-
doc._id.toString() !== change.documentKey._id.toString()
|
|
210
|
-
);
|
|
221
|
+
delete this.#data[k].result[id];
|
|
211
222
|
break;
|
|
223
|
+
default:
|
|
224
|
+
this.#data[k].result[id] = doc;
|
|
212
225
|
}
|
|
213
|
-
|
|
214
|
-
Object.entries(this.#streams).forEach(async (e) => {
|
|
215
|
-
const key = e[0];
|
|
216
|
-
const value = e[1];
|
|
217
|
-
if (value.collection != coll) return;
|
|
218
|
-
const filterResults = await Promise.allSettled(
|
|
219
|
-
this.#cache[coll].map((doc) => value.filter(doc))
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const filtered = this.#cache[coll].filter(
|
|
223
|
-
(_, i) => filterResults[i] && filterResults[i].value
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
this.io.emit(`db:stream:${key}`, filtered);
|
|
227
|
-
this.notifyListeners(`db:stream:${key}`, filtered);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
/** Emit listen events on change */
|
|
232
|
-
changeStream.on("change", async (change) => {
|
|
233
|
-
const colName = change.ns.coll.toLowerCase();
|
|
234
|
-
change.col = colName;
|
|
235
|
-
|
|
236
|
-
const type = change.operationType;
|
|
237
|
-
const id = change.documentKey?._id;
|
|
226
|
+
}
|
|
238
227
|
|
|
239
228
|
const e_change = "db:change";
|
|
240
229
|
const e_change_type = `db:${type}`;
|
|
@@ -260,6 +249,163 @@ class MongoRealtime {
|
|
|
260
249
|
}
|
|
261
250
|
});
|
|
262
251
|
});
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await mongoose.connect(dbUri, dbOptions);
|
|
255
|
+
this.#log(`Connected to db '${mongoose.connection.name}'`, 1);
|
|
256
|
+
onDbConnect?.call(this, mongoose.connection);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
onDbError?.call(this, error);
|
|
259
|
+
this.#log("Failed to init", 4);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.#check(() => mongoose.connection.db, "No database found");
|
|
264
|
+
|
|
265
|
+
watch = watch.map((s) => s.toLowerCase());
|
|
266
|
+
ignore = ignore.map((s) => s.toLowerCase());
|
|
267
|
+
|
|
268
|
+
this.io.use(async (socket, next) => {
|
|
269
|
+
if (!!authentify) {
|
|
270
|
+
try {
|
|
271
|
+
const token =
|
|
272
|
+
socket.handshake.auth.token ||
|
|
273
|
+
socket.handshake.headers.authorization;
|
|
274
|
+
if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
|
|
275
|
+
|
|
276
|
+
const authorized = await authentify(token, socket);
|
|
277
|
+
if (authorized === true) return next(); // exactly returns true
|
|
278
|
+
|
|
279
|
+
return next(new Error("UNAUTHORIZED"));
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return next(new Error("AUTH_ERROR"));
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
return next();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
for (let middleware of middlewares) {
|
|
289
|
+
this.io.use(middleware);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.io.on("connection", (socket) => {
|
|
293
|
+
socket.emit("version", version);
|
|
294
|
+
|
|
295
|
+
socket.on(
|
|
296
|
+
"realtime",
|
|
297
|
+
async ({ streamId, limit, reverse, registerId }) => {
|
|
298
|
+
if (!streamId || !this.#streams[streamId]) return;
|
|
299
|
+
|
|
300
|
+
const stream = this.#streams[streamId];
|
|
301
|
+
const coll = stream.collection;
|
|
302
|
+
|
|
303
|
+
const default_limit = 50;
|
|
304
|
+
limit ??= default_limit;
|
|
305
|
+
try {
|
|
306
|
+
limit = parseInt(limit);
|
|
307
|
+
} catch (_) {
|
|
308
|
+
limit = default_limit;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
reverse = reverse == true;
|
|
312
|
+
registerId ??= "";
|
|
313
|
+
this.#debugLog(
|
|
314
|
+
`Socket ${socket.id} registred for realtime '${coll}:${registerId}'. Limit ${limit}. Reversed ${reverse}`
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
let total;
|
|
318
|
+
const ids = [];
|
|
319
|
+
|
|
320
|
+
do {
|
|
321
|
+
total = await this.connection.db
|
|
322
|
+
.collection(coll)
|
|
323
|
+
.estimatedDocumentCount();
|
|
324
|
+
|
|
325
|
+
const length = ids.length;
|
|
326
|
+
const range = [length, Math.min(total, length + limit)];
|
|
327
|
+
const now = new Date();
|
|
328
|
+
|
|
329
|
+
let cachedKey = `${coll}-${range}`;
|
|
330
|
+
|
|
331
|
+
let cachedResult = this.#data[cachedKey];
|
|
332
|
+
if (cachedResult && cachedResult.expiration < now) {
|
|
333
|
+
delete this.#data[cachedKey];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
cachedResult = this.#data[cachedKey];
|
|
337
|
+
|
|
338
|
+
const result = cachedResult
|
|
339
|
+
? Object.values(cachedResult.result)
|
|
340
|
+
: await this.connection.db
|
|
341
|
+
.collection(coll)
|
|
342
|
+
.find({
|
|
343
|
+
_id: {
|
|
344
|
+
$nin: ids,
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
.limit(limit)
|
|
348
|
+
.sort({ _id: reverse ? -1 : 1 })
|
|
349
|
+
.toArray();
|
|
350
|
+
|
|
351
|
+
ids.push(...result.map((d) => d._id));
|
|
352
|
+
|
|
353
|
+
const delayInMin = 1;
|
|
354
|
+
const expiration = new Date(now.getTime() + delayInMin * 60 * 1000);
|
|
355
|
+
const resultMap = result.reduce((acc, item) => {
|
|
356
|
+
item._id = item._id.toString();
|
|
357
|
+
acc[item._id] = item;
|
|
358
|
+
return acc;
|
|
359
|
+
}, {});
|
|
360
|
+
|
|
361
|
+
if (!cachedResult) {
|
|
362
|
+
this.#data[cachedKey] = {
|
|
363
|
+
expiration,
|
|
364
|
+
result: resultMap,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const filtered = (
|
|
369
|
+
await Promise.all(
|
|
370
|
+
result.map(async (doc) => {
|
|
371
|
+
try {
|
|
372
|
+
return {
|
|
373
|
+
doc,
|
|
374
|
+
ok: await stream.filter(doc),
|
|
375
|
+
};
|
|
376
|
+
} catch (e) {
|
|
377
|
+
return {
|
|
378
|
+
doc,
|
|
379
|
+
ok: false,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
.filter((item) => item.ok)
|
|
386
|
+
.map((item) => item.doc);
|
|
387
|
+
|
|
388
|
+
const data = {
|
|
389
|
+
results: filtered,
|
|
390
|
+
coll,
|
|
391
|
+
count: filtered.length,
|
|
392
|
+
total,
|
|
393
|
+
remaining: total - ids.length,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
socket.emit(`realtime:${streamId}:${registerId}`, data);
|
|
397
|
+
} while (ids.length < total);
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
socket.on("disconnect", (r) => {
|
|
402
|
+
if (offSocket) offSocket(socket, r);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (onSocket) onSocket(socket);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
this.#log(`Initialized`, 1);
|
|
263
409
|
}
|
|
264
410
|
|
|
265
411
|
/**
|
|
@@ -295,16 +441,14 @@ class MongoRealtime {
|
|
|
295
441
|
*
|
|
296
442
|
* Register a new list stream to listen
|
|
297
443
|
*/
|
|
298
|
-
static
|
|
444
|
+
static addStream(streamId, collection, filter) {
|
|
299
445
|
if (!streamId) throw new Error("Stream id is required");
|
|
300
446
|
if (!collection) throw new Error("Collection is required");
|
|
447
|
+
if (this.#streams[streamId] && this.collections.includes(streamId))
|
|
448
|
+
throw new Error(`streamId '${streamId}' cannot be a collection`);
|
|
301
449
|
|
|
302
450
|
filter ??= (_, __) => true;
|
|
303
|
-
|
|
304
|
-
throw new Error(
|
|
305
|
-
`Stream '${streamId}' already registered or is reserved.`
|
|
306
|
-
);
|
|
307
|
-
}
|
|
451
|
+
|
|
308
452
|
this.#streams[streamId] = {
|
|
309
453
|
collection,
|
|
310
454
|
filter,
|
|
@@ -316,7 +460,7 @@ class MongoRealtime {
|
|
|
316
460
|
*
|
|
317
461
|
* Delete a registered stream
|
|
318
462
|
*/
|
|
319
|
-
static
|
|
463
|
+
static removeStream(streamId) {
|
|
320
464
|
delete this.#streams[streamId];
|
|
321
465
|
}
|
|
322
466
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mongo-realtime",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mongo-realtime",
|
|
3
|
+
"version": "2.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": "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
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"mongoose": "^8.17.0",
|
|
25
|
+
"socket.io": "^4.8.1"
|
|
26
|
+
}
|
|
27
|
+
}
|