@fluidframework/container-loader 2.0.0-dev.2.3.0.115467 → 2.0.0-dev.4.1.0.148229

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 (168) hide show
  1. package/.eslintrc.js +18 -21
  2. package/.mocharc.js +2 -2
  3. package/README.md +65 -44
  4. package/api-extractor.json +2 -2
  5. package/closeAndGetPendingLocalState.md +51 -0
  6. package/dist/audience.d.ts +0 -1
  7. package/dist/audience.d.ts.map +1 -1
  8. package/dist/audience.js.map +1 -1
  9. package/dist/catchUpMonitor.d.ts.map +1 -1
  10. package/dist/catchUpMonitor.js.map +1 -1
  11. package/dist/collabWindowTracker.d.ts.map +1 -1
  12. package/dist/collabWindowTracker.js.map +1 -1
  13. package/dist/connectionManager.d.ts +5 -5
  14. package/dist/connectionManager.d.ts.map +1 -1
  15. package/dist/connectionManager.js +107 -44
  16. package/dist/connectionManager.js.map +1 -1
  17. package/dist/connectionState.d.ts.map +1 -1
  18. package/dist/connectionState.js.map +1 -1
  19. package/dist/connectionStateHandler.d.ts +7 -7
  20. package/dist/connectionStateHandler.d.ts.map +1 -1
  21. package/dist/connectionStateHandler.js +50 -21
  22. package/dist/connectionStateHandler.js.map +1 -1
  23. package/dist/container.d.ts +64 -5
  24. package/dist/container.d.ts.map +1 -1
  25. package/dist/container.js +329 -137
  26. package/dist/container.js.map +1 -1
  27. package/dist/containerContext.d.ts +19 -8
  28. package/dist/containerContext.d.ts.map +1 -1
  29. package/dist/containerContext.js +58 -14
  30. package/dist/containerContext.js.map +1 -1
  31. package/dist/containerStorageAdapter.d.ts +41 -2
  32. package/dist/containerStorageAdapter.d.ts.map +1 -1
  33. package/dist/containerStorageAdapter.js +88 -14
  34. package/dist/containerStorageAdapter.js.map +1 -1
  35. package/dist/contracts.d.ts +3 -3
  36. package/dist/contracts.d.ts.map +1 -1
  37. package/dist/contracts.js.map +1 -1
  38. package/dist/deltaManager.d.ts +21 -8
  39. package/dist/deltaManager.d.ts.map +1 -1
  40. package/dist/deltaManager.js +112 -37
  41. package/dist/deltaManager.js.map +1 -1
  42. package/dist/deltaManagerProxy.d.ts +10 -22
  43. package/dist/deltaManagerProxy.d.ts.map +1 -1
  44. package/dist/deltaManagerProxy.js +14 -50
  45. package/dist/deltaManagerProxy.js.map +1 -1
  46. package/dist/deltaQueue.d.ts.map +1 -1
  47. package/dist/deltaQueue.js +4 -2
  48. package/dist/deltaQueue.js.map +1 -1
  49. package/dist/index.d.ts +4 -3
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -3
  52. package/dist/index.js.map +1 -1
  53. package/dist/loader.d.ts +13 -4
  54. package/dist/loader.d.ts.map +1 -1
  55. package/dist/loader.js +38 -24
  56. package/dist/loader.js.map +1 -1
  57. package/dist/packageVersion.d.ts +1 -1
  58. package/dist/packageVersion.js +1 -1
  59. package/dist/packageVersion.js.map +1 -1
  60. package/dist/protocol.d.ts.map +1 -1
  61. package/dist/protocol.js +2 -1
  62. package/dist/protocol.js.map +1 -1
  63. package/dist/protocolTreeDocumentStorageService.d.ts +6 -2
  64. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  65. package/dist/protocolTreeDocumentStorageService.js +7 -4
  66. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  67. package/dist/quorum.d.ts.map +1 -1
  68. package/dist/quorum.js.map +1 -1
  69. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  70. package/dist/retriableDocumentStorageService.js +6 -2
  71. package/dist/retriableDocumentStorageService.js.map +1 -1
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +8 -5
  74. package/dist/utils.js.map +1 -1
  75. package/lib/audience.d.ts +0 -1
  76. package/lib/audience.d.ts.map +1 -1
  77. package/lib/audience.js.map +1 -1
  78. package/lib/catchUpMonitor.d.ts.map +1 -1
  79. package/lib/catchUpMonitor.js.map +1 -1
  80. package/lib/collabWindowTracker.d.ts.map +1 -1
  81. package/lib/collabWindowTracker.js.map +1 -1
  82. package/lib/connectionManager.d.ts +5 -5
  83. package/lib/connectionManager.d.ts.map +1 -1
  84. package/lib/connectionManager.js +110 -47
  85. package/lib/connectionManager.js.map +1 -1
  86. package/lib/connectionState.d.ts.map +1 -1
  87. package/lib/connectionState.js.map +1 -1
  88. package/lib/connectionStateHandler.d.ts +7 -7
  89. package/lib/connectionStateHandler.d.ts.map +1 -1
  90. package/lib/connectionStateHandler.js +50 -21
  91. package/lib/connectionStateHandler.js.map +1 -1
  92. package/lib/container.d.ts +64 -5
  93. package/lib/container.d.ts.map +1 -1
  94. package/lib/container.js +336 -144
  95. package/lib/container.js.map +1 -1
  96. package/lib/containerContext.d.ts +19 -8
  97. package/lib/containerContext.d.ts.map +1 -1
  98. package/lib/containerContext.js +59 -15
  99. package/lib/containerContext.js.map +1 -1
  100. package/lib/containerStorageAdapter.d.ts +41 -2
  101. package/lib/containerStorageAdapter.d.ts.map +1 -1
  102. package/lib/containerStorageAdapter.js +86 -14
  103. package/lib/containerStorageAdapter.js.map +1 -1
  104. package/lib/contracts.d.ts +3 -3
  105. package/lib/contracts.d.ts.map +1 -1
  106. package/lib/contracts.js.map +1 -1
  107. package/lib/deltaManager.d.ts +21 -8
  108. package/lib/deltaManager.d.ts.map +1 -1
  109. package/lib/deltaManager.js +114 -39
  110. package/lib/deltaManager.js.map +1 -1
  111. package/lib/deltaManagerProxy.d.ts +10 -22
  112. package/lib/deltaManagerProxy.d.ts.map +1 -1
  113. package/lib/deltaManagerProxy.js +14 -50
  114. package/lib/deltaManagerProxy.js.map +1 -1
  115. package/lib/deltaQueue.d.ts.map +1 -1
  116. package/lib/deltaQueue.js +4 -2
  117. package/lib/deltaQueue.js.map +1 -1
  118. package/lib/index.d.ts +4 -3
  119. package/lib/index.d.ts.map +1 -1
  120. package/lib/index.js +2 -2
  121. package/lib/index.js.map +1 -1
  122. package/lib/loader.d.ts +13 -4
  123. package/lib/loader.d.ts.map +1 -1
  124. package/lib/loader.js +37 -24
  125. package/lib/loader.js.map +1 -1
  126. package/lib/packageVersion.d.ts +1 -1
  127. package/lib/packageVersion.js +1 -1
  128. package/lib/packageVersion.js.map +1 -1
  129. package/lib/protocol.d.ts.map +1 -1
  130. package/lib/protocol.js +2 -1
  131. package/lib/protocol.js.map +1 -1
  132. package/lib/protocolTreeDocumentStorageService.d.ts +6 -2
  133. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  134. package/lib/protocolTreeDocumentStorageService.js +7 -4
  135. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  136. package/lib/quorum.d.ts.map +1 -1
  137. package/lib/quorum.js.map +1 -1
  138. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  139. package/lib/retriableDocumentStorageService.js +6 -2
  140. package/lib/retriableDocumentStorageService.js.map +1 -1
  141. package/lib/utils.d.ts.map +1 -1
  142. package/lib/utils.js +8 -5
  143. package/lib/utils.js.map +1 -1
  144. package/package.json +67 -56
  145. package/prettier.config.cjs +1 -1
  146. package/src/audience.ts +51 -46
  147. package/src/catchUpMonitor.ts +39 -37
  148. package/src/collabWindowTracker.ts +75 -70
  149. package/src/connectionManager.ts +1040 -941
  150. package/src/connectionState.ts +19 -19
  151. package/src/connectionStateHandler.ts +557 -463
  152. package/src/container.ts +2147 -1784
  153. package/src/containerContext.ts +417 -345
  154. package/src/containerStorageAdapter.ts +268 -154
  155. package/src/contracts.ts +155 -153
  156. package/src/deltaManager.ts +1074 -945
  157. package/src/deltaManagerProxy.ts +88 -137
  158. package/src/deltaQueue.ts +155 -151
  159. package/src/index.ts +13 -17
  160. package/src/loader.ts +434 -427
  161. package/src/packageVersion.ts +1 -1
  162. package/src/protocol.ts +93 -87
  163. package/src/protocolTreeDocumentStorageService.ts +34 -34
  164. package/src/quorum.ts +34 -34
  165. package/src/retriableDocumentStorageService.ts +118 -102
  166. package/src/utils.ts +93 -83
  167. package/tsconfig.esnext.json +6 -6
  168. package/tsconfig.json +8 -12
package/src/container.ts CHANGED
@@ -7,83 +7,77 @@
7
7
  import merge from "lodash/merge";
8
8
  import { v4 as uuid } from "uuid";
9
9
  import {
10
- ITelemetryLogger, ITelemetryProperties,
10
+ ITelemetryLogger,
11
+ ITelemetryProperties,
12
+ TelemetryEventCategory,
11
13
  } from "@fluidframework/common-definitions";
12
14
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
15
+ import { IRequest, IResponse, IFluidRouter, FluidObject } from "@fluidframework/core-interfaces";
13
16
  import {
14
- IRequest,
15
- IResponse,
16
- IFluidRouter,
17
- } from "@fluidframework/core-interfaces";
18
- import {
19
- IAudience,
20
- IConnectionDetails,
21
- IContainer,
22
- IContainerEvents,
23
- IDeltaManager,
24
- ICriticalContainerError,
25
- ContainerWarning,
26
- AttachState,
27
- IThrottlingWarning,
28
- ReadOnlyInfo,
29
- IContainerLoadMode,
30
- IFluidCodeDetails,
31
- isFluidCodeDetails,
32
- IBatchMessage,
17
+ IAudience,
18
+ IConnectionDetailsInternal,
19
+ IContainer,
20
+ IContainerEvents,
21
+ IDeltaManager,
22
+ ICriticalContainerError,
23
+ ContainerWarning,
24
+ AttachState,
25
+ IThrottlingWarning,
26
+ ReadOnlyInfo,
27
+ IContainerLoadMode,
28
+ IFluidCodeDetails,
29
+ isFluidCodeDetails,
30
+ IBatchMessage,
33
31
  } from "@fluidframework/container-definitions";
32
+ import { GenericError, UsageError } from "@fluidframework/container-utils";
34
33
  import {
35
- GenericError,
36
- UsageError,
37
- } from "@fluidframework/container-utils";
38
- import {
39
- IDocumentService,
40
- IDocumentStorageService,
41
- IFluidResolvedUrl,
42
- IResolvedUrl,
34
+ IDocumentService,
35
+ IDocumentStorageService,
36
+ IFluidResolvedUrl,
37
+ IResolvedUrl,
43
38
  } from "@fluidframework/driver-definitions";
44
39
  import {
45
- readAndParse,
46
- OnlineStatus,
47
- isOnline,
48
- ensureFluidResolvedUrl,
49
- combineAppAndProtocolSummary,
50
- runWithRetry,
51
- isFluidResolvedUrl,
40
+ readAndParse,
41
+ OnlineStatus,
42
+ isOnline,
43
+ ensureFluidResolvedUrl,
44
+ combineAppAndProtocolSummary,
45
+ runWithRetry,
46
+ isFluidResolvedUrl,
47
+ isCombinedAppAndProtocolSummary,
52
48
  } from "@fluidframework/driver-utils";
53
49
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
54
50
  import {
55
- IClient,
56
- IClientConfiguration,
57
- IClientDetails,
58
- ICommittedProposal,
59
- IDocumentAttributes,
60
- IDocumentMessage,
61
- IProtocolState,
62
- IQuorumClients,
63
- IQuorumProposals,
64
- ISequencedClient,
65
- ISequencedDocumentMessage,
66
- ISequencedProposal,
67
- ISignalMessage,
68
- ISnapshotTree,
69
- ISummaryContent,
70
- ISummaryTree,
71
- IVersion,
72
- MessageType,
73
- SummaryType,
51
+ IClient,
52
+ IClientConfiguration,
53
+ IClientDetails,
54
+ ICommittedProposal,
55
+ IDocumentAttributes,
56
+ IDocumentMessage,
57
+ IQuorumClients,
58
+ IQuorumProposals,
59
+ ISequencedClient,
60
+ ISequencedDocumentMessage,
61
+ ISequencedProposal,
62
+ ISignalMessage,
63
+ ISnapshotTree,
64
+ ISummaryContent,
65
+ ISummaryTree,
66
+ IVersion,
67
+ MessageType,
68
+ SummaryType,
74
69
  } from "@fluidframework/protocol-definitions";
75
70
  import {
76
- ChildLogger,
77
- EventEmitterWithErrorHandling,
78
- PerformanceEvent,
79
- raiseConnectedEvent,
80
- TelemetryLogger,
81
- connectedEventName,
82
- disconnectedEventName,
83
- normalizeError,
84
- MonitoringContext,
85
- loggerToMonitoringContext,
86
- wrapError,
71
+ ChildLogger,
72
+ EventEmitterWithErrorHandling,
73
+ PerformanceEvent,
74
+ raiseConnectedEvent,
75
+ TelemetryLogger,
76
+ connectedEventName,
77
+ normalizeError,
78
+ MonitoringContext,
79
+ loggerToMonitoringContext,
80
+ wrapError,
87
81
  } from "@fluidframework/telemetry-utils";
88
82
  import { Audience } from "./audience";
89
83
  import { ContainerContext } from "./containerContext";
@@ -92,58 +86,68 @@ import { DeltaManager, IConnectionArgs } from "./deltaManager";
92
86
  import { DeltaManagerProxy } from "./deltaManagerProxy";
93
87
  import { ILoaderOptions, Loader, RelativeLoader } from "./loader";
94
88
  import { pkgVersion } from "./packageVersion";
95
- import { ContainerStorageAdapter } from "./containerStorageAdapter";
96
89
  import {
97
- IConnectionStateHandler,
98
- createConnectionStateHandler,
99
- } from "./connectionStateHandler";
90
+ ContainerStorageAdapter,
91
+ getBlobContentsFromTree,
92
+ getBlobContentsFromTreeWithBlobContents,
93
+ ISerializableBlobContents,
94
+ } from "./containerStorageAdapter";
95
+ import { IConnectionStateHandler, createConnectionStateHandler } from "./connectionStateHandler";
100
96
  import { getProtocolSnapshotTree, getSnapshotTreeFromSerializedContainer } from "./utils";
101
- import { initQuorumValuesFromCodeDetails, getCodeDetailsFromQuorumValues, QuorumProxy } from "./quorum";
97
+ import {
98
+ initQuorumValuesFromCodeDetails,
99
+ getCodeDetailsFromQuorumValues,
100
+ QuorumProxy,
101
+ } from "./quorum";
102
102
  import { CollabWindowTracker } from "./collabWindowTracker";
103
103
  import { ConnectionManager } from "./connectionManager";
104
104
  import { ConnectionState } from "./connectionState";
105
- import {
106
- IProtocolHandler,
107
- ProtocolHandler,
108
- ProtocolHandlerBuilder,
109
- } from "./protocol";
105
+ import { IProtocolHandler, ProtocolHandler, ProtocolHandlerBuilder } from "./protocol";
110
106
 
111
107
  const detachedContainerRefSeqNumber = 0;
112
108
 
113
109
  const dirtyContainerEvent = "dirty";
114
110
  const savedContainerEvent = "saved";
115
111
 
112
+ /**
113
+ * @deprecated this is an internal interface and will not longer be exported in future versions
114
+ * @internal
115
+ */
116
116
  export interface IContainerLoadOptions {
117
- /**
118
- * Disables the Container from reconnecting if false, allows reconnect otherwise.
119
- */
120
- canReconnect?: boolean;
121
- /**
122
- * Client details provided in the override will be merged over the default client.
123
- */
124
- clientDetailsOverride?: IClientDetails;
125
- resolvedUrl: IFluidResolvedUrl;
126
- /**
127
- * Control which snapshot version to load from. See IParsedUrl for detailed information.
128
- */
129
- version: string | undefined;
130
- /**
131
- * Loads the Container in paused state if true, unpaused otherwise.
132
- */
133
- loadMode?: IContainerLoadMode;
117
+ /**
118
+ * Disables the Container from reconnecting if false, allows reconnect otherwise.
119
+ */
120
+ canReconnect?: boolean;
121
+ /**
122
+ * Client details provided in the override will be merged over the default client.
123
+ */
124
+ clientDetailsOverride?: IClientDetails;
125
+ resolvedUrl: IFluidResolvedUrl;
126
+ /**
127
+ * Control which snapshot version to load from. See IParsedUrl for detailed information.
128
+ */
129
+ version: string | undefined;
130
+ /**
131
+ * Loads the Container in paused state if true, unpaused otherwise.
132
+ */
133
+ loadMode?: IContainerLoadMode;
134
134
  }
135
135
 
136
+ /**
137
+ * @deprecated this is an internal interface and will not longer be exported in future versions
138
+ * @internal
139
+ */
136
140
  export interface IContainerConfig {
137
- resolvedUrl?: IFluidResolvedUrl;
138
- canReconnect?: boolean;
139
- /**
140
- * Client details provided in the override will be merged over the default client.
141
- */
142
- clientDetailsOverride?: IClientDetails;
143
- /**
144
- * Serialized state from a previous instance of this container
145
- */
146
- serializedContainerState?: IPendingContainerState;
141
+ resolvedUrl?: IFluidResolvedUrl;
142
+ canReconnect?: boolean;
143
+ /**
144
+ * Client details provided in the override will be merged over the default client.
145
+ */
146
+ clientDetailsOverride?: IClientDetails;
147
+ /**
148
+ * Serialized state from a previous instance of this container
149
+ */
150
+ serializedContainerState?: IPendingContainerState;
147
151
  }
148
152
 
149
153
  /**
@@ -163,1708 +167,2067 @@ export interface IContainerConfig {
163
167
  * @throws an error beginning with `"Container closed"` if the container is closed before it catches up.
164
168
  */
165
169
  export async function waitContainerToCatchUp(container: IContainer) {
166
- // Make sure we stop waiting if container is closed.
167
- if (container.closed) {
168
- throw new UsageError("waitContainerToCatchUp: Container closed");
169
- }
170
-
171
- return new Promise<boolean>((resolve, reject) => {
172
- const deltaManager = container.deltaManager;
173
-
174
- const closedCallback = (err?: ICriticalContainerError | undefined) => {
175
- container.off("closed", closedCallback);
176
- const baseMessage = "Container closed while waiting to catch up";
177
- reject(
178
- err !== undefined
179
- ? wrapError(err, (innerMessage) => new GenericError(`${baseMessage}: ${innerMessage}`))
180
- : new GenericError(baseMessage),
181
- );
182
- };
183
- container.on("closed", closedCallback);
184
-
185
- // Depending on config, transition to "connected" state may include the guarantee
186
- // that all known ops have been processed. If so, we may introduce additional wait here.
187
- // Waiting for "connected" state in either case gets us at least to our own Join op
188
- // which is a reasonable approximation of "caught up"
189
- const waitForOps = () => {
190
- assert(container.connectionState === ConnectionState.CatchingUp
191
- || container.connectionState === ConnectionState.Connected,
192
- 0x0cd /* "Container disconnected while waiting for ops!" */);
193
- const hasCheckpointSequenceNumber = deltaManager.hasCheckpointSequenceNumber;
194
-
195
- const connectionOpSeqNumber = deltaManager.lastKnownSeqNumber;
196
- assert(deltaManager.lastSequenceNumber <= connectionOpSeqNumber,
197
- 0x266 /* "lastKnownSeqNumber should never be below last processed sequence number" */);
198
- if (deltaManager.lastSequenceNumber === connectionOpSeqNumber) {
199
- container.off("closed", closedCallback);
200
- resolve(hasCheckpointSequenceNumber);
201
- return;
202
- }
203
- const callbackOps = (message: ISequencedDocumentMessage) => {
204
- if (connectionOpSeqNumber <= message.sequenceNumber) {
205
- container.off("closed", closedCallback);
206
- resolve(hasCheckpointSequenceNumber);
207
- deltaManager.off("op", callbackOps);
208
- }
209
- };
210
- deltaManager.on("op", callbackOps);
211
- };
212
-
213
- // We can leverage DeltaManager's "connect" event here and test for ConnectionState.Disconnected
214
- // But that works only if service provides us checkPointSequenceNumber
215
- // Our internal testing is based on R11S that does not, but almost all tests connect as "write" and
216
- // use this function to catch up, so leveraging our own join op as a fence/barrier
217
- if (container.connectionState === ConnectionState.Connected) {
218
- waitForOps();
219
- return;
220
- }
221
-
222
- const callback = () => {
223
- container.off(connectedEventName, callback);
224
- waitForOps();
225
- };
226
- container.on(connectedEventName, callback);
227
-
228
- if (container.connectionState === ConnectionState.Disconnected) {
229
- container.connect();
230
- }
231
- });
170
+ // Make sure we stop waiting if container is closed.
171
+ if (container.closed) {
172
+ throw new UsageError("waitContainerToCatchUp: Container closed");
173
+ }
174
+
175
+ return new Promise<boolean>((resolve, reject) => {
176
+ const deltaManager = container.deltaManager;
177
+
178
+ const closedCallback = (err?: ICriticalContainerError | undefined) => {
179
+ container.off("closed", closedCallback);
180
+ const baseMessage = "Container closed while waiting to catch up";
181
+ reject(
182
+ err !== undefined
183
+ ? wrapError(
184
+ err,
185
+ (innerMessage) => new GenericError(`${baseMessage}: ${innerMessage}`),
186
+ )
187
+ : new GenericError(baseMessage),
188
+ );
189
+ };
190
+ container.on("closed", closedCallback);
191
+
192
+ // Depending on config, transition to "connected" state may include the guarantee
193
+ // that all known ops have been processed. If so, we may introduce additional wait here.
194
+ // Waiting for "connected" state in either case gets us at least to our own Join op
195
+ // which is a reasonable approximation of "caught up"
196
+ const waitForOps = () => {
197
+ assert(
198
+ container.connectionState === ConnectionState.CatchingUp ||
199
+ container.connectionState === ConnectionState.Connected,
200
+ 0x0cd /* "Container disconnected while waiting for ops!" */,
201
+ );
202
+ const hasCheckpointSequenceNumber = deltaManager.hasCheckpointSequenceNumber;
203
+
204
+ const connectionOpSeqNumber = deltaManager.lastKnownSeqNumber;
205
+ assert(
206
+ deltaManager.lastSequenceNumber <= connectionOpSeqNumber,
207
+ 0x266 /* "lastKnownSeqNumber should never be below last processed sequence number" */,
208
+ );
209
+ if (deltaManager.lastSequenceNumber === connectionOpSeqNumber) {
210
+ container.off("closed", closedCallback);
211
+ resolve(hasCheckpointSequenceNumber);
212
+ return;
213
+ }
214
+ const callbackOps = (message: ISequencedDocumentMessage) => {
215
+ if (connectionOpSeqNumber <= message.sequenceNumber) {
216
+ container.off("closed", closedCallback);
217
+ resolve(hasCheckpointSequenceNumber);
218
+ deltaManager.off("op", callbackOps);
219
+ }
220
+ };
221
+ deltaManager.on("op", callbackOps);
222
+ };
223
+
224
+ // We can leverage DeltaManager's "connect" event here and test for ConnectionState.Disconnected
225
+ // But that works only if service provides us checkPointSequenceNumber
226
+ // Our internal testing is based on R11S that does not, but almost all tests connect as "write" and
227
+ // use this function to catch up, so leveraging our own join op as a fence/barrier
228
+ if (container.connectionState === ConnectionState.Connected) {
229
+ waitForOps();
230
+ return;
231
+ }
232
+
233
+ const callback = () => {
234
+ container.off(connectedEventName, callback);
235
+ waitForOps();
236
+ };
237
+ container.on(connectedEventName, callback);
238
+
239
+ if (container.connectionState === ConnectionState.Disconnected) {
240
+ container.connect();
241
+ }
242
+ });
232
243
  }
233
244
 
234
245
  const getCodeProposal =
235
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
236
- (quorum: IQuorumProposals) => quorum.get("code") ?? quorum.get("code2");
246
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
247
+ (quorum: IQuorumProposals) => quorum.get("code") ?? quorum.get("code2");
237
248
 
238
249
  /**
239
- * Helper function to report to telemetry cases where operation takes longer than expected (1s)
250
+ * Helper function to report to telemetry cases where operation takes longer than expected (200ms)
240
251
  * @param logger - logger to use
241
252
  * @param eventName - event name
242
253
  * @param action - functor to call and measure
243
254
  */
244
255
  export async function ReportIfTooLong(
245
- logger: ITelemetryLogger,
246
- eventName: string,
247
- action: () => Promise<ITelemetryProperties>,
256
+ logger: ITelemetryLogger,
257
+ eventName: string,
258
+ action: () => Promise<ITelemetryProperties>,
248
259
  ) {
249
- const event = PerformanceEvent.start(logger, { eventName });
250
- const props = await action();
251
- if (event.duration > 200) {
252
- event.end(props);
253
- }
260
+ const event = PerformanceEvent.start(logger, { eventName });
261
+ const props = await action();
262
+ if (event.duration > 200) {
263
+ event.end(props);
264
+ }
254
265
  }
255
266
 
256
267
  /**
257
268
  * State saved by a container at close time, to be used to load a new instance
258
269
  * of the container to the same state
270
+ * @deprecated this is an internal interface and will not longer be exported in future versions
271
+ * @internal
259
272
  */
260
273
  export interface IPendingContainerState {
261
- pendingRuntimeState: unknown;
262
- url: string;
263
- protocol: IProtocolState;
264
- term: number;
265
- clientId?: string;
274
+ pendingRuntimeState: unknown;
275
+ /**
276
+ * Snapshot from which container initially loaded.
277
+ */
278
+ baseSnapshot: ISnapshotTree;
279
+ /**
280
+ * Serializable blobs from the base snapshot. Used to load offline since
281
+ * storage is not available.
282
+ */
283
+ snapshotBlobs: ISerializableBlobContents;
284
+ /**
285
+ * All ops since base snapshot sequence number up to the latest op
286
+ * seen when the container was closed. Used to apply stashed (saved pending)
287
+ * ops at the same sequence number at which they were made.
288
+ */
289
+ savedOps: ISequencedDocumentMessage[];
290
+ url: string;
291
+ term: number;
292
+ clientId?: string;
266
293
  }
267
294
 
268
295
  const summarizerClientType = "summarizer";
269
296
 
270
- export class Container extends EventEmitterWithErrorHandling<IContainerEvents> implements IContainer {
271
- public static version = "^0.1.0";
272
-
273
- /**
274
- * Load an existing container.
275
- */
276
- public static async load(
277
- loader: Loader,
278
- loadOptions: IContainerLoadOptions,
279
- pendingLocalState?: IPendingContainerState,
280
- protocolHandlerBuilder?: ProtocolHandlerBuilder,
281
- ): Promise<Container> {
282
- const container = new Container(
283
- loader,
284
- {
285
- clientDetailsOverride: loadOptions.clientDetailsOverride,
286
- resolvedUrl: loadOptions.resolvedUrl,
287
- canReconnect: loadOptions.canReconnect,
288
- serializedContainerState: pendingLocalState,
289
- },
290
- protocolHandlerBuilder);
291
-
292
- return PerformanceEvent.timedExecAsync(
293
- container.mc.logger,
294
- { eventName: "Load" },
295
- async (event) => new Promise<Container>((resolve, reject) => {
296
- const version = loadOptions.version;
297
-
298
- const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" };
299
- // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
300
- // to return container, so ignore this value and use undefined for opsBeforeReturn
301
- const mode: IContainerLoadMode = pendingLocalState
302
- ? { ...(loadOptions.loadMode ?? defaultMode), opsBeforeReturn: undefined }
303
- : loadOptions.loadMode ?? defaultMode;
304
-
305
- const onClosed = (err?: ICriticalContainerError) => {
306
- // pre-0.58 error message: containerClosedWithoutErrorDuringLoad
307
- reject(err ?? new GenericError("Container closed without error during load"));
308
- };
309
- container.on("closed", onClosed);
310
-
311
- container.load(version, mode, pendingLocalState)
312
- .finally(() => {
313
- container.removeListener("closed", onClosed);
314
- })
315
- .then((props) => {
316
- event.end({ ...props, ...loadOptions.loadMode });
317
- resolve(container);
318
- },
319
- (error) => {
320
- const err = normalizeError(error);
321
- // Depending where error happens, we can be attempting to connect to web socket
322
- // and continuously retrying (consider offline mode)
323
- // Host has no container to close, so it's prudent to do it here
324
- container.close(err);
325
- onClosed(err);
326
- });
327
- }),
328
- { start: true, end: true, cancel: "generic" },
329
- );
330
- }
331
-
332
- /**
333
- * Create a new container in a detached state.
334
- */
335
- public static async createDetached(
336
- loader: Loader,
337
- codeDetails: IFluidCodeDetails,
338
- protocolHandlerBuilder?: ProtocolHandlerBuilder,
339
- ): Promise<Container> {
340
- const container = new Container(
341
- loader,
342
- {},
343
- protocolHandlerBuilder);
344
-
345
- return PerformanceEvent.timedExecAsync(
346
- container.mc.logger,
347
- { eventName: "CreateDetached" },
348
- async (_event) => {
349
- await container.createDetached(codeDetails);
350
- return container;
351
- },
352
- { start: true, end: true, cancel: "generic" });
353
- }
354
-
355
- /**
356
- * Create a new container in a detached state that is initialized with a
357
- * snapshot from a previous detached container.
358
- */
359
- public static async rehydrateDetachedFromSnapshot(
360
- loader: Loader,
361
- snapshot: string,
362
- protocolHandlerBuilder?: ProtocolHandlerBuilder,
363
- ): Promise<Container> {
364
- const container = new Container(
365
- loader,
366
- {},
367
- protocolHandlerBuilder);
368
-
369
- return PerformanceEvent.timedExecAsync(
370
- container.mc.logger,
371
- { eventName: "RehydrateDetachedFromSnapshot" },
372
- async (_event) => {
373
- const deserializedSummary = JSON.parse(snapshot) as ISummaryTree;
374
- await container.rehydrateDetachedFromSnapshot(deserializedSummary);
375
- return container;
376
- },
377
- { start: true, end: true, cancel: "generic" });
378
- }
379
-
380
- public subLogger: TelemetryLogger;
381
-
382
- // Tells if container can reconnect on losing fist connection
383
- // If false, container gets closed on loss of connection.
384
- private readonly _canReconnect: boolean = true;
385
-
386
- private readonly mc: MonitoringContext;
387
-
388
- private _lifecycleState: "loading" | "loaded" | "closing" | "closed" = "loading";
389
-
390
- private setLoaded() {
391
- // It's conceivable the container could be closed when this is called
392
- // Only transition states if currently loading
393
- if (this._lifecycleState === "loading") {
394
- // Propagate current connection state through the system.
395
- this.propagateConnectionState(true /* initial transition */);
396
- this._lifecycleState = "loaded";
397
- }
398
- }
399
-
400
- public get closed(): boolean {
401
- return (this._lifecycleState === "closing" || this._lifecycleState === "closed");
402
- }
403
-
404
- private _attachState = AttachState.Detached;
405
-
406
- private readonly storageService: ContainerStorageAdapter;
407
- public get storage(): IDocumentStorageService {
408
- return this.storageService;
409
- }
410
-
411
- private readonly clientDetailsOverride: IClientDetails | undefined;
412
- private readonly _deltaManager: DeltaManager<ConnectionManager>;
413
- private service: IDocumentService | undefined;
414
-
415
- private _context: ContainerContext | undefined;
416
- private get context() {
417
- if (this._context === undefined) {
418
- throw new GenericError("Attempted to access context before it was defined");
419
- }
420
- return this._context;
421
- }
422
- private _protocolHandler: IProtocolHandler | undefined;
423
- private get protocolHandler() {
424
- if (this._protocolHandler === undefined) {
425
- throw new Error("Attempted to access protocolHandler before it was defined");
426
- }
427
- return this._protocolHandler;
428
- }
429
-
430
- /** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
431
- private inboundQueuePausedFromInit = true;
432
- private firstConnection = true;
433
- private readonly connectionTransitionTimes: number[] = [];
434
- private messageCountAfterDisconnection: number = 0;
435
- private _loadedFromVersion: IVersion | undefined;
436
- private _resolvedUrl: IFluidResolvedUrl | undefined;
437
- private attachStarted = false;
438
- private _dirtyContainer = false;
439
-
440
- private lastVisible: number | undefined;
441
- private readonly visibilityEventHandler: (() => void) | undefined;
442
- private readonly connectionStateHandler: IConnectionStateHandler;
443
-
444
- private setAutoReconnectTime = performance.now();
445
-
446
- private collabWindowTracker: CollabWindowTracker | undefined;
447
-
448
- private get connectionMode() { return this._deltaManager.connectionManager.connectionMode; }
449
-
450
- public get IFluidRouter(): IFluidRouter { return this; }
451
-
452
- public get resolvedUrl(): IResolvedUrl | undefined {
453
- return this._resolvedUrl;
454
- }
455
-
456
- public get loadedFromVersion(): IVersion | undefined {
457
- return this._loadedFromVersion;
458
- }
459
-
460
- public get readOnlyInfo(): ReadOnlyInfo {
461
- return this._deltaManager.readOnlyInfo;
462
- }
463
-
464
- public get closeSignal(): AbortSignal {
465
- return this._deltaManager.closeAbortController.signal;
466
- }
467
-
468
- /**
469
- * Tracks host requiring read-only mode.
470
- */
471
- public forceReadonly(readonly: boolean) {
472
- this._deltaManager.connectionManager.forceReadonly(readonly);
473
- }
474
-
475
- public get deltaManager(): IDeltaManager<ISequencedDocumentMessage, IDocumentMessage> {
476
- return this._deltaManager;
477
- }
478
-
479
- public get connectionState(): ConnectionState {
480
- return this.connectionStateHandler.connectionState;
481
- }
482
-
483
- public get connected(): boolean {
484
- return this.connectionStateHandler.connectionState === ConnectionState.Connected;
485
- }
486
-
487
- /**
488
- * Service configuration details. If running in offline mode will be undefined otherwise will contain service
489
- * configuration details returned as part of the initial connection.
490
- */
491
- public get serviceConfiguration(): IClientConfiguration | undefined {
492
- return this._deltaManager.serviceConfiguration;
493
- }
494
-
495
- private _clientId: string | undefined;
496
-
497
- /**
498
- * The server provided id of the client.
499
- * Set once this.connected is true, otherwise undefined
500
- */
501
- public get clientId(): string | undefined {
502
- return this._clientId;
503
- }
504
-
505
- /**
506
- * The server provided claims of the client.
507
- * Set once this.connected is true, otherwise undefined
508
- */
509
- public get scopes(): string[] | undefined {
510
- return this._deltaManager.connectionManager.scopes;
511
- }
512
-
513
- public get clientDetails(): IClientDetails {
514
- return this._deltaManager.clientDetails;
515
- }
516
-
517
- /**
518
- * Get the code details that are currently specified for the container.
519
- * @returns The current code details if any are specified, undefined if none are specified.
520
- */
521
- public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
522
- return this.getCodeDetailsFromQuorum();
523
- }
524
-
525
- /**
526
- * Get the code details that were used to load the container.
527
- * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
528
- * loaded.
529
- */
530
- public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
531
- return this._context?.codeDetails;
532
- }
533
-
534
- /**
535
- * Retrieves the audience associated with the document
536
- */
537
- public get audience(): IAudience {
538
- return this.protocolHandler.audience;
539
- }
540
-
541
- /**
542
- * Returns true if container is dirty.
543
- * Which means data loss if container is closed at that same moment
544
- * Most likely that happens when there is no network connection to Relay Service
545
- */
546
- public get isDirty() {
547
- return this._dirtyContainer;
548
- }
549
-
550
- private get serviceFactory() { return this.loader.services.documentServiceFactory; }
551
- private get urlResolver() { return this.loader.services.urlResolver; }
552
- public readonly options: ILoaderOptions;
553
- private get scope() { return this.loader.services.scope; }
554
- private get codeLoader() { return this.loader.services.codeLoader; }
555
-
556
- constructor(
557
- private readonly loader: Loader,
558
- config: IContainerConfig,
559
- private readonly protocolHandlerBuilder?: ProtocolHandlerBuilder,
560
- ) {
561
- super((name, error) => {
562
- this.mc.logger.sendErrorEvent(
563
- {
564
- eventName: "ContainerEventHandlerException",
565
- name: typeof name === "string" ? name : undefined,
566
- },
567
- error);
568
- });
569
-
570
- this.clientDetailsOverride = config.clientDetailsOverride;
571
- this._resolvedUrl = config.resolvedUrl;
572
- if (config.canReconnect !== undefined) {
573
- this._canReconnect = config.canReconnect;
574
- }
575
-
576
- // Create logger for data stores to use
577
- const type = this.client.details.type;
578
- const interactive = this.client.details.capabilities.interactive;
579
- const clientType =
580
- `${interactive ? "interactive" : "noninteractive"}${type !== undefined && type !== "" ? `/${type}` : ""}`;
581
- // Need to use the property getter for docId because for detached flow we don't have the docId initially.
582
- // We assign the id later so property getter is used.
583
- this.subLogger = ChildLogger.create(
584
- loader.services.subLogger,
585
- undefined,
586
- {
587
- all: {
588
- clientType, // Differentiating summarizer container from main container
589
- containerId: uuid(),
590
- docId: () => this._resolvedUrl?.id ?? undefined,
591
- containerAttachState: () => this._attachState,
592
- containerLifecycleState: () => this._lifecycleState,
593
- containerConnectionState: () => ConnectionState[this.connectionState],
594
- serializedContainer: config.serializedContainerState !== undefined,
595
- },
596
- // we need to be judicious with our logging here to avoid generating too much data
597
- // all data logged here should be broadly applicable, and not specific to a
598
- // specific error or class of errors
599
- error: {
600
- // load information to associate errors with the specific load point
601
- dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
602
- dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
603
- dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
604
- containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
605
- containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
606
- // message information to associate errors with the specific execution state
607
- // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
608
- dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
609
- dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
610
- dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
611
- connectionStateDuration:
612
- () => performance.now() - this.connectionTransitionTimes[this.connectionState],
613
- },
614
- });
615
-
616
- // Prefix all events in this file with container-loader
617
- this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
618
-
619
- const summarizeProtocolTree =
620
- this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")
621
- ?? this.loader.services.options.summarizeProtocolTree;
622
-
623
- this.options = {
624
- ... this.loader.services.options,
625
- summarizeProtocolTree,
626
- };
627
-
628
- this._deltaManager = this.createDeltaManager();
629
-
630
- this._clientId = config.serializedContainerState?.clientId;
631
- this.connectionStateHandler = createConnectionStateHandler(
632
- {
633
- logger: this.mc.logger,
634
- connectionStateChanged: (value, oldState, reason) => {
635
- if (value === ConnectionState.Connected) {
636
- this._clientId = this.connectionStateHandler.pendingClientId;
637
- }
638
- this.logConnectionStateChangeTelemetry(value, oldState, reason);
639
- if (this._lifecycleState === "loaded") {
640
- this.propagateConnectionState(
641
- false /* initial transition */,
642
- value === ConnectionState.Disconnected ? reason : undefined /* disconnectedReason */,
643
- );
644
- }
645
- },
646
- shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
647
- maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
648
- logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
649
- const mode = this.connectionMode;
650
- // We get here when socket does not receive any ops on "write" connection, including
651
- // its own join op. Attempt recovery option.
652
- this._deltaManager.logConnectionIssue({
653
- eventName,
654
- mode,
655
- duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp],
656
- ...(details === undefined ? {} : { details: JSON.stringify(details) }),
657
- });
658
-
659
- // If this is "write" connection, it took too long to receive join op. But in most cases that's due
660
- // to very slow op fetches and we will eventually get there.
661
- // For "read" connections, we get here due to self join signal not arriving on time. We will need to
662
- // better understand when and why it may happen.
663
- // For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
664
- // current state of audience.
665
- // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
666
- // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
667
- if (mode === "read") {
668
- this.disconnect();
669
- this.connect();
670
- }
671
- },
672
- },
673
- this.deltaManager,
674
- this._clientId,
675
- );
676
-
677
- this.on(savedContainerEvent, () => {
678
- this.connectionStateHandler.containerSaved();
679
- });
680
-
681
- this.storageService = new ContainerStorageAdapter(
682
- this.loader.services.detachedBlobStorage,
683
- this.mc.logger,
684
- this.options.summarizeProtocolTree === true
685
- ? () => this.captureProtocolSummary()
686
- : undefined,
687
- );
688
-
689
- const isDomAvailable = typeof document === "object" &&
690
- document !== null &&
691
- typeof document.addEventListener === "function" &&
692
- document.addEventListener !== null;
693
- // keep track of last time page was visible for telemetry
694
- if (isDomAvailable) {
695
- this.lastVisible = document.hidden ? performance.now() : undefined;
696
- this.visibilityEventHandler = () => {
697
- if (document.hidden) {
698
- this.lastVisible = performance.now();
699
- } else {
700
- // settimeout so this will hopefully fire after disconnect event if being hidden caused it
701
- setTimeout(() => { this.lastVisible = undefined; }, 0);
702
- }
703
- };
704
- document.addEventListener("visibilitychange", this.visibilityEventHandler);
705
- }
706
-
707
- // We observed that most users of platform do not check Container.connected event on load, causing bugs.
708
- // As such, we are raising events when new listener pops up.
709
- // Note that we can raise both "disconnected" & "connect" events at the same time,
710
- // if we are in connecting stage.
711
- this.on("newListener", (event: string, listener: (...args: any[]) => void) => {
712
- // Fire events on the end of JS turn, giving a chance for caller to be in consistent state.
713
- Promise.resolve().then(() => {
714
- switch (event) {
715
- case dirtyContainerEvent:
716
- if (this._dirtyContainer) {
717
- listener();
718
- }
719
- break;
720
- case savedContainerEvent:
721
- if (!this._dirtyContainer) {
722
- listener();
723
- }
724
- break;
725
- case connectedEventName:
726
- if (this.connected) {
727
- listener(this.clientId);
728
- }
729
- break;
730
- case disconnectedEventName:
731
- if (!this.connected) {
732
- listener();
733
- }
734
- break;
735
- default:
736
- }
737
- }).catch((error) => {
738
- this.mc.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
739
- });
740
- });
741
- }
742
-
743
- /**
744
- * Retrieves the quorum associated with the document
745
- */
746
- public getQuorum(): IQuorumClients {
747
- return this.protocolHandler.quorum;
748
- }
749
-
750
- public close(error?: ICriticalContainerError) {
751
- // 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
752
- // 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
753
- // handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
754
- // "closing" will lose that info (can also solve by tracking extra state).
755
- this._deltaManager.close(error);
756
- assert(this.connectionState === ConnectionState.Disconnected,
757
- 0x0cf /* "disconnect event was not raised!" */);
758
-
759
- assert(this._lifecycleState === "closed", 0x314 /* Container properly closed */);
760
- }
761
-
762
- private closeCore(error?: ICriticalContainerError) {
763
- assert(!this.closed, 0x315 /* re-entrancy */);
764
-
765
- try {
766
- // Ensure that we raise all key events even if one of these throws
767
- try {
768
- // Raise event first, to ensure we capture _lifecycleState before transition.
769
- // This gives us a chance to know what errors happened on open vs. on fully loaded container.
770
- this.mc.logger.sendTelemetryEvent(
771
- {
772
- eventName: "ContainerClose",
773
- category: error === undefined ? "generic" : "error",
774
- },
775
- error,
776
- );
777
-
778
- this._lifecycleState = "closing";
779
-
780
- this._protocolHandler?.close();
781
-
782
- this.connectionStateHandler.dispose();
783
-
784
- this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
785
-
786
- this.storageService.dispose();
787
-
788
- // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
789
- // about file, like file being overwritten in storage, but client having stale local cache.
790
- // Driver need to ensure all caches are cleared on critical errors
791
- this.service?.dispose(error);
792
- } catch (exception) {
793
- this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
794
- }
795
-
796
- this.emit("closed", error);
797
-
798
- this.removeAllListeners();
799
- if (this.visibilityEventHandler !== undefined) {
800
- document.removeEventListener("visibilitychange", this.visibilityEventHandler);
801
- }
802
- } finally {
803
- this._lifecycleState = "closed";
804
- }
805
- }
806
-
807
- public closeAndGetPendingLocalState(): string {
808
- // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
809
- // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
810
- // a new clientId and a future container using stale pending state without the new clientId would resubmit them
811
- assert(this.attachState === AttachState.Attached, 0x0d1 /* "Container should be attached before close" */);
812
- assert(this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
813
- 0x0d2 /* "resolved url should be valid Fluid url" */);
814
- assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
815
- assert(this._protocolHandler.attributes.term !== undefined,
816
- 0x37e /* Must have a valid protocol handler instance */);
817
- const pendingState: IPendingContainerState = {
818
- pendingRuntimeState: this.context.getPendingLocalState(),
819
- url: this.resolvedUrl.url,
820
- protocol: this.protocolHandler.getProtocolState(),
821
- term: this._protocolHandler.attributes.term,
822
- clientId: this.clientId,
823
- };
824
-
825
- this.mc.logger.sendTelemetryEvent({ eventName: "CloseAndGetPendingLocalState" });
826
-
827
- this.close();
828
-
829
- return JSON.stringify(pendingState);
830
- }
831
-
832
- public get attachState(): AttachState {
833
- return this._attachState;
834
- }
835
-
836
- public serialize(): string {
837
- assert(this.attachState === AttachState.Detached, 0x0d3 /* "Should only be called in detached container" */);
838
-
839
- const appSummary: ISummaryTree = this.context.createSummary();
840
- const protocolSummary = this.captureProtocolSummary();
841
- const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
842
-
843
- if (this.loader.services.detachedBlobStorage && this.loader.services.detachedBlobStorage.size > 0) {
844
- combinedSummary.tree[".hasAttachmentBlobs"] = { type: SummaryType.Blob, content: "true" };
845
- }
846
- return JSON.stringify(combinedSummary);
847
- }
848
-
849
- public async attach(request: IRequest): Promise<void> {
850
- await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
851
- if (this._lifecycleState !== "loaded") {
852
- // pre-0.58 error message: containerNotValidForAttach
853
- throw new UsageError(`The Container is not in a valid state for attach [${this._lifecycleState}]`);
854
- }
855
-
856
- // If container is already attached or attach is in progress, throw an error.
857
- assert(this._attachState === AttachState.Detached && !this.attachStarted,
858
- 0x205 /* "attach() called more than once" */);
859
- this.attachStarted = true;
860
-
861
- // If attachment blobs were uploaded in detached state we will go through a different attach flow
862
- const hasAttachmentBlobs = this.loader.services.detachedBlobStorage !== undefined
863
- && this.loader.services.detachedBlobStorage.size > 0;
864
-
865
- try {
866
- assert(this.deltaManager.inbound.length === 0,
867
- 0x0d6 /* "Inbound queue should be empty when attaching" */);
868
-
869
- let summary: ISummaryTree;
870
- if (!hasAttachmentBlobs) {
871
- // Get the document state post attach - possibly can just call attach but we need to change the
872
- // semantics around what the attach means as far as async code goes.
873
- const appSummary: ISummaryTree = this.context.createSummary();
874
- const protocolSummary = this.captureProtocolSummary();
875
- summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
876
-
877
- // Set the state as attaching as we are starting the process of attaching container.
878
- // This should be fired after taking the summary because it is the place where we are
879
- // starting to attach the container to storage.
880
- // Also, this should only be fired in detached container.
881
- this._attachState = AttachState.Attaching;
882
- this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
883
- }
884
-
885
- // Actually go and create the resolved document
886
- const createNewResolvedUrl = await this.urlResolver.resolve(request);
887
- ensureFluidResolvedUrl(createNewResolvedUrl);
888
- if (this.service === undefined) {
889
- assert(this.client.details.type !== summarizerClientType,
890
- 0x2c4 /* "client should not be summarizer before container is created" */);
891
- this.service = await runWithRetry(
892
- async () => this.serviceFactory.createContainer(
893
- summary,
894
- createNewResolvedUrl,
895
- this.subLogger,
896
- false, // clientIsSummarizer
897
- ),
898
- "containerAttach",
899
- this.mc.logger,
900
- {
901
- cancel: this.closeSignal,
902
- }, // progress
903
- );
904
- }
905
- const resolvedUrl = this.service.resolvedUrl;
906
- ensureFluidResolvedUrl(resolvedUrl);
907
- this._resolvedUrl = resolvedUrl;
908
- await this.storageService.connectToService(this.service);
909
-
910
- if (hasAttachmentBlobs) {
911
- // upload blobs to storage
912
- assert(!!this.loader.services.detachedBlobStorage, 0x24e /* "assertion for type narrowing" */);
913
-
914
- // build a table mapping IDs assigned locally to IDs assigned by storage and pass it to runtime to
915
- // support blob handles that only know about the local IDs
916
- const redirectTable = new Map<string, string>();
917
- // if new blobs are added while uploading, upload them too
918
- while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
919
- const newIds = this.loader.services.detachedBlobStorage.getBlobIds().filter(
920
- (id) => !redirectTable.has(id));
921
- for (const id of newIds) {
922
- const blob = await this.loader.services.detachedBlobStorage.readBlob(id);
923
- const response = await this.storageService.createBlob(blob);
924
- redirectTable.set(id, response.id);
925
- }
926
- }
927
-
928
- // take summary and upload
929
- const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
930
- const protocolSummary = this.captureProtocolSummary();
931
- summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
932
-
933
- this._attachState = AttachState.Attaching;
934
- this.context.notifyAttaching(getSnapshotTreeFromSerializedContainer(summary));
935
-
936
- await this.storageService.uploadSummaryWithContext(summary, {
937
- referenceSequenceNumber: 0,
938
- ackHandle: undefined,
939
- proposalHandle: undefined,
940
- });
941
- }
942
-
943
- this._attachState = AttachState.Attached;
944
- this.emit("attached");
945
-
946
- if (!this.closed) {
947
- this.resumeInternal({ fetchOpsFromStorage: false, reason: "createDetached" });
948
- }
949
- } catch (error) {
950
- // add resolved URL on error object so that host has the ability to find this document and delete it
951
- const newError = normalizeError(error);
952
- const resolvedUrl = this.resolvedUrl;
953
- if (isFluidResolvedUrl(resolvedUrl)) {
954
- newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
955
- }
956
- this.close(newError);
957
- throw newError;
958
- }
959
- },
960
- { start: true, end: true, cancel: "generic" });
961
- }
962
-
963
- public async request(path: IRequest): Promise<IResponse> {
964
- return PerformanceEvent.timedExecAsync(
965
- this.mc.logger,
966
- { eventName: "Request" },
967
- async () => this.context.request(path),
968
- { end: true, cancel: "error" },
969
- );
970
- }
971
-
972
- private setAutoReconnectInternal(mode: ReconnectMode) {
973
- const currentMode = this._deltaManager.connectionManager.reconnectMode;
974
-
975
- if (currentMode === mode) {
976
- return;
977
- }
978
-
979
- const now = performance.now();
980
- const duration = now - this.setAutoReconnectTime;
981
- this.setAutoReconnectTime = now;
982
-
983
- this.mc.logger.sendTelemetryEvent({
984
- eventName: mode === ReconnectMode.Enabled ? "AutoReconnectEnabled" : "AutoReconnectDisabled",
985
- connectionMode: this.connectionMode,
986
- connectionState: ConnectionState[this.connectionState],
987
- duration,
988
- });
989
-
990
- this._deltaManager.connectionManager.setAutoReconnect(mode);
991
- }
992
-
993
- public connect() {
994
- if (this.closed) {
995
- throw new UsageError(`The Container is closed and cannot be connected`);
996
- } else if (this._attachState !== AttachState.Attached) {
997
- throw new UsageError(`The Container is not attached and cannot be connected`);
998
- } else if (!this.connected) {
999
- // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
1000
- // If there is gap, we will learn about it once connected, but the gap should be small (if any),
1001
- // assuming that connect() is called quickly after initial container boot.
1002
- this.connectInternal({ reason: "DocumentConnect", fetchOpsFromStorage: false });
1003
- }
1004
- }
1005
-
1006
- private connectInternal(args: IConnectionArgs) {
1007
- assert(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
1008
- assert(this._attachState === AttachState.Attached,
1009
- 0x2c6 /* "Attempting to connect() a container that is not attached" */);
1010
-
1011
- // Resume processing ops and connect to delta stream
1012
- this.resumeInternal(args);
1013
-
1014
- // Set Auto Reconnect Mode
1015
- const mode = ReconnectMode.Enabled;
1016
- this.setAutoReconnectInternal(mode);
1017
- }
1018
-
1019
- public disconnect() {
1020
- if (this.closed) {
1021
- throw new UsageError(`The Container is closed and cannot be disconnected`);
1022
- } else {
1023
- this.disconnectInternal();
1024
- }
1025
- }
1026
-
1027
- private disconnectInternal() {
1028
- assert(!this.closed, 0x2c7 /* "Attempting to disconnect() a closed Container" */);
1029
-
1030
- // Set Auto Reconnect Mode
1031
- const mode = ReconnectMode.Disabled;
1032
- this.setAutoReconnectInternal(mode);
1033
- }
1034
-
1035
- private resumeInternal(args: IConnectionArgs) {
1036
- assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
1037
-
1038
- // Resume processing ops
1039
- if (this.inboundQueuePausedFromInit) {
1040
- this.inboundQueuePausedFromInit = false;
1041
- this._deltaManager.inbound.resume();
1042
- this._deltaManager.inboundSignal.resume();
1043
- }
1044
-
1045
- // Ensure connection to web socket
1046
- this.connectToDeltaStream(args);
1047
- }
1048
-
1049
- public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1050
- if (this.resolvedUrl === undefined) {
1051
- return undefined;
1052
- }
1053
-
1054
- return this.urlResolver.getAbsoluteUrl(
1055
- this.resolvedUrl,
1056
- relativeUrl,
1057
- getPackageName(this._context?.codeDetails));
1058
- }
1059
-
1060
- public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1061
- if (!isFluidCodeDetails(codeDetails)) {
1062
- throw new Error("Provided codeDetails are not IFluidCodeDetails");
1063
- }
1064
-
1065
- if (this.codeLoader.IFluidCodeDetailsComparer) {
1066
- const comparison = await this.codeLoader.IFluidCodeDetailsComparer.compare(
1067
- codeDetails,
1068
- this.getCodeDetailsFromQuorum());
1069
- if (comparison !== undefined && comparison <= 0) {
1070
- throw new Error("Proposed code details should be greater than the current");
1071
- }
1072
- }
1073
-
1074
- return this.protocolHandler.quorum.propose("code", codeDetails)
1075
- .then(() => true)
1076
- .catch(() => false);
1077
- }
1078
-
1079
- private async processCodeProposal(): Promise<void> {
1080
- const codeDetails = this.getCodeDetailsFromQuorum();
1081
-
1082
- await Promise.all([
1083
- this.deltaManager.inbound.pause(),
1084
- this.deltaManager.inboundSignal.pause()]);
1085
-
1086
- if ((await this.context.satisfies(codeDetails) === true)) {
1087
- this.deltaManager.inbound.resume();
1088
- this.deltaManager.inboundSignal.resume();
1089
- return;
1090
- }
1091
-
1092
- // pre-0.58 error message: existingContextDoesNotSatisfyIncomingProposal
1093
- this.close(new GenericError("Existing context does not satisfy incoming proposal"));
1094
- }
1095
-
1096
- private async getVersion(version: string | null): Promise<IVersion | undefined> {
1097
- const versions = await this.storageService.getVersions(version, 1);
1098
- return versions[0];
1099
- }
1100
-
1101
- private recordConnectStartTime() {
1102
- if (this.connectionTransitionTimes[ConnectionState.Disconnected] === undefined) {
1103
- this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
1104
- }
1105
- }
1106
-
1107
- private connectToDeltaStream(args: IConnectionArgs) {
1108
- this.recordConnectStartTime();
1109
-
1110
- // All agents need "write" access, including summarizer.
1111
- if (!this._canReconnect || !this.client.details.capabilities.interactive) {
1112
- args.mode = "write";
1113
- }
1114
-
1115
- this._deltaManager.connect(args);
1116
- }
1117
-
1118
- /**
1119
- * Load container.
1120
- *
1121
- * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
1122
- */
1123
- private async load(
1124
- specifiedVersion: string | undefined,
1125
- loadMode: IContainerLoadMode,
1126
- pendingLocalState?: IPendingContainerState,
1127
- ) {
1128
- if (this._resolvedUrl === undefined) {
1129
- throw new Error("Attempting to load without a resolved url");
1130
- }
1131
- this.service = await this.serviceFactory.createDocumentService(
1132
- this._resolvedUrl,
1133
- this.subLogger,
1134
- this.client.details.type === summarizerClientType,
1135
- );
1136
-
1137
- // Ideally we always connect as "read" by default.
1138
- // Currently that works with SPO & r11s, because we get "write" connection when connecting to non-existing file.
1139
- // We should not rely on it by (one of them will address the issue, but we need to address both)
1140
- // 1) switching create new flow to one where we create file by posting snapshot
1141
- // 2) Fixing quorum workflows (have retry logic)
1142
- // That all said, "read" does not work with memorylicious workflows (that opens two simultaneous
1143
- // connections to same file) in two ways:
1144
- // A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
1145
- // B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
1146
- const connectionArgs: IConnectionArgs = { reason: "DocumentOpen", mode: "write", fetchOpsFromStorage: false };
1147
-
1148
- // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1149
- // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1150
- if (loadMode.deltaConnection === undefined) {
1151
- this.connectToDeltaStream(connectionArgs);
1152
- }
1153
-
1154
- if (!pendingLocalState) {
1155
- await this.storageService.connectToService(this.service);
1156
- } else {
1157
- // if we have pendingLocalState we can load without storage; don't wait for connection
1158
- this.storageService.connectToService(this.service).catch((error) => this.close(error));
1159
- }
1160
-
1161
- this._attachState = AttachState.Attached;
1162
-
1163
- // Fetch specified snapshot.
1164
- const { snapshot, versionId } = pendingLocalState === undefined
1165
- ? await this.fetchSnapshotTree(specifiedVersion)
1166
- : { snapshot: undefined, versionId: undefined };
1167
- assert(snapshot !== undefined || pendingLocalState !== undefined, 0x237 /* "Snapshot should exist" */);
1168
-
1169
- const attributes: IDocumentAttributes = pendingLocalState === undefined
1170
- ? await this.getDocumentAttributes(this.storageService, snapshot)
1171
- : {
1172
- sequenceNumber: pendingLocalState.protocol.sequenceNumber,
1173
- minimumSequenceNumber: pendingLocalState.protocol.minimumSequenceNumber,
1174
- term: pendingLocalState.term,
1175
- };
1176
-
1177
- let opsBeforeReturnP: Promise<void> | undefined;
1178
-
1179
- // Attach op handlers to finish initialization and be able to start processing ops
1180
- // Kick off any ops fetching if required.
1181
- switch (loadMode.opsBeforeReturn) {
1182
- case undefined:
1183
- // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
1184
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1185
- this.attachDeltaManagerOpHandler(attributes, loadMode.deltaConnection !== "none" ? "all" : "none");
1186
- break;
1187
- case "cached":
1188
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "cached");
1189
- break;
1190
- case "all":
1191
- opsBeforeReturnP = this.attachDeltaManagerOpHandler(attributes, "all");
1192
- break;
1193
- default:
1194
- unreachableCase(loadMode.opsBeforeReturn);
1195
- }
1196
-
1197
- // ...load in the existing quorum
1198
- // Initialize the protocol handler
1199
- if (pendingLocalState === undefined) {
1200
- await this.initializeProtocolStateFromSnapshot(
1201
- attributes,
1202
- this.storageService,
1203
- snapshot);
1204
- } else {
1205
- this.initializeProtocolState(
1206
- attributes,
1207
- {
1208
- members: pendingLocalState.protocol.members,
1209
- proposals: pendingLocalState.protocol.proposals,
1210
- values: pendingLocalState.protocol.values,
1211
- }, // pending IQuorumSnapshot
1212
- );
1213
- }
1214
-
1215
- const codeDetails = this.getCodeDetailsFromQuorum();
1216
- await this.instantiateContext(
1217
- true, // existing
1218
- codeDetails,
1219
- snapshot,
1220
- pendingLocalState?.pendingRuntimeState,
1221
- );
1222
-
1223
- // We might have hit some failure that did not manifest itself in exception in this flow,
1224
- // do not start op processing in such case - static version of Container.load() will handle it correctly.
1225
- if (!this.closed) {
1226
- if (opsBeforeReturnP !== undefined) {
1227
- this._deltaManager.inbound.resume();
1228
-
1229
- await ReportIfTooLong(
1230
- this.mc.logger,
1231
- "WaitOps",
1232
- async () => { await opsBeforeReturnP; return {}; });
1233
- await ReportIfTooLong(
1234
- this.mc.logger,
1235
- "WaitOpProcessing",
1236
- async () => this._deltaManager.inbound.waitTillProcessingDone());
1237
-
1238
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1239
- this._deltaManager.inbound.pause();
1240
- }
1241
-
1242
- switch (loadMode.deltaConnection) {
1243
- case undefined:
1244
- case "delayed":
1245
- assert(this.inboundQueuePausedFromInit, 0x346 /* inboundQueuePausedFromInit should be true */);
1246
- this.inboundQueuePausedFromInit = false;
1247
- this._deltaManager.inbound.resume();
1248
- this._deltaManager.inboundSignal.resume();
1249
- break;
1250
- case "none":
1251
- break;
1252
- default:
1253
- unreachableCase(loadMode.deltaConnection);
1254
- }
1255
- }
1256
-
1257
- // Safety net: static version of Container.load() should have learned about it through "closed" handler.
1258
- // But if that did not happen for some reason, fail load for sure.
1259
- // Otherwise we can get into situations where container is closed and does not try to connect to ordering
1260
- // service, but caller does not know that (callers do expect container to be not closed on successful path
1261
- // and listen only on "closed" event)
1262
- if (this.closed) {
1263
- throw new Error("Container was closed while load()");
1264
- }
1265
-
1266
- // Internal context is fully loaded at this point
1267
- this.setLoaded();
1268
-
1269
- return {
1270
- sequenceNumber: attributes.sequenceNumber,
1271
- version: versionId,
1272
- dmLastProcessedSeqNumber: this._deltaManager.lastSequenceNumber,
1273
- dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
1274
- };
1275
- }
1276
-
1277
- private async createDetached(source: IFluidCodeDetails) {
1278
- const attributes: IDocumentAttributes = {
1279
- sequenceNumber: detachedContainerRefSeqNumber,
1280
- term: 1,
1281
- minimumSequenceNumber: 0,
1282
- };
1283
-
1284
- await this.attachDeltaManagerOpHandler(attributes);
1285
-
1286
- // Need to just seed the source data in the code quorum. Quorum itself is empty
1287
- const qValues = initQuorumValuesFromCodeDetails(source);
1288
- this.initializeProtocolState(
1289
- attributes,
1290
- {
1291
- members: [],
1292
- proposals: [],
1293
- values: qValues,
1294
- }, // IQuorumSnapShot
1295
- );
1296
-
1297
- // The load context - given we seeded the quorum - will be great
1298
- await this.instantiateContextDetached(
1299
- false, // existing
1300
- );
1301
-
1302
- this.setLoaded();
1303
- }
1304
-
1305
- private async rehydrateDetachedFromSnapshot(detachedContainerSnapshot: ISummaryTree) {
1306
- if (detachedContainerSnapshot.tree[".hasAttachmentBlobs"] !== undefined) {
1307
- assert(!!this.loader.services.detachedBlobStorage && this.loader.services.detachedBlobStorage.size > 0,
1308
- 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */);
1309
- delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
1310
- }
1311
-
1312
- const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1313
- this.storageService.loadSnapshotForRehydratingContainer(snapshotTree);
1314
- const attributes = await this.getDocumentAttributes(this.storageService, snapshotTree);
1315
-
1316
- await this.attachDeltaManagerOpHandler(attributes);
1317
-
1318
- // Initialize the protocol handler
1319
- const baseTree = getProtocolSnapshotTree(snapshotTree);
1320
- const qValues = await readAndParse<[string, ICommittedProposal][]>(
1321
- this.storageService,
1322
- baseTree.blobs.quorumValues,
1323
- );
1324
- const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1325
- this.initializeProtocolState(
1326
- attributes,
1327
- {
1328
- members: [],
1329
- proposals: [],
1330
- values: codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1331
- }, // IQuorumSnapShot
1332
- );
1333
-
1334
- await this.instantiateContextDetached(
1335
- true, // existing
1336
- snapshotTree,
1337
- );
1338
-
1339
- this.setLoaded();
1340
- }
1341
-
1342
- private async getDocumentAttributes(
1343
- storage: IDocumentStorageService,
1344
- tree: ISnapshotTree | undefined,
1345
- ): Promise<IDocumentAttributes> {
1346
- if (tree === undefined) {
1347
- return {
1348
- minimumSequenceNumber: 0,
1349
- sequenceNumber: 0,
1350
- term: 1,
1351
- };
1352
- }
1353
-
1354
- // Backward compatibility: old docs would have ".attributes" instead of "attributes"
1355
- const attributesHash = ".protocol" in tree.trees
1356
- ? tree.trees[".protocol"].blobs.attributes
1357
- : tree.blobs[".attributes"];
1358
-
1359
- const attributes = await readAndParse<IDocumentAttributes>(storage, attributesHash);
1360
-
1361
- // Backward compatibility for older summaries with no term
1362
- if (attributes.term === undefined) {
1363
- attributes.term = 1;
1364
- }
1365
-
1366
- return attributes;
1367
- }
1368
-
1369
- private async initializeProtocolStateFromSnapshot(
1370
- attributes: IDocumentAttributes,
1371
- storage: IDocumentStorageService,
1372
- snapshot: ISnapshotTree | undefined,
1373
- ): Promise<void> {
1374
- const quorumSnapshot: IQuorumSnapshot = {
1375
- members: [],
1376
- proposals: [],
1377
- values: [],
1378
- };
1379
-
1380
- if (snapshot !== undefined) {
1381
- const baseTree = getProtocolSnapshotTree(snapshot);
1382
- [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] = await Promise.all([
1383
- readAndParse<[string, ISequencedClient][]>(storage, baseTree.blobs.quorumMembers),
1384
- readAndParse<[number, ISequencedProposal, string[]][]>(storage, baseTree.blobs.quorumProposals),
1385
- readAndParse<[string, ICommittedProposal][]>(storage, baseTree.blobs.quorumValues),
1386
- ]);
1387
- }
1388
-
1389
- this.initializeProtocolState(attributes, quorumSnapshot);
1390
- }
1391
-
1392
- private initializeProtocolState(
1393
- attributes: IDocumentAttributes,
1394
- quorumSnapshot: IQuorumSnapshot,
1395
- ): void {
1396
- const protocolHandlerBuilder =
1397
- this.protocolHandlerBuilder ?? ((...args) => new ProtocolHandler(...args, new Audience()));
1398
- const protocol = protocolHandlerBuilder(
1399
- attributes,
1400
- quorumSnapshot,
1401
- (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1402
- );
1403
-
1404
- const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1405
-
1406
- protocol.quorum.on("error", (error) => {
1407
- protocolLogger.sendErrorEvent(error);
1408
- });
1409
-
1410
- // Track membership changes and update connection state accordingly
1411
- this.connectionStateHandler.initProtocol(protocol);
1412
-
1413
- protocol.quorum.on("addProposal", (proposal: ISequencedProposal) => {
1414
- if (proposal.key === "code" || proposal.key === "code2") {
1415
- this.emit("codeDetailsProposed", proposal.value, proposal);
1416
- }
1417
- });
1418
-
1419
- protocol.quorum.on(
1420
- "approveProposal",
1421
- (sequenceNumber, key, value) => {
1422
- if (key === "code" || key === "code2") {
1423
- if (!isFluidCodeDetails(value)) {
1424
- this.mc.logger.sendErrorEvent({
1425
- eventName: "CodeProposalNotIFluidCodeDetails",
1426
- });
1427
- }
1428
- this.processCodeProposal().catch((error) => {
1429
- this.close(normalizeError(error));
1430
- throw error;
1431
- });
1432
- }
1433
- });
1434
- // we need to make sure this member get set in a synchronous context,
1435
- // or other things can happen after the object that will be set is created, but not yet set
1436
- // this was breaking this._initialClients handling
1437
- //
1438
- this._protocolHandler = protocol;
1439
- }
1440
-
1441
- private captureProtocolSummary(): ISummaryTree {
1442
- const quorumSnapshot = this.protocolHandler.snapshot();
1443
- const summary: ISummaryTree = {
1444
- tree: {
1445
- attributes: {
1446
- content: JSON.stringify(this.protocolHandler.attributes),
1447
- type: SummaryType.Blob,
1448
- },
1449
- quorumMembers: {
1450
- content: JSON.stringify(quorumSnapshot.members),
1451
- type: SummaryType.Blob,
1452
- },
1453
- quorumProposals: {
1454
- content: JSON.stringify(quorumSnapshot.proposals),
1455
- type: SummaryType.Blob,
1456
- },
1457
- quorumValues: {
1458
- content: JSON.stringify(quorumSnapshot.values),
1459
- type: SummaryType.Blob,
1460
- },
1461
- },
1462
- type: SummaryType.Tree,
1463
- };
1464
-
1465
- return summary;
1466
- }
1467
-
1468
- private getCodeDetailsFromQuorum(): IFluidCodeDetails {
1469
- const quorum = this.protocolHandler.quorum;
1470
-
1471
- const pkg = getCodeProposal(quorum);
1472
-
1473
- return pkg as IFluidCodeDetails;
1474
- }
1475
-
1476
- private get client(): IClient {
1477
- const client: IClient = this.options?.client !== undefined
1478
- ? (this.options.client as IClient)
1479
- : {
1480
- details: {
1481
- capabilities: { interactive: true },
1482
- },
1483
- mode: "read", // default reconnection mode on lost connection / connection error
1484
- permission: [],
1485
- scopes: [],
1486
- user: { id: "" },
1487
- };
1488
-
1489
- if (this.clientDetailsOverride !== undefined) {
1490
- merge(client.details, this.clientDetailsOverride);
1491
- }
1492
- client.details.environment = [client.details.environment, ` loaderVersion:${pkgVersion}`].join(";");
1493
- return client;
1494
- }
1495
-
1496
- /**
1497
- * Returns true if connection is active, i.e. it's "write" connection and
1498
- * container runtime was notified about this connection (i.e. we are up-to-date and could send ops).
1499
- * This happens after client received its own joinOp and thus is in the quorum.
1500
- * If it's not true, runtime is not in position to send ops.
1501
- */
1502
- private activeConnection() {
1503
- return this.connectionState === ConnectionState.Connected &&
1504
- this.connectionMode === "write";
1505
- }
1506
-
1507
- private createDeltaManager() {
1508
- const serviceProvider = () => this.service;
1509
- const deltaManager = new DeltaManager<ConnectionManager>(
1510
- serviceProvider,
1511
- ChildLogger.create(this.subLogger, "DeltaManager"),
1512
- () => this.activeConnection(),
1513
- (props: IConnectionManagerFactoryArgs) => new ConnectionManager(
1514
- serviceProvider,
1515
- this.client,
1516
- this._canReconnect,
1517
- ChildLogger.create(this.subLogger, "ConnectionManager"),
1518
- props),
1519
- );
1520
-
1521
- // Disable inbound queues as Container is not ready to accept any ops until we are fully loaded!
1522
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1523
- deltaManager.inbound.pause();
1524
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1525
- deltaManager.inboundSignal.pause();
1526
-
1527
- deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
1528
- assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1529
- this.connectionStateHandler.receivedConnectEvent(
1530
- details,
1531
- );
1532
- });
1533
-
1534
- deltaManager.on("disconnect", (reason: string) => {
1535
- this.collabWindowTracker?.stopSequenceNumberUpdate();
1536
- this.connectionStateHandler.receivedDisconnectEvent(reason);
1537
- });
1538
-
1539
- deltaManager.on("throttled", (warning: IThrottlingWarning) => {
1540
- const warn = warning as ContainerWarning;
1541
- // Some "warning" events come from outside the container and are logged
1542
- // elsewhere (e.g. summarizing container). We shouldn't log these here.
1543
- if (warn.logged !== true) {
1544
- this.mc.logger.sendTelemetryEvent({ eventName: "ContainerWarning" }, warn);
1545
- }
1546
- this.emit("warning", warn);
1547
- });
1548
-
1549
- deltaManager.on("readonly", (readonly) => {
1550
- this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1551
- this.emit("readonly", readonly);
1552
- });
1553
-
1554
- deltaManager.on("closed", (error?: ICriticalContainerError) => {
1555
- this.closeCore(error);
1556
- });
1557
-
1558
- return deltaManager;
1559
- }
1560
-
1561
- private async attachDeltaManagerOpHandler(
1562
- attributes: IDocumentAttributes,
1563
- prefetchType?: "cached" | "all" | "none") {
1564
- return this._deltaManager.attachOpHandler(
1565
- attributes.minimumSequenceNumber,
1566
- attributes.sequenceNumber,
1567
- attributes.term ?? 1,
1568
- {
1569
- process: (message) => this.processRemoteMessage(message),
1570
- processSignal: (message) => {
1571
- this.processSignal(message);
1572
- },
1573
- },
1574
- prefetchType);
1575
- }
1576
-
1577
- private logConnectionStateChangeTelemetry(
1578
- value: ConnectionState,
1579
- oldState: ConnectionState,
1580
- reason?: string,
1581
- ) {
1582
- // Log actual event
1583
- const time = performance.now();
1584
- this.connectionTransitionTimes[value] = time;
1585
- const duration = time - this.connectionTransitionTimes[oldState];
1586
-
1587
- let durationFromDisconnected: number | undefined;
1588
- let connectionInitiationReason: string | undefined;
1589
- let autoReconnect: ReconnectMode | undefined;
1590
- let checkpointSequenceNumber: number | undefined;
1591
- let opsBehind: number | undefined;
1592
- if (value === ConnectionState.Disconnected) {
1593
- autoReconnect = this._deltaManager.connectionManager.reconnectMode;
1594
- } else {
1595
- if (value === ConnectionState.Connected) {
1596
- durationFromDisconnected = time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1597
- durationFromDisconnected = TelemetryLogger.formatTick(durationFromDisconnected);
1598
- } else {
1599
- // This info is of most interest on establishing connection only.
1600
- checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1601
- if (this.deltaManager.hasCheckpointSequenceNumber) {
1602
- opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1603
- }
1604
- }
1605
- connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1606
- }
1607
-
1608
- this.mc.logger.sendPerformanceEvent({
1609
- eventName: `ConnectionStateChange_${ConnectionState[value]}`,
1610
- from: ConnectionState[oldState],
1611
- duration,
1612
- durationFromDisconnected,
1613
- reason,
1614
- connectionInitiationReason,
1615
- pendingClientId: this.connectionStateHandler.pendingClientId,
1616
- clientId: this.clientId,
1617
- autoReconnect,
1618
- opsBehind,
1619
- online: OnlineStatus[isOnline()],
1620
- lastVisible: this.lastVisible !== undefined ? performance.now() - this.lastVisible : undefined,
1621
- checkpointSequenceNumber,
1622
- quorumSize: this._protocolHandler?.quorum.getMembers().size,
1623
- ...this._deltaManager.connectionProps,
1624
- });
1625
-
1626
- if (value === ConnectionState.Connected) {
1627
- this.firstConnection = false;
1628
- }
1629
- }
1630
-
1631
- private propagateConnectionState(initialTransition: boolean, disconnectedReason?: string) {
1632
- // When container loaded, we want to propagate initial connection state.
1633
- // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1634
- // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1635
- if (!initialTransition &&
1636
- this.connectionState !== ConnectionState.Connected &&
1637
- this.connectionState !== ConnectionState.Disconnected) {
1638
- return;
1639
- }
1640
- const state = this.connectionState === ConnectionState.Connected;
1641
-
1642
- const logOpsOnReconnect: boolean =
1643
- this.connectionState === ConnectionState.Connected &&
1644
- !this.firstConnection &&
1645
- this.connectionMode === "write";
1646
- if (logOpsOnReconnect) {
1647
- this.messageCountAfterDisconnection = 0;
1648
- }
1649
-
1650
- // Both protocol and context should not be undefined if we got so far.
1651
-
1652
- this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1653
- this.protocolHandler.setConnectionState(state, this.clientId);
1654
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1655
-
1656
- if (logOpsOnReconnect) {
1657
- this.mc.logger.sendTelemetryEvent(
1658
- { eventName: "OpsSentOnReconnect", count: this.messageCountAfterDisconnection });
1659
- }
1660
- }
1661
-
1662
- // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1663
- private submitContainerMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1664
- switch (type) {
1665
- case MessageType.Operation:
1666
- return this.submitMessage(
1667
- type,
1668
- JSON.stringify(contents),
1669
- batch,
1670
- metadata);
1671
- case MessageType.Summarize:
1672
- return this.submitSummaryMessage(contents as unknown as ISummaryContent);
1673
- default:
1674
- this.close(new GenericError("invalidContainerSubmitOpType",
1675
- undefined /* error */,
1676
- { messageType: type }));
1677
- return -1;
1678
- }
1679
- }
1680
-
1681
- /** @returns clientSequenceNumber of last message in a batch */
1682
- private submitBatch(batch: IBatchMessage[]): number {
1683
- let clientSequenceNumber = -1;
1684
- for (const message of batch) {
1685
- clientSequenceNumber = this.submitMessage(
1686
- MessageType.Operation,
1687
- message.contents,
1688
- true, // batch
1689
- message.metadata,
1690
- message.compression);
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,
1710
- contents?: string,
1711
- batch?: boolean,
1712
- metadata?: any,
1713
- compression?: string): number {
1714
- if (this.connectionState !== ConnectionState.Connected) {
1715
- this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1716
- return -1;
1717
- }
1718
-
1719
- this.messageCountAfterDisconnection += 1;
1720
- this.collabWindowTracker?.stopSequenceNumberUpdate();
1721
- return this._deltaManager.submit(type, contents, batch, metadata, compression);
1722
- }
1723
-
1724
- private processRemoteMessage(message: ISequencedDocumentMessage) {
1725
- const local = this.clientId === message.clientId;
1726
-
1727
- // Allow the protocol handler to process the message
1728
- const result = this.protocolHandler.processMessage(message, local);
1729
-
1730
- // Forward messages to the loaded runtime for processing
1731
- this.context.process(message, local);
1732
-
1733
- // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1734
- if (this.activeConnection()) {
1735
- if (this.collabWindowTracker === undefined) {
1736
- // Note that config from first connection will be used for this container's lifetime.
1737
- // That means that if relay service changes settings, such changes will impact only newly booted
1738
- // clients.
1739
- // All existing will continue to use settings they got earlier.
1740
- assert(
1741
- this.serviceConfiguration !== undefined,
1742
- 0x2e4 /* "there should be service config for active connection" */);
1743
- this.collabWindowTracker = new CollabWindowTracker(
1744
- (type) => {
1745
- assert(this.activeConnection(),
1746
- 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1747
- this.submitMessage(type);
1748
- },
1749
- this.serviceConfiguration.noopTimeFrequency,
1750
- this.serviceConfiguration.noopCountFrequency,
1751
- );
1752
- }
1753
- this.collabWindowTracker.scheduleSequenceNumberUpdate(message, result.immediateNoOp === true);
1754
- }
1755
-
1756
- this.emit("op", message);
1757
- }
1758
-
1759
- private submitSignal(message: any) {
1760
- this._deltaManager.submitSignal(JSON.stringify(message));
1761
- }
1762
-
1763
- private processSignal(message: ISignalMessage) {
1764
- // No clientId indicates a system signal message.
1765
- if (message.clientId === null) {
1766
- this.protocolHandler.processSignal(message);
1767
- } else {
1768
- const local = this.clientId === message.clientId;
1769
- this.context.processSignal(message, local);
1770
- }
1771
- }
1772
-
1773
- /**
1774
- * Get the most recent snapshot, or a specific version.
1775
- * @param specifiedVersion - The specific version of the snapshot to retrieve
1776
- * @returns The snapshot requested, or the latest snapshot if no version was specified, plus version ID
1777
- */
1778
- private async fetchSnapshotTree(specifiedVersion: string | undefined):
1779
- Promise<{ snapshot?: ISnapshotTree; versionId?: string; }> {
1780
- const version = await this.getVersion(specifiedVersion ?? null);
1781
-
1782
- if (version === undefined && specifiedVersion !== undefined) {
1783
- // We should have a defined version to load from if specified version requested
1784
- this.mc.logger.sendErrorEvent({ eventName: "NoVersionFoundWhenSpecified", id: specifiedVersion });
1785
- }
1786
- this._loadedFromVersion = version;
1787
- const snapshot = await this.storageService.getSnapshotTree(version) ?? undefined;
1788
-
1789
- if (snapshot === undefined && version !== undefined) {
1790
- this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1791
- }
1792
- return { snapshot, versionId: version?.id };
1793
- }
1794
-
1795
- private async instantiateContextDetached(
1796
- existing: boolean,
1797
- snapshot?: ISnapshotTree,
1798
- ) {
1799
- const codeDetails = this.getCodeDetailsFromQuorum();
1800
- if (codeDetails === undefined) {
1801
- throw new Error("pkg should be provided in create flow!!");
1802
- }
1803
-
1804
- await this.instantiateContext(
1805
- existing,
1806
- codeDetails,
1807
- snapshot,
1808
- );
1809
- }
1810
-
1811
- private async instantiateContext(
1812
- existing: boolean,
1813
- codeDetails: IFluidCodeDetails,
1814
- snapshot?: ISnapshotTree,
1815
- pendingLocalState?: unknown,
1816
- ) {
1817
- assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
1818
-
1819
- // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
1820
- // are set. Global requests will still go directly to the loader
1821
- const loader = new RelativeLoader(this, this.loader);
1822
- this._context = await ContainerContext.createOrLoad(
1823
- this,
1824
- this.scope,
1825
- this.codeLoader,
1826
- codeDetails,
1827
- snapshot,
1828
- new DeltaManagerProxy(this._deltaManager),
1829
- new QuorumProxy(this.protocolHandler.quorum),
1830
- loader,
1831
- (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata),
1832
- (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
1833
- (batch: IBatchMessage[]) => this.submitBatch(batch),
1834
- (message) => this.submitSignal(message),
1835
- (error?: ICriticalContainerError) => this.close(error),
1836
- Container.version,
1837
- (dirty: boolean) => this.updateDirtyContainerState(dirty),
1838
- existing,
1839
- pendingLocalState,
1840
- );
1841
-
1842
- this.emit("contextChanged", codeDetails);
1843
- }
1844
-
1845
- private updateDirtyContainerState(dirty: boolean) {
1846
- if (this._dirtyContainer === dirty) {
1847
- return;
1848
- }
1849
- this._dirtyContainer = dirty;
1850
- this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
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
- }
297
+ /**
298
+ * @deprecated - In the next release Container will no longer be exported, IContainer should be used in its place.
299
+ */
300
+ export class Container
301
+ extends EventEmitterWithErrorHandling<IContainerEvents>
302
+ implements IContainer
303
+ {
304
+ public static version = "^0.1.0";
305
+
306
+ /**
307
+ * Load an existing container.
308
+ * @internal
309
+ */
310
+ public static async load(
311
+ loader: Loader,
312
+ loadOptions: IContainerLoadOptions,
313
+ pendingLocalState?: IPendingContainerState,
314
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
315
+ ): Promise<Container> {
316
+ const container = new Container(
317
+ loader,
318
+ {
319
+ clientDetailsOverride: loadOptions.clientDetailsOverride,
320
+ resolvedUrl: loadOptions.resolvedUrl,
321
+ canReconnect: loadOptions.canReconnect,
322
+ serializedContainerState: pendingLocalState,
323
+ },
324
+ protocolHandlerBuilder,
325
+ );
326
+
327
+ return PerformanceEvent.timedExecAsync(
328
+ container.mc.logger,
329
+ { eventName: "Load" },
330
+ async (event) =>
331
+ new Promise<Container>((resolve, reject) => {
332
+ const version = loadOptions.version;
333
+
334
+ const defaultMode: IContainerLoadMode = { opsBeforeReturn: "cached" };
335
+ // if we have pendingLocalState, anything we cached is not useful and we shouldn't wait for connection
336
+ // to return container, so ignore this value and use undefined for opsBeforeReturn
337
+ const mode: IContainerLoadMode = pendingLocalState
338
+ ? { ...(loadOptions.loadMode ?? defaultMode), opsBeforeReturn: undefined }
339
+ : loadOptions.loadMode ?? defaultMode;
340
+
341
+ const onClosed = (err?: ICriticalContainerError) => {
342
+ // pre-0.58 error message: containerClosedWithoutErrorDuringLoad
343
+ reject(
344
+ err ?? new GenericError("Container closed without error during load"),
345
+ );
346
+ };
347
+ container.on("closed", onClosed);
348
+
349
+ container
350
+ .load(version, mode, pendingLocalState)
351
+ .finally(() => {
352
+ container.removeListener("closed", onClosed);
353
+ })
354
+ .then(
355
+ (props) => {
356
+ event.end({ ...props, ...loadOptions.loadMode });
357
+ resolve(container);
358
+ },
359
+ (error) => {
360
+ const err = normalizeError(error);
361
+ // Depending where error happens, we can be attempting to connect to web socket
362
+ // and continuously retrying (consider offline mode)
363
+ // Host has no container to close, so it's prudent to do it here
364
+ container.close(err);
365
+ onClosed(err);
366
+ },
367
+ );
368
+ }),
369
+ { start: true, end: true, cancel: "generic" },
370
+ );
371
+ }
372
+
373
+ /**
374
+ * Create a new container in a detached state.
375
+ */
376
+ public static async createDetached(
377
+ loader: Loader,
378
+ codeDetails: IFluidCodeDetails,
379
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
380
+ ): Promise<Container> {
381
+ const container = new Container(loader, {}, protocolHandlerBuilder);
382
+
383
+ return PerformanceEvent.timedExecAsync(
384
+ container.mc.logger,
385
+ { eventName: "CreateDetached" },
386
+ async (_event) => {
387
+ await container.createDetached(codeDetails);
388
+ return container;
389
+ },
390
+ { start: true, end: true, cancel: "generic" },
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Create a new container in a detached state that is initialized with a
396
+ * snapshot from a previous detached container.
397
+ */
398
+ public static async rehydrateDetachedFromSnapshot(
399
+ loader: Loader,
400
+ snapshot: string,
401
+ protocolHandlerBuilder?: ProtocolHandlerBuilder,
402
+ ): Promise<Container> {
403
+ const container = new Container(loader, {}, protocolHandlerBuilder);
404
+
405
+ return PerformanceEvent.timedExecAsync(
406
+ container.mc.logger,
407
+ { eventName: "RehydrateDetachedFromSnapshot" },
408
+ async (_event) => {
409
+ const deserializedSummary = JSON.parse(snapshot) as ISummaryTree;
410
+ await container.rehydrateDetachedFromSnapshot(deserializedSummary);
411
+ return container;
412
+ },
413
+ { start: true, end: true, cancel: "generic" },
414
+ );
415
+ }
416
+
417
+ public subLogger: TelemetryLogger;
418
+
419
+ // Tells if container can reconnect on losing fist connection
420
+ // If false, container gets closed on loss of connection.
421
+ private readonly _canReconnect: boolean = true;
422
+
423
+ private readonly mc: MonitoringContext;
424
+
425
+ /**
426
+ * Lifecycle state of the container, used mainly to prevent re-entrancy and telemetry
427
+ *
428
+ * States are allowed to progress to further states:
429
+ * "loading" - "loaded" - "closing" - "disposing" - "closed" - "disposed"
430
+ *
431
+ * For example, moving from "closed" to "disposing" is not allowed since it is an earlier state.
432
+ *
433
+ * loading: Container has been created, but is not yet in normal/loaded state
434
+ * loaded: Container is in normal/loaded state
435
+ * closing: Container has started closing process (for re-entrancy prevention)
436
+ * disposing: Container has started disposing process (for re-entrancy prevention)
437
+ * closed: Container has closed
438
+ * disposed: Container has been disposed
439
+ */
440
+ private _lifecycleState:
441
+ | "loading"
442
+ | "loaded"
443
+ | "closing"
444
+ | "disposing"
445
+ | "closed"
446
+ | "disposed" = "loading";
447
+
448
+ private setLoaded() {
449
+ // It's conceivable the container could be closed when this is called
450
+ // Only transition states if currently loading
451
+ if (this._lifecycleState === "loading") {
452
+ // Propagate current connection state through the system.
453
+ this.propagateConnectionState(true /* initial transition */);
454
+ this._lifecycleState = "loaded";
455
+ }
456
+ }
457
+
458
+ public get closed(): boolean {
459
+ return (
460
+ this._lifecycleState === "closing" ||
461
+ this._lifecycleState === "closed" ||
462
+ this._lifecycleState === "disposing" ||
463
+ this._lifecycleState === "disposed"
464
+ );
465
+ }
466
+
467
+ private _attachState = AttachState.Detached;
468
+
469
+ private readonly storageAdapter: ContainerStorageAdapter;
470
+ public get storage(): IDocumentStorageService {
471
+ return this.storageAdapter;
472
+ }
473
+
474
+ private readonly clientDetailsOverride: IClientDetails | undefined;
475
+ private readonly _deltaManager: DeltaManager<ConnectionManager>;
476
+ private service: IDocumentService | undefined;
477
+
478
+ private _context: ContainerContext | undefined;
479
+ private get context() {
480
+ if (this._context === undefined) {
481
+ throw new GenericError("Attempted to access context before it was defined");
482
+ }
483
+ return this._context;
484
+ }
485
+ private _protocolHandler: IProtocolHandler | undefined;
486
+ private get protocolHandler() {
487
+ if (this._protocolHandler === undefined) {
488
+ throw new Error("Attempted to access protocolHandler before it was defined");
489
+ }
490
+ return this._protocolHandler;
491
+ }
492
+
493
+ /** During initialization we pause the inbound queues. We track this state to ensure we only call resume once */
494
+ private inboundQueuePausedFromInit = true;
495
+ private firstConnection = true;
496
+ private readonly connectionTransitionTimes: number[] = [];
497
+ private messageCountAfterDisconnection: number = 0;
498
+ private _loadedFromVersion: IVersion | undefined;
499
+ private _resolvedUrl: IFluidResolvedUrl | undefined;
500
+ private attachStarted = false;
501
+ private _dirtyContainer = false;
502
+ private readonly savedOps: ISequencedDocumentMessage[] = [];
503
+ private baseSnapshot?: ISnapshotTree;
504
+ private baseSnapshotBlobs?: ISerializableBlobContents;
505
+
506
+ private lastVisible: number | undefined;
507
+ private readonly visibilityEventHandler: (() => void) | undefined;
508
+ private readonly connectionStateHandler: IConnectionStateHandler;
509
+
510
+ private setAutoReconnectTime = performance.now();
511
+
512
+ private collabWindowTracker: CollabWindowTracker | undefined;
513
+
514
+ private get connectionMode() {
515
+ return this._deltaManager.connectionManager.connectionMode;
516
+ }
517
+
518
+ public get IFluidRouter(): IFluidRouter {
519
+ return this;
520
+ }
521
+
522
+ public get resolvedUrl(): IResolvedUrl | undefined {
523
+ return this._resolvedUrl;
524
+ }
525
+
526
+ public get loadedFromVersion(): IVersion | undefined {
527
+ return this._loadedFromVersion;
528
+ }
529
+
530
+ public get readOnlyInfo(): ReadOnlyInfo {
531
+ return this._deltaManager.readOnlyInfo;
532
+ }
533
+
534
+ public get closeSignal(): AbortSignal {
535
+ return this._deltaManager.closeAbortController.signal;
536
+ }
537
+
538
+ /**
539
+ * Tracks host requiring read-only mode.
540
+ */
541
+ public forceReadonly(readonly: boolean) {
542
+ this._deltaManager.connectionManager.forceReadonly(readonly);
543
+ }
544
+
545
+ public get deltaManager(): IDeltaManager<ISequencedDocumentMessage, IDocumentMessage> {
546
+ return this._deltaManager;
547
+ }
548
+
549
+ public get connectionState(): ConnectionState {
550
+ return this.connectionStateHandler.connectionState;
551
+ }
552
+
553
+ public get connected(): boolean {
554
+ return this.connectionStateHandler.connectionState === ConnectionState.Connected;
555
+ }
556
+
557
+ /**
558
+ * Service configuration details. If running in offline mode will be undefined otherwise will contain service
559
+ * configuration details returned as part of the initial connection.
560
+ */
561
+ public get serviceConfiguration(): IClientConfiguration | undefined {
562
+ return this._deltaManager.serviceConfiguration;
563
+ }
564
+
565
+ private _clientId: string | undefined;
566
+
567
+ /**
568
+ * The server provided id of the client.
569
+ * Set once this.connected is true, otherwise undefined
570
+ */
571
+ public get clientId(): string | undefined {
572
+ return this._clientId;
573
+ }
574
+
575
+ /**
576
+ * The server provided claims of the client.
577
+ * Set once this.connected is true, otherwise undefined
578
+ */
579
+ public get scopes(): string[] | undefined {
580
+ return this._deltaManager.connectionManager.scopes;
581
+ }
582
+
583
+ public get clientDetails(): IClientDetails {
584
+ return this._deltaManager.clientDetails;
585
+ }
586
+
587
+ private get offlineLoadEnabled(): boolean {
588
+ // summarizer will not have any pending state we want to save
589
+ return (
590
+ (this.mc.config.getBoolean("Fluid.Container.enableOfflineLoad") ?? false) &&
591
+ this.clientDetails.capabilities.interactive
592
+ );
593
+ }
594
+
595
+ /**
596
+ * Get the code details that are currently specified for the container.
597
+ * @returns The current code details if any are specified, undefined if none are specified.
598
+ */
599
+ public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
600
+ return this.getCodeDetailsFromQuorum();
601
+ }
602
+
603
+ /**
604
+ * Get the code details that were used to load the container.
605
+ * @returns The code details that were used to load the container if it is loaded, undefined if it is not yet
606
+ * loaded.
607
+ */
608
+ public getLoadedCodeDetails(): IFluidCodeDetails | undefined {
609
+ return this._context?.codeDetails;
610
+ }
611
+
612
+ /**
613
+ * Retrieves the audience associated with the document
614
+ */
615
+ public get audience(): IAudience {
616
+ return this.protocolHandler.audience;
617
+ }
618
+
619
+ /**
620
+ * Returns true if container is dirty.
621
+ * Which means data loss if container is closed at that same moment
622
+ * Most likely that happens when there is no network connection to Relay Service
623
+ */
624
+ public get isDirty() {
625
+ return this._dirtyContainer;
626
+ }
627
+
628
+ private get serviceFactory() {
629
+ return this.loader.services.documentServiceFactory;
630
+ }
631
+ private get urlResolver() {
632
+ return this.loader.services.urlResolver;
633
+ }
634
+ public readonly options: ILoaderOptions;
635
+ private get scope() {
636
+ return this.loader.services.scope;
637
+ }
638
+ private get codeLoader() {
639
+ return this.loader.services.codeLoader;
640
+ }
641
+
642
+ /**
643
+ * {@inheritDoc @fluidframework/container-definitions#IContainer.entryPoint}
644
+ */
645
+ public async getEntryPoint?(): Promise<FluidObject | undefined> {
646
+ // Only the disposing/disposed lifecycle states should prevent access to the entryPoint; closing/closed should still
647
+ // allow it since they mean a kind of read-only state for the Container.
648
+ // Note that all 4 are lifecycle states but only 'closed' and 'disposed' are emitted as events.
649
+ if (this._lifecycleState === "disposing" || this._lifecycleState === "disposed") {
650
+ throw new UsageError("The container is disposing or disposed");
651
+ }
652
+ while (this._context === undefined) {
653
+ await new Promise<void>((resolve, reject) => {
654
+ const contextChangedHandler = () => {
655
+ resolve();
656
+ this.off("disposed", disposedHandler);
657
+ };
658
+ const disposedHandler = (error) => {
659
+ reject(error ?? "The Container is disposed");
660
+ this.off("contextChanged", contextChangedHandler);
661
+ };
662
+ this.once("contextChanged", contextChangedHandler);
663
+ this.once("disposed", disposedHandler);
664
+ });
665
+ // The Promise above should only resolve (vs reject) if the 'contextChanged' event was emitted and that
666
+ // should have set this._context; making sure.
667
+ assert(
668
+ this._context !== undefined,
669
+ 0x5a2 /* Context still not defined after contextChanged event */,
670
+ );
671
+ }
672
+ // Disable lint rule for the sake of more complete stack traces
673
+ // eslint-disable-next-line no-return-await
674
+ return await this._context.getEntryPoint?.();
675
+ }
676
+
677
+ /**
678
+ * @internal
679
+ */
680
+ constructor(
681
+ private readonly loader: Loader,
682
+ config: IContainerConfig,
683
+ private readonly protocolHandlerBuilder?: ProtocolHandlerBuilder,
684
+ ) {
685
+ super((name, error) => {
686
+ this.mc.logger.sendErrorEvent(
687
+ {
688
+ eventName: "ContainerEventHandlerException",
689
+ name: typeof name === "string" ? name : undefined,
690
+ },
691
+ error,
692
+ );
693
+ });
694
+
695
+ this.clientDetailsOverride = config.clientDetailsOverride;
696
+ this._resolvedUrl = config.resolvedUrl;
697
+ if (config.canReconnect !== undefined) {
698
+ this._canReconnect = config.canReconnect;
699
+ }
700
+
701
+ // Create logger for data stores to use
702
+ const type = this.client.details.type;
703
+ const interactive = this.client.details.capabilities.interactive;
704
+ const clientType = `${interactive ? "interactive" : "noninteractive"}${
705
+ type !== undefined && type !== "" ? `/${type}` : ""
706
+ }`;
707
+ // Need to use the property getter for docId because for detached flow we don't have the docId initially.
708
+ // We assign the id later so property getter is used.
709
+ this.subLogger = ChildLogger.create(loader.services.subLogger, undefined, {
710
+ all: {
711
+ clientType, // Differentiating summarizer container from main container
712
+ containerId: uuid(),
713
+ docId: () => this._resolvedUrl?.id ?? undefined,
714
+ containerAttachState: () => this._attachState,
715
+ containerLifecycleState: () => this._lifecycleState,
716
+ containerConnectionState: () => ConnectionState[this.connectionState],
717
+ serializedContainer: config.serializedContainerState !== undefined,
718
+ },
719
+ // we need to be judicious with our logging here to avoid generating too much data
720
+ // all data logged here should be broadly applicable, and not specific to a
721
+ // specific error or class of errors
722
+ error: {
723
+ // load information to associate errors with the specific load point
724
+ dmInitialSeqNumber: () => this._deltaManager?.initialSequenceNumber,
725
+ dmLastProcessedSeqNumber: () => this._deltaManager?.lastSequenceNumber,
726
+ dmLastKnownSeqNumber: () => this._deltaManager?.lastKnownSeqNumber,
727
+ containerLoadedFromVersionId: () => this.loadedFromVersion?.id,
728
+ containerLoadedFromVersionDate: () => this.loadedFromVersion?.date,
729
+ // message information to associate errors with the specific execution state
730
+ // dmLastMsqSeqNumber: if present, same as dmLastProcessedSeqNumber
731
+ dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
732
+ dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
733
+ dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
734
+ dmLastMsgClientSeq: () => this.deltaManager?.lastMessage?.clientSequenceNumber,
735
+ connectionStateDuration: () =>
736
+ performance.now() - this.connectionTransitionTimes[this.connectionState],
737
+ },
738
+ });
739
+
740
+ // Prefix all events in this file with container-loader
741
+ this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
742
+
743
+ this.options = {
744
+ ...this.loader.services.options,
745
+ };
746
+
747
+ this._deltaManager = this.createDeltaManager();
748
+
749
+ this.connectionStateHandler = createConnectionStateHandler(
750
+ {
751
+ logger: this.mc.logger,
752
+ connectionStateChanged: (value, oldState, reason) => {
753
+ if (value === ConnectionState.Connected) {
754
+ this._clientId = this.connectionStateHandler.pendingClientId;
755
+ }
756
+ this.logConnectionStateChangeTelemetry(value, oldState, reason);
757
+ if (this._lifecycleState === "loaded") {
758
+ this.propagateConnectionState(
759
+ false /* initial transition */,
760
+ value === ConnectionState.Disconnected
761
+ ? reason
762
+ : undefined /* disconnectedReason */,
763
+ );
764
+ }
765
+ },
766
+ shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
767
+ maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
768
+ logConnectionIssue: (
769
+ eventName: string,
770
+ category: TelemetryEventCategory,
771
+ details?: ITelemetryProperties,
772
+ ) => {
773
+ const mode = this.connectionMode;
774
+ // We get here when socket does not receive any ops on "write" connection, including
775
+ // its own join op.
776
+ // Report issues only if we already loaded container - op processing is paused while container is loading,
777
+ // so we always time-out processing of join op in cases where fetching snapshot takes a minute.
778
+ // It's not a problem with op processing itself - such issues should be tracked as part of boot perf monitoring instead.
779
+ this._deltaManager.logConnectionIssue({
780
+ eventName,
781
+ mode,
782
+ category: this._lifecycleState === "loading" ? "generic" : category,
783
+ duration:
784
+ performance.now() -
785
+ this.connectionTransitionTimes[ConnectionState.CatchingUp],
786
+ ...(details === undefined ? {} : { details: JSON.stringify(details) }),
787
+ });
788
+
789
+ // If this is "write" connection, it took too long to receive join op. But in most cases that's due
790
+ // to very slow op fetches and we will eventually get there.
791
+ // For "read" connections, we get here due to self join signal not arriving on time. We will need to
792
+ // better understand when and why it may happen.
793
+ // For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
794
+ // current state of audience.
795
+ // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
796
+ // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
797
+ if (mode === "read") {
798
+ this.disconnect();
799
+ this.connect();
800
+ }
801
+ },
802
+ },
803
+ this.deltaManager,
804
+ config.serializedContainerState?.clientId,
805
+ );
806
+
807
+ this.on(savedContainerEvent, () => {
808
+ this.connectionStateHandler.containerSaved();
809
+ });
810
+
811
+ // We expose our storage publicly, so it's possible others may call uploadSummaryWithContext() with a
812
+ // non-combined summary tree (in particular, ContainerRuntime.submitSummary). We'll intercept those calls
813
+ // using this callback and fix them up.
814
+ const addProtocolSummaryIfMissing = (summaryTree: ISummaryTree) =>
815
+ isCombinedAppAndProtocolSummary(summaryTree) === true
816
+ ? summaryTree
817
+ : combineAppAndProtocolSummary(summaryTree, this.captureProtocolSummary());
818
+
819
+ // Whether the combined summary tree has been forced on by either the loader option or the monitoring context.
820
+ // Even if not forced on via this flag, combined summaries may still be enabled by service policy.
821
+ const forceEnableSummarizeProtocolTree =
822
+ this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2") ??
823
+ this.loader.services.options.summarizeProtocolTree;
824
+
825
+ this.storageAdapter = new ContainerStorageAdapter(
826
+ this.loader.services.detachedBlobStorage,
827
+ this.mc.logger,
828
+ config.serializedContainerState?.snapshotBlobs,
829
+ addProtocolSummaryIfMissing,
830
+ forceEnableSummarizeProtocolTree,
831
+ );
832
+
833
+ const isDomAvailable =
834
+ typeof document === "object" &&
835
+ document !== null &&
836
+ typeof document.addEventListener === "function" &&
837
+ document.addEventListener !== null;
838
+ // keep track of last time page was visible for telemetry
839
+ if (isDomAvailable) {
840
+ this.lastVisible = document.hidden ? performance.now() : undefined;
841
+ this.visibilityEventHandler = () => {
842
+ if (document.hidden) {
843
+ this.lastVisible = performance.now();
844
+ } else {
845
+ // settimeout so this will hopefully fire after disconnect event if being hidden caused it
846
+ setTimeout(() => {
847
+ this.lastVisible = undefined;
848
+ }, 0);
849
+ }
850
+ };
851
+ document.addEventListener("visibilitychange", this.visibilityEventHandler);
852
+ }
853
+ }
854
+
855
+ /**
856
+ * Retrieves the quorum associated with the document
857
+ */
858
+ public getQuorum(): IQuorumClients {
859
+ return this.protocolHandler.quorum;
860
+ }
861
+
862
+ public dispose?(error?: ICriticalContainerError) {
863
+ this._deltaManager.close(error, true /* doDispose */);
864
+ this.verifyClosed();
865
+ }
866
+
867
+ public close(error?: ICriticalContainerError) {
868
+ // 1. Ensure that close sequence is exactly the same no matter if it's initiated by host or by DeltaManager
869
+ // 2. We need to ensure that we deliver disconnect event to runtime properly. See connectionStateChanged
870
+ // handler. We only deliver events if container fully loaded. Transitioning from "loading" ->
871
+ // "closing" will lose that info (can also solve by tracking extra state).
872
+ this._deltaManager.close(error);
873
+ this.verifyClosed();
874
+ }
875
+
876
+ private verifyClosed(): void {
877
+ assert(
878
+ this.connectionState === ConnectionState.Disconnected,
879
+ 0x0cf /* "disconnect event was not raised!" */,
880
+ );
881
+
882
+ assert(
883
+ this._lifecycleState === "closed" || this._lifecycleState === "disposed",
884
+ 0x314 /* Container properly closed */,
885
+ );
886
+ }
887
+
888
+ private closeCore(error?: ICriticalContainerError) {
889
+ assert(!this.closed, 0x315 /* re-entrancy */);
890
+
891
+ try {
892
+ // Ensure that we raise all key events even if one of these throws
893
+ try {
894
+ // Raise event first, to ensure we capture _lifecycleState before transition.
895
+ // This gives us a chance to know what errors happened on open vs. on fully loaded container.
896
+ // Log generic events instead of error events if container is in loading state, as most errors are not really FF errors
897
+ // which can pollute telemetry for real bugs
898
+ this.mc.logger.sendTelemetryEvent(
899
+ {
900
+ eventName: "ContainerClose",
901
+ category:
902
+ this._lifecycleState !== "loading" && error !== undefined
903
+ ? "error"
904
+ : "generic",
905
+ },
906
+ error,
907
+ );
908
+
909
+ this._lifecycleState = "closing";
910
+
911
+ this._protocolHandler?.close();
912
+
913
+ this.connectionStateHandler.dispose();
914
+
915
+ this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
916
+
917
+ this.storageAdapter.dispose();
918
+
919
+ // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
920
+ // about file, like file being overwritten in storage, but client having stale local cache.
921
+ // Driver need to ensure all caches are cleared on critical errors
922
+ this.service?.dispose(error);
923
+ } catch (exception) {
924
+ this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException" }, exception);
925
+ }
926
+
927
+ this.emit("closed", error);
928
+
929
+ if (this.visibilityEventHandler !== undefined) {
930
+ document.removeEventListener("visibilitychange", this.visibilityEventHandler);
931
+ }
932
+ } finally {
933
+ this._lifecycleState = "closed";
934
+ }
935
+ }
936
+
937
+ private _disposed = false;
938
+ private disposeCore(error?: ICriticalContainerError) {
939
+ assert(!this._disposed, 0x54c /* Container already disposed */);
940
+ this._disposed = true;
941
+
942
+ try {
943
+ // Ensure that we raise all key events even if one of these throws
944
+ try {
945
+ // Raise event first, to ensure we capture _lifecycleState before transition.
946
+ // This gives us a chance to know what errors happened on open vs. on fully loaded container.
947
+ this.mc.logger.sendTelemetryEvent(
948
+ {
949
+ eventName: "ContainerDispose",
950
+ category: "generic",
951
+ },
952
+ error,
953
+ );
954
+
955
+ // ! Progressing from "closed" to "disposing" is not allowed
956
+ if (this._lifecycleState !== "closed") {
957
+ this._lifecycleState = "disposing";
958
+ }
959
+
960
+ this._protocolHandler?.close();
961
+
962
+ this.connectionStateHandler.dispose();
963
+
964
+ this._context?.dispose(error !== undefined ? new Error(error.message) : undefined);
965
+
966
+ this.storageAdapter.dispose();
967
+
968
+ // Notify storage about critical errors. They may be due to disconnect between client & server knowledge
969
+ // about file, like file being overwritten in storage, but client having stale local cache.
970
+ // Driver need to ensure all caches are cleared on critical errors
971
+ this.service?.dispose(error);
972
+ } catch (exception) {
973
+ this.mc.logger.sendErrorEvent(
974
+ { eventName: "ContainerDisposeException" },
975
+ exception,
976
+ );
977
+ }
978
+
979
+ this.emit("disposed", error);
980
+
981
+ this.removeAllListeners();
982
+ if (this.visibilityEventHandler !== undefined) {
983
+ document.removeEventListener("visibilitychange", this.visibilityEventHandler);
984
+ }
985
+ } finally {
986
+ this._lifecycleState = "disposed";
987
+ }
988
+ }
989
+
990
+ public closeAndGetPendingLocalState(): string {
991
+ // runtime matches pending ops to successful ones by clientId and client seq num, so we need to close the
992
+ // container at the same time we get pending state, otherwise this container could reconnect and resubmit with
993
+ // a new clientId and a future container using stale pending state without the new clientId would resubmit them
994
+ if (!this.offlineLoadEnabled) {
995
+ throw new UsageError("Can't get pending local state unless offline load is enabled");
996
+ }
997
+ assert(
998
+ this.attachState === AttachState.Attached,
999
+ 0x0d1 /* "Container should be attached before close" */,
1000
+ );
1001
+ assert(
1002
+ this.resolvedUrl !== undefined && this.resolvedUrl.type === "fluid",
1003
+ 0x0d2 /* "resolved url should be valid Fluid url" */,
1004
+ );
1005
+ assert(!!this._protocolHandler, 0x2e3 /* "Must have a valid protocol handler instance" */);
1006
+ assert(
1007
+ this._protocolHandler.attributes.term !== undefined,
1008
+ 0x37e /* Must have a valid protocol handler instance */,
1009
+ );
1010
+ assert(!!this.baseSnapshot, "no base snapshot");
1011
+ assert(!!this.baseSnapshotBlobs, "no snapshot blobs");
1012
+ const pendingState: IPendingContainerState = {
1013
+ pendingRuntimeState: this.context.getPendingLocalState(),
1014
+ baseSnapshot: this.baseSnapshot,
1015
+ snapshotBlobs: this.baseSnapshotBlobs,
1016
+ savedOps: this.savedOps,
1017
+ url: this.resolvedUrl.url,
1018
+ term: this._protocolHandler.attributes.term,
1019
+ clientId: this.clientId,
1020
+ };
1021
+
1022
+ this.mc.logger.sendTelemetryEvent({ eventName: "CloseAndGetPendingLocalState" });
1023
+
1024
+ // Only close here as method name suggests
1025
+ this.close();
1026
+
1027
+ return JSON.stringify(pendingState);
1028
+ }
1029
+
1030
+ public get attachState(): AttachState {
1031
+ return this._attachState;
1032
+ }
1033
+
1034
+ public serialize(): string {
1035
+ assert(
1036
+ this.attachState === AttachState.Detached,
1037
+ 0x0d3 /* "Should only be called in detached container" */,
1038
+ );
1039
+
1040
+ const appSummary: ISummaryTree = this.context.createSummary();
1041
+ const protocolSummary = this.captureProtocolSummary();
1042
+ const combinedSummary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1043
+
1044
+ if (
1045
+ this.loader.services.detachedBlobStorage &&
1046
+ this.loader.services.detachedBlobStorage.size > 0
1047
+ ) {
1048
+ combinedSummary.tree[".hasAttachmentBlobs"] = {
1049
+ type: SummaryType.Blob,
1050
+ content: "true",
1051
+ };
1052
+ }
1053
+ return JSON.stringify(combinedSummary);
1054
+ }
1055
+
1056
+ public async attach(request: IRequest): Promise<void> {
1057
+ await PerformanceEvent.timedExecAsync(
1058
+ this.mc.logger,
1059
+ { eventName: "Attach" },
1060
+ async () => {
1061
+ if (this._lifecycleState !== "loaded") {
1062
+ // pre-0.58 error message: containerNotValidForAttach
1063
+ throw new UsageError(
1064
+ `The Container is not in a valid state for attach [${this._lifecycleState}]`,
1065
+ );
1066
+ }
1067
+
1068
+ // If container is already attached or attach is in progress, throw an error.
1069
+ assert(
1070
+ this._attachState === AttachState.Detached && !this.attachStarted,
1071
+ 0x205 /* "attach() called more than once" */,
1072
+ );
1073
+ this.attachStarted = true;
1074
+
1075
+ // If attachment blobs were uploaded in detached state we will go through a different attach flow
1076
+ const hasAttachmentBlobs =
1077
+ this.loader.services.detachedBlobStorage !== undefined &&
1078
+ this.loader.services.detachedBlobStorage.size > 0;
1079
+
1080
+ try {
1081
+ assert(
1082
+ this.deltaManager.inbound.length === 0,
1083
+ 0x0d6 /* "Inbound queue should be empty when attaching" */,
1084
+ );
1085
+
1086
+ let summary: ISummaryTree;
1087
+ if (!hasAttachmentBlobs) {
1088
+ // Get the document state post attach - possibly can just call attach but we need to change the
1089
+ // semantics around what the attach means as far as async code goes.
1090
+ const appSummary: ISummaryTree = this.context.createSummary();
1091
+ const protocolSummary = this.captureProtocolSummary();
1092
+ summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1093
+
1094
+ // Set the state as attaching as we are starting the process of attaching container.
1095
+ // This should be fired after taking the summary because it is the place where we are
1096
+ // starting to attach the container to storage.
1097
+ // Also, this should only be fired in detached container.
1098
+ this._attachState = AttachState.Attaching;
1099
+ this.emit("attaching");
1100
+ if (this.offlineLoadEnabled) {
1101
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1102
+ this.baseSnapshot = snapshot;
1103
+ this.baseSnapshotBlobs =
1104
+ getBlobContentsFromTreeWithBlobContents(snapshot);
1105
+ }
1106
+ }
1107
+
1108
+ // Actually go and create the resolved document
1109
+ const createNewResolvedUrl = await this.urlResolver.resolve(request);
1110
+ ensureFluidResolvedUrl(createNewResolvedUrl);
1111
+ if (this.service === undefined) {
1112
+ assert(
1113
+ this.client.details.type !== summarizerClientType,
1114
+ 0x2c4 /* "client should not be summarizer before container is created" */,
1115
+ );
1116
+ this.service = await runWithRetry(
1117
+ async () =>
1118
+ this.serviceFactory.createContainer(
1119
+ summary,
1120
+ createNewResolvedUrl,
1121
+ this.subLogger,
1122
+ false, // clientIsSummarizer
1123
+ ),
1124
+ "containerAttach",
1125
+ this.mc.logger,
1126
+ {
1127
+ cancel: this.closeSignal,
1128
+ }, // progress
1129
+ );
1130
+ }
1131
+ const resolvedUrl = this.service.resolvedUrl;
1132
+ ensureFluidResolvedUrl(resolvedUrl);
1133
+ this._resolvedUrl = resolvedUrl;
1134
+ await this.storageAdapter.connectToService(this.service);
1135
+
1136
+ if (hasAttachmentBlobs) {
1137
+ // upload blobs to storage
1138
+ assert(
1139
+ !!this.loader.services.detachedBlobStorage,
1140
+ 0x24e /* "assertion for type narrowing" */,
1141
+ );
1142
+
1143
+ // build a table mapping IDs assigned locally to IDs assigned by storage and pass it to runtime to
1144
+ // support blob handles that only know about the local IDs
1145
+ const redirectTable = new Map<string, string>();
1146
+ // if new blobs are added while uploading, upload them too
1147
+ while (redirectTable.size < this.loader.services.detachedBlobStorage.size) {
1148
+ const newIds = this.loader.services.detachedBlobStorage
1149
+ .getBlobIds()
1150
+ .filter((id) => !redirectTable.has(id));
1151
+ for (const id of newIds) {
1152
+ const blob =
1153
+ await this.loader.services.detachedBlobStorage.readBlob(id);
1154
+ const response = await this.storageAdapter.createBlob(blob);
1155
+ redirectTable.set(id, response.id);
1156
+ }
1157
+ }
1158
+
1159
+ // take summary and upload
1160
+ const appSummary: ISummaryTree = this.context.createSummary(redirectTable);
1161
+ const protocolSummary = this.captureProtocolSummary();
1162
+ summary = combineAppAndProtocolSummary(appSummary, protocolSummary);
1163
+
1164
+ this._attachState = AttachState.Attaching;
1165
+ this.emit("attaching");
1166
+ if (this.offlineLoadEnabled) {
1167
+ const snapshot = getSnapshotTreeFromSerializedContainer(summary);
1168
+ this.baseSnapshot = snapshot;
1169
+ this.baseSnapshotBlobs =
1170
+ getBlobContentsFromTreeWithBlobContents(snapshot);
1171
+ }
1172
+
1173
+ await this.storageAdapter.uploadSummaryWithContext(summary, {
1174
+ referenceSequenceNumber: 0,
1175
+ ackHandle: undefined,
1176
+ proposalHandle: undefined,
1177
+ });
1178
+ }
1179
+
1180
+ this._attachState = AttachState.Attached;
1181
+ this.emit("attached");
1182
+
1183
+ if (!this.closed) {
1184
+ this.resumeInternal({
1185
+ fetchOpsFromStorage: false,
1186
+ reason: "createDetached",
1187
+ });
1188
+ }
1189
+ } catch (error) {
1190
+ // add resolved URL on error object so that host has the ability to find this document and delete it
1191
+ const newError = normalizeError(error);
1192
+ const resolvedUrl = this.resolvedUrl;
1193
+ if (isFluidResolvedUrl(resolvedUrl)) {
1194
+ newError.addTelemetryProperties({ resolvedUrl: resolvedUrl.url });
1195
+ }
1196
+ this.close(newError);
1197
+ this.dispose?.(newError);
1198
+ throw newError;
1199
+ }
1200
+ },
1201
+ { start: true, end: true, cancel: "generic" },
1202
+ );
1203
+ }
1204
+
1205
+ public async request(path: IRequest): Promise<IResponse> {
1206
+ return PerformanceEvent.timedExecAsync(
1207
+ this.mc.logger,
1208
+ { eventName: "Request" },
1209
+ async () => this.context.request(path),
1210
+ { end: true, cancel: "error" },
1211
+ );
1212
+ }
1213
+
1214
+ private setAutoReconnectInternal(mode: ReconnectMode) {
1215
+ const currentMode = this._deltaManager.connectionManager.reconnectMode;
1216
+
1217
+ if (currentMode === mode) {
1218
+ return;
1219
+ }
1220
+
1221
+ const now = performance.now();
1222
+ const duration = now - this.setAutoReconnectTime;
1223
+ this.setAutoReconnectTime = now;
1224
+
1225
+ this.mc.logger.sendTelemetryEvent({
1226
+ eventName:
1227
+ mode === ReconnectMode.Enabled ? "AutoReconnectEnabled" : "AutoReconnectDisabled",
1228
+ connectionMode: this.connectionMode,
1229
+ connectionState: ConnectionState[this.connectionState],
1230
+ duration,
1231
+ });
1232
+
1233
+ this._deltaManager.connectionManager.setAutoReconnect(mode);
1234
+ }
1235
+
1236
+ public connect() {
1237
+ if (this.closed) {
1238
+ throw new UsageError(`The Container is closed and cannot be connected`);
1239
+ } else if (this._attachState !== AttachState.Attached) {
1240
+ throw new UsageError(`The Container is not attached and cannot be connected`);
1241
+ } else if (!this.connected) {
1242
+ // Note: no need to fetch ops as we do it preemptively as part of DeltaManager.attachOpHandler().
1243
+ // If there is gap, we will learn about it once connected, but the gap should be small (if any),
1244
+ // assuming that connect() is called quickly after initial container boot.
1245
+ this.connectInternal({ reason: "DocumentConnect", fetchOpsFromStorage: false });
1246
+ }
1247
+ }
1248
+
1249
+ private connectInternal(args: IConnectionArgs) {
1250
+ assert(!this.closed, 0x2c5 /* "Attempting to connect() a closed Container" */);
1251
+ assert(
1252
+ this._attachState === AttachState.Attached,
1253
+ 0x2c6 /* "Attempting to connect() a container that is not attached" */,
1254
+ );
1255
+
1256
+ // Resume processing ops and connect to delta stream
1257
+ this.resumeInternal(args);
1258
+
1259
+ // Set Auto Reconnect Mode
1260
+ const mode = ReconnectMode.Enabled;
1261
+ this.setAutoReconnectInternal(mode);
1262
+ }
1263
+
1264
+ public disconnect() {
1265
+ if (this.closed) {
1266
+ throw new UsageError(`The Container is closed and cannot be disconnected`);
1267
+ } else {
1268
+ this.disconnectInternal();
1269
+ }
1270
+ }
1271
+
1272
+ private disconnectInternal() {
1273
+ assert(!this.closed, 0x2c7 /* "Attempting to disconnect() a closed Container" */);
1274
+
1275
+ // Set Auto Reconnect Mode
1276
+ const mode = ReconnectMode.Disabled;
1277
+ this.setAutoReconnectInternal(mode);
1278
+ }
1279
+
1280
+ private resumeInternal(args: IConnectionArgs) {
1281
+ assert(!this.closed, 0x0d9 /* "Attempting to connect() a closed DeltaManager" */);
1282
+
1283
+ // Resume processing ops
1284
+ if (this.inboundQueuePausedFromInit) {
1285
+ this.inboundQueuePausedFromInit = false;
1286
+ this._deltaManager.inbound.resume();
1287
+ this._deltaManager.inboundSignal.resume();
1288
+ }
1289
+
1290
+ // Ensure connection to web socket
1291
+ this.connectToDeltaStream(args);
1292
+ }
1293
+
1294
+ public async getAbsoluteUrl(relativeUrl: string): Promise<string | undefined> {
1295
+ if (this.resolvedUrl === undefined) {
1296
+ return undefined;
1297
+ }
1298
+
1299
+ return this.urlResolver.getAbsoluteUrl(
1300
+ this.resolvedUrl,
1301
+ relativeUrl,
1302
+ getPackageName(this._context?.codeDetails),
1303
+ );
1304
+ }
1305
+
1306
+ public async proposeCodeDetails(codeDetails: IFluidCodeDetails) {
1307
+ if (!isFluidCodeDetails(codeDetails)) {
1308
+ throw new Error("Provided codeDetails are not IFluidCodeDetails");
1309
+ }
1310
+
1311
+ if (this.codeLoader.IFluidCodeDetailsComparer) {
1312
+ const comparison = await this.codeLoader.IFluidCodeDetailsComparer.compare(
1313
+ codeDetails,
1314
+ this.getCodeDetailsFromQuorum(),
1315
+ );
1316
+ if (comparison !== undefined && comparison <= 0) {
1317
+ throw new Error("Proposed code details should be greater than the current");
1318
+ }
1319
+ }
1320
+
1321
+ return this.protocolHandler.quorum
1322
+ .propose("code", codeDetails)
1323
+ .then(() => true)
1324
+ .catch(() => false);
1325
+ }
1326
+
1327
+ private async processCodeProposal(): Promise<void> {
1328
+ const codeDetails = this.getCodeDetailsFromQuorum();
1329
+
1330
+ await Promise.all([
1331
+ this.deltaManager.inbound.pause(),
1332
+ this.deltaManager.inboundSignal.pause(),
1333
+ ]);
1334
+
1335
+ if ((await this.context.satisfies(codeDetails)) === true) {
1336
+ this.deltaManager.inbound.resume();
1337
+ this.deltaManager.inboundSignal.resume();
1338
+ return;
1339
+ }
1340
+
1341
+ // pre-0.58 error message: existingContextDoesNotSatisfyIncomingProposal
1342
+ const error = new GenericError("Existing context does not satisfy incoming proposal");
1343
+ this.close(error);
1344
+ this.dispose?.(error);
1345
+ }
1346
+
1347
+ private async getVersion(version: string | null): Promise<IVersion | undefined> {
1348
+ const versions = await this.storageAdapter.getVersions(version, 1);
1349
+ return versions[0];
1350
+ }
1351
+
1352
+ private recordConnectStartTime() {
1353
+ if (this.connectionTransitionTimes[ConnectionState.Disconnected] === undefined) {
1354
+ this.connectionTransitionTimes[ConnectionState.Disconnected] = performance.now();
1355
+ }
1356
+ }
1357
+
1358
+ private connectToDeltaStream(args: IConnectionArgs) {
1359
+ this.recordConnectStartTime();
1360
+
1361
+ // All agents need "write" access, including summarizer.
1362
+ if (!this._canReconnect || !this.client.details.capabilities.interactive) {
1363
+ args.mode = "write";
1364
+ }
1365
+
1366
+ this._deltaManager.connect(args);
1367
+ }
1368
+
1369
+ /**
1370
+ * Load container.
1371
+ *
1372
+ * @param specifiedVersion - Version SHA to load snapshot. If not specified, will fetch the latest snapshot.
1373
+ */
1374
+ private async load(
1375
+ specifiedVersion: string | undefined,
1376
+ loadMode: IContainerLoadMode,
1377
+ pendingLocalState?: IPendingContainerState,
1378
+ ) {
1379
+ if (this._resolvedUrl === undefined) {
1380
+ throw new Error("Attempting to load without a resolved url");
1381
+ }
1382
+ this.service = await this.serviceFactory.createDocumentService(
1383
+ this._resolvedUrl,
1384
+ this.subLogger,
1385
+ this.client.details.type === summarizerClientType,
1386
+ );
1387
+
1388
+ // Ideally we always connect as "read" by default.
1389
+ // Currently that works with SPO & r11s, because we get "write" connection when connecting to non-existing file.
1390
+ // We should not rely on it by (one of them will address the issue, but we need to address both)
1391
+ // 1) switching create new flow to one where we create file by posting snapshot
1392
+ // 2) Fixing quorum workflows (have retry logic)
1393
+ // That all said, "read" does not work with memorylicious workflows (that opens two simultaneous
1394
+ // connections to same file) in two ways:
1395
+ // A) creation flow breaks (as one of the clients "sees" file as existing, and hits #2 above)
1396
+ // B) Once file is created, transition from view-only connection to write does not work - some bugs to be fixed.
1397
+ const connectionArgs: IConnectionArgs = {
1398
+ reason: "DocumentOpen",
1399
+ mode: "write",
1400
+ fetchOpsFromStorage: false,
1401
+ };
1402
+
1403
+ // Start websocket connection as soon as possible. Note that there is no op handler attached yet, but the
1404
+ // DeltaManager is resilient to this and will wait to start processing ops until after it is attached.
1405
+ if (loadMode.deltaConnection === undefined && !pendingLocalState) {
1406
+ this.connectToDeltaStream(connectionArgs);
1407
+ }
1408
+
1409
+ if (!pendingLocalState) {
1410
+ await this.storageAdapter.connectToService(this.service);
1411
+ } else {
1412
+ // if we have pendingLocalState we can load without storage; don't wait for connection
1413
+ this.storageAdapter.connectToService(this.service).catch((error) => {
1414
+ this.close(error);
1415
+ this.dispose?.(error);
1416
+ });
1417
+ }
1418
+
1419
+ this._attachState = AttachState.Attached;
1420
+
1421
+ // Fetch specified snapshot.
1422
+ const { snapshot, versionId } =
1423
+ pendingLocalState === undefined
1424
+ ? await this.fetchSnapshotTree(specifiedVersion)
1425
+ : { snapshot: pendingLocalState.baseSnapshot, versionId: undefined };
1426
+
1427
+ if (pendingLocalState) {
1428
+ this.baseSnapshot = pendingLocalState.baseSnapshot;
1429
+ this.baseSnapshotBlobs = pendingLocalState.snapshotBlobs;
1430
+ } else {
1431
+ assert(snapshot !== undefined, 0x237 /* "Snapshot should exist" */);
1432
+ if (this.offlineLoadEnabled) {
1433
+ this.baseSnapshot = snapshot;
1434
+ // Save contents of snapshot now, otherwise closeAndGetPendingLocalState() must be async
1435
+ this.baseSnapshotBlobs = await getBlobContentsFromTree(snapshot, this.storage);
1436
+ }
1437
+ }
1438
+
1439
+ const attributes: IDocumentAttributes = await this.getDocumentAttributes(
1440
+ this.storageAdapter,
1441
+ snapshot,
1442
+ );
1443
+
1444
+ // If we saved ops, we will replay them and don't need DeltaManager to fetch them
1445
+ const sequenceNumber =
1446
+ pendingLocalState?.savedOps[pendingLocalState.savedOps.length - 1]?.sequenceNumber;
1447
+ const dmAttributes =
1448
+ sequenceNumber !== undefined ? { ...attributes, sequenceNumber } : attributes;
1449
+
1450
+ let opsBeforeReturnP: Promise<void> | undefined;
1451
+
1452
+ // Attach op handlers to finish initialization and be able to start processing ops
1453
+ // Kick off any ops fetching if required.
1454
+ switch (loadMode.opsBeforeReturn) {
1455
+ case undefined:
1456
+ // Start prefetch, but not set opsBeforeReturnP - boot is not blocked by it!
1457
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1458
+ this.attachDeltaManagerOpHandler(
1459
+ dmAttributes,
1460
+ loadMode.deltaConnection !== "none" ? "all" : "none",
1461
+ );
1462
+ break;
1463
+ case "cached":
1464
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "cached");
1465
+ break;
1466
+ case "all":
1467
+ opsBeforeReturnP = this.attachDeltaManagerOpHandler(dmAttributes, "all");
1468
+ break;
1469
+ default:
1470
+ unreachableCase(loadMode.opsBeforeReturn);
1471
+ }
1472
+
1473
+ // ...load in the existing quorum
1474
+ // Initialize the protocol handler
1475
+ await this.initializeProtocolStateFromSnapshot(attributes, this.storageAdapter, snapshot);
1476
+
1477
+ const codeDetails = this.getCodeDetailsFromQuorum();
1478
+ await this.instantiateContext(
1479
+ true, // existing
1480
+ codeDetails,
1481
+ snapshot,
1482
+ pendingLocalState?.pendingRuntimeState,
1483
+ );
1484
+
1485
+ // replay saved ops
1486
+ if (pendingLocalState) {
1487
+ for (const message of pendingLocalState.savedOps) {
1488
+ this.processRemoteMessage(message);
1489
+
1490
+ // allow runtime to apply stashed ops at this op's sequence number
1491
+ await this.context.notifyOpReplay(message);
1492
+ }
1493
+ pendingLocalState.savedOps = [];
1494
+
1495
+ // now set clientId to stashed clientId so live ops are correctly processed as local
1496
+ assert(
1497
+ this.clientId === undefined,
1498
+ "Unexpected clientId when setting stashed clientId",
1499
+ );
1500
+ this._clientId = pendingLocalState?.clientId;
1501
+ }
1502
+
1503
+ // We might have hit some failure that did not manifest itself in exception in this flow,
1504
+ // do not start op processing in such case - static version of Container.load() will handle it correctly.
1505
+ if (!this.closed) {
1506
+ if (opsBeforeReturnP !== undefined) {
1507
+ this._deltaManager.inbound.resume();
1508
+
1509
+ await PerformanceEvent.timedExecAsync(
1510
+ this.mc.logger,
1511
+ { eventName: "WaitOps" },
1512
+ async () => opsBeforeReturnP,
1513
+ );
1514
+ await PerformanceEvent.timedExecAsync(
1515
+ this.mc.logger,
1516
+ { eventName: "WaitOpProcessing" },
1517
+ async () => this._deltaManager.inbound.waitTillProcessingDone(),
1518
+ );
1519
+
1520
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1521
+ this._deltaManager.inbound.pause();
1522
+ }
1523
+
1524
+ switch (loadMode.deltaConnection) {
1525
+ case undefined:
1526
+ if (pendingLocalState) {
1527
+ // connect to delta stream now since we did not before
1528
+ this.connectToDeltaStream(connectionArgs);
1529
+ }
1530
+ // intentional fallthrough
1531
+ case "delayed":
1532
+ assert(
1533
+ this.inboundQueuePausedFromInit,
1534
+ 0x346 /* inboundQueuePausedFromInit should be true */,
1535
+ );
1536
+ this.inboundQueuePausedFromInit = false;
1537
+ this._deltaManager.inbound.resume();
1538
+ this._deltaManager.inboundSignal.resume();
1539
+ break;
1540
+ case "none":
1541
+ break;
1542
+ default:
1543
+ unreachableCase(loadMode.deltaConnection);
1544
+ }
1545
+ }
1546
+
1547
+ // Safety net: static version of Container.load() should have learned about it through "closed" handler.
1548
+ // But if that did not happen for some reason, fail load for sure.
1549
+ // Otherwise we can get into situations where container is closed and does not try to connect to ordering
1550
+ // service, but caller does not know that (callers do expect container to be not closed on successful path
1551
+ // and listen only on "closed" event)
1552
+ if (this.closed) {
1553
+ throw new Error("Container was closed while load()");
1554
+ }
1555
+
1556
+ // Internal context is fully loaded at this point
1557
+ this.setLoaded();
1558
+
1559
+ return {
1560
+ sequenceNumber: attributes.sequenceNumber,
1561
+ version: versionId,
1562
+ dmLastProcessedSeqNumber: this._deltaManager.lastSequenceNumber,
1563
+ dmLastKnownSeqNumber: this._deltaManager.lastKnownSeqNumber,
1564
+ };
1565
+ }
1566
+
1567
+ private async createDetached(source: IFluidCodeDetails) {
1568
+ const attributes: IDocumentAttributes = {
1569
+ sequenceNumber: detachedContainerRefSeqNumber,
1570
+ term: 1,
1571
+ minimumSequenceNumber: 0,
1572
+ };
1573
+
1574
+ await this.attachDeltaManagerOpHandler(attributes);
1575
+
1576
+ // Need to just seed the source data in the code quorum. Quorum itself is empty
1577
+ const qValues = initQuorumValuesFromCodeDetails(source);
1578
+ this.initializeProtocolState(
1579
+ attributes,
1580
+ {
1581
+ members: [],
1582
+ proposals: [],
1583
+ values: qValues,
1584
+ }, // IQuorumSnapShot
1585
+ );
1586
+
1587
+ // The load context - given we seeded the quorum - will be great
1588
+ await this.instantiateContextDetached(
1589
+ false, // existing
1590
+ );
1591
+
1592
+ this.setLoaded();
1593
+ }
1594
+
1595
+ private async rehydrateDetachedFromSnapshot(detachedContainerSnapshot: ISummaryTree) {
1596
+ if (detachedContainerSnapshot.tree[".hasAttachmentBlobs"] !== undefined) {
1597
+ assert(
1598
+ !!this.loader.services.detachedBlobStorage &&
1599
+ this.loader.services.detachedBlobStorage.size > 0,
1600
+ 0x250 /* "serialized container with attachment blobs must be rehydrated with detached blob storage" */,
1601
+ );
1602
+ delete detachedContainerSnapshot.tree[".hasAttachmentBlobs"];
1603
+ }
1604
+
1605
+ const snapshotTree = getSnapshotTreeFromSerializedContainer(detachedContainerSnapshot);
1606
+ this.storageAdapter.loadSnapshotForRehydratingContainer(snapshotTree);
1607
+ const attributes = await this.getDocumentAttributes(this.storageAdapter, snapshotTree);
1608
+
1609
+ await this.attachDeltaManagerOpHandler(attributes);
1610
+
1611
+ // Initialize the protocol handler
1612
+ const baseTree = getProtocolSnapshotTree(snapshotTree);
1613
+ const qValues = await readAndParse<[string, ICommittedProposal][]>(
1614
+ this.storageAdapter,
1615
+ baseTree.blobs.quorumValues,
1616
+ );
1617
+ const codeDetails = getCodeDetailsFromQuorumValues(qValues);
1618
+ this.initializeProtocolState(
1619
+ attributes,
1620
+ {
1621
+ members: [],
1622
+ proposals: [],
1623
+ values:
1624
+ codeDetails !== undefined ? initQuorumValuesFromCodeDetails(codeDetails) : [],
1625
+ }, // IQuorumSnapShot
1626
+ );
1627
+
1628
+ await this.instantiateContextDetached(
1629
+ true, // existing
1630
+ snapshotTree,
1631
+ );
1632
+
1633
+ this.setLoaded();
1634
+ }
1635
+
1636
+ private async getDocumentAttributes(
1637
+ storage: IDocumentStorageService,
1638
+ tree: ISnapshotTree | undefined,
1639
+ ): Promise<IDocumentAttributes> {
1640
+ if (tree === undefined) {
1641
+ return {
1642
+ minimumSequenceNumber: 0,
1643
+ sequenceNumber: 0,
1644
+ term: 1,
1645
+ };
1646
+ }
1647
+
1648
+ // Backward compatibility: old docs would have ".attributes" instead of "attributes"
1649
+ const attributesHash =
1650
+ ".protocol" in tree.trees
1651
+ ? tree.trees[".protocol"].blobs.attributes
1652
+ : tree.blobs[".attributes"];
1653
+
1654
+ const attributes = await readAndParse<IDocumentAttributes>(storage, attributesHash);
1655
+
1656
+ // Backward compatibility for older summaries with no term
1657
+ if (attributes.term === undefined) {
1658
+ attributes.term = 1;
1659
+ }
1660
+
1661
+ return attributes;
1662
+ }
1663
+
1664
+ private async initializeProtocolStateFromSnapshot(
1665
+ attributes: IDocumentAttributes,
1666
+ storage: IDocumentStorageService,
1667
+ snapshot: ISnapshotTree | undefined,
1668
+ ): Promise<void> {
1669
+ const quorumSnapshot: IQuorumSnapshot = {
1670
+ members: [],
1671
+ proposals: [],
1672
+ values: [],
1673
+ };
1674
+
1675
+ if (snapshot !== undefined) {
1676
+ const baseTree = getProtocolSnapshotTree(snapshot);
1677
+ [quorumSnapshot.members, quorumSnapshot.proposals, quorumSnapshot.values] =
1678
+ await Promise.all([
1679
+ readAndParse<[string, ISequencedClient][]>(
1680
+ storage,
1681
+ baseTree.blobs.quorumMembers,
1682
+ ),
1683
+ readAndParse<[number, ISequencedProposal, string[]][]>(
1684
+ storage,
1685
+ baseTree.blobs.quorumProposals,
1686
+ ),
1687
+ readAndParse<[string, ICommittedProposal][]>(
1688
+ storage,
1689
+ baseTree.blobs.quorumValues,
1690
+ ),
1691
+ ]);
1692
+ }
1693
+
1694
+ this.initializeProtocolState(attributes, quorumSnapshot);
1695
+ }
1696
+
1697
+ private initializeProtocolState(
1698
+ attributes: IDocumentAttributes,
1699
+ quorumSnapshot: IQuorumSnapshot,
1700
+ ): void {
1701
+ const protocolHandlerBuilder =
1702
+ this.protocolHandlerBuilder ??
1703
+ ((...args) => new ProtocolHandler(...args, new Audience()));
1704
+ const protocol = protocolHandlerBuilder(attributes, quorumSnapshot, (key, value) =>
1705
+ this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1706
+ );
1707
+
1708
+ const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1709
+
1710
+ protocol.quorum.on("error", (error) => {
1711
+ protocolLogger.sendErrorEvent(error);
1712
+ });
1713
+
1714
+ // Track membership changes and update connection state accordingly
1715
+ this.connectionStateHandler.initProtocol(protocol);
1716
+
1717
+ protocol.quorum.on("addProposal", (proposal: ISequencedProposal) => {
1718
+ if (proposal.key === "code" || proposal.key === "code2") {
1719
+ this.emit("codeDetailsProposed", proposal.value, proposal);
1720
+ }
1721
+ });
1722
+
1723
+ protocol.quorum.on("approveProposal", (sequenceNumber, key, value) => {
1724
+ if (key === "code" || key === "code2") {
1725
+ if (!isFluidCodeDetails(value)) {
1726
+ this.mc.logger.sendErrorEvent({
1727
+ eventName: "CodeProposalNotIFluidCodeDetails",
1728
+ });
1729
+ }
1730
+ this.processCodeProposal().catch((error) => {
1731
+ const normalizedError = normalizeError(error);
1732
+ this.close(normalizedError);
1733
+ this.dispose?.(normalizedError);
1734
+ throw error;
1735
+ });
1736
+ }
1737
+ });
1738
+ // we need to make sure this member get set in a synchronous context,
1739
+ // or other things can happen after the object that will be set is created, but not yet set
1740
+ // this was breaking this._initialClients handling
1741
+ //
1742
+ this._protocolHandler = protocol;
1743
+ }
1744
+
1745
+ private captureProtocolSummary(): ISummaryTree {
1746
+ const quorumSnapshot = this.protocolHandler.snapshot();
1747
+ const summary: ISummaryTree = {
1748
+ tree: {
1749
+ attributes: {
1750
+ content: JSON.stringify(this.protocolHandler.attributes),
1751
+ type: SummaryType.Blob,
1752
+ },
1753
+ quorumMembers: {
1754
+ content: JSON.stringify(quorumSnapshot.members),
1755
+ type: SummaryType.Blob,
1756
+ },
1757
+ quorumProposals: {
1758
+ content: JSON.stringify(quorumSnapshot.proposals),
1759
+ type: SummaryType.Blob,
1760
+ },
1761
+ quorumValues: {
1762
+ content: JSON.stringify(quorumSnapshot.values),
1763
+ type: SummaryType.Blob,
1764
+ },
1765
+ },
1766
+ type: SummaryType.Tree,
1767
+ };
1768
+
1769
+ return summary;
1770
+ }
1771
+
1772
+ private getCodeDetailsFromQuorum(): IFluidCodeDetails {
1773
+ const quorum = this.protocolHandler.quorum;
1774
+
1775
+ const pkg = getCodeProposal(quorum);
1776
+
1777
+ return pkg as IFluidCodeDetails;
1778
+ }
1779
+
1780
+ private get client(): IClient {
1781
+ const client: IClient =
1782
+ this.options?.client !== undefined
1783
+ ? (this.options.client as IClient)
1784
+ : {
1785
+ details: {
1786
+ capabilities: { interactive: true },
1787
+ },
1788
+ mode: "read", // default reconnection mode on lost connection / connection error
1789
+ permission: [],
1790
+ scopes: [],
1791
+ user: { id: "" },
1792
+ };
1793
+
1794
+ if (this.clientDetailsOverride !== undefined) {
1795
+ merge(client.details, this.clientDetailsOverride);
1796
+ }
1797
+ client.details.environment = [
1798
+ client.details.environment,
1799
+ ` loaderVersion:${pkgVersion}`,
1800
+ ].join(";");
1801
+ return client;
1802
+ }
1803
+
1804
+ /**
1805
+ * Returns true if connection is active, i.e. it's "write" connection and
1806
+ * container runtime was notified about this connection (i.e. we are up-to-date and could send ops).
1807
+ * This happens after client received its own joinOp and thus is in the quorum.
1808
+ * If it's not true, runtime is not in position to send ops.
1809
+ */
1810
+ private activeConnection() {
1811
+ return (
1812
+ this.connectionState === ConnectionState.Connected && this.connectionMode === "write"
1813
+ );
1814
+ }
1815
+
1816
+ private createDeltaManager() {
1817
+ const serviceProvider = () => this.service;
1818
+ const deltaManager = new DeltaManager<ConnectionManager>(
1819
+ serviceProvider,
1820
+ ChildLogger.create(this.subLogger, "DeltaManager"),
1821
+ () => this.activeConnection(),
1822
+ (props: IConnectionManagerFactoryArgs) =>
1823
+ new ConnectionManager(
1824
+ serviceProvider,
1825
+ this.client,
1826
+ this._canReconnect,
1827
+ ChildLogger.create(this.subLogger, "ConnectionManager"),
1828
+ props,
1829
+ ),
1830
+ );
1831
+
1832
+ // Disable inbound queues as Container is not ready to accept any ops until we are fully loaded!
1833
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1834
+ deltaManager.inbound.pause();
1835
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1836
+ deltaManager.inboundSignal.pause();
1837
+
1838
+ deltaManager.on("connect", (details: IConnectionDetailsInternal, _opsBehind?: number) => {
1839
+ assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1840
+ this.connectionStateHandler.receivedConnectEvent(details);
1841
+ });
1842
+
1843
+ deltaManager.on("disconnect", (reason: string) => {
1844
+ this.collabWindowTracker?.stopSequenceNumberUpdate();
1845
+ if (!this.closed) {
1846
+ this.connectionStateHandler.receivedDisconnectEvent(reason);
1847
+ }
1848
+ });
1849
+
1850
+ deltaManager.on("throttled", (warning: IThrottlingWarning) => {
1851
+ const warn = warning as ContainerWarning;
1852
+ // Some "warning" events come from outside the container and are logged
1853
+ // elsewhere (e.g. summarizing container). We shouldn't log these here.
1854
+ if (warn.logged !== true) {
1855
+ this.mc.logger.sendTelemetryEvent({ eventName: "ContainerWarning" }, warn);
1856
+ }
1857
+ this.emit("warning", warn);
1858
+ });
1859
+
1860
+ deltaManager.on("readonly", (readonly) => {
1861
+ this.setContextConnectedState(
1862
+ this.connectionState === ConnectionState.Connected,
1863
+ readonly,
1864
+ );
1865
+ this.emit("readonly", readonly);
1866
+ });
1867
+
1868
+ deltaManager.on("closed", (error?: ICriticalContainerError) => {
1869
+ this.closeCore(error);
1870
+ });
1871
+
1872
+ deltaManager.on("disposed", (error?: ICriticalContainerError) => {
1873
+ this.disposeCore(error);
1874
+ });
1875
+
1876
+ return deltaManager;
1877
+ }
1878
+
1879
+ private async attachDeltaManagerOpHandler(
1880
+ attributes: IDocumentAttributes,
1881
+ prefetchType?: "cached" | "all" | "none",
1882
+ ) {
1883
+ return this._deltaManager.attachOpHandler(
1884
+ attributes.minimumSequenceNumber,
1885
+ attributes.sequenceNumber,
1886
+ attributes.term ?? 1,
1887
+ {
1888
+ process: (message) => this.processRemoteMessage(message),
1889
+ processSignal: (message) => {
1890
+ this.processSignal(message);
1891
+ },
1892
+ },
1893
+ prefetchType,
1894
+ );
1895
+ }
1896
+
1897
+ private logConnectionStateChangeTelemetry(
1898
+ value: ConnectionState,
1899
+ oldState: ConnectionState,
1900
+ reason?: string,
1901
+ ) {
1902
+ // Log actual event
1903
+ const time = performance.now();
1904
+ this.connectionTransitionTimes[value] = time;
1905
+ const duration = time - this.connectionTransitionTimes[oldState];
1906
+
1907
+ let durationFromDisconnected: number | undefined;
1908
+ let connectionInitiationReason: string | undefined;
1909
+ let autoReconnect: ReconnectMode | undefined;
1910
+ let checkpointSequenceNumber: number | undefined;
1911
+ let opsBehind: number | undefined;
1912
+ if (value === ConnectionState.Disconnected) {
1913
+ autoReconnect = this._deltaManager.connectionManager.reconnectMode;
1914
+ } else {
1915
+ if (value === ConnectionState.Connected) {
1916
+ durationFromDisconnected =
1917
+ time - this.connectionTransitionTimes[ConnectionState.Disconnected];
1918
+ durationFromDisconnected = TelemetryLogger.formatTick(durationFromDisconnected);
1919
+ } else {
1920
+ // This info is of most interest on establishing connection only.
1921
+ checkpointSequenceNumber = this.deltaManager.lastKnownSeqNumber;
1922
+ if (this.deltaManager.hasCheckpointSequenceNumber) {
1923
+ opsBehind = checkpointSequenceNumber - this.deltaManager.lastSequenceNumber;
1924
+ }
1925
+ }
1926
+ connectionInitiationReason = this.firstConnection ? "InitialConnect" : "AutoReconnect";
1927
+ }
1928
+
1929
+ this.mc.logger.sendPerformanceEvent({
1930
+ eventName: `ConnectionStateChange_${ConnectionState[value]}`,
1931
+ from: ConnectionState[oldState],
1932
+ duration,
1933
+ durationFromDisconnected,
1934
+ reason,
1935
+ connectionInitiationReason,
1936
+ pendingClientId: this.connectionStateHandler.pendingClientId,
1937
+ clientId: this.clientId,
1938
+ autoReconnect,
1939
+ opsBehind,
1940
+ online: OnlineStatus[isOnline()],
1941
+ lastVisible:
1942
+ this.lastVisible !== undefined ? performance.now() - this.lastVisible : undefined,
1943
+ checkpointSequenceNumber,
1944
+ quorumSize: this._protocolHandler?.quorum.getMembers().size,
1945
+ ...this._deltaManager.connectionProps,
1946
+ });
1947
+
1948
+ if (value === ConnectionState.Connected) {
1949
+ this.firstConnection = false;
1950
+ }
1951
+ }
1952
+
1953
+ private propagateConnectionState(initialTransition: boolean, disconnectedReason?: string) {
1954
+ // When container loaded, we want to propagate initial connection state.
1955
+ // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1956
+ // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
1957
+ if (
1958
+ !initialTransition &&
1959
+ this.connectionState !== ConnectionState.Connected &&
1960
+ this.connectionState !== ConnectionState.Disconnected
1961
+ ) {
1962
+ return;
1963
+ }
1964
+ const state = this.connectionState === ConnectionState.Connected;
1965
+
1966
+ const logOpsOnReconnect: boolean =
1967
+ this.connectionState === ConnectionState.Connected &&
1968
+ !this.firstConnection &&
1969
+ this.connectionMode === "write";
1970
+ if (logOpsOnReconnect) {
1971
+ this.messageCountAfterDisconnection = 0;
1972
+ }
1973
+
1974
+ // Both protocol and context should not be undefined if we got so far.
1975
+
1976
+ this.setContextConnectedState(
1977
+ state,
1978
+ this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false,
1979
+ );
1980
+ this.protocolHandler.setConnectionState(state, this.clientId);
1981
+ raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1982
+
1983
+ if (logOpsOnReconnect) {
1984
+ this.mc.logger.sendTelemetryEvent({
1985
+ eventName: "OpsSentOnReconnect",
1986
+ count: this.messageCountAfterDisconnection,
1987
+ });
1988
+ }
1989
+ }
1990
+
1991
+ // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1992
+ private submitContainerMessage(
1993
+ type: MessageType,
1994
+ contents: any,
1995
+ batch?: boolean,
1996
+ metadata?: any,
1997
+ ): number {
1998
+ switch (type) {
1999
+ case MessageType.Operation:
2000
+ return this.submitMessage(type, JSON.stringify(contents), batch, metadata);
2001
+ case MessageType.Summarize:
2002
+ return this.submitSummaryMessage(contents as unknown as ISummaryContent);
2003
+ default: {
2004
+ const newError = new GenericError(
2005
+ "invalidContainerSubmitOpType",
2006
+ undefined /* error */,
2007
+ { messageType: type },
2008
+ );
2009
+ this.close(newError);
2010
+ this.dispose?.(newError);
2011
+ return -1;
2012
+ }
2013
+ }
2014
+ }
2015
+
2016
+ /** @returns clientSequenceNumber of last message in a batch */
2017
+ private submitBatch(batch: IBatchMessage[], referenceSequenceNumber?: number): number {
2018
+ let clientSequenceNumber = -1;
2019
+ for (const message of batch) {
2020
+ clientSequenceNumber = this.submitMessage(
2021
+ MessageType.Operation,
2022
+ message.contents,
2023
+ true, // batch
2024
+ message.metadata,
2025
+ message.compression,
2026
+ referenceSequenceNumber,
2027
+ );
2028
+ }
2029
+ this._deltaManager.flush();
2030
+ return clientSequenceNumber;
2031
+ }
2032
+
2033
+ private submitSummaryMessage(summary: ISummaryContent, referenceSequenceNumber?: number) {
2034
+ // github #6451: this is only needed for staging so the server
2035
+ // know when the protocol tree is included
2036
+ // this can be removed once all clients send
2037
+ // protocol tree by default
2038
+ if (summary.details === undefined) {
2039
+ summary.details = {};
2040
+ }
2041
+ summary.details.includesProtocolTree = this.storageAdapter.summarizeProtocolTree;
2042
+ return this.submitMessage(
2043
+ MessageType.Summarize,
2044
+ JSON.stringify(summary),
2045
+ false /* batch */,
2046
+ undefined /* metadata */,
2047
+ undefined /* compression */,
2048
+ referenceSequenceNumber,
2049
+ );
2050
+ }
2051
+
2052
+ private submitMessage(
2053
+ type: MessageType,
2054
+ contents?: string,
2055
+ batch?: boolean,
2056
+ metadata?: any,
2057
+ compression?: string,
2058
+ referenceSequenceNumber?: number,
2059
+ ): number {
2060
+ if (this.connectionState !== ConnectionState.Connected) {
2061
+ this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
2062
+ return -1;
2063
+ }
2064
+
2065
+ this.messageCountAfterDisconnection += 1;
2066
+ this.collabWindowTracker?.stopSequenceNumberUpdate();
2067
+ return this._deltaManager.submit(
2068
+ type,
2069
+ contents,
2070
+ batch,
2071
+ metadata,
2072
+ compression,
2073
+ referenceSequenceNumber,
2074
+ );
2075
+ }
2076
+
2077
+ private processRemoteMessage(message: ISequencedDocumentMessage) {
2078
+ if (this.offlineLoadEnabled) {
2079
+ this.savedOps.push(message);
2080
+ }
2081
+ const local = this.clientId === message.clientId;
2082
+
2083
+ // Allow the protocol handler to process the message
2084
+ const result = this.protocolHandler.processMessage(message, local);
2085
+
2086
+ // Forward messages to the loaded runtime for processing
2087
+ this.context.process(message, local);
2088
+
2089
+ // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
2090
+ if (this.activeConnection()) {
2091
+ if (this.collabWindowTracker === undefined) {
2092
+ // Note that config from first connection will be used for this container's lifetime.
2093
+ // That means that if relay service changes settings, such changes will impact only newly booted
2094
+ // clients.
2095
+ // All existing will continue to use settings they got earlier.
2096
+ assert(
2097
+ this.serviceConfiguration !== undefined,
2098
+ 0x2e4 /* "there should be service config for active connection" */,
2099
+ );
2100
+ this.collabWindowTracker = new CollabWindowTracker(
2101
+ (type) => {
2102
+ assert(
2103
+ this.activeConnection(),
2104
+ 0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */,
2105
+ );
2106
+ this.submitMessage(type);
2107
+ },
2108
+ this.serviceConfiguration.noopTimeFrequency,
2109
+ this.serviceConfiguration.noopCountFrequency,
2110
+ );
2111
+ }
2112
+ this.collabWindowTracker.scheduleSequenceNumberUpdate(
2113
+ message,
2114
+ result.immediateNoOp === true,
2115
+ );
2116
+ }
2117
+
2118
+ this.emit("op", message);
2119
+ }
2120
+
2121
+ private submitSignal(message: any) {
2122
+ this._deltaManager.submitSignal(JSON.stringify(message));
2123
+ }
2124
+
2125
+ private processSignal(message: ISignalMessage) {
2126
+ // No clientId indicates a system signal message.
2127
+ if (message.clientId === null) {
2128
+ this.protocolHandler.processSignal(message);
2129
+ } else {
2130
+ const local = this.clientId === message.clientId;
2131
+ this.context.processSignal(message, local);
2132
+ }
2133
+ }
2134
+
2135
+ /**
2136
+ * Get the most recent snapshot, or a specific version.
2137
+ * @param specifiedVersion - The specific version of the snapshot to retrieve
2138
+ * @returns The snapshot requested, or the latest snapshot if no version was specified, plus version ID
2139
+ */
2140
+ private async fetchSnapshotTree(
2141
+ specifiedVersion: string | undefined,
2142
+ ): Promise<{ snapshot?: ISnapshotTree; versionId?: string }> {
2143
+ const version = await this.getVersion(specifiedVersion ?? null);
2144
+
2145
+ if (version === undefined && specifiedVersion !== undefined) {
2146
+ // We should have a defined version to load from if specified version requested
2147
+ this.mc.logger.sendErrorEvent({
2148
+ eventName: "NoVersionFoundWhenSpecified",
2149
+ id: specifiedVersion,
2150
+ });
2151
+ }
2152
+ this._loadedFromVersion = version;
2153
+ const snapshot = (await this.storageAdapter.getSnapshotTree(version)) ?? undefined;
2154
+
2155
+ if (snapshot === undefined && version !== undefined) {
2156
+ this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
2157
+ }
2158
+ return { snapshot, versionId: version?.id };
2159
+ }
2160
+
2161
+ private async instantiateContextDetached(existing: boolean, snapshot?: ISnapshotTree) {
2162
+ const codeDetails = this.getCodeDetailsFromQuorum();
2163
+ if (codeDetails === undefined) {
2164
+ throw new Error("pkg should be provided in create flow!!");
2165
+ }
2166
+
2167
+ await this.instantiateContext(existing, codeDetails, snapshot);
2168
+ }
2169
+
2170
+ private async instantiateContext(
2171
+ existing: boolean,
2172
+ codeDetails: IFluidCodeDetails,
2173
+ snapshot?: ISnapshotTree,
2174
+ pendingLocalState?: unknown,
2175
+ ) {
2176
+ assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
2177
+
2178
+ // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
2179
+ // are set. Global requests will still go directly to the loader
2180
+ const loader = new RelativeLoader(this, this.loader);
2181
+ this._context = await ContainerContext.createOrLoad(
2182
+ this,
2183
+ this.scope,
2184
+ this.codeLoader,
2185
+ codeDetails,
2186
+ snapshot,
2187
+ new DeltaManagerProxy(this._deltaManager),
2188
+ new QuorumProxy(this.protocolHandler.quorum),
2189
+ loader,
2190
+ (type, contents, batch, metadata) =>
2191
+ this.submitContainerMessage(type, contents, batch, metadata),
2192
+ (summaryOp: ISummaryContent, referenceSequenceNumber?: number) =>
2193
+ this.submitSummaryMessage(summaryOp, referenceSequenceNumber),
2194
+ (batch: IBatchMessage[], referenceSequenceNumber?: number) =>
2195
+ this.submitBatch(batch, referenceSequenceNumber),
2196
+ (message) => this.submitSignal(message),
2197
+ (error?: ICriticalContainerError) => this.dispose?.(error),
2198
+ (error?: ICriticalContainerError) => this.close(error),
2199
+ Container.version,
2200
+ (dirty: boolean) => this.updateDirtyContainerState(dirty),
2201
+ existing,
2202
+ pendingLocalState,
2203
+ );
2204
+
2205
+ this.emit("contextChanged", codeDetails);
2206
+ }
2207
+
2208
+ private updateDirtyContainerState(dirty: boolean) {
2209
+ if (this._dirtyContainer === dirty) {
2210
+ return;
2211
+ }
2212
+ this._dirtyContainer = dirty;
2213
+ this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
2214
+ }
2215
+
2216
+ /**
2217
+ * Set the connected state of the ContainerContext
2218
+ * This controls the "connected" state of the ContainerRuntime as well
2219
+ * @param state - Is the container currently connected?
2220
+ * @param readonly - Is the container in readonly mode?
2221
+ */
2222
+ private setContextConnectedState(state: boolean, readonly: boolean): void {
2223
+ if (this._context?.disposed === false) {
2224
+ /**
2225
+ * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
2226
+ * ops getting through to the DeltaManager.
2227
+ * The ContainerRuntime's "connected" state simply means it is ok to send ops
2228
+ * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
2229
+ */
2230
+ this.context.setConnectionState(state && !readonly, this.clientId);
2231
+ }
2232
+ }
1870
2233
  }