refactor(core): simplify persistence boundaries (#43929)
This commit is contained in:
@@ -62,31 +62,30 @@ const layer = Layer.effect(
|
||||
value: decode(row.value),
|
||||
})
|
||||
}
|
||||
const storedRows = (rows: ReadonlyArray<typeof CredentialTable.$inferSelect>) =>
|
||||
rows.flatMap((row) => {
|
||||
const credential = stored(row)
|
||||
return credential ? [credential] : []
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
all: Effect.fn("Credential.all")(function* () {
|
||||
return (yield* db
|
||||
all: Effect.fn("Credential.all")(() =>
|
||||
db
|
||||
.select()
|
||||
.from(CredentialTable)
|
||||
.orderBy(asc(CredentialTable.time_created))
|
||||
.all()
|
||||
.pipe(Effect.orDie)).flatMap((row) => {
|
||||
const credential = stored(row)
|
||||
return credential ? [credential] : []
|
||||
})
|
||||
}),
|
||||
list: Effect.fn("Credential.list")(function* (integrationID) {
|
||||
return (yield* db
|
||||
.pipe(Effect.orDie, Effect.map(storedRows)),
|
||||
),
|
||||
list: Effect.fn("Credential.list")((integrationID) =>
|
||||
db
|
||||
.select()
|
||||
.from(CredentialTable)
|
||||
.where(eq(CredentialTable.integration_id, integrationID))
|
||||
.orderBy(asc(CredentialTable.time_created))
|
||||
.all()
|
||||
.pipe(Effect.orDie)).flatMap((row) => {
|
||||
const credential = stored(row)
|
||||
return credential ? [credential] : []
|
||||
})
|
||||
}),
|
||||
.pipe(Effect.orDie, Effect.map(storedRows)),
|
||||
),
|
||||
get: Effect.fn("Credential.get")(function* (id) {
|
||||
const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie)
|
||||
return row ? stored(row) : undefined
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
|
||||
import { identity } from "effect/Function"
|
||||
import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore } from "effect"
|
||||
import { Reactivity } from "effect/unstable/reactivity"
|
||||
import { SqlClient, Statement } from "effect/unstable/sql"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
@@ -167,26 +166,7 @@ const make = (options: Config) =>
|
||||
}),
|
||||
})
|
||||
|
||||
const connection = identity<Connection>({
|
||||
execute(query, params, transformRows) {
|
||||
return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params)
|
||||
},
|
||||
executeRaw(query, params) {
|
||||
return run(query, params)
|
||||
},
|
||||
executeValues(query, params) {
|
||||
return runValues(query, params)
|
||||
},
|
||||
executeValuesUnprepared(query, params) {
|
||||
return runValues(query, params)
|
||||
},
|
||||
executeUnprepared(query, params, transformRows) {
|
||||
return this.execute(query, params, transformRows)
|
||||
},
|
||||
executeStream() {
|
||||
return Stream.die("executeStream not implemented")
|
||||
},
|
||||
})
|
||||
const connection = Sqlite.makeConnection(run, runValues, {})
|
||||
|
||||
const semaphore = yield* Semaphore.make(1)
|
||||
const acquirer = semaphore.withPermits(1)(Effect.succeed(connection))
|
||||
|
||||
@@ -34,6 +34,7 @@ import { MCPStdio } from "./stdio.js"
|
||||
const DEFAULT_STARTUP_TIMEOUT = 30_000
|
||||
const DEFAULT_CATALOG_TIMEOUT = 30_000
|
||||
const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours
|
||||
const toError = (error: unknown) => (error instanceof Error ? error : new Error(String(error)))
|
||||
|
||||
// Some servers advertise tool outputSchemas the SDK's strict validator can't resolve; this drops
|
||||
// only that field so a single bad schema doesn't blank out the whole tool list.
|
||||
@@ -261,7 +262,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
},
|
||||
(result) => result.tools,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.tapError((error) => Effect.logWarning("failed to list MCP tools", { server, error: error.message })),
|
||||
)
|
||||
@@ -286,7 +287,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
},
|
||||
(result) => result.prompts,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
|
||||
@@ -312,7 +313,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
|
||||
(result) => result.resources,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
|
||||
@@ -337,7 +338,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
}),
|
||||
(result) => result.resourceTemplates,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
|
||||
@@ -355,7 +356,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
if (!client.getServerCapabilities()?.resources) return undefined
|
||||
const result = yield* Effect.tryPromise({
|
||||
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.tapError((error) =>
|
||||
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
|
||||
@@ -378,7 +379,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
GetPromptResultSchema,
|
||||
{ signal, timeout: executionTimeout },
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.map((result) => ({
|
||||
messages: result.messages.map((message) => ({ role: message.role, content: message.content })),
|
||||
@@ -393,7 +394,7 @@ export const connect = Effect.fnUntraced(function* (
|
||||
// Keep progress tokens available while enforcing a hard wall-clock execution timeout.
|
||||
{ signal, timeout: executionTimeout, onprogress: () => {} },
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
catch: toError,
|
||||
}).pipe(
|
||||
Effect.map((result) => ({
|
||||
isError: result.isError === true,
|
||||
|
||||
@@ -3,6 +3,7 @@ export * as Provider from "./provider.js"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
|
||||
import { isRecord } from "@opencode-ai/ai/utils/record"
|
||||
import { Npm } from "@opencode-ai/util/npm"
|
||||
import type { DeepMutable } from "./schema.js"
|
||||
import { importModule, resolveModule } from "@opencode-ai/util/runtime-import"
|
||||
@@ -108,18 +109,7 @@ export function mergeOverlay(
|
||||
const left = base[key]
|
||||
const right = overlay[key]
|
||||
if (right === undefined) return [key, left]
|
||||
if (
|
||||
typeof left === "object" &&
|
||||
left !== null &&
|
||||
!Array.isArray(left) &&
|
||||
typeof right === "object" &&
|
||||
right !== null &&
|
||||
!Array.isArray(right)
|
||||
)
|
||||
return [
|
||||
key,
|
||||
mergeOverlay(left as Readonly<Record<string, unknown>>, right as Readonly<Record<string, unknown>>) ?? {},
|
||||
]
|
||||
if (isRecord(left) && isRecord(right)) return [key, mergeOverlay(left, right) ?? {}]
|
||||
return [key, right]
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -88,6 +88,12 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ri
|
||||
|
||||
const failure = (message: string, cause?: unknown) => new Error({ message, cause })
|
||||
|
||||
const normalizePath = (value: string) =>
|
||||
value
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/")
|
||||
|
||||
const isInvalidPattern = (stderr: string) =>
|
||||
stderr.includes("regex parse error") || stderr.includes("error parsing regex")
|
||||
|
||||
@@ -169,13 +175,7 @@ const layer = Layer.effect(
|
||||
"--glob=!**/.git/**",
|
||||
".",
|
||||
],
|
||||
parse: (line) =>
|
||||
Effect.succeed(
|
||||
line
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/"),
|
||||
),
|
||||
parse: (line) => Effect.succeed(normalizePath(line)),
|
||||
}).pipe(
|
||||
Effect.map((result) =>
|
||||
result.items.map((relative) =>
|
||||
@@ -203,10 +203,7 @@ const layer = Layer.effect(
|
||||
".",
|
||||
],
|
||||
parse: (line) => {
|
||||
const relative = line
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/")
|
||||
const relative = normalizePath(line)
|
||||
return Effect.succeed(
|
||||
Entry.make({
|
||||
path: RelativePath.make(relative),
|
||||
@@ -242,7 +239,7 @@ const layer = Layer.effect(
|
||||
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
|
||||
Effect.map((match) => ({
|
||||
...match.data,
|
||||
path: { text: match.data.path.text.replace(/^\.[\\/]/, "") },
|
||||
path: { text: normalizePath(match.data.path.text) },
|
||||
submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
|
||||
})),
|
||||
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
|
||||
@@ -251,14 +248,10 @@ const layer = Layer.effect(
|
||||
),
|
||||
}).pipe(
|
||||
Effect.map((result) =>
|
||||
result.items.map((match) => {
|
||||
const relative = match.path.text
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/")
|
||||
return Match.make({
|
||||
result.items.map((match) =>
|
||||
Match.make({
|
||||
entry: Entry.make({
|
||||
path: RelativePath.make(relative),
|
||||
path: RelativePath.make(match.path.text),
|
||||
type: "file",
|
||||
}),
|
||||
line: match.line_number,
|
||||
@@ -269,8 +262,8 @@ const layer = Layer.effect(
|
||||
start: submatch.start,
|
||||
end: submatch.end,
|
||||
})),
|
||||
})
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
@@ -180,33 +180,33 @@ const layer = Layer.effect(
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
create: Effect.fnUntraced(function* (input: StoredInput, tx?: Transaction) {
|
||||
return (
|
||||
(yield* (tx ?? db)
|
||||
.insert(WorktreeTable)
|
||||
.values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy })
|
||||
.onConflictDoUpdate({
|
||||
target: [WorktreeTable.project_id, WorktreeTable.directory],
|
||||
set: { strategy: input.strategy ?? null },
|
||||
setWhere: input.strategy
|
||||
? or(isNull(WorktreeTable.strategy), ne(WorktreeTable.strategy, input.strategy))
|
||||
: isNotNull(WorktreeTable.strategy),
|
||||
})
|
||||
.returning({ directory: WorktreeTable.directory })
|
||||
.get()
|
||||
.pipe(Effect.orDie)) !== undefined
|
||||
)
|
||||
}),
|
||||
remove: Effect.fnUntraced(function* (projectID: ProjectSchema.ID, directory: AbsolutePath, tx?: Transaction) {
|
||||
return (
|
||||
(yield* (tx ?? db)
|
||||
.delete(WorktreeTable)
|
||||
.where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory)))
|
||||
.returning({ directory: WorktreeTable.directory })
|
||||
.get()
|
||||
.pipe(Effect.orDie)) !== undefined
|
||||
)
|
||||
}),
|
||||
create: (input: StoredInput, tx?: Transaction) =>
|
||||
(tx ?? db)
|
||||
.insert(WorktreeTable)
|
||||
.values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy })
|
||||
.onConflictDoUpdate({
|
||||
target: [WorktreeTable.project_id, WorktreeTable.directory],
|
||||
set: { strategy: input.strategy ?? null },
|
||||
setWhere: input.strategy
|
||||
? or(isNull(WorktreeTable.strategy), ne(WorktreeTable.strategy, input.strategy))
|
||||
: isNotNull(WorktreeTable.strategy),
|
||||
})
|
||||
.returning({ directory: WorktreeTable.directory })
|
||||
.get()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((row) => row !== undefined),
|
||||
),
|
||||
remove: (projectID: ProjectSchema.ID, directory: AbsolutePath, tx?: Transaction) =>
|
||||
(tx ?? db)
|
||||
.delete(WorktreeTable)
|
||||
.where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory)))
|
||||
.returning({ directory: WorktreeTable.directory })
|
||||
.get()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((row) => row !== undefined),
|
||||
),
|
||||
}
|
||||
|
||||
const registry = new Map<StrategyID, Strategy>()
|
||||
|
||||
Reference in New Issue
Block a user