UNPKG

mongo-realtime

Version:

A Node.js package that combines Socket.IO and MongoDB Change Streams to deliver real-time database updates to your WebSocket clients.

152 lines (134 loc) 4.65 kB
const { Server } = require("socket.io"); class MongoRealtime { /** @type {import("socket.io").Server} */ static io; /** @type {import("mongoose").Connection} */ static connection; /** @type {[import("socket.io").Socket]} */ static sockets = []; /** @type {Record<String, [(change:ChangeStreamDocument)=>void]>} */ static #listeners = {}; /** * Initializes the socket system. * * @param {Object} options * @param {import("mongoose").Connection} options.connection - Active Mongoose connection * @param {(socket: import("socket.io").Socket) => void} options.onSocket - Callback triggered when a socket connects * @param {(socket: import("socket.io").Socket, reason: import("socket.io").DisconnectReason) => void} options.offSocket - Callback triggered when a socket disconnects * @param {import("http").Server} options.server - HTTP server to attach Socket.IO to * @param {[String]} options.watch - Collections to watch. If empty, will watch all collections * @param {[String]} options.ignore - Collections to ignore. Can override `watch` * */ static init({ connection, server, onSocket, offSocket, watch = [], ignore = [], }) { if (this.io) this.io.close(() => { this.sockets = []; }); this.io = new Server(server); this.connection = connection; watch = watch.map((s) => s.toLowerCase()); ignore = ignore.map((s) => s.toLowerCase()); this.io.on("connection", (socket) => { this.sockets = [...this.io.sockets.sockets.values()]; if (onSocket) onSocket(socket); socket.on("disconnect", (r) => { this.sockets = [...this.io.sockets.sockets.values()]; if (offSocket) offSocket(socket, r); }); }); connection.once("open", () => { let pipeline = []; if (watch.length !== 0 && ignore.length === 0) { pipeline = [{ $match: { "ns.coll": { $in: watch } } }]; } else if (watch.length === 0 && ignore.length !== 0) { pipeline = [{ $match: { "ns.coll": { $nin: ignore } } }]; } else if (watch.length !== 0 && ignore.length !== 0) { pipeline = [ { $match: { $and: [ { "ns.coll": { $in: watch } }, { "ns.coll": { $nin: ignore } }, ], }, }, ]; } const changeStream = connection.watch(pipeline, { fullDocument: "updateLookup", fullDocumentBeforeChange: "whenAvailable", }); changeStream.on("change", (change) => { const colName = change.ns.coll.toLowerCase(); change.col = colName; const type = change.operationType; const id = change.documentKey?._id; const e_change = "db:change"; const e_change_type = `db:${type}`; const e_change_col = `${e_change}:${colName}`; const e_change_type_col = `${e_change_type}:${colName}`; const events = [ e_change, e_change_type, e_change_col, e_change_type_col, ]; if (id) { change.docId = id; const e_change_doc = `${e_change_col}:${id}`; const e_change_type_doc = `${e_change_type_col}:${id}`; events.push(e_change_doc, e_change_type_doc); } for (let e of events) { this.io.emit(e, change); this.notifyListeners(e, change); } }); }); } /** * Notify all event listeners * * @param {String} e - Name of the event * @param {ChangeStreamDocument} change - Change Stream */ static notifyListeners(e, change) { if (this.#listeners[e]) { for (let c of this.#listeners[e]) { c(change); } } } /** * Subscribe to an event * * @param {String} key - Name of the event * @param {(change:ChangeStreamDocument)=>void} cb - Callback */ static listen(key, cb) { if (!this.#listeners[key]) this.#listeners[key] = []; this.#listeners[key].push(cb); } /** * Remove one or all listeners of an event * * @param {String} key - Name of the event * @param {(change:ChangeStreamDocument)=>void} cb - Callback */ static removeListener(key, cb) { if (cb) this.#listeners[key] = this.#listeners[key].filter((c) => c != cb); else this.#listeners[key] = []; } /** * Unsubscribe to all events */ static removeAllListeners() { this.#listeners = {}; } } module.exports = MongoRealtime;