Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions lib/characteristic.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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();
}
});
}
Expand All @@ -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(
Expand All @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions lib/descriptor.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions lib/noble-event-emitter.js
Original file line number Diff line number Diff line change
@@ -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;
55 changes: 25 additions & 30 deletions lib/noble.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -235,7 +235,7 @@ class Noble extends EventEmitter {
return;
}
if (callback) {
this.once('scanStop', callback);
this.onceExclusive('scanStop', callback);
}
this._bindings.stopScanning();
}
Expand Down Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions lib/peripheral.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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') {
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions lib/service.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Loading