From 2743504e606df9337fffc0c7c2a6df455854aaad Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 20 May 2026 17:04:40 -0400 Subject: [PATCH] refactor(v2): simplify memory storage layer --- .../opencode/src/v2/session/storage-memory.ts | 138 +++++++++--------- .../opencode/test/v2/session-storage.test.ts | 32 ++-- 2 files changed, 84 insertions(+), 86 deletions(-) diff --git a/packages/opencode/src/v2/session/storage-memory.ts b/packages/opencode/src/v2/session/storage-memory.ts index d47d0678b1..42f4d9b72d 100644 --- a/packages/opencode/src/v2/session/storage-memory.ts +++ b/packages/opencode/src/v2/session/storage-memory.ts @@ -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 { SessionStorage } from "./storage" @@ -7,86 +7,84 @@ export interface State { readonly messages: Map } -export class StateService extends Context.Service()("@opencode/v2/session/StorageMemoryState") {} - -const stateLayer = Layer.sync(StateService, () => ({ +const makeState = (): State => ({ sessions: new Map(), messages: new Map(), -})) +}) -const storageLayer = Layer.effect( - SessionStorage.Service, - Effect.gen(function* () { - const state = yield* StateService - return SessionStorage.Service.of({ - get: (sessionID) => Effect.sync(() => state.sessions.get(sessionID)), - list: (input) => - Effect.sync(() => { - const direction = input.cursor?.direction ?? "next" - const order = SessionStorage.pageOrder(input.order ?? "desc", direction) - const rows = Array.from(state.sessions.values()) - .filter((row) => { - if (input.directory && row.directory !== input.directory) return false - if (input.path && row.path !== input.path && !row.path?.startsWith(`${input.path}/`)) return false - if (input.workspaceID && row.workspaceID !== input.workspaceID) return false - if (input.roots && row.parentID) return false - if (input.start && DateTime.toEpochMillis(row.time.updated) < input.start) return false - if (input.search && !row.title.includes(input.search)) return false - if (!input.cursor) return true - return compareCursor(row.id, DateTime.toEpochMillis(row.time.updated), input.cursor, order) - }) - .toSorted((a, b) => - compareRows( - a.id, - DateTime.toEpochMillis(a.time.updated), - 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 - }), - messages: (input) => - Effect.sync(() => { - const direction = input.cursor?.direction ?? "next" - const order = SessionStorage.pageOrder(input.order ?? "desc", direction) - const rows = (state.messages.get(input.sessionID) ?? []) - .filter((message) => { - if (!input.cursor) return true - 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) => +export const make = (state = makeState()) => + SessionStorage.Service.of({ + get: (sessionID) => Effect.sync(() => state.sessions.get(sessionID)), + list: (input) => + Effect.sync(() => { + const direction = input.cursor?.direction ?? "next" + const order = SessionStorage.pageOrder(input.order ?? "desc", direction) + const rows = Array.from(state.sessions.values()) + .filter((row) => { + if (input.directory && row.directory !== input.directory) return false + if (input.path && row.path !== input.path && !row.path?.startsWith(`${input.path}/`)) return false + if (input.workspaceID && row.workspaceID !== input.workspaceID) return false + if (input.roots && row.parentID) return false + if (input.start && DateTime.toEpochMillis(row.time.updated) < input.start) return false + if (input.search && !row.title.includes(input.search)) return false + if (!input.cursor) return true + return compareCursor(row.id, DateTime.toEpochMillis(row.time.updated), input.cursor, order) + }) + .toSorted((a, b) => + compareRows( + a.id, + DateTime.toEpochMillis(a.time.updated), + 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 + }), + messages: (input) => + Effect.sync(() => { + const direction = input.cursor?.direction ?? "next" + const order = SessionStorage.pageOrder(input.order ?? "desc", direction) + const rows = (state.messages.get(input.sessionID) ?? []) + .filter((message) => { + if (!input.cursor) return true + 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), - "asc", + order, ), ) - const index = messages.findLastIndex((message) => message.type === "compaction") - return index === -1 ? messages : messages.slice(index) - }), - }) - }), -) + 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( + 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 diff --git a/packages/opencode/test/v2/session-storage.test.ts b/packages/opencode/test/v2/session-storage.test.ts index 262f613d6a..002b926b8b 100644 --- a/packages/opencode/test/v2/session-storage.test.ts +++ b/packages/opencode/test/v2/session-storage.test.ts @@ -19,6 +19,11 @@ const sessionB = SessionID.make("ses_storage_b") const sessionC = SessionID.make("ses_storage_c") const sessionD = SessionID.make("ses_storage_d") 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 { readonly reset: Effect.Effect @@ -162,29 +167,27 @@ const sqlSeeds: Seeds = { sessionStorageContract("SessionStorageSql", SessionStorageSql.defaultLayer, sqlSeeds) -const memorySeeds: Seeds = { - reset: Effect.gen(function* () { - const memoryState = yield* SessionStorageMemory.StateService +const memorySeeds: Seeds = { + reset: Effect.sync(() => { memoryState.sessions.clear() memoryState.messages.clear() }), project: Effect.void, session: (input) => - Effect.gen(function* () { - const memoryState = yield* SessionStorageMemory.StateService + Effect.sync(() => { memoryState.sessions.set(input.id, makeSessionRow(input)) }), userMessage: (input) => - Effect.gen(function* () { - yield* appendMemoryMessage(makeUserMessage(input)) + Effect.sync(() => { + appendMemoryMessage(makeUserMessage(input)) }), compaction: (input) => - Effect.gen(function* () { - yield* appendMemoryMessage(makeCompaction(input)) + Effect.sync(() => { + appendMemoryMessage(makeCompaction(input)) }), } -sessionStorageContract("SessionStorageMemory", SessionStorageMemory.layer, memorySeeds) +sessionStorageContract("SessionStorageMemory", memoryLayer, memorySeeds) function seedProject() { Database.use((db) => @@ -324,10 +327,7 @@ function seedMessage( } function appendMemoryMessage(message: SessionMessage.Message) { - return Effect.gen(function* () { - const memoryState = yield* SessionStorageMemory.StateService - const current = memoryState.messages.get(sessionA) ?? [] - current.push(message) - memoryState.messages.set(sessionA, current) - }) + const current = memoryState.messages.get(sessionA) ?? [] + current.push(message) + memoryState.messages.set(sessionA, current) }