/** * 메일 엔진의 변화 소식 스트림(JMAP EventSource) 대역 — 단위 테스트 공용. * fetch 본문처럼 바이트를 흘려보내고, 읽는 쪽이 닫으면(abort) 읽던 자리에 오류를 낸다. * 테스트가 `send` · `end` 로 엔진 쪽을 흉내 낸다. 타이머를 쓰지 않는다 (promise 로만 돈다 — 가짜 타이머와 함께 써도 된다). */ const encoder = new TextEncoder(); export class FakeEventStream { /** 읽는 쪽이 닫았다 */ aborted = false; private readonly queue: (Uint8Array | Error | null)[] = []; private wake: (() => void) | null = null; constructor(signal: AbortSignal) { const abort = () => { this.aborted = true; this.push(new Error('aborted')); }; if (signal.aborted) abort(); else signal.addEventListener('abort', abort, { once: true }); } /** 엔진이 글자를 보낸다 */ send(text: string): void { this.push(encoder.encode(text)); } /** 엔진이 스트림을 닫는다 */ end(): void { this.push(null); } async *body(): AsyncGenerator { for (;;) { while (this.queue.length === 0) { await new Promise((resolve) => { this.wake = resolve; }); } const item = this.queue.shift(); if (item === undefined || item === null) return; if (item instanceof Error) throw item; yield item; } } private push(item: Uint8Array | Error | null): void { this.queue.push(item); this.wake?.(); this.wake = null; } } /** 이 계정의 메일 · 메일함 변화 신호 — Stalwart 0.16.27 의 StateChange 모양 (2026-10-28 확인) */ export function stateChangeEvent(accountId: string): string { return `event: state\ndata: ${JSON.stringify({ '@type': 'StateChange', changed: { [accountId]: { Email: 'x', Mailbox: 'y' } }, })}\n\n`; } /** 엔진의 연결 유지 신호 */ export const PING_EVENT = 'event: ping\ndata: {"interval": 30}\n\n'; /** 미뤄 둔 비동기 처리(스트림 읽기 · 엔진 묻기의 결과)를 마저 돌린다 — 타이머는 움직이지 않는다 (setImmediate 는 가짜로 두지 않는다) */ export async function settleAsync(): Promise { for (let round = 0; round < 10; round += 1) { await new Promise((resolve) => setImmediate(resolve)); } }