@fluidframework/container-loader 2.0.0-internal.1.4.4 → 2.0.0-internal.2.0.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 (64) hide show
  1. package/.eslintrc.js +21 -8
  2. package/dist/audience.d.ts +0 -4
  3. package/dist/audience.d.ts.map +1 -1
  4. package/dist/audience.js +6 -11
  5. package/dist/audience.js.map +1 -1
  6. package/dist/connectionManager.d.ts.map +1 -1
  7. package/dist/connectionManager.js +30 -4
  8. package/dist/connectionManager.js.map +1 -1
  9. package/dist/container.d.ts +0 -1
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js +7 -22
  12. package/dist/container.js.map +1 -1
  13. package/dist/containerContext.d.ts +1 -1
  14. package/dist/containerContext.d.ts.map +1 -1
  15. package/dist/containerContext.js +2 -2
  16. package/dist/containerContext.js.map +1 -1
  17. package/dist/containerStorageAdapter.js +1 -1
  18. package/dist/containerStorageAdapter.js.map +1 -1
  19. package/dist/deltaManager.d.ts.map +1 -1
  20. package/dist/deltaManager.js +35 -4
  21. package/dist/deltaManager.js.map +1 -1
  22. package/dist/packageVersion.d.ts +1 -1
  23. package/dist/packageVersion.js +1 -1
  24. package/dist/packageVersion.js.map +1 -1
  25. package/dist/protocol.d.ts +8 -3
  26. package/dist/protocol.d.ts.map +1 -1
  27. package/dist/protocol.js +34 -8
  28. package/dist/protocol.js.map +1 -1
  29. package/lib/audience.d.ts +0 -4
  30. package/lib/audience.d.ts.map +1 -1
  31. package/lib/audience.js +6 -11
  32. package/lib/audience.js.map +1 -1
  33. package/lib/connectionManager.d.ts.map +1 -1
  34. package/lib/connectionManager.js +30 -4
  35. package/lib/connectionManager.js.map +1 -1
  36. package/lib/container.d.ts +0 -1
  37. package/lib/container.d.ts.map +1 -1
  38. package/lib/container.js +7 -22
  39. package/lib/container.js.map +1 -1
  40. package/lib/containerContext.d.ts +1 -1
  41. package/lib/containerContext.d.ts.map +1 -1
  42. package/lib/containerContext.js +2 -2
  43. package/lib/containerContext.js.map +1 -1
  44. package/lib/containerStorageAdapter.js +1 -1
  45. package/lib/containerStorageAdapter.js.map +1 -1
  46. package/lib/deltaManager.d.ts.map +1 -1
  47. package/lib/deltaManager.js +36 -5
  48. package/lib/deltaManager.js.map +1 -1
  49. package/lib/packageVersion.d.ts +1 -1
  50. package/lib/packageVersion.js +1 -1
  51. package/lib/packageVersion.js.map +1 -1
  52. package/lib/protocol.d.ts +8 -3
  53. package/lib/protocol.d.ts.map +1 -1
  54. package/lib/protocol.js +33 -7
  55. package/lib/protocol.js.map +1 -1
  56. package/package.json +16 -23
  57. package/src/audience.ts +6 -12
  58. package/src/connectionManager.ts +34 -7
  59. package/src/container.ts +8 -24
  60. package/src/containerContext.ts +2 -2
  61. package/src/containerStorageAdapter.ts +1 -1
  62. package/src/deltaManager.ts +43 -8
  63. package/src/packageVersion.ts +1 -1
  64. package/src/protocol.ts +31 -8
@@ -18,6 +18,7 @@ import {
18
18
  } from "@fluidframework/container-definitions";
19
19
  import { GenericError, UsageError } from "@fluidframework/container-utils";
20
20
  import {
21
+ IAnyDriverError,
21
22
  IDocumentService,
22
23
  IDocumentDeltaConnection,
23
24
  IDocumentDeltaConnectionEvents,
@@ -27,7 +28,6 @@ import {
27
28
  createWriteError,
28
29
  createGenericNetworkError,
29
30
  getRetryDelayFromError,
30
- IAnyDriverError,
31
31
  waitForConnectedState,
32
32
  DeltaStreamConnectionForbiddenError,
33
33
  logNetworkFailure,
@@ -59,6 +59,7 @@ import {
59
59
  IConnectionManagerFactoryArgs,
60
60
  } from "./contracts";
61
61
  import { DeltaQueue } from "./deltaQueue";
62
+ import { SignalType } from "./protocol";
62
63
 
63
64
  const MaxReconnectDelayInMs = 8000;
64
65
  const InitialReconnectDelayInMs = 1000;
@@ -713,17 +714,43 @@ export class ConnectionManager implements IConnectionManager {
713
714
  initialMessages,
714
715
  this.connectFirstConnection ? "InitialOps" : "ReconnectOps");
715
716
 
716
- if (connection.initialSignals !== undefined) {
717
- for (const signal of connection.initialSignals) {
718
- this.props.signalHandler(signal);
719
- }
720
- }
721
-
722
717
  const details = ConnectionManager.detailsFromConnection(connection);
723
718
  details.checkpointSequenceNumber = checkpointSequenceNumber;
724
719
  this.props.connectHandler(details);
725
720
 
726
721
  this.connectFirstConnection = false;
722
+
723
+ // Synthesize clear & join signals out of initialClients state.
724
+ // This allows us to have single way to process signals, and makes it simpler to initialize
725
+ // protocol in Container.
726
+ const clearSignal: ISignalMessage = {
727
+ clientId: null, // system message
728
+ content: JSON.stringify({
729
+ type: SignalType.Clear,
730
+ }),
731
+ };
732
+ this.props.signalHandler(clearSignal);
733
+
734
+ for (const priorClient of connection.initialClients ?? []) {
735
+ const joinSignal: ISignalMessage = {
736
+ clientId: null, // system signal
737
+ content: JSON.stringify({
738
+ type: SignalType.ClientJoin,
739
+ content: priorClient, // ISignalClient
740
+ }),
741
+ };
742
+ this.props.signalHandler(joinSignal);
743
+ }
744
+
745
+ // Unfortunately, there is no defined order between initialSignals (including join & leave signals)
746
+ // and connection.initialClients. In practice, connection.initialSignals quite often contains join signal
747
+ // for "self" and connection.initialClients does not contain "self", so we have to process them after
748
+ // "clear" signal above.
749
+ if (connection.initialSignals !== undefined) {
750
+ for (const signal of connection.initialSignals) {
751
+ this.props.signalHandler(signal);
752
+ }
753
+ }
727
754
  }
728
755
 
729
756
  /**
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,
@@ -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,7 +637,10 @@ 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(),
@@ -1383,11 +1384,8 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1383
1384
  attributes,
1384
1385
  quorumSnapshot,
1385
1386
  (key, value) => this.submitMessage(MessageType.Propose, JSON.stringify({ key, value })),
1386
- this._initialClients ?? [],
1387
1387
  );
1388
1388
 
1389
- this._initialClients = undefined;
1390
-
1391
1389
  const protocolLogger = ChildLogger.create(this.subLogger, "ProtocolHandler");
1392
1390
 
1393
1391
  protocol.quorum.on("error", (error) => {
@@ -1512,20 +1510,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1512
1510
  deltaManager.inboundSignal.pause();
1513
1511
 
1514
1512
  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
-
1529
1513
  this.connectionStateHandler.receivedConnectEvent(
1530
1514
  this.connectionMode,
1531
1515
  details,
@@ -1629,7 +1613,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1629
1613
  }
1630
1614
  }
1631
1615
 
1632
- private propagateConnectionState(initialTransition: boolean) {
1616
+ private propagateConnectionState(initialTransition: boolean, disconnectedReason?: string) {
1633
1617
  // When container loaded, we want to propagate initial connection state.
1634
1618
  // After that, we communicate only transitions to Connected & Disconnected states, skipping all other states.
1635
1619
  // This can be changed in the future, for example we likely should add "CatchingUp" event on Container.
@@ -1652,7 +1636,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1652
1636
 
1653
1637
  this.setContextConnectedState(state, this._deltaManager.connectionManager.readOnlyInfo.readonly ?? false);
1654
1638
  this.protocolHandler.setConnectionState(state, this.clientId);
1655
- raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1639
+ raiseConnectedEvent(this.mc.logger, this, state, this.clientId, disconnectedReason);
1656
1640
 
1657
1641
  if (logOpsOnReconnect) {
1658
1642
  this.mc.logger.sendTelemetryEvent(
@@ -1724,7 +1708,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1724
1708
  const result = this.protocolHandler.processMessage(message, local);
1725
1709
 
1726
1710
  // Forward messages to the loaded runtime for processing
1727
- this.context.process(message, local, undefined);
1711
+ this.context.process(message, local);
1728
1712
 
1729
1713
  // Inactive (not in quorum or not writers) clients don't take part in the minimum sequence number calculation.
1730
1714
  if (this.activeConnection()) {
@@ -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) {
@@ -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
  }
@@ -42,7 +42,8 @@ import {
42
42
  } from "@fluidframework/protocol-definitions";
43
43
  import {
44
44
  NonRetryableError,
45
- isClientMessage,
45
+ isRuntimeMessage,
46
+ MessageType2,
46
47
  } from "@fluidframework/driver-utils";
47
48
  import {
48
49
  ThrottlingWarning,
@@ -72,6 +73,25 @@ export interface IDeltaManagerInternalEvents extends IDeltaManagerEvents {
72
73
  (event: "closed", listener: (error?: ICriticalContainerError) => void);
73
74
  }
74
75
 
76
+ /**
77
+ * Determines if message was sent by client, not service
78
+ */
79
+ function isClientMessage(message: ISequencedDocumentMessage | IDocumentMessage): boolean {
80
+ if (isRuntimeMessage(message)) {
81
+ return true;
82
+ }
83
+ switch (message.type) {
84
+ case MessageType.Propose:
85
+ case MessageType.Reject:
86
+ case MessageType.NoOp:
87
+ case MessageType2.Accept:
88
+ case MessageType.Summarize:
89
+ return true;
90
+ default:
91
+ return false;
92
+ }
93
+ }
94
+
75
95
  /**
76
96
  * Manages the flow of both inbound and outbound messages. This class ensures that shared objects receive delta
77
97
  * messages in order regardless of possible network conditions or timings causing out of order delivery.
@@ -218,6 +238,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
218
238
  return -1;
219
239
  }
220
240
 
241
+ assert(isClientMessage(message), 0x419 /* client sends non-client message */);
242
+
221
243
  if (contents !== undefined) {
222
244
  this.opsSize += contents.length;
223
245
  }
@@ -795,13 +817,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
795
817
  this.currentlyProcessingOps = true;
796
818
  this.lastProcessedMessage = message;
797
819
 
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
- );
820
+ const isString = typeof message.clientId === "string";
821
+ assert(message.clientId === null || isString, 0x41a /* undefined or string */);
822
+ // All client messages are coming from some client, and should have clientId,
823
+ // and non-client message should not have clientId. But, there are two exceptions:
824
+ // 1. (Legacy) We can see message.type === "attach" or "chunkedOp" for legacy files before RTM
825
+ // 2. Non-immediate noops (contents: null) can be sent by service without clientId
826
+ if (!isString && isClientMessage(message) && message.type !== MessageType.NoOp) {
827
+ throw new DataCorruptionError("Mismatch in clientId",
828
+ { ...extractSafePropertiesFromMessage(message), messageType: message.type });
829
+ }
805
830
 
806
831
  // TODO Remove after SPO picks up the latest build.
807
832
  if (
@@ -822,6 +847,16 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
822
847
  clientId: this.connectionManager.clientId,
823
848
  });
824
849
  }
850
+
851
+ // Client ops: MSN has to be lower than sequence #, as client can continue to send ops with same
852
+ // reference sequence number as this op.
853
+ // System ops (when no clients are connected) are the only ops where equation is possible.
854
+ const diff = message.sequenceNumber - message.minimumSequenceNumber;
855
+ if (diff < 0 || diff === 0 && message.clientId !== null) {
856
+ throw new DataCorruptionError("MSN has to be lower than sequence #",
857
+ extractSafePropertiesFromMessage(message));
858
+ }
859
+
825
860
  this.minSequenceNumber = message.minimumSequenceNumber;
826
861
 
827
862
  if (message.sequenceNumber !== this.lastProcessedSequenceNumber + 1) {
@@ -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.4.4";
9
+ export const pkgVersion = "2.0.0-internal.2.0.1";
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;