Skip to content

Commit 4e9171f

Browse files
authored
Fix useSession return value stability (#1230)
1 parent 09b09eb commit 4e9171f

File tree

2 files changed

+84
-15
lines changed

2 files changed

+84
-15
lines changed

.changeset/quick-rings-thank.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/components-react': patch
3+
---
4+
5+
Fix useSession return value stability

packages/react/src/hooks/useSession.ts

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,84 @@ type UseSessionCommonOptions = {
154154
type UseSessionConfigurableOptions = UseSessionCommonOptions & TokenSourceFetchOptions;
155155
type UseSessionFixedOptions = UseSessionCommonOptions;
156156

157+
/**
158+
* Given two TokenSourceFetchOptions values, check to see if they are deep equal.
159+
*
160+
* FIXME: swap this for an import from livekit-client once
161+
* https://github.com/livekit/client-sdk-js/pull/1733 is merged and published!
162+
* */
163+
function areTokenSourceFetchOptionsEqual(a: TokenSourceFetchOptions, b: TokenSourceFetchOptions) {
164+
const allKeysSet = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<
165+
keyof TokenSourceFetchOptions
166+
>;
167+
168+
for (const key of allKeysSet) {
169+
switch (key) {
170+
case 'roomName':
171+
case 'participantName':
172+
case 'participantIdentity':
173+
case 'participantMetadata':
174+
case 'participantAttributes':
175+
case 'agentName':
176+
case 'agentMetadata':
177+
if (a[key] !== b[key]) {
178+
return false;
179+
}
180+
break;
181+
default:
182+
// ref: https://stackoverflow.com/a/58009992
183+
const exhaustiveCheckedKey: never = key;
184+
throw new Error(`Options key ${exhaustiveCheckedKey} not being checked for equality!`);
185+
}
186+
}
187+
188+
return true;
189+
}
190+
191+
/** Internal hook used by useSession to manage creating a function that properly invokes
192+
* tokenSource.fetch(...) with any fetch options */
193+
function useSessionTokenSourceFetch(
194+
tokenSource: TokenSourceConfigurable | TokenSourceFixed,
195+
unstableRestOptions: Exclude<UseSessionConfigurableOptions, keyof UseSessionCommonOptions>,
196+
) {
197+
const isConfigurable = tokenSource instanceof TokenSourceConfigurable;
198+
199+
const memoizedTokenFetchOptionsRef = React.useRef<TokenSourceFetchOptions | null>(
200+
isConfigurable ? unstableRestOptions : null,
201+
);
202+
203+
React.useEffect(() => {
204+
if (!isConfigurable) {
205+
memoizedTokenFetchOptionsRef.current = null;
206+
return;
207+
}
208+
209+
if (
210+
memoizedTokenFetchOptionsRef.current !== null &&
211+
areTokenSourceFetchOptionsEqual(memoizedTokenFetchOptionsRef.current, unstableRestOptions)
212+
) {
213+
return;
214+
}
215+
216+
memoizedTokenFetchOptionsRef.current = unstableRestOptions;
217+
}, [isConfigurable, unstableRestOptions]);
218+
219+
const tokenSourceFetch = React.useCallback(async () => {
220+
if (isConfigurable) {
221+
if (!memoizedTokenFetchOptionsRef.current) {
222+
throw new Error(
223+
`AgentSession - memoized token fetch options are not set, but the passed tokenSource was an instance of TokenSourceConfigurable. If you are seeing this please make a new GitHub issue!`,
224+
);
225+
}
226+
return tokenSource.fetch(memoizedTokenFetchOptionsRef.current);
227+
} else {
228+
return tokenSource.fetch();
229+
}
230+
}, [isConfigurable, tokenSource]);
231+
232+
return tokenSourceFetch;
233+
}
234+
157235
/**
158236
* A Session represents a manages connection to a Room which can contain Agents.
159237
* @public
@@ -357,9 +435,7 @@ export function useSession(
357435
}, [
358436
sessionInternal,
359437
room,
360-
emitter,
361438
roomConnectionState,
362-
localParticipant,
363439
localCamera,
364440
localMicrophone,
365441
generateDerivedConnectionStateValues,
@@ -427,15 +503,7 @@ export function useSession(
427503
),
428504
);
429505

430-
const tokenSourceFetch = React.useCallback(async () => {
431-
const isConfigurable = tokenSource instanceof TokenSourceConfigurable;
432-
if (isConfigurable) {
433-
const tokenFetchOptions = restOptions as UseSessionConfigurableOptions;
434-
return tokenSource.fetch(tokenFetchOptions);
435-
} else {
436-
return tokenSource.fetch();
437-
}
438-
}, [tokenSource, restOptions]);
506+
const tokenSourceFetch = useSessionTokenSourceFetch(tokenSource, restOptions);
439507

440508
const start = React.useCallback(
441509
async (connectOptions: SessionConnectOptions = {}) => {
@@ -454,8 +522,6 @@ export function useSession(
454522

455523
let tokenDispatchesAgent = false;
456524
await Promise.all([
457-
// FIXME: swap the below line in once the new `livekit-client` changes are published
458-
// room.connect(tokenSource, { tokenSourceOptions }),
459525
tokenSourceFetch().then(({ serverUrl, participantToken }) => {
460526
const participantTokenPayload = decodeTokenPayload(participantToken);
461527
const participantTokenAgentDispatchCount =
@@ -491,8 +557,6 @@ export function useSession(
491557

492558
const prepareConnection = React.useCallback(async () => {
493559
const credentials = await tokenSourceFetch();
494-
// FIXME: swap the below line in once the new `livekit-client` changes are published
495-
// room.prepareConnection(tokenSource, { tokenSourceOptions }),
496560
await room.prepareConnection(credentials.serverUrl, credentials.participantToken);
497561
}, [tokenSourceFetch, room]);
498562
React.useEffect(

0 commit comments

Comments
 (0)