File size: 3,821 Bytes
67d18ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
102
/**
 * Async (off-peak) types: ticket states, server response shapes, error classes.
 *
 * Server-side response fields are snake_case (e.g. `ticket_id`, `next_poll_after`).
 * The client converts them to camelCase on the way out; callers never see the
 * raw server shape.
 *
 * @see _reverse/NOTEPAD.md "Off-Peak / Idle Plan" section for upstream protocol.
 */

/** Ticket lifecycle states reported by the server. */
export type TicketState = "queued" | "ready" | "active" | "settled" | "expired" | "not_found";

/** Non-terminal states β€” ticket may still become `ready`. */
export const TICKET_PENDING_STATES: readonly TicketState[] = ["queued"] as const;
/** Terminal states β€” no further transitions; ticket can be settled. */
export const TICKET_TERMINAL_STATES: readonly TicketState[] = ["settled", "expired", "not_found"] as const;

export function isTicketReady(s: TicketState): boolean {
  return s === "ready" || s === "active";
}

export function isTicketExpired(s: TicketState): boolean {
  return s === "expired" || s === "not_found";
}

/** Result of `GET /ticket/availability`. */
export interface AvailabilityResult {
  canTakeNumber: boolean;
  /** Unix seconds β€” earliest time the user can retry taking a number. Present when `canTakeNumber === false`. */
  nextTakeAt?: number;
}

/** Result of `POST /ticket` (take a number). */
export interface TakeTicketResult {
  ticketId: string;
  state: TicketState;
  /** Queue position (1-indexed). Present while `state === "queued"`. */
  position?: number;
  /** Server-suggested next poll delay in ms. Present when server has a backoff recommendation. */
  nextPollAfterMs?: number;
  /** Local timestamp (ms) when the ticket was registered. */
  registeredAt: number;
}

/** Status of a single ticket from `POST /ticket/status`. */
export interface TicketStatusResult {
  ticketId: string;
  state: TicketState;
  position?: number;
  /** Unix seconds β€” deadline by which the client must use a `ready` ticket. Present on `ready`. */
  activeDeadline?: number;
}

/** Result of `POST /ticket/status` (batch poll). */
export interface BatchStatusResult {
  /** Server-suggested next poll delay in ms (applies to all returned tickets). */
  nextPollAfterMs?: number;
  tickets: TicketStatusResult[];
}

/** Credentials needed to authenticate with the off-peak backend. */
export interface OffPeakCredentials {
  /** ZCode plan JWT β€” goes in `Authorization: Bearer ${jwt}`. Source: `Credential.jwt`. */
  jwt: string;
  /** Coding-plan API key β€” goes in `X-Coding-Plan-Api-Key`. Source: `Credential.apiKey`. */
  codingPlanApiKey: string;
  /** Optional Bigmodel-team org/project headers. */
  bigmodelOrganization?: string;
  bigmodelProject?: string;
}

/** HTTP-status-bearing error from the off-peak server. */
export class OffPeakServerError extends Error {
  readonly httpStatus: number;
  readonly bizCode?: string;
  constructor(message: string, httpStatus: number, bizCode?: string) {
    super(message);
    this.name = "OffPeakServerError";
    this.httpStatus = httpStatus;
    this.bizCode = bizCode;
  }
}

/** Thrown when credentials lack the JWT required for off-peak auth. */
export class OffPeakCredentialsUnavailableError extends Error {
  constructor(message: string = "off-peak requires a logged-in oauth credential (jwt missing)") {
    super(message);
    this.name = "OffPeakCredentialsUnavailableError";
  }
}

/** Detects the upstream "off-peak-ticket-expired" error signal in any error message. */
export function isOffPeakTicketExpiredError(e: unknown): boolean {
  if (e == null) return false;
  if (typeof e === "string") return e.includes("off-peak-ticket-expired");
  if (e instanceof Error) {
    return e.message.includes("off-peak-ticket-expired") || e.name === "OffPeakTicketExpiredError";
  }
  return false;
}