refactor(core): separate out location node functionality and integrate into v2 (#34119)
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class A extends Context.Service<A, {}>()("test/TierA") {}
|
||||
class B extends Context.Service<B, {}>()("test/TierB") {}
|
||||
|
||||
const tiers = LayerNode.tiers(["request", "global"])
|
||||
const request = tiers.make("request")
|
||||
const global = tiers.make("global")
|
||||
const globalA = global({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
|
||||
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
||||
|
||||
request({ service: B, layer: bLayer, deps: [globalA] })
|
||||
|
||||
// @ts-expect-error Global cannot depend on request
|
||||
global({ service: B, layer: bLayer, deps: [request({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })] })
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
@@ -1,169 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/TierValue") {}
|
||||
class Result extends Context.Service<Result, { readonly value: string }>()("test/TierResult") {}
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/TierLeft") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/TierRight") {}
|
||||
class Last extends Context.Service<Last, { readonly value: string }>()("test/TierLast") {}
|
||||
|
||||
test("builds tiers with a custom builder", async () => {
|
||||
let locationBuilds = 0
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const value = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "value" })), deps: [] })
|
||||
const result = location({
|
||||
service: Result,
|
||||
layer: Layer.effect(
|
||||
Result,
|
||||
Effect.gen(function* () {
|
||||
return Result.of({ value: (yield* Value).value })
|
||||
}),
|
||||
),
|
||||
deps: [value],
|
||||
})
|
||||
const layer = LayerNode.buildLayer(LayerNode.group([result]), {
|
||||
tiers,
|
||||
buildTier: (tier, layers) => {
|
||||
if (tier !== "location") return LayerNode.combine(layers)
|
||||
locationBuilds++
|
||||
return LayerNode.combine(layers).pipe(Layer.fresh)
|
||||
},
|
||||
})
|
||||
const program = Effect.gen(function* () {
|
||||
return (yield* Result).value
|
||||
}).pipe(Effect.provide(layer))
|
||||
|
||||
expect(await Effect.runPromise(program)).toBe("value")
|
||||
expect(locationBuilds).toBe(1)
|
||||
})
|
||||
|
||||
test("rejects conflicting higher-tier service implementations", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const first = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
|
||||
const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
|
||||
const left = location({
|
||||
service: Left,
|
||||
layer: Layer.effect(Left, Effect.as(Value, Left.of({ value: "left" }))),
|
||||
deps: [first],
|
||||
})
|
||||
const right = location({
|
||||
service: Right,
|
||||
layer: Layer.effect(Right, Effect.as(Value, Right.of({ value: "right" }))),
|
||||
deps: [second],
|
||||
})
|
||||
|
||||
expect(() => LayerNode.buildLayer(LayerNode.group([left, right]), { tiers })).toThrow(
|
||||
"conflicting implementations for test/TierValue",
|
||||
)
|
||||
})
|
||||
|
||||
test("validates tier dependencies through groups", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
||||
const invalid = global({
|
||||
service: Result,
|
||||
layer: Layer.effect(
|
||||
Result,
|
||||
Effect.map(Value, (value) => Result.of({ value: value.value })),
|
||||
),
|
||||
deps: [LayerNode.group([local])],
|
||||
})
|
||||
|
||||
expect(() => LayerNode.buildLayer(invalid, { tiers })).toThrow("Tier global cannot depend on lower tier location")
|
||||
})
|
||||
|
||||
test("validates shared groups in each consumer tier", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
||||
const shared = LayerNode.group([local])
|
||||
const valid = location({
|
||||
service: Left,
|
||||
layer: Layer.effect(
|
||||
Left,
|
||||
Effect.map(Value, (value) => Left.of({ value: value.value })),
|
||||
),
|
||||
deps: [shared],
|
||||
})
|
||||
const invalid = global({
|
||||
service: Result,
|
||||
layer: Layer.effect(
|
||||
Result,
|
||||
Effect.map(Value, (value) => Result.of({ value: value.value })),
|
||||
),
|
||||
deps: [shared],
|
||||
})
|
||||
|
||||
expect(() => LayerNode.buildLayer(LayerNode.group([valid, invalid]), { tiers })).toThrow(
|
||||
"Tier global cannot depend on lower tier location",
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects a service assigned to multiple tiers", () => {
|
||||
const tiers = LayerNode.tiers(["location", "global"])
|
||||
const global = tiers.make("global")
|
||||
const location = tiers.make("location")
|
||||
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
|
||||
const shared = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "global" })), deps: [] })
|
||||
|
||||
expect(() => LayerNode.buildLayer(LayerNode.group([local, shared]), { tiers })).toThrow(
|
||||
"Service test/TierValue belongs to both tier location and tier global",
|
||||
)
|
||||
})
|
||||
|
||||
test("rebinds same-tier providers without reacquiring them", async () => {
|
||||
let firstAcquisitions = 0
|
||||
const tiers = LayerNode.tiers(["global"])
|
||||
const global = tiers.make("global")
|
||||
const first = global({
|
||||
service: Value,
|
||||
layer: Layer.effect(
|
||||
Value,
|
||||
Effect.sync(() => {
|
||||
firstAcquisitions++
|
||||
return Value.of({ value: "first" })
|
||||
}),
|
||||
),
|
||||
deps: [],
|
||||
})
|
||||
const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
|
||||
const left = global({
|
||||
service: Left,
|
||||
layer: Layer.effect(
|
||||
Left,
|
||||
Effect.map(Value, (value) => Left.of({ value: value.value })),
|
||||
),
|
||||
deps: [first],
|
||||
})
|
||||
const right = global({
|
||||
service: Right,
|
||||
layer: Layer.effect(
|
||||
Right,
|
||||
Effect.map(Value, (value) => Right.of({ value: value.value })),
|
||||
),
|
||||
deps: [second],
|
||||
})
|
||||
const last = global({
|
||||
service: Last,
|
||||
layer: Layer.effect(
|
||||
Last,
|
||||
Effect.map(Value, (value) => Last.of({ value: value.value })),
|
||||
),
|
||||
deps: [first],
|
||||
})
|
||||
const layer = LayerNode.buildLayer(LayerNode.group([left, right, last]), { tiers })
|
||||
const values = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
|
||||
}).pipe(Effect.provide(layer))
|
||||
|
||||
expect(await Effect.runPromise(values)).toEqual(["first", "second", "first"])
|
||||
expect(firstAcquisitions).toBe(1)
|
||||
})
|
||||
@@ -1,66 +0,0 @@
|
||||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class A extends Context.Service<A, {}>()("test/LayerNodeA") {}
|
||||
class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
|
||||
class C extends Context.Service<C, {}>()("test/LayerNodeC") {}
|
||||
class LayerError {
|
||||
readonly _tag = "LayerError"
|
||||
}
|
||||
class OtherError {
|
||||
readonly _tag = "OtherError"
|
||||
}
|
||||
|
||||
const tiers = LayerNode.tiers(["app"])
|
||||
const make = tiers.make("app")
|
||||
const aLayer = Layer.succeed(A, A.of({}))
|
||||
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
|
||||
const cLayer = Layer.effect(
|
||||
C,
|
||||
Effect.gen(function* () {
|
||||
yield* A
|
||||
yield* B
|
||||
return C.of({})
|
||||
}),
|
||||
)
|
||||
const failingA = Layer.effect(A, Effect.fail(new LayerError()))
|
||||
const a = make({ service: A, layer: aLayer, deps: [] })
|
||||
const b = make({ service: B, layer: bLayer, deps: [a] })
|
||||
const c = make({ service: C, layer: cLayer, deps: [a, b] })
|
||||
const failing = make({ service: A, layer: failingA, deps: [] })
|
||||
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
|
||||
|
||||
make({ name: "manual-a", layer: aLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error A node must have a service or name
|
||||
make({ layer: aLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error Service and name are mutually exclusive
|
||||
make({ service: A, name: "a", layer: aLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error B requires A
|
||||
make({ service: B, layer: bLayer, deps: [] })
|
||||
|
||||
// @ts-expect-error C requires A and B
|
||||
make({ service: C, layer: cLayer, deps: [a] })
|
||||
|
||||
const closed = LayerNode.buildLayer(c, { tiers })
|
||||
const closedWithError = LayerNode.buildLayer(dependent, { tiers })
|
||||
const checkClosed: Layer.Layer<C, never, never> = closed
|
||||
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
|
||||
void checkClosed
|
||||
void checkError
|
||||
|
||||
LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
|
||||
|
||||
// @ts-expect-error Replacement must provide A
|
||||
LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
|
||||
|
||||
// @ts-expect-error Replacement cannot introduce a new error
|
||||
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
|
||||
|
||||
// @ts-expect-error Replacement must be closed
|
||||
LayerNode.replace(bLayer, bLayer)
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
@@ -1,86 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
|
||||
class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
|
||||
class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
|
||||
class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
|
||||
class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
|
||||
|
||||
const tiers = LayerNode.tiers(["app"])
|
||||
const make = tiers.make("app")
|
||||
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
|
||||
const greetingLayer = Layer.effect(
|
||||
Greeting,
|
||||
Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })),
|
||||
)
|
||||
const value = make({ service: Value, layer: valueLayer, deps: [] })
|
||||
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
||||
|
||||
describe("layer node", () => {
|
||||
test("builds an untiered graph", async () => {
|
||||
const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
|
||||
const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
|
||||
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(LayerNode.buildLayer(greeting)))
|
||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||
})
|
||||
|
||||
test("builds a dependency graph", async () => {
|
||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||
Effect.provide(LayerNode.buildLayer(greeting, { tiers })),
|
||||
)
|
||||
expect(await Effect.runPromise(program)).toBe("hello production")
|
||||
})
|
||||
|
||||
test("replaces a layer by identity", async () => {
|
||||
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
|
||||
const program = Effect.map(Greeting, (item) => item.value).pipe(
|
||||
Effect.provide(
|
||||
LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(valueLayer, replacement)] }),
|
||||
),
|
||||
)
|
||||
expect(await Effect.runPromise(program)).toBe("hello simulation")
|
||||
})
|
||||
|
||||
test("replaces every use of the same layer", async () => {
|
||||
const leftLayer = Layer.effect(
|
||||
Left,
|
||||
Effect.map(Value, (item) => Left.of({ value: item.value })),
|
||||
)
|
||||
const rightLayer = Layer.effect(
|
||||
Right,
|
||||
Effect.map(Value, (item) => Right.of({ value: item.value })),
|
||||
)
|
||||
const left = make({ service: Left, layer: leftLayer, deps: [value] })
|
||||
const right = make({ service: Right, layer: rightLayer, deps: [value] })
|
||||
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
|
||||
const layer = LayerNode.buildLayer(LayerNode.group([left, right]), {
|
||||
tiers,
|
||||
replacements: [LayerNode.replace(valueLayer, replacement)],
|
||||
})
|
||||
const program = Effect.gen(function* () {
|
||||
return [(yield* Left).value, (yield* Right).value]
|
||||
}).pipe(Effect.provide(layer))
|
||||
expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"])
|
||||
})
|
||||
|
||||
test("does not acquire an unused replacement", async () => {
|
||||
let acquisitions = 0
|
||||
const other = Layer.succeed(Value, Value.of({ value: "other" }))
|
||||
const replacement = Layer.effect(
|
||||
Value,
|
||||
Effect.sync(() => {
|
||||
acquisitions++
|
||||
return Value.of({ value: "replacement" })
|
||||
}),
|
||||
)
|
||||
await Effect.runPromise(
|
||||
Effect.map(Greeting, (item) => item.value).pipe(
|
||||
Effect.provide(
|
||||
LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(other, replacement)] }),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(acquisitions).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
import { test } from "bun:test"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { makeGlobalNode, makeLocationNode } from "@opencode-ai/core/effect/scoped-node"
|
||||
|
||||
class A extends Context.Service<A, {}>()("test/ScopedA") {}
|
||||
class B extends Context.Service<B, {}>()("test/ScopedB") {}
|
||||
|
||||
const a = Layer.succeed(A, A.of({}))
|
||||
const b = Layer.effect(B, Effect.as(A, B.of({})))
|
||||
const globalA = makeGlobalNode({ service: A, layer: a, deps: [] })
|
||||
const locationA = makeLocationNode({ service: A, layer: a, deps: [] })
|
||||
|
||||
makeGlobalNode({ service: B, layer: b, deps: [globalA] })
|
||||
makeLocationNode({ service: B, layer: b, deps: [globalA] })
|
||||
makeLocationNode({ service: B, layer: b, deps: [locationA] })
|
||||
|
||||
// @ts-expect-error Global nodes cannot depend on location nodes
|
||||
makeGlobalNode({ service: B, layer: b, deps: [locationA] })
|
||||
|
||||
// @ts-expect-error B requires A
|
||||
makeLocationNode({ service: B, layer: b, deps: [] })
|
||||
|
||||
test("type exploration compiles", () => {})
|
||||
Reference in New Issue
Block a user