opencode/packages/core/src/effect/layer-node/layer-node.ts

138 lines
4.9 KiB
TypeScript

import { Brand, Context, Layer } from "effect"
type AnyNode = Node<unknown, unknown, any>
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
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 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"],
}
}
export type Replacement = {
readonly source: Layer.Any
readonly replacement: Layer.Any
}
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
? unknown
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
export function replace<A, E, R, E2>(
source: Layer.Layer<A, E, R>,
replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
): Replacement {
return { source, replacement }
}
export * as LayerNode from "./layer-node"