fix(tsconnect): address bridge review findings

Co-Authored-By: gpt-5.6-sol <noreply@openai.com>
This commit is contained in:
2026-08-30 23:32:56 +00:00
co-authored by Codex
parent d3e8c23686
commit 705ba3e00b
3 changed files with 143 additions and 30 deletions
+95 -4
View File
@@ -42,10 +42,19 @@ func TestDriveHandlerThrowReturns500(t *testing.T) {
} }
func TestDriveCancellationDisablesCallbacks(t *testing.T) { func TestDriveCancellationDisablesCallbacks(t *testing.T) {
started := make(chan struct{}) type handlerArgs struct {
req js.Value
res js.Value
}
started := make(chan handlerArgs, 1)
deferred := evalJS(t, `(() => {
let resolve
const promise = new Promise(r => { resolve = r })
return {promise, resolve}
})()`)
handler := js.FuncOf(func(this js.Value, args []js.Value) any { handler := js.FuncOf(func(this js.Value, args []js.Value) any {
close(started) started <- handlerArgs{req: args[0], res: args[1]}
return nil return deferred.Get("promise")
}) })
defer handler.Release() defer handler.Release()
fs := new(jsFileSystemForRemote) fs := new(jsFileSystemForRemote)
@@ -59,8 +68,9 @@ func TestDriveCancellationDisablesCallbacks(t *testing.T) {
close(done) close(done)
}() }()
var jsArgs handlerArgs
select { select {
case <-started: case jsArgs = <-started:
case <-time.After(time.Second): case <-time.After(time.Second):
t.Fatal("drive handler did not start") t.Fatal("drive handler did not start")
} }
@@ -71,6 +81,21 @@ func TestDriveCancellationDisablesCallbacks(t *testing.T) {
t.Fatal("drive handler did not stop after cancellation") t.Fatal("drive handler did not stop after cancellation")
} }
readResult := jsArgs.req.Call("readBodyChunk")
if readResult.Type() != js.TypeObject {
t.Fatalf("readBodyChunk after cancellation returned %s, want Promise", readResult.Type())
}
if result := awaitFulfilledPromise(t, readResult); !result.IsNull() {
t.Fatalf("readBodyChunk after cancellation = %v, want null", result)
}
lateChunk := js.Global().Get("Uint8Array").New(1)
js.CopyBytesToJS(lateChunk, []byte("x"))
jsArgs.res.Call("write", lateChunk)
if recorder.Body.Len() != 0 {
t.Fatalf("late JS write changed response body to %q", recorder.Body.String())
}
deferred.Call("resolve")
writeStarted := make(chan struct{}) writeStarted := make(chan struct{})
allowWrite := make(chan struct{}) allowWrite := make(chan struct{})
response := &driveResponse{ response := &driveResponse{
@@ -106,6 +131,23 @@ func TestDriveCancellationDisablesCallbacks(t *testing.T) {
} }
} }
func TestDriveHandlerFulfillmentCompletesRequest(t *testing.T) {
handler := evalJS(t, `(function() { return Promise.resolve() })`)
fs := new(jsFileSystemForRemote)
fs.setHandler(handler)
recorder := httptest.NewRecorder()
done := make(chan struct{})
go func() {
fs.ServeHTTPWithPerms(nil, recorder, httptest.NewRequest("GET", "/", nil))
close(done)
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("fulfilled drive handler did not complete request")
}
}
func TestJSStreamReaderErrorsAreSticky(t *testing.T) { func TestJSStreamReaderErrorsAreSticky(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
name string name string
@@ -142,6 +184,23 @@ func TestJSStreamReaderCloseErrors(t *testing.T) {
} }
} }
func TestJSStreamReaderCloseTimesOut(t *testing.T) {
r := &jsStreamReader{
reader: evalJS(t, `({cancel() { return new Promise(() => {}) }})`),
testCancelTimeout: 10 * time.Millisecond,
}
started := time.Now()
if err := r.Close(); err == nil || !strings.Contains(err.Error(), "timed out") {
t.Fatalf("Close error = %v, want timeout", err)
}
if elapsed := time.Since(started); elapsed >= 250*time.Millisecond {
t.Fatalf("Close took %v, want less than 250ms", elapsed)
}
if _, err := r.Read(make([]byte, 1)); err == nil || !strings.Contains(err.Error(), "timed out") {
t.Fatalf("Read error after Close timeout = %v, want sticky timeout", err)
}
}
func TestJSStreamReaderIgnoresDuplicateSettlement(t *testing.T) { func TestJSStreamReaderIgnoresDuplicateSettlement(t *testing.T) {
reader := evalJS(t, `({ reader := evalJS(t, `({
read() { read() {
@@ -233,3 +292,35 @@ func awaitPromise(t *testing.T, promise js.Value) js.Value {
return js.Undefined() return js.Undefined()
} }
} }
func awaitFulfilledPromise(t *testing.T, promise js.Value) js.Value {
t.Helper()
type result struct {
value js.Value
rejected bool
}
ch := make(chan result, 1)
resolve := js.FuncOf(func(this js.Value, args []js.Value) any {
ch <- result{value: args[0]}
return nil
})
reject := js.FuncOf(func(this js.Value, args []js.Value) any {
ch <- result{value: args[0], rejected: true}
return nil
})
defer resolve.Release()
defer reject.Release()
if _, err := callJSMethod(promise, "then", resolve, reject); err != nil {
t.Fatal(err)
}
select {
case result := <-ch:
if result.rejected {
t.Fatalf("promise rejected with %v; want fulfillment", result.value)
}
return result.value
case <-time.After(time.Second):
t.Fatal("promise did not settle")
return js.Undefined()
}
}
+27 -19
View File
@@ -138,7 +138,7 @@ func (fs *jsFileSystemForRemote) ServeHTTPWithPerms(
readBodyChunk := js.FuncOf(func(_ js.Value, _ []js.Value) any { readBodyChunk := js.FuncOf(func(_ js.Value, _ []js.Value) any {
if !response.isLive() { if !response.isLive() {
return makePromise(func() (any, error) { return makePromise(func() (any, error) {
return nil, errors.New("drive request is closed") return js.Null(), nil
}) })
} }
return makePromise(func() (any, error) { return makePromise(func() (any, error) {
@@ -176,26 +176,35 @@ func (fs *jsFileSystemForRemote) ServeHTTPWithPerms(
} }
return nil return nil
}) })
rejected := js.FuncOf(func(_ js.Value, args []js.Value) any { var fulfilled, rejected js.Func
var handlerSettled sync.Once
settleHandler := func(err error) {
handlerSettled.Do(func() {
select {
case response.doneCh <- err:
default:
}
readBodyChunk.Release()
writeHead.Release()
write.Release()
end.Release()
fulfilled.Release()
rejected.Release()
})
}
fulfilled = js.FuncOf(func(_ js.Value, _ []js.Value) any {
settleHandler(nil)
return nil
})
rejected = js.FuncOf(func(_ js.Value, args []js.Value) any {
err := errors.New("JavaScript drive handler rejected") err := errors.New("JavaScript drive handler rejected")
if len(args) > 0 && args[0].Type() == js.TypeString { if len(args) > 0 && args[0].Type() == js.TypeString {
err = fmt.Errorf("JavaScript drive handler rejected: %s", args[0].String()) err = fmt.Errorf("JavaScript drive handler rejected: %s", args[0].String())
} }
select { settleHandler(err)
case response.doneCh <- err:
default:
}
return nil return nil
}) })
defer func() {
readBodyChunk.Release()
writeHead.Release()
write.Release()
end.Release()
rejected.Release()
}()
jsReq := map[string]any{ jsReq := map[string]any{
"method": r.Method, "method": r.Method,
"path": r.URL.Path, "path": r.URL.Path,
@@ -214,14 +223,13 @@ func (fs *jsFileSystemForRemote) ServeHTTPWithPerms(
if hasThen, err := hasJSFunctionProperty(result, "then"); err != nil { if hasThen, err := hasJSFunctionProperty(result, "then"); err != nil {
handlerErr = err handlerErr = err
} else if hasThen { } else if hasThen {
_, handlerErr = callJSMethod(result, "then", js.Undefined(), rejected) _, handlerErr = callJSMethod(result, "then", fulfilled, rejected)
} else {
settleHandler(nil)
} }
} }
if handlerErr != nil { if handlerErr != nil {
select { settleHandler(handlerErr)
case response.doneCh <- handlerErr:
default:
}
} }
var resultErr error var resultErr error
+21 -7
View File
@@ -246,13 +246,16 @@ func wireTaildropFileOps(lb *ipnlocal.LocalBackend, jsObj js.Value) {
// ReadableStreamDefaultReader. Each Read call awaits one reader.read() Promise, // ReadableStreamDefaultReader. Each Read call awaits one reader.read() Promise,
// using the channel+FuncOf pattern so Go blocks until JS delivers the chunk. // using the channel+FuncOf pattern so Go blocks until JS delivers the chunk.
type jsStreamReader struct { type jsStreamReader struct {
mu sync.Mutex mu sync.Mutex
reader js.Value reader js.Value
buf []byte buf []byte
done bool done bool
err error err error
testCancelTimeout time.Duration
} }
const jsStreamCancelTimeout = time.Second
func (r *jsStreamReader) Read(p []byte) (int, error) { func (r *jsStreamReader) Read(p []byte) (int, error) {
r.mu.Lock() r.mu.Lock()
if r.err != nil { if r.err != nil {
@@ -386,8 +389,19 @@ func (r *jsStreamReader) Close() error {
if _, err := callJSMethod(promise, "then", resolveFn, rejectFn); err != nil { if _, err := callJSMethod(promise, "then", resolveFn, rejectFn); err != nil {
return r.setError(err) return r.setError(err)
} }
if err := <-resultCh; err != nil { timeout := jsStreamCancelTimeout
return r.setError(err) if r.testCancelTimeout > 0 {
timeout = r.testCancelTimeout
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case err := <-resultCh:
if err != nil {
return r.setError(err)
}
case <-timer.C:
return r.setError(errors.New("JavaScript stream cancel timed out"))
} }
r.mu.Lock() r.mu.Lock()
r.done = true r.done = true