import { createRepo, uploadFiles, downloadFile, whoAmI } from "@huggingface/hub"; import type { AppData, SyncableSettings } from "./store"; import { validateData } from "./store"; /** Optional sync of the app data to a private HF dataset owned by the user. * Requires a user-pasted fine-grained token (write access to the dataset); * the host shell's OAuth token is short-lived and read-only, so it can't be * used for this. The token never leaves the browser except toward hf.co. */ const DATASET_NAME = "cookaiware-data"; const DATA_FILE = "cookaiware-data.json"; const SETTINGS_FILE = "cookaiware-settings.json"; export interface HfSyncIdentity { userName: string; repoId: string; } export async function resolveIdentity(token: string): Promise { const me = await whoAmI({ accessToken: token }); return { userName: me.name, repoId: `datasets/${me.name}/${DATASET_NAME}` }; } async function ensureRepo(token: string, repoId: string): Promise { try { await createRepo({ repo: repoId, accessToken: token, private: true }); } catch (err) { // The repo usually already exists (every save after the first), and the // Hub wording for that error varies — so never fail the save here. If the // repo is genuinely missing/unreachable, uploadFiles will raise the real // error right after and that one is surfaced. console.debug("[hf-sync] createRepo skipped:", err instanceof Error ? err.message : err); } } export async function uploadData( token: string, data: AppData, settings?: SyncableSettings, ): Promise { const identity = await resolveIdentity(token); await ensureRepo(token, identity.repoId); const files = [ { path: DATA_FILE, content: new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }) }, ]; if (settings) { files.push({ path: SETTINGS_FILE, content: new Blob([JSON.stringify(settings, null, 2)], { type: "application/json" }), }); } await uploadFiles({ repo: identity.repoId, accessToken: token, files, commitTitle: "Update CookAIware data", }); return identity; } export async function downloadData(token: string): Promise { const identity = await resolveIdentity(token); const res = await downloadFile({ repo: identity.repoId, path: DATA_FILE, accessToken: token }); if (!res) return null; return validateData(JSON.parse(await res.text())); } /** Fetch the synced settings subset, or null if none has been uploaded yet * (older datasets, or a device that never enabled sync). */ export async function downloadSettings(token: string): Promise { try { const identity = await resolveIdentity(token); const res = await downloadFile({ repo: identity.repoId, path: SETTINGS_FILE, accessToken: token }); if (!res) return null; return JSON.parse(await res.text()); } catch (err) { console.debug("[hf-sync] no settings file:", err instanceof Error ? err.message : err); return null; } }