hooks.ts 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. export * as PluginHooks from "./hooks"
  2. import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
  3. import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
  4. import { Context, Effect, Layer, Scope } from "effect"
  5. import { makeLocationNode } from "../effect/app-node"
  6. import { State } from "../state"
  7. export interface Domains {
  8. readonly aisdk: AISDKHooks
  9. readonly tool: ToolHooks
  10. }
  11. type Callback<Event> = (event: Event) => Effect.Effect<void>
  12. export interface Interface {
  13. readonly register: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
  14. domain: Domain,
  15. name: Name,
  16. callback: Callback<Domains[Domain][Name]>,
  17. ) => Effect.Effect<State.Registration, never, Scope.Scope>
  18. readonly trigger: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
  19. domain: Domain,
  20. name: Name,
  21. event: Domains[Domain][Name],
  22. ) => Effect.Effect<Domains[Domain][Name]>
  23. }
  24. export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
  25. const layer = Layer.effect(
  26. Service,
  27. Effect.gen(function* () {
  28. const callbacks = new Map<string, Function[]>()
  29. const key = (domain: keyof Domains, name: PropertyKey) => `${domain}.${String(name)}`
  30. const register: Interface["register"] = Effect.fn("PluginHooks.register")(function* (domain, name, callback) {
  31. const scope = yield* Scope.Scope
  32. const id = key(domain, name)
  33. let active = true
  34. callbacks.set(id, [...(callbacks.get(id) ?? []), callback])
  35. const dispose = Effect.sync(() => {
  36. if (!active) return
  37. active = false
  38. const next = (callbacks.get(id) ?? []).filter((item) => item !== callback)
  39. if (next.length === 0) callbacks.delete(id)
  40. else callbacks.set(id, next)
  41. })
  42. yield* Scope.addFinalizer(scope, dispose)
  43. return { dispose }
  44. })
  45. const trigger: Interface["trigger"] = Effect.fn("PluginHooks.trigger")(function* (domain, name, event) {
  46. for (const callback of callbacks.get(key(domain, name)) ?? []) {
  47. const result: Effect.Effect<void> = Reflect.apply(callback, undefined, [event])
  48. yield* result
  49. }
  50. return event
  51. })
  52. return Service.of({ register, trigger })
  53. }),
  54. )
  55. export const node = makeLocationNode({ service: Service, layer, deps: [] })