clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / package / runtime / startup-recovery.ts
startup-recovery.ts 4.0K
1import {2 IncompatibleBrowserBrokerConfigurationError,3 type IncompatibleBrowserBrokerConfigurationHandler,4} from "jazz-tools";5import { LofiConfigurationError } from "./app.ts";6import { DurableStorageUnsupportedError } from "./device-capabilities.ts";78/** Stable categories for failures that prevent Lofi's persistent runtime from opening. */9export type RuntimeStartupFailureCode =10 | "broker-incompatible"11 | "configuration-error"12 | "storage-startup-failed"13 | "unsupported-capabilities";1415/** Non-sensitive runtime context retained for diagnostics and recovery UI. */16export type RuntimeStartupFailure = {17 code: RuntimeStartupFailureCode;18 runtimeMode: "local" | "managed";19 message: string;20};2122const startupMessages: Record<RuntimeStartupFailureCode, string> = {23 "broker-incompatible":24 "Another tab for this app is running an incompatible version. Close every other app tab, then reload this tab.",25 "configuration-error":26 "The Lofi runtime configuration is invalid. Run deno task doctor, fix the first blocker, then reload.",27 "storage-startup-failed":28 "Durable storage could not start. Run deno task doctor, fix the first blocker, then reload.",29 "unsupported-capabilities":30 "This browser cannot provide Lofi's durable storage requirements. Open the stable HTTPS app URL in a supported browser.",31};3233/** Lofi-owned error boundary for a rejected persistent runtime startup. */34export class RuntimeStartupError extends Error {35 /** Stable error class name for diagnostics and UI boundaries. */36 override readonly name = "RuntimeStartupError";37 /** Stable public category without vendor-specific error details. */38 readonly code: RuntimeStartupFailureCode;39 /** Non-sensitive failure snapshot retained for diagnostics. */40 readonly failure: RuntimeStartupFailure;4142 /** Creates a stable public rejection while retaining the original error as its cause. */43 constructor(failure: RuntimeStartupFailure, cause?: unknown) {44 super(failure.message, cause === undefined ? undefined : { cause });45 this.code = failure.code;46 this.failure = failure;47 }48}4950/** Maps vendor, capability, configuration, and fallback failures onto stable Lofi categories. */51export function classifyRuntimeStartupFailure(52 error: unknown,53 runtimeMode: RuntimeStartupFailure["runtimeMode"],54): RuntimeStartupFailure {55 const code: RuntimeStartupFailureCode =56 error instanceof IncompatibleBrowserBrokerConfigurationError57 ? "broker-incompatible"58 : error instanceof DurableStorageUnsupportedError59 ? "unsupported-capabilities"60 : error instanceof LofiConfigurationError61 ? "configuration-error"62 : "storage-startup-failed";63 return { code, runtimeMode, message: startupMessages[code] };64}6566/**67 * Creates the package-owned Jazz override. It records the stable failure but68 * deliberately never confirms or reloads; the rejected createDb call remains69 * the startup boundary and recovery stays an explicit user action.70 */71export function createBrokerIncompatibilityHandler(72 runtimeMode: RuntimeStartupFailure["runtimeMode"],73 record: (failure: RuntimeStartupFailure) => void,74): IncompatibleBrowserBrokerConfigurationHandler {75 return (error) => record(classifyRuntimeStartupFailure(error, runtimeMode));76}7778/** Runs one runtime startup attempt, retaining and throwing only Lofi-owned classification. */79export async function runRuntimeStartup<T>(80 runtimeMode: RuntimeStartupFailure["runtimeMode"],81 start: () => Promise<T>,82 record: (failure: RuntimeStartupFailure) => void,83): Promise<T> {84 try {85 return await start();86 } catch (error) {87 if (error instanceof RuntimeStartupError) throw error;88 const failure = classifyRuntimeStartupFailure(error, runtimeMode);89 record(failure);90 throw new RuntimeStartupError(failure, error);91 }92}9394/** Performs the explicit navigation required after closing incompatible app tabs. */95export function reloadAfterRuntimeStartupFailure(96 reload: () => void = () => globalThis.location.reload(),97): void {98 reload();99}