refactor(server): inject runtime options

This commit is contained in:
Dax Raad 2026-07-20 20:49:25 -04:00
commit 0409e6884d
17 changed files with 146 additions and 74 deletions

View file

@ -2,7 +2,7 @@ export * as Database from "./database"
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
import { sqliteLayer } from "#sqlite"
import { Context, Effect, Layer } from "effect"
import { Context, Effect, Layer, Schema } from "effect"
import { Global } from "../global"
import { isAbsolute, join } from "path"
import { DatabaseMigration } from "./migration"
@ -16,9 +16,10 @@ export interface Interface {
db: DatabaseShape
}
export interface Options {
readonly path?: string
}
export const Options = Schema.Struct({
path: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}

View file

@ -5,7 +5,7 @@ import { Observability } from "../observability"
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
const getRuntime = () =>
(rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer<I, E>, {
(rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer()) as Layer.Layer<I, E>, {
memoMap,
}))

View file

@ -2,7 +2,7 @@ export * as FileSystemSearch from "./search"
import { makeLocationNode } from "../effect/app-node"
import path from "path"
import { Context, Effect, Layer, Scope } from "effect"
import { Context, Effect, Layer, Schema, Scope } from "effect"
import { Fff } from "#fff"
import fuzzysort from "fuzzysort"
import { FileSystem } from "../filesystem"
@ -10,7 +10,6 @@ import { FSUtil } from "../fs-util"
import { Location } from "../location"
import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema"
import { Flag } from "../flag/flag"
export interface Interface {
readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]>
@ -18,6 +17,11 @@ export interface Interface {
readonly grep: (input: FileSystem.GrepInput) => Effect.Effect<readonly FileSystem.Match[]>
}
export const Options = Schema.Struct({
fff: Schema.optional(Schema.Boolean),
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem/Search") {}
export const ripgrepLayer = Layer.effect(
@ -232,13 +236,18 @@ export const fffLayer = Layer.effect(
}),
)
const layer = Layer.unwrap(
export const layer = (options?: Options) => Layer.unwrap(
Effect.gen(function* () {
if (Flag.OPENCODE_DISABLE_FFF || !Fff.available()) return ripgrepLayer
if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available())
return ripgrepLayer
const location = yield* Location.Service
// Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree.
return location.vcs ? fffLayer : ripgrepLayer
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] })
export function nodeWith(options?: Options) {
return makeLocationNode({ service: Service, layer: layer(options), deps: [FSUtil.node, Location.node, Ripgrep.node] })
}
export const node = nodeWith()

View file

@ -5,9 +5,8 @@ import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher"
import { FileSystem } from "@opencode-ai/schema/filesystem"
import { makeGlobalNode } from "../effect/app-node"
import { Cause, Context, Effect, Layer, PubSub, Scope, Stream } from "effect"
import { Cause, Context, Effect, Layer, PubSub, Schema, Scope, Stream } from "effect"
import { KeyedMutex } from "../effect/keyed-mutex"
import { Flag } from "../flag/flag"
import { lazy } from "../util/lazy"
import { watch as watchFileSystem } from "node:fs"
import path from "path"
@ -50,14 +49,19 @@ export interface Interface {
readonly subscribe: (input: WatchInput) => Stream.Stream<Update>
}
export const Options = Schema.Struct({
enabled: Schema.optional(Schema.Boolean),
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/Watcher") {}
const layer = Layer.effect(
export const layer = (options?: Options) => Layer.effect(
Service,
Effect.gen(function* () {
const backend = getBackend()
const native = watcher()
if (Flag.OPENCODE_DISABLE_FILEWATCHER) {
if (options?.enabled === false) {
return Service.of({ subscribe: () => Stream.empty })
}
@ -140,7 +144,11 @@ const layer = Layer.effect(
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
export function nodeWith(options?: Options) {
return makeGlobalNode({ service: Service, layer: layer(options), deps: [] })
}
export const node = nodeWith()
function subscribeDirectory(
native: typeof import("@parcel/watcher") | undefined,

View file

@ -530,11 +530,12 @@ export interface Interface {
readonly refresh: (force?: boolean) => Effect.Effect<void>
}
export interface Options {
readonly url?: string
readonly file?: string
readonly fetch?: boolean
}
export const Options = Schema.Struct({
url: Schema.optional(Schema.String),
file: Schema.optional(Schema.String),
fetch: Schema.optional(Schema.Boolean),
})
export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}

View file

@ -2,29 +2,41 @@ export * as Observability from "./observability"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "./effect/layer-node"
import { Effect, Layer, Logger, References } from "effect"
import { Effect, Layer, Logger, References, Schema } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { OtlpSerialization } from "effect/unstable/observability"
import { Logging } from "./observability/logging"
import { Otlp } from "./observability/otlp"
const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
Layer.provide(NodeFileSystem.layer),
Layer.orDie,
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
)
export const Options = Schema.Struct({
endpoint: Schema.optional(Schema.String),
headers: Schema.optional(Schema.String),
})
export type Options = typeof Options.Type
export const layer = Layer.unwrap(
Effect.gen(function* () {
const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers()], { mergeWithExisting: false }).pipe(
Layer.provide(NodeFileSystem.layer),
Layer.provide(OtlpSerialization.layerJson),
Layer.provide(FetchHttpClient.layer),
Layer.orDie,
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
)
return Layer.merge(logs, yield* Effect.promise(Otlp.tracingLayer))
}),
).pipe(Layer.catchCause(() => local))
export function layer(
options: Options = {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
},
) {
const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
Layer.provide(NodeFileSystem.layer),
Layer.orDie,
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
)
return Layer.unwrap(
Effect.gen(function* () {
const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers(options)], { mergeWithExisting: false }).pipe(
Layer.provide(NodeFileSystem.layer),
Layer.provide(OtlpSerialization.layerJson),
Layer.provide(FetchHttpClient.layer),
Layer.orDie,
Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
)
return Layer.merge(logs, yield* Effect.promise(() => Otlp.tracingLayer(options)))
}),
).pipe(Layer.catchCause(() => local))
}
export const node = LayerNode.make({ name: "observability", layer, deps: [] })
export const node = LayerNode.make({ name: "observability", layer: layer(), deps: [] })

View file

@ -4,10 +4,14 @@ import { Flag } from "../flag/flag"
import { InstallationChannel, InstallationVersion } from "../installation/version"
import { runID } from "./shared"
const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
export interface Options {
readonly endpoint?: string
readonly headers?: string
}
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
function parseHeaders(value?: string) {
return value
? value.split(",").reduce(
(acc, entry) => {
const [key, ...value] = entry.split("=")
acc[key] = value.join("=")
@ -15,7 +19,8 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
},
{} as Record<string, string>,
)
: undefined
: undefined
}
function resourceAttributes() {
const value = process.env.OTEL_RESOURCE_ATTRIBUTES
@ -47,13 +52,15 @@ export function resource(): { serviceName: string; serviceVersion: string; attri
}
}
export function loggers() {
if (!endpoint) return []
return [OtlpLogger.make({ url: `${endpoint}/v1/logs`, resource: resource(), headers })]
export function loggers(options?: Options) {
if (!options?.endpoint) return []
return [
OtlpLogger.make({ url: `${options.endpoint}/v1/logs`, resource: resource(), headers: parseHeaders(options.headers) }),
]
}
export async function tracingLayer() {
if (!endpoint) return Layer.empty
export async function tracingLayer(options?: Options) {
if (!options?.endpoint) return Layer.empty
const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
const SdkBase = await import("@opentelemetry/sdk-trace-base")
@ -69,8 +76,8 @@ export async function tracingLayer() {
resource: resource(),
spanProcessor: new SdkBase.BatchSpanProcessor(
new OTLP.OTLPTraceExporter({
url: `${endpoint}/v1/traces`,
headers,
url: `${options.endpoint}/v1/traces`,
headers: parseHeaders(options.headers),
}),
),
}))