feat(server): add GET /v1/watch long-poll endpoint with abort + rename handling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,53 @@ export async function buildServer(cfg: DaemonConfig, store: MailboxStore): Promi
|
||||
}
|
||||
});
|
||||
|
||||
const WATCH_DEFAULT_TIMEOUT_S = 25;
|
||||
const WATCH_MAX_TIMEOUT_S = 300;
|
||||
|
||||
app.get<{ Querystring: { name?: string; timeout?: string } }>(
|
||||
"/v1/watch",
|
||||
async (req, reply) => {
|
||||
const name = (req.query.name ?? "").trim();
|
||||
if (!name) {
|
||||
reply.code(400);
|
||||
return { error: "name is required" };
|
||||
}
|
||||
if (name !== req.mailboxName) {
|
||||
reply.code(403);
|
||||
return { error: "X-Mailbox header must match name." };
|
||||
}
|
||||
|
||||
const rawTimeout = req.query.timeout;
|
||||
const timeoutS = rawTimeout != null ? parseInt(rawTimeout, 10) : WATCH_DEFAULT_TIMEOUT_S;
|
||||
if (!Number.isFinite(timeoutS) || timeoutS <= 0 || timeoutS > WATCH_MAX_TIMEOUT_S) {
|
||||
reply.code(400);
|
||||
return { error: `timeout must be 1..${WATCH_MAX_TIMEOUT_S} seconds` };
|
||||
}
|
||||
|
||||
const ac = new AbortController();
|
||||
req.raw.on("close", () => ac.abort());
|
||||
|
||||
const result = await store.waitForMessage(name, timeoutS * 1000, ac.signal);
|
||||
|
||||
if (result.kind === "message") {
|
||||
const msg = rowToMessage(result.message);
|
||||
reply.code(200);
|
||||
return { ...msg, sentAt: msg.sentAt.toISOString() };
|
||||
}
|
||||
if (result.kind === "renamed") {
|
||||
reply.code(409);
|
||||
return { reason: "renamed", to: result.to };
|
||||
}
|
||||
if (result.kind === "timeout") {
|
||||
reply.code(204);
|
||||
return reply.send();
|
||||
}
|
||||
// aborted — client gone, no response needed (Fastify will swallow).
|
||||
reply.code(499);
|
||||
return reply.send();
|
||||
},
|
||||
);
|
||||
|
||||
await registerMcp(app, store, cfg.hideAfterMinutes);
|
||||
|
||||
return app;
|
||||
|
||||
Reference in New Issue
Block a user