mongo-realtime 1.2.0 → 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 +86 -59
- package/index.js +175 -93
- 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,10 @@ 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
|
|
107
117
|
*
|
|
108
118
|
*/
|
|
109
119
|
static async init({
|
|
@@ -115,23 +125,29 @@ class MongoRealtime {
|
|
|
115
125
|
authentify,
|
|
116
126
|
onSocket,
|
|
117
127
|
offSocket,
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
debug = false,
|
|
129
|
+
autoStream,
|
|
120
130
|
middlewares = [],
|
|
121
131
|
watch = [],
|
|
122
132
|
ignore = [],
|
|
123
133
|
}) {
|
|
124
|
-
console.clear();
|
|
125
134
|
this.#log(`MongoRealtime version (${this.version})`, 2);
|
|
135
|
+
|
|
126
136
|
if (this.io) this.io.close();
|
|
127
137
|
this.#check(() => dbUri);
|
|
128
138
|
this.#check(() => server);
|
|
139
|
+
this.#debug = debug;
|
|
129
140
|
|
|
130
141
|
this.io = new Server(server);
|
|
131
142
|
this.connection.once("open", async () => {
|
|
132
143
|
this.collections = (await this.connection.listCollections()).map(
|
|
133
144
|
(c) => c.name
|
|
134
145
|
);
|
|
146
|
+
this.#debugLog(
|
|
147
|
+
`${this.collections.length} collections found : ${this.collections.join(
|
|
148
|
+
", "
|
|
149
|
+
)}`
|
|
150
|
+
);
|
|
135
151
|
|
|
136
152
|
let pipeline = [];
|
|
137
153
|
if (watch.length !== 0 && ignore.length === 0) {
|
|
@@ -158,70 +174,56 @@ class MongoRealtime {
|
|
|
158
174
|
|
|
159
175
|
/** Setup main streams */
|
|
160
176
|
let collectionsToStream = [];
|
|
161
|
-
if (
|
|
177
|
+
if (autoStream == null) collectionsToStream = this.collections;
|
|
162
178
|
else
|
|
163
179
|
collectionsToStream = this.collections.filter((c) =>
|
|
164
|
-
|
|
180
|
+
autoStream.includes(c)
|
|
165
181
|
);
|
|
166
|
-
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
|
+
);
|
|
167
186
|
|
|
168
|
-
/** Emit
|
|
187
|
+
/** Emit listen events on change */
|
|
169
188
|
changeStream.on("change", async (change) => {
|
|
170
189
|
const coll = change.ns.coll;
|
|
190
|
+
const colName = coll.toLowerCase();
|
|
191
|
+
const doc = change.fullDocument;
|
|
171
192
|
|
|
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;
|
|
193
|
+
this.#debugLog(`Collection '${colName}' changed`);
|
|
183
194
|
|
|
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;
|
|
195
|
+
change.col = colName;
|
|
192
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) {
|
|
193
220
|
case "delete":
|
|
194
|
-
this.#
|
|
195
|
-
(doc) =>
|
|
196
|
-
doc._id.toString() !== change.documentKey._id.toString()
|
|
197
|
-
);
|
|
221
|
+
delete this.#data[k].result[id];
|
|
198
222
|
break;
|
|
223
|
+
default:
|
|
224
|
+
this.#data[k].result[id] = doc;
|
|
199
225
|
}
|
|
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;
|
|
226
|
+
}
|
|
225
227
|
|
|
226
228
|
const e_change = "db:change";
|
|
227
229
|
const e_change_type = `db:${type}`;
|
|
@@ -254,14 +256,12 @@ class MongoRealtime {
|
|
|
254
256
|
onDbConnect?.call(this, mongoose.connection);
|
|
255
257
|
} catch (error) {
|
|
256
258
|
onDbError?.call(this, error);
|
|
257
|
-
this.#log("Failed to init",4);
|
|
259
|
+
this.#log("Failed to init", 4);
|
|
258
260
|
return;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
this.#check(() => mongoose.connection.db, "No database found");
|
|
262
264
|
|
|
263
|
-
this.#safeListStream = !!safeListStream;
|
|
264
|
-
|
|
265
265
|
watch = watch.map((s) => s.toLowerCase());
|
|
266
266
|
ignore = ignore.map((s) => s.toLowerCase());
|
|
267
267
|
|
|
@@ -291,37 +291,121 @@ class MongoRealtime {
|
|
|
291
291
|
|
|
292
292
|
this.io.on("connection", (socket) => {
|
|
293
293
|
socket.emit("version", version);
|
|
294
|
-
socket.on("db:stream[register]", async (streamId, registerId) => {
|
|
295
|
-
const stream = this.#streams[streamId];
|
|
296
|
-
if (!stream) return;
|
|
297
|
-
const coll = stream.collection;
|
|
298
|
-
if (!this.#cache[coll]) {
|
|
299
|
-
this.#cache[coll] = await this.connection.db
|
|
300
|
-
.collection(coll)
|
|
301
|
-
.find({})
|
|
302
|
-
.sort({ _id: -1 })
|
|
303
|
-
.toArray();
|
|
304
|
-
}
|
|
305
|
-
const filterResults = await Promise.allSettled(
|
|
306
|
-
this.#cache[coll].map((doc) => stream.filter(doc))
|
|
307
|
-
);
|
|
308
294
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
|
|
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
|
+
);
|
|
314
400
|
|
|
315
401
|
socket.on("disconnect", (r) => {
|
|
316
402
|
if (offSocket) offSocket(socket, r);
|
|
317
403
|
});
|
|
318
404
|
|
|
319
405
|
if (onSocket) onSocket(socket);
|
|
320
|
-
|
|
321
406
|
});
|
|
322
|
-
|
|
323
407
|
|
|
324
|
-
this.#log(`Initialized`,1);
|
|
408
|
+
this.#log(`Initialized`, 1);
|
|
325
409
|
}
|
|
326
410
|
|
|
327
411
|
/**
|
|
@@ -357,16 +441,14 @@ class MongoRealtime {
|
|
|
357
441
|
*
|
|
358
442
|
* Register a new list stream to listen
|
|
359
443
|
*/
|
|
360
|
-
static
|
|
444
|
+
static addStream(streamId, collection, filter) {
|
|
361
445
|
if (!streamId) throw new Error("Stream id is required");
|
|
362
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`);
|
|
363
449
|
|
|
364
450
|
filter ??= (_, __) => true;
|
|
365
|
-
|
|
366
|
-
throw new Error(
|
|
367
|
-
`Stream '${streamId}' already registered or is reserved.`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
451
|
+
|
|
370
452
|
this.#streams[streamId] = {
|
|
371
453
|
collection,
|
|
372
454
|
filter,
|
|
@@ -378,7 +460,7 @@ class MongoRealtime {
|
|
|
378
460
|
*
|
|
379
461
|
* Delete a registered stream
|
|
380
462
|
*/
|
|
381
|
-
static
|
|
463
|
+
static removeStream(streamId) {
|
|
382
464
|
delete this.#streams[streamId];
|
|
383
465
|
}
|
|
384
466
|
|