fix(socket): Prevent crashes from timeouts and fix memory leak (#1627)

This commit is contained in:
João Lucas de Oliveira Lopes
2025-09-07 08:11:32 -03:00
committed by GitHub
parent d878f01efb
commit 0fbce52fd3
2 changed files with 51 additions and 15 deletions
+5
View File
@@ -5,6 +5,11 @@ interface QueueJob<T> {
} }
const _queueAsyncBuckets = new Map<string | number, Array<QueueJob<any>>>() const _queueAsyncBuckets = new Map<string | number, Array<QueueJob<any>>>()
export function cleanupQueues() {
_queueAsyncBuckets.clear()
}
const _gcLimit = 10000 const _gcLimit = 10000
async function _asyncQueueExecutor(queue: Array<QueueJob<any>>, cleanup: () => void): Promise<void> { async function _asyncQueueExecutor(queue: Array<QueueJob<any>>, cleanup: () => void): Promise<void> {
+46 -15
View File
@@ -10,6 +10,7 @@ import {
MIN_PREKEY_COUNT, MIN_PREKEY_COUNT,
NOISE_WA_HEADER NOISE_WA_HEADER
} from '../Defaults' } from '../Defaults'
import { cleanupQueues } from '../Signal/Group/queue-job'
import type { SocketConfig } from '../Types' import type { SocketConfig } from '../Types'
import { DisconnectReason } from '../Types' import { DisconnectReason } from '../Types'
import { import {
@@ -179,25 +180,44 @@ export const makeSocket = (config: SocketConfig) => {
* @param timeoutMs timeout after which the promise will reject * @param timeoutMs timeout after which the promise will reject
*/ */
const waitForMessage = async <T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => { const waitForMessage = async <T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
let onRecv: (json: any) => void let onRecv: ((data: T) => void) | undefined
let onErr: (err: Boom | Error) => void let onErr: ((err: Error) => void) | undefined
try { try {
const result = await promiseTimeout<T>(timeoutMs, (resolve, reject) => { const result = await promiseTimeout<T>(timeoutMs, (resolve, reject) => {
onRecv = resolve onRecv = data => {
resolve(data)
}
onErr = err => { onErr = err => {
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) reject(
err ||
new Boom('Connection Closed', {
statusCode: DisconnectReason.connectionClosed
})
)
} }
ws.on(`TAG:${msgId}`, onRecv) ws.on(`TAG:${msgId}`, onRecv)
ws.on('close', onErr) // if the socket closes, you'll never receive the message ws.on('close', onErr)
ws.off('error', onErr) ws.on('error', onErr)
})
return result as any return () => reject(new Boom('Query Cancelled'))
})
return result
} catch (error) {
// Catch timeout and return undefined instead of throwing
if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
logger?.warn?.({ msgId }, 'timed out waiting for message')
return undefined
}
throw error
} finally { } finally {
ws.off(`TAG:${msgId}`, onRecv!) if (onRecv) ws.off(`TAG:${msgId}`, onRecv)
ws.off('close', onErr!) // if the socket closes, you'll never receive the message if (onErr) {
ws.off('error', onErr!) ws.off('close', onErr)
ws.off('error', onErr)
}
} }
} }
@@ -209,9 +229,14 @@ export const makeSocket = (config: SocketConfig) => {
const msgId = node.attrs.id const msgId = node.attrs.id
const [result] = await Promise.all([waitForMessage(msgId, timeoutMs), sendNode(node)]) const result = await promiseTimeout<any>(timeoutMs, async (resolve, reject) => {
const result = await waitForMessage(msgId, timeoutMs).catch(reject)
sendNode(node)
.then(() => resolve(result))
.catch(reject)
})
if ('tag' in result) { if (result && 'tag' in result) {
assertNodeErrorFree(result) assertNodeErrorFree(result)
} }
@@ -339,6 +364,8 @@ export const makeSocket = (config: SocketConfig) => {
return return
} }
cleanupQueues()
closed = true closed = true
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed') logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
@@ -629,8 +656,12 @@ export const makeSocket = (config: SocketConfig) => {
}) })
// login complete // login complete
ws.on('CB:success', async (node: BinaryNode) => { ws.on('CB:success', async (node: BinaryNode) => {
await uploadPreKeysToServerIfRequired() try {
await sendPassiveIq('active') await uploadPreKeysToServerIfRequired()
await sendPassiveIq('active')
} catch (err) {
logger.warn({ err }, 'failed to send initial passive iq')
}
logger.info('opened connection to WA') logger.info('opened connection to WA')
clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try