@fluidframework/driver-base 2.0.0-internal.3.0.5 → 2.0.0-internal.3.1.1

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.
@@ -5,32 +5,31 @@
5
5
 
6
6
  import { assert } from "@fluidframework/common-utils";
7
7
  import {
8
- IAnyDriverError,
9
- IDocumentDeltaConnection,
10
- IDocumentDeltaConnectionEvents,
8
+ IAnyDriverError,
9
+ IDocumentDeltaConnection,
10
+ IDocumentDeltaConnectionEvents,
11
11
  } from "@fluidframework/driver-definitions";
12
12
  import { createGenericNetworkError } from "@fluidframework/driver-utils";
13
13
  import {
14
- ConnectionMode,
15
- IClientConfiguration,
16
- IConnect,
17
- IConnected,
18
- IDocumentMessage,
19
- ISequencedDocumentMessage,
20
- ISignalClient,
21
- ISignalMessage,
22
- ITokenClaims,
23
- ScopeType,
14
+ ConnectionMode,
15
+ IClientConfiguration,
16
+ IConnect,
17
+ IConnected,
18
+ IDocumentMessage,
19
+ ISequencedDocumentMessage,
20
+ ISignalClient,
21
+ ISignalMessage,
22
+ ITokenClaims,
23
+ ScopeType,
24
24
  } from "@fluidframework/protocol-definitions";
25
25
  import { IDisposable, ITelemetryLogger } from "@fluidframework/common-definitions";
26
26
  import {
27
- ChildLogger,
28
- extractLogSafeErrorProperties,
29
- getCircularReplacer,
30
- loggerToMonitoringContext,
31
- MonitoringContext,
32
- EventEmitterWithErrorHandling,
33
- normalizeError,
27
+ ChildLogger,
28
+ extractLogSafeErrorProperties,
29
+ getCircularReplacer,
30
+ loggerToMonitoringContext,
31
+ MonitoringContext,
32
+ EventEmitterWithErrorHandling,
34
33
  } from "@fluidframework/telemetry-utils";
35
34
  import type { Socket } from "socket.io-client";
36
35
  // For now, this package is versioned and released in unison with the specific drivers
@@ -40,306 +39,285 @@ import { pkgVersion as driverVersion } from "./packageVersion";
40
39
  * Represents a connection to a stream of delta updates
41
40
  */
42
41
  export class DocumentDeltaConnection
43
- extends EventEmitterWithErrorHandling<IDocumentDeltaConnectionEvents>
44
- implements IDocumentDeltaConnection, IDisposable {
45
- static readonly eventsToForward = ["nack", "op", "signal", "pong"];
46
-
47
- // WARNING: These are critical events that we can't miss, so registration for them has to be in place at all times!
48
- // Including before handshake is over, and after that (but before DeltaManager had a chance to put its own handlers)
49
- static readonly eventsAlwaysForwarded = ["disconnect", "error"];
50
-
51
- /**
52
- * Last known sequence number to ordering service at the time of connection
53
- * It may lap actual last sequence number (quite a bit, if container is very active).
54
- * But it's best information for client to figure out how far it is behind, at least
55
- * for "read" connections. "write" connections may use own "join" op to similar information,
56
- * that is likely to be more up-to-date.
57
- */
58
- public checkpointSequenceNumber: number | undefined;
59
-
60
- // Listen for ops sent before we receive a response to connect_document
61
- protected readonly queuedMessages: ISequencedDocumentMessage[] = [];
62
- protected readonly queuedSignals: ISignalMessage[] = [];
63
-
64
- /**
65
- * A flag to indicate whether we have our handler attached. If it's attached, we're queueing incoming ops
66
- * to later be retrieved via initialMessages.
67
- */
68
- private earlyOpHandlerAttached: boolean = false;
69
-
70
- private socketConnectionTimeout: ReturnType<typeof setTimeout> | undefined;
71
-
72
- private _details: IConnected | undefined;
73
-
74
- // Listeners only needed while the connection is in progress
75
- private readonly connectionListeners: Map<string, (...args: any[]) => void> = new Map();
76
- // Listeners used throughout the lifetime of the DocumentDeltaConnection
77
- private readonly trackedListeners: Map<string, (...args: any[]) => void> = new Map();
78
-
79
- protected get hasDetails(): boolean {
80
- return !!this._details;
81
- }
82
-
83
- public get disposed() {
84
- // Increase the stack trace limit temporarily, so as to debug better in case it occurs.
85
- // We are seeing this in telemetry and we are unable to figure out why it is happening, so this should help.
86
- const originalStackTraceLimit = (Error as any).stackTraceLimit;
87
- try {
88
- (Error as any).stackTraceLimit = 50;
42
+ extends EventEmitterWithErrorHandling<IDocumentDeltaConnectionEvents>
43
+ implements IDocumentDeltaConnection, IDisposable
44
+ {
45
+ static readonly eventsToForward = ["nack", "op", "signal", "pong"];
46
+
47
+ // WARNING: These are critical events that we can't miss, so registration for them has to be in place at all times!
48
+ // Including before handshake is over, and after that (but before DeltaManager had a chance to put its own handlers)
49
+ static readonly eventsAlwaysForwarded = ["disconnect", "error"];
50
+
51
+ /**
52
+ * Last known sequence number to ordering service at the time of connection
53
+ * It may lap actual last sequence number (quite a bit, if container is very active).
54
+ * But it's best information for client to figure out how far it is behind, at least
55
+ * for "read" connections. "write" connections may use own "join" op to similar information,
56
+ * that is likely to be more up-to-date.
57
+ */
58
+ public checkpointSequenceNumber: number | undefined;
59
+
60
+ // Listen for ops sent before we receive a response to connect_document
61
+ protected readonly queuedMessages: ISequencedDocumentMessage[] = [];
62
+ protected readonly queuedSignals: ISignalMessage[] = [];
63
+
64
+ /**
65
+ * A flag to indicate whether we have our handler attached. If it's attached, we're queueing incoming ops
66
+ * to later be retrieved via initialMessages.
67
+ */
68
+ private earlyOpHandlerAttached: boolean = false;
69
+
70
+ private socketConnectionTimeout: ReturnType<typeof setTimeout> | undefined;
71
+
72
+ private _details: IConnected | undefined;
73
+
74
+ // Listeners only needed while the connection is in progress
75
+ private readonly connectionListeners: Map<string, (...args: any[]) => void> = new Map();
76
+ // Listeners used throughout the lifetime of the DocumentDeltaConnection
77
+ private readonly trackedListeners: Map<string, (...args: any[]) => void> = new Map();
78
+
79
+ protected get hasDetails(): boolean {
80
+ return !!this._details;
81
+ }
82
+
83
+ public get disposed() {
84
+ assert(
85
+ this._disposed || this.socket.connected,
86
+ 0x244 /* "Socket is closed, but connection is not!" */,
87
+ );
88
+ return this._disposed;
89
+ }
90
+
91
+ /**
92
+ * Flag to indicate whether the DocumentDeltaConnection is expected to still be capable of sending messages.
93
+ * After disconnection, we flip this to prevent any stale messages from being emitted.
94
+ */
95
+ protected _disposed: boolean = false;
96
+ private readonly mc: MonitoringContext;
97
+
98
+ /**
99
+ * @deprecated Implementors should manage their own logger or monitoring context
100
+ */
101
+ protected get logger(): ITelemetryLogger {
102
+ return this.mc.logger;
103
+ }
104
+
105
+ public get details(): IConnected {
106
+ if (!this._details) {
107
+ throw new Error("Internal error: calling method before _details is initialized!");
108
+ }
109
+ return this._details;
110
+ }
111
+
112
+ /**
113
+ * @param socket - websocket to be used
114
+ * @param documentId - ID of the document
115
+ * @param logger - for reporting telemetry events
116
+ * @param enableLongPollingDowngrades - allow connection to be downgraded to long-polling on websocket failure
117
+ */
118
+ protected constructor(
119
+ protected readonly socket: Socket,
120
+ public documentId: string,
121
+ logger: ITelemetryLogger,
122
+ private readonly enableLongPollingDowngrades: boolean = false,
123
+ ) {
124
+ super((name, error) => {
125
+ logger.sendErrorEvent(
126
+ {
127
+ eventName: "DeltaConnection:EventException",
128
+ name,
129
+ },
130
+ error,
131
+ );
132
+ });
133
+
134
+ this.mc = loggerToMonitoringContext(ChildLogger.create(logger, "DeltaConnection"));
135
+
136
+ this.on("newListener", (event, listener) => {
137
+ assert(!this.disposed, 0x20a /* "register for event on disposed object" */);
138
+
139
+ // Some events are already forwarded - see this.addTrackedListener() calls in initialize().
140
+ if (DocumentDeltaConnection.eventsAlwaysForwarded.includes(event)) {
141
+ assert(this.trackedListeners.has(event), 0x245 /* "tracked listener" */);
142
+ return;
143
+ }
144
+
145
+ if (!DocumentDeltaConnection.eventsToForward.includes(event)) {
146
+ throw new Error(`DocumentDeltaConnection: Registering for unknown event: ${event}`);
147
+ }
148
+
149
+ // Whenever listener is added, we should subscribe on same event on socket, so these two things
150
+ // should be in sync. This currently assumes that nobody unregisters and registers back listeners,
151
+ // and that there are no "internal" listeners installed (like "error" case we skip above)
152
+ // Better flow might be to always unconditionally register all handlers on successful connection,
153
+ // though some logic (naming assert in initialMessages getter) might need to be adjusted (it becomes noop)
89
154
  assert(
90
- this._disposed || this.socket.connected,
91
- 0x244 /* "Socket is closed, but connection is not!" */,
155
+ (this.listeners(event).length !== 0) === this.trackedListeners.has(event),
156
+ 0x20b /* "mismatch" */,
92
157
  );
93
- } catch (error) {
94
- const normalizedError = this.addPropsToError(error);
95
- throw normalizedError;
96
- } finally {
97
- (Error as any).stackTraceLimit = originalStackTraceLimit;
158
+ if (!this.trackedListeners.has(event)) {
159
+ this.addTrackedListener(event, (...args: any[]) => {
160
+ this.emit(event, ...args);
161
+ });
162
+ }
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Get the ID of the client who is sending the message
168
+ *
169
+ * @returns the client ID
170
+ */
171
+ public get clientId(): string {
172
+ return this.details.clientId;
173
+ }
174
+
175
+ /**
176
+ * Get the mode of the client
177
+ *
178
+ * @returns the client mode
179
+ */
180
+ public get mode(): ConnectionMode {
181
+ return this.details.mode;
182
+ }
183
+
184
+ /**
185
+ * Get the claims of the client who is sending the message
186
+ *
187
+ * @returns client claims
188
+ */
189
+ public get claims(): ITokenClaims {
190
+ return this.details.claims;
191
+ }
192
+
193
+ /**
194
+ * Get whether or not this is an existing document
195
+ *
196
+ * @returns true if the document exists
197
+ */
198
+ public get existing(): boolean {
199
+ return this.details.existing;
200
+ }
201
+
202
+ /**
203
+ * Get the maximum size of a message before chunking is required
204
+ *
205
+ * @returns the maximum size of a message before chunking is required
206
+ */
207
+ public get maxMessageSize(): number {
208
+ return this.details.serviceConfiguration.maxMessageSize;
209
+ }
210
+
211
+ /**
212
+ * Semver of protocol being used with the service
213
+ */
214
+ public get version(): string {
215
+ return this.details.version;
216
+ }
217
+
218
+ /**
219
+ * Configuration details provided by the service
220
+ */
221
+ public get serviceConfiguration(): IClientConfiguration {
222
+ return this.details.serviceConfiguration;
223
+ }
224
+
225
+ private checkNotClosed() {
226
+ assert(!this.disposed, 0x20c /* "connection disposed" */);
227
+ }
228
+
229
+ /**
230
+ * Get messages sent during the connection
231
+ *
232
+ * @returns messages sent during the connection
233
+ */
234
+ public get initialMessages(): ISequencedDocumentMessage[] {
235
+ this.checkNotClosed();
236
+
237
+ // If we call this when the earlyOpHandler is not attached, then the queuedMessages may not include the
238
+ // latest ops. This could possibly indicate that initialMessages was called twice.
239
+ assert(this.earlyOpHandlerAttached, 0x08e /* "Potentially missed initial messages" */);
240
+ // We will lose ops and perf will tank as we need to go to storage to become current!
241
+ assert(this.listeners("op").length !== 0, 0x08f /* "No op handler is setup!" */);
242
+
243
+ this.removeEarlyOpHandler();
244
+
245
+ if (this.queuedMessages.length > 0) {
246
+ // Some messages were queued.
247
+ // add them to the list of initialMessages to be processed
248
+ this.details.initialMessages.push(...this.queuedMessages);
249
+ this.details.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
250
+ this.queuedMessages.length = 0;
98
251
  }
99
- return this._disposed;
100
- }
101
-
102
- /**
103
- * Flag to indicate whether the DocumentDeltaConnection is expected to still be capable of sending messages.
104
- * After disconnection, we flip this to prevent any stale messages from being emitted.
105
- */
106
- protected _disposed: boolean = false;
107
- private readonly mc: MonitoringContext;
108
-
109
- /**
110
- * @deprecated Implementors should manage their own logger or monitoring context
111
- */
112
- protected get logger(): ITelemetryLogger {
113
- return this.mc.logger;
114
- }
115
-
116
- public get details(): IConnected {
117
- if (!this._details) {
118
- throw new Error("Internal error: calling method before _details is initialized!");
119
- }
120
- return this._details;
121
- }
122
-
123
- /**
124
- * @param socket - websocket to be used
125
- * @param documentId - ID of the document
126
- * @param logger - for reporting telemetry events
127
- * @param enableLongPollingDowngrades - allow connection to be downgraded to long-polling on websocket failure
128
- */
129
- protected constructor(
130
- protected readonly socket: Socket,
131
- public documentId: string,
132
- logger: ITelemetryLogger,
133
- private readonly enableLongPollingDowngrades: boolean = false,
134
- protected readonly connectionId?: string,
135
- ) {
136
- super((name, error) => {
137
- logger.sendErrorEvent(
138
- {
139
- eventName: "DeltaConnection:EventException",
140
- name,
141
- },
142
- error);
143
- });
144
-
145
- this.mc = loggerToMonitoringContext(
146
- ChildLogger.create(logger, "DeltaConnection"));
147
-
148
- this.on("newListener", (event, listener) => {
149
- assert(!this.disposed, 0x20a /* "register for event on disposed object" */);
150
-
151
- // Some events are already forwarded - see this.addTrackedListener() calls in initialize().
152
- if (DocumentDeltaConnection.eventsAlwaysForwarded.includes(event)) {
153
- assert(this.trackedListeners.has(event), 0x245 /* "tracked listener" */);
154
- return;
155
- }
156
-
157
- if (!DocumentDeltaConnection.eventsToForward.includes(event)) {
158
- throw new Error(`DocumentDeltaConnection: Registering for unknown event: ${event}`);
159
- }
160
-
161
- // Whenever listener is added, we should subscribe on same event on socket, so these two things
162
- // should be in sync. This currently assumes that nobody unregisters and registers back listeners,
163
- // and that there are no "internal" listeners installed (like "error" case we skip above)
164
- // Better flow might be to always unconditionally register all handlers on successful connection,
165
- // though some logic (naming assert in initialMessages getter) might need to be adjusted (it becomes noop)
166
- assert((this.listeners(event).length !== 0) === this.trackedListeners.has(event), 0x20b /* "mismatch" */);
167
- if (!this.trackedListeners.has(event)) {
168
- this.addTrackedListener(
169
- event,
170
- (...args: any[]) => {
171
- this.emit(event, ...args);
172
- });
173
- }
174
- });
175
- }
176
-
177
- /**
178
- * Get the ID of the client who is sending the message
179
- *
180
- * @returns the client ID
181
- */
182
- public get clientId(): string {
183
- return this.details.clientId;
184
- }
185
-
186
- /**
187
- * Get the mode of the client
188
- *
189
- * @returns the client mode
190
- */
191
- public get mode(): ConnectionMode {
192
- return this.details.mode;
193
- }
194
-
195
- /**
196
- * Get the claims of the client who is sending the message
197
- *
198
- * @returns client claims
199
- */
200
- public get claims(): ITokenClaims {
201
- return this.details.claims;
202
- }
203
-
204
- /**
205
- * Get whether or not this is an existing document
206
- *
207
- * @returns true if the document exists
208
- */
209
- public get existing(): boolean {
210
- return this.details.existing;
211
- }
212
-
213
- /**
214
- * Get the maximum size of a message before chunking is required
215
- *
216
- * @returns the maximum size of a message before chunking is required
217
- */
218
- public get maxMessageSize(): number {
219
- return this.details.serviceConfiguration.maxMessageSize;
220
- }
221
-
222
- /**
223
- * Semver of protocol being used with the service
224
- */
225
- public get version(): string {
226
- return this.details.version;
227
- }
228
-
229
- /**
230
- * Configuration details provided by the service
231
- */
232
- public get serviceConfiguration(): IClientConfiguration {
233
- return this.details.serviceConfiguration;
234
- }
235
-
236
- private checkNotClosed() {
237
- // Increase the stack trace limit temporarily, so as to debug better in case it occurs.
238
- // We are seeing this in telemetry and we are unable to figure out why it is happening, so this should help.
239
- const originalStackTraceLimit = (Error as any).stackTraceLimit;
240
- try {
241
- (Error as any).stackTraceLimit = 50;
242
- assert(!this.disposed, 0x20c /* "connection disposed" */);
243
- } catch (error) {
244
- const normalizedError = this.addPropsToError(error);
245
- throw normalizedError;
246
- } finally {
247
- (Error as any).stackTraceLimit = originalStackTraceLimit;
252
+ return this.details.initialMessages;
253
+ }
254
+
255
+ /**
256
+ * Get signals sent during the connection
257
+ *
258
+ * @returns signals sent during the connection
259
+ */
260
+ public get initialSignals(): ISignalMessage[] {
261
+ this.checkNotClosed();
262
+ assert(this.listeners("signal").length !== 0, 0x090 /* "No signal handler is setup!" */);
263
+
264
+ this.removeEarlySignalHandler();
265
+
266
+ if (this.queuedSignals.length > 0) {
267
+ // Some signals were queued.
268
+ // add them to the list of initialSignals to be processed
269
+ this.details.initialSignals.push(...this.queuedSignals);
270
+ this.queuedSignals.length = 0;
248
271
  }
249
- }
250
-
251
- /**
252
- * Get messages sent during the connection
253
- *
254
- * @returns messages sent during the connection
255
- */
256
- public get initialMessages(): ISequencedDocumentMessage[] {
257
- this.checkNotClosed();
258
-
259
- // If we call this when the earlyOpHandler is not attached, then the queuedMessages may not include the
260
- // latest ops. This could possibly indicate that initialMessages was called twice.
261
- assert(this.earlyOpHandlerAttached, 0x08e /* "Potentially missed initial messages" */);
262
- // We will lose ops and perf will tank as we need to go to storage to become current!
263
- assert(this.listeners("op").length !== 0, 0x08f /* "No op handler is setup!" */);
264
-
265
- this.removeEarlyOpHandler();
266
-
267
- if (this.queuedMessages.length > 0) {
268
- // Some messages were queued.
269
- // add them to the list of initialMessages to be processed
270
- this.details.initialMessages.push(...this.queuedMessages);
271
- this.details.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
272
- this.queuedMessages.length = 0;
273
- }
274
- return this.details.initialMessages;
275
- }
276
-
277
- /**
278
- * Get signals sent during the connection
279
- *
280
- * @returns signals sent during the connection
281
- */
282
- public get initialSignals(): ISignalMessage[] {
283
- this.checkNotClosed();
284
- assert(this.listeners("signal").length !== 0, 0x090 /* "No signal handler is setup!" */);
285
-
286
- this.removeEarlySignalHandler();
287
-
288
- if (this.queuedSignals.length > 0) {
289
- // Some signals were queued.
290
- // add them to the list of initialSignals to be processed
291
- this.details.initialSignals.push(...this.queuedSignals);
292
- this.queuedSignals.length = 0;
293
- }
294
- return this.details.initialSignals;
295
- }
296
-
297
- /**
298
- * Get initial client list
299
- *
300
- * @returns initial client list sent during the connection
301
- */
302
- public get initialClients(): ISignalClient[] {
303
- this.checkNotClosed();
304
- return this.details.initialClients;
305
- }
306
-
307
- protected emitMessages(type: string, messages: IDocumentMessage[][]) {
308
- // Although the implementation here disconnects the socket and does not reuse it, other subclasses
309
- // (e.g. OdspDocumentDeltaConnection) may reuse the socket. In these cases, we need to avoid emitting
310
- // on the still-live socket.
311
- if (!this.disposed) {
312
- this.socket.emit(type, this.clientId, messages);
313
- }
314
- }
315
-
316
- protected submitCore(type: string, messages: IDocumentMessage[]) {
317
- this.emitMessages(type, [messages]);
318
- }
319
-
320
- /**
321
- * Submits a new delta operation to the server
322
- *
323
- * @param message - delta operation to submit
324
- */
325
- public submit(messages: IDocumentMessage[]): void {
326
- this.checkNotClosed();
327
- this.submitCore("submitOp", messages);
328
- }
329
-
330
- /**
331
- * Submits a new signal to the server
332
- *
333
- * @param message - signal to submit
334
- */
335
- public submitSignal(message: IDocumentMessage): void {
336
- this.checkNotClosed();
337
- this.submitCore("submitSignal", [message]);
338
- }
339
-
340
- /**
341
- * Disconnect from the websocket and close the websocket too.
342
- */
272
+ return this.details.initialSignals;
273
+ }
274
+
275
+ /**
276
+ * Get initial client list
277
+ *
278
+ * @returns initial client list sent during the connection
279
+ */
280
+ public get initialClients(): ISignalClient[] {
281
+ this.checkNotClosed();
282
+ return this.details.initialClients;
283
+ }
284
+
285
+ protected emitMessages(type: string, messages: IDocumentMessage[][]) {
286
+ // Although the implementation here disconnects the socket and does not reuse it, other subclasses
287
+ // (e.g. OdspDocumentDeltaConnection) may reuse the socket. In these cases, we need to avoid emitting
288
+ // on the still-live socket.
289
+ if (!this.disposed) {
290
+ this.socket.emit(type, this.clientId, messages);
291
+ }
292
+ }
293
+
294
+ protected submitCore(type: string, messages: IDocumentMessage[]) {
295
+ this.emitMessages(type, [messages]);
296
+ }
297
+
298
+ /**
299
+ * Submits a new delta operation to the server
300
+ *
301
+ * @param message - delta operation to submit
302
+ */
303
+ public submit(messages: IDocumentMessage[]): void {
304
+ this.checkNotClosed();
305
+ this.submitCore("submitOp", messages);
306
+ }
307
+
308
+ /**
309
+ * Submits a new signal to the server
310
+ *
311
+ * @param message - signal to submit
312
+ */
313
+ public submitSignal(message: IDocumentMessage): void {
314
+ this.checkNotClosed();
315
+ this.submitCore("submitSignal", [message]);
316
+ }
317
+
318
+ /**
319
+ * Disconnect from the websocket and close the websocket too.
320
+ */
343
321
  private closeSocket(error: IAnyDriverError) {
344
322
  if (this._disposed) {
345
323
  // This would be rare situation due to complexity around socket emitting events.
@@ -351,8 +329,6 @@ export class DocumentDeltaConnection
351
329
  disposed: this._disposed,
352
330
  socketConnected: this.socket?.connected,
353
331
  trackedListenerCount: this.trackedListeners.size,
354
- clientId: this._details?.clientId,
355
- connectionId: this.connectionId,
356
332
  }),
357
333
  },
358
334
  error,
@@ -366,357 +342,341 @@ export class DocumentDeltaConnection
366
342
  this.disconnect(error);
367
343
  }
368
344
 
369
- /**
370
- * Disconnect from the websocket, and permanently disable this DocumentDeltaConnection and close the socket.
371
- * However the OdspDocumentDeltaConnection differ in dispose as in there we don't close the socket. There is no
372
- * multiplexing here, so we need to close the socket here.
373
- */
374
- public dispose() {
375
- this.logger.sendTelemetryEvent({
376
- eventName: "ClientClosingDeltaConnection",
377
- driverVersion,
378
- details: JSON.stringify(
379
- {
380
- disposed: this._disposed,
381
- socketConnected: this.socket.connected,
382
- clientId: this._details?.clientId,
383
- connectionId: this.connectionId,
384
- }
385
- ),
386
- });
387
- this.disconnect(createGenericNetworkError(
388
- // pre-0.58 error message: clientClosingConnection
389
- "Client closing delta connection", { canRetry: true }, { driverVersion }),
390
- );
391
- }
392
-
393
- protected disconnect(err: IAnyDriverError) {
394
- // Can't check this.disposed here, as we get here on socket closure,
395
- // so _disposed & socket.connected might be not in sync while processing
396
- // "dispose" event.
397
- if (this._disposed) {
398
- return;
399
- }
400
-
401
- // We set the disposed flag as a part of the contract for overriding the disconnect method. This is used by
402
- // DocumentDeltaConnection to determine if emitting messages (ops) on the socket is allowed, which is
403
- // important since OdspDocumentDeltaConnection reuses the socket rather than truly disconnecting it. Note that
404
- // OdspDocumentDeltaConnection may still send disconnect_document which is allowed; this is only intended
405
- // to prevent normal messages from being emitted.
406
- this._disposed = true;
407
-
408
- // Let user of connection object know about disconnect. This has to happen in between setting _disposed and
409
- // removing all listeners!
410
- this.emit("disconnect", err);
411
- this.logger.sendTelemetryEvent({
412
- eventName: "AfterDisconnectEvent",
413
- driverVersion,
414
- details: JSON.stringify({
345
+ /**
346
+ * Disconnect from the websocket, and permanently disable this DocumentDeltaConnection and close the socket.
347
+ * However the OdspDocumentDeltaConnection differ in dispose as in there we don't close the socket. There is no
348
+ * multiplexing here, so we need to close the socket here.
349
+ */
350
+ public dispose() {
351
+ this.logger.sendTelemetryEvent({
352
+ eventName: "ClientClosingDeltaConnection",
353
+ driverVersion,
354
+ details: JSON.stringify({
415
355
  disposed: this._disposed,
416
- clientId: this._details?.clientId,
417
- socketConnected: this.socket.connected,
418
- disconnectListenerCount: this.listenerCount("disconnect"),
419
- connectionId: this.connectionId,
420
- }),
421
- });
422
- // user of DeltaConnection should have processed "disconnect" event and removed all listeners. Not clear
423
- // if we want to enforce that, as some users (like LocalDocumentService) do not unregister any handlers
424
- // assert(this.listenerCount("disconnect") === 0, "'disconnect` events should be processed synchronously");
425
-
426
- this.removeTrackedListeners();
427
- this.disconnectCore();
428
- }
429
-
430
- /**
431
- * Disconnect from the websocket.
432
- * @param reason - reason for disconnect
433
- */
434
- protected disconnectCore() {
435
- this.socket.disconnect();
436
- }
437
-
438
- protected async initialize(connectMessage: IConnect, timeout: number) {
439
- this.socket.on("op", this.earlyOpHandler);
440
- this.socket.on("signal", this.earlySignalHandler);
441
- this.earlyOpHandlerAttached = true;
442
-
443
- // Socket.io's reconnect_attempt event is unreliable, so we track connect_error count instead.
444
- let internalSocketConnectionFailureCount: number = 0;
445
- const isInternalSocketReconnectionEnabled = (): boolean => this.socket.io.reconnection();
446
- const getMaxInternalSocketReconnectionAttempts = (): number => isInternalSocketReconnectionEnabled()
447
- ? this.socket.io.reconnectionAttempts()
448
- : 0;
449
- const getMaxAllowedInternalSocketConnectionFailures = (): number =>
450
- getMaxInternalSocketReconnectionAttempts() + 1;
451
-
452
- this._details = await new Promise<IConnected>((resolve, reject) => {
356
+ socketConnected: this.socket.connected,
357
+ }),
358
+ });
359
+ this.disconnect(
360
+ createGenericNetworkError(
361
+ // pre-0.58 error message: clientClosingConnection
362
+ "Client closing delta connection",
363
+ { canRetry: true },
364
+ { driverVersion },
365
+ ),
366
+ );
367
+ }
368
+
369
+ protected disconnect(err: IAnyDriverError) {
370
+ // Can't check this.disposed here, as we get here on socket closure,
371
+ // so _disposed & socket.connected might be not in sync while processing
372
+ // "dispose" event.
373
+ if (this._disposed) {
374
+ return;
375
+ }
376
+
377
+ // We set the disposed flag as a part of the contract for overriding the disconnect method. This is used by
378
+ // DocumentDeltaConnection to determine if emitting messages (ops) on the socket is allowed, which is
379
+ // important since OdspDocumentDeltaConnection reuses the socket rather than truly disconnecting it. Note that
380
+ // OdspDocumentDeltaConnection may still send disconnect_document which is allowed; this is only intended
381
+ // to prevent normal messages from being emitted.
382
+ this._disposed = true;
383
+
384
+ // Let user of connection object know about disconnect. This has to happen in between setting _disposed and
385
+ // removing all listeners!
386
+ this.emit("disconnect", err);
387
+ this.logger.sendTelemetryEvent({
388
+ eventName: "AfterDisconnectEvent",
389
+ driverVersion,
390
+ details: JSON.stringify({
391
+ socketConnected: this.socket.connected,
392
+ disconnectListenerCount: this.listenerCount("disconnect"),
393
+ }),
394
+ });
395
+ // user of DeltaConnection should have processed "disconnect" event and removed all listeners. Not clear
396
+ // if we want to enforce that, as some users (like LocalDocumentService) do not unregister any handlers
397
+ // assert(this.listenerCount("disconnect") === 0, "'disconnect` events should be processed synchronously");
398
+
399
+ this.removeTrackedListeners();
400
+ this.disconnectCore();
401
+ }
402
+
403
+ /**
404
+ * Disconnect from the websocket.
405
+ * @param reason - reason for disconnect
406
+ */
407
+ protected disconnectCore() {
408
+ this.socket.disconnect();
409
+ }
410
+
411
+ protected async initialize(connectMessage: IConnect, timeout: number) {
412
+ this.socket.on("op", this.earlyOpHandler);
413
+ this.socket.on("signal", this.earlySignalHandler);
414
+ this.earlyOpHandlerAttached = true;
415
+
416
+ // Socket.io's reconnect_attempt event is unreliable, so we track connect_error count instead.
417
+ let internalSocketConnectionFailureCount: number = 0;
418
+ const isInternalSocketReconnectionEnabled = (): boolean => this.socket.io.reconnection();
419
+ const getMaxInternalSocketReconnectionAttempts = (): number =>
420
+ isInternalSocketReconnectionEnabled() ? this.socket.io.reconnectionAttempts() : 0;
421
+ const getMaxAllowedInternalSocketConnectionFailures = (): number =>
422
+ getMaxInternalSocketReconnectionAttempts() + 1;
423
+
424
+ this._details = await new Promise<IConnected>((resolve, reject) => {
453
425
  const failAndCloseSocket = (err: IAnyDriverError) => {
454
- try {
455
- this.closeSocket(err);
456
- } catch (failError) {
457
- const normalizedError = this.addPropsToError(failError);
458
- this.logger.sendErrorEvent({ eventName: "CloseSocketError" }, normalizedError);
459
- }
426
+ this.closeSocket(err);
460
427
  reject(err);
461
428
  };
462
429
 
463
430
  const failConnection = (err: IAnyDriverError) => {
464
- try {
465
- this.disconnect(err);
466
- } catch (failError) {
467
- const normalizedError = this.addPropsToError(failError);
468
- this.logger.sendErrorEvent(
469
- { eventName: "FailConnectionError" },
470
- normalizedError,
471
- );
472
- }
431
+ this.disconnect(err);
473
432
  reject(err);
474
433
  };
434
+ // Listen for connection issues
435
+ this.addConnectionListener("connect_error", (error) => {
436
+ internalSocketConnectionFailureCount++;
437
+ let isWebSocketTransportError = false;
438
+ try {
439
+ const description = error?.description;
440
+ const context = error?.context;
441
+
442
+ if (context && typeof context === "object") {
443
+ const statusText = context.statusText?.code;
444
+
445
+ // Self-Signed Certificate ErrorCode Found in error.context
446
+ if (statusText === "DEPTH_ZERO_SELF_SIGNED_CERT") {
447
+ failAndCloseSocket(
448
+ this.createErrorObject("connect_error", error, false),
449
+ );
450
+ return;
451
+ }
452
+ } else if (description && typeof description === "object") {
453
+ const errorCode = description.error?.code;
454
+
455
+ // Self-Signed Certificate ErrorCode Found in error.description
456
+ if (errorCode === "DEPTH_ZERO_SELF_SIGNED_CERT") {
457
+ failAndCloseSocket(
458
+ this.createErrorObject("connect_error", error, false),
459
+ );
460
+ return;
461
+ }
462
+
463
+ if (error.type === "TransportError") {
464
+ isWebSocketTransportError = true;
465
+ }
466
+
467
+ // That's a WebSocket. Clear it as we can't log it.
468
+ description.target = undefined;
469
+ }
470
+ } catch (_e) {}
471
+
472
+ // Handle socket transport downgrading when not offline.
473
+ if (
474
+ isWebSocketTransportError &&
475
+ this.enableLongPollingDowngrades &&
476
+ this.socket.io.opts.transports?.[0] !== "polling"
477
+ ) {
478
+ // Downgrade transports to polling upgrade mechanism.
479
+ this.socket.io.opts.transports = ["polling", "websocket"];
480
+ // Don't alter reconnection behavior if already enabled.
481
+ if (!isInternalSocketReconnectionEnabled()) {
482
+ // Allow single reconnection attempt using polling upgrade mechanism.
483
+ this.socket.io.reconnection(true);
484
+ this.socket.io.reconnectionAttempts(1);
485
+ }
486
+ }
487
+
488
+ // Allow built-in socket.io reconnection handling.
489
+ if (
490
+ isInternalSocketReconnectionEnabled() &&
491
+ internalSocketConnectionFailureCount <
492
+ getMaxAllowedInternalSocketConnectionFailures()
493
+ ) {
494
+ // Reconnection is enabled and maximum reconnect attempts have not been reached.
495
+ return;
496
+ }
497
+
498
+ failAndCloseSocket(this.createErrorObject("connect_error", error));
499
+ });
500
+
501
+ // Listen for timeouts
502
+ this.addConnectionListener("connect_timeout", () => {
503
+ failAndCloseSocket(this.createErrorObject("connect_timeout"));
504
+ });
505
+
506
+ this.addConnectionListener("connect_document_success", (response: IConnected) => {
507
+ // If we sent a nonce and the server supports nonces, check that the nonces match
508
+ if (
509
+ connectMessage.nonce !== undefined &&
510
+ response.nonce !== undefined &&
511
+ response.nonce !== connectMessage.nonce
512
+ ) {
513
+ return;
514
+ }
515
+
516
+ const requestedMode = connectMessage.mode;
517
+ const actualMode = response.mode;
518
+ const writingPermitted = response.claims.scopes.includes(ScopeType.DocWrite);
519
+
520
+ if (writingPermitted) {
521
+ // The only time we expect a mismatch in requested/actual is if we lack write permissions
522
+ // In this case we will get "read", even if we requested "write"
523
+ if (actualMode !== requestedMode) {
524
+ failConnection(
525
+ this.createErrorObject(
526
+ "connect_document_success",
527
+ "Connected in a different mode than was requested",
528
+ false,
529
+ ),
530
+ );
531
+ return;
532
+ }
533
+ } else {
534
+ if (actualMode === "write") {
535
+ failConnection(
536
+ this.createErrorObject(
537
+ "connect_document_success",
538
+ "Connected in write mode without write permissions",
539
+ false,
540
+ ),
541
+ );
542
+ return;
543
+ }
544
+ }
545
+
546
+ this.checkpointSequenceNumber = response.checkpointSequenceNumber;
547
+
548
+ this.removeConnectionListeners();
549
+ resolve(response);
550
+ });
551
+
552
+ // Socket can be disconnected while waiting for Fluid protocol messages
553
+ // (connect_document_error / connect_document_success), as well as before DeltaManager
554
+ // had a chance to register its handlers.
555
+ this.addTrackedListener("disconnect", (reason) => {
556
+ const err = this.createErrorObject("disconnect", reason);
557
+ failAndCloseSocket(err);
558
+ });
559
+
560
+ this.addTrackedListener("error", (error) => {
561
+ // This includes "Invalid namespace" error, which we consider critical (reconnecting will not help)
562
+ const err = this.createErrorObject("error", error, error !== "Invalid namespace");
563
+ // Disconnect socket - required if happened before initial handshake
564
+ failAndCloseSocket(err);
565
+ });
566
+
567
+ this.addConnectionListener("connect_document_error", (error) => {
568
+ // If we sent a nonce and the server supports nonces, check that the nonces match
569
+ if (
570
+ connectMessage.nonce !== undefined &&
571
+ error.nonce !== undefined &&
572
+ error.nonce !== connectMessage.nonce
573
+ ) {
574
+ return;
575
+ }
576
+
577
+ // This is not an socket.io error - it's Fluid protocol error.
578
+ // In this case fail connection and indicate that we were unable to create connection
579
+ failConnection(this.createErrorObject("connect_document_error", error));
580
+ });
475
581
 
476
- // Immediately set the connection timeout.
477
- // Give extra 2 seconds for handshake on top of socket connection timeout.
582
+ this.socket.emit("connect_document", connectMessage);
583
+
584
+ // Give extra 2 seconds for handshake on top of socket connection timeout
478
585
  this.socketConnectionTimeout = setTimeout(() => {
479
586
  failConnection(this.createErrorObject("orderingServiceHandshakeTimeout"));
480
587
  }, timeout + 2000);
481
-
482
- // Listen for connection issues
483
- this.addConnectionListener("connect_error", (error) => {
484
- internalSocketConnectionFailureCount++;
485
- let isWebSocketTransportError = false;
486
- try {
487
- const description = error?.description;
488
- const context = error?.context;
489
-
490
- if (context && typeof context === "object") {
491
- const statusText = context.statusText?.code;
492
-
493
- // Self-Signed Certificate ErrorCode Found in error.context
494
- if (statusText === "DEPTH_ZERO_SELF_SIGNED_CERT") {
495
- failAndCloseSocket(this.createErrorObject("connect_error", error, false));
496
- return;
497
- }
498
- }
499
- else if (description && typeof description === "object") {
500
- const errorCode = description.error?.code;
501
-
502
- // Self-Signed Certificate ErrorCode Found in error.description
503
- if (errorCode === "DEPTH_ZERO_SELF_SIGNED_CERT") {
504
- failAndCloseSocket(this.createErrorObject("connect_error", error, false));
505
- return;
506
- }
507
-
508
- if (error.type === "TransportError") {
509
- isWebSocketTransportError = true;
510
- }
511
-
512
- // That's a WebSocket. Clear it as we can't log it.
513
- description.target = undefined;
514
- }
515
- } catch (_e) { }
516
-
517
- // Handle socket transport downgrading when not offline.
518
- if (
519
- isWebSocketTransportError &&
520
- this.enableLongPollingDowngrades &&
521
- this.socket.io.opts.transports?.[0] !== "polling") {
522
- // Downgrade transports to polling upgrade mechanism.
523
- this.socket.io.opts.transports = ["polling", "websocket"];
524
- // Don't alter reconnection behavior if already enabled.
525
- if (!isInternalSocketReconnectionEnabled()) {
526
- // Allow single reconnection attempt using polling upgrade mechanism.
527
- this.socket.io.reconnection(true);
528
- this.socket.io.reconnectionAttempts(1);
529
- }
530
- }
531
-
532
- // Allow built-in socket.io reconnection handling.
533
- if (isInternalSocketReconnectionEnabled() &&
534
- internalSocketConnectionFailureCount < getMaxAllowedInternalSocketConnectionFailures()) {
535
- // Reconnection is enabled and maximum reconnect attempts have not been reached.
536
- return;
537
- }
538
-
539
- failAndCloseSocket(this.createErrorObject("connect_error", error));
540
- });
541
-
542
- // Listen for timeouts
543
- this.addConnectionListener("connect_timeout", () => {
544
- failAndCloseSocket(this.createErrorObject("connect_timeout"));
545
- });
546
-
547
- this.addConnectionListener("connect_document_success", (response: IConnected) => {
548
- // If we sent a nonce and the server supports nonces, check that the nonces match
549
- if (connectMessage.nonce !== undefined &&
550
- response.nonce !== undefined &&
551
- response.nonce !== connectMessage.nonce) {
552
- return;
553
- }
554
-
555
- const requestedMode = connectMessage.mode;
556
- const actualMode = response.mode;
557
- const writingPermitted = response.claims.scopes.includes(ScopeType.DocWrite);
558
-
559
- if (writingPermitted) {
560
- // The only time we expect a mismatch in requested/actual is if we lack write permissions
561
- // In this case we will get "read", even if we requested "write"
562
- if (actualMode !== requestedMode) {
563
- failConnection(this.createErrorObject(
564
- "connect_document_success",
565
- "Connected in a different mode than was requested",
566
- false,
567
- ));
568
- return;
569
- }
570
- } else {
571
- if (actualMode === "write") {
572
- failConnection(this.createErrorObject(
573
- "connect_document_success",
574
- "Connected in write mode without write permissions",
575
- false,
576
- ));
577
- return;
578
- }
579
- }
580
-
581
- this.checkpointSequenceNumber = response.checkpointSequenceNumber;
582
-
583
- this.removeConnectionListeners();
584
- resolve(response);
585
- });
586
-
587
- // Socket can be disconnected while waiting for Fluid protocol messages
588
- // (connect_document_error / connect_document_success), as well as before DeltaManager
589
- // had a chance to register its handlers.
590
- this.addTrackedListener("disconnect", (reason) => {
591
- const err = this.createErrorObject("disconnect", reason);
592
- failAndCloseSocket(err);
593
- });
594
-
595
- this.addTrackedListener("error", ((error) => {
596
- // This includes "Invalid namespace" error, which we consider critical (reconnecting will not help)
597
- const err = this.createErrorObject("error", error, error !== "Invalid namespace");
598
- // Disconnect socket - required if happened before initial handshake
599
- failAndCloseSocket(err);
600
- }));
601
-
602
- this.addConnectionListener("connect_document_error", ((error) => {
603
- // If we sent a nonce and the server supports nonces, check that the nonces match
604
- if (connectMessage.nonce !== undefined &&
605
- error.nonce !== undefined &&
606
- error.nonce !== connectMessage.nonce) {
607
- return;
608
- }
609
-
610
- // This is not an socket.io error - it's Fluid protocol error.
611
- // In this case fail connection and indicate that we were unable to create connection
612
- failConnection(this.createErrorObject("connect_document_error", error));
613
- }));
614
-
615
- this.socket.emit("connect_document", connectMessage);
616
- });
617
-
618
- assert(!this.disposed, 0x246 /* "checking consistency of socket & _disposed flags" */);
619
- }
620
-
621
- private addPropsToError(errorToBeNormalized: unknown) {
622
- const normalizedError = normalizeError(errorToBeNormalized, {
623
- props: {
624
- details: JSON.stringify({
625
- disposed: this._disposed,
626
- socketConnected: this.socket?.connected,
627
- clientId: this._details?.clientId,
628
- conenctionId: this.connectionId,
629
- }),
630
- },
631
588
  });
632
- return normalizedError;
633
- }
634
-
635
- protected earlyOpHandler = (documentId: string, msgs: ISequencedDocumentMessage[]) => {
636
- this.queuedMessages.push(...msgs);
637
- };
638
-
639
- protected earlySignalHandler = (msg: ISignalMessage) => {
640
- this.queuedSignals.push(msg);
641
- };
642
-
643
- private removeEarlyOpHandler() {
644
- this.socket.removeListener("op", this.earlyOpHandler);
645
- this.earlyOpHandlerAttached = false;
646
- }
647
-
648
- private removeEarlySignalHandler() {
649
- this.socket.removeListener("signal", this.earlySignalHandler);
650
- }
651
-
652
- private addConnectionListener(event: string, listener: (...args: any[]) => void) {
653
- assert(!DocumentDeltaConnection.eventsAlwaysForwarded.includes(event),
654
- 0x247 /* "Use addTrackedListener instead" */);
655
- assert(!DocumentDeltaConnection.eventsToForward.includes(event),
656
- 0x248 /* "should not subscribe to forwarded events" */);
657
- this.socket.on(event, listener);
658
- assert(!this.connectionListeners.has(event), 0x20d /* "double connection listener" */);
659
- this.connectionListeners.set(event, listener);
660
- }
661
-
662
- protected addTrackedListener(event: string, listener: (...args: any[]) => void) {
663
- this.socket.on(event, listener);
664
- assert(!this.trackedListeners.has(event), 0x20e /* "double tracked listener" */);
665
- this.trackedListeners.set(event, listener);
666
- }
667
-
668
- private removeTrackedListeners() {
669
- for (const [event, listener] of this.trackedListeners.entries()) {
670
- this.socket.off(event, listener);
671
- }
672
- // removeTrackedListeners removes all listeners, including connection listeners
673
- this.removeConnectionListeners();
674
-
675
- this.removeEarlyOpHandler();
676
- this.removeEarlySignalHandler();
677
-
678
- this.trackedListeners.clear();
679
- }
680
-
681
- private removeConnectionListeners() {
682
- if (this.socketConnectionTimeout !== undefined) {
683
- clearTimeout(this.socketConnectionTimeout);
684
- }
685
-
686
- for (const [event, listener] of this.connectionListeners.entries()) {
687
- this.socket.off(event, listener);
688
- }
689
- this.connectionListeners.clear();
690
- }
691
-
692
- /**
693
- * Error raising for socket.io issues
694
- */
695
- protected createErrorObject(handler: string, error?: any, canRetry = true): IAnyDriverError {
696
- // Note: we suspect the incoming error object is either:
697
- // - a string: log it in the message (if not a string, it may contain PII but will print as [object Object])
698
- // - an Error object thrown by socket.io engine. Be careful with not recording PII!
699
- let message: string;
700
- if (error?.type === "TransportError") {
701
- // JSON.stringify drops Error.message
702
- const messagePrefix = (error?.message !== undefined)
703
- ? `${error.message}: `
704
- : "";
705
-
706
- // Websocket errors reported by engine.io-client.
707
- // They are Error objects with description containing WS error and description = "TransportError"
708
- // Please see https://github.com/socketio/engine.io-client/blob/7245b80/lib/transport.ts#L44,
709
- message = `${messagePrefix}${JSON.stringify(error, getCircularReplacer())}`;
710
- } else {
711
- message = extractLogSafeErrorProperties(error, true).message;
712
- }
713
-
714
- const errorObj = createGenericNetworkError(
715
- `socket.io (${handler}): ${message}`,
716
- { canRetry },
717
- { driverVersion },
718
- );
719
-
720
- return errorObj;
721
- }
589
+
590
+ assert(!this.disposed, 0x246 /* "checking consistency of socket & _disposed flags" */);
591
+ }
592
+
593
+ protected earlyOpHandler = (documentId: string, msgs: ISequencedDocumentMessage[]) => {
594
+ this.queuedMessages.push(...msgs);
595
+ };
596
+
597
+ protected earlySignalHandler = (msg: ISignalMessage) => {
598
+ this.queuedSignals.push(msg);
599
+ };
600
+
601
+ private removeEarlyOpHandler() {
602
+ this.socket.removeListener("op", this.earlyOpHandler);
603
+ this.earlyOpHandlerAttached = false;
604
+ }
605
+
606
+ private removeEarlySignalHandler() {
607
+ this.socket.removeListener("signal", this.earlySignalHandler);
608
+ }
609
+
610
+ private addConnectionListener(event: string, listener: (...args: any[]) => void) {
611
+ assert(
612
+ !DocumentDeltaConnection.eventsAlwaysForwarded.includes(event),
613
+ 0x247 /* "Use addTrackedListener instead" */,
614
+ );
615
+ assert(
616
+ !DocumentDeltaConnection.eventsToForward.includes(event),
617
+ 0x248 /* "should not subscribe to forwarded events" */,
618
+ );
619
+ this.socket.on(event, listener);
620
+ assert(!this.connectionListeners.has(event), 0x20d /* "double connection listener" */);
621
+ this.connectionListeners.set(event, listener);
622
+ }
623
+
624
+ protected addTrackedListener(event: string, listener: (...args: any[]) => void) {
625
+ this.socket.on(event, listener);
626
+ assert(!this.trackedListeners.has(event), 0x20e /* "double tracked listener" */);
627
+ this.trackedListeners.set(event, listener);
628
+ }
629
+
630
+ private removeTrackedListeners() {
631
+ for (const [event, listener] of this.trackedListeners.entries()) {
632
+ this.socket.off(event, listener);
633
+ }
634
+ // removeTrackedListeners removes all listeners, including connection listeners
635
+ this.removeConnectionListeners();
636
+
637
+ this.removeEarlyOpHandler();
638
+ this.removeEarlySignalHandler();
639
+
640
+ this.trackedListeners.clear();
641
+ }
642
+
643
+ private removeConnectionListeners() {
644
+ if (this.socketConnectionTimeout !== undefined) {
645
+ clearTimeout(this.socketConnectionTimeout);
646
+ }
647
+
648
+ for (const [event, listener] of this.connectionListeners.entries()) {
649
+ this.socket.off(event, listener);
650
+ }
651
+ this.connectionListeners.clear();
652
+ }
653
+
654
+ /**
655
+ * Error raising for socket.io issues
656
+ */
657
+ protected createErrorObject(handler: string, error?: any, canRetry = true): IAnyDriverError {
658
+ // Note: we suspect the incoming error object is either:
659
+ // - a string: log it in the message (if not a string, it may contain PII but will print as [object Object])
660
+ // - an Error object thrown by socket.io engine. Be careful with not recording PII!
661
+ let message: string;
662
+ if (error?.type === "TransportError") {
663
+ // JSON.stringify drops Error.message
664
+ const messagePrefix = error?.message !== undefined ? `${error.message}: ` : "";
665
+
666
+ // Websocket errors reported by engine.io-client.
667
+ // They are Error objects with description containing WS error and description = "TransportError"
668
+ // Please see https://github.com/socketio/engine.io-client/blob/7245b80/lib/transport.ts#L44,
669
+ message = `${messagePrefix}${JSON.stringify(error, getCircularReplacer())}`;
670
+ } else {
671
+ message = extractLogSafeErrorProperties(error, true).message;
672
+ }
673
+
674
+ const errorObj = createGenericNetworkError(
675
+ `socket.io (${handler}): ${message}`,
676
+ { canRetry },
677
+ { driverVersion },
678
+ );
679
+
680
+ return errorObj;
681
+ }
722
682
  }