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.
Files changed (3) hide show
  1. package/README.md +194 -84
  2. package/index.js +154 -17
  3. 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('express');
31
- const http = require('http');
32
- const mongoose = require('mongoose');
33
- const MongoRealtime = require('mongo-realtime');
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('mongodb://localhost:27017/mydb').then((c) => {
39
- console.log("Connected to db",c.connection.name);
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('welcome', { message: 'Connection successful!' });
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('Server started on port 3000');
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 | 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
-
81
- #### Static Properties
82
-
83
- - `MongoRealtime.io`: Socket.IO server instance
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.sockets`: Array of connected sockets
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 | Description | Example |
94
- |-------|-------------|---------|
95
- | `db:change` | All changes | Any collection change |
96
- | `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
97
- | `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
98
- | `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
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 | `db:insert:users:507f1f77bcf86cd799439011` |
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
- socket.on('subscribe:users', () => {
158
- socket.join('users-room');
166
+ socket.on("subscribe:users", () => {
167
+ socket.join("users-room");
159
168
  });
160
169
  },
161
-
162
170
  });
163
171
 
164
- MongoRealtime.io.to('users-room').emit('custom-event', data);
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
- <script src="/socket.io/socket.io.js"></script>
174
- </head>
175
- <body>
176
- <script>
177
- const socket = io();
178
-
179
- socket.on('db:change', (change) => {
180
- console.log('Detected change:', change);
181
- });
182
-
183
- socket.on('db:insert:users', (change) => {
184
- console.log('New user:', change.fullDocument);
185
- });
186
-
187
- const userId = '507f1f77bcf86cd799439011';
188
- socket.on(`db:update:users:${userId}`, (change) => {
189
- console.log('Updated user:', change.fullDocument);
190
- });
191
-
192
- socket.on('db:delete', (change) => {
193
- console.log('Deleted document:', change.documentKey);
194
- });
195
- </script>
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('error', (error) => {
208
- console.error('Socket error:', error);
215
+ socket.on("error", (error) => {
216
+ console.error("Socket error:", error);
209
217
  });
210
218
  },
211
219
  offSocket: (socket, reason) => {
212
- if (reason === 'transport error') {
213
- console.log('Transport error detected');
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
- const verify = AuthService.verifyToken(token);
226
- if(verify){
227
- socket.user = verify.user; // attach user info to socket
228
- return true; // should return true to accept the connection
229
- }
230
- return false;
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
- console.log(`User is authenticated: ${socket.user.email}`);
240
- next();
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 = socket.handshake.auth.token;
48
- if (!token) return next(new Error("No token provided"));
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("Unauthorized"));
98
+ return next(new Error("UNAUTHORIZED"));
54
99
  } catch (error) {
55
- return next(new Error("Authentication 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
- changeStream.on("change", (change) => {
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.3",
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
+ }