1- import { Layer } from "effect"
1+ import { Brand , Context , Layer } from "effect"
22
33type RuntimeLayer = Layer . Layer < never , unknown , unknown >
4- type AnyNode = Node < unknown , unknown >
5- type NodeList = readonly [ ] | readonly [ AnyNode , ...AnyNode [ ] ]
6- type Output < Item > = [ Item ] extends [ never ] ? never : Item extends Node < infer A , unknown > ? A : never
7- type Error < Item > = [ Item ] extends [ never ] ? never : Item extends Node < unknown , infer E > ? E : never
4+ type AnyNode = Node < unknown , unknown , any >
5+ type NodeList < Item extends AnyNode = AnyNode > = readonly [ ] | readonly [ Item , ...Item [ ] ]
6+ type Output < Item > = [ Item ] extends [ never ] ? never : Item extends Node < infer A , unknown , any > ? A : never
7+ type Error < Item > = [ Item ] extends [ never ] ? never : Item extends Node < unknown , infer E , any > ? E : never
88type Missing < Required , Dependencies extends NodeList > = Exclude < Required , Output < Dependencies [ number ] > >
99type CheckDependencies < Implementation extends Layer . Any , Dependencies extends NodeList > = [
1010 Missing < Layer . Services < Implementation > , Dependencies > ,
@@ -14,89 +14,235 @@ type CheckDependencies<Implementation extends Layer.Any, Dependencies extends No
1414declare const $OutputType : unique symbol
1515declare const $ErrorType : unique symbol
1616
17- export type Node < A , E = never > = {
17+ export type Tier < Name extends string = string > = Name & Brand . Brand < "LayerNode.Tier" >
18+
19+ const makeTier = Brand . nominal < Tier > ( )
20+
21+ export type Node < A , E = never , T extends Tier | undefined = undefined > = {
1822 readonly kind : "layer" | "group"
23+ readonly name : string
24+ readonly service ?: Context . Service . Any
1925 readonly implementation ?: Layer . Any
2026 readonly dependencies : readonly AnyNode [ ]
27+ readonly tier ?: T
2128 readonly [ $OutputType ] ?: ( ) => A
2229 readonly [ $ErrorType ] ?: ( ) => E
2330}
2431
25- export function make < const Implementation extends Layer . Any , const Items extends NodeList > (
26- implementation : Implementation ,
27- dependencies : Items & CheckDependencies < Implementation , NoInfer < Items > > ,
28- ) : Node < Layer . Success < Implementation > , Layer . Error < Implementation > | Error < Items [ number ] > > {
29- return { kind : "layer" , implementation : implementation as Layer . Any , dependencies }
32+ type NodeIdentity =
33+ | { readonly service : Context . Service . Any ; readonly name ?: never }
34+ | { readonly name : string ; readonly service ?: never }
35+ type DistributiveOmit < A , K extends PropertyKey > = A extends unknown ? Omit < A , K > : never
36+
37+ type NodeInput <
38+ Implementation extends Layer . Any ,
39+ Items extends NodeList ,
40+ T extends Tier | undefined = undefined ,
41+ > = NodeIdentity & {
42+ readonly layer : Implementation
43+ readonly deps : Items & CheckDependencies < Implementation , NoInfer < Items > >
44+ readonly tier ?: T
45+ }
46+
47+ export function make <
48+ const Implementation extends Layer . Any ,
49+ const Items extends NodeList ,
50+ const T extends Tier | undefined = undefined ,
51+ > (
52+ input : NodeInput < Implementation , Items , T > ,
53+ ) : Node < Layer . Success < Implementation > , Layer . Error < Implementation > | Error < Items [ number ] > , T > {
54+ return {
55+ kind : "layer" ,
56+ name : input . service !== undefined ? input . service . key : input . name ,
57+ service : input . service ,
58+ implementation : input . layer ,
59+ dependencies : input . deps ,
60+ tier : input . tier ,
61+ }
3062}
3163
3264export function group < const Items extends NodeList > (
3365 dependencies : Items ,
3466) : Node < Output < Items [ number ] > , Error < Items [ number ] > > {
35- return { kind : "group" , dependencies }
67+ return { kind : "group" , name : "group" , dependencies }
3668}
3769
38- export type Replacement < A = unknown > = {
39- readonly source : Node < A , unknown >
40- readonly replacement : Node < A , unknown >
70+ type AllowedTierNames < Names extends readonly string [ ] , Name extends Names [ number ] > = Names extends readonly [
71+ infer Head extends string ,
72+ ...infer Tail extends readonly string [ ] ,
73+ ]
74+ ? Head extends Name
75+ ? Head | Tail [ number ]
76+ : AllowedTierNames < Tail , Name >
77+ : never
78+
79+ type NodeInTiers < Names extends string > = Node < unknown , unknown , Tier < Names > >
80+
81+ export interface Tiers < Names extends readonly [ string , ...string [ ] ] > {
82+ readonly names : Names
83+ readonly values : { readonly [ K in Names [ number ] ] : Tier < K > }
84+ readonly make : < Name extends Names [ number ] > (
85+ name : Name ,
86+ ) => <
87+ const Implementation extends Layer . Any ,
88+ const Items extends NodeList < NodeInTiers < AllowedTierNames < Names , Name > > > ,
89+ > (
90+ input : DistributiveOmit < NodeInput < Implementation , Items , Tier < Name > > , "tier" > ,
91+ ) => Node < Layer . Success < Implementation > , Layer . Error < Implementation > | Error < Items [ number ] > , Tier < Name > >
92+ }
93+
94+ export function tiers < const Names extends readonly [ string , ...string [ ] ] > ( names : Names ) : Tiers < Names > {
95+ const values = Object . fromEntries ( names . map ( ( name ) => [ name , makeTier ( name ) ] ) ) as Tiers < Names > [ "values" ]
96+ return {
97+ names,
98+ values,
99+ make : ( ( name : Names [ number ] ) => ( input : DistributiveOmit < NodeInput < Layer . Any , NodeList , Tier > , "tier" > ) =>
100+ make ( { ...input , tier : values [ name ] } ) ) as Tiers < Names > [ "make" ] ,
101+ }
102+ }
103+
104+ const defaultTiers = tiers ( [ "untiered" ] )
105+ const untiered = defaultTiers . values . untiered
106+
107+ export type Replacement = {
108+ readonly source : Layer . Any
109+ readonly replacement : Layer . Any
41110}
42111
43112type CheckReplacementErrors < SourceError , ReplacementError > = [ Exclude < ReplacementError , SourceError > ] extends [ never ]
44113 ? unknown
45114 : { readonly "New replacement errors" : Exclude < ReplacementError , SourceError > }
46115
47- export function replaceWithNode < A , E , E2 > (
48- source : Node < A , E > ,
49- replacement : Node < NoInfer < A > , E2 > & CheckReplacementErrors < E , NoInfer < E2 > > ,
50- ) : Replacement < A > {
116+ export function replace < A , E , R , E2 > (
117+ source : Layer . Layer < A , E , R > ,
118+ replacement : Layer . Layer < NoInfer < A > , E2 , never > & CheckReplacementErrors < E , NoInfer < E2 > > ,
119+ ) : Replacement {
51120 return { source, replacement }
52121}
53122
54- export function replace < A , E , E2 > (
55- source : Node < A , E > ,
56- replacement : Layer . Layer < NoInfer < A > , E2 , never > & CheckReplacementErrors < E , NoInfer < E2 > > ,
57- ) : Replacement < A > {
58- return { source, replacement : make ( replacement as Layer . Layer < A , E2 > , [ ] ) }
123+ export function buildLayer <
124+ A ,
125+ E ,
126+ const Names extends readonly [ string , ...string [ ] ] = readonly [ "untiered" ] ,
127+ const Built extends Layer . Any = Layer . Layer < never , never , never > ,
128+ > (
129+ node : Node < A , E , any > ,
130+ options ?: {
131+ readonly tiers ?: Tiers < Names >
132+ readonly buildTier ?: ( tier : Names [ number ] , layers : readonly Layer . Any [ ] ) => Built
133+ readonly replacements ?: readonly Replacement [ ]
134+ } ,
135+ ) : Layer . Layer < A | Layer . Success < Built > , E | Layer . Error < Built > , never > {
136+ const tiers = options ?. tiers ?? ( defaultTiers as unknown as Tiers < Names > )
137+ const replacementMap = new Map ( options ?. replacements ?. map ( ( item ) => [ item . source , item . replacement ] ) )
138+ const plans = plan ( node , tiers , replacementMap )
139+ const layers : RuntimeLayer [ ] = tiers . names . map ( ( name ) => {
140+ const tier = tiers . values [ name as Names [ number ] ]
141+ const layers = plans . get ( tier ) ?? [ ]
142+ return ( options ?. buildTier ?.( name , layers ) ?? combine ( layers ) ) as RuntimeLayer
143+ } )
144+ if ( layers . length === 0 ) return Layer . empty as never
145+ return layers . slice ( 1 ) . reduce ( ( result , layer ) => result . pipe ( Layer . provideMerge ( layer ) ) , layers [ 0 ] ) as never
146+ }
147+
148+ export function combine ( layers : readonly Layer . Any [ ] ) : RuntimeLayer {
149+ return layers . reduce < RuntimeLayer > (
150+ ( result , layer ) => ( layer as RuntimeLayer ) . pipe ( Layer . provideMerge ( result ) ) ,
151+ Layer . empty as RuntimeLayer ,
152+ )
59153}
60154
61- export function buildLayer < A , E > ( node : Node < A , E > , options ?: { readonly replacements ?: readonly Replacement [ ] } ) {
62- const replacements = new Map ( options ?. replacements ?. map ( ( item ) => [ item . source , item . replacement ] ) )
63- const cache = new Map < AnyNode , RuntimeLayer > ( )
155+ function plan (
156+ root : AnyNode ,
157+ tiers : Tiers < readonly [ string , ...string [ ] ] > ,
158+ replacements : ReadonlyMap < Layer . Any , Layer . Any > ,
159+ ) {
160+ const indexes = new Map ( tiers . names . map ( ( name , index ) => [ tiers . values [ name ] , index ] ) )
161+ const plans = new Map < Tier , Layer . Any [ ] > ( )
162+ const activeImplementations = new Map < Tier , Map < string , AnyNode > > ( )
163+ const serviceTiers = new Map < string , Tier > ( )
64164 const visiting = new Set < AnyNode > ( )
65165 const stack : AnyNode [ ] = [ ]
66- const ids = new Map < AnyNode , number > ( )
166+ const boundaryVisited = new Map < AnyNode , Set < Tier > > ( )
167+ const boundaryServices = new Map < Tier , Map < string , AnyNode > > ( )
168+
169+ const validateBoundary = ( node : AnyNode , origin : Tier ) => {
170+ const checked = boundaryVisited . get ( node ) ?? new Set < Tier > ( )
171+ boundaryVisited . set ( node , checked )
172+ if ( checked . has ( origin ) ) return false
173+ checked . add ( origin )
174+ const services = boundaryServices . get ( origin ) ?? new Map < string , AnyNode > ( )
175+ boundaryServices . set ( origin , services )
176+ const key = node . name
177+ const existing = services . get ( key )
178+ if ( existing && existing !== node ) {
179+ throw new Error ( `Tier ${ origin } has conflicting implementations for ${ key } ` )
180+ }
181+ services . set ( key , node )
182+ return true
183+ }
184+
185+ const visit = ( node : AnyNode , currentTier ?: Tier , origins : readonly Tier [ ] = [ ] ) => {
186+ if ( node . kind === "group" ) {
187+ node . dependencies . forEach ( ( dependency ) => visit ( dependency , currentTier , origins ) )
188+ return
189+ }
190+
191+ const tier = node . tier ?? untiered
192+ if ( ! indexes . has ( tier ) ) throw new Error ( `Node ${ node . name } is not in the tier configuration` )
193+ const key = node . name
194+ const serviceTier = serviceTiers . get ( key )
195+ if ( serviceTier && serviceTier !== tier ) {
196+ throw new Error ( `Service ${ key } belongs to both tier ${ serviceTier } and tier ${ tier } ` )
197+ }
198+ serviceTiers . set ( key , tier )
199+ const nextOrigins = [ ...origins ]
200+ if ( currentTier ) {
201+ const current = indexes . get ( currentTier ) !
202+ const required = indexes . get ( tier ) !
203+ if ( required < current ) {
204+ throw new Error ( `Tier ${ currentTier } cannot depend on lower tier ${ tier } ` )
205+ }
206+ if ( required > current ) nextOrigins . push ( currentTier )
207+ }
208+ const unseenOrigins = nextOrigins . filter ( ( origin ) => validateBoundary ( node , origin ) )
209+
210+ // A node may need to be emitted more than once because the final output is a
211+ // flat list of layers applied with Layer.provideMerge. If another node for
212+ // the same service was emitted afterward, this node is no longer the active
213+ // implementation for subsequent consumers. Re-emitting restores the intended
214+ // implementation ordering while Effect memoization avoids reacquiring the layer.
215+ const implementations = activeImplementations . get ( tier ) ?? new Map < string , AnyNode > ( )
216+ activeImplementations . set ( tier , implementations )
217+ if ( implementations . get ( key ) === node && unseenOrigins . length === 0 ) return
67218
68- const visit = ( input : AnyNode ) : RuntimeLayer => {
69- const node = replacements . get ( input ) ?? input
70- const cached = cache . get ( node )
71- if ( cached ) return cached
72219 if ( visiting . has ( node ) ) {
73220 const start = stack . indexOf ( node )
74- const cycle = [ ...stack . slice ( start ) , node ] . map ( ( item ) => `${ item . kind } #${ ids . get ( item ) } ` ) . join ( " -> " )
75- throw new Error ( `Cycle detected in app graph: ${ cycle } ` )
221+ throw new Error (
222+ `Cycle detected in layer graph: ${ [ ...stack . slice ( start ) , node ] . map ( ( item ) => item . name ) . join ( " -> " ) } ` ,
223+ )
76224 }
77- if ( ! ids . has ( node ) ) ids . set ( node , ids . size + 1 )
225+
78226 visiting . add ( node )
79227 stack . push ( node )
80228 try {
81- const dependencies = node . dependencies . map ( visit )
82- const nonEmpty = dependencies as [ RuntimeLayer , ...RuntimeLayer [ ] ]
83- const result =
84- node . kind === "group"
85- ? dependencies . length === 0
86- ? Layer . empty
87- : Layer . mergeAll ( ...nonEmpty )
88- : dependencies . length === 0
89- ? ( node . implementation as RuntimeLayer )
90- : Layer . provide ( node . implementation as RuntimeLayer , nonEmpty )
91- cache . set ( node , result )
92- return result
229+ node . dependencies . forEach ( ( dependency ) => visit ( dependency , tier , unseenOrigins ) )
230+ const layers = plans . get ( tier ) ?? [ ]
231+ plans . set ( tier , layers )
232+ layers . push ( replacements . get ( node . implementation ! ) ?? node . implementation ! )
233+ implementations . set ( key , node )
93234 } finally {
94235 stack . pop ( )
95236 visiting . delete ( node )
96237 }
97238 }
98239
99- return visit ( node ) as unknown as Layer . Layer < A , E , never >
240+ visit ( root )
241+ return plans
242+ }
243+
244+ function requireTier ( node : AnyNode , indexes : ReadonlyMap < Tier , number > ) {
245+ if ( ! node . tier || ! indexes . has ( node . tier ) ) throw new Error ( `Node ${ node . name } is not in the tier configuration` )
100246}
101247
102248export * as LayerNode from "./layer-node"
0 commit comments