clone

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

feat/184-task-notice-migration
lofi / package / runtime / app.ts
app.ts 5.0K
1/**2 * Author-owned configuration consumed by the package runtime.3 *4 * @example5 * ```ts6 * const config: LofiAppConfig<typeof schema> = {7 * name: "my-app",8 * databaseName: "my-app",9 * schema,10 * storage: "durable",11 * sync: { adapter: "jazz" },12 * };13 * ```14 */15export type LofiAppConfig<Schema = unknown> = {16 /** Display name, used for the PWA and as the passkey label. Must be non-empty. */17 name: string;18 /** Name of the local database the runtime opens. Must be non-empty. */19 databaseName: string;20 /** The app's Jazz schema, as returned by the `@nzip/lofi/schema` define entry points. */21 schema: Schema;22 /** Storage durability contract. `durable` is the only supported mode. */23 storage: "durable";24 /**25 * Allowlist of hostnames considered committed-stable for enrolling device26 * credentials: exact hostnames or `*.` suffix patterns. A deployed HTTPS27 * host not on this list classifies as `unverified` and passkey enrollment28 * refuses there — with the list omitted or empty, enrollment works only on29 * localhost. Add the production hostname before shipping passkey features.30 */31 credentialOrigins?: readonly string[];32 /**33 * Recoverable passkeys are scoped to this relying-party ID. Pin the canonical34 * production hostname before users create backups. When omitted, the current35 * hostname is used, which is convenient locally but preview-host specific.36 */37 passkey?: { rpId?: string };38 /**39 * Optional PWA update-lifecycle preferences. Omitting the block keeps the40 * safe defaults: the framework's minimal read-only banner renders when41 * local data is newer than the running bundle, and tabs left on a stale42 * revision after a worker swap reload onto the new one.43 */44 pwa?: {45 /**46 * `default` renders the framework's minimal schema-compatibility banner;47 * `none` suppresses it for apps that render their own from48 * `useSchemaCompat`.49 */50 updateBanner?: "default" | "none";51 /**52 * How a tab that did not initiate an update behaves once a new worker53 * controls it: `reload` (default) reloads onto the new revision; `prompt`54 * keeps the document read-only and asks the user to reload.55 */56 staleTabs?: "reload" | "prompt";57 /**58 * `default` renders the framework's minimal notice when an installed59 * WebKit app starts in a fresh storage container while local data exists60 * in the browser; `none` suppresses it for apps that render their own61 * from `useStorageFork`.62 */63 forkNotice?: "default" | "none";64 };65 sync: { adapter: "jazz" };66 repositoryUrl?: string;67};6869type AppSlot = { config?: LofiAppConfig };7071const slotName = "__LOFI_APP_DEFINITION__";72const browserGlobal = globalThis as typeof globalThis & { [slotName]?: AppSlot };7374function slot(): AppSlot {75 browserGlobal[slotName] ??= {};76 return browserGlobal[slotName];77}7879/** Raised when package runtime startup cannot resolve valid author-owned app configuration. */80export class LofiConfigurationError extends Error {81 /** Stable error class name for diagnostics and error boundaries. */82 override readonly name = "LofiConfigurationError";83 /** Stable category for runtime startup classification. */84 readonly code = "configuration-error";85}8687/** True when an error is invalid or missing author-owned app configuration. */88export function isLofiConfigurationError(error: unknown): error is LofiConfigurationError {89 return error instanceof LofiConfigurationError;90}9192/**93 * Defines the application-facing values the versioned runtime needs.94 *95 * The returned value retains the exact schema type so author code can select96 * tables without importing or editing framework internals. Calling it also97 * registers the configuration globally so runtime entry points (`bootLofi`,98 * `getRuntime`) can resolve it — import the defining module before booting.99 *100 * @example101 * ```ts102 * import { defineLofiApp } from "@nzip/lofi";103 * import { app as schema } from "./schema.ts";104 *105 * export const app = defineLofiApp({106 * name: "my-app",107 * databaseName: "my-app",108 * schema,109 * storage: "durable",110 * sync: { adapter: "jazz" },111 * });112 * ```113 *114 * @param config The author-owned app configuration, including the raw Jazz schema.115 * @returns The same configuration, typed to the exact schema for table selection.116 * @throws {LofiConfigurationError} When `name` or `databaseName` is empty.117 */118export function defineLofiApp<const Schema>(119 config: LofiAppConfig<Schema>,120): LofiAppConfig<Schema> {121 if (!config.name.trim()) throw new LofiConfigurationError("lofi app name must not be empty");122 if (!config.databaseName.trim()) {123 throw new LofiConfigurationError("lofi database name must not be empty");124 }125 slot().config = config;126 return config;127}128129/** Package-internal lookup used when browser resources are opened lazily. */130export function getLofiApp(): LofiAppConfig {131 const config = slot().config;132 if (!config) {133 throw new LofiConfigurationError(134 "lofi app is not defined; export defineLofiApp({...}) from src/app.ts before booting the runtime",135 );136 }137 return config;138}