All files / web/src/hooks useAbacusDesignSnapshot.ts

0% Statements 0/77
0% Branches 0/1
0% Functions 0/1
0% Lines 0/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78                                                                                                                                                           
'use client'

// Design-snapshot read/write (Gitea #22): the `?design=<id>` deep link's data
// side. The read hydrates the studio once per id (snapshots are immutable
// content behind an immutable id — staleTime Infinity, no retry on the
// permanent 404 the access gate answers). The write is the submit-path helper:
// TOTAL by contract — a failed snapshot degrades the print's edit link to
// today's shallow form, it must never block or fail the print itself.

import { useQuery } from '@tanstack/react-query'
import {
  type AbacusDesignProvenance,
  type AbacusDesignSnapshot,
  parseDesignSnapshot,
} from '@/lib/abacus/design-snapshot'
import { api } from '@/lib/queryClient'
import { abacusDesignKeys } from '@/lib/queryKeys'

/** How long the snapshot POST may take before the submit stops waiting on it.
 *  Generous vs the render it races (which has a 180s budget) but bounded — a
 *  hung snapshot must not hold the whole print hostage. */
const PERSIST_TIMEOUT_MS = 10_000

async function fetchDesignSnapshot(designId: string): Promise<AbacusDesignSnapshot> {
  const res = await api(`abacus/designs/${encodeURIComponent(designId)}`)
  if (!res.ok) throw new Error('Failed to fetch design snapshot')
  const data: unknown = await res.json()
  const design = parseDesignSnapshot((data as { design?: unknown } | null)?.design)
  if (!design) throw new Error('Design snapshot failed to parse')
  return design
}

/**
 * Hook: fetch a stored design snapshot for hydration. `isError` covers the
 * whole degrade family (denied, gone, unparseable) — the page maps it to the
 * "design unavailable" notice, mirroring `playerUnavailable`.
 */
export function useAbacusDesignSnapshot(designId: string | null) {
  return useQuery({
    queryKey: abacusDesignKeys.detail(designId ?? 'none'),
    queryFn: () => fetchDesignSnapshot(designId as string),
    enabled: Boolean(designId),
    staleTime: Number.POSITIVE_INFINITY,
    retry: false,
  })
}

/**
 * Persist the studio's current design; resolves the stored id, or null on ANY
 * failure (network, refusal, timeout, junk response) — never throws. Identical
 * resubmits converge on the same id server-side (owner-scoped content hash).
 *
 * `name` carries a name FORWARD across an edit (#11): a design's id is its
 * content, so editing mints a new row, and without this "Ada's abacus" would
 * quietly become an unnamed row on its first tweak. The server fills a blank
 * name and never overwrites a chosen one, so passing it is always safe.
 */
export async function persistAbacusDesign(
  design: AbacusDesignSnapshot,
  provenance: AbacusDesignProvenance,
  name?: string | null
): Promise<string | null> {
  try {
    const res = await api('abacus/designs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(name ? { design, provenance, name } : { design, provenance }),
      signal: AbortSignal.timeout(PERSIST_TIMEOUT_MS),
    })
    if (!res.ok) return null
    const body: unknown = await res.json().catch(() => null)
    const id = (body as { id?: unknown } | null)?.id
    return typeof id === 'string' && id.length > 0 ? id : null
  } catch {
    return null
  }
}