export * as EventFeed from "./event-feed" import { EventV2 } from "@opencode-ai/core/event" import { isOpenCodeEvent, OpenCodeEvent } from "@opencode-ai/protocol/groups/event" import { Cause, Context, Effect, Layer, Queue, Schema, Scope, Stream } from "effect" export const SubscriberCapacity = 4_096 export class SubscriberOverflowError extends Schema.TaggedErrorClass()( "EventFeed.SubscriberOverflow", { capacity: Schema.Int }, ) {} export class EncodingError extends Schema.TaggedErrorClass()("EventFeed.EncodingError", { eventID: EventV2.ID, eventType: Schema.String, cause: Schema.Defect(), }) {} export type Error = SubscriberOverflowError | EncodingError export interface Interface { readonly subscribe: Effect.Effect, never, Scope.Scope> } export class Service extends Context.Service()("@opencode/server/EventFeed") {} const encode = Schema.encodeUnknownSync(OpenCodeEvent) export function frame(event: OpenCodeEvent) { return `data: ${JSON.stringify(encode(event))}\n\n` } export const make = Effect.fn("EventFeed.make")(function* ( observe: (subscriber: EventV2.Subscriber) => Effect.Effect, options?: { readonly capacity?: number; readonly encode?: (event: OpenCodeEvent) => string }, ) { const capacity = options?.capacity ?? SubscriberCapacity const render = options?.encode ?? frame const subscribers = new Set>() const fail = (error: Error) => Effect.sync(() => { const current = Array.from(subscribers) subscribers.clear() for (const subscriber of current) Queue.failCauseUnsafe(subscriber, Cause.fail(error)) }) const publish = Effect.fnUntraced(function* (event: EventV2.Payload) { if (!isOpenCodeEvent(event)) return if (subscribers.size === 0) return const encoded = yield* Effect.try({ try: () => render(event), catch: (cause) => new EncodingError({ eventID: event.id, eventType: event.type, cause }), }).pipe( Effect.catch((error) => Effect.logError("Failed to encode public event", { eventID: error.eventID, eventType: error.eventType, cause: error.cause, }).pipe(Effect.andThen(fail(error)), Effect.as(undefined)), ), ) if (encoded === undefined) return for (const subscriber of subscribers) { if (Queue.offerUnsafe(subscriber, encoded)) continue subscribers.delete(subscriber) Queue.failCauseUnsafe(subscriber, Cause.fail(new SubscriberOverflowError({ capacity }))) } }) const unsubscribe = yield* observe(publish) yield* Effect.addFinalizer(() => unsubscribe) return Service.of({ subscribe: Effect.acquireRelease( Queue.dropping(capacity).pipe(Effect.tap((queue) => Effect.sync(() => subscribers.add(queue)))), (queue) => Effect.sync(() => subscribers.delete(queue)).pipe(Effect.andThen(Queue.shutdown(queue)), Effect.asVoid), ).pipe(Effect.map(Stream.fromQueue)), }) }) export const layer = Layer.effect( Service, Effect.gen(function* () { const events = yield* EventV2.Service return yield* make(events.listen) }), )