/** * Tauri adapter for {@link DesktopServerGateway} (ticket #68). One of the only * places that calls `invoke()`; features reach it exclusively through the port. * * Commands and payload keys are camelCase, matching the backend DTO convention. * `save_server_exposure_settings` / `preview_server_exposure_settings` take the * settings directly under a `settings` key (no `request` wrapper). * * **Polling note.** The backend exposes no status *event* — `embedded_server.rs` * emits nothing — so `onStatusChanged` is implemented here by polling * `embedded_server_status`. That keeps the port shape push-like (and swappable * for a real event later) while the polling schedule stays an adapter detail. */ import { invoke } from "@tauri-apps/api/core"; import type { EmbeddedServerStatus, ServerExposurePreview, ServerExposureSettings, Unsubscribe, } from "@/domain"; import type { DesktopServerGateway } from "@/ports"; /** Status poll interval (ms). Cheap, in-process command; transitions are coarse. */ const STATUS_POLL_MS = 2000; export class TauriDesktopServerGateway implements DesktopServerGateway { constructor(private readonly pollMs: number = STATUS_POLL_MS) {} getExposureSettings(): Promise { return invoke("get_server_exposure_settings"); } async saveExposureSettings(settings: ServerExposureSettings): Promise { await invoke("save_server_exposure_settings", { settings }); } previewExposure( settings: ServerExposureSettings, ): Promise { return invoke("preview_server_exposure_settings", { settings, }); } status(): Promise { return invoke("embedded_server_status"); } start(): Promise { return invoke("embedded_server_start"); } stop(): Promise { return invoke("embedded_server_stop"); } async onStatusChanged( handler: (status: EmbeddedServerStatus) => void, ): Promise { let stopped = false; const timer = setInterval(() => { void this.status().then( (status) => { // The unsubscribe may land while a poll is in flight; drop late results // so a torn-down consumer never gets called. if (!stopped) handler(status); }, () => { // A failed poll is not a status: swallow it and let the next tick try // again, rather than tearing the subscription down. }, ); }, this.pollMs); return () => { stopped = true; clearInterval(timer); }; } }