Files
ds-galo/frontend/src/lib/sse.ts
T
iu d7a37328f0 fix(photos): faster large-album loading and album cover lifecycle
Large albums opened slowly and flickered; the default album cover was
also lost in the media-objects refactor.

Large-album loading:
- quantize signed object URLs within a window and send Cache-Control so
  previews are reused instead of re-fetched
- publish photo.updated (not photo.created) when variants are ready, so
  clients update in place without reordering the grid
- worker concurrency + newest-first variant priority; raise default batch

Album cover:
- first uploaded photo becomes the cover when none is set
- clear the cover when the cover photo is trashed or moved out
- "set as album cover" in the photo context menu and actions panel

OpenSpec changes: fix-large-album-load, fix-album-cover
2026-10-11 14:15:59 +03:00

79 lines
1.6 KiB
TypeScript

export interface SSEEvent {
type: string;
data: unknown;
}
export interface SSEClientOptions {
path: string;
onEvent: (event: SSEEvent) => void;
onReconnect?: () => void;
}
// Wrapper над EventSource с экспоненциальным backoff и догоном через REST.
export class SSEClient {
private es: EventSource | null = null;
private stopped = false;
private attempt = 0;
private readonly knownEvents = [
'photo.created',
'photo.updated',
'photo.deleted',
'comment.created',
'like.added',
'like.removed',
'member.joined',
'member.left',
'member.updated',
'album.updated',
'album.deleted',
'notification'
];
constructor(private opts: SSEClientOptions) {}
connect(token: string) {
this.stopped = false;
this.open(token);
}
private open(token: string) {
if (this.stopped) return;
const url = `${this.opts.path}?token=${encodeURIComponent(token)}`;
this.es = new EventSource(url);
this.es.onopen = () => {
if (this.attempt > 0) {
this.opts.onReconnect?.();
}
this.attempt = 0;
};
for (const name of this.knownEvents) {
this.es.addEventListener(name, (ev) => {
let data: unknown = null;
try {
data = JSON.parse((ev as MessageEvent).data);
} catch {
data = (ev as MessageEvent).data;
}
this.opts.onEvent({ type: name, data });
});
}
this.es.onerror = () => {
this.es?.close();
this.es = null;
if (this.stopped) return;
const delay = Math.min(1000 * 2 ** this.attempt, 30000);
this.attempt += 1;
setTimeout(() => this.open(token), delay);
};
}
close() {
this.stopped = true;
this.es?.close();
this.es = null;
}
}