@fluidframework/test-utils 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419

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 (98) hide show
  1. package/.eslintrc.js +8 -7
  2. package/.mocharc.js +12 -0
  3. package/CHANGELOG.md +177 -0
  4. package/README.md +44 -14
  5. package/api-extractor-lint.json +4 -0
  6. package/api-extractor.json +2 -2
  7. package/api-report/test-utils.api.md +395 -0
  8. package/dist/DriverWrappers.d.ts +32 -0
  9. package/dist/DriverWrappers.d.ts.map +1 -0
  10. package/dist/DriverWrappers.js +57 -0
  11. package/dist/DriverWrappers.js.map +1 -0
  12. package/dist/TestConfigs.d.ts +10 -0
  13. package/dist/TestConfigs.d.ts.map +1 -0
  14. package/dist/TestConfigs.js +17 -0
  15. package/dist/TestConfigs.js.map +1 -0
  16. package/dist/TestSummaryUtils.d.ts +61 -0
  17. package/dist/TestSummaryUtils.d.ts.map +1 -0
  18. package/dist/TestSummaryUtils.js +135 -0
  19. package/dist/TestSummaryUtils.js.map +1 -0
  20. package/dist/containerUtils.d.ts +46 -0
  21. package/dist/containerUtils.d.ts.map +1 -0
  22. package/dist/containerUtils.js +85 -0
  23. package/dist/containerUtils.js.map +1 -0
  24. package/dist/index.d.ts +14 -9
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +43 -19
  27. package/dist/index.js.map +1 -1
  28. package/dist/interfaces.d.ts +6 -0
  29. package/dist/interfaces.d.ts.map +1 -1
  30. package/dist/interfaces.js.map +1 -1
  31. package/dist/loaderContainerTracker.d.ts +39 -15
  32. package/dist/loaderContainerTracker.d.ts.map +1 -1
  33. package/dist/loaderContainerTracker.js +214 -86
  34. package/dist/loaderContainerTracker.js.map +1 -1
  35. package/dist/localCodeLoader.d.ts +9 -2
  36. package/dist/localCodeLoader.d.ts.map +1 -1
  37. package/dist/localCodeLoader.js +18 -17
  38. package/dist/localCodeLoader.js.map +1 -1
  39. package/dist/localLoader.d.ts +3 -2
  40. package/dist/localLoader.d.ts.map +1 -1
  41. package/dist/localLoader.js +2 -0
  42. package/dist/localLoader.js.map +1 -1
  43. package/dist/packageVersion.d.ts +1 -1
  44. package/dist/packageVersion.d.ts.map +1 -1
  45. package/dist/packageVersion.js +1 -1
  46. package/dist/packageVersion.js.map +1 -1
  47. package/dist/retry.d.ts +1 -0
  48. package/dist/retry.d.ts.map +1 -1
  49. package/dist/retry.js +3 -2
  50. package/dist/retry.js.map +1 -1
  51. package/dist/test-utils-alpha.d.ts +305 -0
  52. package/dist/test-utils-beta.d.ts +202 -0
  53. package/dist/test-utils-public.d.ts +202 -0
  54. package/dist/test-utils-untrimmed.d.ts +1001 -0
  55. package/dist/testContainerRuntimeFactory.d.ts +4 -2
  56. package/dist/testContainerRuntimeFactory.d.ts.map +1 -1
  57. package/dist/testContainerRuntimeFactory.js +67 -12
  58. package/dist/testContainerRuntimeFactory.js.map +1 -1
  59. package/dist/testContainerRuntimeFactoryWithDefaultDataStore.d.ts +23 -0
  60. package/dist/testContainerRuntimeFactoryWithDefaultDataStore.d.ts.map +1 -0
  61. package/dist/testContainerRuntimeFactoryWithDefaultDataStore.js +32 -0
  62. package/dist/testContainerRuntimeFactoryWithDefaultDataStore.js.map +1 -0
  63. package/dist/testFluidObject.d.ts +30 -12
  64. package/dist/testFluidObject.d.ts.map +1 -1
  65. package/dist/testFluidObject.js +74 -43
  66. package/dist/testFluidObject.js.map +1 -1
  67. package/dist/testObjectProvider.d.ts +298 -41
  68. package/dist/testObjectProvider.d.ts.map +1 -1
  69. package/dist/testObjectProvider.js +377 -61
  70. package/dist/testObjectProvider.js.map +1 -1
  71. package/dist/timeoutUtils.d.ts +42 -0
  72. package/dist/timeoutUtils.d.ts.map +1 -1
  73. package/dist/timeoutUtils.js +142 -9
  74. package/dist/timeoutUtils.js.map +1 -1
  75. package/dist/tsdoc-metadata.json +11 -0
  76. package/lib/test-utils-alpha.d.ts +305 -0
  77. package/lib/test-utils-beta.d.ts +202 -0
  78. package/lib/test-utils-public.d.ts +202 -0
  79. package/lib/test-utils-untrimmed.d.ts +1001 -0
  80. package/package.json +106 -72
  81. package/prettier.config.cjs +8 -0
  82. package/src/DriverWrappers.ts +83 -0
  83. package/src/TestConfigs.ts +17 -0
  84. package/src/TestSummaryUtils.ts +210 -0
  85. package/src/containerUtils.ts +90 -0
  86. package/src/index.ts +47 -9
  87. package/src/interfaces.ts +16 -7
  88. package/src/loaderContainerTracker.ts +754 -534
  89. package/src/localCodeLoader.ts +87 -86
  90. package/src/localLoader.ts +27 -26
  91. package/src/packageVersion.ts +1 -1
  92. package/src/retry.ts +33 -26
  93. package/src/testContainerRuntimeFactory.ts +152 -59
  94. package/src/testContainerRuntimeFactoryWithDefaultDataStore.ts +61 -0
  95. package/src/testFluidObject.ts +190 -147
  96. package/src/testObjectProvider.ts +931 -372
  97. package/src/timeoutUtils.ts +226 -37
  98. package/tsconfig.json +9 -12
@@ -1,15 +1,22 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoaderContainerTracker = void 0;
2
4
  /*!
3
5
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
4
6
  * Licensed under the MIT License.
5
7
  */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.LoaderContainerTracker = void 0;
8
- const common_utils_1 = require("@fluidframework/common-utils");
8
+ const core_utils_1 = require("@fluidframework/core-utils");
9
+ const container_loader_1 = require("@fluidframework/container-loader");
10
+ const driver_utils_1 = require("@fluidframework/driver-utils");
9
11
  const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
12
+ const containerUtils_1 = require("./containerUtils");
10
13
  const debug_1 = require("./debug");
14
+ const timeoutUtils_1 = require("./timeoutUtils");
11
15
  const debugOp = debug_1.debug.extend("ops");
12
16
  const debugWait = debug_1.debug.extend("wait");
17
+ /**
18
+ * @alpha
19
+ */
13
20
  class LoaderContainerTracker {
14
21
  constructor(syncSummarizerClients = false) {
15
22
  this.syncSummarizerClients = syncSummarizerClients;
@@ -42,14 +49,28 @@ class LoaderContainerTracker {
42
49
  * @param container - container to add
43
50
  */
44
51
  addContainer(container) {
45
- // ignore summarizer
46
- if (!container.deltaManager.clientDetails.capabilities.interactive && !this.syncSummarizerClients) {
47
- return;
48
- }
49
52
  // don't add container that is already tracked
50
53
  if (this.containers.has(container)) {
51
54
  return;
52
55
  }
56
+ const containerWithClone = container;
57
+ // back-compat: Check for undefined because this function was added recently and older containers won't have it.
58
+ if (containerWithClone.clone !== undefined) {
59
+ const patch = (fn) => {
60
+ const boundFn = fn.bind(containerWithClone);
61
+ return async (...args) => {
62
+ const newContainer = await boundFn(...args);
63
+ this.addContainer(newContainer);
64
+ return newContainer;
65
+ };
66
+ };
67
+ containerWithClone.clone = patch(containerWithClone.clone);
68
+ }
69
+ // ignore summarizer
70
+ if (!container.deltaManager.clientDetails.capabilities.interactive &&
71
+ !this.syncSummarizerClients) {
72
+ return;
73
+ }
53
74
  const record = {
54
75
  index: this.containers.size,
55
76
  paused: false,
@@ -72,7 +93,7 @@ class LoaderContainerTracker {
72
93
  trackTrailingNoOps(container, record) {
73
94
  container.deltaManager.outbound.on("op", (messages) => {
74
95
  for (const msg of messages) {
75
- if (msg.type === protocol_definitions_1.MessageType.NoOp) {
96
+ if ((0, driver_utils_1.canBeCoalescedByService)(msg)) {
76
97
  // Track the NoOp that was sent.
77
98
  if (record.trailingNoOps === 0) {
78
99
  // record the starting sequence number of the trailing no ops if we haven't been tracking yet.
@@ -88,15 +109,15 @@ class LoaderContainerTracker {
88
109
  });
89
110
  container.deltaManager.inbound.on("push", (message) => {
90
111
  // Received the no op back, update the record if we are tracking
91
- if (message.type === protocol_definitions_1.MessageType.NoOp
92
- && message.clientId === container.clientId
93
- && record.trailingNoOps !== 0
94
- && record.startTrailingNoOps <= message.clientSequenceNumber) {
112
+ if ((0, driver_utils_1.canBeCoalescedByService)(message) &&
113
+ message.clientId === container.clientId &&
114
+ record.trailingNoOps !== 0 &&
115
+ record.startTrailingNoOps <= message.clientSequenceNumber) {
95
116
  // NoOp might have coalesced and skipped ahead some sequence number
96
117
  // update the record and skip ahead as well
97
118
  const oldStartTrailingNoOps = record.startTrailingNoOps;
98
119
  record.startTrailingNoOps = message.clientSequenceNumber + 1;
99
- record.trailingNoOps -= (record.startTrailingNoOps - oldStartTrailingNoOps);
120
+ record.trailingNoOps -= record.startTrailingNoOps - oldStartTrailingNoOps;
100
121
  }
101
122
  });
102
123
  container.on("disconnected", () => {
@@ -124,25 +145,34 @@ class LoaderContainerTracker {
124
145
  }
125
146
  /**
126
147
  * Make sure all the tracked containers are synchronized.
127
- * - No isDirty (non-readonly) containers
128
- * - No extra clientId in quorum of any container that is not tracked and still opened.
129
- * - i.e. no pending Join/Leave message.
130
- * - No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
131
- * - lastSequenceNumber of all container is the same
132
- * - clientSequenceNumberObserved is the same as clientSequenceNumber sent
133
- * - this overlaps with !isDirty, but include task scheduler ops.
134
- * - Trailing NoOp is tracked and don't count as pending ops.
148
+ *
149
+ * No isDirty (non-readonly) containers
150
+ * No extra clientId in quorum of any container that is not tracked and still opened.
151
+ * - i.e. no pending Join/Leave message.
152
+ * No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
153
+ * lastSequenceNumber of all container is the same
154
+ * clientSequenceNumberObserved is the same as clientSequenceNumber sent
155
+ * - this overlaps with !isDirty, but include task scheduler ops.
156
+ * - Trailing NoOp is tracked and don't count as pending ops.
157
+ *
158
+ * Containers that are already pause will resume process and paused again once
159
+ * everything is synchronized. Containers that aren't paused will remain unpaused when this
160
+ * function returns.
135
161
  */
136
162
  async ensureSynchronized(...containers) {
137
163
  const resumed = this.resumeProcessing(...containers);
138
- let waitingSequenceNumberSynchronized = false;
164
+ let waitingSequenceNumberSynchronized;
139
165
  // eslint-disable-next-line no-constant-condition
140
166
  while (true) {
167
+ // yield a turn to allow side effect of resuming or the ops we just processed execute before we check
168
+ await new Promise((resolve) => {
169
+ setTimeout(resolve, 0);
170
+ });
141
171
  const containersToApply = this.getContainers(containers);
142
172
  if (containersToApply.length === 0) {
143
173
  break;
144
174
  }
145
- // Ignore readonly dirty containers, because it can't sent up and nothing can be done about it being dirty
175
+ // Ignore readonly dirty containers, because it can't sent ops and nothing can be done about it being dirty
146
176
  const dirtyContainers = containersToApply.filter((c) => {
147
177
  const { deltaManager, isDirty } = c;
148
178
  return deltaManager.readOnlyInfo.readonly !== true && isDirty;
@@ -151,37 +181,46 @@ class LoaderContainerTracker {
151
181
  // Wait for all the leave messages
152
182
  const pendingClients = this.getPendingClients(containersToApply);
153
183
  if (pendingClients.length === 0) {
154
- if (this.isSequenceNumberSynchronized(containersToApply)) {
184
+ const needSync = this.needSequenceNumberSynchronize(containersToApply);
185
+ if (needSync === undefined) {
155
186
  // done, we are in sync
156
187
  break;
157
188
  }
158
- if (!waitingSequenceNumberSynchronized) {
159
- // Only write it out once
160
- waitingSequenceNumberSynchronized = true;
161
- debugWait("Waiting for sequence number synchronized");
162
- await this.waitForAnyInboundOps(containersToApply);
189
+ if (waitingSequenceNumberSynchronized !== needSync.reason) {
190
+ // Don't repeat writing to console if it is the same reason
191
+ waitingSequenceNumberSynchronized = needSync.reason;
192
+ debugWait(needSync.message);
163
193
  }
194
+ // Wait for one inbounds ops which might change the state of things
195
+ await (0, timeoutUtils_1.timeoutAwait)(this.waitForAnyInboundOps(containersToApply), {
196
+ errorMsg: `Timeout on ${needSync.message}`,
197
+ });
164
198
  }
165
199
  else {
166
- waitingSequenceNumberSynchronized = false;
167
- await this.waitForPendingClients(pendingClients);
200
+ waitingSequenceNumberSynchronized = undefined;
201
+ await (0, timeoutUtils_1.timeoutAwait)(this.waitForPendingClients(pendingClients), {
202
+ errorMsg: "Timeout on waiting for pending join or leave op",
203
+ });
168
204
  }
169
205
  }
170
206
  else {
171
207
  // Wait for all the containers to be saved
172
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
173
- debugWait(`Waiting container to be saved ${dirtyContainers.map((c) => this.containers.get(c).index)}`);
174
- waitingSequenceNumberSynchronized = false;
175
- await Promise.all(dirtyContainers.map(async (c) => Promise.race([new Promise((resolve) => c.once("saved", resolve)),
176
- new Promise((resolve) => c.once("closed", resolve))])));
208
+ debugWait(`Waiting container to be saved ${this.containerIndexStrings(dirtyContainers)}`);
209
+ waitingSequenceNumberSynchronized = undefined;
210
+ await Promise.all(dirtyContainers.map(async (c) => Promise.race([
211
+ (0, timeoutUtils_1.timeoutPromise)((resolve) => c.once("saved", () => resolve()), {
212
+ errorMsg: "Timeout on waiting a container to be saved",
213
+ }),
214
+ new Promise((resolve) => c.once("closed", resolve)),
215
+ ])));
177
216
  }
178
- // yield a turn to allow side effect of the ops we just processed execute before we check again
179
- await new Promise((resolve) => { setTimeout(resolve, 0); });
180
217
  }
181
218
  // Pause all container that was resumed
182
219
  // don't call pause if resumed is empty and pause everything, which is not what we want
183
220
  if (resumed.length !== 0) {
184
- await this.pauseProcessing(...resumed);
221
+ await (0, timeoutUtils_1.timeoutAwait)(this.pauseProcessing(...resumed), {
222
+ errorMsg: "Timeout on waiting for pausing all resumed containers",
223
+ });
185
224
  }
186
225
  debugWait("Synchronized");
187
226
  }
@@ -202,7 +241,8 @@ class LoaderContainerTracker {
202
241
  const quorum = container.getQuorum();
203
242
  quorum.getMembers().forEach((client, clientId) => {
204
243
  // ignore summarizer
205
- if (!client.client.details.capabilities.interactive && !this.syncSummarizerClients) {
244
+ if (!client.client.details.capabilities.interactive &&
245
+ !this.syncSummarizerClients) {
206
246
  return;
207
247
  }
208
248
  if (!openedClientId.includes(clientId)) {
@@ -221,42 +261,62 @@ class LoaderContainerTracker {
221
261
  *
222
262
  * @param containersToApply - the set of containers to check
223
263
  */
224
- isSequenceNumberSynchronized(containersToApply) {
264
+ needSequenceNumberSynchronize(containersToApply) {
265
+ // If there is a pending proposal, wait for it to be accepted
266
+ const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
267
+ if (minSeqNum < this.lastProposalSeqNum) {
268
+ return {
269
+ reason: "Proposal",
270
+ message: `waiting for MSN to advance to proposal at sequence number ${this.lastProposalSeqNum}`,
271
+ };
272
+ }
225
273
  // clientSequenceNumber check detects ops in flight, both on the wire and in the outbound queue
226
274
  // We need both client sequence number and isDirty check because:
227
275
  // - Currently isDirty flag ignores ops for task scheduler, so we need the client sequence number check
228
276
  // - But isDirty flags include ops during forceReadonly and disconnected, because we don't submit
229
277
  // the ops in the first place, clientSequenceNumber is not assigned
230
- const isClientSequenceNumberSynchronized = containersToApply.every((container) => {
278
+ const containerWithInflightOps = containersToApply.filter((container) => {
231
279
  if (container.deltaManager.readOnlyInfo.readonly === true) {
232
280
  // Ignore readonly container. the clientSeqNum and clientSeqNumObserved might be out of sync
233
281
  // because we transition to readonly when outbound is not empty or the in transit op got lost
234
- return true;
282
+ return false;
235
283
  }
236
284
  // Note that in read only mode, the op won't be submitted
237
285
  let deltaManager = container.deltaManager;
238
286
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
239
287
  const { trailingNoOps } = this.containers.get(container);
240
- // Back-compat: clientSequenceNumber & clientSequenceNumberObserved moved to ConnectionManager in 0.53
288
+ // Back-compat: lastSubmittedClientId/clientSequenceNumber/clientSequenceNumberObserved moved to ConnectionManager in 0.53
241
289
  if (!("clientSequenceNumber" in deltaManager)) {
242
290
  deltaManager = deltaManager.connectionManager;
243
291
  }
244
- (0, common_utils_1.assert)("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
245
- (0, common_utils_1.assert)("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
246
- return deltaManager.clientSequenceNumber ===
247
- deltaManager.clientSequenceNumberObserved + trailingNoOps;
292
+ (0, core_utils_1.assert)("clientSequenceNumber" in deltaManager, "no clientSequenceNumber");
293
+ (0, core_utils_1.assert)("clientSequenceNumberObserved" in deltaManager, "no clientSequenceNumber");
294
+ // If last submittedClientId isn't the current clientId, then we haven't send any ops
295
+ return (deltaManager.lastSubmittedClientId === container.clientId &&
296
+ deltaManager.clientSequenceNumber !==
297
+ deltaManager.clientSequenceNumberObserved + trailingNoOps);
248
298
  });
249
- if (!isClientSequenceNumberSynchronized) {
250
- return false;
251
- }
252
- const minSeqNum = containersToApply[0].deltaManager.minimumSequenceNumber;
253
- if (minSeqNum < this.lastProposalSeqNum) {
254
- // There is an unresolved proposal
255
- return false;
299
+ if (containerWithInflightOps.length !== 0) {
300
+ return {
301
+ reason: "InflightOps",
302
+ message: `waiting for containers with inflight ops: ${this.containerIndexStrings(containerWithInflightOps)}`,
303
+ };
256
304
  }
257
305
  // Check to see if all the container has process the same number of ops.
258
- const seqNum = containersToApply[0].deltaManager.lastSequenceNumber;
259
- return containersToApply.every((c) => c.deltaManager.lastSequenceNumber === seqNum);
306
+ const maxSeqNum = Math.max(...containersToApply.map((c) => c.deltaManager.lastSequenceNumber));
307
+ const containerWithPendingIncoming = containersToApply.filter((c) => c.deltaManager.lastSequenceNumber !== maxSeqNum);
308
+ if (containerWithPendingIncoming.length !== 0) {
309
+ return {
310
+ reason: "Pending",
311
+ message: `waiting for containers with pending incoming ops up to sequence number ${maxSeqNum}: ${this.containerIndexStrings(containerWithPendingIncoming)}`,
312
+ };
313
+ }
314
+ return undefined;
315
+ }
316
+ containerIndexStrings(containers) {
317
+ return containers.map(
318
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
319
+ (c) => this.containers.get(c).index);
260
320
  }
261
321
  /**
262
322
  * Utility to wait for any clientId in quorum that is NOT associated with any container we
@@ -267,7 +327,7 @@ class LoaderContainerTracker {
267
327
  * @param containersToApply - the set of containers to wait for any inbound ops for
268
328
  */
269
329
  async waitForPendingClients(pendingClients) {
270
- const unconnectedClients = Array.from(this.containers.keys()).filter((c) => !c.closed && !c.connected);
330
+ const unconnectedClients = Array.from(this.containers.keys()).filter((c) => !c.closed && c.connectionState !== container_loader_1.ConnectionState.Connected);
271
331
  return Promise.all(pendingClients.map(async ([container, pendingClientId]) => {
272
332
  return new Promise((resolve) => {
273
333
  const cleanup = () => {
@@ -318,7 +378,8 @@ class LoaderContainerTracker {
318
378
  const containersToApply = this.getContainers(containers);
319
379
  for (const container of containersToApply) {
320
380
  const record = this.containers.get(container);
321
- if ((record === null || record === void 0 ? void 0 : record.paused) === true) {
381
+ (0, core_utils_1.assert)(record?.pauseP === undefined, "Cannot resume container while pausing is in progress");
382
+ if (record?.paused === true) {
322
383
  debugWait(`${record.index}: container resumed`);
323
384
  container.deltaManager.inbound.resume();
324
385
  container.deltaManager.outbound.resume();
@@ -331,25 +392,86 @@ class LoaderContainerTracker {
331
392
  /**
332
393
  * Pause all queue activities on the containers given, or all tracked containers
333
394
  * Any containers given that is not tracked will be ignored.
395
+ *
396
+ * When a container is paused, it is assumed that we want fine grain control over op
397
+ * sequencing. This function will prepare the container and force it into write mode to
398
+ * avoid missing join messages or change the sequence of event when switching from read to
399
+ * write mode.
334
400
  */
335
401
  async pauseProcessing(...containers) {
336
- const pauseP = [];
402
+ const waitP = [];
337
403
  const containersToApply = this.getContainers(containers);
338
404
  for (const container of containersToApply) {
339
405
  const record = this.containers.get(container);
340
406
  if (record !== undefined && !record.paused) {
341
- debugWait(`${record.index}: container paused`);
342
- pauseP.push(container.deltaManager.inbound.pause());
343
- pauseP.push(container.deltaManager.outbound.pause());
344
- record.paused = true;
407
+ if (record.pauseP === undefined) {
408
+ record.pauseP = this.pauseContainer(container, record);
409
+ }
410
+ waitP.push(record.pauseP);
345
411
  }
346
412
  }
347
- await Promise.all(pauseP);
413
+ await Promise.all(waitP);
414
+ }
415
+ /**
416
+ * When a container is paused, it is assumed that we want fine grain control over op
417
+ * sequencing. This function will prepare the container and force it into write mode to
418
+ * avoid missing join messages or change the sequence of event when switching from read to
419
+ * write mode.
420
+ *
421
+ * @param container - the container to pause
422
+ * @param record - the record for the container
423
+ */
424
+ async pauseContainer(container, record) {
425
+ debugWait(`${record.index}: pausing container`);
426
+ (0, core_utils_1.assert)(!container.deltaManager.outbound.paused, "Container should not be paused yet");
427
+ (0, core_utils_1.assert)(!container.deltaManager.inbound.paused, "Container should not be paused yet");
428
+ // Pause outbound
429
+ debugWait(`${record.index}: pausing container outbound queues`);
430
+ await container.deltaManager.outbound.pause();
431
+ // Ensure the container is connected first.
432
+ if (container.connectionState !== container_loader_1.ConnectionState.Connected) {
433
+ debugWait(`${record.index}: Wait for container connection`);
434
+ await (0, containerUtils_1.waitForContainerConnection)(container);
435
+ }
436
+ // Check if the container is in write mode
437
+ if (!container.deltaManager.active) {
438
+ let proposalP;
439
+ if (container.deltaManager.outbound.idle) {
440
+ // Need to generate an op to force write mode
441
+ debugWait(`${record.index}: container force write connection`);
442
+ const maybeContainer = container;
443
+ const codeProposal = maybeContainer.getLoadedCodeDetails
444
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
445
+ container.getLoadedCodeDetails()
446
+ : container.chaincodePackage;
447
+ proposalP = container.proposeCodeDetails(codeProposal);
448
+ }
449
+ // Wait for nack
450
+ debugWait(`${record.index}: Wait for container disconnect`);
451
+ container.deltaManager.outbound.resume();
452
+ await new Promise((resolve) => container.once("disconnected", resolve));
453
+ const accepted = proposalP ? await proposalP : false;
454
+ (0, core_utils_1.assert)(!accepted, "A proposal in read mode should be rejected");
455
+ await container.deltaManager.outbound.pause();
456
+ // Ensure the container is reconnect.
457
+ if (container.connectionState !== container_loader_1.ConnectionState.Connected) {
458
+ debugWait(`${record.index}: Wait for container reconnection`);
459
+ await (0, containerUtils_1.waitForContainerConnection)(container);
460
+ }
461
+ }
462
+ debugWait(`${record.index}: pausing container inbound queues`);
463
+ // Pause inbound
464
+ await container.deltaManager.inbound.pause();
465
+ debugWait(`${record.index}: container paused`);
466
+ record.pauseP = undefined;
467
+ record.paused = true;
348
468
  }
349
469
  /**
350
470
  * Pause all queue activities on all tracked containers, and resume only
351
471
  * inbound to process ops until it is idle. All queues are left in the paused state
352
- * after the function
472
+ * after the function.
473
+ *
474
+ * Pausing will switch the container to write mode. See `pauseProcessing`
353
475
  */
354
476
  async processIncoming(...containers) {
355
477
  return this.processQueue(containers, (container) => container.deltaManager.inbound);
@@ -357,7 +479,9 @@ class LoaderContainerTracker {
357
479
  /**
358
480
  * Pause all queue activities on all tracked containers, and resume only
359
481
  * outbound to process ops until it is idle. All queues are left in the paused state
360
- * after the function
482
+ * after the function.
483
+ *
484
+ * Pausing will switch the container to write mode. See `pauseProcessing`
361
485
  */
362
486
  async processOutgoing(...containers) {
363
487
  return this.processQueue(containers, (container) => container.deltaManager.outbound);
@@ -372,6 +496,7 @@ class LoaderContainerTracker {
372
496
  const inflightTracker = new Map();
373
497
  const cleanup = [];
374
498
  for (const container of containersToApply) {
499
+ (0, core_utils_1.assert)(container.deltaManager.active, "Container should be connected in write mode already");
375
500
  const queue = getQueue(container);
376
501
  // track the outgoing ops (if any) to make sure they make the round trip to at least to the same client
377
502
  // to make sure they are sequenced.
@@ -381,7 +506,9 @@ class LoaderContainerTracker {
381
506
  }
382
507
  while (resumed.some((queue) => !queue.idle)) {
383
508
  debugWait("Wait until queue is idle");
384
- await new Promise((resolve) => { setTimeout(resolve, 0); });
509
+ await new Promise((resolve) => {
510
+ setTimeout(resolve, 0);
511
+ });
385
512
  }
386
513
  // Make sure all the op that we sent out are acked first
387
514
  // This is no op if we are processing incoming
@@ -405,15 +532,15 @@ class LoaderContainerTracker {
405
532
  setupInOutTracker(container, inflightTracker) {
406
533
  const outHandler = (messages) => {
407
534
  for (const message of messages) {
408
- if (message.type !== protocol_definitions_1.MessageType.NoOp) {
535
+ if (!(0, driver_utils_1.canBeCoalescedByService)(message)) {
409
536
  inflightTracker.set(container, message.clientSequenceNumber);
410
537
  }
411
538
  }
412
539
  };
413
540
  const inHandler = (message) => {
414
- if (message.type !== protocol_definitions_1.MessageType.NoOp
415
- && message.clientId === container.clientId
416
- && inflightTracker.get(container) === message.clientSequenceNumber) {
541
+ if (!(0, driver_utils_1.canBeCoalescedByService)(message) &&
542
+ message.clientId === container.clientId &&
543
+ inflightTracker.get(container) === message.clientSequenceNumber) {
417
544
  inflightTracker.delete(container);
418
545
  }
419
546
  };
@@ -430,7 +557,6 @@ class LoaderContainerTracker {
430
557
  setupTrace(container, index) {
431
558
  if (debugOp.enabled) {
432
559
  const getContentsString = (type, msgContents) => {
433
- var _a, _b;
434
560
  try {
435
561
  if (type !== protocol_definitions_1.MessageType.Operation) {
436
562
  if (typeof msgContents === "string") {
@@ -441,14 +567,13 @@ class LoaderContainerTracker {
441
567
  let address = "";
442
568
  // contents comes in the wire as JSON string ("push" event)
443
569
  // But already parsed when apply ("op" event)
444
- let contents = typeof msgContents === "string" ?
445
- JSON.parse(msgContents) : msgContents;
570
+ let contents = typeof msgContents === "string" ? JSON.parse(msgContents) : msgContents;
446
571
  while (contents !== undefined && contents !== null) {
447
- if (((_a = contents.contents) === null || _a === void 0 ? void 0 : _a.address) !== undefined) {
572
+ if (contents.contents?.address !== undefined) {
448
573
  address += `/${contents.contents.address}`;
449
574
  contents = contents.contents.contents;
450
575
  }
451
- else if (((_b = contents.content) === null || _b === void 0 ? void 0 : _b.address) !== undefined) {
576
+ else if (contents.content?.address !== undefined) {
452
577
  address += `/${contents.content.address}`;
453
578
  contents = contents.content.contents;
454
579
  }
@@ -468,19 +593,22 @@ class LoaderContainerTracker {
468
593
  debugOp(`${index}: ADD: clientId: ${container.clientId}`);
469
594
  container.deltaManager.outbound.on("op", (messages) => {
470
595
  for (const msg of messages) {
471
- debugOp(`${index}: OUT: `
472
- + `cli: ${msg.clientSequenceNumber.toString().padStart(3)} `
473
- + `rsq: ${msg.referenceSequenceNumber.toString().padStart(3)} `
474
- + `${msg.type} ${getContentsString(msg.type, msg.contents)}`);
596
+ debugOp(`${index}: OUT: ` +
597
+ `cli: ${msg.clientSequenceNumber.toString().padStart(3)} ` +
598
+ `rsq: ${msg.referenceSequenceNumber.toString().padStart(3)} ` +
599
+ `${msg.type} ${getContentsString(msg.type, msg.contents)}`);
475
600
  }
476
601
  });
477
602
  const getInboundHandler = (type) => {
478
603
  return (msg) => {
479
- const clientSeq = msg.clientId === container.clientId ?
480
- `cli: ${msg.clientSequenceNumber.toString().padStart(3)}` : " ";
481
- debugOp(`${index}: ${type}: seq: ${msg.sequenceNumber.toString().padStart(3)} `
482
- + `${clientSeq} min: ${msg.minimumSequenceNumber.toString().padStart(3)} `
483
- + `${msg.type} ${getContentsString(msg.type, msg.contents)}`);
604
+ const clientSeq = msg.clientId === container.clientId
605
+ ? `cli: ${msg.clientSequenceNumber.toString().padStart(3)}`
606
+ : " ";
607
+ debugOp(`${index}: ${type}: seq: ${msg.sequenceNumber.toString().padStart(3)} ` +
608
+ `${clientSeq} min: ${msg.minimumSequenceNumber
609
+ .toString()
610
+ .padStart(3)} ` +
611
+ `${msg.type} ${getContentsString(msg.type, msg.contents)}`);
484
612
  };
485
613
  };
486
614
  container.deltaManager.inbound.on("push", getInboundHandler("IN "));