@fluidframework/test-utils 2.0.0-rc.1.0.4 → 2.0.0-rc.2.0.0

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