import { WebSocket } from "ws"; import type { AppFeedEntry } from "./client.js"; export interface OpenAppStreamOptions { /** * The app's OWN WebSocket origin + path, e.g. * `wss://grocery-x7k2m9.homespunapps.com/_hs/ws` — derived from the * `url` a deploy/show response returns (swap https->wss, append `_hs/ws`). */ wsUrl: string; /** Agent API key, carried via the `homespun.agentkey.` subprotocol token. */ apiKey: string; /** Resume cursor — replay only feed entries with seq > since. */ since?: number; } export interface AppStreamHandlers { /** Fired once on connect with the app's hello metadata. */ onHello?: (hello: { seq: number; app: { slug: string; name: string; description: string | null; icon: string | null; visibility: string; collections: Array<{ name: string; appendOnly: boolean; }>; }; session: { kind: "owner" | "member" | "anonymous"; humanId: string | null; }; }) => void; /** Fired for every feed entry — replayed (via batch) or live. */ onEntry?: (entry: AppFeedEntry) => void; /** Fired once the initial catch-up (from `opts.since`) is fully drained. */ onCaughtUp?: () => void; /** Fired on a `resync` frame — the caller should full-resync each collection. */ onResync?: () => void; /** * A task was queued for this app: claim now rather than waiting out the poll * interval. A HINT ONLY, carrying nothing, so a consumer that never receives one * (no socket, dropped frame, older relay) must still poll and lose nothing but * time. Only agent-key sockets receive it. */ onAgentTaskAvailable?: () => void; /** Fired on the terminal `_dormant` frame (the app went dormant). */ onDormant?: () => void; /** Fired on the terminal `_suspended` frame (an operator suspended the * app, issue #1041). Distinct from `onDormant`: there is no self-service * recovery here, only an operator can unsuspend. */ onSuspended?: () => void; /** Fired on a relay-side error frame. */ onRelayError?: (error: { code?: string; message?: string; details?: unknown; }) => void; /** Fired when the socket closes (cleanly or otherwise). */ onClose?: (info: { code: number; reason: string; }) => void; /** Fired on a transport-level error (incl. a rejected upgrade). */ onError?: (err: Error) => void; } export interface AppStreamHandle { close(): void; readonly socket: WebSocket; } /** * Open a WebSocket to an app's `/_hs/ws` endpoint as an agent. Drives the * `sub`/`batch` catch-up loop internally (re-subscribing while * `truncated:true`) so callers only handle individual entries. */ export declare function openAppStream(opts: OpenAppStreamOptions, handlers: AppStreamHandlers): AppStreamHandle; export interface OpenWorkerStreamOptions { /** * The relay's API base, e.g. `https://app.homespun.dev` — NOT an app origin, and * NOT a WebSocket URL. The path is appended here so a caller cannot get it wrong, * which the per-app version demonstrated was worth taking out of their hands. */ baseUrl: string; /** Agent API key, carried via the `homespun.agentkey.` subprotocol token. */ apiKey: string; } export interface WorkerStreamHandlers { /** * Fired once on connect. * * `push` is the relay telling the worker whether offering credit will achieve * anything. A worker that promised its capacity to a relay with push switched off * would hold that capacity back from its own polling and starve waiting for frames * that are never coming, so this is answered rather than inferred. An older relay * omits it, and `false` is the safe reading: poll. */ onHello?: (info: { push: boolean; }) => void; /** * A task was queued for one of this owner's apps. `appId` is present so a worker * can narrow its claim, and is the ONLY thing the frame carries: no task id, no * prompt, no row data. A consumer that never receives one (no socket, dropped * frame, older relay) must still poll and lose nothing but time. */ onAgentTaskAvailable?: (info: { appId: string; }) => void; /** * A task has been ASSIGNED to this worker: leased, with the whole envelope. * * Unlike the hint above, this is work in hand. The lease is already running, so a * consumer that ignores the frame holds a task nobody is doing until the lease lapses. * The envelope is the same shape the claim route returns, deliberately, so a consumer * can hand it to the same executor either way. */ onAssign?: (envelope: WorkerTaskEnvelope) => void; /** Fired when the socket closes (cleanly or otherwise). */ onClose?: (info: { code: number; reason: string; }) => void; /** Fired on a transport-level error, including a rejected upgrade. */ onError?: (err: Error) => void; } /** * One assigned task, as the relay sends it. * * Kept OPEN (`[k: string]: unknown`) on purpose. This client does not interpret the * envelope, it forwards it, and a closed type here would silently drop any field the * relay adds later: a consumer piping the whole object to a child process would start * handing over a truncated version of it after a relay upgrade. Only the fields this * layer actually reasons about are named. */ export interface WorkerTaskEnvelope { task_id: string; app_id: string; app_slug: string; task_type: string; [k: string]: unknown; } /** A live worker stream, with the client-to-server half of the protocol. */ export interface WorkerStreamHandle extends AppStreamHandle { /** * Declare how many tasks this worker will accept, right now. * * ABSOLUTE, not an increment: the relay replaces whatever it held. Send it again after * each task finishes to top back up. The worker has to drive this, because an ack goes * over HTTP to whichever replica answers and that is usually not the one holding this * socket, so the relay cannot see a task finish. * * Returns false if the socket was not open, so a caller can tell the difference * between "declared" and "shouted into a closed pipe". */ sendReady(credits: number): boolean; } /** Derive the worker stream's WebSocket URL from an API base (https -> wss). */ export declare function workerWsUrlFromBase(baseUrl: string): string; /** * Open the worker stream: one socket, wakes for every app this key's owner has. * * Does not reconnect. That is the caller's job, because only the caller knows whether * an outage is worth announcing and the CLI already has the backoff-with-one-warning * behaviour this would otherwise duplicate. */ export declare function openWorkerStream(opts: OpenWorkerStreamOptions, handlers: WorkerStreamHandlers): WorkerStreamHandle; /** Derive an app's `/_hs/ws` URL from its `url` field (https -> wss). */ export declare function appWsUrlFromAppUrl(appUrl: string): string;