Skip to content

Commit 41de09f

Browse files
author
hhaensel
committed
fix possible memory leaks in channels.js
1 parent d0a9ddd commit 41de09f

2 files changed

Lines changed: 58 additions & 28 deletions

File tree

assets/js/channels.js

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
** channels.js v1.3 // 7th July 2023
2+
** channels.js v1.4 // 16th March 2026
33
** Author: Adrian Salceanu and contributors // @essenciary
44
** GenieFramework.com // Genie.jl
55
*/
@@ -74,22 +74,30 @@ Genie.initWebChannel = function(channel = Genie.Settings.webchannels_default_rou
7474
WebChannel.processingHandlers = [];
7575

7676
const waitForOpenConnection = (WebChannel) => {
77-
return new Promise((resolve, reject) => {
77+
// Reuse a single shared promise while a reconnection is in progress to
78+
// avoid spawning one interval per queued message (memory leak).
79+
if (WebChannel._openConnectionPromise) {
80+
return WebChannel._openConnectionPromise;
81+
}
82+
WebChannel._openConnectionPromise = new Promise((resolve, reject) => {
7883
const maxNumberOfAttempts = Genie.Settings.webchannels_connection_attempts;
7984
const delay = Genie.Settings.webchannels_reconnect_delay;
8085

8186
let currentAttempt = 0;
8287
const interval = setInterval(() => {
8388
if (currentAttempt > maxNumberOfAttempts - 1) {
8489
clearInterval(interval);
90+
WebChannel._openConnectionPromise = null;
8591
reject(new Error('Maximum number of attempts exceeded: Message not sent.'));
8692
} else if (WebChannel.socket.readyState === 1) {
8793
clearInterval(interval);
94+
WebChannel._openConnectionPromise = null;
8895
resolve();
8996
};
9097
currentAttempt++;
9198
}, delay)
92-
})
99+
});
100+
return WebChannel._openConnectionPromise;
93101
}
94102

95103
WebChannel.socket = newSocketConnection(WebChannel);
@@ -141,7 +149,9 @@ Genie.initWebChannel = function(channel = Genie.Settings.webchannels_default_rou
141149
displayAlert(WebChannel);
142150
if (Genie.Settings.webchannels_autosubscribe) {
143151
if (isDev()) console.info('Attempting to reconnect! ');
144-
setTimeout(function() {
152+
clearTimeout(WebChannel._reconnectTimer);
153+
WebChannel._reconnectTimer = setTimeout(function() {
154+
WebChannel._openConnectionPromise = null;
145155
WebChannel.socket = newSocketConnection(WebChannel);
146156
}, Genie.Settings.webchannels_reconnect_delay);
147157
}
@@ -209,7 +219,7 @@ function displayAlert(WebChannel, content = 'Can not reach the server. Trying to
209219

210220
function deleteAlert(WebChannel) {
211221
WebChannel.ws_disconnected = false;
212-
clearInterval(WebChannel.alertTimeout);
222+
clearTimeout(WebChannel.alertTimeout);
213223
if (WebChannel.parent) WebChannel.parent.ws_disconnected = false;
214224

215225
if (Genie.allConnected()) {
@@ -219,22 +229,35 @@ function deleteAlert(WebChannel) {
219229
}
220230

221231
function newSocketConnection(WebChannel, host = Genie.Settings.websockets_exposed_host) {
232+
// Remove listeners from the previous socket before creating a new one so
233+
// the old WebSocket object can be garbage-collected.
234+
if (WebChannel._socketListeners) {
235+
const prev = WebChannel._socketListeners.socket;
236+
if (prev) {
237+
prev.removeEventListener('open', WebChannel._socketListeners.open);
238+
prev.removeEventListener('message', WebChannel._socketListeners.message);
239+
prev.removeEventListener('error', WebChannel._socketListeners.error);
240+
prev.removeEventListener('close', WebChannel._socketListeners.close);
241+
}
242+
WebChannel._socketListeners = null;
243+
}
244+
222245
let ws = new WebSocket(Genie.Settings.websockets_protocol + '//' + host
223246
+ (Genie.Settings.websockets_exposed_port > 0 ? (':' + Genie.Settings.websockets_exposed_port) : '')
224247
+ ( ((Genie.Settings.base_path.trim() === '' || Genie.Settings.base_path.startsWith('/')) ? '' : '/') + Genie.Settings.base_path)
225248
+ ( ((Genie.Settings.websockets_base_path.trim() === '' || Genie.Settings.websockets_base_path.startsWith('/')) ? '' : '/') + Genie.Settings.websockets_base_path));
226249

227-
ws.addEventListener('open', event => {
250+
const onOpen = event => {
228251
const handlers = WebChannel.openHandlers.concat(Genie.WebChannels.openHandlers)
229252
for (let i = 0; i < handlers.length; i++) {
230253
let f = handlers[i];
231254
if (typeof f === 'function') {
232255
f(event);
233256
}
234257
}
235-
});
258+
};
236259

237-
ws.addEventListener('message', event => {
260+
const onMessage = event => {
238261
const handlers = WebChannel.messageHandlers.concat(Genie.WebChannels.messageHandlers)
239262
for (let i = 0; i < handlers.length; i++) {
240263
let f = handlers[i];
@@ -243,36 +266,35 @@ function newSocketConnection(WebChannel, host = Genie.Settings.websockets_expose
243266
}
244267
}
245268
WebChannel.lastMessageAt = Date.now();
246-
});
269+
};
247270

248-
ws.addEventListener('error', event => {
271+
const onError = event => {
249272
const handlers = WebChannel.errorHandlers.concat(Genie.WebChannels.errorHandlers)
250273
for (let i = 0; i < handlers.length; i++) {
251274
let f = handlers[i];
252275
if (typeof f === 'function') {
253276
f(event);
254277
}
255278
}
256-
});
279+
};
257280

258-
ws.addEventListener('close', event => {
281+
const onClose = event => {
259282
const handlers = WebChannel.closeHandlers.concat(Genie.WebChannels.closeHandlers)
260283
for (let i = 0; i < handlers.length; i++) {
261284
let f = handlers[i];
262285
if (typeof f === 'function') {
263286
f(event);
264287
}
265288
}
266-
ws.onmessage = null;
267-
ws.onerror = null;
268-
ws.onclose = null;
269-
ws.onopen = null;
270-
ws = null;
271-
});
289+
};
272290

273-
ws.addEventListener('error', _ => {
274-
// WebChannel.socket = newSocketConnection();
275-
});
291+
ws.addEventListener('open', onOpen);
292+
ws.addEventListener('message', onMessage);
293+
ws.addEventListener('error', onError);
294+
ws.addEventListener('close', onClose);
295+
296+
// Store named listener refs so they can be removed on the next reconnect.
297+
WebChannel._socketListeners = { socket: ws, open: onOpen, message: onMessage, error: onError, close: onClose };
276298

277299
return ws
278300
}
@@ -377,8 +399,8 @@ function subscribe(WebChannel, trial = 1) {
377399
WebChannel.sendMessageTo(WebChannel.channel, window.Genie.Settings.webchannels_subscribe_channel);
378400
} else if (trial < Genie.Settings.webchannels_subscription_trials) {
379401
if (isDev()) console.warn('Queuing subscription');
380-
trial++;
381-
setTimeout(subscribe.bind(this, WebChannel, trial), Genie.Settings.webchannels_timeout);
402+
clearTimeout(WebChannel._subscribeTimer);
403+
WebChannel._subscribeTimer = setTimeout(subscribe.bind(this, WebChannel, trial + 1), Genie.Settings.webchannels_timeout);
382404
} else {
383405
displayAlert(WebChannel);
384406
}

src/WebChannels.jl

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ end
8383
Subscribes a web socket client `ws` to `channel`.
8484
"""
8585
function subscribe(ws::HTTP.WebSockets.WebSocket, channel::ChannelName) :: ChannelClientsCollection
86+
# Clean up stale entries from previous connections on this channel so that
87+
# disconnected clients (and their handler tasks) do not accumulate on reconnect.
88+
unsubscribe_disconnected_clients(channel)
89+
8690
if haskey(CLIENTS, id(ws))
8791
in(channel, CLIENTS[id(ws)].channels) || push!(CLIENTS[id(ws)].channels, channel)
8892
else
@@ -123,12 +127,14 @@ end
123127
Unsubscribes a web socket client `ws` from all the channels.
124128
"""
125129
function unsubscribe_client(ws::HTTP.WebSockets.WebSocket) :: ChannelClientsCollection
126-
if haskey(CLIENTS, id(ws))
127-
for channel_id in CLIENTS[id(ws)].channels
128-
pop_subscription(id(ws), channel_id)
130+
client_id = id(ws)
131+
if haskey(CLIENTS, client_id)
132+
for channel_id in CLIENTS[client_id].channels
133+
pop_subscription(client_id, channel_id)
129134
end
130135

131-
delete!(CLIENTS, id(ws))
136+
delete_queue!(MESSAGE_QUEUE, client_id)
137+
delete!(CLIENTS, client_id)
132138
end
133139

134140
CLIENTS
@@ -149,7 +155,7 @@ function purge_unnecessary_message_queue()
149155
active_clients = keys(CLIENTS) |> collect
150156
for id in keys(MESSAGE_QUEUE) |> collect
151157
if ! (id in active_clients)
152-
delete!(MESSAGE_QUEUE, id)
158+
delete_queue!(MESSAGE_QUEUE, id) # kills the handler task, not just the dict entry
153159
end
154160
end
155161
end
@@ -326,6 +332,8 @@ function message(client::ClientId, msg::String)
326332
finally
327333
put!(future, nbytes)
328334
end
335+
# Self-terminate when the socket is closed to avoid orphaned tasks.
336+
HTTP.WebSockets.isclosed(ws) && break
329337
end |> errormonitor
330338

331339
queue, handler

0 commit comments

Comments
 (0)