fix: serialize session lifecycle transitions
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
type Send = (event: string, data: object, id: number) => void;
|
||||
|
||||
interface Subscriber {
|
||||
send: Send;
|
||||
close: () => void;
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-session ring buffer of recent events.
|
||||
*
|
||||
@@ -17,6 +23,7 @@ interface BufferedEvent {
|
||||
interface SubscribeOptions {
|
||||
afterId?: number;
|
||||
pending?: object | null;
|
||||
close?: () => void;
|
||||
}
|
||||
|
||||
function descriptorId(value: unknown): string | null {
|
||||
@@ -32,7 +39,7 @@ function bufferedGateId(item: BufferedEvent): string | null {
|
||||
}
|
||||
|
||||
export class SseHub {
|
||||
private subs = new Map<string, Set<Send>>();
|
||||
private subs = new Map<string, Set<Subscriber>>();
|
||||
private buffers = new Map<string, BufferedEvent[]>();
|
||||
private lastIds = new Map<string, number>();
|
||||
|
||||
@@ -42,7 +49,8 @@ export class SseHub {
|
||||
options: SubscribeOptions = {},
|
||||
): () => void {
|
||||
if (!this.subs.has(sessionId)) this.subs.set(sessionId, new Set());
|
||||
this.subs.get(sessionId)!.add(send);
|
||||
const subscriber = { send, close: options.close ?? (() => undefined), closed: false };
|
||||
this.subs.get(sessionId)!.add(subscriber);
|
||||
|
||||
const afterId = Number.isSafeInteger(options.afterId) && (options.afterId ?? 0) >= 0
|
||||
? options.afterId ?? 0
|
||||
@@ -63,12 +71,14 @@ export class SseHub {
|
||||
send(item.event, item.data, item.id);
|
||||
}
|
||||
|
||||
return () => this.subs.get(sessionId)?.delete(send);
|
||||
return () => this.subs.get(sessionId)?.delete(subscriber);
|
||||
}
|
||||
|
||||
publish(sessionId: string, event: string, data: object): number {
|
||||
const item = this.buffer(sessionId, event, data);
|
||||
for (const send of this.subs.get(sessionId) ?? []) send(event, data, item.id);
|
||||
for (const subscriber of this.subs.get(sessionId) ?? []) {
|
||||
subscriber.send(event, data, item.id);
|
||||
}
|
||||
return item.id;
|
||||
}
|
||||
|
||||
@@ -88,6 +98,13 @@ export class SseHub {
|
||||
|
||||
/** Clear buffered/runtime bindings while retaining session event-id monotonicity. */
|
||||
clear(sessionId: string): void {
|
||||
// Snapshot because ending an HTTP response can synchronously/asynchronously unsubscribe it.
|
||||
// Mark before invoking callbacks so even a re-entrant clear cannot close a response twice.
|
||||
for (const subscriber of [...(this.subs.get(sessionId) ?? [])]) {
|
||||
if (subscriber.closed) continue;
|
||||
subscriber.closed = true;
|
||||
try { subscriber.close(); } catch { /* disconnect every remaining subscriber */ }
|
||||
}
|
||||
this.buffers.delete(sessionId);
|
||||
this.subs.delete(sessionId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user