All files / src/features/sorting sessionStatusPatch.ts

100% Statements 12/12
100% Branches 8/8
100% Functions 4/4
100% Lines 12/12

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 516x                                       6x         7x 1x   7x           14x 7x 7x 3x 3x     1x     13x            
"use client";
 
// The shared session status-PATCH client helper (spec §3.2; plan Task 16).
// Every session-state transition that can strand queued decisions routes its
// fetch through here so the flush rule lives in ONE place:
//
// - paused    → best-effort, NON-BLOCKING pre-flush (F8): wait at most
//               PAUSE_PREFLUSH_BOUND_MS, then PATCH regardless — stepping away
//               is never gated on the network (the queue persists and
//               PendingDecisionsGate recovers it on return).
// - terminal  → strict flush-then-complete (spec §3.2 / parent ARCH-2): the
//               drain is fully awaited and a taxonomy-stopped drain (entries
//               left behind) FAILS the transition — a terminal session would
//               409 those entries and recovery would discard them.
//               (No production caller PATCHes a terminal status today; Epic 5's
//               Decide surface becomes the first — it inherits the rule.)
// - active    → plain PATCH, no flush (resuming changes nothing to land).
import { drain, triggerFlush } from "./decisionFlusher";
import { readQueue, type QueueKey } from "./decisionQueue";
 
export const PAUSE_PREFLUSH_BOUND_MS = 2_000;
 
export type SessionStatus = "active" | "paused" | "completed" | "abandoned";
 
async function preflushBounded(key: QueueKey): Promise<void> {
  await Promise.race([
    triggerFlush(key).catch(() => undefined), // a stumbling flush never blocks the person
    new Promise<void>((resolve) => {
      setTimeout(resolve, PAUSE_PREFLUSH_BOUND_MS);
    }),
  ]);
}
 
export async function patchSessionStatus(key: QueueKey, status: SessionStatus): Promise<Response> {
  if (status === "paused") {
    await preflushBounded(key);
  } else if (status === "completed" || status === "abandoned") {
    await drain([key]);
    if (readQueue(key).length > 0) {
      // The drain stopped per the taxonomy (401/403/409/…) with entries still
      // queued — completing now would strand them behind a terminal session.
      throw new Error("Some answers are still on their way — this can finish once they land.");
    }
  }
  return fetch(`/api/sessions/${key.sessionId}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ status }),
  });
}