| 1 | import { app } from "./bridge"; |
| 2 | import type { ManualSessionCreationRequest, ManualSessionCreationView } from "../generated/desktopContract.generated"; |
| 3 | |
| 4 | /** Ephemeral presentation only; the host owns durable recovery and drafts. */ |
| 5 | export type ManualCreationObservation = { |
| 6 | request: ManualSessionCreationRequest; |
| 7 | operation?: ManualSessionCreationView; |
| 8 | pending: boolean; |
| 9 | failed: boolean; |
| 10 | }; |
| 11 | |
| 12 | export async function createManualSession(request: ManualSessionCreationRequest, options?: { |
| 13 | onSurfaceReady: (operation: ManualSessionCreationView) => Promise<void>; |
| 14 | isObservationCurrent?: () => boolean; |
| 15 | onProgress?: (operation: ManualSessionCreationView) => void; |
| 16 | retry?: boolean; |
| 17 | }) { |
| 18 | let operation; |
| 19 | try { operation = await app.BeginManualSessionCreation(request); } |
| 20 | catch (error) { |
| 21 | // A lost response is resolved against the original persisted identity. |
| 22 | try { operation = await app.GetManualSessionCreation(request.operationId); } |
| 23 | catch { throw error; } |
| 24 | } |
| 25 | if (options?.retry && operation.phase !== "ready") operation = await app.RetryManualSessionCreation(request.operationId); |
| 26 | let surfaceOpened = false; |
| 27 | let lastProgress = ""; |
| 28 | while (true) { |
| 29 | // Acknowledgement transfers creation to the host. Leaving this surface |
| 30 | // stops only its observer, never the durable operation or its recovery. |
| 31 | if (options?.isObservationCurrent?.() === false) return operation; |
| 32 | // Elapsed milliseconds change on every read; repaint only meaningful state. |
| 33 | const progress = JSON.stringify([operation.phase, operation.surfaceReady, operation.ref, operation.error, |
| 34 | operation.progress?.status, operation.progress?.slow, operation.progress?.errorCode]); |
| 35 | if (progress !== lastProgress) { lastProgress = progress; options?.onProgress?.(operation); } |
| 36 | // The host publishes a formal session before constructing its runtime. Open |
| 37 | // that identity once so typing can start; completion must never reselect it. |
| 38 | // Older hosts only publish a usable surface at ready. |
| 39 | if (!surfaceOpened && (operation.surfaceReady || operation.phase === "ready")) { |
| 40 | surfaceOpened = true; |
| 41 | await options?.onSurfaceReady(operation); |
| 42 | } |
| 43 | const status = operation.progress?.status; |
| 44 | const recovering = status && ["queued", "running", "waiting_lock", "waiting_workspace", "retrying_storage"].includes(status); |
| 45 | if (options?.isObservationCurrent?.() === false || operation.phase === "ready" || status === "blocked" || status === "stopped" || status === "stopping" |
| 46 | || (!recovering && operation.phase !== "reserved" && operation.phase !== "starting")) return operation; |
| 47 | await new Promise(resolve => setTimeout(resolve, 150)); |
| 48 | if (options?.isObservationCurrent?.() === false) return operation; |
| 49 | operation = await app.GetManualSessionCreation(request.operationId); |
| 50 | } |
| 51 | } |
| 52 |