"use client"; /** * useBlockSaveQueue — React binding for the shared per-page * BlockBatchQueue (lib/batch-queue.ts). * * `queueChange(blockId, content)` debounces (500 ms) into one * POST /blocks/batch. `saveNow(blockId, content)` persists a single block * immediately (blur fallback) and drops the queued copy of it so the final * state never depends on the debounce window closing. The shared queue is * also flushed on unmount. */ import { useCallback, useEffect, useRef } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { blockApi, isApiError } from "@/lib/api"; import { getBlockBatchQueue } from "@/lib/batch-queue"; export function useBlockSaveQueue(pageId: string) { const qc = useQueryClient(); const pageIdRef = useRef(pageId); pageIdRef.current = pageId; const invalidate = useCallback( (pid: string) => qc.invalidateQueries({ queryKey: ["pages", pid, "blocks"] }), [qc] ); const queueChange = useCallback( (blockId: string, content: unknown) => { const id = pageIdRef.current; if (!id) return; getBlockBatchQueue(id).queue({ op: "update", blockId, patch: { content } }); }, [] ); const saveNow = useCallback( async (blockId: string, content: unknown) => { const id = pageIdRef.current; if (!id) return; try { await blockApi.update(blockId, { content }); invalidate(id); } catch (err) { // TASK-0136: a 404 is NOT transient — the block is gone (the // delete-on-blur cleanup removed it while this blur save was // in flight, or a concurrent peer deleted it). Re-queueing would // poison the next batch flush (the batch endpoint 400s on a // missing id and aborts the whole batch). Drop it instead. if (isApiError(err) && err.status === 404) return; // Transient failure: re-queue so the next flush retries. getBlockBatchQueue(id).queue({ op: "update", blockId, patch: { content } }); } }, [invalidate] ); // Flush pending ops when this page unmounts (no lost edits on navigation). useEffect(() => { const id = pageIdRef.current; return () => { if (!id) return; void getBlockBatchQueue(id).flush().then(() => invalidate(id)); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageId]); // Exposed for property-only updates (e.g. the table grid's add/remove // row/col, which patch `properties` directly and otherwise leave the // ["pages", pageId, "blocks"] cache stale until the next refetch). return { queueChange, saveNow, invalidate }; }