@fluidframework/container-loader 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419

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