feat(@messenger): add status constants for send-queue tools

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-21 17:21:30 -07:00
parent 0e0259010c
commit d2df7331e3
4 changed files with 43 additions and 14 deletions

View file

@ -0,0 +1,25 @@
/**
* Lifecycle states for a `macsync.send_queue` row.
*
* Single source of truth for status strings shared across the send-queue
* tools (send / cancel / check-status). These MUST match the mac-sync server
* contract (`SEND_QUEUE_CONTRACTS.md` in the mac-sync server repo):
*
* - a freshly enqueued row is `'queued'` the drain endpoint fetches
* `WHERE status = 'queued'`. It is NOT `'pending'`.
* - the device reports results back as `'sent'` or `'failed'`.
* - `cancel` moves a still-`'queued'` row to `'cancelled'`.
*
* Referencing these constants instead of bare string literals makes a typo
* (`SEND_QUEUE_STATUS.PENDING`) a compile error, and lets the enqueue value
* flow through a typed query parameter rather than being inlined into SQL.
*/
export const SEND_QUEUE_STATUS = {
QUEUED: 'queued',
SENT: 'sent',
FAILED: 'failed',
CANCELLED: 'cancelled',
} as const;
export type SendQueueStatus =
(typeof SEND_QUEUE_STATUS)[keyof typeof SEND_QUEUE_STATUS];

View file

@ -1,10 +1,11 @@
import { query } from '../db';
import { SEND_QUEUE_STATUS, type SendQueueStatus } from '../send-queue-status';
interface CancelMessageParams {
pending_message_id: string;
}
// Schema: macsync.send_queue. Only rows still in 'pending' state can be
// Schema: macsync.send_queue. Only rows still in 'queued' state can be
// cancelled; anything 'sent' or already 'cancelled' is reported back as-is.
export async function cancelMessage(params: CancelMessageParams): Promise<string> {
const { pending_message_id } = params;
@ -31,22 +32,22 @@ export async function cancelMessage(params: CancelMessageParams): Promise<string
}
const row = lookup.rows[0]!;
const status = row.status as string;
const status = row.status as SendQueueStatus;
if (status === 'cancelled') {
if (status === SEND_QUEUE_STATUS.CANCELLED) {
return `Message ${pending_message_id} is already cancelled.`;
}
if (status !== 'pending') {
return `Cannot cancel: message has status "${status}". Only pending messages can be cancelled.`;
if (status !== SEND_QUEUE_STATUS.QUEUED) {
return `Cannot cancel: message has status "${status}". Only queued messages can be cancelled.`;
}
try {
await query(
`UPDATE macsync.send_queue
SET status = 'cancelled', updated_at = NOW()
WHERE id = $1::uuid AND status = 'pending'`,
[pending_message_id],
SET status = $2, updated_at = NOW()
WHERE id = $1::uuid AND status = $3`,
[pending_message_id, SEND_QUEUE_STATUS.CANCELLED, SEND_QUEUE_STATUS.QUEUED],
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);

View file

@ -1,10 +1,11 @@
import { query } from '../db';
import { SEND_QUEUE_STATUS, type SendQueueStatus } from '../send-queue-status';
interface CheckSendStatusParams {
pending_message_id: string;
}
// Schema: macsync.send_queue. Status values observed in the wild: 'pending',
// Schema: macsync.send_queue. Status values observed in the wild: 'queued',
// 'sent', 'cancelled'. `failure_reason` is a text column despite the schema
// note saying timestamptz — we render it as a string when present.
export async function checkSendStatus(params: CheckSendStatusParams): Promise<string> {
@ -44,7 +45,7 @@ export async function checkSendStatus(params: CheckSendStatusParams): Promise<st
}
const row = result.rows[0]!;
const status = row.status as string;
const status = row.status as SendQueueStatus;
const contactName = row.contact_name as string | null;
const toHandle = row.to_handle as string;
const recipient = contactName ? `${contactName} (${toHandle})` : toHandle;
@ -75,12 +76,12 @@ export async function checkSendStatus(params: CheckSendStatusParams): Promise<st
lines.push(`Failure reason: ${row.failure_reason}`);
}
if (status === 'pending') {
if (status === SEND_QUEUE_STATUS.QUEUED) {
lines.push('');
lines.push(`Use cancel_message with id "${row.id}" to cancel before it sends.`);
}
if (status === 'cancelled') {
if (status === SEND_QUEUE_STATUS.CANCELLED) {
lines.push('This message was cancelled and will not be sent.');
}

View file

@ -1,5 +1,6 @@
import { query } from '../db';
import { encode as encodeSourceMarker } from '../source-marker';
import { SEND_QUEUE_STATUS, type SendQueueStatus } from '../send-queue-status';
interface SendMessageParams {
recipient?: string;
@ -127,11 +128,12 @@ export async function sendMessage(params: SendMessageParams): Promise<string> {
let insertResult;
try {
const enqueueStatus: SendQueueStatus = SEND_QUEUE_STATUS.QUEUED;
insertResult = await query(
`INSERT INTO macsync.send_queue (device_id, to_handle, body, status)
VALUES ($1::uuid, $2, $3, 'pending')
VALUES ($1::uuid, $2, $3, $4)
RETURNING id, created_at`,
[deviceId, recipient, markedBody],
[deviceId, recipient, markedBody, enqueueStatus],
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);