clone

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

fix/175-template-quality-pass
lofi / package / runtime / startup-recovery.ts
startup-recovery.ts 4.6K
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 | "reload-loop"13 | "storage-startup-failed"14 | "unsupported-capabilities";1516/** Non-sensitive runtime context retained for diagnostics and recovery UI. */17export type RuntimeStartupFailure = {18 code: RuntimeStartupFailureCode;19 runtimeMode: "local" | "managed";20 message: string;21};2223const startupMessages: Record<RuntimeStartupFailureCode, string> = {24 "broker-incompatible":25 "Another tab for this app is running an incompatible version. Close every other app tab, then reload this tab.",26 "configuration-error":27 "The Lofi runtime configuration is invalid. Run deno task doctor, fix the first blocker, then reload.",28 "reload-loop":29 "The app kept reloading without reaching a working runtime, so automatic reloads stopped. Check the device report's account and sync state, then reload manually.",30 "storage-startup-failed":31 "Durable storage could not start. Run deno task doctor, fix the first blocker, then reload.",32 "unsupported-capabilities":33 "This browser cannot provide Lofi's durable storage requirements. Open the stable HTTPS app URL in a supported browser.",34};3536/** Lofi-owned error boundary for a rejected persistent runtime startup. */37export class RuntimeStartupError extends Error {38 /** Stable error class name for diagnostics and UI boundaries. */39 override readonly name = "RuntimeStartupError";40 /** Stable public category without vendor-specific error details. */41 readonly code: RuntimeStartupFailureCode;42 /** Non-sensitive failure snapshot retained for diagnostics. */43 readonly failure: RuntimeStartupFailure;4445 /** Creates a stable public rejection while retaining the original error as its cause. */46 constructor(failure: RuntimeStartupFailure, cause?: unknown) {47 super(failure.message, cause === undefined ? undefined : { cause });48 this.code = failure.code;49 this.failure = failure;50 }51}5253/** True when an error is a classified persistent-runtime startup failure. */54export function isRuntimeStartupError(error: unknown): error is RuntimeStartupError {55 return error instanceof RuntimeStartupError;56}5758/** Builds the stable failure record for a refused framework-driven reload. */59export function reloadLoopFailure(60 runtimeMode: RuntimeStartupFailure["runtimeMode"],61): RuntimeStartupFailure {62 return { code: "reload-loop", runtimeMode, message: startupMessages["reload-loop"] };63}6465/** Maps vendor, capability, configuration, and fallback failures onto stable Lofi categories. */66export function classifyRuntimeStartupFailure(67 error: unknown,68 runtimeMode: RuntimeStartupFailure["runtimeMode"],69): RuntimeStartupFailure {70 const code: RuntimeStartupFailureCode =71 error instanceof IncompatibleBrowserBrokerConfigurationError72 ? "broker-incompatible"73 : error instanceof DurableStorageUnsupportedError74 ? "unsupported-capabilities"75 : error instanceof LofiConfigurationError76 ? "configuration-error"77 : "storage-startup-failed";78 return { code, runtimeMode, message: startupMessages[code] };79}8081/**82 * Creates the package-owned Jazz override. It records the stable failure but83 * deliberately never confirms or reloads; the rejected createDb call remains84 * the startup boundary and recovery stays an explicit user action.85 */86export function createBrokerIncompatibilityHandler(87 runtimeMode: RuntimeStartupFailure["runtimeMode"],88 record: (failure: RuntimeStartupFailure) => void,89): IncompatibleBrowserBrokerConfigurationHandler {90 return (error) => record(classifyRuntimeStartupFailure(error, runtimeMode));91}9293/** Runs one runtime startup attempt, retaining and throwing only Lofi-owned classification. */94export async function runRuntimeStartup<T>(95 runtimeMode: RuntimeStartupFailure["runtimeMode"],96 start: () => Promise<T>,97 record: (failure: RuntimeStartupFailure) => void,98): Promise<T> {99 try {100 return await start();101 } catch (error) {102 if (error instanceof RuntimeStartupError) throw error;103 const failure = classifyRuntimeStartupFailure(error, runtimeMode);104 record(failure);105 throw new RuntimeStartupError(failure, error);106 }107}108109/** Performs the explicit navigation required after closing incompatible app tabs. */110export function reloadAfterRuntimeStartupFailure(111 reload: () => void = () => globalThis.location.reload(),112): void {113 reload();114}