fix: memory leak hunting and reconnecting on data error
This commit is contained in:
@@ -13,7 +13,7 @@ describe('peer recovery', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('waits a short grace period before replacing a closed data channel in place', () => {
|
||||
it('recreates a peer immediately when the data channel is already closed', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const channel = createDataChannel('closed');
|
||||
@@ -24,29 +24,28 @@ describe('peer recovery', () => {
|
||||
|
||||
scheduleDataChannelRecovery(context, 'bob', channel, 'close', handlers);
|
||||
|
||||
expect(handlers.removePeer).toHaveBeenCalledWith('bob', { preserveReconnectState: true });
|
||||
expect(handlers.createPeerConnection).toHaveBeenCalledWith('bob', true);
|
||||
expect(handlers.createAndSendOffer).toHaveBeenCalledWith('bob');
|
||||
expect(context.state.dataChannelRecoveryTimers.has('bob')).toBe(false);
|
||||
});
|
||||
|
||||
it('waits a short grace period before recreating a peer with a closing data channel', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const channel = createDataChannel('closing');
|
||||
const context = createContext('alice');
|
||||
const handlers = createRecoveryHandlers(context);
|
||||
|
||||
context.state.activePeerConnections.set('bob', createPeerData(channel, 'connected'));
|
||||
|
||||
scheduleDataChannelRecovery(context, 'bob', channel, 'close', handlers);
|
||||
|
||||
vi.advanceTimersByTime(DATA_CHANNEL_RECOVERY_GRACE_MS - 1);
|
||||
expect(handlers.removePeer).not.toHaveBeenCalled();
|
||||
expect(handlers.createPeerConnection).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(handlers.replaceDataChannel).toHaveBeenCalledWith('bob', channel);
|
||||
expect(handlers.removePeer).not.toHaveBeenCalled();
|
||||
expect(handlers.createPeerConnection).not.toHaveBeenCalled();
|
||||
expect(handlers.createAndSendOffer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to full peer recreation when in-place data channel replacement fails', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const channel = createDataChannel('closed');
|
||||
const context = createContext('alice');
|
||||
const handlers = createRecoveryHandlers(context);
|
||||
|
||||
handlers.replaceDataChannel.mockReturnValueOnce(false);
|
||||
context.state.activePeerConnections.set('bob', createPeerData(channel, 'connected'));
|
||||
|
||||
scheduleDataChannelRecovery(context, 'bob', channel, 'close', handlers);
|
||||
vi.advanceTimersByTime(DATA_CHANNEL_RECOVERY_GRACE_MS);
|
||||
|
||||
expect(handlers.removePeer).toHaveBeenCalledWith('bob', { preserveReconnectState: true });
|
||||
expect(handlers.createPeerConnection).toHaveBeenCalledWith('bob', true);
|
||||
@@ -56,7 +55,7 @@ describe('peer recovery', () => {
|
||||
it('does not recreate a peer when a replacement data channel is adopted before the grace expires', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const staleChannel = createDataChannel('closed');
|
||||
const staleChannel = createDataChannel('closing');
|
||||
const replacementChannel = createDataChannel(DATA_CHANNEL_STATE_OPEN);
|
||||
const context = createContext('alice');
|
||||
const handlers = createRecoveryHandlers(context);
|
||||
@@ -90,7 +89,7 @@ describe('peer recovery', () => {
|
||||
expect(handlers.createPeerConnection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves a connected non-initiator peer while waiting for the remote initiator to replace the channel', () => {
|
||||
it('recreates a connected non-initiator peer and waits for the remote initiator offer', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const channel = createDataChannel('closed');
|
||||
@@ -99,11 +98,10 @@ describe('peer recovery', () => {
|
||||
|
||||
context.state.activePeerConnections.set('bob', createPeerData(channel, 'connected', false));
|
||||
scheduleDataChannelRecovery(context, 'bob', channel, 'close', handlers);
|
||||
vi.advanceTimersByTime(DATA_CHANNEL_RECOVERY_GRACE_MS);
|
||||
|
||||
expect(handlers.removePeer).not.toHaveBeenCalled();
|
||||
expect(handlers.removePeer).toHaveBeenCalledWith('bob', { preserveReconnectState: true });
|
||||
expect(handlers.replaceDataChannel).not.toHaveBeenCalled();
|
||||
expect(handlers.createPeerConnection).not.toHaveBeenCalled();
|
||||
expect(handlers.createPeerConnection).toHaveBeenCalledWith('bob', false);
|
||||
expect(handlers.createAndSendOffer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -154,6 +154,18 @@ export function scheduleDataChannelRecovery(
|
||||
if (channel.readyState === DATA_CHANNEL_STATE_OPEN)
|
||||
return;
|
||||
|
||||
if (channel.readyState === 'closed') {
|
||||
logger.warn('[data-channel] Control channel closed; reconnecting peer immediately', {
|
||||
channelLabel: channel.label,
|
||||
connectionState: peerData.connection.connectionState,
|
||||
peerId,
|
||||
reason
|
||||
});
|
||||
|
||||
repairUnavailableDataChannel(context, peerId, channel, reason, handlers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.dataChannelRecoveryTimers.has(peerId))
|
||||
return;
|
||||
|
||||
@@ -183,35 +195,42 @@ export function scheduleDataChannelRecovery(
|
||||
reason
|
||||
});
|
||||
|
||||
if (latestPeerData.connection.connectionState === CONNECTION_STATE_CONNECTED) {
|
||||
if (latestPeerData.isInitiator && handlers.replaceDataChannel(peerId, channel)) {
|
||||
logger.info('[data-channel] Replaced control channel without recreating media transport', {
|
||||
peerId,
|
||||
reason
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!latestPeerData.isInitiator) {
|
||||
logger.info('[data-channel] Waiting for initiator to replace control channel; preserving media transport', {
|
||||
peerId,
|
||||
reason
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
trackDisconnectedPeer(state, peerId);
|
||||
handlers.removePeer(peerId, { preserveReconnectState: true });
|
||||
attemptPeerReconnect(context, peerId, handlers);
|
||||
schedulePeerReconnect(context, peerId, handlers);
|
||||
repairUnavailableDataChannel(context, peerId, channel, reason, handlers);
|
||||
}, DATA_CHANNEL_RECOVERY_GRACE_MS);
|
||||
|
||||
state.dataChannelRecoveryTimers.set(peerId, timer);
|
||||
}
|
||||
|
||||
function repairUnavailableDataChannel(
|
||||
context: PeerConnectionManagerContext,
|
||||
peerId: string,
|
||||
channel: RTCDataChannel,
|
||||
reason: string,
|
||||
handlers: RecoveryHandlers
|
||||
): void {
|
||||
const { logger, state } = context;
|
||||
const peerData = state.activePeerConnections.get(peerId);
|
||||
|
||||
if (!peerData || peerData.dataChannel !== channel)
|
||||
return;
|
||||
|
||||
if (peerData.dataChannel?.readyState === DATA_CHANNEL_STATE_OPEN)
|
||||
return;
|
||||
|
||||
logger.warn('[data-channel] Recreating peer transport after control channel failure', {
|
||||
channelLabel: channel.label,
|
||||
connectionState: peerData.connection.connectionState,
|
||||
peerId,
|
||||
readyState: peerData.dataChannel?.readyState ?? null,
|
||||
reason
|
||||
});
|
||||
|
||||
trackDisconnectedPeer(state, peerId);
|
||||
handlers.removePeer(peerId, { preserveReconnectState: true });
|
||||
attemptPeerReconnect(context, peerId, handlers);
|
||||
schedulePeerReconnect(context, peerId, handlers);
|
||||
}
|
||||
|
||||
export function schedulePeerDisconnectRecovery(
|
||||
context: PeerConnectionManagerContext,
|
||||
peerId: string,
|
||||
|
||||
Reference in New Issue
Block a user