/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/core/src/effect/layer-node.ts
333 строки
12 KB
James Long
fix(core): rewrite replacements while hoisting layers (#35175)
03 июл 2026, 18:26
Не верифицирован
03 июл 2026, 18:26
33ed95f
Код
Авторство
О чём код?
import { Brand, Context, Layer } from "effect" type AnyNode = Node<unknown, unknown, any> type RuntimeLayer = Layer.Layer<never, unknown, unknown> type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]] export type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never export type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never type NodeTag<Item> = [Item] extends [never] ? undefined : Item extends Node<unknown, unknown, infer T> ? T : never type Missing<Required, Dependencies extends NodeList> = Exclude<Required, Output<Dependencies[number]>> type CheckDependencies<Implementation extends Layer.Any, Dependencies extends NodeList> = [ Missing<Layer.Services<Implementation>, Dependencies>, ] extends [never] ? unknown : { readonly "Missing dependencies": Missing<Layer.Services<Implementation>, Dependencies> } declare const $OutputType: unique symbol declare const $ErrorType: unique symbol export type Tag<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tag"> const makeTag = Brand.nominal<Tag>() export interface Node<A, E = never, T extends Tag | undefined = undefined> { 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, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never export type TagConfig = Readonly<Record<string, readonly string[]>> type TagNames<Config extends TagConfig> = keyof Config & string type NodeInTags<Names extends string> = Node<unknown, unknown, Tag<Names> | undefined> type CheckTags<Items extends NodeList, Names extends string> = [Exclude<Items[number], NodeInTags<Names>>] extends [ never, ] ? unknown : { readonly "Invalid tag dependencies": Exclude<Items[number], NodeInTags<Names>> } export interface Tags<Config extends TagConfig> { readonly values: { readonly [Name in TagNames<Config>]: Tag<Name> } readonly make: <Name extends TagNames<Config>>( name: Name, ) => <const Implementation extends Layer.Any, const Items extends NodeList>( input: DistributiveOmit<MakeInput<Implementation, Items, Tag<Name>>, "tag"> & CheckTags<Items, Name | Extract<Config[Name][number], string>>, ) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tag<Name>> } export function tags<const Config extends { readonly [Name in keyof Config]: readonly (keyof Config & string)[] }>( config: Config, ): Tags<Config> { const names = Object.keys(config) as TagNames<Config>[] const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags<Config>["values"] return { values, make: ((name: TagNames<Config>) => (input: DistributiveOmit<MakeInput<Layer.Any, NodeList, Tag>, "tag">) => make({ ...input, tag: values[name] })) as Tags<Config>["make"], } } // Nodes --------------------------------------------------------------------- type MakeInput< Implementation extends Layer.Any, Items extends NodeList, T extends Tag | undefined = undefined, > = NodeIdentity & { readonly layer: Implementation readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>> readonly tag?: T } export function make< const Implementation extends Layer.Any, const Items extends NodeList, const T extends Tag | undefined = undefined, >( input: MakeInput<Implementation, Items, T>, ): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, 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<R, Shape, const T extends Tag>(service: Context.Key<R, Shape>, tag: T): Node<R, never, T> { return { kind: "unbound", name: service.key, service, dependencies: [], tag, } } export function group<const Items extends readonly AnyNode[]>( dependencies: Items, ): Node<Output<Items[number]>, Error<Items[number]>, NodeTag<Items[number]>> { return { kind: "group", name: "group", dependencies } } export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any] export type Replacements = readonly Replacement[] type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never] ? unknown : { readonly "New replacement errors": Exclude<ReplacementError, SourceError> } type CheckReplacement<Item> = Item extends readonly [Node<infer A, infer E, infer T>, infer Replacement] ? Replacement extends Node<NoInfer<A>, infer E2, T> ? CheckReplacementErrors<E, NoInfer<E2>> : Replacement extends Layer.Layer<NoInfer<A>, infer E2, never> ? CheckReplacementErrors<E, NoInfer<E2>> : { readonly "Invalid replacement": Replacement } : { readonly "Invalid replacement": Item } type CheckReplacements<Items extends Replacements> = { readonly [K in keyof Items]: CheckReplacement<Items[K]> } type ValidReplacements<Items extends Replacements> = Items & CheckReplacements<Items> function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) { const replacementNode = isNode(replacement) ? replacement : make({ ...nodeMakeIdentity(source), layer: replacement as Layer.Layer<unknown, unknown>, 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<Result> = (node: AnyNode, context: VisitContext<Result>) => Result type VisitContext<Result> = { readonly cache: Map<AnyNode, Result> readonly visit: (node: AnyNode) => Result } function walk<Result>( root: AnyNode, visit: Visit<Result>, options: { readonly cache?: Map<AnyNode, Result> readonly resolve?: (node: AnyNode) => AnyNode readonly detectCycles?: boolean } = {}, ) { const cache = options.cache ?? new Map<AnyNode, Result>() const visiting = new Set<AnyNode>() 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<A, E, T extends Tag, const Items extends Replacements = readonly []>( root: Node<A, E, any>, tag: T, replacements?: ValidReplacements<Items>, ): { readonly node: Node<A, E> readonly hoisted: Node<unknown, E> } { const hoisted = new Map<string, AnyNode>() const replacementMap = replacementMapFrom(replacements) const node = walk<AnyNode>( 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 !== node) { 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<A, E>, hoisted: group(Array.from(hoisted.values())) as Node<unknown, E>, } } export function compile<A, E, const Items extends Replacements = readonly []>( root: Node<A, E, any>, replacements?: ValidReplacements<Items>, ): Layer.Layer<A, E> { const replacementMap = replacementMapFrom(replacements) const cache = new Map<AnyNode, RuntimeLayer>() const compileNode = (node: AnyNode) => walk<RuntimeLayer>( 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<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty) return layer as Layer.Layer<A, E> } 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<string, AnyNode>()) ?? new Map<string, AnyNode>() ) } function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap<string, AnyNode>) { if (replacements.size === 0) return root const cache = new Map<AnyNode, AnyNode>() const visiting = new Set<AnyNode>() 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<unknown, unknown, any>, source: AnyNode): boolean { if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`) return walk<boolean>(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"