@sfourdrinier/react-native-ble-plx
Version:
React Native Bluetooth Low Energy library
231 lines (211 loc) • 6.72 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ConnectionQueue = void 0;
var _BleError = require("./BleError");
const ignoreConnectionCancellationError = () => {
// Native cancellation can reject if the connection already ended.
};
/**
* Connection attempt state
*/
/**
* Options for queuing a connection attempt
*/
/**
* ConnectionQueue manages connection attempts with automatic retry logic
* and queue management to prevent connection storms.
*
* Features:
* - Queues connection attempts to prevent multiple simultaneous connections
* - Exponential backoff retry logic
* - Configurable retry limits and delays
* - Ability to cancel pending/in-progress connections
*
* @example
* const queue = new ConnectionQueue(bleManager);
*
* // Connect with retry logic
* const device = await queue.connect('AA:BB:CC:DD:EE:FF', {
* maxRetries: 5,
* initialDelayMs: 1000,
* backoffMultiplier: 2
* });
*
* // Cancel a pending connection
* queue.cancel('AA:BB:CC:DD:EE:FF');
*/
class ConnectionQueue {
_queue = new Map();
constructor(manager) {
this._manager = manager;
}
/**
* Queue a connection attempt with retry logic.
*
* @param deviceId Device identifier to connect to
* @param options Connection and retry options
* @returns Promise resolving to connected Device
*/
connect(deviceId, options) {
return new Promise((resolve, reject) => {
// Check if there's already a pending connection for this device
const existing = this._queue.get(deviceId);
if (existing && !existing.cancelled) {
reject(new _BleError.BleError({
errorCode: _BleError.BleErrorCode.OperationCancelled,
attErrorCode: null,
iosErrorCode: null,
androidErrorCode: null,
reason: `Connection already pending for device ${deviceId}`
}, _BleError.BleErrorCodeMessage));
return;
}
const attempt = {
deviceId,
options: options?.connectionOptions,
resolve,
reject,
retryCount: 0,
maxRetries: options?.maxRetries ?? 3,
initialDelayMs: options?.initialDelayMs ?? 1000,
maxDelayMs: options?.maxDelayMs ?? 30000,
backoffMultiplier: options?.backoffMultiplier ?? 2,
cancelled: false,
isConnecting: false
};
this._queue.set(deviceId, attempt);
this._processQueue();
});
}
/**
* Cancel a pending or in-progress connection attempt.
*
* @param deviceId Device identifier to cancel connection for
* @returns True if a connection was cancelled
*/
cancel(deviceId) {
const attempt = this._queue.get(deviceId);
if (!attempt) {
return false;
}
attempt.cancelled = true;
if (attempt.timeoutId) {
clearTimeout(attempt.timeoutId);
}
this._queue.delete(deviceId);
attempt.reject(new _BleError.BleError({
errorCode: _BleError.BleErrorCode.OperationCancelled,
attErrorCode: null,
iosErrorCode: null,
androidErrorCode: null,
reason: `Connection cancelled for device ${deviceId}`
}, _BleError.BleErrorCodeMessage));
// If this was actively connecting, try to disconnect
if (attempt.isConnecting) {
this._manager.cancelDeviceConnection(deviceId).catch(ignoreConnectionCancellationError);
}
// Process queue to start next pending connection
this._processQueue();
return true;
}
/**
* Cancel all pending connections.
*/
cancelAll() {
const deviceIds = Array.from(this._queue.keys());
for (const deviceId of deviceIds) {
this.cancel(deviceId);
}
}
/**
* Get the number of pending connections in the queue.
*/
get pendingCount() {
return this._queue.size;
}
/**
* Check if a device has a pending connection.
*/
isPending(deviceId) {
const attempt = this._queue.get(deviceId);
return attempt != null && !attempt.cancelled;
}
/**
* Process the connection queue
* Allows concurrent connections to different devices
*/
_processQueue() {
// Start connection attempts for all pending (not connecting, not cancelled) devices
for (const attempt of this._queue.values()) {
if (!attempt.cancelled && !attempt.isConnecting) {
// Start this connection attempt asynchronously
this._attemptConnection(attempt);
}
}
}
/**
* Attempt to connect to a device with retry logic
* Runs asynchronously per device, allowing concurrent connections
*/
async _attemptConnection(attempt) {
if (attempt.cancelled) {
this._queue.delete(attempt.deviceId);
return;
}
// Mark as actively connecting (prevents duplicate concurrent attempts to same device)
attempt.isConnecting = true;
try {
const device = await this._manager.connectToDevice(attempt.deviceId, attempt.options);
if (attempt.cancelled) {
// Connection succeeded but was cancelled, disconnect
await this._manager.cancelDeviceConnection(attempt.deviceId).catch(ignoreConnectionCancellationError);
this._queue.delete(attempt.deviceId);
return;
}
// Success!
this._queue.delete(attempt.deviceId);
attempt.resolve(device);
} catch (error) {
if (attempt.cancelled) {
this._queue.delete(attempt.deviceId);
return;
}
attempt.retryCount++;
if (attempt.retryCount >= attempt.maxRetries) {
// Max retries exceeded
this._queue.delete(attempt.deviceId);
// Normalize error to BleError
const bleError = error instanceof _BleError.BleError ? error : new _BleError.BleError({
errorCode: _BleError.BleErrorCode.UnknownError,
attErrorCode: null,
iosErrorCode: null,
androidErrorCode: null,
reason: error instanceof Error ? error.message : String(error)
}, _BleError.BleErrorCodeMessage);
attempt.reject(bleError);
return;
}
// Calculate delay with exponential backoff
const delay = Math.min(attempt.initialDelayMs * Math.pow(attempt.backoffMultiplier, attempt.retryCount - 1), attempt.maxDelayMs);
// Mark as not connecting during delay (allows cancellation)
attempt.isConnecting = false;
// Schedule retry
attempt.timeoutId = setTimeout(() => {
if (!attempt.cancelled) {
this._attemptConnection(attempt);
}
}, delay);
}
}
/**
* Destroy the queue and cancel all pending connections.
*/
destroy() {
this.cancelAll();
this._queue.clear();
}
}
exports.ConnectionQueue = ConnectionQueue;
//# sourceMappingURL=ConnectionQueue.js.map