1- import { describe , expect , test } from "bun:test "
1+ import { NodeHttpServer } from "@effect/platform-node "
22import { EventV2 } from "@opencode-ai/core/event"
3- import { Effect , Fiber , Stream } from "effect"
3+ import { describe , expect } from "bun:test"
4+ import { Context , Effect , Fiber , Layer , Stream } from "effect"
5+ import { HttpServer , HttpServerRequest } from "effect/unstable/http"
6+ import { createServer } from "node:http"
47import { it } from "../../core/test/lib/effect"
58import { EventFeed } from "../src/event-feed"
9+ import { subscribeResponse } from "../src/handlers/event"
610
711function makeSource ( ) {
812 let subscriber : EventV2 . Subscriber | undefined
@@ -18,6 +22,14 @@ function makeSource() {
1822 }
1923}
2024
25+ async function wait ( fn : ( ) => boolean , timeout = 3000 ) {
26+ const start = Date . now ( )
27+ while ( ! fn ( ) ) {
28+ if ( Date . now ( ) - start > timeout ) throw new Error ( "timed out waiting for condition" )
29+ await Bun . sleep ( 10 )
30+ }
31+ }
32+
2133describe ( "EventFeed ownership" , ( ) => {
2234 it . effect ( "canceling a subscriber scope returns diagnostics.active to baseline" , ( ) =>
2335 Effect . gen ( function * ( ) {
@@ -41,51 +53,44 @@ describe("EventFeed ownership", () => {
4153 } ) ,
4254 )
4355
44- test ( "real network abort closes the response body stream" , async ( ) => {
45- let closed = false
46- const server = Bun . serve ( {
47- port : 0 ,
48- fetch ( request ) {
49- let current : ReadableStreamDefaultController < Uint8Array > | undefined
50- const stream = new ReadableStream < Uint8Array > ( {
51- start ( controller ) {
52- current = controller
53- controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"type":"server.connected","data":{}}\n\n' ) )
54- } ,
55- cancel ( ) {
56- closed = true
57- } ,
58- } )
59- request . signal . addEventListener (
60- "abort" ,
61- ( ) => {
62- closed = true
63- try {
64- current ?. close ( )
65- } catch {
66- // already closed by the client disconnect
67- }
68- } ,
69- { once : true } ,
70- )
71- return new Response ( stream , { headers : { "content-type" : "text/event-stream" } } )
72- } ,
73- } )
56+ it . live (
57+ "aborting a real Node HTTP client returns EventFeed.active to baseline" ,
58+ ( ) =>
59+ Effect . gen ( function * ( ) {
60+ const source = makeSource ( )
61+ const feed = yield * EventFeed . make ( source . observe )
62+ const baseline = feed . diagnostics ( ) . active
63+
64+ const context = yield * Layer . build ( NodeHttpServer . layer ( createServer , { host : "127.0.0.1" , port : 0 } ) )
65+ const server = Context . get ( context , HttpServer . HttpServer )
66+ yield * server
67+ . serve ( HttpServerRequest . HttpServerRequest . use ( ( request ) => subscribeResponse ( feed , request , { } ) ) )
68+ . pipe ( Effect . forkScoped )
7469
75- try {
76- const controller = new AbortController ( )
77- const response = await fetch ( `http://127.0.0.1:${ server . port } /api/event` , { signal : controller . signal } )
78- expect ( response . ok ) . toBe ( true )
79- const reader = response . body ! . getReader ( )
80- await reader . read ( )
81- controller . abort ( )
82- await reader . closed . catch ( ( ) => undefined )
83- await Bun . sleep ( 50 )
84- expect ( closed ) . toBe ( true )
85- } finally {
86- await server . stop ( true )
87- }
88- } )
70+ const base = HttpServer . formatAddress ( server . address )
71+ const controller = new AbortController ( )
72+ const response = yield * Effect . promise ( ( ) => fetch ( `${ base } /api/event` , { signal : controller . signal } ) )
73+ expect ( response . ok ) . toBe ( true )
74+ expect ( response . body ) . toBeDefined ( )
75+ const reader = response . body ?. getReader ( )
76+ expect ( reader ) . toBeDefined ( )
77+ if ( ! reader ) throw new Error ( "expected event stream body reader" )
78+
79+ yield * Effect . promise ( ( ) => reader . read ( ) )
80+ expect ( feed . diagnostics ( ) . active ) . toBe ( baseline + 1 )
81+
82+ controller . abort ( )
83+ // Bun may never settle `reader.closed` after abort; cancel and watch feed release.
84+ yield * Effect . promise ( ( ) => reader . cancel ( ) . catch ( ( ) => undefined ) )
85+ yield * Effect . promise ( ( ) => wait ( ( ) => feed . diagnostics ( ) . active === baseline ) )
86+
87+ expect ( feed . diagnostics ( ) . active ) . toBe ( baseline )
88+ expect ( feed . diagnostics ( ) . closes ) . toBeGreaterThanOrEqual ( 1 )
89+ const dump = JSON . stringify ( feed . diagnostics ( ) )
90+ expect ( dump . includes ( "payload" ) ) . toBe ( false )
91+ } ) ,
92+ 15_000 ,
93+ )
8994
9095 it . effect ( "eleven sequential subscribe scopes never accumulate active subscribers" , ( ) =>
9196 Effect . gen ( function * ( ) {
0 commit comments