Skip to content

Conversation

@1egoman
Copy link
Contributor

@1egoman 1egoman commented Nov 10, 2025

In a comment, @thomasyuill-livekit had mentioned that useSession's return value seemed to not be stable (link). I looked into it and this was because the options being passed into tokenSource.fetch(...) for configurable token sources were not stable inherintly because of the api being expiosed (they were passed in on each render as a hook parameter) and that was cascading downwards and causing the reference instability.

So, to fix this, I've reintroduced the areTokenSourceFetchOptionsEqual function which can be run against each new set of options that are received every hook run and compute a stable value, which in my testing leads to much fewer downstream session reference changes.

@changeset-bot
Copy link

changeset-bot bot commented Nov 10, 2025

🦋 Changeset detected

Latest commit: 424bc0b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@livekit/components-react Patch
@livekit/component-example-next Patch
@livekit/components-js-docs Patch
@livekit/component-docs-storybook Patch
@livekit/components-docs-gen Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Contributor

github-actions bot commented Nov 10, 2025

size-limit report 📦

Path Size
LiveKitRoom only 6 KB (0%)
LiveKitRoom with VideoConference 32.23 KB (0%)
All exports 42.79 KB (+0.35% 🔺)

Comment on lines +157 to +189
/**
* Given two TokenSourceFetchOptions values, check to see if they are deep equal.
*
* FIXME: swap this for an import from livekit-client once
* https://github.com/livekit/client-sdk-js/pull/1733 is merged and published!
* */
function areTokenSourceFetchOptionsEqual(a: TokenSourceFetchOptions, b: TokenSourceFetchOptions) {
const allKeysSet = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<
keyof TokenSourceFetchOptions
>;

for (const key of allKeysSet) {
switch (key) {
case 'roomName':
case 'participantName':
case 'participantIdentity':
case 'participantMetadata':
case 'participantAttributes':
case 'agentName':
case 'agentMetadata':
if (a[key] !== b[key]) {
return false;
}
break;
default:
// ref: https://stackoverflow.com/a/58009992
const exhaustiveCheckedKey: never = key;
throw new Error(`Options key ${exhaustiveCheckedKey} not being checked for equality!`);
}
}

return true;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chore: This has been copied from here: livekit/client-sdk-js#1733. I did this so I could test the change but IMO this copied code shouldn't be merged.

So, before this should be merged, get the above linked pull request deployed into a new version on npm, then delete this code and update it to be an import.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I forgot to do this before merging 🤦 . I will do this as part of the follow up change.

Comment on lines 225 to 229
if (!memoizedTokenFetchOptions) {
throw new Error(
`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!`,
);
}
Copy link
Contributor Author

@1egoman 1egoman Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: There might be a better way to word this error message. This should be an invariant that I want to make clear should never happen in actual use but isn't easily representable in another way.

(clarifying a bit further - I don't think this can happen in actual use, but I'm not 100% sure reasoning through it in my head. So if somebody can make this happen down the line I would be curious to get the reproduction instructions!)

) {
const isConfigurable = tokenSource instanceof TokenSourceConfigurable;

const [memoizedTokenFetchOptions, setMemoizedTokenFetchOptions] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (if-minor): there's a nice util library that would allow us to use your compare function directly to compare the deps.

Internally it uses a ref for comparison, which I personally feel would be slightly preferable to the useEffect pattern in case you don't want to use that library directly. Main reason is that the useEffect will always only run on the subsequent tick and makes the flow a bit harder to reason about with multiple dependencies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good - migrated to a ref instead!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that was a misunderstanding, I meant that the ref usage would make things cleaner because it means we can get rid of the useEffect altogether.

If we need to keep the useEffect, then your previous solution works just as well.
FWIW, I think using this approach would allow us to get rid of the useEffect (which was my primary motivation to suggest this change)

Copy link

@thomasyuill-livekit thomasyuill-livekit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing this!

I'm not sure I have the necessary understanding of the code base to test or review this PR

@lukasIO
Copy link
Contributor

lukasIO commented Nov 11, 2025

I just tested this and it looks like session itself is still highly unstable due to connectionState being spread on it.

I think this needs some general refactoring to not wrap the whole return value in a memo but rather the field values returned on it.

@1egoman
Copy link
Contributor Author

1egoman commented Nov 11, 2025

I just tested this and it looks like session itself is still highly unstable due to connectionState being spread on it.

I'm not sure what I could be doing to make this more stable... you are right that I am spreading conversationState, but this isn't problematic because conversationState itself is a dependency of that memo and conversationState itself is memoized, so it is as if this was one big useMemo. I also can't really refactor this all that substantially as you are seemingly suggesting because it needs to be clear to typescript that certain values both are not null in certain states and without that conversationState switch the typing logic breaks signifigantly.

Taking a step back, I'm not sure what the bar for you is for "highly unstable", but inevitably, the reference will have to change periodically as the session state changes, so it can't just be 1. With the way data is currently being modeled, about 5-6 at minimum must happen during the agent connection process. For reference, here's what I see on my end when I add a useEffect(() => console.log('Session reference changed!', session.connectionState), [session]); to my testing example application, and it looks like roughly that amount of reference changes:

Untitled.mov

Especially given somebody could further scope down session (ie, session.status, etc) as a dependency to reduce the number of updates they have to process, this seems to me fairly sane. For reference, what Thom was seeing previously was probably > 25 reference changes during the connection process.

Maybe I'm misunderstanding your objection, but if so let's chat on a call because I'm not really sure what the next steps to get this change merged would look like given your comment, and I think there may be a bit of mounting time pressure.

These don't really change anything in practice but just out of
cleanliness since they aren't used I removed them.
@1egoman
Copy link
Contributor Author

1egoman commented Nov 12, 2025

Discussed with lukas offline - the concern he was raising was mostly about session.start / session.end not being stable, which I think some work can be done to keep those stable so something like useEffect(() => { session.start(); return () => session.end(); }, [session.start, session.end]) could work. I will do this in a follow up change.

@1egoman 1egoman merged commit 4e9171f into main Nov 12, 2025
6 checks passed
@1egoman 1egoman deleted the fix-use-session-return-stability branch November 12, 2025 08:39
@github-actions github-actions bot mentioned this pull request Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants