mongo-realtime 1.1.2 → 1.2.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 +40 -25
  2. package/index.js +143 -74
  3. package/package.json +27 -26
package/README.md CHANGED
@@ -29,18 +29,19 @@ npm install mongo-realtime
29
29
  ```javascript
30
30
  const express = require("express");
31
31
  const http = require("http");
32
- const mongoose = require("mongoose");
33
32
  const MongoRealtime = require("mongo-realtime");
34
33
 
35
34
  const app = express();
36
35
  const server = http.createServer(app);
37
36
 
38
- mongoose.connect("mongodb://localhost:27017/mydb").then((c) => {
39
- console.log("Connected to db", c.connection.name);
40
- });
41
-
42
37
  MongoRealtime.init({
43
- connection: mongoose.connection,
38
+ dbUri: "mongodb://localhost:27017/mydb",
39
+ onDbConnect: (conn) => {
40
+ console.log("Connected to db", conn.name);
41
+ },
42
+ onDbError: (err) => {
43
+ console.log(err.message);
44
+ },
44
45
  server: server,
45
46
  ignore: ["posts"], // ignore 'posts' collection
46
47
  onSocket: (socket) => {
@@ -67,17 +68,20 @@ Initializes the socket system and MongoDB Change Streams.
67
68
 
68
69
  \* means required
69
70
 
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 |
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 |
81
85
 
82
86
  #### Static Properties and Methods
83
87
 
@@ -160,7 +164,7 @@ Each event contains the full MongoDB change object:
160
164
 
161
165
  ```javascript
162
166
  MongoRealtime.init({
163
- connection: connection,
167
+ dbUri: "mongodb://localhost:27017/mydb",
164
168
  server: server,
165
169
  onSocket: (socket) => {
166
170
  socket.on("subscribe:users", () => {
@@ -209,7 +213,7 @@ MongoRealtime.io.to("users-room").emit("custom-event", data);
209
213
 
210
214
  ```javascript
211
215
  MongoRealtime.init({
212
- connection: mongoose.connection,
216
+ dbUri: "mongodb://localhost:27017/mydb",
213
217
  server: server,
214
218
  onSocket: (socket) => {
215
219
  socket.on("error", (error) => {
@@ -228,6 +232,15 @@ MongoRealtime.init({
228
232
 
229
233
  ### Socket Authentication
230
234
 
235
+ You can provide an `authentify` function in the init options to authenticate socket connections.\
236
+ The function receives the token (from `socket.handshake.auth.token` or `socket.handshake.headers.authorization`) and the socket object.\
237
+ When setted, it rejects connections based on this logic:
238
+
239
+ - Token not provided -> error `NO_TOKEN_PROVIDED`
240
+ - Token invalid or returns `false` -> error `UNAUTHORIZED`
241
+ - Any other error -> error `AUTH_ERROR`
242
+ - Return `true` to accept the connection
243
+
231
244
  ```javascript
232
245
  function authenticateSocket(token, socket) {
233
246
  const verify = AuthService.verifyToken(token);
@@ -239,7 +252,7 @@ function authenticateSocket(token, socket) {
239
252
  }
240
253
 
241
254
  MongoRealtime.init({
242
- connection: mongoose.connection,
255
+ dbUri: "mongodb://localhost:27017/mydb",
243
256
  server: server,
244
257
  authentify: authenticateSocket,
245
258
  middlewares: [
@@ -264,7 +277,7 @@ On init, when `safeListStream` is `true`(default), two list streams can't have t
264
277
 
265
278
  ```javascript
266
279
  MongoRealtime.init({
267
- connection: mongoose.connection,
280
+ dbUri: "mongodb://localhost:27017/mydb",
268
281
  server: server,
269
282
  autoListStream: ["users"], // automatically stream users collection only
270
283
  });
@@ -291,16 +304,16 @@ To avoid all these issues, you can set `safeListStream` to `false` in the init o
291
304
 
292
305
  ```javascript
293
306
  MongoRealtime.init({
294
- connection: mongoose.connection,
307
+ dbUri: "mongodb://localhost:27017/mydb",
295
308
  server: server,
296
309
  autoListStream: [], // stream no collection automatically (you can add your own filtered streams later)
297
310
  });
298
311
  // or
299
312
  MongoRealtime.init({
300
- connection: mongoose.connection,
313
+ dbUri: "mongodb://localhost:27017/mydb",
301
314
  server: server,
302
315
  safeListStream: false, // disable safe mode (you can override existing streams)
303
- // Still stream all collections automatically but you can override them
316
+ // Still stream all collections automatically but you can override them
304
317
  }):
305
318
 
306
319
  MongoRealtime.addListStream("posts", "posts", (doc) => !!doc.title); // client can listen to db:stream:posts
@@ -311,7 +324,7 @@ MongoRealtime.addListStream("users", "users", (doc) => !!doc.email); // will not
311
324
 
312
325
  ```javascript
313
326
  MongoRealtime.init({
314
- connection: mongoose.connection,
327
+ dbUri: "mongodb://localhost:27017/mydb",
315
328
  server: server,
316
329
  authentify: (token, socket) => {
317
330
  try {
@@ -379,6 +392,8 @@ MongoRealtime.addListStream(
379
392
 
380
393
  ### MongoDB must be in Replica Set mode
381
394
 
395
+ To use Change Streams, MongoDB must be running as a replica set. For local development, you can initiate a single-node replica set:
396
+
382
397
  ```bash
383
398
  mongod --replSet rs0
384
399
 
package/index.js CHANGED
@@ -1,14 +1,7 @@
1
+ const mongoose = require("mongoose");
1
2
  const { Server } = require("socket.io");
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
- }
3
+ const { version } = require("./package.json");
4
+ const chalk = require("chalk");
12
5
 
13
6
  /**
14
7
  * @typedef {Object} ChangeStreamDocument
@@ -38,7 +31,8 @@ function sortObj(obj = {}) {
38
31
 
39
32
  class MongoRealtime {
40
33
  /** @type {import("socket.io").Server} */ static io;
41
- /** @type {import("mongoose").Connection} */ static connection;
34
+ /** @type {import("mongoose").Connection} */ static connection =
35
+ mongoose.connection;
42
36
  /** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
43
37
  {};
44
38
  /** @type {Record<String, [Object]>} */
@@ -53,13 +47,56 @@ class MongoRealtime {
53
47
 
54
48
  static #safeListStream = true;
55
49
 
50
+ static version = version;
51
+
52
+ static #check(fn, err) {
53
+ const result = fn();
54
+ if (!result) {
55
+ let src = fn.toString().trim();
56
+
57
+ let match =
58
+ src.match(/=>\s*([^{};]+)$/) ||
59
+ src.match(/return\s+([^;}]*)/) ||
60
+ src.match(/{([^}]*)}/);
61
+
62
+ const expr = err ?? (match ? match[1].trim() : src);
63
+
64
+ throw new Error(`MongoRealtime failed to check "${expr}"`);
65
+ }
66
+ }
67
+
68
+ static #log(message, type = 0) {
69
+ const text = `[REALTIME] ${message}`;
70
+ switch (type) {
71
+ case 1:
72
+ console.log(chalk.bold.hex('#11AA60FF')(text));
73
+ break;
74
+ case 2:
75
+ console.log(chalk.bold.bgHex("#257993")(text));
76
+ break;
77
+ case 3:
78
+ console.log(chalk.bold.yellow(text));
79
+ break;
80
+ case 4:
81
+ console.log(chalk.bold.red(text));
82
+ break;
83
+
84
+ default:
85
+ console.log(text);
86
+ break;
87
+ }
88
+ }
89
+
56
90
  /**
57
91
  * Initializes the socket system.
58
92
  *
59
93
  * @param {Object} options
60
- * @param {import("mongoose").Connection} options.connection - Active Mongoose connection
94
+ * @param {String} options.dbUri - Database URI
95
+ * @param {mongoose.ConnectOptions | undefined} options.dbOptions - Database connect options
61
96
  * @param {(token:String, socket: import("socket.io").Socket) => boolean | Promise<boolean>} options.authentify - Auth function that should return true if `token` is valid
62
97
  * @param {[( socket: import("socket.io").Socket, next: (err?: ExtendedError) => void) => void]} options.middlewares - Register mmiddlewares on incoming socket
98
+ * @param {(conn:mongoose.Connection) => void} options.onDbConnect - Callback triggered when a socket connects
99
+ * @param {(err:Error) => void} options.onDbError - Callback triggered when a socket connects
63
100
  * @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
64
101
  * @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
65
102
  * @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
@@ -69,74 +106,30 @@ class MongoRealtime {
69
106
  * @param {bool} options.safeListStream - If true(default), declaring an existing streamId will throw an error
70
107
  *
71
108
  */
72
- static init({
73
- connection,
109
+ static async init({
110
+ dbUri,
111
+ dbOptions,
74
112
  server,
113
+ onDbConnect,
114
+ onDbError,
75
115
  authentify,
76
- middlewares = [],
77
- autoListStream,
78
116
  onSocket,
79
117
  offSocket,
80
118
  safeListStream = true,
119
+ autoListStream,
120
+ middlewares = [],
81
121
  watch = [],
82
122
  ignore = [],
83
123
  }) {
124
+ console.clear();
125
+ this.#log(`MongoRealtime version (${this.version})`, 2);
84
126
  if (this.io) this.io.close();
85
- this.io = new Server(server);
86
- this.connection = connection;
87
- this.#safeListStream = !!safeListStream;
88
-
89
- watch = watch.map((s) => s.toLowerCase());
90
- ignore = ignore.map((s) => s.toLowerCase());
91
-
92
- this.io.use(async (socket, next) => {
93
- if (!!authentify) {
94
- try {
95
- const token =
96
- socket.handshake.auth.token ||
97
- socket.handshake.headers.authorization;
98
- if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
127
+ this.#check(() => dbUri);
128
+ this.#check(() => server);
99
129
 
100
- const authorized = await authentify(token, socket);
101
- if (authorized === true) return next(); // exactly returns true
102
-
103
- return next(new Error("UNAUTHORIZED"));
104
- } catch (error) {
105
- return next(new Error("AUTH_ERROR"));
106
- }
107
- } else {
108
- return next();
109
- }
110
- });
111
-
112
- for (let middleware of middlewares) {
113
- this.io.use(middleware);
114
- }
115
-
116
- this.io.on("connection", (socket) => {
117
- if (onSocket) onSocket(socket);
118
-
119
- socket.on("db:stream[register]", async (streamId, registerId) => {
120
- const stream = this.#streams[streamId];
121
- if (!stream) return;
122
- const coll = stream.collection;
123
-
124
- if (!this.#cache[coll]) {
125
- this.#cache[coll] = await connection.db
126
- .collection(coll)
127
- .find({})
128
- .toArray();
129
- }
130
- this.io.emit(`db:stream[register][${registerId}]`, this.#cache[coll]);
131
- });
132
-
133
- socket.on("disconnect", (r) => {
134
- if (offSocket) offSocket(socket, r);
135
- });
136
- });
137
-
138
- connection.once("open", async () => {
139
- this.collections = (await connection.listCollections()).map(
130
+ this.io = new Server(server);
131
+ this.connection.once("open", async () => {
132
+ this.collections = (await this.connection.listCollections()).map(
140
133
  (c) => c.name
141
134
  );
142
135
 
@@ -158,7 +151,7 @@ class MongoRealtime {
158
151
  ];
159
152
  }
160
153
 
161
- const changeStream = connection.watch(pipeline, {
154
+ const changeStream = this.connection.watch(pipeline, {
162
155
  fullDocument: "updateLookup",
163
156
  fullDocumentBeforeChange: "whenAvailable",
164
157
  });
@@ -177,14 +170,15 @@ class MongoRealtime {
177
170
  const coll = change.ns.coll;
178
171
 
179
172
  if (!this.#cache[coll]) {
180
- this.#cache[coll] = await connection.db
173
+ this.#cache[coll] = await this.connection.db
181
174
  .collection(coll)
182
175
  .find({})
176
+ .sort({ _id: -1 })
183
177
  .toArray();
184
178
  } else
185
179
  switch (change.operationType) {
186
180
  case "insert":
187
- this.#cache[coll].push(change.fullDocument);
181
+ this.#cache[coll].unshift(change.fullDocument); // add to top;
188
182
  break;
189
183
 
190
184
  case "update":
@@ -253,6 +247,81 @@ class MongoRealtime {
253
247
  }
254
248
  });
255
249
  });
250
+
251
+ try {
252
+ await mongoose.connect(dbUri, dbOptions);
253
+ this.#log(`Connected to db '${mongoose.connection.name}'`, 1);
254
+ onDbConnect?.call(this, mongoose.connection);
255
+ } catch (error) {
256
+ onDbError?.call(this, error);
257
+ this.#log("Failed to init",4);
258
+ return;
259
+ }
260
+
261
+ this.#check(() => mongoose.connection.db, "No database found");
262
+
263
+ this.#safeListStream = !!safeListStream;
264
+
265
+ watch = watch.map((s) => s.toLowerCase());
266
+ ignore = ignore.map((s) => s.toLowerCase());
267
+
268
+ this.io.use(async (socket, next) => {
269
+ if (!!authentify) {
270
+ try {
271
+ const token =
272
+ socket.handshake.auth.token ||
273
+ socket.handshake.headers.authorization;
274
+ if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
275
+
276
+ const authorized = await authentify(token, socket);
277
+ if (authorized === true) return next(); // exactly returns true
278
+
279
+ return next(new Error("UNAUTHORIZED"));
280
+ } catch (error) {
281
+ return next(new Error("AUTH_ERROR"));
282
+ }
283
+ } else {
284
+ return next();
285
+ }
286
+ });
287
+
288
+ for (let middleware of middlewares) {
289
+ this.io.use(middleware);
290
+ }
291
+
292
+ this.io.on("connection", (socket) => {
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
+
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
+ });
314
+
315
+ socket.on("disconnect", (r) => {
316
+ if (offSocket) offSocket(socket, r);
317
+ });
318
+
319
+ if (onSocket) onSocket(socket);
320
+
321
+ });
322
+
323
+
324
+ this.#log(`Initialized`,1);
256
325
  }
257
326
 
258
327
  /**
@@ -302,7 +371,7 @@ class MongoRealtime {
302
371
  collection,
303
372
  filter,
304
373
  };
305
- }
374
+ }
306
375
 
307
376
  /**
308
377
  * @param {String} streamId - StreamId of the stream
package/package.json CHANGED
@@ -1,26 +1,27 @@
1
- {
2
- "name": "mongo-realtime",
3
- "version": "1.1.2",
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
- }
1
+ {
2
+ "name": "mongo-realtime",
3
+ "version": "1.2.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
+ "chalk": "^4.1.2",
24
+ "mongoose": "^8.17.0",
25
+ "socket.io": "^4.8.1"
26
+ }
27
+ }