diff --git a/api-report/driver-base.api.md b/api-report/driver-base.api.md index b3fad25e664f..0585ef0c78cb 100644 --- a/api-report/driver-base.api.md +++ b/api-report/driver-base.api.md @@ -23,7 +23,7 @@ import { TypedEventEmitter } from '@fluidframework/common-utils'; // @public export class DocumentDeltaConnection extends TypedEventEmitter implements IDocumentDeltaConnection, IDisposable { - protected constructor(socket: SocketIOClient.Socket, documentId: string, logger: ITelemetryLogger); + protected constructor(socket: SocketIOClient.Socket, documentId: string, logger: ITelemetryLogger, enableLongPollingDowngrades?: boolean); // (undocumented) protected addTrackedListener(event: string, listener: (...args: any[]) => void): void; checkpointSequenceNumber: number | undefined; diff --git a/packages/drivers/driver-base/src/documentDeltaConnection.ts b/packages/drivers/driver-base/src/documentDeltaConnection.ts index d3ff2535a388..96342ffffc49 100644 --- a/packages/drivers/driver-base/src/documentDeltaConnection.ts +++ b/packages/drivers/driver-base/src/documentDeltaConnection.ts @@ -71,6 +71,8 @@ export class DocumentDeltaConnection private _details: IConnected | undefined; + private reconnectAttempts: number = 0; + // Listeners only needed while the connection is in progress private readonly connectionListeners: Map void> = new Map(); // Listeners used throughout the lifetime of the DocumentDeltaConnection @@ -109,11 +111,13 @@ export class DocumentDeltaConnection * @param socket - websocket to be used * @param documentId - ID of the document * @param logger - for reporting telemetry events + * @param enableLongPollingDowngrades - allow connection to be downgraded to long-polling on websocket failure */ protected constructor( protected readonly socket: SocketIOClient.Socket, public documentId: string, logger: ITelemetryLogger, + private readonly enableLongPollingDowngrades: boolean = false, ) { super(); @@ -362,16 +366,46 @@ export class DocumentDeltaConnection // Listen for connection issues this.addConnectionListener("connect_error", (error) => { + let isWebSocketTransportError = false; try { const description = error?.description; if (description && typeof description === "object") { + if (error.type === "TransportError") { + isWebSocketTransportError = true; + } // That's a WebSocket. Clear it as we can't log it. description.target = undefined; } } catch(_e) {} + + // Handle socket transport downgrading. + if (isWebSocketTransportError && + this.enableLongPollingDowngrades && + this.socket.io.opts.transports?.[0] !== "polling") { + // Downgrade transports to polling upgrade mechanism. + this.socket.io.opts.transports = ["polling", "websocket"]; + // Don't alter reconnection behavior if already enabled. + if (!this.socket.io.reconnection()) { + // Allow single reconnection attempt using polling upgrade mechanism. + this.socket.io.reconnection(true); + this.socket.io.reconnectionAttempts(1); + } + } + + // Allow built-in socket.io reconnection handling. + if (this.socket.io.reconnection() && + this.reconnectAttempts < this.socket.io.reconnectionAttempts()) { + // Reconnection is enabled and maximum reconnect attempts have not been reached. + return; + } + fail(true, this.createErrorObject("connectError", error)); }); + this.addConnectionListener("reconnect_attempt", () => { + this.reconnectAttempts++; + }); + // Listen for timeouts this.addConnectionListener("connect_timeout", () => { fail(true, this.createErrorObject("connectTimeout")); diff --git a/packages/drivers/routerlicious-driver/src/documentDeltaConnection.ts b/packages/drivers/routerlicious-driver/src/documentDeltaConnection.ts index 9dbe787b7d99..2dc45ee61cfc 100644 --- a/packages/drivers/routerlicious-driver/src/documentDeltaConnection.ts +++ b/packages/drivers/routerlicious-driver/src/documentDeltaConnection.ts @@ -33,6 +33,7 @@ export class R11sDocumentDeltaConnection extends DocumentDeltaConnection tenantId, }, reconnection: false, + // Default to websocket connection, with long-polling disabled transports: ["websocket"], timeout: timeoutMs, }); @@ -46,7 +47,9 @@ export class R11sDocumentDeltaConnection extends DocumentDeltaConnection versions: protocolVersions, }; - const deltaConnection = new R11sDocumentDeltaConnection(socket, id, logger); + // TODO: expose to host at factory level + const enableLongPollingDowngrades = true; + const deltaConnection = new R11sDocumentDeltaConnection(socket, id, logger, enableLongPollingDowngrades); await deltaConnection.initialize(connectMessage, timeoutMs); return deltaConnection; diff --git a/server/routerlicious/packages/services-shared/src/socketIoServer.ts b/server/routerlicious/packages/services-shared/src/socketIoServer.ts index cb8e3bf3b2f7..fa127135eafc 100644 --- a/server/routerlicious/packages/services-shared/src/socketIoServer.ts +++ b/server/routerlicious/packages/services-shared/src/socketIoServer.ts @@ -117,12 +117,12 @@ export function create( // Create and register a socket.io connection on the server const io = new Server(server, { - // enable compatibility with socket.io v2 clients + // Enable compatibility with socket.io v2 clients allowEIO3: true, // Indicates whether a connection should use compression perMessageDeflate: true, - // ensure long polling is never used - transports: [ "websocket" ], + // Enable long-polling as a fallback + transports: ["websocket", "polling"], cors: { // Explicitly allow all origins by reflecting request origin. // As a service that has potential to host countless different client apps,