mongo-realtime 2.0.4 → 3.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.
package/index.js DELETED
@@ -1,570 +0,0 @@
1
- const mongoose = require("mongoose");
2
- const { Server } = require("socket.io");
3
- const { version } = require("./package.json");
4
- const chalk = require("chalk");
5
-
6
- /**
7
- * @typedef {Object} ChangeStreamDocument
8
- * @property {"insert"|"update"|"replace"|"delete"|"invalidate"|"drop"|"dropDatabase"|"rename"} operationType
9
- * The type of operation that triggered the event.
10
- *
11
- * @property {Object} ns
12
- * @property {string} ns.db - Database name
13
- * @property {string} ns.coll - Collection name
14
- *
15
- * @property {Object} documentKey
16
- * @property {import("bson").ObjectId|string} documentKey._id - The document’s identifier
17
- *
18
- * @property {Object} [fullDocument]
19
- * The full document after the change (only present if `fullDocument: "updateLookup"` is enabled).
20
- *
21
- * @property {Object} [updateDescription]
22
- * @property {Object.<string, any>} [updateDescription.updatedFields]
23
- * Fields that were updated during an update operation.
24
- * @property {string[]} [updateDescription.removedFields]
25
- * Fields that were removed during an update operation.
26
- *
27
- * @property {Object} [rename] - Info about the collection rename (if operationType is "rename").
28
- *
29
- * @property {Date} [clusterTime] - Logical timestamp of the event.
30
- */
31
-
32
- class MongoRealtime {
33
- /** @type {import("socket.io").Server} */ static io;
34
- /** @type {import("mongoose").Connection} */ static connection =
35
- mongoose.connection;
36
- /** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners =
37
- {};
38
-
39
- static sockets = () => [...this.io.sockets.sockets.values()];
40
-
41
- /**@type {Record<String, {collection:String,filter: (doc:Object)=>Promise<boolean>}>} */
42
- static #streams = {};
43
-
44
- /**@type {Record<String, Record<String,{}>>} */
45
- static #data = {};
46
-
47
- /** @type {[String]} - All DB collections */
48
- static collections = [];
49
-
50
- static #debug = false;
51
-
52
- static version = version;
53
-
54
- static #resolvePromise;
55
- static #rejectPromise;
56
-
57
- static get(coll) {
58
- return Object.values(this.#data[coll] ?? {});
59
- }
60
-
61
- static #check(fn, err) {
62
- const result = fn();
63
- if (!result) {
64
- let src = fn.toString().trim();
65
-
66
- let match =
67
- src.match(/=>\s*([^{};]+)$/) ||
68
- src.match(/return\s+([^;}]*)/) ||
69
- src.match(/{([^}]*)}/);
70
-
71
- const expr = err ?? (match ? match[1].trim() : src);
72
-
73
- throw new Error(`MongoRealtime expects "${expr}"`);
74
- }
75
- }
76
-
77
- static #log(message, type = 0) {
78
- const text = `[REALTIME] ${message}`;
79
- switch (type) {
80
- case 1:
81
- console.log(chalk.bold.hex("#11AA60FF")(text));
82
- break;
83
- case 2:
84
- console.log(chalk.bold.bgHex("#257993")(text));
85
- break;
86
- case 3:
87
- console.log(chalk.bold.yellow(text));
88
- break;
89
- case 4:
90
- console.log(chalk.bold.red(text));
91
- break;
92
-
93
- case 5:
94
- console.log(chalk.italic(text));
95
- break;
96
-
97
- default:
98
- console.log(text);
99
- break;
100
- }
101
- }
102
-
103
- static #debugLog(message) {
104
- if (this.#debug) this.#log("[DEBUG] " + message, 5);
105
- }
106
-
107
- /**
108
- * Initializes the socket system.
109
- *
110
- * @param {Object} options
111
- * @param {String} options.dbUri - Database URI
112
- * @param {mongoose.ConnectOptions | undefined} options.dbOptions - Database connect options
113
- * @param {(token:String, socket: import("socket.io").Socket) => boolean | Promise<boolean>} options.authentify - Auth function that should return true if `token` is valid
114
- * @param {[( socket: import("socket.io").Socket, next: (err?: ExtendedError) => void) => void]} options.middlewares - Register mmiddlewares on incoming socket
115
- * @param {(conn:mongoose.Connection) => void} options.onDbConnect - Callback triggered when a socket connects
116
- * @param {(err:Error) => void} options.onDbError - Callback triggered when a socket connects
117
- * @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects
118
- * @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects
119
- * @param {import("http").Server} options.server - HTTP server to attach Socket.IO to
120
- * @param {[String]} options.autoStream - Collections to stream automatically. If empty, will stream no collection. If null, will stream all collections.
121
- * @param {[String]} options.watch - Collections to watch. If empty, will watch all collections
122
- * @param {[String]} options.ignore - Collections to ignore. Can override `watch`
123
- * @param {bool} options.debug - Enable debug mode
124
- * @param {number} options.cacheDelay - Cache delay in minutes. Put 0 if no cache
125
- * @param {number} options.allowDbOperations - If true, you can use find and update operations.
126
- * @param {mongoose} options.mongooseInstance - Running mongoose instance
127
- *
128
- *
129
- */
130
- static async init({
131
- dbUri,
132
- dbOptions,
133
- server,
134
- mongooseInstance,
135
- onDbConnect,
136
- onDbError,
137
- authentify,
138
- onSocket,
139
- offSocket,
140
- debug = false,
141
- autoStream,
142
- middlewares = [],
143
- watch = [],
144
- ignore = [],
145
- cacheDelay = 5,
146
- allowDbOperations = true,
147
- }) {
148
- const promise = new Promise((resolve, reject) => {
149
- this.#resolvePromise = resolve;
150
- this.#rejectPromise = reject;
151
- });
152
-
153
- this.#log(`MongoRealtime version (${this.version})`, 2);
154
-
155
- if (this.io) this.io.close();
156
- this.#check(() => dbUri);
157
- this.#check(() => server);
158
- this.#debug = debug;
159
-
160
- this.io = new Server(server);
161
- this.connection.once("open", async () => {
162
- this.collections = (await this.connection.listCollections()).map(
163
- (c) => c.name,
164
- );
165
- this.#debugLog(
166
- `${this.collections.length} collections found : ${this.collections.join(
167
- ", ",
168
- )}`,
169
- );
170
-
171
- let pipeline = [];
172
- if (watch.length !== 0 && ignore.length === 0) {
173
- pipeline = [{ $match: { "ns.coll": { $in: watch } } }];
174
- } else if (watch.length === 0 && ignore.length !== 0) {
175
- pipeline = [{ $match: { "ns.coll": { $nin: ignore } } }];
176
- } else if (watch.length !== 0 && ignore.length !== 0) {
177
- pipeline = [
178
- {
179
- $match: {
180
- $and: [
181
- { "ns.coll": { $in: watch } },
182
- { "ns.coll": { $nin: ignore } },
183
- ],
184
- },
185
- },
186
- ];
187
- }
188
-
189
- const changeStream = this.connection.watch(pipeline, {
190
- fullDocument: "updateLookup",
191
- fullDocumentBeforeChange: "whenAvailable",
192
- });
193
-
194
- /** Setup main streams */
195
- let collectionsToStream = [];
196
- if (autoStream == null) collectionsToStream = this.collections;
197
- else
198
- collectionsToStream = this.collections.filter((c) =>
199
- autoStream.includes(c),
200
- );
201
- for (let col of collectionsToStream) this.addStream(col, col);
202
- this.#debugLog(
203
- `Auto stream on collections : ${collectionsToStream.join(", ")}`,
204
- );
205
-
206
- /** Emit listen events on change */
207
- changeStream.on("change", async (change) => {
208
- const coll = change.ns.coll;
209
- const colName = coll.toLowerCase();
210
- const id = change.documentKey?._id.toString();
211
- const doc = change.fullDocument ?? { _id: id };
212
-
213
- this.#debugLog(`Collection '${colName}' changed`);
214
-
215
- change.col = colName;
216
-
217
- const type = change.operationType;
218
-
219
- const e_change = "db:change";
220
- const e_change_type = `db:${type}`;
221
- const e_change_col = `${e_change}:${colName}`;
222
- const e_change_type_col = `${e_change_type}:${colName}`;
223
-
224
- const events = [
225
- e_change,
226
- e_change_type,
227
- e_change_col,
228
- e_change_type_col,
229
- ];
230
-
231
- if (id) {
232
- change.docId = id;
233
- const e_change_doc = `${e_change_col}:${id}`;
234
- const e_change_type_doc = `${e_change_type_col}:${id}`;
235
- events.push(e_change_doc, e_change_type_doc);
236
- }
237
- for (let e of events) {
238
- this.io.emit(e, change);
239
- this.notifyListeners(e, change);
240
- }
241
-
242
- for (const k in this.#streams) {
243
- const stream = this.#streams[k];
244
- if (stream.collection != coll) continue;
245
-
246
- Promise.resolve(stream.filter(doc)).then((ok) => {
247
- if (ok) {
248
- const data = { added: [], removed: [] };
249
- if (change.operationType == "delete") data.removed.push(doc);
250
- else data.added.push(doc);
251
-
252
- this.io.emit(`realtime:${k}`, data);
253
- }
254
- });
255
- }
256
- switch (change.operationType) {
257
- case "delete":
258
- delete this.#data[coll][id];
259
- break;
260
- default:
261
- doc._id = id;
262
- this.#data[coll][id] = doc;
263
- }
264
- });
265
-
266
- this.#getAllData();
267
- });
268
-
269
- try {
270
- await mongoose.connect(dbUri, dbOptions);
271
- this.#log(`Connected to db '${mongoose.connection.name}'`, 1);
272
- if (mongooseInstance) mongooseInstance.connection = mongoose.connection;
273
- onDbConnect?.call(this, mongoose.connection);
274
- } catch (error) {
275
- onDbError?.call(this, error);
276
- this.#log("Failed to init", 4);
277
- return;
278
- }
279
-
280
- this.#check(() => mongoose.connection.db, "No database found");
281
-
282
- watch = watch.map((s) => s.toLowerCase());
283
- ignore = ignore.map((s) => s.toLowerCase());
284
-
285
- this.io.use(async (socket, next) => {
286
- if (!!authentify) {
287
- try {
288
- const token =
289
- socket.handshake.auth.token ||
290
- socket.handshake.headers.authorization;
291
- if (!token) return next(new Error("NO_TOKEN_PROVIDED"));
292
-
293
- const authorized = await authentify(token, socket);
294
- if (authorized === true) return next(); // exactly returns true
295
-
296
- return next(new Error("UNAUTHORIZED"));
297
- } catch (error) {
298
- return next(new Error("AUTH_ERROR"));
299
- }
300
- } else {
301
- return next();
302
- }
303
- });
304
-
305
- for (let middleware of middlewares) {
306
- this.io.use(middleware);
307
- }
308
-
309
- this.io.on("connection", (socket) => {
310
- socket.emit("version", version);
311
-
312
- socket.on(
313
- "realtime",
314
- async ({ streamId, limit, reverse, registerId }) => {
315
- if (!streamId || !this.#streams[streamId]) return;
316
-
317
- const stream = this.#streams[streamId];
318
- const coll = stream.collection;
319
- const data = this.#data[coll];
320
-
321
- registerId ??= "";
322
- // this.#debugLog(
323
- // `Socket '${socket.id}' registred for realtime '${coll}:${registerId}'. Limit ${limit}. Reversed ${reverse}`,
324
- // );
325
-
326
- let result = Object.values(data);
327
-
328
- const filtered = (
329
- await Promise.all(
330
- result.map(async (doc) => {
331
- try {
332
- return {
333
- doc,
334
- ok: await stream.filter(doc),
335
- };
336
- } catch (e) {
337
- return {
338
- doc,
339
- ok: false,
340
- };
341
- }
342
- }),
343
- )
344
- )
345
- .filter((item) => item.ok)
346
- .map((item) => item.doc);
347
-
348
- limit ??= filtered.length;
349
- for (let i = 0; i < filtered.length; i += limit) {
350
- socket.emit(`realtime:${streamId}:${registerId}`, {
351
- added: filtered.slice(i, i + limit),
352
- removed: [],
353
- });
354
- }
355
- },
356
- );
357
- if (allowDbOperations) {
358
- socket.on("realtime:count", async ({ coll, query }, ack) => {
359
- if (!coll) return ack(0);
360
- query ??= {};
361
- const c = this.connection.db.collection(coll);
362
- const hasQuery = notEmpty(query);
363
- const count = hasQuery
364
- ? await c.countDocuments(query)
365
- : await c.estimatedDocumentCount();
366
- ack(count);
367
- });
368
-
369
- socket.on(
370
- "realtime:find",
371
- async (
372
- { coll, query, limit, sortBy, project, one, skip, id },
373
- ack,
374
- ) => {
375
- if (!coll) return ack(null);
376
- const c = this.connection.db.collection(coll);
377
-
378
- if (id) {
379
- ack(await c.findOne({ _id: toObjectId(id) }));
380
- return;
381
- }
382
-
383
- query ??= {};
384
- one = one == true;
385
-
386
- if (query["_id"]) {
387
- query["_id"] = toObjectId(query["_id"]);
388
- }
389
-
390
- const options = {
391
- sort: sortBy,
392
- projection: project,
393
- skip: skip,
394
- limit: limit,
395
- };
396
-
397
- if (one) {
398
- ack(await c.findOne(query, options));
399
- return;
400
- }
401
-
402
- let cursor = c.find(query, options);
403
- ack(await cursor.toArray());
404
- },
405
- );
406
-
407
- socket.on(
408
- "realtime:update",
409
- async (
410
- { coll, query, limit, sortBy, project, one, skip, id, update },
411
- ack,
412
- ) => {
413
- if (!coll || !notEmpty(update)) return ack(0);
414
- const c = this.connection.db.collection(coll);
415
-
416
- if (id) {
417
- ack(
418
- (await c.updateOne({ _id: toObjectId(id) }, update))
419
- .modifiedCount,
420
- );
421
- return;
422
- }
423
-
424
- query ??= {};
425
- one = one == true;
426
-
427
- if (query["_id"]) {
428
- query["_id"] = toObjectId(query["_id"]);
429
- }
430
-
431
- const options = {
432
- sort: sortBy,
433
- projection: project,
434
- skip: skip,
435
- limit: limit,
436
- };
437
-
438
- if (one) {
439
- ack((await c.updateOne(query, update, options)).modifiedCount);
440
- return;
441
- }
442
-
443
- let cursor = await c.updateMany(query, update, options);
444
- ack(cursor.modifiedCount);
445
- },
446
- );
447
- }
448
-
449
- socket.on("disconnect", (r) => {
450
- if (offSocket) offSocket(socket, r);
451
- });
452
-
453
- if (onSocket) onSocket(socket);
454
- });
455
-
456
- return promise;
457
- }
458
-
459
- /**
460
- * Notify all event listeners
461
- *
462
- * @param {String} e - Name of the event
463
- * @param {ChangeStreamDocument} change - Change Stream
464
- */
465
- static notifyListeners(e, change) {
466
- if (this.#listeners[e]) {
467
- for (let c of this.#listeners[e]) {
468
- c(change);
469
- }
470
- }
471
- }
472
-
473
- static #getAllData() {
474
- const total = this.collections.length;
475
- for (let coll of this.collections) {
476
- this.connection.db
477
- .collection(coll)
478
- .find()
479
- .toArray()
480
- .then((r) => {
481
- this.#data[coll] = r.reduce((acc, doc) => {
482
- acc[doc._id] = doc;
483
- return acc;
484
- }, {});
485
-
486
- if (Object.keys(this.#data).length == total) {
487
- this.#log(`Initialized`, 1);
488
- this.#resolvePromise(this.#data);
489
- }
490
- });
491
- }
492
- }
493
-
494
- /**
495
- * Subscribe to an event
496
- *
497
- * @param {String} key - Name of the event
498
- * @param {(change:ChangeStreamDocument)=>void} cb - Callback
499
- */
500
- static listen(key, cb) {
501
- if (!this.#listeners[key]) this.#listeners[key] = [];
502
- this.#listeners[key].push(cb);
503
- }
504
-
505
- /**
506
- *
507
- * @param {String} streamId - StreamId of the list stream
508
- * @param {String} collection - Name of the collection to stream
509
- * @param { (doc:Object )=>Promise<boolean>} filter - Collection filter
510
- *
511
- * Register a new list stream to listen
512
- */
513
- static addStream(streamId, collection, filter) {
514
- if (!streamId) throw new Error("Stream id is required");
515
- if (!collection) throw new Error("Collection is required");
516
- if (this.#streams[streamId] && this.collections.includes(streamId))
517
- throw new Error(`streamId '${streamId}' cannot be a collection`);
518
-
519
- filter ??= (_, __) => true;
520
-
521
- this.#streams[streamId] = {
522
- collection,
523
- filter,
524
- };
525
- }
526
-
527
- /**
528
- * @param {String} streamId - StreamId of the stream
529
- *
530
- * Delete a registered stream
531
- */
532
- static removeStream(streamId) {
533
- delete this.#streams[streamId];
534
- }
535
-
536
- /**
537
- * Remove one or all listeners of an event
538
- *
539
- * @param {String} key - Name of the event
540
- * @param {(change:ChangeStreamDocument)=>void} cb - Callback
541
- */
542
- static removeListener(key, cb) {
543
- if (cb) this.#listeners[key] = this.#listeners[key].filter((c) => c != cb);
544
- else this.#listeners[key] = [];
545
- }
546
-
547
- /**
548
- * Unsubscribe to all events
549
- */
550
- static removeAllListeners() {
551
- this.#listeners = {};
552
- }
553
- }
554
-
555
- // utils
556
- function notEmpty(obj) {
557
- obj ??= {};
558
- return Object.keys(obj).length > 0;
559
- }
560
- /** @param {String} id */
561
- function toObjectId(id) {
562
- if (typeof id != "string") return id;
563
- try {
564
- return mongoose.Types.ObjectId.createFromHexString(id);
565
- } catch (_) {
566
- return new mongoose.Types.ObjectId(id); //use deprecated if fail
567
- }
568
- }
569
-
570
- module.exports = MongoRealtime;
package/logo.png DELETED
Binary file