|
6 | 6 | Session,
|
7 | 7 | } from "socket.io-adapter";
|
8 | 8 | import { randomBytes } from "crypto";
|
9 |
| -import { ObjectId } from "mongodb"; |
10 |
| -import type { Collection } from "mongodb"; |
| 9 | +import { ObjectId, MongoServerError } from "mongodb"; |
| 10 | +import type { Collection, ChangeStream, ResumeToken } from "mongodb"; |
11 | 11 |
|
12 | 12 | const randomId = () => randomBytes(8).toString("hex");
|
13 | 13 | const debug = require("debug")("socket.io-mongo-adapter");
|
@@ -149,39 +149,52 @@ const replaceBinaryObjectsByBuffers = (obj: any) => {
|
149 | 149 | * @public
|
150 | 150 | */
|
151 | 151 | export function createAdapter(
|
152 |
| - mongoCollection: any, |
| 152 | + mongoCollection: Collection, |
153 | 153 | opts: Partial<MongoAdapterOptions> = {}
|
154 | 154 | ) {
|
155 | 155 | opts.uid = opts.uid || randomId();
|
156 | 156 |
|
157 | 157 | let isClosed = false;
|
158 | 158 | let adapters = new Map<string, MongoAdapter>();
|
159 |
| - let changeStream: any; |
| 159 | + let changeStream: ChangeStream; |
| 160 | + let resumeToken: ResumeToken; |
160 | 161 |
|
161 | 162 | const initChangeStream = () => {
|
162 |
| - if (isClosed) { |
| 163 | + if (isClosed || (changeStream && !changeStream.closed)) { |
163 | 164 | return;
|
164 | 165 | }
|
165 |
| - if (changeStream) { |
166 |
| - changeStream.removeAllListeners("change"); |
167 |
| - changeStream.removeAllListeners("close"); |
168 |
| - } |
169 |
| - changeStream = mongoCollection.watch([ |
170 |
| - { |
171 |
| - $match: { |
172 |
| - "fullDocument.uid": { |
173 |
| - $ne: opts.uid, // ignore events from self |
| 166 | + debug("opening change stream"); |
| 167 | + changeStream = mongoCollection.watch( |
| 168 | + [ |
| 169 | + { |
| 170 | + $match: { |
| 171 | + "fullDocument.uid": { |
| 172 | + $ne: opts.uid, // ignore events from self |
| 173 | + }, |
174 | 174 | },
|
175 | 175 | },
|
176 |
| - }, |
177 |
| - ]); |
| 176 | + ], |
| 177 | + { |
| 178 | + resumeAfter: resumeToken, |
| 179 | + } |
| 180 | + ); |
178 | 181 |
|
179 | 182 | changeStream.on("change", (event: any) => {
|
180 |
| - adapters.get(event.fullDocument?.nsp)?.onEvent(event); |
| 183 | + if (event.operationType === "insert") { |
| 184 | + resumeToken = changeStream.resumeToken; |
| 185 | + adapters.get(event.fullDocument?.nsp)?.onEvent(event); |
| 186 | + } |
181 | 187 | });
|
182 | 188 |
|
183 | 189 | changeStream.on("error", (err: Error) => {
|
184 | 190 | debug("change stream encountered an error: %s", err.message);
|
| 191 | + if ( |
| 192 | + err instanceof MongoServerError && |
| 193 | + !err.hasErrorLabel("ResumableChangeStreamError") |
| 194 | + ) { |
| 195 | + // the resume token was not found in the oplog |
| 196 | + resumeToken = null; |
| 197 | + } |
185 | 198 | });
|
186 | 199 |
|
187 | 200 | changeStream.on("close", () => {
|
@@ -210,6 +223,7 @@ export function createAdapter(
|
210 | 223 | if (adapters.size === 0) {
|
211 | 224 | changeStream.removeAllListeners("close");
|
212 | 225 | changeStream.close();
|
| 226 | + // @ts-ignore |
213 | 227 | changeStream = null;
|
214 | 228 | isClosed = true;
|
215 | 229 | }
|
@@ -246,7 +260,7 @@ export class MongoAdapter extends Adapter {
|
246 | 260 | */
|
247 | 261 | constructor(
|
248 | 262 | nsp: any,
|
249 |
| - mongoCollection: any, |
| 263 | + mongoCollection: Collection, |
250 | 264 | opts: Partial<MongoAdapterOptions> = {}
|
251 | 265 | ) {
|
252 | 266 | super(nsp);
|
@@ -456,11 +470,11 @@ export class MongoAdapter extends Adapter {
|
456 | 470 | }
|
457 | 471 |
|
458 | 472 | private scheduleHeartbeat() {
|
459 |
| - debug("schedule heartbeat in %d ms", this.heartbeatInterval); |
460 | 473 | if (this.heartbeatTimer) {
|
461 | 474 | clearTimeout(this.heartbeatTimer);
|
462 | 475 | }
|
463 | 476 | this.heartbeatTimer = setTimeout(() => {
|
| 477 | + debug("sending heartbeat"); |
464 | 478 | this.publish({
|
465 | 479 | type: EventType.HEARTBEAT,
|
466 | 480 | });
|
|
0 commit comments