diff --git a/bun.lock b/bun.lock index 47c22ee220..2b2905ec7c 100644 --- a/bun.lock +++ b/bun.lock @@ -409,6 +409,7 @@ "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", + "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8c5aa34998..995b7db52c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -110,6 +110,7 @@ "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", + "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index d93670709e..d0baa9b042 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -5,7 +5,7 @@ import { InstanceState } from "@/effect/instance-state" import { ProjectID } from "@/project/schema" import { MessageID, SessionID } from "@/session/schema" import { PermissionTable } from "@/session/session.sql" -import { Database } from "@/storage/db" +import { DatabaseEffect } from "@/storage/db-effect" import { eq } from "drizzle-orm" import { zod } from "@/util/effect-zod" import * as Log from "@opencode-ai/core/util/log" @@ -153,14 +153,17 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const bus = yield* Bus.Service + const db = yield* DatabaseEffect.Service const state = yield* InstanceState.make( Effect.fn("Permission.state")(function* (ctx) { - const row = Database.use((db) => - db.select().from(PermissionTable).where(eq(PermissionTable.project_id, ctx.project.id)).get(), - ) + const rows = yield* db + .select() + .from(PermissionTable) + .where(eq(PermissionTable.project_id, ctx.project.id)) + .pipe(Effect.orDie) const state = { pending: new Map(), - approved: row?.data ?? [], + approved: rows[0]?.data ?? [], } yield* Effect.addFinalizer(() => @@ -319,6 +322,6 @@ export function disabled(tools: string[], ruleset: Ruleset): Set { return result } -export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) +export const defaultLayer: Layer.Layer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(DatabaseEffect.layer)) export * as Permission from "." diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts index 32a8370464..d828bd9dbe 100644 --- a/packages/opencode/src/session/todo.ts +++ b/packages/opencode/src/session/todo.ts @@ -5,7 +5,7 @@ import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" import { Effect, Layer, Context, Schema } from "effect" import z from "zod" -import { Database } from "@/storage/db" +import { DatabaseEffect } from "@/storage/db-effect" import { eq } from "drizzle-orm" import { asc } from "drizzle-orm" import { TodoTable } from "./session.sql" @@ -42,34 +42,34 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const bus = yield* Bus.Service + const db = yield* DatabaseEffect.Service const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: Info[] }) { - yield* Effect.sync(() => - Database.transaction((db) => { - db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run() - if (input.todos.length === 0) return - db.insert(TodoTable) - .values( - input.todos.map((todo, position) => ({ - session_id: input.sessionID, - content: todo.content, - status: todo.status, - priority: todo.priority, - position, - })), - ) - .run() - }), - ) + yield* Effect.gen(function* () { + yield* db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)) + if (input.todos.length === 0) return + yield* db.insert(TodoTable).values( + input.todos.map((todo, position) => ({ + session_id: input.sessionID, + content: todo.content, + status: todo.status, + priority: todo.priority, + position, + })), + ) + }).pipe(db.withTransaction, Effect.orDie) + yield* bus.publish(Event.Updated, input) }) const get = Effect.fn("Todo.get")(function* (sessionID: SessionID) { - const rows = yield* Effect.sync(() => - Database.use((db) => - db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(), - ), - ) + const rows = yield* db + .select() + .from(TodoTable) + .where(eq(TodoTable.session_id, sessionID)) + .orderBy(asc(TodoTable.position)) + .pipe(Effect.orDie) + return rows.map((row) => ({ content: row.content, status: row.status, @@ -81,6 +81,6 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) +export const defaultLayer: Layer.Layer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(DatabaseEffect.layer)) export * as Todo from "./todo" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 384027436f..754dd817db 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -9,7 +9,7 @@ import { ModelID, ProviderID } from "@/provider/schema" import { Session } from "@/session/session" import { MessageV2 } from "@/session/message-v2" import type { SessionID } from "@/session/schema" -import { Database } from "@/storage/db" +import { DatabaseEffect } from "@/storage/db-effect" import { eq } from "drizzle-orm" import { Config } from "@/config/config" import * as Log from "@opencode-ai/core/util/log" @@ -76,9 +76,6 @@ export interface Interface { export class Service extends Context.Service()("@opencode/ShareNext") {} -const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => - Effect.sync(() => Database.use(fn)) - function api(resource: string): Api { return { create: `/api/${resource}`, @@ -116,6 +113,7 @@ export const layer = Layer.effect( const httpOk = HttpClient.filterStatusOk(http) const provider = yield* Provider.Service const session = yield* Session.Service + const db = yield* DatabaseEffect.Service function sync(sessionID: SessionID, data: Data[]): Effect.Effect { return Effect.gen(function* () { @@ -226,9 +224,12 @@ export const layer = Layer.effect( }) const get = Effect.fnUntraced(function* (sessionID: SessionID) { - const row = yield* db((db) => - db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(), - ) + const rows = yield* db + .select() + .from(SessionShareTable) + .where(eq(SessionShareTable.session_id, sessionID)) + .pipe(Effect.orDie) + const row = rows[0] if (!row) return return { id: row.id, secret: row.secret, url: row.url } satisfies Share }) @@ -314,16 +315,13 @@ export const layer = Layer.effect( Effect.flatMap((r) => httpOk.execute(r)), Effect.flatMap(HttpClientResponse.schemaBodyJson(ShareSchema)), ) - yield* db((db) => - db - .insert(SessionShareTable) - .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url }) - .onConflictDoUpdate({ - target: SessionShareTable.session_id, - set: { id: result.id, secret: result.secret, url: result.url }, - }) - .run(), - ) + yield* db + .insert(SessionShareTable) + .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url }) + .onConflictDoUpdate({ + target: SessionShareTable.session_id, + set: { id: result.id, secret: result.secret, url: result.url }, + }) const s = yield* InstanceState.get(state) s.shared.set(sessionID, result) yield* full(sessionID).pipe( @@ -355,7 +353,7 @@ export const layer = Layer.effect( Effect.flatMap((r) => httpOk.execute(r)), ) - yield* db((db) => db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run()) + yield* db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)) s.shared.delete(sessionID) s.queue.delete(sessionID) }) @@ -364,13 +362,14 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe( +export const defaultLayer: Layer.Layer = layer.pipe( Layer.provide(Bus.layer), Layer.provide(Account.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(FetchHttpClient.layer), Layer.provide(Provider.defaultLayer), Layer.provide(Session.defaultLayer), + Layer.provide(DatabaseEffect.layer), ) export * as ShareNext from "./share-next" diff --git a/packages/opencode/src/storage/db-effect.ts b/packages/opencode/src/storage/db-effect.ts new file mode 100644 index 0000000000..8cbf889279 --- /dev/null +++ b/packages/opencode/src/storage/db-effect.ts @@ -0,0 +1,12 @@ +import { Database } from "@/storage/db" +import * as StorageSchema from "@/storage/schema" +import { Context, Layer } from "effect" +import { drizzle, type EffectSQLiteDatabase } from "@opencode-ai/effect-drizzle-sqlite" + +const schema = { ...StorageSchema } + +export class Service extends Context.Service>()("@opencode/DatabaseEffect") {} + +export const layer = Layer.sync(Service, () => drizzle({ client: Database.Client().$client, schema })) + +export * as DatabaseEffect from "./db-effect" diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 1c3d6fc563..7a268f1473 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -8,6 +8,7 @@ import { PermissionID } from "../../src/permission/schema" import { Instance } from "../../src/project/instance" import { WithInstance } from "../../src/project/with-instance" import { InstanceRuntime } from "../../src/project/instance-runtime" +import { DatabaseEffect } from "../../src/storage/db-effect" import { disposeAllInstances, provideInstance, @@ -19,7 +20,11 @@ import { testEffect } from "../lib/effect" import { MessageID, SessionID } from "../../src/session/schema" const bus = Bus.layer -const env = Layer.mergeAll(Permission.layer.pipe(Layer.provide(bus)), bus, CrossSpawnSpawner.defaultLayer) +const env = Layer.mergeAll( + Permission.layer.pipe(Layer.provide(bus), Layer.provide(DatabaseEffect.layer)), + bus, + CrossSpawnSpawner.defaultLayer, +) const it = testEffect(env) afterEach(async () => { diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index a602c0c8d7..d6425e0811 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -39,6 +39,7 @@ import { Shell } from "../../src/shell/shell" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" +import { DatabaseEffect } from "@/storage/db-effect" import * as Log from "@opencode-ai/core/util/log" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import * as Database from "../../src/storage/db" @@ -170,6 +171,7 @@ function makeHttp() { lsp, mcp, AppFileSystem.defaultLayer, + DatabaseEffect.layer, status, ).pipe(Layer.provideMerge(infra)) const question = Question.layer.pipe(Layer.provideMerge(deps)) diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index ab5a3ab7ed..2b83c62517 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -51,6 +51,7 @@ import { SessionStatus } from "../../src/session/status" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "@/tool/registry" import { Truncate } from "@/tool/truncate" +import { DatabaseEffect } from "@/storage/db-effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" @@ -120,6 +121,7 @@ function makeHttp() { lsp, mcp, AppFileSystem.defaultLayer, + DatabaseEffect.layer, status, ).pipe(Layer.provideMerge(infra)) const question = Question.layer.pipe(Layer.provideMerge(deps)) diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 14ecff7452..e630de6a61 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -15,6 +15,7 @@ import type { SessionID } from "../../src/session/schema" import { ShareNext } from "@/share/share-next" import { SessionShareTable } from "../../src/share/share.sql" import { Database } from "@/storage/db" +import { DatabaseEffect } from "@/storage/db-effect" import { eq } from "drizzle-orm" import { provideTmpdirInstance } from "../fixture/fixture" import { resetDatabase } from "../fixture/db" @@ -48,6 +49,7 @@ function live(client: HttpClient.HttpClient) { Layer.provide(http), Layer.provide(Provider.defaultLayer), Layer.provide(Session.defaultLayer), + Layer.provide(DatabaseEffect.layer), ) } @@ -66,6 +68,7 @@ function wired(client: HttpClient.HttpClient) { Layer.provide(Config.defaultLayer), Layer.provide(http), Layer.provide(Provider.defaultLayer), + Layer.provide(DatabaseEffect.layer), ) }