@fluidframework/container-loader 1.1.0-75972 → 1.2.0-77818

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 (47) hide show
  1. package/dist/collabWindowTracker.js +1 -1
  2. package/dist/collabWindowTracker.js.map +1 -1
  3. package/dist/connectionManager.d.ts +10 -0
  4. package/dist/connectionManager.d.ts.map +1 -1
  5. package/dist/connectionManager.js +59 -21
  6. package/dist/connectionManager.js.map +1 -1
  7. package/dist/connectionState.d.ts +2 -2
  8. package/dist/connectionState.js +2 -2
  9. package/dist/connectionState.js.map +1 -1
  10. package/dist/container.d.ts +2 -1
  11. package/dist/container.d.ts.map +1 -1
  12. package/dist/container.js +15 -11
  13. package/dist/container.js.map +1 -1
  14. package/dist/deltaManager.d.ts +4 -0
  15. package/dist/deltaManager.d.ts.map +1 -1
  16. package/dist/deltaManager.js +8 -3
  17. package/dist/deltaManager.js.map +1 -1
  18. package/dist/packageVersion.d.ts +1 -1
  19. package/dist/packageVersion.js +1 -1
  20. package/dist/packageVersion.js.map +1 -1
  21. package/lib/collabWindowTracker.js +1 -1
  22. package/lib/collabWindowTracker.js.map +1 -1
  23. package/lib/connectionManager.d.ts +10 -0
  24. package/lib/connectionManager.d.ts.map +1 -1
  25. package/lib/connectionManager.js +59 -22
  26. package/lib/connectionManager.js.map +1 -1
  27. package/lib/connectionState.d.ts +2 -2
  28. package/lib/connectionState.js +2 -2
  29. package/lib/connectionState.js.map +1 -1
  30. package/lib/container.d.ts +2 -1
  31. package/lib/container.d.ts.map +1 -1
  32. package/lib/container.js +17 -13
  33. package/lib/container.js.map +1 -1
  34. package/lib/deltaManager.d.ts +4 -0
  35. package/lib/deltaManager.d.ts.map +1 -1
  36. package/lib/deltaManager.js +9 -4
  37. package/lib/deltaManager.js.map +1 -1
  38. package/lib/packageVersion.d.ts +1 -1
  39. package/lib/packageVersion.js +1 -1
  40. package/lib/packageVersion.js.map +1 -1
  41. package/package.json +14 -14
  42. package/src/collabWindowTracker.ts +1 -1
  43. package/src/connectionManager.ts +80 -20
  44. package/src/connectionState.ts +2 -2
  45. package/src/container.ts +33 -27
  46. package/src/deltaManager.ts +11 -2
  47. package/src/packageVersion.ts +1 -1
@@ -3,6 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { default as AbortController } from "abort-controller";
6
7
  import {
7
8
  IDisposable,
8
9
  ITelemetryLogger,
@@ -30,6 +31,7 @@ import {
30
31
  waitForConnectedState,
31
32
  DeltaStreamConnectionForbiddenError,
32
33
  logNetworkFailure,
34
+ // isRuntimeMessage,
33
35
  } from "@fluidframework/driver-utils";
34
36
  import {
35
37
  ConnectionMode,
@@ -118,6 +120,21 @@ class NoDeltaStream
118
120
  public dispose() { this._disposed = true; }
119
121
  }
120
122
 
123
+ /**
124
+ * Interface to track the current in-progress connection attempt.
125
+ */
126
+ interface IPendingConnection {
127
+ /**
128
+ * Used to cancel an in-progress connection attempt.
129
+ */
130
+ abort(): void;
131
+
132
+ /**
133
+ * Desired ConnectionMode of this in-progress connection attempt.
134
+ */
135
+ connectionMode: ConnectionMode;
136
+ }
137
+
121
138
  /**
122
139
  * Implementation of IConnectionManager, used by Container class
123
140
  * Implements constant connectivity to relay service, by reconnecting in case of loast connection or error.
@@ -127,7 +144,12 @@ export class ConnectionManager implements IConnectionManager {
127
144
  /** Connection mode used when reconnecting on error or disconnect. */
128
145
  private readonly defaultReconnectionMode: ConnectionMode;
129
146
 
130
- private pendingConnection = false;
147
+ /**
148
+ * Tracks the current in-progress connection attempt. Undefined if there is none.
149
+ * Note: Once the connection attempt fires and the code becomes asynchronous, its possible that a new connection
150
+ * attempt was fired and this.pendingConnection was overwritten to reflect the new attempt.
151
+ */
152
+ private pendingConnection: IPendingConnection | undefined;
131
153
  private connection: IDocumentDeltaConnection | undefined;
132
154
 
133
155
  /** file ACL - whether user has only read-only access to a file */
@@ -304,7 +326,7 @@ export class ConnectionManager implements IConnectionManager {
304
326
  }
305
327
  this.closed = true;
306
328
 
307
- this.pendingConnection = false;
329
+ this.pendingConnection = undefined;
308
330
 
309
331
  // Ensure that things like triggerConnect() will short circuit
310
332
  this._reconnectMode = ReconnectMode.Never;
@@ -411,11 +433,18 @@ export class ConnectionManager implements IConnectionManager {
411
433
  private async connectCore(connectionMode?: ConnectionMode): Promise<void> {
412
434
  assert(!this.closed, 0x26a /* "not closed" */);
413
435
 
414
- if (this.connection !== undefined || this.pendingConnection) {
415
- return;
436
+ if (this.connection !== undefined) {
437
+ return; // Connection attempt already completed successfully
416
438
  }
417
439
 
418
- let requestedMode = connectionMode ?? this.defaultReconnectionMode;
440
+ let pendingConnectionMode;
441
+ if (this.pendingConnection !== undefined) {
442
+ pendingConnectionMode = this.pendingConnection.connectionMode;
443
+ this.cancelConnection(); // Throw out in-progress connection attempt in favor of new attempt
444
+ assert(this.pendingConnection === undefined, "this.pendingConnection should be undefined");
445
+ }
446
+ // If there is no specified ConnectionMode, try the previous mode, if there is no previous mode use default
447
+ let requestedMode = connectionMode ?? pendingConnectionMode ?? this.defaultReconnectionMode;
419
448
 
420
449
  // if we have any non-acked ops from last connection, reconnect as "write".
421
450
  // without that we would connect in view-only mode, which will result in immediate
@@ -433,31 +462,39 @@ export class ConnectionManager implements IConnectionManager {
433
462
 
434
463
  if (docService.policies?.storageOnly === true) {
435
464
  connection = new NoDeltaStream();
436
- // to keep setupNewSuccessfulConnection happy
437
- this.pendingConnection = true;
438
465
  this.setupNewSuccessfulConnection(connection, "read");
439
- assert(!this.pendingConnection, 0x2b3 /* "logic error" */);
466
+ assert(this.pendingConnection === undefined, 0x2b3 /* "logic error" */);
440
467
  return;
441
468
  }
442
469
 
443
- // this.pendingConnection resets to false as soon as we know the outcome of the connection attempt
444
- this.pendingConnection = true;
445
-
446
470
  let delayMs = InitialReconnectDelayInMs;
447
471
  let connectRepeatCount = 0;
448
472
  const connectStartTime = performance.now();
449
473
  let lastError: any;
450
474
 
475
+ const abortController = new AbortController();
476
+ const abortSignal = abortController.signal;
477
+ this.pendingConnection = { abort: () => { abortController.abort(); }, connectionMode: requestedMode };
478
+
451
479
  // This loop will keep trying to connect until successful, with a delay between each iteration.
452
480
  while (connection === undefined) {
453
481
  if (this.closed) {
454
482
  throw new Error("Attempting to connect a closed DeltaManager");
455
483
  }
484
+ if (abortSignal.aborted === true) {
485
+ this.logger.sendTelemetryEvent({
486
+ eventName: "ConnectionAttemptCancelled",
487
+ attempts: connectRepeatCount,
488
+ duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
489
+ connectionEstablished: false,
490
+ });
491
+ return;
492
+ }
456
493
  connectRepeatCount++;
457
494
 
458
495
  try {
459
496
  this.client.mode = requestedMode;
460
- connection = await docService.connectToDeltaStream(this.client);
497
+ connection = await docService.connectToDeltaStream({ ...this.client, mode: requestedMode });
461
498
 
462
499
  if (connection.disposed) {
463
500
  // Nobody observed this connection, so drop it on the floor and retry.
@@ -515,6 +552,18 @@ export class ConnectionManager implements IConnectionManager {
515
552
  );
516
553
  }
517
554
 
555
+ // Check for abort signal after while loop as well
556
+ if (abortSignal.aborted === true) {
557
+ connection.dispose();
558
+ this.logger.sendTelemetryEvent({
559
+ eventName: "ConnectionAttemptCancelled",
560
+ attempts: connectRepeatCount,
561
+ duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
562
+ connectionEstablished: true,
563
+ });
564
+ return;
565
+ }
566
+
518
567
  this.setupNewSuccessfulConnection(connection, requestedMode);
519
568
  }
520
569
 
@@ -534,15 +583,20 @@ export class ConnectionManager implements IConnectionManager {
534
583
  /**
535
584
  * Disconnect the current connection.
536
585
  * @param reason - Text description of disconnect reason to emit with disconnect event
586
+ * @returns A boolean that indicates if there was an existing connection (or pending connection) to disconnect
537
587
  */
538
588
  private disconnectFromDeltaStream(reason: string): boolean {
539
589
  this.pendingReconnect = false;
540
590
 
541
591
  if (this.connection === undefined) {
592
+ if (this.pendingConnection !== undefined) {
593
+ this.cancelConnection();
594
+ return true;
595
+ }
542
596
  return false;
543
597
  }
544
598
 
545
- assert(!this.pendingConnection, 0x27b /* "reentrancy may result in incorrect behavior" */);
599
+ assert(this.pendingConnection === undefined, 0x27b /* "reentrancy may result in incorrect behavior" */);
546
600
 
547
601
  const connection = this.connection;
548
602
  // Avoid any re-entrancy - clear object reference
@@ -568,6 +622,16 @@ export class ConnectionManager implements IConnectionManager {
568
622
  return true;
569
623
  }
570
624
 
625
+ /**
626
+ * Cancel in-progress connection attempt.
627
+ */
628
+ private cancelConnection() {
629
+ assert(this.pendingConnection !== undefined, "this.pendingConnection is undefined when trying to cancel");
630
+ this.pendingConnection.abort();
631
+ this.pendingConnection = undefined;
632
+ this.logger.sendTelemetryEvent({ eventName: "ConnectionCancelReceived" });
633
+ }
634
+
571
635
  /**
572
636
  * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
573
637
  * initial messages.
@@ -578,11 +642,8 @@ export class ConnectionManager implements IConnectionManager {
578
642
  assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
579
643
  assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
580
644
 
581
- if (this.pendingConnection) {
582
- this.pendingConnection = false;
583
- } else {
584
- assert(this.closed, 0x27f /* "reentrancy may result in incorrect behavior" */);
585
- }
645
+ this.pendingConnection = undefined;
646
+
586
647
  this.connection = connection;
587
648
 
588
649
  // Does information in scopes & mode matches?
@@ -780,7 +841,6 @@ export class ConnectionManager implements IConnectionManager {
780
841
 
781
842
  public sendMessages(messages: IDocumentMessage[]) {
782
843
  assert(this.connected, 0x2b4 /* "not connected on sending ops!" */);
783
-
784
844
  // If connection is "read" or implicit "read" (got leave op for "write" connection),
785
845
  // then op can't make it through - we will get a nack if op is sent.
786
846
  // We can short-circuit this process.
@@ -832,7 +892,7 @@ export class ConnectionManager implements IConnectionManager {
832
892
  this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
833
893
 
834
894
  // Please see #8483 for more details on why maintaining connection further as is would not work.
835
- // Short story - connection properties are immutable, and many processes (consensus DDSs, summarizer)
895
+ // Short story - connection properties are immutable, and many processes (consensus DDSes, summarizer)
836
896
  // assume that connection stays "write" connection until disconnect, and act accordingly, which may
837
897
  // not work well with de-facto "read" connection we are in after receiving own leave op on timeout.
838
898
  // Clients need to be able to transition to "read" state after some time of inactivity!
@@ -18,8 +18,8 @@ export enum ConnectionState {
18
18
  EstablishingConnection = 3,
19
19
 
20
20
  /**
21
- * @see ConnectionState.CatchingUp, which is the new name for this state.
22
- * @deprecated - This state itself is not gone, just being renamed. Please use ConnectionState.CatchingUp.
21
+ * See {@link ConnectionState.CatchingUp}, which is the new name for this state.
22
+ * @deprecated - This state itself is not gone, just being renamed. Please use {@link ConnectionState.CatchingUp}.
23
23
  */
24
24
  Connecting = 1,
25
25
 
package/src/container.ts CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  extractSafePropertiesFromMessage,
36
36
  GenericError,
37
37
  UsageError,
38
- } from "@fluidframework/container-utils";
38
+ } from "@fluidframework/container-utils";
39
39
  import {
40
40
  IDocumentService,
41
41
  IDocumentStorageService,
@@ -50,9 +50,10 @@ import {
50
50
  combineAppAndProtocolSummary,
51
51
  runWithRetry,
52
52
  isFluidResolvedUrl,
53
+ isRuntimeMessage,
54
+ isUnpackedRuntimeMessage,
53
55
  } from "@fluidframework/driver-utils";
54
56
  import {
55
- isSystemMessage,
56
57
  IProtocolHandler,
57
58
  ProtocolOpHandlerWithClientValidation,
58
59
  } from "@fluidframework/protocol-base";
@@ -217,7 +218,9 @@ export async function waitContainerToCatchUp(container: IContainer) {
217
218
  };
218
219
  container.on(connectedEventName, callback);
219
220
 
220
- container.connect();
221
+ if (container.connectionState === ConnectionState.Disconnected) {
222
+ container.connect();
223
+ }
221
224
  });
222
225
  }
223
226
 
@@ -286,14 +289,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
286
289
  event.end({ ...props, ...loadOptions.loadMode });
287
290
  resolve(container);
288
291
  },
289
- (error) => {
290
- const err = normalizeError(error);
291
- // Depending where error happens, we can be attempting to connect to web socket
292
- // and continuously retrying (consider offline mode)
293
- // Host has no container to close, so it's prudent to do it here
294
- container.close(err);
295
- onClosed(err);
296
- });
292
+ (error) => {
293
+ const err = normalizeError(error);
294
+ // Depending where error happens, we can be attempting to connect to web socket
295
+ // and continuously retrying (consider offline mode)
296
+ // Host has no container to close, so it's prudent to do it here
297
+ container.close(err);
298
+ onClosed(err);
299
+ });
297
300
  }),
298
301
  { start: true, end: true, cancel: "generic" },
299
302
  );
@@ -401,7 +404,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
401
404
  return this._protocolHandler;
402
405
  }
403
406
 
404
- private resumedOpProcessingAfterLoad = false;
407
+ /** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
408
+ private inboundQueuePausedFromInit = true;
405
409
  private firstConnection = true;
406
410
  private readonly connectionTransitionTimes: number[] = [];
407
411
  private messageCountAfterDisconnection: number = 0;
@@ -535,7 +539,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
535
539
  name: typeof name === "string" ? name : undefined,
536
540
  },
537
541
  error);
538
- });
542
+ });
539
543
  this._audience = new Audience();
540
544
 
541
545
  this.clientDetailsOverride = config.clientDetailsOverride;
@@ -678,10 +682,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
678
682
  }
679
683
  break;
680
684
  case connectedEventName:
681
- if (this.connected) {
685
+ if (this.connected) {
682
686
  listener(this.clientId);
683
- }
684
- break;
687
+ }
688
+ break;
685
689
  case disconnectedEventName:
686
690
  if (!this.connected) {
687
691
  listener();
@@ -913,7 +917,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
913
917
  throw newError;
914
918
  }
915
919
  },
916
- { start: true, end: true, cancel: "generic" });
920
+ { start: true, end: true, cancel: "generic" });
917
921
  }
918
922
 
919
923
  public async request(path: IRequest): Promise<IResponse> {
@@ -992,8 +996,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
992
996
  assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
993
997
 
994
998
  // Resume processing ops
995
- if (!this.resumedOpProcessingAfterLoad) {
996
- this.resumedOpProcessingAfterLoad = true;
999
+ if (this.inboundQueuePausedFromInit) {
1000
+ this.inboundQueuePausedFromInit = false;
997
1001
  this._deltaManager.inbound.resume();
998
1002
  this._deltaManager.inboundSignal.resume();
999
1003
  }
@@ -1189,13 +1193,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1189
1193
 
1190
1194
  switch (loadMode.deltaConnection) {
1191
1195
  case undefined:
1192
- // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
1193
- // If there is gap, we will learn about it once connected, but the gap should be small (if any),
1194
- // assuming that resumeInternal() is called quickly after initial container boot.
1195
- this.resumeInternal({ reason: "DocumentLoad", fetchOpsFromStorage: false });
1196
- break;
1197
1196
  case "delayed":
1198
- this.resumedOpProcessingAfterLoad = true;
1197
+ assert(this.inboundQueuePausedFromInit, "inboundQueuePausedFromInit should be true");
1198
+ this.inboundQueuePausedFromInit = false;
1199
1199
  this._deltaManager.inbound.resume();
1200
1200
  this._deltaManager.inboundSignal.resume();
1201
1201
  break;
@@ -1395,7 +1395,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1395
1395
  if (key === "code" || key === "code2") {
1396
1396
  if (!isFluidCodeDetails(value)) {
1397
1397
  this.mc.logger.sendErrorEvent({
1398
- eventName: "CodeProposalNotIFluidCodeDetails",
1398
+ eventName: "CodeProposalNotIFluidCodeDetails",
1399
1399
  });
1400
1400
  }
1401
1401
  this.processCodeProposal().catch((error) => {
@@ -1684,8 +1684,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1684
1684
  new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1685
1685
  }
1686
1686
 
1687
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1688
+ if (isUnpackedRuntimeMessage(message) && !isRuntimeMessage(message)) {
1689
+ this.mc.logger.sendTelemetryEvent(
1690
+ { eventName: "UnpackedRuntimeMessage", type: message.type });
1691
+ }
1687
1692
  // Forward non system messages to the loaded runtime for processing
1688
- if (!isSystemMessage(message)) {
1693
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1694
+ if (isRuntimeMessage(message) || isUnpackedRuntimeMessage(message)) {
1689
1695
  this.context.process(message, local, undefined);
1690
1696
  }
1691
1697
 
@@ -31,7 +31,6 @@ import {
31
31
  IDocumentService,
32
32
  DriverErrorType,
33
33
  } from "@fluidframework/driver-definitions";
34
- import { isSystemMessage } from "@fluidframework/protocol-base";
35
34
  import {
36
35
  IDocumentMessage,
37
36
  ISequencedDocumentMessage,
@@ -41,6 +40,7 @@ import {
41
40
  } from "@fluidframework/protocol-definitions";
42
41
  import {
43
42
  NonRetryableError,
43
+ isClientMessage,
44
44
  } from "@fluidframework/driver-utils";
45
45
  import {
46
46
  ThrottlingWarning,
@@ -105,6 +105,10 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
105
105
  private lastProcessedMessage: ISequencedDocumentMessage | undefined;
106
106
  private baseTerm: number = 0;
107
107
 
108
+ /**
109
+ * Track down the ops size.
110
+ */
111
+ private opsSize: number = 0;
108
112
  private prevEnqueueMessagesReason: string | undefined;
109
113
  private previouslyProcessedMessage: ISequencedDocumentMessage | undefined;
110
114
 
@@ -198,6 +202,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
198
202
  return -1;
199
203
  }
200
204
 
205
+ this.opsSize += message.contents.length;
206
+
201
207
  this.messageBuffer.push(message);
202
208
 
203
209
  this.emit("submitOp", message);
@@ -226,6 +232,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
226
232
  public get connectionProps(): ITelemetryProperties {
227
233
  return {
228
234
  sequenceNumber: this.lastSequenceNumber,
235
+ opsSize: this.opsSize > 0 ? this.opsSize : undefined,
229
236
  ...this.connectionManager.connectionProps,
230
237
  };
231
238
  }
@@ -337,6 +344,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
337
344
  // state. As requirements change, so should these checks.
338
345
  assert(this.messageBuffer.length === 0, 0x0e9 /* "messageBuffer is not empty on new connection" */);
339
346
 
347
+ this.opsSize = 0;
348
+
340
349
  this.emit(
341
350
  "connect",
342
351
  connection,
@@ -753,7 +762,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
753
762
  // System messages may have no clientId (but some do, like propose, noop, summarize)
754
763
  assert(
755
764
  message.clientId !== undefined
756
- || isSystemMessage(message),
765
+ || !(isClientMessage(message)),
757
766
  0x0ed /* "non-system message have to have clientId" */,
758
767
  );
759
768
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "1.1.0-75972";
9
+ export const pkgVersion = "1.2.0-77818";