import type { UserPreferences } from "magicbell-js/user-client"; import { Client } from "magicbell-js/user-client"; import { action, createMachine, immediate, invoke, reduce, state, transition } from "robot3"; import { log } from "../lib/log"; type Props = { client: Client; }; type PreferencesContext = { error?: string; preferences: UserPreferences; snapshot: UserPreferences | null; changes: UserPreferences[]; }; function getInitialContext(): PreferencesContext { return { error: undefined, preferences: { categories: [] }, changes: [], snapshot: null }; } function setError(message?: string) { return reduce((ctx, ev) => ({ ...ctx, error: message || ev.error.message })); } function applyChange(ctx: PreferencesContext, change: { categoryKey: string; channelName: string; enabled: boolean; }): UserPreferences { const current = ctx.preferences; const categories = (current.categories || []).map(cat => { if (cat.key !== change.categoryKey) return cat; const channels = (cat.channels || []).map(ch => ch.name === change.channelName ? { ...ch, enabled: change.enabled } : ch); return { ...cat, channels }; }); return { categories: categories }; } export function createUserPreferencesMachine({ client }: Props) { let inflight = false; // we need a ref because ctx is immutable and thus stale const ref = { changes: [] as UserPreferences[] }; function drain(ctx: PreferencesContext): void { ref.changes = ctx.changes; async function run(): Promise { if (inflight) return; inflight = true; const job = ref.changes.pop(); ref.changes.length = 0; try { if (job) { await client.channels.saveUserPreferences(job); } } catch (error) { log.error("userPreferences.save", error); } finally { inflight = false; if (ref.changes.length > 0) { run(); } } } void run(); } return createMachine({ init: state(immediate("fetch")), fetch: invoke(log.wrap("debug", "userPreferences.fetch", async () => { const { data } = await client.channels.fetchUserPreferences(); return { preferences: data }; }), transition("done", "idle", reduce((ctx: PreferencesContext, e: any) => ({ ...ctx, error: undefined, preferences: e.data.preferences, snapshot: e.data.preferences, changes: [] }))), transition("error", "error", setError("Failed to fetch preferences"))), idle: state(transition("UPDATE", "idle", // Apply change optimistically and start a background save reduce((ctx: PreferencesContext, e: any) => { const updated = applyChange(ctx, e); return { ...ctx, error: undefined, preferences: updated, changes: [...ctx.changes, updated] }; }), action(drain)), transition("error", "idle")), error: state(transition("RETRY", "fetch")) }, getInitialContext); }