@fluidframework/container-runtime 2.0.0-internal.5.2.0 → 2.0.0-internal.5.3.1

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 (180) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/blobManager.d.ts +5 -2
  3. package/dist/blobManager.d.ts.map +1 -1
  4. package/dist/blobManager.js +51 -22
  5. package/dist/blobManager.js.map +1 -1
  6. package/dist/connectionTelemetry.d.ts.map +1 -1
  7. package/dist/connectionTelemetry.js +8 -1
  8. package/dist/connectionTelemetry.js.map +1 -1
  9. package/dist/containerRuntime.d.ts +4 -15
  10. package/dist/containerRuntime.d.ts.map +1 -1
  11. package/dist/containerRuntime.js +12 -7
  12. package/dist/containerRuntime.js.map +1 -1
  13. package/dist/dataStoreContext.d.ts.map +1 -1
  14. package/dist/dataStoreContext.js +4 -3
  15. package/dist/dataStoreContext.js.map +1 -1
  16. package/dist/dataStoreContexts.d.ts.map +1 -1
  17. package/dist/dataStoreContexts.js +2 -1
  18. package/dist/dataStoreContexts.js.map +1 -1
  19. package/dist/dataStores.d.ts +1 -2
  20. package/dist/dataStores.d.ts.map +1 -1
  21. package/dist/dataStores.js +2 -1
  22. package/dist/dataStores.js.map +1 -1
  23. package/dist/gc/garbageCollection.d.ts.map +1 -1
  24. package/dist/gc/garbageCollection.js +4 -3
  25. package/dist/gc/garbageCollection.js.map +1 -1
  26. package/dist/gc/gcTelemetry.d.ts +1 -1
  27. package/dist/gc/gcTelemetry.d.ts.map +1 -1
  28. package/dist/gc/gcTelemetry.js.map +1 -1
  29. package/dist/id-compressor/uuidUtilities.d.ts +0 -2
  30. package/dist/id-compressor/uuidUtilities.d.ts.map +1 -1
  31. package/dist/id-compressor/uuidUtilities.js +1 -3
  32. package/dist/id-compressor/uuidUtilities.js.map +1 -1
  33. package/dist/metadata.d.ts +18 -0
  34. package/dist/metadata.d.ts.map +1 -0
  35. package/dist/metadata.js +7 -0
  36. package/dist/metadata.js.map +1 -0
  37. package/dist/opLifecycle/opDecompressor.d.ts.map +1 -1
  38. package/dist/opLifecycle/opDecompressor.js +14 -8
  39. package/dist/opLifecycle/opDecompressor.js.map +1 -1
  40. package/dist/opLifecycle/opGroupingManager.d.ts +1 -1
  41. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  42. package/dist/opLifecycle/opGroupingManager.js +2 -6
  43. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  44. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  45. package/dist/opLifecycle/opSplitter.js +2 -0
  46. package/dist/opLifecycle/opSplitter.js.map +1 -1
  47. package/dist/opLifecycle/outbox.d.ts +5 -1
  48. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  49. package/dist/opLifecycle/outbox.js +45 -27
  50. package/dist/opLifecycle/outbox.js.map +1 -1
  51. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  52. package/dist/opLifecycle/remoteMessageProcessor.js +3 -1
  53. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  54. package/dist/packageVersion.d.ts +1 -1
  55. package/dist/packageVersion.js +1 -1
  56. package/dist/packageVersion.js.map +1 -1
  57. package/dist/pendingStateManager.d.ts +1 -1
  58. package/dist/pendingStateManager.d.ts.map +1 -1
  59. package/dist/pendingStateManager.js +13 -5
  60. package/dist/pendingStateManager.js.map +1 -1
  61. package/dist/scheduleManager.d.ts.map +1 -1
  62. package/dist/scheduleManager.js +8 -2
  63. package/dist/scheduleManager.js.map +1 -1
  64. package/dist/summary/summarizerNode/summarizerNode.d.ts +1 -1
  65. package/dist/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  66. package/dist/summary/summarizerNode/summarizerNode.js.map +1 -1
  67. package/dist/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  68. package/dist/summary/summarizerNode/summarizerNodeWithGc.js +3 -2
  69. package/dist/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  70. package/dist/summary/summarizerTypes.d.ts +2 -1
  71. package/dist/summary/summarizerTypes.d.ts.map +1 -1
  72. package/dist/summary/summarizerTypes.js.map +1 -1
  73. package/dist/summary/summaryCollection.d.ts.map +1 -1
  74. package/dist/summary/summaryCollection.js +4 -0
  75. package/dist/summary/summaryCollection.js.map +1 -1
  76. package/dist/summary/summaryGenerator.d.ts +1 -1
  77. package/dist/summary/summaryGenerator.d.ts.map +1 -1
  78. package/dist/summary/summaryGenerator.js.map +1 -1
  79. package/lib/blobManager.d.ts +5 -2
  80. package/lib/blobManager.d.ts.map +1 -1
  81. package/lib/blobManager.js +51 -22
  82. package/lib/blobManager.js.map +1 -1
  83. package/lib/connectionTelemetry.d.ts.map +1 -1
  84. package/lib/connectionTelemetry.js +8 -1
  85. package/lib/connectionTelemetry.js.map +1 -1
  86. package/lib/containerRuntime.d.ts +4 -15
  87. package/lib/containerRuntime.d.ts.map +1 -1
  88. package/lib/containerRuntime.js +12 -7
  89. package/lib/containerRuntime.js.map +1 -1
  90. package/lib/dataStoreContext.d.ts.map +1 -1
  91. package/lib/dataStoreContext.js +4 -3
  92. package/lib/dataStoreContext.js.map +1 -1
  93. package/lib/dataStoreContexts.d.ts.map +1 -1
  94. package/lib/dataStoreContexts.js +2 -1
  95. package/lib/dataStoreContexts.js.map +1 -1
  96. package/lib/dataStores.d.ts +1 -2
  97. package/lib/dataStores.d.ts.map +1 -1
  98. package/lib/dataStores.js +2 -1
  99. package/lib/dataStores.js.map +1 -1
  100. package/lib/gc/garbageCollection.d.ts.map +1 -1
  101. package/lib/gc/garbageCollection.js +2 -1
  102. package/lib/gc/garbageCollection.js.map +1 -1
  103. package/lib/gc/gcTelemetry.d.ts +1 -1
  104. package/lib/gc/gcTelemetry.d.ts.map +1 -1
  105. package/lib/gc/gcTelemetry.js.map +1 -1
  106. package/lib/id-compressor/uuidUtilities.d.ts +0 -2
  107. package/lib/id-compressor/uuidUtilities.d.ts.map +1 -1
  108. package/lib/id-compressor/uuidUtilities.js +1 -3
  109. package/lib/id-compressor/uuidUtilities.js.map +1 -1
  110. package/lib/metadata.d.ts +18 -0
  111. package/lib/metadata.d.ts.map +1 -0
  112. package/lib/metadata.js +6 -0
  113. package/lib/metadata.js.map +1 -0
  114. package/lib/opLifecycle/opDecompressor.d.ts.map +1 -1
  115. package/lib/opLifecycle/opDecompressor.js +14 -8
  116. package/lib/opLifecycle/opDecompressor.js.map +1 -1
  117. package/lib/opLifecycle/opGroupingManager.d.ts +1 -1
  118. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  119. package/lib/opLifecycle/opGroupingManager.js +2 -6
  120. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  121. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  122. package/lib/opLifecycle/opSplitter.js +2 -0
  123. package/lib/opLifecycle/opSplitter.js.map +1 -1
  124. package/lib/opLifecycle/outbox.d.ts +5 -1
  125. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  126. package/lib/opLifecycle/outbox.js +45 -27
  127. package/lib/opLifecycle/outbox.js.map +1 -1
  128. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  129. package/lib/opLifecycle/remoteMessageProcessor.js +3 -1
  130. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  131. package/lib/packageVersion.d.ts +1 -1
  132. package/lib/packageVersion.js +1 -1
  133. package/lib/packageVersion.js.map +1 -1
  134. package/lib/pendingStateManager.d.ts +1 -1
  135. package/lib/pendingStateManager.d.ts.map +1 -1
  136. package/lib/pendingStateManager.js +13 -5
  137. package/lib/pendingStateManager.js.map +1 -1
  138. package/lib/scheduleManager.d.ts.map +1 -1
  139. package/lib/scheduleManager.js +8 -2
  140. package/lib/scheduleManager.js.map +1 -1
  141. package/lib/summary/summarizerNode/summarizerNode.d.ts +1 -1
  142. package/lib/summary/summarizerNode/summarizerNode.d.ts.map +1 -1
  143. package/lib/summary/summarizerNode/summarizerNode.js.map +1 -1
  144. package/lib/summary/summarizerNode/summarizerNodeWithGc.d.ts.map +1 -1
  145. package/lib/summary/summarizerNode/summarizerNodeWithGc.js +2 -1
  146. package/lib/summary/summarizerNode/summarizerNodeWithGc.js.map +1 -1
  147. package/lib/summary/summarizerTypes.d.ts +2 -1
  148. package/lib/summary/summarizerTypes.d.ts.map +1 -1
  149. package/lib/summary/summarizerTypes.js.map +1 -1
  150. package/lib/summary/summaryCollection.d.ts.map +1 -1
  151. package/lib/summary/summaryCollection.js +4 -0
  152. package/lib/summary/summaryCollection.js.map +1 -1
  153. package/lib/summary/summaryGenerator.d.ts +1 -1
  154. package/lib/summary/summaryGenerator.d.ts.map +1 -1
  155. package/lib/summary/summaryGenerator.js.map +1 -1
  156. package/package.json +19 -18
  157. package/src/blobManager.ts +68 -27
  158. package/src/connectionTelemetry.ts +10 -1
  159. package/src/containerRuntime.ts +14 -25
  160. package/src/dataStoreContext.ts +5 -4
  161. package/src/dataStoreContexts.ts +2 -1
  162. package/src/dataStores.ts +8 -3
  163. package/src/gc/garbageCollection.ts +2 -1
  164. package/src/gc/gcTelemetry.ts +1 -1
  165. package/src/id-compressor/uuidUtilities.ts +1 -4
  166. package/src/metadata.ts +19 -0
  167. package/src/opLifecycle/README.md +20 -0
  168. package/src/opLifecycle/opDecompressor.ts +41 -13
  169. package/src/opLifecycle/opGroupingManager.ts +14 -7
  170. package/src/opLifecycle/opSplitter.ts +3 -1
  171. package/src/opLifecycle/outbox.ts +61 -38
  172. package/src/opLifecycle/remoteMessageProcessor.ts +5 -1
  173. package/src/packageVersion.ts +1 -1
  174. package/src/pendingStateManager.ts +20 -9
  175. package/src/scheduleManager.ts +15 -6
  176. package/src/summary/summarizerNode/summarizerNode.ts +1 -1
  177. package/src/summary/summarizerNode/summarizerNodeWithGc.ts +2 -1
  178. package/src/summary/summarizerTypes.ts +2 -1
  179. package/src/summary/summaryCollection.ts +6 -2
  180. package/src/summary/summaryGenerator.ts +1 -1
package/src/dataStores.ts CHANGED
@@ -3,12 +3,16 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { ITelemetryBaseLogger } from "@fluidframework/common-definitions";
7
6
  import {
8
7
  DataCorruptionError,
9
8
  extractSafePropertiesFromMessage,
10
9
  } from "@fluidframework/container-utils";
11
- import { IDisposable, IFluidHandle, IRequest } from "@fluidframework/core-interfaces";
10
+ import {
11
+ ITelemetryBaseLogger,
12
+ IDisposable,
13
+ IFluidHandle,
14
+ IRequest,
15
+ } from "@fluidframework/core-interfaces";
12
16
  import { FluidObjectHandle } from "@fluidframework/datastore";
13
17
  import { ISequencedDocumentMessage, ISnapshotTree } from "@fluidframework/protocol-definitions";
14
18
  import {
@@ -46,7 +50,8 @@ import {
46
50
  } from "@fluidframework/telemetry-utils";
47
51
  import { AttachState } from "@fluidframework/container-definitions";
48
52
  import { buildSnapshotTree } from "@fluidframework/driver-utils";
49
- import { assert, Lazy } from "@fluidframework/common-utils";
53
+ import { assert } from "@fluidframework/common-utils";
54
+ import { Lazy } from "@fluidframework/core-utils";
50
55
  import { v4 as uuid } from "uuid";
51
56
  import { DataStoreContexts } from "./dataStoreContexts";
52
57
  import {
@@ -3,7 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { LazyPromise, Timer } from "@fluidframework/common-utils";
6
+ import { Timer } from "@fluidframework/common-utils";
7
+ import { LazyPromise } from "@fluidframework/core-utils";
7
8
  import { ClientSessionExpiredError, DataProcessingError } from "@fluidframework/container-utils";
8
9
  import { IRequestHeader } from "@fluidframework/core-interfaces";
9
10
  import {
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { ITelemetryGenericEvent } from "@fluidframework/common-definitions";
6
+ import { ITelemetryGenericEvent } from "@fluidframework/core-interfaces";
7
7
  import { IGarbageCollectionData } from "@fluidframework/runtime-definitions";
8
8
  import { packagePathToTelemetryProperty } from "@fluidframework/runtime-utils";
9
9
  import {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { assert } from "@fluidframework/common-utils";
7
7
  import { StableId, UuidString } from "@fluidframework/runtime-definitions";
8
- import { v4, NIL } from "uuid";
8
+ import { v4 } from "uuid";
9
9
 
10
10
  const hexadecimalCharCodes = Array.from("09afAF").map((c) => c.charCodeAt(0)) as [
11
11
  zero: number,
@@ -24,9 +24,6 @@ function isHexadecimalCharacter(charCode: number): boolean {
24
24
  );
25
25
  }
26
26
 
27
- /** The null (lowest/all-zeros) UUID */
28
- export const nilUuid = assertIsUuidString(NIL);
29
-
30
27
  /**
31
28
  * Asserts that the given string is a UUID
32
29
  */
@@ -0,0 +1,19 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ /**
7
+ * Batching makes assumptions about what might be on the metadata. This interface codifies those assumptions, but does not validate them.
8
+ */
9
+ export interface IBatchMetadata {
10
+ batch?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Blob handling makes assumptions about what might be on the metadata. This interface codifies those assumptions, but does not validate them.
15
+ */
16
+ export interface IBlobMetadata {
17
+ blobId?: string;
18
+ localId?: string;
19
+ }
@@ -51,6 +51,26 @@ and verifying that the following expectation changes won't have any effects:
51
51
  - client sequence numbers on batch messages can only be used to order messages with the same sequenceNumber
52
52
  - requires all ops to be processed by runtime layer (version "2.0.0-internal.1.2.0" or later https://github.com/microsoft/FluidFramework/pull/11832)
53
53
 
54
+ Grouped batching may become problematic for batches which contain reentrant ops. This is the case when changes are made to a DDS inside a DDS 'onChanged' event handler. This means that the reentrant op will have a different reference sequence number than the rest of the ops in the batch, resulting in a different view of the state of the data model.
55
+
56
+ Therefore, when grouped batching is enabled, all batches with reentrant ops are rebased to the current reference sequence number and resubmitted to the data stores so that all ops are in agreement about the state of the data model and ensure eventual consistency.
57
+
58
+ ### How to enable
59
+
60
+ **This feature is disabled by default, currently considered experimental and not ready for production usage.**
61
+
62
+ If all prerequisites in the previous section are met, enabling the feature can be done via the `IContainerRuntimeOptions` as following:
63
+
64
+ ```
65
+ const runtimeOptions: IContainerRuntimeOptions = {
66
+ (...)
67
+ enableGroupedBatching: true,
68
+ (...)
69
+   }
70
+ ```
71
+
72
+ In case of emergency grouped batching can be disabled at runtime, using feature gates. If `"Fluid.ContainerRuntime.DisableGroupedBatching"` is set to `true`, it will disable grouped batching if enabled from `IContainerRuntimeOptions` in the code.
73
+
54
74
  ## Chunking for compression
55
75
 
56
76
  **Op chunking for compression targets payloads which exceed the max batch size after compression.** So, only payloads which are already compressed. By default, the feature is enabled.
@@ -8,8 +8,16 @@ import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions"
8
8
  import { assert, IsoBuffer, Uint8ArrayToString } from "@fluidframework/common-utils";
9
9
  import { ChildLogger, ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
10
10
  import { CompressionAlgorithms } from "../containerRuntime";
11
+ import { IBatchMetadata } from "../metadata";
11
12
  import { IMessageProcessingResult } from "./definitions";
12
13
 
14
+ /**
15
+ * Compression makes assumptions about the shape of message contents. This interface codifies those assumptions, but does not validate them.
16
+ */
17
+ interface IPackedContentsContents {
18
+ packedContents: string;
19
+ }
20
+
13
21
  /**
14
22
  * State machine that "unrolls" contents of compressed batches of ops after decompressing them.
15
23
  * This class relies on some implicit contracts defined below:
@@ -34,7 +42,10 @@ export class OpDecompressor {
34
42
  0x511 /* Only lz4 compression is supported */,
35
43
  );
36
44
 
37
- if (message.metadata?.batch === true && this.isCompressed(message)) {
45
+ if (
46
+ (message.metadata as IBatchMetadata | undefined)?.batch === true &&
47
+ this.isCompressed(message)
48
+ ) {
38
49
  // Beginning of a compressed batch
39
50
  assert(this.activeBatch === false, 0x4b8 /* shouldn't have multiple active batches */);
40
51
  if (message.compression) {
@@ -47,7 +58,10 @@ export class OpDecompressor {
47
58
 
48
59
  this.activeBatch = true;
49
60
 
50
- const contents = IsoBuffer.from(message.contents.packedContents, "base64");
61
+ const contents = IsoBuffer.from(
62
+ (message.contents as IPackedContentsContents).packedContents,
63
+ "base64",
64
+ );
51
65
  const decompressedMessage = decompress(contents);
52
66
  const intoString = Uint8ArrayToString(decompressedMessage);
53
67
  const asObj = JSON.parse(intoString);
@@ -61,7 +75,7 @@ export class OpDecompressor {
61
75
 
62
76
  if (
63
77
  this.rootMessageContents !== undefined &&
64
- message.metadata?.batch === undefined &&
78
+ (message.metadata as IBatchMetadata | undefined)?.batch === undefined &&
65
79
  this.activeBatch
66
80
  ) {
67
81
  assert(message.contents === undefined, 0x512 /* Expecting empty message */);
@@ -73,7 +87,10 @@ export class OpDecompressor {
73
87
  };
74
88
  }
75
89
 
76
- if (this.rootMessageContents !== undefined && message.metadata?.batch === false) {
90
+ if (
91
+ this.rootMessageContents !== undefined &&
92
+ (message.metadata as IBatchMetadata | undefined)?.batch === false
93
+ ) {
77
94
  // End of compressed batch
78
95
  const returnMessage = newMessage(
79
96
  message,
@@ -90,14 +107,20 @@ export class OpDecompressor {
90
107
  };
91
108
  }
92
109
 
93
- if (message.metadata?.batch === undefined && this.isCompressed(message)) {
110
+ if (
111
+ (message.metadata as IBatchMetadata | undefined)?.batch === undefined &&
112
+ this.isCompressed(message)
113
+ ) {
94
114
  // Single compressed message
95
115
  assert(
96
116
  this.activeBatch === false,
97
117
  0x4ba /* shouldn't receive compressed message in middle of a batch */,
98
118
  );
99
119
 
100
- const contents = IsoBuffer.from(message.contents.packedContents, "base64");
120
+ const contents = IsoBuffer.from(
121
+ (message.contents as IPackedContentsContents).packedContents,
122
+ "base64",
123
+ );
101
124
  const decompressedMessage = decompress(contents);
102
125
  const intoString = new TextDecoder().decode(decompressedMessage);
103
126
  const asObj = JSON.parse(intoString);
@@ -135,16 +158,19 @@ export class OpDecompressor {
135
158
  message.contents !== null &&
136
159
  typeof message.contents === "object" &&
137
160
  Object.keys(message.contents).length === 1 &&
138
- message.contents?.packedContents !== undefined &&
139
- typeof message.contents?.packedContents === "string" &&
140
- message.contents.packedContents.length > 0 &&
141
- IsoBuffer.from(message.contents.packedContents, "base64").toString("base64") ===
142
- message.contents.packedContents
161
+ typeof (message.contents as { packedContents?: unknown }).packedContents ===
162
+ "string" &&
163
+ (message.contents as IPackedContentsContents).packedContents.length > 0 &&
164
+ IsoBuffer.from(
165
+ (message.contents as IPackedContentsContents).packedContents,
166
+ "base64",
167
+ ).toString("base64") ===
168
+ (message.contents as IPackedContentsContents).packedContents
143
169
  ) {
144
170
  this.logger.sendTelemetryEvent({
145
171
  eventName: "LegacyCompression",
146
172
  type: message.type,
147
- batch: message.metadata?.batch,
173
+ batch: (message.metadata as IBatchMetadata | undefined)?.batch,
148
174
  });
149
175
  return true;
150
176
  }
@@ -164,5 +190,7 @@ const newMessage = (
164
190
  ...originalMessage,
165
191
  contents,
166
192
  compression: undefined,
167
- metadata: { ...originalMessage.metadata },
193
+ // TODO: It should already be the case that we're not modifying any metadata, not clear if/why this shallow clone should be required.
194
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
195
+ metadata: { ...(originalMessage.metadata as any) },
168
196
  });
@@ -8,6 +8,14 @@ import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions"
8
8
  import { ContainerMessageType } from "..";
9
9
  import { IBatch } from "./definitions";
10
10
 
11
+ /**
12
+ * Grouping makes assumptions about the shape of message contents. This interface codifies those assumptions, but does not validate them.
13
+ */
14
+ interface IGroupedBatchMessageContents {
15
+ type: typeof OpGroupingManager.groupedBatchOp;
16
+ contents: IGroupedMessage[];
17
+ }
18
+
11
19
  interface IGroupedMessage {
12
20
  contents?: unknown;
13
21
  metadata?: Record<string, unknown>;
@@ -15,7 +23,7 @@ interface IGroupedMessage {
15
23
  }
16
24
 
17
25
  export class OpGroupingManager {
18
- static groupedBatchOp = "groupedBatch";
26
+ static readonly groupedBatchOp = "groupedBatch";
19
27
 
20
28
  constructor(private readonly groupedBatchingEnabled: boolean) {}
21
29
 
@@ -25,10 +33,6 @@ export class OpGroupingManager {
25
33
  }
26
34
 
27
35
  for (const message of batch.content) {
28
- // Blob attaches cannot be grouped (grouped batching would hide metadata)
29
- if (message.type === ContainerMessageType.BlobAttach) {
30
- return batch;
31
- }
32
36
  if (message.metadata) {
33
37
  const keys = Object.keys(message.metadata);
34
38
  assert(keys.length < 2, 0x5dd /* cannot group ops with metadata */);
@@ -64,11 +68,14 @@ export class OpGroupingManager {
64
68
  }
65
69
 
66
70
  public ungroupOp(op: ISequencedDocumentMessage): ISequencedDocumentMessage[] {
67
- if (op.contents?.type !== OpGroupingManager.groupedBatchOp) {
71
+ if (
72
+ (op.contents as { type?: unknown } | undefined)?.type !==
73
+ OpGroupingManager.groupedBatchOp
74
+ ) {
68
75
  return [op];
69
76
  }
70
77
 
71
- const messages = op.contents.contents as IGroupedMessage[];
78
+ const messages = (op.contents as IGroupedBatchMessageContents).contents;
72
79
  let fakeCsn = 1;
73
80
  return messages.map((subMessage) => ({
74
81
  ...op,
@@ -52,7 +52,9 @@ export class OpSplitter {
52
52
  };
53
53
  }
54
54
 
55
- const clientId = message.clientId;
55
+ // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
56
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
57
+ const clientId = message.clientId as string;
56
58
  const chunkedContent = message.contents as IChunkedOp;
57
59
  this.addChunk(clientId, chunkedContent, message);
58
60
 
@@ -31,7 +31,7 @@ export interface IOutboxConfig {
31
31
  // The maximum size of a batch that we can send over the wire.
32
32
  readonly maxBatchSizeInBytes: number;
33
33
  readonly disablePartialFlush: boolean;
34
- readonly enableBatchRebasing: boolean;
34
+ readonly enableGroupedBatching: boolean;
35
35
  }
36
36
 
37
37
  export interface IOutboxParameters {
@@ -85,6 +85,7 @@ export class Outbox {
85
85
  private readonly mc: MonitoringContext;
86
86
  private readonly attachFlowBatch: BatchManager;
87
87
  private readonly mainBatch: BatchManager;
88
+ private readonly blobAttachBatch: BatchManager;
88
89
  private readonly defaultAttachFlowSoftLimitInBytes = 320 * 1024;
89
90
  private batchRebasesToReport = 5;
90
91
  private rebasing = false;
@@ -109,10 +110,15 @@ export class Outbox {
109
110
 
110
111
  this.attachFlowBatch = new BatchManager({ hardLimit, softLimit });
111
112
  this.mainBatch = new BatchManager({ hardLimit });
113
+ this.blobAttachBatch = new BatchManager({ hardLimit });
112
114
  }
113
115
 
114
116
  public get isEmpty(): boolean {
115
- return this.attachFlowBatch.length === 0 && this.mainBatch.length === 0;
117
+ return (
118
+ this.attachFlowBatch.length === 0 &&
119
+ this.mainBatch.length === 0 &&
120
+ this.blobAttachBatch.length === 0
121
+ );
116
122
  }
117
123
 
118
124
  /**
@@ -124,9 +130,11 @@ export class Outbox {
124
130
  private maybeFlushPartialBatch() {
125
131
  const mainBatchSeqNums = this.mainBatch.sequenceNumbers;
126
132
  const attachFlowBatchSeqNums = this.attachFlowBatch.sequenceNumbers;
133
+ const blobAttachSeqNums = this.blobAttachBatch.sequenceNumbers;
127
134
  assert(
128
135
  this.params.config.disablePartialFlush ||
129
- sequenceNumbersMatch(mainBatchSeqNums, attachFlowBatchSeqNums),
136
+ (sequenceNumbersMatch(mainBatchSeqNums, attachFlowBatchSeqNums) &&
137
+ sequenceNumbersMatch(mainBatchSeqNums, blobAttachSeqNums)),
130
138
  0x58d /* Reference sequence numbers from both batches must be in sync */,
131
139
  );
132
140
 
@@ -134,7 +142,8 @@ export class Outbox {
134
142
 
135
143
  if (
136
144
  sequenceNumbersMatch(mainBatchSeqNums, currentSequenceNumbers) &&
137
- sequenceNumbersMatch(attachFlowBatchSeqNums, currentSequenceNumbers)
145
+ sequenceNumbersMatch(attachFlowBatchSeqNums, currentSequenceNumbers) &&
146
+ sequenceNumbersMatch(blobAttachSeqNums, currentSequenceNumbers)
138
147
  ) {
139
148
  // The reference sequence numbers are stable, there is nothing to do
140
149
  return;
@@ -149,6 +158,8 @@ export class Outbox {
149
158
  mainClientSequenceNumber: mainBatchSeqNums.clientSequenceNumber,
150
159
  attachReferenceSequenceNumber: attachFlowBatchSeqNums.referenceSequenceNumber,
151
160
  attachClientSequenceNumber: attachFlowBatchSeqNums.clientSequenceNumber,
161
+ blobAttachReferenceSequenceNumber: blobAttachSeqNums.referenceSequenceNumber,
162
+ blobAttachClientSequenceNumber: blobAttachSeqNums.clientSequenceNumber,
152
163
  currentReferenceSequenceNumber: currentSequenceNumbers.referenceSequenceNumber,
153
164
  currentClientSequenceNumber: currentSequenceNumbers.clientSequenceNumber,
154
165
  },
@@ -164,20 +175,7 @@ export class Outbox {
164
175
  public submit(message: BatchMessage) {
165
176
  this.maybeFlushPartialBatch();
166
177
 
167
- if (
168
- !this.mainBatch.push(
169
- message,
170
- this.isContextReentrant(),
171
- this.params.getCurrentSequenceNumbers().clientSequenceNumber,
172
- )
173
- ) {
174
- throw new GenericError("BatchTooLarge", /* error */ undefined, {
175
- opSize: message.contents?.length ?? 0,
176
- batchSize: this.mainBatch.contentSizeInBytes,
177
- count: this.mainBatch.length,
178
- limit: this.mainBatch.options.hardLimit,
179
- });
180
- }
178
+ this.addMessageToBatchManager(this.mainBatch, message);
181
179
  }
182
180
 
183
181
  public submitAttach(message: BatchMessage) {
@@ -194,20 +192,8 @@ export class Outbox {
194
192
  // when queue is not empty.
195
193
  // Flush queue & retry. Failure on retry would mean - single message is bigger than hard limit
196
194
  this.flushInternal(this.attachFlowBatch);
197
- if (
198
- !this.attachFlowBatch.push(
199
- message,
200
- this.isContextReentrant(),
201
- this.params.getCurrentSequenceNumbers().clientSequenceNumber,
202
- )
203
- ) {
204
- throw new GenericError("BatchTooLarge", /* error */ undefined, {
205
- opSize: message.contents?.length ?? 0,
206
- batchSize: this.attachFlowBatch.contentSizeInBytes,
207
- count: this.attachFlowBatch.length,
208
- limit: this.attachFlowBatch.options.hardLimit,
209
- });
210
- }
195
+
196
+ this.addMessageToBatchManager(this.attachFlowBatch, message);
211
197
  }
212
198
 
213
199
  // If compression is enabled, we will always successfully receive
@@ -223,6 +209,41 @@ export class Outbox {
223
209
  }
224
210
  }
225
211
 
212
+ public submitBlobAttach(message: BatchMessage) {
213
+ this.maybeFlushPartialBatch();
214
+
215
+ this.addMessageToBatchManager(this.blobAttachBatch, message);
216
+
217
+ // If compression is enabled, we will always successfully receive
218
+ // blobAttach ops and compress then send them at the next JS turn, regardless
219
+ // of the overall size of the accumulated ops in the batch.
220
+ // However, it is more efficient to flush these ops faster, preferably
221
+ // after they reach a size which would benefit from compression.
222
+ if (
223
+ this.blobAttachBatch.contentSizeInBytes >=
224
+ this.params.config.compressionOptions.minimumBatchSizeInBytes
225
+ ) {
226
+ this.flushInternal(this.blobAttachBatch);
227
+ }
228
+ }
229
+
230
+ private addMessageToBatchManager(batchManager: BatchManager, message: BatchMessage) {
231
+ if (
232
+ !batchManager.push(
233
+ message,
234
+ this.isContextReentrant(),
235
+ this.params.getCurrentSequenceNumbers().clientSequenceNumber,
236
+ )
237
+ ) {
238
+ throw new GenericError("BatchTooLarge", /* error */ undefined, {
239
+ opSize: message.contents?.length ?? 0,
240
+ batchSize: batchManager.contentSizeInBytes,
241
+ count: batchManager.length,
242
+ limit: batchManager.options.hardLimit,
243
+ });
244
+ }
245
+ }
246
+
226
247
  public flush() {
227
248
  if (this.isContextReentrant()) {
228
249
  const error = new UsageError("Flushing is not supported inside DDS event handlers");
@@ -235,16 +256,17 @@ export class Outbox {
235
256
 
236
257
  private flushAll() {
237
258
  this.flushInternal(this.attachFlowBatch);
259
+ this.flushInternal(this.blobAttachBatch, true /* disableGroupedBatching */);
238
260
  this.flushInternal(this.mainBatch);
239
261
  }
240
262
 
241
- private flushInternal(batchManager: BatchManager) {
263
+ private flushInternal(batchManager: BatchManager, disableGroupedBatching: boolean = false) {
242
264
  if (batchManager.empty) {
243
265
  return;
244
266
  }
245
267
 
246
268
  const rawBatch = batchManager.popBatch();
247
- if (rawBatch.hasReentrantOps === true && this.params.config.enableBatchRebasing) {
269
+ if (rawBatch.hasReentrantOps === true && this.params.config.enableGroupedBatching) {
248
270
  assert(!this.rebasing, 0x6fa /* A rebased batch should never have reentrant ops */);
249
271
  // If a batch contains reentrant ops (ops created as a result from processing another op)
250
272
  // it needs to be rebased so that we can ensure consistent reference sequence numbers
@@ -253,7 +275,7 @@ export class Outbox {
253
275
  return;
254
276
  }
255
277
 
256
- const processedBatch = this.compressBatch(rawBatch);
278
+ const processedBatch = this.compressBatch(rawBatch, disableGroupedBatching);
257
279
  this.sendBatch(processedBatch);
258
280
 
259
281
  this.persistBatch(rawBatch.content);
@@ -298,7 +320,7 @@ export class Outbox {
298
320
  return this.params.opReentrancy() && !this.rebasing;
299
321
  }
300
322
 
301
- private compressBatch(batch: IBatch): IBatch {
323
+ private compressBatch(batch: IBatch, disableGroupedBatching: boolean): IBatch {
302
324
  if (
303
325
  batch.content.length === 0 ||
304
326
  this.params.config.compressionOptions === undefined ||
@@ -307,11 +329,11 @@ export class Outbox {
307
329
  this.params.containerContext.submitBatchFn === undefined
308
330
  ) {
309
331
  // Nothing to do if the batch is empty or if compression is disabled or not supported, or if we don't need to compress
310
- return this.params.groupingManager.groupBatch(batch);
332
+ return disableGroupedBatching ? batch : this.params.groupingManager.groupBatch(batch);
311
333
  }
312
334
 
313
335
  const compressedBatch = this.params.compressor.compressBatch(
314
- this.params.groupingManager.groupBatch(batch),
336
+ disableGroupedBatching ? batch : this.params.groupingManager.groupBatch(batch),
315
337
  );
316
338
 
317
339
  if (this.params.splitter.isBatchChunkingEnabled) {
@@ -413,6 +435,7 @@ export class Outbox {
413
435
  return {
414
436
  mainBatch: this.mainBatch.checkpoint(),
415
437
  attachFlowBatch: this.attachFlowBatch.checkpoint(),
438
+ blobAttachBatch: this.blobAttachBatch.checkpoint(),
416
439
  };
417
440
  }
418
441
  }
@@ -120,7 +120,11 @@ export function unpackRuntimeMessage(message: ISequencedDocumentMessage): boolea
120
120
  }
121
121
 
122
122
  // legacy op format?
123
- if (message.contents.address !== undefined && message.contents.type === undefined) {
123
+ // TODO: Unsure if this is a real format we should be concerned with. There doesn't appear to be anything prepared to handle the address member.
124
+ if (
125
+ (message.contents as { address?: unknown }).address !== undefined &&
126
+ (message.contents as { type?: unknown }).type === undefined
127
+ ) {
124
128
  message.type = ContainerMessageType.FluidDataStoreOp;
125
129
  } else {
126
130
  // new format
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.0.0-internal.5.2.0";
9
+ export const pkgVersion = "2.0.0-internal.5.3.1";
@@ -3,14 +3,16 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { IDisposable } from "@fluidframework/core-interfaces";
7
- import { assert, Lazy } from "@fluidframework/common-utils";
6
+ import { IDisposable } from "@fluidframework/common-definitions";
7
+ import { assert } from "@fluidframework/common-utils";
8
8
  import { ICriticalContainerError } from "@fluidframework/container-definitions";
9
9
  import { DataProcessingError } from "@fluidframework/container-utils";
10
+ import { Lazy } from "@fluidframework/core-utils";
10
11
  import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
11
12
  import Deque from "double-ended-queue";
12
13
  import { ContainerMessageType } from "./containerRuntime";
13
14
  import { pkgVersion } from "./packageVersion";
15
+ import { IBatchMetadata } from "./metadata";
14
16
 
15
17
  /**
16
18
  * ! TODO: Remove this interface in "2.0.0-internal.7.0.0" once we only read IPendingMessageNew
@@ -209,9 +211,13 @@ export class PendingStateManager implements IDisposable {
209
211
  }
210
212
  }
211
213
 
212
- // applyStashedOp will cause the DDS to behave as if it has sent the op but not actually send it
213
- const localOpMetadata = await this.stateHandler.applyStashedOp(nextMessage.content);
214
- nextMessage.localOpMetadata = localOpMetadata;
214
+ try {
215
+ // applyStashedOp will cause the DDS to behave as if it has sent the op but not actually send it
216
+ const localOpMetadata = await this.stateHandler.applyStashedOp(nextMessage.content);
217
+ nextMessage.localOpMetadata = localOpMetadata;
218
+ } catch (error) {
219
+ throw DataProcessingError.wrapIfUnrecognized(error, "applyStashedOp", nextMessage);
220
+ }
215
221
 
216
222
  // then we push onto pendingMessages which will cause PendingStateManager to resubmit when we connect
217
223
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -264,7 +270,7 @@ export class PendingStateManager implements IDisposable {
264
270
  */
265
271
  private maybeProcessBatchBegin(message: ISequencedDocumentMessage) {
266
272
  // This message is the first in a batch if the "batch" property on the metadata is set to true
267
- if (message.metadata?.batch) {
273
+ if ((message.metadata as IBatchMetadata | undefined)?.batch) {
268
274
  // We should not already be processing a batch and there should be no pending batch begin message.
269
275
  assert(
270
276
  !this.isProcessingBatch && this.pendingBatchBeginMessage === undefined,
@@ -292,10 +298,12 @@ export class PendingStateManager implements IDisposable {
292
298
  0x16d /* "There is no pending batch begin message" */,
293
299
  );
294
300
 
295
- const batchEndMetadata = message.metadata?.batch;
301
+ const batchEndMetadata = (message.metadata as IBatchMetadata | undefined)?.batch;
296
302
  if (this.pendingMessages.isEmpty() || batchEndMetadata === false) {
297
303
  // Get the batch begin metadata from the first message in the batch.
298
- const batchBeginMetadata = this.pendingBatchBeginMessage.metadata?.batch;
304
+ const batchBeginMetadata = (
305
+ this.pendingBatchBeginMessage.metadata as IBatchMetadata | undefined
306
+ )?.batch;
299
307
 
300
308
  // There could be just a single message in the batch. If so, it should not have any batch metadata. If there
301
309
  // are multiple messages in the batch, verify that we got the correct batch begin and end metadata.
@@ -313,7 +321,10 @@ export class PendingStateManager implements IDisposable {
313
321
  message,
314
322
  {
315
323
  runtimeVersion: pkgVersion,
316
- batchClientId: this.pendingBatchBeginMessage.clientId,
324
+ batchClientId:
325
+ this.pendingBatchBeginMessage.clientId === null
326
+ ? "null"
327
+ : this.pendingBatchBeginMessage.clientId,
317
328
  clientId: this.stateHandler.clientId(),
318
329
  hasBatchStart: batchBeginMetadata === true,
319
330
  hasBatchEnd: batchEndMetadata === false,
@@ -15,6 +15,7 @@ import {
15
15
  } from "@fluidframework/container-utils";
16
16
  import { DeltaScheduler } from "./deltaScheduler";
17
17
  import { pkgVersion } from "./packageVersion";
18
+ import { IBatchMetadata } from "./metadata";
18
19
 
19
20
  type IRuntimeMessageMetadata =
20
21
  | undefined
@@ -61,7 +62,9 @@ export class ScheduleManager {
61
62
  this.deltaScheduler.batchBegin(message);
62
63
 
63
64
  const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
64
- this.batchClientId = batch ? message.clientId : undefined;
65
+ // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
66
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
67
+ this.batchClientId = batch ? (message.clientId as string) : undefined;
65
68
  }
66
69
  }
67
70
 
@@ -127,7 +130,9 @@ class ScheduleManagerCore {
127
130
 
128
131
  // Set the batch flag to false on the last message to indicate the end of the send batch
129
132
  const lastMessage = messages[messages.length - 1];
130
- lastMessage.metadata = { ...lastMessage.metadata, batch: false };
133
+ // TODO: It's not clear if this shallow clone is required, as opposed to just setting "batch" to false.
134
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
135
+ lastMessage.metadata = { ...(lastMessage.metadata as any), batch: false };
131
136
  });
132
137
 
133
138
  // Listen for updates and peek at the inbound
@@ -185,7 +190,7 @@ class ScheduleManagerCore {
185
190
  {
186
191
  type: message.type,
187
192
  contentType: typeof message.contents,
188
- batch: message.metadata?.batch,
193
+ batch: (message.metadata as IBatchMetadata | undefined)?.batch,
189
194
  compression: message.compression,
190
195
  pauseSeqNum: this.pauseSequenceNumber,
191
196
  },
@@ -263,7 +268,8 @@ class ScheduleManagerCore {
263
268
  message,
264
269
  {
265
270
  runtimeVersion: pkgVersion,
266
- batchClientId: this.currentBatchClientId,
271
+ batchClientId:
272
+ this.currentBatchClientId === null ? "null" : this.currentBatchClientId,
267
273
  pauseSequenceNumber: this.pauseSequenceNumber,
268
274
  localBatch: this.currentBatchClientId === this.getClientId(),
269
275
  messageType: message.type,
@@ -297,7 +303,8 @@ class ScheduleManagerCore {
297
303
  ) {
298
304
  throw new DataCorruptionError("OpBatchIncomplete", {
299
305
  runtimeVersion: pkgVersion,
300
- batchClientId: this.currentBatchClientId,
306
+ batchClientId:
307
+ this.currentBatchClientId === null ? "null" : this.currentBatchClientId,
301
308
  pauseSequenceNumber: this.pauseSequenceNumber,
302
309
  localBatch: this.currentBatchClientId === this.getClientId(),
303
310
  localMessage: message.clientId === this.getClientId(),
@@ -321,7 +328,9 @@ class ScheduleManagerCore {
321
328
  0x29f /* "we should be processing ops when there is no active batch" */,
322
329
  );
323
330
  this.pauseSequenceNumber = message.sequenceNumber;
324
- this.currentBatchClientId = message.clientId;
331
+ // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
332
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
333
+ this.currentBatchClientId = message.clientId as string;
325
334
  // Start of the batch
326
335
  // Only pause processing if queue has no other ops!
327
336
  // If there are any other ops in the queue, processing will be stopped when they are processed!