mongo-realtime 1.2.0 → 2.0.1
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 +86 -59
- package/index.js +280 -90
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,44 @@ server.listen(3000, () => {
|
|
|
58
58
|
});
|
|
59
59
|
```
|
|
60
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
|
+
|
|
61
99
|
## 📋 API
|
|
62
100
|
|
|
63
101
|
### `MongoRealtime.init(options)`
|
|
@@ -68,24 +106,25 @@ Initializes the socket system and MongoDB Change Streams.
|
|
|
68
106
|
|
|
69
107
|
\* means required
|
|
70
108
|
|
|
71
|
-
| Parameter
|
|
72
|
-
|
|
|
73
|
-
| `options.dbUri`
|
|
74
|
-
| `options.dbOptions`
|
|
75
|
-
| `options.onDbConnect`
|
|
76
|
-
| `options.onDbError`
|
|
77
|
-
| `options.server`
|
|
78
|
-
| `options.authentify`
|
|
79
|
-
| `options.middlewares`
|
|
80
|
-
| `options.onSocket`
|
|
81
|
-
| `options.offSocket`
|
|
82
|
-
| `options.watch`
|
|
83
|
-
| `options.ignore`
|
|
84
|
-
| `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 |
|
|
85
124
|
|
|
86
125
|
#### Static Properties and Methods
|
|
87
126
|
|
|
88
|
-
- `MongoRealtime.
|
|
127
|
+
- `MongoRealtime.addStream(streamId, collection, filter)`: Manually add a list stream for a specific collection and filter
|
|
89
128
|
- `MongoRealtime.connection`: MongoDB connection
|
|
90
129
|
- `MongoRealtime.collections`: Array of database collections
|
|
91
130
|
- `MongoRealtime.io`: Socket.IO server instance
|
|
@@ -103,15 +142,15 @@ The package automatically emits six types of events for each database change:
|
|
|
103
142
|
|
|
104
143
|
### Event Types
|
|
105
144
|
|
|
106
|
-
| Event
|
|
107
|
-
|
|
|
108
|
-
| `db:change`
|
|
109
|
-
| `db:{type}`
|
|
110
|
-
| `db:change:{collection}`
|
|
111
|
-
| `db:{type}:{collection}`
|
|
112
|
-
| `db:change:{collection}:{id}`
|
|
113
|
-
| `db:{type}:{collection}:{id}`
|
|
114
|
-
| `
|
|
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` |
|
|
115
154
|
|
|
116
155
|
### Event listeners
|
|
117
156
|
|
|
@@ -267,57 +306,47 @@ MongoRealtime.init({
|
|
|
267
306
|
});
|
|
268
307
|
```
|
|
269
308
|
|
|
270
|
-
### Setup
|
|
309
|
+
### Setup streams
|
|
271
310
|
|
|
272
311
|
The server will automatically emit a list of filtered documents from the specified collections after each change.\
|
|
273
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.
|
|
274
313
|
Clients receive the list on the event `db:stream:{streamId}`.\
|
|
275
314
|
|
|
276
|
-
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.
|
|
277
|
-
|
|
278
315
|
```javascript
|
|
279
316
|
MongoRealtime.init({
|
|
280
317
|
dbUri: "mongodb://localhost:27017/mydb",
|
|
281
318
|
server: server,
|
|
282
|
-
|
|
319
|
+
autoStream: ["users"], // automatically stream users collection only
|
|
283
320
|
});
|
|
284
321
|
|
|
285
|
-
MongoRealtime.
|
|
322
|
+
MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // will throw an error as streamId 'users' already exists
|
|
286
323
|
|
|
287
|
-
MongoRealtime.
|
|
288
|
-
MongoRealtime.
|
|
324
|
+
MongoRealtime.removeStream("users"); // remove the previous stream
|
|
325
|
+
MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // client can listen to db:stream:users
|
|
289
326
|
|
|
290
|
-
MongoRealtime.
|
|
327
|
+
MongoRealtime.addStream("usersWithEmail", "users", (doc) => !!doc.email); // client can listen to db:stream:usersWithEmail
|
|
291
328
|
```
|
|
292
329
|
|
|
293
330
|
#### ⚠️ NOTICE
|
|
294
331
|
|
|
295
|
-
When `
|
|
332
|
+
When `autoStream` is not set, all collections are automatically streamed and WITHOUT any filter.\
|
|
296
333
|
That means that if you have a `posts` collection, all documents from this collection will be sent to the clients on each change.\
|
|
297
334
|
|
|
298
|
-
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.\
|
|
299
|
-
|
|
300
|
-
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.\
|
|
301
|
-
Therefore, if you want to add a filtered list stream for all collections, you must set `autoListStream` to an empty array.
|
|
302
|
-
|
|
303
|
-
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.
|
|
304
|
-
|
|
305
335
|
```javascript
|
|
306
336
|
MongoRealtime.init({
|
|
307
337
|
dbUri: "mongodb://localhost:27017/mydb",
|
|
308
338
|
server: server,
|
|
309
|
-
|
|
339
|
+
autoStream: [], // stream no collection automatically (you can add your own filtered streams later)
|
|
310
340
|
});
|
|
311
341
|
// or
|
|
312
342
|
MongoRealtime.init({
|
|
313
343
|
dbUri: "mongodb://localhost:27017/mydb",
|
|
314
344
|
server: server,
|
|
315
|
-
|
|
316
|
-
// Still stream all collections automatically but you can override them
|
|
345
|
+
// Stream all collections automatically but you can override them
|
|
317
346
|
}):
|
|
318
347
|
|
|
319
|
-
MongoRealtime.
|
|
320
|
-
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
|
|
321
350
|
```
|
|
322
351
|
|
|
323
352
|
#### Usecase for id based streams
|
|
@@ -336,7 +365,7 @@ MongoRealtime.init({
|
|
|
336
365
|
},
|
|
337
366
|
onSocket: (socket) => {
|
|
338
367
|
// setup a personal stream for each connected user
|
|
339
|
-
MongoRealtime.
|
|
368
|
+
MongoRealtime.addStream(
|
|
340
369
|
`userPost:${socket.uid}`,
|
|
341
370
|
"posts",
|
|
342
371
|
(doc) => doc._id == socket.uid
|
|
@@ -344,7 +373,7 @@ MongoRealtime.init({
|
|
|
344
373
|
},
|
|
345
374
|
offSocket: (socket) => {
|
|
346
375
|
// clean up when user disconnects
|
|
347
|
-
MongoRealtime.
|
|
376
|
+
MongoRealtime.removeStream(`userPost:${socket.uid}`);
|
|
348
377
|
},
|
|
349
378
|
});
|
|
350
379
|
|
|
@@ -352,31 +381,27 @@ MongoRealtime.init({
|
|
|
352
381
|
// or activate stream from a controller or middleware
|
|
353
382
|
app.get("/my-posts", (req, res) => {
|
|
354
383
|
const { user } = req;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
);
|
|
361
|
-
} catch (e) {
|
|
362
|
-
// stream already exists
|
|
363
|
-
}
|
|
384
|
+
MongoRealtime.addStream(
|
|
385
|
+
`userPosts:${user._id}`,
|
|
386
|
+
"posts",
|
|
387
|
+
(doc) => doc.authorId === user._id
|
|
388
|
+
);
|
|
364
389
|
|
|
365
390
|
res.send("Stream activated");
|
|
366
391
|
});
|
|
367
392
|
```
|
|
368
393
|
|
|
369
|
-
|
|
394
|
+
### Usecase with async filter
|
|
370
395
|
|
|
371
396
|
```javascript
|
|
372
397
|
// MongoRealtime.init({...});
|
|
373
398
|
|
|
374
|
-
MongoRealtime.
|
|
399
|
+
MongoRealtime.addStream("authorizedUsers", "users", async (doc) => {
|
|
375
400
|
const isAdmin = await UserService.isAdmin(doc._id);
|
|
376
401
|
return isAdmin && doc.email.endsWith("@mydomain.com");
|
|
377
402
|
});
|
|
378
403
|
|
|
379
|
-
MongoRealtime.
|
|
404
|
+
MongoRealtime.addStream(
|
|
380
405
|
"bestPosts",
|
|
381
406
|
"posts",
|
|
382
407
|
async (doc) => doc.likes > (await PostService.getLikesThreshold())
|
|
@@ -400,6 +425,8 @@ mongod --replSet rs0
|
|
|
400
425
|
rs.initiate()
|
|
401
426
|
```
|
|
402
427
|
|
|
428
|
+
For any other issues, open an issue on the GitHub repository.
|
|
429
|
+
|
|
403
430
|
## 📄 License
|
|
404
431
|
|
|
405
432
|
MIT
|
package/index.js
CHANGED
|
@@ -35,17 +35,19 @@ class MongoRealtime {
|
|
|
35
35
|
mongoose.connection;
|
|
36
36
|
/** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
|
|
37
37
|
{};
|
|
38
|
-
|
|
39
|
-
static #cache = {};
|
|
38
|
+
|
|
40
39
|
static sockets = () => [...this.io.sockets.sockets.values()];
|
|
41
40
|
|
|
42
41
|
/**@type {Record<String, {collection:String,filter: (doc:Object)=>Promise<boolean>}>} */
|
|
43
42
|
static #streams = {};
|
|
44
43
|
|
|
44
|
+
/**@type {Record<String, {expiration:Date,result:Record<String,{}> }>} */
|
|
45
|
+
static #data = {};
|
|
46
|
+
|
|
45
47
|
/** @type {[String]} - All DB collections */
|
|
46
48
|
static collections = [];
|
|
47
49
|
|
|
48
|
-
static #
|
|
50
|
+
static #debug = false;
|
|
49
51
|
|
|
50
52
|
static version = version;
|
|
51
53
|
|
|
@@ -69,7 +71,7 @@ class MongoRealtime {
|
|
|
69
71
|
const text = `[REALTIME] ${message}`;
|
|
70
72
|
switch (type) {
|
|
71
73
|
case 1:
|
|
72
|
-
console.log(chalk.bold.hex(
|
|
74
|
+
console.log(chalk.bold.hex("#11AA60FF")(text));
|
|
73
75
|
break;
|
|
74
76
|
case 2:
|
|
75
77
|
console.log(chalk.bold.bgHex("#257993")(text));
|
|
@@ -81,12 +83,20 @@ class MongoRealtime {
|
|
|
81
83
|
console.log(chalk.bold.red(text));
|
|
82
84
|
break;
|
|
83
85
|
|
|
86
|
+
case 5:
|
|
87
|
+
console.log(chalk.italic(text));
|
|
88
|
+
break;
|
|
89
|
+
|
|
84
90
|
default:
|
|
85
91
|
console.log(text);
|
|
86
92
|
break;
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
static #debugLog(message) {
|
|
97
|
+
if (this.#debug) this.#log("[DEBUG] " + message, 5);
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
/**
|
|
91
101
|
* Initializes the socket system.
|
|
92
102
|
*
|
|
@@ -100,10 +110,12 @@ class MongoRealtime {
|
|
|
100
110
|
* @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
|
|
101
111
|
* @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
|
|
102
112
|
* @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
|
|
103
|
-
* @param {[String]} options.
|
|
113
|
+
* @param {[String]} options.autoStream - Collections to stream automatically. If empty, will stream no collection. If null, will stream all collections.
|
|
104
114
|
* @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
|
|
105
115
|
* @param {[String]} options.ignore - Collections to ignore. Can override `watch`
|
|
106
|
-
* @param {bool} options.
|
|
116
|
+
* @param {bool} options.debug - Enable debug mode
|
|
117
|
+
* @param {number} options.cacheDelay - Cache delay in minutes. Put 0 if no cache
|
|
118
|
+
* @param {number} options.allowDbOperations - If true, you can use find and update operations.
|
|
107
119
|
*
|
|
108
120
|
*/
|
|
109
121
|
static async init({
|
|
@@ -115,23 +127,31 @@ class MongoRealtime {
|
|
|
115
127
|
authentify,
|
|
116
128
|
onSocket,
|
|
117
129
|
offSocket,
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
debug = false,
|
|
131
|
+
autoStream,
|
|
120
132
|
middlewares = [],
|
|
121
133
|
watch = [],
|
|
122
134
|
ignore = [],
|
|
135
|
+
cacheDelay = 5,
|
|
136
|
+
allowDbOperations = true,
|
|
123
137
|
}) {
|
|
124
|
-
console.clear();
|
|
125
138
|
this.#log(`MongoRealtime version (${this.version})`, 2);
|
|
139
|
+
|
|
126
140
|
if (this.io) this.io.close();
|
|
127
141
|
this.#check(() => dbUri);
|
|
128
142
|
this.#check(() => server);
|
|
143
|
+
this.#debug = debug;
|
|
129
144
|
|
|
130
145
|
this.io = new Server(server);
|
|
131
146
|
this.connection.once("open", async () => {
|
|
132
147
|
this.collections = (await this.connection.listCollections()).map(
|
|
133
148
|
(c) => c.name
|
|
134
149
|
);
|
|
150
|
+
this.#debugLog(
|
|
151
|
+
`${this.collections.length} collections found : ${this.collections.join(
|
|
152
|
+
", "
|
|
153
|
+
)}`
|
|
154
|
+
);
|
|
135
155
|
|
|
136
156
|
let pipeline = [];
|
|
137
157
|
if (watch.length !== 0 && ignore.length === 0) {
|
|
@@ -158,70 +178,56 @@ class MongoRealtime {
|
|
|
158
178
|
|
|
159
179
|
/** Setup main streams */
|
|
160
180
|
let collectionsToStream = [];
|
|
161
|
-
if (
|
|
181
|
+
if (autoStream == null) collectionsToStream = this.collections;
|
|
162
182
|
else
|
|
163
183
|
collectionsToStream = this.collections.filter((c) =>
|
|
164
|
-
|
|
184
|
+
autoStream.includes(c)
|
|
165
185
|
);
|
|
166
|
-
for (let col of collectionsToStream) this.
|
|
186
|
+
for (let col of collectionsToStream) this.addStream(col, col);
|
|
187
|
+
this.#debugLog(
|
|
188
|
+
`Auto stream on collections : ${collectionsToStream.join(", ")}`
|
|
189
|
+
);
|
|
167
190
|
|
|
168
|
-
/** Emit
|
|
191
|
+
/** Emit listen events on change */
|
|
169
192
|
changeStream.on("change", async (change) => {
|
|
170
193
|
const coll = change.ns.coll;
|
|
194
|
+
const colName = coll.toLowerCase();
|
|
195
|
+
const doc = change.fullDocument;
|
|
171
196
|
|
|
172
|
-
|
|
173
|
-
this.#cache[coll] = await this.connection.db
|
|
174
|
-
.collection(coll)
|
|
175
|
-
.find({})
|
|
176
|
-
.sort({ _id: -1 })
|
|
177
|
-
.toArray();
|
|
178
|
-
} else
|
|
179
|
-
switch (change.operationType) {
|
|
180
|
-
case "insert":
|
|
181
|
-
this.#cache[coll].unshift(change.fullDocument); // add to top;
|
|
182
|
-
break;
|
|
197
|
+
this.#debugLog(`Collection '${colName}' changed`);
|
|
183
198
|
|
|
184
|
-
|
|
185
|
-
case "replace":
|
|
186
|
-
this.#cache[coll] = this.#cache[coll].map((doc) =>
|
|
187
|
-
doc._id.toString() === change.documentKey._id.toString()
|
|
188
|
-
? change.fullDocument
|
|
189
|
-
: doc
|
|
190
|
-
);
|
|
191
|
-
break;
|
|
199
|
+
change.col = colName;
|
|
192
200
|
|
|
201
|
+
const type = change.operationType;
|
|
202
|
+
const id = change.documentKey?._id.toString();
|
|
203
|
+
|
|
204
|
+
for (const k in this.#streams) {
|
|
205
|
+
const stream = this.#streams[k];
|
|
206
|
+
if (stream.collection != coll) continue;
|
|
207
|
+
|
|
208
|
+
Promise.resolve(stream.filter(doc)).then((ok) => {
|
|
209
|
+
if (ok) {
|
|
210
|
+
this.io.emit(`realtime:${k}`, {
|
|
211
|
+
results: [doc],
|
|
212
|
+
coll: coll,
|
|
213
|
+
count: 1,
|
|
214
|
+
total: 1,
|
|
215
|
+
remaining: 0,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
for (let k in this.#data) {
|
|
221
|
+
if (!k.startsWith(`${coll}-`) || !this.#data[k].result[id]) continue;
|
|
222
|
+
doc._id = doc._id.toString();
|
|
223
|
+
switch (change.operationType) {
|
|
193
224
|
case "delete":
|
|
194
|
-
this.#
|
|
195
|
-
(doc) =>
|
|
196
|
-
doc._id.toString() !== change.documentKey._id.toString()
|
|
197
|
-
);
|
|
225
|
+
delete this.#data[k].result[id];
|
|
198
226
|
break;
|
|
227
|
+
default:
|
|
228
|
+
this.#data[k].result[id] = doc;
|
|
199
229
|
}
|
|
200
|
-
|
|
201
|
-
Object.entries(this.#streams).forEach(async (e) => {
|
|
202
|
-
const key = e[0];
|
|
203
|
-
const value = e[1];
|
|
204
|
-
if (value.collection != coll) return;
|
|
205
|
-
const filterResults = await Promise.allSettled(
|
|
206
|
-
this.#cache[coll].map((doc) => value.filter(doc))
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
const filtered = this.#cache[coll].filter(
|
|
210
|
-
(_, i) => filterResults[i] && filterResults[i].value
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
this.io.emit(`db:stream:${key}`, filtered);
|
|
214
|
-
this.notifyListeners(`db:stream:${key}`, filtered);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
/** Emit listen events on change */
|
|
219
|
-
changeStream.on("change", async (change) => {
|
|
220
|
-
const colName = change.ns.coll.toLowerCase();
|
|
221
|
-
change.col = colName;
|
|
222
|
-
|
|
223
|
-
const type = change.operationType;
|
|
224
|
-
const id = change.documentKey?._id;
|
|
230
|
+
}
|
|
225
231
|
|
|
226
232
|
const e_change = "db:change";
|
|
227
233
|
const e_change_type = `db:${type}`;
|
|
@@ -254,14 +260,12 @@ class MongoRealtime {
|
|
|
254
260
|
onDbConnect?.call(this, mongoose.connection);
|
|
255
261
|
} catch (error) {
|
|
256
262
|
onDbError?.call(this, error);
|
|
257
|
-
this.#log("Failed to init",4);
|
|
263
|
+
this.#log("Failed to init", 4);
|
|
258
264
|
return;
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
this.#check(() => mongoose.connection.db, "No database found");
|
|
262
268
|
|
|
263
|
-
this.#safeListStream = !!safeListStream;
|
|
264
|
-
|
|
265
269
|
watch = watch.map((s) => s.toLowerCase());
|
|
266
270
|
ignore = ignore.map((s) => s.toLowerCase());
|
|
267
271
|
|
|
@@ -291,37 +295,210 @@ class MongoRealtime {
|
|
|
291
295
|
|
|
292
296
|
this.io.on("connection", (socket) => {
|
|
293
297
|
socket.emit("version", version);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
298
|
+
|
|
299
|
+
socket.on(
|
|
300
|
+
"realtime",
|
|
301
|
+
async ({ streamId, limit, reverse, registerId }) => {
|
|
302
|
+
if (!streamId || !this.#streams[streamId]) return;
|
|
303
|
+
|
|
304
|
+
const stream = this.#streams[streamId];
|
|
305
|
+
const coll = stream.collection;
|
|
306
|
+
|
|
307
|
+
const default_limit = 50;
|
|
308
|
+
limit ??= default_limit;
|
|
309
|
+
try {
|
|
310
|
+
limit = parseInt(limit);
|
|
311
|
+
} catch (_) {
|
|
312
|
+
limit = default_limit;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
reverse = reverse == true;
|
|
316
|
+
registerId ??= "";
|
|
317
|
+
this.#debugLog(
|
|
318
|
+
`Socket ${socket.id} registred for realtime '${coll}:${registerId}'. Limit ${limit}. Reversed ${reverse}`
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
let total;
|
|
322
|
+
const ids = [];
|
|
323
|
+
|
|
324
|
+
do {
|
|
325
|
+
total = await this.connection.db
|
|
326
|
+
.collection(coll)
|
|
327
|
+
.estimatedDocumentCount();
|
|
328
|
+
|
|
329
|
+
const length = ids.length;
|
|
330
|
+
const range = [length, Math.min(total, length + limit)];
|
|
331
|
+
const now = new Date();
|
|
332
|
+
|
|
333
|
+
let cachedKey = `${coll}-${range}`;
|
|
334
|
+
|
|
335
|
+
let cachedResult = this.#data[cachedKey];
|
|
336
|
+
if (cachedResult && cachedResult.expiration < now) {
|
|
337
|
+
delete this.#data[cachedKey];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
cachedResult = this.#data[cachedKey];
|
|
341
|
+
|
|
342
|
+
const result = cachedResult
|
|
343
|
+
? Object.values(cachedResult.result)
|
|
344
|
+
: await this.connection.db
|
|
345
|
+
.collection(coll)
|
|
346
|
+
.find({
|
|
347
|
+
_id: {
|
|
348
|
+
$nin: ids,
|
|
349
|
+
},
|
|
350
|
+
})
|
|
351
|
+
.limit(limit)
|
|
352
|
+
.sort({ _id: reverse ? -1 : 1 })
|
|
353
|
+
.toArray();
|
|
354
|
+
|
|
355
|
+
ids.push(...result.map((d) => d._id));
|
|
356
|
+
|
|
357
|
+
const delayInMin = cacheDelay;
|
|
358
|
+
const expiration = new Date(now.getTime() + delayInMin * 60 * 1000);
|
|
359
|
+
const resultMap = result.reduce((acc, item) => {
|
|
360
|
+
item._id = item._id.toString();
|
|
361
|
+
acc[item._id] = item;
|
|
362
|
+
return acc;
|
|
363
|
+
}, {});
|
|
364
|
+
|
|
365
|
+
if (!cachedResult) {
|
|
366
|
+
this.#data[cachedKey] = {
|
|
367
|
+
expiration,
|
|
368
|
+
result: resultMap,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const filtered = (
|
|
373
|
+
await Promise.all(
|
|
374
|
+
result.map(async (doc) => {
|
|
375
|
+
try {
|
|
376
|
+
return {
|
|
377
|
+
doc,
|
|
378
|
+
ok: await stream.filter(doc),
|
|
379
|
+
};
|
|
380
|
+
} catch (e) {
|
|
381
|
+
return {
|
|
382
|
+
doc,
|
|
383
|
+
ok: false,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
.filter((item) => item.ok)
|
|
390
|
+
.map((item) => item.doc);
|
|
391
|
+
|
|
392
|
+
const data = {
|
|
393
|
+
results: filtered,
|
|
394
|
+
coll,
|
|
395
|
+
count: filtered.length,
|
|
396
|
+
total,
|
|
397
|
+
remaining: total - ids.length,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
socket.emit(`realtime:${streamId}:${registerId}`, data);
|
|
401
|
+
} while (ids.length < total);
|
|
304
402
|
}
|
|
305
|
-
|
|
306
|
-
|
|
403
|
+
);
|
|
404
|
+
if (allowDbOperations) {
|
|
405
|
+
socket.on("realtime:count", async ({ coll, query }, ack) => {
|
|
406
|
+
if (!coll) return ack(0);
|
|
407
|
+
query ??= {};
|
|
408
|
+
const c = this.connection.db.collection(coll);
|
|
409
|
+
const hasQuery = notEmpty(query);
|
|
410
|
+
const count = hasQuery
|
|
411
|
+
? await c.countDocuments(query)
|
|
412
|
+
: await c.estimatedDocumentCount();
|
|
413
|
+
ack(count);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
socket.on(
|
|
417
|
+
"realtime:find",
|
|
418
|
+
async (
|
|
419
|
+
{ coll, query, limit, sortBy, project, one, skip, id },
|
|
420
|
+
ack
|
|
421
|
+
) => {
|
|
422
|
+
if (!coll) return ack(null);
|
|
423
|
+
const c = this.connection.db.collection(coll);
|
|
424
|
+
|
|
425
|
+
if (id) {
|
|
426
|
+
ack(await c.findOne({ _id: toObjectId(id) }));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
query ??= {};
|
|
431
|
+
one = one == true;
|
|
432
|
+
|
|
433
|
+
if (query["_id"]) {
|
|
434
|
+
query["_id"] = toObjectId(query["_id"]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const options = {
|
|
438
|
+
sort: sortBy,
|
|
439
|
+
projection: project,
|
|
440
|
+
skip: skip,
|
|
441
|
+
limit: limit,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
if (one) {
|
|
445
|
+
ack(await c.findOne(query, options));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let cursor = c.find(query, options);
|
|
450
|
+
ack(await cursor.toArray());
|
|
451
|
+
}
|
|
307
452
|
);
|
|
308
453
|
|
|
309
|
-
|
|
310
|
-
|
|
454
|
+
socket.on(
|
|
455
|
+
"realtime:update",
|
|
456
|
+
async (
|
|
457
|
+
{ coll, query, limit, sortBy, project, one, skip, id, update },
|
|
458
|
+
ack
|
|
459
|
+
) => {
|
|
460
|
+
|
|
461
|
+
if (!coll || !notEmpty(update)) return ack(0);
|
|
462
|
+
const c = this.connection.db.collection(coll);
|
|
463
|
+
|
|
464
|
+
if (id) {
|
|
465
|
+
ack((await c.updateOne({ _id: toObjectId(id) }, update)).modifiedCount);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
query ??= {};
|
|
470
|
+
one = one == true;
|
|
471
|
+
|
|
472
|
+
if (query["_id"]) {
|
|
473
|
+
query["_id"] = toObjectId(query["_id"]);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const options = {
|
|
477
|
+
sort: sortBy,
|
|
478
|
+
projection: project,
|
|
479
|
+
skip: skip,
|
|
480
|
+
limit: limit,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
if (one) {
|
|
484
|
+
ack((await c.updateOne(query,update, options)).modifiedCount);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let cursor = await c.updateMany(query,update, options);
|
|
489
|
+
ack(cursor.modifiedCount);
|
|
490
|
+
}
|
|
311
491
|
);
|
|
312
|
-
|
|
313
|
-
});
|
|
492
|
+
}
|
|
314
493
|
|
|
315
494
|
socket.on("disconnect", (r) => {
|
|
316
495
|
if (offSocket) offSocket(socket, r);
|
|
317
496
|
});
|
|
318
497
|
|
|
319
498
|
if (onSocket) onSocket(socket);
|
|
320
|
-
|
|
321
499
|
});
|
|
322
|
-
|
|
323
500
|
|
|
324
|
-
this.#log(`Initialized`,1);
|
|
501
|
+
this.#log(`Initialized`, 1);
|
|
325
502
|
}
|
|
326
503
|
|
|
327
504
|
/**
|
|
@@ -357,16 +534,14 @@ class MongoRealtime {
|
|
|
357
534
|
*
|
|
358
535
|
* Register a new list stream to listen
|
|
359
536
|
*/
|
|
360
|
-
static
|
|
537
|
+
static addStream(streamId, collection, filter) {
|
|
361
538
|
if (!streamId) throw new Error("Stream id is required");
|
|
362
539
|
if (!collection) throw new Error("Collection is required");
|
|
540
|
+
if (this.#streams[streamId] && this.collections.includes(streamId))
|
|
541
|
+
throw new Error(`streamId '${streamId}' cannot be a collection`);
|
|
363
542
|
|
|
364
543
|
filter ??= (_, __) => true;
|
|
365
|
-
|
|
366
|
-
throw new Error(
|
|
367
|
-
`Stream '${streamId}' already registered or is reserved.`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
544
|
+
|
|
370
545
|
this.#streams[streamId] = {
|
|
371
546
|
collection,
|
|
372
547
|
filter,
|
|
@@ -378,7 +553,7 @@ class MongoRealtime {
|
|
|
378
553
|
*
|
|
379
554
|
* Delete a registered stream
|
|
380
555
|
*/
|
|
381
|
-
static
|
|
556
|
+
static removeStream(streamId) {
|
|
382
557
|
delete this.#streams[streamId];
|
|
383
558
|
}
|
|
384
559
|
|
|
@@ -401,4 +576,19 @@ class MongoRealtime {
|
|
|
401
576
|
}
|
|
402
577
|
}
|
|
403
578
|
|
|
579
|
+
// utils
|
|
580
|
+
function notEmpty(obj) {
|
|
581
|
+
obj ??= {};
|
|
582
|
+
return Object.keys(obj).length > 0;
|
|
583
|
+
}
|
|
584
|
+
/** @param {String} id */
|
|
585
|
+
function toObjectId(id) {
|
|
586
|
+
if (typeof id != "string") return id;
|
|
587
|
+
try {
|
|
588
|
+
return mongoose.Types.ObjectId.createFromHexString(id);
|
|
589
|
+
} catch (_) {
|
|
590
|
+
return new mongoose.Types.ObjectId(id); //use deprecated if fail
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
404
594
|
module.exports = MongoRealtime;
|