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