@sfourdrinier/react-native-ble-plx
Version:
React Native Bluetooth Low Energy library
293 lines (265 loc) • 8.66 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ReconnectionManager = void 0;
/**
* Reconnection state for a device
*/
/**
* Event callbacks for reconnection events
*/
/**
* ReconnectionManager automatically handles reconnection when devices disconnect unexpectedly.
*
* Features:
* - Automatic reconnection with exponential backoff
* - Configurable retry limits and delays
* - Event callbacks for monitoring reconnection status
* - Per-device configuration
*
* @example
* const reconnectionManager = new ReconnectionManager(bleManager);
*
* // Enable auto-reconnect for a device
* reconnectionManager.enableAutoReconnect('AA:BB:CC:DD:EE:FF', {
* maxRetries: 10,
* initialDelayMs: 1000,
* maxDelayMs: 60000,
* backoffMultiplier: 1.5
* }, {
* onReconnect: (device) => console.log('Reconnected!', device.id),
* onReconnectFailed: (deviceId, error) => console.log('Failed to reconnect', deviceId)
* });
*
* // Disable auto-reconnect
* reconnectionManager.disableAutoReconnect('AA:BB:CC:DD:EE:FF');
*/
class ReconnectionManager {
_devices = new Map();
_globalCallbacks = {};
constructor(manager) {
this._manager = manager;
}
/**
* Set global callbacks for all reconnection events.
*
* @param callbacks Callback functions for reconnection events
*/
setGlobalCallbacks(callbacks) {
this._globalCallbacks = callbacks;
}
/**
* Enable automatic reconnection for a device.
*
* @param deviceId Device identifier
* @param options Reconnection options (retries, delays, etc.)
* @param callbacks Optional callbacks for this specific device
*/
enableAutoReconnect(deviceId, options, callbacks) {
// If already enabled, disable first
if (this._devices.has(deviceId)) {
this.disableAutoReconnect(deviceId);
}
const reconnectionOptions = {
maxRetries: options?.maxRetries ?? 5,
initialDelayMs: options?.initialDelayMs ?? 1000,
maxDelayMs: options?.maxDelayMs ?? 30000,
backoffMultiplier: options?.backoffMultiplier ?? 2,
connectionOptions: options?.connectionOptions
};
const state = {
deviceId,
options: reconnectionOptions,
isReconnecting: false,
retryCount: 0,
onReconnect: callbacks?.onReconnect,
onReconnectFailed: callbacks?.onReconnectFailed,
cancelled: false
};
// Subscribe to disconnection events for this device
state.disconnectSubscription = this._manager.onDeviceDisconnected(deviceId, (error, _device) => {
// Only auto-reconnect on unexpected disconnections (error is not null)
// If error is null, it was an intentional disconnect via cancelDeviceConnection
if (error) {
this._startReconnection(deviceId);
}
});
this._devices.set(deviceId, state);
}
/**
* Disable automatic reconnection for a device.
*
* @param deviceId Device identifier
* @returns True if auto-reconnect was disabled, false if it wasn't enabled
*/
disableAutoReconnect(deviceId) {
const state = this._devices.get(deviceId);
if (!state) {
return false;
}
// Mark as cancelled to stop in-flight reconnection attempts
state.cancelled = true;
// Clear any pending reconnection attempt
if (state.timeoutId) {
clearTimeout(state.timeoutId);
}
// Remove disconnect subscription
if (state.disconnectSubscription) {
state.disconnectSubscription.remove();
}
this._devices.delete(deviceId);
return true;
}
/**
* Disable auto-reconnect for all devices.
*/
disableAll() {
const deviceIds = Array.from(this._devices.keys());
for (const deviceId of deviceIds) {
this.disableAutoReconnect(deviceId);
}
}
/**
* Check if auto-reconnect is enabled for a device.
*
* @param deviceId Device identifier
* @returns True if auto-reconnect is enabled
*/
isEnabled(deviceId) {
return this._devices.has(deviceId);
}
/**
* Check if a device is currently reconnecting.
*
* @param deviceId Device identifier
* @returns True if reconnection is in progress
*/
isReconnecting(deviceId) {
const state = this._devices.get(deviceId);
return state?.isReconnecting ?? false;
}
/**
* Get the current retry count for a device.
*
* @param deviceId Device identifier
* @returns Current retry count, or -1 if auto-reconnect is not enabled
*/
getRetryCount(deviceId) {
const state = this._devices.get(deviceId);
return state?.retryCount ?? -1;
}
/**
* Manually trigger a reconnection attempt.
* Useful if you want to retry after all automatic retries have been exhausted.
*
* @param deviceId Device identifier
* @returns Promise resolving to connected Device
*/
async manualReconnect(deviceId) {
const state = this._devices.get(deviceId);
if (!state) {
throw new Error(`Auto-reconnect not enabled for device ${deviceId}`);
}
// Reset retry count for manual reconnect
state.retryCount = 0;
return this._attemptReconnection(deviceId);
}
/**
* Start the reconnection process for a device
*/
_startReconnection(deviceId) {
const state = this._devices.get(deviceId);
if (!state || state.isReconnecting) {
return;
}
state.isReconnecting = true;
state.retryCount = 0;
this._scheduleReconnection(deviceId, 0);
}
/**
* Schedule a reconnection attempt with delay
*/
_scheduleReconnection(deviceId, delay) {
const state = this._devices.get(deviceId);
if (!state) {
return;
}
state.timeoutId = setTimeout(async () => {
try {
await this._attemptReconnection(deviceId);
} catch (_error) {
// Error handling is done in _attemptReconnection
}
}, delay);
}
/**
* Attempt to reconnect to a device
*/
async _attemptReconnection(deviceId) {
const state = this._devices.get(deviceId);
if (!state) {
throw new Error(`Auto-reconnect not enabled for device ${deviceId}`);
}
// Check if cancelled before attempting
if (state.cancelled) {
throw new Error(`Auto-reconnect cancelled for device ${deviceId}`);
}
state.retryCount++;
const maxRetries = state.options.maxRetries ?? 5;
// Notify about reconnection attempt
this._globalCallbacks.onReconnecting?.(deviceId, state.retryCount, maxRetries);
try {
const device = await this._manager.connectToDevice(deviceId, state.options.connectionOptions);
// Check if cancelled or removed after await (race condition protection)
const currentState = this._devices.get(deviceId);
if (!currentState || currentState !== state || currentState.cancelled) {
// State was disabled during connection - disconnect immediately
await this._manager.cancelDeviceConnection(deviceId).catch(() => {
// Ignore errors when cleaning up
});
throw new Error(`Auto-reconnect cancelled for device ${deviceId} during connection`);
}
// Success!
state.isReconnecting = false;
state.retryCount = 0;
// Notify callbacks
state.onReconnect?.(device);
this._globalCallbacks.onReconnect?.(device);
return device;
} catch (error) {
// Check if cancelled after await
const currentState = this._devices.get(deviceId);
if (!currentState || currentState !== state || currentState.cancelled) {
// Don't schedule retry or call callbacks if cancelled
throw new Error(`Auto-reconnect cancelled for device ${deviceId}`);
}
if (state.retryCount >= maxRetries) {
// Max retries exceeded
state.isReconnecting = false;
const bleError = error;
// Notify callbacks
state.onReconnectFailed?.(deviceId, bleError);
this._globalCallbacks.onReconnectFailed?.(deviceId, bleError);
throw bleError;
}
// Calculate delay with exponential backoff
const initialDelay = state.options.initialDelayMs ?? 1000;
const maxDelay = state.options.maxDelayMs ?? 30000;
const multiplier = state.options.backoffMultiplier ?? 2;
const delay = Math.min(initialDelay * Math.pow(multiplier, state.retryCount - 1), maxDelay);
// Schedule next attempt
this._scheduleReconnection(deviceId, delay);
throw error;
}
}
/**
* Destroy the manager and disable all auto-reconnect.
*/
destroy() {
this.disableAll();
this._globalCallbacks = {};
}
}
exports.ReconnectionManager = ReconnectionManager;
//# sourceMappingURL=ReconnectionManager.js.map