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.
Files changed (3) hide show
  1. package/README.md +86 -59
  2. package/index.js +280 -90
  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,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.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
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
- safeListStream = true,
119
- autoListStream,
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 (autoListStream == null) collectionsToStream = this.collections;
181
+ if (autoStream == null) collectionsToStream = this.collections;
162
182
  else
163
183
  collectionsToStream = this.collections.filter((c) =>
164
- autoListStream.includes(c)
184
+ autoStream.includes(c)
165
185
  );
166
- for (let col of collectionsToStream) this.addListStream(col, col);
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 streams on change */
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
- 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;
197
+ this.#debugLog(`Collection '${colName}' changed`);
183
198
 
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;
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.#cache[coll] = this.#cache[coll].filter(
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
- 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();
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
- const filterResults = await Promise.allSettled(
306
- this.#cache[coll].map((doc) => stream.filter(doc))
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
- const filtered = this.#cache[coll].filter(
310
- (_, i) => filterResults[i] && filterResults[i].value
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
- this.io.emit(`db:stream[register][${registerId}]`, filtered);
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 addListStream(streamId, collection, filter) {
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
- if (this.#safeListStream && this.#streams[streamId]) {
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 removeListStream(streamId) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mongo-realtime",
3
- "version": "1.2.0",
3
+ "version": "2.0.1",
4
4
  "main": "index.js",
5
5
  "scripts": {},
6
6
  "keywords": [