11import z from "zod"
2+ import { Effect , Layer , PubSub , ServiceMap , Stream } from "effect"
23import { Log } from "../util/log"
34import { Instance } from "../project/instance"
45import { BusEvent } from "./bus-event"
56import { GlobalBus } from "./global"
7+ import { InstanceState } from "@/effect/instance-state"
8+ import { makeRuntime } from "@/effect/run-service"
69
710export namespace Bus {
811 const log = Log . create ( { service : "bus" } )
9- type Subscription = ( event : any ) => void
1012
1113 export const InstanceDisposed = BusEvent . define (
1214 "server.instance.disposed" ,
@@ -15,91 +17,129 @@ export namespace Bus {
1517 } ) ,
1618 )
1719
18- const state = Instance . state (
19- ( ) => {
20- const subscriptions = new Map < any , Subscription [ ] > ( )
20+ type Payload < D extends BusEvent . Definition = BusEvent . Definition > = {
21+ type : D [ "type" ]
22+ properties : z . infer < D [ "properties" ] >
23+ }
24+
25+ type State = {
26+ wildcard : PubSub . PubSub < Payload >
27+ typed : Map < string , PubSub . PubSub < Payload > >
28+ }
29+
30+ export interface Interface {
31+ readonly publish : < D extends BusEvent . Definition > (
32+ def : D ,
33+ properties : z . output < D [ "properties" ] > ,
34+ ) => Effect . Effect < void >
35+ readonly subscribe : < D extends BusEvent . Definition > ( def : D ) => Stream . Stream < Payload < D > >
36+ readonly subscribeAll : ( ) => Stream . Stream < Payload >
37+ }
38+
39+ export class Service extends ServiceMap . Service < Service , Interface > ( ) ( "@opencode/Bus" ) { }
40+
41+ export const layer = Layer . effect (
42+ Service ,
43+ Effect . gen ( function * ( ) {
44+ const cache = yield * InstanceState . make < State > (
45+ Effect . fn ( "Bus.state" ) ( function * ( ctx ) {
46+ const wildcard = yield * PubSub . unbounded < Payload > ( )
47+ const typed = new Map < string , PubSub . PubSub < Payload > > ( )
48+
49+ yield * Effect . addFinalizer ( ( ) =>
50+ Effect . gen ( function * ( ) {
51+ // Publish InstanceDisposed before shutting down so subscribers see it
52+ yield * PubSub . publish ( wildcard , {
53+ type : InstanceDisposed . type ,
54+ properties : { directory : ctx . directory } ,
55+ } )
56+ yield * PubSub . shutdown ( wildcard )
57+ for ( const ps of typed . values ( ) ) {
58+ yield * PubSub . shutdown ( ps )
59+ }
60+ } ) ,
61+ )
62+
63+ return { wildcard, typed }
64+ } ) ,
65+ )
66+
67+ function getOrCreate ( state : State , type : string ) {
68+ return Effect . gen ( function * ( ) {
69+ let ps = state . typed . get ( type )
70+ if ( ! ps ) {
71+ ps = yield * PubSub . unbounded < Payload > ( )
72+ state . typed . set ( type , ps )
73+ }
74+ return ps
75+ } )
76+ }
2177
22- return {
23- subscriptions,
78+ function publish < D extends BusEvent . Definition > ( def : D , properties : z . output < D [ "properties" ] > ) {
79+ return Effect . gen ( function * ( ) {
80+ const state = yield * InstanceState . get ( cache )
81+ const payload : Payload = { type : def . type , properties }
82+ log . info ( "publishing" , { type : def . type } )
83+
84+ const ps = state . typed . get ( def . type )
85+ if ( ps ) yield * PubSub . publish ( ps , payload )
86+ yield * PubSub . publish ( state . wildcard , payload )
87+
88+ GlobalBus . emit ( "event" , {
89+ directory : Instance . directory ,
90+ payload,
91+ } )
92+ } )
2493 }
25- } ,
26- async ( entry ) => {
27- const wildcard = entry . subscriptions . get ( "*" )
28- if ( ! wildcard ) return
29- const event = {
30- type : InstanceDisposed . type ,
31- properties : {
32- directory : Instance . directory ,
33- } ,
94+
95+ function subscribe < D extends BusEvent . Definition > ( def : D ) : Stream . Stream < Payload < D > > {
96+ log . info ( "subscribing" , { type : def . type } )
97+ return Stream . unwrap (
98+ Effect . gen ( function * ( ) {
99+ const state = yield * InstanceState . get ( cache )
100+ const ps = yield * getOrCreate ( state , def . type )
101+ return Stream . fromPubSub ( ps ) as Stream . Stream < Payload < D > >
102+ } ) ,
103+ ) . pipe ( Stream . ensuring ( Effect . sync ( ( ) => log . info ( "unsubscribing" , { type : def . type } ) ) ) )
34104 }
35- for ( const sub of [ ...wildcard ] ) {
36- sub ( event )
105+
106+ function subscribeAll ( ) : Stream . Stream < Payload > {
107+ log . info ( "subscribing" , { type : "*" } )
108+ return Stream . unwrap (
109+ Effect . gen ( function * ( ) {
110+ const state = yield * InstanceState . get ( cache )
111+ return Stream . fromPubSub ( state . wildcard )
112+ } ) ,
113+ ) . pipe ( Stream . ensuring ( Effect . sync ( ( ) => log . info ( "unsubscribing" , { type : "*" } ) ) ) )
37114 }
38- } ,
115+
116+ return Service . of ( { publish, subscribe, subscribeAll } )
117+ } ) ,
39118 )
40119
41- export async function publish < Definition extends BusEvent . Definition > (
42- def : Definition ,
43- properties : z . output < Definition [ "properties" ] > ,
44- ) {
45- const payload = {
46- type : def . type ,
47- properties,
48- }
49- log . info ( "publishing" , {
50- type : def . type ,
51- } )
52- const pending = [ ]
53- for ( const key of [ def . type , "*" ] ) {
54- const match = [ ...( state ( ) . subscriptions . get ( key ) ?? [ ] ) ]
55- for ( const sub of match ) {
56- pending . push ( sub ( payload ) )
57- }
58- }
59- GlobalBus . emit ( "event" , {
60- directory : Instance . directory ,
61- payload,
62- } )
63- return Promise . all ( pending )
120+ const { runPromise, runCallback } = makeRuntime ( Service , layer )
121+
122+ function forkStream < T > ( streamFn : ( svc : Interface ) => Stream . Stream < T > , callback : ( msg : T ) => void ) {
123+ return runCallback ( ( svc ) =>
124+ streamFn ( svc ) . pipe ( Stream . runForEach ( ( msg ) => Effect . sync ( ( ) => callback ( msg ) ) ) ) ,
125+ )
64126 }
65127
66- export function subscribe < Definition extends BusEvent . Definition > (
67- def : Definition ,
68- callback : ( event : { type : Definition [ "type" ] ; properties : z . infer < Definition [ "properties" ] > } ) => void ,
128+ export async function publish < D extends BusEvent . Definition > (
129+ def : D ,
130+ properties : z . output < D [ "properties" ] > ,
69131 ) {
70- return raw ( def . type , callback )
132+ return runPromise ( ( svc ) => svc . publish ( def , properties ) )
71133 }
72134
73- export function once < Definition extends BusEvent . Definition > (
74- def : Definition ,
75- callback : ( event : {
76- type : Definition [ "type" ]
77- properties : z . infer < Definition [ "properties" ] >
78- } ) => "done" | undefined ,
135+ export function subscribe < D extends BusEvent . Definition > (
136+ def : D ,
137+ callback : ( event : { type : D [ "type" ] ; properties : z . infer < D [ "properties" ] > } ) => void ,
79138 ) {
80- const unsub = subscribe ( def , ( event ) => {
81- if ( callback ( event ) ) unsub ( )
82- } )
139+ return forkStream ( ( svc ) => svc . subscribe ( def ) , callback )
83140 }
84141
85142 export function subscribeAll ( callback : ( event : any ) => void ) {
86- return raw ( "*" , callback )
87- }
88-
89- function raw ( type : string , callback : ( event : any ) => void ) {
90- log . info ( "subscribing" , { type } )
91- const subscriptions = state ( ) . subscriptions
92- let match = subscriptions . get ( type ) ?? [ ]
93- match . push ( callback )
94- subscriptions . set ( type , match )
95-
96- return ( ) => {
97- log . info ( "unsubscribing" , { type } )
98- const match = subscriptions . get ( type )
99- if ( ! match ) return
100- const index = match . indexOf ( callback )
101- if ( index === - 1 ) return
102- match . splice ( index , 1 )
103- }
143+ return forkStream ( ( svc ) => svc . subscribeAll ( ) , callback )
104144 }
105145}
0 commit comments