@flighthq/net 0.1.0

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,2 @@
1
+ export * from './net';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './net';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAC"}
package/dist/net.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { NetBackend, NetRequest, NetRequestOptions, NetResponse } from '@flighthq/types';
2
+ export declare function createWebNetBackend(): NetBackend;
3
+ export declare function getNetBackend(): NetBackend;
4
+ export declare function sendNetRequest(request: Readonly<NetRequest>, options?: Readonly<NetRequestOptions>): Promise<NetResponse>;
5
+ export declare function setNetBackend(backend: NetBackend | null): void;
6
+ //# sourceMappingURL=net.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net.d.ts","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EACV,iBAAiB,EACjB,WAAW,EAIZ,MAAM,iBAAiB,CAAC;AAQzB,wBAAgB,mBAAmB,IAAI,UAAU,CAwBhD;AAGD,wBAAgB,aAAa,IAAI,UAAU,CAG1C;AAKD,wBAAgB,cAAc,CAC5B,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC7B,OAAO,CAAC,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GACpC,OAAO,CAAC,WAAW,CAAC,CAEtB;AAGD,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAE9D"}
package/dist/net.js ADDED
@@ -0,0 +1,196 @@
1
+ import { emitSignal } from '@flighthq/signals';
2
+ // Builds the default web backend over fetch + AbortController. Created lazily by getNetBackend — no
3
+ // fetch binding happens at import time, so importing the package has no side effect. Expected
4
+ // transport failures (network error, DNS, timeout, caller abort) resolve to a sentinel NetResponse
5
+ // (status 0, ok false) rather than rejecting; a non-2xx HTTP response is a normal NetResponse with
6
+ // its real status and ok false. Only genuine misuse (a request no correct caller could produce)
7
+ // surfaces as a thrown error from fetch itself.
8
+ export function createWebNetBackend() {
9
+ return {
10
+ async sendNetRequest(request, options) {
11
+ const controller = new AbortController();
12
+ const teardownAbort = _wireNetAbort(controller, request.timeoutMs, options?.signal);
13
+ try {
14
+ const response = await fetch(request.url, _toNetFetchInit(request, controller.signal));
15
+ const headers = _readNetResponseHeaders(response.headers);
16
+ const body = await _readNetResponseBody(response, request.responseType ?? 'text', options?.progress);
17
+ return {
18
+ status: response.status,
19
+ statusText: response.statusText,
20
+ ok: response.ok,
21
+ headers,
22
+ body,
23
+ url: response.url !== '' ? response.url : request.url,
24
+ };
25
+ }
26
+ catch (error) {
27
+ return _netTransportFailure(request.url, controller.signal, error);
28
+ }
29
+ finally {
30
+ teardownAbort();
31
+ }
32
+ },
33
+ };
34
+ }
35
+ // The active net backend, lazily defaulting to the web fetch backend. There is always a backend.
36
+ export function getNetBackend() {
37
+ if (_backend === null)
38
+ _backend = createWebNetBackend();
39
+ return _backend;
40
+ }
41
+ // Issues one HTTP(S) request through the active backend and resolves to a plain-data NetResponse.
42
+ // Expected transport failures resolve as a sentinel response (status 0, ok false); a non-2xx status
43
+ // is a normal response with ok false. Progress and cancellation come from options.
44
+ export function sendNetRequest(request, options) {
45
+ return getNetBackend().sendNetRequest(request, options);
46
+ }
47
+ // Installs a native host transport backend; pass null to fall back to the lazy web default.
48
+ export function setNetBackend(backend) {
49
+ _backend = backend;
50
+ }
51
+ let _backend = null;
52
+ // A sentinel abort reason so a timeout-triggered abort is distinguishable from a caller abort.
53
+ const _netTimeoutReason = { flightNetTimeout: true };
54
+ // Reads the numeric Content-Length, or -1 when it is absent or unparseable.
55
+ function _netContentLength(headers) {
56
+ const raw = headers.get('content-length');
57
+ if (raw === null)
58
+ return -1;
59
+ const parsed = Number(raw);
60
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : -1;
61
+ }
62
+ // Maps a fetch failure onto a sentinel NetResponse. A timeout and a caller abort are distinguished by
63
+ // the controller's abort reason; any other rejection is reported as a network error.
64
+ function _netTransportFailure(url, signal, error) {
65
+ let statusText = 'network error';
66
+ if (signal.aborted) {
67
+ statusText = signal.reason === _netTimeoutReason ? 'timeout' : 'aborted';
68
+ }
69
+ else if (error instanceof Error && error.message !== '') {
70
+ statusText = error.message;
71
+ }
72
+ return { status: 0, statusText, ok: false, headers: {}, body: null, url };
73
+ }
74
+ // Decodes an assembled byte buffer per the response type. JSON parse failure resolves to null rather
75
+ // than throwing — a malformed JSON body is an expected-failure surface, not programmer error.
76
+ function _decodeNetBuffer(buffer, responseType) {
77
+ if (responseType === 'arraybuffer')
78
+ return buffer;
79
+ if (responseType === 'blob')
80
+ return new Blob([buffer]);
81
+ const text = new TextDecoder().decode(buffer);
82
+ if (responseType === 'json') {
83
+ try {
84
+ return JSON.parse(text);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ return text;
91
+ }
92
+ // Reads the whole response body, decoding it per responseType. When a progress signal is supplied the
93
+ // download stream is drained chunk-by-chunk so progress ticks can be emitted; otherwise the native
94
+ // per-type reader is used directly.
95
+ async function _readNetResponseBody(response, responseType, progress) {
96
+ if (progress !== undefined) {
97
+ const buffer = await _readNetResponseWithProgress(response, progress);
98
+ return _decodeNetBuffer(buffer, responseType);
99
+ }
100
+ if (responseType === 'arraybuffer')
101
+ return await response.arrayBuffer();
102
+ if (responseType === 'blob')
103
+ return await response.blob();
104
+ if (responseType === 'json') {
105
+ try {
106
+ return (await response.json());
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ return await response.text();
113
+ }
114
+ // Collects the response body as bytes while emitting download progress ticks. Falls back to a single
115
+ // whole-body read (with one terminal tick) when the response exposes no readable stream.
116
+ async function _readNetResponseWithProgress(response, progress) {
117
+ const total = _netContentLength(response.headers);
118
+ const stream = response.body;
119
+ if (stream === null || typeof stream.getReader !== 'function') {
120
+ const buffer = await response.arrayBuffer();
121
+ emitSignal(progress, {
122
+ phase: 'download',
123
+ loaded: buffer.byteLength,
124
+ total: total >= 0 ? total : buffer.byteLength,
125
+ });
126
+ return buffer;
127
+ }
128
+ const reader = stream.getReader();
129
+ const chunks = [];
130
+ let loaded = 0;
131
+ for (;;) {
132
+ const { done, value } = await reader.read();
133
+ if (done)
134
+ break;
135
+ if (value === undefined)
136
+ continue;
137
+ chunks.push(value);
138
+ loaded += value.byteLength;
139
+ emitSignal(progress, { phase: 'download', loaded, total: total >= 0 ? total : 0 });
140
+ }
141
+ const out = new Uint8Array(loaded);
142
+ let offset = 0;
143
+ for (const chunk of chunks) {
144
+ out.set(chunk, offset);
145
+ offset += chunk.byteLength;
146
+ }
147
+ return out.buffer;
148
+ }
149
+ // Copies the response's headers into a plain, case-insensitive-keyed record.
150
+ function _readNetResponseHeaders(headers) {
151
+ const out = {};
152
+ headers.forEach((value, key) => {
153
+ out[key] = value;
154
+ });
155
+ return out;
156
+ }
157
+ // Maps a NetRequest onto a fetch RequestInit, wiring the merged abort signal. A null/absent body is
158
+ // left off entirely so bodyless methods (GET/HEAD) are valid.
159
+ function _toNetFetchInit(request, signal) {
160
+ const init = { method: request.method, signal };
161
+ if (request.headers !== undefined)
162
+ init.headers = { ...request.headers };
163
+ if (request.body !== undefined && request.body !== null)
164
+ init.body = request.body;
165
+ if (request.credentials !== undefined)
166
+ init.credentials = request.credentials;
167
+ if (request.redirect !== undefined)
168
+ init.redirect = request.redirect;
169
+ return init;
170
+ }
171
+ // Wires the request timeout and the caller's abort signal into one controller. Returns a teardown
172
+ // that clears the timer and detaches the caller listener. A timeout aborts with a sentinel reason so
173
+ // the failure path can label it 'timeout' rather than 'aborted'.
174
+ function _wireNetAbort(controller, timeoutMs, signal) {
175
+ let timer = null;
176
+ if (typeof timeoutMs === 'number' && timeoutMs >= 0) {
177
+ timer = setTimeout(() => controller.abort(_netTimeoutReason), timeoutMs);
178
+ }
179
+ let onAbort = null;
180
+ if (signal !== undefined) {
181
+ if (signal.aborted) {
182
+ controller.abort(signal.reason);
183
+ }
184
+ else {
185
+ onAbort = () => controller.abort(signal.reason);
186
+ signal.addEventListener('abort', onAbort);
187
+ }
188
+ }
189
+ return () => {
190
+ if (timer !== null)
191
+ clearTimeout(timer);
192
+ if (onAbort !== null && signal !== undefined)
193
+ signal.removeEventListener('abort', onAbort);
194
+ };
195
+ }
196
+ //# sourceMappingURL=net.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net.js","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/net",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "HTTP(S) transport (URLLoader/URLRequest) over a swappable web/native backend (fetch by default)",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,265 @@
1
+ import { createSignal } from '@flighthq/signals';
2
+ import type { NetBackend, NetProgress, NetRequest, NetResponse } from '@flighthq/types';
3
+
4
+ import { createWebNetBackend, getNetBackend, sendNetRequest, setNetBackend } from './net';
5
+
6
+ interface FakeResponseInit {
7
+ status?: number;
8
+ statusText?: string;
9
+ headers?: Record<string, string>;
10
+ url?: string;
11
+ text?: string;
12
+ json?: unknown;
13
+ arraybuffer?: ArrayBuffer;
14
+ blob?: Blob;
15
+ streamChunks?: Uint8Array[];
16
+ }
17
+
18
+ function fakeResponse(init: FakeResponseInit): Response {
19
+ const status = init.status ?? 200;
20
+ const headerMap = new Map<string, string>(Object.entries(init.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
21
+ const headers = {
22
+ get: (name: string) => headerMap.get(name.toLowerCase()) ?? null,
23
+ forEach: (cb: (value: string, key: string) => void) => headerMap.forEach((v, k) => cb(v, k)),
24
+ };
25
+ const body =
26
+ init.streamChunks !== undefined ? { getReader: () => makeReader(init.streamChunks as Uint8Array[]) } : null;
27
+ return {
28
+ status,
29
+ statusText: init.statusText ?? '',
30
+ ok: status >= 200 && status < 300,
31
+ url: init.url ?? '',
32
+ headers,
33
+ body,
34
+ text: async () => init.text ?? '',
35
+ json: async () => {
36
+ if (init.json === undefined) throw new SyntaxError('no json');
37
+ return init.json;
38
+ },
39
+ arrayBuffer: async () => init.arraybuffer ?? new ArrayBuffer(0),
40
+ blob: async () => init.blob ?? new Blob([]),
41
+ } as unknown as Response;
42
+ }
43
+
44
+ function makeReader(chunks: readonly Uint8Array[]): { read: () => Promise<{ done: boolean; value?: Uint8Array }> } {
45
+ let i = 0;
46
+ return {
47
+ read: async () => {
48
+ if (i < chunks.length) {
49
+ const value = chunks[i];
50
+ i += 1;
51
+ return { done: false, value };
52
+ }
53
+ return { done: true, value: undefined };
54
+ },
55
+ };
56
+ }
57
+
58
+ let originalFetch: typeof fetch | undefined;
59
+
60
+ beforeEach(() => {
61
+ originalFetch = globalThis.fetch;
62
+ });
63
+
64
+ afterEach(() => {
65
+ if (originalFetch !== undefined) globalThis.fetch = originalFetch;
66
+ setNetBackend(null);
67
+ });
68
+
69
+ describe('createWebNetBackend', () => {
70
+ it('maps method, headers, and body onto the fetch init', async () => {
71
+ let captured: { url?: string; init?: RequestInit } = {};
72
+ globalThis.fetch = (async (url: string, init: RequestInit) => {
73
+ captured = { url, init };
74
+ return fakeResponse({ status: 200, text: 'ok' });
75
+ }) as unknown as typeof fetch;
76
+ const backend = createWebNetBackend();
77
+ const request: NetRequest = {
78
+ method: 'POST',
79
+ url: 'https://example.test/api',
80
+ headers: { 'X-Token': 'abc' },
81
+ body: 'payload',
82
+ };
83
+ await backend.sendNetRequest(request);
84
+ expect(captured.url).toBe('https://example.test/api');
85
+ expect(captured.init?.method).toBe('POST');
86
+ expect(captured.init?.headers).toEqual({ 'X-Token': 'abc' });
87
+ expect(captured.init?.body).toBe('payload');
88
+ expect(captured.init?.signal).toBeInstanceOf(AbortSignal);
89
+ });
90
+
91
+ it('omits the body for a bodyless request', async () => {
92
+ let init: RequestInit | undefined;
93
+ globalThis.fetch = (async (_url: string, i: RequestInit) => {
94
+ init = i;
95
+ return fakeResponse({ status: 200, text: '' });
96
+ }) as unknown as typeof fetch;
97
+ await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'https://example.test' });
98
+ expect(init?.body).toBeUndefined();
99
+ });
100
+
101
+ it('decodes a text response', async () => {
102
+ globalThis.fetch = (async () => fakeResponse({ text: 'hello' })) as unknown as typeof fetch;
103
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u', responseType: 'text' });
104
+ expect(res.body).toBe('hello');
105
+ expect(res.ok).toBe(true);
106
+ });
107
+
108
+ it('decodes a json response', async () => {
109
+ globalThis.fetch = (async () => fakeResponse({ json: { x: 1 } })) as unknown as typeof fetch;
110
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u', responseType: 'json' });
111
+ expect(res.body).toEqual({ x: 1 });
112
+ });
113
+
114
+ it('returns null for a malformed json body without throwing', async () => {
115
+ globalThis.fetch = (async () => fakeResponse({ status: 200 })) as unknown as typeof fetch;
116
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u', responseType: 'json' });
117
+ expect(res.body).toBeNull();
118
+ expect(res.ok).toBe(true);
119
+ });
120
+
121
+ it('decodes an arraybuffer response', async () => {
122
+ const buffer = new Uint8Array([1, 2, 3]).buffer;
123
+ globalThis.fetch = (async () => fakeResponse({ arraybuffer: buffer })) as unknown as typeof fetch;
124
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u', responseType: 'arraybuffer' });
125
+ expect(res.body).toBe(buffer);
126
+ });
127
+
128
+ it('decodes a blob response', async () => {
129
+ const blob = new Blob(['x']);
130
+ globalThis.fetch = (async () => fakeResponse({ blob })) as unknown as typeof fetch;
131
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u', responseType: 'blob' });
132
+ expect(res.body).toBe(blob);
133
+ });
134
+
135
+ it('surfaces a non-2xx response as ok:false with the real status, not a throw', async () => {
136
+ globalThis.fetch = (async () =>
137
+ fakeResponse({ status: 404, statusText: 'Not Found', text: 'nope' })) as unknown as typeof fetch;
138
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u' });
139
+ expect(res.status).toBe(404);
140
+ expect(res.ok).toBe(false);
141
+ expect(res.statusText).toBe('Not Found');
142
+ expect(res.body).toBe('nope');
143
+ });
144
+
145
+ it('resolves a thrown fetch (network error) to a sentinel response', async () => {
146
+ globalThis.fetch = (async () => {
147
+ throw new TypeError('Failed to fetch');
148
+ }) as unknown as typeof fetch;
149
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u' });
150
+ expect(res.status).toBe(0);
151
+ expect(res.ok).toBe(false);
152
+ expect(res.statusText).toBe('Failed to fetch');
153
+ expect(res.body).toBeNull();
154
+ });
155
+
156
+ it('resolves a timeout to an aborted sentinel labeled timeout', async () => {
157
+ globalThis.fetch = ((_url: string, init: RequestInit) =>
158
+ new Promise<Response>((_resolve, reject) => {
159
+ const signal = init.signal as AbortSignal;
160
+ if (signal.aborted) {
161
+ reject(new DOMException('aborted', 'AbortError'));
162
+ return;
163
+ }
164
+ signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')));
165
+ })) as unknown as typeof fetch;
166
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u', timeoutMs: 5 });
167
+ expect(res.status).toBe(0);
168
+ expect(res.ok).toBe(false);
169
+ expect(res.statusText).toBe('timeout');
170
+ });
171
+
172
+ it('resolves a caller abort to an aborted sentinel', async () => {
173
+ globalThis.fetch = ((_url: string, init: RequestInit) =>
174
+ new Promise<Response>((_resolve, reject) => {
175
+ const signal = init.signal as AbortSignal;
176
+ signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')));
177
+ })) as unknown as typeof fetch;
178
+ const controller = new AbortController();
179
+ const promise = createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u' }, { signal: controller.signal });
180
+ controller.abort();
181
+ const res = await promise;
182
+ expect(res.status).toBe(0);
183
+ expect(res.statusText).toBe('aborted');
184
+ });
185
+
186
+ it('emits download progress ticks when a progress signal is supplied', async () => {
187
+ globalThis.fetch = (async () =>
188
+ fakeResponse({
189
+ headers: { 'content-length': '5' },
190
+ streamChunks: [new Uint8Array([104, 105]), new Uint8Array([33, 33, 33])],
191
+ })) as unknown as typeof fetch;
192
+ const progress = createSignal<(progress: Readonly<NetProgress>) => void>();
193
+ const ticks: NetProgress[] = [];
194
+ progress.emit = (tick) => ticks.push({ ...tick });
195
+ const res = await createWebNetBackend().sendNetRequest(
196
+ { method: 'GET', url: 'u', responseType: 'text' },
197
+ { progress },
198
+ );
199
+ expect(ticks).toHaveLength(2);
200
+ expect(ticks[0]).toEqual({ phase: 'download', loaded: 2, total: 5 });
201
+ expect(ticks[1]).toEqual({ phase: 'download', loaded: 5, total: 5 });
202
+ expect(res.body).toBe('hi!!!');
203
+ });
204
+
205
+ it('reads response headers into a plain record', async () => {
206
+ globalThis.fetch = (async () =>
207
+ fakeResponse({ headers: { 'content-type': 'text/plain' }, text: 'x' })) as unknown as typeof fetch;
208
+ const res = await createWebNetBackend().sendNetRequest({ method: 'GET', url: 'u' });
209
+ expect(res.headers['content-type']).toBe('text/plain');
210
+ });
211
+ });
212
+
213
+ describe('getNetBackend', () => {
214
+ it('lazily returns a web backend by default', () => {
215
+ expect(getNetBackend()).not.toBeNull();
216
+ expect(typeof getNetBackend().sendNetRequest).toBe('function');
217
+ });
218
+
219
+ it('returns the installed backend', () => {
220
+ const backend: NetBackend = { sendNetRequest: async () => stubResponse() };
221
+ setNetBackend(backend);
222
+ expect(getNetBackend()).toBe(backend);
223
+ });
224
+ });
225
+
226
+ describe('sendNetRequest', () => {
227
+ it('dispatches through the active backend and passes options', async () => {
228
+ let received: { request?: Readonly<NetRequest>; options?: unknown } = {};
229
+ const backend: NetBackend = {
230
+ sendNetRequest: async (request, options) => {
231
+ received = { request, options };
232
+ return stubResponse();
233
+ },
234
+ };
235
+ setNetBackend(backend);
236
+ const request: NetRequest = { method: 'GET', url: 'https://example.test' };
237
+ const options = {};
238
+ await sendNetRequest(request, options);
239
+ expect(received.request).toBe(request);
240
+ expect(received.options).toBe(options);
241
+ });
242
+
243
+ it('passes the response through unchanged', async () => {
244
+ const response = stubResponse();
245
+ setNetBackend({ sendNetRequest: async () => response });
246
+ const result = await sendNetRequest({ method: 'GET', url: 'u' });
247
+ expect(result).toBe(response);
248
+ });
249
+ });
250
+
251
+ describe('setNetBackend', () => {
252
+ it('restores the lazy web default when passed null', () => {
253
+ const backend: NetBackend = { sendNetRequest: async () => stubResponse() };
254
+ setNetBackend(backend);
255
+ expect(getNetBackend()).toBe(backend);
256
+ setNetBackend(null);
257
+ const web = getNetBackend();
258
+ expect(web).not.toBe(backend);
259
+ expect(typeof web.sendNetRequest).toBe('function');
260
+ });
261
+ });
262
+
263
+ function stubResponse(): NetResponse {
264
+ return { status: 200, statusText: 'OK', ok: true, headers: {}, body: null, url: 'u' };
265
+ }