"use client"; import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ApiError } from "@/lib/auth/api-client"; import { useCurrentUserId } from "@/lib/auth/auth-context"; import { errorMessage } from "@/lib/forms/error-message"; import { CreateOptions, newIdempotencyKey } from "./idempotency"; /** Prefix of every query that reads the offline queue (band, queue sheet). */ export const OFFLINE_QUEUE_QUERY_KEY = ["offline-queue"] as const; export type CaptureOutcome = | { via: "online"; result: R } | { via: "queued"; idempotencyKey: string }; export interface CaptureMutationOptions { create: (input: I, options: CreateOptions) => Promise; queue: (input: I, idempotencyKey: string, ownerUserId: string) => Promise; /** Shown when nothing could be saved and the server gave no reason. */ failureMessage: string; onSaved: (outcome: CaptureOutcome, input: I) => void | Promise; } export interface CaptureMutation { submit: (input: I) => void; isPending: boolean; submitError: string | null; idempotencyKey: string; } /** * Saves a quick-capture form: online first; without a network the entry goes * into the offline queue under the signed-in user. A server rejection * (ApiError) is shown at the form and never queued. */ export function useCaptureMutation({ create, queue, failureMessage, onSaved, }: CaptureMutationOptions): CaptureMutation { const ownerUserId = useCurrentUserId(); const queryClient = useQueryClient(); // One key per opened form: a double tap or an offline retry of the // same entry is recognised by the server instead of saved twice. const [idempotencyKey] = useState(newIdempotencyKey); const [submitError, setSubmitError] = useState(null); const mutation = useMutation({ // Runs while the browser reports offline too: the request fails fast and // the entry is queued, instead of the form waiting for a network. networkMode: "always", retry: false, mutationFn: async (input: I): Promise> => { try { return { via: "online", result: await create(input, { idempotencyKey }) }; } catch (error) { // Never queue without an owner: such an entry would be sent under // whoever signs in next on this device. if (error instanceof ApiError || !ownerUserId) { throw error; } await queue(input, idempotencyKey, ownerUserId); void queryClient.invalidateQueries({ queryKey: OFFLINE_QUEUE_QUERY_KEY }); return { via: "queued", idempotencyKey }; } }, onSuccess: (outcome, input) => onSaved(outcome, input), onError: (error) => setSubmitError(errorMessage(error, failureMessage)), }); function submit(input: I): void { setSubmitError(null); mutation.mutate(input); } return { submit, isPending: mutation.isPending, submitError, idempotencyKey }; }