@fluidframework/driver-base 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419

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