All files / web/src/hooks useThhFilamentCatalog.ts

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

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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                                                                                                                                                                                                         
'use client'

import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import {
  type FilamentCatalog,
  thhFilamentsToCatalog,
} from '@/components/create/abacus/abacus-catalog'
import type {
  PrintUnavailableReason,
  ThhFilamentsResponse,
  ThhPrintersResponse,
} from '@/lib/abacus/print/filament-wire'
import { api } from '@/lib/queryClient'
import { abacusPrintKeys } from '@/lib/queryKeys'

/**
 * A read that can be expectedly unavailable. The proxy expresses "no service
 * / can't reach it / bad token" as HTTP statuses; those become data (not
 * thrown errors) so React Query doesn't retry-storm a printer that's simply
 * offline. Anything else is a real fault and throws.
 */
type Degradable<T> = { ok: true; value: T } | { ok: false; reason: PrintUnavailableReason }

function degradeReason(status: number): PrintUnavailableReason | null {
  if (status === 404) return 'not-configured'
  if (status === 502) return 'unreachable'
  if (status === 401 || status === 403) return 'unauthorized'
  return null
}

async function fetchDegradable<T>(path: string): Promise<Degradable<T>> {
  const res = await api(path)
  if (!res.ok) {
    const reason = degradeReason(res.status)
    if (reason) return { ok: false, reason }
    throw new Error(`${path} failed: ${res.status}`)
  }
  return { ok: true, value: (await res.json()) as T }
}

/**
 * The live filament roster for the Abacus Studio print projection (#8.5),
 * read through the abacus print proxy: discover printers, prefer the
 * multi-material one, project its loaded-AMS roster onto FilamentCatalog.
 *
 * Server state, so it lives behind React Query (never `fetch()` in a
 * component). Returns `catalog: null` plus an `unavailable` reason whenever
 * the service isn't paired or reachable — the studio then falls back to the
 * params-derived, color-only catalog. AMS state changes slowly, so reads are
 * cached for a minute; doorbell invalidations (#8.4) refresh them early.
 */
export function useThhFilamentCatalog() {
  const printers = useQuery({
    queryKey: abacusPrintKeys.printers(),
    queryFn: () => fetchDegradable<ThhPrintersResponse>('abacus/print/printers'),
    staleTime: 60_000,
  })

  const printerId = useMemo(() => {
    if (!printers.data?.ok) return null
    const rows = printers.data.value.printers ?? []
    return (rows.find((p) => p.multiMaterial) ?? rows[0])?.id ?? null
  }, [printers.data])

  const filaments = useQuery({
    queryKey: abacusPrintKeys.filaments(printerId ?? 'none'),
    queryFn: () =>
      fetchDegradable<ThhFilamentsResponse>(
        `abacus/print/printers/${encodeURIComponent(printerId ?? '')}/filaments`
      ),
    enabled: printerId !== null,
    staleTime: 60_000,
  })

  const catalog: FilamentCatalog | null = useMemo(() => {
    if (!filaments.data?.ok) return null
    return thhFilamentsToCatalog(
      filaments.data.value.filaments ?? [],
      new Date(filaments.dataUpdatedAt || Date.now()).toISOString()
    )
  }, [filaments.data, filaments.dataUpdatedAt])

  const unavailable: PrintUnavailableReason | null =
    printers.data && !printers.data.ok
      ? printers.data.reason
      : printers.data?.ok && printerId === null
        ? 'no-printer'
        : filaments.data && !filaments.data.ok
          ? filaments.data.reason
          : null

  return {
    catalog,
    printerId,
    unavailable,
    isLoading: printers.isLoading || (printerId !== null && filaments.isLoading),
    isFetching: printers.isFetching || filaments.isFetching,
  }
}