"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { NoteOp, NotesRead, Note, NotesDoc } from "./types"; // The page's handle on one notes.json: read it, write one op at a time with the // token it read, and on a 409 re-read and say so rather than retrying blind -- // an agent may have replied in between, and the operator should see that reply // before their edit lands on top of it. export type NotesTarget = { article: string } | { project: string }; export function notesQuery(target: NotesTarget): string { return "article" in target ? `article=${encodeURIComponent(target.article)}` : `project=${encodeURIComponent(target.project)}`; } export type UseNotes = { doc: NotesDoc | null; notes: Note[]; source: NotesRead["source"]; error: string | null; loading: boolean; busy: boolean; /** Apply one op. Resolves to the note touched (null on delete), or null after an error (shown in `error`). */ write: (op: NoteOp) => Promise; reload: () => Promise; }; export function useNotes(target: NotesTarget | null, { initial }: { initial?: NotesRead | null } = {}): UseNotes { const [read, setRead] = useState(initial ?? null); const [error, setError] = useState(initial?.error ?? null); const [loading, setLoading] = useState(!initial && !!target); const [busy, setBusy] = useState(false); const token = useRef(initial?.token ?? null); const query = target ? notesQuery(target) : null; const reload = useCallback(async () => { if (!query) return; setLoading(true); try { const res = await fetch(`/api/notes?${query}`, { cache: "no-store" }); const j = await res.json(); if (!res.ok) throw new Error(j.error ?? res.statusText); token.current = j.token; setRead(j); setError(j.error ?? null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, [query]); useEffect(() => { if (!initial) void reload(); // `initial` is the server render's read; only a changed target re-reads. // eslint-disable-next-line react-hooks/exhaustive-deps }, [reload]); const write = useCallback( async (op: NoteOp): Promise => { if (!query) return null; setBusy(true); try { const res = await fetch(`/api/notes?${query}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token: token.current, op }), }); const j = await res.json(); if (res.status === 409) { await reload(); setError(`${j.error ?? "the notes changed"} -- reloaded; check and try again`); return null; } if (!res.ok) throw new Error(j.error ?? res.statusText); token.current = j.token; setRead((r) => (r ? { ...r, doc: j.doc, token: j.token, source: j.doc?.source ?? r.source } : r)); setError(null); return j.note ?? null; } catch (err) { setError(err instanceof Error ? err.message : String(err)); return null; } finally { setBusy(false); } }, [query, reload], ); return { doc: read?.doc ?? null, notes: read?.doc?.notes ?? [], source: read?.source ?? null, error, loading, busy, write, reload }; }