@fluidframework/container-loader 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.225277

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 (335) 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-esm.json +4 -0
  6. package/api-extractor-lint.json +4 -0
  7. package/api-extractor.json +2 -2
  8. package/api-report/container-loader.api.md +143 -0
  9. package/dist/{audience.js → audience.cjs} +15 -13
  10. package/dist/audience.cjs.map +1 -0
  11. package/dist/audience.d.ts +4 -6
  12. package/dist/audience.d.ts.map +1 -1
  13. package/dist/catchUpMonitor.cjs +43 -0
  14. package/dist/catchUpMonitor.cjs.map +1 -0
  15. package/dist/catchUpMonitor.d.ts +29 -0
  16. package/dist/catchUpMonitor.d.ts.map +1 -0
  17. package/dist/{connectionManager.js → connectionManager.cjs} +397 -240
  18. package/dist/connectionManager.cjs.map +1 -0
  19. package/dist/connectionManager.d.ts +23 -33
  20. package/dist/connectionManager.d.ts.map +1 -1
  21. package/dist/{connectionState.js → connectionState.cjs} +5 -7
  22. package/dist/connectionState.cjs.map +1 -0
  23. package/dist/connectionState.d.ts +3 -5
  24. package/dist/connectionState.d.ts.map +1 -1
  25. package/dist/connectionStateHandler.cjs +474 -0
  26. package/dist/connectionStateHandler.cjs.map +1 -0
  27. package/dist/connectionStateHandler.d.ts +127 -29
  28. package/dist/connectionStateHandler.d.ts.map +1 -1
  29. package/dist/container-loader-alpha.d.ts +274 -0
  30. package/dist/container-loader-beta.d.ts +75 -0
  31. package/dist/container-loader-public.d.ts +75 -0
  32. package/dist/container-loader-untrimmed.d.ts +331 -0
  33. package/dist/container.cjs +1585 -0
  34. package/dist/container.cjs.map +1 -0
  35. package/dist/container.d.ts +227 -83
  36. package/dist/container.d.ts.map +1 -1
  37. package/dist/containerContext.cjs +74 -0
  38. package/dist/containerContext.cjs.map +1 -0
  39. package/dist/containerContext.d.ts +33 -59
  40. package/dist/containerContext.d.ts.map +1 -1
  41. package/dist/containerStorageAdapter.cjs +234 -0
  42. package/dist/containerStorageAdapter.cjs.map +1 -0
  43. package/dist/containerStorageAdapter.d.ts +48 -23
  44. package/dist/containerStorageAdapter.d.ts.map +1 -1
  45. package/dist/{contracts.js → contracts.cjs} +5 -5
  46. package/dist/contracts.cjs.map +1 -0
  47. package/dist/contracts.d.ts +45 -17
  48. package/dist/contracts.d.ts.map +1 -1
  49. package/dist/debugLogger.cjs +101 -0
  50. package/dist/debugLogger.cjs.map +1 -0
  51. package/dist/debugLogger.d.ts +30 -0
  52. package/dist/debugLogger.d.ts.map +1 -0
  53. package/dist/{deltaManager.js → deltaManager.cjs} +379 -186
  54. package/dist/deltaManager.cjs.map +1 -0
  55. package/dist/deltaManager.d.ts +54 -18
  56. package/dist/deltaManager.d.ts.map +1 -1
  57. package/dist/{deltaQueue.js → deltaQueue.cjs} +29 -28
  58. package/dist/deltaQueue.cjs.map +1 -0
  59. package/dist/deltaQueue.d.ts +3 -4
  60. package/dist/deltaQueue.d.ts.map +1 -1
  61. package/dist/disposal.cjs +25 -0
  62. package/dist/disposal.cjs.map +1 -0
  63. package/dist/disposal.d.ts +13 -0
  64. package/dist/disposal.d.ts.map +1 -0
  65. package/dist/error.cjs +32 -0
  66. package/dist/error.cjs.map +1 -0
  67. package/dist/error.d.ts +23 -0
  68. package/dist/error.d.ts.map +1 -0
  69. package/dist/index.cjs +19 -0
  70. package/dist/index.cjs.map +1 -0
  71. package/dist/index.d.ts +5 -2
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/loader.cjs +148 -0
  74. package/dist/loader.cjs.map +1 -0
  75. package/dist/loader.d.ts +38 -19
  76. package/dist/loader.d.ts.map +1 -1
  77. package/dist/location-redirection-utilities/index.cjs +11 -0
  78. package/dist/location-redirection-utilities/index.cjs.map +1 -0
  79. package/dist/location-redirection-utilities/index.d.ts +6 -0
  80. package/dist/location-redirection-utilities/index.d.ts.map +1 -0
  81. package/dist/location-redirection-utilities/resolveWithLocationRedirection.cjs +53 -0
  82. package/dist/location-redirection-utilities/resolveWithLocationRedirection.cjs.map +1 -0
  83. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts +24 -0
  84. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -0
  85. package/dist/{collabWindowTracker.js → noopHeuristic.cjs} +37 -39
  86. package/dist/noopHeuristic.cjs.map +1 -0
  87. package/dist/noopHeuristic.d.ts +23 -0
  88. package/dist/noopHeuristic.d.ts.map +1 -0
  89. package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
  90. package/dist/packageVersion.cjs.map +1 -0
  91. package/dist/packageVersion.d.ts +1 -1
  92. package/dist/packageVersion.d.ts.map +1 -1
  93. package/dist/protocol.cjs +99 -0
  94. package/dist/protocol.cjs.map +1 -0
  95. package/dist/protocol.d.ts +38 -0
  96. package/dist/protocol.d.ts.map +1 -0
  97. package/dist/{protocolTreeDocumentStorageService.js → protocolTreeDocumentStorageService.cjs} +8 -5
  98. package/dist/protocolTreeDocumentStorageService.cjs.map +1 -0
  99. package/dist/protocolTreeDocumentStorageService.d.ts +8 -4
  100. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  101. package/dist/quorum.cjs +16 -0
  102. package/dist/quorum.cjs.map +1 -0
  103. package/dist/quorum.d.ts +1 -14
  104. package/dist/quorum.d.ts.map +1 -1
  105. package/dist/{retriableDocumentStorageService.js → retriableDocumentStorageService.cjs} +36 -21
  106. package/dist/retriableDocumentStorageService.cjs.map +1 -0
  107. package/dist/retriableDocumentStorageService.d.ts +7 -5
  108. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  109. package/dist/tsdoc-metadata.json +11 -0
  110. package/dist/{utils.js → utils.cjs} +52 -14
  111. package/dist/utils.cjs.map +1 -0
  112. package/dist/utils.d.ts +34 -1
  113. package/dist/utils.d.ts.map +1 -1
  114. package/lib/{audience.d.ts → audience.d.mts} +5 -11
  115. package/lib/audience.d.mts.map +1 -0
  116. package/lib/{audience.js → audience.mjs} +15 -17
  117. package/lib/audience.mjs.map +1 -0
  118. package/lib/catchUpMonitor.d.mts +29 -0
  119. package/lib/catchUpMonitor.d.mts.map +1 -0
  120. package/lib/catchUpMonitor.mjs +39 -0
  121. package/lib/catchUpMonitor.mjs.map +1 -0
  122. package/lib/{connectionManager.d.ts → connectionManager.d.mts} +24 -34
  123. package/lib/connectionManager.d.mts.map +1 -0
  124. package/lib/{connectionManager.js → connectionManager.mjs} +378 -218
  125. package/lib/connectionManager.mjs.map +1 -0
  126. package/lib/{connectionState.d.ts → connectionState.d.mts} +4 -6
  127. package/lib/connectionState.d.mts.map +1 -0
  128. package/lib/{connectionState.js → connectionState.mjs} +4 -6
  129. package/lib/connectionState.mjs.map +1 -0
  130. package/lib/connectionStateHandler.d.mts +179 -0
  131. package/lib/connectionStateHandler.d.mts.map +1 -0
  132. package/lib/connectionStateHandler.mjs +469 -0
  133. package/lib/connectionStateHandler.mjs.map +1 -0
  134. package/lib/container-loader-alpha.d.mts +274 -0
  135. package/lib/container-loader-beta.d.mts +75 -0
  136. package/lib/container-loader-public.d.mts +75 -0
  137. package/lib/container-loader-untrimmed.d.mts +331 -0
  138. package/lib/container.d.mts +382 -0
  139. package/lib/container.d.mts.map +1 -0
  140. package/lib/container.mjs +1579 -0
  141. package/lib/container.mjs.map +1 -0
  142. package/lib/containerContext.d.mts +58 -0
  143. package/lib/containerContext.d.mts.map +1 -0
  144. package/lib/containerContext.mjs +70 -0
  145. package/lib/containerContext.mjs.map +1 -0
  146. package/lib/containerStorageAdapter.d.mts +73 -0
  147. package/lib/containerStorageAdapter.d.mts.map +1 -0
  148. package/lib/containerStorageAdapter.mjs +228 -0
  149. package/lib/containerStorageAdapter.mjs.map +1 -0
  150. package/lib/{contracts.d.ts → contracts.d.mts} +46 -18
  151. package/lib/contracts.d.mts.map +1 -0
  152. package/lib/{contracts.js → contracts.mjs} +4 -4
  153. package/lib/contracts.mjs.map +1 -0
  154. package/lib/debugLogger.d.mts +30 -0
  155. package/lib/debugLogger.d.mts.map +1 -0
  156. package/lib/debugLogger.mjs +93 -0
  157. package/lib/debugLogger.mjs.map +1 -0
  158. package/lib/{deltaManager.d.ts → deltaManager.d.mts} +55 -19
  159. package/lib/deltaManager.d.mts.map +1 -0
  160. package/lib/{deltaManager.js → deltaManager.mjs} +361 -165
  161. package/lib/deltaManager.mjs.map +1 -0
  162. package/lib/{deltaQueue.d.ts → deltaQueue.d.mts} +4 -5
  163. package/lib/deltaQueue.d.mts.map +1 -0
  164. package/lib/{deltaQueue.js → deltaQueue.mjs} +25 -24
  165. package/lib/deltaQueue.mjs.map +1 -0
  166. package/lib/disposal.d.mts +13 -0
  167. package/lib/disposal.d.mts.map +1 -0
  168. package/lib/disposal.mjs +21 -0
  169. package/lib/disposal.mjs.map +1 -0
  170. package/lib/error.d.mts +23 -0
  171. package/lib/error.d.mts.map +1 -0
  172. package/lib/error.mjs +28 -0
  173. package/lib/error.mjs.map +1 -0
  174. package/lib/index.d.mts +11 -0
  175. package/lib/index.d.mts.map +1 -0
  176. package/lib/index.mjs +10 -0
  177. package/lib/index.mjs.map +1 -0
  178. package/lib/{loader.d.ts → loader.d.mts} +40 -21
  179. package/lib/loader.d.mts.map +1 -0
  180. package/lib/loader.mjs +143 -0
  181. package/lib/loader.mjs.map +1 -0
  182. package/lib/location-redirection-utilities/index.d.mts +6 -0
  183. package/lib/location-redirection-utilities/index.d.mts.map +1 -0
  184. package/lib/location-redirection-utilities/index.mjs +6 -0
  185. package/lib/location-redirection-utilities/index.mjs.map +1 -0
  186. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.mts +24 -0
  187. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.mts.map +1 -0
  188. package/lib/location-redirection-utilities/resolveWithLocationRedirection.mjs +48 -0
  189. package/lib/location-redirection-utilities/resolveWithLocationRedirection.mjs.map +1 -0
  190. package/lib/noopHeuristic.d.mts +23 -0
  191. package/lib/noopHeuristic.d.mts.map +1 -0
  192. package/lib/{collabWindowTracker.js → noopHeuristic.mjs} +33 -35
  193. package/lib/noopHeuristic.mjs.map +1 -0
  194. package/lib/{packageVersion.d.ts → packageVersion.d.mts} +2 -2
  195. package/lib/packageVersion.d.mts.map +1 -0
  196. package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
  197. package/lib/packageVersion.mjs.map +1 -0
  198. package/lib/protocol.d.mts +38 -0
  199. package/lib/protocol.d.mts.map +1 -0
  200. package/lib/protocol.mjs +94 -0
  201. package/lib/protocol.mjs.map +1 -0
  202. package/lib/{protocolTreeDocumentStorageService.d.ts → protocolTreeDocumentStorageService.d.mts} +9 -5
  203. package/lib/protocolTreeDocumentStorageService.d.mts.map +1 -0
  204. package/lib/{protocolTreeDocumentStorageService.js → protocolTreeDocumentStorageService.mjs} +8 -5
  205. package/lib/protocolTreeDocumentStorageService.mjs.map +1 -0
  206. package/lib/quorum.d.mts +4 -0
  207. package/lib/quorum.d.mts.map +1 -0
  208. package/lib/quorum.mjs +12 -0
  209. package/lib/quorum.mjs.map +1 -0
  210. package/lib/{retriableDocumentStorageService.d.ts → retriableDocumentStorageService.d.mts} +8 -6
  211. package/lib/retriableDocumentStorageService.d.mts.map +1 -0
  212. package/lib/{retriableDocumentStorageService.js → retriableDocumentStorageService.mjs} +35 -20
  213. package/lib/retriableDocumentStorageService.mjs.map +1 -0
  214. package/lib/utils.d.mts +67 -0
  215. package/lib/utils.d.mts.map +1 -0
  216. package/lib/{utils.js → utils.mjs} +47 -11
  217. package/lib/utils.mjs.map +1 -0
  218. package/package.json +189 -69
  219. package/prettier.config.cjs +8 -0
  220. package/src/audience.ts +59 -49
  221. package/src/catchUpMonitor.ts +61 -0
  222. package/src/connectionManager.ts +1154 -910
  223. package/src/connectionState.ts +22 -25
  224. package/src/connectionStateHandler.ts +689 -319
  225. package/src/container.ts +2476 -1792
  226. package/src/containerContext.ts +98 -330
  227. package/src/containerStorageAdapter.ts +301 -105
  228. package/src/contracts.ts +184 -146
  229. package/src/debugLogger.ts +123 -0
  230. package/src/deltaManager.ts +1165 -900
  231. package/src/deltaQueue.ts +156 -152
  232. package/src/disposal.ts +25 -0
  233. package/src/error.ts +44 -0
  234. package/src/index.ts +14 -15
  235. package/src/loader.ts +356 -427
  236. package/src/location-redirection-utilities/index.ts +9 -0
  237. package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +61 -0
  238. package/src/noopHeuristic.ts +107 -0
  239. package/src/packageVersion.ts +1 -1
  240. package/src/protocol.ts +150 -0
  241. package/src/protocolTreeDocumentStorageService.ts +35 -35
  242. package/src/quorum.ts +11 -50
  243. package/src/retriableDocumentStorageService.ts +135 -95
  244. package/src/utils.ts +159 -86
  245. package/tsc-multi.test.json +4 -0
  246. package/tsconfig.json +10 -12
  247. package/dist/audience.js.map +0 -1
  248. package/dist/collabWindowTracker.d.ts +0 -19
  249. package/dist/collabWindowTracker.d.ts.map +0 -1
  250. package/dist/collabWindowTracker.js.map +0 -1
  251. package/dist/connectionManager.js.map +0 -1
  252. package/dist/connectionState.js.map +0 -1
  253. package/dist/connectionStateHandler.js +0 -280
  254. package/dist/connectionStateHandler.js.map +0 -1
  255. package/dist/container.js +0 -1284
  256. package/dist/container.js.map +0 -1
  257. package/dist/containerContext.js +0 -217
  258. package/dist/containerContext.js.map +0 -1
  259. package/dist/containerStorageAdapter.js +0 -104
  260. package/dist/containerStorageAdapter.js.map +0 -1
  261. package/dist/contracts.js.map +0 -1
  262. package/dist/deltaManager.js.map +0 -1
  263. package/dist/deltaManagerProxy.d.ts +0 -54
  264. package/dist/deltaManagerProxy.d.ts.map +0 -1
  265. package/dist/deltaManagerProxy.js +0 -115
  266. package/dist/deltaManagerProxy.js.map +0 -1
  267. package/dist/deltaQueue.js.map +0 -1
  268. package/dist/index.js +0 -16
  269. package/dist/index.js.map +0 -1
  270. package/dist/loader.js +0 -241
  271. package/dist/loader.js.map +0 -1
  272. package/dist/packageVersion.js.map +0 -1
  273. package/dist/protocolTreeDocumentStorageService.js.map +0 -1
  274. package/dist/quorum.js +0 -44
  275. package/dist/quorum.js.map +0 -1
  276. package/dist/retriableDocumentStorageService.js.map +0 -1
  277. package/dist/utils.js.map +0 -1
  278. package/lib/audience.d.ts.map +0 -1
  279. package/lib/audience.js.map +0 -1
  280. package/lib/collabWindowTracker.d.ts +0 -19
  281. package/lib/collabWindowTracker.d.ts.map +0 -1
  282. package/lib/collabWindowTracker.js.map +0 -1
  283. package/lib/connectionManager.d.ts.map +0 -1
  284. package/lib/connectionManager.js.map +0 -1
  285. package/lib/connectionState.d.ts.map +0 -1
  286. package/lib/connectionState.js.map +0 -1
  287. package/lib/connectionStateHandler.d.ts +0 -81
  288. package/lib/connectionStateHandler.d.ts.map +0 -1
  289. package/lib/connectionStateHandler.js +0 -276
  290. package/lib/connectionStateHandler.js.map +0 -1
  291. package/lib/container.d.ts +0 -238
  292. package/lib/container.d.ts.map +0 -1
  293. package/lib/container.js +0 -1276
  294. package/lib/container.js.map +0 -1
  295. package/lib/containerContext.d.ts +0 -84
  296. package/lib/containerContext.d.ts.map +0 -1
  297. package/lib/containerContext.js +0 -213
  298. package/lib/containerContext.js.map +0 -1
  299. package/lib/containerStorageAdapter.d.ts +0 -48
  300. package/lib/containerStorageAdapter.d.ts.map +0 -1
  301. package/lib/containerStorageAdapter.js +0 -99
  302. package/lib/containerStorageAdapter.js.map +0 -1
  303. package/lib/contracts.d.ts.map +0 -1
  304. package/lib/contracts.js.map +0 -1
  305. package/lib/deltaManager.d.ts.map +0 -1
  306. package/lib/deltaManager.js.map +0 -1
  307. package/lib/deltaManagerProxy.d.ts +0 -54
  308. package/lib/deltaManagerProxy.d.ts.map +0 -1
  309. package/lib/deltaManagerProxy.js +0 -110
  310. package/lib/deltaManagerProxy.js.map +0 -1
  311. package/lib/deltaQueue.d.ts.map +0 -1
  312. package/lib/deltaQueue.js.map +0 -1
  313. package/lib/index.d.ts +0 -8
  314. package/lib/index.d.ts.map +0 -1
  315. package/lib/index.js +0 -8
  316. package/lib/index.js.map +0 -1
  317. package/lib/loader.d.ts.map +0 -1
  318. package/lib/loader.js +0 -236
  319. package/lib/loader.js.map +0 -1
  320. package/lib/packageVersion.d.ts.map +0 -1
  321. package/lib/packageVersion.js.map +0 -1
  322. package/lib/protocolTreeDocumentStorageService.d.ts.map +0 -1
  323. package/lib/protocolTreeDocumentStorageService.js.map +0 -1
  324. package/lib/quorum.d.ts +0 -21
  325. package/lib/quorum.d.ts.map +0 -1
  326. package/lib/quorum.js +0 -38
  327. package/lib/quorum.js.map +0 -1
  328. package/lib/retriableDocumentStorageService.d.ts.map +0 -1
  329. package/lib/retriableDocumentStorageService.js.map +0 -1
  330. package/lib/utils.d.ts +0 -34
  331. package/lib/utils.d.ts.map +0 -1
  332. package/lib/utils.js.map +0 -1
  333. package/src/collabWindowTracker.ts +0 -102
  334. package/src/deltaManagerProxy.ts +0 -158
  335. package/tsconfig.esnext.json +0 -7
@@ -3,952 +3,1196 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { default as AbortController } from "abort-controller";
6
+ import { IDisposable, ITelemetryProperties, LogLevel } from "@fluidframework/core-interfaces";
7
+ import { assert } from "@fluidframework/core-utils";
8
+ import { performance, TypedEventEmitter } from "@fluid-internal/client-utils";
7
9
  import {
8
- IDisposable,
9
- ITelemetryLogger,
10
- ITelemetryProperties,
11
- } from "@fluidframework/common-definitions";
12
- import { assert, performance, TypedEventEmitter } from "@fluidframework/common-utils";
13
- import {
14
- IDeltaQueue,
15
- ReadOnlyInfo,
16
- IConnectionDetails,
17
- ICriticalContainerError,
10
+ ICriticalContainerError,
11
+ IDeltaQueue,
12
+ ReadOnlyInfo,
18
13
  } from "@fluidframework/container-definitions";
19
- import { GenericError, UsageError } from "@fluidframework/container-utils";
20
14
  import {
21
- IDocumentService,
22
- IDocumentDeltaConnection,
23
- IDocumentDeltaConnectionEvents,
15
+ IAnyDriverError,
16
+ IDocumentService,
17
+ IDocumentDeltaConnection,
18
+ IDocumentDeltaConnectionEvents,
19
+ // eslint-disable-next-line import/no-deprecated
20
+ DriverErrorType,
24
21
  } from "@fluidframework/driver-definitions";
25
22
  import {
26
- canRetryOnError,
27
- createWriteError,
28
- createGenericNetworkError,
29
- getRetryDelayFromError,
30
- IAnyDriverError,
31
- waitForConnectedState,
32
- DeltaStreamConnectionForbiddenError,
33
- logNetworkFailure,
34
- // isRuntimeMessage,
23
+ canRetryOnError,
24
+ createWriteError,
25
+ createGenericNetworkError,
26
+ getRetryDelayFromError,
27
+ logNetworkFailure,
28
+ isRuntimeMessage,
29
+ calculateMaxWaitTime,
35
30
  } from "@fluidframework/driver-utils";
36
31
  import {
37
- ConnectionMode,
38
- IClient,
39
- IClientConfiguration,
40
- IClientDetails,
41
- IDocumentMessage,
42
- INack,
43
- INackContent,
44
- ISequencedDocumentMessage,
45
- ISignalClient,
46
- ISignalMessage,
47
- ITokenClaims,
48
- MessageType,
49
- ScopeType,
50
- ISequencedDocumentSystemMessage,
32
+ ConnectionMode,
33
+ IClient,
34
+ IClientConfiguration,
35
+ IClientDetails,
36
+ IDocumentMessage,
37
+ INack,
38
+ INackContent,
39
+ ISequencedDocumentMessage,
40
+ ISignalClient,
41
+ ISignalMessage,
42
+ ITokenClaims,
43
+ MessageType,
44
+ ScopeType,
45
+ ISequencedDocumentSystemMessage,
51
46
  } from "@fluidframework/protocol-definitions";
52
47
  import {
53
- TelemetryLogger,
54
- normalizeError,
48
+ formatTick,
49
+ GenericError,
50
+ isFluidError,
51
+ ITelemetryLoggerExt,
52
+ normalizeError,
53
+ UsageError,
55
54
  } from "@fluidframework/telemetry-utils";
56
55
  import {
57
- ReconnectMode,
58
- IConnectionManager,
59
- IConnectionManagerFactoryArgs,
56
+ ReconnectMode,
57
+ IConnectionManager,
58
+ IConnectionManagerFactoryArgs,
59
+ IConnectionDetailsInternal,
60
+ IConnectionStateChangeReason,
60
61
  } from "./contracts";
61
62
  import { DeltaQueue } from "./deltaQueue";
63
+ import { SignalType } from "./protocol";
64
+ import { isDeltaStreamConnectionForbiddenError } from "./utils";
62
65
 
63
- const MaxReconnectDelayInMs = 8000;
64
- const InitialReconnectDelayInMs = 1000;
66
+ // We double this value in first try in when we calculate time to wait for in "calculateMaxWaitTime" function.
67
+ const InitialReconnectDelayInMs = 500;
65
68
  const DefaultChunkSize = 16 * 1024;
66
69
 
67
70
  const fatalConnectErrorProp = { fatalConnectError: true };
68
71
 
69
72
  function getNackReconnectInfo(nackContent: INackContent) {
70
- const message = `Nack (${nackContent.type}): ${nackContent.message}`;
71
- const canRetry = nackContent.code !== 403;
72
- const retryAfterMs = nackContent.retryAfter !== undefined ? nackContent.retryAfter * 1000 : undefined;
73
- return createGenericNetworkError(
74
- message,
75
- { canRetry, retryAfterMs },
76
- { statusCode: nackContent.code, driverVersion: undefined });
73
+ const message = `Nack (${nackContent.type}): ${nackContent.message}`;
74
+ const canRetry = nackContent.code !== 403;
75
+ const retryAfterMs =
76
+ nackContent.retryAfter !== undefined ? nackContent.retryAfter * 1000 : undefined;
77
+ return createGenericNetworkError(
78
+ message,
79
+ { canRetry, retryAfterMs },
80
+ { statusCode: nackContent.code, driverVersion: undefined },
81
+ );
77
82
  }
78
83
 
79
84
  /**
80
85
  * Implementation of IDocumentDeltaConnection that does not support submitting
81
86
  * or receiving ops. Used in storage-only mode.
82
87
  */
88
+ const clientNoDeltaStream: IClient = {
89
+ mode: "read",
90
+ details: { capabilities: { interactive: true } },
91
+ permission: [],
92
+ user: { id: "storage-only client" }, // we need some "fake" ID here.
93
+ scopes: [],
94
+ };
95
+ const clientIdNoDeltaStream: string = "storage-only client";
96
+
83
97
  class NoDeltaStream
84
- extends TypedEventEmitter<IDocumentDeltaConnectionEvents>
85
- implements IDocumentDeltaConnection, IDisposable {
86
- clientId: string = "storage-only client";
87
- claims: ITokenClaims = {
88
- scopes: [ScopeType.DocRead],
89
- } as any;
90
- mode: ConnectionMode = "read";
91
- existing: boolean = true;
92
- maxMessageSize: number = 0;
93
- version: string = "";
94
- initialMessages: ISequencedDocumentMessage[] = [];
95
- initialSignals: ISignalMessage[] = [];
96
- initialClients: ISignalClient[] = [];
97
- serviceConfiguration: IClientConfiguration = {
98
- maxMessageSize: 0,
99
- blockSize: 0,
100
- summary: undefined as any,
101
- };
102
- checkpointSequenceNumber?: number | undefined = undefined;
103
- submit(messages: IDocumentMessage[]): void {
104
- this.emit("nack", this.clientId, messages.map((operation) => {
105
- return {
106
- operation,
107
- content: { message: "Cannot submit with storage-only connection", code: 403 },
108
- };
109
- }));
110
- }
111
- submitSignal(message: any): void {
112
- this.emit("nack", this.clientId, {
113
- operation: message,
114
- content: { message: "Cannot submit signal with storage-only connection", code: 403 },
115
- });
116
- }
117
-
118
- private _disposed = false;
119
- public get disposed() { return this._disposed; }
120
- public dispose() { this._disposed = true; }
98
+ extends TypedEventEmitter<IDocumentDeltaConnectionEvents>
99
+ implements IDocumentDeltaConnection, IDisposable
100
+ {
101
+ clientId = clientIdNoDeltaStream;
102
+ claims: ITokenClaims = {
103
+ scopes: [ScopeType.DocRead],
104
+ } as any;
105
+ mode: ConnectionMode = "read";
106
+ existing: boolean = true;
107
+ maxMessageSize: number = 0;
108
+ version: string = "";
109
+ initialMessages: ISequencedDocumentMessage[] = [];
110
+ initialSignals: ISignalMessage[] = [];
111
+ initialClients: ISignalClient[] = [
112
+ { client: clientNoDeltaStream, clientId: clientIdNoDeltaStream },
113
+ ];
114
+ serviceConfiguration: IClientConfiguration = {
115
+ maxMessageSize: 0,
116
+ blockSize: 0,
117
+ };
118
+ checkpointSequenceNumber?: number | undefined = undefined;
119
+ /**
120
+ * Connection which is not connected to socket.
121
+ * @param storageOnlyReason - Reason on why the connection to delta stream is not allowed.
122
+ * @param readonlyConnectionReason - reason/error if any which lead to using NoDeltaStream.
123
+ */
124
+ constructor(
125
+ public readonly storageOnlyReason?: string,
126
+ public readonly readonlyConnectionReason?: IConnectionStateChangeReason,
127
+ ) {
128
+ super();
129
+ }
130
+ submit(messages: IDocumentMessage[]): void {
131
+ this.emit(
132
+ "nack",
133
+ this.clientId,
134
+ messages.map((operation) => {
135
+ return {
136
+ operation,
137
+ content: { message: "Cannot submit with storage-only connection", code: 403 },
138
+ };
139
+ }),
140
+ );
141
+ }
142
+ submitSignal(message: any): void {
143
+ this.emit("nack", this.clientId, {
144
+ operation: message,
145
+ content: { message: "Cannot submit signal with storage-only connection", code: 403 },
146
+ });
147
+ }
148
+
149
+ private _disposed = false;
150
+ public get disposed() {
151
+ return this._disposed;
152
+ }
153
+ public dispose() {
154
+ this._disposed = true;
155
+ }
121
156
  }
122
157
 
158
+ function isNoDeltaStreamConnection(connection: any): connection is NoDeltaStream {
159
+ return connection instanceof NoDeltaStream;
160
+ }
161
+
162
+ const waitForOnline = async (): Promise<void> => {
163
+ // Only wait if we have a strong signal that we're offline - otherwise assume we're online.
164
+ if (globalThis.navigator?.onLine === false && globalThis.addEventListener !== undefined) {
165
+ return new Promise<void>((resolve) => {
166
+ const resolveAndRemoveListener = () => {
167
+ resolve();
168
+ globalThis.removeEventListener("online", resolveAndRemoveListener);
169
+ };
170
+ globalThis.addEventListener("online", resolveAndRemoveListener);
171
+ });
172
+ }
173
+ };
174
+
123
175
  /**
124
176
  * Interface to track the current in-progress connection attempt.
125
177
  */
126
178
  interface IPendingConnection {
127
- /**
128
- * Used to cancel an in-progress connection attempt.
129
- */
130
- abort(): void;
131
-
132
- /**
133
- * Desired ConnectionMode of this in-progress connection attempt.
134
- */
135
- connectionMode: ConnectionMode;
179
+ /**
180
+ * Used to cancel an in-progress connection attempt.
181
+ */
182
+ abort(): void;
183
+
184
+ /**
185
+ * Desired ConnectionMode of this in-progress connection attempt.
186
+ */
187
+ connectionMode: ConnectionMode;
136
188
  }
137
189
 
138
190
  /**
139
191
  * Implementation of IConnectionManager, used by Container class
140
- * Implements constant connectivity to relay service, by reconnecting in case of loast connection or error.
141
- * Exposes various controls to influecen this process, including manual reconnects, forced read-only mode, etc.
192
+ * Implements constant connectivity to relay service, by reconnecting in case of lost connection or error.
193
+ * Exposes various controls to influence this process, including manual reconnects, forced read-only mode, etc.
142
194
  */
143
195
  export class ConnectionManager implements IConnectionManager {
144
- /** Connection mode used when reconnecting on error or disconnect. */
145
- private readonly defaultReconnectionMode: ConnectionMode;
146
-
147
- /**
148
- * Tracks the current in-progress connection attempt. Undefined if there is none.
149
- * Note: Once the connection attempt fires and the code becomes asynchronous, its possible that a new connection
150
- * attempt was fired and this.pendingConnection was overwritten to reflect the new attempt.
151
- */
152
- private pendingConnection: IPendingConnection | undefined;
153
- private connection: IDocumentDeltaConnection | undefined;
154
-
155
- /** file ACL - whether user has only read-only access to a file */
156
- private _readonlyPermissions: boolean | undefined;
157
-
158
- /** tracks host requiring read-only mode. */
159
- private _forceReadonly = false;
160
-
161
- /**
162
- * Controls whether the DeltaManager will automatically reconnect to the delta stream after receiving a disconnect.
163
- */
164
- private _reconnectMode: ReconnectMode;
165
-
166
- /** True if there is pending (async) reconnection from "read" to "write" */
167
- private pendingReconnect = false;
168
-
169
- private clientSequenceNumber = 0;
170
- private clientSequenceNumberObserved = 0;
171
- /** Counts the number of noops sent by the client which may not be acked. */
172
- private trailingNoopCount = 0;
173
-
174
- /** track clientId used last time when we sent any ops */
175
- private lastSubmittedClientId: string | undefined;
176
-
177
- private connectFirstConnection = true;
178
-
179
- private _connectionVerboseProps: Record<string, string | number> = {};
180
-
181
- private _connectionProps: ITelemetryProperties = {};
182
-
183
- private closed = false;
184
-
185
- private readonly _outbound: DeltaQueue<IDocumentMessage[]>;
186
-
187
- public get connectionVerboseProps() { return this._connectionVerboseProps; }
188
-
189
- public readonly clientDetails: IClientDetails;
190
-
191
- /**
192
- * The current connection mode, initially read.
193
- */
194
- public get connectionMode(): ConnectionMode {
195
- return this.connection?.mode ?? "read";
196
- }
197
-
198
- public get connected() { return this.connection !== undefined; }
199
-
200
- public get clientId() { return this.connection?.clientId; }
201
- /**
202
- * Automatic reconnecting enabled or disabled.
203
- * If set to Never, then reconnecting will never be allowed.
204
- */
205
- public get reconnectMode(): ReconnectMode {
206
- return this._reconnectMode;
207
- }
208
-
209
- public get maxMessageSize(): number {
210
- return this.connection?.serviceConfiguration?.maxMessageSize
211
- ?? DefaultChunkSize;
212
- }
213
-
214
- public get version(): string {
215
- if (this.connection === undefined) {
216
- throw new Error("Cannot check version without a connection");
217
- }
218
- return this.connection.version;
219
- }
220
-
221
- public get serviceConfiguration(): IClientConfiguration | undefined {
222
- return this.connection?.serviceConfiguration;
223
- }
224
-
225
- public get scopes(): string[] | undefined {
226
- return this.connection?.claims.scopes;
227
- }
228
-
229
- public get outbound(): IDeltaQueue<IDocumentMessage[]> {
230
- return this._outbound;
231
- }
232
-
233
- /**
234
- * Returns set of props that can be logged in telemetry that provide some insights / statistics
235
- * about current or last connection (if there is no connection at the moment)
236
- */
237
- public get connectionProps(): ITelemetryProperties {
238
- if (this.connection !== undefined) {
239
- return this._connectionProps;
240
- } else {
241
- return {
242
- ...this._connectionProps,
243
- // Report how many ops this client sent in last disconnected session
244
- sentOps: this.clientSequenceNumber,
245
- };
246
- }
247
- }
248
-
249
- public shouldJoinWrite(): boolean {
250
- // We don't have to wait for ack for topmost NoOps. So subtract those.
251
- return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.trailingNoopCount);
252
- }
253
-
254
- /**
255
- * Tells if container is in read-only mode.
256
- * Data stores should listen for "readonly" notifications and disallow user
257
- * making changes to data stores.
258
- * Readonly state can be because of no storage write permission,
259
- * or due to host forcing readonly mode for container.
260
- * It is undefined if we have not yet established websocket connection
261
- * and do not know if user has write access to a file.
262
- */
263
- private get readonly() {
264
- if (this._forceReadonly) {
265
- return true;
266
- }
267
- return this._readonlyPermissions;
268
- }
269
-
270
- public get readOnlyInfo(): ReadOnlyInfo {
271
- const storageOnly = this.connection !== undefined && this.connection instanceof NoDeltaStream;
272
- if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
273
- return {
274
- readonly: true,
275
- forced: this._forceReadonly,
276
- permissions: this._readonlyPermissions,
277
- storageOnly,
278
- };
279
- }
280
-
281
- return { readonly: this._readonlyPermissions };
282
- }
283
-
284
- private static detailsFromConnection(connection: IDocumentDeltaConnection): IConnectionDetails {
285
- return {
286
- claims: connection.claims,
287
- clientId: connection.clientId,
288
- existing: connection.existing,
289
- checkpointSequenceNumber: connection.checkpointSequenceNumber,
290
- get initialClients() { return connection.initialClients; },
291
- mode: connection.mode,
292
- serviceConfiguration: connection.serviceConfiguration,
293
- version: connection.version,
294
- };
295
- }
296
-
297
- constructor(
298
- private readonly serviceProvider: () => IDocumentService | undefined,
299
- private client: IClient,
300
- reconnectAllowed: boolean,
301
- private readonly logger: ITelemetryLogger,
302
- private readonly props: IConnectionManagerFactoryArgs,
303
- ) {
304
- this.clientDetails = this.client.details;
305
- this.defaultReconnectionMode = this.client.mode;
306
- this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
307
-
308
- // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
309
- // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
310
- this._outbound = new DeltaQueue<IDocumentMessage[]>(
311
- (messages) => {
312
- if (this.connection === undefined) {
313
- throw new Error("Attempted to submit an outbound message without connection");
314
- }
315
- this.connection.submit(messages);
316
- });
317
-
318
- this._outbound.on("error", (error) => {
319
- this.props.closeHandler(normalizeError(error));
320
- });
321
- }
322
-
323
- public dispose(error?: ICriticalContainerError) {
324
- if (this.closed) {
325
- return;
326
- }
327
- this.closed = true;
328
-
329
- this.pendingConnection = undefined;
330
-
331
- // Ensure that things like triggerConnect() will short circuit
332
- this._reconnectMode = ReconnectMode.Never;
333
-
334
- this._outbound.clear();
335
-
336
- const disconnectReason = error !== undefined
337
- ? `Closing DeltaManager (${error.message})`
338
- : "Closing DeltaManager";
339
-
340
- // This raises "disconnect" event if we have active connection.
341
- this.disconnectFromDeltaStream(disconnectReason);
342
-
343
- // Notify everyone we are in read-only state.
344
- // Useful for data stores in case we hit some critical error,
345
- // to switch to a mode where user edits are not accepted
346
- this.set_readonlyPermissions(true);
347
- }
348
-
349
- /**
350
- * Enables or disables automatic reconnecting.
351
- * Will throw an error if reconnectMode set to Never.
352
- */
353
- public setAutoReconnect(mode: ReconnectMode): void {
354
- assert(mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never,
355
- 0x278 /* "API is not supported for non-connecting or closed container" */);
356
-
357
- this._reconnectMode = mode;
358
-
359
- if (mode !== ReconnectMode.Enabled) {
360
- // immediately disconnect - do not rely on service eventually dropping connection.
361
- this.disconnectFromDeltaStream("setAutoReconnect");
362
- }
363
- }
364
-
365
- /**
366
- * Sends signal to runtime (and data stores) to be read-only.
367
- * Hosts may have read only views, indicating to data stores that no edits are allowed.
368
- * This is independent from this._readonlyPermissions (permissions) and this.connectionMode
369
- * (server can return "write" mode even when asked for "read")
370
- * Leveraging same "readonly" event as runtime & data stores should behave the same in such case
371
- * as in read-only permissions.
372
- * But this.active can be used by some DDSes to figure out if ops can be sent
373
- * (for example, read-only view still participates in code proposals / upgrades decisions)
374
- *
375
- * Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
376
- * the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
377
- * there are pending in the outbound queue, it will stop sending until force readonly is
378
- * cleared.
379
- *
380
- * @param readonly - set or clear force readonly.
381
- */
382
- public forceReadonly(readonly: boolean) {
383
- if (readonly !== this._forceReadonly) {
384
- this.logger.sendTelemetryEvent({
385
- eventName: "ForceReadOnly",
386
- value: readonly,
387
- });
388
- }
389
- const oldValue = this.readonly;
390
- this._forceReadonly = readonly;
391
-
392
- if (oldValue !== this.readonly) {
393
- if (this._reconnectMode === ReconnectMode.Never) {
394
- throw new UsageError("API is not supported for non-connecting or closed container");
395
- }
396
- let reconnect = false;
397
- if (this.readonly === true) {
398
- // If we switch to readonly while connected, we should disconnect first
399
- // See comment in the "readonly" event handler to deltaManager set up by
400
- // the ContainerRuntime constructor
401
-
402
- if (this.shouldJoinWrite()) {
403
- // If we have pending changes, then we will never send them - it smells like
404
- // host logic error.
405
- this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
406
- }
407
-
408
- reconnect = this.disconnectFromDeltaStream("Force readonly");
409
- }
410
- this.props.readonlyChangeHandler(this.readonly);
411
- if (reconnect) {
412
- // reconnect if we disconnected from before.
413
- this.triggerConnect("read");
414
- }
415
- }
416
- }
417
-
418
- private set_readonlyPermissions(readonly: boolean) {
419
- const oldValue = this.readonly;
420
- this._readonlyPermissions = readonly;
421
- if (oldValue !== this.readonly) {
422
- this.props.readonlyChangeHandler(this.readonly);
423
- }
424
- }
425
-
426
- public connect(connectionMode?: ConnectionMode) {
427
- this.connectCore(connectionMode).catch((error) => {
428
- const normalizedError = normalizeError(error, { props: fatalConnectErrorProp });
429
- this.props.closeHandler(normalizedError);
430
- });
431
- }
432
-
433
- private async connectCore(connectionMode?: ConnectionMode): Promise<void> {
434
- assert(!this.closed, 0x26a /* "not closed" */);
435
-
436
- if (this.connection !== undefined) {
437
- return; // Connection attempt already completed successfully
438
- }
439
-
440
- let pendingConnectionMode;
441
- if (this.pendingConnection !== undefined) {
442
- pendingConnectionMode = this.pendingConnection.connectionMode;
443
- this.cancelConnection(); // Throw out in-progress connection attempt in favor of new attempt
444
- assert(this.pendingConnection === undefined, 0x344 /* this.pendingConnection should be undefined */);
445
- }
446
- // If there is no specified ConnectionMode, try the previous mode, if there is no previous mode use default
447
- let requestedMode = connectionMode ?? pendingConnectionMode ?? this.defaultReconnectionMode;
448
-
449
- // if we have any non-acked ops from last connection, reconnect as "write".
450
- // without that we would connect in view-only mode, which will result in immediate
451
- // firing of "connected" event from Container and switch of current clientId (as tracked
452
- // by all DDSes). This will make it impossible to figure out if ops actually made it through,
453
- // so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
454
- if (this.shouldJoinWrite()) {
455
- requestedMode = "write";
456
- }
457
-
458
- const docService = this.serviceProvider();
459
- assert(docService !== undefined, 0x2a7 /* "Container is not attached" */);
460
-
461
- let connection: IDocumentDeltaConnection | undefined;
462
-
463
- if (docService.policies?.storageOnly === true) {
464
- connection = new NoDeltaStream();
465
- this.setupNewSuccessfulConnection(connection, "read");
466
- assert(this.pendingConnection === undefined, 0x2b3 /* "logic error" */);
467
- return;
468
- }
469
-
470
- let delayMs = InitialReconnectDelayInMs;
471
- let connectRepeatCount = 0;
472
- const connectStartTime = performance.now();
473
- let lastError: any;
474
-
475
- const abortController = new AbortController();
476
- const abortSignal = abortController.signal;
477
- this.pendingConnection = { abort: () => { abortController.abort(); }, connectionMode: requestedMode };
478
-
479
- // This loop will keep trying to connect until successful, with a delay between each iteration.
480
- while (connection === undefined) {
481
- if (this.closed) {
482
- throw new Error("Attempting to connect a closed DeltaManager");
483
- }
484
- if (abortSignal.aborted === true) {
485
- this.logger.sendTelemetryEvent({
486
- eventName: "ConnectionAttemptCancelled",
487
- attempts: connectRepeatCount,
488
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
489
- connectionEstablished: false,
490
- });
491
- return;
492
- }
493
- connectRepeatCount++;
494
-
495
- try {
496
- this.client.mode = requestedMode;
497
- connection = await docService.connectToDeltaStream({ ...this.client, mode: requestedMode });
498
-
499
- if (connection.disposed) {
500
- // Nobody observed this connection, so drop it on the floor and retry.
501
- this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
502
- connection = undefined;
503
- }
504
- } catch (origError: any) {
505
- if (typeof origError === "object" && origError !== null &&
506
- origError?.errorType === DeltaStreamConnectionForbiddenError.errorType) {
507
- connection = new NoDeltaStream();
508
- requestedMode = "read";
509
- break;
510
- }
511
-
512
- // Socket.io error when we connect to wrong socket, or hit some multiplexing bug
513
- if (!canRetryOnError(origError)) {
514
- const error = normalizeError(origError, { props: fatalConnectErrorProp });
515
- this.props.closeHandler(error);
516
- throw error;
517
- }
518
-
519
- // Since the error is retryable this will not log to the error table
520
- logNetworkFailure(
521
- this.logger,
522
- {
523
- attempts: connectRepeatCount,
524
- delay: delayMs, // milliseconds
525
- eventName: "DeltaConnectionFailureToConnect",
526
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
527
- },
528
- origError);
529
-
530
- lastError = origError;
531
-
532
- const retryDelayFromError = getRetryDelayFromError(origError);
533
- delayMs = retryDelayFromError ?? Math.min(delayMs * 2, MaxReconnectDelayInMs);
534
-
535
- if (retryDelayFromError !== undefined) {
536
- this.props.reconnectionDelayHandler(retryDelayFromError, origError);
537
- }
538
- await waitForConnectedState(delayMs);
539
- }
540
- }
541
-
542
- // If we retried more than once, log an event about how long it took (this will not log to error table)
543
- if (connectRepeatCount > 1) {
544
- logNetworkFailure(
545
- this.logger,
546
- {
547
- eventName: "MultipleDeltaConnectionFailures",
548
- attempts: connectRepeatCount,
549
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
550
- },
551
- lastError,
552
- );
553
- }
554
-
555
- // Check for abort signal after while loop as well
556
- if (abortSignal.aborted === true) {
557
- connection.dispose();
558
- this.logger.sendTelemetryEvent({
559
- eventName: "ConnectionAttemptCancelled",
560
- attempts: connectRepeatCount,
561
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
562
- connectionEstablished: true,
563
- });
564
- return;
565
- }
566
-
567
- this.setupNewSuccessfulConnection(connection, requestedMode);
568
- }
569
-
570
- /**
571
- * Start the connection. Any error should result in container being close.
572
- * And report the error if it excape for any reason.
573
- * @param args - The connection arguments
574
- */
575
- private triggerConnect(connectionMode: ConnectionMode) {
576
- assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
577
- if (this.reconnectMode !== ReconnectMode.Enabled) {
578
- return;
579
- }
580
- this.connect(connectionMode);
581
- }
582
-
583
- /**
584
- * Disconnect the current connection.
585
- * @param reason - Text description of disconnect reason to emit with disconnect event
586
- * @returns A boolean that indicates if there was an existing connection (or pending connection) to disconnect
587
- */
588
- private disconnectFromDeltaStream(reason: string): boolean {
589
- this.pendingReconnect = false;
590
-
591
- if (this.connection === undefined) {
592
- if (this.pendingConnection !== undefined) {
593
- this.cancelConnection();
594
- return true;
595
- }
596
- return false;
597
- }
598
-
599
- assert(this.pendingConnection === undefined, 0x27b /* "reentrancy may result in incorrect behavior" */);
600
-
601
- const connection = this.connection;
602
- // Avoid any re-entrancy - clear object reference
603
- this.connection = undefined;
604
-
605
- // Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
606
- connection.off("op", this.opHandler);
607
- connection.off("signal", this.props.signalHandler);
608
- connection.off("nack", this.nackHandler);
609
- connection.off("disconnect", this.disconnectHandlerInternal);
610
- connection.off("error", this.errorHandler);
611
- connection.off("pong", this.props.pongHandler);
612
-
613
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
614
- this._outbound.pause();
615
- this._outbound.clear();
616
- this.props.disconnectHandler(reason);
617
-
618
- connection.dispose();
619
-
620
- this._connectionVerboseProps = {};
621
-
622
- return true;
623
- }
624
-
625
- /**
626
- * Cancel in-progress connection attempt.
627
- */
628
- private cancelConnection() {
629
- assert(this.pendingConnection !== undefined,
630
- 0x345 /* this.pendingConnection is undefined when trying to cancel */);
631
- this.pendingConnection.abort();
632
- this.pendingConnection = undefined;
633
- this.logger.sendTelemetryEvent({ eventName: "ConnectionCancelReceived" });
634
- }
635
-
636
- /**
637
- * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
638
- * initial messages.
639
- * @param connection - The newly established connection
640
- */
641
- private setupNewSuccessfulConnection(connection: IDocumentDeltaConnection, requestedMode: ConnectionMode) {
642
- // Old connection should have been cleaned up before establishing a new one
643
- assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
644
- assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
645
-
646
- this.pendingConnection = undefined;
647
-
648
- this.connection = connection;
649
-
650
- // Does information in scopes & mode matches?
651
- // If we asked for "write" and got "read", then file is read-only
652
- // But if we ask read, server can still give us write.
653
- const readonly = !connection.claims.scopes.includes(ScopeType.DocWrite);
654
-
655
- // This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
656
- // removed after those packages have released and become ubiquitous.
657
- assert(requestedMode === "read" || readonly === (this.connectionMode === "read"),
658
- 0x0e7 /* "claims/connectionMode mismatch" */);
659
- assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
660
-
661
- this.set_readonlyPermissions(readonly);
662
-
663
- if (this.closed) {
664
- // Raise proper events, Log telemetry event and close connection.
665
- this.disconnectFromDeltaStream("ConnectionManager already closed");
666
- return;
667
- }
668
-
669
- this._outbound.resume();
670
-
671
- connection.on("op", this.opHandler);
672
- connection.on("signal", this.props.signalHandler);
673
- connection.on("nack", this.nackHandler);
674
- connection.on("disconnect", this.disconnectHandlerInternal);
675
- connection.on("error", this.errorHandler);
676
- connection.on("pong", this.props.pongHandler);
677
-
678
- // Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
679
- // ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
680
- // fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
681
- // we could avoid
682
- const initialMessages = connection.initialMessages.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
683
-
684
- // Some storages may provide checkpointSequenceNumber to identify how far client is behind.
685
- let checkpointSequenceNumber = connection.checkpointSequenceNumber;
686
-
687
- this._connectionVerboseProps = {
688
- clientId: connection.clientId,
689
- mode: connection.mode,
690
- };
691
-
692
- // reset connection props
693
- this._connectionProps = {};
694
-
695
- if (connection.relayServiceAgent !== undefined) {
696
- this._connectionVerboseProps.relayServiceAgent = connection.relayServiceAgent;
697
- this._connectionProps.relayServiceAgent = connection.relayServiceAgent;
698
- }
699
- this._connectionProps.socketDocumentId = connection.claims.documentId;
700
- this._connectionProps.connectionMode = connection.mode;
701
-
702
- let last = -1;
703
- if (initialMessages.length !== 0) {
704
- this._connectionVerboseProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
705
- last = initialMessages[initialMessages.length - 1].sequenceNumber;
706
- this._connectionVerboseProps.connectionInitialOpsTo = last + 1;
707
- // Update knowledge of how far we are behind, before raising "connect" event
708
- // This is duplication of what incomingOpHandler() does, but we have to raise event before we get there,
709
- // so duplicating update logic here as well.
710
- if (checkpointSequenceNumber === undefined || checkpointSequenceNumber < last) {
711
- checkpointSequenceNumber = last;
712
- }
713
- }
714
-
715
- this.props.incomingOpHandler(
716
- initialMessages,
717
- this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
718
-
719
- if (connection.initialSignals !== undefined) {
720
- for (const signal of connection.initialSignals) {
721
- this.props.signalHandler(signal);
722
- }
723
- }
724
-
725
- const details = ConnectionManager.detailsFromConnection(connection);
726
- details.checkpointSequenceNumber = checkpointSequenceNumber;
727
- this.props.connectHandler(details);
728
-
729
- this.connectFirstConnection = false;
730
- }
731
-
732
- /**
733
- * Disconnect the current connection and reconnect. Closes the container if it fails.
734
- * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
735
- * @param requestedMode - Read or write
736
- * @param error - Error reconnect information including whether or not to reconnect
737
- * @returns A promise that resolves when the connection is reestablished or we stop trying
738
- */
739
- private reconnectOnError(
740
- requestedMode: ConnectionMode,
741
- error: IAnyDriverError,
742
- ) {
743
- this.reconnect(
744
- requestedMode,
745
- error.message,
746
- error)
747
- .catch(this.props.closeHandler);
748
- }
749
-
750
- /**
751
- * Disconnect the current connection and reconnect.
752
- * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
753
- * @param requestedMode - Read or write
754
- * @param error - Error reconnect information including whether or not to reconnect
755
- * @returns A promise that resolves when the connection is reestablished or we stop trying
756
- */
757
- private async reconnect(
758
- requestedMode: ConnectionMode,
759
- disconnectMessage: string,
760
- error?: IAnyDriverError,
761
- ) {
762
- // We quite often get protocol errors before / after observing nack/disconnect
763
- // we do not want to run through same sequence twice.
764
- // If we're already disconnected/disconnecting it's not appropriate to call this again.
765
- assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
766
-
767
- this.disconnectFromDeltaStream(disconnectMessage);
768
-
769
- // We will always trigger reconnect, even if canRetry is false.
770
- // Any truly fatal error state will result in container close upon attempted reconnect,
771
- // which is a preferable to closing abruptly when a live connection fails.
772
- if (error !== undefined && !error.canRetry) {
773
- this.logger.sendTelemetryEvent({
774
- eventName: "reconnectingDespiteFatalError",
775
- reconnectMode: this.reconnectMode,
776
- }, error);
777
- }
778
-
779
- if (this.reconnectMode === ReconnectMode.Never) {
780
- // Do not raise container error if we are closing just because we lost connection.
781
- // Those errors (like IdleDisconnect) would show up in telemetry dashboards and
782
- // are very misleading, as first initial reaction - some logic is broken.
783
- this.props.closeHandler();
784
- }
785
-
786
- // If closed then we can't reconnect
787
- if (this.closed || this.reconnectMode !== ReconnectMode.Enabled) {
788
- return;
789
- }
790
-
791
- const delayMs = getRetryDelayFromError(error);
792
- if (error !== undefined && delayMs !== undefined) {
793
- this.props.reconnectionDelayHandler(delayMs, error);
794
- await waitForConnectedState(delayMs);
795
- }
796
-
797
- this.triggerConnect(requestedMode);
798
- }
799
-
800
- public prepareMessageToSend(message: Omit<IDocumentMessage, "clientSequenceNumber">): IDocumentMessage | undefined {
801
- if (this.readonly === true) {
802
- assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
803
- const error = new GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
804
- readonly: this.readOnlyInfo.readonly,
805
- forcedReadonly: this.readOnlyInfo.forced,
806
- readonlyPermissions: this.readOnlyInfo.permissions,
807
- storageOnly: this.readOnlyInfo.storageOnly,
808
- });
809
- this.props.closeHandler(error);
810
- return undefined;
811
- }
812
-
813
- // reset clientSequenceNumber if we are using new clientId.
814
- // we keep info about old connection as long as possible to be able to account for all non-acked ops
815
- // that we pick up on next connection.
816
- assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
817
- if (this.lastSubmittedClientId !== this.connection?.clientId) {
818
- this.lastSubmittedClientId = this.connection?.clientId;
819
- this.clientSequenceNumber = 0;
820
- this.clientSequenceNumberObserved = 0;
821
- }
822
-
823
- if (message.type === MessageType.NoOp) {
824
- this.trailingNoopCount++;
825
- } else {
826
- this.trailingNoopCount = 0;
827
- }
828
-
829
- return {
830
- ...message,
831
- clientSequenceNumber: ++this.clientSequenceNumber,
832
- };
833
- }
834
-
835
- public submitSignal(content: any) {
836
- if (this.connection !== undefined) {
837
- this.connection.submitSignal(content);
838
- } else {
839
- this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
840
- }
841
- }
842
-
843
- public sendMessages(messages: IDocumentMessage[]) {
844
- assert(this.connected, 0x2b4 /* "not connected on sending ops!" */);
845
- // If connection is "read" or implicit "read" (got leave op for "write" connection),
846
- // then op can't make it through - we will get a nack if op is sent.
847
- // We can short-circuit this process.
848
- // Note that we also want nacks to be rare and be treated as catastrophic failures.
849
- // Be careful with reentrancy though - disconnected event should not be be raised in the
850
- // middle of the current workflow, but rather on clean stack!
851
- if (this.connectionMode === "read") {
852
- if (!this.pendingReconnect) {
853
- this.pendingReconnect = true;
854
- Promise.resolve().then(async () => {
855
- if (this.pendingReconnect) { // still valid?
856
- await this.reconnect(
857
- "write", // connectionMode
858
- "Switch to write", // message
859
- );
860
- }
861
- })
862
- .catch(() => {});
863
- }
864
- return;
865
- }
866
-
867
- assert(!this.pendingReconnect, 0x2b5 /* "logic error" */);
868
-
869
- this._outbound.push(messages);
870
- }
871
-
872
- public beforeProcessingIncomingOp(message: ISequencedDocumentMessage) {
873
- // if we have connection, and message is local, then we better treat is as local!
874
- assert(this.clientId !== message.clientId || this.lastSubmittedClientId === message.clientId,
875
- 0x0ee /* "Not accounting local messages correctly" */,
876
- );
877
-
878
- if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
879
- const clientSequenceNumber = message.clientSequenceNumber;
880
-
881
- assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
882
- assert(clientSequenceNumber <= this.clientSequenceNumber,
883
- 0x0f0 /* "Incoming local client seq# > generated by this client" */);
884
-
885
- this.clientSequenceNumberObserved = clientSequenceNumber;
886
- }
887
-
888
- if (message.type === MessageType.ClientLeave) {
889
- const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
890
- const clientId = JSON.parse(systemLeaveMessage.data) as string;
891
- if (clientId === this.clientId) {
892
- // We have been kicked out from quorum
893
- this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
894
-
895
- // Please see #8483 for more details on why maintaining connection further as is would not work.
896
- // Short story - connection properties are immutable, and many processes (consensus DDSes, summarizer)
897
- // assume that connection stays "write" connection until disconnect, and act accordingly, which may
898
- // not work well with de-facto "read" connection we are in after receiving own leave op on timeout.
899
- // Clients need to be able to transition to "read" state after some time of inactivity!
900
- // Note - this may close container!
901
- this.reconnect(
902
- "read", // connectionMode
903
- "Switch to read", // message
904
- ).catch((error) => {
905
- this.logger.sendErrorEvent({ eventName: "SwitchToReadConnection" }, error);
906
- });
907
- }
908
- }
909
- }
910
-
911
- private readonly opHandler = (documentId: string, messagesArg: ISequencedDocumentMessage[]) => {
912
- const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
913
- this.props.incomingOpHandler(messages, "opHandler");
914
- };
915
-
916
- // Always connect in write mode after getting nacked.
917
- private readonly nackHandler = (documentId: string, messages: INack[]) => {
918
- const message = messages[0];
919
- if (this._readonlyPermissions === true) {
920
- this.props.closeHandler(createWriteError("writeOnReadOnlyDocument", { driverVersion: undefined }));
921
- return;
922
- }
923
-
924
- const reconnectInfo = getNackReconnectInfo(message.content);
925
-
926
- // If the nack indicates we cannot retry, then close the container outright
927
- if (!reconnectInfo.canRetry) {
928
- this.props.closeHandler(reconnectInfo);
929
- return;
930
- }
931
-
932
- this.reconnectOnError(
933
- "write",
934
- reconnectInfo,
935
- );
936
- };
937
-
938
- // Connection mode is always read on disconnect/error unless the system mode was write.
939
- private readonly disconnectHandlerInternal = (disconnectReason: IAnyDriverError) => {
940
- // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
941
- // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
942
- this.reconnectOnError(
943
- this.defaultReconnectionMode,
944
- disconnectReason,
945
- );
946
- };
947
-
948
- private readonly errorHandler = (error: IAnyDriverError) => {
949
- this.reconnectOnError(
950
- this.defaultReconnectionMode,
951
- error,
952
- );
953
- };
196
+ /** Connection mode used when reconnecting on error or disconnect. */
197
+ private readonly defaultReconnectionMode: ConnectionMode;
198
+
199
+ /**
200
+ * Tracks the current in-progress connection attempt. Undefined if there is none.
201
+ * Note: Once the connection attempt fires and the code becomes asynchronous, its possible that a new connection
202
+ * attempt was fired and this.pendingConnection was overwritten to reflect the new attempt.
203
+ */
204
+ private pendingConnection: IPendingConnection | undefined;
205
+ private connection: IDocumentDeltaConnection | undefined;
206
+
207
+ /** file ACL - whether user has only read-only access to a file */
208
+ private _readonlyPermissions: boolean | undefined;
209
+
210
+ /** tracks host requiring read-only mode. */
211
+ private _forceReadonly = false;
212
+
213
+ /**
214
+ * Controls whether the DeltaManager will automatically reconnect to the delta stream after receiving a disconnect.
215
+ */
216
+ private _reconnectMode: ReconnectMode;
217
+
218
+ /** True if there is pending (async) reconnection from "read" to "write" */
219
+ private pendingReconnect = false;
220
+
221
+ private clientSequenceNumber = 0;
222
+ private clientSequenceNumberObserved = 0;
223
+ /** Counts the number of non-runtime ops sent by the client which may not be acked. */
224
+ private localOpsToIgnore = 0;
225
+
226
+ /** track clientId used last time when we sent any ops */
227
+ private lastSubmittedClientId: string | undefined;
228
+
229
+ private connectFirstConnection = true;
230
+
231
+ private _connectionVerboseProps: Record<string, string | number> = {};
232
+
233
+ private _connectionProps: ITelemetryProperties = {};
234
+
235
+ private _disposed = false;
236
+
237
+ private readonly _outbound: DeltaQueue<IDocumentMessage[]>;
238
+
239
+ public get connectionVerboseProps() {
240
+ return this._connectionVerboseProps;
241
+ }
242
+
243
+ public readonly clientDetails: IClientDetails;
244
+
245
+ /**
246
+ * The current connection mode, initially read.
247
+ */
248
+ public get connectionMode(): ConnectionMode {
249
+ return this.connection?.mode ?? "read";
250
+ }
251
+
252
+ public get connected() {
253
+ return this.connection !== undefined;
254
+ }
255
+
256
+ public get clientId() {
257
+ return this.connection?.clientId;
258
+ }
259
+ /**
260
+ * Automatic reconnecting enabled or disabled.
261
+ * If set to Never, then reconnecting will never be allowed.
262
+ */
263
+ public get reconnectMode(): ReconnectMode {
264
+ return this._reconnectMode;
265
+ }
266
+
267
+ public get maxMessageSize(): number {
268
+ return this.connection?.serviceConfiguration?.maxMessageSize ?? DefaultChunkSize;
269
+ }
270
+
271
+ public get version(): string {
272
+ if (this.connection === undefined) {
273
+ throw new Error("Cannot check version without a connection");
274
+ }
275
+ return this.connection.version;
276
+ }
277
+
278
+ public get serviceConfiguration(): IClientConfiguration | undefined {
279
+ return this.connection?.serviceConfiguration;
280
+ }
281
+
282
+ public get scopes(): string[] | undefined {
283
+ return this.connection?.claims.scopes;
284
+ }
285
+
286
+ public get outbound(): IDeltaQueue<IDocumentMessage[]> {
287
+ return this._outbound;
288
+ }
289
+
290
+ /**
291
+ * Returns set of props that can be logged in telemetry that provide some insights / statistics
292
+ * about current or last connection (if there is no connection at the moment)
293
+ */
294
+ public get connectionProps(): ITelemetryProperties {
295
+ return this.connection !== undefined
296
+ ? this._connectionProps
297
+ : {
298
+ ...this._connectionProps,
299
+ // Report how many ops this client sent in last disconnected session
300
+ sentOps: this.clientSequenceNumber,
301
+ };
302
+ }
303
+
304
+ public shouldJoinWrite(): boolean {
305
+ // We don't have to wait for ack for topmost NoOps. So subtract those.
306
+ const outstandingOps =
307
+ this.clientSequenceNumberObserved < this.clientSequenceNumber - this.localOpsToIgnore;
308
+
309
+ // Previous behavior was to force write mode here only when there are outstanding ops (besides
310
+ // no-ops). The dirty signal from runtime should provide the same behavior, but also support
311
+ // stashed ops that weren't submitted to container layer yet. For safety, we want to retain the
312
+ // same behavior whenever dirty is false.
313
+ const isDirty = this.containerDirty();
314
+ if (outstandingOps !== isDirty) {
315
+ this.logger.sendTelemetryEvent({
316
+ eventName: "DesiredConnectionModeMismatch",
317
+ details: JSON.stringify({ outstandingOps, isDirty }),
318
+ });
319
+ }
320
+ return outstandingOps || isDirty;
321
+ }
322
+
323
+ /**
324
+ * Tells if container is in read-only mode.
325
+ * Data stores should listen for "readonly" notifications and disallow user
326
+ * making changes to data stores.
327
+ * Readonly state can be because of no storage write permission,
328
+ * or due to host forcing readonly mode for container.
329
+ * It is undefined if we have not yet established websocket connection
330
+ * and do not know if user has write access to a file.
331
+ */
332
+ private get readonly(): boolean | undefined {
333
+ return this.readOnlyInfo.readonly;
334
+ }
335
+
336
+ public get readOnlyInfo(): ReadOnlyInfo {
337
+ let storageOnly: boolean = false;
338
+ let storageOnlyReason: string | undefined;
339
+ if (isNoDeltaStreamConnection(this.connection)) {
340
+ storageOnly = true;
341
+ storageOnlyReason = this.connection.storageOnlyReason;
342
+ }
343
+ if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
344
+ return {
345
+ readonly: true,
346
+ forced: this._forceReadonly,
347
+ permissions: this._readonlyPermissions,
348
+ storageOnly,
349
+ storageOnlyReason,
350
+ };
351
+ }
352
+
353
+ return { readonly: this._readonlyPermissions };
354
+ }
355
+
356
+ private static detailsFromConnection(
357
+ connection: IDocumentDeltaConnection,
358
+ reason: IConnectionStateChangeReason,
359
+ ): IConnectionDetailsInternal {
360
+ return {
361
+ claims: connection.claims,
362
+ clientId: connection.clientId,
363
+ checkpointSequenceNumber: connection.checkpointSequenceNumber,
364
+ get initialClients() {
365
+ return connection.initialClients;
366
+ },
367
+ mode: connection.mode,
368
+ serviceConfiguration: connection.serviceConfiguration,
369
+ version: connection.version,
370
+ reason,
371
+ };
372
+ }
373
+
374
+ constructor(
375
+ private readonly serviceProvider: () => IDocumentService | undefined,
376
+ public readonly containerDirty: () => boolean,
377
+ private readonly client: IClient,
378
+ reconnectAllowed: boolean,
379
+ private readonly logger: ITelemetryLoggerExt,
380
+ private readonly props: IConnectionManagerFactoryArgs,
381
+ ) {
382
+ this.clientDetails = this.client.details;
383
+ this.defaultReconnectionMode = this.client.mode;
384
+ this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
385
+
386
+ // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
387
+ // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
388
+ this._outbound = new DeltaQueue<IDocumentMessage[]>((messages) => {
389
+ if (this.connection === undefined) {
390
+ throw new Error("Attempted to submit an outbound message without connection");
391
+ }
392
+ this.connection.submit(messages);
393
+ });
394
+
395
+ this._outbound.on("error", (error) => {
396
+ this.props.closeHandler(normalizeError(error));
397
+ });
398
+ }
399
+
400
+ public dispose(error?: ICriticalContainerError, switchToReadonly: boolean = true) {
401
+ if (this._disposed) {
402
+ return;
403
+ }
404
+ this._disposed = true;
405
+
406
+ // Ensure that things like triggerConnect() will short circuit
407
+ this._reconnectMode = ReconnectMode.Never;
408
+
409
+ this._outbound.clear();
410
+
411
+ const disconnectReason: IConnectionStateChangeReason = {
412
+ text: "Closing DeltaManager",
413
+ error,
414
+ };
415
+
416
+ const oldReadonlyValue = this.readonly;
417
+ // This raises "disconnect" event if we have active connection.
418
+ this.disconnectFromDeltaStream(disconnectReason);
419
+
420
+ if (switchToReadonly) {
421
+ // Notify everyone we are in read-only state.
422
+ // Useful for data stores in case we hit some critical error,
423
+ // to switch to a mode where user edits are not accepted
424
+ this.set_readonlyPermissions(true, oldReadonlyValue, disconnectReason);
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Enables or disables automatic reconnecting.
430
+ * Will throw an error if reconnectMode set to Never.
431
+ */
432
+ public setAutoReconnect(mode: ReconnectMode, reason: IConnectionStateChangeReason): void {
433
+ assert(
434
+ mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never,
435
+ 0x278 /* "API is not supported for non-connecting or closed container" */,
436
+ );
437
+
438
+ this._reconnectMode = mode;
439
+
440
+ if (mode !== ReconnectMode.Enabled) {
441
+ // immediately disconnect - do not rely on service eventually dropping connection.
442
+ this.disconnectFromDeltaStream(reason);
443
+ }
444
+ }
445
+
446
+ /**
447
+ * {@inheritDoc Container.forceReadonly}
448
+ */
449
+ public forceReadonly(readonly: boolean) {
450
+ if (readonly !== this._forceReadonly) {
451
+ this.logger.sendTelemetryEvent({
452
+ eventName: "ForceReadOnly",
453
+ value: readonly,
454
+ });
455
+ }
456
+ const oldValue = this.readonly;
457
+ this._forceReadonly = readonly;
458
+
459
+ if (oldValue !== this.readonly) {
460
+ if (this._reconnectMode === ReconnectMode.Never) {
461
+ throw new UsageError("API is not supported for non-connecting or closed container");
462
+ }
463
+ let reconnect = false;
464
+ if (this.readonly === true) {
465
+ // If we switch to readonly while connected, we should disconnect first
466
+ // See comment in the "readonly" event handler to deltaManager set up by
467
+ // the ContainerRuntime constructor
468
+
469
+ if (this.shouldJoinWrite()) {
470
+ // If we have pending changes, then we will never send them - it smells like
471
+ // host logic error.
472
+ this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
473
+ }
474
+
475
+ reconnect = this.disconnectFromDeltaStream({ text: "Force readonly" });
476
+ }
477
+ this.props.readonlyChangeHandler(this.readonly);
478
+ if (reconnect) {
479
+ // reconnect if we disconnected from before.
480
+ this.triggerConnect({ text: "Force Readonly" }, "read");
481
+ }
482
+ }
483
+ }
484
+
485
+ private set_readonlyPermissions(
486
+ newReadonlyValue: boolean,
487
+ oldReadonlyValue: boolean | undefined,
488
+ readonlyConnectionReason?: IConnectionStateChangeReason,
489
+ ) {
490
+ this._readonlyPermissions = newReadonlyValue;
491
+ if (oldReadonlyValue !== this.readonly) {
492
+ this.props.readonlyChangeHandler(this.readonly, readonlyConnectionReason);
493
+ }
494
+ }
495
+
496
+ public connect(reason: IConnectionStateChangeReason, connectionMode?: ConnectionMode) {
497
+ this.connectCore(reason, connectionMode).catch((e) => {
498
+ const normalizedError = normalizeError(e, { props: fatalConnectErrorProp });
499
+ this.props.closeHandler(normalizedError);
500
+ });
501
+ }
502
+
503
+ private async connectCore(
504
+ reason: IConnectionStateChangeReason,
505
+ connectionMode?: ConnectionMode,
506
+ ): Promise<void> {
507
+ assert(!this._disposed, 0x26a /* "not closed" */);
508
+
509
+ if (this.connection !== undefined) {
510
+ return; // Connection attempt already completed successfully
511
+ }
512
+
513
+ let pendingConnectionMode;
514
+ if (this.pendingConnection !== undefined) {
515
+ pendingConnectionMode = this.pendingConnection.connectionMode;
516
+ this.cancelConnection(reason); // Throw out in-progress connection attempt in favor of new attempt
517
+ assert(
518
+ this.pendingConnection === undefined,
519
+ 0x344 /* this.pendingConnection should be undefined */,
520
+ );
521
+ }
522
+ // If there is no specified ConnectionMode, try the previous mode, if there is no previous mode use default
523
+ let requestedMode = connectionMode ?? pendingConnectionMode ?? this.defaultReconnectionMode;
524
+
525
+ // if we have any non-acked ops from last connection, reconnect as "write".
526
+ // without that we would connect in view-only mode, which will result in immediate
527
+ // firing of "connected" event from Container and switch of current clientId (as tracked
528
+ // by all DDSes). This will make it impossible to figure out if ops actually made it through,
529
+ // so DDSes will immediately resubmit all pending ops, and some of them will be duplicates, corrupting document
530
+ if (this.shouldJoinWrite()) {
531
+ requestedMode = "write";
532
+ }
533
+
534
+ const docService = this.serviceProvider();
535
+ assert(docService !== undefined, 0x2a7 /* "Container is not attached" */);
536
+
537
+ let connection: IDocumentDeltaConnection | undefined;
538
+
539
+ if (docService.policies?.storageOnly === true) {
540
+ connection = new NoDeltaStream();
541
+ this.setupNewSuccessfulConnection(connection, "read", reason);
542
+ assert(this.pendingConnection === undefined, 0x2b3 /* "logic error" */);
543
+ return;
544
+ }
545
+
546
+ let delayMs = InitialReconnectDelayInMs;
547
+ let connectRepeatCount = 0;
548
+ const connectStartTime = performance.now();
549
+ let lastError: any;
550
+
551
+ const abortController = new AbortController();
552
+ const abortSignal = abortController.signal;
553
+ this.pendingConnection = {
554
+ abort: () => {
555
+ abortController.abort();
556
+ },
557
+ connectionMode: requestedMode,
558
+ };
559
+
560
+ this.props.establishConnectionHandler(reason);
561
+ // This loop will keep trying to connect until successful, with a delay between each iteration.
562
+ while (connection === undefined) {
563
+ if (this._disposed) {
564
+ throw new Error("Attempting to connect a closed DeltaManager");
565
+ }
566
+ if (abortSignal.aborted === true) {
567
+ this.logger.sendTelemetryEvent({
568
+ eventName: "ConnectionAttemptCancelled",
569
+ attempts: connectRepeatCount,
570
+ duration: formatTick(performance.now() - connectStartTime),
571
+ connectionEstablished: false,
572
+ });
573
+ return;
574
+ }
575
+ connectRepeatCount++;
576
+
577
+ try {
578
+ this.client.mode = requestedMode;
579
+ connection = await docService.connectToDeltaStream({
580
+ ...this.client,
581
+ mode: requestedMode,
582
+ });
583
+
584
+ if (connection.disposed) {
585
+ // Nobody observed this connection, so drop it on the floor and retry.
586
+ this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
587
+ connection = undefined;
588
+ }
589
+ this.logger.sendTelemetryEvent(
590
+ {
591
+ eventName: "ConnectionReceived",
592
+ connected: connection !== undefined && connection.disposed === false,
593
+ },
594
+ undefined,
595
+ LogLevel.verbose,
596
+ );
597
+ } catch (origError: any) {
598
+ this.logger.sendTelemetryEvent(
599
+ {
600
+ eventName: "ConnectToDeltaStreamException",
601
+ connected: connection !== undefined && connection.disposed === false,
602
+ },
603
+ undefined,
604
+ LogLevel.verbose,
605
+ );
606
+ if (isDeltaStreamConnectionForbiddenError(origError)) {
607
+ connection = new NoDeltaStream(origError.storageOnlyReason, {
608
+ text: origError.message,
609
+ error: origError,
610
+ });
611
+ requestedMode = "read";
612
+ break;
613
+ } else if (
614
+ isFluidError(origError) &&
615
+ // eslint-disable-next-line import/no-deprecated
616
+ origError.errorType === DriverErrorType.outOfStorageError
617
+ ) {
618
+ // If we get out of storage error from calling joinsession, then use the NoDeltaStream object so
619
+ // that user can at least load the container.
620
+ connection = new NoDeltaStream(undefined, {
621
+ text: origError.message,
622
+ error: origError,
623
+ });
624
+ requestedMode = "read";
625
+ break;
626
+ }
627
+
628
+ // Socket.io error when we connect to wrong socket, or hit some multiplexing bug
629
+ if (!canRetryOnError(origError)) {
630
+ const error = normalizeError(origError, { props: fatalConnectErrorProp });
631
+ this.props.closeHandler(error);
632
+ throw error;
633
+ }
634
+
635
+ // Since the error is retryable this will not log to the error table
636
+ logNetworkFailure(
637
+ this.logger,
638
+ {
639
+ attempts: connectRepeatCount,
640
+ delay: delayMs, // milliseconds
641
+ eventName: "DeltaConnectionFailureToConnect",
642
+ duration: formatTick(performance.now() - connectStartTime),
643
+ },
644
+ origError,
645
+ );
646
+
647
+ lastError = origError;
648
+
649
+ const waitStartTime = performance.now();
650
+ const retryDelayFromError = getRetryDelayFromError(origError);
651
+ // If the error told us to wait or browser signals us that we are offline, then calculate the time we
652
+ // want to wait for before retrying. then we wait for that time. If the error didn't tell us to wait,
653
+ // let's still wait a little bit before retrying. We can skip this delay if we're confident we're offline,
654
+ // because we probably just need to wait to come back online. But we never have strong signal of being
655
+ // offline, so we at least wait for sometime.
656
+ if (retryDelayFromError !== undefined || globalThis.navigator?.onLine !== false) {
657
+ delayMs = calculateMaxWaitTime(delayMs, origError);
658
+ }
659
+ // Raise event in case the delay was there.
660
+ this.props.reconnectionDelayHandler(delayMs, origError);
661
+ await new Promise<void>((resolve) => {
662
+ setTimeout(resolve, delayMs);
663
+ });
664
+
665
+ // If we believe we're offline, we assume there's no point in trying until we at least think we're online.
666
+ // NOTE: This isn't strictly true for drivers that don't require network (e.g. local driver). Really this logic
667
+ // should probably live in the driver.
668
+ await waitForOnline();
669
+ this.logger.sendPerformanceEvent({
670
+ eventName: "WaitBetweenConnectionAttempts",
671
+ duration: performance.now() - waitStartTime,
672
+ details: JSON.stringify({
673
+ retryDelayFromError,
674
+ delayMs,
675
+ }),
676
+ });
677
+ }
678
+ }
679
+
680
+ // If we retried more than once, log an event about how long it took (this will not log to error table)
681
+ if (connectRepeatCount > 1) {
682
+ logNetworkFailure(
683
+ this.logger,
684
+ {
685
+ eventName: "MultipleDeltaConnectionFailures",
686
+ attempts: connectRepeatCount,
687
+ duration: formatTick(performance.now() - connectStartTime),
688
+ },
689
+ lastError,
690
+ );
691
+ }
692
+
693
+ // Check for abort signal after while loop as well or we've been disposed
694
+ if (abortSignal.aborted === true || this._disposed) {
695
+ connection.dispose();
696
+ this.logger.sendTelemetryEvent({
697
+ eventName: "ConnectionAttemptCancelled",
698
+ attempts: connectRepeatCount,
699
+ duration: formatTick(performance.now() - connectStartTime),
700
+ connectionEstablished: true,
701
+ });
702
+ return;
703
+ }
704
+
705
+ this.setupNewSuccessfulConnection(connection, requestedMode, reason);
706
+ }
707
+
708
+ /**
709
+ * Start the connection. Any error should result in container being closed.
710
+ * And report the error if it escapes for any reason.
711
+ * @param args - The connection arguments
712
+ */
713
+ private triggerConnect(reason: IConnectionStateChangeReason, connectionMode: ConnectionMode) {
714
+ // reconnect() includes async awaits, and that causes potential race conditions
715
+ // where we might already have a connection. If it were to happen, it's possible that we will connect
716
+ // with different mode to `connectionMode`. Glancing through the caller chains, it looks like code should be
717
+ // fine (if needed, reconnect flow will get triggered again). Places where new mode matters should encode it
718
+ // directly in connectCore - see this.shouldJoinWrite() test as an example.
719
+ // assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
720
+
721
+ if (this.reconnectMode !== ReconnectMode.Enabled) {
722
+ return;
723
+ }
724
+ this.connect(reason, connectionMode);
725
+ }
726
+
727
+ /**
728
+ * Disconnect the current connection.
729
+ * @param reason - Text description of disconnect reason to emit with disconnect event
730
+ * @param error - Error causing the disconnect if any.
731
+ * @returns A boolean that indicates if there was an existing connection (or pending connection) to disconnect
732
+ */
733
+ private disconnectFromDeltaStream(reason: IConnectionStateChangeReason): boolean {
734
+ this.pendingReconnect = false;
735
+
736
+ if (this.connection === undefined) {
737
+ if (this.pendingConnection !== undefined) {
738
+ this.cancelConnection(reason);
739
+ return true;
740
+ }
741
+ return false;
742
+ }
743
+
744
+ assert(
745
+ this.pendingConnection === undefined,
746
+ 0x27b /* "reentrancy may result in incorrect behavior" */,
747
+ );
748
+
749
+ const connection = this.connection;
750
+ // Avoid any re-entrancy - clear object reference
751
+ this.connection = undefined;
752
+
753
+ // Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
754
+ connection.off("op", this.opHandler);
755
+ connection.off("signal", this.signalHandler);
756
+ connection.off("nack", this.nackHandler);
757
+ connection.off("disconnect", this.disconnectHandlerInternal);
758
+ connection.off("error", this.errorHandler);
759
+ connection.off("pong", this.props.pongHandler);
760
+
761
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
762
+ this._outbound.pause();
763
+ this._outbound.clear();
764
+ connection.dispose();
765
+
766
+ this.props.disconnectHandler(reason);
767
+
768
+ this._connectionVerboseProps = {};
769
+
770
+ return true;
771
+ }
772
+
773
+ /**
774
+ * Cancel in-progress connection attempt.
775
+ */
776
+ private cancelConnection(reason: IConnectionStateChangeReason) {
777
+ assert(
778
+ this.pendingConnection !== undefined,
779
+ 0x345 /* this.pendingConnection is undefined when trying to cancel */,
780
+ );
781
+ this.pendingConnection.abort();
782
+ this.pendingConnection = undefined;
783
+ this.logger.sendTelemetryEvent({ eventName: "ConnectionCancelReceived" });
784
+ this.props.cancelConnectionHandler({
785
+ text: `Cancel Pending Connection due to ${reason.text}`,
786
+ error: reason.error,
787
+ });
788
+ }
789
+
790
+ /**
791
+ * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
792
+ * initial messages.
793
+ * @param connection - The newly established connection
794
+ */
795
+ private setupNewSuccessfulConnection(
796
+ connection: IDocumentDeltaConnection,
797
+ requestedMode: ConnectionMode,
798
+ reason: IConnectionStateChangeReason,
799
+ ) {
800
+ // Old connection should have been cleaned up before establishing a new one
801
+ assert(
802
+ this.connection === undefined,
803
+ 0x0e6 /* "old connection exists on new connection setup" */,
804
+ );
805
+ assert(
806
+ !connection.disposed,
807
+ 0x28a /* "can't be disposed - Callers need to ensure that!" */,
808
+ );
809
+
810
+ this.pendingConnection = undefined;
811
+
812
+ const oldReadonlyValue = this.readonly;
813
+ this.connection = connection;
814
+
815
+ // Does information in scopes & mode matches?
816
+ // If we asked for "write" and got "read", then file is read-only
817
+ // But if we ask read, server can still give us write.
818
+ const readonly = !connection.claims.scopes.includes(ScopeType.DocWrite);
819
+
820
+ if (connection.mode !== requestedMode) {
821
+ this.logger.sendTelemetryEvent({
822
+ eventName: "ConnectionModeMismatch",
823
+ requestedMode,
824
+ mode: connection.mode,
825
+ });
826
+ }
827
+ // This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
828
+ // removed after those packages have released and become ubiquitous.
829
+ assert(
830
+ requestedMode === "read" || readonly === (this.connectionMode === "read"),
831
+ 0x0e7 /* "claims/connectionMode mismatch" */,
832
+ );
833
+ assert(
834
+ !readonly || this.connectionMode === "read",
835
+ 0x0e8 /* "readonly perf with write connection" */,
836
+ );
837
+
838
+ this.set_readonlyPermissions(
839
+ readonly,
840
+ oldReadonlyValue,
841
+ isNoDeltaStreamConnection(connection) ? connection.readonlyConnectionReason : undefined,
842
+ );
843
+
844
+ if (this._disposed) {
845
+ // Raise proper events, Log telemetry event and close connection.
846
+ this.disconnectFromDeltaStream({ text: "ConnectionManager already closed" });
847
+ return;
848
+ }
849
+
850
+ this._outbound.resume();
851
+
852
+ connection.on("op", this.opHandler);
853
+ connection.on("signal", this.signalHandler);
854
+ connection.on("nack", this.nackHandler);
855
+ connection.on("disconnect", this.disconnectHandlerInternal);
856
+ connection.on("error", this.errorHandler);
857
+ connection.on("pong", this.props.pongHandler);
858
+
859
+ // Initial messages are always sorted. However, due to early op handler installed by drivers and appending those
860
+ // ops to initialMessages, resulting set is no longer sorted, which would result in client hitting storage to
861
+ // fill in gap. We will recover by cancelling this request once we process remaining ops, but it's a waste that
862
+ // we could avoid
863
+ const initialMessages = connection.initialMessages.sort(
864
+ (a, b) => a.sequenceNumber - b.sequenceNumber,
865
+ );
866
+
867
+ // Some storages may provide checkpointSequenceNumber to identify how far client is behind.
868
+ let checkpointSequenceNumber = connection.checkpointSequenceNumber;
869
+
870
+ this._connectionVerboseProps = {
871
+ clientId: connection.clientId,
872
+ mode: connection.mode,
873
+ };
874
+
875
+ // reset connection props
876
+ this._connectionProps = {};
877
+
878
+ if (connection.relayServiceAgent !== undefined) {
879
+ this._connectionVerboseProps.relayServiceAgent = connection.relayServiceAgent;
880
+ this._connectionProps.relayServiceAgent = connection.relayServiceAgent;
881
+ }
882
+ this._connectionProps.socketDocumentId = connection.claims.documentId;
883
+ this._connectionProps.connectionMode = connection.mode;
884
+
885
+ let last = -1;
886
+ if (initialMessages.length !== 0) {
887
+ this._connectionVerboseProps.connectionInitialOpsFrom =
888
+ initialMessages[0].sequenceNumber;
889
+ last = initialMessages[initialMessages.length - 1].sequenceNumber;
890
+ this._connectionVerboseProps.connectionInitialOpsTo = last + 1;
891
+ // Update knowledge of how far we are behind, before raising "connect" event
892
+ // This is duplication of what incomingOpHandler() does, but we have to raise event before we get there,
893
+ // so duplicating update logic here as well.
894
+ if (checkpointSequenceNumber === undefined || checkpointSequenceNumber < last) {
895
+ checkpointSequenceNumber = last;
896
+ }
897
+ }
898
+
899
+ this.props.incomingOpHandler(
900
+ initialMessages,
901
+ this.connectFirstConnection ? "InitialOps" : "ReconnectOps",
902
+ );
903
+
904
+ const details = ConnectionManager.detailsFromConnection(connection, reason);
905
+ details.checkpointSequenceNumber = checkpointSequenceNumber;
906
+ this.props.connectHandler(details);
907
+
908
+ this.connectFirstConnection = false;
909
+
910
+ // Synthesize clear & join signals out of initialClients state.
911
+ // This allows us to have single way to process signals, and makes it simpler to initialize
912
+ // protocol in Container.
913
+ const clearSignal: ISignalMessage = {
914
+ clientId: null, // system message
915
+ content: JSON.stringify({
916
+ type: SignalType.Clear,
917
+ }),
918
+ };
919
+
920
+ // list of signals to process due to this new connection
921
+ let signalsToProcess: ISignalMessage[] = [clearSignal];
922
+
923
+ const clientJoinSignals: ISignalMessage[] = (connection.initialClients ?? []).map(
924
+ (priorClient) => ({
925
+ clientId: null, // system signal
926
+ content: JSON.stringify({
927
+ type: SignalType.ClientJoin,
928
+ content: priorClient, // ISignalClient
929
+ }),
930
+ }),
931
+ );
932
+ if (clientJoinSignals.length > 0) {
933
+ signalsToProcess = signalsToProcess.concat(clientJoinSignals);
934
+ }
935
+
936
+ // Unfortunately, there is no defined order between initialSignals (including join & leave signals)
937
+ // and connection.initialClients. In practice, connection.initialSignals quite often contains join signal
938
+ // for "self" and connection.initialClients does not contain "self", so we have to process them after
939
+ // "clear" signal above.
940
+ if (connection.initialSignals !== undefined && connection.initialSignals.length > 0) {
941
+ signalsToProcess = signalsToProcess.concat(connection.initialSignals);
942
+ }
943
+
944
+ this.props.signalHandler(signalsToProcess);
945
+ }
946
+
947
+ /**
948
+ * Disconnect the current connection and reconnect. Closes the container if it fails.
949
+ * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
950
+ * @param requestedMode - Read or write
951
+ * @param error - Error reconnect information including whether or not to reconnect
952
+ * @returns A promise that resolves when the connection is reestablished or we stop trying
953
+ */
954
+ private reconnectOnError(requestedMode: ConnectionMode, error: IAnyDriverError) {
955
+ this.reconnect(requestedMode, { text: error.message, error }).catch(
956
+ this.props.closeHandler,
957
+ );
958
+ }
959
+
960
+ /**
961
+ * Disconnect the current connection and reconnect.
962
+ * @param connection - The connection that wants to reconnect - no-op if it's different from this.connection
963
+ * @param requestedMode - Read or write
964
+ * @param error - Error reconnect information including whether or not to reconnect
965
+ * @returns A promise that resolves when the connection is reestablished or we stop trying
966
+ */
967
+ private async reconnect(
968
+ requestedMode: ConnectionMode,
969
+ reason: IConnectionStateChangeReason<IAnyDriverError>,
970
+ ) {
971
+ // We quite often get protocol errors before / after observing nack/disconnect
972
+ // we do not want to run through same sequence twice.
973
+ // If we're already disconnected/disconnecting it's not appropriate to call this again.
974
+ assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
975
+
976
+ this.disconnectFromDeltaStream(reason);
977
+
978
+ // We will always trigger reconnect, even if canRetry is false.
979
+ // Any truly fatal error state will result in container close upon attempted reconnect,
980
+ // which is a preferable to closing abruptly when a live connection fails.
981
+ if (reason.error?.canRetry === false) {
982
+ this.logger.sendTelemetryEvent(
983
+ {
984
+ eventName: "reconnectingDespiteFatalError",
985
+ reconnectMode: this.reconnectMode,
986
+ },
987
+ reason.error,
988
+ );
989
+ }
990
+
991
+ if (this.reconnectMode === ReconnectMode.Never) {
992
+ // Do not raise container error if we are closing just because we lost connection.
993
+ // Those errors (like IdleDisconnect) would show up in telemetry dashboards and
994
+ // are very misleading, as first initial reaction - some logic is broken.
995
+ this.props.closeHandler();
996
+ }
997
+
998
+ // If closed then we can't reconnect
999
+ if (this._disposed || this.reconnectMode !== ReconnectMode.Enabled) {
1000
+ return;
1001
+ }
1002
+
1003
+ // If the error tells us to wait before retrying, then do so.
1004
+ const delayMs = getRetryDelayFromError(reason.error);
1005
+ if (reason.error !== undefined && delayMs !== undefined) {
1006
+ this.props.reconnectionDelayHandler(delayMs, reason.error);
1007
+ await new Promise<void>((resolve) => {
1008
+ setTimeout(resolve, delayMs);
1009
+ });
1010
+ }
1011
+
1012
+ // If we believe we're offline, we assume there's no point in trying again until we at least think we're online.
1013
+ // NOTE: This isn't strictly true for drivers that don't require network (e.g. local driver). Really this logic
1014
+ // should probably live in the driver.
1015
+ await waitForOnline();
1016
+
1017
+ this.triggerConnect(
1018
+ {
1019
+ text:
1020
+ reason.error !== undefined
1021
+ ? "Reconnecting due to Error"
1022
+ : `Reconnecting due to: ${reason.text}`,
1023
+ error: reason.error,
1024
+ },
1025
+ requestedMode,
1026
+ );
1027
+ }
1028
+
1029
+ public prepareMessageToSend(
1030
+ message: Omit<IDocumentMessage, "clientSequenceNumber">,
1031
+ ): IDocumentMessage | undefined {
1032
+ if (this.readonly === true) {
1033
+ assert(
1034
+ this.readOnlyInfo.readonly === true,
1035
+ 0x1f0 /* "Unexpected mismatch in readonly" */,
1036
+ );
1037
+ const error = new GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
1038
+ readonly: this.readOnlyInfo.readonly,
1039
+ forcedReadonly: this.readOnlyInfo.forced,
1040
+ readonlyPermissions: this.readOnlyInfo.permissions,
1041
+ storageOnly: this.readOnlyInfo.storageOnly,
1042
+ storageOnlyReason: this.readOnlyInfo.storageOnlyReason,
1043
+ });
1044
+ this.props.closeHandler(error);
1045
+ return undefined;
1046
+ }
1047
+
1048
+ // reset clientSequenceNumber if we are using new clientId.
1049
+ // we keep info about old connection as long as possible to be able to account for all non-acked ops
1050
+ // that we pick up on next connection.
1051
+ assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
1052
+ if (this.lastSubmittedClientId !== this.connection?.clientId) {
1053
+ this.lastSubmittedClientId = this.connection?.clientId;
1054
+ this.clientSequenceNumber = 0;
1055
+ this.clientSequenceNumberObserved = 0;
1056
+ }
1057
+
1058
+ if (!isRuntimeMessage(message)) {
1059
+ this.localOpsToIgnore++;
1060
+ } else {
1061
+ this.localOpsToIgnore = 0;
1062
+ }
1063
+
1064
+ return {
1065
+ ...message,
1066
+ clientSequenceNumber: ++this.clientSequenceNumber,
1067
+ };
1068
+ }
1069
+
1070
+ public submitSignal(content: any, targetClientId?: string) {
1071
+ if (this.connection !== undefined) {
1072
+ this.connection.submitSignal(content, targetClientId);
1073
+ } else {
1074
+ this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
1075
+ }
1076
+ }
1077
+
1078
+ public sendMessages(messages: IDocumentMessage[]) {
1079
+ assert(this.connected, 0x2b4 /* "not connected on sending ops!" */);
1080
+ // If connection is "read" or implicit "read" (got leave op for "write" connection),
1081
+ // then op can't make it through - we will get a nack if op is sent.
1082
+ // We can short-circuit this process.
1083
+ // Note that we also want nacks to be rare and be treated as catastrophic failures.
1084
+ // Be careful with reentrancy though - disconnected event should not be be raised in the
1085
+ // middle of the current workflow, but rather on clean stack!
1086
+ if (this.connectionMode === "read") {
1087
+ if (!this.pendingReconnect) {
1088
+ this.pendingReconnect = true;
1089
+ Promise.resolve()
1090
+ .then(async () => {
1091
+ if (this.pendingReconnect) {
1092
+ // still valid?
1093
+ await this.reconnect(
1094
+ "write", // connectionMode
1095
+ { text: "Switch to write" }, // message
1096
+ );
1097
+ }
1098
+ })
1099
+ .catch(() => {});
1100
+ }
1101
+ return;
1102
+ }
1103
+
1104
+ assert(!this.pendingReconnect, 0x2b5 /* "logic error" */);
1105
+
1106
+ this._outbound.push(messages);
1107
+ }
1108
+
1109
+ public beforeProcessingIncomingOp(message: ISequencedDocumentMessage) {
1110
+ // if we have connection, and message is local, then we better treat is as local!
1111
+ assert(
1112
+ this.clientId !== message.clientId || this.lastSubmittedClientId === message.clientId,
1113
+ 0x0ee /* "Not accounting local messages correctly" */,
1114
+ );
1115
+
1116
+ if (
1117
+ this.lastSubmittedClientId !== undefined &&
1118
+ this.lastSubmittedClientId === message.clientId
1119
+ ) {
1120
+ const clientSequenceNumber = message.clientSequenceNumber;
1121
+
1122
+ assert(
1123
+ this.clientSequenceNumberObserved < clientSequenceNumber,
1124
+ 0x0ef /* "client seq# not growing" */,
1125
+ );
1126
+ assert(
1127
+ clientSequenceNumber <= this.clientSequenceNumber,
1128
+ 0x0f0 /* "Incoming local client seq# > generated by this client" */,
1129
+ );
1130
+
1131
+ this.clientSequenceNumberObserved = clientSequenceNumber;
1132
+ }
1133
+
1134
+ if (message.type === MessageType.ClientLeave) {
1135
+ const systemLeaveMessage = message as ISequencedDocumentSystemMessage;
1136
+ const clientId = JSON.parse(systemLeaveMessage.data) as string;
1137
+ if (clientId === this.clientId) {
1138
+ // We have been kicked out from quorum
1139
+ this.logger.sendPerformanceEvent({ eventName: "ReadConnectionTransition" });
1140
+
1141
+ // Please see #8483 for more details on why maintaining connection further as is would not work.
1142
+ // Short story - connection properties are immutable, and many processes (consensus DDSes, summarizer)
1143
+ // assume that connection stays "write" connection until disconnect, and act accordingly, which may
1144
+ // not work well with de-facto "read" connection we are in after receiving own leave op on timeout.
1145
+ // Clients need to be able to transition to "read" state after some time of inactivity!
1146
+ // Note - this may close container!
1147
+ this.reconnect(
1148
+ "read", // connectionMode
1149
+ { text: "Switch to read" }, // message
1150
+ ).catch((error) => {
1151
+ this.logger.sendErrorEvent({ eventName: "SwitchToReadConnection" }, error);
1152
+ });
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ private readonly opHandler = (documentId: string, messagesArg: ISequencedDocumentMessage[]) => {
1158
+ const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
1159
+ this.props.incomingOpHandler(messages, "opHandler");
1160
+ };
1161
+
1162
+ private readonly signalHandler = (signalsArg: ISignalMessage | ISignalMessage[]) => {
1163
+ const signals = Array.isArray(signalsArg) ? signalsArg : [signalsArg];
1164
+ this.props.signalHandler(signals);
1165
+ };
1166
+
1167
+ // Always connect in write mode after getting nacked.
1168
+ private readonly nackHandler = (documentId: string, messages: INack[]) => {
1169
+ const message = messages[0];
1170
+ if (this._readonlyPermissions === true) {
1171
+ this.props.closeHandler(
1172
+ createWriteError("writeOnReadOnlyDocument", { driverVersion: undefined }),
1173
+ );
1174
+ return;
1175
+ }
1176
+
1177
+ const reconnectInfo = getNackReconnectInfo(message.content);
1178
+
1179
+ // If the nack indicates we cannot retry, then close the container outright
1180
+ if (!reconnectInfo.canRetry) {
1181
+ this.props.closeHandler(reconnectInfo);
1182
+ return;
1183
+ }
1184
+
1185
+ this.reconnectOnError("write", reconnectInfo);
1186
+ };
1187
+
1188
+ // Connection mode is always read on disconnect/error unless the system mode was write.
1189
+ private readonly disconnectHandlerInternal = (disconnectReason: IAnyDriverError) => {
1190
+ // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
1191
+ // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
1192
+ this.reconnectOnError(this.defaultReconnectionMode, disconnectReason);
1193
+ };
1194
+
1195
+ private readonly errorHandler = (error: IAnyDriverError) => {
1196
+ this.reconnectOnError(this.defaultReconnectionMode, error);
1197
+ };
954
1198
  }