@fluidframework/container-runtime 2.3.0-288113 → 2.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 (183) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/api-report/container-runtime.legacy.alpha.api.md +15 -0
  3. package/container-runtime.test-files.tar +0 -0
  4. package/dist/channelCollection.d.ts +1 -1
  5. package/dist/channelCollection.d.ts.map +1 -1
  6. package/dist/channelCollection.js +1 -16
  7. package/dist/channelCollection.js.map +1 -1
  8. package/dist/connectionTelemetry.d.ts +27 -3
  9. package/dist/connectionTelemetry.d.ts.map +1 -1
  10. package/dist/connectionTelemetry.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +68 -13
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +262 -180
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/deltaManagerProxies.d.ts.map +1 -1
  16. package/dist/deltaManagerProxies.js +11 -4
  17. package/dist/deltaManagerProxies.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +0 -2
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcHelpers.d.ts.map +1 -1
  22. package/dist/gc/gcHelpers.js +0 -8
  23. package/dist/gc/gcHelpers.js.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/legacy.d.ts +3 -1
  29. package/dist/messageTypes.d.ts +0 -9
  30. package/dist/messageTypes.d.ts.map +1 -1
  31. package/dist/messageTypes.js.map +1 -1
  32. package/dist/opLifecycle/batchManager.d.ts +9 -0
  33. package/dist/opLifecycle/batchManager.d.ts.map +1 -1
  34. package/dist/opLifecycle/batchManager.js +19 -6
  35. package/dist/opLifecycle/batchManager.js.map +1 -1
  36. package/dist/opLifecycle/duplicateBatchDetector.d.ts +32 -0
  37. package/dist/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
  38. package/dist/opLifecycle/duplicateBatchDetector.js +68 -0
  39. package/dist/opLifecycle/duplicateBatchDetector.js.map +1 -0
  40. package/dist/opLifecycle/index.d.ts +3 -2
  41. package/dist/opLifecycle/index.d.ts.map +1 -1
  42. package/dist/opLifecycle/index.js +4 -1
  43. package/dist/opLifecycle/index.js.map +1 -1
  44. package/dist/opLifecycle/opCompressor.d.ts.map +1 -1
  45. package/dist/opLifecycle/opCompressor.js +0 -4
  46. package/dist/opLifecycle/opCompressor.js.map +1 -1
  47. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  48. package/dist/opLifecycle/opGroupingManager.js +0 -4
  49. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  50. package/dist/opLifecycle/opSplitter.d.ts.map +1 -1
  51. package/dist/opLifecycle/opSplitter.js +1 -6
  52. package/dist/opLifecycle/opSplitter.js.map +1 -1
  53. package/dist/opLifecycle/outbox.d.ts.map +1 -1
  54. package/dist/opLifecycle/outbox.js +1 -4
  55. package/dist/opLifecycle/outbox.js.map +1 -1
  56. package/dist/opLifecycle/remoteMessageProcessor.d.ts +37 -17
  57. package/dist/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  58. package/dist/opLifecycle/remoteMessageProcessor.js +47 -37
  59. package/dist/opLifecycle/remoteMessageProcessor.js.map +1 -1
  60. package/dist/packageVersion.d.ts +1 -1
  61. package/dist/packageVersion.d.ts.map +1 -1
  62. package/dist/packageVersion.js +1 -1
  63. package/dist/packageVersion.js.map +1 -1
  64. package/dist/pendingStateManager.d.ts +27 -17
  65. package/dist/pendingStateManager.d.ts.map +1 -1
  66. package/dist/pendingStateManager.js +85 -56
  67. package/dist/pendingStateManager.js.map +1 -1
  68. package/dist/scheduleManager.d.ts +2 -4
  69. package/dist/scheduleManager.d.ts.map +1 -1
  70. package/dist/scheduleManager.js +6 -37
  71. package/dist/scheduleManager.js.map +1 -1
  72. package/dist/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  73. package/dist/summary/summarizerNode/summarizerNodeUtils.js +0 -2
  74. package/dist/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  75. package/dist/summary/summaryCollection.d.ts.map +1 -1
  76. package/dist/summary/summaryCollection.js +5 -7
  77. package/dist/summary/summaryCollection.js.map +1 -1
  78. package/dist/summary/summaryFormat.d.ts.map +1 -1
  79. package/dist/summary/summaryFormat.js +1 -4
  80. package/dist/summary/summaryFormat.js.map +1 -1
  81. package/lib/channelCollection.d.ts +1 -1
  82. package/lib/channelCollection.d.ts.map +1 -1
  83. package/lib/channelCollection.js +1 -16
  84. package/lib/channelCollection.js.map +1 -1
  85. package/lib/connectionTelemetry.d.ts +27 -3
  86. package/lib/connectionTelemetry.d.ts.map +1 -1
  87. package/lib/connectionTelemetry.js.map +1 -1
  88. package/lib/containerRuntime.d.ts +68 -13
  89. package/lib/containerRuntime.d.ts.map +1 -1
  90. package/lib/containerRuntime.js +262 -181
  91. package/lib/containerRuntime.js.map +1 -1
  92. package/lib/deltaManagerProxies.d.ts.map +1 -1
  93. package/lib/deltaManagerProxies.js +11 -4
  94. package/lib/deltaManagerProxies.js.map +1 -1
  95. package/lib/gc/garbageCollection.d.ts.map +1 -1
  96. package/lib/gc/garbageCollection.js +0 -2
  97. package/lib/gc/garbageCollection.js.map +1 -1
  98. package/lib/gc/gcHelpers.d.ts.map +1 -1
  99. package/lib/gc/gcHelpers.js +0 -8
  100. package/lib/gc/gcHelpers.js.map +1 -1
  101. package/lib/index.d.ts +1 -1
  102. package/lib/index.d.ts.map +1 -1
  103. package/lib/index.js +1 -1
  104. package/lib/index.js.map +1 -1
  105. package/lib/legacy.d.ts +3 -1
  106. package/lib/messageTypes.d.ts +0 -9
  107. package/lib/messageTypes.d.ts.map +1 -1
  108. package/lib/messageTypes.js.map +1 -1
  109. package/lib/opLifecycle/batchManager.d.ts +9 -0
  110. package/lib/opLifecycle/batchManager.d.ts.map +1 -1
  111. package/lib/opLifecycle/batchManager.js +17 -5
  112. package/lib/opLifecycle/batchManager.js.map +1 -1
  113. package/lib/opLifecycle/duplicateBatchDetector.d.ts +32 -0
  114. package/lib/opLifecycle/duplicateBatchDetector.d.ts.map +1 -0
  115. package/lib/opLifecycle/duplicateBatchDetector.js +64 -0
  116. package/lib/opLifecycle/duplicateBatchDetector.js.map +1 -0
  117. package/lib/opLifecycle/index.d.ts +3 -2
  118. package/lib/opLifecycle/index.d.ts.map +1 -1
  119. package/lib/opLifecycle/index.js +2 -1
  120. package/lib/opLifecycle/index.js.map +1 -1
  121. package/lib/opLifecycle/opCompressor.d.ts.map +1 -1
  122. package/lib/opLifecycle/opCompressor.js +0 -4
  123. package/lib/opLifecycle/opCompressor.js.map +1 -1
  124. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  125. package/lib/opLifecycle/opGroupingManager.js +0 -4
  126. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  127. package/lib/opLifecycle/opSplitter.d.ts.map +1 -1
  128. package/lib/opLifecycle/opSplitter.js +1 -6
  129. package/lib/opLifecycle/opSplitter.js.map +1 -1
  130. package/lib/opLifecycle/outbox.d.ts.map +1 -1
  131. package/lib/opLifecycle/outbox.js +1 -4
  132. package/lib/opLifecycle/outbox.js.map +1 -1
  133. package/lib/opLifecycle/remoteMessageProcessor.d.ts +37 -17
  134. package/lib/opLifecycle/remoteMessageProcessor.d.ts.map +1 -1
  135. package/lib/opLifecycle/remoteMessageProcessor.js +47 -37
  136. package/lib/opLifecycle/remoteMessageProcessor.js.map +1 -1
  137. package/lib/packageVersion.d.ts +1 -1
  138. package/lib/packageVersion.d.ts.map +1 -1
  139. package/lib/packageVersion.js +1 -1
  140. package/lib/packageVersion.js.map +1 -1
  141. package/lib/pendingStateManager.d.ts +27 -17
  142. package/lib/pendingStateManager.d.ts.map +1 -1
  143. package/lib/pendingStateManager.js +85 -56
  144. package/lib/pendingStateManager.js.map +1 -1
  145. package/lib/scheduleManager.d.ts +2 -4
  146. package/lib/scheduleManager.d.ts.map +1 -1
  147. package/lib/scheduleManager.js +6 -37
  148. package/lib/scheduleManager.js.map +1 -1
  149. package/lib/summary/summarizerNode/summarizerNodeUtils.d.ts.map +1 -1
  150. package/lib/summary/summarizerNode/summarizerNodeUtils.js +0 -2
  151. package/lib/summary/summarizerNode/summarizerNodeUtils.js.map +1 -1
  152. package/lib/summary/summaryCollection.d.ts.map +1 -1
  153. package/lib/summary/summaryCollection.js +5 -7
  154. package/lib/summary/summaryCollection.js.map +1 -1
  155. package/lib/summary/summaryFormat.d.ts.map +1 -1
  156. package/lib/summary/summaryFormat.js +1 -4
  157. package/lib/summary/summaryFormat.js.map +1 -1
  158. package/lib/tsdoc-metadata.json +1 -1
  159. package/package.json +50 -24
  160. package/src/channelCollection.ts +7 -21
  161. package/src/connectionTelemetry.ts +33 -3
  162. package/src/containerRuntime.ts +382 -233
  163. package/src/deltaManagerProxies.ts +11 -4
  164. package/src/gc/garbageCollection.ts +1 -3
  165. package/src/gc/gcHelpers.ts +4 -12
  166. package/src/index.ts +2 -0
  167. package/src/messageTypes.ts +0 -10
  168. package/src/opLifecycle/batchManager.ts +29 -7
  169. package/src/opLifecycle/duplicateBatchDetector.ts +78 -0
  170. package/src/opLifecycle/index.ts +4 -1
  171. package/src/opLifecycle/opCompressor.ts +2 -6
  172. package/src/opLifecycle/opGroupingManager.ts +2 -6
  173. package/src/opLifecycle/opSplitter.ts +2 -6
  174. package/src/opLifecycle/outbox.ts +1 -3
  175. package/src/opLifecycle/remoteMessageProcessor.ts +87 -59
  176. package/src/packageVersion.ts +1 -1
  177. package/src/pendingStateManager.ts +114 -66
  178. package/src/scheduleManager.ts +8 -47
  179. package/src/summary/summarizerNode/summarizerNodeUtils.ts +1 -3
  180. package/src/summary/summaryCollection.ts +7 -9
  181. package/src/summary/summaryFormat.ts +1 -3
  182. package/src/summary/summaryFormats.md +11 -9
  183. package/tsconfig.json +1 -0
@@ -20,7 +20,13 @@ import {
20
20
  type LocalContainerRuntimeMessage,
21
21
  } from "./messageTypes.js";
22
22
  import { asBatchMetadata, asEmptyBatchLocalOpMetadata } from "./metadata.js";
23
- import { BatchId, BatchMessage, generateBatchId, InboundBatch } from "./opLifecycle/index.js";
23
+ import {
24
+ BatchId,
25
+ BatchMessage,
26
+ getEffectiveBatchId,
27
+ BatchStartInfo,
28
+ InboundMessageResult,
29
+ } from "./opLifecycle/index.js";
24
30
 
25
31
  /**
26
32
  * This represents a message that has been submitted and is added to the pending queue when `submit` is called on the
@@ -37,11 +43,14 @@ export interface IPendingMessage {
37
43
  sequenceNumber?: number;
38
44
  /** Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect */
39
45
  batchInfo: {
40
- /** The Batch's original clientId, from when it was first flushed to be submitted */
46
+ /**
47
+ * The Batch's original clientId, from when it was first flushed to be submitted.
48
+ * Or, a random uuid if it was never submitted (and batchStartCsn will be -1)
49
+ */
41
50
  clientId: string;
42
51
  /**
43
52
  * The Batch's original clientSequenceNumber, from when it was first flushed to be submitted
44
- * @remarks A negative value means it was not yet submitted when queued here (e.g. disconnected right before flush fired)
53
+ * Or, -1 if it was never submitted (and clientId will be a random uuid)
45
54
  */
46
55
  batchStartCsn: number;
47
56
  /** length of the batch (how many runtime messages here) */
@@ -125,20 +134,6 @@ function withoutLocalOpMetadata(message: IPendingMessage): IPendingMessage {
125
134
  };
126
135
  }
127
136
 
128
- /**
129
- * Get the effective batch ID for a pending message.
130
- * If the batch ID is already present in the message's op metadata, return it.
131
- * Otherwise, generate a new batch ID using the client ID and batch start CSN.
132
- * @param pendingMessage - The pending message
133
- * @returns The effective batch ID
134
- */
135
- function getEffectiveBatchId(pendingMessage: IPendingMessage): string {
136
- return (
137
- asBatchMetadata(pendingMessage.opMetadata)?.batchId ??
138
- generateBatchId(pendingMessage.batchInfo.clientId, pendingMessage.batchInfo.batchStartCsn)
139
- );
140
- }
141
-
142
137
  /**
143
138
  * PendingStateManager is responsible for maintaining the messages that have not been sent or have not yet been
144
139
  * acknowledged by the server. It also maintains the batch information for both automatically and manually flushed
@@ -159,9 +154,6 @@ export class PendingStateManager implements IDisposable {
159
154
  */
160
155
  private savedOps: IPendingMessage[] = [];
161
156
 
162
- /** Used to stand in for batchStartCsn for messages that weren't submitted (so no CSN) */
163
- private negativeCounter: number = -1;
164
-
165
157
  private readonly disposeOnce = new Lazy<void>(() => {
166
158
  this.initialMessages.clear();
167
159
  this.pendingMessages.clear();
@@ -249,15 +241,19 @@ export class PendingStateManager implements IDisposable {
249
241
  * or undefined if the batch was not yet sent (e.g. by the time we flushed we lost the connection)
250
242
  */
251
243
  public onFlushBatch(batch: BatchMessage[], clientSequenceNumber: number | undefined) {
252
- // If we're connected this is the client of the current connection,
253
- // otherwise it's the clientId that just disconnected
254
- // It's only undefined if we've NEVER connected. This is a tight corner case and we can
255
- // simply make up a unique ID in this case.
256
- const clientId = this.stateHandler.clientId() ?? uuid();
257
-
258
- // If the batch was not yet sent, we need to assign a unique batchStartCsn
259
- // Use a negative number to distinguish these from real CSNs
260
- const batchStartCsn = clientSequenceNumber ?? this.negativeCounter--;
244
+ // clientId and batchStartCsn are used for generating the batchId so we can detect container forks
245
+ // where this batch was submitted by two different clients rehydrating from the same local state.
246
+ // In the typical case where the batch was actually sent, use the clientId and clientSequenceNumber.
247
+ // In the case where the batch was not sent, use a random uuid for clientId, and -1 for clientSequenceNumber to indicate this case.
248
+ // This will guarantee uniqueness of the batchId, and is a suitable fallback since clientId/CSN is only needed if the batch was actually sent/sequenced.
249
+ const batchWasSent = clientSequenceNumber !== undefined;
250
+ const [clientId, batchStartCsn] = batchWasSent
251
+ ? [this.stateHandler.clientId(), clientSequenceNumber]
252
+ : [uuid(), -1]; // -1 will indicate not a real clientId/CSN pair
253
+ assert(
254
+ clientId !== undefined,
255
+ 0xa33 /* clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case */,
256
+ );
261
257
 
262
258
  for (const message of batch) {
263
259
  const {
@@ -326,49 +322,83 @@ export class PendingStateManager implements IDisposable {
326
322
  }
327
323
 
328
324
  /**
329
- * Processes an inbound batch of messages - May be local or remote.
325
+ * Compares the batch ID of the incoming batch with the pending batch ID for this client.
326
+ * They should not match, as that would indicate a forked container.
327
+ * @param remoteBatchStart - BatchStartInfo for an incoming batch *NOT* submitted by this client
328
+ * @returns whether the batch IDs match
329
+ */
330
+ private remoteBatchMatchesPendingBatch(remoteBatchStart: BatchStartInfo): boolean {
331
+ // We may have no pending changes - if so, no match, no problem.
332
+ const pendingMessage = this.pendingMessages.peekFront();
333
+ if (pendingMessage === undefined) {
334
+ return false;
335
+ }
336
+
337
+ // We must compare the effective batch IDs, since one of these ops
338
+ // may have been the original, not resubmitted, so wouldn't have its batch ID stamped yet.
339
+ const pendingBatchId = getEffectiveBatchId(pendingMessage);
340
+ const inboundBatchId = getEffectiveBatchId(remoteBatchStart);
341
+
342
+ return pendingBatchId === inboundBatchId;
343
+ }
344
+
345
+ /**
346
+ * Processes an inbound message or batch of messages - May be local or remote.
330
347
  *
331
- * @param batch - The inbound batch of messages to process. Could be local or remote.
332
- * @param local - true if we submitted this batch and expect corresponding pending messages
333
- * @returns The inbound batch's messages with localOpMetadata "zipped" in.
348
+ * @param inbound - The inbound message(s) to process, with extra info (e.g. about the start of a batch). Could be local or remote.
349
+ * @param local - true if we submitted these messages and expect corresponding pending messages
350
+ * @returns The inbound messages with localOpMetadata "zipped" in.
334
351
  *
335
- * @remarks Closes the container if:
336
- * - The batchStartCsn doesn't match for local batches
352
+ * @throws a DataProcessingError in either of these cases:
353
+ * - The pending message content doesn't match the incoming message content for any message here
354
+ * - The batch IDs *do match* but it's not local (indicates Container forking).
337
355
  */
338
- public processInboundBatch(
339
- batch: InboundBatch,
356
+ public processInboundMessages(
357
+ inbound: InboundMessageResult,
340
358
  local: boolean,
341
359
  ): {
342
360
  message: InboundSequencedContainerRuntimeMessage;
343
361
  localOpMetadata?: unknown;
344
362
  }[] {
345
363
  if (local) {
346
- return this.processPendingLocalBatch(batch);
364
+ return this.processPendingLocalMessages(inbound);
365
+ }
366
+
367
+ // An inbound remote batch should not match the pending batch ID for this client.
368
+ // That would indicate the container forked (two instances trying to submit the same local state)
369
+ if ("batchStart" in inbound && this.remoteBatchMatchesPendingBatch(inbound.batchStart)) {
370
+ throw DataProcessingError.create(
371
+ "Forked Container Error! Matching batchIds but mismatched clientId",
372
+ "PendingStateManager.processInboundMessages",
373
+ inbound.batchStart.keyMessage,
374
+ );
347
375
  }
348
376
 
349
377
  // No localOpMetadata for remote messages
350
- return batch.messages.map((message) => ({ message }));
378
+ const messages = inbound.type === "fullBatch" ? inbound.messages : [inbound.nextMessage];
379
+ return messages.map((message) => ({ message }));
351
380
  }
352
381
 
353
382
  /**
354
- * Processes the incoming batch from the server that was submitted by this client.
355
- * It verifies that messages are received in the right order and that the batch information is correct.
356
- * @param batch - The inbound batch (originating from this client) to correlate with the pending local state
357
- * @returns The inbound batch's messages with localOpMetadata "zipped" in.
383
+ * Processes the incoming message(s) from the server that were submitted by this client.
384
+ * It verifies that messages are received in the right order and that any batch information is correct.
385
+ * @param inbound - The inbound message(s) (originating from this client) to correlate with the pending local state
386
+ * @throws DataProcessingError if the pending message content doesn't match the incoming message content for any message here
387
+ * @returns The inbound messages with localOpMetadata "zipped" in.
358
388
  */
359
- private processPendingLocalBatch(batch: InboundBatch): {
389
+ private processPendingLocalMessages(inbound: InboundMessageResult): {
360
390
  message: InboundSequencedContainerRuntimeMessage;
361
391
  localOpMetadata: unknown;
362
392
  }[] {
363
- this.onLocalBatchBegin(batch);
393
+ if ("batchStart" in inbound) {
394
+ this.onLocalBatchBegin(inbound.batchStart, inbound.length);
395
+ }
364
396
 
365
397
  // Empty batch
366
- if (batch.messages.length === 0) {
367
- assert(
368
- batch.emptyBatchSequenceNumber !== undefined,
369
- 0x9fb /* Expected sequence number for empty batch */,
398
+ if (inbound.length === 0) {
399
+ const localOpMetadata = this.processNextPendingMessage(
400
+ inbound.batchStart.keyMessage.sequenceNumber,
370
401
  );
371
- const localOpMetadata = this.processNextPendingMessage(batch.emptyBatchSequenceNumber);
372
402
  assert(
373
403
  asEmptyBatchLocalOpMetadata(localOpMetadata)?.emptyBatch === true,
374
404
  0xa20 /* Expected empty batch marker */,
@@ -376,7 +406,9 @@ export class PendingStateManager implements IDisposable {
376
406
  return [];
377
407
  }
378
408
 
379
- return batch.messages.map((message) => ({
409
+ const messages = inbound.type === "fullBatch" ? inbound.messages : [inbound.nextMessage];
410
+
411
+ return messages.map((message) => ({
380
412
  message,
381
413
  localOpMetadata: this.processNextPendingMessage(message.sequenceNumber, message),
382
414
  }));
@@ -448,7 +480,7 @@ export class PendingStateManager implements IDisposable {
448
480
  /**
449
481
  * Check if the incoming batch matches the batch info for the next pending message.
450
482
  */
451
- private onLocalBatchBegin(batch: InboundBatch) {
483
+ private onLocalBatchBegin(batchStart: BatchStartInfo, batchLength?: number) {
452
484
  // Get the next message from the pending queue. Verify a message exists.
453
485
  const pendingMessage = this.pendingMessages.peekFront();
454
486
  assert(
@@ -456,29 +488,44 @@ export class PendingStateManager implements IDisposable {
456
488
  0xa21 /* No pending message found as we start processing this remote batch */,
457
489
  );
458
490
 
459
- // Note: This could be undefined if this batch became empty on resubmit.
460
- // In this case the next pending message is an empty batch marker.
461
- // Empty batches became empty on Resubmit, and submit them and track them in case
462
- // a different fork of this container also submitted the same batch (and it may not be empty for that fork).
463
- const firstMessage = batch.messages.length > 0 ? batch.messages[0] : undefined;
464
- const expectedPendingBatchLength = batch.messages.length === 0 ? 1 : batch.messages.length;
491
+ // If this batch became empty on resubmit, batch.messages will be empty (so firstMessage undefined)
492
+ // and the next pending message should be an empty batch marker.
493
+ // More Info: We must submit empty batches and track them in case a different fork
494
+ // of this container also submitted the same batch (and it may not be empty for that fork).
495
+ const firstMessage = batchStart.keyMessage;
496
+ // -1 length is for back compat, undefined length means we actually don't know it
497
+ const skipLengthCheck =
498
+ pendingMessage.batchInfo.length === -1 || batchLength === undefined;
499
+ const expectedPendingBatchLength =
500
+ batchLength === 0
501
+ ? 1 // For an empty batch, expect a singleton array with the empty batch marker
502
+ : batchLength; // Otherwise, the lengths should actually match
503
+
504
+ // Note: We don't need to use getEffectiveBatchId here, just check the explicit stamped batchID
505
+ // That logic is needed only when comparing across potential container forks.
506
+ // Furthermore, we also are comparing the batch IDs constituent data - clientId (it's local) and batchStartCsn.
507
+ const pendingBatchId = asBatchMetadata(pendingMessage.opMetadata)?.batchId;
508
+ const inboundBatchId = batchStart.batchId;
465
509
 
466
510
  // We expect the incoming batch to be of the same length, starting at the same clientSequenceNumber,
467
- // as the batch we originally submitted.
511
+ // as the batch we originally submitted. The batchIds should match as well, if set (or neither should be set)
468
512
  // We have another later check to compare the message contents, which we'd expect to fail if this check does,
469
- // so we don't throw here, merely log. In a later release this check may replace that one.
513
+ // so we don't throw here, merely log. In a later release this check may replace that one since it's cheaper.
470
514
  if (
471
- pendingMessage.batchInfo.batchStartCsn !== batch.batchStartCsn ||
472
- (pendingMessage.batchInfo.length >= 0 && // -1 length is back compat and isn't suitable for this check
473
- pendingMessage.batchInfo.length !== expectedPendingBatchLength)
515
+ pendingMessage.batchInfo.batchStartCsn !== batchStart.batchStartCsn ||
516
+ (!skipLengthCheck && pendingMessage.batchInfo.length !== expectedPendingBatchLength) ||
517
+ pendingBatchId !== inboundBatchId
474
518
  ) {
475
519
  this.logger?.sendErrorEvent({
476
520
  eventName: "BatchInfoMismatch",
477
521
  details: {
478
522
  pendingBatchCsn: pendingMessage.batchInfo.batchStartCsn,
479
- batchStartCsn: batch.batchStartCsn,
523
+ batchStartCsn: batchStart.batchStartCsn,
480
524
  pendingBatchLength: pendingMessage.batchInfo.length,
481
- batchLength: batch.messages.length,
525
+ expectedPendingBatchLength,
526
+ batchLength,
527
+ pendingBatchId,
528
+ inboundBatchId,
482
529
  pendingMessageBatchMetadata: asBatchMetadata(pendingMessage.opMetadata)?.batch,
483
530
  messageBatchMetadata: asBatchMetadata(firstMessage?.metadata)?.batch,
484
531
  },
@@ -526,8 +573,9 @@ export class PendingStateManager implements IDisposable {
526
573
 
527
574
  // The next message starts a batch (possibly single-message), and we'll need its batchId.
528
575
  const batchId = getEffectiveBatchId(pendingMessage);
529
- // Resubmit no messages, with the batchId. Will result in another empty batch marker.
576
+
530
577
  if (asEmptyBatchLocalOpMetadata(pendingMessage.localOpMetadata)?.emptyBatch === true) {
578
+ // Resubmit no messages, with the batchId. Will result in another empty batch marker.
531
579
  this.stateHandler.reSubmitBatch([], batchId);
532
580
  continue;
533
581
  }
@@ -41,8 +41,6 @@ type IRuntimeMessageMetadata =
41
41
  */
42
42
  export class ScheduleManager {
43
43
  private readonly deltaScheduler: DeltaScheduler;
44
- private batchClientId: string | undefined;
45
- private hitError = false;
46
44
 
47
45
  constructor(
48
46
  private readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
@@ -57,47 +55,14 @@ export class ScheduleManager {
57
55
  void new ScheduleManagerCore(deltaManager, getClientId, logger);
58
56
  }
59
57
 
60
- public beforeOpProcessing(message: ISequencedDocumentMessage) {
61
- if (this.batchClientId !== message.clientId) {
62
- assert(
63
- this.batchClientId === undefined,
64
- 0x2a2 /* "Batch is interrupted by other client op. Should be caught by trackPending()" */,
65
- );
66
-
67
- // This could be the beginning of a new batch or an individual message.
68
- this.emitter.emit("batchBegin", message);
69
- this.deltaScheduler.batchBegin(message);
70
-
71
- const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
72
- // TODO: Verify whether this should be able to handle server-generated ops (with null clientId)
73
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
74
- this.batchClientId = batch ? (message.clientId as string) : undefined;
75
- }
58
+ public batchBegin(message: ISequencedDocumentMessage) {
59
+ this.emitter.emit("batchBegin", message);
60
+ this.deltaScheduler.batchBegin(message);
76
61
  }
77
62
 
78
- public afterOpProcessing(error: any | undefined, message: ISequencedDocumentMessage) {
79
- // If this is no longer true, we need to revisit what we do where we set this.hitError.
80
- assert(!this.hitError, 0x2a3 /* "container should be closed on any error" */);
81
-
82
- if (error) {
83
- // We assume here that loader will close container and stop processing all future ops.
84
- // This is implicit dependency. If this flow changes, this code might no longer be correct.
85
- this.hitError = true;
86
- this.batchClientId = undefined;
87
- this.emitter.emit("batchEnd", error, message);
88
- this.deltaScheduler.batchEnd(message);
89
- return;
90
- }
91
-
92
- const batch = (message?.metadata as IRuntimeMessageMetadata)?.batch;
93
- // If no batchClientId has been set then we're in an individual batch. Else, if we get
94
- // batch end metadata, this is end of the current batch.
95
- if (this.batchClientId === undefined || batch === false) {
96
- this.batchClientId = undefined;
97
- this.emitter.emit("batchEnd", undefined, message);
98
- this.deltaScheduler.batchEnd(message);
99
- return;
100
- }
63
+ public batchEnd(error: any | undefined, message: ISequencedDocumentMessage) {
64
+ this.emitter.emit("batchEnd", error, message);
65
+ this.deltaScheduler.batchEnd(message);
101
66
  }
102
67
  }
103
68
 
@@ -124,9 +89,7 @@ class ScheduleManagerCore {
124
89
  }
125
90
 
126
91
  // First message will have the batch flag set to true if doing a batched send
127
- // Non null asserting because of the length check above
128
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129
- const firstMessageMetadata = messages[0]!.metadata as IRuntimeMessageMetadata;
92
+ const firstMessageMetadata = messages[0].metadata as IRuntimeMessageMetadata;
130
93
  if (!firstMessageMetadata?.batch) {
131
94
  return;
132
95
  }
@@ -138,9 +101,7 @@ class ScheduleManagerCore {
138
101
  }
139
102
 
140
103
  // Set the batch flag to false on the last message to indicate the end of the send batch
141
- // Non null asserting here because of the length check at the start of the function
142
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143
- const lastMessage = messages[messages.length - 1]!;
104
+ const lastMessage = messages[messages.length - 1];
144
105
  // TODO: It's not clear if this shallow clone is required, as opposed to just setting "batch" to false.
145
106
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
146
107
  lastMessage.metadata = { ...(lastMessage.metadata as any), batch: false };
@@ -73,9 +73,7 @@ export class EscapedPath {
73
73
  public static createAndConcat(pathParts: string[]): EscapedPath {
74
74
  let ret = EscapedPath.create(pathParts[0] ?? "");
75
75
  for (let i = 1; i < pathParts.length; i++) {
76
- // Non null asserting here since we are iterating over pathParts
77
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
78
- ret = ret.concat(EscapedPath.create(pathParts[i]!));
76
+ ret = ret.concat(EscapedPath.create(pathParts[i]));
79
77
  }
80
78
  return ret;
81
79
  }
@@ -353,12 +353,11 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
353
353
  }
354
354
 
355
355
  private parseContent(op: ISequencedDocumentMessage) {
356
- // back-compat: ADO #1385: Make this unconditional in the future,
357
- // when Container.processRemoteMessage stops parsing contents. That said, we should move to
358
- // listen for "op" events from ContainerRuntime, and parsing may not be required at all if
359
- // ContainerRuntime.process() would parse it for all types of ops.
360
- // Can make either of those changes only when LTS moves to a version that has no content
361
- // parsing in loader layer!
356
+ // This should become unconditional once (Loader LTS) DeltaManager.processInboundMessage() stops parsing content (ADO #12052)
357
+ // Note: Until that change is made in the loader, this case will never be hit.
358
+ // Then there will be a long time of needing both cases, until LTS catches up to the change.
359
+ // That said, we may instead move to listen for "op" events from ContainerRuntime,
360
+ // and parsing may not be required at all if ContainerRuntime.process() would parse it for all types of ops.
362
361
  if (typeof op.contents === "string") {
363
362
  op.contents = JSON.parse(op.contents);
364
363
  }
@@ -378,9 +377,8 @@ export class SummaryCollection extends TypedEventEmitter<ISummaryCollectionOpEve
378
377
  case MessageType.SummaryAck:
379
378
  case MessageType.SummaryNack:
380
379
  // Old files (prior to PR #10077) may not contain this info
381
- // back-compat: ADO #1385: remove cast when ISequencedDocumentMessage changes are propagated
382
- if ((op as any).data !== undefined) {
383
- op.contents = JSON.parse((op as any).data);
380
+ if (op.data !== undefined) {
381
+ op.contents = JSON.parse(op.data);
384
382
  } else {
385
383
  this.parseContent(op);
386
384
  }
@@ -281,9 +281,7 @@ export async function getFluidDataStoreAttributes(
281
281
  ): Promise<ReadFluidDataStoreAttributes> {
282
282
  const attributes = await readAndParse<ReadFluidDataStoreAttributes>(
283
283
  storage,
284
- // TODO why are we non null asserting here?
285
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
286
- snapshot.blobs[dataStoreAttributesBlobName]!,
284
+ snapshot.blobs[dataStoreAttributesBlobName],
287
285
  );
288
286
  // Use the snapshotFormatVersion to determine how the pkg is encoded in the snapshot.
289
287
  // For snapshotFormatVersion = "0.1" (1) or above, pkg is jsonified, otherwise it is just a string.
@@ -2,13 +2,15 @@
2
2
 
3
3
  ## Table of contents
4
4
 
5
- - [Introduction](#introduction)
6
- - [Summary Format](#summary-format)
7
- - [Snapshot Format](#snapshot-format)
8
- - [Summary / Snapshot Tree Visualization](#summary--snapshot-tree-visualization)
9
- - [Protocol Tree](#protocol-tree)
10
- - [App Tree](#app-tree)
11
- - [Summary tree distinction - Incremental summaries](#summary-tree-distinction---incremental-summaries)
5
+ - [Summary and snapshot formats](#summary-and-snapshot-formats)
6
+ - [Table of contents](#table-of-contents)
7
+ - [Introduction](#introduction)
8
+ - [Summary Format](#summary-format)
9
+ - [Snapshot Format](#snapshot-format)
10
+ - [Summary / Snapshot Tree Visualization](#summary--snapshot-tree-visualization)
11
+ - [Protocol tree](#protocol-tree)
12
+ - [App tree](#app-tree)
13
+ - [Summary tree distinction - Incremental summaries](#summary-tree-distinction---incremental-summaries)
12
14
 
13
15
  ## Introduction
14
16
 
@@ -211,9 +213,9 @@ flowchart TD
211
213
  C --> D["handle: '/data store 1'"]:::handle
212
214
  C --> E["data store 2"]:::tree
213
215
  E --> F[".channels"]:::tree
214
- F --> G["handle: '/data store 2/DDS 1'"]:::handle
216
+ F --> G["handle: '/.channels/data store 2/.channels/DDS 1'"]:::handle
215
217
  F --> H["DDS 2"]:::tree
216
- H --> I["handle: '/data store 2/DDS 2/sub node'"]:::handle
218
+ H --> I["handle: '/.channels/data store 2/.channels/DDS 2/sub node'"]:::handle
217
219
  F --> J["DDS N"]:::tree
218
220
  E --> K["other nodes"]:::others
219
221
  C --> L["data store N"]:::tree
package/tsconfig.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "compilerOptions": {
6
6
  "rootDir": "./src",
7
7
  "outDir": "./lib",
8
+ "noUncheckedIndexedAccess": false,
8
9
  "exactOptionalPropertyTypes": false,
9
10
  },
10
11
  }