@askrjs/fetch 0.4.1 → 0.4.2
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.
- package/dist/client.js +73 -11
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/client.ts
|
|
2
2
|
const media = (response) => response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() ?? null;
|
|
3
3
|
const compatible = (codec, type) => codec.kind === "empty" || codec.kind === "blob" || codec.kind === "arrayBuffer" || codec.kind === "stream" || !!type && codec.mediaTypes.some((expected) => expected === "*/*" || expected === type || expected === "text/*" && type.startsWith("text/") || expected === "+json" && type.endsWith("+json"));
|
|
4
|
+
const selectCodec = (codec, response) => codec.kind === "content" ? codec.variants?.[media(response) ?? ""] : codec;
|
|
4
5
|
const validationFailure = (result) => "error" in result ? result.error : result.issues;
|
|
5
6
|
async function decode(response, codec) {
|
|
6
7
|
const type = media(response);
|
|
@@ -39,6 +40,58 @@ async function decode(response, codec) {
|
|
|
39
40
|
}
|
|
40
41
|
return value;
|
|
41
42
|
}
|
|
43
|
+
function responseWithStreamCleanup(response, cleanup) {
|
|
44
|
+
const source = response.body;
|
|
45
|
+
if (!source) {
|
|
46
|
+
cleanup();
|
|
47
|
+
return response;
|
|
48
|
+
}
|
|
49
|
+
const reader = source.getReader();
|
|
50
|
+
let finished = false;
|
|
51
|
+
const finish = () => {
|
|
52
|
+
if (finished) return;
|
|
53
|
+
finished = true;
|
|
54
|
+
cleanup();
|
|
55
|
+
};
|
|
56
|
+
const body = new ReadableStream({
|
|
57
|
+
async pull(streamController) {
|
|
58
|
+
try {
|
|
59
|
+
const chunk = await reader.read();
|
|
60
|
+
if (chunk.done) {
|
|
61
|
+
finish();
|
|
62
|
+
streamController.close();
|
|
63
|
+
} else streamController.enqueue(chunk.value);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
finish();
|
|
66
|
+
streamController.error(error);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async cancel(reason) {
|
|
70
|
+
finish();
|
|
71
|
+
return reader.cancel(reason);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const wrapped = new Response(body, {
|
|
75
|
+
status: response.status,
|
|
76
|
+
statusText: response.statusText,
|
|
77
|
+
headers: response.headers
|
|
78
|
+
});
|
|
79
|
+
Object.defineProperties(wrapped, {
|
|
80
|
+
url: {
|
|
81
|
+
value: response.url,
|
|
82
|
+
enumerable: true
|
|
83
|
+
},
|
|
84
|
+
redirected: {
|
|
85
|
+
value: response.redirected,
|
|
86
|
+
enumerable: true
|
|
87
|
+
},
|
|
88
|
+
type: {
|
|
89
|
+
value: response.type,
|
|
90
|
+
enumerable: true
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return wrapped;
|
|
94
|
+
}
|
|
42
95
|
function encode(value, codec, headers, requestedMediaType) {
|
|
43
96
|
if (codec.validator) {
|
|
44
97
|
const parsed = codec.validator.safeParse(value);
|
|
@@ -176,17 +229,19 @@ function createFetch(options = {}) {
|
|
|
176
229
|
const controller = new AbortController();
|
|
177
230
|
let timedOut = false;
|
|
178
231
|
let timer;
|
|
232
|
+
let streamOwnsCleanup = false;
|
|
179
233
|
const abort = () => controller.abort(call.signal?.reason);
|
|
234
|
+
const cleanup = () => {
|
|
235
|
+
if (timer) clearTimeout(timer);
|
|
236
|
+
call.signal?.removeEventListener("abort", abort);
|
|
237
|
+
};
|
|
238
|
+
controller.signal.addEventListener("abort", cleanup, { once: true });
|
|
180
239
|
call.signal?.addEventListener("abort", abort, { once: true });
|
|
181
|
-
if (call.signal?.aborted) abort();
|
|
182
240
|
if (timeout !== void 0) timer = setTimeout(() => {
|
|
183
241
|
timedOut = true;
|
|
184
242
|
controller.abort(new DOMException("Timed out", "TimeoutError"));
|
|
185
243
|
}, timeout);
|
|
186
|
-
|
|
187
|
-
if (timer) clearTimeout(timer);
|
|
188
|
-
call.signal?.removeEventListener("abort", abort);
|
|
189
|
-
};
|
|
244
|
+
if (call.signal?.aborted) abort();
|
|
190
245
|
if (controller.signal.aborted) {
|
|
191
246
|
cleanup();
|
|
192
247
|
return failure("abort", controller.signal.reason, url);
|
|
@@ -216,7 +271,14 @@ function createFetch(options = {}) {
|
|
|
216
271
|
const codec = response.ok ? call.responses?.[response.status] ?? call.response : call.errors?.[response.status] ?? call.errors?.default;
|
|
217
272
|
if (!codec) return failure("decode", /* @__PURE__ */ new TypeError(`No codec for status ${response.status}`), url, response);
|
|
218
273
|
try {
|
|
219
|
-
const
|
|
274
|
+
const selected = selectCodec(codec, response);
|
|
275
|
+
let data = await decode(selected?.kind === "stream" ? response : response.clone(), codec);
|
|
276
|
+
let resultResponse = response;
|
|
277
|
+
if (selected?.kind === "stream" && response.body) {
|
|
278
|
+
resultResponse = responseWithStreamCleanup(response, cleanup);
|
|
279
|
+
streamOwnsCleanup = true;
|
|
280
|
+
data = resultResponse.body;
|
|
281
|
+
}
|
|
220
282
|
return response.ok ? {
|
|
221
283
|
ok: true,
|
|
222
284
|
kind: "success",
|
|
@@ -224,8 +286,8 @@ function createFetch(options = {}) {
|
|
|
224
286
|
data,
|
|
225
287
|
mediaType: media(response),
|
|
226
288
|
headers: response.headers,
|
|
227
|
-
url:
|
|
228
|
-
response
|
|
289
|
+
url: resultResponse.url || url,
|
|
290
|
+
response: resultResponse
|
|
229
291
|
} : {
|
|
230
292
|
ok: false,
|
|
231
293
|
kind: "http",
|
|
@@ -233,8 +295,8 @@ function createFetch(options = {}) {
|
|
|
233
295
|
error: data,
|
|
234
296
|
mediaType: media(response),
|
|
235
297
|
headers: response.headers,
|
|
236
|
-
url:
|
|
237
|
-
response
|
|
298
|
+
url: resultResponse.url || url,
|
|
299
|
+
response: resultResponse
|
|
238
300
|
};
|
|
239
301
|
} catch (error) {
|
|
240
302
|
return failure("decode", error, url, response);
|
|
@@ -255,7 +317,7 @@ function createFetch(options = {}) {
|
|
|
255
317
|
} catch (error) {
|
|
256
318
|
return failure("middleware", error, url);
|
|
257
319
|
} finally {
|
|
258
|
-
cleanup();
|
|
320
|
+
if (!streamOwnsCleanup) cleanup();
|
|
259
321
|
}
|
|
260
322
|
};
|
|
261
323
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -97,6 +97,7 @@ export type SuccessResult<T = unknown, S extends number = number> = {
|
|
|
97
97
|
mediaType: string | null;
|
|
98
98
|
headers: Headers;
|
|
99
99
|
url: string;
|
|
100
|
+
/** For stream codecs, this body is the same one-shot stream exposed by `data`. */
|
|
100
101
|
response: Response;
|
|
101
102
|
};
|
|
102
103
|
/** A non-2xx fetch outcome where the server responded with a decodable error body. */
|
|
@@ -108,6 +109,7 @@ export type HttpResult<T = unknown, S extends number = number> = {
|
|
|
108
109
|
mediaType: string | null;
|
|
109
110
|
headers: Headers;
|
|
110
111
|
url: string;
|
|
112
|
+
/** For stream codecs, this body is the same one-shot stream exposed by `error`. */
|
|
111
113
|
response: Response;
|
|
112
114
|
};
|
|
113
115
|
/** Categorizes why a fetch could not produce an {@link HttpResult} or {@link SuccessResult}. */
|