refactor(schema): move session pagination cursors to schema

This commit is contained in:
Kit Langton 2026-07-31 15:23:21 -04:00
commit 903acdf0d4
7 changed files with 225 additions and 166 deletions

View file

@ -1,29 +1,10 @@
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Session } from "@opencode-ai/core/session"
import { Effect, Schema } from "effect"
import { SessionMessagesCursor } from "@opencode-ai/schema/session-messages-cursor"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "@opencode-ai/protocol/errors"
const DefaultMessagesLimit = 50
const Cursor = Schema.Struct({
id: SessionMessage.ID,
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
})
const decodeCursor = Schema.decodeUnknownSync(Cursor)
const cursor = {
encode(message: SessionMessage.Info, order: "asc" | "desc", direction: "previous" | "next") {
return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url")
},
decode(input: string) {
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
},
}
export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handlers) =>
Effect.gen(function* () {
const session = yield* Session.Service
@ -33,15 +14,16 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
Effect.fn(function* (ctx) {
if (ctx.query.cursor && ctx.query.order !== undefined)
return yield* new InvalidCursorError({ message: "Cursor cannot be combined with order" })
const decoded = yield* Effect.try({
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
catch: () => new InvalidCursorError({ message: "Invalid cursor" }),
})
const decoded = ctx.query.cursor
? yield* SessionMessagesCursor.parse(ctx.query.cursor).pipe(
Effect.mapError(() => new InvalidCursorError({ message: "Invalid cursor" })),
)
: undefined
const order = decoded?.order ?? ctx.query.order ?? "desc"
const messages = yield* session
.messages({
sessionID: ctx.params.sessionID,
limit: ctx.query.limit ?? DefaultMessagesLimit,
limit: ctx.query.limit ?? SessionMessagesCursor.DefaultLimit,
order,
cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined,
})
@ -66,14 +48,9 @@ export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handl
)
}),
)
const first = messages[0]
const last = messages.at(-1)
return {
data: messages,
cursor: {
previous: first ? cursor.encode(first, order, "previous") : undefined,
next: last ? cursor.encode(last, order, "next") : undefined,
},
cursor: SessionMessagesCursor.page(messages, order),
}
}),
)

View file

@ -3,7 +3,7 @@ import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry"
import { DateTime, Effect, Stream } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { SessionsCursor } from "@opencode-ai/protocol/groups/session"
import { SessionsQuery } from "@opencode-ai/schema/sessions-query"
import {
ConflictError,
CommandEvaluationError,
@ -19,8 +19,6 @@ import {
} from "@opencode-ai/protocol/errors"
import { AbsolutePath } from "@opencode-ai/core/schema"
const DefaultSessionsLimit = 50
export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
Effect.gen(function* () {
const session = yield* Session.Service
@ -31,42 +29,18 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
Effect.fn(function* (ctx) {
const query =
ctx.query.cursor !== undefined
? yield* SessionsCursor.parse(ctx.query.cursor).pipe(
? yield* SessionsQuery.Cursor.parse(ctx.query.cursor).pipe(
Effect.mapError(() => new InvalidCursorError({ message: "Invalid cursor" })),
)
: ctx.query
const page = yield* session.list({
...query,
workspaceID: query.workspace,
limit: ctx.query.limit ?? DefaultSessionsLimit,
limit: ctx.query.limit ?? SessionsQuery.DefaultLimit,
})
const sessions = page.data
const first = sessions[0]
const last = sessions.at(-1)
return {
data: sessions,
cursor: {
previous: first
? SessionsCursor.make({
...query,
anchor: {
id: first.id,
time: DateTime.toEpochMillis(first.time.updated),
direction: "previous",
},
})
: undefined,
next: last
? SessionsCursor.make({
...query,
anchor: {
id: last.id,
time: DateTime.toEpochMillis(last.time.updated),
direction: "next",
},
})
: undefined,
},
data: page.data,
cursor: SessionsQuery.Cursor.page(query, page.data),
}
}),
)