fix: logo 右半部分从 CODING 改为 CODE
去掉难以正确渲染的 N 和 G 字母,右半部分简化为 CODE(4 字母), 与左半部分 AIR 组合为 AIR CODE。
This commit is contained in:
45
packages/core/src/effect/keyed-mutex.ts
Normal file
45
packages/core/src/effect/keyed-mutex.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export * as KeyedMutex from "./keyed-mutex"
|
||||
|
||||
import { Effect, Semaphore } from "effect"
|
||||
|
||||
export interface KeyedMutex<in Key> {
|
||||
readonly size: Effect.Effect<number>
|
||||
readonly withLock: (key: Key) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an in-memory mutex with one lock per key. Entries are removed when no
|
||||
* holder or waiter remains.
|
||||
*
|
||||
* same key -> queue
|
||||
* different key -> run independently
|
||||
*
|
||||
* `users` counts holders and waiters so an entry is not removed while a waiter
|
||||
* will reuse it.
|
||||
*/
|
||||
export const makeUnsafe = <Key>(): KeyedMutex<Key> => {
|
||||
const locks = new Map<Key, { readonly semaphore: Semaphore.Semaphore; users: number }>()
|
||||
|
||||
const withLock =
|
||||
(key: Key) =>
|
||||
<A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
Effect.suspend(() => {
|
||||
const current = locks.get(key)
|
||||
const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 }
|
||||
if (!current) locks.set(key, entry)
|
||||
entry.users++
|
||||
return entry.semaphore.withPermit(effect).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
entry.users--
|
||||
if (entry.users === 0) locks.delete(key)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return { size: Effect.sync(() => locks.size), withLock }
|
||||
}
|
||||
|
||||
/** Creates an in-memory keyed mutex inside an Effect workflow. */
|
||||
export const make = <Key>(): Effect.Effect<KeyedMutex<Key>> => Effect.sync(makeUnsafe<Key>)
|
||||
12
packages/core/src/effect/layer-node-platform.ts
Normal file
12
packages/core/src/effect/layer-node-platform.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { LayerNode } from "./layer-node"
|
||||
|
||||
export const filesystem = LayerNode.make(NodeFileSystem.layer, [])
|
||||
export const path = LayerNode.make(NodePath.layer, [])
|
||||
export const httpClient = LayerNode.make(FetchHttpClient.layer, [])
|
||||
export const requestExecutor = LayerNode.make(RequestExecutor.layer, [httpClient])
|
||||
export const llmClient = LayerNode.make(LLMClient.layer, [requestExecutor])
|
||||
|
||||
export * as LayerNodePlatform from "./layer-node-platform"
|
||||
102
packages/core/src/effect/layer-node.ts
Normal file
102
packages/core/src/effect/layer-node.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Layer } from "effect"
|
||||
|
||||
type RuntimeLayer = Layer.Layer<never, unknown, unknown>
|
||||
type AnyNode = Node<unknown, unknown>
|
||||
type NodeList = readonly [] | readonly [AnyNode, ...AnyNode[]]
|
||||
type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown> ? A : never
|
||||
type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E> ? E : 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 Node<A, E = never> = {
|
||||
readonly kind: "layer" | "group"
|
||||
readonly implementation?: Layer.Any
|
||||
readonly dependencies: readonly AnyNode[]
|
||||
readonly [$OutputType]?: () => A
|
||||
readonly [$ErrorType]?: () => E
|
||||
}
|
||||
|
||||
export function make<const Implementation extends Layer.Any, const Items extends NodeList>(
|
||||
implementation: Implementation,
|
||||
dependencies: Items & CheckDependencies<Implementation, NoInfer<Items>>,
|
||||
): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>> {
|
||||
return { kind: "layer", implementation: implementation as Layer.Any, dependencies }
|
||||
}
|
||||
|
||||
export function group<const Items extends NodeList>(
|
||||
dependencies: Items,
|
||||
): Node<Output<Items[number]>, Error<Items[number]>> {
|
||||
return { kind: "group", dependencies }
|
||||
}
|
||||
|
||||
export type Replacement<A = unknown> = {
|
||||
readonly source: Node<A, unknown>
|
||||
readonly replacement: Node<A, unknown>
|
||||
}
|
||||
|
||||
type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
|
||||
? unknown
|
||||
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }
|
||||
|
||||
export function replaceWithNode<A, E, E2>(
|
||||
source: Node<A, E>,
|
||||
replacement: Node<NoInfer<A>, E2> & CheckReplacementErrors<E, NoInfer<E2>>,
|
||||
): Replacement<A> {
|
||||
return { source, replacement }
|
||||
}
|
||||
|
||||
export function replace<A, E, E2>(
|
||||
source: Node<A, E>,
|
||||
replacement: Layer.Layer<NoInfer<A>, E2, never> & CheckReplacementErrors<E, NoInfer<E2>>,
|
||||
): Replacement<A> {
|
||||
return { source, replacement: make(replacement as Layer.Layer<A, E2>, []) }
|
||||
}
|
||||
|
||||
export function buildLayer<A, E>(node: Node<A, E>, options?: { readonly replacements?: readonly Replacement[] }) {
|
||||
const replacements = new Map(options?.replacements?.map((item) => [item.source, item.replacement]))
|
||||
const cache = new Map<AnyNode, RuntimeLayer>()
|
||||
const visiting = new Set<AnyNode>()
|
||||
const stack: AnyNode[] = []
|
||||
const ids = new Map<AnyNode, number>()
|
||||
|
||||
const visit = (input: AnyNode): RuntimeLayer => {
|
||||
const node = replacements.get(input) ?? input
|
||||
const cached = cache.get(node)
|
||||
if (cached) return cached
|
||||
if (visiting.has(node)) {
|
||||
const start = stack.indexOf(node)
|
||||
const cycle = [...stack.slice(start), node].map((item) => `${item.kind}#${ids.get(item)}`).join(" -> ")
|
||||
throw new Error(`Cycle detected in app graph: ${cycle}`)
|
||||
}
|
||||
if (!ids.has(node)) ids.set(node, ids.size + 1)
|
||||
visiting.add(node)
|
||||
stack.push(node)
|
||||
try {
|
||||
const dependencies = node.dependencies.map(visit)
|
||||
const nonEmpty = dependencies as [RuntimeLayer, ...RuntimeLayer[]]
|
||||
const result =
|
||||
node.kind === "group"
|
||||
? dependencies.length === 0
|
||||
? Layer.empty
|
||||
: Layer.mergeAll(...nonEmpty)
|
||||
: dependencies.length === 0
|
||||
? (node.implementation as RuntimeLayer)
|
||||
: Layer.provide(node.implementation as RuntimeLayer, nonEmpty)
|
||||
cache.set(node, result)
|
||||
return result
|
||||
} finally {
|
||||
stack.pop()
|
||||
visiting.delete(node)
|
||||
}
|
||||
}
|
||||
|
||||
return visit(node) as unknown as Layer.Layer<A, E, never>
|
||||
}
|
||||
|
||||
export * as LayerNode from "./layer-node"
|
||||
3
packages/core/src/effect/memo-map.ts
Normal file
3
packages/core/src/effect/memo-map.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Layer } from "effect"
|
||||
|
||||
export const memoMap = Layer.makeMemoMapUnsafe()
|
||||
21
packages/core/src/effect/runtime.ts
Normal file
21
packages/core/src/effect/runtime.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Layer, type Context, ManagedRuntime, type Effect } from "effect"
|
||||
import { memoMap } from "./memo-map"
|
||||
import { Observability } from "../observability"
|
||||
|
||||
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
|
||||
let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
|
||||
const getRuntime = () =>
|
||||
(rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer<I, E>, {
|
||||
memoMap,
|
||||
}))
|
||||
|
||||
return {
|
||||
runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runSync(service.use(fn)),
|
||||
runPromiseExit: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) =>
|
||||
getRuntime().runPromiseExit(service.use(fn), options),
|
||||
runPromise: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>, options?: Effect.RunOptions) =>
|
||||
getRuntime().runPromise(service.use(fn), options),
|
||||
runFork: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runFork(service.use(fn)),
|
||||
runCallback: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runCallback(service.use(fn)),
|
||||
}
|
||||
}
|
||||
43
packages/core/src/effect/service-use.ts
Normal file
43
packages/core/src/effect/service-use.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Context, Effect } from "effect"
|
||||
|
||||
type EffectMethod = (...args: ReadonlyArray<never>) => Effect.Effect<unknown, unknown, unknown>
|
||||
|
||||
type ServiceUse<Identifier, Shape> = {
|
||||
readonly [Key in keyof Shape as Shape[Key] extends EffectMethod ? Key : never]: Shape[Key] extends (
|
||||
...args: infer Args
|
||||
) => infer Return
|
||||
? Args extends ReadonlyArray<unknown>
|
||||
? Return extends Effect.Effect<infer A, infer E, infer R>
|
||||
? (...args: Args) => Effect.Effect<A, E, R | Identifier>
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
}
|
||||
|
||||
export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, Shape>) => {
|
||||
const cache = new Map<string, (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>>()
|
||||
// This is the only dynamic boundary: TypeScript knows the accessor shape,
|
||||
// but Proxy property names are runtime values.
|
||||
const access = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, key) => {
|
||||
if (typeof key !== "string") return undefined
|
||||
const cached = cache.get(key)
|
||||
if (cached) return cached
|
||||
const accessor = (...args: unknown[]) =>
|
||||
tag.use((service) => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime.
|
||||
const method = service[key as keyof Shape]
|
||||
if (typeof method !== "function") return Effect.die(new Error(`Service method not found: ${key}`))
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods.
|
||||
return (method as (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>)(...args)
|
||||
})
|
||||
cache.set(key, accessor)
|
||||
return accessor
|
||||
},
|
||||
},
|
||||
)
|
||||
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy implements the mapped accessor surface lazily.
|
||||
return access as ServiceUse<Identifier, Shape>
|
||||
}
|
||||
Reference in New Issue
Block a user