import { Brand, Context, Layer } from "effect" type AnyNode = Node type RuntimeLayer = Layer.Layer type NodeList = readonly [] | readonly [Item, ...Item[]] export type Output = [Item] extends [never] ? never : Item extends Node ? A : never export type Error = [Item] extends [never] ? never : Item extends Node ? E : never type NodeTag = [Item] extends [never] ? undefined : Item extends Node ? T : never type Missing = Exclude> type CheckDependencies = [ Missing, Dependencies>, ] extends [never] ? unknown : { readonly "Missing dependencies": Missing, Dependencies> } declare const $OutputType: unique symbol declare const $ErrorType: unique symbol export type Tag = Name & Brand.Brand<"LayerNode.Tag"> const makeTag = Brand.nominal() export interface Node { readonly kind: "layer" | "unbound" | "group" readonly name: string readonly service?: Context.Service.Any readonly implementation?: Layer.Any readonly dependencies: readonly AnyNode[] readonly tag?: T readonly [$OutputType]?: () => A readonly [$ErrorType]?: () => E } type NodeIdentity = | { readonly service: Context.Service.Any; readonly name?: never } | { readonly name: string; readonly service?: never } type DistributiveOmit = A extends unknown ? Omit : never export type TagConfig = Readonly> type TagNames = keyof Config & string type NodeInTags = Node | undefined> type CheckTags = [Exclude>] extends [ never, ] ? unknown : { readonly "Invalid tag dependencies": Exclude> } export interface Tags { readonly values: { readonly [Name in TagNames]: Tag } readonly make: >( name: Name, ) => ( input: DistributiveOmit>, "tag"> & CheckTags>, ) => Node, 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: TagNames) => (input: DistributiveOmit, "tag">) => make({ ...input, tag: values[name] })) as Tags["make"], } } // Nodes --------------------------------------------------------------------- type MakeInput< Implementation extends Layer.Any, Items extends NodeList, T extends Tag | undefined = undefined, > = NodeIdentity & { readonly layer: Implementation readonly deps: Items & CheckDependencies> readonly tag?: T } export function make< const Implementation extends Layer.Any, const Items extends NodeList, const T extends Tag | undefined = undefined, >( input: MakeInput, ): Node, Layer.Error | Error, T> { return { kind: "layer", name: input.service !== undefined ? input.service.key : input.name, service: input.service, implementation: input.layer, dependencies: input.deps, tag: input.tag, } } export function unbound(service: Context.Key, tag: T): Node { return { kind: "unbound", name: service.key, service, dependencies: [], tag, } } export function group( dependencies: Items, ): Node, Error, NodeTag> { return { kind: "group", name: "group", dependencies } } export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any] export type Replacements = readonly Replacement[] type CheckReplacementErrors = [Exclude] extends [never] ? unknown : { readonly "New replacement errors": Exclude } type CheckReplacement = Item extends readonly [Node, infer Replacement] ? Replacement extends Node, infer E2, T> ? CheckReplacementErrors> : Replacement extends Layer.Layer, infer E2, never> ? CheckReplacementErrors> : { readonly "Invalid replacement": Replacement } : { readonly "Invalid replacement": Item } type CheckReplacements = { readonly [K in keyof Items]: CheckReplacement } type ValidReplacements = Items & CheckReplacements function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) { const replacementNode = isNode(replacement) ? replacement : make({ ...nodeMakeIdentity(source), layer: replacement as Layer.Layer, deps: [], tag: source.tag, }) if (source.name !== replacementNode.name) { throw new Error(`Cannot replace ${source.name} with ${replacementNode.name}`) } if (source.tag !== replacementNode.tag) { throw new Error(`Cannot replace ${source.name} across tags`) } return replacementNode } function nodeMakeIdentity(node: AnyNode): NodeIdentity { if (node.service !== undefined) return { service: node.service } return { name: node.name } } function isNode(input: Layer.Any | AnyNode): input is AnyNode { return "kind" in input && "dependencies" in input } // Tree ----------------------------------------------------------------------- type Visit = (node: AnyNode, context: VisitContext) => Result type VisitContext = { readonly cache: Map readonly visit: (node: AnyNode) => Result } function walk( root: AnyNode, visit: Visit, options: { readonly cache?: Map readonly resolve?: (node: AnyNode) => AnyNode readonly detectCycles?: boolean } = {}, ) { const cache = options.cache ?? new Map() const visiting = new Set() const stack: AnyNode[] = [] const recur = (node: AnyNode): Result => { const target = options.resolve?.(node) ?? node const cached = cache.get(target) if (cached !== undefined || cache.has(target)) return cached! if (options.detectCycles !== false && visiting.has(target)) { const start = stack.indexOf(target) throw new Error( `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`, ) } visiting.add(target) stack.push(target) try { const result = visit(target, { cache, visit: recur }) if (!cache.has(target)) cache.set(target, result) return result } finally { stack.pop() visiting.delete(target) } } return recur(root) } export function hoist( root: Node, tag: T, replacements?: ValidReplacements, ): { readonly node: Node readonly hoisted: Node } { const hoisted = new Map() const replacementMap = replacementMapFrom(replacements) const node = walk( root, (node, context) => { if (node.kind === "group") { return { ...node, dependencies: node.dependencies.map(context.visit) } } if (node.tag === tag) { const existing = hoisted.get(node.name) if (existing && existing.implementation !== node.implementation) { throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`) } hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap)) return group([]) } if (node.kind === "unbound") { return node } return { ...node, dependencies: node.dependencies.map(context.visit) } }, { resolve: (node) => replacementMap.get(node.name) ?? node }, ) return { node: node as Node, hoisted: group(Array.from(hoisted.values())) as Node, } } export function compile( root: Node, replacements?: ValidReplacements, ): Layer.Layer { const replacementMap = replacementMapFrom(replacements) const cache = new Map() const compileNode = (node: AnyNode) => walk( node, (node, context) => { if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`) const dependencies = node.dependencies.flatMap(flatten).map(context.visit) const implementation = node.implementation! as RuntimeLayer return dependencies.length === 0 ? implementation : implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]])) }, { cache, resolve: (node) => replacementMap.get(node.name) ?? node }, ) const layers = flatten(root).map((node) => compileNode(node)) const layer = layers.reduce((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty) return layer as Layer.Layer } function replacementMapFrom(replacements?: Replacements) { return ( replacements?.reduce((map, [source, replacement]) => { const normalized = rewriteReplacementDependencies(replacementNode(source, replacement), map) const current = new Map([[source.name, normalized]]) for (const [name, node] of map) map.set(name, rewriteReplacementDependencies(node, current)) map.set(source.name, normalized) return map }, new Map()) ?? new Map() ) } function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap) { if (replacements.size === 0) return root const cache = new Map() const visiting = new Set() const stack: AnyNode[] = [] const recur = (node: AnyNode, isRoot = false): AnyNode => { const target = isRoot ? node : (replacements.get(node.name) ?? node) const cached = cache.get(target) if (cached !== undefined || cache.has(target)) return cached! if (visiting.has(target)) { const start = stack.indexOf(target) throw new Error( `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`, ) } visiting.add(target) stack.push(target) try { const dependencies = target.dependencies.map((dependency) => recur(dependency)) const result = dependencies.every((dependency, index) => dependency === target.dependencies[index]) ? target : { ...target, dependencies } cache.set(target, result) return result } finally { stack.pop() visiting.delete(target) } } return recur(root, true) } export function hasUnbound(root: Node, source: AnyNode): boolean { if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`) return walk(root, (node, context) => { if (node === source) return true return node.dependencies.some(context.visit) }) } function flatten(node: AnyNode): readonly AnyNode[] { return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node] } export * as LayerNode from "./layer-node"