@fluidframework/container-runtime 2.0.0-internal.2.1.1 → 2.0.0-internal.2.2.0

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 (161) hide show
  1. package/.eslintrc.js +1 -1
  2. package/dist/blobManager.d.ts +20 -5
  3. package/dist/blobManager.d.ts.map +1 -1
  4. package/dist/blobManager.js +57 -15
  5. package/dist/blobManager.js.map +1 -1
  6. package/dist/containerRuntime.d.ts +39 -40
  7. package/dist/containerRuntime.d.ts.map +1 -1
  8. package/dist/containerRuntime.js +115 -278
  9. package/dist/containerRuntime.js.map +1 -1
  10. package/dist/dataStoreContext.d.ts +3 -1
  11. package/dist/dataStoreContext.d.ts.map +1 -1
  12. package/dist/dataStoreContext.js +21 -3
  13. package/dist/dataStoreContext.js.map +1 -1
  14. package/dist/dataStores.d.ts +8 -5
  15. package/dist/dataStores.d.ts.map +1 -1
  16. package/dist/dataStores.js +26 -13
  17. package/dist/dataStores.js.map +1 -1
  18. package/dist/garbageCollection.d.ts +15 -17
  19. package/dist/garbageCollection.d.ts.map +1 -1
  20. package/dist/garbageCollection.js +92 -106
  21. package/dist/garbageCollection.js.map +1 -1
  22. package/dist/garbageCollectionConstants.d.ts +19 -0
  23. package/dist/garbageCollectionConstants.d.ts.map +1 -0
  24. package/dist/garbageCollectionConstants.js +34 -0
  25. package/dist/garbageCollectionConstants.js.map +1 -0
  26. package/dist/gcSweepReadyUsageDetection.js +2 -2
  27. package/dist/gcSweepReadyUsageDetection.js.map +1 -1
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +8 -6
  31. package/dist/index.js.map +1 -1
  32. package/dist/opLifecycle/batchManager.d.ts +30 -0
  33. package/dist/opLifecycle/batchManager.d.ts.map +1 -0
  34. package/dist/{batchManager.js → opLifecycle/batchManager.js} +25 -10
  35. package/dist/opLifecycle/batchManager.js.map +1 -0
  36. package/dist/opLifecycle/definitions.d.ts +40 -0
  37. package/dist/opLifecycle/definitions.d.ts.map +1 -0
  38. package/dist/opLifecycle/definitions.js +7 -0
  39. package/dist/opLifecycle/definitions.js.map +1 -0
  40. package/dist/opLifecycle/index.d.ts +12 -0
  41. package/dist/opLifecycle/index.d.ts.map +1 -0
  42. package/dist/opLifecycle/index.js +21 -0
  43. package/dist/opLifecycle/index.js.map +1 -0
  44. package/dist/opLifecycle/opCompressor.d.ts +18 -0
  45. package/dist/opLifecycle/opCompressor.d.ts.map +1 -0
  46. package/dist/opLifecycle/opCompressor.js +53 -0
  47. package/dist/opLifecycle/opCompressor.js.map +1 -0
  48. package/dist/opLifecycle/opDecompressor.d.ts +20 -0
  49. package/dist/opLifecycle/opDecompressor.d.ts.map +1 -0
  50. package/dist/opLifecycle/opDecompressor.js +72 -0
  51. package/dist/opLifecycle/opDecompressor.js.map +1 -0
  52. package/dist/opLifecycle/opSplitter.d.ts +17 -0
  53. package/dist/opLifecycle/opSplitter.d.ts.map +1 -0
  54. package/dist/opLifecycle/opSplitter.js +61 -0
  55. package/dist/opLifecycle/opSplitter.js.map +1 -0
  56. package/dist/opLifecycle/outbox.d.ts +47 -0
  57. package/dist/opLifecycle/outbox.d.ts.map +1 -0
  58. package/dist/opLifecycle/outbox.js +153 -0
  59. package/dist/opLifecycle/outbox.js.map +1 -0
  60. package/dist/opLifecycle/remoteMessageProcessor.d.ts +26 -0
  61. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -0
  62. package/dist/opLifecycle/remoteMessageProcessor.js +81 -0
  63. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -0
  64. package/dist/packageVersion.d.ts +1 -1
  65. package/dist/packageVersion.js +1 -1
  66. package/dist/packageVersion.js.map +1 -1
  67. package/dist/summaryFormat.js +2 -2
  68. package/dist/summaryFormat.js.map +1 -1
  69. package/lib/blobManager.d.ts +20 -5
  70. package/lib/blobManager.d.ts.map +1 -1
  71. package/lib/blobManager.js +59 -17
  72. package/lib/blobManager.js.map +1 -1
  73. package/lib/containerRuntime.d.ts +39 -40
  74. package/lib/containerRuntime.d.ts.map +1 -1
  75. package/lib/containerRuntime.js +113 -275
  76. package/lib/containerRuntime.js.map +1 -1
  77. package/lib/dataStoreContext.d.ts +3 -1
  78. package/lib/dataStoreContext.d.ts.map +1 -1
  79. package/lib/dataStoreContext.js +23 -5
  80. package/lib/dataStoreContext.js.map +1 -1
  81. package/lib/dataStores.d.ts +8 -5
  82. package/lib/dataStores.d.ts.map +1 -1
  83. package/lib/dataStores.js +28 -15
  84. package/lib/dataStores.js.map +1 -1
  85. package/lib/garbageCollection.d.ts +15 -17
  86. package/lib/garbageCollection.d.ts.map +1 -1
  87. package/lib/garbageCollection.js +72 -86
  88. package/lib/garbageCollection.js.map +1 -1
  89. package/lib/garbageCollectionConstants.d.ts +19 -0
  90. package/lib/garbageCollectionConstants.d.ts.map +1 -0
  91. package/lib/garbageCollectionConstants.js +31 -0
  92. package/lib/garbageCollectionConstants.js.map +1 -0
  93. package/lib/gcSweepReadyUsageDetection.js +1 -1
  94. package/lib/gcSweepReadyUsageDetection.js.map +1 -1
  95. package/lib/index.d.ts +4 -2
  96. package/lib/index.d.ts.map +1 -1
  97. package/lib/index.js +3 -2
  98. package/lib/index.js.map +1 -1
  99. package/lib/opLifecycle/batchManager.d.ts +30 -0
  100. package/lib/opLifecycle/batchManager.d.ts.map +1 -0
  101. package/lib/{batchManager.js → opLifecycle/batchManager.js} +25 -10
  102. package/lib/opLifecycle/batchManager.js.map +1 -0
  103. package/lib/opLifecycle/definitions.d.ts +40 -0
  104. package/lib/opLifecycle/definitions.d.ts.map +1 -0
  105. package/lib/opLifecycle/definitions.js +6 -0
  106. package/lib/opLifecycle/definitions.js.map +1 -0
  107. package/lib/opLifecycle/index.d.ts +12 -0
  108. package/lib/opLifecycle/index.d.ts.map +1 -0
  109. package/lib/opLifecycle/index.js +11 -0
  110. package/lib/opLifecycle/index.js.map +1 -0
  111. package/lib/opLifecycle/opCompressor.d.ts +18 -0
  112. package/lib/opLifecycle/opCompressor.d.ts.map +1 -0
  113. package/lib/opLifecycle/opCompressor.js +49 -0
  114. package/lib/opLifecycle/opCompressor.js.map +1 -0
  115. package/lib/opLifecycle/opDecompressor.d.ts +20 -0
  116. package/lib/opLifecycle/opDecompressor.d.ts.map +1 -0
  117. package/lib/opLifecycle/opDecompressor.js +68 -0
  118. package/lib/opLifecycle/opDecompressor.js.map +1 -0
  119. package/lib/opLifecycle/opSplitter.d.ts +17 -0
  120. package/lib/opLifecycle/opSplitter.d.ts.map +1 -0
  121. package/lib/opLifecycle/opSplitter.js +57 -0
  122. package/lib/opLifecycle/opSplitter.js.map +1 -0
  123. package/lib/opLifecycle/outbox.d.ts +47 -0
  124. package/lib/opLifecycle/outbox.d.ts.map +1 -0
  125. package/lib/opLifecycle/outbox.js +149 -0
  126. package/lib/opLifecycle/outbox.js.map +1 -0
  127. package/lib/opLifecycle/remoteMessageProcessor.d.ts +26 -0
  128. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -0
  129. package/lib/opLifecycle/remoteMessageProcessor.js +76 -0
  130. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -0
  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/summaryFormat.js +1 -1
  135. package/lib/summaryFormat.js.map +1 -1
  136. package/package.json +35 -21
  137. package/prettier.config.cjs +8 -0
  138. package/src/blobManager.ts +74 -19
  139. package/src/containerRuntime.ts +144 -341
  140. package/src/dataStoreContext.ts +33 -5
  141. package/src/dataStores.ts +32 -16
  142. package/src/garbageCollection.ts +106 -82
  143. package/src/garbageCollectionConstants.ts +35 -0
  144. package/src/gcSweepReadyUsageDetection.ts +1 -1
  145. package/src/index.ts +6 -4
  146. package/src/{batchManager.ts → opLifecycle/batchManager.ts} +41 -23
  147. package/src/opLifecycle/definitions.ts +44 -0
  148. package/src/opLifecycle/index.ts +17 -0
  149. package/src/opLifecycle/opCompressor.ts +64 -0
  150. package/src/opLifecycle/opDecompressor.ts +84 -0
  151. package/src/opLifecycle/opSplitter.ts +78 -0
  152. package/src/opLifecycle/outbox.ts +204 -0
  153. package/src/opLifecycle/remoteMessageProcessor.ts +90 -0
  154. package/src/packageVersion.ts +1 -1
  155. package/src/summaryFormat.ts +1 -1
  156. package/dist/batchManager.d.ts +0 -36
  157. package/dist/batchManager.d.ts.map +0 -1
  158. package/dist/batchManager.js.map +0 -1
  159. package/lib/batchManager.d.ts +0 -36
  160. package/lib/batchManager.d.ts.map +0 -1
  161. package/lib/batchManager.js.map +0 -1
@@ -0,0 +1,35 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+
7
+ // The key for the GC tree in summary.
8
+ export const gcTreeKey = "gc";
9
+ // They prefix for GC blobs in the GC tree in summary.
10
+ export const gcBlobPrefix = "__gc";
11
+ // The key for tombstone blob in the GC tree in summary.
12
+ export const gcTombstoneBlobKey = "__tombstones";
13
+
14
+ // Feature gate key to turn GC on / off.
15
+ export const runGCKey = "Fluid.GarbageCollection.RunGC";
16
+ // Feature gate key to turn GC sweep on / off.
17
+ export const runSweepKey = "Fluid.GarbageCollection.RunSweep";
18
+ // Feature gate key to turn GC test mode on / off.
19
+ export const gcTestModeKey = "Fluid.GarbageCollection.GCTestMode";
20
+ // Feature gate key to expire a session after a set period of time.
21
+ export const runSessionExpiryKey = "Fluid.GarbageCollection.RunSessionExpiry";
22
+ // Feature gate key to write the gc blob as a handle if the data is the same.
23
+ export const trackGCStateKey = "Fluid.GarbageCollection.TrackGCState";
24
+ // Feature gate key to turn GC sweep log off.
25
+ export const disableSweepLogKey = "Fluid.GarbageCollection.DisableSweepLog";
26
+ // Feature gate key to disable the tombstone feature, i.e., tombstone information is not read / written into summary.
27
+ export const disableTombstoneKey = "Fluid.GarbageCollection.DisableTombstone";
28
+ // Feature gate to enable throwing an error when tombstone object is used.
29
+ export const throwOnTombstoneUsageKey = "Fluid.GarbageCollection.ThrowOnTombstoneUsage";
30
+
31
+ // One day in milliseconds.
32
+ export const oneDayMs = 1 * 24 * 60 * 60 * 1000;
33
+
34
+ export const defaultInactiveTimeoutMs = 7 * oneDayMs; // 7 days
35
+ export const defaultSessionExpiryDurationMs = 30 * oneDayMs; // 30 days
@@ -11,7 +11,7 @@ import {
11
11
  LoggingError,
12
12
  MonitoringContext,
13
13
  } from "@fluidframework/telemetry-utils";
14
- import { oneDayMs } from "./garbageCollection";
14
+ import { oneDayMs } from "./garbageCollectionConstants";
15
15
 
16
16
  /**
17
17
  * Feature Gate Key -
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@
5
5
 
6
6
  export {
7
7
  ContainerMessageType,
8
- IChunkedOp,
9
8
  ContainerRuntimeMessage,
10
9
  IGCRuntimeOptions,
11
10
  ISummaryRuntimeOptions,
@@ -17,21 +16,23 @@ export {
17
16
  IRootSummaryTreeWithStats,
18
17
  isRuntimeMessage,
19
18
  RuntimeMessage,
20
- unpackRuntimeMessage,
21
19
  agentSchedulerId,
22
20
  ContainerRuntime,
23
21
  RuntimeHeaders,
24
22
  ISummaryConfiguration,
25
23
  DefaultSummaryConfiguration,
26
24
  ICompressionRuntimeOptions,
25
+ CompressionAlgorithms,
27
26
  } from "./containerRuntime";
28
27
  export { FluidDataStoreRegistry } from "./dataStoreRegistry";
28
+ export {
29
+ IGCStats,
30
+ } from "./garbageCollection";
29
31
  export {
30
32
  gcBlobPrefix,
31
33
  gcTombstoneBlobKey,
32
34
  gcTreeKey,
33
- IGCStats,
34
- } from "./garbageCollection";
35
+ } from "./garbageCollectionConstants";
35
36
  export {
36
37
  IPendingFlush,
37
38
  IPendingLocalState,
@@ -81,3 +82,4 @@ export {
81
82
  SummaryCollection,
82
83
  } from "./summaryCollection";
83
84
  export { ICancellableSummarizerController, neverCancelledSummaryToken } from "./runWhileConnectedCoordinator";
85
+ export { IChunkedOp, unpackRuntimeMessage } from "./opLifecycle";
@@ -3,17 +3,14 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { IBatchMessage } from "@fluidframework/container-definitions";
7
- import { ContainerRuntimeMessage } from "./containerRuntime";
6
+ import { ICompressionRuntimeOptions } from "../containerRuntime";
7
+ import { BatchMessage, IBatch, IBatchCheckpoint } from "./definitions";
8
8
 
9
- /**
10
- * Message type used by BatchManager
11
- */
12
- export type BatchMessage = IBatchMessage & {
13
- localOpMetadata: unknown;
14
- deserializedContent: ContainerRuntimeMessage;
15
- referenceSequenceNumber: number;
16
- };
9
+ export interface IBatchManagerOptions {
10
+ readonly hardLimit: number;
11
+ readonly softLimit?: number;
12
+ readonly compressionOptions?: ICompressionRuntimeOptions;
13
+ }
17
14
 
18
15
  /**
19
16
  * Helper class that manages partial batch & rollback.
@@ -23,15 +20,12 @@ export class BatchManager {
23
20
  private batchContentSize = 0;
24
21
 
25
22
  public get length() { return this.pendingBatch.length; }
26
- public get limit() { return this.hardLimit; }
23
+ public get contentSizeInBytes() { return this.batchContentSize; }
27
24
 
28
- constructor(
29
- private readonly hardLimit: number,
30
- public readonly softLimit?: number,
31
- ) { }
25
+ constructor(public readonly options: IBatchManagerOptions) { }
32
26
 
33
27
  public push(message: BatchMessage): boolean {
34
- const contentSize = this.batchContentSize + message.contents.length;
28
+ const contentSize = this.batchContentSize + (message.contents?.length ?? 0);
35
29
  const opCount = this.pendingBatch.length;
36
30
 
37
31
  // Attempt to estimate batch size, aka socket message size.
@@ -44,11 +38,15 @@ export class BatchManager {
44
38
  // If we were provided soft limit, check for exceeding it.
45
39
  // But only if we have any ops, as the intention here is to flush existing ops (on exceeding this limit)
46
40
  // and start over. That's not an option if we have no ops.
47
- if (this.softLimit !== undefined && this.length > 0 && socketMessageSize >= this.softLimit) {
41
+ // If compression is enabled, the soft and hard limit are ignored and the message will be pushed anyways.
42
+ // Cases where the message is still too large will be handled by the maxConsecutiveReconnects path.
43
+ if (this.options.softLimit !== undefined
44
+ && this.length > 0
45
+ && socketMessageSize >= this.options.softLimit) {
48
46
  return false;
49
47
  }
50
48
 
51
- if (socketMessageSize >= this.limit) {
49
+ if (socketMessageSize >= this.options.hardLimit) {
52
50
  return false;
53
51
  }
54
52
 
@@ -59,24 +57,29 @@ export class BatchManager {
59
57
 
60
58
  public get empty() { return this.pendingBatch.length === 0; }
61
59
 
62
- public popBatch() {
63
- const batch = this.pendingBatch;
60
+ public popBatch(): IBatch {
61
+ const batch: IBatch = {
62
+ content: this.pendingBatch,
63
+ contentSizeInBytes: this.batchContentSize,
64
+ };
65
+
64
66
  this.pendingBatch = [];
65
67
  this.batchContentSize = 0;
66
- return batch;
68
+
69
+ return addBatchMetadata(batch);
67
70
  }
68
71
 
69
72
  /**
70
73
  * Capture the pending state at this point
71
74
  */
72
- public checkpoint() {
75
+ public checkpoint(): IBatchCheckpoint {
73
76
  const startPoint = this.pendingBatch.length;
74
77
  return {
75
78
  rollback: (process: (message: BatchMessage) => void) => {
76
79
  for (let i = this.pendingBatch.length; i > startPoint;) {
77
80
  i--;
78
81
  const message = this.pendingBatch[i];
79
- this.batchContentSize -= message.contents.length;
82
+ this.batchContentSize -= message.contents?.length ?? 0;
80
83
  process(message);
81
84
  }
82
85
 
@@ -85,3 +88,18 @@ export class BatchManager {
85
88
  };
86
89
  }
87
90
  }
91
+
92
+ const addBatchMetadata = (batch: IBatch): IBatch => {
93
+ if (batch.content.length > 1) {
94
+ batch.content[0].metadata = {
95
+ ...batch.content[0].metadata,
96
+ batch: true
97
+ };
98
+ batch.content[batch.content.length - 1].metadata = {
99
+ ...batch.content[batch.content.length - 1].metadata,
100
+ batch: false
101
+ };
102
+ }
103
+
104
+ return batch;
105
+ };
@@ -0,0 +1,44 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { IBatchMessage } from "@fluidframework/container-definitions";
7
+ import { MessageType } from "@fluidframework/protocol-definitions";
8
+ import { CompressionAlgorithms, ContainerMessageType, ContainerRuntimeMessage } from "..";
9
+
10
+ /**
11
+ * Batch message type used internally by the runtime
12
+ */
13
+ export type BatchMessage = IBatchMessage & {
14
+ localOpMetadata: unknown;
15
+ deserializedContent: ContainerRuntimeMessage;
16
+ referenceSequenceNumber: number;
17
+ compression?: CompressionAlgorithms;
18
+ };
19
+
20
+ /**
21
+ * Batch interface used internally by the runtime.
22
+ */
23
+ export interface IBatch {
24
+ /**
25
+ * Sum of the in-memory content sizes of all messages in the batch.
26
+ * If the batch is compressed, this number reflects the post-compression size.
27
+ */
28
+ readonly contentSizeInBytes: number;
29
+ /**
30
+ * All the messages in the batch
31
+ */
32
+ readonly content: BatchMessage[];
33
+ }
34
+
35
+ export interface IBatchCheckpoint {
36
+ rollback: (action: (message: BatchMessage) => void) => void;
37
+ }
38
+
39
+ export interface IChunkedOp {
40
+ chunkId: number;
41
+ totalChunks: number;
42
+ contents: string;
43
+ originalType: MessageType | ContainerMessageType;
44
+ }
@@ -0,0 +1,17 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ export { BatchManager } from "./batchManager";
7
+ export {
8
+ BatchMessage,
9
+ IBatch,
10
+ IBatchCheckpoint,
11
+ IChunkedOp,
12
+ } from "./definitions";
13
+ export { Outbox } from "./outbox";
14
+ export { OpCompressor } from "./opCompressor";
15
+ export { OpDecompressor } from "./opDecompressor";
16
+ export { OpSplitter } from "./opSplitter";
17
+ export { RemoteMessageProcessor, unpackRuntimeMessage } from "./remoteMessageProcessor";
@@ -0,0 +1,64 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
+ import { IsoBuffer } from "@fluidframework/common-utils";
8
+ import { ChildLogger } from "@fluidframework/telemetry-utils";
9
+ import { compress } from "lz4js";
10
+ import { CompressionAlgorithms, ContainerRuntimeMessage } from "../containerRuntime";
11
+ import { IBatch, BatchMessage } from "./definitions";
12
+
13
+ /**
14
+ * Compresses batches of ops. It generates a single compressed op that contains
15
+ * the contents of each op in the batch. It then submits empty ops for each original
16
+ * op to reserve sequence numbers.
17
+ */
18
+ export class OpCompressor {
19
+ private readonly logger;
20
+ private compressedBatchCount = 0;
21
+
22
+ constructor(logger: ITelemetryLogger) {
23
+ this.logger = ChildLogger.create(logger, "OpCompressor");
24
+ }
25
+
26
+ public compressBatch(batch: IBatch): IBatch {
27
+ const messages: BatchMessage[] = [];
28
+ this.compressedBatchCount++;
29
+ const contentToCompress: ContainerRuntimeMessage[] = [];
30
+ for (const message of batch.content) {
31
+ contentToCompress.push(message.deserializedContent);
32
+ }
33
+
34
+ const compressionStart = Date.now();
35
+ const contentsAsBuffer = new TextEncoder().encode(JSON.stringify(contentToCompress));
36
+ const compressedContents = compress(contentsAsBuffer);
37
+ const compressedContent = IsoBuffer.from(compressedContents).toString("base64");
38
+ const duration = Date.now() - compressionStart;
39
+
40
+ if (batch.contentSizeInBytes > 200000 || this.compressedBatchCount % 25) {
41
+ this.logger.sendPerformanceEvent({
42
+ eventName: "CompressedBatch",
43
+ duration,
44
+ sizeBeforeCompression: batch.contentSizeInBytes,
45
+ sizeAfterCompression: compressedContent.length,
46
+ });
47
+ }
48
+
49
+ messages.push({
50
+ ...batch.content[0], contents: JSON.stringify({ packedContents: compressedContent }),
51
+ metadata: { ...batch.content[0].metadata, compressed: true },
52
+ compression: CompressionAlgorithms.lz4,
53
+ });
54
+
55
+ for (const message of batch.content.slice(1)) {
56
+ messages.push({ ...message, contents: undefined });
57
+ }
58
+
59
+ return {
60
+ contentSizeInBytes: compressedContent.length,
61
+ content: messages,
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,84 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { decompress } from "lz4js";
7
+ import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
8
+ import { assert, IsoBuffer, Uint8ArrayToString } from "@fluidframework/common-utils";
9
+ import { CompressionAlgorithms } from "../containerRuntime";
10
+
11
+ /**
12
+ * State machine that "unrolls" contents of compressed batches of ops after decompressing them.
13
+ * This class relies on some implicit contracts defined below:
14
+ * 1. A compressed batch will have its first message with batch metadata set to true and compressed set to true
15
+ * 2. Messages in the middle of a compressed batch will have neither batch metadata nor the compression property set
16
+ * 3. The final message of a batch will have batch metadata set to false
17
+ * 4. An individually compressed op will have undefined batch metadata and compression set to true
18
+ */
19
+ export class OpDecompressor {
20
+ private activeBatch = false;
21
+ private rootMessageContents: any | undefined;
22
+ private processedCount = 0;
23
+
24
+ public processMessage(message: ISequencedDocumentMessage): ISequencedDocumentMessage {
25
+ // We're checking for compression = true or top level compression property so
26
+ // that we can enable compression without waiting on all ordering services
27
+ // to pick up protocol change. Eventually only the top level property should
28
+ // be used.
29
+ if (message.metadata?.batch === true
30
+ && (message.metadata?.compressed || message.compression !== undefined)) {
31
+ // Beginning of a compressed batch
32
+ assert(this.activeBatch === false, 0x4b8 /* shouldn't have multiple active batches */);
33
+ if (message.compression) {
34
+ // lz4 is the only supported compression algorithm for now
35
+ assert(message.compression === CompressionAlgorithms.lz4,
36
+ 0x4b9 /* lz4 is currently the only supported compression algorithm */);
37
+ }
38
+
39
+ this.activeBatch = true;
40
+
41
+ const contents = IsoBuffer.from(message.contents.packedContents, "base64");
42
+ const decompressedMessage = decompress(contents);
43
+ const intoString = Uint8ArrayToString(decompressedMessage);
44
+ const asObj = JSON.parse(intoString);
45
+ this.rootMessageContents = asObj;
46
+
47
+ return { ...message, contents: this.rootMessageContents[this.processedCount++] };
48
+ }
49
+
50
+ if (this.rootMessageContents !== undefined && message.metadata?.batch === undefined && this.activeBatch) {
51
+ // Continuation of compressed batch
52
+ return { ...message, contents: this.rootMessageContents[this.processedCount++] };
53
+ }
54
+
55
+ if (this.rootMessageContents !== undefined && message.metadata?.batch === false) {
56
+ // End of compressed batch
57
+ const returnMessage = {
58
+ ...message,
59
+ contents: this.rootMessageContents[this.processedCount++]
60
+ };
61
+
62
+ this.activeBatch = false;
63
+ this.rootMessageContents = undefined;
64
+ this.processedCount = 0;
65
+
66
+ return returnMessage;
67
+ }
68
+
69
+ if (message.metadata?.batch === undefined &&
70
+ (message.metadata?.compressed || message.compression === CompressionAlgorithms.lz4)) {
71
+ // Single compressed message
72
+ assert(this.activeBatch === false, 0x4ba /* shouldn't receive compressed message in middle of a batch */);
73
+
74
+ const contents = IsoBuffer.from(message.contents.packedContents, "base64");
75
+ const decompressedMessage = decompress(contents);
76
+ const intoString = new TextDecoder().decode(decompressedMessage);
77
+ const asObj = JSON.parse(intoString);
78
+
79
+ return { ...message, contents: asObj[0] };
80
+ }
81
+
82
+ return message;
83
+ }
84
+ }
@@ -0,0 +1,78 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { DataCorruptionError, extractSafePropertiesFromMessage } from "@fluidframework/container-utils";
7
+ import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
8
+ import { ContainerMessageType } from "../containerRuntime";
9
+ import { IChunkedOp } from "./definitions";
10
+
11
+ /**
12
+ * Responsible for creating and reconstructing chunked messages.
13
+ */
14
+ export class OpSplitter {
15
+ // Local copy of incomplete received chunks.
16
+ private readonly chunkMap: Map<string, string[]>;
17
+
18
+ constructor(chunks: [string, string[]][]) {
19
+ this.chunkMap = new Map<string, string[]>(chunks);
20
+ }
21
+
22
+ public get chunks(): ReadonlyMap<string, string[]> {
23
+ return this.chunkMap;
24
+ }
25
+
26
+ public processRemoteMessage(message: ISequencedDocumentMessage): ISequencedDocumentMessage {
27
+ if (message.type !== ContainerMessageType.ChunkedOp) {
28
+ return message;
29
+ }
30
+
31
+ const clientId = message.clientId;
32
+ const chunkedContent = message.contents as IChunkedOp;
33
+ this.addChunk(clientId, chunkedContent, message);
34
+
35
+ if (chunkedContent.chunkId < chunkedContent.totalChunks) {
36
+ // We are processing the op in chunks but haven't reached
37
+ // the last chunk yet in order to reconstruct the original op
38
+ return message;
39
+ }
40
+
41
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
42
+ const serializedContent = this.chunkMap.get(clientId)!.join("");
43
+ this.clearPartialChunks(clientId);
44
+
45
+ const newMessage = { ...message };
46
+ newMessage.contents = serializedContent === "" ? undefined : JSON.parse(serializedContent);
47
+ newMessage.type = chunkedContent.originalType;
48
+ return newMessage;
49
+ }
50
+
51
+ public clearPartialChunks(clientId: string) {
52
+ if (this.chunkMap.has(clientId)) {
53
+ this.chunkMap.delete(clientId);
54
+ }
55
+ }
56
+
57
+ private addChunk(clientId: string, chunkedContent: IChunkedOp, originalMessage: ISequencedDocumentMessage) {
58
+ let map = this.chunkMap.get(clientId);
59
+ if (map === undefined) {
60
+ map = [];
61
+ this.chunkMap.set(clientId, map);
62
+ }
63
+
64
+ if (chunkedContent.chunkId !== map.length + 1) {
65
+ // We are expecting the chunks to be processed sequentially, in the same order as they are sent.
66
+ // Therefore, the chunkId of the incoming op needs to match the length of the array (1-based indexing)
67
+ // holding the existing chunks for that particular clientId.
68
+ throw new DataCorruptionError("Chunk Id mismatch", {
69
+ ...extractSafePropertiesFromMessage(originalMessage),
70
+ chunkMapLength: map.length,
71
+ chunkId: chunkedContent.chunkId,
72
+ totalChunks: chunkedContent.totalChunks,
73
+ });
74
+ }
75
+
76
+ map.push(chunkedContent.contents);
77
+ }
78
+ }