@fluidframework/container-loader 2.0.0-internal.1.1.0 → 2.0.0-internal.1.2.0.93071

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 (67) hide show
  1. package/dist/collabWindowTracker.d.ts +1 -1
  2. package/dist/collabWindowTracker.d.ts.map +1 -1
  3. package/dist/collabWindowTracker.js +2 -1
  4. package/dist/collabWindowTracker.js.map +1 -1
  5. package/dist/connectionManager.d.ts +1 -1
  6. package/dist/connectionManager.d.ts.map +1 -1
  7. package/dist/connectionManager.js +5 -5
  8. package/dist/connectionManager.js.map +1 -1
  9. package/dist/container.d.ts +10 -0
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js +54 -42
  12. package/dist/container.js.map +1 -1
  13. package/dist/containerContext.d.ts +18 -7
  14. package/dist/containerContext.d.ts.map +1 -1
  15. package/dist/containerContext.js +18 -8
  16. package/dist/containerContext.js.map +1 -1
  17. package/dist/deltaManager.d.ts +1 -1
  18. package/dist/deltaManager.d.ts.map +1 -1
  19. package/dist/deltaManager.js +18 -6
  20. package/dist/deltaManager.js.map +1 -1
  21. package/dist/loader.d.ts +1 -1
  22. package/dist/loader.js.map +1 -1
  23. package/dist/packageVersion.d.ts +1 -1
  24. package/dist/packageVersion.d.ts.map +1 -1
  25. package/dist/packageVersion.js +1 -1
  26. package/dist/packageVersion.js.map +1 -1
  27. package/dist/protocol.d.ts.map +1 -1
  28. package/dist/protocol.js +2 -1
  29. package/dist/protocol.js.map +1 -1
  30. package/lib/collabWindowTracker.d.ts +1 -1
  31. package/lib/collabWindowTracker.d.ts.map +1 -1
  32. package/lib/collabWindowTracker.js +3 -2
  33. package/lib/collabWindowTracker.js.map +1 -1
  34. package/lib/connectionManager.d.ts +1 -1
  35. package/lib/connectionManager.d.ts.map +1 -1
  36. package/lib/connectionManager.js +6 -8
  37. package/lib/connectionManager.js.map +1 -1
  38. package/lib/container.d.ts +10 -0
  39. package/lib/container.d.ts.map +1 -1
  40. package/lib/container.js +56 -44
  41. package/lib/container.js.map +1 -1
  42. package/lib/containerContext.d.ts +18 -7
  43. package/lib/containerContext.d.ts.map +1 -1
  44. package/lib/containerContext.js +19 -9
  45. package/lib/containerContext.js.map +1 -1
  46. package/lib/deltaManager.d.ts +1 -1
  47. package/lib/deltaManager.d.ts.map +1 -1
  48. package/lib/deltaManager.js +18 -6
  49. package/lib/deltaManager.js.map +1 -1
  50. package/lib/loader.d.ts +1 -1
  51. package/lib/loader.js.map +1 -1
  52. package/lib/packageVersion.d.ts +1 -1
  53. package/lib/packageVersion.d.ts.map +1 -1
  54. package/lib/packageVersion.js +1 -1
  55. package/lib/packageVersion.js.map +1 -1
  56. package/lib/protocol.d.ts.map +1 -1
  57. package/lib/protocol.js +2 -1
  58. package/lib/protocol.js.map +1 -1
  59. package/package.json +12 -12
  60. package/src/collabWindowTracker.ts +4 -3
  61. package/src/connectionManager.ts +6 -6
  62. package/src/container.ts +67 -50
  63. package/src/containerContext.ts +22 -8
  64. package/src/deltaManager.ts +20 -7
  65. package/src/loader.ts +1 -1
  66. package/src/packageVersion.ts +1 -1
  67. package/src/protocol.ts +2 -1
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { assert, Timer } from "@fluidframework/common-utils";
7
7
  import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
8
- import { isRuntimeMessage } from "@fluidframework/driver-utils";
8
+ import { isRuntimeMessage, MessageType2 } from "@fluidframework/driver-utils";
9
9
 
10
10
  const defaultNoopTimeFrequency = 2000;
11
11
  const defaultNoopCountFrequency = 50;
@@ -34,7 +34,7 @@ export class CollabWindowTracker {
34
34
  private readonly timer: Timer | undefined;
35
35
 
36
36
  constructor(
37
- private readonly submit: (type: MessageType, contents: any) => void,
37
+ private readonly submit: (type: MessageType) => void,
38
38
  NoopTimeFrequency: number = defaultNoopTimeFrequency,
39
39
  private readonly NoopCountFrequency: number = defaultNoopCountFrequency,
40
40
  ) {
@@ -93,7 +93,8 @@ export class CollabWindowTracker {
93
93
 
94
94
  private submitNoop(immediate: boolean) {
95
95
  // Anything other than null is immediate noop
96
- this.submit(MessageType.NoOp, immediate ? "" : null);
96
+ // ADO:1385: Remove cast and use MessageType once definition changes propagate
97
+ this.submit(immediate ? (MessageType2.Accept as unknown as MessageType) : MessageType.NoOp);
97
98
  assert(this.opsCountSinceNoop === 0,
98
99
  0x243 /* "stopSequenceNumberUpdate should be called as result of sending any op!" */);
99
100
  }
@@ -31,7 +31,7 @@ import {
31
31
  waitForConnectedState,
32
32
  DeltaStreamConnectionForbiddenError,
33
33
  logNetworkFailure,
34
- // isRuntimeMessage,
34
+ isRuntimeMessage,
35
35
  } from "@fluidframework/driver-utils";
36
36
  import {
37
37
  ConnectionMode,
@@ -168,7 +168,7 @@ export class ConnectionManager implements IConnectionManager {
168
168
  private clientSequenceNumber = 0;
169
169
  private clientSequenceNumberObserved = 0;
170
170
  /** Counts the number of noops sent by the client which may not be acked. */
171
- private trailingNoopCount = 0;
171
+ private localOpsToIgnore = 0;
172
172
 
173
173
  /** track clientId used last time when we sent any ops */
174
174
  private lastSubmittedClientId: string | undefined;
@@ -247,7 +247,7 @@ export class ConnectionManager implements IConnectionManager {
247
247
 
248
248
  public shouldJoinWrite(): boolean {
249
249
  // We don't have to wait for ack for topmost NoOps. So subtract those.
250
- return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.trailingNoopCount);
250
+ return this.clientSequenceNumberObserved < (this.clientSequenceNumber - this.localOpsToIgnore);
251
251
  }
252
252
 
253
253
  /**
@@ -819,10 +819,10 @@ export class ConnectionManager implements IConnectionManager {
819
819
  this.clientSequenceNumberObserved = 0;
820
820
  }
821
821
 
822
- if (message.type === MessageType.NoOp) {
823
- this.trailingNoopCount++;
822
+ if (!isRuntimeMessage(message)) {
823
+ this.localOpsToIgnore++;
824
824
  } else {
825
- this.trailingNoopCount = 0;
825
+ this.localOpsToIgnore = 0;
826
826
  }
827
827
 
828
828
  return {
package/src/container.ts CHANGED
@@ -29,10 +29,9 @@ import {
29
29
  IContainerLoadMode,
30
30
  IFluidCodeDetails,
31
31
  isFluidCodeDetails,
32
+ IBatchMessage,
32
33
  } from "@fluidframework/container-definitions";
33
34
  import {
34
- DataCorruptionError,
35
- extractSafePropertiesFromMessage,
36
35
  GenericError,
37
36
  UsageError,
38
37
  } from "@fluidframework/container-utils";
@@ -50,8 +49,6 @@ import {
50
49
  combineAppAndProtocolSummary,
51
50
  runWithRetry,
52
51
  isFluidResolvedUrl,
53
- isRuntimeMessage,
54
- isUnpackedRuntimeMessage,
55
52
  } from "@fluidframework/driver-utils";
56
53
  import { IQuorumSnapshot } from "@fluidframework/protocol-base";
57
54
  import {
@@ -61,7 +58,6 @@ import {
61
58
  ICommittedProposal,
62
59
  IDocumentAttributes,
63
60
  IDocumentMessage,
64
- IProcessMessageResult,
65
61
  IProtocolState,
66
62
  IQuorumClients,
67
63
  IQuorumProposals,
@@ -1416,7 +1412,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1416
1412
  const protocol = protocolHandlerBuilder(
1417
1413
  attributes,
1418
1414
  quorumSnapshot,
1419
- (key, value) => this.submitMessage(MessageType.Propose, { key, value }),
1415
+ (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1420
1416
  this._initialClients ?? [],
1421
1417
  );
1422
1418
 
@@ -1585,6 +1581,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1585
1581
  });
1586
1582
 
1587
1583
  deltaManager.on("readonly", (readonly) => {
1584
+ this.setContextConnectedState(this.connectionState === ConnectionState.Connected, readonly);
1588
1585
  this.emit("readonly", readonly);
1589
1586
  });
1590
1587
 
@@ -1682,9 +1679,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1682
1679
 
1683
1680
  // Both protocol and context should not be undefined if we got so far.
1684
1681
 
1685
- if (this._context?.disposed === false) {
1686
- this.context.setConnectionState(state, this.clientId);
1687
- }
1682
+ this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1688
1683
  this.protocolHandler.setConnectionState(state, this.clientId);
1689
1684
  raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1690
1685
 
@@ -1694,35 +1689,53 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1694
1689
  }
1695
1690
  }
1696
1691
 
1692
+ // back-compat: ADO #1385: Remove in the future, summary op should come through submitSummaryMessage()
1697
1693
  private submitContainerMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1698
- const outboundMessageType: string = type;
1699
- switch (outboundMessageType) {
1694
+ switch (type) {
1700
1695
  case MessageType.Operation:
1701
- case MessageType.RemoteHelp:
1702
- break;
1703
- case MessageType.Summarize: {
1704
- // github #6451: this is only needed for staging so the server
1705
- // know when the protocol tree is included
1706
- // this can be removed once all clients send
1707
- // protocol tree by default
1708
- const summary = contents as ISummaryContent;
1709
- if (summary.details === undefined) {
1710
- summary.details = {};
1711
- }
1712
- summary.details.includesProtocolTree =
1713
- this.options.summarizeProtocolTree === true;
1714
- break;
1715
- }
1696
+ return this.submitMessage(
1697
+ type,
1698
+ JSON.stringify(contents),
1699
+ batch,
1700
+ metadata);
1701
+ case MessageType.Summarize:
1702
+ return this.submitSummaryMessage(contents as unknown as ISummaryContent);
1716
1703
  default:
1717
1704
  this.close(new GenericError("invalidContainerSubmitOpType",
1718
1705
  undefined /* error */,
1719
1706
  { messageType: type }));
1720
1707
  return -1;
1721
1708
  }
1722
- return this.submitMessage(type, contents, batch, metadata);
1723
1709
  }
1724
1710
 
1725
- private submitMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1711
+ /** @returns clientSequenceNumber of last message in a batch */
1712
+ private submitBatch(batch: IBatchMessage[]): number {
1713
+ let clientSequenceNumber = -1;
1714
+ for (const message of batch) {
1715
+ clientSequenceNumber = this.submitMessage(
1716
+ MessageType.Operation,
1717
+ message.contents,
1718
+ true, // batch
1719
+ message.metadata);
1720
+ }
1721
+ this._deltaManager.flush();
1722
+ return clientSequenceNumber;
1723
+ }
1724
+
1725
+ private submitSummaryMessage(summary: ISummaryContent) {
1726
+ // github #6451: this is only needed for staging so the server
1727
+ // know when the protocol tree is included
1728
+ // this can be removed once all clients send
1729
+ // protocol tree by default
1730
+ if (summary.details === undefined) {
1731
+ summary.details = {};
1732
+ }
1733
+ summary.details.includesProtocolTree =
1734
+ this.options.summarizeProtocolTree === true;
1735
+ return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1736
+ }
1737
+
1738
+ private submitMessage(type: MessageType, contents?: string, batch?: boolean, metadata?: any): number {
1726
1739
  if (this.connectionState !== ConnectionState.Connected) {
1727
1740
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1728
1741
  return -1;
@@ -1733,28 +1746,14 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1733
1746
  return this._deltaManager.submit(type, contents, batch, metadata);
1734
1747
  }
1735
1748
 
1736
- private processRemoteMessage(message: ISequencedDocumentMessage): IProcessMessageResult {
1749
+ private processRemoteMessage(message: ISequencedDocumentMessage) {
1737
1750
  const local = this.clientId === message.clientId;
1738
1751
 
1739
1752
  // Allow the protocol handler to process the message
1740
- let result: IProcessMessageResult = { immediateNoOp: false };
1741
- try {
1742
- result = this.protocolHandler.processMessage(message, local);
1743
- } catch (error) {
1744
- this.close(wrapError(error, (errorMessage) =>
1745
- new DataCorruptionError(errorMessage, extractSafePropertiesFromMessage(message))));
1746
- }
1753
+ const result = this.protocolHandler.processMessage(message, local);
1747
1754
 
1748
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1749
- if (isUnpackedRuntimeMessage(message) && !isRuntimeMessage(message)) {
1750
- this.mc.logger.sendTelemetryEvent(
1751
- { eventName: "UnpackedRuntimeMessage", type: message.type });
1752
- }
1753
- // Forward non system messages to the loaded runtime for processing
1754
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1755
- if (isRuntimeMessage(message) || isUnpackedRuntimeMessage(message)) {
1756
- this.context.process(message, local, undefined);
1757
- }
1755
+ // Forward messages to the loaded runtime for processing
1756
+ this.context.process(message, local, undefined);
1758
1757
 
1759
1758
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1760
1759
  if (this.activeConnection()) {
@@ -1767,10 +1766,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1767
1766
  this.serviceConfiguration !== undefined,
1768
1767
  0x2e4 /* "there should be service config for active connection" */);
1769
1768
  this.collabWindowTracker = new CollabWindowTracker(
1770
- (type, contents) => {
1769
+ (type) => {
1771
1770
  assert(this.activeConnection(),
1772
1771
  0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1773
- this.submitMessage(type, contents);
1772
+ this.submitMessage(type);
1774
1773
  },
1775
1774
  this.serviceConfiguration.noopTimeFrequency,
1776
1775
  this.serviceConfiguration.noopCountFrequency,
@@ -1780,8 +1779,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1780
1779
  }
1781
1780
 
1782
1781
  this.emit("op", message);
1783
-
1784
- return result;
1785
1782
  }
1786
1783
 
1787
1784
  private submitSignal(message: any) {
@@ -1857,6 +1854,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1857
1854
  new QuorumProxy(this.protocolHandler.quorum),
1858
1855
  loader,
1859
1856
  (type, contents, batch, metadata) => this.submitContainerMessage(type, contents, batch, metadata),
1857
+ (summaryOp: ISummaryContent) => this.submitSummaryMessage(summaryOp),
1858
+ (batch: IBatchMessage[]) => this.submitBatch(batch),
1860
1859
  (message) => this.submitSignal(message),
1861
1860
  (error?: ICriticalContainerError) => this.close(error),
1862
1861
  Container.version,
@@ -1879,4 +1878,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1879
1878
  private logContainerError(warning: ContainerWarning) {
1880
1879
  this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1881
1880
  }
1881
+
1882
+ /**
1883
+ * Set the connected state of the ContainerContext
1884
+ * This controls the "connected" state of the ContainerRuntime as well
1885
+ * @param state - Is the container currently connected?
1886
+ * @param readonly - Is the container in readonly mode?
1887
+ */
1888
+ private setContextConnectedState(state: boolean, readonly: boolean): void {
1889
+ if (this._context?.disposed === false) {
1890
+ /**
1891
+ * We want to lie to the ContainerRuntime when we are in readonly mode to prevent issues with pending
1892
+ * ops getting through to the DeltaManager.
1893
+ * The ContainerRuntime's "connected" state simply means it is ok to send ops
1894
+ * See https://dev.azure.com/fluidframework/internal/_workitems/edit/1246
1895
+ */
1896
+ this.context.setConnectionState(state && !readonly, this.clientId);
1897
+ }
1898
+ }
1882
1899
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
- import { assert, LazyPromise } from "@fluidframework/common-utils";
7
+ import { LazyPromise } from "@fluidframework/common-utils";
8
8
  import {
9
9
  IAudience,
10
10
  IContainerContext,
@@ -22,6 +22,7 @@ import {
22
22
  ICodeDetailsLoader,
23
23
  IFluidModuleWithDetails,
24
24
  ISnapshotTreeWithBlobContents,
25
+ IBatchMessage,
25
26
  } from "@fluidframework/container-definitions";
26
27
  import {
27
28
  IRequest,
@@ -42,6 +43,7 @@ import {
42
43
  ISummaryTree,
43
44
  IVersion,
44
45
  MessageType,
46
+ ISummaryContent,
45
47
  } from "@fluidframework/protocol-definitions";
46
48
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
47
49
  import { Container } from "./container";
@@ -59,6 +61,8 @@ export class ContainerContext implements IContainerContext {
59
61
  quorum: IQuorum,
60
62
  loader: ILoader,
61
63
  submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
64
+ submitSummaryFn: (summaryOp: ISummaryContent) => number,
65
+ submitBatchFn: (batch: IBatchMessage[]) => number,
62
66
  submitSignalFn: (contents: any) => void,
63
67
  closeFn: (error?: ICriticalContainerError) => void,
64
68
  version: string,
@@ -76,6 +80,8 @@ export class ContainerContext implements IContainerContext {
76
80
  quorum,
77
81
  loader,
78
82
  submitFn,
83
+ submitSummaryFn,
84
+ submitBatchFn,
79
85
  submitSignalFn,
80
86
  closeFn,
81
87
  version,
@@ -107,8 +113,13 @@ export class ContainerContext implements IContainerContext {
107
113
  return this.container.clientDetails;
108
114
  }
109
115
 
116
+ private _connected: boolean;
117
+ /**
118
+ * When true, ops are free to flow
119
+ * When false, ops should be kept as pending or rejected
120
+ */
110
121
  public get connected(): boolean {
111
- return this.container.connected;
122
+ return this._connected;
112
123
  }
113
124
 
114
125
  public get canSummarize(): boolean {
@@ -166,6 +177,9 @@ export class ContainerContext implements IContainerContext {
166
177
  quorum: IQuorum,
167
178
  public readonly loader: ILoader,
168
179
  public readonly submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
180
+ public readonly submitSummaryFn: (summaryOp: ISummaryContent) => number,
181
+ /** @returns clientSequenceNumber of last message in a batch */
182
+ public readonly submitBatchFn: (batch: IBatchMessage[]) => number,
169
183
  public readonly submitSignalFn: (contents: any) => void,
170
184
  public readonly closeFn: (error?: ICriticalContainerError) => void,
171
185
  public readonly version: string,
@@ -174,6 +188,7 @@ export class ContainerContext implements IContainerContext {
174
188
  public readonly pendingLocalState?: unknown,
175
189
 
176
190
  ) {
191
+ this._connected = this.container.connected;
177
192
  this._quorum = quorum;
178
193
  this.taggedLogger = container.subLogger;
179
194
  this._fluidModuleP = new LazyPromise<IFluidModuleWithDetails>(
@@ -183,9 +198,10 @@ export class ContainerContext implements IContainerContext {
183
198
  }
184
199
 
185
200
  /**
186
- * @deprecated - Temporary migratory API, to be removed when customers no longer need it. When removed,
187
- * ContainerContext should only take an IQuorumClients rather than an IQuorum. See IContainerContext for more
188
- * details.
201
+ * @deprecated Temporary migratory API, to be removed when customers no longer need it.
202
+ * When removed, `ContainerContext` should only take an {@link @fluidframework/container-definitions#IQuorumClients}
203
+ * rather than an {@link @fluidframework/protocol-definitions#IQuorum}.
204
+ * See {@link @fluidframework/container-definitions#IContainerContext} for more details.
189
205
  */
190
206
  public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
191
207
  return (this._quorum.get("code") ?? this._quorum.get("code2")) as IFluidCodeDetails | undefined;
@@ -223,9 +239,7 @@ export class ContainerContext implements IContainerContext {
223
239
 
224
240
  public setConnectionState(connected: boolean, clientId?: string) {
225
241
  const runtime = this.runtime;
226
-
227
- assert(connected === this.connected, 0x0de /* "Mismatch in connection state while setting" */);
228
-
242
+ this._connected = connected;
229
243
  runtime.setConnectionState(connected, clientId);
230
244
  }
231
245
 
@@ -199,12 +199,12 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
199
199
  public get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
200
200
  public get clientDetails() { return this.connectionManager.clientDetails; }
201
201
 
202
- public submit(type: MessageType, contents: any, batch = false, metadata?: any) {
202
+ public submit(type: MessageType, contents?: string, batch = false, metadata?: any) {
203
203
  if (this.currentlyProcessingOps && this.preventConcurrentOpSend) {
204
204
  this.close(new UsageError("Making changes to data model is disallowed while processing ops."));
205
205
  }
206
206
  const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
207
- contents: JSON.stringify(contents),
207
+ contents,
208
208
  metadata,
209
209
  referenceSequenceNumber: this.lastProcessedSequenceNumber,
210
210
  type,
@@ -218,7 +218,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
218
218
  return -1;
219
219
  }
220
220
 
221
- this.opsSize += message.contents.length;
221
+ if (contents !== undefined) {
222
+ this.opsSize += contents.length;
223
+ }
222
224
 
223
225
  this.messageBuffer.push(message);
224
226
 
@@ -233,15 +235,26 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
233
235
  public submitSignal(content: any) { return this.connectionManager.submitSignal(content); }
234
236
 
235
237
  public flush() {
236
- if (this.messageBuffer.length === 0) {
238
+ const batch = this.messageBuffer;
239
+ if (batch.length === 0) {
237
240
  return;
238
241
  }
239
242
 
243
+ this.messageBuffer = [];
244
+
240
245
  // The prepareFlush event allows listeners to append metadata to the batch prior to submission.
241
- this.emit("prepareSend", this.messageBuffer);
246
+ this.emit("prepareSend", batch);
242
247
 
243
- this.connectionManager.sendMessages(this.messageBuffer);
244
- this.messageBuffer = [];
248
+ if (batch.length === 1) {
249
+ assert(batch[0].metadata?.batch === undefined, "no batch markup on single message");
250
+ } else {
251
+ assert(batch[0].metadata?.batch === true, "no start batch markup");
252
+ assert(batch[batch.length - 1].metadata?.batch === false, "no end batch markup");
253
+ }
254
+
255
+ this.connectionManager.sendMessages(batch);
256
+
257
+ assert(this.messageBuffer.length === 0, "reentrancy");
245
258
  }
246
259
 
247
260
  public get connectionProps(): ITelemetryProperties {
package/src/loader.ts CHANGED
@@ -133,7 +133,7 @@ export interface ILoaderOptions extends ILoaderOptions1 {
133
133
 
134
134
  /**
135
135
  * @deprecated IFluidModuleWithDetails interface is moved to
136
- * {@link @fluidframework/container-definition#IFluidModuleWithDetails}
136
+ * {@link @fluidframework/container-definitions#IFluidModuleWithDetails}
137
137
  * to have all the code loading modules in one package. #8193
138
138
  * Encapsulates a module entry point with corresponding code details.
139
139
  */
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.0.0-internal.1.1.0";
9
+ export const pkgVersion = "2.0.0-internal.1.2.0.93071";
package/src/protocol.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  ISignalMessage,
19
19
  MessageType,
20
20
  } from "@fluidframework/protocol-definitions";
21
+ import { canBeCoalescedByService } from "@fluidframework/driver-utils";
21
22
 
22
23
  /**
23
24
  * Function to be used for creating a protocol handler.
@@ -68,7 +69,7 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
68
69
  throw new Error("Remote message's clientId is missing from the quorum");
69
70
  }
70
71
 
71
- if (client?.shouldHaveLeft === true && message.type !== MessageType.NoOp) {
72
+ if (client?.shouldHaveLeft === true && !canBeCoalescedByService(message)) {
72
73
  // pre-0.58 error message: messageClientIdShouldHaveLeft
73
74
  throw new Error("Remote message's clientId already should have left");
74
75
  }