feat: unwrap storage namespaces to flat exports + barrel (#22747)

This commit is contained in:
Kit Langton
2026-04-15 23:30:49 -04:00
committed by GitHub
parent 1ca257e356
commit f24207844f
40 changed files with 874 additions and 854 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { Effect, Layer, Option, Schema, Context } from "effect" import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage"
import { AccountStateTable, AccountTable } from "./account.sql" import { AccountStateTable, AccountTable } from "./account.sql"
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema" import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
import { normalizeServerUrl } from "./url" import { normalizeServerUrl } from "./url"
+2 -2
View File
@@ -1,11 +1,11 @@
import type { Argv } from "yargs" import type { Argv } from "yargs"
import { spawn } from "child_process" import { spawn } from "child_process"
import { Database } from "../../storage/db" import { Database } from "../../storage"
import { drizzle } from "drizzle-orm/bun-sqlite" import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database as BunDatabase } from "bun:sqlite" import { Database as BunDatabase } from "bun:sqlite"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { JsonMigration } from "../../storage/json-migration" import { JsonMigration } from "../../storage"
import { EOL } from "os" import { EOL } from "os"
import { errorMessage } from "../../util/error" import { errorMessage } from "../../util/error"
+1 -1
View File
@@ -4,7 +4,7 @@ import { Session } from "../../session"
import { MessageV2 } from "../../session/message-v2" import { MessageV2 } from "../../session/message-v2"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db" import { Database } from "../../storage"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql" import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { ShareNext } from "../../share" import { ShareNext } from "../../share"
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Argv } from "yargs"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { Session } from "../../session" import { Session } from "../../session"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { Database } from "../../storage/db" import { Database } from "../../storage"
import { SessionTable } from "../../session/session.sql" import { SessionTable } from "../../session/session.sql"
import { Project } from "../../project" import { Project } from "../../project"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
@@ -1,7 +1,7 @@
import z from "zod" import z from "zod"
import { setTimeout as sleep } from "node:timers/promises" import { setTimeout as sleep } from "node:timers/promises"
import { fn } from "@/util/fn" import { fn } from "@/util/fn"
import { Database, asc, eq, inArray } from "@/storage/db" import { Database, asc, eq, inArray } from "@/storage"
import { Project } from "@/project" import { Project } from "@/project"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
@@ -114,7 +114,7 @@ export namespace Workspace {
await adaptor.create(config) await adaptor.create(config)
void startSync(info) startSync(info)
await waitEvent({ await waitEvent({
timeout: TIMEOUT, timeout: TIMEOUT,
@@ -294,7 +294,7 @@ export namespace Workspace {
) )
const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id)) const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
for (const space of spaces) void startSync(space) for (const space of spaces) startSync(space)
return spaces return spaces
} }
@@ -307,7 +307,7 @@ export namespace Workspace {
export const get = fn(WorkspaceID.zod, async (id) => { export const get = fn(WorkspaceID.zod, async (id) => {
const space = lookup(id) const space = lookup(id)
if (!space) return if (!space) return
void startSync(space) startSync(space)
return space return space
}) })
+1 -1
View File
@@ -12,7 +12,7 @@ import { Ripgrep } from "@/file/ripgrep"
import { FileTime } from "@/file/time" import { FileTime } from "@/file/time"
import { File } from "@/file" import { File } from "@/file"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file/watcher"
import { Storage } from "@/storage/storage" import { Storage } from "@/storage"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Provider } from "@/provider" import { Provider } from "@/provider"
+2 -2
View File
@@ -31,8 +31,8 @@ import { SessionCommand } from "./cli/cmd/session"
import { DbCommand } from "./cli/cmd/db" import { DbCommand } from "./cli/cmd/db"
import path from "path" import path from "path"
import { Global } from "./global" import { Global } from "./global"
import { JsonMigration } from "./storage/json-migration" import { JsonMigration } from "./storage"
import { Database } from "./storage/db" import { Database } from "./storage"
import { errorMessage } from "./util/error" import { errorMessage } from "./util/error"
import { PluginCommand } from "./cli/cmd/plug" import { PluginCommand } from "./cli/cmd/plug"
import { Heap } from "./cli/heap" import { Heap } from "./cli/heap"
+2 -2
View File
@@ -2,5 +2,5 @@ export { Config } from "./config"
export { Server } from "./server/server" export { Server } from "./server/server"
export { bootstrap } from "./cli/bootstrap" export { bootstrap } from "./cli/bootstrap"
export { Log } from "./util" export { Log } from "./util"
export { Database } from "./storage/db" export { Database } from "./storage"
export { JsonMigration } from "./storage/json-migration" export { JsonMigration } from "./storage"
@@ -5,7 +5,7 @@ import { InstanceState } from "@/effect"
import { ProjectID } from "@/project/schema" import { ProjectID } from "@/project/schema"
import { MessageID, SessionID } from "@/session/schema" import { MessageID, SessionID } from "@/session/schema"
import { PermissionTable } from "@/session/session.sql" import { PermissionTable } from "@/session/session.sql"
import { Database, eq } from "@/storage/db" import { Database, eq } from "@/storage"
import { zod } from "@/util/effect-zod" import { zod } from "@/util/effect-zod"
import { Log } from "@/util" import { Log } from "@/util"
import { withStatics } from "@/util/schema" import { withStatics } from "@/util/schema"
+1 -1
View File
@@ -1,5 +1,5 @@
import z from "zod" import z from "zod"
import { and, Database, eq } from "../storage/db" import { and, Database, eq } from "../storage"
import { ProjectTable } from "./project.sql" import { ProjectTable } from "./project.sql"
import { SessionTable } from "../session/session.sql" import { SessionTable } from "../session/session.sql"
import { Log } from "../util" import { Log } from "../util"
+1 -1
View File
@@ -1,6 +1,6 @@
import { resolver } from "hono-openapi" import { resolver } from "hono-openapi"
import z from "zod" import z from "zod"
import { NotFoundError } from "../storage/db" import { NotFoundError } from "../storage"
export const ERRORS = { export const ERRORS = {
400: { 400: {
+1 -1
View File
@@ -1,5 +1,5 @@
import type { MiddlewareHandler } from "hono" import type { MiddlewareHandler } from "hono"
import { Database, inArray } from "@/storage/db" import { Database, inArray } from "@/storage"
import { EventSequenceTable } from "@/sync/event.sql" import { EventSequenceTable } from "@/sync/event.sql"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
import type { WorkspaceID } from "@/control-plane/schema" import type { WorkspaceID } from "@/control-plane/schema"
+1 -1
View File
@@ -6,7 +6,7 @@ import z from "zod"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Pty } from "@/pty" import { Pty } from "@/pty"
import { PtyID } from "@/pty/schema" import { PtyID } from "@/pty/schema"
import { NotFoundError } from "../../storage/db" import { NotFoundError } from "../../storage"
import { errors } from "../error" import { errors } from "../error"
export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
@@ -2,7 +2,7 @@ import z from "zod"
import { Hono } from "hono" import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi" import { describeRoute, validator, resolver } from "hono-openapi"
import { SyncEvent } from "@/sync" import { SyncEvent } from "@/sync"
import { Database, asc, and, not, or, lte, eq } from "@/storage/db" import { Database, asc, and, not, or, lte, eq } from "@/storage"
import { EventTable } from "@/sync/event.sql" import { EventTable } from "@/sync/event.sql"
import { lazy } from "@/util/lazy" import { lazy } from "@/util/lazy"
import { Log } from "@/util" import { Log } from "@/util"
+1 -1
View File
@@ -1,6 +1,6 @@
import { Provider } from "../provider" import { Provider } from "../provider"
import { NamedError } from "@opencode-ai/shared/util/error" import { NamedError } from "@opencode-ai/shared/util/error"
import { NotFoundError } from "../storage/db" import { NotFoundError } from "../storage"
import { Session } from "../session" import { Session } from "../session"
import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ContentfulStatusCode } from "hono/utils/http-status"
import type { ErrorHandler, MiddlewareHandler } from "hono" import type { ErrorHandler, MiddlewareHandler } from "hono"
+1 -1
View File
@@ -3,7 +3,7 @@ import sessionProjectors from "../session/projectors"
import { SyncEvent } from "@/sync" import { SyncEvent } from "@/sync"
import { Session } from "@/session" import { Session } from "@/session"
import { SessionTable } from "@/session/session.sql" import { SessionTable } from "@/session/session.sql"
import { Database, eq } from "@/storage/db" import { Database, eq } from "@/storage"
export function initProjectors() { export function initProjectors() {
SyncEvent.init({ SyncEvent.init({
+1 -1
View File
@@ -11,7 +11,7 @@ import { SessionProcessor } from "./processor"
import { Agent } from "@/agent/agent" import { Agent } from "@/agent/agent"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Config } from "@/config" import { Config } from "@/config"
import { NotFoundError } from "@/storage/db" import { NotFoundError } from "@/storage"
import { ModelID, ProviderID } from "@/provider/schema" import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, Context } from "effect"
import { InstanceState } from "@/effect" import { InstanceState } from "@/effect"
+1 -1
View File
@@ -6,7 +6,7 @@ import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessag
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { SyncEvent } from "../sync" import { SyncEvent } from "../sync"
import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage/db" import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage"
import { MessageTable, PartTable, SessionTable } from "./session.sql" import { MessageTable, PartTable, SessionTable } from "./session.sql"
import { ProviderError } from "@/provider/error" import { ProviderError } from "@/provider/error"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
+1 -1
View File
@@ -1,4 +1,4 @@
import { NotFoundError, eq, and } from "../storage/db" import { NotFoundError, eq, and } from "../storage"
import { SyncEvent } from "@/sync" import { SyncEvent } from "@/sync"
import { Session } from "." import { Session } from "."
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
+1 -1
View File
@@ -2,7 +2,7 @@ import z from "zod"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, Context } from "effect"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Snapshot } from "../snapshot" import { Snapshot } from "../snapshot"
import { Storage } from "@/storage/storage" import { Storage } from "@/storage"
import { SyncEvent } from "../sync" import { SyncEvent } from "../sync"
import { Log } from "../util" import { Log } from "../util"
import { Session } from "." import { Session } from "."
+3 -3
View File
@@ -8,12 +8,12 @@ import { type ProviderMetadata, type LanguageModelUsage } from "ai"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Installation } from "../installation" import { Installation } from "../installation"
import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage/db" import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage"
import { SyncEvent } from "../sync" import { SyncEvent } from "../sync"
import type { SQL } from "../storage/db" import type { SQL } from "../storage"
import { PartTable, SessionTable } from "./session.sql" import { PartTable, SessionTable } from "./session.sql"
import { ProjectTable } from "../project/project.sql" import { ProjectTable } from "../project/project.sql"
import { Storage } from "@/storage/storage" import { Storage } from "@/storage"
import { Log } from "../util" import { Log } from "../util"
import { updateSchema } from "../util/update-schema" import { updateSchema } from "../util/update-schema"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
+1 -1
View File
@@ -2,7 +2,7 @@ import z from "zod"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, Context } from "effect"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Storage } from "@/storage/storage" import { Storage } from "@/storage"
import { Session } from "." import { Session } from "."
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { SessionID, MessageID } from "./schema" import { SessionID, MessageID } from "./schema"
+1 -1
View File
@@ -3,7 +3,7 @@ import { Bus } from "@/bus"
import { SessionID } from "./schema" import { SessionID } from "./schema"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, Context } from "effect"
import z from "zod" import z from "zod"
import { Database, eq, asc } from "../storage/db" import { Database, eq, asc } from "../storage"
import { TodoTable } from "./session.sql" import { TodoTable } from "./session.sql"
export namespace Todo { export namespace Todo {
+1 -1
View File
@@ -9,7 +9,7 @@ import { ModelID, ProviderID } from "@/provider/schema"
import { Session } from "@/session" import { Session } from "@/session"
import { MessageV2 } from "@/session/message-v2" import { MessageV2 } from "@/session/message-v2"
import type { SessionID } from "@/session/schema" import type { SessionID } from "@/session/schema"
import { Database, eq } from "@/storage/db" import { Database, eq } from "@/storage"
import { Config } from "@/config" import { Config } from "@/config"
import { Log } from "@/util" import { Log } from "@/util"
import { SessionShareTable } from "./share.sql" import { SessionShareTable } from "./share.sql"
+28 -30
View File
@@ -27,29 +27,28 @@ export const NotFoundError = NamedError.create(
const log = Log.create({ service: "db" }) const log = Log.create({ service: "db" })
export namespace Database { export function getChannelPath() {
export function getChannelPath() {
if (["latest", "beta", "prod"].includes(CHANNEL) || Flag.OPENCODE_DISABLE_CHANNEL_DB) if (["latest", "beta", "prod"].includes(CHANNEL) || Flag.OPENCODE_DISABLE_CHANNEL_DB)
return path.join(Global.Path.data, "opencode.db") return path.join(Global.Path.data, "opencode.db")
const safe = CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-") const safe = CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-")
return path.join(Global.Path.data, `opencode-${safe}.db`) return path.join(Global.Path.data, `opencode-${safe}.db`)
} }
export const Path = iife(() => { export const Path = iife(() => {
if (Flag.OPENCODE_DB) { if (Flag.OPENCODE_DB) {
if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
return path.join(Global.Path.data, Flag.OPENCODE_DB) return path.join(Global.Path.data, Flag.OPENCODE_DB)
} }
return getChannelPath() return getChannelPath()
}) })
export type Transaction = SQLiteTransaction<"sync", void> export type Transaction = SQLiteTransaction<"sync", void>
type Client = SQLiteBunDatabase type Client = SQLiteBunDatabase
type Journal = { sql: string; timestamp: number; name: string }[] type Journal = { sql: string; timestamp: number; name: string }[]
function time(tag: string) { function time(tag: string) {
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag)
if (!match) return 0 if (!match) return 0
return Date.UTC( return Date.UTC(
@@ -60,9 +59,9 @@ export namespace Database {
Number(match[5]), Number(match[5]),
Number(match[6]), Number(match[6]),
) )
} }
function migrations(dir: string): Journal { function migrations(dir: string): Journal {
const dirs = readdirSync(dir, { withFileTypes: true }) const dirs = readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory()) .filter((entry) => entry.isDirectory())
.map((entry) => entry.name) .map((entry) => entry.name)
@@ -80,9 +79,9 @@ export namespace Database {
.filter(Boolean) as Journal .filter(Boolean) as Journal
return sql.sort((a, b) => a.timestamp - b.timestamp) return sql.sort((a, b) => a.timestamp - b.timestamp)
} }
export const Client = lazy(() => { export const Client = lazy(() => {
log.info("opening database", { path: Path }) log.info("opening database", { path: Path })
const db = init(Path) const db = init(Path)
@@ -113,51 +112,51 @@ export namespace Database {
} }
return db return db
}) })
export function close() { export function close() {
Client().$client.close() Client().$client.close()
Client.reset() Client.reset()
} }
export type TxOrDb = Transaction | Client export type TxOrDb = Transaction | Client
const ctx = LocalContext.create<{ const ctx = LocalContext.create<{
tx: TxOrDb tx: TxOrDb
effects: (() => void | Promise<void>)[] effects: (() => void | Promise<void>)[]
}>("database") }>("database")
export function use<T>(callback: (trx: TxOrDb) => T): T { export function use<T>(callback: (trx: TxOrDb) => T): T {
try { try {
return callback(ctx.use().tx) return callback(ctx.use().tx)
} catch (err) { } catch (err) {
if (err instanceof LocalContext.NotFound) { if (err instanceof LocalContext.NotFound) {
const effects: (() => void | Promise<void>)[] = [] const effects: (() => void | Promise<void>)[] = []
const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) const result = ctx.provide({ effects, tx: Client() }, () => callback(Client()))
for (const effect of effects) void effect() for (const effect of effects) effect()
return result return result
} }
throw err throw err
} }
} }
export function effect(fn: () => any | Promise<any>) { export function effect(fn: () => any | Promise<any>) {
const bound = InstanceState.bind(fn) const bound = InstanceState.bind(fn)
try { try {
ctx.use().effects.push(bound) ctx.use().effects.push(bound)
} catch { } catch {
void bound() bound()
}
} }
}
type NotPromise<T> = T extends Promise<any> ? never : T type NotPromise<T> = T extends Promise<any> ? never : T
export function transaction<T>( export function transaction<T>(
callback: (tx: TxOrDb) => NotPromise<T>, callback: (tx: TxOrDb) => NotPromise<T>,
options?: { options?: {
behavior?: "deferred" | "immediate" | "exclusive" behavior?: "deferred" | "immediate" | "exclusive"
}, },
): NotPromise<T> { ): NotPromise<T> {
try { try {
return callback(ctx.use().tx) return callback(ctx.use().tx)
} catch (err) { } catch (err) {
@@ -165,10 +164,9 @@ export namespace Database {
const effects: (() => void | Promise<void>)[] = [] const effects: (() => void | Promise<void>)[] = []
const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx)))
const result = Client().transaction(txCallback, { behavior: options?.behavior }) const result = Client().transaction(txCallback, { behavior: options?.behavior })
for (const effect of effects) void effect() for (const effect of effects) effect()
return result as NotPromise<T> return result as NotPromise<T>
} }
throw err throw err
} }
}
} }
+26
View File
@@ -0,0 +1,26 @@
export * as JsonMigration from "./json-migration"
export * as Database from "./db"
export * as Storage from "./storage"
export {
asc,
eq,
and,
or,
inArray,
desc,
not,
sql,
isNull,
isNotNull,
count,
like,
exists,
between,
gt,
gte,
lt,
lte,
ne,
} from "drizzle-orm"
export type { SQL } from "drizzle-orm"
export { NotFoundError } from "./storage"
@@ -10,20 +10,19 @@ import { existsSync } from "fs"
import { Filesystem } from "../util" import { Filesystem } from "../util"
import { Glob } from "@opencode-ai/shared/util/glob" import { Glob } from "@opencode-ai/shared/util/glob"
export namespace JsonMigration { const log = Log.create({ service: "json-migration" })
const log = Log.create({ service: "json-migration" })
export type Progress = { export type Progress = {
current: number current: number
total: number total: number
label: string label: string
} }
type Options = { type Options = {
progress?: (event: Progress) => void progress?: (event: Progress) => void
} }
export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<any, any>, options?: Options) { export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<any, any>, options?: Options) {
const storageDir = path.join(Global.Path.data, "storage") const storageDir = path.join(Global.Path.data, "storage")
if (!existsSync(storageDir)) { if (!existsSync(storageDir)) {
@@ -425,5 +424,4 @@ export namespace JsonMigration {
progress?.({ current: total, total, label: "complete" }) progress?.({ current: total, total, label: "complete" })
return stats return stats
}
} }
+34 -36
View File
@@ -7,85 +7,84 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect" import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect"
import { Git } from "@/git" import { Git } from "@/git"
export namespace Storage { const log = Log.create({ service: "storage" })
const log = Log.create({ service: "storage" })
type Migration = ( type Migration = (
dir: string, dir: string,
fs: AppFileSystem.Interface, fs: AppFileSystem.Interface,
git: Git.Interface, git: Git.Interface,
) => Effect.Effect<void, AppFileSystem.Error> ) => Effect.Effect<void, AppFileSystem.Error>
export const NotFoundError = NamedError.create( export const NotFoundError = NamedError.create(
"NotFoundError", "NotFoundError",
z.object({ z.object({
message: z.string(), message: z.string(),
}), }),
) )
export type Error = AppFileSystem.Error | InstanceType<typeof NotFoundError> export type Error = AppFileSystem.Error | InstanceType<typeof NotFoundError>
const RootFile = Schema.Struct({ const RootFile = Schema.Struct({
path: Schema.optional( path: Schema.optional(
Schema.Struct({ Schema.Struct({
root: Schema.optional(Schema.String), root: Schema.optional(Schema.String),
}), }),
), ),
}) })
const SessionFile = Schema.Struct({ const SessionFile = Schema.Struct({
id: Schema.String, id: Schema.String,
}) })
const MessageFile = Schema.Struct({ const MessageFile = Schema.Struct({
id: Schema.String, id: Schema.String,
}) })
const DiffFile = Schema.Struct({ const DiffFile = Schema.Struct({
additions: Schema.Number, additions: Schema.Number,
deletions: Schema.Number, deletions: Schema.Number,
}) })
const SummaryFile = Schema.Struct({ const SummaryFile = Schema.Struct({
id: Schema.String, id: Schema.String,
projectID: Schema.String, projectID: Schema.String,
summary: Schema.Struct({ diffs: Schema.Array(DiffFile) }), summary: Schema.Struct({ diffs: Schema.Array(DiffFile) }),
}) })
const decodeRoot = Schema.decodeUnknownOption(RootFile) const decodeRoot = Schema.decodeUnknownOption(RootFile)
const decodeSession = Schema.decodeUnknownOption(SessionFile) const decodeSession = Schema.decodeUnknownOption(SessionFile)
const decodeMessage = Schema.decodeUnknownOption(MessageFile) const decodeMessage = Schema.decodeUnknownOption(MessageFile)
const decodeSummary = Schema.decodeUnknownOption(SummaryFile) const decodeSummary = Schema.decodeUnknownOption(SummaryFile)
export interface Interface { export interface Interface {
readonly remove: (key: string[]) => Effect.Effect<void, AppFileSystem.Error> readonly remove: (key: string[]) => Effect.Effect<void, AppFileSystem.Error>
readonly read: <T>(key: string[]) => Effect.Effect<T, Error> readonly read: <T>(key: string[]) => Effect.Effect<T, Error>
readonly update: <T>(key: string[], fn: (draft: T) => void) => Effect.Effect<T, Error> readonly update: <T>(key: string[], fn: (draft: T) => void) => Effect.Effect<T, Error>
readonly write: <T>(key: string[], content: T) => Effect.Effect<void, AppFileSystem.Error> readonly write: <T>(key: string[], content: T) => Effect.Effect<void, AppFileSystem.Error>
readonly list: (prefix: string[]) => Effect.Effect<string[][], AppFileSystem.Error> readonly list: (prefix: string[]) => Effect.Effect<string[][], AppFileSystem.Error>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Storage") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Storage") {}
function file(dir: string, key: string[]) { function file(dir: string, key: string[]) {
return path.join(dir, ...key) + ".json" return path.join(dir, ...key) + ".json"
} }
function missing(err: unknown) { function missing(err: unknown) {
if (!err || typeof err !== "object") return false if (!err || typeof err !== "object") return false
if ("code" in err && err.code === "ENOENT") return true if ("code" in err && err.code === "ENOENT") return true
if ("reason" in err && err.reason && typeof err.reason === "object" && "_tag" in err.reason) { if ("reason" in err && err.reason && typeof err.reason === "object" && "_tag" in err.reason) {
return err.reason._tag === "NotFound" return err.reason._tag === "NotFound"
} }
return false return false
} }
function parseMigration(text: string) { function parseMigration(text: string) {
const value = Number.parseInt(text, 10) const value = Number.parseInt(text, 10)
return Number.isNaN(value) ? 0 : value return Number.isNaN(value) ? 0 : value
} }
const MIGRATIONS: Migration[] = [ const MIGRATIONS: Migration[] = [
Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) { Effect.fn("Storage.migration.1")(function* (dir: string, fs: AppFileSystem.Interface, git: Git.Interface) {
const project = path.resolve(dir, "../project") const project = path.resolve(dir, "../project")
if (!(yield* fs.isDir(project))) return if (!(yield* fs.isDir(project))) return
@@ -215,9 +214,9 @@ export namespace Storage {
) )
} }
}), }),
] ]
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const fs = yield* AppFileSystem.Service const fs = yield* AppFileSystem.Service
@@ -327,7 +326,6 @@ export namespace Storage {
list, list,
}) })
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer)) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
}
+1 -1
View File
@@ -1,6 +1,6 @@
import z from "zod" import z from "zod"
import type { ZodObject } from "zod" import type { ZodObject } from "zod"
import { Database, eq } from "@/storage/db" import { Database, eq } from "@/storage"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { Bus as ProjectBus } from "@/bus" import { Bus as ProjectBus } from "@/bus"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
+1 -1
View File
@@ -4,7 +4,7 @@ import { Global } from "../global"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { InstanceBootstrap } from "../project/bootstrap" import { InstanceBootstrap } from "../project/bootstrap"
import { Project } from "../project" import { Project } from "../project"
import { Database, eq } from "../storage/db" import { Database, eq } from "../storage"
import { ProjectTable } from "../project/project.sql" import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema" import type { ProjectID } from "../project/schema"
import { Log } from "../util" import { Log } from "../util"
+1 -1
View File
@@ -3,7 +3,7 @@ import { Effect, Layer, Option } from "effect"
import { AccountRepo } from "../../src/account/repo" import { AccountRepo } from "../../src/account/repo"
import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
import { Database } from "../../src/storage/db" import { Database } from "../../src/storage"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const truncate = Layer.effectDiscard( const truncate = Layer.effectDiscard(
@@ -15,7 +15,7 @@ import {
RefreshToken, RefreshToken,
UserCode, UserCode,
} from "../../src/account/schema" } from "../../src/account/schema"
import { Database } from "../../src/storage/db" import { Database } from "../../src/storage"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
const truncate = Layer.effectDiscard( const truncate = Layer.effectDiscard(
+1 -1
View File
@@ -1,6 +1,6 @@
import { rm } from "fs/promises" import { rm } from "fs/promises"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { Database } from "../../src/storage/db" import { Database } from "../../src/storage"
export async function resetDatabase() { export async function resetDatabase() {
await Instance.disposeAll().catch(() => undefined) await Instance.disposeAll().catch(() => undefined)
+1 -1
View File
@@ -10,7 +10,7 @@ import { afterAll } from "bun:test"
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true }) await fs.mkdir(dir, { recursive: true })
afterAll(async () => { afterAll(async () => {
const { Database } = await import("../src/storage/db") const { Database } = await import("../src/storage")
Database.close() Database.close()
const busy = (error: unknown) => const busy = (error: unknown) =>
typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY" typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY"
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project" import { Project } from "../../src/project"
import { Database, eq } from "../../src/storage/db" import { Database, eq } from "../../src/storage"
import { SessionTable } from "../../src/session/session.sql" import { SessionTable } from "../../src/session/session.sql"
import { ProjectTable } from "../../src/project/project.sql" import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema" import { ProjectID } from "../../src/project/schema"
@@ -10,7 +10,7 @@ import { $ } from "bun"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { Effect } from "effect" import { Effect } from "effect"
void Log.init({ print: false }) Log.init({ print: false })
function run<A>(fn: (svc: Project.Interface) => Effect.Effect<A>) { function run<A>(fn: (svc: Project.Interface) => Effect.Effect<A>) {
return Effect.runPromise( return Effect.runPromise(
@@ -14,7 +14,7 @@ import { Session } from "../../src/session"
import type { SessionID } from "../../src/session/schema" import type { SessionID } from "../../src/session/schema"
import { ShareNext } from "../../src/share" import { ShareNext } from "../../src/share"
import { SessionShareTable } from "../../src/share/share.sql" import { SessionShareTable } from "../../src/share/share.sql"
import { Database, eq } from "../../src/storage/db" import { Database, eq } from "../../src/storage"
import { provideTmpdirInstance } from "../fixture/fixture" import { provideTmpdirInstance } from "../fixture/fixture"
import { resetDatabase } from "../fixture/db" import { resetDatabase } from "../fixture/db"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
+1 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
import path from "path" import path from "path"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { Installation } from "../../src/installation" import { Installation } from "../../src/installation"
import { Database } from "../../src/storage/db" import { Database } from "../../src/storage"
describe("Database.Path", () => { describe("Database.Path", () => {
test("returns database path for the current channel", () => { test("returns database path for the current channel", () => {
@@ -5,7 +5,7 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { readFileSync, readdirSync } from "fs" import { readFileSync, readdirSync } from "fs"
import { JsonMigration } from "../../src/storage/json-migration" import { JsonMigration } from "../../src/storage"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { ProjectTable } from "../../src/project/project.sql" import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema" import { ProjectID } from "../../src/project/schema"
@@ -5,7 +5,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Git } from "../../src/git" import { Git } from "../../src/git"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { Storage } from "../../src/storage/storage" import { Storage } from "../../src/storage"
import { tmpdirScoped } from "../fixture/fixture" import { tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
+1 -1
View File
@@ -4,7 +4,7 @@ import z from "zod"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { SyncEvent } from "../../src/sync" import { SyncEvent } from "../../src/sync"
import { Database } from "../../src/storage/db" import { Database } from "../../src/storage"
import { EventTable } from "../../src/sync/event.sql" import { EventTable } from "../../src/sync/event.sql"
import { Identifier } from "../../src/id/id" import { Identifier } from "../../src/id/id"
import { Flag } from "../../src/flag/flag" import { Flag } from "../../src/flag/flag"