@fluidframework/container-runtime 2.41.0 → 2.43.0-343119

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 (136) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/container-runtime.test-files.tar +0 -0
  3. package/dist/channelCollection.d.ts +1 -1
  4. package/dist/channelCollection.d.ts.map +1 -1
  5. package/dist/channelCollection.js +4 -4
  6. package/dist/channelCollection.js.map +1 -1
  7. package/dist/compatUtils.d.ts +24 -1
  8. package/dist/compatUtils.d.ts.map +1 -1
  9. package/dist/compatUtils.js +109 -7
  10. package/dist/compatUtils.js.map +1 -1
  11. package/dist/containerRuntime.d.ts +36 -15
  12. package/dist/containerRuntime.d.ts.map +1 -1
  13. package/dist/containerRuntime.js +186 -71
  14. package/dist/containerRuntime.js.map +1 -1
  15. package/dist/dataStore.d.ts.map +1 -1
  16. package/dist/dataStore.js +5 -0
  17. package/dist/dataStore.js.map +1 -1
  18. package/dist/gc/garbageCollection.d.ts.map +1 -1
  19. package/dist/gc/garbageCollection.js +2 -0
  20. package/dist/gc/garbageCollection.js.map +1 -1
  21. package/dist/gc/gcDefinitions.d.ts +1 -1
  22. package/dist/gc/gcDefinitions.d.ts.map +1 -1
  23. package/dist/gc/gcDefinitions.js.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/messageTypes.d.ts +5 -4
  28. package/dist/messageTypes.d.ts.map +1 -1
  29. package/dist/messageTypes.js.map +1 -1
  30. package/dist/metadata.d.ts +1 -1
  31. package/dist/metadata.d.ts.map +1 -1
  32. package/dist/metadata.js.map +1 -1
  33. package/dist/opLifecycle/definitions.d.ts +6 -5
  34. package/dist/opLifecycle/definitions.d.ts.map +1 -1
  35. package/dist/opLifecycle/definitions.js.map +1 -1
  36. package/dist/opLifecycle/index.d.ts +1 -1
  37. package/dist/opLifecycle/index.d.ts.map +1 -1
  38. package/dist/opLifecycle/index.js.map +1 -1
  39. package/dist/opLifecycle/opGroupingManager.d.ts +9 -0
  40. package/dist/opLifecycle/opGroupingManager.d.ts.map +1 -1
  41. package/dist/opLifecycle/opGroupingManager.js +6 -4
  42. package/dist/opLifecycle/opGroupingManager.js.map +1 -1
  43. package/dist/opLifecycle/opSerialization.d.ts +2 -1
  44. package/dist/opLifecycle/opSerialization.d.ts.map +1 -1
  45. package/dist/opLifecycle/opSerialization.js.map +1 -1
  46. package/dist/packageVersion.d.ts +1 -1
  47. package/dist/packageVersion.d.ts.map +1 -1
  48. package/dist/packageVersion.js +1 -1
  49. package/dist/packageVersion.js.map +1 -1
  50. package/dist/pendingStateManager.d.ts +18 -5
  51. package/dist/pendingStateManager.d.ts.map +1 -1
  52. package/dist/pendingStateManager.js +20 -13
  53. package/dist/pendingStateManager.js.map +1 -1
  54. package/dist/summary/documentSchema.d.ts +79 -16
  55. package/dist/summary/documentSchema.d.ts.map +1 -1
  56. package/dist/summary/documentSchema.js +119 -53
  57. package/dist/summary/documentSchema.js.map +1 -1
  58. package/dist/summary/index.d.ts +1 -1
  59. package/dist/summary/index.d.ts.map +1 -1
  60. package/dist/summary/index.js.map +1 -1
  61. package/lib/channelCollection.d.ts +1 -1
  62. package/lib/channelCollection.d.ts.map +1 -1
  63. package/lib/channelCollection.js +4 -4
  64. package/lib/channelCollection.js.map +1 -1
  65. package/lib/compatUtils.d.ts +24 -1
  66. package/lib/compatUtils.d.ts.map +1 -1
  67. package/lib/compatUtils.js +102 -3
  68. package/lib/compatUtils.js.map +1 -1
  69. package/lib/containerRuntime.d.ts +36 -15
  70. package/lib/containerRuntime.d.ts.map +1 -1
  71. package/lib/containerRuntime.js +188 -73
  72. package/lib/containerRuntime.js.map +1 -1
  73. package/lib/dataStore.d.ts.map +1 -1
  74. package/lib/dataStore.js +5 -0
  75. package/lib/dataStore.js.map +1 -1
  76. package/lib/gc/garbageCollection.d.ts.map +1 -1
  77. package/lib/gc/garbageCollection.js +2 -0
  78. package/lib/gc/garbageCollection.js.map +1 -1
  79. package/lib/gc/gcDefinitions.d.ts +1 -1
  80. package/lib/gc/gcDefinitions.d.ts.map +1 -1
  81. package/lib/gc/gcDefinitions.js.map +1 -1
  82. package/lib/index.d.ts +2 -2
  83. package/lib/index.d.ts.map +1 -1
  84. package/lib/index.js.map +1 -1
  85. package/lib/messageTypes.d.ts +5 -4
  86. package/lib/messageTypes.d.ts.map +1 -1
  87. package/lib/messageTypes.js.map +1 -1
  88. package/lib/metadata.d.ts +1 -1
  89. package/lib/metadata.d.ts.map +1 -1
  90. package/lib/metadata.js.map +1 -1
  91. package/lib/opLifecycle/definitions.d.ts +6 -5
  92. package/lib/opLifecycle/definitions.d.ts.map +1 -1
  93. package/lib/opLifecycle/definitions.js.map +1 -1
  94. package/lib/opLifecycle/index.d.ts +1 -1
  95. package/lib/opLifecycle/index.d.ts.map +1 -1
  96. package/lib/opLifecycle/index.js.map +1 -1
  97. package/lib/opLifecycle/opGroupingManager.d.ts +9 -0
  98. package/lib/opLifecycle/opGroupingManager.d.ts.map +1 -1
  99. package/lib/opLifecycle/opGroupingManager.js +6 -4
  100. package/lib/opLifecycle/opGroupingManager.js.map +1 -1
  101. package/lib/opLifecycle/opSerialization.d.ts +2 -1
  102. package/lib/opLifecycle/opSerialization.d.ts.map +1 -1
  103. package/lib/opLifecycle/opSerialization.js.map +1 -1
  104. package/lib/packageVersion.d.ts +1 -1
  105. package/lib/packageVersion.d.ts.map +1 -1
  106. package/lib/packageVersion.js +1 -1
  107. package/lib/packageVersion.js.map +1 -1
  108. package/lib/pendingStateManager.d.ts +18 -5
  109. package/lib/pendingStateManager.d.ts.map +1 -1
  110. package/lib/pendingStateManager.js +20 -13
  111. package/lib/pendingStateManager.js.map +1 -1
  112. package/lib/summary/documentSchema.d.ts +79 -16
  113. package/lib/summary/documentSchema.d.ts.map +1 -1
  114. package/lib/summary/documentSchema.js +119 -53
  115. package/lib/summary/documentSchema.js.map +1 -1
  116. package/lib/summary/index.d.ts +1 -1
  117. package/lib/summary/index.d.ts.map +1 -1
  118. package/lib/summary/index.js.map +1 -1
  119. package/package.json +18 -18
  120. package/src/channelCollection.ts +4 -4
  121. package/src/compatUtils.ts +147 -10
  122. package/src/containerRuntime.ts +242 -85
  123. package/src/dataStore.ts +7 -0
  124. package/src/gc/garbageCollection.ts +2 -0
  125. package/src/gc/gcDefinitions.ts +1 -1
  126. package/src/index.ts +4 -2
  127. package/src/messageTypes.ts +12 -5
  128. package/src/metadata.ts +1 -1
  129. package/src/opLifecycle/definitions.ts +7 -3
  130. package/src/opLifecycle/index.ts +1 -0
  131. package/src/opLifecycle/opGroupingManager.ts +17 -4
  132. package/src/opLifecycle/opSerialization.ts +6 -1
  133. package/src/packageVersion.ts +1 -1
  134. package/src/pendingStateManager.ts +49 -22
  135. package/src/summary/documentSchema.ts +228 -83
  136. package/src/summary/index.ts +3 -1
package/src/dataStore.ts CHANGED
@@ -11,6 +11,8 @@ import {
11
11
  AliasResult,
12
12
  IDataStore,
13
13
  IFluidDataStoreChannel,
14
+ // eslint-disable-next-line import/no-deprecated
15
+ type IContainerRuntimeBaseExperimental,
14
16
  } from "@fluidframework/runtime-definitions/internal";
15
17
  import {
16
18
  ITelemetryLoggerExt,
@@ -78,6 +80,11 @@ class DataStore implements IDataStore {
78
80
  if (alias.includes("/")) {
79
81
  throw new UsageError(`The alias cannot contain slashes: '${alias}'`);
80
82
  }
83
+ // eslint-disable-next-line import/no-deprecated
84
+ const runtime = this.parentContext.containerRuntime as IContainerRuntimeBaseExperimental;
85
+ if (runtime.inStagingMode === true) {
86
+ throw new UsageError("Cannot set aliases while in staging mode");
87
+ }
81
88
 
82
89
  switch (this.aliasState) {
83
90
  // If we're already aliasing, check if it's for the same value and return
@@ -1045,6 +1045,8 @@ export class GarbageCollector implements IGarbageCollector {
1045
1045
 
1046
1046
  // Any time we log a Tombstone Loaded error (via Telemetry Tracker),
1047
1047
  // we want to also trigger autorecovery to avoid the object being deleted
1048
+ // i.e. this will be preceded by one of these telemetry events;
1049
+ // GC_Tombstone_DataStore_Requested, GC_Tombstone_SubDataStore_Requested, GC_Tombstone_Blob_Requested
1048
1050
  // Note: We don't need to trigger on "Changed" because any change will cause the object
1049
1051
  // to be loaded by the Summarizer, and auto-recovery will be triggered then.
1050
1052
  if (isTombstoned && reason === "Loaded") {
@@ -448,7 +448,7 @@ export interface IGarbageCollector {
448
448
  * Returns true if this node has been deleted by GC during sweep phase.
449
449
  */
450
450
  isNodeDeleted(nodePath: string): boolean;
451
- setConnectionState(connected: boolean, clientId?: string): void;
451
+ setConnectionState(canSendOps: boolean, clientId?: string): void;
452
452
  dispose(): void;
453
453
  }
454
454
 
package/src/index.ts CHANGED
@@ -31,7 +31,7 @@ export {
31
31
  ChannelCollectionFactory,
32
32
  AllowTombstoneRequestHeaderKey,
33
33
  } from "./channelCollection.js";
34
- export type { MinimumVersionForCollab } from "./compatUtils.js";
34
+ export type { MinimumVersionForCollab, SemanticVersion } from "./compatUtils.js";
35
35
  export {
36
36
  GCNodeType,
37
37
  IGCMetadata,
@@ -91,11 +91,13 @@ export {
91
91
  IRetriableFailureError,
92
92
  IdCompressorMode,
93
93
  IDocumentSchema,
94
+ IDocumentSchemaInfo,
94
95
  DocumentSchemaValueType,
95
96
  IDocumentSchemaCurrent,
96
97
  currentDocumentVersionSchema,
97
98
  DocumentsSchemaController,
98
- IDocumentSchemaChangeMessage,
99
+ IDocumentSchemaChangeMessageIncoming,
100
+ IDocumentSchemaChangeMessageOutgoing,
99
101
  IDocumentSchemaFeatures,
100
102
  ReadFluidDataStoreAttributes,
101
103
  IFluidDataStoreAttributes0,
@@ -14,7 +14,10 @@ import {
14
14
  import { IDataStoreAliasMessage } from "./dataStore.js";
15
15
  import { GarbageCollectionMessage } from "./gc/index.js";
16
16
  import { IChunkedOp } from "./opLifecycle/index.js";
17
- import { IDocumentSchemaChangeMessage } from "./summary/index.js";
17
+ import {
18
+ type IDocumentSchemaChangeMessageIncoming,
19
+ type IDocumentSchemaChangeMessageOutgoing,
20
+ } from "./summary/index.js";
18
21
 
19
22
  /**
20
23
  * @legacy
@@ -112,9 +115,13 @@ export type ContainerRuntimeGCMessage = TypedContainerRuntimeMessage<
112
115
  ContainerMessageType.GC,
113
116
  GarbageCollectionMessage
114
117
  >;
115
- export type ContainerRuntimeDocumentSchemaMessage = TypedContainerRuntimeMessage<
118
+ export type InboundContainerRuntimeDocumentSchemaMessage = TypedContainerRuntimeMessage<
119
+ ContainerMessageType.DocumentSchemaChange,
120
+ IDocumentSchemaChangeMessageIncoming
121
+ >;
122
+ export type OutboundContainerRuntimeDocumentSchemaMessage = TypedContainerRuntimeMessage<
116
123
  ContainerMessageType.DocumentSchemaChange,
117
- IDocumentSchemaChangeMessage
124
+ IDocumentSchemaChangeMessageOutgoing
118
125
  >;
119
126
 
120
127
  /**
@@ -147,7 +154,7 @@ export type InboundContainerRuntimeMessage =
147
154
  | ContainerRuntimeAliasMessage
148
155
  | ContainerRuntimeIdAllocationMessage
149
156
  | ContainerRuntimeGCMessage
150
- | ContainerRuntimeDocumentSchemaMessage
157
+ | InboundContainerRuntimeDocumentSchemaMessage
151
158
  // Inbound messages may include unknown types from other clients, so we include that as a special case here
152
159
  | UnknownContainerRuntimeMessage;
153
160
 
@@ -163,7 +170,7 @@ export type LocalContainerRuntimeMessage =
163
170
  | ContainerRuntimeAliasMessage
164
171
  | ContainerRuntimeIdAllocationMessage
165
172
  | ContainerRuntimeGCMessage
166
- | ContainerRuntimeDocumentSchemaMessage
173
+ | OutboundContainerRuntimeDocumentSchemaMessage
167
174
  // In rare cases (e.g. related to stashed ops) we could have a local message of an unknown type
168
175
  | UnknownContainerRuntimeMessage;
169
176
 
package/src/metadata.ts CHANGED
@@ -26,7 +26,7 @@ export function asEmptyBatchLocalOpMetadata(
26
26
  */
27
27
  export interface IEmptyBatchMetadata {
28
28
  // Set to true on localOpMetadata for empty batches
29
- emptyBatch?: boolean;
29
+ emptyBatch?: true;
30
30
  }
31
31
  /**
32
32
  * Properties put on the op metadata object for batch tracking
@@ -7,6 +7,9 @@ import type { IBatchMessage } from "@fluidframework/container-definitions/intern
7
7
 
8
8
  import { CompressionAlgorithms } from "../compressionDefinitions.js";
9
9
  import type { LocalContainerRuntimeMessage } from "../messageTypes.js";
10
+ import type { IEmptyBatchMetadata } from "../metadata.js";
11
+
12
+ import type { EmptyGroupedBatch } from "./opGroupingManager.js";
10
13
 
11
14
  /**
12
15
  * Local Batch message, before it is virtualized and sent to the ordering service
@@ -34,7 +37,7 @@ export interface LocalBatchMessage {
34
37
  staged?: boolean;
35
38
 
36
39
  /**
37
- * @deprecated Use serializedOp
40
+ * @deprecated Use runtimeOp
38
41
  */
39
42
  contents?: never; // To ensure we don't leave this one when converting from OutboundBatchMessage
40
43
  }
@@ -44,8 +47,9 @@ export interface LocalBatchMessage {
44
47
  */
45
48
  export interface LocalEmptyBatchPlaceholder {
46
49
  metadata?: Record<string, unknown>;
47
- localOpMetadata: { emptyBatch: true };
50
+ localOpMetadata: Required<IEmptyBatchMetadata>;
48
51
  referenceSequenceNumber: number;
52
+ runtimeOp: EmptyGroupedBatch;
49
53
  }
50
54
 
51
55
  /**
@@ -59,7 +63,7 @@ export type OutboundBatchMessage = IBatchMessage & {
59
63
  /**
60
64
  * @deprecated Use contents
61
65
  */
62
- serializedOp?: never; // To ensure we don't leave this one when converting from LocalBatchMessage
66
+ runtimeOp?: never; // To ensure we don't leave this one when converting from LocalBatchMessage
63
67
  };
64
68
 
65
69
  /**
@@ -43,6 +43,7 @@ export {
43
43
  unpackRuntimeMessage,
44
44
  } from "./remoteMessageProcessor.js";
45
45
  export {
46
+ EmptyGroupedBatch,
46
47
  OpGroupingManager,
47
48
  OpGroupingManagerConfig,
48
49
  isGroupedBatch,
@@ -46,6 +46,16 @@ export interface OpGroupingManagerConfig {
46
46
  readonly groupedBatchingEnabled: boolean;
47
47
  }
48
48
 
49
+ /**
50
+ * This is the type of an empty grouped batch we send over the wire
51
+ * We also put this in the placeholder for an empty batch in the PendingStateManager.
52
+ * But most places throughout the ContainerRuntime, this will not be used (just as Grouped Batches in general don't appear outside opLifecycle dir)
53
+ */
54
+ export interface EmptyGroupedBatch {
55
+ type: typeof OpGroupingManager.groupedBatchOp;
56
+ contents: readonly unknown[];
57
+ }
58
+
49
59
  export class OpGroupingManager {
50
60
  static readonly groupedBatchOp = "groupedBatch";
51
61
  private readonly logger: ITelemetryLoggerExt;
@@ -75,19 +85,22 @@ export class OpGroupingManager {
75
85
  this.config.groupedBatchingEnabled,
76
86
  0xa00 /* cannot create empty grouped batch when grouped batching is disabled */,
77
87
  );
78
- const serializedOp = JSON.stringify({
79
- type: OpGroupingManager.groupedBatchOp,
88
+
89
+ const emptyGroupedBatch: EmptyGroupedBatch = {
90
+ type: "groupedBatch",
80
91
  contents: [],
81
- });
92
+ };
93
+ const serializedOp = JSON.stringify(emptyGroupedBatch);
82
94
 
83
95
  const placeholderMessage: LocalEmptyBatchPlaceholder = {
84
96
  metadata: { batchId: resubmittingBatchId },
85
97
  localOpMetadata: { emptyBatch: true },
86
98
  referenceSequenceNumber,
99
+ runtimeOp: emptyGroupedBatch,
87
100
  };
88
101
  const outboundBatch: OutboundSingletonBatch = {
89
102
  contentSizeInBytes: 0,
90
- messages: [{ ...placeholderMessage, contents: serializedOp }],
103
+ messages: [{ ...placeholderMessage, runtimeOp: undefined, contents: serializedOp }],
91
104
  referenceSequenceNumber,
92
105
  };
93
106
  return { outboundBatch, placeholderMessage };
@@ -12,6 +12,8 @@ import {
12
12
 
13
13
  import type { LocalContainerRuntimeMessage } from "../messageTypes.js";
14
14
 
15
+ import type { EmptyGroupedBatch } from "./opGroupingManager.js";
16
+
15
17
  /**
16
18
  * Takes an incoming runtime message (outer type "op"), JSON.parses the message's contents in place,
17
19
  * if needed (old Loader does this for us).
@@ -34,7 +36,10 @@ export function ensureContentsDeserialized(mutableMessage: ISequencedDocumentMes
34
36
  * @param toSerialize - op message to serialize. Also supports an array of ops.
35
37
  */
36
38
  export function serializeOp(
37
- toSerialize: LocalContainerRuntimeMessage | LocalContainerRuntimeMessage[],
39
+ toSerialize:
40
+ | EmptyGroupedBatch
41
+ | LocalContainerRuntimeMessage
42
+ | LocalContainerRuntimeMessage[],
38
43
  ): string {
39
44
  return JSON.stringify(
40
45
  toSerialize,
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-runtime";
9
- export const pkgVersion = "2.41.0";
9
+ export const pkgVersion = "2.43.0-343119";
@@ -23,6 +23,7 @@ import {
23
23
  } from "./messageTypes.js";
24
24
  import { asBatchMetadata, asEmptyBatchLocalOpMetadata } from "./metadata.js";
25
25
  import {
26
+ EmptyGroupedBatch,
26
27
  LocalBatchMessage,
27
28
  getEffectiveBatchId,
28
29
  BatchStartInfo,
@@ -49,9 +50,20 @@ export interface IPendingMessage {
49
50
  * The original runtime op that was submitted to the ContainerRuntime
50
51
  * Unless this pending message came from stashed content, in which case this was roundtripped through string
51
52
  */
52
- runtimeOp?: LocalContainerRuntimeMessage | undefined; // Undefined for empty batches and initial messages before parsing
53
+ runtimeOp?: LocalContainerRuntimeMessage | EmptyGroupedBatch | undefined; // Undefined for initial messages before parsing
54
+ /**
55
+ * Local Op Metadata that was passed to the ContainerRuntime when the op was submitted.
56
+ * This contains state needed when processing the ack, or to resubmit or rollback the op.
57
+ */
53
58
  localOpMetadata: unknown;
59
+ /**
60
+ * Metadata that was passed to the ContainerRuntime when the op was submitted.
61
+ * This is rarely used, and may be inspected by the service (as opposed to op contents which is opaque)
62
+ */
54
63
  opMetadata: Record<string, unknown> | undefined;
64
+ /**
65
+ * Populated upon processing the op's ack, before moving the pending message to savedOps.
66
+ */
55
67
  sequenceNumber?: number;
56
68
  /**
57
69
  * Info about the batch this pending message belongs to, for validation and for computing the batchId on reconnect
@@ -135,9 +147,7 @@ export interface IRuntimeStateHandler {
135
147
  }
136
148
 
137
149
  function isEmptyBatchPendingMessage(message: IPendingMessageFromStash): boolean {
138
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
139
- const content = JSON.parse(message.content);
140
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
150
+ const content = JSON.parse(message.content) as Partial<EmptyGroupedBatch>;
141
151
  return content.type === "groupedBatch" && content.contents?.length === 0;
142
152
  }
143
153
 
@@ -289,8 +299,11 @@ export class PendingStateManager implements IDisposable {
289
299
  public hasPendingUserChanges(): boolean {
290
300
  for (let i = 0; i < this.pendingMessages.length; i++) {
291
301
  const element = this.pendingMessages.get(i);
292
- // Missing runtimeOp implies not dirtyable: This only happens for empty batches
293
- if (element?.runtimeOp !== undefined && isContainerMessageDirtyable(element.runtimeOp)) {
302
+ if (
303
+ element?.runtimeOp !== undefined &&
304
+ isNotEmptyGroupedBatch(element) &&
305
+ isContainerMessageDirtyable(element.runtimeOp)
306
+ ) {
294
307
  return true;
295
308
  }
296
309
  }
@@ -374,12 +387,7 @@ export class PendingStateManager implements IDisposable {
374
387
  clientSequenceNumber: number | undefined,
375
388
  staged: boolean,
376
389
  ): void {
377
- // We have to cast because runtimeOp doesn't apply for empty batches and is missing on LocalEmptyBatchPlaceholder
378
- this.onFlushBatch(
379
- [placeholder satisfies Omit<LocalBatchMessage, "runtimeOp"> as LocalBatchMessage],
380
- clientSequenceNumber,
381
- staged,
382
- );
390
+ this.onFlushBatch([placeholder], clientSequenceNumber, staged);
383
391
  }
384
392
 
385
393
  /**
@@ -392,7 +400,7 @@ export class PendingStateManager implements IDisposable {
392
400
  * @param ignoreBatchId - Whether to ignore the batchId in the batchStartInfo
393
401
  */
394
402
  public onFlushBatch(
395
- batch: LocalBatchMessage[],
403
+ batch: LocalBatchMessage[] | [LocalEmptyBatchPlaceholder],
396
404
  clientSequenceNumber: number | undefined,
397
405
  staged: boolean,
398
406
  ignoreBatchId?: boolean,
@@ -458,7 +466,9 @@ export class PendingStateManager implements IDisposable {
458
466
  // We still need to track it for resubmission.
459
467
  try {
460
468
  if (isEmptyBatchPendingMessage(nextMessage)) {
461
- nextMessage.localOpMetadata = { emptyBatch: true }; // equivalent to applyStashedOp for empty batch
469
+ nextMessage.localOpMetadata = {
470
+ emptyBatch: true,
471
+ } satisfies LocalEmptyBatchPlaceholder["localOpMetadata"]; // equivalent to applyStashedOp for empty batch
462
472
  patchbatchInfo(nextMessage); // Back compat
463
473
  this.pendingMessages.push(nextMessage);
464
474
  continue;
@@ -676,7 +686,10 @@ export class PendingStateManager implements IDisposable {
676
686
  pendingMessage !== undefined,
677
687
  0xa21 /* No pending message found as we start processing this remote batch */,
678
688
  );
679
- assert(!pendingMessage.batchInfo.staged, 0xb85 /* Can't get an ack from a staged batch */);
689
+ assert(
690
+ !pendingMessage.batchInfo.staged,
691
+ 0xb85 /* Pending state mismatch, ack came in but next pending message is staged */,
692
+ );
680
693
 
681
694
  // If this batch became empty on resubmit, batch.messages will be empty (but keyMessage is always set)
682
695
  // and the next pending message should be an empty batch marker.
@@ -781,7 +794,9 @@ export class PendingStateManager implements IDisposable {
781
794
  assert(batchMetadataFlag !== false, 0x41b /* We cannot process batches in chunks */);
782
795
 
783
796
  // The next message starts a batch (possibly single-message), and we'll need its batchId.
784
- const batchId = getEffectiveBatchId(pendingMessage);
797
+ const batchId = pendingMessage.batchInfo.ignoreBatchId
798
+ ? undefined
799
+ : getEffectiveBatchId(pendingMessage);
785
800
 
786
801
  const staged = pendingMessage.batchInfo.staged;
787
802
 
@@ -792,7 +807,7 @@ export class PendingStateManager implements IDisposable {
792
807
  }
793
808
 
794
809
  assert(
795
- pendingMessage.runtimeOp !== undefined,
810
+ pendingMessage.runtimeOp !== undefined && isNotEmptyGroupedBatch(pendingMessage),
796
811
  0xb87 /* viableOp is only undefined for empty batches */,
797
812
  );
798
813
 
@@ -828,7 +843,7 @@ export class PendingStateManager implements IDisposable {
828
843
  // check is >= because batch end may be last pending message
829
844
  while (remainingPendingMessagesCount >= 0) {
830
845
  assert(
831
- pendingMessage.runtimeOp !== undefined,
846
+ pendingMessage.runtimeOp !== undefined && isNotEmptyGroupedBatch(pendingMessage),
832
847
  0xb88 /* viableOp is only undefined for empty batches */,
833
848
  );
834
849
  batch.push({
@@ -871,14 +886,20 @@ export class PendingStateManager implements IDisposable {
871
886
  }
872
887
 
873
888
  /**
874
- * Pops all staged batches, invoking the callback on each one in order (LIFO)
889
+ * Pops all staged batches, invoking the callback on each constituent op in order (LIFO)
875
890
  */
876
- public popStagedBatches(callback: (stagedMessage: IPendingMessage) => void): void {
891
+ public popStagedBatches(
892
+ callback: (
893
+ stagedMessage: IPendingMessage & { runtimeOp?: LocalContainerRuntimeMessage }, // exclude empty grouped batches
894
+ ) => void,
895
+ ): void {
877
896
  while (!this.pendingMessages.isEmpty()) {
878
897
  const stagedMessage = this.pendingMessages.peekBack();
879
898
  if (stagedMessage?.batchInfo.staged === true) {
880
- callback(stagedMessage);
881
- this.pendingMessages.pop();
899
+ if (isNotEmptyGroupedBatch(stagedMessage)) {
900
+ callback(stagedMessage);
901
+ this.pendingMessages.pop();
902
+ }
882
903
  } else {
883
904
  break; // no more staged messages
884
905
  }
@@ -902,3 +923,9 @@ function patchbatchInfo(
902
923
  message.batchInfo = { clientId: uuid(), batchStartCsn: -1, length: -1, staged: false };
903
924
  }
904
925
  }
926
+
927
+ function isNotEmptyGroupedBatch(
928
+ message: IPendingMessage,
929
+ ): message is IPendingMessage & { runtimeOp: LocalContainerRuntimeMessage } {
930
+ return message.runtimeOp !== undefined && message.runtimeOp.type !== "groupedBatch";
931
+ }