feat(plugin): add v2 effect host (#33111)

This commit is contained in:
Dax 2026-06-21 14:05:49 +02:00 committed by GitHub
commit c780d7cee7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 3740 additions and 1943 deletions

View file

@ -7,6 +7,7 @@ import type { ModelV2 } from "./model"
import type { Catalog } from "./catalog"
import { EventV2 } from "./event"
import { KeyedMutex } from "./effect/keyed-mutex"
import { State } from "./state"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type
@ -22,7 +23,7 @@ export const Event = {
type HookSpec = {
"catalog.transform": {
input: Catalog.Editor
input: Catalog.Draft
output: {}
}
"aisdk.language": {
@ -62,18 +63,16 @@ export type HookFunctions = {
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
export type Effect<R = never> = Effect.Effect<HookFunctions | void, never, R | Scope.Scope>
export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
return input
}
export interface Interface {
readonly add: (input: {
id: ID
id: string
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
}) => Effect.Effect<void, never, never>
readonly remove: (id: ID) => Effect.Effect<void>
readonly hook: <Name extends keyof Hooks>(
name: Name,
callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
) => Effect.Effect<State.Registration, never, Scope.Scope>
readonly triggerFor: <Name extends keyof Hooks>(
id: ID,
name: Name,
@ -97,35 +96,40 @@ export const layer = Layer.effect(
hooks: HookFunctions
scope: Scope.Closeable
}[] = []
let registrations: {
[Name in keyof Hooks]: {
name: Name
callback: (input: Hooks[Name]) => Effect.Effect<void> | void
}
}[keyof Hooks][] = []
const events = yield* EventV2.Service
const scope = yield* Scope.Scope
const locks = KeyedMutex.makeUnsafe<ID>()
const svc = Service.of({
add: Effect.fn("Plugin.add")(function* (input) {
yield* locks.withLock(input.id)(
const id = ID.make(input.id)
yield* locks.withLock(id)(
Effect.gen(function* () {
const existing = hooks.find((item) => item.id === input.id)
const existing = hooks.find((item) => item.id === id)
if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
const childScope = yield* Scope.fork(scope)
const result = yield* input.effect.pipe(
Scope.provide(childScope),
Effect.withSpan("Plugin.load", {
attributes: {
"plugin.id": input.id,
"plugin.id": id,
},
}),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
)
hooks = [
...hooks.filter((item) => item.id !== input.id),
{
id: input.id,
hooks: result ?? {},
scope: childScope,
},
]
yield* events.publish(Event.Added, { id: input.id })
const next = {
id,
hooks: result ?? {},
scope: childScope,
}
hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
yield* events.publish(Event.Added, { id })
}),
)
}),
@ -160,6 +164,12 @@ export const layer = Layer.effect(
)
}
for (const item of registrations) {
if (item.name !== name) continue
const result = item.callback(event as never)
if (Effect.isEffect(result)) yield* result
}
for (const [field, draft] of draftEntries) {
event[field] = finishDraft(draft)
}
@ -175,6 +185,19 @@ export const layer = Layer.effect(
}),
)
}),
hook: Effect.fn("Plugin.hook")(function* (name, callback) {
const scope = yield* Scope.Scope
const registration = { name, callback } as (typeof registrations)[number]
let active = true
registrations = [...registrations, registration]
const dispose = Effect.sync(() => {
if (!active) return
active = false
registrations = registrations.filter((item) => item !== registration)
})
yield* Scope.addFinalizer(scope, dispose)
return { dispose }
}),
})
return svc
}),