chore: merge dev into v2 (#34788)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
This commit is contained in:
co-authored by
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
parent
932a40cfd9
commit
8c94e9005f
@@ -1,24 +1,23 @@
|
||||
import { Layer } from "effect"
|
||||
import { buildLocationServiceMap } from "../location-services"
|
||||
import { LocationServiceMap } from "../location-service-map"
|
||||
import { LayerNode } from "./layer-node"
|
||||
import { makeGlobalNode } from "./app-node"
|
||||
|
||||
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements?: readonly LayerNode.Replacement[]) {
|
||||
const replacementMap = new Map(replacements?.map((item) => [item.source, item.replacement]))
|
||||
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) {
|
||||
let allReplacements = replacements
|
||||
|
||||
if (!LayerNode.hasUnbound(root, LocationServiceMap.node)) {
|
||||
// If the location service map is not needed, we shouldn't pull it
|
||||
// in. Compile the graph normally
|
||||
return LayerNode.compile(root, replacementMap)
|
||||
// Only build the location service map if it's actually needed
|
||||
if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) {
|
||||
const locationMap = buildLocationServiceMap(replacements)
|
||||
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
||||
allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]])
|
||||
}
|
||||
|
||||
const locationMap = buildLocationServiceMap(replacementMap)
|
||||
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
|
||||
return LayerNode.compile(root, allReplacements)
|
||||
}
|
||||
|
||||
const app = LayerNode.bind(root, LocationServiceMap.node, locationMapNode)
|
||||
|
||||
return LayerNode.compile(app, replacementMap)
|
||||
function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node<unknown, unknown, any>) {
|
||||
return replacements.some(([source]) => source.name === node.name)
|
||||
}
|
||||
|
||||
export * as AppNodeBuilder from "./app-node-builder"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/
|
||||
@@ -111,20 +111,52 @@ export function group<const Items extends readonly AnyNode[]>(
|
||||
return { kind: "group", name: "group", dependencies }
|
||||
}
|
||||
|
||||
export type Replacement = {
|
||||
readonly source: Layer.Any
|
||||
readonly replacement: Layer.Any
|
||||
}
|
||||
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> }
|
||||
|
||||
export function replace<A, E, R, E2>(
|
||||
source: Layer.Layer<A, E, R>,
|
||||
replacement: Layer.Layer<NoInfer<A>, E2, NoInfer<R>> & CheckReplacementErrors<E, NoInfer<E2>>,
|
||||
): Replacement {
|
||||
return { source, replacement }
|
||||
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 -----------------------------------------------------------------------
|
||||
@@ -176,32 +208,38 @@ function walk<Result>(
|
||||
return recur(root)
|
||||
}
|
||||
|
||||
export function hoist<A, E, T extends Tag>(
|
||||
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}`)
|
||||
const node = walk<AnyNode>(
|
||||
root,
|
||||
(node, context) => {
|
||||
if (node.kind === "group") {
|
||||
return { ...node, dependencies: node.dependencies.map(context.visit) }
|
||||
}
|
||||
hoisted.set(node.name, node)
|
||||
return group([])
|
||||
}
|
||||
if (node.kind === "unbound") {
|
||||
return node
|
||||
}
|
||||
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) }
|
||||
},
|
||||
{ resolve: (node) => replacementMap.get(node.name) ?? node },
|
||||
)
|
||||
|
||||
return {
|
||||
node: node as Node<A, E>,
|
||||
@@ -209,10 +247,11 @@ export function hoist<A, E, T extends Tag>(
|
||||
}
|
||||
}
|
||||
|
||||
export function compile<A, E>(
|
||||
export function compile<A, E, const Items extends Replacements = readonly []>(
|
||||
root: Node<A, E, any>,
|
||||
replacements?: ReadonlyMap<Layer.Any, Layer.Any>,
|
||||
replacements?: ValidReplacements<Items>,
|
||||
): Layer.Layer<A, E> {
|
||||
const replacementMap = replacementMapFrom(replacements)
|
||||
const cache = new Map<AnyNode, RuntimeLayer>()
|
||||
const compileNode = (node: AnyNode) =>
|
||||
walk<RuntimeLayer>(
|
||||
@@ -220,18 +259,65 @@ export function compile<A, E>(
|
||||
(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
|
||||
const implementation = node.implementation! as RuntimeLayer
|
||||
return dependencies.length === 0
|
||||
? implementation
|
||||
: implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]]))
|
||||
},
|
||||
{ cache },
|
||||
{ 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) => {
|
||||
@@ -240,32 +326,6 @@ export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode):
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user