|
|
|
@@ -1,10 +1,11 @@
|
|
|
|
|
import { Brand, Context, Layer } from "effect"
|
|
|
|
|
|
|
|
|
|
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
|
|
|
|
|
type AnyNode = Node<unknown, unknown, any>
|
|
|
|
|
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
|
|
|
|
|
type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
|
|
|
|
|
type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
|
|
|
|
|
type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never
|
|
|
|
|
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>,
|
|
|
|
@@ -14,17 +15,17 @@ type CheckDependencies<Implementation extends Layer.Any, Dependencies extends No
|
|
|
|
|
declare const $OutputType: unique symbol
|
|
|
|
|
declare const $ErrorType: unique symbol
|
|
|
|
|
|
|
|
|
|
export type Tier<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tier">
|
|
|
|
|
export type Tag<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tag">
|
|
|
|
|
|
|
|
|
|
const makeTier = Brand.nominal<Tier>()
|
|
|
|
|
const makeTag = Brand.nominal<Tag>()
|
|
|
|
|
|
|
|
|
|
export type Node<A, E = never, T extends Tier | undefined = undefined> = {
|
|
|
|
|
readonly kind: "layer" | "group"
|
|
|
|
|
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 tier?: T
|
|
|
|
|
readonly tag?: T
|
|
|
|
|
readonly [$OutputType]?: () => A
|
|
|
|
|
readonly [$ErrorType]?: () => E
|
|
|
|
|
}
|
|
|
|
@@ -34,22 +35,55 @@ type NodeIdentity =
|
|
|
|
|
| { readonly name: string; readonly service?: never }
|
|
|
|
|
type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
|
|
|
|
|
|
|
|
|
|
type NodeInput<
|
|
|
|
|
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 Tier | undefined = undefined,
|
|
|
|
|
T extends Tag | undefined = undefined,
|
|
|
|
|
> = NodeIdentity & {
|
|
|
|
|
readonly layer: Implementation
|
|
|
|
|
readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>>
|
|
|
|
|
readonly tier?: T
|
|
|
|
|
readonly tag?: T
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function make<
|
|
|
|
|
const Implementation extends Layer.Any,
|
|
|
|
|
const Items extends NodeList,
|
|
|
|
|
const T extends Tier | undefined = undefined,
|
|
|
|
|
const T extends Tag | undefined = undefined,
|
|
|
|
|
>(
|
|
|
|
|
input: NodeInput<Implementation, Items, T>,
|
|
|
|
|
input: MakeInput<Implementation, Items, T>,
|
|
|
|
|
): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, T> {
|
|
|
|
|
return {
|
|
|
|
|
kind: "layer",
|
|
|
|
@@ -57,53 +91,26 @@ export function make<
|
|
|
|
|
service: input.service,
|
|
|
|
|
implementation: input.layer,
|
|
|
|
|
dependencies: input.deps,
|
|
|
|
|
tier: input.tier,
|
|
|
|
|
tag: input.tag,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function group<const Items extends NodeList>(
|
|
|
|
|
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]>> {
|
|
|
|
|
): Node<Output<Items[number]>, Error<Items[number]>, NodeTag<Items[number]>> {
|
|
|
|
|
return { kind: "group", name: "group", dependencies }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AllowedTierNames<Names extends readonly string[], Name extends Names[number]> = Names extends readonly [
|
|
|
|
|
infer Head extends string,
|
|
|
|
|
...infer Tail extends readonly string[],
|
|
|
|
|
]
|
|
|
|
|
? Head extends Name
|
|
|
|
|
? Head | Tail[number]
|
|
|
|
|
: AllowedTierNames<Tail, Name>
|
|
|
|
|
: never
|
|
|
|
|
|
|
|
|
|
type NodeInTiers<Names extends string> = Node<unknown, unknown, Tier<Names>>
|
|
|
|
|
|
|
|
|
|
export interface Tiers<Names extends readonly [string, ...string[]]> {
|
|
|
|
|
readonly names: Names
|
|
|
|
|
readonly values: { readonly [K in Names[number]]: Tier<K> }
|
|
|
|
|
readonly make: <Name extends Names[number]>(
|
|
|
|
|
name: Name,
|
|
|
|
|
) => <
|
|
|
|
|
const Implementation extends Layer.Any,
|
|
|
|
|
const Items extends NodeList<NodeInTiers<AllowedTierNames<Names, Name>>>,
|
|
|
|
|
>(
|
|
|
|
|
input: DistributiveOmit<NodeInput<Implementation, Items, Tier<Name>>, "tier">,
|
|
|
|
|
) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tier<Name>>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function tiers<const Names extends readonly [string, ...string[]]>(names: Names): Tiers<Names> {
|
|
|
|
|
const values = Object.fromEntries(names.map((name) => [name, makeTier(name)])) as Tiers<Names>["values"]
|
|
|
|
|
return {
|
|
|
|
|
names,
|
|
|
|
|
values,
|
|
|
|
|
make: ((name: Names[number]) => (input: DistributiveOmit<NodeInput<Layer.Any, NodeList, Tier>, "tier">) =>
|
|
|
|
|
make({ ...input, tier: values[name] })) as Tiers<Names>["make"],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultTiers = tiers(["untiered"])
|
|
|
|
|
const untiered = defaultTiers.values.untiered
|
|
|
|
|
|
|
|
|
|
export type Replacement = {
|
|
|
|
|
readonly source: Layer.Any
|
|
|
|
|
readonly replacement: Layer.Any
|
|
|
|
@@ -120,129 +127,147 @@ export function replace<A, E, R, E2>(
|
|
|
|
|
return { source, replacement }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildLayer<
|
|
|
|
|
A,
|
|
|
|
|
E,
|
|
|
|
|
const Names extends readonly [string, ...string[]] = readonly ["untiered"],
|
|
|
|
|
const Built extends Layer.Any = Layer.Layer<never, never, never>,
|
|
|
|
|
>(
|
|
|
|
|
node: Node<A, E, any>,
|
|
|
|
|
options?: {
|
|
|
|
|
readonly tiers?: Tiers<Names>
|
|
|
|
|
readonly buildTier?: (tier: Names[number], layers: readonly Layer.Any[]) => Built
|
|
|
|
|
readonly replacements?: readonly Replacement[]
|
|
|
|
|
},
|
|
|
|
|
): Layer.Layer<A | Layer.Success<Built>, E | Layer.Error<Built>, never> {
|
|
|
|
|
const tiers = options?.tiers ?? (defaultTiers as unknown as Tiers<Names>)
|
|
|
|
|
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
|
|
|
|
|
// Tree -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
type Visit<Result> = (node: AnyNode, context: VisitContext<Result>) => Result
|
|
|
|
|
|
|
|
|
|
type VisitContext<Result> = {
|
|
|
|
|
readonly cache: Map<AnyNode, Result>
|
|
|
|
|
readonly visit: (node: AnyNode) => Result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function combine(layers: readonly Layer.Any[]): RuntimeLayer {
|
|
|
|
|
return layers.reduce<RuntimeLayer>(
|
|
|
|
|
(result, layer) => (layer as RuntimeLayer).pipe(Layer.provideMerge(result)),
|
|
|
|
|
Layer.empty as RuntimeLayer,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function plan(
|
|
|
|
|
function walk<Result>(
|
|
|
|
|
root: AnyNode,
|
|
|
|
|
tiers: Tiers<readonly [string, ...string[]]>,
|
|
|
|
|
replacements: ReadonlyMap<Layer.Any, Layer.Any>,
|
|
|
|
|
visit: Visit<Result>,
|
|
|
|
|
options: {
|
|
|
|
|
readonly cache?: Map<AnyNode, Result>
|
|
|
|
|
readonly resolve?: (node: AnyNode) => AnyNode
|
|
|
|
|
readonly detectCycles?: boolean
|
|
|
|
|
} = {},
|
|
|
|
|
) {
|
|
|
|
|
const indexes = new Map(tiers.names.map((name, index) => [tiers.values[name], index]))
|
|
|
|
|
const plans = new Map<Tier, Layer.Any[]>()
|
|
|
|
|
const activeImplementations = new Map<Tier, Map<string, AnyNode>>()
|
|
|
|
|
const serviceTiers = new Map<string, Tier>()
|
|
|
|
|
const cache = options.cache ?? new Map<AnyNode, Result>()
|
|
|
|
|
const visiting = new Set<AnyNode>()
|
|
|
|
|
const stack: AnyNode[] = []
|
|
|
|
|
const boundaryVisited = new Map<AnyNode, Set<Tier>>()
|
|
|
|
|
const boundaryServices = new Map<Tier, Map<string, AnyNode>>()
|
|
|
|
|
|
|
|
|
|
const validateBoundary = (node: AnyNode, origin: Tier) => {
|
|
|
|
|
const checked = boundaryVisited.get(node) ?? new Set<Tier>()
|
|
|
|
|
boundaryVisited.set(node, checked)
|
|
|
|
|
if (checked.has(origin)) return false
|
|
|
|
|
checked.add(origin)
|
|
|
|
|
const services = boundaryServices.get(origin) ?? new Map<string, AnyNode>()
|
|
|
|
|
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 recur = (node: AnyNode): Result => {
|
|
|
|
|
const target = options.resolve?.(node) ?? node
|
|
|
|
|
const cached = cache.get(target)
|
|
|
|
|
if (cached !== undefined || cache.has(target)) return cached!
|
|
|
|
|
|
|
|
|
|
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<string, AnyNode>()
|
|
|
|
|
activeImplementations.set(tier, implementations)
|
|
|
|
|
if (implementations.get(key) === node && unseenOrigins.length === 0) return
|
|
|
|
|
|
|
|
|
|
if (visiting.has(node)) {
|
|
|
|
|
const start = stack.indexOf(node)
|
|
|
|
|
if (options.detectCycles !== false && visiting.has(target)) {
|
|
|
|
|
const start = stack.indexOf(target)
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Cycle detected in layer graph: ${[...stack.slice(start), node].map((item) => item.name).join(" -> ")}`,
|
|
|
|
|
`Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visiting.add(node)
|
|
|
|
|
stack.push(node)
|
|
|
|
|
visiting.add(target)
|
|
|
|
|
stack.push(target)
|
|
|
|
|
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)
|
|
|
|
|
const result = visit(target, { cache, visit: recur })
|
|
|
|
|
if (!cache.has(target)) cache.set(target, result)
|
|
|
|
|
return result
|
|
|
|
|
} finally {
|
|
|
|
|
stack.pop()
|
|
|
|
|
visiting.delete(node)
|
|
|
|
|
visiting.delete(target)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visit(root)
|
|
|
|
|
return plans
|
|
|
|
|
return recur(root)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requireTier(node: AnyNode, indexes: ReadonlyMap<Tier, number>) {
|
|
|
|
|
if (!node.tier || !indexes.has(node.tier)) throw new Error(`Node ${node.name} is not in the tier configuration`)
|
|
|
|
|
export function hoist<A, E, T extends Tag>(
|
|
|
|
|
root: Node<A, E, any>,
|
|
|
|
|
tag: T,
|
|
|
|
|
): {
|
|
|
|
|
readonly node: Node<A, E>
|
|
|
|
|
readonly hoisted: Node<unknown, E>
|
|
|
|
|
} {
|
|
|
|
|
const hoisted = new Map<string, AnyNode>()
|
|
|
|
|
|
|
|
|
|
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, node)
|
|
|
|
|
return group([])
|
|
|
|
|
}
|
|
|
|
|
if (node.kind === "unbound") {
|
|
|
|
|
return node
|
|
|
|
|
}
|
|
|
|
|
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
node: node as Node<A, E>,
|
|
|
|
|
hoisted: group(Array.from(hoisted.values())) as Node<unknown, E>,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function compile<A, E>(
|
|
|
|
|
root: Node<A, E, any>,
|
|
|
|
|
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
|
|
|
|
): Layer.Layer<A, E> {
|
|
|
|
|
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 = (replacements?.get(node.implementation!) ?? node.implementation!) as RuntimeLayer
|
|
|
|
|
return dependencies.length === 0
|
|
|
|
|
? implementation
|
|
|
|
|
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
|
|
|
|
},
|
|
|
|
|
{ cache },
|
|
|
|
|
)
|
|
|
|
|
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>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function bind<A, E, T extends Tag | undefined>(
|
|
|
|
|
root: Node<A, E, T>,
|
|
|
|
|
source: AnyNode,
|
|
|
|
|
replacement: AnyNode,
|
|
|
|
|
): Node<A, E, T> {
|
|
|
|
|
if (source.kind !== "unbound") throw new Error(`Cannot bind non-unbound layer node: ${source.name}`)
|
|
|
|
|
if (source.name !== replacement.name) {
|
|
|
|
|
throw new Error(`Cannot bind ${source.name} to ${replacement.name}`)
|
|
|
|
|
}
|
|
|
|
|
if (source.tag !== replacement.tag) {
|
|
|
|
|
throw new Error(`Cannot bind ${source.name} across tags`)
|
|
|
|
|
}
|
|
|
|
|
return walk<AnyNode>(
|
|
|
|
|
root,
|
|
|
|
|
(target, context) => {
|
|
|
|
|
if (target.kind === "unbound") return target
|
|
|
|
|
const dependencies: AnyNode[] = []
|
|
|
|
|
const clone = { ...target, dependencies }
|
|
|
|
|
context.cache.set(target, clone)
|
|
|
|
|
dependencies.push(...target.dependencies.map(context.visit))
|
|
|
|
|
return clone
|
|
|
|
|
},
|
|
|
|
|
{ detectCycles: false, resolve: (node) => (node === source ? replacement : node) },
|
|
|
|
|
) as Node<A, E, T>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function flatten(node: AnyNode): readonly AnyNode[] {
|
|
|
|
|
return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export * as LayerNode from "./layer-node"
|
|
|
|
|