layer-node.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import { Brand, Context, Layer } from "effect"
  2. type AnyNode = Node<unknown, unknown, any>
  3. type RuntimeLayer = Layer.Layer<never, unknown, unknown>
  4. type NodeList<Item extends AnyNode = AnyNode> = readonly [] | readonly [Item, ...Item[]]
  5. export type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown, any> ? A : never
  6. export type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E, any> ? E : never
  7. type NodeTag<Item> = [Item] extends [never] ? undefined : Item extends Node<unknown, unknown, infer T> ? T : never
  8. type Missing<Required, Dependencies extends NodeList> = Exclude<Required, Output<Dependencies[number]>>
  9. type CheckDependencies<Implementation extends Layer.Any, Dependencies extends NodeList> = [
  10. Missing<Layer.Services<Implementation>, Dependencies>,
  11. ] extends [never]
  12. ? unknown
  13. : { readonly "Missing dependencies": Missing<Layer.Services<Implementation>, Dependencies> }
  14. declare const $OutputType: unique symbol
  15. declare const $ErrorType: unique symbol
  16. export type Tag<Name extends string = string> = Name & Brand.Brand<"LayerNode.Tag">
  17. const makeTag = Brand.nominal<Tag>()
  18. export interface Node<A, E = never, T extends Tag | undefined = undefined> {
  19. readonly kind: "layer" | "unbound" | "group"
  20. readonly name: string
  21. readonly service?: Context.Service.Any
  22. readonly implementation?: Layer.Any
  23. readonly dependencies: readonly AnyNode[]
  24. readonly tag?: T
  25. readonly [$OutputType]?: () => A
  26. readonly [$ErrorType]?: () => E
  27. }
  28. type NodeIdentity =
  29. | { readonly service: Context.Service.Any; readonly name?: never }
  30. | { readonly name: string; readonly service?: never }
  31. type DistributiveOmit<A, K extends PropertyKey> = A extends unknown ? Omit<A, K> : never
  32. export type TagConfig = Readonly<Record<string, readonly string[]>>
  33. type TagNames<Config extends TagConfig> = keyof Config & string
  34. type NodeInTags<Names extends string> = Node<unknown, unknown, Tag<Names> | undefined>
  35. type CheckTags<Items extends NodeList, Names extends string> = [Exclude<Items[number], NodeInTags<Names>>] extends [
  36. never,
  37. ]
  38. ? unknown
  39. : { readonly "Invalid tag dependencies": Exclude<Items[number], NodeInTags<Names>> }
  40. export interface Tags<Config extends TagConfig> {
  41. readonly values: { readonly [Name in TagNames<Config>]: Tag<Name> }
  42. readonly make: <Name extends TagNames<Config>>(
  43. name: Name,
  44. ) => <const Implementation extends Layer.Any, const Items extends NodeList>(
  45. input: DistributiveOmit<MakeInput<Implementation, Items, Tag<Name>>, "tag"> &
  46. CheckTags<Items, Name | Extract<Config[Name][number], string>>,
  47. ) => Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, Tag<Name>>
  48. }
  49. export function tags<const Config extends { readonly [Name in keyof Config]: readonly (keyof Config & string)[] }>(
  50. config: Config,
  51. ): Tags<Config> {
  52. const names = Object.keys(config) as TagNames<Config>[]
  53. const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags<Config>["values"]
  54. return {
  55. values,
  56. make: ((name: TagNames<Config>) => (input: DistributiveOmit<MakeInput<Layer.Any, NodeList, Tag>, "tag">) =>
  57. make({ ...input, tag: values[name] })) as Tags<Config>["make"],
  58. }
  59. }
  60. // Nodes ---------------------------------------------------------------------
  61. type MakeInput<
  62. Implementation extends Layer.Any,
  63. Items extends NodeList,
  64. T extends Tag | undefined = undefined,
  65. > = NodeIdentity & {
  66. readonly layer: Implementation
  67. readonly deps: Items & CheckDependencies<Implementation, NoInfer<Items>>
  68. readonly tag?: T
  69. }
  70. export function make<
  71. const Implementation extends Layer.Any,
  72. const Items extends NodeList,
  73. const T extends Tag | undefined = undefined,
  74. >(
  75. input: MakeInput<Implementation, Items, T>,
  76. ): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>, T> {
  77. return {
  78. kind: "layer",
  79. name: input.service !== undefined ? input.service.key : input.name,
  80. service: input.service,
  81. implementation: input.layer,
  82. dependencies: input.deps,
  83. tag: input.tag,
  84. }
  85. }
  86. export function unbound<R, Shape, const T extends Tag>(service: Context.Key<R, Shape>, tag: T): Node<R, never, T> {
  87. return {
  88. kind: "unbound",
  89. name: service.key,
  90. service,
  91. dependencies: [],
  92. tag,
  93. }
  94. }
  95. export function group<const Items extends readonly AnyNode[]>(
  96. dependencies: Items,
  97. ): Node<Output<Items[number]>, Error<Items[number]>, NodeTag<Items[number]>> {
  98. return { kind: "group", name: "group", dependencies }
  99. }
  100. export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any]
  101. export type Replacements = readonly Replacement[]
  102. type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
  103. ? unknown
  104. : { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
  105. type CheckReplacement<Item> = Item extends readonly [Node<infer A, infer E, infer T>, infer Replacement]
  106. ? Replacement extends Node<NoInfer<A>, infer E2, T>
  107. ? CheckReplacementErrors<E, NoInfer<E2>>
  108. : Replacement extends Layer.Layer<NoInfer<A>, infer E2, never>
  109. ? CheckReplacementErrors<E, NoInfer<E2>>
  110. : { readonly "Invalid replacement": Replacement }
  111. : { readonly "Invalid replacement": Item }
  112. type CheckReplacements<Items extends Replacements> = {
  113. readonly [K in keyof Items]: CheckReplacement<Items[K]>
  114. }
  115. type ValidReplacements<Items extends Replacements> = Items & CheckReplacements<Items>
  116. function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) {
  117. const replacementNode = isNode(replacement)
  118. ? replacement
  119. : make({
  120. ...nodeMakeIdentity(source),
  121. layer: replacement as Layer.Layer<unknown, unknown>,
  122. deps: [],
  123. tag: source.tag,
  124. })
  125. if (source.name !== replacementNode.name) {
  126. throw new Error(`Cannot replace ${source.name} with ${replacementNode.name}`)
  127. }
  128. if (source.tag !== replacementNode.tag) {
  129. throw new Error(`Cannot replace ${source.name} across tags`)
  130. }
  131. return replacementNode
  132. }
  133. function nodeMakeIdentity(node: AnyNode): NodeIdentity {
  134. if (node.service !== undefined) return { service: node.service }
  135. return { name: node.name }
  136. }
  137. function isNode(input: Layer.Any | AnyNode): input is AnyNode {
  138. return "kind" in input && "dependencies" in input
  139. }
  140. // Tree -----------------------------------------------------------------------
  141. type Visit<Result> = (node: AnyNode, context: VisitContext<Result>) => Result
  142. type VisitContext<Result> = {
  143. readonly cache: Map<AnyNode, Result>
  144. readonly visit: (node: AnyNode) => Result
  145. }
  146. function walk<Result>(
  147. root: AnyNode,
  148. visit: Visit<Result>,
  149. options: {
  150. readonly cache?: Map<AnyNode, Result>
  151. readonly resolve?: (node: AnyNode) => AnyNode
  152. readonly detectCycles?: boolean
  153. } = {},
  154. ) {
  155. const cache = options.cache ?? new Map<AnyNode, Result>()
  156. const visiting = new Set<AnyNode>()
  157. const stack: AnyNode[] = []
  158. const recur = (node: AnyNode): Result => {
  159. const target = options.resolve?.(node) ?? node
  160. const cached = cache.get(target)
  161. if (cached !== undefined || cache.has(target)) return cached!
  162. if (options.detectCycles !== false && visiting.has(target)) {
  163. const start = stack.indexOf(target)
  164. throw new Error(
  165. `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`,
  166. )
  167. }
  168. visiting.add(target)
  169. stack.push(target)
  170. try {
  171. const result = visit(target, { cache, visit: recur })
  172. if (!cache.has(target)) cache.set(target, result)
  173. return result
  174. } finally {
  175. stack.pop()
  176. visiting.delete(target)
  177. }
  178. }
  179. return recur(root)
  180. }
  181. export function hoist<A, E, T extends Tag, const Items extends Replacements = readonly []>(
  182. root: Node<A, E, any>,
  183. tag: T,
  184. replacements?: ValidReplacements<Items>,
  185. ): {
  186. readonly node: Node<A, E>
  187. readonly hoisted: Node<unknown, E>
  188. } {
  189. const hoisted = new Map<string, AnyNode>()
  190. const replacementMap = replacementMapFrom(replacements)
  191. const node = walk<AnyNode>(
  192. root,
  193. (node, context) => {
  194. if (node.kind === "group") {
  195. return { ...node, dependencies: node.dependencies.map(context.visit) }
  196. }
  197. if (node.tag === tag) {
  198. const existing = hoisted.get(node.name)
  199. if (existing && existing !== node) {
  200. throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`)
  201. }
  202. hoisted.set(node.name, node)
  203. return group([])
  204. }
  205. if (node.kind === "unbound") {
  206. return node
  207. }
  208. return { ...node, dependencies: node.dependencies.map(context.visit) }
  209. },
  210. { resolve: (node) => replacementMap.get(node.name) ?? node },
  211. )
  212. return {
  213. node: node as Node<A, E>,
  214. hoisted: group(Array.from(hoisted.values())) as Node<unknown, E>,
  215. }
  216. }
  217. export function compile<A, E, const Items extends Replacements = readonly []>(
  218. root: Node<A, E, any>,
  219. replacements?: ValidReplacements<Items>,
  220. ): Layer.Layer<A, E> {
  221. const replacementMap = replacementMapFrom(replacements)
  222. const cache = new Map<AnyNode, RuntimeLayer>()
  223. const compileNode = (node: AnyNode) =>
  224. walk<RuntimeLayer>(
  225. node,
  226. (node, context) => {
  227. if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`)
  228. const dependencies = node.dependencies.flatMap(flatten).map(context.visit)
  229. const implementation = node.implementation! as RuntimeLayer
  230. return dependencies.length === 0
  231. ? implementation
  232. : implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
  233. },
  234. { cache, resolve: (node) => replacementMap.get(node.name) ?? node },
  235. )
  236. const layers = flatten(root).map((node) => compileNode(node))
  237. const layer = layers.reduce<RuntimeLayer>((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty)
  238. return layer as Layer.Layer<A, E>
  239. }
  240. function replacementMapFrom(replacements?: Replacements) {
  241. return (
  242. replacements?.reduce((map, [source, replacement]) => {
  243. const normalized = rewriteReplacementDependencies(replacementNode(source, replacement), map)
  244. const current = new Map([[source.name, normalized]])
  245. for (const [name, node] of map) map.set(name, rewriteReplacementDependencies(node, current))
  246. map.set(source.name, normalized)
  247. return map
  248. }, new Map<string, AnyNode>()) ?? new Map<string, AnyNode>()
  249. )
  250. }
  251. function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap<string, AnyNode>) {
  252. if (replacements.size === 0) return root
  253. const cache = new Map<AnyNode, AnyNode>()
  254. const visiting = new Set<AnyNode>()
  255. const stack: AnyNode[] = []
  256. const recur = (node: AnyNode, isRoot = false): AnyNode => {
  257. const target = isRoot ? node : (replacements.get(node.name) ?? node)
  258. const cached = cache.get(target)
  259. if (cached !== undefined || cache.has(target)) return cached!
  260. if (visiting.has(target)) {
  261. const start = stack.indexOf(target)
  262. throw new Error(
  263. `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`,
  264. )
  265. }
  266. visiting.add(target)
  267. stack.push(target)
  268. try {
  269. const dependencies = target.dependencies.map((dependency) => recur(dependency))
  270. const result = dependencies.every((dependency, index) => dependency === target.dependencies[index])
  271. ? target
  272. : { ...target, dependencies }
  273. cache.set(target, result)
  274. return result
  275. } finally {
  276. stack.pop()
  277. visiting.delete(target)
  278. }
  279. }
  280. return recur(root, true)
  281. }
  282. export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {
  283. if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`)
  284. return walk<boolean>(root, (node, context) => {
  285. if (node === source) return true
  286. return node.dependencies.some(context.visit)
  287. })
  288. }
  289. function flatten(node: AnyNode): readonly AnyNode[] {
  290. return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node]
  291. }
  292. export * as LayerNode from "./layer-node"