@xxxaz/json-rpc-schema-typing 0.10.20 → 0.10.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ import { type WebSocket as NodeWebSocket, type RawData } from 'ws';
2
+ import { type JsonSerializable } from "@xxxaz/stream-api-json";
3
+
4
+ export type WrapableWebSocket = WebSocket|NodeWebSocket;
5
+ export function wrapWebSocket(socket: WrapableWebSocket, listener: SocketListener, closedListener: ScokcetClosedListener) {
6
+ if ('WebSocket' in globalThis && socket instanceof WebSocket) {
7
+ return new BrowserWebSocketWrapper(socket, listener, closedListener);
8
+ }
9
+ return new NodeWebSocketWrapper(socket as NodeWebSocket, listener, closedListener);
10
+ }
11
+
12
+ type ClosedData = {
13
+ code: number;
14
+ reason: string;
15
+ wasClean?: boolean;
16
+ wrapper: WebSocketWrapper;
17
+ };
18
+
19
+ type SocketListener = (data: JsonSerializable) => void;
20
+ type ScokcetClosedListener = (data: ClosedData) => void;
21
+
22
+ type ConnectionState = 'CONNECTING'|'OPEN'|'CLOSING'|'CLOSED';
23
+ export type WebSocketState = (typeof WebSocket)[ConnectionState];
24
+
25
+ export interface WebSocketWrapper<Socket extends WrapableWebSocket = WrapableWebSocket> {
26
+ readonly socket: Socket;
27
+ readonly listener: SocketListener;
28
+ readonly readyState: WebSocketState;
29
+ send(data: JsonSerializable): void;
30
+ detach(): void;
31
+ close(): void;
32
+ }
33
+
34
+ export class NodeWebSocketWrapper implements WebSocketWrapper<NodeWebSocket> {
35
+ constructor(
36
+ readonly socket: NodeWebSocket,
37
+ readonly listener: SocketListener,
38
+ readonly closedListener: ScokcetClosedListener
39
+ ) {
40
+ socket.on('message', this.#onMessage);
41
+ socket.on('close', (code, reasonBuffer) => {
42
+ const reason = reasonBuffer.toString();
43
+ this.closedListener({ code, reason, wrapper: this });
44
+ this.socket.off('message', this.#onMessage);
45
+ });
46
+ }
47
+
48
+ get readyState(): WebSocketState {
49
+ return this.socket.readyState;
50
+ }
51
+
52
+ send(data: JsonSerializable): void {
53
+ this.socket.send(JSON.stringify(data));
54
+ }
55
+
56
+ detach(): void {
57
+ this.socket.off('message', this.#onMessage);
58
+ }
59
+
60
+ close(): void {
61
+ this.detach();
62
+ this.socket.close();
63
+ }
64
+
65
+ readonly #onMessage = async (data: RawData) => {
66
+ const parsed = await this.#parseMessage(data);
67
+ this.listener(parsed);
68
+ };
69
+
70
+ async #parseMessage(data: RawData) : Promise<JsonSerializable> {
71
+ if (data instanceof ArrayBuffer) {
72
+ return JSON.parse(await new Blob([data]).text());
73
+ }
74
+ const json
75
+ = data instanceof Array
76
+ ? data.map(d=>JSON.parse(d.toString())).join('')
77
+ : data.toString()
78
+ return JSON.parse(json);
79
+ }
80
+ }
81
+
82
+ export class BrowserWebSocketWrapper implements WebSocketWrapper<WebSocket> {
83
+ constructor(
84
+ readonly socket: WebSocket,
85
+ readonly listener: SocketListener,
86
+ readonly closedListener: ScokcetClosedListener
87
+ ) {
88
+ socket.addEventListener('message', this.#onMessage);
89
+ socket.addEventListener('close', ev => {
90
+ closedListener({ code: ev.code, reason: ev.reason, wasClean: ev.wasClean, wrapper: this });
91
+ this.socket.removeEventListener('message', this.#onMessage);
92
+ });
93
+ }
94
+
95
+ get readyState(): WebSocketState {
96
+ return this.socket.readyState as WebSocketState;
97
+ }
98
+
99
+ readonly #onMessage = async (ev: MessageEvent) => {
100
+ const parsed = await this.#parseMessage(ev.data);
101
+ this.listener(parsed);
102
+ };
103
+
104
+ async #parseMessage(data: ArrayBuffer|string|object) : Promise<JsonSerializable> {
105
+ if (data instanceof ArrayBuffer) {
106
+ return JSON.parse(await new Blob([data]).text());
107
+ }
108
+ try {
109
+ if (typeof data === 'string') {
110
+ return JSON.parse(data);
111
+ }
112
+ } catch (e) {
113
+ console.error('failed to parse message', data, e);
114
+ }
115
+ return data as JsonSerializable;
116
+ }
117
+
118
+ send(data: JsonSerializable): void {
119
+ this.socket.send(JSON.stringify(data));
120
+ }
121
+
122
+ detach(): void {
123
+ this.socket.removeEventListener('message', this.#onMessage);
124
+ }
125
+
126
+ close(): void {
127
+ this.detach();
128
+ this.socket.close();
129
+ }
130
+ }
@@ -0,0 +1,269 @@
1
+ import { JsonStreamingParser, ParsingJsonArray } from '@xxxaz/stream-api-json';
2
+ import { ClientUncaughtError, InvalidParams, JsonRpcException } from "../JsonRpcException.js";
3
+ import { JsonRpcMethodSchema, ParameterSchema, Params, Return } from "../JsonRpcMethod.js";
4
+ import { JsonRpcValidator } from "../JsonRpcValidator.js";
5
+ import { type JsonRpcSchema } from "../router/JsonRpcRouter.js";
6
+ import { JsonRpcRequest, JsonRpcResponse } from "../types.js";
7
+ import { LazyResolvers } from '@xxxaz/stream-api-json/utility';
8
+ import { stringifyStream } from '../utility.js';
9
+
10
+
11
+ type PostRpc = (request: ReadableStream<string>) => Promise<ReadableStream<string>>;
12
+ export type GenereteId = () => Promise<string|number>;
13
+
14
+ type JsonRpcClientOptions<Sch extends JsonRpcSchema> = {
15
+ schema: Sch;
16
+ post: PostRpc;
17
+ batch?: PostRpc;
18
+ generateId?: GenereteId;
19
+ };
20
+
21
+ const $generateId: unique symbol = Symbol('GenereteId');
22
+ const $requestStack: unique symbol = Symbol('RequestsStack');
23
+ const $methodPath: unique symbol = Symbol('MethodPath');
24
+
25
+ type TriggerFunction<ParamSch extends ParameterSchema, RtnSch> = {
26
+ (...args: Params<ParamSch>): Promise<Return<RtnSch>>;
27
+ notice(...args: Params<ParamSch>): void;
28
+ };
29
+
30
+ type JsonRpcCaller<Schema extends JsonRpcSchema> = {
31
+ readonly [K in keyof Schema]
32
+ : Schema[K] extends JsonRpcMethodSchema<infer P, infer R>
33
+ ? TriggerFunction<P, R>
34
+ : Schema[K] extends JsonRpcSchema
35
+ ? JsonRpcCaller<Schema[K]>
36
+ : never;
37
+ } & {
38
+ readonly [$requestStack]: RequestsStack;
39
+ readonly [$methodPath]: string[];
40
+ };
41
+
42
+ type RpcWait = {
43
+ request: JsonRpcRequest;
44
+ promise: PromiseWithResolvers<JsonRpcResponse<any>|void>;
45
+ };
46
+
47
+ export class JsonRpcClient<Schema extends JsonRpcSchema> {
48
+ readonly #schema: Schema;
49
+ readonly #postRpc: PostRpc;
50
+ readonly #postBatch: PostRpc;
51
+ readonly [$generateId]: GenereteId;
52
+
53
+ constructor(options: JsonRpcClientOptions<Schema>) {
54
+ this.#schema = options.schema;
55
+ this.#postRpc = options.post;
56
+ this.#postBatch = options.batch ?? options.post;
57
+ this[$generateId] = options.generateId ?? JsonRpcClient.defaultIdGenerator;
58
+ }
59
+
60
+ static async defaultIdGenerator() {
61
+ return crypto.randomUUID();
62
+ }
63
+
64
+ async postRpc(request: JsonRpcRequest) {
65
+ const response = await this.#postRpc(stringifyStream(request));
66
+ const streamJson = await JsonStreamingParser.readFrom(response).root();
67
+ const result = await streamJson.all();
68
+ if ('error' in result) {
69
+ throw JsonRpcException.deserialize(result.error);
70
+ }
71
+ if (request.id == null) return;
72
+ return result as JsonRpcResponse<any>;
73
+ }
74
+
75
+ async postBatch(requests: RpcWait[]) {
76
+ const waits = new Map(
77
+ requests
78
+ .filter(({ request: { id } }) => id != null)
79
+ .map(({ request, promise }) => [request.id as string, { request, promise }])
80
+ );
81
+ const noWaits = new Set(requests.filter(({ request: { id } }) => id == null));
82
+
83
+ try {
84
+ const requestList = requests.map(({ request: r }) => r);
85
+ const response = await this.#postBatch(stringifyStream(requestList));
86
+ const streamJson = await JsonStreamingParser.readFrom(response).root();
87
+ if (streamJson instanceof ParsingJsonArray) {
88
+ const results = [] as JsonRpcResponse<any>[];
89
+ for await (const responseStream of streamJson) {
90
+ const res: JsonRpcResponse<any> = await responseStream.all() ?? {};
91
+ results.push(res);
92
+
93
+ const { id, error } = res as any;
94
+ const { promise } = waits.get(id) ?? {};
95
+ waits.delete(id);
96
+
97
+ if (!promise) {
98
+ console.warn('Orphan rpc response.', res)
99
+ continue;
100
+ }
101
+ if (error) {
102
+ promise.reject(JsonRpcException.deserialize(error));
103
+ continue;
104
+ }
105
+ promise.resolve(res);
106
+ }
107
+ return results;
108
+ }
109
+
110
+ const singleResult = await streamJson.all();
111
+ const { id, error } = singleResult ?? {};
112
+ if (id != null) {
113
+ const { promise } = waits.get(id) ?? {};
114
+ if (promise) {
115
+ if (error) {
116
+ promise.reject(JsonRpcException.deserialize(error));
117
+ } else {
118
+ promise.resolve(singleResult);
119
+ }
120
+ } else {
121
+ console.warn('Orphan rpc response.', singleResult);
122
+ }
123
+ return [singleResult];
124
+ }
125
+
126
+ const exceptin = error
127
+ ? JsonRpcException.deserialize(error)
128
+ : new ClientUncaughtError('Unexpected response.', singleResult);
129
+ for (const { promise } of waits.values()) promise.reject(exceptin);
130
+ for (const { promise } of noWaits) promise.reject(exceptin);
131
+ noWaits.clear();
132
+ return [];
133
+ } catch (e) {
134
+ console.error(e);
135
+ throw e;
136
+ } finally {
137
+ for(const { promise, request } of waits.values()) {
138
+ promise.reject(new ClientUncaughtError('Orphan rpc request.', request));
139
+ }
140
+ noWaits.forEach(({ promise }) => promise.resolve());
141
+ }
142
+ }
143
+
144
+ #rpc?: JsonRpcCaller<Schema>;
145
+ get rpc(): JsonRpcCaller<Schema> {
146
+ return this.#rpc ??= proxyRpc(new NoStack(this), this.#schema);
147
+ }
148
+
149
+ #batch?: JsonRpcCaller<Schema>;
150
+ get batch(): JsonRpcCaller<Schema> {
151
+ return this.#batch ??= proxyRpc(new BatchStack(this), this.#schema);
152
+ }
153
+ kickBatch() {
154
+ return (this.batch[$requestStack] as BatchStack).kick();
155
+ }
156
+
157
+ lazy(delayMs: number = 0): JsonRpcCaller<Schema> {
158
+ return proxyRpc(new LazyStack(this, delayMs), this.#schema);
159
+ }
160
+ }
161
+
162
+ abstract class RequestsStack {
163
+ abstract stack(id: boolean, method: string[], params: any): Promise<JsonRpcResponse<any>|void>;
164
+
165
+ constructor(readonly client: JsonRpcClient<any>) {}
166
+ protected async buildRequest(requireId: boolean, methodPath: string[], params: any) : Promise<JsonRpcRequest> {
167
+ const jsonrpc = '2.0' as const;
168
+ const method = methodPath.join('.');
169
+ const id = requireId ? await this.client[$generateId]() : null;
170
+ return id ? { jsonrpc, id, method, params }: { jsonrpc, method, params };
171
+ }
172
+ }
173
+
174
+ class NoStack extends RequestsStack {
175
+ async stack(requireId: boolean, methodPath: string[], params: any) {
176
+ const request = await this.buildRequest(requireId, methodPath, params);
177
+ return this.client.postRpc(request);
178
+ }
179
+ }
180
+
181
+ class BatchStack extends RequestsStack {
182
+ #requests: Promise<RpcWait>[] = [];
183
+ get currentSize() {
184
+ return this.#requests.length;
185
+ }
186
+
187
+ stack(requireId: boolean, methodPath: string[], params: any) {
188
+ const resolver = new LazyResolvers<JsonRpcResponse<any>|void>();
189
+ const wait
190
+ = this.buildRequest(requireId, methodPath, params)
191
+ .then((request)=> ({ request, promise: resolver }));
192
+ this.#requests.push(wait);
193
+ return resolver.promise;
194
+ }
195
+
196
+ async kick() {
197
+ if (!this.#requests.length) return [];
198
+ const requestsPromises = this.#requests;
199
+ this.#requests = [];
200
+ const requests = await Promise.all(requestsPromises);
201
+ return this.client.postBatch(requests);
202
+ }
203
+ }
204
+
205
+ class LazyStack extends BatchStack {
206
+ constructor(client: JsonRpcClient<any>, readonly delayMs: number) {
207
+ super(client);
208
+ }
209
+
210
+ stack(requireId: boolean, methodPath: string[], params: any) {
211
+ if (!this.currentSize) {
212
+ setTimeout(() => this.kick(), this.delayMs);
213
+ }
214
+ return super.stack(requireId, methodPath, params);
215
+ }
216
+ }
217
+
218
+ function proxyRpc<Sch extends JsonRpcSchema>(stack: RequestsStack, schema: Sch, methodPath: string[] = []) : JsonRpcCaller<Sch> {
219
+ type Property = TriggerFunction<any, any>|JsonRpcCaller<any>|undefined;
220
+ const pickProperty = (key: string): Property => {
221
+ const route = schema[key];
222
+ if(!route) return undefined;
223
+ const path = [...methodPath, key];
224
+ if('$params' in route || '$return' in route) {
225
+ const fn = triggerFunction(stack, route as JsonRpcMethodSchema<any, any>, path);
226
+ return fn;
227
+ }
228
+ return proxyRpc(stack, route as JsonRpcSchema, path) as JsonRpcCaller<any>;
229
+ };
230
+
231
+ const cache = { [$requestStack]: stack } as Record<string, Property>;
232
+ return new Proxy(schema as any, {
233
+ get(_, key: string) {
234
+ return cache[key] ??= pickProperty(key);
235
+ }
236
+ });
237
+ }
238
+
239
+
240
+ function triggerFunction<Sch extends JsonRpcMethodSchema<any, any>>(stack: RequestsStack, schema: Sch, methodPath: string[]) : TriggerFunction<Sch['$params'], Sch['$return']> {
241
+ const validator = new JsonRpcValidator(schema);
242
+ const validateParams = (params: any[]) => {
243
+ if(schema.$params?.type === 'object') {
244
+ if (params instanceof Array && params.length === 1) {
245
+ validator.validateParams(params[0]);
246
+ return params[0];
247
+ } else {
248
+ throw new InvalidParams('Expected params to be an object but received multiple parameters.');
249
+ }
250
+ }
251
+ validator.validateParams(params);
252
+ return params;
253
+ };
254
+
255
+ const fn = async (...params: any[]) => {
256
+ params = validateParams(params);
257
+ const response = await stack.stack(true, methodPath, params) ?? {} as JsonRpcResponse<any>;
258
+ if ('error' in response) {
259
+ throw JsonRpcException.deserialize(response.error);
260
+ }
261
+ validator.validateReturn(response.result);
262
+ return response.result;
263
+ };
264
+ fn.notice = (...params: any[]) => {
265
+ params = validateParams(params);
266
+ stack.stack(false, methodPath, params);
267
+ };
268
+ return fn;
269
+ }
@@ -0,0 +1,114 @@
1
+ import http2, { OutgoingHttpHeaders } from 'http2';
2
+ import { ClientHttpError } from "../JsonRpcException.js";
3
+ import { JsonRpcSchema } from "../router/JsonRpcRouter.js";
4
+ import { GenereteId, JsonRpcClient } from "./JsonRpcClient.js";
5
+ import { readStreamAll } from "../utility.js";
6
+
7
+ type HeadersGenerator = (body: Blob) => Promise<HeadersInit>|HeadersInit;
8
+
9
+ type JsonRpcHttpClientOptions<Sch extends JsonRpcSchema> = {
10
+ schema: Sch;
11
+ generateId?: GenereteId;
12
+ postUrl: string|URL;
13
+ batchUrl?: string|URL;
14
+ requestConverter?: (request: ReadableStream<string>) => ReadableStream<Uint8Array>;
15
+ responseConverter?: (response: ReadableStream<Uint8Array>, contentType: string|null) => ReadableStream<string>;
16
+ headers?: HeadersGenerator|HeadersInit;
17
+ init?: OutgoingHttpHeaders;
18
+ };
19
+
20
+ export class JsonRpcHttp2Client<Sch extends JsonRpcSchema> extends JsonRpcClient<Sch> {
21
+
22
+ constructor(options: JsonRpcHttpClientOptions<Sch>) {
23
+ const { schema, generateId, postUrl, batchUrl } = options;
24
+ super({
25
+ schema,
26
+ generateId,
27
+ post: (request) => this.#post(postUrl, request),
28
+ batch: (request) => this.#post(batchUrl ?? postUrl, request),
29
+ });
30
+ this.postUrl = postUrl;
31
+ this.batchUrl = batchUrl ?? postUrl;
32
+
33
+ this.#requestConverter = options.requestConverter;
34
+ this.#responseConverter = options.responseConverter;
35
+ this.#initGenerator = async (body) => {
36
+ const init = options.headers;
37
+ const headers = (typeof init === 'function')
38
+ ? new Headers(await init(body))
39
+ : new Headers(init);
40
+ if (!headers.has('Content-Type')) {
41
+ headers.set('Content-Type', 'application/json');
42
+ }
43
+ return {
44
+ ...options.init,
45
+ ...Object.fromEntries(headers.entries()),
46
+ };
47
+ };
48
+ }
49
+
50
+ readonly postUrl: string|URL;
51
+ readonly batchUrl: string|URL;
52
+ readonly #requestConverter?: (request: ReadableStream<string>) => ReadableStream<any>;
53
+ readonly #responseConverter?: (response: ReadableStream<any>, contentType: string|null) => ReadableStream<string>;
54
+ readonly #initGenerator: (body: Blob) => Promise<OutgoingHttpHeaders>;
55
+
56
+ async #post(url: string|URL, request: ReadableStream<string>) {
57
+ if (this.#requestConverter) request = this.#requestConverter(request);
58
+ url = new URL(url);
59
+ const body = await readStreamAll(request);
60
+ const init = await this.#initGenerator(body);
61
+
62
+ const client = http2.connect(url.origin, {
63
+ protocol: url.protocol === 'http:' ? 'http:' : undefined,
64
+ });
65
+ const clientError = new Promise<void>((_, reject) => {
66
+ client.on('error', (err) => reject(err));
67
+ });
68
+
69
+ const {
70
+ HTTP2_HEADER_METHOD,
71
+ HTTP2_HEADER_PATH,
72
+ HTTP2_HEADER_STATUS,
73
+ } = http2.constants;
74
+
75
+ try{
76
+ const req = client.request({
77
+ ...init,
78
+ [HTTP2_HEADER_METHOD]: 'POST',
79
+ [HTTP2_HEADER_PATH]: url.pathname + url.search,
80
+ });
81
+
82
+ const headerPromise = Promise.race([
83
+ new Promise((resolve, reject) => {
84
+ req.on('error', reject);
85
+ req.on('response', resolve);
86
+ }),
87
+ clientError,
88
+ ]);
89
+
90
+ req.setEncoding('utf8');
91
+ req.write(new Uint8Array(await body.arrayBuffer()));
92
+ req.end();
93
+
94
+ const headers = await headerPromise as Record<string, string|string[]>;
95
+ const status = Number(headers[HTTP2_HEADER_STATUS] instanceof Array ? headers[HTTP2_HEADER_STATUS][0] : headers[HTTP2_HEADER_STATUS]);
96
+ if (Math.floor(status / 100) !== 2) {
97
+ const message = `${status} Satus Code is not OK`;
98
+ console.warn(new ClientHttpError(message, { headers }));
99
+ }
100
+ const stream = new ReadableStream({
101
+ start(controller) {
102
+ req.on('error', (error) => controller.error(error));
103
+ req.on('data', (chunk) => controller.enqueue(chunk));
104
+ req.on('end', () => controller.close());
105
+ }
106
+ });
107
+ return this.#responseConverter
108
+ ? this.#responseConverter(stream, headers['content-type'] as string)
109
+ : stream;
110
+ } finally {
111
+ client.close();
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,71 @@
1
+ import { responseToTextStream } from "@xxxaz/stream-api-json/helpers-browser";
2
+ import { ClientHttpError } from "../JsonRpcException.js";
3
+ import { JsonRpcSchema } from "../router/JsonRpcRouter.js";
4
+ import { GenereteId, JsonRpcClient } from "./JsonRpcClient.js";
5
+ import { emptyStrem, readStreamAll } from "../utility.js";
6
+
7
+ type HeadersGenerator = (body: Blob) => Promise<HeadersInit>|HeadersInit;
8
+
9
+ type JsonRpcHttpClientOptions<Sch extends JsonRpcSchema> = {
10
+ schema: Sch;
11
+ generateId?: GenereteId;
12
+ postUrl: string|URL;
13
+ batchUrl?: string|URL;
14
+ requestConverter?: (request: ReadableStream<string>) => ReadableStream<Uint8Array>;
15
+ responseConverter?: (response: ReadableStream<Uint8Array>, contentType: string|null) => ReadableStream<string>;
16
+ headers?: HeadersGenerator|HeadersInit;
17
+ init?: Pick<RequestInit, 'credentials'|'keepalive'|'mode'|'priority'|'redirect'|'referrer'|'referrerPolicy'>;
18
+ };
19
+
20
+ export class JsonRpcHttpClient<Sch extends JsonRpcSchema> extends JsonRpcClient<Sch> {
21
+
22
+ constructor(options: JsonRpcHttpClientOptions<Sch>) {
23
+ const { schema, generateId, postUrl, batchUrl } = options;
24
+ super({
25
+ schema,
26
+ generateId,
27
+ post: (request) => this.#post(postUrl, request),
28
+ batch: (request) => this.#post(batchUrl ?? postUrl, request),
29
+ });
30
+ this.postUrl = postUrl;
31
+ this.batchUrl = batchUrl ?? postUrl;
32
+
33
+ this.#requestConverter = options.requestConverter;
34
+ this.#responseConverter = options.responseConverter;
35
+ this.#initGenerator = async (body) => {
36
+ const init = options.headers;
37
+ const headers = (typeof init === 'function')
38
+ ? new Headers(await init(body))
39
+ : new Headers(init);
40
+ if (!headers.has('Content-Type')) {
41
+ headers.set('Content-Type', 'application/json');
42
+ }
43
+ return {
44
+ ...options.init,
45
+ method: 'POST',
46
+ headers,
47
+ };
48
+ };
49
+ }
50
+
51
+ readonly postUrl: string|URL;
52
+ readonly batchUrl: string|URL;
53
+ readonly #requestConverter?: (request: ReadableStream<string>) => ReadableStream<any>;
54
+ readonly #responseConverter?: (response: ReadableStream<any>, contentType: string|null) => ReadableStream<string>;
55
+ readonly #initGenerator: (body: Blob) => Promise<RequestInit>;
56
+
57
+ async #post(url: string|URL, request: ReadableStream<string>) {
58
+ if (this.#requestConverter) request = this.#requestConverter(request);
59
+ const body = await readStreamAll(request);
60
+ const init = await this.#initGenerator(body);
61
+ const response = await fetch(url, { ...init, body });
62
+ if (!response.ok) {
63
+ const message = `${response.status} ${response.statusText}`;
64
+ const headers = Object.fromEntries(response.headers.entries());
65
+ console.warn(new ClientHttpError(message, { headers }));
66
+ }
67
+ return this.#responseConverter
68
+ ? this.#responseConverter(response.body ?? emptyStrem(), response.headers.get('Content-Type'))
69
+ : responseToTextStream(response);
70
+ }
71
+ }