mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-24 22:14:37 +00:00
feat(opencode): pilot effect sqlite database service
This commit is contained in:
parent
f6f6cd0515
commit
4faa6c64d6
10 changed files with 78 additions and 50 deletions
1
bun.lock
1
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:*",
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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<State>(
|
||||
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<PermissionID, PendingEntry>(),
|
||||
approved: row?.data ?? [],
|
||||
approved: rows[0]?.data ?? [],
|
||||
}
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
|
|
@ -319,6 +322,6 @@ export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
|
|||
return result
|
||||
}
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
||||
export const defaultLayer: Layer.Layer<Service> = layer.pipe(Layer.provide(Bus.layer), Layer.provide(DatabaseEffect.layer))
|
||||
|
||||
export * as Permission from "."
|
||||
|
|
|
|||
|
|
@ -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<Service> = layer.pipe(Layer.provide(Bus.layer), Layer.provide(DatabaseEffect.layer))
|
||||
|
||||
export * as Todo from "./todo"
|
||||
|
|
|
|||
|
|
@ -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<Service, Interface>()("@opencode/ShareNext") {}
|
||||
|
||||
const db = <T>(fn: (d: Parameters<typeof Database.use>[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<void> {
|
||||
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<Service> = 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"
|
||||
|
|
|
|||
12
packages/opencode/src/storage/db-effect.ts
Normal file
12
packages/opencode/src/storage/db-effect.ts
Normal file
|
|
@ -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<Service, EffectSQLiteDatabase<typeof schema>>()("@opencode/DatabaseEffect") {}
|
||||
|
||||
export const layer = Layer.sync(Service, () => drizzle({ client: Database.Client().$client, schema }))
|
||||
|
||||
export * as DatabaseEffect from "./db-effect"
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue