Skip to content

Conversation

Shubhrakanti
Copy link
Contributor

@Shubhrakanti Shubhrakanti commented Jul 18, 2025

This ports over a fix that was done in the python side, but never implemented in the node sdk (PR).

User reported that the Room would throw an error if there was already a participant inside and they tried to connect. This is because we were processing the participant_connected event before the trackSubscribed event.

Before fix:

  1. call room.connect
  2. receive trackSubscribed ← Tries to find participant that was never created!
  3. throw participant not found error

After fix:

  1. call room.connect
  2. receive participants_updated
  3. receive participant_connectedCreates the participant
  4. receive track_published
  5. receive track_subscribedWorks because participant exists

Copy link

changeset-bot bot commented Jul 18, 2025

🦋 Changeset detected

Latest commit: 772570f

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

This PR includes changesets to release 6 packages
Name Type
@livekit/rtc-node Patch
@livekit/rtc-node-darwin-arm64 Patch
@livekit/rtc-node-darwin-x64 Patch
@livekit/rtc-node-linux-arm64-gnu Patch
@livekit/rtc-node-linux-x64-gnu Patch
@livekit/rtc-node-win32-x64-msvc 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

@Shubhrakanti Shubhrakanti changed the title Shubhra/ Add buffer to FFI events [Bugifx] Shubhra/ Add buffer to FFI events Jul 18, 2025
tsconfig.json Outdated
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["es2015"],
"lib": ["es2015", "ES2021.WeakRef"],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need this so that linter doesn't throw an error when using FinalizationRegistry

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lukasIO is there a reason this is es2015 can we just update it to something later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like we don't need FinalizationRegistry. But still would be good to upgrade this?

tsconfig.json Outdated
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["es2015"],
"lib": ["es2015", "ES2021.WeakRef"],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lukasIO is there a reason this is es2015 can we just update it to something later.

}
}

subscribe(): ReadableStream<T> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I chose this to be the equivalent of the asyncio's Queue we use in python. The main feature we care about is asynchronous reads and waiting for events while the stream is empty.

@@ -88,8 +90,19 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
remoteParticipants: Map<string, RemoteParticipant> = new Map();
localParticipant?: LocalParticipant;

private static cleanupRegistry = new FinalizationRegistry((cleanup: () => void) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use FinalizationRegistry (see docs) to act as an equivalent of python's __del__ method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's also Symbol.dispose as defined in a ECMA proposal https://github.com/tc39/proposal-explicit-resource-management but I wasn't sure if this was merged in yet.

} catch (error) {
log.debug(error, 'Listen task ended');
} finally {
this.listenTaskPromise = undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

is it better to type it as | null?

@Shubhrakanti
Copy link
Contributor Author

@davidzhao this should fix the issues folks were having with inbound sip calls in agents js

Comment on lines 45 to 53
} catch (error: unknown) {
log.error(error, 'Error enqueuing item to stream');
toRemove.add(controller);
}
}

for (const controller of toRemove) {
this.subscribers.delete(controller);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

is this overkill? Just want to make sure there are no memory leaks.

Comment on lines 72 to 80
/**
* @throws "TypeError: Invalid state: ReadableStream is locked"
* if the stream is locked, make sure the stream is not being read from
* before calling this method.
*/
unsubscribe(stream: ReadableStream<T>): void {
stream.cancel();
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't love this, but it works for now.

constructor() {
super();
// Register a finalizer to disconnect the room when it's garbage collected
Room.cleanupRegistry.register(this, () => {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need this. If the FfiHandle is dropped, the Rust side will already close the room

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good to know. I saw in the python side so that's why I implemented it - but had a felling we didn't need this.

@@ -180,6 +193,9 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
options,
});

// subscribe before connecting so we don't miss any events
this.ffiQueue = FfiClient.instance.queue.subscribe();
Copy link
Member

Choose a reason for hiding this comment

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

On JS it may be simpler, just hooking into the FfiEvent using on. We could just append to a list?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe it's fine, but IIRC that was the only difference. JS using events, but python using queues

Copy link
Contributor Author

@Shubhrakanti Shubhrakanti Jul 19, 2025

Choose a reason for hiding this comment

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

On JS it may be simpler, just hooking into the FfiEvent using on that's exactly what this is doing.

We grab the events and append them to a queue. The only reason it's a stream is so reads are async and we don't block the main event loop.

Copy link
Contributor Author

@Shubhrakanti Shubhrakanti Jul 19, 2025

Choose a reason for hiding this comment

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

Do you mean get rid of FfiClient.instance.queue all together. Replace this with

connect(){
// wire up the event listener first
FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onFfiEvent)

// send the connection request
const res = FfiClient.instance.request<ConnectResponse>({ ...... })

}
//
onEvent(event): {
   this.buffer.push(event)
  // only start processing the events once we've connected.
   if (connected) {
      this.proccessEvent(event)
...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As I understand it the order of operations are.

  1. Create a buffer for all the events coming in
  2. Connect to the room
  3. Immediately populate the buffer so we don't drop events
  4. Start processing events

Right now step 1 is done with this.ffiQueue = FfiClient.instance.queue.subscribe(); if we replace it with FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onFfiEvent) we still need to buffer the events to process until we receive the ConnectResponse?

@Shubhrakanti Shubhrakanti requested a review from theomonnom July 19, 2025 19:31
@Shubhrakanti
Copy link
Contributor Author

Discussed offline with @theomonnom - we're just going to buffer the events that come in before the connect in room.ts. Once we have connected - we will process them in batch.

@Shubhrakanti Shubhrakanti merged commit 37211ca into main Jul 22, 2025
17 checks passed
@Shubhrakanti Shubhrakanti deleted the shubhra/add-buffer-to-ffi-events branch July 22, 2025 03:47
@github-actions github-actions bot mentioned this pull request Jul 22, 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.

2 participants