clone

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

main
lofi / package / runtime / device-capabilities.ts
device-capabilities.ts 4.6K
1import { type PrfSupport, readPrfSupport } from "./auth.ts";23/** Browser capabilities that determine whether lofi can provide its runtime guarantees. */4export type DeviceCapabilityReport = {5 // Package-owned device capability checks.6 secureContext: boolean;7 opfs: boolean;8 sharedWorker: boolean;9 webLocks: boolean;10 messageChannel: boolean;11 durableDriverSupported: boolean;12 webAuthn: boolean;13 prf: PrfSupport;14 persistentPermission: "granted" | "not-granted" | "unavailable" | "error";15 displayMode: "standalone" | "browser";16};1718/** Capability report that excludes the separately requested persistence permission. */19export type DurableCapabilityReport = Omit<DeviceCapabilityReport, "persistentPermission">;2021type StorageManagerWithOpfs = StorageManager & {22 getDirectory?: () => Promise<FileSystemDirectoryHandle>;23};2425function browserStorage(): StorageManagerWithOpfs | undefined {26 return typeof navigator === "undefined"27 ? undefined28 : navigator.storage as StorageManagerWithOpfs | undefined;29}3031/** Reads synchronous browser capabilities needed by the durable Jazz driver. */32export function durableCapabilityReport(): DurableCapabilityReport {33 const storage = browserStorage();34 const locks = typeof navigator === "undefined" ? undefined : navigator.locks;35 const secureContext = globalThis.isSecureContext === true;36 const opfs = typeof storage?.getDirectory === "function";37 const sharedWorker = typeof globalThis.SharedWorker === "function";38 const webLocks = typeof locks?.request === "function";39 const messageChannel = typeof globalThis.MessageChannel === "function";40 return {41 secureContext,42 opfs,43 sharedWorker,44 webLocks,45 messageChannel,46 durableDriverSupported: secureContext && opfs && sharedWorker && webLocks && messageChannel,47 webAuthn: typeof PublicKeyCredential === "function",48 prf: "unknown",49 displayMode: typeof globalThis.matchMedia === "function" &&50 globalThis.matchMedia("(display-mode: standalone)").matches51 ? "standalone"52 : "browser",53 };54}5556/** Reads the complete capability report without requesting new browser permission. */57export async function readDeviceCapabilityReport(): Promise<DeviceCapabilityReport> {58 const base = durableCapabilityReport();59 const storage = browserStorage();60 const prf = await readPrfSupport(base.webAuthn);61 if (typeof storage?.persisted !== "function") {62 return { ...base, prf, persistentPermission: "unavailable" };63 }64 try {65 return {66 ...base,67 prf,68 persistentPermission: await storage.persisted() ? "granted" : "not-granted",69 };70 } catch {71 return { ...base, prf, persistentPermission: "error" };72 }73}7475/** Requests eviction protection, then returns the browser's authoritative capability report. */76export async function requestPersistentStorage(): Promise<DeviceCapabilityReport> {77 const storage = browserStorage();78 if (typeof storage?.persist !== "function") return await readDeviceCapabilityReport();79 try {80 await storage.persist();81 } catch {82 // The subsequent read reports the browser's authoritative persisted state.83 }84 return await readDeviceCapabilityReport();85}8687/** Raised when the browser cannot provide the durable local-storage contract. */88export class DurableStorageUnsupportedError extends Error {89 /** Stable error class name for diagnostics and error boundaries. */90 override name = "DurableStorageUnsupportedError";91 /** Capabilities observed when the runtime rejected startup. */92 readonly report: DurableCapabilityReport;9394 /** Creates an error that identifies every missing durable-driver capability. */95 constructor(report: DurableCapabilityReport = durableCapabilityReport()) {96 const missing = Object.entries({97 secureContext: report.secureContext,98 opfs: report.opfs,99 sharedWorker: report.sharedWorker,100 webLocks: report.webLocks,101 messageChannel: report.messageChannel,102 }).filter(([, available]) => !available).map(([name]) => name);103 super(104 `Durable local storage is unsupported; missing ${105 missing.join(", ")106 }. Use the stable HTTPS device URL in a supported browser.`,107 );108 this.report = report;109 }110}111112/** True when an error is a browser unable to provide the durable-storage contract. */113export function isDurableStorageUnsupportedError(114 error: unknown,115): error is DurableStorageUnsupportedError {116 return error instanceof DurableStorageUnsupportedError;117}118119/** Throws unless the current browser can open lofi's persistent local driver. */120export function assertDurableBrowser(): void {121 const report = durableCapabilityReport();122 if (!report.durableDriverSupported) throw new DurableStorageUnsupportedError(report);123}