@fluidframework/test-utils 2.0.0-dev.2.3.0.115467 → 2.0.0-dev.4.1.0.148229

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 (68) hide show
  1. package/.eslintrc.js +8 -10
  2. package/README.md +41 -11
  3. package/api-extractor.json +2 -2
  4. package/dist/DriverWrappers.d.ts.map +1 -1
  5. package/dist/DriverWrappers.js.map +1 -1
  6. package/dist/TestConfigs.d.ts.map +1 -1
  7. package/dist/TestConfigs.js +3 -4
  8. package/dist/TestConfigs.js.map +1 -1
  9. package/dist/TestSummaryUtils.d.ts +20 -4
  10. package/dist/TestSummaryUtils.d.ts.map +1 -1
  11. package/dist/TestSummaryUtils.js +41 -33
  12. package/dist/TestSummaryUtils.js.map +1 -1
  13. package/dist/containerUtils.d.ts +29 -0
  14. package/dist/containerUtils.d.ts.map +1 -0
  15. package/dist/containerUtils.js +45 -0
  16. package/dist/containerUtils.js.map +1 -0
  17. package/dist/index.d.ts +5 -4
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/interfaces.d.ts.map +1 -1
  22. package/dist/interfaces.js.map +1 -1
  23. package/dist/loaderContainerTracker.d.ts +3 -9
  24. package/dist/loaderContainerTracker.d.ts.map +1 -1
  25. package/dist/loaderContainerTracker.js +45 -47
  26. package/dist/loaderContainerTracker.js.map +1 -1
  27. package/dist/localCodeLoader.d.ts.map +1 -1
  28. package/dist/localCodeLoader.js.map +1 -1
  29. package/dist/localLoader.d.ts.map +1 -1
  30. package/dist/localLoader.js.map +1 -1
  31. package/dist/packageVersion.d.ts +1 -1
  32. package/dist/packageVersion.js +1 -1
  33. package/dist/packageVersion.js.map +1 -1
  34. package/dist/retry.d.ts.map +1 -1
  35. package/dist/retry.js.map +1 -1
  36. package/dist/testContainerRuntimeFactory.d.ts +2 -2
  37. package/dist/testContainerRuntimeFactory.d.ts.map +1 -1
  38. package/dist/testContainerRuntimeFactory.js +3 -3
  39. package/dist/testContainerRuntimeFactory.js.map +1 -1
  40. package/dist/testFluidObject.d.ts.map +1 -1
  41. package/dist/testFluidObject.js +7 -3
  42. package/dist/testFluidObject.js.map +1 -1
  43. package/dist/testObjectProvider.d.ts +6 -1
  44. package/dist/testObjectProvider.d.ts.map +1 -1
  45. package/dist/testObjectProvider.js +49 -16
  46. package/dist/testObjectProvider.js.map +1 -1
  47. package/dist/timeoutUtils.d.ts +11 -2
  48. package/dist/timeoutUtils.d.ts.map +1 -1
  49. package/dist/timeoutUtils.js +122 -18
  50. package/dist/timeoutUtils.js.map +1 -1
  51. package/package.json +65 -58
  52. package/prettier.config.cjs +1 -1
  53. package/src/DriverWrappers.ts +40 -37
  54. package/src/TestConfigs.ts +7 -7
  55. package/src/TestSummaryUtils.ts +123 -124
  56. package/src/containerUtils.ts +48 -0
  57. package/src/index.ts +24 -23
  58. package/src/interfaces.ts +10 -7
  59. package/src/loaderContainerTracker.ts +618 -577
  60. package/src/localCodeLoader.ts +85 -77
  61. package/src/localLoader.ts +24 -24
  62. package/src/packageVersion.ts +1 -1
  63. package/src/retry.ts +31 -25
  64. package/src/testContainerRuntimeFactory.ts +59 -56
  65. package/src/testFluidObject.ts +168 -152
  66. package/src/testObjectProvider.ts +477 -384
  67. package/src/timeoutUtils.ts +191 -41
  68. package/tsconfig.json +9 -12
@@ -2,13 +2,15 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- /* eslint-disable @typescript-eslint/strict-boolean-expressions */
6
-
7
5
  import { assert } from "@fluidframework/common-utils";
8
6
  import { IContainer, IDeltaQueue, IHostLoader } from "@fluidframework/container-definitions";
9
- import { Container } from "@fluidframework/container-loader";
7
+ import { ConnectionState } from "@fluidframework/container-loader";
10
8
  import { canBeCoalescedByService } from "@fluidframework/driver-utils";
11
- import { IDocumentMessage, ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
9
+ import {
10
+ IDocumentMessage,
11
+ ISequencedDocumentMessage,
12
+ MessageType,
13
+ } from "@fluidframework/protocol-definitions";
12
14
  import { debug } from "./debug";
13
15
  import { IOpProcessingController } from "./testObjectProvider";
14
16
  import { timeoutAwait, timeoutPromise } from "./timeoutUtils";
@@ -16,584 +18,623 @@ import { timeoutAwait, timeoutPromise } from "./timeoutUtils";
16
18
  const debugOp = debug.extend("ops");
17
19
  const debugWait = debug.extend("wait");
18
20
 
19
- // set the maximum timeout value as 5 mins
20
- const defaultMaxTimeout = 5 * 6000;
21
-
22
21
  interface ContainerRecord {
23
- // A short number for debug output
24
- index: number;
22
+ // A short number for debug output
23
+ index: number;
25
24
 
26
- // LoaderContainerTracker paused state
27
- paused: boolean;
25
+ // LoaderContainerTracker paused state
26
+ paused: boolean;
28
27
 
29
- // Tracking trailing no-op that may or may be acked by the server so we can discount them
30
- // See issue #5629
31
- startTrailingNoOps: number;
32
- trailingNoOps: number;
28
+ // Tracking trailing no-op that may or may be acked by the server so we can discount them
29
+ // See issue #5629
30
+ startTrailingNoOps: number;
31
+ trailingNoOps: number;
33
32
 
34
- // Track last proposal to ensure no unresolved proposal
35
- lastProposal: number;
33
+ // Track last proposal to ensure no unresolved proposal
34
+ lastProposal: number;
36
35
  }
37
36
 
38
37
  export class LoaderContainerTracker implements IOpProcessingController {
39
- private readonly containers = new Map<IContainer, ContainerRecord>();
40
- private lastProposalSeqNum: number = 0;
41
-
42
- constructor(private readonly syncSummarizerClients: boolean = false) {}
43
-
44
- /**
45
- * Add a loader to start to track any container created from them
46
- * @param loader - loader to start tracking any container created.
47
- */
48
- public add<LoaderType extends IHostLoader>(loader: LoaderType) {
49
- // TODO: Expose Loader API to able to intercept container creation (See issue #5114)
50
- const patch = <T, C extends IContainer>(fn: (...args) => Promise<C>) => {
51
- const boundFn = fn.bind(loader);
52
- return async (...args: T[]) => {
53
- const container = await boundFn(...args);
54
- this.addContainer(container);
55
- return container;
56
- };
57
- };
58
- /* eslint-disable @typescript-eslint/unbound-method */
59
- loader.resolve = patch(loader.resolve);
60
- loader.createDetachedContainer = patch(loader.createDetachedContainer);
61
- loader.rehydrateDetachedContainerFromSnapshot = patch(loader.rehydrateDetachedContainerFromSnapshot);
62
- /* eslint-enable @typescript-eslint/unbound-method */
63
- }
64
-
65
- /**
66
- * Utility function to add container to be tracked.
67
- *
68
- * @param container - container to add
69
- */
70
- private addContainer(container: IContainer) {
71
- // ignore summarizer
72
- if (!container.deltaManager.clientDetails.capabilities.interactive && !this.syncSummarizerClients) { return; }
73
-
74
- // don't add container that is already tracked
75
- if (this.containers.has(container)) { return; }
76
-
77
- const record = {
78
- index: this.containers.size,
79
- paused: false,
80
- startTrailingNoOps: 0,
81
- trailingNoOps: 0,
82
- lastProposal: 0,
83
- };
84
- this.containers.set(container, record);
85
- this.trackTrailingNoOps(container, record);
86
- this.trackLastProposal(container);
87
- this.setupTrace(container, record.index);
88
- }
89
-
90
- /**
91
- * Keep track of the trailing NoOp that was sent so we can discount them in the clientSequenceNumber tracking.
92
- * The server might coalesce them with other ops, or a single NoOp, or delay it if it don't think it is necessary.
93
- *
94
- * @param container - the container to track
95
- * @param record - the record to update the trailing op information
96
- */
97
- private trackTrailingNoOps(container: IContainer, record: ContainerRecord) {
98
- container.deltaManager.outbound.on("op", (messages) => {
99
- for (const msg of messages) {
100
- if (canBeCoalescedByService(msg)) {
101
- // Track the NoOp that was sent.
102
- if (record.trailingNoOps === 0) {
103
- // record the starting sequence number of the trailing no ops if we haven't been tracking yet.
104
- record.startTrailingNoOps = msg.clientSequenceNumber;
105
- }
106
- record.trailingNoOps++;
107
- } else {
108
- // Other ops has been sent. We would like to see those ack'ed, so no more need to track NoOps
109
- record.trailingNoOps = 0;
110
- }
111
- }
112
- });
113
-
114
- container.deltaManager.inbound.on("push", (message) => {
115
- // Received the no op back, update the record if we are tracking
116
- if (canBeCoalescedByService(message)
117
- && message.clientId === (container as Container).clientId
118
- && record.trailingNoOps !== 0
119
- && record.startTrailingNoOps <= message.clientSequenceNumber
120
- ) {
121
- // NoOp might have coalesced and skipped ahead some sequence number
122
- // update the record and skip ahead as well
123
- const oldStartTrailingNoOps = record.startTrailingNoOps;
124
- record.startTrailingNoOps = message.clientSequenceNumber + 1;
125
- record.trailingNoOps -= (record.startTrailingNoOps - oldStartTrailingNoOps);
126
- }
127
- });
128
-
129
- container.on("disconnected", () => {
130
- // reset on disconnect.
131
- record.trailingNoOps = 0;
132
- });
133
- }
134
-
135
- private trackLastProposal(container: IContainer) {
136
- container.on("codeDetailsProposed", (value, proposal) => {
137
- if (proposal.sequenceNumber > this.lastProposalSeqNum) {
138
- this.lastProposalSeqNum = proposal.sequenceNumber;
139
- }
140
- });
141
- }
142
-
143
- /**
144
- * Reset the tracker, closing all containers and stop tracking them.
145
- */
146
- public reset() {
147
- this.lastProposalSeqNum = 0;
148
- for (const container of this.containers.keys()) {
149
- container.close();
150
- }
151
- this.containers.clear();
152
-
153
- // REVIEW: do we need to unpatch the loaders?
154
- }
155
-
156
- /**
157
- * Ensure all tracked containers are synchronized
158
- */
159
- public async ensureSynchronized(...containers: IContainer[]): Promise<void> {
160
- await this.processSynchronized(undefined, ...containers);
161
- }
162
-
163
- /**
164
- * Ensure all tracked containers are synchronized with a time limit
165
- */
166
- public async ensureSynchronizedWithTimeout?(timeoutDuration: number | undefined, ...containers: IContainer[]) {
167
- await this.processSynchronized(timeoutDuration, ...containers);
168
- }
169
-
170
- /**
171
- * Make sure all the tracked containers are synchronized.
172
- *
173
- * No isDirty (non-readonly) containers
174
- *
175
- * No extra clientId in quorum of any container that is not tracked and still opened.
176
- *
177
- * - i.e. no pending Join/Leave message.
178
- *
179
- * No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
180
- *
181
- * lastSequenceNumber of all container is the same
182
- *
183
- * clientSequenceNumberObserved is the same as clientSequenceNumber sent
184
- *
185
- * - this overlaps with !isDirty, but include task scheduler ops.
186
- *
187
- * - Trailing NoOp is tracked and don't count as pending ops.
188
- */
189
- private async processSynchronized(timeoutDuration: number | undefined, ...containers: IContainer[]) {
190
- const start = Date.now();
191
- const resumed = this.resumeProcessing(...containers);
192
-
193
- let waitingSequenceNumberSynchronized = false;
194
- // eslint-disable-next-line no-constant-condition
195
- while (true) {
196
- const containersToApply = this.getContainers(containers);
197
- if (containersToApply.length === 0) { break; }
198
-
199
- // Ignore readonly dirty containers, because it can't sent up and nothing can be done about it being dirty
200
- const dirtyContainers = containersToApply.filter((c) => {
201
- const { deltaManager, isDirty } = c;
202
- return deltaManager.readOnlyInfo.readonly !== true && isDirty;
203
- });
204
- if (dirtyContainers.length === 0) {
205
- // Wait for all the leave messages
206
- const pendingClients = this.getPendingClients(containersToApply);
207
- if (pendingClients.length === 0) {
208
- if (this.isSequenceNumberSynchronized(containersToApply)) {
209
- // done, we are in sync
210
- break;
211
- }
212
- if (!waitingSequenceNumberSynchronized) {
213
- // Only write it out once
214
- waitingSequenceNumberSynchronized = true;
215
- debugWait("Waiting for sequence number synchronized");
216
- await timeoutAwait(this.waitForAnyInboundOps(containersToApply), {
217
- durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout,
218
- errorMsg: "Timeout on waiting for sequence number synchronized",
219
- });
220
- }
221
- } else {
222
- waitingSequenceNumberSynchronized = false;
223
- await timeoutAwait(this.waitForPendingClients(pendingClients), {
224
- durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout,
225
- errorMsg: "Timeout on waiting for pending join or leave op",
226
- });
227
- }
228
- } else {
229
- // Wait for all the containers to be saved
230
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
231
- debugWait(`Waiting container to be saved ${dirtyContainers.map((c) => this.containers.get(c)!.index)}`);
232
- waitingSequenceNumberSynchronized = false;
233
- const remainedDuration = timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout;
234
- await Promise.all(dirtyContainers.map(async (c) => Promise.race(
235
- [timeoutPromise(
236
- (resolve) => c.once("saved", () => resolve()),
237
- {
238
- durationMs: remainedDuration,
239
- errorMsg: "Timeout on waiting a container to be saved",
240
- },
241
- ),
242
- new Promise((resolve) => c.once("closed", resolve)),
243
- ],
244
- )));
245
- }
246
-
247
- // yield a turn to allow side effect of the ops we just processed execute before we check again
248
- await new Promise<void>((resolve) => { setTimeout(resolve, 0); });
249
- }
250
-
251
- // Pause all container that was resumed
252
- // don't call pause if resumed is empty and pause everything, which is not what we want
253
- if (resumed.length !== 0) {
254
- await timeoutAwait(this.pauseProcessing(...resumed), {
255
- durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout,
256
- errorMsg: "Timeout on waiting for pausing all resumed containers",
257
- });
258
- }
259
-
260
- debugWait("Synchronized");
261
- }
262
-
263
- /**
264
- * Utility to calculate the set of clientId per container in quorum that is NOT associated with
265
- * any container we tracked, indicating there is a pending join or leave op that we need to wait.
266
- *
267
- * @param containersToApply - the set of containers to check
268
- */
269
- private getPendingClients(containersToApply: IContainer[]) {
270
- // All the clientId we track should be a superset of the quorum, otherwise, we are missing
271
- // leave messages
272
- const openedDocuments = Array.from(this.containers.keys()).filter((c) => !c.closed);
273
- const openedClientId = openedDocuments.map((container) => (container as Container).clientId);
274
-
275
- const pendingClients: [IContainer, Set<string>][] = [];
276
- containersToApply.forEach((container) => {
277
- const pendingClientId = new Set<string>();
278
- const quorum = container.getQuorum();
279
- quorum.getMembers().forEach((client, clientId) => {
280
- // ignore summarizer
281
- if (!client.client.details.capabilities.interactive && !this.syncSummarizerClients) { return; }
282
- if (!openedClientId.includes(clientId)) {
283
- pendingClientId.add(clientId);
284
- }
285
- });
286
-
287
- if (pendingClientId.size !== 0) {
288
- pendingClients.push([container, pendingClientId]);
289
- }
290
- });
291
- return pendingClients;
292
- }
293
-
294
- /**
295
- * Utility to check synchronization based on sequence number
296
- * See ensureSynchronized for more detail
297
- *
298
- * @param containersToApply - the set of containers to check
299
- */
300
- private isSequenceNumberSynchronized(containersToApply: IContainer[]) {
301
- // clientSequenceNumber check detects ops in flight, both on the wire and in the outbound queue
302
- // We need both client sequence number and isDirty check because:
303
- // - Currently isDirty flag ignores ops for task scheduler, so we need the client sequence number check
304
- // - But isDirty flags include ops during forceReadonly and disconnected, because we don't submit
305
- // the ops in the first place, clientSequenceNumber is not assigned
306
-
307
- const isClientSequenceNumberSynchronized = containersToApply.every((container) => {
308
- if (container.deltaManager.readOnlyInfo.readonly === true) {
309
- // Ignore readonly container. the clientSeqNum and clientSeqNumObserved might be out of sync
310
- // because we transition to readonly when outbound is not empty or the in transit op got lost
311
- return true;
312
- }
313
- // Note that in read only mode, the op won't be submitted
314
- let deltaManager = (container.deltaManager as any);
315
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
316
- const { trailingNoOps } = this.containers.get(container)!;
317
- // Back-compat: clientSequenceNumber & clientSequenceNumberObserved moved to ConnectionManager in 0.53
318
- if (!("clientSequenceNumber" in deltaManager)) {
319
- deltaManager = deltaManager.connectionManager;
320
- }
321
- assert("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
322
- assert("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
323
- return deltaManager.clientSequenceNumber ===
324
- (deltaManager.clientSequenceNumberObserved as number) + trailingNoOps;
325
- });
326
-
327
- if (!isClientSequenceNumberSynchronized) {
328
- return false;
329
- }
330
-
331
- const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
332
- if (minSeqNum < this.lastProposalSeqNum) {
333
- // There is an unresolved proposal
334
- return false;
335
- }
336
-
337
- // Check to see if all the container has process the same number of ops.
338
- const seqNum = containersToApply[0].deltaManager.lastSequenceNumber;
339
- return containersToApply.every((c) => c.deltaManager.lastSequenceNumber === seqNum);
340
- }
341
-
342
- /**
343
- * Utility to wait for any clientId in quorum that is NOT associated with any container we
344
- * tracked, indicating there is a pending join or leave op that we need to wait.
345
- *
346
- * Note that this function doesn't account for container that got added after we started waiting
347
- *
348
- * @param containersToApply - the set of containers to wait for any inbound ops for
349
- */
350
- private async waitForPendingClients(pendingClients: [IContainer, Set<string>][]) {
351
- const unconnectedClients =
352
- Array.from(this.containers.keys()).filter((c) => !c.closed && !(c as Container).connected);
353
- return Promise.all(pendingClients.map(async ([container, pendingClientId]) => {
354
- return new Promise<void>((resolve) => {
355
- const cleanup = () => {
356
- unconnectedClients.forEach((c) => c.off("connected", handler));
357
- container.getQuorum().off("removeMember", handler);
358
- };
359
- const handler = (clientId: string) => {
360
- pendingClientId.delete(clientId);
361
- if (pendingClientId.size === 0) {
362
- cleanup();
363
- resolve();
364
- }
365
- };
366
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
367
- const index = this.containers.get(container)!.index;
368
- debugWait(`${index}: Waiting for pending clients ${Array.from(pendingClientId.keys())}`);
369
- unconnectedClients.forEach((c) => c.on("connected", handler));
370
- container.getQuorum().on("removeMember", handler);
371
- container.on("closed", () => {
372
- cleanup();
373
- resolve();
374
- });
375
- });
376
- }));
377
- }
378
-
379
- /**
380
- * Utility to wait for any inbound ops from a set of containers
381
- * @param containersToApply - the set of containers to wait for any inbound ops for
382
- */
383
- private async waitForAnyInboundOps(containersToApply: IContainer[]) {
384
- return new Promise<void>((resolve) => {
385
- const handler = () => {
386
- containersToApply.map((c) => {
387
- c.deltaManager.inbound.off("push", handler);
388
- });
389
- resolve();
390
- };
391
- containersToApply.map((c) => {
392
- c.deltaManager.inbound.on("push", handler);
393
- });
394
- });
395
- }
396
-
397
- /**
398
- * Resume all queue activities on all paused tracked containers and return them
399
- */
400
- public resumeProcessing(...containers: IContainer[]) {
401
- const resumed: IContainer[] = [];
402
- const containersToApply = this.getContainers(containers);
403
- for (const container of containersToApply) {
404
- const record = this.containers.get(container);
405
- if (record?.paused === true) {
406
- debugWait(`${record.index}: container resumed`);
407
- container.deltaManager.inbound.resume();
408
- container.deltaManager.outbound.resume();
409
- resumed.push(container);
410
- record.paused = false;
411
- }
412
- }
413
- return resumed;
414
- }
415
-
416
- /**
417
- * Pause all queue activities on the containers given, or all tracked containers
418
- * Any containers given that is not tracked will be ignored.
419
- */
420
- public async pauseProcessing(...containers: IContainer[]) {
421
- const pauseP: Promise<void>[] = [];
422
- const containersToApply = this.getContainers(containers);
423
- for (const container of containersToApply) {
424
- const record = this.containers.get(container);
425
- if (record !== undefined && !record.paused) {
426
- debugWait(`${record.index}: container paused`);
427
- pauseP.push(container.deltaManager.inbound.pause());
428
- pauseP.push(container.deltaManager.outbound.pause());
429
- record.paused = true;
430
- }
431
- }
432
- await Promise.all(pauseP);
433
- }
434
-
435
- /**
436
- * Pause all queue activities on all tracked containers, and resume only
437
- * inbound to process ops until it is idle. All queues are left in the paused state
438
- * after the function
439
- */
440
- public async processIncoming(...containers: IContainer[]) {
441
- return this.processQueue(containers, (container) => container.deltaManager.inbound);
442
- }
443
-
444
- /**
445
- * Pause all queue activities on all tracked containers, and resume only
446
- * outbound to process ops until it is idle. All queues are left in the paused state
447
- * after the function
448
- */
449
- public async processOutgoing(...containers: IContainer[]) {
450
- return this.processQueue(containers, (container) => container.deltaManager.outbound);
451
- }
452
-
453
- /**
454
- * Implementation of processIncoming and processOutgoing
455
- */
456
- private async processQueue<U>(containers: IContainer[], getQueue: (container: IContainer) => IDeltaQueue<U>) {
457
- await this.pauseProcessing(...containers);
458
- const resumed: IDeltaQueue<U>[] = [];
459
-
460
- const containersToApply = this.getContainers(containers);
461
- const inflightTracker = new Map<IContainer, number>();
462
- const cleanup: (() => void)[] = [];
463
- for (const container of containersToApply) {
464
- const queue = getQueue(container);
465
-
466
- // track the outgoing ops (if any) to make sure they make the round trip to at least to the same client
467
- // to make sure they are sequenced.
468
- cleanup.push(this.setupInOutTracker(container, inflightTracker));
469
- queue.resume();
470
- resumed.push(queue);
471
- }
472
-
473
- while (resumed.some((queue) => !queue.idle)) {
474
- debugWait("Wait until queue is idle");
475
- await new Promise<void>((resolve) => { setTimeout(resolve, 0); });
476
- }
477
-
478
- // Make sure all the op that we sent out are acked first
479
- // This is no op if we are processing incoming
480
- if (inflightTracker.size) {
481
- debugWait("Wait for inflight ops");
482
- do {
483
- await this.waitForAnyInboundOps(containersToApply);
484
- } while (inflightTracker.size);
485
- }
486
-
487
- // remove the handlers
488
- cleanup.forEach((clean) => clean());
489
-
490
- await Promise.all(resumed.map(async (queue) => queue.pause()));
491
- }
492
-
493
- /**
494
- * Utility to set up listener to track the outbound ops until it round trip back
495
- * Returns a function to remove the handler after it is done.
496
- *
497
- * @param container - the container to setup
498
- * @param inflightTracker - a map to track the clientSequenceNumber per container it expect to get ops back
499
- */
500
- private setupInOutTracker(container: IContainer, inflightTracker: Map<IContainer, number>) {
501
- const outHandler = (messages: IDocumentMessage[]) => {
502
- for (const message of messages) {
503
- if (!canBeCoalescedByService(message)) {
504
- inflightTracker.set(container, message.clientSequenceNumber);
505
- }
506
- }
507
- };
508
- const inHandler = (message: ISequencedDocumentMessage) => {
509
- if (!canBeCoalescedByService(message)
510
- && message.clientId === (container as Container).clientId
511
- && inflightTracker.get(container) === message.clientSequenceNumber) {
512
- inflightTracker.delete(container);
513
- }
514
- };
515
-
516
- container.deltaManager.outbound.on("op", outHandler);
517
- container.deltaManager.inbound.on("push", inHandler);
518
-
519
- return () => {
520
- container.deltaManager.outbound.off("op", outHandler);
521
- container.deltaManager.inbound.off("push", inHandler);
522
- };
523
- }
524
-
525
- /**
526
- * Setup debug traces for connection and ops
527
- */
528
- private setupTrace(container: IContainer, index: number) {
529
- if (debugOp.enabled) {
530
- const getContentsString = (type: string, msgContents: any) => {
531
- try {
532
- if (type !== MessageType.Operation) {
533
- if (typeof msgContents === "string") { return msgContents; }
534
- return JSON.stringify(msgContents);
535
- }
536
- let address = "";
537
-
538
- // contents comes in the wire as JSON string ("push" event)
539
- // But already parsed when apply ("op" event)
540
- let contents = typeof msgContents === "string" ?
541
- JSON.parse(msgContents) : msgContents;
542
- while (contents !== undefined && contents !== null) {
543
- if (contents.contents?.address !== undefined) {
544
- address += `/${contents.contents.address}`;
545
- contents = contents.contents.contents;
546
- } else if (contents.content?.address !== undefined) {
547
- address += `/${contents.content.address}`;
548
- contents = contents.content.contents;
549
- } else {
550
- break;
551
- }
552
- }
553
- if (address) {
554
- return `${address} ${JSON.stringify(contents)}`;
555
- }
556
- return JSON.stringify(contents);
557
- } catch (e: any) {
558
- return `${e.message}: ${e.stack}`;
559
- }
560
- };
561
- debugOp(`${index}: ADD: clientId: ${(container as Container).clientId}`);
562
- container.deltaManager.outbound.on("op", (messages) => {
563
- for (const msg of messages) {
564
- debugOp(`${index}: OUT: `
565
- + `cli: ${msg.clientSequenceNumber.toString().padStart(3)} `
566
- + `rsq: ${msg.referenceSequenceNumber.toString().padStart(3)} `
567
- + `${msg.type} ${getContentsString(msg.type, msg.contents)}`);
568
- }
569
- });
570
- const getInboundHandler = (type: string) => {
571
- return (msg: ISequencedDocumentMessage) => {
572
- const clientSeq = msg.clientId === (container as Container).clientId ?
573
- `cli: ${msg.clientSequenceNumber.toString().padStart(3)}` : " ";
574
- debugOp(`${index}: ${type}: seq: ${msg.sequenceNumber.toString().padStart(3)} `
575
- + `${clientSeq} min: ${msg.minimumSequenceNumber.toString().padStart(3)} `
576
- + `${msg.type} ${getContentsString(msg.type, msg.contents)}`);
577
- };
578
- };
579
- container.deltaManager.inbound.on("push", getInboundHandler("IN "));
580
- container.deltaManager.inbound.on("op", getInboundHandler("OP "));
581
- container.deltaManager.on("connect", (details) => {
582
- debugOp(`${index}: CON: clientId: ${details.clientId}`);
583
- });
584
- container.deltaManager.on("disconnect", (reason) => {
585
- debugOp(`${index}: DIS: ${reason}`);
586
- });
587
- }
588
- }
589
-
590
- /**
591
- * Filter out the opened containers based on param.
592
- * @param containers - The container to filter to. If the array is empty, it means don't filter and return
593
- * all open containers.
594
- */
595
- private getContainers(containers: IContainer[]) {
596
- const containersToApply = containers.length === 0 ? Array.from(this.containers.keys()) : containers;
597
- return containersToApply.filter((container) => !container.closed);
598
- }
38
+ private readonly containers = new Map<IContainer, ContainerRecord>();
39
+ private lastProposalSeqNum: number = 0;
40
+
41
+ constructor(private readonly syncSummarizerClients: boolean = false) {}
42
+
43
+ /**
44
+ * Add a loader to start to track any container created from them
45
+ * @param loader - loader to start tracking any container created.
46
+ */
47
+ public add<LoaderType extends IHostLoader>(loader: LoaderType) {
48
+ // TODO: Expose Loader API to able to intercept container creation (See issue #5114)
49
+ const patch = <T, C extends IContainer>(fn: (...args) => Promise<C>) => {
50
+ const boundFn = fn.bind(loader);
51
+ return async (...args: T[]) => {
52
+ const container = await boundFn(...args);
53
+ this.addContainer(container);
54
+ return container;
55
+ };
56
+ };
57
+ /* eslint-disable @typescript-eslint/unbound-method */
58
+ loader.resolve = patch(loader.resolve);
59
+ loader.createDetachedContainer = patch(loader.createDetachedContainer);
60
+ loader.rehydrateDetachedContainerFromSnapshot = patch(
61
+ loader.rehydrateDetachedContainerFromSnapshot,
62
+ );
63
+ /* eslint-enable @typescript-eslint/unbound-method */
64
+ }
65
+
66
+ /**
67
+ * Utility function to add container to be tracked.
68
+ *
69
+ * @param container - container to add
70
+ */
71
+ private addContainer(container: IContainer) {
72
+ // ignore summarizer
73
+ if (
74
+ !container.deltaManager.clientDetails.capabilities.interactive &&
75
+ !this.syncSummarizerClients
76
+ ) {
77
+ return;
78
+ }
79
+
80
+ // don't add container that is already tracked
81
+ if (this.containers.has(container)) {
82
+ return;
83
+ }
84
+
85
+ const record = {
86
+ index: this.containers.size,
87
+ paused: false,
88
+ startTrailingNoOps: 0,
89
+ trailingNoOps: 0,
90
+ lastProposal: 0,
91
+ };
92
+ this.containers.set(container, record);
93
+ this.trackTrailingNoOps(container, record);
94
+ this.trackLastProposal(container);
95
+ this.setupTrace(container, record.index);
96
+ }
97
+
98
+ /**
99
+ * Keep track of the trailing NoOp that was sent so we can discount them in the clientSequenceNumber tracking.
100
+ * The server might coalesce them with other ops, or a single NoOp, or delay it if it don't think it is necessary.
101
+ *
102
+ * @param container - the container to track
103
+ * @param record - the record to update the trailing op information
104
+ */
105
+ private trackTrailingNoOps(container: IContainer, record: ContainerRecord) {
106
+ container.deltaManager.outbound.on("op", (messages) => {
107
+ for (const msg of messages) {
108
+ if (canBeCoalescedByService(msg)) {
109
+ // Track the NoOp that was sent.
110
+ if (record.trailingNoOps === 0) {
111
+ // record the starting sequence number of the trailing no ops if we haven't been tracking yet.
112
+ record.startTrailingNoOps = msg.clientSequenceNumber;
113
+ }
114
+ record.trailingNoOps++;
115
+ } else {
116
+ // Other ops has been sent. We would like to see those ack'ed, so no more need to track NoOps
117
+ record.trailingNoOps = 0;
118
+ }
119
+ }
120
+ });
121
+
122
+ container.deltaManager.inbound.on("push", (message) => {
123
+ // Received the no op back, update the record if we are tracking
124
+ if (
125
+ canBeCoalescedByService(message) &&
126
+ message.clientId === container.clientId &&
127
+ record.trailingNoOps !== 0 &&
128
+ record.startTrailingNoOps <= message.clientSequenceNumber
129
+ ) {
130
+ // NoOp might have coalesced and skipped ahead some sequence number
131
+ // update the record and skip ahead as well
132
+ const oldStartTrailingNoOps = record.startTrailingNoOps;
133
+ record.startTrailingNoOps = message.clientSequenceNumber + 1;
134
+ record.trailingNoOps -= record.startTrailingNoOps - oldStartTrailingNoOps;
135
+ }
136
+ });
137
+
138
+ container.on("disconnected", () => {
139
+ // reset on disconnect.
140
+ record.trailingNoOps = 0;
141
+ });
142
+ }
143
+
144
+ private trackLastProposal(container: IContainer) {
145
+ container.on("codeDetailsProposed", (value, proposal) => {
146
+ if (proposal.sequenceNumber > this.lastProposalSeqNum) {
147
+ this.lastProposalSeqNum = proposal.sequenceNumber;
148
+ }
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Reset the tracker, closing all containers and stop tracking them.
154
+ */
155
+ public reset() {
156
+ this.lastProposalSeqNum = 0;
157
+ for (const container of this.containers.keys()) {
158
+ container.close();
159
+ }
160
+ this.containers.clear();
161
+
162
+ // REVIEW: do we need to unpatch the loaders?
163
+ }
164
+
165
+ /**
166
+ * Ensure all tracked containers are synchronized with a time limit
167
+ *
168
+ * @deprecated - this method is equivalent to @see {@link LoaderContainerTracker.ensureSynchronized}, please configure the test timeout instead
169
+ */
170
+ public async ensureSynchronizedWithTimeout?(
171
+ timeoutDuration: number | undefined,
172
+ ...containers: IContainer[]
173
+ ) {
174
+ await this.ensureSynchronized(...containers);
175
+ }
176
+
177
+ /**
178
+ * Make sure all the tracked containers are synchronized.
179
+ *
180
+ * No isDirty (non-readonly) containers
181
+ *
182
+ * No extra clientId in quorum of any container that is not tracked and still opened.
183
+ *
184
+ * - i.e. no pending Join/Leave message.
185
+ *
186
+ * No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
187
+ *
188
+ * lastSequenceNumber of all container is the same
189
+ *
190
+ * clientSequenceNumberObserved is the same as clientSequenceNumber sent
191
+ *
192
+ * - this overlaps with !isDirty, but include task scheduler ops.
193
+ *
194
+ * - Trailing NoOp is tracked and don't count as pending ops.
195
+ */
196
+ public async ensureSynchronized(...containers: IContainer[]): Promise<void> {
197
+ const resumed = this.resumeProcessing(...containers);
198
+
199
+ let waitingSequenceNumberSynchronized = false;
200
+ // eslint-disable-next-line no-constant-condition
201
+ while (true) {
202
+ const containersToApply = this.getContainers(containers);
203
+ if (containersToApply.length === 0) {
204
+ break;
205
+ }
206
+
207
+ // Ignore readonly dirty containers, because it can't sent up and nothing can be done about it being dirty
208
+ const dirtyContainers = containersToApply.filter((c) => {
209
+ const { deltaManager, isDirty } = c;
210
+ return deltaManager.readOnlyInfo.readonly !== true && isDirty;
211
+ });
212
+ if (dirtyContainers.length === 0) {
213
+ // Wait for all the leave messages
214
+ const pendingClients = this.getPendingClients(containersToApply);
215
+ if (pendingClients.length === 0) {
216
+ if (this.isSequenceNumberSynchronized(containersToApply)) {
217
+ // done, we are in sync
218
+ break;
219
+ }
220
+ if (!waitingSequenceNumberSynchronized) {
221
+ // Only write it out once
222
+ waitingSequenceNumberSynchronized = true;
223
+ debugWait("Waiting for sequence number synchronized");
224
+ await timeoutAwait(this.waitForAnyInboundOps(containersToApply), {
225
+ errorMsg: "Timeout on waiting for sequence number synchronized",
226
+ });
227
+ }
228
+ } else {
229
+ waitingSequenceNumberSynchronized = false;
230
+ await timeoutAwait(this.waitForPendingClients(pendingClients), {
231
+ errorMsg: "Timeout on waiting for pending join or leave op",
232
+ });
233
+ }
234
+ } else {
235
+ // Wait for all the containers to be saved
236
+ debugWait(
237
+ `Waiting container to be saved ${dirtyContainers.map(
238
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
239
+ (c) => this.containers.get(c)!.index,
240
+ )}`,
241
+ );
242
+ waitingSequenceNumberSynchronized = false;
243
+ await Promise.all(
244
+ dirtyContainers.map(async (c) =>
245
+ Promise.race([
246
+ timeoutPromise((resolve) => c.once("saved", () => resolve()), {
247
+ errorMsg: "Timeout on waiting a container to be saved",
248
+ }),
249
+ new Promise((resolve) => c.once("closed", resolve)),
250
+ ]),
251
+ ),
252
+ );
253
+ }
254
+
255
+ // yield a turn to allow side effect of the ops we just processed execute before we check again
256
+ await new Promise<void>((resolve) => {
257
+ setTimeout(resolve, 0);
258
+ });
259
+ }
260
+
261
+ // Pause all container that was resumed
262
+ // don't call pause if resumed is empty and pause everything, which is not what we want
263
+ if (resumed.length !== 0) {
264
+ await timeoutAwait(this.pauseProcessing(...resumed), {
265
+ errorMsg: "Timeout on waiting for pausing all resumed containers",
266
+ });
267
+ }
268
+
269
+ debugWait("Synchronized");
270
+ }
271
+
272
+ /**
273
+ * Utility to calculate the set of clientId per container in quorum that is NOT associated with
274
+ * any container we tracked, indicating there is a pending join or leave op that we need to wait.
275
+ *
276
+ * @param containersToApply - the set of containers to check
277
+ */
278
+ private getPendingClients(containersToApply: IContainer[]) {
279
+ // All the clientId we track should be a superset of the quorum, otherwise, we are missing
280
+ // leave messages
281
+ const openedDocuments = Array.from(this.containers.keys()).filter((c) => !c.closed);
282
+ const openedClientId = openedDocuments.map((container) => container.clientId);
283
+
284
+ const pendingClients: [IContainer, Set<string>][] = [];
285
+ containersToApply.forEach((container) => {
286
+ const pendingClientId = new Set<string>();
287
+ const quorum = container.getQuorum();
288
+ quorum.getMembers().forEach((client, clientId) => {
289
+ // ignore summarizer
290
+ if (
291
+ !client.client.details.capabilities.interactive &&
292
+ !this.syncSummarizerClients
293
+ ) {
294
+ return;
295
+ }
296
+ if (!openedClientId.includes(clientId)) {
297
+ pendingClientId.add(clientId);
298
+ }
299
+ });
300
+
301
+ if (pendingClientId.size !== 0) {
302
+ pendingClients.push([container, pendingClientId]);
303
+ }
304
+ });
305
+ return pendingClients;
306
+ }
307
+
308
+ /**
309
+ * Utility to check synchronization based on sequence number
310
+ * See ensureSynchronized for more detail
311
+ *
312
+ * @param containersToApply - the set of containers to check
313
+ */
314
+ private isSequenceNumberSynchronized(containersToApply: IContainer[]) {
315
+ // clientSequenceNumber check detects ops in flight, both on the wire and in the outbound queue
316
+ // We need both client sequence number and isDirty check because:
317
+ // - Currently isDirty flag ignores ops for task scheduler, so we need the client sequence number check
318
+ // - But isDirty flags include ops during forceReadonly and disconnected, because we don't submit
319
+ // the ops in the first place, clientSequenceNumber is not assigned
320
+
321
+ const isClientSequenceNumberSynchronized = containersToApply.every((container) => {
322
+ if (container.deltaManager.readOnlyInfo.readonly === true) {
323
+ // Ignore readonly container. the clientSeqNum and clientSeqNumObserved might be out of sync
324
+ // because we transition to readonly when outbound is not empty or the in transit op got lost
325
+ return true;
326
+ }
327
+ // Note that in read only mode, the op won't be submitted
328
+ let deltaManager = container.deltaManager as any;
329
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
330
+ const { trailingNoOps } = this.containers.get(container)!;
331
+ // Back-compat: clientSequenceNumber & clientSequenceNumberObserved moved to ConnectionManager in 0.53
332
+ if (!("clientSequenceNumber" in deltaManager)) {
333
+ deltaManager = deltaManager.connectionManager;
334
+ }
335
+ assert("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
336
+ assert("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
337
+ return (
338
+ deltaManager.clientSequenceNumber ===
339
+ (deltaManager.clientSequenceNumberObserved as number) + trailingNoOps
340
+ );
341
+ });
342
+
343
+ if (!isClientSequenceNumberSynchronized) {
344
+ return false;
345
+ }
346
+
347
+ const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
348
+ if (minSeqNum < this.lastProposalSeqNum) {
349
+ // There is an unresolved proposal
350
+ return false;
351
+ }
352
+
353
+ // Check to see if all the container has process the same number of ops.
354
+ const seqNum = containersToApply[0].deltaManager.lastSequenceNumber;
355
+ return containersToApply.every((c) => c.deltaManager.lastSequenceNumber === seqNum);
356
+ }
357
+
358
+ /**
359
+ * Utility to wait for any clientId in quorum that is NOT associated with any container we
360
+ * tracked, indicating there is a pending join or leave op that we need to wait.
361
+ *
362
+ * Note that this function doesn't account for container that got added after we started waiting
363
+ *
364
+ * @param containersToApply - the set of containers to wait for any inbound ops for
365
+ */
366
+ private async waitForPendingClients(pendingClients: [IContainer, Set<string>][]) {
367
+ const unconnectedClients = Array.from(this.containers.keys()).filter(
368
+ (c) => !c.closed && c.connectionState !== ConnectionState.Connected,
369
+ );
370
+ return Promise.all(
371
+ pendingClients.map(async ([container, pendingClientId]) => {
372
+ return new Promise<void>((resolve) => {
373
+ const cleanup = () => {
374
+ unconnectedClients.forEach((c) => c.off("connected", handler));
375
+ container.getQuorum().off("removeMember", handler);
376
+ };
377
+ const handler = (clientId: string) => {
378
+ pendingClientId.delete(clientId);
379
+ if (pendingClientId.size === 0) {
380
+ cleanup();
381
+ resolve();
382
+ }
383
+ };
384
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
385
+ const index = this.containers.get(container)!.index;
386
+ debugWait(
387
+ `${index}: Waiting for pending clients ${Array.from(
388
+ pendingClientId.keys(),
389
+ )}`,
390
+ );
391
+ unconnectedClients.forEach((c) => c.on("connected", handler));
392
+ container.getQuorum().on("removeMember", handler);
393
+ container.on("closed", () => {
394
+ cleanup();
395
+ resolve();
396
+ });
397
+ });
398
+ }),
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Utility to wait for any inbound ops from a set of containers
404
+ * @param containersToApply - the set of containers to wait for any inbound ops for
405
+ */
406
+ private async waitForAnyInboundOps(containersToApply: IContainer[]) {
407
+ return new Promise<void>((resolve) => {
408
+ const handler = () => {
409
+ containersToApply.map((c) => {
410
+ c.deltaManager.inbound.off("push", handler);
411
+ });
412
+ resolve();
413
+ };
414
+ containersToApply.map((c) => {
415
+ c.deltaManager.inbound.on("push", handler);
416
+ });
417
+ });
418
+ }
419
+
420
+ /**
421
+ * Resume all queue activities on all paused tracked containers and return them
422
+ */
423
+ public resumeProcessing(...containers: IContainer[]) {
424
+ const resumed: IContainer[] = [];
425
+ const containersToApply = this.getContainers(containers);
426
+ for (const container of containersToApply) {
427
+ const record = this.containers.get(container);
428
+ if (record?.paused === true) {
429
+ debugWait(`${record.index}: container resumed`);
430
+ container.deltaManager.inbound.resume();
431
+ container.deltaManager.outbound.resume();
432
+ resumed.push(container);
433
+ record.paused = false;
434
+ }
435
+ }
436
+ return resumed;
437
+ }
438
+
439
+ /**
440
+ * Pause all queue activities on the containers given, or all tracked containers
441
+ * Any containers given that is not tracked will be ignored.
442
+ */
443
+ public async pauseProcessing(...containers: IContainer[]) {
444
+ const pauseP: Promise<void>[] = [];
445
+ const containersToApply = this.getContainers(containers);
446
+ for (const container of containersToApply) {
447
+ const record = this.containers.get(container);
448
+ if (record !== undefined && !record.paused) {
449
+ debugWait(`${record.index}: container paused`);
450
+ pauseP.push(container.deltaManager.inbound.pause());
451
+ pauseP.push(container.deltaManager.outbound.pause());
452
+ record.paused = true;
453
+ }
454
+ }
455
+ await Promise.all(pauseP);
456
+ }
457
+
458
+ /**
459
+ * Pause all queue activities on all tracked containers, and resume only
460
+ * inbound to process ops until it is idle. All queues are left in the paused state
461
+ * after the function
462
+ */
463
+ public async processIncoming(...containers: IContainer[]) {
464
+ return this.processQueue(containers, (container) => container.deltaManager.inbound);
465
+ }
466
+
467
+ /**
468
+ * Pause all queue activities on all tracked containers, and resume only
469
+ * outbound to process ops until it is idle. All queues are left in the paused state
470
+ * after the function
471
+ */
472
+ public async processOutgoing(...containers: IContainer[]) {
473
+ return this.processQueue(containers, (container) => container.deltaManager.outbound);
474
+ }
475
+
476
+ /**
477
+ * Implementation of processIncoming and processOutgoing
478
+ */
479
+ private async processQueue<U>(
480
+ containers: IContainer[],
481
+ getQueue: (container: IContainer) => IDeltaQueue<U>,
482
+ ) {
483
+ await this.pauseProcessing(...containers);
484
+ const resumed: IDeltaQueue<U>[] = [];
485
+
486
+ const containersToApply = this.getContainers(containers);
487
+ const inflightTracker = new Map<IContainer, number>();
488
+ const cleanup: (() => void)[] = [];
489
+ for (const container of containersToApply) {
490
+ const queue = getQueue(container);
491
+
492
+ // track the outgoing ops (if any) to make sure they make the round trip to at least to the same client
493
+ // to make sure they are sequenced.
494
+ cleanup.push(this.setupInOutTracker(container, inflightTracker));
495
+ queue.resume();
496
+ resumed.push(queue);
497
+ }
498
+
499
+ while (resumed.some((queue) => !queue.idle)) {
500
+ debugWait("Wait until queue is idle");
501
+ await new Promise<void>((resolve) => {
502
+ setTimeout(resolve, 0);
503
+ });
504
+ }
505
+
506
+ // Make sure all the op that we sent out are acked first
507
+ // This is no op if we are processing incoming
508
+ if (inflightTracker.size) {
509
+ debugWait("Wait for inflight ops");
510
+ do {
511
+ await this.waitForAnyInboundOps(containersToApply);
512
+ } while (inflightTracker.size);
513
+ }
514
+
515
+ // remove the handlers
516
+ cleanup.forEach((clean) => clean());
517
+
518
+ await Promise.all(resumed.map(async (queue) => queue.pause()));
519
+ }
520
+
521
+ /**
522
+ * Utility to set up listener to track the outbound ops until it round trip back
523
+ * Returns a function to remove the handler after it is done.
524
+ *
525
+ * @param container - the container to setup
526
+ * @param inflightTracker - a map to track the clientSequenceNumber per container it expect to get ops back
527
+ */
528
+ private setupInOutTracker(container: IContainer, inflightTracker: Map<IContainer, number>) {
529
+ const outHandler = (messages: IDocumentMessage[]) => {
530
+ for (const message of messages) {
531
+ if (!canBeCoalescedByService(message)) {
532
+ inflightTracker.set(container, message.clientSequenceNumber);
533
+ }
534
+ }
535
+ };
536
+ const inHandler = (message: ISequencedDocumentMessage) => {
537
+ if (
538
+ !canBeCoalescedByService(message) &&
539
+ message.clientId === container.clientId &&
540
+ inflightTracker.get(container) === message.clientSequenceNumber
541
+ ) {
542
+ inflightTracker.delete(container);
543
+ }
544
+ };
545
+
546
+ container.deltaManager.outbound.on("op", outHandler);
547
+ container.deltaManager.inbound.on("push", inHandler);
548
+
549
+ return () => {
550
+ container.deltaManager.outbound.off("op", outHandler);
551
+ container.deltaManager.inbound.off("push", inHandler);
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Setup debug traces for connection and ops
557
+ */
558
+ private setupTrace(container: IContainer, index: number) {
559
+ if (debugOp.enabled) {
560
+ const getContentsString = (type: string, msgContents: any) => {
561
+ try {
562
+ if (type !== MessageType.Operation) {
563
+ if (typeof msgContents === "string") {
564
+ return msgContents;
565
+ }
566
+ return JSON.stringify(msgContents);
567
+ }
568
+ let address = "";
569
+
570
+ // contents comes in the wire as JSON string ("push" event)
571
+ // But already parsed when apply ("op" event)
572
+ let contents =
573
+ typeof msgContents === "string" ? JSON.parse(msgContents) : msgContents;
574
+ while (contents !== undefined && contents !== null) {
575
+ if (contents.contents?.address !== undefined) {
576
+ address += `/${contents.contents.address}`;
577
+ contents = contents.contents.contents;
578
+ } else if (contents.content?.address !== undefined) {
579
+ address += `/${contents.content.address}`;
580
+ contents = contents.content.contents;
581
+ } else {
582
+ break;
583
+ }
584
+ }
585
+ if (address) {
586
+ return `${address} ${JSON.stringify(contents)}`;
587
+ }
588
+ return JSON.stringify(contents);
589
+ } catch (e: any) {
590
+ return `${e.message}: ${e.stack}`;
591
+ }
592
+ };
593
+ debugOp(`${index}: ADD: clientId: ${container.clientId}`);
594
+ container.deltaManager.outbound.on("op", (messages) => {
595
+ for (const msg of messages) {
596
+ debugOp(
597
+ `${index}: OUT: ` +
598
+ `cli: ${msg.clientSequenceNumber.toString().padStart(3)} ` +
599
+ `rsq: ${msg.referenceSequenceNumber.toString().padStart(3)} ` +
600
+ `${msg.type} ${getContentsString(msg.type, msg.contents)}`,
601
+ );
602
+ }
603
+ });
604
+ const getInboundHandler = (type: string) => {
605
+ return (msg: ISequencedDocumentMessage) => {
606
+ const clientSeq =
607
+ msg.clientId === container.clientId
608
+ ? `cli: ${msg.clientSequenceNumber.toString().padStart(3)}`
609
+ : " ";
610
+ debugOp(
611
+ `${index}: ${type}: seq: ${msg.sequenceNumber.toString().padStart(3)} ` +
612
+ `${clientSeq} min: ${msg.minimumSequenceNumber
613
+ .toString()
614
+ .padStart(3)} ` +
615
+ `${msg.type} ${getContentsString(msg.type, msg.contents)}`,
616
+ );
617
+ };
618
+ };
619
+ container.deltaManager.inbound.on("push", getInboundHandler("IN "));
620
+ container.deltaManager.inbound.on("op", getInboundHandler("OP "));
621
+ container.deltaManager.on("connect", (details) => {
622
+ debugOp(`${index}: CON: clientId: ${details.clientId}`);
623
+ });
624
+ container.deltaManager.on("disconnect", (reason) => {
625
+ debugOp(`${index}: DIS: ${reason}`);
626
+ });
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Filter out the opened containers based on param.
632
+ * @param containers - The container to filter to. If the array is empty, it means don't filter and return
633
+ * all open containers.
634
+ */
635
+ private getContainers(containers: IContainer[]) {
636
+ const containersToApply =
637
+ containers.length === 0 ? Array.from(this.containers.keys()) : containers;
638
+ return containersToApply.filter((container) => !container.closed);
639
+ }
599
640
  }