@fluidframework/container-loader 2.0.0-dev.1.4.6.106135 → 2.0.0-dev.2.3.0.115467

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 (96) hide show
  1. package/.eslintrc.js +21 -8
  2. package/README.md +21 -11
  3. package/dist/audience.d.ts +0 -4
  4. package/dist/audience.d.ts.map +1 -1
  5. package/dist/audience.js +11 -11
  6. package/dist/audience.js.map +1 -1
  7. package/dist/collabWindowTracker.js +5 -4
  8. package/dist/collabWindowTracker.js.map +1 -1
  9. package/dist/connectionManager.d.ts.map +1 -1
  10. package/dist/connectionManager.js +49 -7
  11. package/dist/connectionManager.js.map +1 -1
  12. package/dist/connectionStateHandler.d.ts +20 -11
  13. package/dist/connectionStateHandler.d.ts.map +1 -1
  14. package/dist/connectionStateHandler.js +65 -36
  15. package/dist/connectionStateHandler.js.map +1 -1
  16. package/dist/container.d.ts +8 -2
  17. package/dist/container.d.ts.map +1 -1
  18. package/dist/container.js +32 -36
  19. package/dist/container.js.map +1 -1
  20. package/dist/containerContext.d.ts +1 -1
  21. package/dist/containerContext.d.ts.map +1 -1
  22. package/dist/containerContext.js +7 -3
  23. package/dist/containerContext.js.map +1 -1
  24. package/dist/containerStorageAdapter.js +1 -1
  25. package/dist/containerStorageAdapter.js.map +1 -1
  26. package/dist/contracts.d.ts +8 -0
  27. package/dist/contracts.d.ts.map +1 -1
  28. package/dist/contracts.js.map +1 -1
  29. package/dist/deltaManager.d.ts +1 -3
  30. package/dist/deltaManager.d.ts.map +1 -1
  31. package/dist/deltaManager.js +40 -16
  32. package/dist/deltaManager.js.map +1 -1
  33. package/dist/packageVersion.d.ts +1 -1
  34. package/dist/packageVersion.js +1 -1
  35. package/dist/packageVersion.js.map +1 -1
  36. package/dist/protocol.d.ts +8 -3
  37. package/dist/protocol.d.ts.map +1 -1
  38. package/dist/protocol.js +34 -8
  39. package/dist/protocol.js.map +1 -1
  40. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  41. package/dist/retriableDocumentStorageService.js +7 -3
  42. package/dist/retriableDocumentStorageService.js.map +1 -1
  43. package/lib/audience.d.ts +0 -4
  44. package/lib/audience.d.ts.map +1 -1
  45. package/lib/audience.js +11 -11
  46. package/lib/audience.js.map +1 -1
  47. package/lib/collabWindowTracker.js +5 -4
  48. package/lib/collabWindowTracker.js.map +1 -1
  49. package/lib/connectionManager.d.ts.map +1 -1
  50. package/lib/connectionManager.js +49 -7
  51. package/lib/connectionManager.js.map +1 -1
  52. package/lib/connectionStateHandler.d.ts +20 -11
  53. package/lib/connectionStateHandler.d.ts.map +1 -1
  54. package/lib/connectionStateHandler.js +65 -36
  55. package/lib/connectionStateHandler.js.map +1 -1
  56. package/lib/container.d.ts +8 -2
  57. package/lib/container.d.ts.map +1 -1
  58. package/lib/container.js +31 -36
  59. package/lib/container.js.map +1 -1
  60. package/lib/containerContext.d.ts +1 -1
  61. package/lib/containerContext.d.ts.map +1 -1
  62. package/lib/containerContext.js +7 -3
  63. package/lib/containerContext.js.map +1 -1
  64. package/lib/containerStorageAdapter.js +1 -1
  65. package/lib/containerStorageAdapter.js.map +1 -1
  66. package/lib/contracts.d.ts +8 -0
  67. package/lib/contracts.d.ts.map +1 -1
  68. package/lib/contracts.js.map +1 -1
  69. package/lib/deltaManager.d.ts +1 -3
  70. package/lib/deltaManager.d.ts.map +1 -1
  71. package/lib/deltaManager.js +43 -19
  72. package/lib/deltaManager.js.map +1 -1
  73. package/lib/packageVersion.d.ts +1 -1
  74. package/lib/packageVersion.js +1 -1
  75. package/lib/packageVersion.js.map +1 -1
  76. package/lib/protocol.d.ts +8 -3
  77. package/lib/protocol.d.ts.map +1 -1
  78. package/lib/protocol.js +33 -7
  79. package/lib/protocol.js.map +1 -1
  80. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  81. package/lib/retriableDocumentStorageService.js +7 -3
  82. package/lib/retriableDocumentStorageService.js.map +1 -1
  83. package/package.json +27 -29
  84. package/prettier.config.cjs +8 -0
  85. package/src/audience.ts +11 -12
  86. package/src/collabWindowTracker.ts +5 -5
  87. package/src/connectionManager.ts +56 -11
  88. package/src/connectionStateHandler.ts +87 -39
  89. package/src/container.ts +36 -38
  90. package/src/containerContext.ts +10 -4
  91. package/src/containerStorageAdapter.ts +1 -1
  92. package/src/contracts.ts +8 -0
  93. package/src/deltaManager.ts +61 -39
  94. package/src/packageVersion.ts +1 -1
  95. package/src/protocol.ts +31 -8
  96. package/src/retriableDocumentStorageService.ts +7 -3
package/src/container.ts CHANGED
@@ -64,7 +64,6 @@ import {
64
64
  ISequencedClient,
65
65
  ISequencedDocumentMessage,
66
66
  ISequencedProposal,
67
- ISignalClient,
68
67
  ISignalMessage,
69
68
  ISnapshotTree,
70
69
  ISummaryContent,
@@ -242,14 +241,14 @@ const getCodeProposal =
242
241
  * @param eventName - event name
243
242
  * @param action - functor to call and measure
244
243
  */
245
- async function ReportIfTooLong(
244
+ export async function ReportIfTooLong(
246
245
  logger: ITelemetryLogger,
247
246
  eventName: string,
248
247
  action: () => Promise<ITelemetryProperties>,
249
248
  ) {
250
249
  const event = PerformanceEvent.start(logger, { eventName });
251
250
  const props = await action();
252
- if (event.duration > 1000) {
251
+ if (event.duration > 200) {
253
252
  event.end(props);
254
253
  }
255
254
  }
@@ -412,7 +411,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
412
411
  private readonly clientDetailsOverride: IClientDetails | undefined;
413
412
  private readonly _deltaManager: DeltaManager<ConnectionManager>;
414
413
  private service: IDocumentService | undefined;
415
- private _initialClients: ISignalClient[] | undefined;
416
414
 
417
415
  private _context: ContainerContext | undefined;
418
416
  private get context() {
@@ -619,7 +617,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
619
617
  this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
620
618
 
621
619
  const summarizeProtocolTree =
622
- this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree")
620
+ this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree2")
623
621
  ?? this.loader.services.options.summarizeProtocolTree;
624
622
 
625
623
  this.options = {
@@ -639,19 +637,37 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
639
637
  }
640
638
  this.logConnectionStateChangeTelemetry(value, oldState, reason);
641
639
  if (this._lifecycleState === "loaded") {
642
- this.propagateConnectionState(false /* initial transition */);
640
+ this.propagateConnectionState(
641
+ false /* initial transition */,
642
+ value === ConnectionState.Disconnected ? reason : undefined /* disconnectedReason */,
643
+ );
643
644
  }
644
645
  },
645
646
  shouldClientJoinWrite: () => this._deltaManager.connectionManager.shouldJoinWrite(),
646
647
  maxClientLeaveWaitTime: this.loader.services.options.maxClientLeaveWaitTime,
647
648
  logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => {
649
+ const mode = this.connectionMode;
648
650
  // We get here when socket does not receive any ops on "write" connection, including
649
651
  // its own join op. Attempt recovery option.
650
652
  this._deltaManager.logConnectionIssue({
651
653
  eventName,
654
+ mode,
652
655
  duration: performance.now() - this.connectionTransitionTimes[ConnectionState.CatchingUp],
653
656
  ...(details === undefined ? {} : { details: JSON.stringify(details) }),
654
657
  });
658
+
659
+ // If this is "write" connection, it took too long to receive join op. But in most cases that's due
660
+ // to very slow op fetches and we will eventually get there.
661
+ // For "read" connections, we get here due to self join signal not arriving on time. We will need to
662
+ // better understand when and why it may happen.
663
+ // For now, attempt to recover by reconnecting. In future, maybe we can query relay service for
664
+ // current state of audience.
665
+ // Other possible recovery path - move to connected state (i.e. ConnectionStateHandler.joinOpTimer
666
+ // to call this.applyForConnectedState("addMemberEvent") for "read" connections)
667
+ if (mode === "read") {
668
+ this.disconnect();
669
+ this.connect();
670
+ }
655
671
  },
656
672
  },
657
673
  this.deltaManager,
@@ -1383,11 +1399,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1383
1399
  attributes,
1384
1400
  quorumSnapshot,
1385
1401
  (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1386
- this._initialClients ?? [],
1387
1402
  );
1388
1403
 
1389
- this._initialClients = undefined;
1390
-
1391
1404
  const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1392
1405
 
1393
1406
  protocol.quorum.on("error", (error) => {
@@ -1512,22 +1525,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1512
1525
  deltaManager.inboundSignal.pause();
1513
1526
 
1514
1527
  deltaManager.on("connect", (details: IConnectionDetails, _opsBehind?: number) => {
1515
- if (this._protocolHandler === undefined) {
1516
- // Store the initial clients so that they can be submitted to the
1517
- // protocol handler when it is created.
1518
- this._initialClients = details.initialClients;
1519
- } else {
1520
- // When reconnecting, the protocol handler is already created,
1521
- // so we can update the audience right now.
1522
- this._protocolHandler.audience.clear();
1523
-
1524
- for (const priorClient of details.initialClients ?? []) {
1525
- this._protocolHandler.audience.addMember(priorClient.clientId, priorClient.client);
1526
- }
1527
- }
1528
-
1528
+ assert(this.connectionMode === details.mode, 0x4b7 /* mismatch */);
1529
1529
  this.connectionStateHandler.receivedConnectEvent(
1530
- this.connectionMode,
1531
1530
  details,
1532
1531
  );
1533
1532
  });
@@ -1542,7 +1541,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1542
1541
  // Some "warning" events come from outside the container and are logged
1543
1542
  // elsewhere (e.g. summarizing container). We shouldn't log these here.
1544
1543
  if (warn.logged !== true) {
1545
- this.logContainerError(warn);
1544
+ this.mc.logger.sendTelemetryEvent({ eventName: "ContainerWarning" }, warn);
1546
1545
  }
1547
1546
  this.emit("warning", warn);
1548
1547
  });
@@ -1629,7 +1628,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1629
1628
  }
1630
1629
  }
1631
1630
 
1632
- private propagateConnectionState(initialTransition: boolean) {
1631
+ private propagateConnectionState(initialTransition: boolean, disconnectedReason?: string) {
1633
1632
  // When container loaded, we want to propagate initial connection state.
1634
1633
  // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1635
1634
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
@@ -1652,7 +1651,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1652
1651
 
1653
1652
  this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1654
1653
  this.protocolHandler.setConnectionState(state, this.clientId);
1655
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1654
+ raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1656
1655
 
1657
1656
  if (logOpsOnReconnect) {
1658
1657
  this.mc.logger.sendTelemetryEvent(
@@ -1687,7 +1686,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1687
1686
  MessageType.Operation,
1688
1687
  message.contents,
1689
1688
  true, // batch
1690
- message.metadata);
1689
+ message.metadata,
1690
+ message.compression);
1691
1691
  }
1692
1692
  this._deltaManager.flush();
1693
1693
  return clientSequenceNumber;
@@ -1706,7 +1706,11 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1706
1706
  return this.submitMessage(MessageType.Summarize, JSON.stringify(summary), false /* batch */);
1707
1707
  }
1708
1708
 
1709
- private submitMessage(type: MessageType, contents?: string, batch?: boolean, metadata?: any): number {
1709
+ private submitMessage(type: MessageType,
1710
+ contents?: string,
1711
+ batch?: boolean,
1712
+ metadata?: any,
1713
+ compression?: string): number {
1710
1714
  if (this.connectionState !== ConnectionState.Connected) {
1711
1715
  this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1712
1716
  return -1;
@@ -1714,7 +1718,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1714
1718
 
1715
1719
  this.messageCountAfterDisconnection += 1;
1716
1720
  this.collabWindowTracker?.stopSequenceNumberUpdate();
1717
- return this._deltaManager.submit(type, contents, batch, metadata);
1721
+ return this._deltaManager.submit(type, contents, batch, metadata, compression);
1718
1722
  }
1719
1723
 
1720
1724
  private processRemoteMessage(message: ISequencedDocumentMessage) {
@@ -1724,7 +1728,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1724
1728
  const result = this.protocolHandler.processMessage(message, local);
1725
1729
 
1726
1730
  // Forward messages to the loaded runtime for processing
1727
- this.context.process(message, local, undefined);
1731
+ this.context.process(message, local);
1728
1732
 
1729
1733
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1730
1734
  if (this.activeConnection()) {
@@ -1740,9 +1744,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1740
1744
  (type) => {
1741
1745
  assert(this.activeConnection(),
1742
1746
  0x241 /* "disconnect should result in stopSequenceNumberUpdate() call" */);
1743
- // back-compat: There is a bug in 1.2 runtime where clients cannot handle
1744
- // ops whose contents are undefined
1745
- this.submitMessage(type, null as any);
1747
+ this.submitMessage(type);
1746
1748
  },
1747
1749
  this.serviceConfiguration.noopTimeFrequency,
1748
1750
  this.serviceConfiguration.noopCountFrequency,
@@ -1848,10 +1850,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1848
1850
  this.emit(dirty ? dirtyContainerEvent : savedContainerEvent);
1849
1851
  }
1850
1852
 
1851
- private logContainerError(warning: ContainerWarning) {
1852
- this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1853
- }
1854
-
1855
1853
  /**
1856
1854
  * Set the connected state of the ContainerContext
1857
1855
  * This controls the "connected" state of the ContainerRuntime as well
@@ -46,7 +46,7 @@ import {
46
46
  ISummaryContent,
47
47
  } from "@fluidframework/protocol-definitions";
48
48
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
49
- import { Container } from "./container";
49
+ import { Container, ReportIfTooLong } from "./container";
50
50
 
51
51
  const PackageNotFactoryError = "Code package does not implement IRuntimeFactory";
52
52
 
@@ -243,8 +243,8 @@ export class ContainerContext implements IContainerContext {
243
243
  runtime.setConnectionState(connected, clientId);
244
244
  }
245
245
 
246
- public process(message: ISequencedDocumentMessage, local: boolean, context: any) {
247
- this.runtime.process(message, local, context);
246
+ public process(message: ISequencedDocumentMessage, local: boolean) {
247
+ this.runtime.process(message, local);
248
248
  }
249
249
 
250
250
  public processSignal(message: ISignalMessage, local: boolean) {
@@ -324,7 +324,13 @@ export class ContainerContext implements IContainerContext {
324
324
 
325
325
  private async instantiateRuntime(existing: boolean) {
326
326
  const runtimeFactory = await this.getRuntimeFactory();
327
- this._runtime = await runtimeFactory.instantiateRuntime(this, existing);
327
+ await ReportIfTooLong(
328
+ this.taggedLogger,
329
+ "instantiateRuntime",
330
+ async () => {
331
+ this._runtime = await runtimeFactory.instantiateRuntime(this, existing);
332
+ return {};
333
+ });
328
334
  }
329
335
 
330
336
  private attachListener() {
@@ -176,7 +176,7 @@ class BlobOnlyStorage implements IDocumentStorageService {
176
176
  // some browsers may not populate stack unless exception is thrown
177
177
  throw new Error("BlobOnlyStorage not implemented method used");
178
178
  } catch (err) {
179
- this.logger.sendErrorEvent({ eventName: "BlobOnlyStorageWrongCall" }, err);
179
+ this.logger.sendTelemetryEvent({ eventName: "BlobOnlyStorageWrongCall" }, err);
180
180
  throw err;
181
181
  }
182
182
  }
package/src/contracts.ts CHANGED
@@ -148,15 +148,23 @@ export interface IConnectionManagerFactoryArgs {
148
148
 
149
149
  /**
150
150
  * Called whenever ping/pong messages are roundtripped on connection.
151
+ *
152
+ * @deprecated No replacement API intended.
151
153
  */
152
154
  readonly pongHandler: (latency: number) => void;
153
155
 
154
156
  /**
155
157
  * Called whenever connection type changes from writable to read-only or vice versa.
158
+ *
159
+ * @remarks
160
+ *
156
161
  * Connection can be read-only if user has no edit permissions, or if container forced
157
162
  * connection to be read-only.
158
163
  * This should not be confused with "read" / "write"connection mode which is internal
159
164
  * optimization.
165
+ *
166
+ * @param readonly - Whether or not the container is now read-only.
167
+ * `undefined` indicates that user permissions are not yet known.
160
168
  */
161
169
  readonly readonlyChangeHandler: (readonly?: boolean) => void;
162
170
  }
@@ -25,8 +25,6 @@ import {
25
25
  normalizeError,
26
26
  logIfFalse,
27
27
  safeRaiseEvent,
28
- MonitoringContext,
29
- loggerToMonitoringContext,
30
28
  } from "@fluidframework/telemetry-utils";
31
29
  import {
32
30
  IDocumentDeltaStorageService,
@@ -42,20 +40,20 @@ import {
42
40
  } from "@fluidframework/protocol-definitions";
43
41
  import {
44
42
  NonRetryableError,
45
- isClientMessage,
43
+ isRuntimeMessage,
44
+ MessageType2,
46
45
  } from "@fluidframework/driver-utils";
47
46
  import {
48
47
  ThrottlingWarning,
49
48
  DataCorruptionError,
50
49
  extractSafePropertiesFromMessage,
51
50
  DataProcessingError,
52
- UsageError,
53
51
  } from "@fluidframework/container-utils";
54
52
  import { DeltaQueue } from "./deltaQueue";
55
53
  import {
56
54
  IConnectionManagerFactoryArgs,
57
55
  IConnectionManager,
58
- } from "./contracts";
56
+ } from "./contracts";
59
57
 
60
58
  export interface IConnectionArgs {
61
59
  mode?: ConnectionMode;
@@ -72,6 +70,25 @@ export interface IDeltaManagerInternalEvents extends IDeltaManagerEvents {
72
70
  (event: "closed", listener: (error?: ICriticalContainerError) => void);
73
71
  }
74
72
 
73
+ /**
74
+ * Determines if message was sent by client, not service
75
+ */
76
+ function isClientMessage(message: ISequencedDocumentMessage | IDocumentMessage): boolean {
77
+ if (isRuntimeMessage(message)) {
78
+ return true;
79
+ }
80
+ switch (message.type) {
81
+ case MessageType.Propose:
82
+ case MessageType.Reject:
83
+ case MessageType.NoOp:
84
+ case MessageType2.Accept:
85
+ case MessageType.Summarize:
86
+ return true;
87
+ default:
88
+ return false;
89
+ }
90
+ }
91
+
75
92
  /**
76
93
  * Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
77
94
  * messages in order regardless of possible network conditions or timings causing out of order delivery.
@@ -92,23 +109,17 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
92
109
  private pending: ISequencedDocumentMessage[] = [];
93
110
  private fetchReason: string | undefined;
94
111
 
95
- private readonly mc: MonitoringContext;
96
-
97
112
  // A boolean used to assert that ops are not being sent while processing another op.
98
113
  private currentlyProcessingOps: boolean = false;
99
114
 
100
- // Feature gate that closes a container when sending an op if the container is
101
- // concurrently processing another op
102
- private readonly preventConcurrentOpSend: boolean = true;
103
-
104
115
  // The minimum sequence number and last sequence number received from the server
105
116
  private minSequenceNumber: number = 0;
106
117
 
107
118
  // There are three numbers we track
108
119
  // * lastQueuedSequenceNumber is the last queued sequence number. If there are gaps in seq numbers, then this number
109
120
  // is not updated until we cover that gap, so it increases each time by 1.
110
- // * lastObservedSeqNumber is an estimation of last known sequence number for container in storage. It's initially
111
- // populated at web socket connection time (if storage provides that info) and is updated once ops shows up.
121
+ // * lastObservedSeqNumber is an estimation of last known sequence number for container in storage. It's initially
122
+ // populated at web socket connection time (if storage provides that info) and is updated once ops shows up.
112
123
  // It's never less than lastQueuedSequenceNumber
113
124
  // * lastProcessedSequenceNumber - last processed sequence number
114
125
  private lastQueuedSequenceNumber: number = 0;
@@ -185,7 +196,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
185
196
  * Tells if current connection has checkpoint information.
186
197
  * I.e. we know how far behind the client was at the time of establishing connection
187
198
  */
188
- public get hasCheckpointSequenceNumber() {
199
+ public get hasCheckpointSequenceNumber() {
189
200
  // Valid to be called only if we have active connection.
190
201
  assert(this.connectionManager.connected, 0x0df /* "Missing active connection" */);
191
202
  return this._checkpointSequenceNumber !== undefined;
@@ -199,15 +210,13 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
199
210
  public get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
200
211
  public get clientDetails() { return this.connectionManager.clientDetails; }
201
212
 
202
- public submit(type: MessageType, contents?: string, batch = false, metadata?: any) {
203
- if (this.currentlyProcessingOps && this.preventConcurrentOpSend) {
204
- this.close(new UsageError("Making changes to data model is disallowed while processing ops."));
205
- }
213
+ public submit(type: MessageType, contents?: string, batch = false, metadata?: any, compression?: string) {
206
214
  const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
207
215
  contents,
208
216
  metadata,
209
217
  referenceSequenceNumber: this.lastProcessedSequenceNumber,
210
218
  type,
219
+ compression,
211
220
  };
212
221
 
213
222
  if (!batch) {
@@ -218,7 +227,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
218
227
  return -1;
219
228
  }
220
229
 
221
- if (contents !== undefined && contents !== null) {
230
+ assert(isClientMessage(message), 0x419 /* client sends non-client message */);
231
+
232
+ if (contents !== undefined) {
222
233
  this.opsSize += contents.length;
223
234
  }
224
235
 
@@ -321,8 +332,6 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
321
332
  };
322
333
 
323
334
  this.connectionManager = createConnectionManager(props);
324
- this.mc = loggerToMonitoringContext(logger);
325
- this.preventConcurrentOpSend = this.mc.config.getBoolean("Fluid.Container.ConcurrentOpSend") === true;
326
335
  this._inbound = new DeltaQueue<ISequencedDocumentMessage>(
327
336
  (op) => {
328
337
  this.processInboundMessage(op);
@@ -390,7 +399,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
390
399
  if (checkpointSequenceNumber > this.lastQueuedSequenceNumber) {
391
400
  this.fetchMissingDeltas("AfterConnection");
392
401
  }
393
- // we do not know the gap, and we will not learn about it if socket is quite - have to ask.
402
+ // we do not know the gap, and we will not learn about it if socket is quite - have to ask.
394
403
  } else if (connection.mode === "read") {
395
404
  this.fetchMissingDeltas("AfterReadConnection");
396
405
  }
@@ -708,9 +717,9 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
708
717
  // Report if we found some issues
709
718
  if (duplicate !== 0 || gap !== 0 && !allowGaps || initialGap > 0 && this.fetchReason === undefined) {
710
719
  eventName = "enqueueMessages";
711
- // Also report if we are fetching ops, and same range comes in, thus making this fetch obsolete.
720
+ // Also report if we are fetching ops, and same range comes in, thus making this fetch obsolete.
712
721
  } else if (this.fetchReason !== undefined && this.fetchReason !== reason &&
713
- (from <= this.lastQueuedSequenceNumber + 1 && last > this.lastQueuedSequenceNumber)) {
722
+ (from <= this.lastQueuedSequenceNumber + 1 && last > this.lastQueuedSequenceNumber)) {
714
723
  eventName = "enqueueMessagesExtraFetch";
715
724
  }
716
725
 
@@ -795,13 +804,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
795
804
  this.currentlyProcessingOps = true;
796
805
  this.lastProcessedMessage = message;
797
806
 
798
- // All non-system messages are coming from some client, and should have clientId
799
- // System messages may have no clientId (but some do, like propose, noop, summarize)
800
- assert(
801
- message.clientId !== undefined
802
- || !(isClientMessage(message)),
803
- 0x0ed /* "non-system message have to have clientId" */,
804
- );
807
+ const isString = typeof message.clientId === "string";
808
+ assert(message.clientId === null || isString, 0x41a /* undefined or string */);
809
+ // All client messages are coming from some client, and should have clientId,
810
+ // and non-client message should not have clientId. But, there are two exceptions:
811
+ // 1. (Legacy) We can see message.type === "attach" or "chunkedOp" for legacy files before RTM
812
+ // 2. Non-immediate noops (contents: null) can be sent by service without clientId
813
+ if (!isString && isClientMessage(message) && message.type !== MessageType.NoOp) {
814
+ throw new DataCorruptionError("Mismatch in clientId",
815
+ { ...extractSafePropertiesFromMessage(message), messageType: message.type });
816
+ }
805
817
 
806
818
  // TODO Remove after SPO picks up the latest build.
807
819
  if (
@@ -822,6 +834,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
822
834
  clientId: this.connectionManager.clientId,
823
835
  });
824
836
  }
837
+
838
+ // Client ops: MSN has to be lower than sequence #, as client can continue to send ops with same
839
+ // reference sequence number as this op.
840
+ // System ops (when no clients are connected) are the only ops where equation is possible.
841
+ const diff = message.sequenceNumber - message.minimumSequenceNumber;
842
+ if (diff < 0 || diff === 0 && message.clientId !== null) {
843
+ throw new DataCorruptionError("MSN has to be lower than sequence #",
844
+ extractSafePropertiesFromMessage(message));
845
+ }
846
+
825
847
  this.minSequenceNumber = message.minimumSequenceNumber;
826
848
 
827
849
  if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
@@ -858,15 +880,15 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
858
880
  /**
859
881
  * Retrieves the missing deltas between the given sequence numbers
860
882
  */
861
- private fetchMissingDeltas(reasonArg: string, to?: number) {
862
- this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
863
- this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
864
- });
865
- }
883
+ private fetchMissingDeltas(reasonArg: string, to?: number) {
884
+ this.fetchMissingDeltasCore(reasonArg, false /* cacheOnly */, to).catch((error) => {
885
+ this.logger.sendErrorEvent({ eventName: "fetchMissingDeltasException" }, error);
886
+ });
887
+ }
866
888
 
867
- /**
868
- * Retrieves the missing deltas between the given sequence numbers
869
- */
889
+ /**
890
+ * Retrieves the missing deltas between the given sequence numbers
891
+ */
870
892
  private async fetchMissingDeltasCore(
871
893
  reason: string,
872
894
  cacheOnly: boolean,
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.0.0-dev.1.4.6.106135";
9
+ export const pkgVersion = "2.0.0-dev.2.3.0.115467";
package/src/protocol.ts CHANGED
@@ -20,6 +20,13 @@ import {
20
20
  } from "@fluidframework/protocol-definitions";
21
21
  import { canBeCoalescedByService } from "@fluidframework/driver-utils";
22
22
 
23
+ // ADO: #1986: Start using enum from protocol-base.
24
+ export enum SignalType {
25
+ ClientJoin = "join", // same value as MessageType.ClientJoin,
26
+ ClientLeave = "leave", // same value as MessageType.ClientLeave,
27
+ Clear = "clear", // used only by client for synthetic signals
28
+ }
29
+
23
30
  /**
24
31
  * Function to be used for creating a protocol handler.
25
32
  */
@@ -27,7 +34,6 @@ export type ProtocolHandlerBuilder = (
27
34
  attributes: IDocumentAttributes,
28
35
  snapshot: IQuorumSnapshot,
29
36
  sendProposal: (key: string, value: any) => number,
30
- initialClients: ISignalClient[],
31
37
  ) => IProtocolHandler;
32
38
 
33
39
  export interface IProtocolHandler extends IBaseProtocolHandler {
@@ -40,7 +46,6 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
40
46
  attributes: IDocumentAttributes,
41
47
  quorumSnapshot: IQuorumSnapshot,
42
48
  sendProposal: (key: string, value: any) => number,
43
- initialClients: ISignalClient[],
44
49
  readonly audience: IAudienceOwner,
45
50
  ) {
46
51
  super(
@@ -53,8 +58,11 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
53
58
  sendProposal,
54
59
  );
55
60
 
56
- for (const initialClient of initialClients) {
57
- this.audience.addMember(initialClient.clientId, initialClient.client);
61
+ // Join / leave signals are ignored for "write" clients in favor of join / leave ops
62
+ this.quorum.on("addMember", (clientId, details) => audience.addMember(clientId, details.client));
63
+ this.quorum.on("removeMember", (clientId) => audience.removeMember(clientId));
64
+ for (const [clientId, details] of this.quorum.getMembers()) {
65
+ this.audience.addMember(clientId, details.client);
58
66
  }
59
67
  }
60
68
 
@@ -81,14 +89,29 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
81
89
  public processSignal(message: ISignalMessage) {
82
90
  const innerContent = message.content as { content: any; type: string; };
83
91
  switch (innerContent.type) {
84
- case MessageType.ClientJoin: {
92
+ case SignalType.Clear: {
93
+ const members = this.audience.getMembers();
94
+ for (const [clientId, client] of members) {
95
+ if (client.mode === "read") {
96
+ this.audience.removeMember(clientId);
97
+ }
98
+ }
99
+ break;
100
+ }
101
+ case SignalType.ClientJoin: {
85
102
  const newClient = innerContent.content as ISignalClient;
86
- this.audience.addMember(newClient.clientId, newClient.client);
103
+ // Ignore write clients - quorum will control such clients.
104
+ if (newClient.client.mode === "read") {
105
+ this.audience.addMember(newClient.clientId, newClient.client);
106
+ }
87
107
  break;
88
108
  }
89
- case MessageType.ClientLeave: {
109
+ case SignalType.ClientLeave: {
90
110
  const leftClientId = innerContent.content as string;
91
- this.audience.removeMember(leftClientId);
111
+ // Ignore write clients - quorum will control such clients.
112
+ if (this.audience.getMember(leftClientId)?.mode === "read") {
113
+ this.audience.removeMember(leftClientId);
114
+ }
92
115
  break;
93
116
  }
94
117
  default: break;
@@ -104,12 +104,16 @@ export class RetriableDocumentStorageService implements IDocumentStorageService,
104
104
  );
105
105
  }
106
106
 
107
- private checkStorageDisposed() {
107
+ private checkStorageDisposed(callName: string, error: unknown) {
108
108
  if (this._disposed) {
109
+ this.logger.sendTelemetryEvent({
110
+ eventName: `${callName}_abortedStorageDisposed`,
111
+ fetchCallName: callName, // fetchCallName matches logs in runWithRetry.ts
112
+ }, error);
109
113
  // pre-0.58 error message: storageServiceDisposedCannotRetry
110
114
  throw new GenericError("Storage Service is disposed. Cannot retry", { canRetry: false });
111
115
  }
112
- return undefined;
116
+ return;
113
117
  }
114
118
 
115
119
  private async runWithRetry<T>(api: () => Promise<T>, callName: string): Promise<T> {
@@ -118,7 +122,7 @@ export class RetriableDocumentStorageService implements IDocumentStorageService,
118
122
  callName,
119
123
  this.logger,
120
124
  {
121
- onRetry: () => this.checkStorageDisposed(),
125
+ onRetry: (_delayInMs: number, error: unknown) => this.checkStorageDisposed(callName, error),
122
126
  },
123
127
  );
124
128
  }