@fluidframework/presence 2.70.0-361788 → 2.71.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 (49) hide show
  1. package/dist/getPresence.d.ts +7 -0
  2. package/dist/getPresence.d.ts.map +1 -1
  3. package/dist/getPresence.js +66 -3
  4. package/dist/getPresence.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/legacy.alpha.d.ts +86 -0
  10. package/dist/package.json +4 -0
  11. package/dist/packageVersion.d.ts +9 -0
  12. package/dist/packageVersion.d.ts.map +1 -0
  13. package/dist/packageVersion.js +12 -0
  14. package/dist/packageVersion.js.map +1 -0
  15. package/dist/presenceDatastoreManager.d.ts +71 -4
  16. package/dist/presenceDatastoreManager.d.ts.map +1 -1
  17. package/dist/presenceDatastoreManager.js +365 -71
  18. package/dist/presenceDatastoreManager.js.map +1 -1
  19. package/dist/presenceManager.d.ts.map +1 -1
  20. package/dist/presenceManager.js +4 -3
  21. package/dist/presenceManager.js.map +1 -1
  22. package/dist/protocol.d.ts +9 -0
  23. package/dist/protocol.d.ts.map +1 -1
  24. package/dist/protocol.js.map +1 -1
  25. package/lib/getPresence.d.ts +7 -0
  26. package/lib/getPresence.d.ts.map +1 -1
  27. package/lib/getPresence.js +65 -3
  28. package/lib/getPresence.js.map +1 -1
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.d.ts.map +1 -1
  31. package/lib/index.js +1 -1
  32. package/lib/index.js.map +1 -1
  33. package/lib/legacy.alpha.d.ts +86 -0
  34. package/lib/packageVersion.d.ts +9 -0
  35. package/lib/packageVersion.d.ts.map +1 -0
  36. package/lib/packageVersion.js +9 -0
  37. package/lib/packageVersion.js.map +1 -0
  38. package/lib/presenceDatastoreManager.d.ts +71 -4
  39. package/lib/presenceDatastoreManager.d.ts.map +1 -1
  40. package/lib/presenceDatastoreManager.js +364 -70
  41. package/lib/presenceDatastoreManager.js.map +1 -1
  42. package/lib/presenceManager.d.ts.map +1 -1
  43. package/lib/presenceManager.js +4 -3
  44. package/lib/presenceManager.js.map +1 -1
  45. package/lib/protocol.d.ts +9 -0
  46. package/lib/protocol.d.ts.map +1 -1
  47. package/lib/protocol.js.map +1 -1
  48. package/package.json +39 -23
  49. package/lib/tsdoc-metadata.json +0 -11
@@ -4,7 +4,7 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.PresenceDatastoreManagerImpl = exports.isValueDirectory = void 0;
7
+ exports.PresenceDatastoreManagerImpl = exports.broadcastJoinResponseDelaysMs = exports.isValueDirectory = void 0;
8
8
  const internal_1 = require("@fluidframework/core-utils/internal");
9
9
  const internalUtils_js_1 = require("./internalUtils.js");
10
10
  const presenceStates_js_1 = require("./presenceStates.js");
@@ -57,6 +57,27 @@ function mergeGeneralDatastoreMessageContent(base, newData) {
57
57
  }
58
58
  return queueDatastore;
59
59
  }
60
+ /**
61
+ * Delays used for broadcasting join responses to clients.
62
+ *
63
+ * @remarks
64
+ * Exported for test coordination.
65
+ * These could be made customizable in the future to accommodate different
66
+ * session configurations.
67
+ */
68
+ exports.broadcastJoinResponseDelaysMs = {
69
+ /**
70
+ * The delay in milliseconds before a join response is sent to any client.
71
+ * This is used to accumulate other join response requests and reduce
72
+ * network traffic.
73
+ */
74
+ namedResponder: 200,
75
+ /**
76
+ * The additional delay in milliseconds a backup responder waits before sending
77
+ * a join response to allow others to respond first.
78
+ */
79
+ backupResponderIncrement: 40,
80
+ };
60
81
  /**
61
82
  * Manages singleton datastore for all Presence.
62
83
  */
@@ -69,22 +90,123 @@ class PresenceDatastoreManagerImpl {
69
90
  this.presence = presence;
70
91
  this.averageLatency = 0;
71
92
  this.returnedMessages = 0;
72
- this.refreshBroadcastRequested = false;
73
- this.timer = new timerManager_js_1.TimerManager();
93
+ this.sendMessageTimer = new timerManager_js_1.TimerManager();
74
94
  this.workspaces = new Map();
95
+ /**
96
+ * Map of outstanding broadcast (join response) requests.
97
+ */
98
+ this.broadcastRequests = new Map();
99
+ /**
100
+ * Timer for managing broadcast (join response) request timing.
101
+ */
102
+ this.broadcastRequestsTimer = new timerManager_js_1.TimerManager();
103
+ /**
104
+ * Broadcasts a join response (complete datastore update message)
105
+ * if there is an outstanding join response request.
106
+ */
107
+ this.sendJoinResponseIfStillNeeded = () => {
108
+ // Make sure we are currently connected and a broadcast is still needed.
109
+ // If not connected, nothing we can do.
110
+ if (this.runtime.getJoinedStatus() !== "disconnected" && this.broadcastRequests.size > 0) {
111
+ // Confirm that of remaining requests, now is the time to respond.
112
+ const now = Date.now();
113
+ let minResponseTime = Number.POSITIVE_INFINITY;
114
+ for (const { deadlineTime } of this.broadcastRequests.values()) {
115
+ minResponseTime = Math.min(minResponseTime, deadlineTime);
116
+ }
117
+ if (minResponseTime <= now) {
118
+ if (this.reasonForCompleteSnapshot) {
119
+ this.broadcastAllKnownState();
120
+ }
121
+ }
122
+ else {
123
+ // No response needed yet - schedule a later attempt
124
+ this.broadcastRequestsTimer.setTimeout(this.sendJoinResponseIfStillNeeded, minResponseTime - now);
125
+ }
126
+ }
127
+ };
75
128
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
76
129
  this.datastore = { "system:presence": systemWorkspaceDatastore };
77
130
  this.workspaces.set("system:presence", systemWorkspace);
78
131
  this.targetedSignalSupport = this.runtime.supportedFeatures.has("submit_signals_v2");
132
+ // If audience member is removed, they won't need a broadcast response.
133
+ this.runtime.getAudience().on("removeMember", (clientId) => {
134
+ this.broadcastRequests.delete(clientId);
135
+ });
136
+ }
137
+ getAudienceInformation(selfClientId) {
138
+ const audience = this.runtime.getAudience();
139
+ const members = audience.getMembers();
140
+ const all = new Set();
141
+ const writers = new Set();
142
+ const selfPresent = members.has(selfClientId);
143
+ if (selfPresent) {
144
+ // Remove self
145
+ members.delete(selfClientId);
146
+ }
147
+ // Gather interactive client IDs
148
+ for (const [id, client] of members) {
149
+ if (client.details.capabilities.interactive) {
150
+ all.add(id);
151
+ if (client.mode === "write") {
152
+ writers.add(id);
153
+ }
154
+ }
155
+ }
156
+ return {
157
+ audience,
158
+ selfPresent,
159
+ interactiveMembersExcludingSelf: {
160
+ all,
161
+ writers,
162
+ },
163
+ };
79
164
  }
80
- joinSession(clientId) {
165
+ joinSession(selfClientId, alternateProvider = undefined) {
166
+ // Before broadcasting the join message, check that there is at least
167
+ // one audience member present (self or another). This is useful to
168
+ // optimize join messages while not using targeted join responses.
169
+ // (We need at least one other to be able to elect them as update
170
+ // provider.)
171
+ // Lack of anyone likely means that this client is very freshly joined
172
+ // and has not received any Join Signals (type="join") from the service
173
+ // yet.
174
+ const { audience, selfPresent, interactiveMembersExcludingSelf } = this.getAudienceInformation(selfClientId);
175
+ if (interactiveMembersExcludingSelf.all.size === 0 && alternateProvider !== undefined) {
176
+ if (selfPresent) {
177
+ // If there aren't any members connected except self, then this client
178
+ // must have complete information.
179
+ this.reasonForCompleteSnapshot = "alone";
180
+ // It would be possible to return at this time and skip ClientJoin
181
+ // signal. Instead continue in case audience information is
182
+ // inaccurate. This client might temporarily erroneously believe it
183
+ // has complete information, but the other(s) should respond to
184
+ // ClientJoin soon rectifying that and covering for bad incomplete
185
+ // responses this client sent in the meantime.
186
+ }
187
+ else {
188
+ // No one is known. Not even self. Defer judgement on
189
+ // complete snapshot until at least self is known to be present.
190
+ this.listenForSelfInAudience(selfClientId, audience);
191
+ return;
192
+ }
193
+ }
81
194
  // Broadcast join message to all clients
82
- const updateProviders = [...this.runtime.getQuorum().getMembers().keys()].filter((quorumClientId) => quorumClientId !== clientId);
195
+ // Select primary update providers
196
+ // Use write members if any, then fallback to read-only members.
197
+ const updateProviders = [
198
+ ...(interactiveMembersExcludingSelf.writers.size > 0
199
+ ? interactiveMembersExcludingSelf.writers
200
+ : interactiveMembersExcludingSelf.all),
201
+ ];
83
202
  // Limit to three providers to prevent flooding the network.
84
203
  // If none respond, others present will (should) after a delay.
85
204
  if (updateProviders.length > 3) {
86
205
  updateProviders.length = 3;
87
206
  }
207
+ else if (updateProviders.length === 0 && alternateProvider !== undefined) {
208
+ updateProviders.push(alternateProvider);
209
+ }
88
210
  this.runtime.submitSignal({
89
211
  type: protocol_js_1.joinMessageType,
90
212
  content: {
@@ -94,6 +216,58 @@ class PresenceDatastoreManagerImpl {
94
216
  updateProviders,
95
217
  },
96
218
  });
219
+ this.logger?.sendTelemetryEvent({
220
+ eventName: "JoinRequested",
221
+ details: {
222
+ attendeeId: this.attendeeId,
223
+ connectionId: selfClientId,
224
+ // Empty updateProviders is indicative of join when alone.
225
+ updateProviders: JSON.stringify(updateProviders),
226
+ // If false and providers is single entry, then join was probably forced.
227
+ selfPresent,
228
+ },
229
+ });
230
+ }
231
+ listenForSelfInAudience(selfClientId, audience) {
232
+ this.logger?.sendTelemetryEvent({
233
+ eventName: "JoinDeferred",
234
+ details: {
235
+ attendeeId: this.attendeeId,
236
+ connectionId: selfClientId,
237
+ },
238
+ });
239
+ // Prepare to join once self audience member joins.
240
+ // Alternatively, processSignal may force a join when a presence
241
+ // signal is received even without audience members (assumes
242
+ // audience signals were lost).
243
+ const joinWhenSelfAudienceMemberAdded = (addedClientId) => {
244
+ if (addedClientId !== selfClientId) {
245
+ // Keep listening
246
+ return;
247
+ }
248
+ // No need to force here by providing alternate provider as self is
249
+ // now present.
250
+ // Do avoid forcing so that reasonForCompleteSnapshot is set correctly
251
+ // if no others have been added.
252
+ this.stopWaitingAndJoin(selfClientId, /* alternateProvider */ undefined);
253
+ };
254
+ audience.on("addMember", joinWhenSelfAudienceMemberAdded);
255
+ this.stopWaitingForSelfInAudience = () => {
256
+ audience.off("addMember", joinWhenSelfAudienceMemberAdded);
257
+ };
258
+ }
259
+ stopWaitingAndJoin(selfClientId, alternateProvider) {
260
+ this.stopWaitingForSelfInAudience?.();
261
+ this.stopWaitingForSelfInAudience = undefined;
262
+ // Confirm not currently disconnected
263
+ if (this.runtime.getJoinedStatus() !== "disconnected") {
264
+ this.joinSession(selfClientId, alternateProvider);
265
+ }
266
+ }
267
+ onDisconnected() {
268
+ delete this.reasonForCompleteSnapshot;
269
+ this.stopWaitingForSelfInAudience?.();
270
+ this.stopWaitingForSelfInAudience = undefined;
97
271
  }
98
272
  getWorkspace(internalWorkspaceAddress, requestedContent, controls) {
99
273
  const existing = this.workspaces.get(internalWorkspaceAddress);
@@ -130,9 +304,14 @@ class PresenceDatastoreManagerImpl {
130
304
  * the send timer, other messages in the queue, the configured allowed latency, etc.
131
305
  */
132
306
  enqueueMessage(data, options) {
133
- // Merging the message with any queued messages effectively queues the message.
134
- // It is OK to queue all incoming messages as long as when we send, we send the queued data.
135
- this.queuedData = mergeGeneralDatastoreMessageContent(this.queuedData, data);
307
+ if (this.queuedData !== "sendAll") {
308
+ this.queuedData =
309
+ data === "sendAll"
310
+ ? "sendAll"
311
+ : // Merging the message with any queued messages effectively queues the message.
312
+ // It is OK to queue all incoming messages as long as when we send, we send the queued data.
313
+ mergeGeneralDatastoreMessageContent(this.queuedData, data);
314
+ }
136
315
  const { allowableUpdateLatencyMs } = options;
137
316
  const now = Date.now();
138
317
  const thisMessageDeadline = now + allowableUpdateLatencyMs;
@@ -140,20 +319,20 @@ class PresenceDatastoreManagerImpl {
140
319
  // If the timer has not expired, we can short-circuit because the timer will fire
141
320
  // and cover this update. In other words, queuing this will be fast enough to
142
321
  // meet its deadline, because a timer is already scheduled to fire before its deadline.
143
- !this.timer.hasExpired() &&
322
+ !this.sendMessageTimer.hasExpired() &&
144
323
  // If the deadline for this message is later than the overall send deadline, then
145
324
  // we can exit early since a timer will take care of sending it.
146
- thisMessageDeadline >= this.timer.expireTime) {
325
+ thisMessageDeadline >= this.sendMessageTimer.expireTime) {
147
326
  return;
148
327
  }
149
328
  // Either we need to send this message immediately, or we need to schedule a timer
150
329
  // to fire at the send deadline that will take care of it.
151
- // Note that timeoutInMs === allowableUpdateLatency, but the calculation is done this way for clarity.
330
+ // Note that timeoutInMs === allowableUpdateLatencyMs, but the calculation is done this way for clarity.
152
331
  const timeoutInMs = thisMessageDeadline - now;
153
332
  const scheduleForLater = timeoutInMs > 0;
154
333
  if (scheduleForLater) {
155
334
  // Schedule the queued messages to be sent at the updateDeadline
156
- this.timer.setTimeout(this.sendQueuedMessage.bind(this), timeoutInMs);
335
+ this.sendMessageTimer.setTimeout(this.sendQueuedMessage.bind(this), timeoutInMs);
157
336
  }
158
337
  else {
159
338
  this.sendQueuedMessage();
@@ -163,7 +342,7 @@ class PresenceDatastoreManagerImpl {
163
342
  * Send any queued signal immediately. Does nothing if no message is queued.
164
343
  */
165
344
  sendQueuedMessage() {
166
- this.timer.clearTimeout();
345
+ this.sendMessageTimer.clearTimeout();
167
346
  if (this.queuedData === undefined) {
168
347
  return;
169
348
  }
@@ -174,12 +353,16 @@ class PresenceDatastoreManagerImpl {
174
353
  this.queuedData = undefined;
175
354
  return;
176
355
  }
356
+ if (this.queuedData === "sendAll") {
357
+ this.broadcastAllKnownState();
358
+ return;
359
+ }
177
360
  const clientConnectionId = this.runtime.getClientId();
178
361
  (0, internal_1.assert)(clientConnectionId !== undefined, 0xa59 /* Client connected without clientId */);
179
362
  const currentClientToSessionValueState =
180
363
  // When connected, `clientToSessionId` must always have current connection entry.
181
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
182
364
  this.datastore["system:presence"].clientToSessionId[clientConnectionId];
365
+ (0, internal_1.assert)(currentClientToSessionValueState !== undefined, "Client connection update missing");
183
366
  const newMessage = {
184
367
  sendTimestamp: Date.now(),
185
368
  avgLatency: this.averageLatency,
@@ -252,16 +435,50 @@ class PresenceDatastoreManagerImpl {
252
435
  return valueData;
253
436
  }
254
437
  broadcastAllKnownState() {
438
+ const content = {
439
+ sendTimestamp: Date.now(),
440
+ avgLatency: this.averageLatency,
441
+ isComplete: true,
442
+ data: this.stripValidationMetadata(this.datastore),
443
+ };
444
+ const primaryRequestors = [];
445
+ const secondaryRequestors = [];
446
+ if (this.broadcastRequests.size > 0) {
447
+ content.joinResponseFor = [...this.broadcastRequests.keys()];
448
+ if (this.logger) {
449
+ // Build telemetry data
450
+ for (const [requestor, { responseOrder }] of this.broadcastRequests.entries()) {
451
+ if (responseOrder === undefined) {
452
+ primaryRequestors.push(requestor);
453
+ }
454
+ else {
455
+ secondaryRequestors.push([requestor, responseOrder]);
456
+ }
457
+ }
458
+ }
459
+ this.broadcastRequests.clear();
460
+ }
461
+ // This broadcast will satisfy all requests; clear any remaining timer.
462
+ this.broadcastRequestsTimer.clearTimeout();
463
+ this.sendMessageTimer.clearTimeout();
255
464
  this.runtime.submitSignal({
256
465
  type: protocol_js_1.datastoreUpdateMessageType,
257
- content: {
258
- sendTimestamp: Date.now(),
259
- avgLatency: this.averageLatency,
260
- isComplete: true,
261
- data: this.stripValidationMetadata(this.datastore),
262
- },
466
+ content,
263
467
  });
264
- this.refreshBroadcastRequested = false;
468
+ if (content.joinResponseFor) {
469
+ this.logger?.sendTelemetryEvent({
470
+ eventName: "JoinResponse",
471
+ details: {
472
+ type: "broadcastAll",
473
+ attendeeId: this.attendeeId,
474
+ connectionId: this.runtime.getClientId(),
475
+ primaryResponses: JSON.stringify(primaryRequestors),
476
+ secondaryResponses: JSON.stringify(secondaryRequestors),
477
+ },
478
+ });
479
+ }
480
+ // Sending all must account for anything queued before.
481
+ this.queuedData = undefined;
265
482
  }
266
483
  processSignal(message, local, optional) {
267
484
  const received = Date.now();
@@ -282,8 +499,22 @@ class PresenceDatastoreManagerImpl {
282
499
  this.returnedMessages;
283
500
  return;
284
501
  }
502
+ const selfClientId = this.runtime.getClientId();
503
+ (0, internal_1.assert)(selfClientId !== undefined, "Received signal without clientId");
504
+ // Check for undesired case of receiving a remote presence signal
505
+ // without having been alerted to self audience join. (Perhaps join
506
+ // signal was dropped.)
507
+ // In practice it is commonly observed that local signals can be
508
+ // returned ahead of audience join notification. So, it is reasonable
509
+ // to expect that audience join notification may be delayed until after
510
+ // other presence signals are received. One is enough to get things
511
+ // rolling.
512
+ if (this.stopWaitingForSelfInAudience !== undefined) {
513
+ this.stopWaitingAndJoin(selfClientId, /* alternateProvider */ message.clientId);
514
+ }
285
515
  const timeModifier = received -
286
516
  (this.averageLatency + message.content.avgLatency + message.content.sendTimestamp);
517
+ const postUpdateActions = [];
287
518
  if (message.type === protocol_js_1.joinMessageType) {
288
519
  // It is possible for some signals to come in while client is not connected due
289
520
  // to how work is scheduled. If we are not connected, we can't respond to the
@@ -295,8 +526,48 @@ class PresenceDatastoreManagerImpl {
295
526
  // connected.
296
527
  }
297
528
  else {
298
- if (message.content.isComplete) {
299
- this.refreshBroadcastRequested = false;
529
+ // Update join response requests that are now satisfied.
530
+ const joinResponseFor = message.content.joinResponseFor;
531
+ if (joinResponseFor) {
532
+ let justGainedCompleteSnapshot = false;
533
+ if (joinResponseFor.includes(selfClientId)) {
534
+ if (this.reasonForCompleteSnapshot) {
535
+ if (this.reasonForCompleteSnapshot === "alone") {
536
+ // No response was expected. This might happen when
537
+ // either cautionary ClientJoin signal is received
538
+ // by audience member that was unknown.
539
+ this.logger?.sendTelemetryEvent({
540
+ eventName: "JoinResponseWhenAlone",
541
+ details: {
542
+ attendeeId: this.attendeeId,
543
+ connectionId: this.runtime.getClientId(),
544
+ },
545
+ });
546
+ }
547
+ }
548
+ else {
549
+ // If we are the intended recipient of the join response,
550
+ // we can consider our knowledge complete and can respond
551
+ // to others join requests.
552
+ justGainedCompleteSnapshot = true;
553
+ }
554
+ this.reasonForCompleteSnapshot = "join response";
555
+ }
556
+ if (this.broadcastRequests.size > 0) {
557
+ for (const responseFor of joinResponseFor) {
558
+ this.broadcastRequests.delete(responseFor);
559
+ }
560
+ if (this.broadcastRequests.size === 0) {
561
+ // If no more requests are pending, clear any timer.
562
+ this.broadcastRequestsTimer.clearTimeout();
563
+ }
564
+ else if (justGainedCompleteSnapshot) {
565
+ // May or may not be time to respond to remaining requests.
566
+ // Clear the timer and recheck after processing.
567
+ this.broadcastRequestsTimer.clearTimeout();
568
+ postUpdateActions.push(this.sendJoinResponseIfStillNeeded);
569
+ }
570
+ }
300
571
  }
301
572
  // If the message requests an acknowledgement, we will send a targeted acknowledgement message back to just the requestor.
302
573
  if (message.content.acknowledgementId !== undefined) {
@@ -326,7 +597,6 @@ class PresenceDatastoreManagerImpl {
326
597
  const internalWorkspaceType = internalWorkspaceTypes[prefix] ?? "Unknown";
327
598
  this.events.emit("workspaceActivated", publicWorkspaceAddress, internalWorkspaceType);
328
599
  }
329
- const postUpdateActions = [];
330
600
  // While the system workspace is processed here too, it is declared as
331
601
  // conforming to the general schema. So drop its override.
332
602
  const data = message.content.data;
@@ -362,67 +632,91 @@ class PresenceDatastoreManagerImpl {
362
632
  * correlation with other telemetry where it is often called just `clientId`.
363
633
  */
364
634
  prepareJoinResponse(updateProviders, requestor) {
365
- this.refreshBroadcastRequested = true;
366
635
  // We must be connected to receive this message, so clientId should be defined.
367
- // If it isn't then, not really a problem; just won't be in provider or quorum list.
636
+ // If it isn't then, not really a problem; just won't be in provider or audience list.
368
637
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
369
- const clientId = this.runtime.getClientId();
370
- // const requestor = message.clientId;
371
- if (updateProviders.includes(clientId)) {
372
- // Send all current state to the new client
373
- this.broadcastAllKnownState();
374
- this.logger?.sendTelemetryEvent({
375
- eventName: "JoinResponse",
376
- details: {
377
- type: "broadcastAll",
378
- requestor,
379
- role: "primary",
380
- },
381
- });
382
- }
383
- else {
638
+ const selfClientId = this.runtime.getClientId();
639
+ let joinResponseDelayMs = exports.broadcastJoinResponseDelaysMs.namedResponder;
640
+ let relativeResponseOrder;
641
+ if (!updateProviders.includes(selfClientId)) {
384
642
  // Schedule a broadcast to the new client after a delay only to send if
385
- // another broadcast hasn't been seen in the meantime. The delay is based
386
- // on the position in the quorum list. It doesn't have to be a stable
387
- // list across all clients. We need something to provide suggested order
388
- // to prevent a flood of broadcasts.
389
- let relativeResponseOrder;
643
+ // another broadcast satisfying the request hasn't been seen in the
644
+ // meantime. The delay is based on the position in the quorum list. It
645
+ // doesn't have to be a stable list across all clients. We need
646
+ // something to provide suggested order to prevent a flood of broadcasts.
390
647
  const quorumMembers = this.runtime.getQuorum().getMembers();
391
- const self = quorumMembers.get(clientId);
648
+ const self = quorumMembers.get(selfClientId);
392
649
  if (self) {
393
650
  // Compute order quorum join order (indicated by sequenceNumber)
394
651
  relativeResponseOrder = 0;
395
- for (const { sequenceNumber } of quorumMembers.values()) {
396
- if (sequenceNumber < self.sequenceNumber) {
652
+ for (const { client, sequenceNumber } of quorumMembers.values()) {
653
+ if (sequenceNumber < self.sequenceNumber &&
654
+ client.details.capabilities.interactive) {
397
655
  relativeResponseOrder++;
398
656
  }
399
657
  }
400
658
  }
401
659
  else {
402
660
  // Order past quorum members + arbitrary additional offset up to 10
403
- relativeResponseOrder = quorumMembers.size + Math.random() * 10;
404
- }
405
- // These numbers have been chosen arbitrarily to start with.
406
- // 20 is minimum wait time, 20 is the additional wait time per provider
407
- // given an chance before us with named providers given more time.
408
- const waitTime = 20 + 20 * (3 * updateProviders.length + relativeResponseOrder);
409
- setTimeout(() => {
410
- // Make sure a broadcast is still needed and we are currently connected.
411
- // If not connected, nothing we can do.
412
- if (this.refreshBroadcastRequested &&
413
- this.runtime.getJoinedStatus() !== "disconnected") {
414
- this.broadcastAllKnownState();
415
- this.logger?.sendTelemetryEvent({
416
- eventName: "JoinResponse",
417
- details: {
418
- type: "broadcastAll",
419
- requestor,
420
- role: "secondary",
421
- order: relativeResponseOrder,
422
- },
423
- });
661
+ let possibleQuorumRespondents = 0;
662
+ for (const { client } of quorumMembers.values()) {
663
+ if (client.details.capabilities.interactive) {
664
+ possibleQuorumRespondents++;
665
+ }
424
666
  }
425
- }, waitTime);
667
+ relativeResponseOrder = possibleQuorumRespondents + Math.random() * 10;
668
+ }
669
+ // When not named to provide update, wait an additional amount
670
+ // of time for those named or others to respond.
671
+ joinResponseDelayMs +=
672
+ exports.broadcastJoinResponseDelaysMs.backupResponderIncrement *
673
+ (3 * updateProviders.length + relativeResponseOrder);
674
+ }
675
+ // Add the requestor to the list of clients that will receive the broadcast.
676
+ const deadlineTime = Date.now() + joinResponseDelayMs;
677
+ this.broadcastRequests.set(requestor, {
678
+ deadlineTime,
679
+ responseOrder: relativeResponseOrder,
680
+ });
681
+ if (!this.reasonForCompleteSnapshot) {
682
+ // Check if requestor count meets or exceeds count of other audience
683
+ // members indicating that we effectively have a complete snapshot
684
+ // (once the current message being processed is processed).
685
+ const { selfPresent, interactiveMembersExcludingSelf } = this.getAudienceInformation(selfClientId);
686
+ if (
687
+ // Self-present check is done to help ensure that audience
688
+ // information is accurate. If self is not present, audience
689
+ // information might be incomplete.
690
+ selfPresent &&
691
+ this.broadcastRequests.size >= interactiveMembersExcludingSelf.all.size) {
692
+ // Note that no action is taken here specifically.
693
+ // We want action to be queued so that it takes place after
694
+ // current message is completely processed. All of the actions
695
+ // below should be delayed (not immediate).
696
+ this.reasonForCompleteSnapshot = "full requests";
697
+ }
698
+ }
699
+ // Check if capable of full primary response. If requested to provide
700
+ // primary response, but do not yet have complete snapshot, we need to
701
+ // delay a full response, until we think we have complete snapshot. In
702
+ // the meantime we will send partial updates as usual.
703
+ if (this.reasonForCompleteSnapshot && updateProviders.includes(selfClientId)) {
704
+ // Use regular message queue to handle timing of the broadcast.
705
+ // Any more immediate broadcasts will accelerate the response time.
706
+ // As a primary responder, it is expected that broadcast will happen and
707
+ // using the regular queue allows other updates to avoid merge work.
708
+ this.enqueueMessage("sendAll", {
709
+ allowableUpdateLatencyMs: joinResponseDelayMs,
710
+ });
711
+ }
712
+ else {
713
+ // Check if there isn't already a timer scheduled to send a join
714
+ // response with in this request's deadline.
715
+ if (this.broadcastRequestsTimer.hasExpired() ||
716
+ deadlineTime < this.broadcastRequestsTimer.expireTime) {
717
+ // Set or update the timer.
718
+ this.broadcastRequestsTimer.setTimeout(this.sendJoinResponseIfStillNeeded, joinResponseDelayMs);
719
+ }
426
720
  }
427
721
  }
428
722
  }