@@ -50,7 +50,7 @@ const deviceCharacteristics = new Map<string, CharacteristicPair>();
5050const notificationCallbacks = new Map < string , ( data : string ) => void > ( ) ;
5151const subscribedDevices = new Map < string , boolean > ( ) ; // Track subscription status
5252
53- // 🔒 Add subscription operation state tracking to prevent race conditions
53+ // 🔒 Subscription operation state tracking to prevent race conditions
5454const subscriptionOperations = new Map < string , 'subscribing' | 'unsubscribing' | 'idle' > ( ) ;
5555
5656// Packet reassembly state for each device
@@ -1367,25 +1367,28 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
13671367 // 🔒 Set operation state to prevent race conditions
13681368 subscriptionOperations . set ( deviceId , 'unsubscribing' ) ;
13691369
1370- return new Promise < void > ( resolve => {
1371- notifyCharacteristic . unsubscribe ( ( error : Error | undefined ) => {
1372- if ( error ) {
1373- logger ?. error ( '[NobleBLE] Notification unsubscription failed:' , error ) ;
1374- } else {
1375- logger ?. info ( '[NobleBLE] Notification unsubscription successful' ) ;
1376- }
1377-
1378- // Remove all listeners and clear subscription status
1379- notifyCharacteristic . removeAllListeners ( 'data' ) ;
1380- notificationCallbacks . delete ( deviceId ) ;
1381- devicePacketStates . delete ( deviceId ) ;
1382- subscribedDevices . delete ( deviceId ) ;
1383-
1384- // 🔒 Clear operation state
1385- subscriptionOperations . set ( deviceId , 'idle' ) ;
1386- resolve ( ) ;
1370+ try {
1371+ await new Promise < void > ( ( resolve , reject ) => {
1372+ notifyCharacteristic . unsubscribe ( ( error : Error | undefined ) => {
1373+ if ( error ) {
1374+ logger ?. error ( '[NobleBLE] Notification unsubscription failed:' , error ) ;
1375+ reject ( error ) ;
1376+ } else {
1377+ logger ?. info ( '[NobleBLE] Notification unsubscription successful' ) ;
1378+ resolve ( ) ;
1379+ }
1380+ } ) ;
13871381 } ) ;
1388- } ) ;
1382+
1383+ // Remove all listeners and clear subscription status
1384+ notifyCharacteristic . removeAllListeners ( 'data' ) ;
1385+ notificationCallbacks . delete ( deviceId ) ;
1386+ devicePacketStates . delete ( deviceId ) ;
1387+ subscribedDevices . delete ( deviceId ) ;
1388+ } finally {
1389+ // 🔒 CRITICAL: Always clear operation state (even on error)
1390+ subscriptionOperations . set ( deviceId , 'idle' ) ;
1391+ }
13891392}
13901393
13911394// Subscribe to notifications
@@ -1406,20 +1409,37 @@ async function subscribeNotifications(
14061409 const { notify : notifyCharacteristic } = characteristics ;
14071410
14081411 logger ?. info ( '[NobleBLE] Subscribing to notifications for device:' , deviceId ) ;
1412+
1413+ // 🔒 CRITICAL: Check operation state FIRST to prevent race conditions
1414+ const opState = subscriptionOperations . get ( deviceId ) ;
1415+
14091416 logger ?. info ( '[NobleBLE] Subscribe context' , {
14101417 deviceId,
1411- opStateBefore : subscriptionOperations . get ( deviceId ) || 'idle' ,
1418+ opStateBefore : opState || 'idle' ,
14121419 paired : false ,
14131420 hasController : false ,
14141421 } ) ;
1422+
14151423 // If a subscription is already in progress, dedupe
1416- const opState = subscriptionOperations . get ( deviceId ) ;
14171424 if ( opState === 'subscribing' ) {
1418- // Subscription in progress; update callback and return
1425+ logger ?. info ( '[NobleBLE] Subscription already in progress, updating callback only' ) ;
14191426 notificationCallbacks . set ( deviceId , callback ) ;
14201427 return Promise . resolve ( ) ;
14211428 }
14221429
1430+ // 🚨 CRITICAL: Reject subscribe if unsubscribe is in progress
1431+ // Let upper layer handle retry after device reconnection
1432+ if ( opState === 'unsubscribing' ) {
1433+ logger ?. error ( '[NobleBLE] Cannot subscribe while unsubscribe is in progress' , {
1434+ deviceId,
1435+ opState,
1436+ } ) ;
1437+ throw ERRORS . TypedError (
1438+ HardwareErrorCode . DeviceBusy ,
1439+ `Device ${ deviceId } is currently unsubscribing, please retry after reconnection`
1440+ ) ;
1441+ }
1442+
14231443 // 🔒 Set operation state to prevent race conditions
14241444 subscriptionOperations . set ( deviceId , 'subscribing' ) ;
14251445
0 commit comments