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