@fluidframework/container-loader 1.3.0 → 2.0.0-dev.1.4.5.105745

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 (148) hide show
  1. package/.eslintrc.js +8 -21
  2. package/.mocharc.js +12 -0
  3. package/dist/audience.d.ts +2 -2
  4. package/dist/audience.d.ts.map +1 -1
  5. package/dist/audience.js.map +1 -1
  6. package/dist/catchUpMonitor.d.ts +29 -0
  7. package/dist/catchUpMonitor.d.ts.map +1 -0
  8. package/dist/catchUpMonitor.js +43 -0
  9. package/dist/catchUpMonitor.js.map +1 -0
  10. package/dist/collabWindowTracker.d.ts +1 -1
  11. package/dist/collabWindowTracker.d.ts.map +1 -1
  12. package/dist/collabWindowTracker.js +12 -4
  13. package/dist/collabWindowTracker.js.map +1 -1
  14. package/dist/connectionManager.d.ts +5 -5
  15. package/dist/connectionManager.d.ts.map +1 -1
  16. package/dist/connectionManager.js +13 -18
  17. package/dist/connectionManager.js.map +1 -1
  18. package/dist/connectionState.d.ts +0 -5
  19. package/dist/connectionState.d.ts.map +1 -1
  20. package/dist/connectionState.js +0 -5
  21. package/dist/connectionState.js.map +1 -1
  22. package/dist/connectionStateHandler.d.ts +84 -22
  23. package/dist/connectionStateHandler.d.ts.map +1 -1
  24. package/dist/connectionStateHandler.js +172 -59
  25. package/dist/connectionStateHandler.js.map +1 -1
  26. package/dist/container.d.ts +30 -17
  27. package/dist/container.d.ts.map +1 -1
  28. package/dist/container.js +173 -165
  29. package/dist/container.js.map +1 -1
  30. package/dist/containerContext.d.ts +18 -7
  31. package/dist/containerContext.d.ts.map +1 -1
  32. package/dist/containerContext.js +18 -8
  33. package/dist/containerContext.js.map +1 -1
  34. package/dist/containerStorageAdapter.d.ts +11 -25
  35. package/dist/containerStorageAdapter.d.ts.map +1 -1
  36. package/dist/containerStorageAdapter.js +51 -17
  37. package/dist/containerStorageAdapter.js.map +1 -1
  38. package/dist/contracts.d.ts +5 -5
  39. package/dist/contracts.js.map +1 -1
  40. package/dist/deltaManager.d.ts +4 -1
  41. package/dist/deltaManager.d.ts.map +1 -1
  42. package/dist/deltaManager.js +33 -6
  43. package/dist/deltaManager.js.map +1 -1
  44. package/dist/deltaQueue.js +3 -3
  45. package/dist/deltaQueue.js.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/loader.d.ts +8 -1
  50. package/dist/loader.d.ts.map +1 -1
  51. package/dist/loader.js +4 -3
  52. package/dist/loader.js.map +1 -1
  53. package/dist/packageVersion.d.ts +1 -1
  54. package/dist/packageVersion.d.ts.map +1 -1
  55. package/dist/packageVersion.js +1 -1
  56. package/dist/packageVersion.js.map +1 -1
  57. package/dist/protocol.d.ts +22 -0
  58. package/dist/protocol.d.ts.map +1 -0
  59. package/dist/protocol.js +53 -0
  60. package/dist/protocol.js.map +1 -0
  61. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  62. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  63. package/dist/retriableDocumentStorageService.d.ts +2 -2
  64. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  65. package/dist/retriableDocumentStorageService.js +2 -2
  66. package/dist/retriableDocumentStorageService.js.map +1 -1
  67. package/lib/audience.d.ts +2 -2
  68. package/lib/audience.d.ts.map +1 -1
  69. package/lib/audience.js.map +1 -1
  70. package/lib/catchUpMonitor.d.ts +29 -0
  71. package/lib/catchUpMonitor.d.ts.map +1 -0
  72. package/lib/catchUpMonitor.js +39 -0
  73. package/lib/catchUpMonitor.js.map +1 -0
  74. package/lib/collabWindowTracker.d.ts +1 -1
  75. package/lib/collabWindowTracker.d.ts.map +1 -1
  76. package/lib/collabWindowTracker.js +13 -5
  77. package/lib/collabWindowTracker.js.map +1 -1
  78. package/lib/connectionManager.d.ts +5 -5
  79. package/lib/connectionManager.d.ts.map +1 -1
  80. package/lib/connectionManager.js +14 -21
  81. package/lib/connectionManager.js.map +1 -1
  82. package/lib/connectionState.d.ts +0 -5
  83. package/lib/connectionState.d.ts.map +1 -1
  84. package/lib/connectionState.js +0 -5
  85. package/lib/connectionState.js.map +1 -1
  86. package/lib/connectionStateHandler.d.ts +84 -22
  87. package/lib/connectionStateHandler.d.ts.map +1 -1
  88. package/lib/connectionStateHandler.js +171 -59
  89. package/lib/connectionStateHandler.js.map +1 -1
  90. package/lib/container.d.ts +30 -17
  91. package/lib/container.d.ts.map +1 -1
  92. package/lib/container.js +176 -168
  93. package/lib/container.js.map +1 -1
  94. package/lib/containerContext.d.ts +18 -7
  95. package/lib/containerContext.d.ts.map +1 -1
  96. package/lib/containerContext.js +19 -9
  97. package/lib/containerContext.js.map +1 -1
  98. package/lib/containerStorageAdapter.d.ts +11 -25
  99. package/lib/containerStorageAdapter.d.ts.map +1 -1
  100. package/lib/containerStorageAdapter.js +51 -16
  101. package/lib/containerStorageAdapter.js.map +1 -1
  102. package/lib/contracts.d.ts +5 -5
  103. package/lib/contracts.js.map +1 -1
  104. package/lib/deltaManager.d.ts +4 -1
  105. package/lib/deltaManager.d.ts.map +1 -1
  106. package/lib/deltaManager.js +35 -8
  107. package/lib/deltaManager.js.map +1 -1
  108. package/lib/deltaQueue.js +3 -3
  109. package/lib/deltaQueue.js.map +1 -1
  110. package/lib/index.d.ts +1 -0
  111. package/lib/index.d.ts.map +1 -1
  112. package/lib/index.js.map +1 -1
  113. package/lib/loader.d.ts +8 -1
  114. package/lib/loader.d.ts.map +1 -1
  115. package/lib/loader.js +4 -3
  116. package/lib/loader.js.map +1 -1
  117. package/lib/packageVersion.d.ts +1 -1
  118. package/lib/packageVersion.d.ts.map +1 -1
  119. package/lib/packageVersion.js +1 -1
  120. package/lib/packageVersion.js.map +1 -1
  121. package/lib/protocol.d.ts +22 -0
  122. package/lib/protocol.d.ts.map +1 -0
  123. package/lib/protocol.js +49 -0
  124. package/lib/protocol.js.map +1 -0
  125. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  126. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  127. package/lib/retriableDocumentStorageService.d.ts +2 -2
  128. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  129. package/lib/retriableDocumentStorageService.js +2 -2
  130. package/lib/retriableDocumentStorageService.js.map +1 -1
  131. package/package.json +26 -20
  132. package/src/audience.ts +2 -2
  133. package/src/catchUpMonitor.ts +59 -0
  134. package/src/collabWindowTracker.ts +15 -6
  135. package/src/connectionManager.ts +23 -27
  136. package/src/connectionState.ts +0 -6
  137. package/src/connectionStateHandler.ts +235 -70
  138. package/src/container.ts +223 -209
  139. package/src/containerContext.ts +22 -8
  140. package/src/containerStorageAdapter.ts +71 -16
  141. package/src/contracts.ts +7 -7
  142. package/src/deltaManager.ts +42 -11
  143. package/src/deltaQueue.ts +3 -3
  144. package/src/index.ts +4 -0
  145. package/src/loader.ts +14 -3
  146. package/src/packageVersion.ts +1 -1
  147. package/src/protocol.ts +97 -0
  148. package/src/retriableDocumentStorageService.ts +8 -2
package/src/container.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import merge from "lodash/merge";
8
8
  import { v4 as uuid } from "uuid";
9
9
  import {
10
- IDisposable, ITelemetryLogger, ITelemetryProperties,
10
+ ITelemetryLogger, ITelemetryProperties,
11
11
  } from "@fluidframework/common-definitions";
12
12
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
13
13
  import {
@@ -29,10 +29,9 @@ import {
29
29
  IContainerLoadMode,
30
30
  IFluidCodeDetails,
31
31
  isFluidCodeDetails,
32
+ IBatchMessage,
32
33
  } from "@fluidframework/container-definitions";
33
34
  import {
34
- DataCorruptionError,
35
- extractSafePropertiesFromMessage,
36
35
  GenericError,
37
36
  UsageError,
38
37
  } from "@fluidframework/container-utils";
@@ -50,13 +49,8 @@ import {
50
49
  combineAppAndProtocolSummary,
51
50
  runWithRetry,
52
51
  isFluidResolvedUrl,
53
- isRuntimeMessage,
54
- isUnpackedRuntimeMessage,
55
52
  } from "@fluidframework/driver-utils";
56
- import {
57
- IProtocolHandler,
58
- ProtocolOpHandlerWithClientValidation,
59
- } from "@fluidframework/protocol-base";
53
+ import { IQuorumSnapshot } from "@fluidframework/protocol-base";
60
54
  import {
61
55
  IClient,
62
56
  IClientConfiguration,
@@ -64,7 +58,6 @@ import {
64
58
  ICommittedProposal,
65
59
  IDocumentAttributes,
66
60
  IDocumentMessage,
67
- IProcessMessageResult,
68
61
  IProtocolState,
69
62
  IQuorumClients,
70
63
  IQuorumProposals,
@@ -100,15 +93,21 @@ import { DeltaManager, IConnectionArgs } from "./deltaManager";
100
93
  import { DeltaManagerProxy } from "./deltaManagerProxy";
101
94
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
102
95
  import { pkgVersion } from "./packageVersion";
103
- import { ConnectionStateHandler } from "./connectionStateHandler";
104
- import { RetriableDocumentStorageService } from "./retriableDocumentStorageService";
105
- import { ProtocolTreeStorageService } from "./protocolTreeDocumentStorageService";
106
- import { BlobOnlyStorage, ContainerStorageAdapter } from "./containerStorageAdapter";
96
+ import { ContainerStorageAdapter } from "./containerStorageAdapter";
97
+ import {
98
+ IConnectionStateHandler,
99
+ createConnectionStateHandler,
100
+ } from "./connectionStateHandler";
107
101
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
108
102
  import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy } from "./quorum";
109
103
  import { CollabWindowTracker } from "./collabWindowTracker";
110
104
  import { ConnectionManager } from "./connectionManager";
111
105
  import { ConnectionState } from "./connectionState";
106
+ import {
107
+ IProtocolHandler,
108
+ ProtocolHandler,
109
+ ProtocolHandlerBuilder,
110
+ } from "./protocol";
112
111
 
113
112
  const detachedContainerRefSeqNumber = 0;
114
113
 
@@ -149,14 +148,19 @@ export interface IContainerConfig {
149
148
  }
150
149
 
151
150
  /**
152
- * Waits until container connects to delta storage and gets up-to-date
151
+ * Waits until container connects to delta storage and gets up-to-date.
152
+ *
153
153
  * Useful when resolving URIs and hitting 404, due to container being loaded from (stale) snapshot and not being
154
154
  * up to date. Host may chose to wait in such case and retry resolving URI.
155
+ *
155
156
  * Warning: Will wait infinitely for connection to establish if there is no connection.
156
157
  * May result in deadlock if Container.disconnect() is called and never followed by a call to Container.connect().
157
- * @returns true: container is up to date, it processed all the ops that were know at the time of first connection
158
- * false: storage does not provide indication of how far the client is. Container processed
159
- * all the ops known to it, but it maybe still behind.
158
+ *
159
+ * @returns `true`: container is up to date, it processed all the ops that were know at the time of first connection.
160
+ *
161
+ * `false`: storage does not provide indication of how far the client is. Container processed all the ops known to it,
162
+ * but it maybe still behind.
163
+ *
160
164
  * @throws an error beginning with `"Container closed"` if the container is closed before it catches up.
161
165
  */
162
166
  export async function waitContainerToCatchUp(container: IContainer) {
@@ -179,6 +183,10 @@ export async function waitContainerToCatchUp(container: IContainer) {
179
183
  };
180
184
  container.on("closed", closedCallback);
181
185
 
186
+ // Depending on config, transition to "connected" state may include the guarantee
187
+ // that all known ops have been processed. If so, we may introduce additional wait here.
188
+ // Waiting for "connected" state in either case gets us at least to our own Join op
189
+ // which is a reasonable approximation of "caught up"
182
190
  const waitForOps = () => {
183
191
  assert(container.connectionState === ConnectionState.CatchingUp
184
192
  || container.connectionState === ConnectionState.Connected,
@@ -270,6 +278,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
270
278
  loader: Loader,
271
279
  loadOptions: IContainerLoadOptions,
272
280
  pendingLocalState?: IPendingContainerState,
281
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
273
282
  ): Promise<Container> {
274
283
  const container = new Container(
275
284
  loader,
@@ -278,7 +287,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
278
287
  resolvedUrl: loadOptions.resolvedUrl,
279
288
  canReconnect: loadOptions.canReconnect,
280
289
  serializedContainerState: pendingLocalState,
281
- });
290
+ },
291
+ protocolHandlerBuilder);
282
292
 
283
293
  return PerformanceEvent.timedExecAsync(
284
294
  container.mc.logger,
@@ -326,10 +336,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
326
336
  public static async createDetached(
327
337
  loader: Loader,
328
338
  codeDetails: IFluidCodeDetails,
339
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
329
340
  ): Promise<Container> {
330
341
  const container = new Container(
331
342
  loader,
332
- {});
343
+ {},
344
+ protocolHandlerBuilder);
333
345
 
334
346
  return PerformanceEvent.timedExecAsync(
335
347
  container.mc.logger,
@@ -348,10 +360,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
348
360
  public static async rehydrateDetachedFromSnapshot(
349
361
  loader: Loader,
350
362
  snapshot: string,
363
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
351
364
  ): Promise<Container> {
352
365
  const container = new Container(
353
366
  loader,
354
- {});
367
+ {},
368
+ protocolHandlerBuilder);
369
+
355
370
  return PerformanceEvent.timedExecAsync(
356
371
  container.mc.logger,
357
372
  { eventName: "RehydrateDetachedFromSnapshot" },
@@ -378,7 +393,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
378
393
  // Only transition states if currently loading
379
394
  if (this._lifecycleState === "loading") {
380
395
  // Propagate current connection state through the system.
381
- this.propagateConnectionState();
396
+ this.propagateConnectionState(true /* initial transition */);
382
397
  this._lifecycleState = "loaded";
383
398
  }
384
399
  }
@@ -389,23 +404,15 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
389
404
 
390
405
  private _attachState = AttachState.Detached;
391
406
 
392
- private readonly _storage: ContainerStorageAdapter;
407
+ private readonly storageService: ContainerStorageAdapter;
393
408
  public get storage(): IDocumentStorageService {
394
- return this._storage;
395
- }
396
-
397
- private _storageService: IDocumentStorageService & IDisposable | undefined;
398
- private get storageService(): IDocumentStorageService {
399
- if (this._storageService === undefined) {
400
- throw new Error("Attempted to access storageService before it was defined");
401
- }
402
- return this._storageService;
409
+ return this.storageService;
403
410
  }
404
411
 
405
412
  private readonly clientDetailsOverride: IClientDetails | undefined;
406
413
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
407
414
  private service: IDocumentService | undefined;
408
- private readonly _audience: Audience;
415
+ private _initialClients: ISignalClient[] | undefined;
409
416
 
410
417
  private _context: ContainerContext | undefined;
411
418
  private get context() {
@@ -434,7 +441,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
434
441
 
435
442
  private lastVisible: number | undefined;
436
443
  private readonly visibilityEventHandler: (() => void) | undefined;
437
- private readonly connectionStateHandler: ConnectionStateHandler;
444
+ private readonly connectionStateHandler: IConnectionStateHandler;
438
445
 
439
446
  private setAutoReconnectTime = performance.now();
440
447
 
@@ -476,7 +483,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
476
483
  }
477
484
 
478
485
  public get connected(): boolean {
479
- return this.connectionStateHandler.connected;
486
+ return this.connectionStateHandler.connectionState === ConnectionState.Connected;
480
487
  }
481
488
 
482
489
  /**
@@ -487,12 +494,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
487
494
  return this._deltaManager.serviceConfiguration;
488
495
  }
489
496
 
497
+ private _clientId: string | undefined;
498
+
490
499
  /**
491
500
  * The server provided id of the client.
492
501
  * Set once this.connected is true, otherwise undefined
493
502
  */
494
503
  public get clientId(): string | undefined {
495
- return this.connectionStateHandler.clientId;
504
+ return this._clientId;
496
505
  }
497
506
 
498
507
  /**
@@ -528,13 +537,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
528
537
  * Retrieves the audience associated with the document
529
538
  */
530
539
  public get audience(): IAudience {
531
- return this._audience;
540
+ return this.protocolHandler.audience;
532
541
  }
533
542
 
534
543
  /**
535
544
  * Returns true if container is dirty.
536
545
  * Which means data loss if container is closed at that same moment
537
- * Most likely that happens when there is no network connection to ordering service
546
+ * Most likely that happens when there is no network connection to Relay Service
538
547
  */
539
548
  public get isDirty() {
540
549
  return this._dirtyContainer;
@@ -549,6 +558,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
549
558
  constructor(
550
559
  private readonly loader: Loader,
551
560
  config: IContainerConfig,
561
+ private readonly protocolHandlerBuilder?: ProtocolHandlerBuilder,
552
562
  ) {
553
563
  super((name, error) => {
554
564
  this.mc.logger.sendErrorEvent(
@@ -558,7 +568,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
558
568
  },
559
569
  error);
560
570
  });
561
- this._audience = new Audience();
562
571
 
563
572
  this.clientDetailsOverride = config.clientDetailsOverride;
564
573
  this._resolvedUrl = config.resolvedUrl;
@@ -618,11 +627,21 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
618
627
  summarizeProtocolTree,
619
628
  };
620
629
 
621
- this.connectionStateHandler = new ConnectionStateHandler(
630
+ this._deltaManager = this.createDeltaManager();
631
+
632
+ this._clientId = config.serializedContainerState?.clientId;
633
+ this.connectionStateHandler = createConnectionStateHandler(
622
634
  {
623
- quorumClients: () => this._protocolHandler?.quorum,
624
- logConnectionStateChangeTelemetry: (value, oldState, reason) =>
625
- this.logConnectionStateChangeTelemetry(value, oldState, reason),
635
+ logger: this.mc.logger,
636
+ connectionStateChanged: (value, oldState, reason) => {
637
+ if (value === ConnectionState.Connected) {
638
+ this._clientId = this.connectionStateHandler.pendingClientId;
639
+ }
640
+ this.logConnectionStateChangeTelemetry(value, oldState, reason);
641
+ if (this._lifecycleState === "loaded") {
642
+ this.propagateConnectionState(false /* initial transition */);
643
+ }
644
+ },
626
645
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
627
646
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
628
647
  logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
@@ -634,35 +653,21 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
634
653
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
635
654
  });
636
655
  },
637
- connectionStateChanged: () => {
638
- // Fire events only if container is fully loaded and not closed
639
- if (this._lifecycleState === "loaded") {
640
- this.propagateConnectionState();
641
- }
642
- },
643
656
  },
644
- this.mc.logger,
645
- config.serializedContainerState?.clientId,
657
+ this.deltaManager,
658
+ this._clientId,
646
659
  );
647
660
 
648
661
  this.on(savedContainerEvent, () => {
649
662
  this.connectionStateHandler.containerSaved();
650
663
  });
651
664
 
652
- this._deltaManager = this.createDeltaManager();
653
- this._storage = new ContainerStorageAdapter(
654
- () => {
655
- if (this.attachState !== AttachState.Attached) {
656
- if (this.loader.services.detachedBlobStorage !== undefined) {
657
- return new BlobOnlyStorage(this.loader.services.detachedBlobStorage, this.mc.logger);
658
- }
659
- this.mc.logger.sendErrorEvent({
660
- eventName: "NoRealStorageInDetachedContainer",
661
- });
662
- throw new Error("Real storage calls not allowed in Unattached container");
663
- }
664
- return this.storageService;
665
- },
665
+ this.storageService = new ContainerStorageAdapter(
666
+ this.loader.services.detachedBlobStorage,
667
+ this.mc.logger,
668
+ this.options.summarizeProtocolTree === true
669
+ ? () => this.captureProtocolSummary()
670
+ : undefined,
666
671
  );
667
672
 
668
673
  const isDomAvailable = typeof document === "object" &&
@@ -762,7 +767,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
762
767
 
763
768
  this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
764
769
 
765
- this._storageService?.dispose();
770
+ this.storageService.dispose();
766
771
 
767
772
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
768
773
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -787,13 +792,12 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
787
792
  // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
788
793
  // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
789
794
  // a new clientId and a future container using stale pending state without the new clientId would resubmit them
790
-
791
795
  assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
792
796
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
793
797
  0x0d2 /* "resolved url should be valid Fluid url" */);
794
798
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
795
799
  assert(this._protocolHandler.attributes.term !== undefined,
796
- 0x30b /* Must have a valid protocol handler instance */);
800
+ 0x37e /* Must have a valid protocol handler instance */);
797
801
  const pendingState: IPendingContainerState = {
798
802
  pendingRuntimeState: this.context.getPendingLocalState(),
799
803
  url: this.resolvedUrl.url,
@@ -802,6 +806,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
802
806
  clientId: this.clientId,
803
807
  };
804
808
 
809
+ this.mc.logger.sendTelemetryEvent({ eventName: "CloseAndGetPendingLocalState" });
810
+
805
811
  this.close();
806
812
 
807
813
  return JSON.stringify(pendingState);
@@ -883,7 +889,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
883
889
  const resolvedUrl = this.service.resolvedUrl;
884
890
  ensureFluidResolvedUrl(resolvedUrl);
885
891
  this._resolvedUrl = resolvedUrl;
886
- await this.connectStorageService();
892
+ await this.storageService.connectToService(this.service);
887
893
 
888
894
  if (hasAttachmentBlobs) {
889
895
  // upload blobs to storage
@@ -921,8 +927,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
921
927
  this._attachState = AttachState.Attached;
922
928
  this.emit("attached");
923
929
 
924
- // Propagate current connection state through the system.
925
- this.propagateConnectionState();
926
930
  if (!this.closed) {
927
931
  this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
928
932
  }
@@ -1098,9 +1102,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1098
1102
  /**
1099
1103
  * Load container.
1100
1104
  *
1101
- * @param specifiedVersion - one of the following
1102
- * - undefined - fetch latest snapshot
1103
- * - otherwise, version sha to load snapshot
1105
+ * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
1104
1106
  */
1105
1107
  private async load(
1106
1108
  specifiedVersion: string | undefined,
@@ -1134,10 +1136,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1134
1136
  }
1135
1137
 
1136
1138
  if (!pendingLocalState) {
1137
- await this.connectStorageService();
1139
+ await this.storageService.connectToService(this.service);
1138
1140
  } else {
1139
1141
  // if we have pendingLocalState we can load without storage; don't wait for connection
1140
- this.connectStorageService().catch((error) => this.close(error));
1142
+ this.storageService.connectToService(this.service).catch((error) => this.close(error));
1141
1143
  }
1142
1144
 
1143
1145
  this._attachState = AttachState.Attached;
@@ -1178,14 +1180,21 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1178
1180
 
1179
1181
  // ...load in the existing quorum
1180
1182
  // Initialize the protocol handler
1181
- this._protocolHandler = pendingLocalState === undefined
1182
- ? await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot)
1183
- : await this.initializeProtocolState(
1183
+ if (pendingLocalState === undefined) {
1184
+ await this.initializeProtocolStateFromSnapshot(
1185
+ attributes,
1186
+ this.storageService,
1187
+ snapshot);
1188
+ } else {
1189
+ this.initializeProtocolState(
1184
1190
  attributes,
1185
- pendingLocalState.protocol.members,
1186
- pendingLocalState.protocol.proposals,
1187
- pendingLocalState.protocol.values,
1191
+ {
1192
+ members: pendingLocalState.protocol.members,
1193
+ proposals: pendingLocalState.protocol.proposals,
1194
+ values: pendingLocalState.protocol.values,
1195
+ }, // pending IQuorumSnapshot
1188
1196
  );
1197
+ }
1189
1198
 
1190
1199
  const codeDetails = this.getCodeDetailsFromQuorum();
1191
1200
  await this.instantiateContext(
@@ -1260,11 +1269,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1260
1269
 
1261
1270
  // Need to just seed the source data in the code quorum. Quorum itself is empty
1262
1271
  const qValues = initQuorumValuesFromCodeDetails(source);
1263
- this._protocolHandler = await this.initializeProtocolState(
1272
+ this.initializeProtocolState(
1264
1273
  attributes,
1265
- [], // members
1266
- [], // proposals
1267
- qValues,
1274
+ {
1275
+ members: [],
1276
+ proposals: [],
1277
+ values: qValues,
1278
+ }, // IQuorumSnapShot
1268
1279
  );
1269
1280
 
1270
1281
  // The load context - given we seeded the quorum - will be great
@@ -1283,24 +1294,26 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1283
1294
  }
1284
1295
 
1285
1296
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1286
- this._storage.loadSnapshotForRehydratingContainer(snapshotTree);
1287
- const attributes = await this.getDocumentAttributes(this._storage, snapshotTree);
1297
+ this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1298
+ const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1288
1299
 
1289
1300
  await this.attachDeltaManagerOpHandler(attributes);
1290
1301
 
1291
1302
  // Initialize the protocol handler
1292
1303
  const baseTree = getProtocolSnapshotTree(snapshotTree);
1293
1304
  const qValues = await readAndParse<[string, ICommittedProposal][]>(
1294
- this._storage,
1305
+ this.storageService,
1295
1306
  baseTree.blobs.quorumValues,
1296
1307
  );
1297
1308
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1298
- this._protocolHandler =
1299
- await this.initializeProtocolState(
1300
- attributes,
1301
- [], // members
1302
- [], // proposals
1303
- codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : []);
1309
+ this.initializeProtocolState(
1310
+ attributes,
1311
+ {
1312
+ members: [],
1313
+ proposals: [],
1314
+ values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1315
+ }, // IQuorumSnapShot
1316
+ );
1304
1317
 
1305
1318
  await this.instantiateContextDetached(
1306
1319
  true, // existing
@@ -1310,28 +1323,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1310
1323
  this.setLoaded();
1311
1324
  }
1312
1325
 
1313
- private async connectStorageService(): Promise<void> {
1314
- if (this._storageService !== undefined) {
1315
- return;
1316
- }
1317
-
1318
- assert(this.service !== undefined, 0x1ef /* "services must be defined" */);
1319
- const storageService = await this.service.connectToStorage();
1320
-
1321
- this._storageService =
1322
- new RetriableDocumentStorageService(storageService, this.mc.logger);
1323
-
1324
- if (this.options.summarizeProtocolTree === true) {
1325
- this.mc.logger.sendTelemetryEvent({ eventName: "summarizeProtocolTreeEnabled" });
1326
- this._storageService =
1327
- new ProtocolTreeStorageService(this._storageService, () => this.captureProtocolSummary());
1328
- }
1329
-
1330
- // ensure we did not lose that policy in the process of wrapping
1331
- assert(storageService.policies?.minBlobSize === this.storageService.policies?.minBlobSize,
1332
- 0x0e0 /* "lost minBlobSize policy" */);
1333
- }
1334
-
1335
1326
  private async getDocumentAttributes(
1336
1327
  storage: IDocumentStorageService,
1337
1328
  tree: ISnapshotTree | undefined,
@@ -1363,45 +1354,40 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1363
1354
  attributes: IDocumentAttributes,
1364
1355
  storage: IDocumentStorageService,
1365
1356
  snapshot: ISnapshotTree | undefined,
1366
- ): Promise<IProtocolHandler> {
1367
- let members: [string, ISequencedClient][] = [];
1368
- let proposals: [number, ISequencedProposal, string[]][] = [];
1369
- let values: [string, any][] = [];
1357
+ ): Promise<void> {
1358
+ const quorumSnapshot: IQuorumSnapshot = {
1359
+ members: [],
1360
+ proposals: [],
1361
+ values: [],
1362
+ };
1370
1363
 
1371
1364
  if (snapshot !== undefined) {
1372
1365
  const baseTree = getProtocolSnapshotTree(snapshot);
1373
- [members, proposals, values] = await Promise.all([
1366
+ [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
1374
1367
  readAndParse<[string, ISequencedClient][]>(storage, baseTree.blobs.quorumMembers),
1375
1368
  readAndParse<[number, ISequencedProposal, string[]][]>(storage, baseTree.blobs.quorumProposals),
1376
1369
  readAndParse<[string, ICommittedProposal][]>(storage, baseTree.blobs.quorumValues),
1377
1370
  ]);
1378
1371
  }
1379
1372
 
1380
- const protocolHandler = await this.initializeProtocolState(
1381
- attributes,
1382
- members,
1383
- proposals,
1384
- values);
1385
-
1386
- return protocolHandler;
1373
+ this.initializeProtocolState(attributes, quorumSnapshot);
1387
1374
  }
1388
1375
 
1389
- private async initializeProtocolState(
1376
+ private initializeProtocolState(
1390
1377
  attributes: IDocumentAttributes,
1391
- members: [string, ISequencedClient][],
1392
- proposals: [number, ISequencedProposal, string[]][],
1393
- values: [string, any][],
1394
- ): Promise<IProtocolHandler> {
1395
- const protocol = new ProtocolOpHandlerWithClientValidation(
1396
- attributes.minimumSequenceNumber,
1397
- attributes.sequenceNumber,
1398
- attributes.term,
1399
- members,
1400
- proposals,
1401
- values,
1402
- (key, value) => this.submitMessage(MessageType.Propose, { key, value }),
1378
+ quorumSnapshot: IQuorumSnapshot,
1379
+ ): void {
1380
+ const protocolHandlerBuilder =
1381
+ this.protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
1382
+ const protocol = protocolHandlerBuilder(
1383
+ attributes,
1384
+ quorumSnapshot,
1385
+ (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1386
+ this._initialClients ?? [],
1403
1387
  );
1404
1388
 
1389
+ this._initialClients = undefined;
1390
+
1405
1391
  const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1406
1392
 
1407
1393
  protocol.quorum.on("error", (error) => {
@@ -1432,8 +1418,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1432
1418
  });
1433
1419
  }
1434
1420
  });
1435
-
1436
- return protocol;
1421
+ // we need to make sure this member get set in a synchronous context,
1422
+ // or other things can happen after the object that will be set is created, but not yet set
1423
+ // this was breaking this._initialClients handling
1424
+ //
1425
+ this._protocolHandler = protocol;
1437
1426
  }
1438
1427
 
1439
1428
  private captureProtocolSummary(): ISummaryTree {
@@ -1522,12 +1511,19 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1522
1511
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1523
1512
  deltaManager.inboundSignal.pause();
1524
1513
 
1525
- deltaManager.on("connect", (details: IConnectionDetails, opsBehind?: number) => {
1526
- // Back-compat for new client and old server.
1527
- this._audience.clear();
1514
+ deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
1515
+ if (this._protocolHandler === undefined) {
1516
+ // Store the initial clients so that they can be submitted to the
1517
+ // protocol handler when it is created.
1518
+ this._initialClients = details.initialClients;
1519
+ } else {
1520
+ // When reconnecting, the protocol handler is already created,
1521
+ // so we can update the audience right now.
1522
+ this._protocolHandler.audience.clear();
1528
1523
 
1529
- for (const priorClient of details.initialClients ?? []) {
1530
- this._audience.addMember(priorClient.clientId, priorClient.client);
1524
+ for (const priorClient of details.initialClients ?? []) {
1525
+ this._protocolHandler.audience.addMember(priorClient.clientId, priorClient.client);
1526
+ }
1531
1527
  }
1532
1528
 
1533
1529
  this.connectionStateHandler.receivedConnectEvent(
@@ -1552,6 +1548,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1552
1548
  });
1553
1549
 
1554
1550
  deltaManager.on("readonly", (readonly) => {
1551
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1555
1552
  this.emit("readonly", readonly);
1556
1553
  });
1557
1554
 
@@ -1606,11 +1603,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1606
1603
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1607
1604
  }
1608
1605
  }
1609
- if (this.firstConnection) {
1610
- connectionInitiationReason = "InitialConnect";
1611
- } else {
1612
- connectionInitiationReason = "AutoReconnect";
1613
- }
1606
+ connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1614
1607
  }
1615
1608
 
1616
1609
  this.mc.logger.sendPerformanceEvent({
@@ -1636,7 +1629,17 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1636
1629
  }
1637
1630
  }
1638
1631
 
1639
- private propagateConnectionState() {
1632
+ private propagateConnectionState(initialTransition: boolean) {
1633
+ // When container loaded, we want to propagate initial connection state.
1634
+ // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1635
+ // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1636
+ if (!initialTransition &&
1637
+ this.connectionState !== ConnectionState.Connected &&
1638
+ this.connectionState !== ConnectionState.Disconnected) {
1639
+ return;
1640
+ }
1641
+ const state = this.connectionState === ConnectionState.Connected;
1642
+
1640
1643
  const logOpsOnReconnect: boolean =
1641
1644
  this.connectionState === ConnectionState.Connected &&
1642
1645
  !this.firstConnection &&
@@ -1645,13 +1648,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1645
1648
  this.messageCountAfterDisconnection = 0;
1646
1649
  }
1647
1650
 
1648
- const state = this.connectionState === ConnectionState.Connected;
1649
-
1650
1651
  // Both protocol and context should not be undefined if we got so far.
1651
1652
 
1652
- if (this._context?.disposed === false) {
1653
- this.context.setConnectionState(state, this.clientId);
1654
- }
1653
+ this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1655
1654
  this.protocolHandler.setConnectionState(state, this.clientId);
1656
1655
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1657
1656
 
@@ -1661,35 +1660,53 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1661
1660
  }
1662
1661
  }
1663
1662
 
1663
+ // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1664
1664
  private submitContainerMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1665
- const outboundMessageType: string = type;
1666
- switch (outboundMessageType) {
1665
+ switch (type) {
1667
1666
  case MessageType.Operation:
1668
- case MessageType.RemoteHelp:
1669
- break;
1670
- case MessageType.Summarize: {
1671
- // github #6451: this is only needed for staging so the server
1672
- // know when the protocol tree is included
1673
- // this can be removed once all clients send
1674
- // protocol tree by default
1675
- const summary = contents as ISummaryContent;
1676
- if (summary.details === undefined) {
1677
- summary.details = {};
1678
- }
1679
- summary.details.includesProtocolTree =
1680
- this.options.summarizeProtocolTree === true;
1681
- break;
1682
- }
1667
+ return this.submitMessage(
1668
+ type,
1669
+ JSON.stringify(contents),
1670
+ batch,
1671
+ metadata);
1672
+ case MessageType.Summarize:
1673
+ return this.submitSummaryMessage(contents as unknown as ISummaryContent);
1683
1674
  default:
1684
1675
  this.close(new GenericError("invalidContainerSubmitOpType",
1685
1676
  undefined /* error */,
1686
1677
  { messageType: type }));
1687
1678
  return -1;
1688
1679
  }
1689
- return this.submitMessage(type, contents, batch, metadata);
1690
1680
  }
1691
1681
 
1692
- private submitMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1682
+ /** @returns clientSequenceNumber of last message in a batch */
1683
+ private submitBatch(batch: IBatchMessage[]): number {
1684
+ let clientSequenceNumber = -1;
1685
+ for (const message of batch) {
1686
+ clientSequenceNumber = this.submitMessage(
1687
+ MessageType.Operation,
1688
+ message.contents,
1689
+ true, // batch
1690
+ message.metadata);
1691
+ }
1692
+ this._deltaManager.flush();
1693
+ return clientSequenceNumber;
1694
+ }
1695
+
1696
+ private submitSummaryMessage(summary: ISummaryContent) {
1697
+ // github #6451: this is only needed for staging so the server
1698
+ // know when the protocol tree is included
1699
+ // this can be removed once all clients send
1700
+ // protocol tree by default
1701
+ if (summary.details === undefined) {
1702
+ summary.details = {};
1703
+ }
1704
+ summary.details.includesProtocolTree =
1705
+ this.options.summarizeProtocolTree === true;
1706
+ return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1707
+ }
1708
+
1709
+ private submitMessage(type: MessageType, contents?: string, batch?: boolean, metadata?: any): number {
1693
1710
  if (this.connectionState !== ConnectionState.Connected) {
1694
1711
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1695
1712
  return -1;
@@ -1700,28 +1717,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1700
1717
  return this._deltaManager.submit(type, contents, batch, metadata);
1701
1718
  }
1702
1719
 
1703
- private processRemoteMessage(message: ISequencedDocumentMessage): IProcessMessageResult {
1720
+ private processRemoteMessage(message: ISequencedDocumentMessage) {
1704
1721
  const local = this.clientId === message.clientId;
1705
1722
 
1706
1723
  // Allow the protocol handler to process the message
1707
- let result: IProcessMessageResult = { immediateNoOp: false };
1708
- try {
1709
- result = this.protocolHandler.processMessage(message, local);
1710
- } catch (error) {
1711
- this.close(wrapError(error, (errorMessage) =>
1712
- new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1713
- }
1724
+ const result = this.protocolHandler.processMessage(message, local);
1714
1725
 
1715
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1716
- if (isUnpackedRuntimeMessage(message) && !isRuntimeMessage(message)) {
1717
- this.mc.logger.sendTelemetryEvent(
1718
- { eventName: "UnpackedRuntimeMessage", type: message.type });
1719
- }
1720
- // Forward non system messages to the loaded runtime for processing
1721
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1722
- if (isRuntimeMessage(message) || isUnpackedRuntimeMessage(message)) {
1723
- this.context.process(message, local, undefined);
1724
- }
1726
+ // Forward messages to the loaded runtime for processing
1727
+ this.context.process(message, local, undefined);
1725
1728
 
1726
1729
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1727
1730
  if (this.activeConnection()) {
@@ -1734,10 +1737,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1734
1737
  this.serviceConfiguration !== undefined,
1735
1738
  0x2e4 /* "there should be service config for active connection" */);
1736
1739
  this.collabWindowTracker = new CollabWindowTracker(
1737
- (type, contents) => {
1740
+ (type) => {
1738
1741
  assert(this.activeConnection(),
1739
1742
  0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1740
- this.submitMessage(type, contents);
1743
+ this.submitMessage(type);
1741
1744
  },
1742
1745
  this.serviceConfiguration.noopTimeFrequency,
1743
1746
  this.serviceConfiguration.noopCountFrequency,
@@ -1747,8 +1750,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1747
1750
  }
1748
1751
 
1749
1752
  this.emit("op", message);
1750
-
1751
- return result;
1752
1753
  }
1753
1754
 
1754
1755
  private submitSignal(message: any) {
@@ -1758,14 +1759,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1758
1759
  private processSignal(message: ISignalMessage) {
1759
1760
  // No clientId indicates a system signal message.
1760
1761
  if (message.clientId === null) {
1761
- const innerContent = message.content as { content: any; type: string; };
1762
- if (innerContent.type === MessageType.ClientJoin) {
1763
- const newClient = innerContent.content as ISignalClient;
1764
- this._audience.addMember(newClient.clientId, newClient.client);
1765
- } else if (innerContent.type === MessageType.ClientLeave) {
1766
- const leftClientId = innerContent.content as string;
1767
- this._audience.removeMember(leftClientId);
1768
- }
1762
+ this.protocolHandler.processSignal(message);
1769
1763
  } else {
1770
1764
  const local = this.clientId === message.clientId;
1771
1765
  this.context.processSignal(message, local);
@@ -1831,6 +1825,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1831
1825
  new QuorumProxy(this.protocolHandler.quorum),
1832
1826
  loader,
1833
1827
  (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata),
1828
+ (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
1829
+ (batch: IBatchMessage[]) => this.submitBatch(batch),
1834
1830
  (message) => this.submitSignal(message),
1835
1831
  (error?: ICriticalContainerError) => this.close(error),
1836
1832
  Container.version,
@@ -1853,4 +1849,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1853
1849
  private logContainerError(warning: ContainerWarning) {
1854
1850
  this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1855
1851
  }
1852
+
1853
+ /**
1854
+ * Set the connected state of the ContainerContext
1855
+ * This controls the "connected" state of the ContainerRuntime as well
1856
+ * @param state - Is the container currently connected?
1857
+ * @param readonly - Is the container in readonly mode?
1858
+ */
1859
+ private setContextConnectedState(state: boolean, readonly: boolean): void {
1860
+ if (this._context?.disposed === false) {
1861
+ /**
1862
+ * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1863
+ * ops getting through to the DeltaManager.
1864
+ * The ContainerRuntime's "connected" state simply means it is ok to send ops
1865
+ * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1866
+ */
1867
+ this.context.setConnectionState(state && !readonly, this.clientId);
1868
+ }
1869
+ }
1856
1870
  }