@fluidframework/container-loader 2.0.0-internal.1.2.0.93071 → 2.0.0-internal.1.2.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 (58) hide show
  1. package/dist/catchUpMonitor.d.ts +6 -17
  2. package/dist/catchUpMonitor.d.ts.map +1 -1
  3. package/dist/catchUpMonitor.js +5 -36
  4. package/dist/catchUpMonitor.js.map +1 -1
  5. package/dist/connectionManager.d.ts.map +1 -1
  6. package/dist/connectionManager.js +3 -6
  7. package/dist/connectionManager.js.map +1 -1
  8. package/dist/connectionStateHandler.d.ts +80 -26
  9. package/dist/connectionStateHandler.d.ts.map +1 -1
  10. package/dist/connectionStateHandler.js +170 -89
  11. package/dist/connectionStateHandler.js.map +1 -1
  12. package/dist/container.d.ts +12 -11
  13. package/dist/container.d.ts.map +1 -1
  14. package/dist/container.js +76 -100
  15. package/dist/container.js.map +1 -1
  16. package/dist/containerStorageAdapter.d.ts +10 -24
  17. package/dist/containerStorageAdapter.d.ts.map +1 -1
  18. package/dist/containerStorageAdapter.js +50 -16
  19. package/dist/containerStorageAdapter.js.map +1 -1
  20. package/dist/deltaManager.js +4 -4
  21. package/dist/deltaManager.js.map +1 -1
  22. package/dist/packageVersion.d.ts +1 -1
  23. package/dist/packageVersion.d.ts.map +1 -1
  24. package/dist/packageVersion.js +1 -1
  25. package/dist/packageVersion.js.map +1 -1
  26. package/lib/catchUpMonitor.d.ts +6 -17
  27. package/lib/catchUpMonitor.d.ts.map +1 -1
  28. package/lib/catchUpMonitor.js +5 -35
  29. package/lib/catchUpMonitor.js.map +1 -1
  30. package/lib/connectionManager.d.ts.map +1 -1
  31. package/lib/connectionManager.js +3 -6
  32. package/lib/connectionManager.js.map +1 -1
  33. package/lib/connectionStateHandler.d.ts +80 -26
  34. package/lib/connectionStateHandler.d.ts.map +1 -1
  35. package/lib/connectionStateHandler.js +170 -90
  36. package/lib/connectionStateHandler.js.map +1 -1
  37. package/lib/container.d.ts +12 -11
  38. package/lib/container.d.ts.map +1 -1
  39. package/lib/container.js +77 -101
  40. package/lib/container.js.map +1 -1
  41. package/lib/containerStorageAdapter.d.ts +10 -24
  42. package/lib/containerStorageAdapter.d.ts.map +1 -1
  43. package/lib/containerStorageAdapter.js +50 -15
  44. package/lib/containerStorageAdapter.js.map +1 -1
  45. package/lib/deltaManager.js +4 -4
  46. package/lib/deltaManager.js.map +1 -1
  47. package/lib/packageVersion.d.ts +1 -1
  48. package/lib/packageVersion.d.ts.map +1 -1
  49. package/lib/packageVersion.js +1 -1
  50. package/lib/packageVersion.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/catchUpMonitor.ts +7 -47
  53. package/src/connectionManager.ts +3 -5
  54. package/src/connectionStateHandler.ts +231 -106
  55. package/src/container.ts +89 -118
  56. package/src/containerStorageAdapter.ts +64 -15
  57. package/src/deltaManager.ts +4 -4
  58. package/src/packageVersion.ts +1 -1
@@ -6,18 +6,20 @@
6
6
  import { ITelemetryLogger, ITelemetryProperties } from "@fluidframework/common-definitions";
7
7
  import { assert, Timer } from "@fluidframework/common-utils";
8
8
  import { IConnectionDetails, IDeltaManager } from "@fluidframework/container-definitions";
9
- import { ILocalSequencedClient, IProtocolHandler } from "@fluidframework/protocol-base";
10
- import { ConnectionMode, IQuorumClients } from "@fluidframework/protocol-definitions";
11
- import { PerformanceEvent } from "@fluidframework/telemetry-utils";
9
+ import { ILocalSequencedClient } from "@fluidframework/protocol-base";
10
+ import { ConnectionMode } from "@fluidframework/protocol-definitions";
11
+ import { PerformanceEvent, loggerToMonitoringContext } from "@fluidframework/telemetry-utils";
12
12
  import { ConnectionState } from "./connectionState";
13
- import { CatchUpMonitor, ICatchUpMonitor, ImmediateCatchUpMonitor } from "./catchUpMonitor";
13
+ import { CatchUpMonitor, ICatchUpMonitor } from "./catchUpMonitor";
14
+ import { IProtocolHandler } from "./protocol";
15
+
16
+ const JoinOpTimeoutMs = 45000;
14
17
 
15
18
  /** Constructor parameter type for passing in dependencies needed by the ConnectionStateHandler */
16
19
  export interface IConnectionStateHandlerInputs {
17
- /** Provides access to the clients currently in the quorum */
18
- quorumClients: () => IQuorumClients | undefined;
20
+ logger: ITelemetryLogger;
19
21
  /** Log to telemetry any change in state, included to Connecting */
20
- logConnectionStateChangeTelemetry:
22
+ connectionStateChanged:
21
23
  (value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) => void;
22
24
  /** Whether to expect the client to join in write mode on next connection */
23
25
  shouldClientJoinWrite: () => boolean;
@@ -25,11 +27,155 @@ export interface IConnectionStateHandlerInputs {
25
27
  maxClientLeaveWaitTime: number | undefined;
26
28
  /** Log an issue encountered while in the Connecting state. details will be logged as a JSON string */
27
29
  logConnectionIssue: (eventName: string, details?: ITelemetryProperties) => void;
28
- /** Callback whenever the ConnectionState changes between Disconnected and Connected */
29
- connectionStateChanged: () => void;
30
30
  }
31
31
 
32
- const JoinOpTimeoutMs = 45000;
32
+ /**
33
+ * interface that connection state handler implements
34
+ */
35
+ export interface IConnectionStateHandler {
36
+ readonly connectionState: ConnectionState;
37
+ readonly pendingClientId: string | undefined;
38
+
39
+ containerSaved(): void;
40
+ dispose(): void;
41
+ initProtocol(protocol: IProtocolHandler): void;
42
+ receivedConnectEvent(connectionMode: ConnectionMode, details: IConnectionDetails): void;
43
+ receivedDisconnectEvent(reason: string): void;
44
+ }
45
+
46
+ export function createConnectionStateHandler(
47
+ inputs: IConnectionStateHandlerInputs,
48
+ deltaManager: IDeltaManager<any, any>,
49
+ clientId?: string,
50
+ ) {
51
+ const mc = loggerToMonitoringContext(inputs.logger);
52
+ return createConnectionStateHandlerCore(
53
+ mc.config.getBoolean("Fluid.Container.CatchUpBeforeDeclaringConnected") === true,
54
+ inputs,
55
+ deltaManager,
56
+ clientId,
57
+ );
58
+ }
59
+
60
+ export function createConnectionStateHandlerCore(
61
+ wait: boolean,
62
+ inputs: IConnectionStateHandlerInputs,
63
+ deltaManager: IDeltaManager<any, any>,
64
+ clientId?: string,
65
+ ) {
66
+ if (!wait) {
67
+ return new ConnectionStateHandler(inputs, clientId);
68
+ }
69
+ return new ConnectionStateCatchup(
70
+ inputs,
71
+ (handler: IConnectionStateHandlerInputs) => new ConnectionStateHandler(handler, clientId),
72
+ deltaManager);
73
+ }
74
+
75
+ /**
76
+ * Class that can be used as a base class for building IConnectionStateHandler adapters / pipeline.
77
+ * It implements both ends of communication interfaces and passes data back and forward
78
+ */
79
+ class ConnectionStateHandlerPassThrough implements IConnectionStateHandler, IConnectionStateHandlerInputs {
80
+ protected readonly pimpl: IConnectionStateHandler;
81
+
82
+ constructor(
83
+ protected readonly inputs: IConnectionStateHandlerInputs,
84
+ pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
85
+ ) {
86
+ this.pimpl = pimplFactory(this);
87
+ }
88
+
89
+ /**
90
+ * IConnectionStateHandler
91
+ */
92
+ public get connectionState() { return this.pimpl.connectionState; }
93
+ public get pendingClientId() { return this.pimpl.pendingClientId; }
94
+
95
+ public containerSaved() { return this.pimpl.containerSaved(); }
96
+ public dispose() { return this.pimpl.dispose(); }
97
+ public initProtocol(protocol: IProtocolHandler) { return this.pimpl.initProtocol(protocol); }
98
+ public receivedDisconnectEvent(reason: string) { return this.pimpl.receivedDisconnectEvent(reason); }
99
+
100
+ public receivedConnectEvent(connectionMode: ConnectionMode, details: IConnectionDetails) {
101
+ return this.pimpl.receivedConnectEvent(connectionMode, details);
102
+ }
103
+
104
+ /**
105
+ * IConnectionStateHandlerInputs
106
+ */
107
+
108
+ public get logger() { return this.inputs.logger; }
109
+ public connectionStateChanged(
110
+ value: ConnectionState,
111
+ oldState: ConnectionState,
112
+ reason?: string | undefined,
113
+ ) {
114
+ return this.inputs.connectionStateChanged(value, oldState, reason);
115
+ }
116
+ public shouldClientJoinWrite() { return this.inputs.shouldClientJoinWrite(); }
117
+ public get maxClientLeaveWaitTime() { return this.inputs.maxClientLeaveWaitTime; }
118
+ public logConnectionIssue(eventName: string, details?: ITelemetryProperties) {
119
+ return this.inputs.logConnectionIssue(eventName, details);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Implementation of IConnectionStateHandler pass-through adapter that waits for specific sequence number
125
+ * before raising connected event
126
+ */
127
+ class ConnectionStateCatchup extends ConnectionStateHandlerPassThrough {
128
+ private catchUpMonitor: ICatchUpMonitor | undefined;
129
+
130
+ constructor(
131
+ inputs: IConnectionStateHandlerInputs,
132
+ pimplFactory: (handler: IConnectionStateHandlerInputs) => IConnectionStateHandler,
133
+ private readonly deltaManager: IDeltaManager<any, any>,
134
+ ) {
135
+ super(inputs, pimplFactory);
136
+ this._connectionState = this.pimpl.connectionState;
137
+ }
138
+
139
+ private _connectionState: ConnectionState;
140
+ public get connectionState() {
141
+ return this._connectionState;
142
+ }
143
+
144
+ public connectionStateChanged(value: ConnectionState, oldState: ConnectionState, reason?: string | undefined) {
145
+ switch (value) {
146
+ case ConnectionState.Connected:
147
+ assert(this._connectionState === ConnectionState.CatchingUp, 0x3e1 /* connectivity transitions */);
148
+ // Create catch-up monitor here (not earlier), as we might get more exact info by now about how far
149
+ // client is behind through join signal. This is only true if base layer uses signals (i.e. audience,
150
+ // not quorum, including for "rea" connections) to make decisions about moving to "connected" state.
151
+ // In addition to that, in its current form, doing this in ConnectionState.CatchingUp is dangerous as
152
+ // we might get callback right away, and it will screw up state transition (as code outside of switch
153
+ // statement will overwrite current state).
154
+ assert(this.catchUpMonitor === undefined, 0x3eb /* catchUpMonitor should be gone */);
155
+ this.catchUpMonitor = new CatchUpMonitor(this.deltaManager, this.transitionToConnectedState);
156
+ return;
157
+ case ConnectionState.Disconnected:
158
+ this.catchUpMonitor?.dispose();
159
+ this.catchUpMonitor = undefined;
160
+ break;
161
+ case ConnectionState.CatchingUp:
162
+ assert(this._connectionState === ConnectionState.Disconnected, 0x3e3 /* connectivity transitions */);
163
+ break;
164
+ default:
165
+ }
166
+ this._connectionState = value;
167
+ this.inputs.connectionStateChanged(value, oldState, reason);
168
+ }
169
+
170
+ private readonly transitionToConnectedState = () => {
171
+ // Defensive measure, we should always be in Connecting state when this is called.
172
+ const state = this.pimpl.connectionState;
173
+ assert(state === ConnectionState.Connected, 0x3e5 /* invariant broken */);
174
+ assert(this._connectionState === ConnectionState.CatchingUp, 0x3e6 /* invariant broken */);
175
+ this._connectionState = ConnectionState.Connected;
176
+ this.inputs.connectionStateChanged(ConnectionState.Connected, ConnectionState.CatchingUp, "caught up");
177
+ };
178
+ }
33
179
 
34
180
  /**
35
181
  * In the lifetime of a container, the connection will likely disconnect and reconnect periodically.
@@ -43,24 +189,29 @@ const JoinOpTimeoutMs = 45000;
43
189
  *
44
190
  * The job of this class is to encapsulate the transition period during reconnect, which is identified by
45
191
  * ConnectionState.CatchingUp. Specifically, before moving to Connected state with the new clientId, it ensures that:
46
- * (A) We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
47
- * that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
48
- * pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
49
- * (B) We process the Join op for the new clientId (identified when the underlying connection was first established),
50
- * indicating the service is ready to sequence ops sent with the new clientId.
51
- * (C) We process all ops known at the time the underlying connection was established (so we are "caught up")
52
192
  *
53
- * For (A) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
54
- * For (B) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed
193
+ * a. We process the Leave op for the previous clientId. This allows us to properly handle any acks from in-flight ops
194
+ * that got sequenced with the old clientId (we'll recognize them as local ops). After the Leave op, any other
195
+ * pending ops can safely be submitted with the new clientId without fear of duplication in the sequenced op stream.
196
+ *
197
+ * b. We process the Join op for the new clientId (identified when the underlying connection was first established),
198
+ * indicating the service is ready to sequence ops sent with the new clientId.
199
+ *
200
+ * c. We process all ops known at the time the underlying connection was established (so we are "caught up")
201
+ *
202
+ * For (a) we give up waiting after some time (same timeout as server uses), and go ahead and transition to Connected.
203
+ *
204
+ * For (b) we log telemetry if it takes too long, but still only transition to Connected when the Join op is processed
55
205
  * and we are added to the Quorum.
56
- * For (C) this is optional behavior, controlled by the parameters of receivedConnectEvent
206
+ *
207
+ * For (c) this is optional behavior, controlled by the parameters of receivedConnectEvent
57
208
  */
58
- export class ConnectionStateHandler {
209
+ class ConnectionStateHandler implements IConnectionStateHandler {
59
210
  private _connectionState = ConnectionState.Disconnected;
60
211
  private _pendingClientId: string | undefined;
61
- private catchUpMonitor: ICatchUpMonitor | undefined;
62
212
  private readonly prevClientLeftTimer: Timer;
63
213
  private readonly joinOpTimer: Timer;
214
+ private protocol?: IProtocolHandler;
64
215
 
65
216
  private waitEvent: PerformanceEvent | undefined;
66
217
 
@@ -68,11 +219,7 @@ export class ConnectionStateHandler {
68
219
  return this._connectionState;
69
220
  }
70
221
 
71
- public get connected(): boolean {
72
- return this.connectionState === ConnectionState.Connected;
73
- }
74
-
75
- public get clientId(): string | undefined {
222
+ private get clientId(): string | undefined {
76
223
  return this._clientId;
77
224
  }
78
225
 
@@ -82,7 +229,6 @@ export class ConnectionStateHandler {
82
229
 
83
230
  constructor(
84
231
  private readonly handler: IConnectionStateHandlerInputs,
85
- private readonly logger: ITelemetryLogger,
86
232
  private _clientId?: string,
87
233
  ) {
88
234
  this.prevClientLeftTimer = new Timer(
@@ -90,7 +236,7 @@ export class ConnectionStateHandler {
90
236
  // the max time on server after which leave op is sent.
91
237
  this.handler.maxClientLeaveWaitTime ?? 300000,
92
238
  () => {
93
- assert(!this.connected,
239
+ assert(this.connectionState !== ConnectionState.Connected,
94
240
  0x2ac /* "Connected when timeout waiting for leave from previous session fired!" */);
95
241
  this.applyForConnectedState("timeout");
96
242
  },
@@ -107,11 +253,10 @@ export class ConnectionStateHandler {
107
253
  if (this.connectionState !== ConnectionState.CatchingUp) {
108
254
  return;
109
255
  }
110
- const quorumClients = this.handler.quorumClients();
111
256
  const details = {
112
- quorumInitialized: quorumClients !== undefined,
257
+ protocolInitialized: this.protocol !== undefined,
113
258
  pendingClientId: this.pendingClientId,
114
- inQuorum: quorumClients?.getMember(this.pendingClientId ?? "") !== undefined,
259
+ clientJoined: this.hasMember(this.pendingClientId),
115
260
  waitingForLeaveOp: this.waitingForLeaveOp,
116
261
  };
117
262
  this.handler.logConnectionIssue("NoJoinOp", details);
@@ -159,7 +304,7 @@ export class ConnectionStateHandler {
159
304
  }
160
305
  // Start the event in case we are waiting for leave or timeout.
161
306
  if (this.waitingForLeaveOp) {
162
- this.waitEvent = PerformanceEvent.start(this.logger, {
307
+ this.waitEvent = PerformanceEvent.start(this.handler.logger, {
163
308
  eventName: "WaitBeforeClientLeave",
164
309
  details: JSON.stringify({
165
310
  waitOnClientId: this._clientId,
@@ -172,29 +317,23 @@ export class ConnectionStateHandler {
172
317
  }
173
318
 
174
319
  private applyForConnectedState(source: "removeMemberEvent" | "addMemberEvent" | "timeout" | "containerSaved") {
175
- const quorumClients = this.handler.quorumClients();
176
- assert(quorumClients !== undefined, 0x236 /* "In all cases it should be already installed" */);
320
+ assert(this.protocol !== undefined, 0x236 /* "In all cases it should be already installed" */);
177
321
 
178
- assert(this.waitingForLeaveOp === false ||
179
- (this.clientId !== undefined && quorumClients.getMember(this.clientId) !== undefined),
322
+ assert(!this.waitingForLeaveOp || this.hasMember(this.clientId),
180
323
  0x2e2 /* "Must only wait for leave message when clientId in quorum" */);
181
324
 
182
325
  // Move to connected state only if we are in Connecting state, we have seen our join op
183
326
  // and there is no timer running which means we are not waiting for previous client to leave
184
327
  // or timeout has occurred while doing so.
185
328
  if (this.pendingClientId !== this.clientId
186
- && this.pendingClientId !== undefined
187
- && quorumClients.getMember(this.pendingClientId) !== undefined
329
+ && this.hasMember(this.pendingClientId)
188
330
  && !this.waitingForLeaveOp
189
331
  ) {
190
332
  this.waitEvent?.end({ source });
191
-
192
- assert(this.catchUpMonitor !== undefined,
193
- 0x37d /* catchUpMonitor should always be set if pendingClientId is set */);
194
- this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
333
+ this.setConnectionState(ConnectionState.Connected);
195
334
  } else {
196
335
  // Adding this event temporarily so that we can get help debugging if something goes wrong.
197
- this.logger.sendTelemetryEvent({
336
+ this.handler.logger.sendTelemetryEvent({
198
337
  eventName: "connectedStateRejected",
199
338
  category: source === "timeout" ? "error" : "generic",
200
339
  details: JSON.stringify({
@@ -202,7 +341,7 @@ export class ConnectionStateHandler {
202
341
  pendingClientId: this.pendingClientId,
203
342
  clientId: this.clientId,
204
343
  waitingForLeaveOp: this.waitingForLeaveOp,
205
- inQuorum: quorumClients?.getMember(this.pendingClientId ?? "") !== undefined,
344
+ clientJoined: this.hasMember(this.pendingClientId),
206
345
  }),
207
346
  });
208
347
  }
@@ -220,18 +359,6 @@ export class ConnectionStateHandler {
220
359
  this.setConnectionState(ConnectionState.Disconnected, reason);
221
360
  }
222
361
 
223
- private readonly transitionToConnectedState = () => {
224
- // Defensive measure, we should always be in CatchingUp state when this is called.
225
- if (this._connectionState === ConnectionState.CatchingUp) {
226
- this.setConnectionState(ConnectionState.Connected);
227
- } else {
228
- this.logger.sendTelemetryEvent({
229
- eventName: "cannotTransitionToConnectedState",
230
- connectionState: ConnectionState[this._connectionState],
231
- });
232
- }
233
- };
234
-
235
362
  /**
236
363
  * The "connect" event indicates the connection to the Relay Service is live.
237
364
  * However, some additional conditions must be met before we can fully transition to
@@ -244,23 +371,18 @@ export class ConnectionStateHandler {
244
371
  public receivedConnectEvent(
245
372
  connectionMode: ConnectionMode,
246
373
  details: IConnectionDetails,
247
- deltaManager?: IDeltaManager<any, any>,
248
374
  ) {
249
375
  const oldState = this._connectionState;
250
376
  this._connectionState = ConnectionState.CatchingUp;
251
377
 
252
378
  const writeConnection = connectionMode === "write";
253
- assert(!this.handler.shouldClientJoinWrite() || writeConnection,
254
- 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
255
- assert(!this.waitingForLeaveOp || writeConnection,
256
- 0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
257
379
 
258
- // Defensive measure in case catchUpMonitor from previous connection attempt wasn't already cleared
259
- this.catchUpMonitor?.dispose();
260
-
261
- // Note that this may be undefined since the connection is established proactively on load
262
- // and the quorum may still be under initialization.
263
- const quorumClients: IQuorumClients | undefined = this.handler.quorumClients();
380
+ // The following checks are wrong. They are only valid if user has write access to a file.
381
+ // If user lost such access mid-session, user will not be able to get "write" connection.
382
+ // assert(!this.handler.shouldClientJoinWrite() || writeConnection,
383
+ // 0x30a /* shouldClientJoinWrite should imply this is a writeConnection */);
384
+ // assert(!this.waitingForLeaveOp || writeConnection,
385
+ // 0x2a6 /* "waitingForLeaveOp should imply writeConnection (we need to be ready to flush pending ops)" */);
264
386
 
265
387
  // Stash the clientID to detect when transitioning from connecting (socket.io channel open) to connected
266
388
  // (have received the join message for the client ID)
@@ -270,20 +392,15 @@ export class ConnectionStateHandler {
270
392
  // we know there can no longer be outstanding ops that we sent with the previous client id.
271
393
  this._pendingClientId = details.clientId;
272
394
 
273
- // We may want to catch up to known ops as of now before transitioning to Connected state
274
- this.catchUpMonitor = deltaManager !== undefined
275
- ? new CatchUpMonitor(deltaManager)
276
- : new ImmediateCatchUpMonitor();
277
-
278
395
  // IMPORTANT: Report telemetry after we set _pendingClientId, but before transitioning to Connected state
279
- this.handler.logConnectionStateChangeTelemetry(ConnectionState.CatchingUp, oldState);
396
+ this.handler.connectionStateChanged(ConnectionState.CatchingUp, oldState);
280
397
 
281
398
  // For write connections, this pending clientId could be in the quorum already (i.e. join op already processed).
282
399
  // We are fetching ops from storage in parallel to connecting to Relay Service,
283
400
  // and given async processes, it's possible that we have already processed our own join message before
284
401
  // connection was fully established.
285
- // If quorumClients itself is undefined, we expect it will process the join op after it's initialized.
286
- const waitingForJoinOp = writeConnection && quorumClients?.getMember(this._pendingClientId) === undefined;
402
+ // If protocol is not initialized yet, we expect it will process the join op after it's initialized.
403
+ const waitingForJoinOp = writeConnection && !this.hasMember(this._pendingClientId);
287
404
 
288
405
  if (waitingForJoinOp) {
289
406
  // Previous client left, and we are waiting for our own join op. When it is processed we'll join the quorum
@@ -291,39 +408,27 @@ export class ConnectionStateHandler {
291
408
  this.startJoinOpTimer();
292
409
  } else if (!this.waitingForLeaveOp) {
293
410
  // We're not waiting for Join or Leave op (if read-only connection those don't even apply),
294
- // but we do need to wait until we are caught up (to now-known ops) before transitioning to Connected state.
295
- this.catchUpMonitor.on("caughtUp", this.transitionToConnectedState);
411
+ // go ahead and declare the state to be Connected!
412
+ // If we are waiting for Leave op still, do nothing for now, we will transition to Connected later.
413
+ this.setConnectionState(ConnectionState.Connected);
296
414
  }
297
415
  // else - We are waiting for Leave op still, do nothing for now, we will transition to Connected later
298
416
  }
299
417
 
300
- /** Clear all the state used during the Connecting phase (set in receivedConnectEvent) */
301
- private clearPendingConnectionState() {
302
- this._pendingClientId = undefined;
303
-
304
- this.catchUpMonitor?.dispose();
305
- this.catchUpMonitor = undefined;
306
-
307
- if (this.joinOpTimer.hasTimer) {
308
- this.stopJoinOpTimer();
309
- }
310
- }
311
-
312
418
  private setConnectionState(value: ConnectionState.Disconnected, reason: string): void;
313
419
  private setConnectionState(value: ConnectionState.Connected): void;
314
- private setConnectionState(value: ConnectionState, reason?: string): void {
420
+ private setConnectionState(value: ConnectionState.Disconnected | ConnectionState.Connected, reason?: string): void {
315
421
  if (this.connectionState === value) {
316
422
  // Already in the desired state - exit early
317
- this.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
423
+ this.handler.logger.sendErrorEvent({ eventName: "setConnectionStateSame", value });
318
424
  return;
319
425
  }
320
426
 
321
427
  const oldState = this._connectionState;
322
428
  this._connectionState = value;
323
- const quorumClients = this.handler.quorumClients();
324
429
  let client: ILocalSequencedClient | undefined;
325
430
  if (this._clientId !== undefined) {
326
- client = quorumClients?.getMember(this._clientId);
431
+ client = this.protocol?.quorum?.getMember(this._clientId);
327
432
  }
328
433
  if (value === ConnectionState.Connected) {
329
434
  assert(oldState === ConnectionState.CatchingUp,
@@ -335,24 +440,27 @@ export class ConnectionStateHandler {
335
440
  this._clientId = this.pendingClientId;
336
441
  } else if (value === ConnectionState.Disconnected) {
337
442
  // Clear pending state immediately to prepare for reconnect
338
- this.clearPendingConnectionState();
443
+ this._pendingClientId = undefined;
339
444
 
340
- // Only wait for "leave" message if the connected client exists in the quorum because only the write
341
- // client will exist in the quorum and only for those clients we will receive "removeMember" event and
342
- // the client has some unacked ops.
343
- // Also server would not accept ops from read client. Also check if the timer is not already running as
445
+ if (this.joinOpTimer.hasTimer) {
446
+ this.stopJoinOpTimer();
447
+ }
448
+
449
+ // Only wait for "leave" message if the connected client exists in the quorum and had some non-acked ops
450
+ // Also check if the timer is not already running as
344
451
  // we could receive "Disconnected" event multiple times without getting connected and in that case we
345
452
  // don't want to reset the timer as we still want to wait on original client which started this timer.
346
453
  if (client !== undefined
347
454
  && this.handler.shouldClientJoinWrite()
348
- && this.prevClientLeftTimer.hasTimer === false
455
+ && !this.waitingForLeaveOp // same as !this.prevClientLeftTimer.hasTimer
349
456
  ) {
350
457
  this.prevClientLeftTimer.restart();
351
458
  } else {
352
459
  // Adding this event temporarily so that we can get help debugging if something goes wrong.
353
- this.logger.sendTelemetryEvent({
460
+ this.handler.logger.sendTelemetryEvent({
354
461
  eventName: "noWaitOnDisconnected",
355
462
  details: JSON.stringify({
463
+ clientId: this._clientId,
356
464
  inQuorum: client !== undefined,
357
465
  waitingForLeaveOp: this.waitingForLeaveOp,
358
466
  hadOutstandingOps: this.handler.shouldClientJoinWrite(),
@@ -362,24 +470,41 @@ export class ConnectionStateHandler {
362
470
  }
363
471
 
364
472
  // Report transition before we propagate event across layers
365
- this.handler.logConnectionStateChangeTelemetry(this._connectionState, oldState, reason);
473
+ this.handler.connectionStateChanged(this._connectionState, oldState, reason);
474
+ }
366
475
 
367
- // Propagate event across layers
368
- this.handler.connectionStateChanged();
476
+ // Helper method to switch between quorum and audience.
477
+ // Old design was checking only quorum for "write" clients.
478
+ // Latest change checks audience for all types of connections.
479
+ protected get membership() {
480
+ return this.protocol?.quorum;
369
481
  }
370
482
 
371
483
  public initProtocol(protocol: IProtocolHandler) {
372
- protocol.quorum.on("addMember", (clientId, _details) => {
484
+ this.protocol = protocol;
485
+
486
+ this.membership?.on("addMember", (clientId) => {
373
487
  this.receivedAddMemberEvent(clientId);
374
488
  });
375
489
 
376
- protocol.quorum.on("removeMember", (clientId) => {
490
+ this.membership?.on("removeMember", (clientId) => {
377
491
  this.receivedRemoveMemberEvent(clientId);
378
492
  });
379
493
 
494
+ // Very unlikely race condition, but theoretically can happen - our new connection is already
495
+ // summarized and we are loading from such summary.
496
+ if (this.hasMember(this.pendingClientId)) {
497
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
498
+ this.receivedAddMemberEvent(this.pendingClientId!);
499
+ }
500
+
380
501
  // if we have a clientId from a previous container we need to wait for its leave message
381
- if (this.clientId !== undefined && protocol.quorum.getMember(this.clientId) !== undefined) {
502
+ if (this.clientId !== undefined && this.hasMember(this.clientId)) {
382
503
  this.prevClientLeftTimer.restart();
383
504
  }
384
505
  }
506
+
507
+ protected hasMember(clientId?: string) {
508
+ return this.membership?.getMember(clientId ?? "") !== undefined;
509
+ }
385
510
  }