refactor(v2): simplify memory storage layer

This commit is contained in:
Kit Langton 2026-05-20 17:04:40 -04:00
commit 2743504e60
2 changed files with 84 additions and 86 deletions

View file

@ -1,4 +1,4 @@
import { Context, DateTime, Effect, Layer } from "effect" import { DateTime, Effect, Layer } from "effect"
import { SessionMessage } from "@opencode-ai/core/session-message" import { SessionMessage } from "@opencode-ai/core/session-message"
import { SessionStorage } from "./storage" import { SessionStorage } from "./storage"
@ -7,86 +7,84 @@ export interface State {
readonly messages: Map<string, SessionMessage.Message[]> readonly messages: Map<string, SessionMessage.Message[]>
} }
export class StateService extends Context.Service<StateService, State>()("@opencode/v2/session/StorageMemoryState") {} const makeState = (): State => ({
const stateLayer = Layer.sync(StateService, () => ({
sessions: new Map(), sessions: new Map(),
messages: new Map(), messages: new Map(),
})) })
const storageLayer = Layer.effect( export const make = (state = makeState()) =>
SessionStorage.Service, SessionStorage.Service.of({
Effect.gen(function* () { get: (sessionID) => Effect.sync(() => state.sessions.get(sessionID)),
const state = yield* StateService list: (input) =>
return SessionStorage.Service.of({ Effect.sync(() => {
get: (sessionID) => Effect.sync(() => state.sessions.get(sessionID)), const direction = input.cursor?.direction ?? "next"
list: (input) => const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
Effect.sync(() => { const rows = Array.from(state.sessions.values())
const direction = input.cursor?.direction ?? "next" .filter((row) => {
const order = SessionStorage.pageOrder(input.order ?? "desc", direction) if (input.directory && row.directory !== input.directory) return false
const rows = Array.from(state.sessions.values()) if (input.path && row.path !== input.path && !row.path?.startsWith(`${input.path}/`)) return false
.filter((row) => { if (input.workspaceID && row.workspaceID !== input.workspaceID) return false
if (input.directory && row.directory !== input.directory) return false if (input.roots && row.parentID) return false
if (input.path && row.path !== input.path && !row.path?.startsWith(`${input.path}/`)) return false if (input.start && DateTime.toEpochMillis(row.time.updated) < input.start) return false
if (input.workspaceID && row.workspaceID !== input.workspaceID) return false if (input.search && !row.title.includes(input.search)) return false
if (input.roots && row.parentID) return false if (!input.cursor) return true
if (input.start && DateTime.toEpochMillis(row.time.updated) < input.start) return false return compareCursor(row.id, DateTime.toEpochMillis(row.time.updated), input.cursor, order)
if (input.search && !row.title.includes(input.search)) return false })
if (!input.cursor) return true .toSorted((a, b) =>
return compareCursor(row.id, DateTime.toEpochMillis(row.time.updated), input.cursor, order) compareRows(
}) a.id,
.toSorted((a, b) => DateTime.toEpochMillis(a.time.updated),
compareRows( b.id,
a.id, DateTime.toEpochMillis(b.time.updated),
DateTime.toEpochMillis(a.time.updated), order,
b.id, ),
DateTime.toEpochMillis(b.time.updated), )
order, const limited = input.limit === undefined ? rows : rows.slice(0, input.limit)
), return direction === "previous" ? limited.toReversed() : limited
) }),
const limited = input.limit === undefined ? rows : rows.slice(0, input.limit) messages: (input) =>
return direction === "previous" ? limited.toReversed() : limited Effect.sync(() => {
}), const direction = input.cursor?.direction ?? "next"
messages: (input) => const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
Effect.sync(() => { const rows = (state.messages.get(input.sessionID) ?? [])
const direction = input.cursor?.direction ?? "next" .filter((message) => {
const order = SessionStorage.pageOrder(input.order ?? "desc", direction) if (!input.cursor) return true
const rows = (state.messages.get(input.sessionID) ?? []) return compareCursor(message.id, DateTime.toEpochMillis(message.time.created), input.cursor, order)
.filter((message) => { })
if (!input.cursor) return true .toSorted((a, b) =>
return compareCursor(message.id, DateTime.toEpochMillis(message.time.created), input.cursor, order)
})
.toSorted((a, b) =>
compareRows(
a.id,
DateTime.toEpochMillis(a.time.created),
b.id,
DateTime.toEpochMillis(b.time.created),
order,
),
)
const limited = input.limit === undefined ? rows : rows.slice(0, input.limit)
return direction === "previous" ? limited.toReversed() : limited
}),
context: (sessionID) =>
Effect.sync(() => {
const messages = (state.messages.get(sessionID) ?? []).toSorted((a, b) =>
compareRows( compareRows(
a.id, a.id,
DateTime.toEpochMillis(a.time.created), DateTime.toEpochMillis(a.time.created),
b.id, b.id,
DateTime.toEpochMillis(b.time.created), DateTime.toEpochMillis(b.time.created),
"asc", order,
), ),
) )
const index = messages.findLastIndex((message) => message.type === "compaction") const limited = input.limit === undefined ? rows : rows.slice(0, input.limit)
return index === -1 ? messages : messages.slice(index) return direction === "previous" ? limited.toReversed() : limited
}), }),
}) context: (sessionID) =>
}), Effect.sync(() => {
) const messages = (state.messages.get(sessionID) ?? []).toSorted((a, b) =>
compareRows(
a.id,
DateTime.toEpochMillis(a.time.created),
b.id,
DateTime.toEpochMillis(b.time.created),
"asc",
),
)
const index = messages.findLastIndex((message) => message.type === "compaction")
return index === -1 ? messages : messages.slice(index)
}),
})
export const layer = storageLayer.pipe(Layer.provideMerge(stateLayer)) const storageLayer = Layer.sync(SessionStorage.Service, () => {
const state = makeState()
return make(state)
})
export const layer = storageLayer
export const defaultLayer = layer export const defaultLayer = layer

View file

@ -19,6 +19,11 @@ const sessionB = SessionID.make("ses_storage_b")
const sessionC = SessionID.make("ses_storage_c") const sessionC = SessionID.make("ses_storage_c")
const sessionD = SessionID.make("ses_storage_d") const sessionD = SessionID.make("ses_storage_d")
const encodeMessage = Schema.encodeSync(SessionMessage.Message) const encodeMessage = Schema.encodeSync(SessionMessage.Message)
const memoryState: SessionStorageMemory.State = {
sessions: new Map(),
messages: new Map(),
}
const memoryLayer = Layer.sync(SessionStorage.Service, () => SessionStorageMemory.make(memoryState))
interface Seeds<R> { interface Seeds<R> {
readonly reset: Effect.Effect<void, never, R> readonly reset: Effect.Effect<void, never, R>
@ -162,29 +167,27 @@ const sqlSeeds: Seeds<never> = {
sessionStorageContract("SessionStorageSql", SessionStorageSql.defaultLayer, sqlSeeds) sessionStorageContract("SessionStorageSql", SessionStorageSql.defaultLayer, sqlSeeds)
const memorySeeds: Seeds<SessionStorageMemory.StateService> = { const memorySeeds: Seeds<never> = {
reset: Effect.gen(function* () { reset: Effect.sync(() => {
const memoryState = yield* SessionStorageMemory.StateService
memoryState.sessions.clear() memoryState.sessions.clear()
memoryState.messages.clear() memoryState.messages.clear()
}), }),
project: Effect.void, project: Effect.void,
session: (input) => session: (input) =>
Effect.gen(function* () { Effect.sync(() => {
const memoryState = yield* SessionStorageMemory.StateService
memoryState.sessions.set(input.id, makeSessionRow(input)) memoryState.sessions.set(input.id, makeSessionRow(input))
}), }),
userMessage: (input) => userMessage: (input) =>
Effect.gen(function* () { Effect.sync(() => {
yield* appendMemoryMessage(makeUserMessage(input)) appendMemoryMessage(makeUserMessage(input))
}), }),
compaction: (input) => compaction: (input) =>
Effect.gen(function* () { Effect.sync(() => {
yield* appendMemoryMessage(makeCompaction(input)) appendMemoryMessage(makeCompaction(input))
}), }),
} }
sessionStorageContract("SessionStorageMemory", SessionStorageMemory.layer, memorySeeds) sessionStorageContract("SessionStorageMemory", memoryLayer, memorySeeds)
function seedProject() { function seedProject() {
Database.use((db) => Database.use((db) =>
@ -324,10 +327,7 @@ function seedMessage(
} }
function appendMemoryMessage(message: SessionMessage.Message) { function appendMemoryMessage(message: SessionMessage.Message) {
return Effect.gen(function* () { const current = memoryState.messages.get(sessionA) ?? []
const memoryState = yield* SessionStorageMemory.StateService current.push(message)
const current = memoryState.messages.get(sessionA) ?? [] memoryState.messages.set(sessionA, current)
current.push(message)
memoryState.messages.set(sessionA, current)
})
} }