@fluidframework/container-runtime 1.2.6 → 2.0.0-dev.1.3.0.96595

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 (221) hide show
  1. package/.mocharc.js +12 -0
  2. package/dist/batchManager.d.ts +37 -0
  3. package/dist/batchManager.d.ts.map +1 -0
  4. package/dist/batchManager.js +73 -0
  5. package/dist/batchManager.js.map +1 -0
  6. package/dist/batchTracker.d.ts +1 -2
  7. package/dist/batchTracker.d.ts.map +1 -1
  8. package/dist/batchTracker.js +2 -3
  9. package/dist/batchTracker.js.map +1 -1
  10. package/dist/blobManager.d.ts +87 -25
  11. package/dist/blobManager.d.ts.map +1 -1
  12. package/dist/blobManager.js +317 -99
  13. package/dist/blobManager.js.map +1 -1
  14. package/dist/containerRuntime.d.ts +109 -124
  15. package/dist/containerRuntime.d.ts.map +1 -1
  16. package/dist/containerRuntime.js +349 -542
  17. package/dist/containerRuntime.js.map +1 -1
  18. package/dist/dataStore.js +29 -24
  19. package/dist/dataStore.js.map +1 -1
  20. package/dist/dataStoreContext.d.ts +20 -14
  21. package/dist/dataStoreContext.d.ts.map +1 -1
  22. package/dist/dataStoreContext.js +49 -58
  23. package/dist/dataStoreContext.js.map +1 -1
  24. package/dist/dataStores.d.ts +12 -5
  25. package/dist/dataStores.d.ts.map +1 -1
  26. package/dist/dataStores.js +21 -20
  27. package/dist/dataStores.js.map +1 -1
  28. package/dist/deltaScheduler.d.ts +6 -4
  29. package/dist/deltaScheduler.d.ts.map +1 -1
  30. package/dist/deltaScheduler.js +6 -4
  31. package/dist/deltaScheduler.js.map +1 -1
  32. package/dist/garbageCollection.d.ts +74 -14
  33. package/dist/garbageCollection.d.ts.map +1 -1
  34. package/dist/garbageCollection.js +249 -170
  35. package/dist/garbageCollection.js.map +1 -1
  36. package/dist/gcSweepReadyUsageDetection.d.ts +53 -0
  37. package/dist/gcSweepReadyUsageDetection.d.ts.map +1 -0
  38. package/dist/gcSweepReadyUsageDetection.js +126 -0
  39. package/dist/gcSweepReadyUsageDetection.js.map +1 -0
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -2
  43. package/dist/index.js.map +1 -1
  44. package/dist/opProperties.d.ts +7 -0
  45. package/dist/opProperties.d.ts.map +1 -0
  46. package/dist/opProperties.js +20 -0
  47. package/dist/opProperties.js.map +1 -0
  48. package/dist/orderedClientElection.d.ts +28 -10
  49. package/dist/orderedClientElection.d.ts.map +1 -1
  50. package/dist/orderedClientElection.js +14 -4
  51. package/dist/orderedClientElection.js.map +1 -1
  52. package/dist/packageVersion.d.ts +1 -1
  53. package/dist/packageVersion.d.ts.map +1 -1
  54. package/dist/packageVersion.js +1 -1
  55. package/dist/packageVersion.js.map +1 -1
  56. package/dist/pendingStateManager.d.ts +0 -11
  57. package/dist/pendingStateManager.d.ts.map +1 -1
  58. package/dist/pendingStateManager.js +24 -46
  59. package/dist/pendingStateManager.js.map +1 -1
  60. package/dist/runningSummarizer.d.ts +14 -4
  61. package/dist/runningSummarizer.d.ts.map +1 -1
  62. package/dist/runningSummarizer.js +68 -26
  63. package/dist/runningSummarizer.js.map +1 -1
  64. package/dist/scheduleManager.d.ts +31 -0
  65. package/dist/scheduleManager.d.ts.map +1 -0
  66. package/dist/scheduleManager.js +243 -0
  67. package/dist/scheduleManager.js.map +1 -0
  68. package/dist/summarizer.d.ts +0 -2
  69. package/dist/summarizer.d.ts.map +1 -1
  70. package/dist/summarizer.js +1 -12
  71. package/dist/summarizer.js.map +1 -1
  72. package/dist/summarizerHeuristics.d.ts +26 -4
  73. package/dist/summarizerHeuristics.d.ts.map +1 -1
  74. package/dist/summarizerHeuristics.js +95 -18
  75. package/dist/summarizerHeuristics.js.map +1 -1
  76. package/dist/summarizerTypes.d.ts +45 -18
  77. package/dist/summarizerTypes.d.ts.map +1 -1
  78. package/dist/summarizerTypes.js +1 -1
  79. package/dist/summarizerTypes.js.map +1 -1
  80. package/dist/summaryCollection.d.ts +1 -0
  81. package/dist/summaryCollection.d.ts.map +1 -1
  82. package/dist/summaryCollection.js +31 -15
  83. package/dist/summaryCollection.js.map +1 -1
  84. package/dist/summaryFormat.d.ts +0 -5
  85. package/dist/summaryFormat.d.ts.map +1 -1
  86. package/dist/summaryFormat.js.map +1 -1
  87. package/dist/summaryGenerator.d.ts +1 -0
  88. package/dist/summaryGenerator.d.ts.map +1 -1
  89. package/dist/summaryGenerator.js +11 -9
  90. package/dist/summaryGenerator.js.map +1 -1
  91. package/dist/summaryManager.d.ts +2 -2
  92. package/dist/summaryManager.d.ts.map +1 -1
  93. package/dist/summaryManager.js +22 -7
  94. package/dist/summaryManager.js.map +1 -1
  95. package/lib/batchManager.d.ts +37 -0
  96. package/lib/batchManager.d.ts.map +1 -0
  97. package/lib/batchManager.js +69 -0
  98. package/lib/batchManager.js.map +1 -0
  99. package/lib/batchTracker.d.ts +1 -2
  100. package/lib/batchTracker.d.ts.map +1 -1
  101. package/lib/batchTracker.js +2 -3
  102. package/lib/batchTracker.js.map +1 -1
  103. package/lib/blobManager.d.ts +87 -25
  104. package/lib/blobManager.d.ts.map +1 -1
  105. package/lib/blobManager.js +319 -101
  106. package/lib/blobManager.js.map +1 -1
  107. package/lib/containerRuntime.d.ts +109 -124
  108. package/lib/containerRuntime.d.ts.map +1 -1
  109. package/lib/containerRuntime.js +355 -547
  110. package/lib/containerRuntime.js.map +1 -1
  111. package/lib/dataStore.js +29 -24
  112. package/lib/dataStore.js.map +1 -1
  113. package/lib/dataStoreContext.d.ts +20 -14
  114. package/lib/dataStoreContext.d.ts.map +1 -1
  115. package/lib/dataStoreContext.js +46 -55
  116. package/lib/dataStoreContext.js.map +1 -1
  117. package/lib/dataStores.d.ts +12 -5
  118. package/lib/dataStores.d.ts.map +1 -1
  119. package/lib/dataStores.js +21 -20
  120. package/lib/dataStores.js.map +1 -1
  121. package/lib/deltaScheduler.d.ts +6 -4
  122. package/lib/deltaScheduler.d.ts.map +1 -1
  123. package/lib/deltaScheduler.js +6 -4
  124. package/lib/deltaScheduler.js.map +1 -1
  125. package/lib/garbageCollection.d.ts +74 -14
  126. package/lib/garbageCollection.d.ts.map +1 -1
  127. package/lib/garbageCollection.js +238 -160
  128. package/lib/garbageCollection.js.map +1 -1
  129. package/lib/gcSweepReadyUsageDetection.d.ts +53 -0
  130. package/lib/gcSweepReadyUsageDetection.d.ts.map +1 -0
  131. package/lib/gcSweepReadyUsageDetection.js +121 -0
  132. package/lib/gcSweepReadyUsageDetection.js.map +1 -0
  133. package/lib/index.d.ts +2 -1
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +2 -1
  136. package/lib/index.js.map +1 -1
  137. package/lib/opProperties.d.ts +7 -0
  138. package/lib/opProperties.d.ts.map +1 -0
  139. package/lib/opProperties.js +16 -0
  140. package/lib/opProperties.js.map +1 -0
  141. package/lib/orderedClientElection.d.ts +28 -10
  142. package/lib/orderedClientElection.d.ts.map +1 -1
  143. package/lib/orderedClientElection.js +14 -4
  144. package/lib/orderedClientElection.js.map +1 -1
  145. package/lib/packageVersion.d.ts +1 -1
  146. package/lib/packageVersion.d.ts.map +1 -1
  147. package/lib/packageVersion.js +1 -1
  148. package/lib/packageVersion.js.map +1 -1
  149. package/lib/pendingStateManager.d.ts +0 -11
  150. package/lib/pendingStateManager.d.ts.map +1 -1
  151. package/lib/pendingStateManager.js +24 -46
  152. package/lib/pendingStateManager.js.map +1 -1
  153. package/lib/runningSummarizer.d.ts +14 -4
  154. package/lib/runningSummarizer.d.ts.map +1 -1
  155. package/lib/runningSummarizer.js +68 -26
  156. package/lib/runningSummarizer.js.map +1 -1
  157. package/lib/scheduleManager.d.ts +31 -0
  158. package/lib/scheduleManager.d.ts.map +1 -0
  159. package/lib/scheduleManager.js +239 -0
  160. package/lib/scheduleManager.js.map +1 -0
  161. package/lib/summarizer.d.ts +0 -2
  162. package/lib/summarizer.d.ts.map +1 -1
  163. package/lib/summarizer.js +1 -12
  164. package/lib/summarizer.js.map +1 -1
  165. package/lib/summarizerHeuristics.d.ts +26 -4
  166. package/lib/summarizerHeuristics.d.ts.map +1 -1
  167. package/lib/summarizerHeuristics.js +95 -18
  168. package/lib/summarizerHeuristics.js.map +1 -1
  169. package/lib/summarizerTypes.d.ts +45 -18
  170. package/lib/summarizerTypes.d.ts.map +1 -1
  171. package/lib/summarizerTypes.js +1 -1
  172. package/lib/summarizerTypes.js.map +1 -1
  173. package/lib/summaryCollection.d.ts +1 -0
  174. package/lib/summaryCollection.d.ts.map +1 -1
  175. package/lib/summaryCollection.js +31 -15
  176. package/lib/summaryCollection.js.map +1 -1
  177. package/lib/summaryFormat.d.ts +0 -5
  178. package/lib/summaryFormat.d.ts.map +1 -1
  179. package/lib/summaryFormat.js.map +1 -1
  180. package/lib/summaryGenerator.d.ts +1 -0
  181. package/lib/summaryGenerator.d.ts.map +1 -1
  182. package/lib/summaryGenerator.js +11 -9
  183. package/lib/summaryGenerator.js.map +1 -1
  184. package/lib/summaryManager.d.ts +2 -2
  185. package/lib/summaryManager.d.ts.map +1 -1
  186. package/lib/summaryManager.js +22 -7
  187. package/lib/summaryManager.js.map +1 -1
  188. package/package.json +65 -24
  189. package/src/batchManager.ts +91 -0
  190. package/src/batchTracker.ts +2 -3
  191. package/src/blobManager.ts +385 -118
  192. package/src/containerRuntime.ts +529 -740
  193. package/src/dataStore.ts +49 -37
  194. package/src/dataStoreContext.ts +44 -56
  195. package/src/dataStores.ts +34 -30
  196. package/src/deltaScheduler.ts +6 -4
  197. package/src/garbageCollection.ts +297 -206
  198. package/src/gcSweepReadyUsageDetection.ts +139 -0
  199. package/src/index.ts +1 -2
  200. package/src/opProperties.ts +19 -0
  201. package/src/orderedClientElection.ts +31 -10
  202. package/src/packageVersion.ts +1 -1
  203. package/src/pendingStateManager.ts +27 -59
  204. package/src/runningSummarizer.ts +75 -22
  205. package/src/scheduleManager.ts +314 -0
  206. package/src/summarizer.ts +1 -18
  207. package/src/summarizerHeuristics.ts +133 -19
  208. package/src/summarizerTypes.ts +53 -18
  209. package/src/summaryCollection.ts +33 -18
  210. package/src/summaryFormat.ts +0 -6
  211. package/src/summaryGenerator.ts +40 -22
  212. package/src/summaryManager.ts +22 -7
  213. package/dist/opTelemetry.d.ts +0 -22
  214. package/dist/opTelemetry.d.ts.map +0 -1
  215. package/dist/opTelemetry.js +0 -59
  216. package/dist/opTelemetry.js.map +0 -1
  217. package/lib/opTelemetry.d.ts +0 -22
  218. package/lib/opTelemetry.d.ts.map +0 -1
  219. package/lib/opTelemetry.js +0 -55
  220. package/lib/opTelemetry.js.map +0 -1
  221. package/src/opTelemetry.ts +0 -71
@@ -1,9 +1,9 @@
1
1
  import { AttachState, LoaderHeader, } from "@fluidframework/container-definitions";
2
- import { assert, Trace, TypedEventEmitter, unreachableCase, performance, } from "@fluidframework/common-utils";
3
- import { ChildLogger, raiseConnectedEvent, PerformanceEvent, TaggedLoggerAdapter, loggerToMonitoringContext, TelemetryDataTag, } from "@fluidframework/telemetry-utils";
4
- import { DriverHeader } from "@fluidframework/driver-definitions";
5
- import { readAndParse, isUnpackedRuntimeMessage } from "@fluidframework/driver-utils";
6
- import { DataCorruptionError, DataProcessingError, GenericError, UsageError, extractSafePropertiesFromMessage, } from "@fluidframework/container-utils";
2
+ import { assert, Trace, TypedEventEmitter, unreachableCase, } from "@fluidframework/common-utils";
3
+ import { ChildLogger, raiseConnectedEvent, PerformanceEvent, TaggedLoggerAdapter, loggerToMonitoringContext, wrapError, } from "@fluidframework/telemetry-utils";
4
+ import { DriverHeader, FetchSource, } from "@fluidframework/driver-definitions";
5
+ import { readAndParse } from "@fluidframework/driver-utils";
6
+ import { DataCorruptionError, DataProcessingError, GenericError, UsageError, } from "@fluidframework/container-utils";
7
7
  import { MessageType, SummaryType, } from "@fluidframework/protocol-definitions";
8
8
  import { FlushMode, channelsTreeName, } from "@fluidframework/runtime-definitions";
9
9
  import { addBlobToSummary, addSummarizeResultToSummary, addTreeToSummary, createRootSummarizerNodeWithGC, RequestParser, create404Response, exceptionToResponse, requestFluidObject, responseToException, seqFromTree, calculateStats, TelemetryContext, } from "@fluidframework/runtime-utils";
@@ -13,9 +13,9 @@ import { ContainerFluidHandleContext } from "./containerHandleContext";
13
13
  import { FluidDataStoreRegistry } from "./dataStoreRegistry";
14
14
  import { Summarizer } from "./summarizer";
15
15
  import { SummaryManager } from "./summaryManager";
16
- import { DeltaScheduler } from "./deltaScheduler";
17
- import { ReportOpPerfTelemetry, latencyThreshold, } from "./connectionTelemetry";
18
- import { PendingStateManager } from "./pendingStateManager";
16
+ import { ReportOpPerfTelemetry, } from "./connectionTelemetry";
17
+ import { PendingStateManager, } from "./pendingStateManager";
18
+ import { BatchManager } from "./batchManager";
19
19
  import { pkgVersion } from "./packageVersion";
20
20
  import { BlobManager } from "./blobManager";
21
21
  import { DataStores, getSummaryForDatastores } from "./dataStores";
@@ -29,7 +29,7 @@ import { GarbageCollector, GCNodeType, gcTreeKey, } from "./garbageCollection";
29
29
  import { channelToDataStore, isDataStoreAliasMessage, } from "./dataStore";
30
30
  import { BindBatchTracker } from "./batchTracker";
31
31
  import { SerializedSnapshotStorage } from "./serializedSnapshotStorage";
32
- import { OpTracker } from "./opTelemetry";
32
+ import { ScheduleManager } from "./scheduleManager";
33
33
  export var ContainerMessageType;
34
34
  (function (ContainerMessageType) {
35
35
  // An op to be delivered to store
@@ -47,14 +47,18 @@ export var ContainerMessageType;
47
47
  })(ContainerMessageType || (ContainerMessageType = {}));
48
48
  export const DefaultSummaryConfiguration = {
49
49
  state: "enabled",
50
- idleTime: 5000 * 3,
51
- maxTime: 5000 * 12,
50
+ idleTime: 15 * 1000,
51
+ minIdleTime: 0,
52
+ maxIdleTime: 30 * 1000,
53
+ maxTime: 60 * 1000,
52
54
  maxOps: 100,
53
55
  minOpsForLastSummaryAttempt: 10,
54
- maxAckWaitTime: 6 * 10 * 1000,
56
+ maxAckWaitTime: 10 * 60 * 1000,
55
57
  maxOpsSinceLastSummary: 7000,
56
- initialSummarizerDelayMs: 5000,
58
+ initialSummarizerDelayMs: 5 * 1000,
57
59
  summarizerClientElection: false,
60
+ nonRuntimeOpWeight: 0.1,
61
+ runtimeOpWeight: 1.0,
58
62
  };
59
63
  /**
60
64
  * Accepted header keys for requests coming to the runtime.
@@ -71,21 +75,11 @@ export var RuntimeHeaders;
71
75
  /** True if the request is coming from an IFluidHandle. */
72
76
  RuntimeHeaders["viaHandle"] = "viaHandle";
73
77
  })(RuntimeHeaders || (RuntimeHeaders = {}));
74
- const useDataStoreAliasingKey = "Fluid.ContainerRuntime.UseDataStoreAliasing";
75
78
  const maxConsecutiveReconnectsKey = "Fluid.ContainerRuntime.MaxConsecutiveReconnects";
76
- // Feature gate for the max op size. If the value is negative, chunking is enabled
77
- // and all ops over 16k would be chunked. If the value is positive, all ops with
78
- // a size strictly larger will be rejected and the container closed with an error.
79
- const maxOpSizeInBytesKey = "Fluid.ContainerRuntime.MaxOpSizeInBytes";
80
- // By default, we should reject any op larger than 768KB,
81
- // in order to account for some extra overhead from serialization
82
- // to not reach the 1MB limits in socket.io and Kafka.
83
- const defaultMaxOpSizeInBytes = 768000;
84
- // By default, the size of the contents for the incoming ops is tracked.
85
- // However, in certain situations, this may incur a performance hit.
86
- // The feature-gate below can be used to disable this feature.
87
- const disableOpTrackingKey = "Fluid.ContainerRuntime.DisableOpTracking";
88
79
  const defaultFlushMode = FlushMode.TurnBased;
80
+ /**
81
+ * @deprecated - use ContainerRuntimeMessage instead
82
+ */
89
83
  export var RuntimeMessage;
90
84
  (function (RuntimeMessage) {
91
85
  RuntimeMessage["FluidDataStoreOp"] = "component";
@@ -96,12 +90,24 @@ export var RuntimeMessage;
96
90
  RuntimeMessage["Alias"] = "alias";
97
91
  RuntimeMessage["Operation"] = "op";
98
92
  })(RuntimeMessage || (RuntimeMessage = {}));
93
+ /**
94
+ * @deprecated - please use version in driver-utils
95
+ */
99
96
  export function isRuntimeMessage(message) {
100
97
  if (Object.values(RuntimeMessage).includes(message.type)) {
101
98
  return true;
102
99
  }
103
100
  return false;
104
101
  }
102
+ /**
103
+ * Unpacks runtime messages
104
+ *
105
+ * @remarks This API makes no promises regarding backward-compatability. This is internal API.
106
+ * @param message - message (as it observed in storage / service)
107
+ * @returns unpacked runtime message
108
+ *
109
+ * @internal
110
+ */
105
111
  export function unpackRuntimeMessage(message) {
106
112
  if (message.type === MessageType.Operation) {
107
113
  // legacy op format?
@@ -115,236 +121,14 @@ export function unpackRuntimeMessage(message) {
115
121
  message.type = innerContents.type;
116
122
  message.contents = innerContents.contents;
117
123
  }
118
- assert(isUnpackedRuntimeMessage(message), 0x122 /* "Message to unpack is not proper runtime message" */);
124
+ return true;
119
125
  }
120
126
  else {
121
127
  // Legacy format, but it's already "unpacked",
122
128
  // i.e. message.type is actually ContainerMessageType.
129
+ // Or it's non-runtime message.
123
130
  // Nothing to do in such case.
124
- }
125
- return message;
126
- }
127
- /**
128
- * This class controls pausing and resuming of inbound queue to ensure that we never
129
- * start processing ops in a batch IF we do not have all ops in the batch.
130
- */
131
- class ScheduleManagerCore {
132
- constructor(deltaManager, logger) {
133
- this.deltaManager = deltaManager;
134
- this.logger = logger;
135
- this.localPaused = false;
136
- this.timePaused = 0;
137
- this.batchCount = 0;
138
- // Listen for delta manager sends and add batch metadata to messages
139
- this.deltaManager.on("prepareSend", (messages) => {
140
- if (messages.length === 0) {
141
- return;
142
- }
143
- // First message will have the batch flag set to true if doing a batched send
144
- const firstMessageMetadata = messages[0].metadata;
145
- if (!(firstMessageMetadata === null || firstMessageMetadata === void 0 ? void 0 : firstMessageMetadata.batch)) {
146
- return;
147
- }
148
- // If the batch contains only a single op, clear the batch flag.
149
- if (messages.length === 1) {
150
- delete firstMessageMetadata.batch;
151
- return;
152
- }
153
- // Set the batch flag to false on the last message to indicate the end of the send batch
154
- const lastMessage = messages[messages.length - 1];
155
- lastMessage.metadata = Object.assign(Object.assign({}, lastMessage.metadata), { batch: false });
156
- });
157
- // Listen for updates and peek at the inbound
158
- this.deltaManager.inbound.on("push", (message) => {
159
- this.trackPending(message);
160
- });
161
- // Start with baseline - empty inbound queue.
162
- assert(!this.localPaused, 0x293 /* "initial state" */);
163
- const allPending = this.deltaManager.inbound.toArray();
164
- for (const pending of allPending) {
165
- this.trackPending(pending);
166
- }
167
- // We are intentionally directly listening to the "op" to inspect system ops as well.
168
- // If we do not observe system ops, we are likely to hit 0x296 assert when system ops
169
- // precedes start of incomplete batch.
170
- this.deltaManager.on("op", (message) => this.afterOpProcessing(message.sequenceNumber));
171
- }
172
- /**
173
- * The only public function in this class - called when we processed an op,
174
- * to make decision if op processing should be paused or not afer that.
175
- */
176
- afterOpProcessing(sequenceNumber) {
177
- assert(!this.localPaused, 0x294 /* "can't have op processing paused if we are processing an op" */);
178
- // If the inbound queue is ever empty, nothing to do!
179
- if (this.deltaManager.inbound.length === 0) {
180
- assert(this.pauseSequenceNumber === undefined, 0x295 /* "there should be no pending batch if we have no ops" */);
181
- return;
182
- }
183
- // The queue is
184
- // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
185
- // - here (processing ops until reaching start of incomplete batch)
186
- // - in trackPending(), when queue was empty and start of batch showed up.
187
- // 2. resumed when batch end comes in (in trackPending())
188
- // do we have incomplete batch to worry about?
189
- if (this.pauseSequenceNumber !== undefined) {
190
- assert(sequenceNumber < this.pauseSequenceNumber, 0x296 /* "we should never start processing incomplete batch!" */);
191
- // If the next op is the start of incomplete batch, then we can't process it until it's fully in - pause!
192
- if (sequenceNumber + 1 === this.pauseSequenceNumber) {
193
- this.pauseQueue();
194
- }
195
- }
196
- }
197
- pauseQueue() {
198
- assert(!this.localPaused, 0x297 /* "always called from resumed state" */);
199
- this.localPaused = true;
200
- this.timePaused = performance.now();
201
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
202
- this.deltaManager.inbound.pause();
203
- }
204
- resumeQueue(startBatch, messageEndBatch) {
205
- const endBatch = messageEndBatch.sequenceNumber;
206
- const duration = this.localPaused ? (performance.now() - this.timePaused) : undefined;
207
- this.batchCount++;
208
- if (this.batchCount % 1000 === 1) {
209
- this.logger.sendTelemetryEvent({
210
- eventName: "BatchStats",
211
- sequenceNumber: endBatch,
212
- length: endBatch - startBatch + 1,
213
- msnDistance: endBatch - messageEndBatch.minimumSequenceNumber,
214
- duration,
215
- batchCount: this.batchCount,
216
- interrupted: this.localPaused,
217
- });
218
- }
219
- // Return early if no change in value
220
- if (!this.localPaused) {
221
- return;
222
- }
223
- this.localPaused = false;
224
- // Random round number - we want to know when batch waiting paused op processing.
225
- if (duration !== undefined && duration > latencyThreshold) {
226
- this.logger.sendErrorEvent({
227
- eventName: "MaxBatchWaitTimeExceeded",
228
- duration,
229
- sequenceNumber: endBatch,
230
- length: endBatch - startBatch,
231
- });
232
- }
233
- this.deltaManager.inbound.resume();
234
- }
235
- /**
236
- * Called for each incoming op (i.e. inbound "push" notification)
237
- */
238
- trackPending(message) {
239
- assert(this.deltaManager.inbound.length !== 0, 0x298 /* "we have something in the queue that generates this event" */);
240
- assert((this.currentBatchClientId === undefined) === (this.pauseSequenceNumber === undefined), 0x299 /* "non-synchronized state" */);
241
- const metadata = message.metadata;
242
- const batchMetadata = metadata === null || metadata === void 0 ? void 0 : metadata.batch;
243
- // Protocol messages are never part of a runtime batch of messages
244
- if (!isUnpackedRuntimeMessage(message)) {
245
- // Protocol messages should never show up in the middle of the batch!
246
- assert(this.currentBatchClientId === undefined, 0x29a /* "System message in the middle of batch!" */);
247
- assert(batchMetadata === undefined, 0x29b /* "system op in a batch?" */);
248
- assert(!this.localPaused, 0x29c /* "we should be processing ops when there is no active batch" */);
249
- return;
250
- }
251
- if (this.currentBatchClientId === undefined && batchMetadata === undefined) {
252
- assert(!this.localPaused, 0x29d /* "we should be processing ops when there is no active batch" */);
253
- return;
254
- }
255
- // If the client ID changes then we can move the pause point. If it stayed the same then we need to check.
256
- // If batchMetadata is not undefined then if it's true we've begun a new batch - if false we've ended
257
- // the previous one
258
- if (this.currentBatchClientId !== undefined || batchMetadata === false) {
259
- if (this.currentBatchClientId !== message.clientId) {
260
- // "Batch not closed, yet message from another client!"
261
- throw new DataCorruptionError("OpBatchIncomplete", Object.assign({ runtimeVersion: pkgVersion, batchClientId: this.currentBatchClientId }, extractSafePropertiesFromMessage(message)));
262
- }
263
- }
264
- // The queue is
265
- // 1. paused only when the next message to be processed is the beginning of a batch. Done in two places:
266
- // - in afterOpProcessing() - processing ops until reaching start of incomplete batch
267
- // - here (batchMetadata == false below), when queue was empty and start of batch showed up.
268
- // 2. resumed when batch end comes in (batchMetadata === true case below)
269
- if (batchMetadata) {
270
- assert(this.currentBatchClientId === undefined, 0x29e /* "there can't be active batch" */);
271
- assert(!this.localPaused, 0x29f /* "we should be processing ops when there is no active batch" */);
272
- this.pauseSequenceNumber = message.sequenceNumber;
273
- this.currentBatchClientId = message.clientId;
274
- // Start of the batch
275
- // Only pause processing if queue has no other ops!
276
- // If there are any other ops in the queue, processing will be stopped when they are processed!
277
- if (this.deltaManager.inbound.length === 1) {
278
- this.pauseQueue();
279
- }
280
- }
281
- else if (batchMetadata === false) {
282
- assert(this.pauseSequenceNumber !== undefined, 0x2a0 /* "batch presence was validated above" */);
283
- // Batch is complete, we can process it!
284
- this.resumeQueue(this.pauseSequenceNumber, message);
285
- this.pauseSequenceNumber = undefined;
286
- this.currentBatchClientId = undefined;
287
- }
288
- else {
289
- // Continuation of current batch. Do nothing
290
- assert(this.currentBatchClientId !== undefined, 0x2a1 /* "logic error" */);
291
- }
292
- }
293
- }
294
- /**
295
- * This class has the following responsibilities:
296
- * 1. It tracks batches as we process ops and raises "batchBegin" and "batchEnd" events.
297
- * As part of it, it validates batch correctness (i.e. no system ops in the middle of batch)
298
- * 2. It creates instance of ScheduleManagerCore that ensures we never start processing ops from batch
299
- * unless all ops of the batch are in.
300
- */
301
- export class ScheduleManager {
302
- constructor(deltaManager, emitter, logger) {
303
- this.deltaManager = deltaManager;
304
- this.emitter = emitter;
305
- this.logger = logger;
306
- this.hitError = false;
307
- this.deltaScheduler = new DeltaScheduler(this.deltaManager, ChildLogger.create(this.logger, "DeltaScheduler"));
308
- void new ScheduleManagerCore(deltaManager, logger);
309
- }
310
- beforeOpProcessing(message) {
311
- var _a;
312
- if (this.batchClientId !== message.clientId) {
313
- assert(this.batchClientId === undefined, 0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */);
314
- // This could be the beginning of a new batch or an individual message.
315
- this.emitter.emit("batchBegin", message);
316
- this.deltaScheduler.batchBegin(message);
317
- const batch = (_a = message === null || message === void 0 ? void 0 : message.metadata) === null || _a === void 0 ? void 0 : _a.batch;
318
- if (batch) {
319
- this.batchClientId = message.clientId;
320
- }
321
- else {
322
- this.batchClientId = undefined;
323
- }
324
- }
325
- }
326
- afterOpProcessing(error, message) {
327
- var _a;
328
- // If this is no longer true, we need to revisit what we do where we set this.hitError.
329
- assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
330
- if (error) {
331
- // We assume here that loader will close container and stop processing all future ops.
332
- // This is implicit dependency. If this flow changes, this code might no longer be correct.
333
- this.hitError = true;
334
- this.batchClientId = undefined;
335
- this.emitter.emit("batchEnd", error, message);
336
- this.deltaScheduler.batchEnd(message);
337
- return;
338
- }
339
- const batch = (_a = message === null || message === void 0 ? void 0 : message.metadata) === null || _a === void 0 ? void 0 : _a.batch;
340
- // If no batchClientId has been set then we're in an individual batch. Else, if we get
341
- // batch end metadata, this is end of the current batch.
342
- if (this.batchClientId === undefined || batch === false) {
343
- this.batchClientId = undefined;
344
- this.emitter.emit("batchEnd", undefined, message);
345
- this.deltaScheduler.batchEnd(message);
346
- return;
347
- }
131
+ return false;
348
132
  }
349
133
  }
350
134
  /**
@@ -373,7 +157,7 @@ export function getDeviceSpec() {
373
157
  */
374
158
  export class ContainerRuntime extends TypedEventEmitter {
375
159
  constructor(context, registry, metadata, electedSummarizerData, chunks, dataStoreAliasMap, runtimeOptions, containerScope, logger, existing, blobManagerSnapshot, _storage, requestHandler, summaryConfiguration) {
376
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
160
+ var _a, _b, _c, _d;
377
161
  if (summaryConfiguration === void 0) { summaryConfiguration = Object.assign(Object.assign({}, DefaultSummaryConfiguration), (_a = runtimeOptions.summaryOptions) === null || _a === void 0 ? void 0 : _a.summaryConfigOverrides); }
378
162
  super();
379
163
  this.context = context;
@@ -384,9 +168,8 @@ export class ContainerRuntime extends TypedEventEmitter {
384
168
  this._storage = _storage;
385
169
  this.requestHandler = requestHandler;
386
170
  this.summaryConfiguration = summaryConfiguration;
387
- this.defaultMaxConsecutiveReconnects = 15;
171
+ this.defaultMaxConsecutiveReconnects = 7;
388
172
  this._orderSequentiallyCalls = 0;
389
- this.needsFlush = false;
390
173
  this.flushTrigger = false;
391
174
  this.savedOps = [];
392
175
  this.consecutiveReconnects = 0;
@@ -399,6 +182,12 @@ export class ContainerRuntime extends TypedEventEmitter {
399
182
  signalTimestamp: 0,
400
183
  trackingSignalSequenceNumber: undefined,
401
184
  };
185
+ // Provide lower soft limit - we want to have some number of ops to get efficiency in compression & bandwidth usage,
186
+ // but at the same time we want to send these ops sooner, to reduce overall latency of processing a batch.
187
+ // So there is some ballance here, that depends on compression algorithm and its efficiency working with smaller
188
+ // payloads. That number represents final (compressed) bits (once compression is implemented).
189
+ this.pendingAttachBatch = new BatchManager(64 * 1024);
190
+ this.pendingBatch = new BatchManager();
402
191
  this.summarizeOnDemand = (...args) => {
403
192
  if (this.clientDetails.type === summarizerClientType) {
404
193
  return this.summarizer.summarizeOnDemand(...args);
@@ -428,26 +217,23 @@ export class ContainerRuntime extends TypedEventEmitter {
428
217
  }
429
218
  };
430
219
  this.messageAtLastSummary = metadata === null || metadata === void 0 ? void 0 : metadata.message;
431
- // Default to false (enabled).
432
- this.disableIsolatedChannels = (_b = this.runtimeOptions.summaryOptions.disableIsolatedChannels) !== null && _b !== void 0 ? _b : false;
433
220
  this._connected = this.context.connected;
434
221
  this.chunkMap = new Map(chunks);
435
222
  this.handleContext = new ContainerFluidHandleContext("", this);
436
223
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.logger, "ContainerRuntime"));
224
+ if (this.summaryConfiguration.state === "enabled") {
225
+ this.validateSummaryHeuristicConfiguration(this.summaryConfiguration);
226
+ }
437
227
  this.summariesDisabled = this.isSummariesDisabled();
438
228
  this.heuristicsDisabled = this.isHeuristicsDisabled();
439
229
  this.summarizerClientElectionEnabled = this.isSummarizerClientElectionEnabled();
440
230
  this.maxOpsSinceLastSummary = this.getMaxOpsSinceLastSummary();
441
231
  this.initialSummarizerDelayMs = this.getInitialSummarizerDelayMs();
442
- this._aliasingEnabled =
443
- ((_c = this.mc.config.getBoolean(useDataStoreAliasingKey)) !== null && _c !== void 0 ? _c : false) ||
444
- ((_d = runtimeOptions.useDataStoreAliasing) !== null && _d !== void 0 ? _d : false);
445
- this._maxOpSizeInBytes = ((_e = this.mc.config.getNumber(maxOpSizeInBytesKey)) !== null && _e !== void 0 ? _e : defaultMaxOpSizeInBytes);
446
232
  this.maxConsecutiveReconnects =
447
- (_f = this.mc.config.getNumber(maxConsecutiveReconnectsKey)) !== null && _f !== void 0 ? _f : this.defaultMaxConsecutiveReconnects;
233
+ (_b = this.mc.config.getNumber(maxConsecutiveReconnectsKey)) !== null && _b !== void 0 ? _b : this.defaultMaxConsecutiveReconnects;
448
234
  this._flushMode = runtimeOptions.flushMode;
449
235
  const pendingRuntimeState = context.pendingLocalState;
450
- const baseSnapshot = (_g = pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.baseSnapshot) !== null && _g !== void 0 ? _g : context.baseSnapshot;
236
+ const baseSnapshot = (_c = pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.baseSnapshot) !== null && _c !== void 0 ? _c : context.baseSnapshot;
451
237
  this.garbageCollector = GarbageCollector.create({
452
238
  runtime: this,
453
239
  gcOptions: this.runtimeOptions.gcOptions,
@@ -459,6 +245,8 @@ export class ContainerRuntime extends TypedEventEmitter {
459
245
  getNodePackagePath: async (nodePath) => this.getGCNodePackagePath(nodePath),
460
246
  getLastSummaryTimestampMs: () => { var _a; return (_a = this.messageAtLastSummary) === null || _a === void 0 ? void 0 : _a.timestamp; },
461
247
  readAndParseBlob: async (id) => readAndParse(this.storage, id),
248
+ getContainerDiagnosticId: () => this.context.id,
249
+ activeConnection: () => this.deltaManager.active,
462
250
  });
463
251
  const loadedFromSequenceNumber = this.deltaManager.initialSequenceNumber;
464
252
  this.summarizerNode = createRootSummarizerNodeWithGC(ChildLogger.create(this.logger, "SummarizerNode"),
@@ -481,8 +269,12 @@ export class ContainerRuntime extends TypedEventEmitter {
481
269
  this.summarizerNode.loadBaseSummaryWithoutDifferential(baseSnapshot);
482
270
  }
483
271
  this.dataStores = new DataStores(getSummaryForDatastores(baseSnapshot, metadata), this, (attachMsg) => this.submit(ContainerMessageType.Attach, attachMsg), (id, createParam) => (summarizeInternal, getGCDataFn, getBaseGCDetailsFn) => this.summarizerNode.createChild(summarizeInternal, id, createParam, undefined, getGCDataFn, getBaseGCDetailsFn), (id) => this.summarizerNode.deleteChild(id), this.mc.logger, async () => this.garbageCollector.getBaseGCDetails(), (path, timestampMs, packagePath) => this.garbageCollector.nodeUpdated(path, "Changed", timestampMs, packagePath), new Map(dataStoreAliasMap), this.garbageCollector.writeDataAtRoot);
484
- this.blobManager = new BlobManager(this.handleContext, blobManagerSnapshot, () => this.storage, (blobId) => this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId }), (blobPath) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"), this, this.logger);
485
- this.scheduleManager = new ScheduleManager(context.deltaManager, this, ChildLogger.create(this.logger, "ScheduleManager"));
272
+ this.blobManager = new BlobManager(this.handleContext, blobManagerSnapshot, () => this.storage, (blobId, localId) => {
273
+ if (!this.disposed) {
274
+ this.submit(ContainerMessageType.BlobAttach, undefined, undefined, { blobId, localId });
275
+ }
276
+ }, (blobPath) => this.garbageCollector.nodeUpdated(blobPath, "Loaded"), this, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pendingAttachmentBlobs);
277
+ this.scheduleManager = new ScheduleManager(context.deltaManager, this, () => this.clientId, ChildLogger.create(this.logger, "ScheduleManager"));
486
278
  this.deltaSender = this.deltaManager;
487
279
  this.pendingStateManager = new PendingStateManager({
488
280
  applyStashedOp: this.applyStashedOp.bind(this),
@@ -492,7 +284,6 @@ export class ContainerRuntime extends TypedEventEmitter {
492
284
  flush: this.flush.bind(this),
493
285
  flushMode: () => this.flushMode,
494
286
  reSubmit: this.reSubmit.bind(this),
495
- rollback: this.rollback.bind(this),
496
287
  setFlushMode: (mode) => this.setFlushMode(mode),
497
288
  }, this._flushMode, pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.pending);
498
289
  this.context.quorum.on("removeMember", (clientId) => {
@@ -572,9 +363,9 @@ export class ContainerRuntime extends TypedEventEmitter {
572
363
  createContainerRuntimeVersion: metadata === null || metadata === void 0 ? void 0 : metadata.createContainerRuntimeVersion,
573
364
  createContainerTimestamp: metadata === null || metadata === void 0 ? void 0 : metadata.createContainerTimestamp,
574
365
  };
575
- // back-compat 0.59.3000 - Older document may either write summaryCount or not write it at all. If it does
576
- // not write it, initialize summaryNumber to 0.
577
- loadSummaryNumber = (_j = (_h = metadata === null || metadata === void 0 ? void 0 : metadata.summaryNumber) !== null && _h !== void 0 ? _h : metadata === null || metadata === void 0 ? void 0 : metadata.summaryCount) !== null && _j !== void 0 ? _j : 0;
366
+ // summaryNumber was renamed from summaryCount. For older docs that haven't been opened for a long time,
367
+ // the count is reset to 0.
368
+ loadSummaryNumber = (_d = metadata === null || metadata === void 0 ? void 0 : metadata.summaryNumber) !== null && _d !== void 0 ? _d : 0;
578
369
  }
579
370
  else {
580
371
  this.createContainerMetadata = {
@@ -587,7 +378,6 @@ export class ContainerRuntime extends TypedEventEmitter {
587
378
  this.logger.sendTelemetryEvent(Object.assign(Object.assign(Object.assign({ eventName: "ContainerLoadStats" }, this.createContainerMetadata), this.dataStores.containerLoadStats), { summaryNumber: loadSummaryNumber, summaryFormatVersion: metadata === null || metadata === void 0 ? void 0 : metadata.summaryFormatVersion, disableIsolatedChannels: metadata === null || metadata === void 0 ? void 0 : metadata.disableIsolatedChannels, gcVersion: metadata === null || metadata === void 0 ? void 0 : metadata.gcFeature }));
588
379
  ReportOpPerfTelemetry(this.context.clientId, this.deltaManager, this.logger);
589
380
  BindBatchTracker(this, this.logger);
590
- this.opTracker = new OpTracker(this.deltaManager, this.mc.config.getBoolean(disableOpTrackingKey) === true);
591
381
  }
592
382
  get IContainerRuntime() { return this; }
593
383
  get IFluidRouter() { return this; }
@@ -610,7 +400,7 @@ export class ContainerRuntime extends TypedEventEmitter {
610
400
  runtimeVersion: pkgVersion,
611
401
  },
612
402
  });
613
- const { summaryOptions = {}, gcOptions = {}, loadSequenceNumberVerification = "close", useDataStoreAliasing = false, flushMode = defaultFlushMode, enableOfflineLoad = false, } = runtimeOptions;
403
+ const { summaryOptions = {}, gcOptions = {}, loadSequenceNumberVerification = "close", flushMode = defaultFlushMode, enableOfflineLoad = false, } = runtimeOptions;
614
404
  const pendingRuntimeState = context.pendingLocalState;
615
405
  const baseSnapshot = (_b = pendingRuntimeState === null || pendingRuntimeState === void 0 ? void 0 : pendingRuntimeState.baseSnapshot) !== null && _b !== void 0 ? _b : context.baseSnapshot;
616
406
  const storage = !pendingRuntimeState ?
@@ -663,7 +453,6 @@ export class ContainerRuntime extends TypedEventEmitter {
663
453
  summaryOptions,
664
454
  gcOptions,
665
455
  loadSequenceNumberVerification,
666
- useDataStoreAliasing,
667
456
  flushMode,
668
457
  enableOfflineLoad,
669
458
  }, containerScope, logger, loadExisting, blobManagerSnapshot, storage, requestHandler);
@@ -721,6 +510,9 @@ export class ContainerRuntime extends TypedEventEmitter {
721
510
  return (_a = this.summarizerClientElection) === null || _a === void 0 ? void 0 : _a.electedClientId;
722
511
  }
723
512
  get disposed() { return this._disposed; }
513
+ get emptyBatch() {
514
+ return this.pendingBatch.empty && this.pendingAttachBatch.empty;
515
+ }
724
516
  get summarizer() {
725
517
  assert(this._summarizer !== undefined, 0x257 /* "This is not summarizing container" */);
726
518
  return this._summarizer;
@@ -752,12 +544,9 @@ export class ContainerRuntime extends TypedEventEmitter {
752
544
  if (this.runtimeOptions.summaryOptions.summarizerClientElection === true) {
753
545
  return true;
754
546
  }
755
- if (this.summaryConfiguration.state !== "disabled") {
756
- return this.summaryConfiguration.summarizerClientElection === true;
757
- }
758
- else {
759
- return false;
760
- }
547
+ return this.summaryConfiguration.state !== "disabled"
548
+ ? this.summaryConfiguration.summarizerClientElection === true
549
+ : false;
761
550
  }
762
551
  getMaxOpsSinceLastSummary() {
763
552
  // back-compat: maxOpsSinceLastSummary was moved from ISummaryRuntimeOptions
@@ -765,12 +554,9 @@ export class ContainerRuntime extends TypedEventEmitter {
765
554
  if (this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary !== undefined) {
766
555
  return this.runtimeOptions.summaryOptions.maxOpsSinceLastSummary;
767
556
  }
768
- if (this.summaryConfiguration.state !== "disabled") {
769
- return this.summaryConfiguration.maxOpsSinceLastSummary;
770
- }
771
- else {
772
- return 0;
773
- }
557
+ return this.summaryConfiguration.state !== "disabled"
558
+ ? this.summaryConfiguration.maxOpsSinceLastSummary
559
+ : 0;
774
560
  }
775
561
  getInitialSummarizerDelayMs() {
776
562
  // back-compat: initialSummarizerDelayMs was moved from ISummaryRuntimeOptions
@@ -778,12 +564,9 @@ export class ContainerRuntime extends TypedEventEmitter {
778
564
  if (this.runtimeOptions.summaryOptions.initialSummarizerDelayMs !== undefined) {
779
565
  return this.runtimeOptions.summaryOptions.initialSummarizerDelayMs;
780
566
  }
781
- if (this.summaryConfiguration.state !== "disabled") {
782
- return this.summaryConfiguration.initialSummarizerDelayMs;
783
- }
784
- else {
785
- return 0;
786
- }
567
+ return this.summaryConfiguration.state !== "disabled"
568
+ ? this.summaryConfiguration.initialSummarizerDelayMs
569
+ : 0;
787
570
  }
788
571
  dispose(error) {
789
572
  var _a;
@@ -856,17 +639,13 @@ export class ContainerRuntime extends TypedEventEmitter {
856
639
  return this.resolveHandle(requestParser.createSubRequest(1));
857
640
  }
858
641
  if (id === BlobManager.basePath && requestParser.isLeaf(2)) {
859
- const handle = await this.blobManager.getBlob(requestParser.pathParts[1]);
860
- if (handle) {
861
- return {
642
+ const blob = await this.blobManager.getBlob(requestParser.pathParts[1]);
643
+ return blob
644
+ ? {
862
645
  status: 200,
863
646
  mimeType: "fluid/object",
864
- value: handle.get(),
865
- };
866
- }
867
- else {
868
- return create404Response(request);
869
- }
647
+ value: blob,
648
+ } : create404Response(request);
870
649
  }
871
650
  else if (requestParser.pathParts.length > 0) {
872
651
  const dataStore = await this.getDataStoreFromRequest(id, request);
@@ -884,13 +663,14 @@ export class ContainerRuntime extends TypedEventEmitter {
884
663
  }
885
664
  internalId(maybeAlias) {
886
665
  var _a;
887
- return (_a = this.dataStores.aliases().get(maybeAlias)) !== null && _a !== void 0 ? _a : maybeAlias;
666
+ return (_a = this.dataStores.aliases.get(maybeAlias)) !== null && _a !== void 0 ? _a : maybeAlias;
888
667
  }
889
668
  async getDataStoreFromRequest(id, request) {
890
669
  var _a, _b, _c;
891
670
  const wait = typeof ((_a = request.headers) === null || _a === void 0 ? void 0 : _a[RuntimeHeaders.wait]) === "boolean"
892
671
  ? (_b = request.headers) === null || _b === void 0 ? void 0 : _b[RuntimeHeaders.wait]
893
672
  : true;
673
+ await this.dataStores.waitIfPendingAlias(id);
894
674
  const internalId = this.internalId(id);
895
675
  const dataStoreContext = await this.dataStores.getDataStore(internalId, wait);
896
676
  /**
@@ -920,10 +700,8 @@ export class ContainerRuntime extends TypedEventEmitter {
920
700
  addMetadataToSummary(summaryTree) {
921
701
  var _a;
922
702
  const metadata = Object.assign(Object.assign(Object.assign(Object.assign({}, this.createContainerMetadata), {
923
- // back-compat 0.59.3000: This is renamed to summaryNumber. Can be removed when 0.59.3000 saturates.
924
- summaryCount: this.nextSummaryNumber,
925
703
  // Increment the summary number for the next summary that will be generated.
926
- summaryNumber: this.nextSummaryNumber++, summaryFormatVersion: 1, disableIsolatedChannels: this.disableIsolatedChannels || undefined }), this.garbageCollector.getMetadata()), {
704
+ summaryNumber: this.nextSummaryNumber++, summaryFormatVersion: 1 }), this.garbageCollector.getMetadata()), {
927
705
  // The last message processed at the time of summary. If there are no new messages, use the message from the
928
706
  // last summary.
929
707
  message: (_a = extractSummaryMetadataMessage(this.deltaManager.lastMessage)) !== null && _a !== void 0 ? _a : this.messageAtLastSummary });
@@ -936,7 +714,7 @@ export class ContainerRuntime extends TypedEventEmitter {
936
714
  const content = JSON.stringify([...this.chunkMap]);
937
715
  addBlobToSummary(summaryTree, chunksBlobName, content);
938
716
  }
939
- const dataStoreAliases = this.dataStores.aliases();
717
+ const dataStoreAliases = this.dataStores.aliases;
940
718
  if (dataStoreAliases.size > 0) {
941
719
  addBlobToSummary(summaryTree, aliasBlobName, JSON.stringify([...dataStoreAliases]));
942
720
  }
@@ -967,7 +745,7 @@ export class ContainerRuntime extends TypedEventEmitter {
967
745
  // Feature disabled, we never stop reconnecting
968
746
  return true;
969
747
  }
970
- if (!this.pendingStateManager.hasPendingMessages()) {
748
+ if (!this.hasPendingMessages()) {
971
749
  // If there are no pending messages, we can always reconnect
972
750
  this.resetReconnectCount();
973
751
  return true;
@@ -1033,22 +811,55 @@ export class ContainerRuntime extends TypedEventEmitter {
1033
811
  }
1034
812
  }
1035
813
  setConnectionState(connected, clientId) {
814
+ if (connected === false && this.delayConnectClientId !== undefined) {
815
+ this.delayConnectClientId = undefined;
816
+ this.mc.logger.sendTelemetryEvent({
817
+ eventName: "UnsuccessfulConnectedTransition",
818
+ });
819
+ // Don't propagate "disconnected" event because we didn't propagate the previous "connected" event
820
+ return;
821
+ }
822
+ // If attachment blobs were added while disconnected, we need to delay
823
+ // propagation of the "connected" event until we have uploaded them to
824
+ // ensure we don't submit ops referencing a blob that has not been uploaded
825
+ const connecting = connected && !this._connected && !this.deltaManager.readOnlyInfo.readonly;
826
+ if (connecting && this.blobManager.hasPendingOfflineUploads) {
827
+ assert(!this.delayConnectClientId, 0x392 /* Connect event delay must be canceled before subsequent connect event */);
828
+ assert(!!clientId, 0x393 /* Must have clientId when connecting */);
829
+ this.delayConnectClientId = clientId;
830
+ this.blobManager.onConnected().then(() => {
831
+ // make sure we didn't reconnect before the promise resolved
832
+ if (this.delayConnectClientId === clientId && !this.disposed) {
833
+ this.delayConnectClientId = undefined;
834
+ this.setConnectionStateCore(connected, clientId);
835
+ }
836
+ }, (error) => this.closeFn(error));
837
+ return;
838
+ }
839
+ this.setConnectionStateCore(connected, clientId);
840
+ }
841
+ setConnectionStateCore(connected, clientId) {
842
+ assert(!this.delayConnectClientId, 0x394 /* connect event delay must be cleared before propagating connect event */);
1036
843
  this.verifyNotClosed();
1037
844
  // There might be no change of state due to Container calling this API after loading runtime.
1038
845
  const changeOfState = this._connected !== connected;
1039
- const reconnection = changeOfState && connected;
846
+ const reconnection = changeOfState && !connected;
1040
847
  this._connected = connected;
1041
848
  if (!connected) {
1042
849
  this._perfSignalData.signalsLost = 0;
1043
850
  this._perfSignalData.signalTimestamp = 0;
1044
851
  this._perfSignalData.trackingSignalSequenceNumber = undefined;
1045
852
  }
853
+ else {
854
+ assert(this.attachState === AttachState.Attached, 0x3cd /* Connection is possible only if container exists in storage */);
855
+ }
856
+ // Fail while disconnected
1046
857
  if (reconnection) {
1047
858
  this.consecutiveReconnects++;
1048
859
  if (!this.shouldContinueReconnecting()) {
1049
- this.closeFn(
1050
- // pre-0.58 error message: MaxReconnectsWithNoProgress
1051
- DataProcessingError.create("Runtime detected too many reconnects with no progress syncing local ops", "setConnectionState", undefined, {
860
+ this.closeFn(DataProcessingError.create(
861
+ // eslint-disable-next-line max-len
862
+ "Runtime detected too many reconnects with no progress syncing local ops. Batch of ops is likely too large (over 1Mb)", "setConnectionState", undefined, {
1052
863
  dataLoss: 1,
1053
864
  attempts: this.consecutiveReconnects,
1054
865
  pendingMessages: this.pendingStateManager.pendingMessagesCount,
@@ -1060,46 +871,48 @@ export class ContainerRuntime extends TypedEventEmitter {
1060
871
  this.replayPendingStates();
1061
872
  }
1062
873
  this.dataStores.setConnectionState(connected, clientId);
874
+ this.garbageCollector.setConnectionState(connected, clientId);
1063
875
  raiseConnectedEvent(this.mc.logger, this, connected, clientId);
1064
876
  }
1065
877
  process(messageArg, local) {
1066
- var _a, _b;
878
+ var _a;
1067
879
  this.verifyNotClosed();
1068
- // If it's not message for runtime, bail out right away.
1069
- if (!isUnpackedRuntimeMessage(messageArg)) {
1070
- return;
1071
- }
1072
- if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
1073
- this.savedOps.push(messageArg);
1074
- }
1075
880
  // Do shallow copy of message, as methods below will modify it.
1076
881
  // There might be multiple container instances receiving same message
1077
882
  // We do not need to make deep copy, as each layer will just replace message.content itself,
1078
883
  // but would not modify contents details
1079
884
  let message = Object.assign({}, messageArg);
885
+ // back-compat: ADO #1385: eventually should become unconditional, but only for runtime messages!
886
+ // System message may have no contents, or in some cases (mostly for back-compat) they may have actual objects.
887
+ // Old ops may contain empty string (I assume noops).
888
+ if (typeof message.contents === "string" && message.contents !== "") {
889
+ message.contents = JSON.parse(message.contents);
890
+ }
891
+ // Caveat: This will return false for runtime message in very old format, that are used in snapshot tests
892
+ // This format was not shipped to production workflows.
893
+ const runtimeMessage = unpackRuntimeMessage(message);
894
+ if ((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad) {
895
+ this.savedOps.push(messageArg);
896
+ }
1080
897
  // Surround the actual processing of the operation with messages to the schedule manager indicating
1081
898
  // the beginning and end. This allows it to emit appropriate events and/or pause the processing of new
1082
899
  // messages once a batch has been fully processed.
1083
900
  this.scheduleManager.beforeOpProcessing(message);
1084
901
  try {
1085
- message = unpackRuntimeMessage(message);
1086
902
  // Chunk processing must come first given that we will transform the message to the unchunked version
1087
903
  // once all pieces are available
1088
904
  message = this.processRemoteChunkedMessage(message);
1089
905
  let localOpMetadata;
1090
- if (local) {
1091
- // Call the PendingStateManager to process local messages.
1092
- // Do not process local chunked ops until all pieces are available.
1093
- if (message.type !== ContainerMessageType.ChunkedOp) {
1094
- localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1095
- }
906
+ if (local && runtimeMessage) {
907
+ localOpMetadata = this.pendingStateManager.processPendingLocalMessage(message);
1096
908
  }
1097
909
  // If there are no more pending messages after processing a local message,
1098
910
  // the document is no longer dirty.
1099
- if (!this.pendingStateManager.hasPendingMessages()) {
911
+ if (!this.hasPendingMessages()) {
1100
912
  this.updateDocumentDirtyState(false);
1101
913
  }
1102
- switch (message.type) {
914
+ const type = message.type;
915
+ switch (type) {
1103
916
  case ContainerMessageType.Attach:
1104
917
  this.dataStores.processAttachMessage(message, local);
1105
918
  break;
@@ -1110,12 +923,18 @@ export class ContainerRuntime extends TypedEventEmitter {
1110
923
  this.dataStores.processFluidDataStoreOp(message, local, localOpMetadata);
1111
924
  break;
1112
925
  case ContainerMessageType.BlobAttach:
1113
- assert((_b = message === null || message === void 0 ? void 0 : message.metadata) === null || _b === void 0 ? void 0 : _b.blobId, 0x12a /* "Missing blob id on metadata" */);
1114
- this.blobManager.processBlobAttachOp(message.metadata.blobId, local);
926
+ this.blobManager.processBlobAttachOp(message, local);
927
+ break;
928
+ case ContainerMessageType.ChunkedOp:
929
+ case ContainerMessageType.Rejoin:
1115
930
  break;
1116
931
  default:
932
+ assert(!runtimeMessage, 0x3ce /* Runtime message of unknown type */);
933
+ }
934
+ // For back-compat, notify only about runtime messages for now.
935
+ if (runtimeMessage) {
936
+ this.emit("op", message, runtimeMessage);
1117
937
  }
1118
- this.emit("op", message);
1119
938
  this.scheduleManager.afterOpProcessing(undefined, message);
1120
939
  if (local) {
1121
940
  // If we have processed a local op, this means that the container is
@@ -1181,6 +1000,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1181
1000
  this.dataStores.processSignal(envelope.address, transformed, local);
1182
1001
  }
1183
1002
  async getRootDataStore(id, wait = true) {
1003
+ return this.getRootDataStoreChannel(id, wait);
1004
+ }
1005
+ async getRootDataStoreChannel(id, wait = true) {
1006
+ await this.dataStores.waitIfPendingAlias(id);
1184
1007
  const internalId = this.internalId(id);
1185
1008
  const context = await this.dataStores.getDataStore(internalId, wait);
1186
1009
  assert(await context.isRoot(), 0x12b /* "did not get root data store" */);
@@ -1205,25 +1028,57 @@ export class ContainerRuntime extends TypedEventEmitter {
1205
1028
  }
1206
1029
  flush() {
1207
1030
  assert(this._orderSequentiallyCalls === 0, 0x24c /* "Cannot call `flush()` from `orderSequentially`'s callback" */);
1208
- if (!this.deltaSender) {
1209
- return;
1031
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1032
+ this.flushBatch(this.pendingBatch.popBatch());
1033
+ assert(this.emptyBatch, 0x3cf /* reentrancy */);
1034
+ }
1035
+ flushBatch(batch) {
1036
+ const length = batch.length;
1037
+ if (length > 1) {
1038
+ batch[0].metadata = Object.assign(Object.assign({}, batch[0].metadata), { batch: true });
1039
+ batch[length - 1].metadata = Object.assign(Object.assign({}, batch[length - 1].metadata), { batch: false });
1040
+ // This assert fires for the following reason (there might be more cases like that):
1041
+ // AgentScheduler will send ops in response to ConsensusRegisterCollection's "atomicChanged" event handler,
1042
+ // i.e. in the middle of op processing!
1043
+ // Sending ops while processing ops is not good idea - it's not defined when
1044
+ // referenceSequenceNumber changes in op processing sequence (at the beginning or end of op processing),
1045
+ // If we send ops in response to processing multiple ops, then we for sure hit this assert!
1046
+ // Tracked via ADO #1834
1047
+ // assert(batch[0].referenceSequenceNumber === batch[length - 1].referenceSequenceNumber,
1048
+ // "Batch should be generated synchronously, without processing ops in the middle!");
1210
1049
  }
1211
- // Let the PendingStateManager know that there was an attempt to flush messages.
1212
- // Note that this should happen before the `this.needsFlush` check below because in the scenario where we are
1213
- // not connected, `this.needsFlush` will be false but the PendingStateManager might have pending messages and
1214
- // hence needs to track this.
1215
- this.pendingStateManager.onFlush();
1216
- // If flush has already been called then exit early
1217
- if (!this.needsFlush) {
1218
- return;
1219
- }
1220
- this.needsFlush = false;
1050
+ let clientSequenceNumber = -1;
1221
1051
  // Did we disconnect in the middle of turn-based batch?
1222
1052
  // If so, do nothing, as pending state manager will resubmit it correctly on reconnect.
1223
- if (!this.canSendOps()) {
1224
- return;
1053
+ if (this.canSendOps()) {
1054
+ if (this.context.submitBatchFn !== undefined) {
1055
+ const batchToSend = [];
1056
+ for (const message of batch) {
1057
+ batchToSend.push({ contents: message.contents, metadata: message.metadata });
1058
+ }
1059
+ // returns clientSequenceNumber of last message in a batch
1060
+ clientSequenceNumber = this.context.submitBatchFn(batchToSend);
1061
+ }
1062
+ else {
1063
+ // Legacy path - supporting old loader versions. Can be removed only when LTS moves above
1064
+ // version that has support for batches (submitBatchFn)
1065
+ for (const message of batch) {
1066
+ clientSequenceNumber = this.context.submitFn(MessageType.Operation, message.deserializedContent, true, // batch
1067
+ message.metadata);
1068
+ }
1069
+ this.deltaSender.flush();
1070
+ }
1071
+ // Convert from clientSequenceNumber of last message in the batch to clientSequenceNumber of first message.
1072
+ clientSequenceNumber -= batch.length - 1;
1073
+ assert(clientSequenceNumber >= 0, 0x3d0 /* clientSequenceNumber can't be negative */);
1225
1074
  }
1226
- return this.deltaSender.flush();
1075
+ // Let the PendingStateManager know that a message was submitted.
1076
+ // In future, need to shift toward keeping batch as a whole!
1077
+ for (const message of batch) {
1078
+ this.pendingStateManager.onSubmitMessage(message.deserializedContent.type, clientSequenceNumber, message.referenceSequenceNumber, message.deserializedContent.contents, message.localOpMetadata, message.metadata);
1079
+ clientSequenceNumber++;
1080
+ }
1081
+ this.pendingStateManager.onFlush();
1227
1082
  }
1228
1083
  orderSequentially(callback) {
1229
1084
  // If flush mode is already TurnBased we are either
@@ -1248,7 +1103,10 @@ export class ContainerRuntime extends TypedEventEmitter {
1248
1103
  trackOrderSequentiallyCalls(callback) {
1249
1104
  let checkpoint;
1250
1105
  if (this.mc.config.getBoolean("Fluid.ContainerRuntime.EnableRollback")) {
1251
- checkpoint = this.pendingStateManager.checkpoint();
1106
+ // Note: we are not touching this.pendingAttachBatch here, for two reasons:
1107
+ // 1. It would not help, as we flush attach ops as they become available.
1108
+ // 2. There is no way to undo process of data store creation.
1109
+ checkpoint = this.pendingBatch.checkpoint();
1252
1110
  }
1253
1111
  try {
1254
1112
  this._orderSequentiallyCalls++;
@@ -1257,7 +1115,16 @@ export class ContainerRuntime extends TypedEventEmitter {
1257
1115
  catch (error) {
1258
1116
  if (checkpoint) {
1259
1117
  // This will throw and close the container if rollback fails
1260
- checkpoint.rollback();
1118
+ try {
1119
+ checkpoint.rollback((message) => this.rollback(message.deserializedContent.type, message.deserializedContent.contents, message.localOpMetadata));
1120
+ }
1121
+ catch (err) {
1122
+ const error2 = wrapError(err, (message) => {
1123
+ return DataProcessingError.create(`RollbackError: ${message}`, "checkpointRollback", undefined);
1124
+ });
1125
+ this.closeFn(error2);
1126
+ throw error2;
1127
+ }
1261
1128
  }
1262
1129
  else {
1263
1130
  // pre-0.58 error message: orderSequentiallyCallbackException
@@ -1271,67 +1138,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1271
1138
  }
1272
1139
  async createDataStore(pkg) {
1273
1140
  const internalId = uuid();
1274
- return channelToDataStore(await this._createDataStore(pkg, false /* isRoot */, internalId), internalId, this, this.dataStores, this.mc.logger);
1275
- }
1276
- /**
1277
- * Creates a root datastore directly with a user generated id and attaches it to storage.
1278
- * It is vulnerable to name collisions and should not be used.
1279
- *
1280
- * This method will be removed. See #6465.
1281
- */
1282
- async createRootDataStoreLegacy(pkg, rootDataStoreId) {
1283
- const fluidDataStore = await this._createDataStore(pkg, true /* isRoot */, rootDataStoreId);
1284
- // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel. For
1285
- // older versions, we still have to call bindToContext.
1286
- if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
1287
- fluidDataStore.makeVisibleAndAttachGraph();
1288
- }
1289
- else {
1290
- fluidDataStore.bindToContext();
1291
- }
1292
- return fluidDataStore;
1293
- }
1294
- /**
1295
- * @deprecated - will be removed in an upcoming release. See #9660.
1296
- */
1297
- async createRootDataStore(pkg, rootDataStoreId) {
1298
- if (rootDataStoreId.includes("/")) {
1299
- throw new UsageError(`Id cannot contain slashes: '${rootDataStoreId}'`);
1300
- }
1301
- return this._aliasingEnabled === true ?
1302
- this.createAndAliasDataStore(pkg, rootDataStoreId) :
1303
- this.createRootDataStoreLegacy(pkg, rootDataStoreId);
1304
- }
1305
- /**
1306
- * Creates a data store then attempts to alias it.
1307
- * If aliasing fails, it will raise an exception.
1308
- *
1309
- * This method will be removed. See #6465.
1310
- *
1311
- * @param pkg - Package name of the data store
1312
- * @param alias - Alias to be assigned to the data store
1313
- * @param props - Properties for the data store
1314
- * @returns - An aliased data store which can can be found / loaded by alias.
1315
- */
1316
- async createAndAliasDataStore(pkg, alias, props) {
1317
- const internalId = uuid();
1318
- const dataStore = await this._createDataStore(pkg, false /* isRoot */, internalId, props);
1319
- const aliasedDataStore = channelToDataStore(dataStore, internalId, this, this.dataStores, this.mc.logger);
1320
- const result = await aliasedDataStore.trySetAlias(alias);
1321
- if (result !== "Success") {
1322
- throw new GenericError("dataStoreAliasFailure", undefined /* error */, {
1323
- alias: {
1324
- value: alias,
1325
- tag: TelemetryDataTag.UserData,
1326
- },
1327
- internalId: {
1328
- value: internalId,
1329
- tag: TelemetryDataTag.PackageData,
1330
- },
1331
- aliasResult: result,
1332
- });
1333
- }
1334
- return aliasedDataStore;
1141
+ return channelToDataStore(await this._createDataStore(pkg, internalId), internalId, this, this.dataStores, this.mc.logger);
1335
1142
  }
1336
1143
  createDetachedRootDataStore(pkg, rootDataStoreId) {
1337
1144
  if (rootDataStoreId.includes("/")) {
@@ -1342,38 +1149,13 @@ export class ContainerRuntime extends TypedEventEmitter {
1342
1149
  createDetachedDataStore(pkg) {
1343
1150
  return this.dataStores.createDetachedDataStoreCore(pkg, false);
1344
1151
  }
1345
- /**
1346
- * Creates a possibly root datastore directly with a possibly user generated id and attaches it to storage.
1347
- * It is vulnerable to name collisions if both aforementioned conditions are true, and should not be used.
1348
- *
1349
- * This method will be removed. See #6465.
1350
- */
1351
- async _createDataStoreWithPropsLegacy(pkg, props, id = uuid(), isRoot = false) {
1352
- const fluidDataStore = await this.dataStores._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props).realize();
1353
- if (isRoot) {
1354
- // back-compat 0.59.1000 - makeVisibleAndAttachGraph was added in this version to IFluidDataStoreChannel.
1355
- // For older versions, we still have to call bindToContext.
1356
- if (fluidDataStore.makeVisibleAndAttachGraph !== undefined) {
1357
- fluidDataStore.makeVisibleAndAttachGraph();
1358
- }
1359
- else {
1360
- fluidDataStore.bindToContext();
1361
- }
1362
- this.logger.sendTelemetryEvent({
1363
- eventName: "Root datastore with props",
1364
- hasProps: props !== undefined,
1365
- });
1366
- }
1152
+ async _createDataStoreWithProps(pkg, props, id = uuid()) {
1153
+ const fluidDataStore = await this.dataStores._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props).realize();
1367
1154
  return channelToDataStore(fluidDataStore, id, this, this.dataStores, this.mc.logger);
1368
1155
  }
1369
- async _createDataStoreWithProps(pkg, props, id = uuid(), isRoot = false) {
1370
- return this._aliasingEnabled === true && isRoot ?
1371
- this.createAndAliasDataStore(pkg, id, props) :
1372
- this._createDataStoreWithPropsLegacy(pkg, props, id, isRoot);
1373
- }
1374
- async _createDataStore(pkg, isRoot, id = uuid(), props) {
1156
+ async _createDataStore(pkg, id = uuid(), props) {
1375
1157
  return this.dataStores
1376
- ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, isRoot, props)
1158
+ ._createFluidDataStoreContext(Array.isArray(pkg) ? pkg : [pkg], id, props)
1377
1159
  .realize();
1378
1160
  }
1379
1161
  canSendOps() {
@@ -1447,7 +1229,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1447
1229
  assert(this.attachState === AttachState.Attached, 0x12e /* "Container Context should already be in attached state" */);
1448
1230
  this.emit("attached");
1449
1231
  }
1450
- if (attachState === AttachState.Attached && !this.pendingStateManager.hasPendingMessages()) {
1232
+ if (attachState === AttachState.Attached && !this.hasPendingMessages()) {
1451
1233
  this.updateDocumentDirtyState(false);
1452
1234
  }
1453
1235
  this.dataStores.setAttachState(attachState);
@@ -1465,10 +1247,8 @@ export class ContainerRuntime extends TypedEventEmitter {
1465
1247
  this.blobManager.setRedirectTable(blobRedirectTable);
1466
1248
  }
1467
1249
  const summarizeResult = this.dataStores.createSummary(telemetryContext);
1468
- if (!this.disableIsolatedChannels) {
1469
- // Wrap data store summaries in .channels subtree.
1470
- wrapSummaryInChannelsTree(summarizeResult);
1471
- }
1250
+ // Wrap data store summaries in .channels subtree.
1251
+ wrapSummaryInChannelsTree(summarizeResult);
1472
1252
  this.addContainerStateToSummary(summarizeResult, true /* fullTree */, false /* trackState */, telemetryContext);
1473
1253
  return summarizeResult.summary;
1474
1254
  }
@@ -1483,12 +1263,9 @@ export class ContainerRuntime extends TypedEventEmitter {
1483
1263
  }
1484
1264
  async summarizeInternal(fullTree, trackState, telemetryContext) {
1485
1265
  const summarizeResult = await this.dataStores.summarize(fullTree, trackState, telemetryContext);
1486
- let pathPartsForChildren;
1487
- if (!this.disableIsolatedChannels) {
1488
- // Wrap data store summaries in .channels subtree.
1489
- wrapSummaryInChannelsTree(summarizeResult);
1490
- pathPartsForChildren = [channelsTreeName];
1491
- }
1266
+ // Wrap data store summaries in .channels subtree.
1267
+ wrapSummaryInChannelsTree(summarizeResult);
1268
+ const pathPartsForChildren = [channelsTreeName];
1492
1269
  this.addContainerStateToSummary(summarizeResult, fullTree, trackState, telemetryContext);
1493
1270
  return Object.assign(Object.assign({}, summarizeResult), { id: "", pathPartsForChildren });
1494
1271
  }
@@ -1616,7 +1393,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1616
1393
  }
1617
1394
  /**
1618
1395
  * Runs garbage collection and updates the reference / used state of the nodes in the container.
1619
- * @returns the statistics of the garbage collection run.
1396
+ * @returns the statistics of the garbage collection run; undefined if GC did not run.
1620
1397
  */
1621
1398
  async collectGarbage(options) {
1622
1399
  return this.garbageCollector.collectGarbage(options);
@@ -1639,7 +1416,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1639
1416
  * @param options - options controlling how the summary is generated or submitted
1640
1417
  */
1641
1418
  async submitSummary(options) {
1642
- var _a, _b, _c;
1419
+ var _a, _b;
1643
1420
  const { fullTree, refreshLatestAck, summaryLogger } = options;
1644
1421
  // The summary number for this summary. This will be updated during the summary process, so get it now and
1645
1422
  // use it for all events logged during this summary.
@@ -1647,6 +1424,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1647
1424
  const summaryNumberLogger = ChildLogger.create(summaryLogger, undefined, {
1648
1425
  all: { summaryNumber },
1649
1426
  });
1427
+ assert(this.emptyBatch, 0x3d1 /* Can't trigger summary in the middle of a batch */);
1650
1428
  let latestSnapshotVersionId;
1651
1429
  if (refreshLatestAck) {
1652
1430
  const latestSnapshotInfo = await this.refreshLatestSummaryAckFromServer(ChildLogger.create(summaryNumberLogger, undefined, { all: { safeSummary: true } }));
@@ -1667,17 +1445,11 @@ export class ContainerRuntime extends TypedEventEmitter {
1667
1445
  const summaryRefSeqNum = this.deltaManager.lastSequenceNumber;
1668
1446
  const minimumSequenceNumber = this.deltaManager.minimumSequenceNumber;
1669
1447
  const message = `Summary @${summaryRefSeqNum}:${this.deltaManager.minimumSequenceNumber}`;
1670
- // We should be here is we haven't processed be here. If we are of if the last message's sequence number
1671
- // doesn't match the last processed sequence number, log an error.
1672
- if (summaryRefSeqNum !== ((_a = this.deltaManager.lastMessage) === null || _a === void 0 ? void 0 : _a.sequenceNumber)) {
1673
- summaryNumberLogger.sendErrorEvent({
1674
- eventName: "LastSequenceMismatch",
1675
- error: message,
1676
- });
1677
- }
1448
+ const lastAck = this.summaryCollection.latestAck;
1678
1449
  this.summarizerNode.startSummary(summaryRefSeqNum, summaryNumberLogger);
1679
1450
  // Helper function to check whether we should still continue between each async step.
1680
1451
  const checkContinue = () => {
1452
+ var _a;
1681
1453
  // Do not check for loss of connectivity directly! Instead leave it up to
1682
1454
  // RunWhileConnectedCoordinator to control policy in a single place.
1683
1455
  // This will allow easier change of design if we chose to. For example, we may chose to allow
@@ -1701,6 +1473,14 @@ export class ContainerRuntime extends TypedEventEmitter {
1701
1473
  error: `lastSequenceNumber changed before uploading to storage. ${this.deltaManager.lastSequenceNumber} !== ${summaryRefSeqNum}`,
1702
1474
  };
1703
1475
  }
1476
+ assert(summaryRefSeqNum === ((_a = this.deltaManager.lastMessage) === null || _a === void 0 ? void 0 : _a.sequenceNumber), 0x395 /* it's one and the same thing */);
1477
+ if (lastAck !== this.summaryCollection.latestAck) {
1478
+ return {
1479
+ continue: false,
1480
+ // eslint-disable-next-line max-len
1481
+ error: `Last summary changed while summarizing. ${this.summaryCollection.latestAck} !== ${lastAck}`,
1482
+ };
1483
+ }
1704
1484
  return { continue: true };
1705
1485
  };
1706
1486
  let continueResult = checkContinue();
@@ -1719,7 +1499,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1719
1499
  const forcedFullTree = this.garbageCollector.summaryStateNeedsReset;
1720
1500
  try {
1721
1501
  summarizeResult = await this.summarize({
1722
- fullTree: fullTree || forcedFullTree,
1502
+ fullTree: fullTree !== null && fullTree !== void 0 ? fullTree : forcedFullTree,
1723
1503
  trackState: true,
1724
1504
  summaryLogger: summaryNumberLogger,
1725
1505
  runGC: this.garbageCollector.shouldRunGC,
@@ -1739,13 +1519,13 @@ export class ContainerRuntime extends TypedEventEmitter {
1739
1519
  // Counting dataStores and handles
1740
1520
  // Because handles are unchanged dataStores in the current logic,
1741
1521
  // summarized dataStore count is total dataStore count minus handle count
1742
- const dataStoreTree = this.disableIsolatedChannels ? summaryTree : summaryTree.tree[channelsTreeName];
1522
+ const dataStoreTree = summaryTree.tree[channelsTreeName];
1743
1523
  assert(dataStoreTree.type === SummaryType.Tree, 0x1fc /* "summary is not a tree" */);
1744
1524
  const handleCount = Object.values(dataStoreTree.tree).filter((value) => value.type === SummaryType.Handle).length;
1745
1525
  const gcSummaryTreeStats = summaryTree.tree[gcTreeKey]
1746
1526
  ? calculateStats(summaryTree.tree[gcTreeKey])
1747
1527
  : undefined;
1748
- const summaryStats = Object.assign({ dataStoreCount: this.dataStores.size, summarizedDataStoreCount: this.dataStores.size - handleCount, gcStateUpdatedDataStoreCount: (_b = summarizeResult.gcStats) === null || _b === void 0 ? void 0 : _b.updatedDataStoreCount, gcBlobNodeCount: gcSummaryTreeStats === null || gcSummaryTreeStats === void 0 ? void 0 : gcSummaryTreeStats.blobNodeCount, gcTotalBlobsSize: gcSummaryTreeStats === null || gcSummaryTreeStats === void 0 ? void 0 : gcSummaryTreeStats.totalBlobSize, opsSizesSinceLastSummary: this.opTracker.opsSizeAccumulator, nonSystemOpsSinceLastSummary: this.opTracker.nonSystemOpCount, summaryNumber }, partialStats);
1528
+ const summaryStats = Object.assign({ dataStoreCount: this.dataStores.size, summarizedDataStoreCount: this.dataStores.size - handleCount, gcStateUpdatedDataStoreCount: (_a = summarizeResult.gcStats) === null || _a === void 0 ? void 0 : _a.updatedDataStoreCount, gcBlobNodeCount: gcSummaryTreeStats === null || gcSummaryTreeStats === void 0 ? void 0 : gcSummaryTreeStats.blobNodeCount, gcTotalBlobsSize: gcSummaryTreeStats === null || gcSummaryTreeStats === void 0 ? void 0 : gcSummaryTreeStats.totalBlobSize, summaryNumber }, partialStats);
1749
1529
  const generateSummaryData = {
1750
1530
  referenceSequenceNumber: summaryRefSeqNum,
1751
1531
  minimumSequenceNumber,
@@ -1763,7 +1543,6 @@ export class ContainerRuntime extends TypedEventEmitter {
1763
1543
  // submitting the summaryOp then we can't rely on summaryAck. So in case we have
1764
1544
  // latestSnapshotVersionId from storage and it does not match with the lastAck ackHandle, then use
1765
1545
  // the one fetched from storage as parent as that is the latest.
1766
- const lastAck = this.summaryCollection.latestAck;
1767
1546
  let summaryContext;
1768
1547
  if ((lastAck === null || lastAck === void 0 ? void 0 : lastAck.summaryAck.contents.handle) !== latestSnapshotVersionId
1769
1548
  && latestSnapshotVersionId !== undefined) {
@@ -1776,7 +1555,7 @@ export class ContainerRuntime extends TypedEventEmitter {
1776
1555
  else if (lastAck === undefined) {
1777
1556
  summaryContext = {
1778
1557
  proposalHandle: undefined,
1779
- ackHandle: (_c = this.context.getLoadedFromVersion()) === null || _c === void 0 ? void 0 : _c.id,
1558
+ ackHandle: (_b = this.context.getLoadedFromVersion()) === null || _b === void 0 ? void 0 : _b.id,
1780
1559
  referenceSequenceNumber: summaryRefSeqNum,
1781
1560
  };
1782
1561
  }
@@ -1809,14 +1588,13 @@ export class ContainerRuntime extends TypedEventEmitter {
1809
1588
  }
1810
1589
  let clientSequenceNumber;
1811
1590
  try {
1812
- clientSequenceNumber = this.submitSystemMessage(MessageType.Summarize, summaryMessage);
1591
+ clientSequenceNumber = this.submitSummaryMessage(summaryMessage);
1813
1592
  }
1814
1593
  catch (error) {
1815
1594
  return Object.assign(Object.assign({ stage: "upload" }, uploadData), { error });
1816
1595
  }
1817
1596
  const submitData = Object.assign(Object.assign({ stage: "submit" }, uploadData), { clientSequenceNumber, submitOpDuration: trace.trace().duration });
1818
1597
  this.summarizerNode.completeSummary(handle);
1819
- this.opTracker.reset();
1820
1598
  return submitData;
1821
1599
  }
1822
1600
  finally {
@@ -1858,7 +1636,17 @@ export class ContainerRuntime extends TypedEventEmitter {
1858
1636
  this.chunkMap.delete(clientId);
1859
1637
  }
1860
1638
  }
1639
+ hasPendingMessages() {
1640
+ return this.pendingStateManager.hasPendingMessages() || !this.emptyBatch;
1641
+ }
1861
1642
  updateDocumentDirtyState(dirty) {
1643
+ if (this.attachState !== AttachState.Attached) {
1644
+ assert(dirty, 0x3d2 /* Non-attached container is dirty */);
1645
+ }
1646
+ else {
1647
+ // Other way is not true = see this.isContainerMessageDirtyable()
1648
+ assert(!dirty || this.hasPendingMessages(), 0x3d3 /* if doc is dirty, there has to be pending ops */);
1649
+ }
1862
1650
  if (this.dirtyContainer === dirty) {
1863
1651
  return;
1864
1652
  }
@@ -1886,99 +1674,100 @@ export class ContainerRuntime extends TypedEventEmitter {
1886
1674
  this.verifyNotClosed();
1887
1675
  return this.blobManager.createBlob(blob);
1888
1676
  }
1889
- submit(type, content, localOpMetadata = undefined, opMetadata = undefined) {
1677
+ submit(type, contents, localOpMetadata = undefined, metadata = undefined) {
1890
1678
  this.verifyNotClosed();
1891
1679
  // There should be no ops in detached container state!
1892
1680
  assert(this.attachState !== AttachState.Detached, 0x132 /* "sending ops in detached container" */);
1893
- let clientSequenceNumber = -1;
1894
- let opMetadataInternal = opMetadata;
1895
- if (this.canSendOps()) {
1896
- const serializedContent = JSON.stringify(content);
1897
- const maxOpSize = this.context.deltaManager.maxMessageSize;
1898
- // If in TurnBased flush mode we will trigger a flush at the next turn break
1899
- if (this.flushMode === FlushMode.TurnBased && !this.needsFlush) {
1900
- opMetadataInternal = Object.assign(Object.assign({}, opMetadata), { batch: true });
1901
- this.needsFlush = true;
1902
- // Use Promise.resolve().then() to queue a microtask to detect the end of the turn and force a flush.
1903
- if (!this.flushTrigger) {
1904
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1905
- Promise.resolve().then(() => {
1906
- this.flushTrigger = false;
1907
- this.flush();
1681
+ const deserializedContent = { type, contents };
1682
+ const serializedContent = JSON.stringify(deserializedContent);
1683
+ if (this.deltaManager.readOnlyInfo.readonly) {
1684
+ this.logger.sendErrorEvent({ eventName: "SubmitOpInReadonly" });
1685
+ }
1686
+ const message = {
1687
+ contents: serializedContent,
1688
+ deserializedContent,
1689
+ metadata,
1690
+ localOpMetadata,
1691
+ referenceSequenceNumber: this.deltaManager.lastSequenceNumber,
1692
+ };
1693
+ try {
1694
+ // If this is attach message for new data store, and we are in a batch, send this op out of order
1695
+ // Is it safe:
1696
+ // Yes, this should be safe reordering. Newly created data stores are not visible through API surface.
1697
+ // They become visible only when aliased, or handle to some sub-element of newly created datastore
1698
+ // is stored in some DDS, i.e. only after some other op.
1699
+ // Why:
1700
+ // Attach ops are large, and expensive to process. Plus there are scenarios where a lot of new data
1701
+ // stores are created, causing issues like relay service throttling (too many ops) and catastrophic
1702
+ // failure (batch is too large). Pushing them earlier and outside of main batch should alleviate
1703
+ // these issues.
1704
+ // Cons:
1705
+ // 1. With large batches, relay service may throttle clients. Clients may disconnect while throttled.
1706
+ // This change creates new possibility of a lot of newly created data stores never being referenced
1707
+ // because client died before it had a change to submit the rest of the ops. This will create more
1708
+ // garbage that needs to be collected leveraging GC (Garbage Collection) feature.
1709
+ // 2. Sending ops out of order means they are excluded from rollback functionality. This is not an issue
1710
+ // today as rollback can't undo creation of data store. To some extent not sending them is a bigger
1711
+ // issue than sending.
1712
+ // Please note that this does not change file format, so it can be disabled in the future if this
1713
+ // optimization no longer makes sense (for example, batch compression may make it less appealing).
1714
+ if (type === ContainerMessageType.Attach &&
1715
+ this.mc.config.getBoolean("Fluid.ContainerRuntime.disableAttachOpReorder") !== true) {
1716
+ if (!this.pendingAttachBatch.push(message)) {
1717
+ // BatchManager has two limits - soft limit & hard limit. Soft limit is only engaged
1718
+ // when queue is not empty.
1719
+ // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
1720
+ this.flushBatch(this.pendingAttachBatch.popBatch());
1721
+ if (!this.pendingAttachBatch.push(message)) {
1722
+ throw new GenericError("BatchTooLarge",
1723
+ /* error */ undefined, {
1724
+ opSize: message.contents.length,
1725
+ count: this.pendingAttachBatch.length,
1726
+ limit: this.pendingAttachBatch.limit,
1727
+ });
1728
+ }
1729
+ }
1730
+ }
1731
+ else {
1732
+ if (!this.pendingBatch.push(message)) {
1733
+ throw new GenericError("BatchTooLarge",
1734
+ /* error */ undefined, {
1735
+ opSize: message.contents.length,
1736
+ count: this.pendingBatch.length,
1737
+ limit: this.pendingBatch.limit,
1908
1738
  });
1909
1739
  }
1910
1740
  }
1911
- clientSequenceNumber = this.submitMaybeChunkedMessages(type, content, serializedContent, maxOpSize, this._flushMode === FlushMode.TurnBased, opMetadataInternal);
1741
+ if (this._flushMode !== FlushMode.TurnBased) {
1742
+ this.flush();
1743
+ }
1744
+ else if (!this.flushTrigger) {
1745
+ this.flushTrigger = true;
1746
+ // Queue a microtask to detect the end of the turn and force a flush.
1747
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1748
+ Promise.resolve().then(() => {
1749
+ this.flushTrigger = false;
1750
+ this.flush();
1751
+ });
1752
+ }
1912
1753
  }
1913
- // Let the PendingStateManager know that a message was submitted.
1914
- this.pendingStateManager.onSubmitMessage(type, clientSequenceNumber, this.deltaManager.lastSequenceNumber, content, localOpMetadata, opMetadataInternal);
1915
- if (this.isContainerMessageDirtyable(type, content)) {
1916
- this.updateDocumentDirtyState(true);
1754
+ catch (error) {
1755
+ this.closeFn(error);
1756
+ throw error;
1917
1757
  }
1918
- }
1919
- submitMaybeChunkedMessages(type, content, serializedContent, serverMaxOpSize, batch, opMetadataInternal = undefined) {
1920
- if (this._maxOpSizeInBytes >= 0) {
1921
- // Chunking disabled
1922
- if (!serializedContent || serializedContent.length <= this._maxOpSizeInBytes) {
1923
- return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
1924
- }
1925
- // When chunking is disabled, we ignore the server max message size
1926
- // and if the content length is larger than the client configured message size
1927
- // instead of splitting the content, we will fail by explicitly close the container
1928
- this.closeFn(new GenericError("OpTooLarge",
1929
- /* error */ undefined, {
1930
- length: {
1931
- value: serializedContent.length,
1932
- tag: TelemetryDataTag.PackageData,
1933
- },
1934
- limit: {
1935
- value: this._maxOpSizeInBytes,
1936
- tag: TelemetryDataTag.PackageData,
1937
- },
1938
- }));
1939
- return -1;
1940
- }
1941
- // Chunking enabled, fallback on the server's max message size
1942
- // and split the content accordingly
1943
- if (!serializedContent || serializedContent.length <= serverMaxOpSize) {
1944
- return this.submitRuntimeMessage(type, content, batch, opMetadataInternal);
1945
- }
1946
- return this.submitChunkedMessage(type, serializedContent, serverMaxOpSize);
1947
- }
1948
- submitChunkedMessage(type, content, maxOpSize) {
1949
- const contentLength = content.length;
1950
- const chunkN = Math.floor((contentLength - 1) / maxOpSize) + 1;
1951
- let offset = 0;
1952
- let clientSequenceNumber = 0;
1953
- for (let i = 1; i <= chunkN; i = i + 1) {
1954
- const chunkedOp = {
1955
- chunkId: i,
1956
- contents: content.substr(offset, maxOpSize),
1957
- originalType: type,
1958
- totalChunks: chunkN,
1959
- };
1960
- offset += maxOpSize;
1961
- clientSequenceNumber = this.submitRuntimeMessage(ContainerMessageType.ChunkedOp, chunkedOp, false);
1758
+ if (this.isContainerMessageDirtyable(type, contents)) {
1759
+ this.updateDocumentDirtyState(true);
1962
1760
  }
1963
- return clientSequenceNumber;
1964
1761
  }
1965
- submitSystemMessage(type, contents) {
1762
+ submitSummaryMessage(contents) {
1966
1763
  this.verifyNotClosed();
1967
1764
  assert(this.connected, 0x133 /* "Container disconnected when trying to submit system message" */);
1968
1765
  // System message should not be sent in the middle of the batch.
1969
- // That said, we can preserve existing behavior by not flushing existing buffer.
1970
- // That might be not what caller hopes to get, but we can look deeper if telemetry tells us it's a problem.
1971
- const middleOfBatch = this.flushMode === FlushMode.TurnBased && this.needsFlush;
1972
- if (middleOfBatch) {
1973
- this.mc.logger.sendErrorEvent({ eventName: "submitSystemMessageError", type });
1974
- }
1975
- return this.context.submitFn(type, contents, middleOfBatch);
1976
- }
1977
- submitRuntimeMessage(type, contents, batch, appData) {
1978
- this.verifyNotClosed();
1979
- assert(this.connected, 0x259 /* "Container disconnected when trying to submit system message" */);
1980
- const payload = { type, contents };
1981
- return this.context.submitFn(MessageType.Operation, payload, batch, appData);
1766
+ assert(this.emptyBatch, 0x3d4 /* System op in the middle of a batch */);
1767
+ // back-compat: ADO #1385: Make this call unconditional in the future
1768
+ return this.context.submitSummaryFn !== undefined
1769
+ ? this.context.submitSummaryFn(contents)
1770
+ : this.context.submitFn(MessageType.Summarize, contents, false);
1982
1771
  }
1983
1772
  /**
1984
1773
  * Throw an error if the runtime is closed. Methods that are expected to potentially
@@ -2009,7 +1798,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2009
1798
  case ContainerMessageType.ChunkedOp:
2010
1799
  throw new Error(`chunkedOp not expected here`);
2011
1800
  case ContainerMessageType.BlobAttach:
2012
- this.submit(type, content, localOpMetadata, opMetadata);
1801
+ this.blobManager.reSubmit(opMetadata);
2013
1802
  break;
2014
1803
  case ContainerMessageType.Rejoin:
2015
1804
  this.submit(type, content);
@@ -2057,7 +1846,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2057
1846
  const { snapshotTree, versionId } = await this.fetchSnapshotFromStorage(null, summaryLogger, {
2058
1847
  eventName: "RefreshLatestSummaryGetSnapshot",
2059
1848
  fetchLatest: true,
2060
- });
1849
+ }, FetchSource.noCache);
2061
1850
  const readAndParseBlob = async (id) => readAndParse(this.storage, id);
2062
1851
  const latestSnapshotRefSeq = await seqFromTree(snapshotTree, readAndParseBlob);
2063
1852
  const result = await this.summarizerNode.refreshLatestSummary(undefined, latestSnapshotRefSeq, async () => snapshotTree, readAndParseBlob, summaryLogger);
@@ -2065,11 +1854,11 @@ export class ContainerRuntime extends TypedEventEmitter {
2065
1854
  await this.garbageCollector.latestSummaryStateRefreshed(result, readAndParseBlob);
2066
1855
  return { latestSnapshotRefSeq, latestSnapshotVersionId: versionId };
2067
1856
  }
2068
- async fetchSnapshotFromStorage(versionId, logger, event) {
1857
+ async fetchSnapshotFromStorage(versionId, logger, event, fetchSource) {
2069
1858
  return PerformanceEvent.timedExecAsync(logger, event, async (perfEvent) => {
2070
1859
  const stats = {};
2071
1860
  const trace = Trace.start();
2072
- const versions = await this.storage.getVersions(versionId, 1);
1861
+ const versions = await this.storage.getVersions(versionId, 1, "refreshLatestSummaryAckFromServer", fetchSource);
2073
1862
  assert(!!versions && !!versions[0], 0x137 /* "Failed to get version from storage" */);
2074
1863
  stats.getVersionDuration = trace.trace().duration;
2075
1864
  const maybeSnapshot = await this.storage.getSnapshotTree(versions[0]);
@@ -2099,10 +1888,15 @@ export class ContainerRuntime extends TypedEventEmitter {
2099
1888
  if (!((_a = this.mc.config.getBoolean("enableOfflineLoad")) !== null && _a !== void 0 ? _a : this.runtimeOptions.enableOfflineLoad)) {
2100
1889
  throw new UsageError("can't get state when offline load disabled");
2101
1890
  }
1891
+ // Flush pending batch.
1892
+ // getPendingLocalState() is only exposed through Container.closeAndGetPendingLocalState(), so it's safe
1893
+ // to close current batch.
1894
+ this.flush();
2102
1895
  const previousPendingState = this.context.pendingLocalState;
2103
1896
  if (previousPendingState) {
2104
1897
  return {
2105
1898
  pending: this.pendingStateManager.getLocalState(),
1899
+ pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
2106
1900
  snapshotBlobs: previousPendingState.snapshotBlobs,
2107
1901
  baseSnapshot: previousPendingState.baseSnapshot,
2108
1902
  savedOps: this.savedOps,
@@ -2112,6 +1906,7 @@ export class ContainerRuntime extends TypedEventEmitter {
2112
1906
  assert(!!this.baseSnapshotBlobs, 0x2e7 /* "Must serialize base snapshot blobs before getting runtime state" */);
2113
1907
  return {
2114
1908
  pending: this.pendingStateManager.getLocalState(),
1909
+ pendingAttachmentBlobs: this.blobManager.getPendingBlobs(),
2115
1910
  snapshotBlobs: this.baseSnapshotBlobs,
2116
1911
  baseSnapshot: this.context.baseSnapshot,
2117
1912
  savedOps: this.savedOps,
@@ -2151,6 +1946,19 @@ export class ContainerRuntime extends TypedEventEmitter {
2151
1946
  // we may not have seen every sequence number (because of system ops) so apply everything once we
2152
1947
  // don't have any more saved ops
2153
1948
  await this.pendingStateManager.applyStashedOpsAt();
1949
+ // If it's not the case, we should take it into account when calculating dirty state.
1950
+ assert(this.context.attachState === AttachState.Attached, 0x3d5 /* this function is called for attached containers only */);
1951
+ if (!this.hasPendingMessages()) {
1952
+ this.updateDocumentDirtyState(false);
1953
+ }
1954
+ }
1955
+ validateSummaryHeuristicConfiguration(configuration) {
1956
+ // eslint-disable-next-line no-restricted-syntax
1957
+ for (const prop in configuration) {
1958
+ if (typeof configuration[prop] === "number" && configuration[prop] < 0) {
1959
+ throw new UsageError(`Summary heuristic configuration property "${prop}" cannot be less than 0`);
1960
+ }
1961
+ }
2154
1962
  }
2155
1963
  }
2156
1964
  /**