diff --git a/lib/characteristic.js b/lib/characteristic.js index acb97478..35deaccf 100644 --- a/lib/characteristic.js +++ b/lib/characteristic.js @@ -1,8 +1,8 @@ -const { EventEmitter } = require('events'); +const NobleEventEmitter = require('./noble-event-emitter'); const characteristics = require('./characteristics.json'); -class Characteristic extends EventEmitter { +class Characteristic extends NobleEventEmitter { constructor (noble, peripheralId, serviceUuid, uuid, properties) { super(); @@ -81,7 +81,7 @@ class Characteristic extends EventEmitter { } if (callback) { - this.once('write', error => callback(error)); + this.onceExclusive('write', error => callback(error)); } this._noble.write( @@ -134,7 +134,7 @@ class Characteristic extends EventEmitter { } if (callback) { - this.once('notify', (state, error) => callback(error, state)); + this.onceExclusive('notify', (state, error) => callback(error, state)); } this._noble.notify( @@ -180,30 +180,30 @@ class Characteristic extends EventEmitter { } else if (notifying) { // Wait for more data or notify=false await new Promise(resolve => { - // Create listeners that automatically remove themselves - const tempDataListener = (...args) => { + let resolved = false; + + const cleanup = () => { + if (resolved) return; + resolved = true; this.removeListener('data', tempDataListener); this.removeListener('notify', tempNotifyListener); resolve(); }; - + + const tempDataListener = () => cleanup(); + const tempNotifyListener = (state) => { if (state === false) { - this.removeListener('data', tempDataListener); - this.removeListener('notify', tempNotifyListener); - resolve(); + cleanup(); } }; - - // Set up temporary listeners + this.once('data', tempDataListener); this.once('notify', tempNotifyListener); - + // Clean up if we already have notifications (race condition) if (notifications.length > 0) { - this.removeListener('data', tempDataListener); - this.removeListener('notify', tempNotifyListener); - resolve(); + cleanup(); } }); } @@ -219,7 +219,7 @@ class Characteristic extends EventEmitter { discoverDescriptors (callback) { if (callback) { - this.once('descriptorsDiscover', (descriptors, error) => callback(error, descriptors)); + this.onceExclusive('descriptorsDiscover', (descriptors, error) => callback(error, descriptors)); } this._noble.discoverDescriptors( @@ -239,7 +239,7 @@ class Characteristic extends EventEmitter { broadcast (broadcast, callback) { if (callback) { - this.once('broadcast', error => callback(error)); + this.onceExclusive('broadcast', error => callback(error)); } this._noble.broadcast( diff --git a/lib/descriptor.js b/lib/descriptor.js index 59574311..b48b5787 100644 --- a/lib/descriptor.js +++ b/lib/descriptor.js @@ -1,7 +1,7 @@ -const { EventEmitter } = require('events'); +const NobleEventEmitter = require('./noble-event-emitter'); const descriptors = require('./descriptors.json'); -class Descriptor extends EventEmitter { +class Descriptor extends NobleEventEmitter { constructor (noble, peripheralId, serviceUuid, characteristicUuid, uuid) { super(); @@ -31,7 +31,7 @@ class Descriptor extends EventEmitter { readValue (callback) { if (callback) { - this.once('valueRead', (data, error) => callback(error, data)); + this.onceExclusive('valueRead', (data, error) => callback(error, data)); } this._noble.readValue( this._peripheralId, @@ -53,7 +53,7 @@ class Descriptor extends EventEmitter { } if (callback) { - this.once('valueWrite', error => callback(error)); + this.onceExclusive('valueWrite', error => callback(error)); } this._noble.writeValue( this._peripheralId, diff --git a/lib/noble-event-emitter.js b/lib/noble-event-emitter.js new file mode 100644 index 00000000..d3f8c7cd --- /dev/null +++ b/lib/noble-event-emitter.js @@ -0,0 +1,27 @@ +const { EventEmitter } = require('events'); + +class NobleEventEmitter extends EventEmitter { + /** + * Like once(), but ensures at most one listener exists for the given event. + * If a previous exclusive listener was registered for the same event, it is + * removed before the new one is added. This prevents listener accumulation + * when a method is called repeatedly before the event fires. + */ + onceExclusive (event, callback) { + if (!this._exclusiveCallbacks) { + this._exclusiveCallbacks = new Map(); + } + const prev = this._exclusiveCallbacks.get(event); + if (prev) { + this.removeListener(event, prev); + } + const wrappedCallback = (...args) => { + this._exclusiveCallbacks.delete(event); + callback(...args); + }; + this._exclusiveCallbacks.set(event, wrappedCallback); + this.once(event, wrappedCallback); + } +} + +module.exports = NobleEventEmitter; diff --git a/lib/noble.js b/lib/noble.js index d2e55ca7..94b4dc7b 100644 --- a/lib/noble.js +++ b/lib/noble.js @@ -1,13 +1,13 @@ const debug = require('debug')('noble'); -const { EventEmitter } = require('events'); +const NobleEventEmitter = require('./noble-event-emitter'); const Peripheral = require('./peripheral'); const Service = require('./service'); const Characteristic = require('./characteristic'); const Descriptor = require('./descriptor'); -class Noble extends EventEmitter { +class Noble extends NobleEventEmitter { constructor (bindings) { super(); @@ -142,7 +142,7 @@ class Noble extends EventEmitter { setScanParameters (interval, window, callback) { if (callback) { - this.once('scanParametersSet', callback); + this.onceExclusive('scanParametersSet', callback); } this._bindings.setScanParameters(interval, window); } @@ -200,7 +200,7 @@ class Noble extends EventEmitter { } } else { if (callback) { - this.once('scanStart', filterDuplicates => callback(null, filterDuplicates)); + this.onceExclusive('scanStart', filterDuplicates => callback(null, filterDuplicates)); } this._discoveredPeripherals.clear(); @@ -235,7 +235,7 @@ class Noble extends EventEmitter { return; } if (callback) { - this.once('scanStop', callback); + this.onceExclusive('scanStop', callback); } this._bindings.stopScanning(); } @@ -272,38 +272,33 @@ class Noble extends EventEmitter { } else if (scanning) { // Wait for either a new device or scan stop await new Promise(resolve => { - const tempDiscoverListener = () => resolve(); - - // Set up a temporary discover listener - this.once('discover', tempDiscoverListener); - - // Set up a cleanup for when scanning stops - const tempScanStopListener = () => { + let resolved = false; + let timeoutId = null; + + const cleanup = () => { + if (resolved) return; + resolved = true; this.removeListener('discover', tempDiscoverListener); + this.removeListener('scanStop', tempScanStopListener); + if (timeoutId) clearTimeout(timeoutId); resolve(); }; + + const tempDiscoverListener = () => cleanup(); + const tempScanStopListener = () => cleanup(); + + this.once('discover', tempDiscoverListener); this.once('scanStop', tempScanStopListener); - + // Handle race condition where a device might arrive during promise setup if (deviceQueue.length > 0) { - this.removeListener('discover', tempDiscoverListener); - this.removeListener('scanStop', tempScanStopListener); - resolve(); + cleanup(); + return; } - - // Optional: Add a maximum wait time, but with proper cleanup - // This can be removed to eliminate timer dependency + + // Add a maximum wait time with proper cleanup if (scanning) { - const timeoutId = setTimeout(() => { - this.removeListener('discover', tempDiscoverListener); - this.removeListener('scanStop', tempScanStopListener); - resolve(); - }, 1000); - - // Make sure we clear the timeout if we resolve before timeout - const clearTimeoutFn = () => clearTimeout(timeoutId); - this.once('discover', clearTimeoutFn); - this.once('scanStop', clearTimeoutFn); + timeoutId = setTimeout(() => cleanup(), 1000); } }); } @@ -383,7 +378,7 @@ class Noble extends EventEmitter { // Check if callback is a function if (typeof callback === 'function') { // Add a one-time listener for this specific event - this.once(`connect:${identifier}`, error => callback(error, this._peripherals.get(identifier))); + this.onceExclusive(`connect:${identifier}`, error => callback(error, this._peripherals.get(identifier))); } // Proceed to initiate the connection diff --git a/lib/peripheral.js b/lib/peripheral.js index 5bde27ef..8ace6f50 100644 --- a/lib/peripheral.js +++ b/lib/peripheral.js @@ -1,6 +1,6 @@ -const { EventEmitter } = require('events'); +const NobleEventEmitter = require('./noble-event-emitter'); -class Peripheral extends EventEmitter { +class Peripheral extends NobleEventEmitter { constructor (noble, id, address, addressType, connectable, advertisement, rssi, scannable) { super(); this._noble = noble; @@ -40,7 +40,7 @@ class Peripheral extends EventEmitter { } if (callback) { - this.once('connect', error => callback(error)); + this.onceExclusive('connect', error => callback(error)); } if (this.state === 'connected') { @@ -66,7 +66,7 @@ class Peripheral extends EventEmitter { disconnect (callback) { if (callback) { - this.once('disconnect', () => callback(null)); + this.onceExclusive('disconnect', () => callback(null)); } this.state = 'disconnecting'; this._noble.disconnect(this.id); @@ -80,7 +80,7 @@ class Peripheral extends EventEmitter { updateRssi (callback) { if (callback) { - this.once('rssiUpdate', (rssi, error) => callback(error, rssi)); + this.onceExclusive('rssiUpdate', (rssi, error) => callback(error, rssi)); } this._noble.updateRssi(this.id); } @@ -95,7 +95,7 @@ class Peripheral extends EventEmitter { discoverServices (uuids, callback) { if (callback) { - this.once('servicesDiscover', (services, error) => callback(error, services)); + this.onceExclusive('servicesDiscover', (services, error) => callback(error, services)); } this._noble.discoverServices(this.id, uuids); } @@ -172,7 +172,7 @@ class Peripheral extends EventEmitter { readHandle (handle, callback) { if (callback) { - this.once(`handleRead${handle}`, (data, error) => callback(error, data)); + this.onceExclusive(`handleRead${handle}`, (data, error) => callback(error, data)); } this._noble.readHandle(this.id, handle); } @@ -191,7 +191,7 @@ class Peripheral extends EventEmitter { } if (callback) { - this.once(`handleWrite${handle}`, (error) => callback(error)); + this.onceExclusive(`handleWrite${handle}`, (error) => callback(error)); } this._noble.writeHandle(this.id, handle, data, withoutResponse); diff --git a/lib/service.js b/lib/service.js index fd34c7c1..c04212d1 100644 --- a/lib/service.js +++ b/lib/service.js @@ -1,7 +1,7 @@ -const { EventEmitter } = require('events'); +const NobleEventEmitter = require('./noble-event-emitter'); const services = require('./services.json'); -class Service extends EventEmitter { +class Service extends NobleEventEmitter { constructor (noble, peripheralId, uuid) { super(); @@ -33,7 +33,7 @@ class Service extends EventEmitter { discoverIncludedServices (serviceUuids, callback) { if (callback) { - this.once('includedServicesDiscover', (includedServiceUuids, error) => callback(error, includedServiceUuids)); + this.onceExclusive('includedServicesDiscover', (includedServiceUuids, error) => callback(error, includedServiceUuids)); } this._noble.discoverIncludedServices( @@ -53,7 +53,7 @@ class Service extends EventEmitter { discoverCharacteristics (characteristicUuids, callback) { if (callback) { - this.once('characteristicsDiscover', (characteristics, error) => callback(error, characteristics)); + this.onceExclusive('characteristicsDiscover', (characteristics, error) => callback(error, characteristics)); } this._noble.discoverCharacteristics(