Продвинутые Асинхронные Механизмы в JavaScript и node.js
На примере WebSocket
Обо мне
- SDET с 2018
- Senior SDET @b2broker
- Certified node.js application developer (JSNAD 2023)
- автор TG канала @haradkou_sdet
- Консультирую кампании
Telegram: haradkou_sdet
Agenda
- Websocket ↔️
- Сообщения и логика 📩
- Закрытие сокета🚪
- Следим за памятью 👀
Контекст проекта
- Trading terminal
- Node.js as a client to .NET server
- .NET signalR in server
Трейдинг терминал

Фото из официального сайта b2broker.com/bbp
Проблемы
- нужно закрывать сокет самим
- тесты параллельные, что может приводить к утечкам памяти
- разнообразная логика на приходящие сообщения
WebSocket

WS API
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('open', () => {
console.log('socket open!');
})
ws.on('message', (msg) => {
console.log('socket message:', msg);
})
ws.on('error', (err) => {
console.log('socket error:', err);
})
ws.on('close', () => {
console.log('socket close!');
})Плюсы ws
- большое комьюнити
- понятная технология
- прост в создании POC
Минусы ws
- события добавляют когнитивную нагрузку - нужно помнить о том, что есть подписки
- нужно уметь управлять жизненным циклом
- событие на сообщение может иметь большую логику
- нет встренного итератора по сообщениям, например для фильтрации по какому-либо признаку
- логирование\трассировка
- как понять что освободилась память после закрытия сокета?
- как не потерять контекст в случае работы с внешними данными
Нужно тюнить!

Набросок
import ws from 'ws'
class MyWs {
_ws: ws
_messages: WsMessage[] = []
_events: Function[] = []
on(...args): void {}
emit(): void {}
}
Ч1. итератор по сообщениям
проблема: события могут иметь большую логику обработки
Ч1. Ждем события
export function waitForEvent<T>(
source: EventEmitter,
eventName: string
) {
return new Promise<T>((resolve, reject) => {
const eventHandler = (data: T) => {
source.off(eventName, eventHandler)
resolve(data)
}
source.once(eventName, eventHandler)
})
}Ч1. Итератор
import ws from 'ws'
import waitForEvent from './wait'
class MyWs {
_ws: ws
_events: Function[] = []
on(event: string, ...args): void {}
emit(event: string): void {}
async *[Symbol.asyncIterator](){
const message = await waitForEvent(this._ws, 'message')
yield message
}
async *[Symbol.iterator](){
const message = await waitForEvent(this._ws, 'message')
yield message
}
}
Ч1. Используем
import WebSocket from './my-ws'
async function main(){
const ws = new WebSocket(/** */)
for await (const message of ws) {
console.log('Got message!', message)
if(message === criteria) break
}
}
main()Ч1. Бонус
import ws from 'ws'
import waitForEvent from './wait'
class MyWs {
async *[Symbol.asyncIterator](){ /** реализация */ }
async *[Symbol.iterator](){ /** реализация */ }
*messageFilter(cb: (data: Data) => boolean) {
for await (const data of this) {
const isMatched = cb(data);
if(isMatched) yield data;
else continue;
}
}
}
Ч1. Бонус
import WebSocket from "./my-ws";
async function main() {
const ws = new WebSocket(/** */);
for await (const message of ws.messageFilter(
(data) => typeof data === "string",
)) {
console.log("Got string message!", message);
}
}
main();
Ч1. Бонус Iterator Helpers
import WebSocket from "./my-ws";
async function main() {
const ws = new WebSocket(/** */);
for await (
const message of ws
.filter((data) => typeof data === "string")
.take(10)
) {
console.log("Got only 10 string messages!", message);
}
}
main();
Ч2. Автоматическое закрытие сокета
-
Timeout
-
Promise.race + setTimeout
-
Abort signal
-
-
Dispose & AsyncDispose
Ч2. Timeout
import ws from 'ws'
class MyWs {
constructor(opts){
this.signal = opts.signal ?? null
if(this.signal) {
this.signal.onabort = async (e) => {
this.abort(e)
}
}
}
abort(reason) {
this.close(reason)
}
close(reason) {/* реализация */}
}
Ч2. используем Timeout
import WebSocket from './my-ws'
async function main(){
const signal = AbortSignal.timeout(3000)
const ws = new WebSocket({ signal })
// ws automatically closes after 3 sec
ws.on('message', (msg) => {
console.log(msg)
})
}
main()(Async) Dispose
-
Stage 2
-
Typescript 5.2+
-
Тоже что и defer в Go/Zig/Swift
-
По аналогии с iterator есть async версия
function loggy(id: string): AsyncDisposable {
console.log(`Constructing ${id}`);
return {
async [Symbol.asyncDispose]() {
console.log(`Disposing (async) ${id}`);
await doWork();
},
}
}
async function func() {
await using a = loggy("a");
{
await using b = loggy("b");
await using c = loggy("c");
}
}
func();
// Constructing a
// Constructing b
// Constructing c
// Disposing (async) c
// Disposing (async) b
// Disposing (async) aЧ2. Async Dispose
import ws from 'ws'
import waitForEvent from './wait'
class MyWs implements AsyncDispose {
_ws: ws
on(...args): void {}
emit(): void {}
async close() {}
async *[Symbol.asyncIterator](){ /** реализация */ }
async *[Symbol.iterator](){ /** реализация */ }
async [Symbol.asyncDispose](){
await this._ws.close()
}
}
Ч2. Используем
import WebSocket from './my-ws'
async function main(){
await using ws = new WebSocket(/** */)
for await (const message of ws) {
console.log('Got message!', message)
if(message === criteria) break
}
// ws automatically closes here
}
main()Подводные камни
!Нельзя передать как аргумент в другие функции!
Ч3. Следим за памятью 👀
Дисклеймер! Эта фича у нас только планируется

Ч3. Для чего?
- Для трассировки
- Очистка ресурсов
- Менеджмент кэша
Ч3. Минусы
- недетериминированное поведение GC
- не использовать для важных ресурсов!
Ч3. Can I use?

Ч3. Как это выглядит?
import WebSocket from 'ws';
// Создаем новый FinalizationRegistry и регистрируем callback функцию
const registry = new FinalizationRegistry((ws) => {
// send analytics
});
class MyWs {
constructor(url) {
// Регистрируем объект в FinalizationRegistry
registry.register(this, this.ws);
}
close() {
if (this.ws) {
this.ws.close();
registry.unregister(this);
}
this.ws = null
}
}
Ч4. Streams
(мем с погружением)
Ч4. Streams

Ч4. Streams

Ч4. Streams
- Readable
- Writable
- Duplex
- Transform
Ч4. Streams
import {
ReadableStream,
TransformStream,
} from 'node:stream/web';
const stream = new ReadableStream({
start(controller) {
controller.enqueue('a');
},
});
const transform = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
});
const transformedStream = stream.pipeThrough(transform);
for await (const chunk of transformedStream) {
console.log(chunk);
// Prints: A
}
Ч4. Streams

Не покрытые темы
- Web Streams
- Async Resource
- Async LocalStorage

Список литературы
Ч5. Async Resource
Text
PiterJS 2025. Async mechanics
By vitalic gorodkov
PiterJS 2025. Async mechanics
- 123