Prevents race conditions when multiple connection attempts are made to the same device simultaneously by: - Adding connectingDevices Set to track in-progress connections - Checking for concurrent connections before starting new attempt - Returning early if device is already connected - Using finally block to ensure cleanup of connecting state - Clearing connectingDevices set on cleanup Includes comprehensive test suite for concurrent connection scenarios including edge cases like rapid connect/disconnect cycles and cleanup during connection attempts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
257 lines
9.1 KiB
TypeScript
257 lines
9.1 KiB
TypeScript
/**
|
|
* Tests for BLE Concurrent Connection Protection
|
|
*/
|
|
|
|
import { MockBLEManager } from '../MockBLEManager';
|
|
import { BLEConnectionState } from '../types';
|
|
|
|
describe('BLE Concurrent Connection Protection', () => {
|
|
let manager: MockBLEManager;
|
|
|
|
beforeEach(() => {
|
|
manager = new MockBLEManager();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await manager.cleanup();
|
|
});
|
|
|
|
describe('Concurrent Connection Prevention', () => {
|
|
it('should prevent concurrent connection attempts to the same device', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// Start two connection attempts simultaneously
|
|
const connection1Promise = manager.connectDevice(deviceId);
|
|
const connection2Promise = manager.connectDevice(deviceId);
|
|
|
|
// Wait for both to complete
|
|
const [result1, result2] = await Promise.all([connection1Promise, connection2Promise]);
|
|
|
|
// One should succeed, one should fail
|
|
expect(result1 !== result2).toBe(true);
|
|
|
|
// The device should end up connected (from the successful attempt)
|
|
const finalState = manager.getConnectionState(deviceId);
|
|
expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(finalState);
|
|
});
|
|
|
|
it('should allow reconnection after first connection completes', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// First connection
|
|
const result1 = await manager.connectDevice(deviceId);
|
|
expect(result1).toBe(true);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
|
|
// Disconnect
|
|
await manager.disconnectDevice(deviceId);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.DISCONNECTED);
|
|
|
|
// Second connection should succeed
|
|
const result2 = await manager.connectDevice(deviceId);
|
|
expect(result2).toBe(true);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
});
|
|
|
|
it('should handle multiple concurrent connections to different devices', async () => {
|
|
const deviceIds = ['device-1', 'device-2', 'device-3'];
|
|
|
|
// Connect to all devices simultaneously
|
|
const connectionPromises = deviceIds.map(id => manager.connectDevice(id));
|
|
const results = await Promise.all(connectionPromises);
|
|
|
|
// All connections should succeed
|
|
expect(results).toEqual([true, true, true]);
|
|
|
|
// All devices should be in READY state
|
|
deviceIds.forEach(id => {
|
|
expect(manager.getConnectionState(id)).toBe(BLEConnectionState.READY);
|
|
});
|
|
});
|
|
|
|
it('should allow connection if device is already connected', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// First connection
|
|
const result1 = await manager.connectDevice(deviceId);
|
|
expect(result1).toBe(true);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
|
|
// Second connection attempt while already connected should return true
|
|
const result2 = await manager.connectDevice(deviceId);
|
|
expect(result2).toBe(true);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
});
|
|
|
|
it('should emit connection_failed event for concurrent connection attempt', async () => {
|
|
const deviceId = 'test-device';
|
|
const events: Array<{ event: string; error?: string }> = [];
|
|
|
|
const listener = (id: string, event: string, data?: any) => {
|
|
if (id === deviceId) {
|
|
events.push({ event, error: data?.error });
|
|
}
|
|
};
|
|
|
|
manager.addEventListener(listener);
|
|
|
|
// Start two connection attempts simultaneously
|
|
await Promise.all([
|
|
manager.connectDevice(deviceId),
|
|
manager.connectDevice(deviceId),
|
|
]);
|
|
|
|
// Should have at least one connection_failed event
|
|
const failedEvents = events.filter(e => e.event === 'connection_failed');
|
|
expect(failedEvents.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// The error should mention concurrent connection
|
|
const concurrentError = failedEvents.find(e =>
|
|
e.error?.includes('Connection already in progress')
|
|
);
|
|
expect(concurrentError).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Connection State During Concurrent Attempts', () => {
|
|
it('should maintain consistent state during concurrent attempts', async () => {
|
|
const deviceId = 'test-device';
|
|
const states: BLEConnectionState[] = [];
|
|
|
|
const listener = (id: string, event: string, data?: any) => {
|
|
if (id === deviceId && event === 'state_changed') {
|
|
states.push(data.state);
|
|
}
|
|
};
|
|
|
|
manager.addEventListener(listener);
|
|
|
|
// Start concurrent connections
|
|
await Promise.all([
|
|
manager.connectDevice(deviceId),
|
|
manager.connectDevice(deviceId),
|
|
]);
|
|
|
|
// States should follow valid transitions
|
|
// Should not have duplicate CONNECTING states (only one should enter)
|
|
const connectingCount = states.filter(s => s === BLEConnectionState.CONNECTING).length;
|
|
expect(connectingCount).toBeLessThanOrEqual(2); // Max 2: one success, one immediate error
|
|
|
|
// Final state should be either READY or ERROR
|
|
const finalState = manager.getConnectionState(deviceId);
|
|
expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(finalState);
|
|
});
|
|
|
|
it('should track connection attempts correctly with getAllConnections', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// Start concurrent connections
|
|
await Promise.all([
|
|
manager.connectDevice(deviceId),
|
|
manager.connectDevice(deviceId),
|
|
]);
|
|
|
|
const connections = manager.getAllConnections();
|
|
const deviceConnection = connections.get(deviceId);
|
|
|
|
expect(deviceConnection).toBeDefined();
|
|
expect(deviceConnection?.deviceId).toBe(deviceId);
|
|
expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(deviceConnection?.state);
|
|
});
|
|
});
|
|
|
|
describe('Cleanup During Connection', () => {
|
|
it('should properly cleanup concurrent connection attempts', async () => {
|
|
const deviceIds = ['device-1', 'device-2', 'device-3'];
|
|
|
|
// Start connections
|
|
const connectionPromises = deviceIds.map(id => manager.connectDevice(id));
|
|
|
|
// Don't wait for connections, cleanup immediately
|
|
await manager.cleanup();
|
|
|
|
// All connections should be cleaned up
|
|
const connections = manager.getAllConnections();
|
|
expect(connections.size).toBe(0);
|
|
|
|
// All devices should be disconnected
|
|
deviceIds.forEach(id => {
|
|
expect(manager.getConnectionState(id)).toBe(BLEConnectionState.DISCONNECTED);
|
|
});
|
|
});
|
|
|
|
it('should clear connecting devices set on cleanup', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// Start connection (but don't await)
|
|
const connectionPromise = manager.connectDevice(deviceId);
|
|
|
|
// Cleanup before connection completes
|
|
await manager.cleanup();
|
|
|
|
// Try to connect again after cleanup - should work
|
|
const result = await manager.connectDevice(deviceId);
|
|
expect(result).toBe(true);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle rapid connect/disconnect/connect cycles', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// Connect
|
|
await manager.connectDevice(deviceId);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
|
|
// Disconnect
|
|
await manager.disconnectDevice(deviceId);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.DISCONNECTED);
|
|
|
|
// Reconnect immediately
|
|
const result = await manager.connectDevice(deviceId);
|
|
expect(result).toBe(true);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
});
|
|
|
|
it('should handle connection attempt during disconnection', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// Connect first
|
|
await manager.connectDevice(deviceId);
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
|
|
// Start disconnection (don't await)
|
|
const disconnectPromise = manager.disconnectDevice(deviceId);
|
|
|
|
// Try to connect while disconnecting
|
|
const connectResult = await manager.connectDevice(deviceId);
|
|
|
|
// Wait for disconnect to complete
|
|
await disconnectPromise;
|
|
|
|
// Connection attempt should have a defined result
|
|
expect(typeof connectResult).toBe('boolean');
|
|
});
|
|
|
|
it('should handle multiple rapid concurrent attempts', async () => {
|
|
const deviceId = 'test-device';
|
|
|
|
// Start 5 concurrent connection attempts
|
|
const attempts = Array(5).fill(null).map(() => manager.connectDevice(deviceId));
|
|
const results = await Promise.all(attempts);
|
|
|
|
// At least one should succeed or all should fail consistently
|
|
const successCount = results.filter(r => r === true).length;
|
|
const failureCount = results.filter(r => r === false).length;
|
|
|
|
expect(successCount + failureCount).toBe(5);
|
|
|
|
// If any succeeded, device should be connected
|
|
if (successCount > 0) {
|
|
expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY);
|
|
}
|
|
});
|
|
});
|
|
});
|