← Files BranchaARCHIVED FILE

skills/brancha/app/src/server/realtime/events.ts

1.77 KB · Oct 5, 2026 · 18:29 UTC

↓ Download file

import type { IncomingMessage, ServerResponse } from 'node:http';
import type { AppSettings } from '../../shared/types.js';
import { BASE_SECURITY_HEADERS } from '../http/security.js';

type Client = { response: ServerResponse; projectId: string | null };

class ProjectEvents {
  private readonly clients = new Set<Client>();

  constructor() {
    setInterval(() => this.ping(), 25_000).unref();
  }

  connect(req: IncomingMessage, res: ServerResponse) {
    const projectId = new URL(req.url || '/', 'http://localhost').searchParams.get('project');
    res.writeHead(200, {
      ...BASE_SECURITY_HEADERS,
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
      'Content-Type': 'text/event-stream',
    });
    res.write('retry: 2000\n\n');
    const client = { response: res, projectId };
    this.clients.add(client);
    req.on('close', () => this.clients.delete(client));
  }

  publish(projectId: string) {
    const payload = `data: ${JSON.stringify({ project: projectId })}\n\n`;
    for (const client of this.clients) {
      if (!client.projectId || client.projectId === projectId) this.write(client, payload);
    }
  }

  publishAll() {
    const payload = `data: ${JSON.stringify({ project: null })}\n\n`;
    for (const client of this.clients) this.write(client, payload);
  }

  publishSettings(settings: AppSettings) {
    const payload = `event: settings\ndata: ${JSON.stringify(settings)}\n\n`;
    for (const client of this.clients) this.write(client, payload);
  }

  private ping() {
    for (const client of this.clients) this.write(client, ': ping\n\n');
  }

  private write(client: Client, payload: string) {
    try {
      client.response.write(payload);
    } catch {
      this.clients.delete(client);
    }
  }
}

export const projectEvents = new ProjectEvents();

SHA-256: cf35db0ddb71bcb5237f4bec8958693903547ee810837b9919d0afb7856a58bf