mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-21 06:51:24 +02:00
180 lines
6.0 KiB
TypeScript
180 lines
6.0 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
|
|
/**
|
|
* Remote sessions: a profile opened on a leased Windows, macOS or Linux host.
|
|
*
|
|
* The desktop holds no part of the session's life. It asks the backend to
|
|
* start or stop one and is told what happened; the hosts, the session cap,
|
|
* the profile lock and the billing all live server-side.
|
|
*/
|
|
|
|
/**
|
|
* `provisioning` -> `ready` -> `live` -> `closed`, plus `error` for a session
|
|
* that failed on the fleet.
|
|
*
|
|
* Kept open rather than a closed union: the state machine is the server's, and
|
|
* a state added there must render as itself instead of failing to decode.
|
|
*/
|
|
export type RemoteSessionPhase = string;
|
|
|
|
export interface RemoteSessionState {
|
|
session_id: string;
|
|
profile_id?: string | null;
|
|
platform?: string | null;
|
|
/** Named to match the field the API returns. */
|
|
state: RemoteSessionPhase;
|
|
/** The relay is up, so the session can actually be driven. */
|
|
cdp_ready: boolean;
|
|
/** `interactive` or `cookie_bot`. */
|
|
kind?: string | null;
|
|
/** Set when the session belongs to a cookie-bot run. */
|
|
run_id?: string | null;
|
|
/** The team the hours are attributed to. */
|
|
team_id?: string | null;
|
|
started_at?: string | null;
|
|
/** When it finished. One timestamp, not a ready/closed pair. */
|
|
ended_at?: string | null;
|
|
/** Why it ended, e.g. `stopped_by_user`, `max_duration`. */
|
|
close_reason?: string | null;
|
|
/** What it has cost so far — a running figure while it is live. */
|
|
billed_seconds?: number | null;
|
|
}
|
|
|
|
export interface RemoteSessionEnded {
|
|
session_id: string;
|
|
status: string;
|
|
billed_seconds: number;
|
|
}
|
|
|
|
/**
|
|
* Why a profile cannot be opened on this computer right now.
|
|
*
|
|
* - `running`: a browser is open on the fleet holding this profile.
|
|
* - `pending_sync`: a session has finished and what it wrote is still being
|
|
* pulled down. Opening the local copy now would make the local files look
|
|
* newer than the host's push, and the next sync would then upload the stale
|
|
* copy over the session's work and delete the rest of it.
|
|
*
|
|
* Both states are temporary and neither is an error.
|
|
*/
|
|
export type RemoteHandoffState = "running" | "pending_sync";
|
|
|
|
/**
|
|
* States a session cannot leave under its own steam.
|
|
*
|
|
* `error` is terminal just as `closed` is, and there is never a `closed` frame
|
|
* after one — the server keeps a fleet-reported failure visible rather than
|
|
* flattening it into "finished". A consumer that only watched for `closed`
|
|
* would leave a failed session pinned in its live list for ever, and the
|
|
* profile would look busy until the app restarted.
|
|
*/
|
|
export function isSessionOver(session: RemoteSessionState): boolean {
|
|
return session.state === "closed" || session.state === "error";
|
|
}
|
|
|
|
/** Everything the caller owns, pushed once when the stream connects. */
|
|
export interface RemoteSessionSnapshot {
|
|
sessions: RemoteSessionState[];
|
|
}
|
|
|
|
/** Whether the desktop is currently receiving transitions. */
|
|
export interface RemoteSessionStreamStatus {
|
|
connected: boolean;
|
|
reason?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Tauri event names. A transition arrives here rather than being polled for:
|
|
* `POST /api/remote-sessions` answers `provisioning` and nothing more, so
|
|
* without the stream the desktop is blind between launch and stop.
|
|
*/
|
|
export const REMOTE_SESSION_EVENTS = {
|
|
/** One session changed. Payload: `RemoteSessionState`. */
|
|
state: "remote-session-state",
|
|
/** Connect snapshot. Payload: `RemoteSessionSnapshot`. */
|
|
snapshot: "remote-session-snapshot",
|
|
/** Stream connectivity. Payload: `RemoteSessionStreamStatus`. */
|
|
stream: "remote-session-stream",
|
|
/**
|
|
* The set of profiles that cannot be launched locally changed.
|
|
* Payload: `Record<profileId, RemoteHandoffState>`.
|
|
*/
|
|
handoff: "remote-handoff-changed",
|
|
} as const;
|
|
|
|
/** Which profiles are blocked from launching locally, and why. */
|
|
export function getRemoteHandoffStates(): Promise<
|
|
Record<string, RemoteHandoffState>
|
|
> {
|
|
return invoke<Record<string, RemoteHandoffState>>(
|
|
"get_remote_handoff_states",
|
|
);
|
|
}
|
|
|
|
export function onRemoteHandoffChanged(
|
|
handler: (states: Record<string, RemoteHandoffState>) => void,
|
|
): Promise<UnlistenFn> {
|
|
return listen<Record<string, RemoteHandoffState>>(
|
|
REMOTE_SESSION_EVENTS.handoff,
|
|
(event) => handler(event.payload),
|
|
);
|
|
}
|
|
|
|
export function listRemoteSessions(): Promise<RemoteSessionState[]> {
|
|
return invoke<RemoteSessionState[]>("list_remote_sessions");
|
|
}
|
|
|
|
export function getRemoteSession(
|
|
sessionId: string,
|
|
): Promise<RemoteSessionState> {
|
|
return invoke<RemoteSessionState>("get_remote_session", { sessionId });
|
|
}
|
|
|
|
export function stopRemoteSession(
|
|
sessionId: string,
|
|
): Promise<RemoteSessionEnded> {
|
|
return invoke<RemoteSessionEnded>("stop_remote_session", { sessionId });
|
|
}
|
|
|
|
/** Subscribe to transitions. Idempotent; call once the user is signed in. */
|
|
export function startRemoteSessionEvents(): Promise<void> {
|
|
return invoke<void>("start_remote_session_events");
|
|
}
|
|
|
|
/** Unsubscribe. Call on sign-out. */
|
|
export function stopRemoteSessionEvents(): Promise<void> {
|
|
return invoke<void>("stop_remote_session_events");
|
|
}
|
|
|
|
/** Whether the subscriber is alive, for a UI that mounted after it started. */
|
|
export function getRemoteSessionEventsStatus(): Promise<boolean> {
|
|
return invoke<boolean>("get_remote_session_events_status");
|
|
}
|
|
|
|
export function onRemoteSessionState(
|
|
handler: (session: RemoteSessionState) => void,
|
|
): Promise<UnlistenFn> {
|
|
return listen<RemoteSessionState>(REMOTE_SESSION_EVENTS.state, (event) =>
|
|
handler(event.payload),
|
|
);
|
|
}
|
|
|
|
export function onRemoteSessionSnapshot(
|
|
handler: (snapshot: RemoteSessionSnapshot) => void,
|
|
): Promise<UnlistenFn> {
|
|
return listen<RemoteSessionSnapshot>(
|
|
REMOTE_SESSION_EVENTS.snapshot,
|
|
(event) => handler(event.payload),
|
|
);
|
|
}
|
|
|
|
export function onRemoteSessionStream(
|
|
handler: (status: RemoteSessionStreamStatus) => void,
|
|
): Promise<UnlistenFn> {
|
|
return listen<RemoteSessionStreamStatus>(
|
|
REMOTE_SESSION_EVENTS.stream,
|
|
(event) => handler(event.payload),
|
|
);
|
|
}
|