diff --git a/client_ts_test.go b/client_ts_test.go index cc3b21d..3c0e4c7 100644 --- a/client_ts_test.go +++ b/client_ts_test.go @@ -167,6 +167,153 @@ result = await requestInterceptorClient.request("Svc", "interceptors", { value: assert.deepEqual(requestInterceptorSeen, { value: 1 }); assert.equal(result.msg, "intercepted"); +const stateClient = new Client("http://example.test"); +stateClient.setState({ shared: "global", globalOnly: "yes" }); +const stateContexts = new Map(); +const nativeResponses = new Map(); +const payloads = new Map(); +let releaseFirst; +const firstMayContinue = new Promise(resolve => { releaseFirst = resolve; }); +let firstInterceptorEntered; +const firstDidEnter = new Promise(resolve => { firstInterceptorEntered = resolve; }); +let firstStateReference; +stateClient.addRequestInterceptor(async (_service, method, state, dto) => { + state.interceptor = dto.id; + if (method === "first") { + firstStateReference = state; + firstInterceptorEntered(); + await firstMayContinue; + } + state.completed = dto.id; +}); +stateClient.addResponseInterceptor((_service, method, _value, context) => { + stateContexts.set(method, context); +}); +globalThis.fetch = async (_url, init) => { + const payload = JSON.parse(init.body); + payloads.set(payload.methodName, payload); + const response = json({ status: 0, data: payload.methodName }); + nativeResponses.set(payload.methodName, response); + return response; +}; +const firstOverride = { shared: "first", requestOnly: "one" }; +const firstRequest = stateClient.request("Svc", "first", { id: "one" }, { state: firstOverride }); +await firstDidEnter; +firstOverride.shared = "mutated outside"; +const secondRequest = stateClient.request("Svc", "second", { id: "two" }, { + state: { shared: "second", requestOnly: "two" }, +}); +assert.equal((await secondRequest).status, 0); +releaseFirst(); +assert.equal((await firstRequest).status, 0); +assert.deepEqual(payloads.get("first").state, { + shared: "first", + globalOnly: "yes", + requestOnly: "one", + interceptor: "one", + completed: "one", +}); +assert.deepEqual(payloads.get("second").state, { + shared: "second", + globalOnly: "yes", + requestOnly: "two", + interceptor: "two", + completed: "two", +}); +firstStateReference.shared = "mutated after snapshot"; +for (const method of ["first", "second"]) { + const context = stateContexts.get(method); + assert.deepEqual(context.requestState, payloads.get(method).state); + assert.equal(Object.isFrozen(context.requestState), true); + assert.equal(Object.isFrozen(context), true); + assert.equal(context.response, nativeResponses.get(method)); + assert.ok(context.response instanceof Response); +} +assert.equal(stateContexts.get("first").requestState.shared, "first"); + +const throwingState = Object.defineProperty({}, "token", { + enumerable: true, + get() { throw new Error("state read failed"); }, +}); +const stateFailureClient = new Client("http://example.test"); +let stateFailureSeen = []; +stateFailureClient.addResponseInterceptor((_service, _method, value, context) => { + stateFailureSeen.push([value.status, context.requestState]); +}); +globalThis.fetch = async () => assert.fail("fetch must not run for state failure"); +result = await stateFailureClient.request("Svc", "requestStateFailure", undefined, { state: throwingState }); +assert.equal(result.status, 1); +assert.match(result.msg, /prepare request state/i); +result = await stateFailureClient.stream("Svc", "streamStateFailure", undefined, () => {}, { state: throwingState }); +assert.equal(result.status, 1); +assert.match(result.msg, /prepare request state/i); +assert.deepEqual(stateFailureSeen, [[1, {}], [1, {}]]); + +const snapshotFailureClient = new Client("http://example.test"); +snapshotFailureClient.addRequestInterceptor((_service, _method, state) => { + Object.defineProperty(state, "broken", { + enumerable: true, + get() { throw new Error("snapshot read failed"); }, + }); +}); +globalThis.fetch = async () => assert.fail("fetch must not run for snapshot failure"); +result = await snapshotFailureClient.request("Svc", "requestSnapshotFailure"); +assert.equal(result.status, 1); +assert.match(result.msg, /snapshot request state/i); +result = await snapshotFailureClient.stream("Svc", "streamSnapshotFailure", undefined, () => {}); +assert.equal(result.status, 1); +assert.match(result.msg, /snapshot request state/i); + +const contextClient = new Client("http://example.test"); +contextClient.setState({ base: "global" }); +const outcomeContexts = new Map(); +contextClient.addResponseInterceptor((_service, method, _value, context) => { + outcomeContexts.set(method, context); +}); +contextClient.addRequestInterceptor((_service, method, state) => { + state.method = method; + if (method === "requestHookFailure") throw new Error("request context hook"); +}); +globalThis.fetch = async () => assert.fail("fetch must not run before serialization"); +const contextCyclic = {}; +contextCyclic.self = contextCyclic; +result = await contextClient.request("Svc", "serializeContext", contextCyclic, { state: { base: "request" } }); +assert.equal(result.status, 1); +assert.deepEqual(outcomeContexts.get("serializeContext").requestState, { + base: "request", + method: "serializeContext", +}); +assert.equal(outcomeContexts.get("serializeContext").response, undefined); +result = await contextClient.request("Svc", "requestHookFailure", undefined, { state: { request: "hook" } }); +assert.equal(result.status, 1); +assert.deepEqual(outcomeContexts.get("requestHookFailure").requestState, { + base: "global", + request: "hook", + method: "requestHookFailure", +}); +assert.equal(outcomeContexts.get("requestHookFailure").response, undefined); +globalThis.fetch = async () => { throw new TypeError("offline"); }; +result = await contextClient.request("Svc", "networkContext", undefined, { state: { request: "network" } }); +assert.equal(result.status, 4); +assert.deepEqual(outcomeContexts.get("networkContext").requestState, { + base: "global", + request: "network", + method: "networkContext", +}); +assert.equal(outcomeContexts.get("networkContext").response, undefined); +const bodyReadResponse = new Response(new ReadableStream({ + pull(controller) { controller.error(new Error("context body read failed")); }, +})); +globalThis.fetch = async () => bodyReadResponse; +result = await contextClient.request("Svc", "bodyReadContext", undefined, { state: { request: "body" } }); +assert.equal(result.status, 4); +assert.deepEqual(outcomeContexts.get("bodyReadContext").requestState, { + base: "global", + request: "body", + method: "bodyReadContext", +}); +assert.equal(outcomeContexts.get("bodyReadContext").response, bodyReadResponse); + const failingRequestInterceptor = new Client("http://example.test"); let normalized = []; failingRequestInterceptor.addRequestInterceptor(() => { throw new Error("request hook"); }); @@ -299,6 +446,81 @@ streamHookClient.addResponseInterceptor((_s, _m, value) => { normalized.push(val result = await streamHookClient.stream("Svc", "hook", undefined, () => {}); assert.equal(result.status, 1); assert.deepEqual(normalized, [1]); + +const streamStateClient = new Client("http://example.test"); +streamStateClient.setState({ shared: "global", globalOnly: "stream" }); +const streamContexts = new Map(); +const streamResponses = new Map(); +const streamPayloads = new Map(); +streamStateClient.addRequestInterceptor((_service, method, state) => { + state.interceptor = method; + if (method === "streamHookFailure") throw new Error("stream context hook"); +}); +streamStateClient.addResponseInterceptor((_service, method, _value, context) => { + streamContexts.set(method, context); +}); +globalThis.fetch = async (_url, init) => { + const payload = JSON.parse(init.body); + streamPayloads.set(payload.methodName, payload); + if (payload.methodName === "streamNetwork") throw new TypeError("stream offline"); + const response = payload.methodName === "streamWrongMedia" + ? json({ status: 0 }) + : payload.methodName === "streamReadFailure" + ? new Response(new ReadableStream({ + pull(controller) { controller.error(new Error("stream context read failed")); }, + }), { headers: { "Content-Type": "application/x-ndjson" } }) + : ndjson('{"n":1}\n'); + streamResponses.set(payload.methodName, response); + return response; +}; +result = await streamStateClient.stream("Svc", "streamSuccess", undefined, () => {}, { + state: { shared: "request", requestOnly: "success" }, +}); +assert.equal(result.status, 0); +result = await streamStateClient.stream("Svc", "streamWrongMedia", undefined, () => {}, { + state: { shared: "wrong-media" }, +}); +assert.equal(result.status, 1); +result = await streamStateClient.stream("Svc", "streamNetwork", undefined, () => {}, { + state: { shared: "network" }, +}); +assert.equal(result.status, 4); +result = await streamStateClient.stream("Svc", "streamReadFailure", undefined, () => {}, { + state: { shared: "read" }, +}); +assert.equal(result.status, 4); +const streamCyclic = {}; +streamCyclic.self = streamCyclic; +result = await streamStateClient.stream("Svc", "streamSerialize", streamCyclic, () => {}, { + state: { shared: "serialize" }, +}); +assert.equal(result.status, 1); +result = await streamStateClient.stream("Svc", "streamHookFailure", undefined, () => {}, { + state: { shared: "hook" }, +}); +assert.equal(result.status, 1); +for (const method of ["streamSuccess", "streamWrongMedia", "streamNetwork", "streamReadFailure"]) { + const context = streamContexts.get(method); + assert.deepEqual(context.requestState, streamPayloads.get(method).state); + assert.equal(Object.isFrozen(context.requestState), true); + assert.equal(Object.isFrozen(context), true); +} +assert.equal(streamContexts.get("streamSuccess").response, streamResponses.get("streamSuccess")); +assert.equal(streamContexts.get("streamWrongMedia").response, streamResponses.get("streamWrongMedia")); +assert.equal(streamContexts.get("streamReadFailure").response, streamResponses.get("streamReadFailure")); +assert.equal(streamContexts.get("streamNetwork").response, undefined); +assert.deepEqual(streamContexts.get("streamSerialize").requestState, { + shared: "serialize", + globalOnly: "stream", + interceptor: "streamSerialize", +}); +assert.equal(streamContexts.get("streamSerialize").response, undefined); +assert.deepEqual(streamContexts.get("streamHookFailure").requestState, { + shared: "hook", + globalOnly: "stream", + interceptor: "streamHookFailure", +}); +assert.equal(streamContexts.get("streamHookFailure").response, undefined); ` scriptPath := filepath.Join(dir, "behavior.mjs") if err := os.WriteFile(scriptPath, []byte(script), 0o644); err != nil { @@ -320,8 +542,35 @@ func TestTypeScriptClientStrictTypecheck(t *testing.T) { if err := os.WriteFile(path, []byte(templateTs{}.genClientTemplate()), 0o644); err != nil { t.Fatal(err) } + usagePath := filepath.Join(dir, "usage.ts") + const usage = `import { Client, type ContextResponseInterceptor, type RequestOptions, type ResponseContext, type ResponseInterceptor, type StreamOptions } from "./client"; + +const requestOptions: RequestOptions = { state: { token: "request" } }; +const streamOptions: StreamOptions = { state: { token: "stream" } }; +const legacy: ResponseInterceptor = (_service, _method, result) => result; +const legacyConsumer: (service: string, method: string, result: import("./client").result) => unknown = legacy; +const current: ContextResponseInterceptor = (_service, _method, result, context) => { + const token: string | undefined = context.requestState.token; + const response: Response | undefined = context.response; + void token; + void response; + return result; +}; +void legacyConsumer; +const context = {} as ResponseContext; +// @ts-expect-error requestState is readonly +context.requestState.token = "changed"; +const client = new Client("/"); +client.addResponseInterceptor(legacy); +client.addResponseInterceptor(current); +void client.request("Svc", "method", undefined, requestOptions); +void client.stream("Svc", "method", undefined, () => {}, streamOptions); +` + if err := os.WriteFile(usagePath, []byte(usage), 0o644); err != nil { + t.Fatal(err) + } command := exec.Command(tsc, - "--strict", "--noEmit", "--target", "ES2022", "--module", "ESNext", "--lib", "ES2022,DOM", path) + "--strict", "--noEmit", "--target", "ES2022", "--module", "ESNext", "--lib", "ES2022,DOM", path, usagePath) if output, err := command.CombinedOutput(); err != nil { t.Fatalf("strict TypeScript check failed: %v\n%s", err, output) } diff --git a/fun.go b/fun.go index bad5a0d..b11dc50 100644 --- a/fun.go +++ b/fun.go @@ -12,9 +12,9 @@ type Fun struct { methods map[string]methodInfo routes map[string]RouteHandler // 自定义路由:"GET /path" → 处理器(精确匹配) wildcardRoutes map[string][]wildcardRoute - boxes *sync.Map // 依赖容器:reflect.Type → reflect.Value - guards []*any // 全局 Guard - serviceGuards map[string][]*any // 服务级 Guard,按服务名 + boxes *sync.Map // 依赖容器:reflect.Type → reflect.Value + guards []*any // 全局 Guard + serviceGuards map[string][]*any // 服务级 Guard,按服务名 } // wildcardRoute 通配符路由(BindRoute path 以 "/*" 结尾注册): @@ -68,16 +68,7 @@ func GetFun() *Fun { // // guardList 为该服务绑定的 Guard,方法调用前按注册顺序执行 func (f *Fun) BindService(service any, guardList ...Guard) { - t := reflect.TypeOf(service) - // 必须是指针指向的结构体,匿名类型无法注册 - if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { - panic("fun: BindService requires a pointer to a struct") - } - name := t.Elem().Name() - if name == "" { - panic("fun: BindService requires a named type") - } - + t, name := serviceType(service) boxWired(service, f) serviceGuards := make([]*any, 0, len(guardList)) @@ -86,7 +77,29 @@ func (f *Fun) BindService(service any, guardList ...Guard) { serviceGuards = append(serviceGuards, serviceGuardWired(guard, f)) } f.serviceGuards[name] = serviceGuards + f.bindServiceMethods(t, name) +} +// BindServiceForGen registers service metadata for code generation without +// constructing runtime dependencies or guards. +func (f *Fun) BindServiceForGen(service any) { + t, name := serviceType(service) + f.bindServiceMethods(t, name) +} + +func serviceType(service any) (reflect.Type, string) { + t := reflect.TypeOf(service) + if t == nil || t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { + panic("fun: BindService requires a pointer to a struct") + } + name := t.Elem().Name() + if name == "" { + panic("fun: BindService requires a named type") + } + return t, name +} + +func (f *Fun) bindServiceMethods(t reflect.Type, name string) { for m := range t.Methods() { m := m // Ctx 命名持有 *fasthttp.RequestCtx(非嵌入),服务方法集只含业务方法,无需过滤提升方法 diff --git a/gen_reliability_test.go b/gen_reliability_test.go index 83ad770..2fdb955 100644 --- a/gen_reliability_test.go +++ b/gen_reliability_test.go @@ -3,6 +3,7 @@ package fun import ( "os" "path/filepath" + "reflect" "sort" "strings" "testing" @@ -31,6 +32,16 @@ type ZebraGenSvc struct{} func (*ZebraGenSvc) Watch() (*Stream, error) { return &Stream{}, nil } +type GenOnlyDependency struct{} + +func (*GenOnlyDependency) New() { panic("generation initialized a runtime dependency") } + +type DependencyGenSvc struct { + Dependency *GenOnlyDependency +} + +func (*DependencyGenSvc) Ping() error { return nil } + func isolateGeneratorGlobals(t *testing.T) { t.Helper() oldFun, oldDirectory := fun, directory @@ -69,6 +80,18 @@ func generatedFiles(t *testing.T, root string) map[string]string { return files } +func TestBindServiceForGenDoesNotInitializeDependencies(t *testing.T) { + isolateGeneratorGlobals(t) + f := GetFun() + f.BindServiceForGen(&DependencyGenSvc{}) + if _, ok := f.methods["DependencyGenSvc.Ping"]; !ok { + t.Fatal("generation-only service method was not registered") + } + if _, ok := f.boxes.Load(reflect.TypeFor[*GenOnlyDependency]()); ok { + t.Fatal("generation-only registration stored a runtime dependency") + } +} + func TestGeneratedTypeScriptSignaturesAndImports(t *testing.T) { isolateGeneratorGlobals(t) f := GetFun() @@ -87,6 +110,20 @@ func TestGeneratedTypeScriptSignaturesAndImports(t *testing.T) { return string(body) } + client := read("client.ts") + for _, want := range []string{ + `state?: Record;`, + `export type ResponseContext = {`, + `readonly requestState: Readonly>;`, + `readonly response?: Response;`, + `state = { ...this.state, ...options?.state };`, + `interceptor(serviceName, methodName, current, context)`, + } { + if !strings.Contains(client, want) { + t.Errorf("client.ts missing %q:\n%s", want, client) + } + } + alpha := read("alphaGenSvc.ts") if first := strings.SplitN(alpha, "\n", 2)[0]; first != `import { Client, type result, type RequestOptions } from "./client";` { t.Fatalf("unexpected request-only imports: %s", first) diff --git a/template_ts.go b/template_ts.go index 270bd41..69d393b 100644 --- a/template_ts.go +++ b/template_ts.go @@ -16,10 +16,12 @@ export type resultStatus = 0 | 1 | 2 | 4 | 5; export type RequestOptions = { signal?: AbortSignal; + state?: Record; }; export type StreamOptions = { signal?: AbortSignal; + state?: Record; }; export type RequestInterceptor = ( @@ -29,12 +31,24 @@ export type RequestInterceptor = ( dto?: any ) => Promise | void; +export type ResponseContext = { + readonly requestState: Readonly>; + readonly response?: Response; +}; + export type ResponseInterceptor = ( serviceName: string, methodName: string, result: result ) => Promise | void> | result | void; +export type ContextResponseInterceptor = ( + serviceName: string, + methodName: string, + result: result, + context: ResponseContext +) => Promise | void> | result | void; + function messageOf(error: unknown): string { if (error instanceof Error && error.message) return error.message; if (typeof error === "string" && error) return error; @@ -137,7 +151,7 @@ export class Client { private url: string; private state: Record = {}; private requestInterceptors: RequestInterceptor[] = []; - private responseInterceptors: ResponseInterceptor[] = []; + private responseInterceptors: ContextResponseInterceptor[] = []; constructor(url: string) { this.url = url.replace(/\/+$/, ""); @@ -151,19 +165,26 @@ export class Client { this.requestInterceptors.push(interceptor); } - addResponseInterceptor(interceptor: ResponseInterceptor) { - this.responseInterceptors.push(interceptor); + addResponseInterceptor(interceptor: ResponseInterceptor): void; + addResponseInterceptor(interceptor: ContextResponseInterceptor): void; + addResponseInterceptor(interceptor: ResponseInterceptor | ContextResponseInterceptor) { + this.responseInterceptors.push(interceptor as ContextResponseInterceptor); } private async interceptResponse( serviceName: string, methodName: string, - initial: result + initial: result, + requestState: Readonly>, + response?: Response ): Promise> { let current = initial; + const context: ResponseContext = Object.freeze( + response === undefined ? { requestState } : { requestState, response } + ); for (const interceptor of this.responseInterceptors) { try { - const replaced = await interceptor(serviceName, methodName, current); + const replaced = await interceptor(serviceName, methodName, current, context); if (replaced) current = replaced; } catch (error) { current = failure(1, ` + "`Response interceptor failed: ${messageOf(error)}`" + `); @@ -172,12 +193,19 @@ export class Client { return current; } - private async requestState(serviceName: string, methodName: string, dto: any): Promise> { - const state: Record = { ...this.state }; + private async interceptRequest( + serviceName: string, + methodName: string, + state: Record, + dto: any + ): Promise { for (const interceptor of this.requestInterceptors) { await interceptor(serviceName, methodName, state, dto); } - return state; + } + + private snapshotState(state: Record): Readonly> { + return Object.freeze({ ...state }); } async request( @@ -188,12 +216,45 @@ export class Client { ): Promise> { let state: Record; try { - state = await this.requestState(serviceName, methodName, dto); + state = { ...this.state, ...options?.state }; } catch (error) { return await this.interceptResponse( serviceName, methodName, - failure(1, ` + "`Request interceptor failed: ${messageOf(error)}`" + `) + failure(1, ` + "`Could not prepare request state: ${messageOf(error)}`" + `), + Object.freeze({}) + ) as result; + } + try { + await this.interceptRequest(serviceName, methodName, state, dto); + } catch (error) { + let requestState: Readonly>; + try { + requestState = this.snapshotState(state); + } catch (snapshotError) { + return await this.interceptResponse( + serviceName, + methodName, + failure(1, ` + "`Could not snapshot request state: ${messageOf(snapshotError)}`" + `), + Object.freeze({}) + ) as result; + } + return await this.interceptResponse( + serviceName, + methodName, + failure(1, ` + "`Request interceptor failed: ${messageOf(error)}`" + `), + requestState + ) as result; + } + let requestState: Readonly>; + try { + requestState = this.snapshotState(state); + } catch (error) { + return await this.interceptResponse( + serviceName, + methodName, + failure(1, ` + "`Could not snapshot request state: ${messageOf(error)}`" + `), + Object.freeze({}) ) as result; } @@ -203,7 +264,7 @@ export class Client { serviceName, methodName, data: dto, - ...(Object.keys(state).length ? { state } : {}), + ...(Object.keys(requestState).length ? { state: requestState } : {}), }); if (serialized === undefined) throw new Error("serialization produced no output"); body = serialized; @@ -211,13 +272,15 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - failure(1, ` + "`Could not serialize request: ${messageOf(error)}`" + `) + failure(1, ` + "`Could not serialize request: ${messageOf(error)}`" + `), + requestState ) as result; } let output: result; + let response: Response | undefined; try { - const response = await fetch(` + "`${this.url}/cell`" + `, { + response = await fetch(` + "`${this.url}/cell`" + `, { method: "POST", headers: { "Content-Type": "application/json" }, body, @@ -231,7 +294,7 @@ export class Client { } catch (error) { output = requestFailure(error, options?.signal, false); } - return await this.interceptResponse(serviceName, methodName, output) as result; + return await this.interceptResponse(serviceName, methodName, output, requestState, response) as result; } async stream( @@ -243,12 +306,45 @@ export class Client { ): Promise> { let state: Record; try { - state = await this.requestState(serviceName, methodName, dto); + state = { ...this.state, ...options?.state }; } catch (error) { return await this.interceptResponse( serviceName, methodName, - failure(1, ` + "`Request interceptor failed: ${messageOf(error)}`" + `) + failure(1, ` + "`Could not prepare request state: ${messageOf(error)}`" + `), + Object.freeze({}) + ); + } + try { + await this.interceptRequest(serviceName, methodName, state, dto); + } catch (error) { + let requestState: Readonly>; + try { + requestState = this.snapshotState(state); + } catch (snapshotError) { + return await this.interceptResponse( + serviceName, + methodName, + failure(1, ` + "`Could not snapshot request state: ${messageOf(snapshotError)}`" + `), + Object.freeze({}) + ); + } + return await this.interceptResponse( + serviceName, + methodName, + failure(1, ` + "`Request interceptor failed: ${messageOf(error)}`" + `), + requestState + ); + } + let requestState: Readonly>; + try { + requestState = this.snapshotState(state); + } catch (error) { + return await this.interceptResponse( + serviceName, + methodName, + failure(1, ` + "`Could not snapshot request state: ${messageOf(error)}`" + `), + Object.freeze({}) ); } @@ -258,7 +354,7 @@ export class Client { serviceName, methodName, data: dto, - ...(Object.keys(state).length ? { state } : {}), + ...(Object.keys(requestState).length ? { state: requestState } : {}), }); if (serialized === undefined) throw new Error("serialization produced no output"); body = serialized; @@ -266,7 +362,8 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - failure(1, ` + "`Could not serialize request: ${messageOf(error)}`" + `) + failure(1, ` + "`Could not serialize request: ${messageOf(error)}`" + `), + requestState ); } @@ -282,7 +379,8 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - requestFailure(error, options?.signal, true) + requestFailure(error, options?.signal, true), + requestState ); } @@ -294,10 +392,18 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - responseReadFailure(error, response, options?.signal, true) + responseReadFailure(error, response, options?.signal, true), + requestState, + response ); } - return await this.interceptResponse(serviceName, methodName, parseResult(response, text)); + return await this.interceptResponse( + serviceName, + methodName, + parseResult(response, text), + requestState, + response + ); } if (mediaType(response) !== "application/x-ndjson") { @@ -308,7 +414,9 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - responseReadFailure(error, response, options?.signal, true) + responseReadFailure(error, response, options?.signal, true), + requestState, + response ); } const rpcResult = parseResult(response, text); @@ -317,12 +425,20 @@ export class Client { methodName, rpcResult.status === 0 ? failure(1, "Expected application/x-ndjson response") - : rpcResult + : rpcResult, + requestState, + response ); } if (!response.body) { - return await this.interceptResponse(serviceName, methodName, { status: 0 }); + return await this.interceptResponse( + serviceName, + methodName, + { status: 0 }, + requestState, + response + ); } let reader: ReadableStreamDefaultReader; @@ -332,7 +448,9 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - responseReadFailure(error, response, options?.signal, true) + responseReadFailure(error, response, options?.signal, true), + requestState, + response ); } const decoder = new TextDecoder("utf-8", { fatal: true }); @@ -426,7 +544,9 @@ export class Client { return await this.interceptResponse( serviceName, methodName, - failed || { status: 0 } + failed || { status: 0 }, + requestState, + response ); } }`