// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // @ts-check /// /// /// /// /// /// /// /// /// "use strict"; ((window) => { const core = window.Deno.core; const webidl = globalThis.__bootstrap.webidl; const { parseUrlEncoded } = globalThis.__bootstrap.url; const { parseFormData, formDataFromEntries, formDataToBlob } = globalThis.__bootstrap.formData; const mimesniff = globalThis.__bootstrap.mimesniff; const { isReadableStreamDisturbed, errorReadableStream, createProxy } = globalThis.__bootstrap.streams; const { ArrayBuffer, ArrayBufferIsView, ArrayPrototypePush, ArrayPrototypeMap, JSONParse, ObjectDefineProperties, PromiseResolve, TypedArrayPrototypeSet, TypedArrayPrototypeSlice, TypeError, Uint8Array, } = window.__bootstrap.primordials; class InnerBody { /** * @param {ReadableStream | { body: Uint8Array, consumed: boolean }} stream */ constructor(stream) { /** @type {ReadableStream | { body: Uint8Array, consumed: boolean }} */ this.streamOrStatic = stream ?? { body: new Uint8Array(), consumed: false }; /** @type {null | Uint8Array | Blob | FormData} */ this.source = null; /** @type {null | number} */ this.length = null; } get stream() { if (!(this.streamOrStatic instanceof ReadableStream)) { const { body, consumed } = this.streamOrStatic; if (consumed) { this.streamOrStatic = new ReadableStream(); this.streamOrStatic.getReader(); } else { this.streamOrStatic = new ReadableStream({ start(controller) { controller.enqueue(body); controller.close(); }, }); } } return this.streamOrStatic; } /** * https://fetch.spec.whatwg.org/#body-unusable * @returns {boolean} */ unusable() { if (this.streamOrStatic instanceof ReadableStream) { return this.streamOrStatic.locked || isReadableStreamDisturbed(this.streamOrStatic); } return this.streamOrStatic.consumed; } /** * @returns {boolean} */ consumed() { if (this.streamOrStatic instanceof ReadableStream) { return isReadableStreamDisturbed(this.streamOrStatic); } return this.streamOrStatic.consumed; } /** * https://fetch.spec.whatwg.org/#concept-body-consume-body * @returns {Promise} */ async consume() { if (this.unusable()) throw new TypeError("Body already consumed."); if (this.streamOrStatic instanceof ReadableStream) { const reader = this.stream.getReader(); /** @type {Uint8Array[]} */ const chunks = []; let totalLength = 0; while (true) { const { value: chunk, done } = await reader.read(); if (done) break; ArrayPrototypePush(chunks, chunk); totalLength += chunk.byteLength; } const finalBuffer = new Uint8Array(totalLength); let i = 0; for (const chunk of chunks) { TypedArrayPrototypeSet(finalBuffer, chunk, i); i += chunk.byteLength; } return finalBuffer; } else { this.streamOrStatic.consumed = true; return this.streamOrStatic.body; } } cancel(error) { if (this.streamOrStatic instanceof ReadableStream) { this.streamOrStatic.cancel(error); } else { this.streamOrStatic.consumed = true; } } error(error) { if (this.streamOrStatic instanceof ReadableStream) { errorReadableStream(this.streamOrStatic, error); } else { this.streamOrStatic.consumed = true; } } /** * @returns {InnerBody} */ clone() { const [out1, out2] = this.stream.tee(); this.streamOrStatic = out1; const second = new InnerBody(out2); second.source = core.deserialize(core.serialize(this.source)); second.length = this.length; return second; } /** * @returns {InnerBody} */ createProxy() { let proxyStreamOrStatic; if (this.streamOrStatic instanceof ReadableStream) { proxyStreamOrStatic = createProxy(this.streamOrStatic); } else { proxyStreamOrStatic = { ...this.streamOrStatic }; this.streamOrStatic.consumed = true; } const proxy = new InnerBody(proxyStreamOrStatic); proxy.source = this.source; proxy.length = this.length; return proxy; } } /** * @param {any} prototype * @param {symbol} bodySymbol * @param {symbol} mimeTypeSymbol * @returns {void} */ function mixinBody(prototype, bodySymbol, mimeTypeSymbol) { function consumeBody(object) { if (object[bodySymbol] !== null) { return object[bodySymbol].consume(); } return PromiseResolve(new Uint8Array()); } /** @type {PropertyDescriptorMap} */ const mixin = { body: { /** * @returns {ReadableStream | null} */ get() { webidl.assertBranded(this, prototype); if (this[bodySymbol] === null) { return null; } else { return this[bodySymbol].stream; } }, configurable: true, enumerable: true, }, bodyUsed: { /** * @returns {boolean} */ get() { webidl.assertBranded(this, prototype); if (this[bodySymbol] !== null) { return this[bodySymbol].consumed(); } return false; }, configurable: true, enumerable: true, }, arrayBuffer: { /** @returns {Promise} */ value: async function arrayBuffer() { webidl.assertBranded(this, prototype); const body = await consumeBody(this); return packageData(body, "ArrayBuffer"); }, writable: true, configurable: true, enumerable: true, }, blob: { /** @returns {Promise} */ value: async function blob() { webidl.assertBranded(this, prototype); const body = await consumeBody(this); return packageData(body, "Blob", this[mimeTypeSymbol]); }, writable: true, configurable: true, enumerable: true, }, formData: { /** @returns {Promise} */ value: async function formData() { webidl.assertBranded(this, prototype); const body = await consumeBody(this); return packageData(body, "FormData", this[mimeTypeSymbol]); }, writable: true, configurable: true, enumerable: true, }, json: { /** @returns {Promise} */ value: async function json() { webidl.assertBranded(this, prototype); const body = await consumeBody(this); return packageData(body, "JSON"); }, writable: true, configurable: true, enumerable: true, }, text: { /** @returns {Promise} */ value: async function text() { webidl.assertBranded(this, prototype); const body = await consumeBody(this); return packageData(body, "text"); }, writable: true, configurable: true, enumerable: true, }, }; return ObjectDefineProperties(prototype.prototype, mixin); } /** * https://fetch.spec.whatwg.org/#concept-body-package-data * @param {Uint8Array} bytes * @param {"ArrayBuffer" | "Blob" | "FormData" | "JSON" | "text"} type * @param {MimeType | null} [mimeType] */ function packageData(bytes, type, mimeType) { switch (type) { case "ArrayBuffer": return bytes.buffer; case "Blob": return new Blob([bytes], { type: mimeType !== null ? mimesniff.serializeMimeType(mimeType) : "", }); case "FormData": { if (mimeType !== null) { const essence = mimesniff.essence(mimeType); if (essence === "multipart/form-data") { const boundary = mimeType.parameters.get("boundary"); if (boundary === null) { throw new TypeError( "Missing boundary parameter in mime type of multipart formdata.", ); } return parseFormData(bytes, boundary); } else if (essence === "application/x-www-form-urlencoded") { const entries = parseUrlEncoded(bytes); return formDataFromEntries( ArrayPrototypeMap( entries, (x) => ({ name: x[0], value: x[1] }), ), ); } throw new TypeError("Body can not be decoded as form data"); } throw new TypeError("Missing content type"); } case "JSON": return JSONParse(core.decode(bytes)); case "text": return core.decode(bytes); } } /** * @param {BodyInit} object * @returns {{body: InnerBody, contentType: string | null}} */ function extractBody(object) { /** @type {ReadableStream | { body: Uint8Array, consumed: boolean }} */ let stream; let source = null; let length = null; let contentType = null; if (object instanceof Blob) { stream = object.stream(); source = object; length = object.size; if (object.type.length !== 0) { contentType = object.type; } } else if (object instanceof Uint8Array) { // Fast(er) path for common case of Uint8Array const copy = TypedArrayPrototypeSlice(object, 0, object.byteLength); source = copy; } else if (ArrayBufferIsView(object) || object instanceof ArrayBuffer) { const u8 = ArrayBufferIsView(object) ? new Uint8Array( object.buffer, object.byteOffset, object.byteLength, ) : new Uint8Array(object); const copy = TypedArrayPrototypeSlice(u8, 0, u8.byteLength); source = copy; } else if (object instanceof FormData) { const res = formDataToBlob(object); stream = res.stream(); source = res; length = res.size; contentType = res.type; } else if (object instanceof URLSearchParams) { // TODO(@satyarohith): not sure what primordial here. source = core.encode(object.toString()); contentType = "application/x-www-form-urlencoded;charset=UTF-8"; } else if (typeof object === "string") { source = core.encode(object); contentType = "text/plain;charset=UTF-8"; } else if (object instanceof ReadableStream) { stream = object; if (object.locked || isReadableStreamDisturbed(object)) { throw new TypeError("ReadableStream is locked or disturbed"); } } if (source instanceof Uint8Array) { stream = { body: source, consumed: false }; length = source.byteLength; } const body = new InnerBody(stream); body.source = source; body.length = length; return { body, contentType }; } webidl.converters["BodyInit_DOMString"] = (V, opts) => { // Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString) if (V instanceof ReadableStream) { // TODO(lucacasonato): ReadableStream is not branded return V; } else if (V instanceof Blob) { return webidl.converters["Blob"](V, opts); } else if (V instanceof FormData) { return webidl.converters["FormData"](V, opts); } else if (V instanceof URLSearchParams) { // TODO(lucacasonato): URLSearchParams is not branded return V; } if (typeof V === "object") { if (V instanceof ArrayBuffer || V instanceof SharedArrayBuffer) { return webidl.converters["ArrayBuffer"](V, opts); } if (ArrayBufferIsView(V)) { return webidl.converters["ArrayBufferView"](V, opts); } } // BodyInit conversion is passed to extractBody(), which calls core.encode(). // core.encode() will UTF-8 encode strings with replacement, being equivalent to the USV normalization. // Therefore we can convert to DOMString instead of USVString and avoid a costly redundant conversion. return webidl.converters["DOMString"](V, opts); }; webidl.converters["BodyInit_DOMString?"] = webidl.createNullableConverter( webidl.converters["BodyInit_DOMString"], ); window.__bootstrap.fetchBody = { mixinBody, InnerBody, extractBody }; })(globalThis);