Files
iistwin/tests/sse-connection-index.test.ts
Ильяс Султанов 667a321341 perf(sse+tasks): индекс SSE-соединений, getTaskTree на recursive CTE, батчи N+1
Шаги 1.4 и 1.5 плана production-готовности:
- SseConnectionIndex (byOrg/byUser), один heartbeat-таймер, протокол SSE не тронут
- getTaskTree/getTaskParentChain: WITH RECURSIVE CTE (было 1+2N запросов)
- sync initial/delta: Promise.all по формам
- embedding queue/reindex: батч-предзагрузки вместо поштучных запросов
- 8 новых тестов (96/96)
2026-09-07 23:29:21 +03:00

91 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from 'vitest';
import { SseConnectionIndex, type IndexedSSEConnection } from '../server/utils/sse-connection-index';
function conn(id: string, userId: number, organizationId: number): IndexedSSEConnection {
return { id, userId, organizationId };
}
describe('SseConnectionIndex', () => {
it('add/remove ведут учёт размера и очищают оба индекса', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
index.add(conn('b', 1, 10));
index.add(conn('c', 2, 20));
expect(index.size).toBe(3);
index.remove('a');
expect(index.size).toBe(2);
// Соединение удалено из всех индексов — не придёт ни по org, ни по user
expect(Array.from(index.getForEventTarget(10)).map(c => c.id)).toEqual(['b']);
expect(Array.from(index.getForEventTarget(10, 1)).map(c => c.id)).toEqual(['b']);
index.remove('b');
index.remove('c');
expect(index.size).toBe(0);
// Пустые Set'ы убраны из карт
expect(Array.from(index.getForEventTarget(10))).toEqual([]);
expect(Array.from(index.getForEventTarget(20, 2))).toEqual([]);
});
it('remove несуществующего id — no-op', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
expect(index.remove('missing')).toBeUndefined();
expect(index.size).toBe(1);
});
it('событие организации уходит только соединениям этой организации', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
index.add(conn('b', 2, 10));
index.add(conn('c', 3, 20));
const targets = Array.from(index.getForEventTarget(10)).map(c => c.id);
expect(targets).toEqual(['a', 'b']);
});
it('событие пользователя без organizationId уходит всем его соединениям (любая org)', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
index.add(conn('b', 1, 20));
index.add(conn('c', 2, 10));
const targets = Array.from(index.getForEventTarget(undefined, 1)).map(c => c.id);
expect(targets).toEqual(['a', 'b']);
});
it('событие пользователя + organizationId уходит только его соединениям в этой org', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
index.add(conn('b', 1, 20));
index.add(conn('c', 2, 10));
const targets = Array.from(index.getForEventTarget(10, 1)).map(c => c.id);
expect(targets).toEqual(['a']);
});
it('событие без userId и organizationId — broadcast на все соединения', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
index.add(conn('b', 2, 20));
const targets = Array.from(index.getForEventTarget(undefined, undefined)).map(c => c.id);
expect(targets).toEqual(['a', 'b']);
});
it('isUserConnected учитывает и пользователя, и организацию', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
expect(index.isUserConnected(1, 10)).toBe(true);
expect(index.isUserConnected(1, 20)).toBe(false);
expect(index.isUserConnected(2, 10)).toBe(false);
index.remove('a');
expect(index.isUserConnected(1, 10)).toBe(false);
});
it('forEach обходит все соединения (глобальный heartbeat)', () => {
const index = new SseConnectionIndex<IndexedSSEConnection>();
index.add(conn('a', 1, 10));
index.add(conn('b', 2, 20));
const seen: string[] = [];
index.forEach(c => seen.push(c.id));
expect(seen).toEqual(['a', 'b']);
});
});