From 9df630ae002c22a39312e1f80fe3304166026b4b Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:53:06 -0700 Subject: [PATCH] Remove stale data track subscriber handles --- .changeset/cleanup-old-data-track-handles.md | 5 ++ .../incoming/IncomingDataTrackManager.test.ts | 61 +++++++++++++++++++ .../incoming/IncomingDataTrackManager.ts | 1 + 3 files changed, 67 insertions(+) create mode 100644 .changeset/cleanup-old-data-track-handles.md diff --git a/.changeset/cleanup-old-data-track-handles.md b/.changeset/cleanup-old-data-track-handles.md new file mode 100644 index 0000000000..3e85e2e20a --- /dev/null +++ b/.changeset/cleanup-old-data-track-handles.md @@ -0,0 +1,5 @@ +--- +"livekit-client": patch +--- + +Remove stale data track subscriber handles from the packet routing table diff --git a/src/room/data-track/incoming/IncomingDataTrackManager.test.ts b/src/room/data-track/incoming/IncomingDataTrackManager.test.ts index 0d5e19822f..4c866179c5 100644 --- a/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +++ b/src/room/data-track/incoming/IncomingDataTrackManager.test.ts @@ -1159,6 +1159,67 @@ describe('DataTrackIncomingManager', () => { expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05])); }); + it('should stop routing packets on the old handle once a new handle is assigned', async () => { + const manager = new IncomingDataTrackManager(); + const managerEvents = subscribeToEvents(manager, [ + 'sfuUpdateSubscription', + 'trackPublished', + ]); + + const pubHandle = DataTrackHandle.fromNumber(5); + const oldSid = 'old sid'; + const newSid = 'new sid'; + const oldSubHandle = DataTrackHandle.fromNumber(6); + const newSubHandle = DataTrackHandle.fromNumber(7); + + // Simulate track published + await manager.receiveSfuPublicationUpdates( + new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]), + ); + await managerEvents.waitFor('trackPublished'); + + // Subscribe to the track + const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(oldSid); + const reader = stream.getReader(); + await managerEvents.waitFor('sfuUpdateSubscription'); + + // Simulate SFU assigning subscriber handle + manager.receivedSfuSubscriberHandles(new Map([[oldSubHandle, oldSid]])); + await sfuSubscriptionComplete; + + // Simulate publisher full reconnect: same track, new SID + await manager.receiveSfuPublicationUpdates( + new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]), + ); + await managerEvents.waitFor('sfuUpdateSubscription'); + + // Simulate SFU assigning a new subscriber handle + manager.receivedSfuSubscriberHandles(new Map([[newSubHandle, newSid]])); + + const buildPacket = (trackHandle: DataTrackHandle, frameNumber: number, payload: number[]) => + new DataTrackPacket( + new DataTrackPacketHeader({ + extensions: new DataTrackExtensions(), + frameNumber: WrapAroundUnsignedInt.u16(frameNumber), + marker: FrameMarker.Single, + sequence: WrapAroundUnsignedInt.u16(frameNumber), + timestamp: DataTrackTimestamp.fromRtpTicks(0), + trackHandle, + }), + new Uint8Array(payload), + ).toBinary(); + + // A packet received on the old handle must be dropped, not routed to the subscriber + await manager.packetReceived(buildPacket(oldSubHandle, 0, [0xaa, 0xbb])); + + // A packet received on the new handle reaches the subscriber + await manager.packetReceived(buildPacket(newSubHandle, 1, [0x01, 0x02])); + + const { value, done } = await reader.read(); + expect(done).toStrictEqual(false); + expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02])); + }); + it('should terminate the sfu subscription when the stream is cancelled after a sid reassignment', async () => { const manager = new IncomingDataTrackManager(); const managerEvents = subscribeToEvents(manager, [ diff --git a/src/room/data-track/incoming/IncomingDataTrackManager.ts b/src/room/data-track/incoming/IncomingDataTrackManager.ts index c024d3ce91..4c89db3f96 100644 --- a/src/room/data-track/incoming/IncomingDataTrackManager.ts +++ b/src/room/data-track/incoming/IncomingDataTrackManager.ts @@ -600,6 +600,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () => } case 'active': { // Update handle for an active subscription. This can occur following a full reconnect. + this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle); descriptor.subscription.subcriptionHandle = assignedHandle; this.subscriptionHandles.set(assignedHandle, sid); return;