@fluidframework/container-loader 1.0.0 → 1.1.0-76254

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 (85) hide show
  1. package/dist/connectionManager.d.ts.map +1 -1
  2. package/dist/connectionManager.js +3 -1
  3. package/dist/connectionManager.js.map +1 -1
  4. package/dist/connectionStateHandler.d.ts +44 -10
  5. package/dist/connectionStateHandler.d.ts.map +1 -1
  6. package/dist/connectionStateHandler.js +90 -35
  7. package/dist/connectionStateHandler.js.map +1 -1
  8. package/dist/container.d.ts +2 -2
  9. package/dist/container.d.ts.map +1 -1
  10. package/dist/container.js +48 -70
  11. package/dist/container.js.map +1 -1
  12. package/dist/containerStorageAdapter.d.ts +2 -2
  13. package/dist/containerStorageAdapter.d.ts.map +1 -1
  14. package/dist/containerStorageAdapter.js +4 -4
  15. package/dist/containerStorageAdapter.js.map +1 -1
  16. package/dist/contracts.d.ts +1 -1
  17. package/dist/contracts.js +1 -1
  18. package/dist/contracts.js.map +1 -1
  19. package/dist/deltaManager.d.ts.map +1 -1
  20. package/dist/deltaManager.js +9 -1
  21. package/dist/deltaManager.js.map +1 -1
  22. package/dist/deltaQueue.d.ts.map +1 -1
  23. package/dist/deltaQueue.js +8 -3
  24. package/dist/deltaQueue.js.map +1 -1
  25. package/dist/packageVersion.d.ts +1 -1
  26. package/dist/packageVersion.d.ts.map +1 -1
  27. package/dist/packageVersion.js +1 -1
  28. package/dist/packageVersion.js.map +1 -1
  29. package/dist/protocolTreeDocumentStorageService.d.ts +2 -2
  30. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  31. package/dist/retriableDocumentStorageService.d.ts +2 -2
  32. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  33. package/dist/retriableDocumentStorageService.js +4 -4
  34. package/dist/retriableDocumentStorageService.js.map +1 -1
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +3 -2
  37. package/dist/utils.js.map +1 -1
  38. package/lib/connectionManager.d.ts.map +1 -1
  39. package/lib/connectionManager.js +4 -2
  40. package/lib/connectionManager.js.map +1 -1
  41. package/lib/connectionStateHandler.d.ts +44 -10
  42. package/lib/connectionStateHandler.d.ts.map +1 -1
  43. package/lib/connectionStateHandler.js +90 -35
  44. package/lib/connectionStateHandler.js.map +1 -1
  45. package/lib/container.d.ts +2 -2
  46. package/lib/container.d.ts.map +1 -1
  47. package/lib/container.js +49 -71
  48. package/lib/container.js.map +1 -1
  49. package/lib/containerStorageAdapter.d.ts +2 -2
  50. package/lib/containerStorageAdapter.d.ts.map +1 -1
  51. package/lib/containerStorageAdapter.js +4 -4
  52. package/lib/containerStorageAdapter.js.map +1 -1
  53. package/lib/contracts.d.ts +1 -1
  54. package/lib/contracts.js +1 -1
  55. package/lib/contracts.js.map +1 -1
  56. package/lib/deltaManager.d.ts.map +1 -1
  57. package/lib/deltaManager.js +9 -1
  58. package/lib/deltaManager.js.map +1 -1
  59. package/lib/deltaQueue.d.ts.map +1 -1
  60. package/lib/deltaQueue.js +8 -3
  61. package/lib/deltaQueue.js.map +1 -1
  62. package/lib/packageVersion.d.ts +1 -1
  63. package/lib/packageVersion.d.ts.map +1 -1
  64. package/lib/packageVersion.js +1 -1
  65. package/lib/packageVersion.js.map +1 -1
  66. package/lib/protocolTreeDocumentStorageService.d.ts +2 -2
  67. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  68. package/lib/retriableDocumentStorageService.d.ts +2 -2
  69. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  70. package/lib/retriableDocumentStorageService.js +4 -4
  71. package/lib/retriableDocumentStorageService.js.map +1 -1
  72. package/lib/utils.d.ts.map +1 -1
  73. package/lib/utils.js +3 -2
  74. package/lib/utils.js.map +1 -1
  75. package/package.json +15 -28
  76. package/src/connectionManager.ts +4 -6
  77. package/src/connectionStateHandler.ts +113 -54
  78. package/src/container.ts +66 -89
  79. package/src/containerStorageAdapter.ts +4 -4
  80. package/src/contracts.ts +1 -1
  81. package/src/deltaManager.ts +8 -2
  82. package/src/deltaQueue.ts +7 -3
  83. package/src/packageVersion.ts +1 -1
  84. package/src/retriableDocumentStorageService.ts +4 -4
  85. package/src/utils.ts +3 -2
package/lib/container.js CHANGED
@@ -9,7 +9,7 @@ import { assert, performance, unreachableCase } from "@fluidframework/common-uti
9
9
  import { AttachState, isFluidCodeDetails, } from "@fluidframework/container-definitions";
10
10
  import { DataCorruptionError, extractSafePropertiesFromMessage, GenericError, UsageError, } from "@fluidframework/container-utils";
11
11
  import { readAndParse, OnlineStatus, isOnline, ensureFluidResolvedUrl, combineAppAndProtocolSummary, runWithRetry, isFluidResolvedUrl, } from "@fluidframework/driver-utils";
12
- import { isSystemMessage, ProtocolOpHandler, } from "@fluidframework/protocol-base";
12
+ import { isSystemMessage, ProtocolOpHandlerWithClientValidation, } from "@fluidframework/protocol-base";
13
13
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
14
14
  import { ChildLogger, EventEmitterWithErrorHandling, PerformanceEvent, raiseConnectedEvent, TelemetryLogger, connectedEventName, disconnectedEventName, normalizeError, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
15
15
  import { Audience } from "./audience";
@@ -110,7 +110,7 @@ export class Container extends EventEmitterWithErrorHandling {
110
110
  // Tells if container can reconnect on losing fist connection
111
111
  // If false, container gets closed on loss of connection.
112
112
  this._canReconnect = true;
113
- this._lifecycleState = "created";
113
+ this._lifecycleState = "loading";
114
114
  this._attachState = AttachState.Detached;
115
115
  this.resumedOpProcessingAfterLoad = false;
116
116
  this.firstConnection = true;
@@ -166,16 +166,14 @@ export class Container extends EventEmitterWithErrorHandling {
166
166
  logConnectionStateChangeTelemetry: (value, oldState, reason) => this.logConnectionStateChangeTelemetry(value, oldState, reason),
167
167
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
168
168
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
169
- logConnectionIssue: (eventName) => {
169
+ logConnectionIssue: (eventName, details) => {
170
170
  // We get here when socket does not receive any ops on "write" connection, including
171
171
  // its own join op. Attempt recovery option.
172
- this._deltaManager.logConnectionIssue({
173
- eventName,
174
- duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp],
175
- });
172
+ this._deltaManager.logConnectionIssue(Object.assign({ eventName, duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp] }, (details === undefined ? {} : { details: JSON.stringify(details) })));
176
173
  },
177
174
  connectionStateChanged: () => {
178
- if (this.loaded) {
175
+ // Fire events only if container is fully loaded and not closed
176
+ if (this._lifecycleState === "loaded") {
179
177
  this.propagateConnectionState();
180
178
  }
181
179
  },
@@ -261,7 +259,6 @@ export class Container extends EventEmitterWithErrorHandling {
261
259
  });
262
260
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "Load" }, async (event) => new Promise((resolve, reject) => {
263
261
  var _a, _b;
264
- container._lifecycleState = "loading";
265
262
  const version = loadOptions.version;
266
263
  const defaultMode = { opsBeforeReturn: "cached" };
267
264
  // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
@@ -296,7 +293,6 @@ export class Container extends EventEmitterWithErrorHandling {
296
293
  static async createDetached(loader, codeDetails) {
297
294
  const container = new Container(loader, {});
298
295
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "CreateDetached" }, async (_event) => {
299
- container._lifecycleState = "loading";
300
296
  await container.createDetached(codeDetails);
301
297
  return container;
302
298
  }, { start: true, end: true, cancel: "generic" });
@@ -309,20 +305,16 @@ export class Container extends EventEmitterWithErrorHandling {
309
305
  const container = new Container(loader, {});
310
306
  return PerformanceEvent.timedExecAsync(container.mc.logger, { eventName: "RehydrateDetachedFromSnapshot" }, async (_event) => {
311
307
  const deserializedSummary = JSON.parse(snapshot);
312
- container._lifecycleState = "loading";
313
308
  await container.rehydrateDetachedFromSnapshot(deserializedSummary);
314
309
  return container;
315
310
  }, { start: true, end: true, cancel: "generic" });
316
311
  }
317
- get loaded() {
318
- return (this._lifecycleState !== "created" && this._lifecycleState !== "loading");
319
- }
320
- set loaded(t) {
321
- assert(t, 0x27d /* "Setting loaded state to false is not supported" */);
322
- assert(this._lifecycleState !== "created", 0x27e /* "Must go through loading state before loaded" */);
312
+ setLoaded() {
323
313
  // It's conceivable the container could be closed when this is called
324
314
  // Only transition states if currently loading
325
315
  if (this._lifecycleState === "loading") {
316
+ // Propagate current connection state through the system.
317
+ this.propagateConnectionState();
326
318
  this._lifecycleState = "loaded";
327
319
  }
328
320
  }
@@ -444,19 +436,30 @@ export class Container extends EventEmitterWithErrorHandling {
444
436
  return this.protocolHandler.quorum;
445
437
  }
446
438
  close(error) {
439
+ // 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
440
+ // 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
441
+ // handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
442
+ // "closing" will lose that info (can also solve by tracking extra state).
443
+ this._deltaManager.close(error);
444
+ assert(this.connectionState === ConnectionState.Disconnected, 0x0cf /* "disconnect event was not raised!" */);
445
+ assert(this._lifecycleState === "closed", 0x314 /* Container properly closed */);
446
+ }
447
+ closeCore(error) {
447
448
  var _a, _b, _c, _d;
448
- if (this.closed) {
449
- return;
450
- }
449
+ assert(!this.closed, 0x315 /* re-entrancy */);
451
450
  try {
452
- this._lifecycleState = "closing";
453
451
  // Ensure that we raise all key events even if one of these throws
454
452
  try {
455
- this._deltaManager.close(error);
453
+ // Raise event first, to ensure we capture _lifecycleState before transition.
454
+ // This gives us a chance to know what errors happened on open vs. on fully loaded container.
455
+ this.mc.logger.sendTelemetryEvent({
456
+ eventName: "ContainerClose",
457
+ category: error === undefined ? "generic" : "error",
458
+ }, error);
459
+ this._lifecycleState = "closing";
456
460
  (_a = this._protocolHandler) === null || _a === void 0 ? void 0 : _a.close();
457
461
  this.connectionStateHandler.dispose();
458
462
  (_b = this._context) === null || _b === void 0 ? void 0 : _b.dispose(error !== undefined ? new Error(error.message) : undefined);
459
- assert(this.connectionState === ConnectionState.Disconnected, 0x0cf /* "disconnect event was not raised!" */);
460
463
  (_c = this._storageService) === null || _c === void 0 ? void 0 : _c.dispose();
461
464
  // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
462
465
  // about file, like file being overwritten in storage, but client having stale local cache.
@@ -466,10 +469,6 @@ export class Container extends EventEmitterWithErrorHandling {
466
469
  catch (exception) {
467
470
  this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
468
471
  }
469
- this.mc.logger.sendTelemetryEvent({
470
- eventName: "ContainerClose",
471
- category: error === undefined ? "generic" : "error",
472
- }, error);
473
472
  this.emit("closed", error);
474
473
  this.removeAllListeners();
475
474
  if (this.visibilityEventHandler !== undefined) {
@@ -487,11 +486,12 @@ export class Container extends EventEmitterWithErrorHandling {
487
486
  assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
488
487
  assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid", 0x0d2 /* "resolved url should be valid Fluid url" */);
489
488
  assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
489
+ assert(this._protocolHandler.attributes.term !== undefined, 0x30b /* Must have a valid protocol handler instance */);
490
490
  const pendingState = {
491
491
  pendingRuntimeState: this.context.getPendingLocalState(),
492
492
  url: this.resolvedUrl.url,
493
- protocol: this._protocolHandler.getProtocolState(),
494
- term: this._protocolHandler.term,
493
+ protocol: this.protocolHandler.getProtocolState(),
494
+ term: this._protocolHandler.attributes.term,
495
495
  clientId: this.clientId,
496
496
  };
497
497
  this.close();
@@ -790,10 +790,8 @@ export class Container extends EventEmitterWithErrorHandling {
790
790
  const codeDetails = this.getCodeDetailsFromQuorum();
791
791
  await this.instantiateContext(true, // existing
792
792
  codeDetails, snapshot, pendingLocalState === null || pendingLocalState === void 0 ? void 0 : pendingLocalState.pendingRuntimeState);
793
- // Propagate current connection state through the system.
794
- this.propagateConnectionState();
795
793
  // Internal context is fully loaded at this point
796
- this.loaded = true;
794
+ this.setLoaded();
797
795
  // We might have hit some failure that did not manifest itself in exception in this flow,
798
796
  // do not start op processing in such case - static version of Container.load() will handle it correctly.
799
797
  if (!this.closed) {
@@ -849,8 +847,7 @@ export class Container extends EventEmitterWithErrorHandling {
849
847
  qValues);
850
848
  // The load context - given we seeded the quorum - will be great
851
849
  await this.instantiateContextDetached(false);
852
- this.propagateConnectionState();
853
- this.loaded = true;
850
+ this.setLoaded();
854
851
  }
855
852
  async rehydrateDetachedFromSnapshot(detachedContainerSnapshot) {
856
853
  if (detachedContainerSnapshot.tree[".hasAttachmentBlobs"] !== undefined) {
@@ -871,8 +868,7 @@ export class Container extends EventEmitterWithErrorHandling {
871
868
  codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : []);
872
869
  await this.instantiateContextDetached(true, // existing
873
870
  snapshotTree);
874
- this.loaded = true;
875
- this.propagateConnectionState();
871
+ this.setLoaded();
876
872
  }
877
873
  async connectStorageService() {
878
874
  var _a, _b;
@@ -926,7 +922,7 @@ export class Container extends EventEmitterWithErrorHandling {
926
922
  return protocolHandler;
927
923
  }
928
924
  async initializeProtocolState(attributes, members, proposals, values) {
929
- const protocol = new ProtocolOpHandler(attributes.minimumSequenceNumber, attributes.sequenceNumber, attributes.term, members, proposals, values, (key, value) => this.submitMessage(MessageType.Propose, { key, value }));
925
+ const protocol = new ProtocolOpHandlerWithClientValidation(attributes.minimumSequenceNumber, attributes.sequenceNumber, attributes.term, members, proposals, values, (key, value) => this.submitMessage(MessageType.Propose, { key, value }));
930
926
  const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
931
927
  protocol.quorum.on("error", (error) => {
932
928
  protocolLogger.sendErrorEvent(error);
@@ -954,17 +950,11 @@ export class Container extends EventEmitterWithErrorHandling {
954
950
  return protocol;
955
951
  }
956
952
  captureProtocolSummary() {
957
- const quorumSnapshot = this.protocolHandler.quorum.snapshot();
958
- // Save attributes for the document
959
- const documentAttributes = {
960
- minimumSequenceNumber: this.protocolHandler.minimumSequenceNumber,
961
- sequenceNumber: this.protocolHandler.sequenceNumber,
962
- term: this.protocolHandler.term,
963
- };
953
+ const quorumSnapshot = this.protocolHandler.snapshot();
964
954
  const summary = {
965
955
  tree: {
966
956
  attributes: {
967
- content: JSON.stringify(documentAttributes),
957
+ content: JSON.stringify(this.protocolHandler.attributes),
968
958
  type: SummaryType.Blob,
969
959
  },
970
960
  quorumMembers: {
@@ -1053,7 +1043,7 @@ export class Container extends EventEmitterWithErrorHandling {
1053
1043
  this.emit("readonly", readonly);
1054
1044
  });
1055
1045
  deltaManager.on("closed", (error) => {
1056
- this.close(error);
1046
+ this.closeCore(error);
1057
1047
  });
1058
1048
  return deltaManager;
1059
1049
  }
@@ -1109,6 +1099,7 @@ export class Container extends EventEmitterWithErrorHandling {
1109
1099
  }
1110
1100
  }
1111
1101
  propagateConnectionState() {
1102
+ var _a;
1112
1103
  const logOpsOnReconnect = this.connectionState === ConnectionState.Connected &&
1113
1104
  !this.firstConnection &&
1114
1105
  this.connectionMode === "write";
@@ -1116,11 +1107,11 @@ export class Container extends EventEmitterWithErrorHandling {
1116
1107
  this.messageCountAfterDisconnection = 0;
1117
1108
  }
1118
1109
  const state = this.connectionState === ConnectionState.Connected;
1119
- if (!this.context.disposed) {
1110
+ // Both protocol and context should not be undefined if we got so far.
1111
+ if (((_a = this._context) === null || _a === void 0 ? void 0 : _a.disposed) === false) {
1120
1112
  this.context.setConnectionState(state, this.clientId);
1121
1113
  }
1122
- assert(this.protocolHandler !== undefined, 0x0dc /* "Protocol handler should be set here" */);
1123
- this.protocolHandler.quorum.setConnectionState(state, this.clientId);
1114
+ this.protocolHandler.setConnectionState(state, this.clientId);
1124
1115
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1125
1116
  if (logOpsOnReconnect) {
1126
1117
  this.mc.logger.sendTelemetryEvent({ eventName: "OpsSentOnReconnect", count: this.messageCountAfterDisconnection });
@@ -1162,32 +1153,19 @@ export class Container extends EventEmitterWithErrorHandling {
1162
1153
  return this._deltaManager.submit(type, contents, batch, metadata);
1163
1154
  }
1164
1155
  processRemoteMessage(message) {
1165
- var _a, _b;
1166
- // Check and report if we're getting messages from a clientId that we previously
1167
- // flagged as shouldHaveLeft, or from a client that's not in the quorum but should be
1168
- if (message.clientId != null) {
1169
- let errorMsg;
1170
- const client = this.getQuorum().getMember(message.clientId);
1171
- if (client === undefined && message.type !== MessageType.ClientJoin) {
1172
- // pre-0.58 error message: messageClientIdMissingFromQuorum
1173
- errorMsg = "Remote message's clientId is missing from the quorum";
1174
- }
1175
- else if ((client === null || client === void 0 ? void 0 : client.shouldHaveLeft) === true && message.type !== MessageType.NoOp) {
1176
- // pre-0.58 error message: messageClientIdShouldHaveLeft
1177
- errorMsg = "Remote message's clientId already should have left";
1178
- }
1179
- if (errorMsg !== undefined) {
1180
- const error = new DataCorruptionError(errorMsg, extractSafePropertiesFromMessage(message));
1181
- this.close(normalizeError(error));
1182
- }
1183
- }
1184
1156
  const local = this.clientId === message.clientId;
1157
+ // Allow the protocol handler to process the message
1158
+ let result = { immediateNoOp: false };
1159
+ try {
1160
+ result = this.protocolHandler.processMessage(message, local);
1161
+ }
1162
+ catch (error) {
1163
+ this.close(wrapError(error, (errorMessage) => new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1164
+ }
1185
1165
  // Forward non system messages to the loaded runtime for processing
1186
1166
  if (!isSystemMessage(message)) {
1187
1167
  this.context.process(message, local, undefined);
1188
1168
  }
1189
- // Allow the protocol handler to process the message
1190
- const result = this.protocolHandler.processMessage(message, local);
1191
1169
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1192
1170
  if (this.activeConnection()) {
1193
1171
  if (this.collabWindowTracker === undefined) {
@@ -1199,7 +1177,7 @@ export class Container extends EventEmitterWithErrorHandling {
1199
1177
  this.collabWindowTracker = new CollabWindowTracker((type, contents) => {
1200
1178
  assert(this.activeConnection(), 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1201
1179
  this.submitMessage(type, contents);
1202
- }, (_a = this.serviceConfiguration) === null || _a === void 0 ? void 0 : _a.noopTimeFrequency, (_b = this.serviceConfiguration) === null || _b === void 0 ? void 0 : _b.noopCountFrequency);
1180
+ }, this.serviceConfiguration.noopTimeFrequency, this.serviceConfiguration.noopCountFrequency);
1203
1181
  }
1204
1182
  this.collabWindowTracker.scheduleSequenceNumberUpdate(message, result.immediateNoOp === true);
1205
1183
  }