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.
Files changed (3) hide show
  1. package/README.md +86 -59
  2. package/index.js +175 -93
  3. 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 | Type | Description |
72
- | ------------------------ | ----------------- | -------------------------------------------------------------------------------- |
73
- | `options.dbUri` | `String`\* | Database URI |
74
- | `options.dbOptions` | `Object` | Mongoose connection options |
75
- | `options.onDbConnect` | `Function` | Callback on successful database connection |
76
- | `options.onDbError` | `Function` | Callback on database connection error |
77
- | `options.server` | `http.Server`\* | HTTP server to attach Socket.IO |
78
- | `options.authentify` | `Function` | Function to authenticate socket connections. Should return true if authenticated |
79
- | `options.middlewares` | `Array[Function]` | Array of Socket.IO middlewares |
80
- | `options.onSocket` | `Function` | Callback on socket connection |
81
- | `options.offSocket` | `Function` | Callback on socket disconnection |
82
- | `options.watch` | `Array[String]` | Collections to only watch. Listen to all when is empty |
83
- | `options.ignore` | `Array[String]` | Collections to only ignore. Overrides watch array |
84
- | `options.autoListStream` | `Array[String]` | Collections to automatically stream to clients. Default is all |
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.addListStream(streamId, collection, filter)`: Manually add a list stream for a specific collection and filter
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 | Description | Example |
107
- | ----------------------------- | ----------------- | ------------------------------------------ |
108
- | `db:change` | All changes | Any collection change |
109
- | `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
110
- | `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
111
- | `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
112
- | `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
113
- | `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
114
- | `db:stream:{streamId}` | By stream | `db:stream:myStreamId` |
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 list streams
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
- autoListStream: ["users"], // automatically stream users collection only
319
+ autoStream: ["users"], // automatically stream users collection only
283
320
  });
284
321
 
285
- MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // will throw an error as streamId 'users' already exists
322
+ MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // will throw an error as streamId 'users' already exists
286
323
 
287
- MongoRealtime.removeListStream("users"); // remove the previous stream
288
- MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // client can listen to db:stream:users
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.addListStream("usersWithEmail", "users", (doc) => !!doc.email); // client can listen to db:stream:usersWithEmail
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 `autoListStream` is not set, all collections are automatically streamed and WITHOUT any filter.\
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
- autoListStream: [], // stream no collection automatically (you can add your own filtered streams later)
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
- safeListStream: false, // disable safe mode (you can override existing streams)
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.addListStream("posts", "posts", (doc) => !!doc.title); // client can listen to db:stream:posts
320
- MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // will not throw an error
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.addListStream(
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.removeListStream(`userPost:${socket.uid}`);
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
- try {
356
- MongoRealtime.addListStream(
357
- `userPosts:${user._id}`,
358
- "posts",
359
- (doc) => doc.authorId === user._id
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
- #### Usecase with async filter
394
+ ### Usecase with async filter
370
395
 
371
396
  ```javascript
372
397
  // MongoRealtime.init({...});
373
398
 
374
- MongoRealtime.addListStream("authorizedUsers", "users", async (doc) => {
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.addListStream(
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
- /** @type {Record<String, [Object]>} */
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 #safeListStream = true;
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('#11AA60FF')(text));
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.autoListStream - Collections to stream automatically. If empty, will stream no collection. If null, will stream all collections.
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.safeListStream - If true(default), declaring an existing streamId will throw an error
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
- safeListStream = true,
119
- autoListStream,
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 (autoListStream == null) collectionsToStream = this.collections;
177
+ if (autoStream == null) collectionsToStream = this.collections;
162
178
  else
163
179
  collectionsToStream = this.collections.filter((c) =>
164
- autoListStream.includes(c)
180
+ autoStream.includes(c)
165
181
  );
166
- for (let col of collectionsToStream) this.addListStream(col, col);
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 streams on change */
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
- if (!this.#cache[coll]) {
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
- case "update":
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.#cache[coll] = this.#cache[coll].filter(
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
- const filtered = this.#cache[coll].filter(
310
- (_, i) => filterResults[i] && filterResults[i].value
311
- );
312
- this.io.emit(`db:stream[register][${registerId}]`, filtered);
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 addListStream(streamId, collection, filter) {
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
- if (this.#safeListStream && this.#streams[streamId]) {
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 removeListStream(streamId) {
463
+ static removeStream(streamId) {
382
464
  delete this.#streams[streamId];
383
465
  }
384
466
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mongo-realtime",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "main": "index.js",
5
5
  "scripts": {},
6
6
  "keywords": [