@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/lib/container.js CHANGED
@@ -7,9 +7,8 @@ import merge from "lodash/merge";
7
7
  import { v4 as uuid } from "uuid";
8
8
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
9
9
  import { AttachState, isFluidCodeDetails, } from "@fluidframework/container-definitions";
10
- import { DataCorruptionError, extractSafePropertiesFromMessage, GenericError, UsageError, } from "@fluidframework/container-utils";
11
- import { readAndParse, OnlineStatus, isOnline, ensureFluidResolvedUrl, combineAppAndProtocolSummary, runWithRetry, isFluidResolvedUrl, isRuntimeMessage, isUnpackedRuntimeMessage, } from "@fluidframework/driver-utils";
12
- import { ProtocolOpHandlerWithClientValidation, } from "@fluidframework/protocol-base";
10
+ import { GenericError, UsageError, } from "@fluidframework/container-utils";
11
+ import { readAndParse, OnlineStatus, isOnline, ensureFluidResolvedUrl, combineAppAndProtocolSummary, runWithRetry, isFluidResolvedUrl, } from "@fluidframework/driver-utils";
13
12
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
14
13
  import { ChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, TelemetryLogger, connectedEventName, disconnectedEventName, normalizeError, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
15
14
  import { Audience } from "./audience";
@@ -19,27 +18,31 @@ import { DeltaManager } from "./deltaManager";
19
18
  import { DeltaManagerProxy } from "./deltaManagerProxy";
20
19
  import { RelativeLoader } from "./loader";
21
20
  import { pkgVersion } from "./packageVersion";
22
- import { ConnectionStateHandler } from "./connectionStateHandler";
23
- import { RetriableDocumentStorageService } from "./retriableDocumentStorageService";
24
- import { ProtocolTreeStorageService } from "./protocolTreeDocumentStorageService";
25
- import { BlobOnlyStorage, ContainerStorageAdapter } from "./containerStorageAdapter";
21
+ import { ContainerStorageAdapter } from "./containerStorageAdapter";
22
+ import { createConnectionStateHandler, } from "./connectionStateHandler";
26
23
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
27
24
  import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy } from "./quorum";
28
25
  import { CollabWindowTracker } from "./collabWindowTracker";
29
26
  import { ConnectionManager } from "./connectionManager";
30
27
  import { ConnectionState } from "./connectionState";
28
+ import { ProtocolHandler, } from "./protocol";
31
29
  const detachedContainerRefSeqNumber = 0;
32
30
  const dirtyContainerEvent = "dirty";
33
31
  const savedContainerEvent = "saved";
34
32
  /**
35
- * Waits until container connects to delta storage and gets up-to-date
33
+ * Waits until container connects to delta storage and gets up-to-date.
34
+ *
36
35
  * Useful when resolving URIs and hitting 404, due to container being loaded from (stale) snapshot and not being
37
36
  * up to date. Host may chose to wait in such case and retry resolving URI.
37
+ *
38
38
  * Warning: Will wait infinitely for connection to establish if there is no connection.
39
39
  * May result in deadlock if Container.disconnect() is called and never followed by a call to Container.connect().
40
- * @returns true: container is up to date, it processed all the ops that were know at the time of first connection
41
- * false: storage does not provide indication of how far the client is. Container processed
42
- * all the ops known to it, but it maybe still behind.
40
+ *
41
+ * @returns `true`: container is up to date, it processed all the ops that were know at the time of first connection.
42
+ *
43
+ * `false`: storage does not provide indication of how far the client is. Container processed all the ops known to it,
44
+ * but it maybe still behind.
45
+ *
43
46
  * @throws an error beginning with `"Container closed"` if the container is closed before it catches up.
44
47
  */
45
48
  export async function waitContainerToCatchUp(container) {
@@ -57,6 +60,10 @@ export async function waitContainerToCatchUp(container) {
57
60
  : new GenericError(baseMessage));
58
61
  };
59
62
  container.on("closed", closedCallback);
63
+ // Depending on config, transition to "connected" state may include the guarantee
64
+ // that all known ops have been processed. If so, we may introduce additional wait here.
65
+ // Waiting for "connected" state in either case gets us at least to our own Join op
66
+ // which is a reasonable approximation of "caught up"
60
67
  const waitForOps = () => {
61
68
  assert(container.connectionState === ConnectionState.CatchingUp
62
69
  || container.connectionState === ConnectionState.Connected, 0x0cd /* "Container disconnected while waiting for ops!" */);
@@ -113,7 +120,7 @@ async function ReportIfTooLong(logger, eventName, action) {
113
120
  }
114
121
  const summarizerClientType = "summarizer";
115
122
  export class Container extends EventEmitterWithErrorHandling {
116
- constructor(loader, config) {
123
+ constructor(loader, config, protocolHandlerBuilder) {
117
124
  var _a, _b;
118
125
  super((name, error) => {
119
126
  this.mc.logger.sendErrorEvent({
@@ -122,6 +129,7 @@ export class Container extends EventEmitterWithErrorHandling {
122
129
  }, error);
123
130
  });
124
131
  this.loader = loader;
132
+ this.protocolHandlerBuilder = protocolHandlerBuilder;
125
133
  // Tells if container can reconnect on losing fist connection
126
134
  // If false, container gets closed on loss of connection.
127
135
  this._canReconnect = true;
@@ -135,7 +143,6 @@ export class Container extends EventEmitterWithErrorHandling {
135
143
  this.attachStarted = false;
136
144
  this._dirtyContainer = false;
137
145
  this.setAutoReconnectTime = performance.now();
138
- this._audience = new Audience();
139
146
  this.clientDetailsOverride = config.clientDetailsOverride;
140
147
  this._resolvedUrl = config.resolvedUrl;
141
148
  if (config.canReconnect !== undefined) {
@@ -179,9 +186,19 @@ export class Container extends EventEmitterWithErrorHandling {
179
186
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
180
187
  const summarizeProtocolTree = (_a = this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree")) !== null && _a !== void 0 ? _a : this.loader.services.options.summarizeProtocolTree;
181
188
  this.options = Object.assign(Object.assign({}, this.loader.services.options), { summarizeProtocolTree });
182
- this.connectionStateHandler = new ConnectionStateHandler({
183
- quorumClients: () => { var _a; return (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.quorum; },
184
- logConnectionStateChangeTelemetry: (value, oldState, reason) => this.logConnectionStateChangeTelemetry(value, oldState, reason),
189
+ this._deltaManager = this.createDeltaManager();
190
+ this._clientId = (_b = config.serializedContainerState) === null || _b === void 0 ? void 0 : _b.clientId;
191
+ this.connectionStateHandler = createConnectionStateHandler({
192
+ logger: this.mc.logger,
193
+ connectionStateChanged: (value, oldState, reason) => {
194
+ if (value === ConnectionState.Connected) {
195
+ this._clientId = this.connectionStateHandler.pendingClientId;
196
+ }
197
+ this.logConnectionStateChangeTelemetry(value, oldState, reason);
198
+ if (this._lifecycleState === "loaded") {
199
+ this.propagateConnectionState(false /* initial transition */);
200
+ }
201
+ },
185
202
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
186
203
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
187
204
  logConnectionIssue: (eventName, details) => {
@@ -189,29 +206,13 @@ export class Container extends EventEmitterWithErrorHandling {
189
206
  // its own join op. Attempt recovery option.
190
207
  this._deltaManager.logConnectionIssue(Object.assign({ eventName, duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
191
208
  },
192
- connectionStateChanged: () => {
193
- // Fire events only if container is fully loaded and not closed
194
- if (this._lifecycleState === "loaded") {
195
- this.propagateConnectionState();
196
- }
197
- },
198
- }, this.mc.logger, (_b = config.serializedContainerState) === null || _b === void 0 ? void 0 : _b.clientId);
209
+ }, this.deltaManager, this._clientId);
199
210
  this.on(savedContainerEvent, () => {
200
211
  this.connectionStateHandler.containerSaved();
201
212
  });
202
- this._deltaManager = this.createDeltaManager();
203
- this._storage = new ContainerStorageAdapter(() => {
204
- if (this.attachState !== AttachState.Attached) {
205
- if (this.loader.services.detachedBlobStorage !== undefined) {
206
- return new BlobOnlyStorage(this.loader.services.detachedBlobStorage, this.mc.logger);
207
- }
208
- this.mc.logger.sendErrorEvent({
209
- eventName: "NoRealStorageInDetachedContainer",
210
- });
211
- throw new Error("Real storage calls not allowed in Unattached container");
212
- }
213
- return this.storageService;
214
- });
213
+ this.storageService = new ContainerStorageAdapter(this.loader.services.detachedBlobStorage, this.mc.logger, this.options.summarizeProtocolTree === true
214
+ ? () => this.captureProtocolSummary()
215
+ : undefined);
215
216
  const isDomAvailable = typeof document === "object" &&
216
217
  document !== null &&
217
218
  typeof document.addEventListener === "function" &&
@@ -268,13 +269,13 @@ export class Container extends EventEmitterWithErrorHandling {
268
269
  /**
269
270
  * Load an existing container.
270
271
  */
271
- static async load(loader, loadOptions, pendingLocalState) {
272
+ static async load(loader, loadOptions, pendingLocalState, protocolHandlerBuilder) {
272
273
  const container = new Container(loader, {
273
274
  clientDetailsOverride: loadOptions.clientDetailsOverride,
274
275
  resolvedUrl: loadOptions.resolvedUrl,
275
276
  canReconnect: loadOptions.canReconnect,
276
277
  serializedContainerState: pendingLocalState,
277
- });
278
+ }, protocolHandlerBuilder);
278
279
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
279
280
  var _a, _b;
280
281
  const version = loadOptions.version;
@@ -308,8 +309,8 @@ export class Container extends EventEmitterWithErrorHandling {
308
309
  /**
309
310
  * Create a new container in a detached state.
310
311
  */
311
- static async createDetached(loader, codeDetails) {
312
- const container = new Container(loader, {});
312
+ static async createDetached(loader, codeDetails, protocolHandlerBuilder) {
313
+ const container = new Container(loader, {}, protocolHandlerBuilder);
313
314
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "CreateDetached" }, async (_event) => {
314
315
  await container.createDetached(codeDetails);
315
316
  return container;
@@ -319,8 +320,8 @@ export class Container extends EventEmitterWithErrorHandling {
319
320
  * Create a new container in a detached state that is initialized with a
320
321
  * snapshot from a previous detached container.
321
322
  */
322
- static async rehydrateDetachedFromSnapshot(loader, snapshot) {
323
- const container = new Container(loader, {});
323
+ static async rehydrateDetachedFromSnapshot(loader, snapshot, protocolHandlerBuilder) {
324
+ const container = new Container(loader, {}, protocolHandlerBuilder);
324
325
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "RehydrateDetachedFromSnapshot" }, async (_event) => {
325
326
  const deserializedSummary = JSON.parse(snapshot);
326
327
  await container.rehydrateDetachedFromSnapshot(deserializedSummary);
@@ -332,7 +333,7 @@ export class Container extends EventEmitterWithErrorHandling {
332
333
  // Only transition states if currently loading
333
334
  if (this._lifecycleState === "loading") {
334
335
  // Propagate current connection state through the system.
335
- this.propagateConnectionState();
336
+ this.propagateConnectionState(true /* initial transition */);
336
337
  this._lifecycleState = "loaded";
337
338
  }
338
339
  }
@@ -340,13 +341,7 @@ export class Container extends EventEmitterWithErrorHandling {
340
341
  return (this._lifecycleState === "closing" || this._lifecycleState === "closed");
341
342
  }
342
343
  get storage() {
343
- return this._storage;
344
- }
345
- get storageService() {
346
- if (this._storageService === undefined) {
347
- throw new Error("Attempted to access storageService before it was defined");
348
- }
349
- return this._storageService;
344
+ return this.storageService;
350
345
  }
351
346
  get context() {
352
347
  if (this._context === undefined) {
@@ -387,7 +382,7 @@ export class Container extends EventEmitterWithErrorHandling {
387
382
  return this.connectionStateHandler.connectionState;
388
383
  }
389
384
  get connected() {
390
- return this.connectionStateHandler.connected;
385
+ return this.connectionStateHandler.connectionState === ConnectionState.Connected;
391
386
  }
392
387
  /**
393
388
  * Service configuration details. If running in offline mode will be undefined otherwise will contain service
@@ -401,7 +396,7 @@ export class Container extends EventEmitterWithErrorHandling {
401
396
  * Set once this.connected is true, otherwise undefined
402
397
  */
403
398
  get clientId() {
404
- return this.connectionStateHandler.clientId;
399
+ return this._clientId;
405
400
  }
406
401
  /**
407
402
  * The server provided claims of the client.
@@ -433,12 +428,12 @@ export class Container extends EventEmitterWithErrorHandling {
433
428
  * Retrieves the audience associated with the document
434
429
  */
435
430
  get audience() {
436
- return this._audience;
431
+ return this.protocolHandler.audience;
437
432
  }
438
433
  /**
439
434
  * Returns true if container is dirty.
440
435
  * Which means data loss if container is closed at that same moment
441
- * Most likely that happens when there is no network connection to ordering service
436
+ * Most likely that happens when there is no network connection to Relay Service
442
437
  */
443
438
  get isDirty() {
444
439
  return this._dirtyContainer;
@@ -463,7 +458,7 @@ export class Container extends EventEmitterWithErrorHandling {
463
458
  assert(this._lifecycleState === "closed", 0x314 /* Container properly closed */);
464
459
  }
465
460
  closeCore(error) {
466
- var _a, _b, _c, _d;
461
+ var _a, _b, _c;
467
462
  assert(!this.closed, 0x315 /* re-entrancy */);
468
463
  try {
469
464
  // Ensure that we raise all key events even if one of these throws
@@ -478,11 +473,11 @@ export class Container extends EventEmitterWithErrorHandling {
478
473
  (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
479
474
  this.connectionStateHandler.dispose();
480
475
  (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
481
- (_c = this._storageService) === null || _c === void 0 ? void 0 : _c.dispose();
476
+ this.storageService.dispose();
482
477
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
483
478
  // about file, like file being overwritten in storage, but client having stale local cache.
484
479
  // Driver need to ensure all caches are cleared on critical errors
485
- (_d = this.service) === null || _d === void 0 ? void 0 : _d.dispose(error);
480
+ (_c = this.service) === null || _c === void 0 ? void 0 : _c.dispose(error);
486
481
  }
487
482
  catch (exception) {
488
483
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
@@ -504,7 +499,7 @@ export class Container extends EventEmitterWithErrorHandling {
504
499
  assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
505
500
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
506
501
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
507
- assert(this._protocolHandler.attributes.term !== undefined, 0x30b /* Must have a valid protocol handler instance */);
502
+ assert(this._protocolHandler.attributes.term !== undefined, 0x37e /* Must have a valid protocol handler instance */);
508
503
  const pendingState = {
509
504
  pendingRuntimeState: this.context.getPendingLocalState(),
510
505
  url: this.resolvedUrl.url,
@@ -512,6 +507,7 @@ export class Container extends EventEmitterWithErrorHandling {
512
507
  term: this._protocolHandler.attributes.term,
513
508
  clientId: this.clientId,
514
509
  };
510
+ this.mc.logger.sendTelemetryEvent({ eventName: "CloseAndGetPendingLocalState" });
515
511
  this.close();
516
512
  return JSON.stringify(pendingState);
517
513
  }
@@ -568,7 +564,7 @@ export class Container extends EventEmitterWithErrorHandling {
568
564
  const resolvedUrl = this.service.resolvedUrl;
569
565
  ensureFluidResolvedUrl(resolvedUrl);
570
566
  this._resolvedUrl = resolvedUrl;
571
- await this.connectStorageService();
567
+ await this.storageService.connectToService(this.service);
572
568
  if (hasAttachmentBlobs) {
573
569
  // upload blobs to storage
574
570
  assert(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
@@ -598,8 +594,6 @@ export class Container extends EventEmitterWithErrorHandling {
598
594
  }
599
595
  this._attachState = AttachState.Attached;
600
596
  this.emit("attached");
601
- // Propagate current connection state through the system.
602
- this.propagateConnectionState();
603
597
  if (!this.closed) {
604
598
  this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
605
599
  }
@@ -738,9 +732,7 @@ export class Container extends EventEmitterWithErrorHandling {
738
732
  /**
739
733
  * Load container.
740
734
  *
741
- * @param specifiedVersion - one of the following
742
- * - undefined - fetch latest snapshot
743
- * - otherwise, version sha to load snapshot
735
+ * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
744
736
  */
745
737
  async load(specifiedVersion, loadMode, pendingLocalState) {
746
738
  if (this._resolvedUrl === undefined) {
@@ -763,11 +755,11 @@ export class Container extends EventEmitterWithErrorHandling {
763
755
  this.connectToDeltaStream(connectionArgs);
764
756
  }
765
757
  if (!pendingLocalState) {
766
- await this.connectStorageService();
758
+ await this.storageService.connectToService(this.service);
767
759
  }
768
760
  else {
769
761
  // if we have pendingLocalState we can load without storage; don't wait for connection
770
- this.connectStorageService().catch((error) => this.close(error));
762
+ this.storageService.connectToService(this.service).catch((error) => this.close(error));
771
763
  }
772
764
  this._attachState = AttachState.Attached;
773
765
  // Fetch specified snapshot.
@@ -802,9 +794,16 @@ export class Container extends EventEmitterWithErrorHandling {
802
794
  }
803
795
  // ...load in the existing quorum
804
796
  // Initialize the protocol handler
805
- this._protocolHandler = pendingLocalState === undefined
806
- ? await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot)
807
- : await this.initializeProtocolState(attributes, pendingLocalState.protocol.members, pendingLocalState.protocol.proposals, pendingLocalState.protocol.values);
797
+ if (pendingLocalState === undefined) {
798
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageService, snapshot);
799
+ }
800
+ else {
801
+ this.initializeProtocolState(attributes, {
802
+ members: pendingLocalState.protocol.members,
803
+ proposals: pendingLocalState.protocol.proposals,
804
+ values: pendingLocalState.protocol.values,
805
+ });
806
+ }
808
807
  const codeDetails = this.getCodeDetailsFromQuorum();
809
808
  await this.instantiateContext(true, // existing
810
809
  codeDetails, snapshot, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.pendingRuntimeState);
@@ -858,9 +857,11 @@ export class Container extends EventEmitterWithErrorHandling {
858
857
  await this.attachDeltaManagerOpHandler(attributes);
859
858
  // Need to just seed the source data in the code quorum. Quorum itself is empty
860
859
  const qValues = initQuorumValuesFromCodeDetails(source);
861
- this._protocolHandler = await this.initializeProtocolState(attributes, [], // members
862
- [], // proposals
863
- qValues);
860
+ this.initializeProtocolState(attributes, {
861
+ members: [],
862
+ proposals: [],
863
+ values: qValues,
864
+ });
864
865
  // The load context - given we seeded the quorum - will be great
865
866
  await this.instantiateContextDetached(false);
866
867
  this.setLoaded();
@@ -871,38 +872,22 @@ export class Container extends EventEmitterWithErrorHandling {
871
872
  delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
872
873
  }
873
874
  const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
874
- this._storage.loadSnapshotForRehydratingContainer(snapshotTree);
875
- const attributes = await this.getDocumentAttributes(this._storage, snapshotTree);
875
+ this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
876
+ const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
876
877
  await this.attachDeltaManagerOpHandler(attributes);
877
878
  // Initialize the protocol handler
878
879
  const baseTree = getProtocolSnapshotTree(snapshotTree);
879
- const qValues = await readAndParse(this._storage, baseTree.blobs.quorumValues);
880
+ const qValues = await readAndParse(this.storageService, baseTree.blobs.quorumValues);
880
881
  const codeDetails = getCodeDetailsFromQuorumValues(qValues);
881
- this._protocolHandler =
882
- await this.initializeProtocolState(attributes, [], // members
883
- [], // proposals
884
- codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : []);
882
+ this.initializeProtocolState(attributes, {
883
+ members: [],
884
+ proposals: [],
885
+ values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
886
+ });
885
887
  await this.instantiateContextDetached(true, // existing
886
888
  snapshotTree);
887
889
  this.setLoaded();
888
890
  }
889
- async connectStorageService() {
890
- var _a, _b;
891
- if (this._storageService !== undefined) {
892
- return;
893
- }
894
- assert(this.service !== undefined, 0x1ef /* "services must be defined" */);
895
- const storageService = await this.service.connectToStorage();
896
- this._storageService =
897
- new RetriableDocumentStorageService(storageService, this.mc.logger);
898
- if (this.options.summarizeProtocolTree === true) {
899
- this.mc.logger.sendTelemetryEvent({ eventName: "summarizeProtocolTreeEnabled" });
900
- this._storageService =
901
- new ProtocolTreeStorageService(this._storageService, () => this.captureProtocolSummary());
902
- }
903
- // ensure we did not lose that policy in the process of wrapping
904
- assert(((_a = storageService.policies) === null || _a === void 0 ? void 0 : _a.minBlobSize) === ((_b = this.storageService.policies) === null || _b === void 0 ? void 0 : _b.minBlobSize), 0x0e0 /* "lost minBlobSize policy" */);
905
- }
906
891
  async getDocumentAttributes(storage, tree) {
907
892
  if (tree === undefined) {
908
893
  return {
@@ -923,22 +908,26 @@ export class Container extends EventEmitterWithErrorHandling {
923
908
  return attributes;
924
909
  }
925
910
  async initializeProtocolStateFromSnapshot(attributes, storage, snapshot) {
926
- let members = [];
927
- let proposals = [];
928
- let values = [];
911
+ const quorumSnapshot = {
912
+ members: [],
913
+ proposals: [],
914
+ values: [],
915
+ };
929
916
  if (snapshot !== undefined) {
930
917
  const baseTree = getProtocolSnapshotTree(snapshot);
931
- [members, proposals, values] = await Promise.all([
918
+ [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
932
919
  readAndParse(storage, baseTree.blobs.quorumMembers),
933
920
  readAndParse(storage, baseTree.blobs.quorumProposals),
934
921
  readAndParse(storage, baseTree.blobs.quorumValues),
935
922
  ]);
936
923
  }
937
- const protocolHandler = await this.initializeProtocolState(attributes, members, proposals, values);
938
- return protocolHandler;
924
+ this.initializeProtocolState(attributes, quorumSnapshot);
939
925
  }
940
- async initializeProtocolState(attributes, members, proposals, values) {
941
- const protocol = new ProtocolOpHandlerWithClientValidation(attributes.minimumSequenceNumber, attributes.sequenceNumber, attributes.term, members, proposals, values, (key, value) => this.submitMessage(MessageType.Propose, { key, value }));
926
+ initializeProtocolState(attributes, quorumSnapshot) {
927
+ var _a, _b;
928
+ const protocolHandlerBuilder = (_a = this.protocolHandlerBuilder) !== null && _a !== void 0 ? _a : ((...args) => new ProtocolHandler(...args, new Audience()));
929
+ const protocol = protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })), (_b = this._initialClients) !== null && _b !== void 0 ? _b : []);
930
+ this._initialClients = undefined;
942
931
  const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
943
932
  protocol.quorum.on("error", (error) => {
944
933
  protocolLogger.sendErrorEvent(error);
@@ -963,7 +952,11 @@ export class Container extends EventEmitterWithErrorHandling {
963
952
  });
964
953
  }
965
954
  });
966
- return protocol;
955
+ // we need to make sure this member get set in a synchronous context,
956
+ // or other things can happen after the object that will be set is created, but not yet set
957
+ // this was breaking this._initialClients handling
958
+ //
959
+ this._protocolHandler = protocol;
967
960
  }
968
961
  captureProtocolSummary() {
969
962
  const quorumSnapshot = this.protocolHandler.snapshot();
@@ -1032,12 +1025,20 @@ export class Container extends EventEmitterWithErrorHandling {
1032
1025
  deltaManager.inbound.pause();
1033
1026
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
1034
1027
  deltaManager.inboundSignal.pause();
1035
- deltaManager.on("connect", (details, opsBehind) => {
1028
+ deltaManager.on("connect", (details, _opsBehind) => {
1036
1029
  var _a;
1037
- // Back-compat for new client and old server.
1038
- this._audience.clear();
1039
- for (const priorClient of (_a = details.initialClients) !== null && _a !== void 0 ? _a : []) {
1040
- this._audience.addMember(priorClient.clientId, priorClient.client);
1030
+ if (this._protocolHandler === undefined) {
1031
+ // Store the initial clients so that they can be submitted to the
1032
+ // protocol handler when it is created.
1033
+ this._initialClients = details.initialClients;
1034
+ }
1035
+ else {
1036
+ // When reconnecting, the protocol handler is already created,
1037
+ // so we can update the audience right now.
1038
+ this._protocolHandler.audience.clear();
1039
+ for (const priorClient of (_a = details.initialClients) !== null && _a !== void 0 ? _a : []) {
1040
+ this._protocolHandler.audience.addMember(priorClient.clientId, priorClient.client);
1041
+ }
1041
1042
  }
1042
1043
  this.connectionStateHandler.receivedConnectEvent(this.connectionMode, details);
1043
1044
  });
@@ -1056,6 +1057,7 @@ export class Container extends EventEmitterWithErrorHandling {
1056
1057
  this.emit("warning", warn);
1057
1058
  });
1058
1059
  deltaManager.on("readonly", (readonly) => {
1060
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1059
1061
  this.emit("readonly", readonly);
1060
1062
  });
1061
1063
  deltaManager.on("closed", (error) => {
@@ -1098,12 +1100,7 @@ export class Container extends EventEmitterWithErrorHandling {
1098
1100
  opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1099
1101
  }
1100
1102
  }
1101
- if (this.firstConnection) {
1102
- connectionInitiationReason = "InitialConnect";
1103
- }
1104
- else {
1105
- connectionInitiationReason = "AutoReconnect";
1106
- }
1103
+ connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1107
1104
  }
1108
1105
  this.mc.logger.sendPerformanceEvent(Object.assign({ eventName: `ConnectionStateChange_${ConnectionState[value]}`, from: ConnectionState[oldState], duration,
1109
1106
  durationFromDisconnected,
@@ -1114,49 +1111,64 @@ export class Container extends EventEmitterWithErrorHandling {
1114
1111
  this.firstConnection = false;
1115
1112
  }
1116
1113
  }
1117
- propagateConnectionState() {
1114
+ propagateConnectionState(initialTransition) {
1118
1115
  var _a;
1116
+ // When container loaded, we want to propagate initial connection state.
1117
+ // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1118
+ // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1119
+ if (!initialTransition &&
1120
+ this.connectionState !== ConnectionState.Connected &&
1121
+ this.connectionState !== ConnectionState.Disconnected) {
1122
+ return;
1123
+ }
1124
+ const state = this.connectionState === ConnectionState.Connected;
1119
1125
  const logOpsOnReconnect = this.connectionState === ConnectionState.Connected &&
1120
1126
  !this.firstConnection &&
1121
1127
  this.connectionMode === "write";
1122
1128
  if (logOpsOnReconnect) {
1123
1129
  this.messageCountAfterDisconnection = 0;
1124
1130
  }
1125
- const state = this.connectionState === ConnectionState.Connected;
1126
1131
  // Both protocol and context should not be undefined if we got so far.
1127
- if (((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) === false) {
1128
- this.context.setConnectionState(state, this.clientId);
1129
- }
1132
+ this.setContextConnectedState(state, (_a = this._deltaManager.connectionManager.readOnlyInfo.readonly) !== null && _a !== void 0 ? _a : false);
1130
1133
  this.protocolHandler.setConnectionState(state, this.clientId);
1131
1134
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1132
1135
  if (logOpsOnReconnect) {
1133
1136
  this.mc.logger.sendTelemetryEvent({ eventName: "OpsSentOnReconnect", count: this.messageCountAfterDisconnection });
1134
1137
  }
1135
1138
  }
1139
+ // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1136
1140
  submitContainerMessage(type, contents, batch, metadata) {
1137
- const outboundMessageType = type;
1138
- switch (outboundMessageType) {
1141
+ switch (type) {
1139
1142
  case MessageType.Operation:
1140
- case MessageType.RemoteHelp:
1141
- break;
1142
- case MessageType.Summarize: {
1143
- // github #6451: this is only needed for staging so the server
1144
- // know when the protocol tree is included
1145
- // this can be removed once all clients send
1146
- // protocol tree by default
1147
- const summary = contents;
1148
- if (summary.details === undefined) {
1149
- summary.details = {};
1150
- }
1151
- summary.details.includesProtocolTree =
1152
- this.options.summarizeProtocolTree === true;
1153
- break;
1154
- }
1143
+ return this.submitMessage(type, JSON.stringify(contents), batch, metadata);
1144
+ case MessageType.Summarize:
1145
+ return this.submitSummaryMessage(contents);
1155
1146
  default:
1156
1147
  this.close(new GenericError("invalidContainerSubmitOpType", undefined /* error */, { messageType: type }));
1157
1148
  return -1;
1158
1149
  }
1159
- return this.submitMessage(type, contents, batch, metadata);
1150
+ }
1151
+ /** @returns clientSequenceNumber of last message in a batch */
1152
+ submitBatch(batch) {
1153
+ let clientSequenceNumber = -1;
1154
+ for (const message of batch) {
1155
+ clientSequenceNumber = this.submitMessage(MessageType.Operation, message.contents, true, // batch
1156
+ message.metadata);
1157
+ }
1158
+ this._deltaManager.flush();
1159
+ return clientSequenceNumber;
1160
+ }
1161
+ submitSummaryMessage(summary) {
1162
+ // github #6451: this is only needed for staging so the server
1163
+ // know when the protocol tree is included
1164
+ // this can be removed once all clients send
1165
+ // protocol tree by default
1166
+ if (summary.details === undefined) {
1167
+ summary.details = {};
1168
+ }
1169
+ summary.details.includesProtocolTree =
1170
+ this.options.summarizeProtocolTree === true;
1171
+ return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1160
1172
  }
1161
1173
  submitMessage(type, contents, batch, metadata) {
1162
1174
  var _a;
@@ -1171,22 +1183,9 @@ export class Container extends EventEmitterWithErrorHandling {
1171
1183
  processRemoteMessage(message) {
1172
1184
  const local = this.clientId === message.clientId;
1173
1185
  // Allow the protocol handler to process the message
1174
- let result = { immediateNoOp: false };
1175
- try {
1176
- result = this.protocolHandler.processMessage(message, local);
1177
- }
1178
- catch (error) {
1179
- this.close(wrapError(error, (errorMessage) => new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1180
- }
1181
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1182
- if (isUnpackedRuntimeMessage(message) && !isRuntimeMessage(message)) {
1183
- this.mc.logger.sendTelemetryEvent({ eventName: "UnpackedRuntimeMessage", type: message.type });
1184
- }
1185
- // Forward non system messages to the loaded runtime for processing
1186
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1187
- if (isRuntimeMessage(message) || isUnpackedRuntimeMessage(message)) {
1188
- this.context.process(message, local, undefined);
1189
- }
1186
+ const result = this.protocolHandler.processMessage(message, local);
1187
+ // Forward messages to the loaded runtime for processing
1188
+ this.context.process(message, local, undefined);
1190
1189
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1191
1190
  if (this.activeConnection()) {
1192
1191
  if (this.collabWindowTracker === undefined) {
@@ -1195,15 +1194,14 @@ export class Container extends EventEmitterWithErrorHandling {
1195
1194
  // clients.
1196
1195
  // All existing will continue to use settings they got earlier.
1197
1196
  assert(this.serviceConfiguration !== undefined, 0x2e4 /* "there should be service config for active connection" */);
1198
- this.collabWindowTracker = new CollabWindowTracker((type, contents) => {
1197
+ this.collabWindowTracker = new CollabWindowTracker((type) => {
1199
1198
  assert(this.activeConnection(), 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1200
- this.submitMessage(type, contents);
1199
+ this.submitMessage(type);
1201
1200
  }, this.serviceConfiguration.noopTimeFrequency, this.serviceConfiguration.noopCountFrequency);
1202
1201
  }
1203
1202
  this.collabWindowTracker.scheduleSequenceNumberUpdate(message, result.immediateNoOp === true);
1204
1203
  }
1205
1204
  this.emit("op", message);
1206
- return result;
1207
1205
  }
1208
1206
  submitSignal(message) {
1209
1207
  this._deltaManager.submitSignal(JSON.stringify(message));
@@ -1211,15 +1209,7 @@ export class Container extends EventEmitterWithErrorHandling {
1211
1209
  processSignal(message) {
1212
1210
  // No clientId indicates a system signal message.
1213
1211
  if (message.clientId === null) {
1214
- const innerContent = message.content;
1215
- if (innerContent.type === MessageType.ClientJoin) {
1216
- const newClient = innerContent.content;
1217
- this._audience.addMember(newClient.clientId, newClient.client);
1218
- }
1219
- else if (innerContent.type === MessageType.ClientLeave) {
1220
- const leftClientId = innerContent.content;
1221
- this._audience.removeMember(leftClientId);
1222
- }
1212
+ this.protocolHandler.processSignal(message);
1223
1213
  }
1224
1214
  else {
1225
1215
  const local = this.clientId === message.clientId;
@@ -1258,7 +1248,7 @@ export class Container extends EventEmitterWithErrorHandling {
1258
1248
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
1259
1249
  // are set. Global requests will still go directly to the loader
1260
1250
  const loader = new RelativeLoader(this, this.loader);
1261
- this._context = await ContainerContext.createOrLoad(this, this.scope, this.codeLoader, codeDetails, snapshot, new DeltaManagerProxy(this._deltaManager), new QuorumProxy(this.protocolHandler.quorum), loader, (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata), (message) => this.submitSignal(message), (error) => this.close(error), Container.version, (dirty) => this.updateDirtyContainerState(dirty), existing, pendingLocalState);
1251
+ this._context = await ContainerContext.createOrLoad(this, this.scope, this.codeLoader, codeDetails, snapshot, new DeltaManagerProxy(this._deltaManager), new QuorumProxy(this.protocolHandler.quorum), loader, (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata), (summaryOp) => this.submitSummaryMessage(summaryOp), (batch) => this.submitBatch(batch), (message) => this.submitSignal(message), (error) => this.close(error), Container.version, (dirty) => this.updateDirtyContainerState(dirty), existing, pendingLocalState);
1262
1252
  this.emit("contextChanged", codeDetails);
1263
1253
  }
1264
1254
  updateDirtyContainerState(dirty) {
@@ -1271,6 +1261,24 @@ export class Container extends EventEmitterWithErrorHandling {
1271
1261
  logContainerError(warning) {
1272
1262
  this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1273
1263
  }
1264
+ /**
1265
+ * Set the connected state of the ContainerContext
1266
+ * This controls the "connected" state of the ContainerRuntime as well
1267
+ * @param state - Is the container currently connected?
1268
+ * @param readonly - Is the container in readonly mode?
1269
+ */
1270
+ setContextConnectedState(state, readonly) {
1271
+ var _a;
1272
+ if (((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) === false) {
1273
+ /**
1274
+ * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1275
+ * ops getting through to the DeltaManager.
1276
+ * The ContainerRuntime's "connected" state simply means it is ok to send ops
1277
+ * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1278
+ */
1279
+ this.context.setConnectionState(state && !readonly, this.clientId);
1280
+ }
1281
+ }
1274
1282
  }
1275
1283
  Container.version = "^0.1.0";
1276
1284
  //# sourceMappingURL=container.js.map