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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/.eslintrc.js +18 -21
  2. package/.mocharc.js +12 -0
  3. package/CHANGELOG.md +364 -0
  4. package/README.md +152 -56
  5. package/api-extractor-lint.json +4 -0
  6. package/api-extractor.json +2 -2
  7. package/api-report/container-loader.api.md +143 -0
  8. package/dist/{audience.js → audience.cjs} +15 -13
  9. package/dist/audience.cjs.map +1 -0
  10. package/dist/audience.d.ts +4 -6
  11. package/dist/audience.d.ts.map +1 -1
  12. package/dist/catchUpMonitor.cjs +43 -0
  13. package/dist/catchUpMonitor.cjs.map +1 -0
  14. package/dist/catchUpMonitor.d.ts +29 -0
  15. package/dist/catchUpMonitor.d.ts.map +1 -0
  16. package/dist/{connectionManager.js → connectionManager.cjs} +397 -240
  17. package/dist/connectionManager.cjs.map +1 -0
  18. package/dist/connectionManager.d.ts +23 -33
  19. package/dist/connectionManager.d.ts.map +1 -1
  20. package/dist/{connectionState.js → connectionState.cjs} +5 -7
  21. package/dist/connectionState.cjs.map +1 -0
  22. package/dist/connectionState.d.ts +3 -5
  23. package/dist/connectionState.d.ts.map +1 -1
  24. package/dist/connectionStateHandler.cjs +474 -0
  25. package/dist/connectionStateHandler.cjs.map +1 -0
  26. package/dist/connectionStateHandler.d.ts +127 -29
  27. package/dist/connectionStateHandler.d.ts.map +1 -1
  28. package/dist/container-loader-alpha.d.ts +274 -0
  29. package/dist/container-loader-beta.d.ts +75 -0
  30. package/dist/container-loader-public.d.ts +75 -0
  31. package/dist/container-loader-untrimmed.d.ts +331 -0
  32. package/dist/container.cjs +1585 -0
  33. package/dist/container.cjs.map +1 -0
  34. package/dist/container.d.ts +227 -83
  35. package/dist/container.d.ts.map +1 -1
  36. package/dist/containerContext.cjs +74 -0
  37. package/dist/containerContext.cjs.map +1 -0
  38. package/dist/containerContext.d.ts +33 -59
  39. package/dist/containerContext.d.ts.map +1 -1
  40. package/dist/containerStorageAdapter.cjs +234 -0
  41. package/dist/containerStorageAdapter.cjs.map +1 -0
  42. package/dist/containerStorageAdapter.d.ts +48 -23
  43. package/dist/containerStorageAdapter.d.ts.map +1 -1
  44. package/dist/{contracts.js → contracts.cjs} +5 -5
  45. package/dist/contracts.cjs.map +1 -0
  46. package/dist/contracts.d.ts +45 -17
  47. package/dist/contracts.d.ts.map +1 -1
  48. package/dist/debugLogger.cjs +101 -0
  49. package/dist/debugLogger.cjs.map +1 -0
  50. package/dist/debugLogger.d.ts +30 -0
  51. package/dist/debugLogger.d.ts.map +1 -0
  52. package/dist/{deltaManager.js → deltaManager.cjs} +379 -186
  53. package/dist/deltaManager.cjs.map +1 -0
  54. package/dist/deltaManager.d.ts +54 -18
  55. package/dist/deltaManager.d.ts.map +1 -1
  56. package/dist/{deltaQueue.js → deltaQueue.cjs} +29 -28
  57. package/dist/deltaQueue.cjs.map +1 -0
  58. package/dist/deltaQueue.d.ts +3 -4
  59. package/dist/deltaQueue.d.ts.map +1 -1
  60. package/dist/disposal.cjs +25 -0
  61. package/dist/disposal.cjs.map +1 -0
  62. package/dist/disposal.d.ts +13 -0
  63. package/dist/disposal.d.ts.map +1 -0
  64. package/dist/error.cjs +32 -0
  65. package/dist/error.cjs.map +1 -0
  66. package/dist/error.d.ts +23 -0
  67. package/dist/error.d.ts.map +1 -0
  68. package/dist/index.cjs +19 -0
  69. package/dist/index.cjs.map +1 -0
  70. package/dist/index.d.ts +5 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/loader.cjs +148 -0
  73. package/dist/loader.cjs.map +1 -0
  74. package/dist/loader.d.ts +38 -19
  75. package/dist/loader.d.ts.map +1 -1
  76. package/dist/location-redirection-utilities/index.cjs +11 -0
  77. package/dist/location-redirection-utilities/index.cjs.map +1 -0
  78. package/dist/location-redirection-utilities/index.d.ts +6 -0
  79. package/dist/location-redirection-utilities/index.d.ts.map +1 -0
  80. package/dist/location-redirection-utilities/resolveWithLocationRedirection.cjs +53 -0
  81. package/dist/location-redirection-utilities/resolveWithLocationRedirection.cjs.map +1 -0
  82. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts +24 -0
  83. package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -0
  84. package/dist/{collabWindowTracker.js → noopHeuristic.cjs} +37 -39
  85. package/dist/noopHeuristic.cjs.map +1 -0
  86. package/dist/noopHeuristic.d.ts +23 -0
  87. package/dist/noopHeuristic.d.ts.map +1 -0
  88. package/dist/{packageVersion.js → packageVersion.cjs} +2 -2
  89. package/dist/packageVersion.cjs.map +1 -0
  90. package/dist/packageVersion.d.ts +1 -1
  91. package/dist/packageVersion.d.ts.map +1 -1
  92. package/dist/protocol.cjs +99 -0
  93. package/dist/protocol.cjs.map +1 -0
  94. package/dist/protocol.d.ts +38 -0
  95. package/dist/protocol.d.ts.map +1 -0
  96. package/dist/{protocolTreeDocumentStorageService.js → protocolTreeDocumentStorageService.cjs} +8 -5
  97. package/dist/protocolTreeDocumentStorageService.cjs.map +1 -0
  98. package/dist/protocolTreeDocumentStorageService.d.ts +8 -4
  99. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  100. package/dist/quorum.cjs +16 -0
  101. package/dist/quorum.cjs.map +1 -0
  102. package/dist/quorum.d.ts +1 -14
  103. package/dist/quorum.d.ts.map +1 -1
  104. package/dist/{retriableDocumentStorageService.js → retriableDocumentStorageService.cjs} +36 -21
  105. package/dist/retriableDocumentStorageService.cjs.map +1 -0
  106. package/dist/retriableDocumentStorageService.d.ts +7 -5
  107. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  108. package/dist/tsdoc-metadata.json +11 -0
  109. package/dist/{utils.js → utils.cjs} +52 -14
  110. package/dist/utils.cjs.map +1 -0
  111. package/dist/utils.d.ts +34 -1
  112. package/dist/utils.d.ts.map +1 -1
  113. package/lib/{audience.d.ts → audience.d.mts} +4 -10
  114. package/lib/audience.d.mts.map +1 -0
  115. package/lib/{audience.js → audience.mjs} +15 -17
  116. package/lib/audience.mjs.map +1 -0
  117. package/lib/catchUpMonitor.d.mts +29 -0
  118. package/lib/catchUpMonitor.d.mts.map +1 -0
  119. package/lib/catchUpMonitor.mjs +39 -0
  120. package/lib/catchUpMonitor.mjs.map +1 -0
  121. package/lib/{connectionManager.d.ts → connectionManager.d.mts} +23 -33
  122. package/lib/connectionManager.d.mts.map +1 -0
  123. package/lib/{connectionManager.js → connectionManager.mjs} +378 -218
  124. package/lib/connectionManager.mjs.map +1 -0
  125. package/lib/{connectionState.d.ts → connectionState.d.mts} +3 -5
  126. package/lib/connectionState.d.mts.map +1 -0
  127. package/lib/{connectionState.js → connectionState.mjs} +4 -6
  128. package/lib/connectionState.mjs.map +1 -0
  129. package/lib/connectionStateHandler.d.mts +179 -0
  130. package/lib/connectionStateHandler.d.mts.map +1 -0
  131. package/lib/connectionStateHandler.mjs +469 -0
  132. package/lib/connectionStateHandler.mjs.map +1 -0
  133. package/lib/container-loader-alpha.d.mts +274 -0
  134. package/lib/container-loader-beta.d.mts +75 -0
  135. package/lib/container-loader-public.d.mts +75 -0
  136. package/lib/container-loader-untrimmed.d.mts +331 -0
  137. package/lib/container.d.mts +382 -0
  138. package/lib/container.d.mts.map +1 -0
  139. package/lib/container.mjs +1579 -0
  140. package/lib/container.mjs.map +1 -0
  141. package/lib/containerContext.d.mts +58 -0
  142. package/lib/containerContext.d.mts.map +1 -0
  143. package/lib/containerContext.mjs +70 -0
  144. package/lib/containerContext.mjs.map +1 -0
  145. package/lib/containerStorageAdapter.d.mts +73 -0
  146. package/lib/containerStorageAdapter.d.mts.map +1 -0
  147. package/lib/containerStorageAdapter.mjs +228 -0
  148. package/lib/containerStorageAdapter.mjs.map +1 -0
  149. package/lib/{contracts.d.ts → contracts.d.mts} +45 -17
  150. package/lib/contracts.d.mts.map +1 -0
  151. package/lib/{contracts.js → contracts.mjs} +4 -4
  152. package/lib/contracts.mjs.map +1 -0
  153. package/lib/debugLogger.d.mts +30 -0
  154. package/lib/debugLogger.d.mts.map +1 -0
  155. package/lib/debugLogger.mjs +93 -0
  156. package/lib/debugLogger.mjs.map +1 -0
  157. package/lib/{deltaManager.d.ts → deltaManager.d.mts} +54 -18
  158. package/lib/deltaManager.d.mts.map +1 -0
  159. package/lib/{deltaManager.js → deltaManager.mjs} +361 -165
  160. package/lib/deltaManager.mjs.map +1 -0
  161. package/lib/{deltaQueue.d.ts → deltaQueue.d.mts} +3 -4
  162. package/lib/deltaQueue.d.mts.map +1 -0
  163. package/lib/{deltaQueue.js → deltaQueue.mjs} +25 -24
  164. package/lib/deltaQueue.mjs.map +1 -0
  165. package/lib/disposal.d.mts +13 -0
  166. package/lib/disposal.d.mts.map +1 -0
  167. package/lib/disposal.mjs +21 -0
  168. package/lib/disposal.mjs.map +1 -0
  169. package/lib/error.d.mts +23 -0
  170. package/lib/error.d.mts.map +1 -0
  171. package/lib/error.mjs +28 -0
  172. package/lib/error.mjs.map +1 -0
  173. package/lib/index.d.mts +11 -0
  174. package/lib/index.d.mts.map +1 -0
  175. package/lib/index.mjs +10 -0
  176. package/lib/index.mjs.map +1 -0
  177. package/lib/{loader.d.ts → loader.d.mts} +39 -20
  178. package/lib/loader.d.mts.map +1 -0
  179. package/lib/loader.mjs +143 -0
  180. package/lib/loader.mjs.map +1 -0
  181. package/lib/location-redirection-utilities/index.d.mts +6 -0
  182. package/lib/location-redirection-utilities/index.d.mts.map +1 -0
  183. package/lib/location-redirection-utilities/index.mjs +6 -0
  184. package/lib/location-redirection-utilities/index.mjs.map +1 -0
  185. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.mts +24 -0
  186. package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.mts.map +1 -0
  187. package/lib/location-redirection-utilities/resolveWithLocationRedirection.mjs +48 -0
  188. package/lib/location-redirection-utilities/resolveWithLocationRedirection.mjs.map +1 -0
  189. package/lib/noopHeuristic.d.mts +23 -0
  190. package/lib/noopHeuristic.d.mts.map +1 -0
  191. package/lib/{collabWindowTracker.js → noopHeuristic.mjs} +33 -35
  192. package/lib/noopHeuristic.mjs.map +1 -0
  193. package/lib/{packageVersion.d.ts → packageVersion.d.mts} +1 -1
  194. package/lib/{packageVersion.d.ts.map → packageVersion.d.mts.map} +1 -1
  195. package/lib/{packageVersion.js → packageVersion.mjs} +2 -2
  196. package/lib/packageVersion.mjs.map +1 -0
  197. package/lib/protocol.d.mts +38 -0
  198. package/lib/protocol.d.mts.map +1 -0
  199. package/lib/protocol.mjs +94 -0
  200. package/lib/protocol.mjs.map +1 -0
  201. package/lib/{protocolTreeDocumentStorageService.d.ts → protocolTreeDocumentStorageService.d.mts} +8 -4
  202. package/lib/protocolTreeDocumentStorageService.d.mts.map +1 -0
  203. package/lib/{protocolTreeDocumentStorageService.js → protocolTreeDocumentStorageService.mjs} +8 -5
  204. package/lib/protocolTreeDocumentStorageService.mjs.map +1 -0
  205. package/lib/quorum.d.mts +4 -0
  206. package/lib/quorum.d.mts.map +1 -0
  207. package/lib/quorum.mjs +12 -0
  208. package/lib/quorum.mjs.map +1 -0
  209. package/lib/{retriableDocumentStorageService.d.ts → retriableDocumentStorageService.d.mts} +7 -5
  210. package/lib/retriableDocumentStorageService.d.mts.map +1 -0
  211. package/lib/{retriableDocumentStorageService.js → retriableDocumentStorageService.mjs} +35 -20
  212. package/lib/retriableDocumentStorageService.mjs.map +1 -0
  213. package/lib/utils.d.mts +67 -0
  214. package/lib/utils.d.mts.map +1 -0
  215. package/lib/{utils.js → utils.mjs} +47 -11
  216. package/lib/utils.mjs.map +1 -0
  217. package/package.json +163 -70
  218. package/prettier.config.cjs +8 -0
  219. package/src/audience.ts +59 -49
  220. package/src/catchUpMonitor.ts +61 -0
  221. package/src/connectionManager.ts +1154 -910
  222. package/src/connectionState.ts +22 -25
  223. package/src/connectionStateHandler.ts +689 -319
  224. package/src/container.ts +2476 -1792
  225. package/src/containerContext.ts +98 -330
  226. package/src/containerStorageAdapter.ts +301 -105
  227. package/src/contracts.ts +184 -146
  228. package/src/debugLogger.ts +123 -0
  229. package/src/deltaManager.ts +1165 -900
  230. package/src/deltaQueue.ts +156 -152
  231. package/src/disposal.ts +25 -0
  232. package/src/error.ts +44 -0
  233. package/src/index.ts +14 -15
  234. package/src/loader.ts +356 -427
  235. package/src/location-redirection-utilities/index.ts +9 -0
  236. package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +61 -0
  237. package/src/noopHeuristic.ts +107 -0
  238. package/src/packageVersion.ts +1 -1
  239. package/src/protocol.ts +150 -0
  240. package/src/protocolTreeDocumentStorageService.ts +35 -35
  241. package/src/quorum.ts +11 -50
  242. package/src/retriableDocumentStorageService.ts +135 -95
  243. package/src/utils.ts +159 -86
  244. package/tsc-multi.test.json +4 -0
  245. package/tsconfig.json +10 -12
  246. package/dist/audience.js.map +0 -1
  247. package/dist/collabWindowTracker.d.ts +0 -19
  248. package/dist/collabWindowTracker.d.ts.map +0 -1
  249. package/dist/collabWindowTracker.js.map +0 -1
  250. package/dist/connectionManager.js.map +0 -1
  251. package/dist/connectionState.js.map +0 -1
  252. package/dist/connectionStateHandler.js +0 -280
  253. package/dist/connectionStateHandler.js.map +0 -1
  254. package/dist/container.js +0 -1284
  255. package/dist/container.js.map +0 -1
  256. package/dist/containerContext.js +0 -217
  257. package/dist/containerContext.js.map +0 -1
  258. package/dist/containerStorageAdapter.js +0 -104
  259. package/dist/containerStorageAdapter.js.map +0 -1
  260. package/dist/contracts.js.map +0 -1
  261. package/dist/deltaManager.js.map +0 -1
  262. package/dist/deltaManagerProxy.d.ts +0 -54
  263. package/dist/deltaManagerProxy.d.ts.map +0 -1
  264. package/dist/deltaManagerProxy.js +0 -115
  265. package/dist/deltaManagerProxy.js.map +0 -1
  266. package/dist/deltaQueue.js.map +0 -1
  267. package/dist/index.js +0 -16
  268. package/dist/index.js.map +0 -1
  269. package/dist/loader.js +0 -241
  270. package/dist/loader.js.map +0 -1
  271. package/dist/packageVersion.js.map +0 -1
  272. package/dist/protocolTreeDocumentStorageService.js.map +0 -1
  273. package/dist/quorum.js +0 -44
  274. package/dist/quorum.js.map +0 -1
  275. package/dist/retriableDocumentStorageService.js.map +0 -1
  276. package/dist/utils.js.map +0 -1
  277. package/lib/audience.d.ts.map +0 -1
  278. package/lib/audience.js.map +0 -1
  279. package/lib/collabWindowTracker.d.ts +0 -19
  280. package/lib/collabWindowTracker.d.ts.map +0 -1
  281. package/lib/collabWindowTracker.js.map +0 -1
  282. package/lib/connectionManager.d.ts.map +0 -1
  283. package/lib/connectionManager.js.map +0 -1
  284. package/lib/connectionState.d.ts.map +0 -1
  285. package/lib/connectionState.js.map +0 -1
  286. package/lib/connectionStateHandler.d.ts +0 -81
  287. package/lib/connectionStateHandler.d.ts.map +0 -1
  288. package/lib/connectionStateHandler.js +0 -276
  289. package/lib/connectionStateHandler.js.map +0 -1
  290. package/lib/container.d.ts +0 -238
  291. package/lib/container.d.ts.map +0 -1
  292. package/lib/container.js +0 -1276
  293. package/lib/container.js.map +0 -1
  294. package/lib/containerContext.d.ts +0 -84
  295. package/lib/containerContext.d.ts.map +0 -1
  296. package/lib/containerContext.js +0 -213
  297. package/lib/containerContext.js.map +0 -1
  298. package/lib/containerStorageAdapter.d.ts +0 -48
  299. package/lib/containerStorageAdapter.d.ts.map +0 -1
  300. package/lib/containerStorageAdapter.js +0 -99
  301. package/lib/containerStorageAdapter.js.map +0 -1
  302. package/lib/contracts.d.ts.map +0 -1
  303. package/lib/contracts.js.map +0 -1
  304. package/lib/deltaManager.d.ts.map +0 -1
  305. package/lib/deltaManager.js.map +0 -1
  306. package/lib/deltaManagerProxy.d.ts +0 -54
  307. package/lib/deltaManagerProxy.d.ts.map +0 -1
  308. package/lib/deltaManagerProxy.js +0 -110
  309. package/lib/deltaManagerProxy.js.map +0 -1
  310. package/lib/deltaQueue.d.ts.map +0 -1
  311. package/lib/deltaQueue.js.map +0 -1
  312. package/lib/index.d.ts +0 -8
  313. package/lib/index.d.ts.map +0 -1
  314. package/lib/index.js +0 -8
  315. package/lib/index.js.map +0 -1
  316. package/lib/loader.d.ts.map +0 -1
  317. package/lib/loader.js +0 -236
  318. package/lib/loader.js.map +0 -1
  319. package/lib/packageVersion.js.map +0 -1
  320. package/lib/protocolTreeDocumentStorageService.d.ts.map +0 -1
  321. package/lib/protocolTreeDocumentStorageService.js.map +0 -1
  322. package/lib/quorum.d.ts +0 -21
  323. package/lib/quorum.d.ts.map +0 -1
  324. package/lib/quorum.js +0 -38
  325. package/lib/quorum.js.map +0 -1
  326. package/lib/retriableDocumentStorageService.d.ts.map +0 -1
  327. package/lib/retriableDocumentStorageService.js.map +0 -1
  328. package/lib/utils.d.ts +0 -34
  329. package/lib/utils.d.ts.map +0 -1
  330. package/lib/utils.js.map +0 -1
  331. package/src/collabWindowTracker.ts +0 -102
  332. package/src/deltaManagerProxy.ts +0 -158
  333. package/tsconfig.esnext.json +0 -7
@@ -2,18 +2,21 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { default as AbortController } from "abort-controller";
6
- import { assert, performance, TypedEventEmitter } from "@fluidframework/common-utils";
7
- import { GenericError, UsageError } from "@fluidframework/container-utils";
8
- import { canRetryOnError, createWriteError, createGenericNetworkError, getRetryDelayFromError, waitForConnectedState, DeltaStreamConnectionForbiddenError, logNetworkFailure,
9
- // isRuntimeMessage,
10
- } from "@fluidframework/driver-utils";
5
+ import { LogLevel } from "@fluidframework/core-interfaces";
6
+ import { assert } from "@fluidframework/core-utils";
7
+ import { performance, TypedEventEmitter } from "@fluid-internal/client-utils";
8
+ import {
9
+ // eslint-disable-next-line import/no-deprecated
10
+ DriverErrorType, } from "@fluidframework/driver-definitions";
11
+ import { canRetryOnError, createWriteError, createGenericNetworkError, getRetryDelayFromError, logNetworkFailure, isRuntimeMessage, calculateMaxWaitTime, } from "@fluidframework/driver-utils";
11
12
  import { MessageType, ScopeType, } from "@fluidframework/protocol-definitions";
12
- import { TelemetryLogger, normalizeError, } from "@fluidframework/telemetry-utils";
13
- import { ReconnectMode, } from "./contracts";
14
- import { DeltaQueue } from "./deltaQueue";
15
- const MaxReconnectDelayInMs = 8000;
16
- const InitialReconnectDelayInMs = 1000;
13
+ import { formatTick, GenericError, isFluidError, normalizeError, UsageError, } from "@fluidframework/telemetry-utils";
14
+ import { ReconnectMode, } from "./contracts.mjs";
15
+ import { DeltaQueue } from "./deltaQueue.mjs";
16
+ import { SignalType } from "./protocol.mjs";
17
+ import { isDeltaStreamConnectionForbiddenError } from "./utils.mjs";
18
+ // We double this value in first try in when we calculate time to wait for in "calculateMaxWaitTime" function.
19
+ const InitialReconnectDelayInMs = 500;
17
20
  const DefaultChunkSize = 16 * 1024;
18
21
  const fatalConnectErrorProp = { fatalConnectError: true };
19
22
  function getNackReconnectInfo(nackContent) {
@@ -26,10 +29,25 @@ function getNackReconnectInfo(nackContent) {
26
29
  * Implementation of IDocumentDeltaConnection that does not support submitting
27
30
  * or receiving ops. Used in storage-only mode.
28
31
  */
32
+ const clientNoDeltaStream = {
33
+ mode: "read",
34
+ details: { capabilities: { interactive: true } },
35
+ permission: [],
36
+ user: { id: "storage-only client" },
37
+ scopes: [],
38
+ };
39
+ const clientIdNoDeltaStream = "storage-only client";
29
40
  class NoDeltaStream extends TypedEventEmitter {
30
- constructor() {
31
- super(...arguments);
32
- this.clientId = "storage-only client";
41
+ /**
42
+ * Connection which is not connected to socket.
43
+ * @param storageOnlyReason - Reason on why the connection to delta stream is not allowed.
44
+ * @param readonlyConnectionReason - reason/error if any which lead to using NoDeltaStream.
45
+ */
46
+ constructor(storageOnlyReason, readonlyConnectionReason) {
47
+ super();
48
+ this.storageOnlyReason = storageOnlyReason;
49
+ this.readonlyConnectionReason = readonlyConnectionReason;
50
+ this.clientId = clientIdNoDeltaStream;
33
51
  this.claims = {
34
52
  scopes: [ScopeType.DocRead],
35
53
  };
@@ -39,11 +57,12 @@ class NoDeltaStream extends TypedEventEmitter {
39
57
  this.version = "";
40
58
  this.initialMessages = [];
41
59
  this.initialSignals = [];
42
- this.initialClients = [];
60
+ this.initialClients = [
61
+ { client: clientNoDeltaStream, clientId: clientIdNoDeltaStream },
62
+ ];
43
63
  this.serviceConfiguration = {
44
64
  maxMessageSize: 0,
45
65
  blockSize: 0,
46
- summary: undefined,
47
66
  };
48
67
  this.checkpointSequenceNumber = undefined;
49
68
  this._disposed = false;
@@ -62,85 +81,49 @@ class NoDeltaStream extends TypedEventEmitter {
62
81
  content: { message: "Cannot submit signal with storage-only connection", code: 403 },
63
82
  });
64
83
  }
65
- get disposed() { return this._disposed; }
66
- dispose() { this._disposed = true; }
84
+ get disposed() {
85
+ return this._disposed;
86
+ }
87
+ dispose() {
88
+ this._disposed = true;
89
+ }
90
+ }
91
+ function isNoDeltaStreamConnection(connection) {
92
+ return connection instanceof NoDeltaStream;
67
93
  }
94
+ const waitForOnline = async () => {
95
+ // Only wait if we have a strong signal that we're offline - otherwise assume we're online.
96
+ if (globalThis.navigator?.onLine === false && globalThis.addEventListener !== undefined) {
97
+ return new Promise((resolve) => {
98
+ const resolveAndRemoveListener = () => {
99
+ resolve();
100
+ globalThis.removeEventListener("online", resolveAndRemoveListener);
101
+ };
102
+ globalThis.addEventListener("online", resolveAndRemoveListener);
103
+ });
104
+ }
105
+ };
68
106
  /**
69
107
  * Implementation of IConnectionManager, used by Container class
70
- * Implements constant connectivity to relay service, by reconnecting in case of loast connection or error.
71
- * Exposes various controls to influecen this process, including manual reconnects, forced read-only mode, etc.
108
+ * Implements constant connectivity to relay service, by reconnecting in case of lost connection or error.
109
+ * Exposes various controls to influence this process, including manual reconnects, forced read-only mode, etc.
72
110
  */
73
111
  export class ConnectionManager {
74
- constructor(serviceProvider, client, reconnectAllowed, logger, props) {
75
- this.serviceProvider = serviceProvider;
76
- this.client = client;
77
- this.logger = logger;
78
- this.props = props;
79
- /** tracks host requiring read-only mode. */
80
- this._forceReadonly = false;
81
- /** True if there is pending (async) reconnection from "read" to "write" */
82
- this.pendingReconnect = false;
83
- this.clientSequenceNumber = 0;
84
- this.clientSequenceNumberObserved = 0;
85
- /** Counts the number of noops sent by the client which may not be acked. */
86
- this.trailingNoopCount = 0;
87
- this.connectFirstConnection = true;
88
- this._connectionVerboseProps = {};
89
- this._connectionProps = {};
90
- this.closed = false;
91
- this.opHandler = (documentId, messagesArg) => {
92
- const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
93
- this.props.incomingOpHandler(messages, "opHandler");
94
- };
95
- // Always connect in write mode after getting nacked.
96
- this.nackHandler = (documentId, messages) => {
97
- const message = messages[0];
98
- if (this._readonlyPermissions === true) {
99
- this.props.closeHandler(createWriteError("writeOnReadOnlyDocument", { driverVersion: undefined }));
100
- return;
101
- }
102
- const reconnectInfo = getNackReconnectInfo(message.content);
103
- // If the nack indicates we cannot retry, then close the container outright
104
- if (!reconnectInfo.canRetry) {
105
- this.props.closeHandler(reconnectInfo);
106
- return;
107
- }
108
- this.reconnectOnError("write", reconnectInfo);
109
- };
110
- // Connection mode is always read on disconnect/error unless the system mode was write.
111
- this.disconnectHandlerInternal = (disconnectReason) => {
112
- // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
113
- // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
114
- this.reconnectOnError(this.defaultReconnectionMode, disconnectReason);
115
- };
116
- this.errorHandler = (error) => {
117
- this.reconnectOnError(this.defaultReconnectionMode, error);
118
- };
119
- this.clientDetails = this.client.details;
120
- this.defaultReconnectionMode = this.client.mode;
121
- this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
122
- // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
123
- // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
124
- this._outbound = new DeltaQueue((messages) => {
125
- if (this.connection === undefined) {
126
- throw new Error("Attempted to submit an outbound message without connection");
127
- }
128
- this.connection.submit(messages);
129
- });
130
- this._outbound.on("error", (error) => {
131
- this.props.closeHandler(normalizeError(error));
132
- });
112
+ get connectionVerboseProps() {
113
+ return this._connectionVerboseProps;
133
114
  }
134
- get connectionVerboseProps() { return this._connectionVerboseProps; }
135
115
  /**
136
116
  * The current connection mode, initially read.
137
117
  */
138
118
  get connectionMode() {
139
- var _a, _b;
140
- return (_b = (_a = this.connection) === null || _a === void 0 ? void 0 : _a.mode) !== null && _b !== void 0 ? _b : "read";
119
+ return this.connection?.mode ?? "read";
120
+ }
121
+ get connected() {
122
+ return this.connection !== undefined;
123
+ }
124
+ get clientId() {
125
+ return this.connection?.clientId;
141
126
  }
142
- get connected() { return this.connection !== undefined; }
143
- get clientId() { var _a; return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId; }
144
127
  /**
145
128
  * Automatic reconnecting enabled or disabled.
146
129
  * If set to Never, then reconnecting will never be allowed.
@@ -149,8 +132,7 @@ export class ConnectionManager {
149
132
  return this._reconnectMode;
150
133
  }
151
134
  get maxMessageSize() {
152
- var _a, _b, _c;
153
- return (_c = (_b = (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration) === null || _b === void 0 ? void 0 : _b.maxMessageSize) !== null && _c !== void 0 ? _c : DefaultChunkSize;
135
+ return this.connection?.serviceConfiguration?.maxMessageSize ?? DefaultChunkSize;
154
136
  }
155
137
  get version() {
156
138
  if (this.connection === undefined) {
@@ -159,12 +141,10 @@ export class ConnectionManager {
159
141
  return this.connection.version;
160
142
  }
161
143
  get serviceConfiguration() {
162
- var _a;
163
- return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.serviceConfiguration;
144
+ return this.connection?.serviceConfiguration;
164
145
  }
165
146
  get scopes() {
166
- var _a;
167
- return (_a = this.connection) === null || _a === void 0 ? void 0 : _a.claims.scopes;
147
+ return this.connection?.claims.scopes;
168
148
  }
169
149
  get outbound() {
170
150
  return this._outbound;
@@ -172,20 +152,31 @@ export class ConnectionManager {
172
152
  /**
173
153
  * Returns set of props that can be logged in telemetry that provide some insights / statistics
174
154
  * about current or last connection (if there is no connection at the moment)
175
- */
155
+ */
176
156
  get connectionProps() {
177
- if (this.connection !== undefined) {
178
- return this._connectionProps;
179
- }
180
- else {
181
- return Object.assign(Object.assign({}, this._connectionProps), {
157
+ return this.connection !== undefined
158
+ ? this._connectionProps
159
+ : {
160
+ ...this._connectionProps,
182
161
  // Report how many ops this client sent in last disconnected session
183
- sentOps: this.clientSequenceNumber });
184
- }
162
+ sentOps: this.clientSequenceNumber,
163
+ };
185
164
  }
186
165
  shouldJoinWrite() {
187
166
  // We don't have to wait for ack for topmost NoOps. So subtract those.
188
- return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.trailingNoopCount);
167
+ const outstandingOps = this.clientSequenceNumberObserved < this.clientSequenceNumber - this.localOpsToIgnore;
168
+ // Previous behavior was to force write mode here only when there are outstanding ops (besides
169
+ // no-ops). The dirty signal from runtime should provide the same behavior, but also support
170
+ // stashed ops that weren't submitted to container layer yet. For safety, we want to retain the
171
+ // same behavior whenever dirty is false.
172
+ const isDirty = this.containerDirty();
173
+ if (outstandingOps !== isDirty) {
174
+ this.logger.sendTelemetryEvent({
175
+ eventName: "DesiredConnectionModeMismatch",
176
+ details: JSON.stringify({ outstandingOps, isDirty }),
177
+ });
178
+ }
179
+ return outstandingOps || isDirty;
189
180
  }
190
181
  /**
191
182
  * Tells if container is in read-only mode.
@@ -197,82 +188,141 @@ export class ConnectionManager {
197
188
  * and do not know if user has write access to a file.
198
189
  */
199
190
  get readonly() {
200
- if (this._forceReadonly) {
201
- return true;
202
- }
203
- return this._readonlyPermissions;
191
+ return this.readOnlyInfo.readonly;
204
192
  }
205
193
  get readOnlyInfo() {
206
- const storageOnly = this.connection !== undefined && this.connection instanceof NoDeltaStream;
194
+ let storageOnly = false;
195
+ let storageOnlyReason;
196
+ if (isNoDeltaStreamConnection(this.connection)) {
197
+ storageOnly = true;
198
+ storageOnlyReason = this.connection.storageOnlyReason;
199
+ }
207
200
  if (storageOnly || this._forceReadonly || this._readonlyPermissions === true) {
208
201
  return {
209
202
  readonly: true,
210
203
  forced: this._forceReadonly,
211
204
  permissions: this._readonlyPermissions,
212
205
  storageOnly,
206
+ storageOnlyReason,
213
207
  };
214
208
  }
215
209
  return { readonly: this._readonlyPermissions };
216
210
  }
217
- static detailsFromConnection(connection) {
211
+ static detailsFromConnection(connection, reason) {
218
212
  return {
219
213
  claims: connection.claims,
220
214
  clientId: connection.clientId,
221
- existing: connection.existing,
222
215
  checkpointSequenceNumber: connection.checkpointSequenceNumber,
223
- get initialClients() { return connection.initialClients; },
216
+ get initialClients() {
217
+ return connection.initialClients;
218
+ },
224
219
  mode: connection.mode,
225
220
  serviceConfiguration: connection.serviceConfiguration,
226
221
  version: connection.version,
222
+ reason,
227
223
  };
228
224
  }
229
- dispose(error) {
230
- if (this.closed) {
225
+ constructor(serviceProvider, containerDirty, client, reconnectAllowed, logger, props) {
226
+ this.serviceProvider = serviceProvider;
227
+ this.containerDirty = containerDirty;
228
+ this.client = client;
229
+ this.logger = logger;
230
+ this.props = props;
231
+ /** tracks host requiring read-only mode. */
232
+ this._forceReadonly = false;
233
+ /** True if there is pending (async) reconnection from "read" to "write" */
234
+ this.pendingReconnect = false;
235
+ this.clientSequenceNumber = 0;
236
+ this.clientSequenceNumberObserved = 0;
237
+ /** Counts the number of non-runtime ops sent by the client which may not be acked. */
238
+ this.localOpsToIgnore = 0;
239
+ this.connectFirstConnection = true;
240
+ this._connectionVerboseProps = {};
241
+ this._connectionProps = {};
242
+ this._disposed = false;
243
+ this.opHandler = (documentId, messagesArg) => {
244
+ const messages = Array.isArray(messagesArg) ? messagesArg : [messagesArg];
245
+ this.props.incomingOpHandler(messages, "opHandler");
246
+ };
247
+ this.signalHandler = (signalsArg) => {
248
+ const signals = Array.isArray(signalsArg) ? signalsArg : [signalsArg];
249
+ this.props.signalHandler(signals);
250
+ };
251
+ // Always connect in write mode after getting nacked.
252
+ this.nackHandler = (documentId, messages) => {
253
+ const message = messages[0];
254
+ if (this._readonlyPermissions === true) {
255
+ this.props.closeHandler(createWriteError("writeOnReadOnlyDocument", { driverVersion: undefined }));
256
+ return;
257
+ }
258
+ const reconnectInfo = getNackReconnectInfo(message.content);
259
+ // If the nack indicates we cannot retry, then close the container outright
260
+ if (!reconnectInfo.canRetry) {
261
+ this.props.closeHandler(reconnectInfo);
262
+ return;
263
+ }
264
+ this.reconnectOnError("write", reconnectInfo);
265
+ };
266
+ // Connection mode is always read on disconnect/error unless the system mode was write.
267
+ this.disconnectHandlerInternal = (disconnectReason) => {
268
+ // Note: we might get multiple disconnect calls on same socket, as early disconnect notification
269
+ // ("server_disconnect", ODSP-specific) is mapped to "disconnect"
270
+ this.reconnectOnError(this.defaultReconnectionMode, disconnectReason);
271
+ };
272
+ this.errorHandler = (error) => {
273
+ this.reconnectOnError(this.defaultReconnectionMode, error);
274
+ };
275
+ this.clientDetails = this.client.details;
276
+ this.defaultReconnectionMode = this.client.mode;
277
+ this._reconnectMode = reconnectAllowed ? ReconnectMode.Enabled : ReconnectMode.Never;
278
+ // Outbound message queue. The outbound queue is represented as a queue of an array of ops. Ops contained
279
+ // within an array *must* fit within the maxMessageSize and are guaranteed to be ordered sequentially.
280
+ this._outbound = new DeltaQueue((messages) => {
281
+ if (this.connection === undefined) {
282
+ throw new Error("Attempted to submit an outbound message without connection");
283
+ }
284
+ this.connection.submit(messages);
285
+ });
286
+ this._outbound.on("error", (error) => {
287
+ this.props.closeHandler(normalizeError(error));
288
+ });
289
+ }
290
+ dispose(error, switchToReadonly = true) {
291
+ if (this._disposed) {
231
292
  return;
232
293
  }
233
- this.closed = true;
234
- this.pendingConnection = undefined;
294
+ this._disposed = true;
235
295
  // Ensure that things like triggerConnect() will short circuit
236
296
  this._reconnectMode = ReconnectMode.Never;
237
297
  this._outbound.clear();
238
- const disconnectReason = error !== undefined
239
- ? `Closing DeltaManager (${error.message})`
240
- : "Closing DeltaManager";
298
+ const disconnectReason = {
299
+ text: "Closing DeltaManager",
300
+ error,
301
+ };
302
+ const oldReadonlyValue = this.readonly;
241
303
  // This raises "disconnect" event if we have active connection.
242
304
  this.disconnectFromDeltaStream(disconnectReason);
243
- // Notify everyone we are in read-only state.
244
- // Useful for data stores in case we hit some critical error,
245
- // to switch to a mode where user edits are not accepted
246
- this.set_readonlyPermissions(true);
305
+ if (switchToReadonly) {
306
+ // Notify everyone we are in read-only state.
307
+ // Useful for data stores in case we hit some critical error,
308
+ // to switch to a mode where user edits are not accepted
309
+ this.set_readonlyPermissions(true, oldReadonlyValue, disconnectReason);
310
+ }
247
311
  }
248
312
  /**
249
313
  * Enables or disables automatic reconnecting.
250
314
  * Will throw an error if reconnectMode set to Never.
251
- */
252
- setAutoReconnect(mode) {
315
+ */
316
+ setAutoReconnect(mode, reason) {
253
317
  assert(mode !== ReconnectMode.Never && this._reconnectMode !== ReconnectMode.Never, 0x278 /* "API is not supported for non-connecting or closed container" */);
254
318
  this._reconnectMode = mode;
255
319
  if (mode !== ReconnectMode.Enabled) {
256
320
  // immediately disconnect - do not rely on service eventually dropping connection.
257
- this.disconnectFromDeltaStream("setAutoReconnect");
321
+ this.disconnectFromDeltaStream(reason);
258
322
  }
259
323
  }
260
324
  /**
261
- * Sends signal to runtime (and data stores) to be read-only.
262
- * Hosts may have read only views, indicating to data stores that no edits are allowed.
263
- * This is independent from this._readonlyPermissions (permissions) and this.connectionMode
264
- * (server can return "write" mode even when asked for "read")
265
- * Leveraging same "readonly" event as runtime & data stores should behave the same in such case
266
- * as in read-only permissions.
267
- * But this.active can be used by some DDSes to figure out if ops can be sent
268
- * (for example, read-only view still participates in code proposals / upgrades decisions)
269
- *
270
- * Forcing Readonly does not prevent DDS from generating ops. It is up to user code to honour
271
- * the readonly flag. If ops are generated, they will accumulate locally and not be sent. If
272
- * there are pending in the outbound queue, it will stop sending until force readonly is
273
- * cleared.
274
- *
275
- * @param readonly - set or clear force readonly.
325
+ * {@inheritDoc Container.forceReadonly}
276
326
  */
277
327
  forceReadonly(readonly) {
278
328
  if (readonly !== this._forceReadonly) {
@@ -297,42 +347,40 @@ export class ConnectionManager {
297
347
  // host logic error.
298
348
  this.logger.sendErrorEvent({ eventName: "ForceReadonlyPendingChanged" });
299
349
  }
300
- reconnect = this.disconnectFromDeltaStream("Force readonly");
350
+ reconnect = this.disconnectFromDeltaStream({ text: "Force readonly" });
301
351
  }
302
352
  this.props.readonlyChangeHandler(this.readonly);
303
353
  if (reconnect) {
304
354
  // reconnect if we disconnected from before.
305
- this.triggerConnect("read");
355
+ this.triggerConnect({ text: "Force Readonly" }, "read");
306
356
  }
307
357
  }
308
358
  }
309
- set_readonlyPermissions(readonly) {
310
- const oldValue = this.readonly;
311
- this._readonlyPermissions = readonly;
312
- if (oldValue !== this.readonly) {
313
- this.props.readonlyChangeHandler(this.readonly);
359
+ set_readonlyPermissions(newReadonlyValue, oldReadonlyValue, readonlyConnectionReason) {
360
+ this._readonlyPermissions = newReadonlyValue;
361
+ if (oldReadonlyValue !== this.readonly) {
362
+ this.props.readonlyChangeHandler(this.readonly, readonlyConnectionReason);
314
363
  }
315
364
  }
316
- connect(connectionMode) {
317
- this.connectCore(connectionMode).catch((error) => {
318
- const normalizedError = normalizeError(error, { props: fatalConnectErrorProp });
365
+ connect(reason, connectionMode) {
366
+ this.connectCore(reason, connectionMode).catch((e) => {
367
+ const normalizedError = normalizeError(e, { props: fatalConnectErrorProp });
319
368
  this.props.closeHandler(normalizedError);
320
369
  });
321
370
  }
322
- async connectCore(connectionMode) {
323
- var _a, _b;
324
- assert(!this.closed, 0x26a /* "not closed" */);
371
+ async connectCore(reason, connectionMode) {
372
+ assert(!this._disposed, 0x26a /* "not closed" */);
325
373
  if (this.connection !== undefined) {
326
374
  return; // Connection attempt already completed successfully
327
375
  }
328
376
  let pendingConnectionMode;
329
377
  if (this.pendingConnection !== undefined) {
330
378
  pendingConnectionMode = this.pendingConnection.connectionMode;
331
- this.cancelConnection(); // Throw out in-progress connection attempt in favor of new attempt
379
+ this.cancelConnection(reason); // Throw out in-progress connection attempt in favor of new attempt
332
380
  assert(this.pendingConnection === undefined, 0x344 /* this.pendingConnection should be undefined */);
333
381
  }
334
382
  // If there is no specified ConnectionMode, try the previous mode, if there is no previous mode use default
335
- let requestedMode = (_a = connectionMode !== null && connectionMode !== void 0 ? connectionMode : pendingConnectionMode) !== null && _a !== void 0 ? _a : this.defaultReconnectionMode;
383
+ let requestedMode = connectionMode ?? pendingConnectionMode ?? this.defaultReconnectionMode;
336
384
  // if we have any non-acked ops from last connection, reconnect as "write".
337
385
  // without that we would connect in view-only mode, which will result in immediate
338
386
  // firing of "connected" event from Container and switch of current clientId (as tracked
@@ -344,9 +392,9 @@ export class ConnectionManager {
344
392
  const docService = this.serviceProvider();
345
393
  assert(docService !== undefined, 0x2a7 /* "Container is not attached" */);
346
394
  let connection;
347
- if (((_b = docService.policies) === null || _b === void 0 ? void 0 : _b.storageOnly) === true) {
395
+ if (docService.policies?.storageOnly === true) {
348
396
  connection = new NoDeltaStream();
349
- this.setupNewSuccessfulConnection(connection, "read");
397
+ this.setupNewSuccessfulConnection(connection, "read", reason);
350
398
  assert(this.pendingConnection === undefined, 0x2b3 /* "logic error" */);
351
399
  return;
352
400
  }
@@ -356,17 +404,23 @@ export class ConnectionManager {
356
404
  let lastError;
357
405
  const abortController = new AbortController();
358
406
  const abortSignal = abortController.signal;
359
- this.pendingConnection = { abort: () => { abortController.abort(); }, connectionMode: requestedMode };
407
+ this.pendingConnection = {
408
+ abort: () => {
409
+ abortController.abort();
410
+ },
411
+ connectionMode: requestedMode,
412
+ };
413
+ this.props.establishConnectionHandler(reason);
360
414
  // This loop will keep trying to connect until successful, with a delay between each iteration.
361
415
  while (connection === undefined) {
362
- if (this.closed) {
416
+ if (this._disposed) {
363
417
  throw new Error("Attempting to connect a closed DeltaManager");
364
418
  }
365
419
  if (abortSignal.aborted === true) {
366
420
  this.logger.sendTelemetryEvent({
367
421
  eventName: "ConnectionAttemptCancelled",
368
422
  attempts: connectRepeatCount,
369
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
423
+ duration: formatTick(performance.now() - connectStartTime),
370
424
  connectionEstablished: false,
371
425
  });
372
426
  return;
@@ -374,17 +428,42 @@ export class ConnectionManager {
374
428
  connectRepeatCount++;
375
429
  try {
376
430
  this.client.mode = requestedMode;
377
- connection = await docService.connectToDeltaStream(Object.assign(Object.assign({}, this.client), { mode: requestedMode }));
431
+ connection = await docService.connectToDeltaStream({
432
+ ...this.client,
433
+ mode: requestedMode,
434
+ });
378
435
  if (connection.disposed) {
379
436
  // Nobody observed this connection, so drop it on the floor and retry.
380
437
  this.logger.sendTelemetryEvent({ eventName: "ReceivedClosedConnection" });
381
438
  connection = undefined;
382
439
  }
440
+ this.logger.sendTelemetryEvent({
441
+ eventName: "ConnectionReceived",
442
+ connected: connection !== undefined && connection.disposed === false,
443
+ }, undefined, LogLevel.verbose);
383
444
  }
384
445
  catch (origError) {
385
- if (typeof origError === "object" && origError !== null &&
386
- (origError === null || origError === void 0 ? void 0 : origError.errorType) === DeltaStreamConnectionForbiddenError.errorType) {
387
- connection = new NoDeltaStream();
446
+ this.logger.sendTelemetryEvent({
447
+ eventName: "ConnectToDeltaStreamException",
448
+ connected: connection !== undefined && connection.disposed === false,
449
+ }, undefined, LogLevel.verbose);
450
+ if (isDeltaStreamConnectionForbiddenError(origError)) {
451
+ connection = new NoDeltaStream(origError.storageOnlyReason, {
452
+ text: origError.message,
453
+ error: origError,
454
+ });
455
+ requestedMode = "read";
456
+ break;
457
+ }
458
+ else if (isFluidError(origError) &&
459
+ // eslint-disable-next-line import/no-deprecated
460
+ origError.errorType === DriverErrorType.outOfStorageError) {
461
+ // If we get out of storage error from calling joinsession, then use the NoDeltaStream object so
462
+ // that user can at least load the container.
463
+ connection = new NoDeltaStream(undefined, {
464
+ text: origError.message,
465
+ error: origError,
466
+ });
388
467
  requestedMode = "read";
389
468
  break;
390
469
  }
@@ -399,15 +478,36 @@ export class ConnectionManager {
399
478
  attempts: connectRepeatCount,
400
479
  delay: delayMs,
401
480
  eventName: "DeltaConnectionFailureToConnect",
402
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
481
+ duration: formatTick(performance.now() - connectStartTime),
403
482
  }, origError);
404
483
  lastError = origError;
484
+ const waitStartTime = performance.now();
405
485
  const retryDelayFromError = getRetryDelayFromError(origError);
406
- delayMs = retryDelayFromError !== null && retryDelayFromError !== void 0 ? retryDelayFromError : Math.min(delayMs * 2, MaxReconnectDelayInMs);
407
- if (retryDelayFromError !== undefined) {
408
- this.props.reconnectionDelayHandler(retryDelayFromError, origError);
486
+ // If the error told us to wait or browser signals us that we are offline, then calculate the time we
487
+ // want to wait for before retrying. then we wait for that time. If the error didn't tell us to wait,
488
+ // let's still wait a little bit before retrying. We can skip this delay if we're confident we're offline,
489
+ // because we probably just need to wait to come back online. But we never have strong signal of being
490
+ // offline, so we at least wait for sometime.
491
+ if (retryDelayFromError !== undefined || globalThis.navigator?.onLine !== false) {
492
+ delayMs = calculateMaxWaitTime(delayMs, origError);
409
493
  }
410
- await waitForConnectedState(delayMs);
494
+ // Raise event in case the delay was there.
495
+ this.props.reconnectionDelayHandler(delayMs, origError);
496
+ await new Promise((resolve) => {
497
+ setTimeout(resolve, delayMs);
498
+ });
499
+ // If we believe we're offline, we assume there's no point in trying until we at least think we're online.
500
+ // NOTE: This isn't strictly true for drivers that don't require network (e.g. local driver). Really this logic
501
+ // should probably live in the driver.
502
+ await waitForOnline();
503
+ this.logger.sendPerformanceEvent({
504
+ eventName: "WaitBetweenConnectionAttempts",
505
+ duration: performance.now() - waitStartTime,
506
+ details: JSON.stringify({
507
+ retryDelayFromError,
508
+ delayMs,
509
+ }),
510
+ });
411
511
  }
412
512
  }
413
513
  // If we retried more than once, log an event about how long it took (this will not log to error table)
@@ -415,44 +515,50 @@ export class ConnectionManager {
415
515
  logNetworkFailure(this.logger, {
416
516
  eventName: "MultipleDeltaConnectionFailures",
417
517
  attempts: connectRepeatCount,
418
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
518
+ duration: formatTick(performance.now() - connectStartTime),
419
519
  }, lastError);
420
520
  }
421
- // Check for abort signal after while loop as well
422
- if (abortSignal.aborted === true) {
521
+ // Check for abort signal after while loop as well or we've been disposed
522
+ if (abortSignal.aborted === true || this._disposed) {
423
523
  connection.dispose();
424
524
  this.logger.sendTelemetryEvent({
425
525
  eventName: "ConnectionAttemptCancelled",
426
526
  attempts: connectRepeatCount,
427
- duration: TelemetryLogger.formatTick(performance.now() - connectStartTime),
527
+ duration: formatTick(performance.now() - connectStartTime),
428
528
  connectionEstablished: true,
429
529
  });
430
530
  return;
431
531
  }
432
- this.setupNewSuccessfulConnection(connection, requestedMode);
532
+ this.setupNewSuccessfulConnection(connection, requestedMode, reason);
433
533
  }
434
534
  /**
435
- * Start the connection. Any error should result in container being close.
436
- * And report the error if it excape for any reason.
535
+ * Start the connection. Any error should result in container being closed.
536
+ * And report the error if it escapes for any reason.
437
537
  * @param args - The connection arguments
438
538
  */
439
- triggerConnect(connectionMode) {
440
- assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
539
+ triggerConnect(reason, connectionMode) {
540
+ // reconnect() includes async awaits, and that causes potential race conditions
541
+ // where we might already have a connection. If it were to happen, it's possible that we will connect
542
+ // with different mode to `connectionMode`. Glancing through the caller chains, it looks like code should be
543
+ // fine (if needed, reconnect flow will get triggered again). Places where new mode matters should encode it
544
+ // directly in connectCore - see this.shouldJoinWrite() test as an example.
545
+ // assert(this.connection === undefined, 0x239 /* "called only in disconnected state" */);
441
546
  if (this.reconnectMode !== ReconnectMode.Enabled) {
442
547
  return;
443
548
  }
444
- this.connect(connectionMode);
549
+ this.connect(reason, connectionMode);
445
550
  }
446
551
  /**
447
552
  * Disconnect the current connection.
448
553
  * @param reason - Text description of disconnect reason to emit with disconnect event
554
+ * @param error - Error causing the disconnect if any.
449
555
  * @returns A boolean that indicates if there was an existing connection (or pending connection) to disconnect
450
556
  */
451
557
  disconnectFromDeltaStream(reason) {
452
558
  this.pendingReconnect = false;
453
559
  if (this.connection === undefined) {
454
560
  if (this.pendingConnection !== undefined) {
455
- this.cancelConnection();
561
+ this.cancelConnection(reason);
456
562
  return true;
457
563
  }
458
564
  return false;
@@ -463,7 +569,7 @@ export class ConnectionManager {
463
569
  this.connection = undefined;
464
570
  // Remove listeners first so we don't try to retrigger this flow accidentally through reconnectOnError
465
571
  connection.off("op", this.opHandler);
466
- connection.off("signal", this.props.signalHandler);
572
+ connection.off("signal", this.signalHandler);
467
573
  connection.off("nack", this.nackHandler);
468
574
  connection.off("disconnect", this.disconnectHandlerInternal);
469
575
  connection.off("error", this.errorHandler);
@@ -471,48 +577,60 @@ export class ConnectionManager {
471
577
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
472
578
  this._outbound.pause();
473
579
  this._outbound.clear();
474
- this.props.disconnectHandler(reason);
475
580
  connection.dispose();
581
+ this.props.disconnectHandler(reason);
476
582
  this._connectionVerboseProps = {};
477
583
  return true;
478
584
  }
479
585
  /**
480
586
  * Cancel in-progress connection attempt.
481
587
  */
482
- cancelConnection() {
588
+ cancelConnection(reason) {
483
589
  assert(this.pendingConnection !== undefined, 0x345 /* this.pendingConnection is undefined when trying to cancel */);
484
590
  this.pendingConnection.abort();
485
591
  this.pendingConnection = undefined;
486
592
  this.logger.sendTelemetryEvent({ eventName: "ConnectionCancelReceived" });
593
+ this.props.cancelConnectionHandler({
594
+ text: `Cancel Pending Connection due to ${reason.text}`,
595
+ error: reason.error,
596
+ });
487
597
  }
488
598
  /**
489
599
  * Once we've successfully gotten a connection, we need to set up state, attach event listeners, and process
490
600
  * initial messages.
491
601
  * @param connection - The newly established connection
492
602
  */
493
- setupNewSuccessfulConnection(connection, requestedMode) {
603
+ setupNewSuccessfulConnection(connection, requestedMode, reason) {
494
604
  // Old connection should have been cleaned up before establishing a new one
495
605
  assert(this.connection === undefined, 0x0e6 /* "old connection exists on new connection setup" */);
496
606
  assert(!connection.disposed, 0x28a /* "can't be disposed - Callers need to ensure that!" */);
497
607
  this.pendingConnection = undefined;
608
+ const oldReadonlyValue = this.readonly;
498
609
  this.connection = connection;
499
610
  // Does information in scopes & mode matches?
500
611
  // If we asked for "write" and got "read", then file is read-only
501
612
  // But if we ask read, server can still give us write.
502
613
  const readonly = !connection.claims.scopes.includes(ScopeType.DocWrite);
614
+ if (connection.mode !== requestedMode) {
615
+ this.logger.sendTelemetryEvent({
616
+ eventName: "ConnectionModeMismatch",
617
+ requestedMode,
618
+ mode: connection.mode,
619
+ });
620
+ }
503
621
  // This connection mode validation logic is moving to the driver layer in 0.44. These two asserts can be
504
622
  // removed after those packages have released and become ubiquitous.
505
623
  assert(requestedMode === "read" || readonly === (this.connectionMode === "read"), 0x0e7 /* "claims/connectionMode mismatch" */);
506
624
  assert(!readonly || this.connectionMode === "read", 0x0e8 /* "readonly perf with write connection" */);
507
- this.set_readonlyPermissions(readonly);
508
- if (this.closed) {
625
+ this.set_readonlyPermissions(readonly, oldReadonlyValue, isNoDeltaStreamConnection(connection) ? connection.readonlyConnectionReason : undefined);
626
+ if (this._disposed) {
509
627
  // Raise proper events, Log telemetry event and close connection.
510
- this.disconnectFromDeltaStream("ConnectionManager already closed");
628
+ this.disconnectFromDeltaStream({ text: "ConnectionManager already closed" });
511
629
  return;
512
630
  }
513
631
  this._outbound.resume();
514
632
  connection.on("op", this.opHandler);
515
- connection.on("signal", this.props.signalHandler);
633
+ connection.on("signal", this.signalHandler);
516
634
  connection.on("nack", this.nackHandler);
517
635
  connection.on("disconnect", this.disconnectHandlerInternal);
518
636
  connection.on("error", this.errorHandler);
@@ -538,7 +656,8 @@ export class ConnectionManager {
538
656
  this._connectionProps.connectionMode = connection.mode;
539
657
  let last = -1;
540
658
  if (initialMessages.length !== 0) {
541
- this._connectionVerboseProps.connectionInitialOpsFrom = initialMessages[0].sequenceNumber;
659
+ this._connectionVerboseProps.connectionInitialOpsFrom =
660
+ initialMessages[0].sequenceNumber;
542
661
  last = initialMessages[initialMessages.length - 1].sequenceNumber;
543
662
  this._connectionVerboseProps.connectionInitialOpsTo = last + 1;
544
663
  // Update knowledge of how far we are behind, before raising "connect" event
@@ -549,15 +668,39 @@ export class ConnectionManager {
549
668
  }
550
669
  }
551
670
  this.props.incomingOpHandler(initialMessages, this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
552
- if (connection.initialSignals !== undefined) {
553
- for (const signal of connection.initialSignals) {
554
- this.props.signalHandler(signal);
555
- }
556
- }
557
- const details = ConnectionManager.detailsFromConnection(connection);
671
+ const details = ConnectionManager.detailsFromConnection(connection, reason);
558
672
  details.checkpointSequenceNumber = checkpointSequenceNumber;
559
673
  this.props.connectHandler(details);
560
674
  this.connectFirstConnection = false;
675
+ // Synthesize clear & join signals out of initialClients state.
676
+ // This allows us to have single way to process signals, and makes it simpler to initialize
677
+ // protocol in Container.
678
+ const clearSignal = {
679
+ clientId: null,
680
+ content: JSON.stringify({
681
+ type: SignalType.Clear,
682
+ }),
683
+ };
684
+ // list of signals to process due to this new connection
685
+ let signalsToProcess = [clearSignal];
686
+ const clientJoinSignals = (connection.initialClients ?? []).map((priorClient) => ({
687
+ clientId: null,
688
+ content: JSON.stringify({
689
+ type: SignalType.ClientJoin,
690
+ content: priorClient, // ISignalClient
691
+ }),
692
+ }));
693
+ if (clientJoinSignals.length > 0) {
694
+ signalsToProcess = signalsToProcess.concat(clientJoinSignals);
695
+ }
696
+ // Unfortunately, there is no defined order between initialSignals (including join & leave signals)
697
+ // and connection.initialClients. In practice, connection.initialSignals quite often contains join signal
698
+ // for "self" and connection.initialClients does not contain "self", so we have to process them after
699
+ // "clear" signal above.
700
+ if (connection.initialSignals !== undefined && connection.initialSignals.length > 0) {
701
+ signalsToProcess = signalsToProcess.concat(connection.initialSignals);
702
+ }
703
+ this.props.signalHandler(signalsToProcess);
561
704
  }
562
705
  /**
563
706
  * Disconnect the current connection and reconnect. Closes the container if it fails.
@@ -567,8 +710,7 @@ export class ConnectionManager {
567
710
  * @returns A promise that resolves when the connection is reestablished or we stop trying
568
711
  */
569
712
  reconnectOnError(requestedMode, error) {
570
- this.reconnect(requestedMode, error.message, error)
571
- .catch(this.props.closeHandler);
713
+ this.reconnect(requestedMode, { text: error.message, error }).catch(this.props.closeHandler);
572
714
  }
573
715
  /**
574
716
  * Disconnect the current connection and reconnect.
@@ -577,20 +719,20 @@ export class ConnectionManager {
577
719
  * @param error - Error reconnect information including whether or not to reconnect
578
720
  * @returns A promise that resolves when the connection is reestablished or we stop trying
579
721
  */
580
- async reconnect(requestedMode, disconnectMessage, error) {
722
+ async reconnect(requestedMode, reason) {
581
723
  // We quite often get protocol errors before / after observing nack/disconnect
582
724
  // we do not want to run through same sequence twice.
583
725
  // If we're already disconnected/disconnecting it's not appropriate to call this again.
584
726
  assert(this.connection !== undefined, 0x0eb /* "Missing connection for reconnect" */);
585
- this.disconnectFromDeltaStream(disconnectMessage);
727
+ this.disconnectFromDeltaStream(reason);
586
728
  // We will always trigger reconnect, even if canRetry is false.
587
729
  // Any truly fatal error state will result in container close upon attempted reconnect,
588
730
  // which is a preferable to closing abruptly when a live connection fails.
589
- if (error !== undefined && !error.canRetry) {
731
+ if (reason.error?.canRetry === false) {
590
732
  this.logger.sendTelemetryEvent({
591
733
  eventName: "reconnectingDespiteFatalError",
592
734
  reconnectMode: this.reconnectMode,
593
- }, error);
735
+ }, reason.error);
594
736
  }
595
737
  if (this.reconnectMode === ReconnectMode.Never) {
596
738
  // Do not raise container error if we are closing just because we lost connection.
@@ -599,18 +741,29 @@ export class ConnectionManager {
599
741
  this.props.closeHandler();
600
742
  }
601
743
  // If closed then we can't reconnect
602
- if (this.closed || this.reconnectMode !== ReconnectMode.Enabled) {
744
+ if (this._disposed || this.reconnectMode !== ReconnectMode.Enabled) {
603
745
  return;
604
746
  }
605
- const delayMs = getRetryDelayFromError(error);
606
- if (error !== undefined && delayMs !== undefined) {
607
- this.props.reconnectionDelayHandler(delayMs, error);
608
- await waitForConnectedState(delayMs);
747
+ // If the error tells us to wait before retrying, then do so.
748
+ const delayMs = getRetryDelayFromError(reason.error);
749
+ if (reason.error !== undefined && delayMs !== undefined) {
750
+ this.props.reconnectionDelayHandler(delayMs, reason.error);
751
+ await new Promise((resolve) => {
752
+ setTimeout(resolve, delayMs);
753
+ });
609
754
  }
610
- this.triggerConnect(requestedMode);
755
+ // If we believe we're offline, we assume there's no point in trying again until we at least think we're online.
756
+ // NOTE: This isn't strictly true for drivers that don't require network (e.g. local driver). Really this logic
757
+ // should probably live in the driver.
758
+ await waitForOnline();
759
+ this.triggerConnect({
760
+ text: reason.error !== undefined
761
+ ? "Reconnecting due to Error"
762
+ : `Reconnecting due to: ${reason.text}`,
763
+ error: reason.error,
764
+ }, requestedMode);
611
765
  }
612
766
  prepareMessageToSend(message) {
613
- var _a, _b;
614
767
  if (this.readonly === true) {
615
768
  assert(this.readOnlyInfo.readonly === true, 0x1f0 /* "Unexpected mismatch in readonly" */);
616
769
  const error = new GenericError("deltaManagerReadonlySubmit", undefined /* error */, {
@@ -618,6 +771,7 @@ export class ConnectionManager {
618
771
  forcedReadonly: this.readOnlyInfo.forced,
619
772
  readonlyPermissions: this.readOnlyInfo.permissions,
620
773
  storageOnly: this.readOnlyInfo.storageOnly,
774
+ storageOnlyReason: this.readOnlyInfo.storageOnlyReason,
621
775
  });
622
776
  this.props.closeHandler(error);
623
777
  return undefined;
@@ -626,22 +780,25 @@ export class ConnectionManager {
626
780
  // we keep info about old connection as long as possible to be able to account for all non-acked ops
627
781
  // that we pick up on next connection.
628
782
  assert(!!this.connection, 0x0e4 /* "Lost old connection!" */);
629
- if (this.lastSubmittedClientId !== ((_a = this.connection) === null || _a === void 0 ? void 0 : _a.clientId)) {
630
- this.lastSubmittedClientId = (_b = this.connection) === null || _b === void 0 ? void 0 : _b.clientId;
783
+ if (this.lastSubmittedClientId !== this.connection?.clientId) {
784
+ this.lastSubmittedClientId = this.connection?.clientId;
631
785
  this.clientSequenceNumber = 0;
632
786
  this.clientSequenceNumberObserved = 0;
633
787
  }
634
- if (message.type === MessageType.NoOp) {
635
- this.trailingNoopCount++;
788
+ if (!isRuntimeMessage(message)) {
789
+ this.localOpsToIgnore++;
636
790
  }
637
791
  else {
638
- this.trailingNoopCount = 0;
792
+ this.localOpsToIgnore = 0;
639
793
  }
640
- return Object.assign(Object.assign({}, message), { clientSequenceNumber: ++this.clientSequenceNumber });
794
+ return {
795
+ ...message,
796
+ clientSequenceNumber: ++this.clientSequenceNumber,
797
+ };
641
798
  }
642
- submitSignal(content) {
799
+ submitSignal(content, targetClientId) {
643
800
  if (this.connection !== undefined) {
644
- this.connection.submitSignal(content);
801
+ this.connection.submitSignal(content, targetClientId);
645
802
  }
646
803
  else {
647
804
  this.logger.sendErrorEvent({ eventName: "submitSignalDisconnected" });
@@ -658,10 +815,12 @@ export class ConnectionManager {
658
815
  if (this.connectionMode === "read") {
659
816
  if (!this.pendingReconnect) {
660
817
  this.pendingReconnect = true;
661
- Promise.resolve().then(async () => {
662
- if (this.pendingReconnect) { // still valid?
818
+ Promise.resolve()
819
+ .then(async () => {
820
+ if (this.pendingReconnect) {
821
+ // still valid?
663
822
  await this.reconnect("write", // connectionMode
664
- "Switch to write");
823
+ { text: "Switch to write" });
665
824
  }
666
825
  })
667
826
  .catch(() => { });
@@ -674,7 +833,8 @@ export class ConnectionManager {
674
833
  beforeProcessingIncomingOp(message) {
675
834
  // if we have connection, and message is local, then we better treat is as local!
676
835
  assert(this.clientId !== message.clientId || this.lastSubmittedClientId === message.clientId, 0x0ee /* "Not accounting local messages correctly" */);
677
- if (this.lastSubmittedClientId !== undefined && this.lastSubmittedClientId === message.clientId) {
836
+ if (this.lastSubmittedClientId !== undefined &&
837
+ this.lastSubmittedClientId === message.clientId) {
678
838
  const clientSequenceNumber = message.clientSequenceNumber;
679
839
  assert(this.clientSequenceNumberObserved < clientSequenceNumber, 0x0ef /* "client seq# not growing" */);
680
840
  assert(clientSequenceNumber <= this.clientSequenceNumber, 0x0f0 /* "Incoming local client seq# > generated by this client" */);
@@ -693,11 +853,11 @@ export class ConnectionManager {
693
853
  // Clients need to be able to transition to "read" state after some time of inactivity!
694
854
  // Note - this may close container!
695
855
  this.reconnect("read", // connectionMode
696
- "Switch to read").catch((error) => {
856
+ { text: "Switch to read" }).catch((error) => {
697
857
  this.logger.sendErrorEvent({ eventName: "SwitchToReadConnection" }, error);
698
858
  });
699
859
  }
700
860
  }
701
861
  }
702
862
  }
703
- //# sourceMappingURL=connectionManager.js.map
863
+ //# sourceMappingURL=connectionManager.mjs.map