Skip to content

Commit 6174e00

Browse files
authored
Add callIdHistory to context + internal context (#1817)
* - Add callIdHistory to context + internal context - Update all visit to callId using latestCallId - Encapsule the access to internalMap directly to ensure no leaks
1 parent 5cf76e2 commit 6174e00

6 files changed

Lines changed: 92 additions & 38 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "- Add callIdHistory to context + internal context - Update all visit to callId using latestCallId - Encapsule the access to internalMap directly to ensure no leaks",
4+
"packageName": "@internal/calling-stateful-client",
5+
"email": "jiangnanhello@live.com",
6+
"dependentChangeType": "patch"
7+
}

packages/calling-stateful-client/src/CallAgentDeclarative.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ describe('declarative call agent', () => {
214214
mockCallAgent.calls = [];
215215
callAgentDeclaratify(mockCallAgent, context, internalContext);
216216
expect(Object.keys(context.getState().calls).length).toBe(0);
217-
expect(internalContext.getRemoteRenderInfos().size).toBe(0);
217+
expect(Array.from(internalContext.getCallIds()).length).toBe(0);
218218
});
219219

220220
test('should update state with new call when startCall is invoked', () => {

packages/calling-stateful-client/src/CallContext.ts

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
CallError
2727
} from './CallClientState';
2828
import { callingStatefulLogger } from './Logger';
29+
import { CallIdHistory } from './CallIdHistory';
2930

3031
enableMapSet();
3132
// Needed to generate state diff for verbose logging.
@@ -45,6 +46,7 @@ export class CallContext {
4546
private _state: CallClientState;
4647
private _emitter: EventEmitter;
4748
private _atomicId: number;
49+
private _callIdHistory: CallIdHistory = new CallIdHistory();
4850

4951
constructor(userId: CommunicationIdentifierKind, maxListeners = 50) {
5052
this._logger = createClientLogger('communication-react:calling-context');
@@ -114,7 +116,8 @@ export class CallContext {
114116

115117
public setCall(call: CallState): void {
116118
this.modifyState((draft: CallClientState) => {
117-
const existingCall = draft.calls[call.id];
119+
const latestCallId = this._callIdHistory.latestCallId(call.id);
120+
const existingCall = draft.calls[latestCallId];
118121
if (existingCall) {
119122
existingCall.callerInfo = call.callerInfo;
120123
existingCall.state = call.state;
@@ -128,44 +131,46 @@ export class CallContext {
128131
existingCall.recording.isRecordingActive = call.recording.isRecordingActive;
129132
// We don't update the startTime and endTime if we are updating an existing active call
130133
} else {
131-
draft.calls[call.id] = call;
134+
draft.calls[latestCallId] = call;
132135
}
133136
});
134137
}
135138

136139
public removeCall(callId: string): void {
137140
this.modifyState((draft: CallClientState) => {
138-
delete draft.calls[callId];
141+
delete draft.calls[this._callIdHistory.latestCallId(callId)];
139142
});
140143
}
141144

142145
public setCallEnded(callId: string, callEndReason: CallEndReason | undefined): void {
146+
const latestCallId = this._callIdHistory.latestCallId(callId);
143147
this.modifyState((draft: CallClientState) => {
144-
const call = draft.calls[callId];
148+
const call = draft.calls[latestCallId];
145149
if (call) {
146150
call.endTime = new Date();
147151
call.callEndReason = callEndReason;
148-
delete draft.calls[callId];
152+
delete draft.calls[latestCallId];
149153
// Performance note: This loop should run only once because the number of entries
150154
// is never allowed to exceed MAX_CALL_HISTORY_LENGTH. A loop is used for correctness.
151155
while (Object.keys(draft.callsEnded).length >= MAX_CALL_HISTORY_LENGTH) {
152156
delete draft.callsEnded[findOldestCallEnded(draft.callsEnded)];
153157
}
154-
draft.callsEnded[callId] = call;
158+
draft.callsEnded[latestCallId] = call;
155159
}
156160
});
157161
}
158162

159163
public setCallState(callId: string, state: CallStatus): void {
160164
this.modifyState((draft: CallClientState) => {
161-
const call = draft.calls[callId];
165+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
162166
if (call) {
163167
call.state = state;
164168
}
165169
});
166170
}
167171

168172
public setCallId(newCallId: string, oldCallId: string): void {
173+
this._callIdHistory.updateCallIdHistory(newCallId, oldCallId);
169174
this.modifyState((draft: CallClientState) => {
170175
const call = draft.calls[oldCallId];
171176
if (call) {
@@ -177,7 +182,7 @@ export class CallContext {
177182

178183
public setCallIsScreenSharingOn(callId: string, isScreenSharingOn: boolean): void {
179184
this.modifyState((draft: CallClientState) => {
180-
const call = draft.calls[callId];
185+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
181186
if (call) {
182187
call.isScreenSharingOn = isScreenSharingOn;
183188
}
@@ -190,7 +195,7 @@ export class CallContext {
190195
removeRemoteParticipant: string[]
191196
): void {
192197
this.modifyState((draft: CallClientState) => {
193-
const call = draft.calls[callId];
198+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
194199
if (call) {
195200
removeRemoteParticipant.forEach((id: string) => {
196201
delete call.remoteParticipants[id];
@@ -208,7 +213,7 @@ export class CallContext {
208213
removeRemoteParticipant: string[]
209214
): void {
210215
this.modifyState((draft: CallClientState) => {
211-
const call = draft.calls[callId];
216+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
212217
if (call) {
213218
removeRemoteParticipant.forEach((id: string) => {
214219
delete call.remoteParticipantsEnded[id];
@@ -222,7 +227,7 @@ export class CallContext {
222227

223228
public setCallLocalVideoStream(callId: string, streams: LocalVideoStreamState[]): void {
224229
this.modifyState((draft: CallClientState) => {
225-
const call = draft.calls[callId];
230+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
226231
if (call) {
227232
call.localVideoStreams = streams;
228233
}
@@ -231,7 +236,7 @@ export class CallContext {
231236

232237
public setCallIsMicrophoneMuted(callId: string, isMicrophoneMuted: boolean): void {
233238
this.modifyState((draft: CallClientState) => {
234-
const call = draft.calls[callId];
239+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
235240
if (call) {
236241
call.isMuted = isMicrophoneMuted;
237242
}
@@ -240,7 +245,7 @@ export class CallContext {
240245

241246
public setCallDominantSpeakers(callId: string, dominantSpeakers: DominantSpeakersInfo): void {
242247
this.modifyState((draft: CallClientState) => {
243-
const call = draft.calls[callId];
248+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
244249
if (call) {
245250
call.dominantSpeakers = dominantSpeakers;
246251
}
@@ -249,7 +254,7 @@ export class CallContext {
249254

250255
public setCallRecordingActive(callId: string, isRecordingActive: boolean): void {
251256
this.modifyState((draft: CallClientState) => {
252-
const call = draft.calls[callId];
257+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
253258
if (call) {
254259
call.recording.isRecordingActive = isRecordingActive;
255260
}
@@ -258,7 +263,7 @@ export class CallContext {
258263

259264
public setCallTranscriptionActive(callId: string, isTranscriptionActive: boolean): void {
260265
this.modifyState((draft: CallClientState) => {
261-
const call = draft.calls[callId];
266+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
262267
if (call) {
263268
call.transcription.isTranscriptionActive = isTranscriptionActive;
264269
}
@@ -267,7 +272,7 @@ export class CallContext {
267272

268273
public setCallScreenShareParticipant(callId: string, participantKey: string | undefined): void {
269274
this.modifyState((draft: CallClientState) => {
270-
const call = draft.calls[callId];
275+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
271276
if (call) {
272277
call.screenShareRemoteParticipant = participantKey;
273278
}
@@ -276,7 +281,7 @@ export class CallContext {
276281

277282
public setLocalVideoStreamRendererView(callId: string, view: VideoStreamRendererViewState | undefined): void {
278283
this.modifyState((draft: CallClientState) => {
279-
const call = draft.calls[callId];
284+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
280285
if (call) {
281286
if (call.localVideoStreams.length > 0) {
282287
call.localVideoStreams[0].view = view;
@@ -287,7 +292,7 @@ export class CallContext {
287292

288293
public setParticipantState(callId: string, participantKey: string, state: RemoteParticipantStatus): void {
289294
this.modifyState((draft: CallClientState) => {
290-
const call = draft.calls[callId];
295+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
291296
if (call) {
292297
const participant = call.remoteParticipants[participantKey];
293298
if (participant) {
@@ -299,7 +304,7 @@ export class CallContext {
299304

300305
public setParticipantIsMuted(callId: string, participantKey: string, muted: boolean): void {
301306
this.modifyState((draft: CallClientState) => {
302-
const call = draft.calls[callId];
307+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
303308
if (call) {
304309
const participant = call.remoteParticipants[participantKey];
305310
if (participant) {
@@ -311,7 +316,7 @@ export class CallContext {
311316

312317
public setParticipantDisplayName(callId: string, participantKey: string, displayName: string): void {
313318
this.modifyState((draft: CallClientState) => {
314-
const call = draft.calls[callId];
319+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
315320
if (call) {
316321
const participant = call.remoteParticipants[participantKey];
317322
if (participant) {
@@ -323,7 +328,7 @@ export class CallContext {
323328

324329
public setParticipantIsSpeaking(callId: string, participantKey: string, isSpeaking: boolean): void {
325330
this.modifyState((draft: CallClientState) => {
326-
const call = draft.calls[callId];
331+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
327332
if (call) {
328333
const participant = call.remoteParticipants[participantKey];
329334
if (participant) {
@@ -335,7 +340,7 @@ export class CallContext {
335340

336341
public setParticipantVideoStream(callId: string, participantKey: string, stream: RemoteVideoStreamState): void {
337342
this.modifyState((draft: CallClientState) => {
338-
const call = draft.calls[callId];
343+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
339344
if (call) {
340345
const participant = call.remoteParticipants[participantKey];
341346
if (participant) {
@@ -360,7 +365,7 @@ export class CallContext {
360365
isAvailable: boolean
361366
): void {
362367
this.modifyState((draft: CallClientState) => {
363-
const call = draft.calls[callId];
368+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
364369
if (call) {
365370
const participant = call.remoteParticipants[participantKey];
366371
if (participant) {
@@ -380,7 +385,7 @@ export class CallContext {
380385
removeRemoteVideoStream: number[]
381386
): void {
382387
this.modifyState((draft: CallClientState) => {
383-
const call = draft.calls[callId];
388+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
384389
if (call) {
385390
const participant = call.remoteParticipants[participantKey];
386391
if (participant) {
@@ -411,7 +416,7 @@ export class CallContext {
411416
view: VideoStreamRendererViewState | undefined
412417
): void {
413418
this.modifyState((draft: CallClientState) => {
414-
const call = draft.calls[callId];
419+
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
415420
if (call) {
416421
const participant = call.remoteParticipants[participantKey];
417422
if (participant) {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
/**
5+
* @private
6+
* Call Id will change during the call for at least 1 time
7+
* This is to avoid async bug that call id has been changed during an async-await function
8+
* but the function still uses stale call id to access state
9+
*/
10+
export class CallIdHistory {
11+
private _callIdHistory = new Map<string, string>();
12+
13+
public updateCallIdHistory(newCallId: string, oldCallId: string): void {
14+
// callId for a call can fluctuate between some set of values.
15+
// But if a newCallId already exists, and maps to different call, we're in trouble.
16+
// This can only happen if a callId is reused across two distinct calls.
17+
const existing = this._callIdHistory.get(newCallId);
18+
if (existing !== undefined && this.latestCallId(newCallId) !== oldCallId) {
19+
console.trace(`${newCallId} alredy exists and maps to ${existing}, which is not the same as ${oldCallId}`);
20+
}
21+
22+
// The latest callId never maps to another callId.
23+
this._callIdHistory.delete(newCallId);
24+
this._callIdHistory.set(oldCallId, newCallId);
25+
}
26+
27+
public latestCallId(callId: string): string {
28+
let latest = callId;
29+
/* eslint no-constant-condition: ["error", { "checkLoops": false }] */
30+
while (true) {
31+
const newer = this._callIdHistory.get(latest);
32+
if (newer === undefined) {
33+
break;
34+
}
35+
latest = newer;
36+
}
37+
return latest;
38+
}
39+
}

packages/calling-stateful-client/src/InternalCallContext.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { LocalVideoStream, RemoteVideoStream, VideoStreamRenderer } from '@azure/communication-calling';
55
import { LocalVideoStreamState } from './CallClientState';
6+
import { CallIdHistory } from './CallIdHistory';
67

78
/**
89
* Internally tracked render status. Stores the status of a video render of a stream as rendering could take a long
@@ -50,8 +51,10 @@ export class InternalCallContext {
5051
// Used for keeping track of rendered LocalVideoStreams that are not part of a Call.
5152
// The key is the stream ID. We assume each stream ID to only have one owning render info
5253
private _unparentedRenderInfos = new Map<string, LocalRenderInfo>();
54+
private _callIdHistory = new CallIdHistory();
5355

5456
public setCallId(newCallId: string, oldCallId: string): void {
57+
this._callIdHistory.updateCallIdHistory(newCallId, oldCallId);
5558
const remoteRenderInfos = this._remoteRenderInfos.get(oldCallId);
5659
if (remoteRenderInfos) {
5760
this._remoteRenderInfos.delete(oldCallId);
@@ -65,20 +68,20 @@ export class InternalCallContext {
6568
}
6669
}
6770

68-
public getRemoteRenderInfos(): Map<string, Map<string, Map<number, RemoteRenderInfo>>> {
69-
return this._remoteRenderInfos;
71+
public getCallIds(): IterableIterator<string> {
72+
return this._remoteRenderInfos.keys();
7073
}
7174

7275
public getRemoteRenderInfoForCall(callId: string): Map<string, Map<number, RemoteRenderInfo>> | undefined {
73-
return this._remoteRenderInfos.get(callId);
76+
return this._remoteRenderInfos.get(this._callIdHistory.latestCallId(callId));
7477
}
7578

7679
public getRemoteRenderInfoForParticipant(
7780
callId: string,
7881
participantKey: string,
7982
streamId: number
8083
): RemoteRenderInfo | undefined {
81-
const callRenderInfos = this._remoteRenderInfos.get(callId);
84+
const callRenderInfos = this._remoteRenderInfos.get(this._callIdHistory.latestCallId(callId));
8285
if (!callRenderInfos) {
8386
return undefined;
8487
}
@@ -97,10 +100,10 @@ export class InternalCallContext {
97100
status: RenderStatus,
98101
renderer: VideoStreamRenderer | undefined
99102
): void {
100-
let callRenderInfos = this._remoteRenderInfos.get(callId);
103+
let callRenderInfos = this._remoteRenderInfos.get(this._callIdHistory.latestCallId(callId));
101104
if (!callRenderInfos) {
102105
callRenderInfos = new Map<string, Map<number, RemoteRenderInfo>>();
103-
this._remoteRenderInfos.set(callId, callRenderInfos);
106+
this._remoteRenderInfos.set(this._callIdHistory.latestCallId(callId), callRenderInfos);
104107
}
105108

106109
let participantRenderInfos = callRenderInfos.get(participantKey);
@@ -113,7 +116,7 @@ export class InternalCallContext {
113116
}
114117

115118
public deleteRemoteRenderInfo(callId: string, participantKey: string, streamId: number): void {
116-
const callRenderInfos = this._remoteRenderInfos.get(callId);
119+
const callRenderInfos = this._remoteRenderInfos.get(this._callIdHistory.latestCallId(callId));
117120
if (!callRenderInfos) {
118121
return;
119122
}
@@ -132,15 +135,15 @@ export class InternalCallContext {
132135
status: RenderStatus,
133136
renderer: VideoStreamRenderer | undefined
134137
): void {
135-
this._localRenderInfos.set(callId, { stream, status, renderer });
138+
this._localRenderInfos.set(this._callIdHistory.latestCallId(callId), { stream, status, renderer });
136139
}
137140

138141
public getLocalRenderInfo(callId: string): LocalRenderInfo | undefined {
139-
return this._localRenderInfos.get(callId);
142+
return this._localRenderInfos.get(this._callIdHistory.latestCallId(callId));
140143
}
141144

142145
public deleteLocalRenderInfo(callId: string): void {
143-
this._localRenderInfos.delete(callId);
146+
this._localRenderInfos.delete(this._callIdHistory.latestCallId(callId));
144147
}
145148

146149
public getUnparentedRenderInfo(localVideoStream: LocalVideoStreamState): LocalRenderInfo | undefined {

packages/calling-stateful-client/src/StreamUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,8 @@ export function disposeAllViewsFromCall(
451451
* @private
452452
*/
453453
export function disposeAllViews(context: CallContext, internalContext: InternalCallContext): void {
454-
const remoteStreamAndRenderers = internalContext.getRemoteRenderInfos();
455-
for (const [callId] of remoteStreamAndRenderers.entries()) {
454+
const callIds = internalContext.getCallIds();
455+
for (const callId of callIds) {
456456
disposeAllViewsFromCall(context, internalContext, callId);
457457
}
458458
}

0 commit comments

Comments
 (0)