All files / web/src/hooks useAbacusPrintJobs.ts

100% Statements 96/96
75% Branches 9/12
100% Functions 8/8
100% Lines 96/96

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 971x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 2x 2x 1x 1x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
'use client'
 
// Abacus Studio — print-jobs roster + the two actions that RESOLVE a parked
// job (Gitea #9). All three go through abaci's ownership-gated proxy; the proxy
// relays THH byte-for-byte, so the roster read carries each job's `attention`
// reasons (see ./create/abacus/print-jobs projection) and the mutations post
// the exact bodies THH's start/cancel endpoints expect.
//
// State moves on doorbell rings (usePrintJobRing invalidates the roster key),
// so these mutations only need to invalidate the same key on success — the
// follow-up phase ring and this invalidate converge on a single refetch. No
// poll, no optimistic phase-guessing: the service is the source of truth.
 
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { type JobRow, normalizeJobs } from '@/components/create/abacus/print-jobs'
import { PrintServiceError } from '@/components/create/abacus/print-submit-failure'
import { api } from '@/lib/queryClient'
import { abacusPrintKeys } from '@/lib/queryKeys'
 
/** Start (or acknowledge-and-start) a parked job. `acknowledge` must list every
 *  reason code the job is parked on — THH refuses `acknowledgement_required`
 *  otherwise; a bare `ready` job takes `[]`. */
export interface StartPrintJobVars {
  jobId: string
  acknowledge: string[]
}
 
/** Cancel a job. A `printing` job requires `stopPrint: true` (THH refuses
 *  `job_printing` otherwise); a parked job cancels cleanly with `false`. */
export interface CancelPrintJobVars {
  jobId: string
  stopPrint: boolean
}
 
/**
 * The jobs roster, normalized. Read-only companion to the ring: the same query
 * key `usePrintJobRing` invalidates, so a ring or a mutation both land here.
 */
export function useAbacusPrintJobs(options: { enabled: boolean; connectionId?: string }) {
  const { enabled, connectionId } = options
  const query = useQuery({
    queryKey: abacusPrintKeys.jobs(connectionId),
    queryFn: async (): Promise<unknown> => {
      const cq = connectionId ? `?connectionId=${encodeURIComponent(connectionId)}` : ''
      const res = await api(`abacus/print/jobs${cq}`)
      if (!res.ok) throw new Error(`jobs read failed: ${res.status}`)
      return (await res.json()) as unknown
    },
    enabled,
    staleTime: 5_000,
  })
  const jobRows: JobRow[] = normalizeJobs(query.data)
  return { ...query, jobRows }
}
 
/** POST the start, throwing a {@link PrintServiceError} on refusal so the caller
 *  can render THH's honest, coded reason. Invalidates the roster on success. */
export function useStartPrintJob() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async ({ jobId, acknowledge }: StartPrintJobVars): Promise<unknown> => {
      const res = await api(`abacus/print/jobs/${encodeURIComponent(jobId)}/start`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ acknowledge }),
      })
      const body: unknown = await res.json().catch(() => null)
      if (!res.ok) throw new PrintServiceError(res.status, body)
      return body
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: abacusPrintKeys.jobs() })
    },
  })
}
 
/** POST the cancel/stop, throwing a {@link PrintServiceError} on refusal.
 *  Invalidates the roster on success. */
export function useCancelPrintJob() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async ({ jobId, stopPrint }: CancelPrintJobVars): Promise<unknown> => {
      const res = await api(`abacus/print/jobs/${encodeURIComponent(jobId)}/cancel`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ stopPrint }),
      })
      const body: unknown = await res.json().catch(() => null)
      if (!res.ok) throw new PrintServiceError(res.status, body)
      return body
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: abacusPrintKeys.jobs() })
    },
  })
}