1// Package-owned environment and adapter configuration.23import type { DbConfig, IncompatibleBrowserBrokerConfigurationHandler } from "jazz-tools";4import { getLofiApp } from "./app.ts";5import { anchorAppId, configuredServerUrl, readDeclaredSink } from "./data-sink.ts";67/**8 * The device-local anchor app id: storage keys, the account secret store, and9 * the local database namespace key off this value, so state written before a10 * sink is declared survives the declaration. See `data-sink.ts` for how it is11 * derived.12 */13export const appId = anchorAppId;1415/** The sync location currently in effect and where it came from. */16export type ActiveSink = {17 /** `declared` is a runtime-declared sink; `default` is the compiled managed app. */18 source: "declared" | "default";19 appId: string;20 serverUrl: string;21 label?: string;22};2324/**25 * Resolves the sync location in effect: a runtime-declared sink wins, the26 * compiled managed app (when the deployment has one) is the default, and27 * `null` means this device is local-only until the user declares one.28 */29export function activeSink(): ActiveSink | null {30 const declared = readDeclaredSink();31 if (declared) {32 return {33 source: "declared",34 appId: declared.appId,35 serverUrl: declared.serverUrl,36 ...(declared.label ? { label: declared.label } : {}),37 };38 }39 return configuredServerUrl40 ? { source: "default", appId: anchorAppId, serverUrl: configuredServerUrl }41 : null;42}4344/** The server URL of the sync location in effect, if any. */45export function activeServerUrl(): string | undefined {46 return activeSink()?.serverUrl;47}4849/** The Jazz app id of the sync location in effect, falling back to the anchor. */50export function activeAppId(): string {51 return activeSink()?.appId ?? anchorAppId;52}5354/** Whether a sync location exists (declared or compiled), so sync/backup is possible. */55export function syncAvailable(): boolean {56 return activeSink() !== null;57}5859const syncElectionKey = `lofi:sync-elected:${appId}`;6061/**62 * Whether the user on this device has elected to back up and sync this account.63 * First boot is local-only; the account only reaches the network once the user64 * opts in (see `session.ts`), so nothing leaves the device by default.65 */66export function syncElected(): boolean {67 if (!syncAvailable() || typeof localStorage === "undefined") return false;68 try {69 return localStorage.getItem(syncElectionKey) === "1";70 } catch {71 return false;72 }73}7475/** Records (or clears) the sync election. Ignored when no sync location exists. */76export function setSyncElected(elected: boolean): void {77 if (!syncAvailable() || typeof localStorage === "undefined") return;78 try {79 if (elected) localStorage.setItem(syncElectionKey, "1");80 else localStorage.removeItem(syncElectionKey);81 } catch {82 // A private-mode storage failure must not break account setup.83 }84}8586/** Whether writes actually replicate right now: a sync location exists *and* is elected. */87export function syncing(): boolean {88 return syncAvailable() && syncElected();89}9091export function databaseConfig(92 secret: string,93 accountNamespace: string,94 mode: "local" | "managed" = syncing() ? "managed" : "local",95 connect = mode === "managed" && syncing(),96 onIncompatibleBrowserBrokerConfiguration: IncompatibleBrowserBrokerConfigurationHandler =97 () => {},98): DbConfig {99 const app = getLofiApp();100 const sink = activeSink();101 // The local tier stays on the anchor so first-boot data is always found; the102 // managed tier belongs to the active sink's store, so a declared sink gets103 // its own database namespace and connection identity.104 const connectionAppId = mode === "managed" ? (sink?.appId ?? appId) : appId;105 return {106 appId: connectionAppId,107 // The same local-first secret opens the same account whether or not a server108 // is attached, so electing to sync later preserves all existing data.109 ...(connect && sink ? { serverUrl: sink.serverUrl } : {}),110 secret,111 driver: {112 type: "persistent",113 dbName: `${app.databaseName}-${connectionAppId}-${accountNamespace}-${mode}`,114 },115 onIncompatibleBrowserBrokerConfiguration,116 };117}