import { Brand, Context, Layer } from "effect" type RuntimeLayer = Layer.Layer type AnyNode = Node type NodeList = readonly [] | readonly [Item, ...Item[]] type Output = [Item] extends [never] ? never : Item extends Node ? A : never type Error = [Item] extends [never] ? never : Item extends Node ? E : 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 Tier = Name & Brand.Brand<"LayerNode.Tier"> const makeTier = Brand.nominal() export type Node = { readonly kind: "layer" | "group" readonly name: string readonly service?: Context.Service.Any readonly implementation?: Layer.Any readonly dependencies: readonly AnyNode[] readonly tier?: 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 type NodeInput< Implementation extends Layer.Any, Items extends NodeList, T extends Tier | undefined = undefined, > = NodeIdentity & { readonly layer: Implementation readonly deps: Items & CheckDependencies> readonly tier?: T } export function make< const Implementation extends Layer.Any, const Items extends NodeList, const T extends Tier | undefined = undefined, >( input: NodeInput, ): 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, tier: input.tier, } } export function group( dependencies: Items, ): Node, Error> { return { kind: "group", name: "group", dependencies } } type AllowedTierNames = Names extends readonly [ infer Head extends string, ...infer Tail extends readonly string[], ] ? Head extends Name ? Head | Tail[number] : AllowedTierNames : never type NodeInTiers = Node> export interface Tiers { readonly names: Names readonly values: { readonly [K in Names[number]]: Tier } readonly make: ( name: Name, ) => < const Implementation extends Layer.Any, const Items extends NodeList>>, >( input: DistributiveOmit>, "tier">, ) => Node, Layer.Error | Error, Tier> } export function tiers(names: Names): Tiers { const values = Object.fromEntries(names.map((name) => [name, makeTier(name)])) as Tiers["values"] return { names, values, make: ((name: Names[number]) => (input: DistributiveOmit, "tier">) => make({ ...input, tier: values[name] })) as Tiers["make"], } } const defaultTiers = tiers(["untiered"]) const untiered = defaultTiers.values.untiered export type Replacement = { readonly source: Layer.Any readonly replacement: Layer.Any } type CheckReplacementErrors = [Exclude] extends [never] ? unknown : { readonly "New replacement errors": Exclude } export function replace( source: Layer.Layer, replacement: Layer.Layer, E2, never> & CheckReplacementErrors>, ): Replacement { return { source, replacement } } export function buildLayer< A, E, const Names extends readonly [string, ...string[]] = readonly ["untiered"], const Built extends Layer.Any = Layer.Layer, >( node: Node, options?: { readonly tiers?: Tiers readonly buildTier?: (tier: Names[number], layers: readonly Layer.Any[]) => Built readonly replacements?: readonly Replacement[] }, ): Layer.Layer, E | Layer.Error, never> { const tiers = options?.tiers ?? (defaultTiers as unknown as Tiers) const replacementMap = new Map(options?.replacements?.map((item) => [item.source, item.replacement])) const plans = plan(node, tiers, replacementMap) const layers: RuntimeLayer[] = tiers.names.map((name) => { const tier = tiers.values[name as Names[number]] const layers = plans.get(tier) ?? [] return (options?.buildTier?.(name, layers) ?? combine(layers)) as RuntimeLayer }) if (layers.length === 0) return Layer.empty as never return layers.slice(1).reduce((result, layer) => result.pipe(Layer.provideMerge(layer)), layers[0]) as never } export function combine(layers: readonly Layer.Any[]): RuntimeLayer { return layers.reduce( (result, layer) => (layer as RuntimeLayer).pipe(Layer.provideMerge(result)), Layer.empty as RuntimeLayer, ) } function plan( root: AnyNode, tiers: Tiers, replacements: ReadonlyMap, ) { const indexes = new Map(tiers.names.map((name, index) => [tiers.values[name], index])) const plans = new Map() const activeImplementations = new Map>() const serviceTiers = new Map() const visiting = new Set() const stack: AnyNode[] = [] const boundaryVisited = new Map>() const boundaryServices = new Map>() const validateBoundary = (node: AnyNode, origin: Tier) => { const checked = boundaryVisited.get(node) ?? new Set() boundaryVisited.set(node, checked) if (checked.has(origin)) return false checked.add(origin) const services = boundaryServices.get(origin) ?? new Map() boundaryServices.set(origin, services) const key = node.name const existing = services.get(key) if (existing && existing !== node) { throw new Error(`Tier ${origin} has conflicting implementations for ${key}`) } services.set(key, node) return true } const visit = (node: AnyNode, currentTier?: Tier, origins: readonly Tier[] = []) => { if (node.kind === "group") { node.dependencies.forEach((dependency) => visit(dependency, currentTier, origins)) return } const tier = node.tier ?? untiered if (!indexes.has(tier)) throw new Error(`Node ${node.name} is not in the tier configuration`) const key = node.name const serviceTier = serviceTiers.get(key) if (serviceTier && serviceTier !== tier) { throw new Error(`Service ${key} belongs to both tier ${serviceTier} and tier ${tier}`) } serviceTiers.set(key, tier) const nextOrigins = [...origins] if (currentTier) { const current = indexes.get(currentTier)! const required = indexes.get(tier)! if (required < current) { throw new Error(`Tier ${currentTier} cannot depend on lower tier ${tier}`) } if (required > current) nextOrigins.push(currentTier) } const unseenOrigins = nextOrigins.filter((origin) => validateBoundary(node, origin)) // A node may need to be emitted more than once because the final output is a // flat list of layers applied with Layer.provideMerge. If another node for // the same service was emitted afterward, this node is no longer the active // implementation for subsequent consumers. Re-emitting restores the intended // implementation ordering while Effect memoization avoids reacquiring the layer. const implementations = activeImplementations.get(tier) ?? new Map() activeImplementations.set(tier, implementations) if (implementations.get(key) === node && unseenOrigins.length === 0) return if (visiting.has(node)) { const start = stack.indexOf(node) throw new Error( `Cycle detected in layer graph: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`, ) } visiting.add(node) stack.push(node) try { node.dependencies.forEach((dependency) => visit(dependency, tier, unseenOrigins)) const layers = plans.get(tier) ?? [] plans.set(tier, layers) layers.push(replacements.get(node.implementation!) ?? node.implementation!) implementations.set(key, node) } finally { stack.pop() visiting.delete(node) } } visit(root) return plans } function requireTier(node: AnyNode, indexes: ReadonlyMap) { if (!node.tier || !indexes.has(node.tier)) throw new Error(`Node ${node.name} is not in the tier configuration`) } export * as LayerNode from "./layer-node"