import { Brand, Context, Effect, Layer, type Scope } from "effect" export * as LayerNode from "./layer-node.js" const GraphTypeId = Symbol("LayerNode.Graph") const VarianceTypeId = Symbol("LayerNode.Variance") const NodeTypeId = Symbol("LayerNode.Node") const ReplacementTypeId = Symbol("LayerNode.Replacement") export type Tag = Name & Brand.Brand<"LayerNode.Tag"> const makeTag = Brand.nominal() export type Graph = GraphValue export type Node = NodeValue export type Provider = ProviderNode export type Replacement = ReplacementValue export type Replacements = readonly Replacement[] type AnyGraph = Graph type AnyNode = AnyGraph & { readonly [NodeTypeId]: unknown; readonly tag: Tag | undefined } type RuntimeLayer = Layer.Layer type GraphList = readonly [] | readonly [AnyGraph, ...AnyGraph[]] export type Output = CommonOutput ? A : never> export type Error = Item extends Graph ? E : never type GraphTag = [Item] extends [never] ? undefined : Item extends Graph ? T : never type CommonOutput = A extends unknown ? ([Item] extends [Graph] ? A : never) : never type LayerOutput> = A extends unknown ? [Item] extends [Layer.Layer] ? A : never : never type ListOutput = [Items] extends [ readonly [infer Head extends AnyGraph, ...infer Tail extends readonly AnyGraph[]], ] ? ListOutput> : A type Definition = | { readonly kind: "group"; readonly name: string; readonly dependencies: readonly AnyGraph[] } | { readonly kind: "unbound"; readonly name: string } | { readonly kind: "layer" readonly name: string readonly implementation: RuntimeLayer readonly dependencies: readonly AnyGraph[] } class GraphValue { declare private readonly graph: void // Public symbol types survive declaration emit; the private brand prevents structural forgery. declare readonly [VarianceTypeId]: { readonly output: (_: A) => void readonly error: () => E readonly tags: () => T } readonly [GraphTypeId]: Definition constructor(definition: Definition) { this[GraphTypeId] = definition } get name() { return this[GraphTypeId].name } } class NodeValue extends GraphValue { // Forgetting part of a node's contract must not authorize a weaker replacement. declare readonly [NodeTypeId]: (_: [A, E, T]) => [A, E, T] constructor( definition: Exclude, readonly tag: T, ) { super(definition) } /** Replace this declaration, including every dependency on it, without acquiring its old wiring. */ replace( this: Node, replacement: Target & CheckReplacement, ): Replacement { if (replacement instanceof NodeValue) { if (replacement.tag !== this.tag) throw new Error(`Cannot replace ${this.name} across tags`) return new ReplacementValue(this, replacement) } if (!Layer.isLayer(replacement)) throw new Error("A replacement must be a node or an Effect Layer") return new ReplacementValue(this, makeProvider({ name: this.name, layer: replacement, deps: [] }, this.tag)) } } class ProviderNode extends NodeValue { /** Decorate the implementation while preserving its dependency wiring and service contract. */ mapLayer(this: Provider, f: (layer: Layer.Layer) => Layer.Layer): Provider { const definition = this[GraphTypeId] if (definition.kind !== "layer") throw new Error(`Cannot map unbound layer node: ${this.name}`) return new ProviderNode( { ...definition, implementation: f(definition.implementation as Layer.Layer) }, this.tag, ) } } class ReplacementValue { declare private readonly checked: void readonly [ReplacementTypeId]: { readonly source: AnyNode; readonly target: AnyNode } constructor(source: AnyNode, target: AnyNode) { this[ReplacementTypeId] = { source, target } } } type CheckErrors = [Exclude] extends [never] ? never : { readonly "New replacement errors": Exclude } type CheckReplacement = [ReplacementErrors] extends [never] ? unknown : ReplacementErrors type ReplacementErrors = Target extends AnyNode ? [Exclude>] extends [never] ? [GraphTag] extends [T] ? CheckErrors> : { readonly "Invalid replacement tag": GraphTag } : { readonly "Missing replacement outputs": Exclude> } : Target extends Layer.Layer ? CheckErrors : { readonly "Replacement must be a closed layer": Target } type CheckDependencies = [ Exclude, ListOutput>, ] extends [never] ? unknown : { readonly "Missing dependencies": Exclude, ListOutput> } type Identity = | { readonly service: Context.Service.Any; readonly name?: never } | { readonly name: string; readonly service?: never } type CheckLayer = [Implementation] extends [RuntimeLayer] ? unknown : { readonly "Layer contract must be preserved": Implementation } type TagInput = { readonly tag: T } | ([T] extends [undefined] ? { readonly tag?: undefined } : never) type MakeInput = Identity & TagInput & { readonly layer: Implementation & CheckLayer> readonly deps: Items & CheckDependencies> } type DistributiveOmit = A extends unknown ? Omit : never export type TagConfig = Readonly> type TagNames = keyof Config & string type CheckTags = [ Exclude, Tag | undefined>, ] extends [never] ? unknown : { readonly "Invalid tag dependencies": Exclude, Tag | undefined> } export interface Tags { readonly values: { readonly [Name in TagNames]: Tag } readonly make: >( name: Name, ) => ( input: DistributiveOmit>, "tag"> & CheckTags>, ) => Provider, Layer.Error | Error, Tag> } export function tags( config: Config, ): Tags { const names = Object.keys(config) as TagNames[] const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags["values"] return { values, make: >(name: Name) => ( input: DistributiveOmit>, "tag"> & CheckTags>, ) => makeProvider>(input, values[name]), } } export function make< const Implementation extends Layer.Any, const Items extends GraphList, const T extends Tag | undefined = undefined, >( input: MakeInput, ): Provider, Layer.Error | Error, T> { return makeProvider(input, input.tag as T) } function makeProvider( input: Identity & { readonly layer: Implementation; readonly deps: Items }, tag: T, ): Provider, Layer.Error | Error, T> { if (!Layer.isLayer(input.layer)) throw new Error("A layer node requires an Effect Layer") return new ProviderNode( { kind: "layer", name: input.service !== undefined ? input.service.key : input.name, implementation: input.layer, dependencies: [...input.deps], }, tag, ) } export function unbound(service: Context.Key, tag: T): Node { return new NodeValue({ kind: "unbound", name: service.key }, tag) } /** Ordered, associative composition. Only these roots' outputs are exposed; their dependencies remain private. */ export function group( dependencies: Items, ): Graph, Error, GraphTag> { return new GraphValue({ kind: "group", name: "group", dependencies: [...dependencies] }) } export interface CompileOptions { readonly replacements?: Replacements /** Share subgraphs rooted at this tag; give the remaining wiring a fresh map for each build. */ readonly shared?: Tag } type Resolved = { readonly implementation: RuntimeLayer readonly dependencies: readonly Resolved[] readonly shared: boolean } /** Resolve the final overrides before validating or acquiring anything. Effect owns acquisition and finalization. */ export function compile(root: Graph, options: CompileOptions = {}): Layer.Layer { const shared = options.shared const replacements = new Map( options.replacements?.map((item) => { const replacement = item[ReplacementTypeId] return [replacement.source, replacement.target] as const }), ) const cache = { shared: new Map(), local: new Map(), } const stack: AnyGraph[] = [] const definitions = { shared: new Map(), local: new Map() } const resolve = (graph: AnyGraph, inherited = false): readonly Resolved[] => { const definition = graph[GraphTypeId] const isShared = inherited || (shared !== undefined && graph instanceof NodeValue && graph.tag === shared) const resolved = isShared ? cache.shared : cache.local const cached = resolved.get(graph) if (cached) return cached const cycle = stack.indexOf(graph) if (cycle !== -1) { throw new Error( `Cycle detected in layer graph: ${[...stack.slice(cycle), graph].map((item) => item.name).join(" -> ")}`, ) } stack.push(graph) const replacement = replacements.get(graph) const result = (() => { if (replacement && replacement !== graph) return resolve(replacement, isShared) if (definition.kind === "group") return definition.dependencies.flatMap((dependency) => resolve(dependency, isShared)) if (definition.kind === "unbound") throw new Error(`Unbound layer node: ${definition.name}`) const node: Resolved = { implementation: definition.implementation, dependencies: definition.dependencies.flatMap((dependency) => resolve(dependency, isShared)), shared: isShared, } const registry = node.shared ? definitions.shared : definitions.local const existing = registry.get(node.implementation) if (existing) { if ( existing.dependencies.length !== node.dependencies.length || existing.dependencies.some((dependency, index) => dependency !== node.dependencies[index]) ) { throw new Error( `Layer ${definition.name} is wired to different dependencies; use a distinct implementation Layer`, ) } return [existing] } registry.set(node.implementation, node) return [node] })() stack.pop() resolved.set(graph, result) return result } const roots = resolve(root) return Layer.fromBuild((memoMap, scope) => { const local = shared === undefined ? memoMap : Layer.makeMemoMapUnsafe() const ambient = Layer.succeed(Layer.CurrentMemoMap, memoMap) const layers = new Map() const build = (node: Resolved): RuntimeLayer => { const cached = layers.get(node) if (cached) return cached const dependencies = node.dependencies.map(build) // Acquisition uses the selected cache, while lazy LayerMaps inherit the enclosing shared cache. const implementation = Layer.suspend(() => node.implementation.pipe(Layer.provide([ambient, ...dependencies]))) const layer = Layer.fromBuild((_, scope) => buildContext(implementation, node.shared ? memoMap : local, scope)) layers.set(node, layer) return layer } return buildContext( roots.map(build).reduce((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty), memoMap, scope, ) }) as Layer.Layer } class BuildResult extends Context.Service>()("@opencode/LayerNode/BuildResult") {} function buildContext(layer: RuntimeLayer, memoMap: Layer.MemoMap, scope: Scope.Scope) { // Preserve the exact output context before Effect appends its own memo-map metadata. return Layer.buildWithMemoMap( layer.pipe(Layer.flatMap((context) => Layer.succeed(BuildResult, context))), memoMap, scope, ).pipe(Effect.map(Context.get(BuildResult))) }