Skip to the content

Capsid menu

app/components/admin/use-draft-buffer.ts

83 lines and 9 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of use-draft-buffer.ts
FunctionLinesComplexityCoveredCRAPMutation
useDraftBuffer22 to 931No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:43>43 to 431No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:52>52 to 572No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:59>59 to 611No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:64>64 to 744No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:76>76 to 834No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:85>85 to 903No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:88>88 to 881No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:89>89 to 891No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports app/lib/editor/draft-buffer.ts, app/lib/editor/feedback.ts. Imported by app/components/admin/post-editor.tsx.

Source

import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";

import {
  bufferDiffers,
  clearAllBuffersFor,
  draftKey,
  purgeLegacyBuffers,
  readBuffer,
  readForm,
  writeBuffer,
  type DraftBuffer,
} from "~/lib/editor/draft-buffer";
import type { EditorFeedback } from "~/lib/editor/feedback";

// The label reads in whole minutes, so 30s lags by at most half a unit; a 1s tick would re-render sixty times per change.
const BUFFER_AGE_TICK_MS = 30_000;

/**
 * The editor's browser-local crash net: the buffer write, the offer to restore an earlier session's
 * buffer, its clearing once a save lands, and the clock its age label reads.
 */
export function useDraftBuffer({
  formRef,
  slug,
  headSha,
  isNew,
  feedback,
  dirty,
  setDirty,
  setRestoredFrom,
}: {
  formRef: React.RefObject<HTMLFormElement | null>;
  /** The committed slug, empty for a new post. */
  slug: string;
  headSha: string;
  isNew: boolean;
  feedback: EditorFeedback | null | undefined;
  dirty: boolean;
  setDirty: Dispatch<SetStateAction<boolean>>;
  setRestoredFrom: Dispatch<SetStateAction<string | null>>;
}) {
  const [savedAt, setSavedAt] = useState<string | null>(null);
  const [ageNow, setAgeNow] = useState(() => Date.now());
  // writeBuffer returns null both before it has run and when storage refuses, so the attempt is tracked separately.
  const [bufferTried, setBufferTried] = useState(false);
  const [offer, setOffer] = useState<DraftBuffer | null>(null);

  const autosaveTimer = useRef<number>(0);
  const storageKey = draftKey(slug, headSha);

  // Never commits: a browser-local crash net. The save path is the only writer.
  const persist = useCallback(() => {
    const form = formRef.current;
    if (!form) return;
    setBufferTried(true);
    setSavedAt(writeBuffer(storageKey, readForm(form)));
  }, [storageKey, formRef]);

  useEffect(() => {
    purgeLegacyBuffers();
  }, []);

  // Restoring is never automatic: replacing committed content with older local text is the surprise this prevents.
  useEffect(() => {
    const form = formRef.current;
    if (!form) return;
    const stored = readBuffer(storageKey);
    if (!stored) return;
    if (!bufferDiffers(stored, readForm(form))) {
      clearAllBuffersFor(slug);
      return;
    }
    setOffer(stored);
  }, [storageKey, slug, formRef]);

  useEffect(() => {
    if (!feedback || feedback.state === "failed") return;
    clearAllBuffersFor(slug);
    if (isNew) clearAllBuffersFor("");
    setOffer(null);
    setDirty(false);
    setRestoredFrom(null);
  }, [feedback, slug, isNew, setDirty, setRestoredFrom]);

  useEffect(() => {
    if (!dirty || !savedAt) return;
    setAgeNow(Date.now());
    const id = window.setInterval(() => setAgeNow(Date.now()), BUFFER_AGE_TICK_MS);
    return () => window.clearInterval(id);
  }, [dirty, savedAt]);

  return { offer, setOffer, savedAt, ageNow, bufferTried, persist, autosaveTimer };
}