diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 18:37:09 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 18:37:09 +0900 |
| commit | dbf77ba78ff840e0ed5f6294030523fe3ab121fa (patch) | |
| tree | e768aef3edd0c126212058c3d1433355594c49be /packages/transport-http/src/app.ts | |
| parent | 6689eb51b467d8e370f31495840d88661f978168 (diff) | |
| download | dispatch-dbf77ba78ff840e0ed5f6294030523fe3ab121fa.tar.gz dispatch-dbf77ba78ff840e0ed5f6294030523fe3ab121fa.zip | |
feat(history): CR-5 windowed reads — ?limit= / ?beforeSeq= on GET /conversations/:id
Selection sinceSeq < seq < beforeSeq; newest-limit window, ascending; positive-
integer validation (400, store never sees an invalid window); 1-based gap-free
seq codified as the contractual has-older mechanism (no earliestSeq field).
transport-contract 0.9.0->0.10.0, wire 0.6.0->0.6.1 (doc-only).
conversation-store +8 tests, transport-http +20; 935 vitest + 112 bun green.
Live-verified: 6/6 probe checks OK. FE courier: frontend-history-windowing-handoff.md
Diffstat (limited to 'packages/transport-http/src/app.ts')
| -rw-r--r-- | packages/transport-http/src/app.ts | 39 |
1 files changed, 38 insertions, 1 deletions
diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts index 11d2850..ae24922 100644 --- a/packages/transport-http/src/app.ts +++ b/packages/transport-http/src/app.ts @@ -17,9 +17,11 @@ import { computeExpectedCacheRate, isParseError, isSinceSeqError, + isWindowParamError, parseChatBody, parseSinceSeq, parseWarmBody, + parseWindowParam, serializeEventLine, } from "./logic.js"; import { @@ -147,8 +149,43 @@ export function createApp(opts: CreateServerOptions): Hono { return c.json({ error: sinceSeqResult.error }, 400); } + // `limit` / `beforeSeq` are optional positive-integer history-window + // params. The store is deliberately forgiving (a 0/negative bound is + // treated as ABSENT), so we MUST reject malformed values here and never + // forward an invalid window. + const beforeSeqResult = parseWindowParam(c.req.query("beforeSeq"), "beforeSeq"); + if (isWindowParamError(beforeSeqResult)) { + log.warn("conversations: invalid beforeSeq", { + conversationId, + error: beforeSeqResult.error, + }); + return c.json({ error: beforeSeqResult.error }, 400); + } + const limitResult = parseWindowParam(c.req.query("limit"), "limit"); + if (isWindowParamError(limitResult)) { + log.warn("conversations: invalid limit", { + conversationId, + error: limitResult.error, + }); + return c.json({ error: limitResult.error }, 400); + } + + // Include only the fields actually provided (exactOptionalPropertyTypes), + // and omit the window argument entirely when neither was given — keeping + // the pre-windowing call shape byte-identical for existing callers. + const window: { readonly beforeSeq?: number; readonly limit?: number } | undefined = + beforeSeqResult !== undefined || limitResult !== undefined + ? { + ...(beforeSeqResult !== undefined ? { beforeSeq: beforeSeqResult } : {}), + ...(limitResult !== undefined ? { limit: limitResult } : {}), + } + : undefined; + try { - const chunks = await opts.conversationStore.loadSince(conversationId, sinceSeqResult); + const chunks = + window !== undefined + ? await opts.conversationStore.loadSince(conversationId, sinceSeqResult, window) + : await opts.conversationStore.loadSince(conversationId, sinceSeqResult); const latestSeq = chunks.length > 0 ? (chunks[chunks.length - 1]?.seq ?? sinceSeqResult) : sinceSeqResult; log.info("conversations: read", { |
