diff --git a/codebase/@features/broadcast/controller/broadcast-controller.ts b/codebase/@features/broadcast/controller/broadcast-controller.ts index b91dbdac..c989b995 100644 --- a/codebase/@features/broadcast/controller/broadcast-controller.ts +++ b/codebase/@features/broadcast/controller/broadcast-controller.ts @@ -13,15 +13,15 @@ * consistent state after every mutation. */ -import type { Destination, RelayStatus, ObsScene } from "../shared/types"; +import type { Destination, RelayStatus, ObsScene, IDestinationStore } from "../shared/types"; import { ObsClient, ObsError } from "./obs-client"; import { FanoutManager } from "./fanout-manager"; -import { DestinationStore } from "./destination-store"; export interface BroadcastControllerOptions { obs: ObsClient; fanout: FanoutManager; - store: DestinationStore; + /** Any IDestinationStore impl (file-backed in controller, Postgres in backend-api). */ + store: IDestinationStore; } export class BroadcastController { @@ -35,7 +35,7 @@ export class BroadcastController { return this.opts.fanout; } - get store(): DestinationStore { + get store(): IDestinationStore { return this.opts.store; } diff --git a/codebase/@features/broadcast/controller/destination-store.ts b/codebase/@features/broadcast/controller/destination-store.ts index 8ca04e43..a2a710ec 100644 --- a/codebase/@features/broadcast/controller/destination-store.ts +++ b/codebase/@features/broadcast/controller/destination-store.ts @@ -10,11 +10,11 @@ * - All mutations await save. */ -import type { Destination } from "../shared/types"; +import type { Destination, IDestinationStore } from "../shared/types"; const DEFAULT_FILE = "/data/destinations.json"; -export class DestinationStore { +export class DestinationStore implements IDestinationStore { private destinations: Destination[] = []; private filePath: string; private readonly intendedFilePath: string; diff --git a/codebase/@features/broadcast/shared/types.ts b/codebase/@features/broadcast/shared/types.ts index ebcef73e..677321a8 100644 --- a/codebase/@features/broadcast/shared/types.ts +++ b/codebase/@features/broadcast/shared/types.ts @@ -15,6 +15,43 @@ export interface Destination { url: string; } +/** + * Default tenant id for the Quinn-first phase. Every persisted row carries a + * tenant_id so flipping to multi-performer later is a data activation, not a + * schema rewrite. Until SSO/`my` performer identity is wired as the tenant key, + * all broadcast rows belong to this seeded tenant. + */ +export const DEFAULT_TENANT_ID = 1; + +/** + * Persistence contract for RTMP destinations. + * + * Two implementations satisfy this (DIP — BroadcastController depends only on + * the interface, never the concrete class): + * - file-backed `DestinationStore` (controller/, zero-dep, bare-droplet path) + * - `PgDestinationStore` (backend-api/, Postgres, always-on control plane) + * + * `list()`/`find()` are synchronous: both impls keep an authoritative in-memory + * snapshot, hydrated by `load()` and kept in sync on every mutation. + */ +export interface IDestinationStore { + /** Hydrate in-memory state. `initialFromEnv` seeds when the backing store is empty. */ + load(initialFromEnv?: Destination[]): Promise; + /** Snapshot copy (safe to mutate by caller). */ + list(): Destination[]; + find(name: string): Destination | undefined; + /** Idempotent upsert by name. Returns the new full list. */ + add(dest: Destination): Promise; + /** Remove by name (no-op if absent). Returns the new full list. */ + remove(name: string): Promise; + /** Replace the whole set. Returns the new full list. */ + replaceAll(newList: Destination[]): Promise; + /** Diagnostic: where state persists (file path, "postgres", or "" for memory-only). */ + readonly path: string; + /** True when persistence degraded to a fallback/memory-only location. */ + readonly isUsingFallback: boolean; +} + export interface FanoutStatus { name: string; url: string; // may be redacted in some contexts