Dev environment: NodeJS version 18.9.0 (Windows)
Production environment: RaspberryPi (Latest Raspbian, NodeJS)
I am trying to continuously poll a remote device over a TCP port using the built-in Sockets library (node:net). This sockets library is event-based, and I have written a wrapper that converts it to Promises.
When I monitor memoryUsage() with code that directly uses event listeners to continuously poll the remote device, heapTotal stays constant and heapUsed fluctuates up and down.
When I monitor memoryUsage() with my Promises wrapper which also continuously polls the remote device, heapTotal and heapUsed slowly increase the longer the program runs, indicating a memory leak.
Here's a generalized version of the way I wrapped the events in Promises. If this is wrong, please show me how to properly wrap an Event-based class for Promises.
Please note that this code polls as fast as possible. This may or may not be the desired effect in production, so delays could be added for slower polling. For this example, faster = better to illustrate the memory leak.
Please kindly overlook syntax errors because I typed this code in the question editor for brevity. Instead please try to see and comment on the functionality as a whole.
const net = require("node:net");
class Client
{
constructor()
{
this.socket = null; // A socket object for remote TCP connection
this.connected = false; // Is the socket connected?
this.timeout = 0; // Read timeout (ms)
this.pending = false; // Is a read pending?
this.timer = false; // Read Timeout timer
this.resolve = false; // A promise resolver for successful reads
this.reject = false; // A promise reject function for failed reads
}
connect(uri, port, timeout)
{
// Create a new socket:
let s = this.socket = new net.Socket();
// Save timeout
this.timeout = timeout;
// Return a promise that resolves on successful connection:
return new Promise((resolve, reject) => {
// Set up event listeners:
s.on('connect', () => { this.connected = true; resolve(); });
s.on('error', (err) => { reject(err); });
s.on('data', (data) => { this.dataHandler(data); });
// Attempt to connect:
this.socket.connect(port, uri);
});
}
// Request data from remote device:
read(props)
{
// Return a promise that resolves on successful data read from device
return new Promise((resolve, reject) =>
{
// If not connected, or another request is already pending, fail.
if (!this.connected) { reject("disconnected"); }
if (this.pending) { reject("pending"); }
// Save properties for later:
this.pending = true;
this.resolve = resolve;
this.reject = reject;
// Create a timer that automatically rejects if the response takes too long:
this.timer = setTimeout(() => { timeoutHandler(); }, this.timeout);
// Send data to the socket which initiates a response from the remote device
this.socket.write(props.requestMessage);
});
}
// A handler for received data on the socket:
dataHandler(data)
{
// Do some checks (omitted in this example) to validate incoming data
if (!dataIsValid()) { return; } // Ignore invalid data
// Clear the pending status and cancel the timeout:
this.pending = false;
clearTimeout(this.timer);
// Resolve the promise and send the data back to the waiting function.
this.resolve(data);
}
// A handler for timed out requests:
timeoutHandler()
{
// Indicate no request is pending
this.pending = false;
// Reject the promise and indicate reason for failure:
this.reject("Timeout");
}
}
This wrapper allows me to do the following in my main function:
// Create a new client and connect to 'my_url' device at port #1234, using a 1000ms timeout:
const c = new Client();
c.connect("my_url", 1234, 1000)
.then(() => {
// On successful connection, begin polling the device:
console.log("Successful connection!");
readFromDevice();
})
.catch((err) => {
console.error("Unable to connect: ", err);
});
// Continuous polling:
function readFromDevice()
{
// Initiate a read of the device by sending a read request:
c.read("someAddress")
.then((data) => {
// If a response was received, output the data and restart polling
console.log("Here's the data read from the device.", data);
setTimeout(() => { readFromDevice(); }, 0); // Immediately read again
})
.catch((err) => {
console.error("Unable to read from device:", err);
});
}