@fluidframework/container-loader 2.0.0-internal.3.0.5 → 2.0.0-internal.3.1.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 (148) hide show
  1. package/.eslintrc.js +18 -21
  2. package/.mocharc.js +2 -2
  3. package/README.md +45 -43
  4. package/api-extractor.json +2 -2
  5. package/closeAndGetPendingLocalState.md +51 -0
  6. package/dist/audience.d.ts.map +1 -1
  7. package/dist/audience.js.map +1 -1
  8. package/dist/catchUpMonitor.d.ts.map +1 -1
  9. package/dist/catchUpMonitor.js.map +1 -1
  10. package/dist/collabWindowTracker.d.ts.map +1 -1
  11. package/dist/collabWindowTracker.js.map +1 -1
  12. package/dist/connectionManager.d.ts +2 -2
  13. package/dist/connectionManager.d.ts.map +1 -1
  14. package/dist/connectionManager.js +51 -24
  15. package/dist/connectionManager.js.map +1 -1
  16. package/dist/connectionState.d.ts.map +1 -1
  17. package/dist/connectionState.js.map +1 -1
  18. package/dist/connectionStateHandler.d.ts.map +1 -1
  19. package/dist/connectionStateHandler.js +35 -16
  20. package/dist/connectionStateHandler.js.map +1 -1
  21. package/dist/container.d.ts +1 -10
  22. package/dist/container.d.ts.map +1 -1
  23. package/dist/container.js +89 -44
  24. package/dist/container.js.map +1 -1
  25. package/dist/containerContext.d.ts.map +1 -1
  26. package/dist/containerContext.js +6 -2
  27. package/dist/containerContext.js.map +1 -1
  28. package/dist/containerStorageAdapter.d.ts.map +1 -1
  29. package/dist/containerStorageAdapter.js +2 -4
  30. package/dist/containerStorageAdapter.js.map +1 -1
  31. package/dist/contracts.d.ts.map +1 -1
  32. package/dist/contracts.js.map +1 -1
  33. package/dist/deltaManager.d.ts +3 -3
  34. package/dist/deltaManager.d.ts.map +1 -1
  35. package/dist/deltaManager.js +56 -27
  36. package/dist/deltaManager.js.map +1 -1
  37. package/dist/deltaManagerProxy.d.ts.map +1 -1
  38. package/dist/deltaManagerProxy.js.map +1 -1
  39. package/dist/deltaQueue.d.ts.map +1 -1
  40. package/dist/deltaQueue.js +4 -2
  41. package/dist/deltaQueue.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/loader.d.ts +3 -3
  46. package/dist/loader.d.ts.map +1 -1
  47. package/dist/loader.js +18 -15
  48. package/dist/loader.js.map +1 -1
  49. package/dist/packageVersion.d.ts +1 -1
  50. package/dist/packageVersion.js +1 -1
  51. package/dist/packageVersion.js.map +1 -1
  52. package/dist/protocol.d.ts.map +1 -1
  53. package/dist/protocol.js +2 -1
  54. package/dist/protocol.js.map +1 -1
  55. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  56. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  57. package/dist/quorum.d.ts.map +1 -1
  58. package/dist/quorum.js.map +1 -1
  59. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  60. package/dist/retriableDocumentStorageService.js +6 -2
  61. package/dist/retriableDocumentStorageService.js.map +1 -1
  62. package/dist/utils.d.ts.map +1 -1
  63. package/dist/utils.js +6 -4
  64. package/dist/utils.js.map +1 -1
  65. package/lib/audience.d.ts.map +1 -1
  66. package/lib/audience.js.map +1 -1
  67. package/lib/catchUpMonitor.d.ts.map +1 -1
  68. package/lib/catchUpMonitor.js.map +1 -1
  69. package/lib/collabWindowTracker.d.ts.map +1 -1
  70. package/lib/collabWindowTracker.js.map +1 -1
  71. package/lib/connectionManager.d.ts +2 -2
  72. package/lib/connectionManager.d.ts.map +1 -1
  73. package/lib/connectionManager.js +53 -26
  74. package/lib/connectionManager.js.map +1 -1
  75. package/lib/connectionState.d.ts.map +1 -1
  76. package/lib/connectionState.js.map +1 -1
  77. package/lib/connectionStateHandler.d.ts.map +1 -1
  78. package/lib/connectionStateHandler.js +35 -16
  79. package/lib/connectionStateHandler.js.map +1 -1
  80. package/lib/container.d.ts +1 -10
  81. package/lib/container.d.ts.map +1 -1
  82. package/lib/container.js +93 -48
  83. package/lib/container.js.map +1 -1
  84. package/lib/containerContext.d.ts.map +1 -1
  85. package/lib/containerContext.js +6 -2
  86. package/lib/containerContext.js.map +1 -1
  87. package/lib/containerStorageAdapter.d.ts.map +1 -1
  88. package/lib/containerStorageAdapter.js +2 -4
  89. package/lib/containerStorageAdapter.js.map +1 -1
  90. package/lib/contracts.d.ts.map +1 -1
  91. package/lib/contracts.js.map +1 -1
  92. package/lib/deltaManager.d.ts +3 -3
  93. package/lib/deltaManager.d.ts.map +1 -1
  94. package/lib/deltaManager.js +58 -29
  95. package/lib/deltaManager.js.map +1 -1
  96. package/lib/deltaManagerProxy.d.ts.map +1 -1
  97. package/lib/deltaManagerProxy.js.map +1 -1
  98. package/lib/deltaQueue.d.ts.map +1 -1
  99. package/lib/deltaQueue.js +4 -2
  100. package/lib/deltaQueue.js.map +1 -1
  101. package/lib/index.d.ts +1 -1
  102. package/lib/index.d.ts.map +1 -1
  103. package/lib/index.js.map +1 -1
  104. package/lib/loader.d.ts +3 -3
  105. package/lib/loader.d.ts.map +1 -1
  106. package/lib/loader.js +18 -15
  107. package/lib/loader.js.map +1 -1
  108. package/lib/packageVersion.d.ts +1 -1
  109. package/lib/packageVersion.js +1 -1
  110. package/lib/packageVersion.js.map +1 -1
  111. package/lib/protocol.d.ts.map +1 -1
  112. package/lib/protocol.js +2 -1
  113. package/lib/protocol.js.map +1 -1
  114. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  115. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  116. package/lib/quorum.d.ts.map +1 -1
  117. package/lib/quorum.js.map +1 -1
  118. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  119. package/lib/retriableDocumentStorageService.js +6 -2
  120. package/lib/retriableDocumentStorageService.js.map +1 -1
  121. package/lib/utils.d.ts.map +1 -1
  122. package/lib/utils.js +6 -4
  123. package/lib/utils.js.map +1 -1
  124. package/package.json +115 -114
  125. package/prettier.config.cjs +1 -1
  126. package/src/audience.ts +51 -46
  127. package/src/catchUpMonitor.ts +39 -37
  128. package/src/collabWindowTracker.ts +75 -70
  129. package/src/connectionManager.ts +1006 -944
  130. package/src/connectionState.ts +19 -19
  131. package/src/connectionStateHandler.ts +544 -465
  132. package/src/container.ts +2056 -1909
  133. package/src/containerContext.ts +350 -340
  134. package/src/containerStorageAdapter.ts +163 -153
  135. package/src/contracts.ts +155 -153
  136. package/src/deltaManager.ts +1069 -992
  137. package/src/deltaManagerProxy.ts +143 -137
  138. package/src/deltaQueue.ts +155 -151
  139. package/src/index.ts +14 -17
  140. package/src/loader.ts +428 -430
  141. package/src/packageVersion.ts +1 -1
  142. package/src/protocol.ts +93 -87
  143. package/src/protocolTreeDocumentStorageService.ts +30 -33
  144. package/src/quorum.ts +34 -34
  145. package/src/retriableDocumentStorageService.ts +118 -102
  146. package/src/utils.ts +89 -82
  147. package/tsconfig.esnext.json +6 -6
  148. package/tsconfig.json +8 -12
@@ -3,7 +3,11 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { ITelemetryLogger, ITelemetryProperties, TelemetryEventCategory } from "@fluidframework/common-definitions";
6
+ import {
7
+ ITelemetryLogger,
8
+ ITelemetryProperties,
9
+ TelemetryEventCategory,
10
+ } from "@fluidframework/common-definitions";
7
11
  import { assert, Timer } from "@fluidframework/common-utils";
8
12
  import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
9
13
  import { ILocalSequencedClient } from "@fluidframework/protocol-base";
@@ -23,122 +27,153 @@ const JoinSignalTimeoutMs = 5000;
23
27
 
24
28
  /** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
25
29
  export interface IConnectionStateHandlerInputs {
26
- logger: ITelemetryLogger;
27
- /** Log to telemetry any change in state, included to Connecting */
28
- connectionStateChanged:
29
- (value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) => void;
30
- /** Whether to expect the client to join in write mode on next connection */
31
- shouldClientJoinWrite: () => boolean;
32
- /** (Optional) How long should we wait on our previous client's Leave op before transitioning to Connected again */
33
- maxClientLeaveWaitTime: number | undefined;
34
- /** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */
35
- logConnectionIssue: (eventName: string, category: TelemetryEventCategory, details?: ITelemetryProperties) => void;
30
+ logger: ITelemetryLogger;
31
+ /** Log to telemetry any change in state, included to Connecting */
32
+ connectionStateChanged: (
33
+ value: ConnectionState,
34
+ oldState: ConnectionState,
35
+ reason?: string | undefined,
36
+ ) => void;
37
+ /** Whether to expect the client to join in write mode on next connection */
38
+ shouldClientJoinWrite: () => boolean;
39
+ /** (Optional) How long should we wait on our previous client's Leave op before transitioning to Connected again */
40
+ maxClientLeaveWaitTime: number | undefined;
41
+ /** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */
42
+ logConnectionIssue: (
43
+ eventName: string,
44
+ category: TelemetryEventCategory,
45
+ details?: ITelemetryProperties,
46
+ ) => void;
36
47
  }
37
48
 
38
49
  /**
39
50
  * interface that connection state handler implements
40
51
  */
41
52
  export interface IConnectionStateHandler {
42
- readonly connectionState: ConnectionState;
43
- readonly pendingClientId: string | undefined;
44
-
45
- containerSaved(): void;
46
- dispose(): void;
47
- initProtocol(protocol: IProtocolHandler): void;
48
- receivedConnectEvent(details: IConnectionDetails): void;
49
- receivedDisconnectEvent(reason: string): void;
53
+ readonly connectionState: ConnectionState;
54
+ readonly pendingClientId: string | undefined;
55
+
56
+ containerSaved(): void;
57
+ dispose(): void;
58
+ initProtocol(protocol: IProtocolHandler): void;
59
+ receivedConnectEvent(details: IConnectionDetails): void;
60
+ receivedDisconnectEvent(reason: string): void;
50
61
  }
51
62
 
52
63
  export function createConnectionStateHandler(
53
- inputs: IConnectionStateHandlerInputs,
54
- deltaManager: IDeltaManager<any, any>,
55
- clientId?: string,
64
+ inputs: IConnectionStateHandlerInputs,
65
+ deltaManager: IDeltaManager<any, any>,
66
+ clientId?: string,
56
67
  ) {
57
- const mc = loggerToMonitoringContext(inputs.logger);
58
- return createConnectionStateHandlerCore(
59
- mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true, // connectedRaisedWhenCaughtUp
60
- mc.config.getBoolean("Fluid.Container.EnableJoinSignalWait") === true, // readClientsWaitForJoinSignal
61
- inputs,
62
- deltaManager,
63
- clientId,
64
- );
68
+ const mc = loggerToMonitoringContext(inputs.logger);
69
+ return createConnectionStateHandlerCore(
70
+ mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true, // connectedRaisedWhenCaughtUp
71
+ mc.config.getBoolean("Fluid.Container.EnableJoinSignalWait") === true, // readClientsWaitForJoinSignal
72
+ inputs,
73
+ deltaManager,
74
+ clientId,
75
+ );
65
76
  }
66
77
 
67
78
  export function createConnectionStateHandlerCore(
68
- connectedRaisedWhenCaughtUp: boolean,
69
- readClientsWaitForJoinSignal: boolean,
70
- inputs: IConnectionStateHandlerInputs,
71
- deltaManager: IDeltaManager<any, any>,
72
- clientId?: string,
79
+ connectedRaisedWhenCaughtUp: boolean,
80
+ readClientsWaitForJoinSignal: boolean,
81
+ inputs: IConnectionStateHandlerInputs,
82
+ deltaManager: IDeltaManager<any, any>,
83
+ clientId?: string,
73
84
  ) {
74
- if (!connectedRaisedWhenCaughtUp) {
75
- return new ConnectionStateHandler(inputs, readClientsWaitForJoinSignal, clientId);
76
- }
77
- return new ConnectionStateCatchup(
78
- inputs,
79
- (handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(
80
- handler,
81
- readClientsWaitForJoinSignal,
82
- clientId),
83
- deltaManager);
85
+ if (!connectedRaisedWhenCaughtUp) {
86
+ return new ConnectionStateHandler(inputs, readClientsWaitForJoinSignal, clientId);
87
+ }
88
+ return new ConnectionStateCatchup(
89
+ inputs,
90
+ (handler: IConnectionStateHandlerInputs) =>
91
+ new ConnectionStateHandler(handler, readClientsWaitForJoinSignal, clientId),
92
+ deltaManager,
93
+ );
84
94
  }
85
95
 
86
96
  /**
87
97
  * Helper internal interface to abstract away Audience & Quorum
88
98
  */
89
99
  interface IMembership {
90
- on(
91
- eventName: "addMember" | "removeMember",
92
- listener: (clientId: string, details: IClient | ISequencedClient) => void);
93
- getMember(clientId: string): undefined | unknown;
100
+ on(
101
+ eventName: "addMember" | "removeMember",
102
+ listener: (clientId: string, details: IClient | ISequencedClient) => void,
103
+ );
104
+ getMember(clientId: string): undefined | unknown;
94
105
  }
95
106
 
96
107
  /**
97
108
  * Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.
98
109
  * It implements both ends of communication interfaces and passes data back and forward
99
110
  */
100
- class ConnectionStateHandlerPassThrough implements IConnectionStateHandler, IConnectionStateHandlerInputs {
101
- protected readonly pimpl: IConnectionStateHandler;
102
-
103
- constructor(
104
- protected readonly inputs: IConnectionStateHandlerInputs,
105
- pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
106
- ) {
107
- this.pimpl = pimplFactory(this);
108
- }
109
-
110
- /**
111
- * IConnectionStateHandler
112
- */
113
- public get connectionState() { return this.pimpl.connectionState; }
114
- public get pendingClientId() { return this.pimpl.pendingClientId; }
115
-
116
- public containerSaved() { return this.pimpl.containerSaved(); }
117
- public dispose() { return this.pimpl.dispose(); }
118
- public initProtocol(protocol: IProtocolHandler) { return this.pimpl.initProtocol(protocol); }
119
- public receivedDisconnectEvent(reason: string) { return this.pimpl.receivedDisconnectEvent(reason); }
120
-
121
- public receivedConnectEvent(details: IConnectionDetails) {
122
- return this.pimpl.receivedConnectEvent(details);
123
- }
124
-
125
- /**
126
- * IConnectionStateHandlerInputs
127
- */
128
-
129
- public get logger() { return this.inputs.logger; }
130
- public connectionStateChanged(
131
- value: ConnectionState,
132
- oldState: ConnectionState,
133
- reason?: string | undefined,
134
- ) {
135
- return this.inputs.connectionStateChanged(value, oldState, reason);
136
- }
137
- public shouldClientJoinWrite() { return this.inputs.shouldClientJoinWrite(); }
138
- public get maxClientLeaveWaitTime() { return this.inputs.maxClientLeaveWaitTime; }
139
- public logConnectionIssue(eventName: string, category: TelemetryEventCategory, details?: ITelemetryProperties) {
140
- return this.inputs.logConnectionIssue(eventName, category, details);
141
- }
111
+ class ConnectionStateHandlerPassThrough
112
+ implements IConnectionStateHandler, IConnectionStateHandlerInputs
113
+ {
114
+ protected readonly pimpl: IConnectionStateHandler;
115
+
116
+ constructor(
117
+ protected readonly inputs: IConnectionStateHandlerInputs,
118
+ pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
119
+ ) {
120
+ this.pimpl = pimplFactory(this);
121
+ }
122
+
123
+ /**
124
+ * IConnectionStateHandler
125
+ */
126
+ public get connectionState() {
127
+ return this.pimpl.connectionState;
128
+ }
129
+ public get pendingClientId() {
130
+ return this.pimpl.pendingClientId;
131
+ }
132
+
133
+ public containerSaved() {
134
+ return this.pimpl.containerSaved();
135
+ }
136
+ public dispose() {
137
+ return this.pimpl.dispose();
138
+ }
139
+ public initProtocol(protocol: IProtocolHandler) {
140
+ return this.pimpl.initProtocol(protocol);
141
+ }
142
+ public receivedDisconnectEvent(reason: string) {
143
+ return this.pimpl.receivedDisconnectEvent(reason);
144
+ }
145
+
146
+ public receivedConnectEvent(details: IConnectionDetails) {
147
+ return this.pimpl.receivedConnectEvent(details);
148
+ }
149
+
150
+ /**
151
+ * IConnectionStateHandlerInputs
152
+ */
153
+
154
+ public get logger() {
155
+ return this.inputs.logger;
156
+ }
157
+ public connectionStateChanged(
158
+ value: ConnectionState,
159
+ oldState: ConnectionState,
160
+ reason?: string | undefined,
161
+ ) {
162
+ return this.inputs.connectionStateChanged(value, oldState, reason);
163
+ }
164
+ public shouldClientJoinWrite() {
165
+ return this.inputs.shouldClientJoinWrite();
166
+ }
167
+ public get maxClientLeaveWaitTime() {
168
+ return this.inputs.maxClientLeaveWaitTime;
169
+ }
170
+ public logConnectionIssue(
171
+ eventName: string,
172
+ category: TelemetryEventCategory,
173
+ details?: ITelemetryProperties,
174
+ ) {
175
+ return this.inputs.logConnectionIssue(eventName, category, details);
176
+ }
142
177
  }
143
178
 
144
179
  /**
@@ -146,56 +181,76 @@ class ConnectionStateHandlerPassThrough implements IConnectionStateHandler, ICon
146
181
  * before raising connected event
147
182
  */
148
183
  class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
149
- private catchUpMonitor: ICatchUpMonitor | undefined;
150
-
151
- constructor(
152
- inputs: IConnectionStateHandlerInputs,
153
- pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
154
- private readonly deltaManager: IDeltaManager<any, any>,
155
- ) {
156
- super(inputs, pimplFactory);
157
- this._connectionState = this.pimpl.connectionState;
158
- }
159
-
160
- private _connectionState: ConnectionState;
161
- public get connectionState() {
162
- return this._connectionState;
163
- }
164
-
165
- public connectionStateChanged(value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) {
166
- switch (value) {
167
- case ConnectionState.Connected:
168
- assert(this._connectionState === ConnectionState.CatchingUp, 0x3e1 /* connectivity transitions */);
169
- // Create catch-up monitor here (not earlier), as we might get more exact info by now about how far
170
- // client is behind through join signal. This is only true if base layer uses signals (i.e. audience,
171
- // not quorum, including for "rea" connections) to make decisions about moving to "connected" state.
172
- // In addition to that, in its current form, doing this in ConnectionState.CatchingUp is dangerous as
173
- // we might get callback right away, and it will screw up state transition (as code outside of switch
174
- // statement will overwrite current state).
175
- assert(this.catchUpMonitor === undefined, 0x3eb /* catchUpMonitor should be gone */);
176
- this.catchUpMonitor = new CatchUpMonitor(this.deltaManager, this.transitionToConnectedState);
177
- return;
178
- case ConnectionState.Disconnected:
179
- this.catchUpMonitor?.dispose();
180
- this.catchUpMonitor = undefined;
181
- break;
182
- case ConnectionState.CatchingUp:
183
- assert(this._connectionState === ConnectionState.Disconnected, 0x3e3 /* connectivity transitions */);
184
- break;
185
- default:
186
- }
187
- this._connectionState = value;
188
- this.inputs.connectionStateChanged(value, oldState, reason);
189
- }
190
-
191
- private readonly transitionToConnectedState = () => {
192
- // Defensive measure, we should always be in Connecting state when this is called.
193
- const state = this.pimpl.connectionState;
194
- assert(state === ConnectionState.Connected, 0x3e5 /* invariant broken */);
195
- assert(this._connectionState === ConnectionState.CatchingUp, 0x3e6 /* invariant broken */);
196
- this._connectionState = ConnectionState.Connected;
197
- this.inputs.connectionStateChanged(ConnectionState.Connected, ConnectionState.CatchingUp, "caught up");
198
- };
184
+ private catchUpMonitor: ICatchUpMonitor | undefined;
185
+
186
+ constructor(
187
+ inputs: IConnectionStateHandlerInputs,
188
+ pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
189
+ private readonly deltaManager: IDeltaManager<any, any>,
190
+ ) {
191
+ super(inputs, pimplFactory);
192
+ this._connectionState = this.pimpl.connectionState;
193
+ }
194
+
195
+ private _connectionState: ConnectionState;
196
+ public get connectionState() {
197
+ return this._connectionState;
198
+ }
199
+
200
+ public connectionStateChanged(
201
+ value: ConnectionState,
202
+ oldState: ConnectionState,
203
+ reason?: string | undefined,
204
+ ) {
205
+ switch (value) {
206
+ case ConnectionState.Connected:
207
+ assert(
208
+ this._connectionState === ConnectionState.CatchingUp,
209
+ 0x3e1 /* connectivity transitions */,
210
+ );
211
+ // Create catch-up monitor here (not earlier), as we might get more exact info by now about how far
212
+ // client is behind through join signal. This is only true if base layer uses signals (i.e. audience,
213
+ // not quorum, including for "rea" connections) to make decisions about moving to "connected" state.
214
+ // In addition to that, in its current form, doing this in ConnectionState.CatchingUp is dangerous as
215
+ // we might get callback right away, and it will screw up state transition (as code outside of switch
216
+ // statement will overwrite current state).
217
+ assert(
218
+ this.catchUpMonitor === undefined,
219
+ 0x3eb /* catchUpMonitor should be gone */,
220
+ );
221
+ this.catchUpMonitor = new CatchUpMonitor(
222
+ this.deltaManager,
223
+ this.transitionToConnectedState,
224
+ );
225
+ return;
226
+ case ConnectionState.Disconnected:
227
+ this.catchUpMonitor?.dispose();
228
+ this.catchUpMonitor = undefined;
229
+ break;
230
+ case ConnectionState.CatchingUp:
231
+ assert(
232
+ this._connectionState === ConnectionState.Disconnected,
233
+ 0x3e3 /* connectivity transitions */,
234
+ );
235
+ break;
236
+ default:
237
+ }
238
+ this._connectionState = value;
239
+ this.inputs.connectionStateChanged(value, oldState, reason);
240
+ }
241
+
242
+ private readonly transitionToConnectedState = () => {
243
+ // Defensive measure, we should always be in Connecting state when this is called.
244
+ const state = this.pimpl.connectionState;
245
+ assert(state === ConnectionState.Connected, 0x3e5 /* invariant broken */);
246
+ assert(this._connectionState === ConnectionState.CatchingUp, 0x3e6 /* invariant broken */);
247
+ this._connectionState = ConnectionState.Connected;
248
+ this.inputs.connectionStateChanged(
249
+ ConnectionState.Connected,
250
+ ConnectionState.CatchingUp,
251
+ "caught up",
252
+ );
253
+ };
199
254
  }
200
255
 
201
256
  /**
@@ -228,334 +283,358 @@ class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
228
283
  * For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
229
284
  */
230
285
  class ConnectionStateHandler implements IConnectionStateHandler {
231
- private _connectionState = ConnectionState.Disconnected;
232
- private _pendingClientId: string | undefined;
233
- private readonly prevClientLeftTimer: Timer;
234
- private readonly joinOpTimer: Timer;
235
- private protocol?: IProtocolHandler;
236
- private connection?: IConnectionDetails;
237
- private _clientId?: string;
238
-
239
- private waitEvent: PerformanceEvent | undefined;
240
-
241
- public get connectionState(): ConnectionState {
242
- return this._connectionState;
243
- }
244
-
245
- private get clientId(): string | undefined {
246
- return this._clientId;
247
- }
248
-
249
- public get pendingClientId(): string | undefined {
250
- return this._pendingClientId;
251
- }
252
-
253
- constructor(
254
- private readonly handler: IConnectionStateHandlerInputs,
255
- private readonly readClientsWaitForJoinSignal: boolean,
256
- clientIdFromPausedSession?: string,
257
- ) {
258
- this._clientId = clientIdFromPausedSession;
259
- this.prevClientLeftTimer = new Timer(
260
- // Default is 5 min for which we are going to wait for its own "leave" message. This is same as
261
- // the max time on server after which leave op is sent.
262
- this.handler.maxClientLeaveWaitTime ?? 300000,
263
- () => {
264
- assert(this.connectionState !== ConnectionState.Connected,
265
- 0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */);
266
- this.applyForConnectedState("timeout");
267
- },
268
- );
269
-
270
- this.joinOpTimer = new Timer(
271
- 0, // default value is not used - startJoinOpTimer() explicitly provides timeout
272
- () => {
273
- // I've observed timer firing within couple ms from disconnect event, looks like
274
- // queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.
275
- if (this.connectionState !== ConnectionState.CatchingUp) {
276
- return;
277
- }
278
- const details = {
279
- protocolInitialized: this.protocol !== undefined,
280
- pendingClientId: this.pendingClientId,
281
- clientJoined: this.hasMember(this.pendingClientId),
282
- waitingForLeaveOp: this.waitingForLeaveOp,
283
- };
284
- this.handler.logConnectionIssue("NoJoinOp", "error", details);
285
- },
286
- );
287
- }
288
-
289
- private startJoinOpTimer() {
290
- assert(!this.joinOpTimer.hasTimer, 0x234 /* "has joinOpTimer" */);
291
- assert(this.connection !== undefined, 0x4b3 /* have connection */);
292
- this.joinOpTimer.start(
293
- this.connection.mode === "write" ? JoinOpTimeoutMs : JoinSignalTimeoutMs,
294
- );
295
- }
296
-
297
- private stopJoinOpTimer() {
298
- assert(this.joinOpTimer.hasTimer, 0x235 /* "no joinOpTimer" */);
299
- this.joinOpTimer.clear();
300
- }
301
-
302
- private get waitingForLeaveOp() {
303
- return this.prevClientLeftTimer.hasTimer;
304
- }
305
-
306
- public dispose() {
307
- assert(!this.joinOpTimer.hasTimer, 0x2a5 /* "join timer" */);
308
- this.prevClientLeftTimer.clear();
309
- }
310
-
311
- public containerSaved() {
312
- // If we were waiting for moving to Connected state, then only apply for state change. Since the container
313
- // is now saved and we don't have any ops to roundtrip, we can clear the timer and apply for connected state.
314
- if (this.waitingForLeaveOp) {
315
- this.prevClientLeftTimer.clear();
316
- this.applyForConnectedState("containerSaved");
317
- }
318
- }
319
-
320
- private receivedAddMemberEvent(clientId: string) {
321
- // This is the only one that requires the pending client ID
322
- if (clientId === this.pendingClientId) {
323
- if (this.joinOpTimer.hasTimer) {
324
- this.stopJoinOpTimer();
325
- } else if (this.shouldWaitForJoinSignal()) {
326
- // timer has already fired, meaning it took too long to get join op/signal.
327
- // Record how long it actually took to recover.
328
- // This is generic event, as it by itself is not an error.
329
- // We also have a case where NoJoinOp happens during container boot (we do not report it as error in such case),
330
- // if this log statement happens after boot - we do not want to consider it error case.
331
- this.handler.logConnectionIssue("ReceivedJoinOp", "generic");
332
- }
333
- // Start the event in case we are waiting for leave or timeout.
334
- if (this.waitingForLeaveOp) {
335
- this.waitEvent = PerformanceEvent.start(this.handler.logger, {
336
- eventName: "WaitBeforeClientLeave",
337
- details: JSON.stringify({
338
- waitOnClientId: this._clientId,
339
- hadOutstandingOps: this.handler.shouldClientJoinWrite(),
340
- }),
341
- });
342
- }
343
- this.applyForConnectedState("addMemberEvent");
344
- }
345
- }
346
-
347
- private applyForConnectedState(source: "removeMemberEvent" | "addMemberEvent" | "timeout" | "containerSaved") {
348
- assert(this.protocol !== undefined, 0x236 /* "In all cases it should be already installed" */);
349
-
350
- assert(!this.waitingForLeaveOp || this.hasMember(this.clientId),
351
- 0x2e2 /* "Must only wait for leave message when clientId in quorum" */);
352
-
353
- // Move to connected state only if we are in Connecting state, we have seen our join op
354
- // and there is no timer running which means we are not waiting for previous client to leave
355
- // or timeout has occurred while doing so.
356
- if (this.pendingClientId !== this.clientId
357
- && this.hasMember(this.pendingClientId)
358
- && !this.waitingForLeaveOp
359
- ) {
360
- this.waitEvent?.end({ source });
361
- this.setConnectionState(ConnectionState.Connected);
362
- } else {
363
- // Adding this event temporarily so that we can get help debugging if something goes wrong.
364
- // We may not see any ops due to being disconnected all that time - that's not an error!
365
- const error = source === "timeout" && this.connectionState !== ConnectionState.Disconnected;
366
- this.handler.logger.sendTelemetryEvent({
367
- eventName: "connectedStateRejected",
368
- category: error ? "error" : "generic",
369
- details: JSON.stringify({
370
- source,
371
- pendingClientId: this.pendingClientId,
372
- clientId: this.clientId,
373
- waitingForLeaveOp: this.waitingForLeaveOp,
374
- clientJoined: this.hasMember(this.pendingClientId),
375
- }),
376
- });
377
- }
378
- }
379
-
380
- private receivedRemoveMemberEvent(clientId: string) {
381
- // If the client which has left was us, then finish the timer.
382
- if (this.clientId === clientId) {
383
- this.prevClientLeftTimer.clear();
384
- this.applyForConnectedState("removeMemberEvent");
385
- }
386
- }
387
-
388
- public receivedDisconnectEvent(reason: string) {
389
- this.connection = undefined;
390
- this.setConnectionState(ConnectionState.Disconnected, reason);
391
- }
392
-
393
- private shouldWaitForJoinSignal() {
394
- assert(this.connection !== undefined, 0x4b4 /* all callers call here with active connection */);
395
- return this.connection.mode === "write" || this.readClientsWaitForJoinSignal;
396
- }
397
-
398
- /**
399
- * The "connect" event indicates the connection to the Relay Service is live.
400
- * However, some additional conditions must be met before we can fully transition to
401
- * "Connected" state. This function handles that interim period, known as "Connecting" state.
402
- * @param details - Connection details returned from the Relay Service
403
- * @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
404
- * If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
405
- */
406
- public receivedConnectEvent(
407
- details: IConnectionDetails,
408
- ) {
409
- this.connection = details;
410
-
411
- const oldState = this._connectionState;
412
- this._connectionState = ConnectionState.CatchingUp;
413
-
414
- // The following checks are wrong. They are only valid if user has write access to a file.
415
- // If user lost such access mid-session, user will not be able to get "write" connection.
416
- //
417
- // const writeConnection = details.mode === "write";
418
- // assert(!this.handler.shouldClientJoinWrite() || writeConnection,
419
- // 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
420
- // assert(!this.waitingForLeaveOp || writeConnection,
421
- // 0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
422
-
423
- // Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected
424
- // (have received the join message for the client ID)
425
- // This is especially important in the reconnect case. It's possible there could be outstanding
426
- // ops sent by this client, so we should keep the old client id until we see our own client's
427
- // join message. after we see the join message for our new connection with our new client id,
428
- // we know there can no longer be outstanding ops that we sent with the previous client id.
429
- this._pendingClientId = details.clientId;
430
-
431
- // IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
432
- this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
433
-
434
- // Check if we need to wait for join op/signal, and if we need to wait for leave op from previous connection.
435
- // Pending clientId could have joined already (i.e. join op/signal already processed):
436
- // We are fetching ops from storage in parallel to connecting to Relay Service,
437
- // and given async processes, it's possible that we have already processed our own join message before
438
- // connection was fully established.
439
- if (!this.hasMember(this._pendingClientId) && this.shouldWaitForJoinSignal()) {
440
- // We are waiting for our own join op / signal. When it is processed
441
- // we'll attempt to transition to Connected state via receivedAddMemberEvent() flow.
442
- this.startJoinOpTimer();
443
- } else if (!this.waitingForLeaveOp) {
444
- // We're not waiting for Join or Leave op (if read-only connection those don't even apply),
445
- // go ahead and declare the state to be Connected!
446
- // If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
447
- this.setConnectionState(ConnectionState.Connected);
448
- }
449
- // else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
450
- }
451
-
452
- private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;
453
- private setConnectionState(value: ConnectionState.Connected): void;
454
- private setConnectionState(value: ConnectionState.Disconnected | ConnectionState.Connected, reason?: string): void {
455
- if (this.connectionState === value) {
456
- // Already in the desired state - exit early
457
- this.handler.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
458
- return;
459
- }
460
-
461
- const oldState = this._connectionState;
462
- this._connectionState = value;
463
-
464
- // This is the only place in code that deals with quorum. The rest works with audience
465
- // The code below ensures that we do not send ops until we know that old "write" client's disconnect
466
- // produced (and sequenced) leave op
467
- let client: ILocalSequencedClient | undefined;
468
- if (this._clientId !== undefined) {
469
- client = this.protocol?.quorum?.getMember(this._clientId);
470
- }
471
- if (value === ConnectionState.Connected) {
472
- assert(oldState === ConnectionState.CatchingUp,
473
- 0x1d8 /* "Should only transition from Connecting state" */);
474
- // Mark our old client should have left in the quorum if it's still there
475
- if (client !== undefined) {
476
- client.shouldHaveLeft = true;
477
- }
478
- this._clientId = this.pendingClientId;
479
- } else if (value === ConnectionState.Disconnected) {
480
- // Clear pending state immediately to prepare for reconnect
481
- this._pendingClientId = undefined;
482
-
483
- if (this.joinOpTimer.hasTimer) {
484
- this.stopJoinOpTimer();
485
- }
486
-
487
- // Only wait for "leave" message if the connected client exists in the quorum and had some non-acked ops
488
- // Also check if the timer is not already running as
489
- // we could receive "Disconnected" event multiple times without getting connected and in that case we
490
- // don't want to reset the timer as we still want to wait on original client which started this timer.
491
- if (client !== undefined
492
- && this.handler.shouldClientJoinWrite()
493
- && !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer
494
- ) {
495
- this.prevClientLeftTimer.restart();
496
- } else {
497
- // Adding this event temporarily so that we can get help debugging if something goes wrong.
498
- this.handler.logger.sendTelemetryEvent({
499
- eventName: "noWaitOnDisconnected",
500
- details: JSON.stringify({
501
- clientId: this._clientId,
502
- inQuorum: client !== undefined,
503
- waitingForLeaveOp: this.waitingForLeaveOp,
504
- hadOutstandingOps: this.handler.shouldClientJoinWrite(),
505
- }),
506
- });
507
- }
508
- }
509
-
510
- // Report transition before we propagate event across layers
511
- this.handler.connectionStateChanged(this._connectionState, oldState, reason);
512
- }
513
-
514
- // Helper method to switch between quorum and audience.
515
- // Old design was checking only quorum for "write" clients.
516
- // Latest change checks audience for all types of connections.
517
- protected get membership(): IMembership | undefined {
518
- // We could always use audience here, and in practice it will probably be correct.
519
- // (including case when this.readClientsWaitForJoinSignal === false).
520
- // But only if it's superset of quorum, i.e. when filtered to "write" clients, they are always identical!
521
- // It's safer to assume that we have bugs and engaging kill-bit switch should bring us back to well-known
522
- // and tested state!
523
- return this.readClientsWaitForJoinSignal ? this.protocol?.audience : this.protocol?.quorum;
524
- }
525
-
526
- public initProtocol(protocol: IProtocolHandler) {
527
- this.protocol = protocol;
528
-
529
- this.membership?.on("addMember", (clientId, details) => {
530
- assert((details as IClient).mode === "read" || protocol.quorum.getMember(clientId) !== undefined,
531
- 0x4b5 /* Audience is subset of quorum */);
532
- this.receivedAddMemberEvent(clientId);
533
- });
534
-
535
- this.membership?.on("removeMember", (clientId) => {
536
- assert(protocol.quorum.getMember(clientId) === undefined, 0x4b6 /* Audience is subset of quorum */);
537
- this.receivedRemoveMemberEvent(clientId);
538
- });
539
-
540
- /* There is a tiny tiny race possible, where these events happen in this order:
286
+ private _connectionState = ConnectionState.Disconnected;
287
+ private _pendingClientId: string | undefined;
288
+ private readonly prevClientLeftTimer: Timer;
289
+ private readonly joinOpTimer: Timer;
290
+ private protocol?: IProtocolHandler;
291
+ private connection?: IConnectionDetails;
292
+ private _clientId?: string;
293
+
294
+ private waitEvent: PerformanceEvent | undefined;
295
+
296
+ public get connectionState(): ConnectionState {
297
+ return this._connectionState;
298
+ }
299
+
300
+ private get clientId(): string | undefined {
301
+ return this._clientId;
302
+ }
303
+
304
+ public get pendingClientId(): string | undefined {
305
+ return this._pendingClientId;
306
+ }
307
+
308
+ constructor(
309
+ private readonly handler: IConnectionStateHandlerInputs,
310
+ private readonly readClientsWaitForJoinSignal: boolean,
311
+ clientIdFromPausedSession?: string,
312
+ ) {
313
+ this._clientId = clientIdFromPausedSession;
314
+ this.prevClientLeftTimer = new Timer(
315
+ // Default is 5 min for which we are going to wait for its own "leave" message. This is same as
316
+ // the max time on server after which leave op is sent.
317
+ this.handler.maxClientLeaveWaitTime ?? 300000,
318
+ () => {
319
+ assert(
320
+ this.connectionState !== ConnectionState.Connected,
321
+ 0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */,
322
+ );
323
+ this.applyForConnectedState("timeout");
324
+ },
325
+ );
326
+
327
+ this.joinOpTimer = new Timer(
328
+ 0, // default value is not used - startJoinOpTimer() explicitly provides timeout
329
+ () => {
330
+ // I've observed timer firing within couple ms from disconnect event, looks like
331
+ // queued timer callback is not cancelled if timer is cancelled while callback sits in the queue.
332
+ if (this.connectionState !== ConnectionState.CatchingUp) {
333
+ return;
334
+ }
335
+ const details = {
336
+ protocolInitialized: this.protocol !== undefined,
337
+ pendingClientId: this.pendingClientId,
338
+ clientJoined: this.hasMember(this.pendingClientId),
339
+ waitingForLeaveOp: this.waitingForLeaveOp,
340
+ };
341
+ this.handler.logConnectionIssue("NoJoinOp", "error", details);
342
+ },
343
+ );
344
+ }
345
+
346
+ private startJoinOpTimer() {
347
+ assert(!this.joinOpTimer.hasTimer, 0x234 /* "has joinOpTimer" */);
348
+ assert(this.connection !== undefined, 0x4b3 /* have connection */);
349
+ this.joinOpTimer.start(
350
+ this.connection.mode === "write" ? JoinOpTimeoutMs : JoinSignalTimeoutMs,
351
+ );
352
+ }
353
+
354
+ private stopJoinOpTimer() {
355
+ assert(this.joinOpTimer.hasTimer, 0x235 /* "no joinOpTimer" */);
356
+ this.joinOpTimer.clear();
357
+ }
358
+
359
+ private get waitingForLeaveOp() {
360
+ return this.prevClientLeftTimer.hasTimer;
361
+ }
362
+
363
+ public dispose() {
364
+ assert(!this.joinOpTimer.hasTimer, 0x2a5 /* "join timer" */);
365
+ this.prevClientLeftTimer.clear();
366
+ }
367
+
368
+ public containerSaved() {
369
+ // If we were waiting for moving to Connected state, then only apply for state change. Since the container
370
+ // is now saved and we don't have any ops to roundtrip, we can clear the timer and apply for connected state.
371
+ if (this.waitingForLeaveOp) {
372
+ this.prevClientLeftTimer.clear();
373
+ this.applyForConnectedState("containerSaved");
374
+ }
375
+ }
376
+
377
+ private receivedAddMemberEvent(clientId: string) {
378
+ // This is the only one that requires the pending client ID
379
+ if (clientId === this.pendingClientId) {
380
+ if (this.joinOpTimer.hasTimer) {
381
+ this.stopJoinOpTimer();
382
+ } else if (this.shouldWaitForJoinSignal()) {
383
+ // timer has already fired, meaning it took too long to get join op/signal.
384
+ // Record how long it actually took to recover.
385
+ // This is generic event, as it by itself is not an error.
386
+ // We also have a case where NoJoinOp happens during container boot (we do not report it as error in such case),
387
+ // if this log statement happens after boot - we do not want to consider it error case.
388
+ this.handler.logConnectionIssue("ReceivedJoinOp", "generic");
389
+ }
390
+ // Start the event in case we are waiting for leave or timeout.
391
+ if (this.waitingForLeaveOp) {
392
+ this.waitEvent = PerformanceEvent.start(this.handler.logger, {
393
+ eventName: "WaitBeforeClientLeave",
394
+ details: JSON.stringify({
395
+ waitOnClientId: this._clientId,
396
+ hadOutstandingOps: this.handler.shouldClientJoinWrite(),
397
+ }),
398
+ });
399
+ }
400
+ this.applyForConnectedState("addMemberEvent");
401
+ }
402
+ }
403
+
404
+ private applyForConnectedState(
405
+ source: "removeMemberEvent" | "addMemberEvent" | "timeout" | "containerSaved",
406
+ ) {
407
+ assert(
408
+ this.protocol !== undefined,
409
+ 0x236 /* "In all cases it should be already installed" */,
410
+ );
411
+
412
+ assert(
413
+ !this.waitingForLeaveOp || this.hasMember(this.clientId),
414
+ 0x2e2 /* "Must only wait for leave message when clientId in quorum" */,
415
+ );
416
+
417
+ // Move to connected state only if we are in Connecting state, we have seen our join op
418
+ // and there is no timer running which means we are not waiting for previous client to leave
419
+ // or timeout has occurred while doing so.
420
+ if (
421
+ this.pendingClientId !== this.clientId &&
422
+ this.hasMember(this.pendingClientId) &&
423
+ !this.waitingForLeaveOp
424
+ ) {
425
+ this.waitEvent?.end({ source });
426
+ this.setConnectionState(ConnectionState.Connected);
427
+ } else {
428
+ // Adding this event temporarily so that we can get help debugging if something goes wrong.
429
+ // We may not see any ops due to being disconnected all that time - that's not an error!
430
+ const error =
431
+ source === "timeout" && this.connectionState !== ConnectionState.Disconnected;
432
+ this.handler.logger.sendTelemetryEvent({
433
+ eventName: "connectedStateRejected",
434
+ category: error ? "error" : "generic",
435
+ details: JSON.stringify({
436
+ source,
437
+ pendingClientId: this.pendingClientId,
438
+ clientId: this.clientId,
439
+ waitingForLeaveOp: this.waitingForLeaveOp,
440
+ clientJoined: this.hasMember(this.pendingClientId),
441
+ }),
442
+ });
443
+ }
444
+ }
445
+
446
+ private receivedRemoveMemberEvent(clientId: string) {
447
+ // If the client which has left was us, then finish the timer.
448
+ if (this.clientId === clientId) {
449
+ this.prevClientLeftTimer.clear();
450
+ this.applyForConnectedState("removeMemberEvent");
451
+ }
452
+ }
453
+
454
+ public receivedDisconnectEvent(reason: string) {
455
+ this.connection = undefined;
456
+ this.setConnectionState(ConnectionState.Disconnected, reason);
457
+ }
458
+
459
+ private shouldWaitForJoinSignal() {
460
+ assert(
461
+ this.connection !== undefined,
462
+ 0x4b4 /* all callers call here with active connection */,
463
+ );
464
+ return this.connection.mode === "write" || this.readClientsWaitForJoinSignal;
465
+ }
466
+
467
+ /**
468
+ * The "connect" event indicates the connection to the Relay Service is live.
469
+ * However, some additional conditions must be met before we can fully transition to
470
+ * "Connected" state. This function handles that interim period, known as "Connecting" state.
471
+ * @param details - Connection details returned from the Relay Service
472
+ * @param deltaManager - DeltaManager to be used for delaying Connected transition until caught up.
473
+ * If it's undefined, then don't delay and transition to Connected as soon as Leave/Join op are accounted for
474
+ */
475
+ public receivedConnectEvent(details: IConnectionDetails) {
476
+ this.connection = details;
477
+
478
+ const oldState = this._connectionState;
479
+ this._connectionState = ConnectionState.CatchingUp;
480
+
481
+ // The following checks are wrong. They are only valid if user has write access to a file.
482
+ // If user lost such access mid-session, user will not be able to get "write" connection.
483
+ //
484
+ // const writeConnection = details.mode === "write";
485
+ // assert(!this.handler.shouldClientJoinWrite() || writeConnection,
486
+ // 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
487
+ // assert(!this.waitingForLeaveOp || writeConnection,
488
+ // 0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
489
+
490
+ // Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected
491
+ // (have received the join message for the client ID)
492
+ // This is especially important in the reconnect case. It's possible there could be outstanding
493
+ // ops sent by this client, so we should keep the old client id until we see our own client's
494
+ // join message. after we see the join message for our new connection with our new client id,
495
+ // we know there can no longer be outstanding ops that we sent with the previous client id.
496
+ this._pendingClientId = details.clientId;
497
+
498
+ // IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
499
+ this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
500
+
501
+ // Check if we need to wait for join op/signal, and if we need to wait for leave op from previous connection.
502
+ // Pending clientId could have joined already (i.e. join op/signal already processed):
503
+ // We are fetching ops from storage in parallel to connecting to Relay Service,
504
+ // and given async processes, it's possible that we have already processed our own join message before
505
+ // connection was fully established.
506
+ if (!this.hasMember(this._pendingClientId) && this.shouldWaitForJoinSignal()) {
507
+ // We are waiting for our own join op / signal. When it is processed
508
+ // we'll attempt to transition to Connected state via receivedAddMemberEvent() flow.
509
+ this.startJoinOpTimer();
510
+ } else if (!this.waitingForLeaveOp) {
511
+ // We're not waiting for Join or Leave op (if read-only connection those don't even apply),
512
+ // go ahead and declare the state to be Connected!
513
+ // If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
514
+ this.setConnectionState(ConnectionState.Connected);
515
+ }
516
+ // else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
517
+ }
518
+
519
+ private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;
520
+ private setConnectionState(value: ConnectionState.Connected): void;
521
+ private setConnectionState(
522
+ value: ConnectionState.Disconnected | ConnectionState.Connected,
523
+ reason?: string,
524
+ ): void {
525
+ if (this.connectionState === value) {
526
+ // Already in the desired state - exit early
527
+ this.handler.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
528
+ return;
529
+ }
530
+
531
+ const oldState = this._connectionState;
532
+ this._connectionState = value;
533
+
534
+ // This is the only place in code that deals with quorum. The rest works with audience
535
+ // The code below ensures that we do not send ops until we know that old "write" client's disconnect
536
+ // produced (and sequenced) leave op
537
+ let client: ILocalSequencedClient | undefined;
538
+ if (this._clientId !== undefined) {
539
+ client = this.protocol?.quorum?.getMember(this._clientId);
540
+ }
541
+ if (value === ConnectionState.Connected) {
542
+ assert(
543
+ oldState === ConnectionState.CatchingUp,
544
+ 0x1d8 /* "Should only transition from Connecting state" */,
545
+ );
546
+ // Mark our old client should have left in the quorum if it's still there
547
+ if (client !== undefined) {
548
+ client.shouldHaveLeft = true;
549
+ }
550
+ this._clientId = this.pendingClientId;
551
+ } else if (value === ConnectionState.Disconnected) {
552
+ // Clear pending state immediately to prepare for reconnect
553
+ this._pendingClientId = undefined;
554
+
555
+ if (this.joinOpTimer.hasTimer) {
556
+ this.stopJoinOpTimer();
557
+ }
558
+
559
+ // Only wait for "leave" message if the connected client exists in the quorum and had some non-acked ops
560
+ // Also check if the timer is not already running as
561
+ // we could receive "Disconnected" event multiple times without getting connected and in that case we
562
+ // don't want to reset the timer as we still want to wait on original client which started this timer.
563
+ if (
564
+ client !== undefined &&
565
+ this.handler.shouldClientJoinWrite() &&
566
+ !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer
567
+ ) {
568
+ this.prevClientLeftTimer.restart();
569
+ } else {
570
+ // Adding this event temporarily so that we can get help debugging if something goes wrong.
571
+ this.handler.logger.sendTelemetryEvent({
572
+ eventName: "noWaitOnDisconnected",
573
+ details: JSON.stringify({
574
+ clientId: this._clientId,
575
+ inQuorum: client !== undefined,
576
+ waitingForLeaveOp: this.waitingForLeaveOp,
577
+ hadOutstandingOps: this.handler.shouldClientJoinWrite(),
578
+ }),
579
+ });
580
+ }
581
+ }
582
+
583
+ // Report transition before we propagate event across layers
584
+ this.handler.connectionStateChanged(this._connectionState, oldState, reason);
585
+ }
586
+
587
+ // Helper method to switch between quorum and audience.
588
+ // Old design was checking only quorum for "write" clients.
589
+ // Latest change checks audience for all types of connections.
590
+ protected get membership(): IMembership | undefined {
591
+ // We could always use audience here, and in practice it will probably be correct.
592
+ // (including case when this.readClientsWaitForJoinSignal === false).
593
+ // But only if it's superset of quorum, i.e. when filtered to "write" clients, they are always identical!
594
+ // It's safer to assume that we have bugs and engaging kill-bit switch should bring us back to well-known
595
+ // and tested state!
596
+ return this.readClientsWaitForJoinSignal ? this.protocol?.audience : this.protocol?.quorum;
597
+ }
598
+
599
+ public initProtocol(protocol: IProtocolHandler) {
600
+ this.protocol = protocol;
601
+
602
+ this.membership?.on("addMember", (clientId, details) => {
603
+ assert(
604
+ (details as IClient).mode === "read" ||
605
+ protocol.quorum.getMember(clientId) !== undefined,
606
+ 0x4b5 /* Audience is subset of quorum */,
607
+ );
608
+ this.receivedAddMemberEvent(clientId);
609
+ });
610
+
611
+ this.membership?.on("removeMember", (clientId) => {
612
+ assert(
613
+ protocol.quorum.getMember(clientId) === undefined,
614
+ 0x4b6 /* Audience is subset of quorum */,
615
+ );
616
+ this.receivedRemoveMemberEvent(clientId);
617
+ });
618
+
619
+ /* There is a tiny tiny race possible, where these events happen in this order:
541
620
  1. A connection is established (no "cached" mode is used, so it happens in parallel / faster than other steps)
542
621
  2. Some other client produces a summary
543
622
  3. We get "lucky" and load from that summary as our initial snapshot
544
623
  4. ConnectionStateHandler.initProtocol is called, "self" is already in the quorum.
545
624
  We could avoid this sequence (and delete test case for it) if we move connection lower in Container.load()
546
625
  */
547
- if (this.hasMember(this.pendingClientId)) {
548
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
549
- this.receivedAddMemberEvent(this.pendingClientId!);
550
- }
551
-
552
- // if we have a clientId from a previous container we need to wait for its leave message
553
- if (this.clientId !== undefined && this.hasMember(this.clientId)) {
554
- this.prevClientLeftTimer.restart();
555
- }
556
- }
557
-
558
- protected hasMember(clientId?: string) {
559
- return this.membership?.getMember(clientId ?? "") !== undefined;
560
- }
626
+ if (this.hasMember(this.pendingClientId)) {
627
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
628
+ this.receivedAddMemberEvent(this.pendingClientId!);
629
+ }
630
+
631
+ // if we have a clientId from a previous container we need to wait for its leave message
632
+ if (this.clientId !== undefined && this.hasMember(this.clientId)) {
633
+ this.prevClientLeftTimer.restart();
634
+ }
635
+ }
636
+
637
+ protected hasMember(clientId?: string) {
638
+ return this.membership?.getMember(clientId ?? "") !== undefined;
639
+ }
561
640
  }