livekit-client 2.18.7 → 2.18.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +2 -2
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +408 -255
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/api/SignalClient.d.ts.map +1 -1
  10. package/dist/src/logger.d.ts +11 -1
  11. package/dist/src/logger.d.ts.map +1 -1
  12. package/dist/src/room/PCTransport.d.ts +13 -3
  13. package/dist/src/room/PCTransport.d.ts.map +1 -1
  14. package/dist/src/room/PCTransportManager.d.ts +3 -1
  15. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts.map +1 -1
  18. package/dist/src/room/data-track/LocalDataTrack.d.ts +32 -0
  19. package/dist/src/room/data-track/LocalDataTrack.d.ts.map +1 -1
  20. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  21. package/dist/src/room/data-track/handle.d.ts +1 -0
  22. package/dist/src/room/data-track/handle.d.ts.map +1 -1
  23. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  24. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  25. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  26. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  27. package/dist/src/room/data-track/outgoing/types.d.ts +7 -0
  28. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  29. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  30. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  31. package/dist/src/utils/subscribeToEvents.d.ts.map +1 -1
  32. package/dist/ts4.2/logger.d.ts +11 -1
  33. package/dist/ts4.2/room/PCTransport.d.ts +13 -3
  34. package/dist/ts4.2/room/PCTransportManager.d.ts +3 -1
  35. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +32 -0
  36. package/dist/ts4.2/room/data-track/handle.d.ts +1 -0
  37. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +4 -3
  38. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +18 -3
  39. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/src/api/SignalClient.ts +19 -31
  42. package/src/logger.test.ts +61 -0
  43. package/src/logger.ts +38 -4
  44. package/src/room/PCTransport.ts +26 -3
  45. package/src/room/PCTransportManager.test.ts +281 -0
  46. package/src/room/PCTransportManager.ts +45 -31
  47. package/src/room/RTCEngine.ts +34 -52
  48. package/src/room/Room.ts +37 -59
  49. package/src/room/data-track/LocalDataTrack.ts +60 -1
  50. package/src/room/data-track/RemoteDataTrack.ts +4 -1
  51. package/src/room/data-track/handle.ts +4 -0
  52. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +72 -2
  53. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +5 -3
  54. package/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +387 -1
  55. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +51 -3
  56. package/src/room/data-track/outgoing/types.ts +5 -0
  57. package/src/room/participant/LocalParticipant.ts +59 -144
  58. package/src/room/participant/Participant.ts +4 -1
  59. package/src/utils/subscribeToEvents.ts +11 -8
@@ -586,7 +586,7 @@ describe('DataTrackOutgoingManager', () => {
586
586
  ]);
587
587
 
588
588
  // Shut down the manager
589
- const shutdownPromise = manager.shutdown();
589
+ const shutdownPromise = manager.reset();
590
590
 
591
591
  // The pending data track should be cancelled
592
592
  await expect(pendingDescriptor.completionFuture.promise).rejects.toThrowError(
@@ -602,4 +602,390 @@ describe('DataTrackOutgoingManager', () => {
602
602
 
603
603
  await shutdownPromise;
604
604
  });
605
+
606
+ describe('localDataTrack.flush()', () => {
607
+ it('should resolve flush() after a single tryPush once the packet is acknowledged', async () => {
608
+ const pubHandle = 5;
609
+ const manager = OutgoingDataTrackManager.withDescriptors(
610
+ new Map([
611
+ [
612
+ DataTrackHandle.fromNumber(pubHandle),
613
+ Descriptor.active(
614
+ {
615
+ sid: 'bogus-sid',
616
+ pubHandle,
617
+ name: 'test',
618
+ usesE2ee: false,
619
+ },
620
+ null,
621
+ ),
622
+ ],
623
+ ]),
624
+ );
625
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
626
+ 'packetAvailable',
627
+ 'packetsFlushedChange',
628
+ ]);
629
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
630
+ { name: 'track name' },
631
+ manager,
632
+ pubHandle,
633
+ );
634
+
635
+ // 1. Push a single-packet payload
636
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]) });
637
+
638
+ // 2. The packet should have been emitted to be sent over the data channel
639
+ const packetEvent = await managerEvents.waitFor('packetAvailable');
640
+ expect(packetEvent.handle).toStrictEqual(pubHandle);
641
+
642
+ // 3. A event should be sent indicating that the data track is no longer "flushed"
643
+ const noLongerFlushedEvent = await managerEvents.waitFor('packetsFlushedChange');
644
+ expect(noLongerFlushedEvent.handle).toStrictEqual(pubHandle);
645
+ expect(noLongerFlushedEvent.isFlushed).toStrictEqual(false);
646
+
647
+ // 3. Calling flush() right after tryPush() should not resolve until the packet
648
+ // is acknowledged via handlePacketSendComplete
649
+ let flushed = false;
650
+ const flushPromise = localDataTrack.flush().then(() => {
651
+ flushed = true;
652
+ });
653
+
654
+ await new Promise((resolve) => setTimeout(resolve, 0));
655
+ expect(flushed).toStrictEqual(false);
656
+ expect(managerEvents.areThereBufferedEvents('packetsFlushedChange')).toBe(false);
657
+
658
+ // 4. Acknowledge that the packet has been sent over the data channel
659
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
660
+
661
+ // 5. The packetsFlushed event fires once the in-flight packet counter reaches 0
662
+ const flushedEvent = await managerEvents.waitFor('packetsFlushedChange');
663
+ expect(flushedEvent.handle).toStrictEqual(pubHandle);
664
+ expect(flushedEvent.isFlushed).toStrictEqual(true);
665
+
666
+ // 6. The flush() promise resolves
667
+ await flushPromise;
668
+ expect(flushed).toStrictEqual(true);
669
+ });
670
+
671
+ it('should resolve flush() only after all packets in a multi-packet payload are acknowledged', async () => {
672
+ const pubHandle = 5;
673
+ const manager = OutgoingDataTrackManager.withDescriptors(
674
+ new Map([
675
+ [
676
+ DataTrackHandle.fromNumber(pubHandle),
677
+ Descriptor.active(
678
+ {
679
+ sid: 'bogus-sid',
680
+ pubHandle,
681
+ name: 'test',
682
+ usesE2ee: false,
683
+ },
684
+ null,
685
+ ),
686
+ ],
687
+ ]),
688
+ );
689
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
690
+ 'packetAvailable',
691
+ 'packetsFlushedChange',
692
+ ]);
693
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
694
+ { name: 'track name' },
695
+ manager,
696
+ pubHandle,
697
+ );
698
+
699
+ // 1. Push a payload large enough to span multiple packets (24k > single packet mtu)
700
+ await localDataTrack.tryPush({ payload: new Uint8Array(24_000).fill(0xbe) });
701
+
702
+ // 2. A event should be sent indicating that the data track is no longer "flushed"
703
+ const noLongerFlushedEvent = await managerEvents.waitFor('packetsFlushedChange');
704
+ expect(noLongerFlushedEvent.handle).toStrictEqual(pubHandle);
705
+ expect(noLongerFlushedEvent.isFlushed).toStrictEqual(false);
706
+
707
+ // 3. Two packetAvailable events should be emitted for this payload
708
+ await managerEvents.waitFor('packetAvailable');
709
+ await managerEvents.waitFor('packetAvailable');
710
+
711
+ // 4. Call flush() before any of the packets have been acknowledged
712
+ let flushed = false;
713
+ const flushPromise = localDataTrack.flush().then(() => {
714
+ flushed = true;
715
+ });
716
+
717
+ // 5. Acknowledge the first packet -- flush should not resolve yet, in-flight counter still > 0
718
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
719
+ await new Promise((resolve) => setTimeout(resolve, 0));
720
+ expect(flushed).toStrictEqual(false);
721
+ expect(managerEvents.areThereBufferedEvents('packetsFlushedChange')).toBe(false);
722
+
723
+ // 6. Acknowledge the second packet -- flush resolves once the counter reaches 0
724
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
725
+
726
+ const flushedEvent = await managerEvents.waitFor('packetsFlushedChange');
727
+ expect(flushedEvent.handle).toStrictEqual(pubHandle);
728
+ expect(flushedEvent.isFlushed).toStrictEqual(true);
729
+
730
+ await flushPromise;
731
+ expect(flushed).toStrictEqual(true);
732
+ });
733
+
734
+ it('should resolve any pending flush() calls when the manager is reset', async () => {
735
+ const pubHandle = 5;
736
+ const manager = OutgoingDataTrackManager.withDescriptors(
737
+ new Map([
738
+ [
739
+ DataTrackHandle.fromNumber(pubHandle),
740
+ Descriptor.active(
741
+ {
742
+ sid: 'bogus-sid',
743
+ pubHandle,
744
+ name: 'test',
745
+ usesE2ee: false,
746
+ },
747
+ null,
748
+ ),
749
+ ],
750
+ ]),
751
+ );
752
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
753
+ 'packetAvailable',
754
+ 'packetsFlushedChange',
755
+ 'reset',
756
+ ]);
757
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
758
+ { name: 'track name' },
759
+ manager,
760
+ pubHandle,
761
+ );
762
+
763
+ // 1. Push a single-packet payload
764
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]) });
765
+
766
+ // 2. A event should be sent indicating that the data track is no longer "flushed"
767
+ const noLongerFlushedEvent = await managerEvents.waitFor('packetsFlushedChange');
768
+ expect(noLongerFlushedEvent.handle).toStrictEqual(pubHandle);
769
+ expect(noLongerFlushedEvent.isFlushed).toStrictEqual(false);
770
+
771
+ await managerEvents.waitFor('packetAvailable');
772
+
773
+ // 3. Call flush() before the in-flight packet is acknowledged -- it should remain
774
+ // pending because the in-flight counter is still > 0
775
+ let flushed = false;
776
+ const flushPromise = localDataTrack.flush().then(() => {
777
+ flushed = true;
778
+ });
779
+
780
+ await new Promise((resolve) => setTimeout(resolve, 0));
781
+ expect(flushed).toStrictEqual(false);
782
+ expect(managerEvents.areThereBufferedEvents('packetsFlushedChange')).toBe(false);
783
+
784
+ // 4. Reset the manager. This simulates a RTCEngine disconnect and should resolve
785
+ // the pending flush() even though the packet was never acknowledged.
786
+ await manager.reset();
787
+ await managerEvents.waitFor('reset');
788
+
789
+ // 5. The flush() promise resolves
790
+ await flushPromise;
791
+ expect(flushed).toStrictEqual(true);
792
+
793
+ // 6. No packetsFlushed event was emitted -- reset short-circuits the flush directly
794
+ // on the LocalDataTrack rather than going through the in-flight counter.
795
+ expect(managerEvents.areThereBufferedEvents('packetsFlushedChange')).toBe(false);
796
+ });
797
+
798
+ it('should resolve flush() at the end of a batch of tryPush calls after all packets are later acknowledged as "sent"', async () => {
799
+ const pubHandle = 5;
800
+ const manager = OutgoingDataTrackManager.withDescriptors(
801
+ new Map([
802
+ [
803
+ DataTrackHandle.fromNumber(pubHandle),
804
+ Descriptor.active(
805
+ {
806
+ sid: 'bogus-sid',
807
+ pubHandle,
808
+ name: 'test',
809
+ usesE2ee: false,
810
+ },
811
+ null,
812
+ ),
813
+ ],
814
+ ]),
815
+ );
816
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
817
+ 'packetAvailable',
818
+ 'packetsFlushedChange',
819
+ ]);
820
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
821
+ { name: 'track name' },
822
+ manager,
823
+ pubHandle,
824
+ );
825
+
826
+ // 1. Run a batch of tryPush calls
827
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01]) });
828
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x02]) });
829
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x03]) });
830
+
831
+ // 2. A event should have been sent indicating that the data track is no longer "flushed"
832
+ const noLongerFlushedEvent = await managerEvents.waitFor('packetsFlushedChange');
833
+ expect(noLongerFlushedEvent.handle).toStrictEqual(pubHandle);
834
+ expect(noLongerFlushedEvent.isFlushed).toStrictEqual(false);
835
+
836
+ // 3. Three packetAvailable events should be emitted, one per pushed frame
837
+ await managerEvents.waitFor('packetAvailable');
838
+ await managerEvents.waitFor('packetAvailable');
839
+ await managerEvents.waitFor('packetAvailable');
840
+
841
+ // 4. After the batch is enqueued, call flush() to wait for the SFU to drain them
842
+ let flushed = false;
843
+ const flushPromise = localDataTrack.flush().then(() => {
844
+ flushed = true;
845
+ });
846
+
847
+ // 5. Acknowledge two of the three packets -- flush should not resolve yet
848
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
849
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
850
+ await new Promise((resolve) => setTimeout(resolve, 0));
851
+ expect(flushed).toStrictEqual(false);
852
+ expect(managerEvents.areThereBufferedEvents('packetsFlushedChange')).toBe(false);
853
+
854
+ // 6. Acknowledge the last packet -- flush resolves once the counter reaches 0
855
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
856
+
857
+ const flushedEvent = await managerEvents.waitFor('packetsFlushedChange');
858
+ expect(flushedEvent.handle).toStrictEqual(pubHandle);
859
+
860
+ await flushPromise;
861
+ expect(flushed).toStrictEqual(true);
862
+ });
863
+
864
+ it('should resolve flush() if there are no tryPush calls in flight', async () => {
865
+ const pubHandle = 5;
866
+ const manager = OutgoingDataTrackManager.withDescriptors(
867
+ new Map([
868
+ [
869
+ DataTrackHandle.fromNumber(pubHandle),
870
+ Descriptor.active(
871
+ {
872
+ sid: 'bogus-sid',
873
+ pubHandle,
874
+ name: 'test',
875
+ usesE2ee: false,
876
+ },
877
+ null,
878
+ ),
879
+ ],
880
+ ]),
881
+ );
882
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
883
+ { name: 'track name' },
884
+ manager,
885
+ pubHandle,
886
+ );
887
+
888
+ // Call flush, make sure it resolves on its own
889
+ await localDataTrack.flush();
890
+ });
891
+
892
+ it('should resolve flush() at the end of a batch of tryPush calls which have all fully sent their data', async () => {
893
+ const pubHandle = 5;
894
+ const manager = OutgoingDataTrackManager.withDescriptors(
895
+ new Map([
896
+ [
897
+ DataTrackHandle.fromNumber(pubHandle),
898
+ Descriptor.active(
899
+ {
900
+ sid: 'bogus-sid',
901
+ pubHandle,
902
+ name: 'test',
903
+ usesE2ee: false,
904
+ },
905
+ null,
906
+ ),
907
+ ],
908
+ ]),
909
+ );
910
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
911
+ 'packetAvailable',
912
+ ]);
913
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
914
+ { name: 'track name' },
915
+ manager,
916
+ pubHandle,
917
+ );
918
+
919
+ // 1. Run a batch of tryPush calls
920
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01]) });
921
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x02]) });
922
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x03]) });
923
+
924
+ // 2. Three packetAvailable events should be emitted, one per pushed frame
925
+ await managerEvents.waitFor('packetAvailable');
926
+ await managerEvents.waitFor('packetAvailable');
927
+ await managerEvents.waitFor('packetAvailable');
928
+
929
+ // 3. Acknowledge all three packets
930
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
931
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
932
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
933
+
934
+ // 4. Call flush and ensure that it resolves immediately
935
+ await localDataTrack.flush();
936
+ });
937
+
938
+ it('should send packetsFlushedChange events in between tryPush / handlePacketSendComplete calls', async () => {
939
+ const pubHandle = 5;
940
+ const manager = OutgoingDataTrackManager.withDescriptors(
941
+ new Map([
942
+ [
943
+ DataTrackHandle.fromNumber(pubHandle),
944
+ Descriptor.active(
945
+ {
946
+ sid: 'bogus-sid',
947
+ pubHandle,
948
+ name: 'test',
949
+ usesE2ee: false,
950
+ },
951
+ null,
952
+ ),
953
+ ],
954
+ ]),
955
+ );
956
+ const managerEvents = subscribeToEvents<DataTrackOutgoingManagerCallbacks>(manager, [
957
+ 'packetsFlushedChange',
958
+ ]);
959
+ const localDataTrack = LocalDataTrack.withExplicitHandle(
960
+ { name: 'track name' },
961
+ manager,
962
+ pubHandle,
963
+ );
964
+
965
+ // 1. Send a single packet frame
966
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x01]) });
967
+
968
+ // 2. Ensure the data track is no longer "flushed"
969
+ const noLongerFlushedEvent = await managerEvents.waitFor('packetsFlushedChange');
970
+ expect(noLongerFlushedEvent.handle).toStrictEqual(pubHandle);
971
+ expect(noLongerFlushedEvent.isFlushed).toStrictEqual(false);
972
+
973
+ // 3. Send more single packet frames
974
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x02]) });
975
+ await localDataTrack.tryPush({ payload: new Uint8Array([0x03]) });
976
+
977
+ // 3. Acknowledge the first two packets
978
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
979
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
980
+ expect(managerEvents.areThereBufferedEvents('packetsFlushedChange')).toBe(false);
981
+
982
+ // 4. Acknowledge the last packet
983
+ manager.handlePacketSendComplete(DataTrackHandle.fromNumber(pubHandle));
984
+
985
+ // 4. The data track should now be "flushed" again
986
+ const isFlushedEvent = await managerEvents.waitFor('packetsFlushedChange');
987
+ expect(isFlushedEvent.handle).toStrictEqual(pubHandle);
988
+ expect(isFlushedEvent.isFlushed).toStrictEqual(true);
989
+ });
990
+ });
605
991
  });
@@ -18,6 +18,7 @@ import DataTrackOutgoingPipeline from './pipeline';
18
18
  import {
19
19
  type DataTrackOptions,
20
20
  type EventPacketAvailable,
21
+ type EventPacketsFlushedChange,
21
22
  type EventSfuPublishRequest,
22
23
  type EventSfuUnpublishRequest,
23
24
  type EventTrackPublished,
@@ -74,6 +75,11 @@ export type DataTrackOutgoingManagerCallbacks = {
74
75
  trackPublished: (event: EventTrackPublished) => void;
75
76
  /** A {@link LocalDataTrack} has been unpublished */
76
77
  trackUnpublished: (event: EventTrackUnpublished) => void;
78
+ /** A {@link LocalDataTrack} has had all of its in flight packets sent via the rtc data channel. */
79
+ packetsFlushedChange: (event: EventPacketsFlushedChange) => void;
80
+ /** The manager has been reset and all state has been cleared in preparation for the next room
81
+ * connection. */
82
+ reset: () => void;
77
83
  };
78
84
 
79
85
  type OutgoingDataTrackManagerOptions = {
@@ -95,6 +101,11 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
95
101
 
96
102
  private descriptors = new Map<DataTrackHandle, Descriptor>();
97
103
 
104
+ /** Number of packets for each data track which have been emitted via the `packetAvailable` event
105
+ * and which have not yet been sent via the rtc data channel yet. Once this goes to 0, then
106
+ * all in flight packets have been delivered, and the data tracks is "flushed". */
107
+ private inFlightPacketCounter = new Map<DataTrackHandle, number>();
108
+
98
109
  constructor(options?: OutgoingDataTrackManagerOptions) {
99
110
  super();
100
111
  this.e2eeManager = options?.e2eeManager ?? null;
@@ -154,7 +165,15 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
154
165
 
155
166
  try {
156
167
  for await (const packet of descriptor.pipeline.processFrame(frame)) {
157
- this.emit('packetAvailable', { bytes: packet.toBinary() });
168
+ const prev = this.inFlightPacketCounter.get(handle) ?? 0;
169
+ this.inFlightPacketCounter.set(handle, prev + 1);
170
+ if (prev === 0) {
171
+ // A new packet has been sent, so there are now packets in the rtc data channel buffer for
172
+ // this data track frame
173
+ this.emit('packetsFlushedChange', { handle, isFlushed: false });
174
+ }
175
+
176
+ this.emit('packetAvailable', { handle, bytes: packet.toBinary() });
158
177
  }
159
178
  } catch (err) {
160
179
  // NOTE: In the rust implementation this "dropped" error means something different (not enough room
@@ -163,6 +182,27 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
163
182
  }
164
183
  }
165
184
 
185
+ /** The client has sent a packet over the rtc data channel. This signal is used for determining
186
+ * once all packets are sent and a data track has been "flushed".
187
+ *
188
+ * @internal */
189
+ handlePacketSendComplete(handle: DataTrackHandle) {
190
+ const prev = this.inFlightPacketCounter.get(handle) ?? 0;
191
+ let counter = prev - 1;
192
+
193
+ if (counter < 0) {
194
+ log.warn(
195
+ `OutgoingDataTrackManager.handlePacketSendComplete: inFlightPacketCounter was decremented below 0 (got ${this.inFlightPacketCounter} - resetting to 0. Were more packets send than were emitted?`,
196
+ );
197
+ counter = 0;
198
+ }
199
+ this.inFlightPacketCounter.set(handle, counter);
200
+
201
+ if (counter === 0) {
202
+ this.emit('packetsFlushedChange', { handle, isFlushed: true });
203
+ }
204
+ }
205
+
166
206
  /**
167
207
  * Client requested to publish a track.
168
208
  *
@@ -266,6 +306,7 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
266
306
 
267
307
  await descriptor.unpublishingFuture.promise;
268
308
 
309
+ this.inFlightPacketCounter.delete(handle);
269
310
  this.emit('trackUnpublished', { sid: descriptor.info.sid });
270
311
  }
271
312
 
@@ -360,10 +401,13 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
360
401
  }
361
402
 
362
403
  /**
363
- * Shuts down the manager and all associated tracks.
404
+ * Reset's the state of the manager and all associated tracks. Run on room disconnect to get
405
+ * the manager ready for the next room connection.
364
406
  * @internal
365
407
  **/
366
- async shutdown() {
408
+ async reset() {
409
+ this.handleAllocator.reset();
410
+
367
411
  for (const descriptor of this.descriptors.values()) {
368
412
  switch (descriptor.type) {
369
413
  case 'pending':
@@ -379,5 +423,9 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
379
423
  }
380
424
  }
381
425
  this.descriptors.clear();
426
+
427
+ this.inFlightPacketCounter.clear();
428
+
429
+ this.emit('reset');
382
430
  }
383
431
  }
@@ -34,6 +34,8 @@ export type EventSfuUnpublishRequest = {
34
34
 
35
35
  /** A serialized packet is ready to be sent over the transport. */
36
36
  export type EventPacketAvailable = {
37
+ /** The handle associated with the data track which this packet bytes belong to. */
38
+ handle: DataTrackHandle;
37
39
  bytes: Uint8Array;
38
40
  };
39
41
 
@@ -43,3 +45,6 @@ export type EventTrackPublished = { track: LocalDataTrack };
43
45
 
44
46
  /** A track has been unpublished by a remote participant and can no longer be subscribed to. */
45
47
  export type EventTrackUnpublished = { sid: DataTrackSid };
48
+
49
+ /** A track has had all of its in flight packets sent via the rtc data channel. */
50
+ export type EventPacketsFlushedChange = { handle: DataTrackHandle; isFlushed: boolean };