From 1189565662a3c32f8cca91cb3f0e7bfb48770f4c Mon Sep 17 00:00:00 2001 From: AloisKlingler <11095760+AloisKlingler@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:02:40 +0200 Subject: [PATCH] Fix memory leak: time out unanswered get()/refresh() requests _send() (used by get() and refresh()) stored a resolver in this._resolvers keyed by sequence number but, unlike set(), had no timeout. If a device never replies to a query (common when polling local devices, and with protocol 3.4/3.5 sequence handling), the inner Promise never settled: the resolver closure, encoded buffer and the awaiting async frame were retained forever, and the caller_s .catch() never fired. Frequent polling leaked a few KB per dropped request. Wrap the _send() Promise in pTimeout (mirroring set()), deleting the pending resolver and rejecting on timeout so pRetry can retry and the caller is notified. --- index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 5618d73..b327d44 100644 --- a/index.js +++ b/index.js @@ -474,7 +474,7 @@ class TuyaDevice extends EventEmitter { const sequenceNo = this._currentSequenceN; // Retry up to 5 times return pRetry(() => { - return new Promise((resolve, reject) => { + return pTimeout(new Promise((resolve, reject) => { // Send data this.connect().then(() => { try { @@ -487,6 +487,12 @@ class TuyaDevice extends EventEmitter { } }) .catch(error => reject(error)); + }), this._responseTimeout * 2500, () => { + // On timeout, drop the pending resolver so it can't leak, + // then reject so pRetry can retry (and, once retries are + // exhausted, the caller's .catch() finally fires). + delete this._resolvers[sequenceNo]; + throw new Error('Timeout waiting for response from device id: ' + this.device.id); }); }, { onFailedAttempt: error => {