mongo-realtime 2.0.4 → 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/README.md CHANGED
@@ -1,438 +1,362 @@
1
- # Mongo Realtime
2
-
3
- A Node.js package that combines Socket.IO and MongoDB Change Streams to deliver real-time database updates to your WebSocket clients.
4
-
5
- ![Banner](logo.png)
6
-
7
- ## 🚀 Features
8
-
9
- - **Real-time updates**: Automatically detects changes in MongoDB and broadcasts them via Socket.IO
10
- - **Granular events**: Emits specific events by operation type, collection, and document
11
- - **Connection management**: Customizable callbacks for socket connections/disconnections
12
- - **TypeScript compatible**: JSDoc annotations for better development experience
13
-
14
- ## 📦 Installation
15
-
16
- ```bash
17
- npm install mongo-realtime
18
- ```
19
-
20
- ## Setup
21
-
22
- ### Prerequisites
23
-
24
- - MongoDB running as a replica set (required for Change Streams)
25
- - Node.js HTTP server (See below how to configure an HTTP server with Express)
26
-
27
- ### Example setup
28
-
29
- ```javascript
30
- const express = require("express");
31
- const http = require("http");
32
- const MongoRealtime = require("mongo-realtime");
33
-
34
- const app = express();
35
- const server = http.createServer(app);
36
-
37
- MongoRealtime.init({
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
- },
45
- server: server,
46
- ignore: ["posts"], // ignore 'posts' collection
47
- onSocket: (socket) => {
48
- console.log(`Client connected: ${socket.id}`);
49
- socket.emit("welcome", { message: "Connection successful!" });
50
- },
51
- offSocket: (socket, reason) => {
52
- console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
53
- },
54
- }).then(() => {
55
- // It's better to start the server after the db connection is established
56
- server.listen(3000, () => {
57
- console.log("Server listening on port 3000");
58
- });
59
- });
60
-
61
- ```
62
-
63
- ## Breaking Changes since v2.x.x
64
-
65
- In older versions, streams receive all documents and send them to sockets. This can lead to performance issues with large collections.\
66
- From version 2.x.x, streams now require a limit (default 100 documents) from the client side to avoid overloading the server and network.\
67
- Receiving streamed docs now works like this:
68
-
69
- ```javascript
70
- socket.emit("realtime", {
71
- limit: 100,
72
- streamId: "users",
73
- reverse: true,
74
- registerId: "random-client-generated-id",
75
- });
76
- /*
77
- * Request 100 documents from 'users' stream (streamId defined server-side or collection name if autoStream)
78
- * 'reverse' to get the latest documents first
79
- * 'registerId' is a unique id generated by the client to identify this request. It will be used to listen to the response event.
80
- * The response return an object {results: [...], count: number, total:number, remaining:number, coll:string}
81
- */
82
-
83
- socket.on("realtime:users:random-client-generated-id", (data) => {
84
- console.log("Received streamed documents:", data.results); // 100 max per
85
- });
86
-
87
- socket.emit("realtime", {
88
- limit: 50,
89
- streamId: "usersWithEmail", // make sure this stream is defined server-side
90
- registerId: "specific-request-id",
91
- });
92
-
93
- socket.on("realtime:usersWithEmail:specific-request-id", (data) => {
94
- console.log("Received streamed documents with email:", data.results); // 50 max per
95
- });
96
-
97
- // Make sure to listen before emitting the request to avoid missing the response
98
- // To stop receiving streamed documents for a specific request just use socket.off
99
- ```
100
-
101
- ## 📋 API
102
-
103
- ### `MongoRealtime.init(options)`
104
-
105
- Initializes the socket system and MongoDB Change Streams.
106
-
107
- #### Parameters
108
-
109
- \* means required
110
-
111
- | Parameter | Type | Description |
112
- | --------------------- | ----------------- | -------------------------------------------------------------------------------- |
113
- | `options.dbUri` | `String`\* | Database URI |
114
- | `options.dbOptions` | `Object` | Mongoose connection options |
115
- | `options.onDbConnect` | `Function` | Callback on successful database connection |
116
- | `options.onDbError` | `Function` | Callback on database connection error |
117
- | `options.server` | `http.Server`\* | HTTP server to attach Socket.IO |
118
- | `options.authentify` | `Function` | Function to authenticate socket connections. Should return true if authenticated |
119
- | `options.middlewares` | `Array[Function]` | Array of Socket.IO middlewares |
120
- | `options.onSocket` | `Function` | Callback on socket connection |
121
- | `options.offSocket` | `Function` | Callback on socket disconnection |
122
- | `options.watch` | `Array[String]` | Collections to only watch. Listen to all when is empty |
123
- | `options.ignore` | `Array[String]` | Collections to only ignore. Overrides watch array |
124
- | `options.autoStream` | `Array[String]` | Collections to automatically stream to clients. Default is all |
125
- | `options.debug` | `Boolean` | Enable debug logs |
126
-
127
- #### Static Properties and Methods
128
-
129
- - `MongoRealtime.addStream(streamId, collection, filter)`: Manually add a list stream for a specific collection and filter
130
- - `MongoRealtime.connection`: MongoDB connection
131
- - `MongoRealtime.collections`: Array of database collections
132
- - `MongoRealtime.io`: Socket.IO server instance
133
- - `MongoRealtime.init(options)`: Initialize the package with options
134
- - `MongoRealtime.listen(event, callback)`: Add an event listener for database changes
135
- - `MongoRealtime.notifyListeners(event, data)`: Manually emit an event to all listeners
136
- - `MongoRealtime.removeStream(streamId)`: Remove a previously added stream by id
137
- - `MongoRealtime.removeListener(event, callback)`: Remove a specific event listener or all listeners for an event
138
- - `MongoRealtime.removeAllListeners()`: Remove all event listeners
139
- - `MongoRealtime.sockets()`: Returns an array of connected sockets
140
-
141
- ## 🎯 Emitted Events
142
-
143
- The package automatically emits six types of events for each database change:
144
-
145
- ### Event Types
146
-
147
- | Event | Description | Example |
148
- | ---------------------------------- | ----------------- | ------------------------------------------ |
149
- | `db:change` | All changes | Any collection change |
150
- | `db:{type}` | By operation type | `db:insert`, `db:update`, `db:delete` |
151
- | `db:change:{collection}` | By collection | `db:change:users`, `db:change:posts` |
152
- | `db:{type}:{collection}` | Type + collection | `db:insert:users`, `db:update:posts` |
153
- | `db:change:{collection}:{id}` | Specific document | `db:change:users:507f1f77bcf86cd799439011` |
154
- | `db:{type}:{collection}:{id}` | Type + document | `db:insert:users:507f1f77bcf86cd799439011` |
155
- | `realtime:{streamId}:{registerId}` | By stream | `realtime:myStreamId:registerId` |
156
-
157
- ### Event listeners
158
-
159
- You can add serverside listeners to those db events to trigger specific actions on the server:
160
-
161
- ```js
162
- function sendNotification(change) {
163
- const userId = change.docId; // or change.documentKey._id
164
- NotificationService.send(userId, "Welcome to DB");
165
- }
166
-
167
- MongoRealtime.listen("db:insert:users", sendNotification);
168
- ```
169
-
170
- #### Adding many callback to one event
171
-
172
- ```js
173
- MongoRealtime.listen("db:insert:users", anotherAction);
174
- MongoRealtime.listen("db:insert:users", anotherAction2);
175
- ```
176
-
177
- #### Removing event listeners
178
-
179
- ```js
180
- MongoRealtime.removeListener("db:insert:users", sendNotification); // remove this specific action from this event
181
- MongoRealtime.removeListener("db:insert:users"); // remove all actions from this event
182
- MongoRealtime.removeAllListeners(); // remove all listeners
183
- ```
184
-
185
- ### Event Payload Structure
186
-
187
- Each event contains the full MongoDB change object:
188
-
189
- ```javascript
190
- {
191
- "_id": {...},
192
- "col":"users", // same as ns.coll
193
- "docId":"...", // same as documentKey._id
194
- "operationType": "insert|update|delete|replace",
195
- "documentKey": { "_id": "..." },
196
- "ns": { "db": "mydb", "coll": "users" },
197
- "fullDocument": {...},
198
- "fullDocumentBeforeChange": {...}
199
- }
200
- ```
201
-
202
- ## 🔨 Usage Examples
203
-
204
- ### Server-side - Listening to specific events
205
-
206
- ```javascript
207
- MongoRealtime.init({
208
- dbUri: "mongodb://localhost:27017/mydb",
209
- server: server,
210
- onSocket: (socket) => {
211
- socket.on("subscribe:users", () => {
212
- socket.join("users-room");
213
- });
214
- },
215
- });
216
-
217
- MongoRealtime.io.to("users-room").emit("custom-event", data);
218
- ```
219
-
220
- ### Client-side - Receiving updates
221
-
222
- ```html
223
- <!DOCTYPE html>
224
- <html>
225
- <head>
226
- <script src="/socket.io/socket.io.js"></script>
227
- </head>
228
- <body>
229
- <script>
230
- const socket = io();
231
-
232
- socket.on("db:change", (change) => {
233
- console.log("Detected change:", change);
234
- });
235
-
236
- socket.on("db:insert:users", (change) => {
237
- console.log("New user:", change.fullDocument);
238
- });
239
-
240
- const userId = "507f1f77bcf86cd799439011";
241
- socket.on(`db:update:users:${userId}`, (change) => {
242
- console.log("Updated user:", change.fullDocument);
243
- });
244
-
245
- socket.on("db:delete", (change) => {
246
- console.log("Deleted document:", change.documentKey);
247
- });
248
- </script>
249
- </body>
250
- </html>
251
- ```
252
-
253
- ## Error Handling
254
-
255
- ```javascript
256
- MongoRealtime.init({
257
- dbUri: "mongodb://localhost:27017/mydb",
258
- server: server,
259
- onSocket: (socket) => {
260
- socket.on("error", (error) => {
261
- console.error("Socket error:", error);
262
- });
263
- },
264
- offSocket: (socket, reason) => {
265
- if (reason === "transport error") {
266
- console.log("Transport error detected");
267
- }
268
- },
269
- });
270
- ```
271
-
272
- ## 🔒 Security
273
-
274
- ### Socket Authentication
275
-
276
- You can provide an `authentify` function in the init options to authenticate socket connections.\
277
- The function receives the token (from `socket.handshake.auth.token` or `socket.handshake.headers.authorization`) and the socket object.\
278
- When setted, it rejects connections based on this logic:
279
-
280
- - Token not provided -> error `NO_TOKEN_PROVIDED`
281
- - Token invalid or returns `false` -> error `UNAUTHORIZED`
282
- - Any other error -> error `AUTH_ERROR`
283
- - Return `true` to accept the connection
284
-
285
- ```javascript
286
- function authenticateSocket(token, socket) {
287
- const verify = AuthService.verifyToken(token);
288
- if (verify) {
289
- socket.user = verify.user; // attach user info to socket
290
- return true; // should return true to accept the connection
291
- }
292
- return false;
293
- }
294
-
295
- MongoRealtime.init({
296
- dbUri: "mongodb://localhost:27017/mydb",
297
- server: server,
298
- authentify: authenticateSocket,
299
- middlewares: [
300
- (socket, next) => {
301
- console.log(`User is authenticated: ${socket.user.email}`);
302
- next();
303
- },
304
- ],
305
- offSocket: (socket, reason) => {
306
- console.log(`Socket ${socket.id} disconnected: ${reason}`);
307
- },
308
- });
309
- ```
310
-
311
- ### Setup streams
312
-
313
- The server will automatically emit a list of filtered documents from the specified collections after each change.\
314
- 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.
315
- Clients receive the list on the event `db:stream:{streamId}`.\
316
-
317
- ```javascript
318
- MongoRealtime.init({
319
- dbUri: "mongodb://localhost:27017/mydb",
320
- server: server,
321
- autoStream: ["users"], // automatically stream users collection only
322
- });
323
-
324
- MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // will throw an error as streamId 'users' already exists
325
-
326
- MongoRealtime.removeStream("users"); // remove the previous stream
327
- MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // client can listen to db:stream:users
328
-
329
- MongoRealtime.addStream("usersWithEmail", "users", (doc) => !!doc.email); // client can listen to db:stream:usersWithEmail
330
- ```
331
-
332
- #### ⚠️ NOTICE
333
-
334
- When `autoStream` is not set, all collections are automatically streamed and WITHOUT any filter.\
335
- That means that if you have a `posts` collection, all documents from this collection will be sent to the clients on each change.\
336
-
337
- ```javascript
338
- MongoRealtime.init({
339
- dbUri: "mongodb://localhost:27017/mydb",
340
- server: server,
341
- autoStream: [], // stream no collection automatically (you can add your own filtered streams later)
342
- });
343
- // or
344
- MongoRealtime.init({
345
- dbUri: "mongodb://localhost:27017/mydb",
346
- server: server,
347
- // Stream all collections automatically but you can override them
348
- }):
349
-
350
- MongoRealtime.addStream("postsWithTitle", "posts", (doc) => !!doc.title); // client can listen to db:stream:posts
351
- MongoRealtime.addStream("users", "users", (doc) => !!doc.email); // will not throw an error cause 'users' is already streamed but with no filter
352
- ```
353
-
354
- #### Usecase for id based streams
355
-
356
- ```javascript
357
- MongoRealtime.init({
358
- dbUri: "mongodb://localhost:27017/mydb",
359
- server: server,
360
- authentify: (token, socket) => {
361
- try {
362
- socket.uid = decodeToken(token).uid; // setup user id from token
363
- return true;
364
- } catch (error) {
365
- return false;
366
- }
367
- },
368
- onSocket: (socket) => {
369
- // setup a personal stream for each connected user
370
- MongoRealtime.addStream(
371
- `userPost:${socket.uid}`,
372
- "posts",
373
- (doc) => doc._id == socket.uid
374
- );
375
- },
376
- offSocket: (socket) => {
377
- // clean up when user disconnects
378
- MongoRealtime.removeStream(`userPost:${socket.uid}`);
379
- },
380
- });
381
-
382
- // ...
383
- // or activate stream from a controller or middleware
384
- app.get("/my-posts", (req, res) => {
385
- const { user } = req;
386
- MongoRealtime.addStream(
387
- `userPosts:${user._id}`,
388
- "posts",
389
- (doc) => doc.authorId === user._id
390
- );
391
-
392
- res.send("Stream activated");
393
- });
394
- ```
395
-
396
- ### Usecase with async filter
397
-
398
- ```javascript
399
- // MongoRealtime.init({...});
400
-
401
- MongoRealtime.addStream("authorizedUsers", "users", async (doc) => {
402
- const isAdmin = await UserService.isAdmin(doc._id);
403
- return isAdmin && doc.email.endsWith("@mydomain.com");
404
- });
405
-
406
- MongoRealtime.addStream(
407
- "bestPosts",
408
- "posts",
409
- async (doc) => doc.likes > (await PostService.getLikesThreshold())
410
- );
411
- ```
412
-
413
- ## 📚 Dependencies
414
-
415
- - `socket.io`: WebSocket management
416
- - `mongoose`: MongoDB ODM with Change Streams support
417
-
418
- ## 🐛 Troubleshooting
419
-
420
- ### MongoDB must be in Replica Set mode
421
-
422
- To use Change Streams, MongoDB must be running as a replica set. For local development, you can initiate a single-node replica set:
423
-
424
- ```bash
425
- mongod --replSet rs0
426
-
427
- rs.initiate()
428
- ```
429
-
430
- For any other issues, open an issue on the GitHub repository.
431
-
432
- ## 📄 License
433
-
434
- MIT
435
-
436
- ## 🤝 Contributing
437
-
438
- Contributions are welcome! Feel free to open an issue or submit a pull request.
1
+ # Mongo Realtime
2
+
3
+ MongoRealTime exposes MongoDB Change Streams over native WebSockets.\
4
+ Clients can subscribe to live updates on query results, perform CRUD operations, and send custom commands to the server.
5
+
6
+ ## 📦 Installation
7
+
8
+ ```bash
9
+ npm install mongo-realtime
10
+ ```
11
+
12
+ ## Quick start
13
+
14
+ ```js
15
+ const { MongoRealTimeServer } = require("mongo-realtime");
16
+
17
+ const server = new MongoRealTimeServer({
18
+ mongoUri: "mongodb://localhost:27017/mydb",
19
+ dbName: "mydb",
20
+ });
21
+
22
+ // Register a handler for custom commands sent with `realtime:emit`.
23
+ server.on("calculate", (payload) => {
24
+ return payload.reduce((sum, value) => sum + value, 0);
25
+ });
26
+
27
+ await server.start();
28
+ ```
29
+
30
+ ## Server configuration
31
+
32
+ The constructor accepts these options:
33
+
34
+ - `host` - Host for the built-in HTTP server (`0.0.0.0` by default).
35
+ - `port` - Port for the built-in HTTP server (`3000` by default).
36
+ - `path` - WebSocket upgrade path (`/` by default).
37
+ - `mongoUri` - MongoDB connection URI.
38
+ - `dbName` - MongoDB database name.
39
+ - `cacheTtlMs` - Query result cache TTL in milliseconds (`300000` by default).
40
+ - `authenticate` - Optional async function to validate incoming socket connections.
41
+ - `server` - Optional existing HTTP server to attach the WebSocket endpoint.
42
+ - `mongoClient` - Optional existing `MongoClient` instance.
43
+ - `db` - Optional existing MongoDB `Db` instance.
44
+ - `logger` - Optional `{ info?, warn? }` logger object.
45
+
46
+ When `server` is omitted, the package creates and owns an HTTP server.
47
+
48
+ ## Environment variables
49
+
50
+ The package can also read configuration from `.env`:
51
+
52
+ - `HOST`
53
+ - `PORT`
54
+ - `WS_PATH`
55
+ - `MONGODB_URI` or `MONGO_URI`
56
+ - `MONGODB_DB_NAME` or `MONGO_DB`
57
+ - `CACHE_TTL_MS`
58
+ - `CACHE_TTL_SECONDS`
59
+
60
+ ## WebSocket protocol
61
+
62
+ ### Supported message types
63
+
64
+ From client to server:
65
+
66
+ - `realtime:subscribe`
67
+ - `realtime:unsubscribe`
68
+ - `realtime:fetch`
69
+ - `realtime:insert`
70
+ - `realtime:update`
71
+ - `realtime:delete`
72
+ - `realtime:emit`
73
+
74
+ ### `realtime:subscribe`
75
+
76
+ Subscribe to live matching documents and receive an initial result set.
77
+
78
+ ```js
79
+ socket.send(
80
+ JSON.stringify({
81
+ type: "realtime:subscribe",
82
+ collection: "users",
83
+ filter: { active: true },
84
+ sort: { createdAt: -1 },
85
+ limit: 50,
86
+ queryId: "my-query-id",
87
+ }),
88
+ );
89
+ ```
90
+
91
+ The server replies with:
92
+
93
+ ```js
94
+ {
95
+ type: 'realtime:initial',
96
+ collection: 'users',
97
+ queryId: 'my-query-id',
98
+ documents: [ ... ],
99
+ }
100
+ ```
101
+
102
+ Live changes are delivered as separate events:
103
+
104
+ - `realtime:insert`
105
+ - `realtime:update`
106
+ - `realtime:delete`
107
+
108
+ ### `realtime:fetch`
109
+
110
+ Fetch the current document set without keeping a live subscription.
111
+
112
+ ```js
113
+ socket.send(
114
+ JSON.stringify({
115
+ type: "realtime:fetch",
116
+ collection: "users",
117
+ filter: { active: true },
118
+ sort: { createdAt: -1 },
119
+ limit: 50,
120
+ queryId: "fetch-1",
121
+ }),
122
+ );
123
+ ```
124
+
125
+ ### `realtime:unsubscribe`
126
+
127
+ Stop a live subscription by its `queryId`:
128
+
129
+ ```js
130
+ socket.send(
131
+ JSON.stringify({
132
+ type: "realtime:unsubscribe",
133
+ queryId: "my-query-id",
134
+ }),
135
+ );
136
+ ```
137
+
138
+ ### `realtime:insert`
139
+
140
+ Insert a new document into a collection:
141
+
142
+ ```js
143
+ socket.send(
144
+ JSON.stringify({
145
+ type: "realtime:insert",
146
+ collection: "users",
147
+ document: { name: "Alice", active: true },
148
+ }),
149
+ );
150
+ ```
151
+
152
+ ### `realtime:update`
153
+
154
+ Update matching documents in a collection:
155
+
156
+ ```js
157
+ socket.send(
158
+ JSON.stringify({
159
+ type: "realtime:update",
160
+ collection: "users",
161
+ filter: { _id: "507f1f77bcf86cd799439011" },
162
+ update: { $set: { active: false } },
163
+ }),
164
+ );
165
+ ```
166
+
167
+ The server accepts both operator-style updates (`$set`, `$inc`, etc.) and replacement-style updates.
168
+
169
+ ### `realtime:delete`
170
+
171
+ Delete matching documents from a collection:
172
+
173
+ ```js
174
+ socket.send(
175
+ JSON.stringify({
176
+ type: "realtime:delete",
177
+ collection: "users",
178
+ filter: { active: false },
179
+ }),
180
+ );
181
+ ```
182
+
183
+ ### `realtime:emit`
184
+
185
+ Send a custom command to the server and receive a response.
186
+
187
+ ```js
188
+ socket.send(
189
+ JSON.stringify({
190
+ type: "realtime:emit",
191
+ event: "calculate",
192
+ payload: [1, 2, 3],
193
+ requestId: "request-1",
194
+ }),
195
+ );
196
+ ```
197
+
198
+ Server responses:
199
+
200
+ ```js
201
+ {
202
+ type: 'realtime:emit:result',
203
+ event: 'calculate',
204
+ requestId: 'request-1',
205
+ data: 6,
206
+ }
207
+ ```
208
+
209
+ or:
210
+
211
+ ```js
212
+ {
213
+ type: 'realtime:emit:error',
214
+ event: 'calculate',
215
+ requestId: 'request-1',
216
+ error: 'No handler registered for event "calculate".',
217
+ }
218
+ ```
219
+
220
+ ### Server internal events
221
+
222
+ The server emit internal events that can be listened inside the backend code using `server.on('MY_INTERNAL_EVENT', handler)`.\
223
+ `MY_INTERNAL_EVENT` follows these patterns:
224
+
225
+ - `db:OPERATION_TYPE` : For any operation type on any collection
226
+ - `db:OPERATION_TYPE:COLLECTION_NAME` : For a specific operation type on a specific collection
227
+ - `db:OPERATION_TYPE:COLLECTION_NAME:DOCUMENT_ID` : For a specific operation type on a specific document
228
+
229
+ `OPERATION_TYPE` can be `insert`, `update`, `delete` or `change` (matches all).\
230
+ `COLLECTION_NAME` is the name of the collection, e.g. `users`.\
231
+ `DOCUMENT_ID` is the string representation of the document's `_id`, e.g. `507f1f77bcf86cd799439011`.
232
+
233
+ Example:
234
+
235
+ ```js
236
+ server.on("db:insert:users", (change) => {
237
+ console.log("A new user was inserted:", change.document);
238
+ });
239
+ server.on("db:update:orders:507f1f77bcf86cd799439011", (change) => {
240
+ console.log(
241
+ "Order 507f1f77bcf86cd799439011 was updated:",
242
+ change.updateDescription,
243
+ );
244
+ });
245
+ ```
246
+
247
+ ### Error responses
248
+
249
+ If a request fails, the server sends:
250
+
251
+ ```js
252
+ {
253
+ type: 'realtime:error',
254
+ error: 'Error message',
255
+ queryId?: 'my-query-id',
256
+ }
257
+ ```
258
+
259
+ ## Message payloads from server
260
+
261
+ Live change messages follow this shape:
262
+
263
+ ```js
264
+ {
265
+ type: 'realtime:insert' | 'realtime:update' | 'realtime:delete',
266
+ collection: 'users',
267
+ document: { ... } | null,
268
+ before?: { ... },
269
+ documentId?: '507f1f77bcf86cd799439011',
270
+ }
271
+ ```
272
+
273
+ ## Query filters
274
+
275
+ Supported filter operators include:
276
+
277
+ - `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`
278
+ - `$in`, `$nin`
279
+ - `$exists`
280
+ - `$regex`
281
+ - `$and`, `$or`, `$nor`
282
+
283
+ Nested document paths are supported. `_id` strings are automatically converted to `ObjectId` when possible.
284
+
285
+ ## API
286
+
287
+ ### `new MongoRealTimeServer(options)`
288
+
289
+ Creates a new server instance.
290
+
291
+ ### `server.start()`
292
+
293
+ Connects to MongoDB, attaches WebSocket handlers, and starts listening if the package owns the HTTP server.
294
+
295
+ ### `server.stop()`
296
+
297
+ Closes active subscriptions, connected sockets, and owned resources.
298
+
299
+ ### `server.on(eventName, handler)`
300
+
301
+ Registers a handler for `realtime:emit` messages.
302
+
303
+ ### `server.collection(name)`
304
+
305
+ Returns a MongoDB collection handle for direct access.
306
+
307
+ ## Authentication
308
+
309
+ Provide an `authenticate` function to validate WebSocket connections. The incoming payload is read from the `auth` request header and parsed as JSON when possible.
310
+
311
+ Example:
312
+
313
+ ```js
314
+ const server = new MongoRealTimeServer({
315
+ authenticate: async (authData) => {
316
+ // Perform your authentication logic here, e.g. check a token or session.
317
+ // In this example, we simply check if the authData matches a hardcoded token.
318
+ return authData === "my-auth-token";
319
+ },
320
+ });
321
+ ```
322
+
323
+ ## Example: attach to Express
324
+
325
+ ```js
326
+ const http = require("node:http");
327
+ const express = require("express");
328
+ const { MongoRealTimeServer } = require("mongo-realtime");
329
+
330
+ const app = express();
331
+ const httpServer = http.createServer(app);
332
+
333
+ const realtimeServer = new MongoRealTimeServer({
334
+ server: httpServer, // needs to be the raw HTTP server, not the Express app
335
+ path: "/", // WebSocket path
336
+ mongoUri: "mongodb://localhost:27017/mydb",
337
+ dbName: "mydb",
338
+ });
339
+
340
+ await new Promise((resolve, reject) => {
341
+ httpServer.once("error", reject);
342
+ httpServer.listen(3000, "0.0.0.0", resolve);
343
+ });
344
+
345
+ await realtimeServer.start(); // start the MongoRealTimeServer after the HTTP server is listening
346
+ ```
347
+
348
+ ## Notes
349
+
350
+ - MongoDB must run as a replica set for Change Streams.
351
+ - The package now uses native WebSockets (`ws`), no longer Socket.IO.
352
+ - Query results are cached for `cacheTtlMs` milliseconds when using `subscribe` or `fetch`.
353
+
354
+ ## Dependencies
355
+
356
+ - `mongodb`
357
+ - `ws`
358
+ - `dotenv`
359
+
360
+ ## License
361
+
362
+ MIT