From 7845740637eb646c0b13dc541f043fd65136fc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 18 Sep 2020 15:20:55 +0200 Subject: refactor: deno_fetch op crate (#7524) --- Cargo.lock | 10 + Cargo.toml | 1 + cli/Cargo.toml | 2 + cli/build.rs | 9 + cli/dts/lib.deno.shared_globals.d.ts | 629 +----- cli/js.rs | 1 + cli/main.rs | 3 +- cli/ops/fetch.rs | 195 +- cli/rt/01_web_util.js | 32 - cli/rt/02_console.js | 5 +- cli/rt/03_dom_iterable.js | 77 - cli/rt/11_streams.js | 3280 ------------------------------- cli/rt/20_blob.js | 223 --- cli/rt/20_headers.js | 256 --- cli/rt/20_streams_queuing_strategy.js | 50 - cli/rt/21_dom_file.js | 27 - cli/rt/21_filereader.js | 264 --- cli/rt/22_form_data.js | 116 -- cli/rt/23_multipart.js | 199 -- cli/rt/24_body.js | 220 --- cli/rt/25_request.js | 139 -- cli/rt/26_fetch.js | 391 ---- cli/rt/28_filereader.js | 264 +++ cli/rt/99_main.js | 17 +- cli/state.rs | 11 + cli/tests/performance_stats.out | 2 +- cli/tests/unit/blob_test.ts | 2 + cli/tests/unit/dom_iterable_test.ts | 4 + cli/tsc/99_main_compiler.js | 5 + op_crates/fetch/01_fetch_util.js | 20 + op_crates/fetch/03_dom_iterable.js | 77 + op_crates/fetch/11_streams.js | 3418 +++++++++++++++++++++++++++++++++ op_crates/fetch/20_headers.js | 256 +++ op_crates/fetch/26_fetch.js | 1390 ++++++++++++++ op_crates/fetch/Cargo.toml | 19 + op_crates/fetch/lib.deno_fetch.d.ts | 636 ++++++ op_crates/fetch/lib.rs | 266 +++ std/http/file_server_test.ts | 2 +- 38 files changed, 6414 insertions(+), 6104 deletions(-) delete mode 100644 cli/rt/03_dom_iterable.js delete mode 100644 cli/rt/11_streams.js delete mode 100644 cli/rt/20_blob.js delete mode 100644 cli/rt/20_headers.js delete mode 100644 cli/rt/20_streams_queuing_strategy.js delete mode 100644 cli/rt/21_dom_file.js delete mode 100644 cli/rt/21_filereader.js delete mode 100644 cli/rt/22_form_data.js delete mode 100644 cli/rt/23_multipart.js delete mode 100644 cli/rt/24_body.js delete mode 100644 cli/rt/25_request.js delete mode 100644 cli/rt/26_fetch.js create mode 100644 cli/rt/28_filereader.js create mode 100644 op_crates/fetch/01_fetch_util.js create mode 100644 op_crates/fetch/03_dom_iterable.js create mode 100644 op_crates/fetch/11_streams.js create mode 100644 op_crates/fetch/20_headers.js create mode 100644 op_crates/fetch/26_fetch.js create mode 100644 op_crates/fetch/Cargo.toml create mode 100644 op_crates/fetch/lib.deno_fetch.d.ts create mode 100644 op_crates/fetch/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5ffc17e79..02f5ad7bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,6 +403,7 @@ dependencies = [ "clap", "deno_core", "deno_doc", + "deno_fetch", "deno_lint", "deno_web", "dissimilar", @@ -483,6 +484,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "deno_fetch" +version = "0.1.0" +dependencies = [ + "deno_core", + "reqwest", + "serde", +] + [[package]] name = "deno_lint" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 91e591b73..128e62778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "core", "test_plugin", "test_util", + "op_crates/fetch", "op_crates/web", ] exclude = [ diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1c19dabdb..bb4d2714b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -22,6 +22,7 @@ path = "./bench/main.rs" [build-dependencies] deno_core = { path = "../core", version = "0.57.0" } deno_web = { path = "../op_crates/web", version = "0.8.0" } +deno_fetch = { path = "../op_crates/fetch", version = "0.1.0" } [target.'cfg(windows)'.build-dependencies] winres = "0.1.11" @@ -32,6 +33,7 @@ deno_core = { path = "../core", version = "0.57.0" } deno_doc = "0.1.9" deno_lint = { version = "0.2.0", features = ["json"] } deno_web = { path = "../op_crates/web", version = "0.8.0" } +deno_fetch = { path = "../op_crates/fetch", version = "0.1.0" } atty = "0.2.14" base64 = "0.12.3" diff --git a/cli/build.rs b/cli/build.rs index 7ed947f4e..d969a3415 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -16,6 +16,7 @@ fn create_snapshot( files: Vec, ) { deno_web::init(&mut isolate); + deno_fetch::init(&mut isolate); // TODO(nayeemrmn): https://github.com/rust-lang/cargo/issues/3946 to get the // workspace root. let display_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); @@ -52,6 +53,10 @@ fn create_compiler_snapshot( let mut custom_libs: HashMap = HashMap::new(); custom_libs .insert("lib.deno.web.d.ts".to_string(), deno_web::get_declaration()); + custom_libs.insert( + "lib.deno.fetch.d.ts".to_string(), + deno_fetch::get_declaration(), + ); custom_libs.insert( "lib.deno.window.d.ts".to_string(), cwd.join("dts/lib.deno.window.d.ts"), @@ -112,6 +117,10 @@ fn main() { "cargo:rustc-env=DENO_WEB_LIB_PATH={}", deno_web::get_declaration().display() ); + println!( + "cargo:rustc-env=DENO_FETCH_LIB_PATH={}", + deno_fetch::get_declaration().display() + ); println!( "cargo:rustc-env=TARGET={}", diff --git a/cli/dts/lib.deno.shared_globals.d.ts b/cli/dts/lib.deno.shared_globals.d.ts index c9423b5f3..786b06f1b 100644 --- a/cli/dts/lib.deno.shared_globals.d.ts +++ b/cli/dts/lib.deno.shared_globals.d.ts @@ -5,6 +5,7 @@ /// /// /// +/// declare namespace WebAssembly { interface CompileError { @@ -227,255 +228,6 @@ declare function removeEventListener( options?: boolean | EventListenerOptions | undefined, ): void; -interface DomIterable { - keys(): IterableIterator; - values(): IterableIterator; - entries(): IterableIterator<[K, V]>; - [Symbol.iterator](): IterableIterator<[K, V]>; - forEach( - callback: (value: V, key: K, parent: this) => void, - thisArg?: any, - ): void; -} - -interface ReadableStreamReadDoneResult { - done: true; - value?: T; -} - -interface ReadableStreamReadValueResult { - done: false; - value: T; -} - -type ReadableStreamReadResult = - | ReadableStreamReadValueResult - | ReadableStreamReadDoneResult; - -interface ReadableStreamDefaultReader { - readonly closed: Promise; - cancel(reason?: any): Promise; - read(): Promise>; - releaseLock(): void; -} - -interface ReadableStreamReader { - cancel(): Promise; - read(): Promise>; - releaseLock(): void; -} - -interface ReadableByteStreamControllerCallback { - (controller: ReadableByteStreamController): void | PromiseLike; -} - -interface UnderlyingByteSource { - autoAllocateChunkSize?: number; - cancel?: ReadableStreamErrorCallback; - pull?: ReadableByteStreamControllerCallback; - start?: ReadableByteStreamControllerCallback; - type: "bytes"; -} - -interface UnderlyingSource { - cancel?: ReadableStreamErrorCallback; - pull?: ReadableStreamDefaultControllerCallback; - start?: ReadableStreamDefaultControllerCallback; - type?: undefined; -} - -interface ReadableStreamErrorCallback { - (reason: any): void | PromiseLike; -} - -interface ReadableStreamDefaultControllerCallback { - (controller: ReadableStreamDefaultController): void | PromiseLike; -} - -interface ReadableStreamDefaultController { - readonly desiredSize: number | null; - close(): void; - enqueue(chunk: R): void; - error(error?: any): void; -} - -interface ReadableByteStreamController { - readonly byobRequest: undefined; - readonly desiredSize: number | null; - close(): void; - enqueue(chunk: ArrayBufferView): void; - error(error?: any): void; -} - -interface PipeOptions { - preventAbort?: boolean; - preventCancel?: boolean; - preventClose?: boolean; - signal?: AbortSignal; -} - -interface QueuingStrategySizeCallback { - (chunk: T): number; -} - -interface QueuingStrategy { - highWaterMark?: number; - size?: QueuingStrategySizeCallback; -} - -/** This Streams API interface provides a built-in byte length queuing strategy - * that can be used when constructing streams. */ -declare class CountQueuingStrategy implements QueuingStrategy { - constructor(options: { highWaterMark: number }); - highWaterMark: number; - size(chunk: any): 1; -} - -declare class ByteLengthQueuingStrategy - implements QueuingStrategy { - constructor(options: { highWaterMark: number }); - highWaterMark: number; - size(chunk: ArrayBufferView): number; -} - -/** This Streams API interface represents a readable stream of byte data. The - * Fetch API offers a concrete instance of a ReadableStream through the body - * property of a Response object. */ -interface ReadableStream { - readonly locked: boolean; - cancel(reason?: any): Promise; - getIterator(options?: { preventCancel?: boolean }): AsyncIterableIterator; - // getReader(options: { mode: "byob" }): ReadableStreamBYOBReader; - getReader(): ReadableStreamDefaultReader; - pipeThrough( - { - writable, - readable, - }: { - writable: WritableStream; - readable: ReadableStream; - }, - options?: PipeOptions, - ): ReadableStream; - pipeTo(dest: WritableStream, options?: PipeOptions): Promise; - tee(): [ReadableStream, ReadableStream]; - [Symbol.asyncIterator](options?: { - preventCancel?: boolean; - }): AsyncIterableIterator; -} - -declare var ReadableStream: { - prototype: ReadableStream; - new ( - underlyingSource: UnderlyingByteSource, - strategy?: { highWaterMark?: number; size?: undefined }, - ): ReadableStream; - new ( - underlyingSource?: UnderlyingSource, - strategy?: QueuingStrategy, - ): ReadableStream; -}; - -interface WritableStreamDefaultControllerCloseCallback { - (): void | PromiseLike; -} - -interface WritableStreamDefaultControllerStartCallback { - (controller: WritableStreamDefaultController): void | PromiseLike; -} - -interface WritableStreamDefaultControllerWriteCallback { - (chunk: W, controller: WritableStreamDefaultController): - | void - | PromiseLike< - void - >; -} - -interface WritableStreamErrorCallback { - (reason: any): void | PromiseLike; -} - -interface UnderlyingSink { - abort?: WritableStreamErrorCallback; - close?: WritableStreamDefaultControllerCloseCallback; - start?: WritableStreamDefaultControllerStartCallback; - type?: undefined; - write?: WritableStreamDefaultControllerWriteCallback; -} - -/** This Streams API interface provides a standard abstraction for writing - * streaming data to a destination, known as a sink. This object comes with - * built-in backpressure and queuing. */ -declare class WritableStream { - constructor( - underlyingSink?: UnderlyingSink, - strategy?: QueuingStrategy, - ); - readonly locked: boolean; - abort(reason?: any): Promise; - close(): Promise; - getWriter(): WritableStreamDefaultWriter; -} - -/** This Streams API interface represents a controller allowing control of a - * WritableStream's state. When constructing a WritableStream, the underlying - * sink is given a corresponding WritableStreamDefaultController instance to - * manipulate. */ -interface WritableStreamDefaultController { - error(error?: any): void; -} - -/** This Streams API interface is the object returned by - * WritableStream.getWriter() and once created locks the < writer to the - * WritableStream ensuring that no other streams can write to the underlying - * sink. */ -interface WritableStreamDefaultWriter { - readonly closed: Promise; - readonly desiredSize: number | null; - readonly ready: Promise; - abort(reason?: any): Promise; - close(): Promise; - releaseLock(): void; - write(chunk: W): Promise; -} - -declare class TransformStream { - constructor( - transformer?: Transformer, - writableStrategy?: QueuingStrategy, - readableStrategy?: QueuingStrategy, - ); - readonly readable: ReadableStream; - readonly writable: WritableStream; -} - -interface TransformStreamDefaultController { - readonly desiredSize: number | null; - enqueue(chunk: O): void; - error(reason?: any): void; - terminate(): void; -} - -interface Transformer { - flush?: TransformStreamDefaultControllerCallback; - readableType?: undefined; - start?: TransformStreamDefaultControllerCallback; - transform?: TransformStreamDefaultControllerTransformCallback; - writableType?: undefined; -} - -interface TransformStreamDefaultControllerCallback { - (controller: TransformStreamDefaultController): void | PromiseLike; -} - -interface TransformStreamDefaultControllerTransformCallback { - ( - chunk: I, - controller: TransformStreamDefaultController, - ): void | PromiseLike; -} - interface DOMStringList { /** Returns the number of strings in strings. */ readonly length: number; @@ -487,43 +239,6 @@ interface DOMStringList { } type BufferSource = ArrayBufferView | ArrayBuffer; -type BlobPart = BufferSource | Blob | string; - -interface BlobPropertyBag { - type?: string; - ending?: "transparent" | "native"; -} - -/** A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. */ -interface Blob { - readonly size: number; - readonly type: string; - arrayBuffer(): Promise; - slice(start?: number, end?: number, contentType?: string): Blob; - stream(): ReadableStream; - text(): Promise; -} - -declare const Blob: { - prototype: Blob; - new (blobParts?: BlobPart[], options?: BlobPropertyBag): Blob; -}; - -interface FilePropertyBag extends BlobPropertyBag { - lastModified?: number; -} - -/** Provides information about files and allows JavaScript in a web page to - * access their content. */ -interface File extends Blob { - readonly lastModified: number; - readonly name: string; -} - -declare const File: { - prototype: File; - new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File; -}; interface FileReaderEventMap { "abort": ProgressEvent; @@ -653,328 +368,6 @@ declare interface Crypto { ): T; } -type FormDataEntryValue = File | string; - -/** Provides a way to easily construct a set of key/value pairs representing - * form fields and their values, which can then be easily sent using the - * XMLHttpRequest.send() method. It uses the same format a form would use if the - * encoding type were set to "multipart/form-data". */ -interface FormData extends DomIterable { - append(name: string, value: string | Blob, fileName?: string): void; - delete(name: string): void; - get(name: string): FormDataEntryValue | null; - getAll(name: string): FormDataEntryValue[]; - has(name: string): boolean; - set(name: string, value: string | Blob, fileName?: string): void; -} - -declare const FormData: { - prototype: FormData; - // TODO(ry) FormData constructor is non-standard. - // new(form?: HTMLFormElement): FormData; - new (): FormData; -}; - -interface Body { - /** A simple getter used to expose a `ReadableStream` of the body contents. */ - readonly body: ReadableStream | null; - /** Stores a `Boolean` that declares whether the body has been used in a - * response yet. - */ - readonly bodyUsed: boolean; - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with an `ArrayBuffer`. - */ - arrayBuffer(): Promise; - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with a `Blob`. - */ - blob(): Promise; - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with a `FormData` object. - */ - formData(): Promise; - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with the result of parsing the body text as JSON. - */ - json(): Promise; - /** Takes a `Response` stream and reads it to completion. It returns a promise - * that resolves with a `USVString` (text). - */ - text(): Promise; -} - -type HeadersInit = Headers | string[][] | Record; - -/** This Fetch API interface allows you to perform various actions on HTTP - * request and response headers. These actions include retrieving, setting, - * adding to, and removing. A Headers object has an associated header list, - * which is initially empty and consists of zero or more name and value pairs. - *  You can add to this using methods like append() (see Examples.) In all - * methods of this interface, header names are matched by case-insensitive byte - * sequence. */ -interface Headers { - append(name: string, value: string): void; - delete(name: string): void; - get(name: string): string | null; - has(name: string): boolean; - set(name: string, value: string): void; - forEach( - callbackfn: (value: string, key: string, parent: Headers) => void, - thisArg?: any, - ): void; -} - -interface Headers extends DomIterable { - /** Appends a new value onto an existing header inside a `Headers` object, or - * adds the header if it does not already exist. - */ - append(name: string, value: string): void; - /** Deletes a header from a `Headers` object. */ - delete(name: string): void; - /** Returns an iterator allowing to go through all key/value pairs - * contained in this Headers object. The both the key and value of each pairs - * are ByteString objects. - */ - entries(): IterableIterator<[string, string]>; - /** Returns a `ByteString` sequence of all the values of a header within a - * `Headers` object with a given name. - */ - get(name: string): string | null; - /** Returns a boolean stating whether a `Headers` object contains a certain - * header. - */ - has(name: string): boolean; - /** Returns an iterator allowing to go through all keys contained in - * this Headers object. The keys are ByteString objects. - */ - keys(): IterableIterator; - /** Sets a new value for an existing header inside a Headers object, or adds - * the header if it does not already exist. - */ - set(name: string, value: string): void; - /** Returns an iterator allowing to go through all values contained in - * this Headers object. The values are ByteString objects. - */ - values(): IterableIterator; - forEach( - callbackfn: (value: string, key: string, parent: this) => void, - thisArg?: any, - ): void; - /** The Symbol.iterator well-known symbol specifies the default - * iterator for this Headers object - */ - [Symbol.iterator](): IterableIterator<[string, string]>; -} - -declare const Headers: { - prototype: Headers; - new (init?: HeadersInit): Headers; -}; - -type RequestInfo = Request | string; -type RequestCache = - | "default" - | "force-cache" - | "no-cache" - | "no-store" - | "only-if-cached" - | "reload"; -type RequestCredentials = "include" | "omit" | "same-origin"; -type RequestMode = "cors" | "navigate" | "no-cors" | "same-origin"; -type RequestRedirect = "error" | "follow" | "manual"; -type ReferrerPolicy = - | "" - | "no-referrer" - | "no-referrer-when-downgrade" - | "origin" - | "origin-when-cross-origin" - | "same-origin" - | "strict-origin" - | "strict-origin-when-cross-origin" - | "unsafe-url"; -type BodyInit = - | Blob - | BufferSource - | FormData - | URLSearchParams - | ReadableStream - | string; -type RequestDestination = - | "" - | "audio" - | "audioworklet" - | "document" - | "embed" - | "font" - | "image" - | "manifest" - | "object" - | "paintworklet" - | "report" - | "script" - | "sharedworker" - | "style" - | "track" - | "video" - | "worker" - | "xslt"; - -interface RequestInit { - /** - * A BodyInit object or null to set request's body. - */ - body?: BodyInit | null; - /** - * A string indicating how the request will interact with the browser's cache - * to set request's cache. - */ - cache?: RequestCache; - /** - * A string indicating whether credentials will be sent with the request - * always, never, or only when sent to a same-origin URL. Sets request's - * credentials. - */ - credentials?: RequestCredentials; - /** - * A Headers object, an object literal, or an array of two-item arrays to set - * request's headers. - */ - headers?: HeadersInit; - /** - * A cryptographic hash of the resource to be fetched by request. Sets - * request's integrity. - */ - integrity?: string; - /** - * A boolean to set request's keepalive. - */ - keepalive?: boolean; - /** - * A string to set request's method. - */ - method?: string; - /** - * A string to indicate whether the request will use CORS, or will be - * restricted to same-origin URLs. Sets request's mode. - */ - mode?: RequestMode; - /** - * A string indicating whether request follows redirects, results in an error - * upon encountering a redirect, or returns the redirect (in an opaque - * fashion). Sets request's redirect. - */ - redirect?: RequestRedirect; - /** - * A string whose value is a same-origin URL, "about:client", or the empty - * string, to set request's referrer. - */ - referrer?: string; - /** - * A referrer policy to set request's referrerPolicy. - */ - referrerPolicy?: ReferrerPolicy; - /** - * An AbortSignal to set request's signal. - */ - signal?: AbortSignal | null; - /** - * Can only be null. Used to disassociate request from any Window. - */ - window?: any; -} - -/** This Fetch API interface represents a resource request. */ -interface Request extends Body { - /** - * Returns the cache mode associated with request, which is a string - * indicating how the request will interact with the browser's cache when - * fetching. - */ - readonly cache: RequestCache; - /** - * Returns the credentials mode associated with request, which is a string - * indicating whether credentials will be sent with the request always, never, - * or only when sent to a same-origin URL. - */ - readonly credentials: RequestCredentials; - /** - * Returns the kind of resource requested by request, e.g., "document" or "script". - */ - readonly destination: RequestDestination; - /** - * Returns a Headers object consisting of the headers associated with request. - * Note that headers added in the network layer by the user agent will not be - * accounted for in this object, e.g., the "Host" header. - */ - readonly headers: Headers; - /** - * Returns request's subresource integrity metadata, which is a cryptographic - * hash of the resource being fetched. Its value consists of multiple hashes - * separated by whitespace. [SRI] - */ - readonly integrity: string; - /** - * Returns a boolean indicating whether or not request is for a history - * navigation (a.k.a. back-forward navigation). - */ - readonly isHistoryNavigation: boolean; - /** - * Returns a boolean indicating whether or not request is for a reload - * navigation. - */ - readonly isReloadNavigation: boolean; - /** - * Returns a boolean indicating whether or not request can outlive the global - * in which it was created. - */ - readonly keepalive: boolean; - /** - * Returns request's HTTP method, which is "GET" by default. - */ - readonly method: string; - /** - * Returns the mode associated with request, which is a string indicating - * whether the request will use CORS, or will be restricted to same-origin - * URLs. - */ - readonly mode: RequestMode; - /** - * Returns the redirect mode associated with request, which is a string - * indicating how redirects for the request will be handled during fetching. A - * request will follow redirects by default. - */ - readonly redirect: RequestRedirect; - /** - * Returns the referrer of request. Its value can be a same-origin URL if - * explicitly set in init, the empty string to indicate no referrer, and - * "about:client" when defaulting to the global's default. This is used during - * fetching to determine the value of the `Referer` header of the request - * being made. - */ - readonly referrer: string; - /** - * Returns the referrer policy associated with request. This is used during - * fetching to compute the value of the request's referrer. - */ - readonly referrerPolicy: ReferrerPolicy; - /** - * Returns the signal associated with request, which is an AbortSignal object - * indicating whether or not request has been aborted, and its abort event - * handler. - */ - readonly signal: AbortSignal; - /** - * Returns the URL of request as a string. - */ - readonly url: string; - clone(): Request; -} - -declare const Request: { - prototype: Request; - new (input: RequestInfo, init?: RequestInit): Request; -}; interface ResponseInit { headers?: HeadersInit; @@ -1003,26 +396,6 @@ interface Response extends Body { clone(): Response; } -declare const Response: { - prototype: Response; - new (body?: BodyInit | null, init?: ResponseInit): Response; - error(): Response; - redirect(url: string, status?: number): Response; -}; - -/** Fetch a resource from the network. It returns a Promise that resolves to the - * Response to that request, whether it is successful or not. - * - * const response = await fetch("http://my.json.host/data.json"); - * console.log(response.status); // e.g. 200 - * console.log(response.statusText); // e.g. "OK" - * const jsonData = await response.json(); - */ -declare function fetch( - input: Request | URL | string, - init?: RequestInit, -): Promise; - interface URLSearchParams { /** Appends a specified key/value pair as a new search parameter. * diff --git a/cli/js.rs b/cli/js.rs index 1c738af76..ef6d6c7e8 100644 --- a/cli/js.rs +++ b/cli/js.rs @@ -10,6 +10,7 @@ pub static COMPILER_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/COMPILER_SNAPSHOT.bin")); pub static DENO_NS_LIB: &str = include_str!("dts/lib.deno.ns.d.ts"); pub static DENO_WEB_LIB: &str = include_str!(env!("DENO_WEB_LIB_PATH")); +pub static DENO_FETCH_LIB: &str = include_str!(env!("DENO_FETCH_LIB_PATH")); pub static SHARED_GLOBALS_LIB: &str = include_str!("dts/lib.deno.shared_globals.d.ts"); pub static WINDOW_LIB: &str = include_str!("dts/lib.deno.window.d.ts"); diff --git a/cli/main.rs b/cli/main.rs index ea8b596d5..18dd3ffcb 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -140,9 +140,10 @@ fn print_cache_info( fn get_types(unstable: bool) -> String { let mut types = format!( - "{}\n{}\n{}\n{}", + "{}\n{}\n{}\n{}\n{}", crate::js::DENO_NS_LIB, crate::js::DENO_WEB_LIB, + crate::js::DENO_FETCH_LIB, crate::js::SHARED_GLOBALS_LIB, crate::js::WINDOW_LIB, ); diff --git a/cli/ops/fetch.rs b/cli/ops/fetch.rs index be11955cf..cbfcfb874 100644 --- a/cli/ops/fetch.rs +++ b/cli/ops/fetch.rs @@ -1,191 +1,12 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::http_util::create_http_client; -use deno_core::error::bad_resource_id; -use deno_core::error::type_error; -use deno_core::error::AnyError; -use deno_core::url; -use deno_core::BufVec; -use deno_core::OpState; -use deno_core::ZeroCopyBuf; -use http::header::HeaderName; -use http::header::HeaderValue; -use http::Method; -use reqwest::Client; -use reqwest::Response; -use serde::Deserialize; -use serde_json::Value; -use std::cell::RefCell; -use std::convert::From; -use std::path::PathBuf; -use std::rc::Rc; +use crate::state::CliState; pub fn init(rt: &mut deno_core::JsRuntime) { - super::reg_json_async(rt, "op_fetch", op_fetch); - super::reg_json_async(rt, "op_fetch_read", op_fetch_read); - super::reg_json_sync(rt, "op_create_http_client", op_create_http_client); -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct FetchArgs { - method: Option, - url: String, - headers: Vec<(String, String)>, - client_rid: Option, -} - -async fn op_fetch( - state: Rc>, - args: Value, - data: BufVec, -) -> Result { - let args: FetchArgs = serde_json::from_value(args)?; - let url = args.url; - - let client = if let Some(rid) = args.client_rid { - let state = state.borrow(); - let r = state - .resource_table - .get::(rid) - .ok_or_else(bad_resource_id)?; - r.client.clone() - } else { - let state_ = state.borrow(); - let client = state_.borrow::(); - client.clone() - }; - - let method = match args.method { - Some(method_str) => Method::from_bytes(method_str.as_bytes())?, - None => Method::GET, - }; - - let url_ = url::Url::parse(&url)?; - - // Check scheme before asking for net permission - let scheme = url_.scheme(); - if scheme != "http" && scheme != "https" { - return Err(type_error(format!("scheme '{}' not supported", scheme))); - } - - super::cli_state2(&state).check_net_url(&url_)?; - - let mut request = client.request(method, url_); - - match data.len() { - 0 => {} - 1 => request = request.body(Vec::from(&*data[0])), - _ => panic!("Invalid number of arguments"), - } - - for (key, value) in args.headers { - let name = HeaderName::from_bytes(key.as_bytes()).unwrap(); - let v = HeaderValue::from_str(&value).unwrap(); - request = request.header(name, v); - } - debug!("Before fetch {}", url); - - let res = request.send().await?; - - debug!("Fetch response {}", url); - let status = res.status(); - let mut res_headers = Vec::new(); - for (key, val) in res.headers().iter() { - res_headers.push((key.to_string(), val.to_str().unwrap().to_owned())); - } - - let rid = state - .borrow_mut() - .resource_table - .add("httpBody", Box::new(res)); - - Ok(json!({ - "bodyRid": rid, - "status": status.as_u16(), - "statusText": status.canonical_reason().unwrap_or(""), - "headers": res_headers - })) -} - -async fn op_fetch_read( - state: Rc>, - args: Value, - _data: BufVec, -) -> Result { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - struct Args { - rid: u32, - } - - let args: Args = serde_json::from_value(args)?; - let rid = args.rid; - - use futures::future::poll_fn; - use futures::ready; - use futures::FutureExt; - let f = poll_fn(move |cx| { - let mut state = state.borrow_mut(); - let response = state - .resource_table - .get_mut::(rid as u32) - .ok_or_else(bad_resource_id)?; - - let mut chunk_fut = response.chunk().boxed_local(); - let r = ready!(chunk_fut.poll_unpin(cx))?; - if let Some(chunk) = r { - Ok(json!({ "chunk": &*chunk })).into() - } else { - Ok(json!({ "chunk": null })).into() - } - }); - f.await - /* - // I'm programming this as I want it to be programmed, even though it might be - // incorrect, normally we would use poll_fn here. We need to make this await pattern work. - let chunk = response.chunk().await?; - if let Some(chunk) = chunk { - // TODO(ry) This is terribly inefficient. Make this zero-copy. - Ok(json!({ "chunk": &*chunk })) - } else { - Ok(json!({ "chunk": null })) - } - */ -} - -struct HttpClientResource { - client: Client, -} - -impl HttpClientResource { - fn new(client: Client) -> Self { - Self { client } - } -} - -#[derive(Deserialize, Default, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -struct CreateHttpClientOptions { - ca_file: Option, -} - -fn op_create_http_client( - state: &mut OpState, - args: Value, - _zero_copy: &mut [ZeroCopyBuf], -) -> Result { - let args: CreateHttpClientOptions = serde_json::from_value(args)?; - - if let Some(ca_file) = args.ca_file.clone() { - super::cli_state(state).check_read(&PathBuf::from(ca_file))?; - } - - let client = create_http_client(args.ca_file.as_deref()).unwrap(); - - let rid = state - .resource_table - .add("httpClient", Box::new(HttpClientResource::new(client))); - Ok(json!(rid)) + super::reg_json_async(rt, "op_fetch", deno_fetch::op_fetch::); + super::reg_json_async(rt, "op_fetch_read", deno_fetch::op_fetch_read); + super::reg_json_sync( + rt, + "op_create_http_client", + deno_fetch::op_create_http_client::, + ); } diff --git a/cli/rt/01_web_util.js b/cli/rt/01_web_util.js index d64ef28c3..1b7f7b83a 100644 --- a/cli/rt/01_web_util.js +++ b/cli/rt/01_web_util.js @@ -1,10 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. ((window) => { - function isTypedArray(x) { - return ArrayBuffer.isView(x) && !(x instanceof DataView); - } - function isInvalidDate(x) { return isNaN(x.getTime()); } @@ -149,40 +145,12 @@ } } - function getHeaderValueParams(value) { - const params = new Map(); - // Forced to do so for some Map constructor param mismatch - value - .split(";") - .slice(1) - .map((s) => s.trim().split("=")) - .filter((arr) => arr.length > 1) - .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")]) - .forEach(([k, v]) => params.set(k, v)); - return params; - } - - function hasHeaderValueOf(s, value) { - return new RegExp(`^${value}[\t\s]*;?`).test(s); - } - - /** An internal function which provides a function name for some generated - * functions, so stack traces are a bit more readable. - */ - function setFunctionName(fn, value) { - Object.defineProperty(fn, "name", { value, configurable: true }); - } - window.__bootstrap.webUtil = { - isTypedArray, isInvalidDate, requiredArguments, immutableDefine, hasOwnProperty, cloneValue, defineEnumerableProps, - getHeaderValueParams, - hasHeaderValueOf, - setFunctionName, }; })(this); diff --git a/cli/rt/02_console.js b/cli/rt/02_console.js index a5e6595b9..34e106ff2 100644 --- a/cli/rt/02_console.js +++ b/cli/rt/02_console.js @@ -15,7 +15,6 @@ } = window.__bootstrap.colors; const { - isTypedArray, isInvalidDate, hasOwnProperty, } = window.__bootstrap.webUtil; @@ -23,6 +22,10 @@ // Copyright Joyent, Inc. and other Node contributors. MIT license. // Forked from Node's lib/internal/cli_table.js + function isTypedArray(x) { + return ArrayBuffer.isView(x) && !(x instanceof DataView); + } + const tableChars = { middleMiddle: "─", rowMiddle: "┼", diff --git a/cli/rt/03_dom_iterable.js b/cli/rt/03_dom_iterable.js deleted file mode 100644 index cd190b9cd..000000000 --- a/cli/rt/03_dom_iterable.js +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { requiredArguments } = window.__bootstrap.webUtil; - const { exposeForTest } = window.__bootstrap.internals; - - function DomIterableMixin( - Base, - dataSymbol, - ) { - // we have to cast `this` as `any` because there is no way to describe the - // Base class in a way where the Symbol `dataSymbol` is defined. So the - // runtime code works, but we do lose a little bit of type safety. - - // Additionally, we have to not use .keys() nor .values() since the internal - // slot differs in type - some have a Map, which yields [K, V] in - // Symbol.iterator, and some have an Array, which yields V, in this case - // [K, V] too as they are arrays of tuples. - - const DomIterable = class extends Base { - *entries() { - for (const entry of this[dataSymbol]) { - yield entry; - } - } - - *keys() { - for (const [key] of this[dataSymbol]) { - yield key; - } - } - - *values() { - for (const [, value] of this[dataSymbol]) { - yield value; - } - } - - forEach( - callbackfn, - thisArg, - ) { - requiredArguments( - `${this.constructor.name}.forEach`, - arguments.length, - 1, - ); - callbackfn = callbackfn.bind( - thisArg == null ? globalThis : Object(thisArg), - ); - for (const [key, value] of this[dataSymbol]) { - callbackfn(value, key, this); - } - } - - *[Symbol.iterator]() { - for (const entry of this[dataSymbol]) { - yield entry; - } - } - }; - - // we want the Base class name to be the name of the class. - Object.defineProperty(DomIterable, "name", { - value: Base.name, - configurable: true, - }); - - return DomIterable; - } - - exposeForTest("DomIterableMixin", DomIterableMixin); - - window.__bootstrap.domIterable = { - DomIterableMixin, - }; -})(this); diff --git a/cli/rt/11_streams.js b/cli/rt/11_streams.js deleted file mode 100644 index 630878e74..000000000 --- a/cli/rt/11_streams.js +++ /dev/null @@ -1,3280 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// This code closely follows the WHATWG Stream Specification -// See: https://streams.spec.whatwg.org/ -// -// There are some parts that are not fully implemented, and there are some -// comments which point to steps of the specification that are not implemented. - -((window) => { - /* eslint-disable @typescript-eslint/no-explicit-any,require-await */ - - const { cloneValue, setFunctionName } = window.__bootstrap.webUtil; - const { assert, AssertionError } = window.__bootstrap.util; - - const customInspect = Symbol.for("Deno.customInspect"); - - const sym = { - abortAlgorithm: Symbol("abortAlgorithm"), - abortSteps: Symbol("abortSteps"), - asyncIteratorReader: Symbol("asyncIteratorReader"), - autoAllocateChunkSize: Symbol("autoAllocateChunkSize"), - backpressure: Symbol("backpressure"), - backpressureChangePromise: Symbol("backpressureChangePromise"), - byobRequest: Symbol("byobRequest"), - cancelAlgorithm: Symbol("cancelAlgorithm"), - cancelSteps: Symbol("cancelSteps"), - closeAlgorithm: Symbol("closeAlgorithm"), - closedPromise: Symbol("closedPromise"), - closeRequest: Symbol("closeRequest"), - closeRequested: Symbol("closeRequested"), - controlledReadableByteStream: Symbol( - "controlledReadableByteStream", - ), - controlledReadableStream: Symbol("controlledReadableStream"), - controlledTransformStream: Symbol("controlledTransformStream"), - controlledWritableStream: Symbol("controlledWritableStream"), - disturbed: Symbol("disturbed"), - errorSteps: Symbol("errorSteps"), - flushAlgorithm: Symbol("flushAlgorithm"), - forAuthorCode: Symbol("forAuthorCode"), - inFlightWriteRequest: Symbol("inFlightWriteRequest"), - inFlightCloseRequest: Symbol("inFlightCloseRequest"), - isFakeDetached: Symbol("isFakeDetached"), - ownerReadableStream: Symbol("ownerReadableStream"), - ownerWritableStream: Symbol("ownerWritableStream"), - pendingAbortRequest: Symbol("pendingAbortRequest"), - preventCancel: Symbol("preventCancel"), - pullAgain: Symbol("pullAgain"), - pullAlgorithm: Symbol("pullAlgorithm"), - pulling: Symbol("pulling"), - pullSteps: Symbol("pullSteps"), - queue: Symbol("queue"), - queueTotalSize: Symbol("queueTotalSize"), - readable: Symbol("readable"), - readableStreamController: Symbol("readableStreamController"), - reader: Symbol("reader"), - readRequests: Symbol("readRequests"), - readyPromise: Symbol("readyPromise"), - started: Symbol("started"), - state: Symbol("state"), - storedError: Symbol("storedError"), - strategyHWM: Symbol("strategyHWM"), - strategySizeAlgorithm: Symbol("strategySizeAlgorithm"), - transformAlgorithm: Symbol("transformAlgorithm"), - transformStreamController: Symbol("transformStreamController"), - writableStreamController: Symbol("writableStreamController"), - writeAlgorithm: Symbol("writeAlgorithm"), - writable: Symbol("writable"), - writer: Symbol("writer"), - writeRequests: Symbol("writeRequests"), - }; - class ReadableByteStreamController { - constructor() { - throw new TypeError( - "ReadableByteStreamController's constructor cannot be called.", - ); - } - - get byobRequest() { - return undefined; - } - - get desiredSize() { - if (!isReadableByteStreamController(this)) { - throw new TypeError("Invalid ReadableByteStreamController."); - } - return readableByteStreamControllerGetDesiredSize(this); - } - - close() { - if (!isReadableByteStreamController(this)) { - throw new TypeError("Invalid ReadableByteStreamController."); - } - if (this[sym.closeRequested]) { - throw new TypeError("Closed already requested."); - } - if (this[sym.controlledReadableByteStream][sym.state] !== "readable") { - throw new TypeError( - "ReadableByteStreamController's stream is not in a readable state.", - ); - } - readableByteStreamControllerClose(this); - } - - enqueue(chunk) { - if (!isReadableByteStreamController(this)) { - throw new TypeError("Invalid ReadableByteStreamController."); - } - if (this[sym.closeRequested]) { - throw new TypeError("Closed already requested."); - } - if (this[sym.controlledReadableByteStream][sym.state] !== "readable") { - throw new TypeError( - "ReadableByteStreamController's stream is not in a readable state.", - ); - } - if (!ArrayBuffer.isView(chunk)) { - throw new TypeError( - "You can only enqueue array buffer views when using a ReadableByteStreamController", - ); - } - if (isDetachedBuffer(chunk.buffer)) { - throw new TypeError( - "Cannot enqueue a view onto a detached ArrayBuffer", - ); - } - readableByteStreamControllerEnqueue(this, chunk); - } - - error(error) { - if (!isReadableByteStreamController(this)) { - throw new TypeError("Invalid ReadableByteStreamController."); - } - readableByteStreamControllerError(this, error); - } - - [sym.cancelSteps](reason) { - // 3.11.5.1.1 If this.[[pendingPullIntos]] is not empty, - resetQueue(this); - const result = this[sym.cancelAlgorithm](reason); - readableByteStreamControllerClearAlgorithms(this); - return result; - } - - [sym.pullSteps]() { - const stream = this[sym.controlledReadableByteStream]; - assert(readableStreamHasDefaultReader(stream)); - if (this[sym.queueTotalSize] > 0) { - assert(readableStreamGetNumReadRequests(stream) === 0); - const entry = this[sym.queue].shift(); - assert(entry); - this[sym.queueTotalSize] -= entry.size; - readableByteStreamControllerHandleQueueDrain(this); - const view = new Uint8Array(entry.value, entry.offset, entry.size); - return Promise.resolve( - readableStreamCreateReadResult( - view, - false, - stream[sym.reader][sym.forAuthorCode], - ), - ); - } - // 3.11.5.2.5 If autoAllocateChunkSize is not undefined, - const promise = readableStreamAddReadRequest(stream); - readableByteStreamControllerCallPullIfNeeded(this); - return promise; - } - - [customInspect]() { - return `${this.constructor.name} { byobRequest: ${ - String(this.byobRequest) - }, desiredSize: ${String(this.desiredSize)} }`; - } - } - - class ReadableStreamDefaultController { - constructor() { - throw new TypeError( - "ReadableStreamDefaultController's constructor cannot be called.", - ); - } - - get desiredSize() { - if (!isReadableStreamDefaultController(this)) { - throw new TypeError("Invalid ReadableStreamDefaultController."); - } - return readableStreamDefaultControllerGetDesiredSize(this); - } - - close() { - if (!isReadableStreamDefaultController(this)) { - throw new TypeError("Invalid ReadableStreamDefaultController."); - } - if (!readableStreamDefaultControllerCanCloseOrEnqueue(this)) { - throw new TypeError( - "ReadableStreamDefaultController cannot close or enqueue.", - ); - } - readableStreamDefaultControllerClose(this); - } - - enqueue(chunk) { - if (!isReadableStreamDefaultController(this)) { - throw new TypeError("Invalid ReadableStreamDefaultController."); - } - if (!readableStreamDefaultControllerCanCloseOrEnqueue(this)) { - throw new TypeError("ReadableSteamController cannot enqueue."); - } - return readableStreamDefaultControllerEnqueue(this, chunk); - } - - error(error) { - if (!isReadableStreamDefaultController(this)) { - throw new TypeError("Invalid ReadableStreamDefaultController."); - } - readableStreamDefaultControllerError(this, error); - } - - [sym.cancelSteps](reason) { - resetQueue(this); - const result = this[sym.cancelAlgorithm](reason); - readableStreamDefaultControllerClearAlgorithms(this); - return result; - } - - [sym.pullSteps]() { - const stream = this[sym.controlledReadableStream]; - if (this[sym.queue].length) { - const chunk = dequeueValue(this); - if (this[sym.closeRequested] && this[sym.queue].length === 0) { - readableStreamDefaultControllerClearAlgorithms(this); - readableStreamClose(stream); - } else { - readableStreamDefaultControllerCallPullIfNeeded(this); - } - return Promise.resolve( - readableStreamCreateReadResult( - chunk, - false, - stream[sym.reader][sym.forAuthorCode], - ), - ); - } - const pendingPromise = readableStreamAddReadRequest(stream); - readableStreamDefaultControllerCallPullIfNeeded(this); - return pendingPromise; - } - - [customInspect]() { - return `${this.constructor.name} { desiredSize: ${ - String(this.desiredSize) - } }`; - } - } - - class ReadableStreamDefaultReader { - constructor(stream) { - if (!isReadableStream(stream)) { - throw new TypeError("stream is not a ReadableStream."); - } - if (isReadableStreamLocked(stream)) { - throw new TypeError("stream is locked."); - } - readableStreamReaderGenericInitialize(this, stream); - this[sym.readRequests] = []; - } - - get closed() { - if (!isReadableStreamDefaultReader(this)) { - return Promise.reject( - new TypeError("Invalid ReadableStreamDefaultReader."), - ); - } - return ( - this[sym.closedPromise].promise ?? - Promise.reject(new TypeError("Invalid reader.")) - ); - } - - cancel(reason) { - if (!isReadableStreamDefaultReader(this)) { - return Promise.reject( - new TypeError("Invalid ReadableStreamDefaultReader."), - ); - } - if (!this[sym.ownerReadableStream]) { - return Promise.reject(new TypeError("Invalid reader.")); - } - return readableStreamReaderGenericCancel(this, reason); - } - - read() { - if (!isReadableStreamDefaultReader(this)) { - return Promise.reject( - new TypeError("Invalid ReadableStreamDefaultReader."), - ); - } - if (!this[sym.ownerReadableStream]) { - return Promise.reject(new TypeError("Invalid reader.")); - } - return readableStreamDefaultReaderRead(this); - } - - releaseLock() { - if (!isReadableStreamDefaultReader(this)) { - throw new TypeError("Invalid ReadableStreamDefaultReader."); - } - if (this[sym.ownerReadableStream] === undefined) { - return; - } - if (this[sym.readRequests].length) { - throw new TypeError("Cannot release lock with pending read requests."); - } - readableStreamReaderGenericRelease(this); - } - - [customInspect]() { - return `${this.constructor.name} { closed: Promise }`; - } - } - - const AsyncIteratorPrototype = Object - .getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); - - const ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf({ - next() { - if (!isReadableStreamAsyncIterator(this)) { - return Promise.reject( - new TypeError("invalid ReadableStreamAsyncIterator."), - ); - } - const reader = this[sym.asyncIteratorReader]; - if (!reader[sym.ownerReadableStream]) { - return Promise.reject( - new TypeError("reader owner ReadableStream is undefined."), - ); - } - return readableStreamDefaultReaderRead(reader).then((result) => { - assert(typeof result === "object"); - const { done } = result; - assert(typeof done === "boolean"); - if (done) { - readableStreamReaderGenericRelease(reader); - } - const { value } = result; - return readableStreamCreateReadResult(value, done, true); - }); - }, - return( - value, - ) { - if (!isReadableStreamAsyncIterator(this)) { - return Promise.reject( - new TypeError("invalid ReadableStreamAsyncIterator."), - ); - } - const reader = this[sym.asyncIteratorReader]; - if (!reader[sym.ownerReadableStream]) { - return Promise.reject( - new TypeError("reader owner ReadableStream is undefined."), - ); - } - if (reader[sym.readRequests].length) { - return Promise.reject( - new TypeError("reader has outstanding read requests."), - ); - } - if (!this[sym.preventCancel]) { - const result = readableStreamReaderGenericCancel(reader, value); - readableStreamReaderGenericRelease(reader); - return result.then(() => - readableStreamCreateReadResult(value, true, true) - ); - } - readableStreamReaderGenericRelease(reader); - return Promise.resolve( - readableStreamCreateReadResult(value, true, true), - ); - }, - }, AsyncIteratorPrototype); - - class ReadableStream { - constructor( - underlyingSource = {}, - strategy = {}, - ) { - initializeReadableStream(this); - const { size } = strategy; - let { highWaterMark } = strategy; - const { type } = underlyingSource; - - if (underlyingSource.type == "bytes") { - if (size !== undefined) { - throw new RangeError( - `When underlying source is "bytes", strategy.size must be undefined.`, - ); - } - highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark ?? 0); - setUpReadableByteStreamControllerFromUnderlyingSource( - this, - underlyingSource, - highWaterMark, - ); - } else if (type === undefined) { - const sizeAlgorithm = makeSizeAlgorithmFromSizeFunction(size); - highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark ?? 1); - setUpReadableStreamDefaultControllerFromUnderlyingSource( - this, - underlyingSource, - highWaterMark, - sizeAlgorithm, - ); - } else { - throw new RangeError( - `Valid values for underlyingSource are "bytes" or undefined. Received: "${type}".`, - ); - } - } - - get locked() { - if (!isReadableStream(this)) { - throw new TypeError("Invalid ReadableStream."); - } - return isReadableStreamLocked(this); - } - - cancel(reason) { - if (!isReadableStream(this)) { - return Promise.reject(new TypeError("Invalid ReadableStream.")); - } - if (isReadableStreamLocked(this)) { - return Promise.reject( - new TypeError("Cannot cancel a locked ReadableStream."), - ); - } - return readableStreamCancel(this, reason); - } - - getIterator({ - preventCancel, - } = {}) { - if (!isReadableStream(this)) { - throw new TypeError("Invalid ReadableStream."); - } - const reader = acquireReadableStreamDefaultReader(this); - const iterator = Object.create(ReadableStreamAsyncIteratorPrototype); - iterator[sym.asyncIteratorReader] = reader; - iterator[sym.preventCancel] = Boolean(preventCancel); - return iterator; - } - - getReader({ mode } = {}) { - if (!isReadableStream(this)) { - throw new TypeError("Invalid ReadableStream."); - } - if (mode === undefined) { - return acquireReadableStreamDefaultReader(this, true); - } - mode = String(mode); - // 3.2.5.4.4 If mode is "byob", return ? AcquireReadableStreamBYOBReader(this, true). - throw new RangeError(`Unsupported mode "${mode}"`); - } - - pipeThrough( - { - writable, - readable, - }, - { preventClose, preventAbort, preventCancel, signal } = {}, - ) { - if (!isReadableStream(this)) { - throw new TypeError("Invalid ReadableStream."); - } - if (!isWritableStream(writable)) { - throw new TypeError("writable is not a valid WritableStream."); - } - if (!isReadableStream(readable)) { - throw new TypeError("readable is not a valid ReadableStream."); - } - preventClose = Boolean(preventClose); - preventAbort = Boolean(preventAbort); - preventCancel = Boolean(preventCancel); - if (signal && !(signal instanceof AbortSignal)) { - throw new TypeError("Invalid signal."); - } - if (isReadableStreamLocked(this)) { - throw new TypeError("ReadableStream is locked."); - } - if (isWritableStreamLocked(writable)) { - throw new TypeError("writable is locked."); - } - const promise = readableStreamPipeTo( - this, - writable, - preventClose, - preventAbort, - preventCancel, - signal, - ); - setPromiseIsHandledToTrue(promise); - return readable; - } - - pipeTo( - dest, - { preventClose, preventAbort, preventCancel, signal } = {}, - ) { - if (!isReadableStream(this)) { - return Promise.reject(new TypeError("Invalid ReadableStream.")); - } - if (!isWritableStream(dest)) { - return Promise.reject( - new TypeError("dest is not a valid WritableStream."), - ); - } - preventClose = Boolean(preventClose); - preventAbort = Boolean(preventAbort); - preventCancel = Boolean(preventCancel); - if (signal && !(signal instanceof AbortSignal)) { - return Promise.reject(new TypeError("Invalid signal.")); - } - if (isReadableStreamLocked(this)) { - return Promise.reject(new TypeError("ReadableStream is locked.")); - } - if (isWritableStreamLocked(dest)) { - return Promise.reject(new TypeError("dest is locked.")); - } - return readableStreamPipeTo( - this, - dest, - preventClose, - preventAbort, - preventCancel, - signal, - ); - } - - tee() { - if (!isReadableStream(this)) { - throw new TypeError("Invalid ReadableStream."); - } - return readableStreamTee(this, false); - } - - [customInspect]() { - return `${this.constructor.name} { locked: ${String(this.locked)} }`; - } - - [Symbol.asyncIterator]( - options = {}, - ) { - return this.getIterator(options); - } - } - - class TransformStream { - constructor( - transformer = {}, - writableStrategy = {}, - readableStrategy = {}, - ) { - const writableSizeFunction = writableStrategy.size; - let writableHighWaterMark = writableStrategy.highWaterMark; - const readableSizeFunction = readableStrategy.size; - let readableHighWaterMark = readableStrategy.highWaterMark; - const writableType = transformer.writableType; - if (writableType !== undefined) { - throw new RangeError( - `Expected transformer writableType to be undefined, received "${ - String(writableType) - }"`, - ); - } - const writableSizeAlgorithm = makeSizeAlgorithmFromSizeFunction( - writableSizeFunction, - ); - if (writableHighWaterMark === undefined) { - writableHighWaterMark = 1; - } - writableHighWaterMark = validateAndNormalizeHighWaterMark( - writableHighWaterMark, - ); - const readableType = transformer.readableType; - if (readableType !== undefined) { - throw new RangeError( - `Expected transformer readableType to be undefined, received "${ - String(readableType) - }"`, - ); - } - const readableSizeAlgorithm = makeSizeAlgorithmFromSizeFunction( - readableSizeFunction, - ); - if (readableHighWaterMark === undefined) { - readableHighWaterMark = 1; - } - readableHighWaterMark = validateAndNormalizeHighWaterMark( - readableHighWaterMark, - ); - const startPromise = getDeferred(); - initializeTransformStream( - this, - startPromise.promise, - writableHighWaterMark, - writableSizeAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, - ); - // the brand check expects this, and the brand check occurs in the following - // but the property hasn't been defined. - Object.defineProperty(this, sym.transformStreamController, { - value: undefined, - writable: true, - configurable: true, - }); - setUpTransformStreamDefaultControllerFromTransformer(this, transformer); - const startResult = invokeOrNoop( - transformer, - "start", - this[sym.transformStreamController], - ); - startPromise.resolve(startResult); - } - - get readable() { - if (!isTransformStream(this)) { - throw new TypeError("Invalid TransformStream."); - } - return this[sym.readable]; - } - - get writable() { - if (!isTransformStream(this)) { - throw new TypeError("Invalid TransformStream."); - } - return this[sym.writable]; - } - - [customInspect]() { - return this.constructor.name; - } - } - - class TransformStreamDefaultController { - constructor() { - throw new TypeError( - "TransformStreamDefaultController's constructor cannot be called.", - ); - } - - get desiredSize() { - if (!isTransformStreamDefaultController(this)) { - throw new TypeError("Invalid TransformStreamDefaultController."); - } - const readableController = this[sym.controlledTransformStream][ - sym.readable - ][sym.readableStreamController]; - return readableStreamDefaultControllerGetDesiredSize( - readableController, - ); - } - - enqueue(chunk) { - if (!isTransformStreamDefaultController(this)) { - throw new TypeError("Invalid TransformStreamDefaultController."); - } - transformStreamDefaultControllerEnqueue(this, chunk); - } - - error(reason) { - if (!isTransformStreamDefaultController(this)) { - throw new TypeError("Invalid TransformStreamDefaultController."); - } - transformStreamDefaultControllerError(this, reason); - } - - terminate() { - if (!isTransformStreamDefaultController(this)) { - throw new TypeError("Invalid TransformStreamDefaultController."); - } - transformStreamDefaultControllerTerminate(this); - } - - [customInspect]() { - return `${this.constructor.name} { desiredSize: ${ - String(this.desiredSize) - } }`; - } - } - - class WritableStreamDefaultController { - constructor() { - throw new TypeError( - "WritableStreamDefaultController's constructor cannot be called.", - ); - } - - error(e) { - if (!isWritableStreamDefaultController(this)) { - throw new TypeError("Invalid WritableStreamDefaultController."); - } - const state = this[sym.controlledWritableStream][sym.state]; - if (state !== "writable") { - return; - } - writableStreamDefaultControllerError(this, e); - } - - [sym.abortSteps](reason) { - const result = this[sym.abortAlgorithm](reason); - writableStreamDefaultControllerClearAlgorithms(this); - return result; - } - - [sym.errorSteps]() { - resetQueue(this); - } - - [customInspect]() { - return `${this.constructor.name} { }`; - } - } - - class WritableStreamDefaultWriter { - constructor(stream) { - if (!isWritableStream(stream)) { - throw new TypeError("Invalid stream."); - } - if (isWritableStreamLocked(stream)) { - throw new TypeError("Cannot create a writer for a locked stream."); - } - this[sym.ownerWritableStream] = stream; - stream[sym.writer] = this; - const state = stream[sym.state]; - if (state === "writable") { - if ( - !writableStreamCloseQueuedOrInFlight(stream) && - stream[sym.backpressure] - ) { - this[sym.readyPromise] = getDeferred(); - } else { - this[sym.readyPromise] = { promise: Promise.resolve() }; - } - this[sym.closedPromise] = getDeferred(); - } else if (state === "erroring") { - this[sym.readyPromise] = { - promise: Promise.reject(stream[sym.storedError]), - }; - setPromiseIsHandledToTrue(this[sym.readyPromise].promise); - this[sym.closedPromise] = getDeferred(); - } else if (state === "closed") { - this[sym.readyPromise] = { promise: Promise.resolve() }; - this[sym.closedPromise] = { promise: Promise.resolve() }; - } else { - assert(state === "errored"); - const storedError = stream[sym.storedError]; - this[sym.readyPromise] = { promise: Promise.reject(storedError) }; - setPromiseIsHandledToTrue(this[sym.readyPromise].promise); - this[sym.closedPromise] = { promise: Promise.reject(storedError) }; - setPromiseIsHandledToTrue(this[sym.closedPromise].promise); - } - } - - get closed() { - if (!isWritableStreamDefaultWriter(this)) { - return Promise.reject( - new TypeError("Invalid WritableStreamDefaultWriter."), - ); - } - return this[sym.closedPromise].promise; - } - - get desiredSize() { - if (!isWritableStreamDefaultWriter(this)) { - throw new TypeError("Invalid WritableStreamDefaultWriter."); - } - if (!this[sym.ownerWritableStream]) { - throw new TypeError("WritableStreamDefaultWriter has no owner."); - } - return writableStreamDefaultWriterGetDesiredSize(this); - } - - get ready() { - if (!isWritableStreamDefaultWriter(this)) { - return Promise.reject( - new TypeError("Invalid WritableStreamDefaultWriter."), - ); - } - return this[sym.readyPromise].promise; - } - - abort(reason) { - if (!isWritableStreamDefaultWriter(this)) { - return Promise.reject( - new TypeError("Invalid WritableStreamDefaultWriter."), - ); - } - if (!this[sym.ownerWritableStream]) { - Promise.reject( - new TypeError("WritableStreamDefaultWriter has no owner."), - ); - } - return writableStreamDefaultWriterAbort(this, reason); - } - - close() { - if (!isWritableStreamDefaultWriter(this)) { - return Promise.reject( - new TypeError("Invalid WritableStreamDefaultWriter."), - ); - } - const stream = this[sym.ownerWritableStream]; - if (!stream) { - Promise.reject( - new TypeError("WritableStreamDefaultWriter has no owner."), - ); - } - if (writableStreamCloseQueuedOrInFlight(stream)) { - Promise.reject( - new TypeError("Stream is in an invalid state to be closed."), - ); - } - return writableStreamDefaultWriterClose(this); - } - - releaseLock() { - if (!isWritableStreamDefaultWriter(this)) { - throw new TypeError("Invalid WritableStreamDefaultWriter."); - } - const stream = this[sym.ownerWritableStream]; - if (!stream) { - return; - } - assert(stream[sym.writer]); - writableStreamDefaultWriterRelease(this); - } - - write(chunk) { - if (!isWritableStreamDefaultWriter(this)) { - return Promise.reject( - new TypeError("Invalid WritableStreamDefaultWriter."), - ); - } - if (!this[sym.ownerWritableStream]) { - Promise.reject( - new TypeError("WritableStreamDefaultWriter has no owner."), - ); - } - return writableStreamDefaultWriterWrite(this, chunk); - } - - [customInspect]() { - return `${this.constructor.name} { closed: Promise, desiredSize: ${ - String(this.desiredSize) - }, ready: Promise }`; - } - } - - class WritableStream { - constructor( - underlyingSink = {}, - strategy = {}, - ) { - initializeWritableStream(this); - const size = strategy.size; - let highWaterMark = strategy.highWaterMark ?? 1; - const { type } = underlyingSink; - if (type !== undefined) { - throw new RangeError(`Sink type of "${String(type)}" not supported.`); - } - const sizeAlgorithm = makeSizeAlgorithmFromSizeFunction(size); - highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); - setUpWritableStreamDefaultControllerFromUnderlyingSink( - this, - underlyingSink, - highWaterMark, - sizeAlgorithm, - ); - } - - get locked() { - if (!isWritableStream(this)) { - throw new TypeError("Invalid WritableStream."); - } - return isWritableStreamLocked(this); - } - - abort(reason) { - if (!isWritableStream(this)) { - return Promise.reject(new TypeError("Invalid WritableStream.")); - } - if (isWritableStreamLocked(this)) { - return Promise.reject( - new TypeError("Cannot abort a locked WritableStream."), - ); - } - return writableStreamAbort(this, reason); - } - - close() { - if (!isWritableStream(this)) { - return Promise.reject(new TypeError("Invalid WritableStream.")); - } - if (isWritableStreamLocked(this)) { - return Promise.reject( - new TypeError("Cannot abort a locked WritableStream."), - ); - } - if (writableStreamCloseQueuedOrInFlight(this)) { - return Promise.reject( - new TypeError("Cannot close an already closing WritableStream."), - ); - } - return writableStreamClose(this); - } - - getWriter() { - if (!isWritableStream(this)) { - throw new TypeError("Invalid WritableStream."); - } - return acquireWritableStreamDefaultWriter(this); - } - - [customInspect]() { - return `${this.constructor.name} { locked: ${String(this.locked)} }`; - } - } - - function acquireReadableStreamDefaultReader( - stream, - forAuthorCode = false, - ) { - const reader = new ReadableStreamDefaultReader(stream); - reader[sym.forAuthorCode] = forAuthorCode; - return reader; - } - - function acquireWritableStreamDefaultWriter( - stream, - ) { - return new WritableStreamDefaultWriter(stream); - } - - function call( - fn, - v, - args, - ) { - return Function.prototype.apply.call(fn, v, args); - } - - function createAlgorithmFromUnderlyingMethod( - underlyingObject, - methodName, - algoArgCount, - ...extraArgs - ) { - const method = underlyingObject[methodName]; - if (method) { - if (!isCallable(method)) { - throw new TypeError("method is not callable"); - } - if (algoArgCount === 0) { - return async () => call(method, underlyingObject, extraArgs); - } else { - return async (arg) => { - const fullArgs = [arg, ...extraArgs]; - return call(method, underlyingObject, fullArgs); - }; - } - } - return async () => undefined; - } - - function createReadableStream( - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - highWaterMark = 1, - sizeAlgorithm = () => 1, - ) { - highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); - const stream = Object.create( - ReadableStream.prototype, - ); - initializeReadableStream(stream); - const controller = Object.create( - ReadableStreamDefaultController.prototype, - ); - setUpReadableStreamDefaultController( - stream, - controller, - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - highWaterMark, - sizeAlgorithm, - ); - return stream; - } - - function createWritableStream( - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark = 1, - sizeAlgorithm = () => 1, - ) { - highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); - const stream = Object.create(WritableStream.prototype); - initializeWritableStream(stream); - const controller = Object.create( - WritableStreamDefaultController.prototype, - ); - setUpWritableStreamDefaultController( - stream, - controller, - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, - ); - return stream; - } - - function dequeueValue(container) { - assert(sym.queue in container && sym.queueTotalSize in container); - assert(container[sym.queue].length); - const pair = container[sym.queue].shift(); - container[sym.queueTotalSize] -= pair.size; - if (container[sym.queueTotalSize] <= 0) { - container[sym.queueTotalSize] = 0; - } - return pair.value; - } - - function enqueueValueWithSize( - container, - value, - size, - ) { - assert(sym.queue in container && sym.queueTotalSize in container); - size = Number(size); - if (!isFiniteNonNegativeNumber(size)) { - throw new RangeError("size must be a finite non-negative number."); - } - container[sym.queue].push({ value, size }); - container[sym.queueTotalSize] += size; - } - - /** Non-spec mechanism to "unwrap" a promise and store it to be resolved - * later. */ - function getDeferred() { - let resolve; - let reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve: resolve, reject: reject }; - } - - function initializeReadableStream( - stream, - ) { - stream[sym.state] = "readable"; - stream[sym.reader] = stream[sym.storedError] = undefined; - stream[sym.disturbed] = false; - } - - function initializeTransformStream( - stream, - startPromise, - writableHighWaterMark, - writableSizeAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, - ) { - const startAlgorithm = () => startPromise; - const writeAlgorithm = (chunk) => - transformStreamDefaultSinkWriteAlgorithm(stream, chunk); - const abortAlgorithm = (reason) => - transformStreamDefaultSinkAbortAlgorithm(stream, reason); - const closeAlgorithm = () => - transformStreamDefaultSinkCloseAlgorithm(stream); - stream[sym.writable] = createWritableStream( - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - writableHighWaterMark, - writableSizeAlgorithm, - ); - const pullAlgorithm = () => - transformStreamDefaultSourcePullAlgorithm(stream); - const cancelAlgorithm = (reason) => { - transformStreamErrorWritableAndUnblockWrite(stream, reason); - return Promise.resolve(undefined); - }; - stream[sym.readable] = createReadableStream( - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, - ); - stream[sym.backpressure] = stream[sym.backpressureChangePromise] = - undefined; - transformStreamSetBackpressure(stream, true); - Object.defineProperty(stream, sym.transformStreamController, { - value: undefined, - configurable: true, - }); - } - - function initializeWritableStream( - stream, - ) { - stream[sym.state] = "writable"; - stream[sym.storedError] = stream[sym.writer] = stream[ - sym.writableStreamController - ] = stream[sym.inFlightWriteRequest] = stream[sym.closeRequest] = stream[ - sym.inFlightCloseRequest - ] = stream[sym.pendingAbortRequest] = undefined; - stream[sym.writeRequests] = []; - stream[sym.backpressure] = false; - } - - function invokeOrNoop( - o, - p, - ...args - ) { - assert(o); - const method = o[p]; - if (!method) { - return undefined; - } - return call(method, o, args); - } - - function isCallable(value) { - return typeof value === "function"; - } - - function isDetachedBuffer(value) { - return sym.isFakeDetached in value; - } - - function isFiniteNonNegativeNumber(v) { - return Number.isFinite(v) && (v) >= 0; - } - - function isReadableByteStreamController( - x, - ) { - return !( - typeof x !== "object" || - x === null || - !(sym.controlledReadableByteStream in x) - ); - } - - function isReadableStream(x) { - return !( - typeof x !== "object" || - x === null || - !(sym.readableStreamController in x) - ); - } - - function isReadableStreamAsyncIterator( - x, - ) { - if (typeof x !== "object" || x === null) { - return false; - } - return sym.asyncIteratorReader in x; - } - - function isReadableStreamDefaultController( - x, - ) { - return !( - typeof x !== "object" || - x === null || - !(sym.controlledReadableStream in x) - ); - } - - function isReadableStreamDefaultReader( - x, - ) { - return !(typeof x !== "object" || x === null || !(sym.readRequests in x)); - } - - function isReadableStreamLocked(stream) { - assert(isReadableStream(stream)); - return !!stream[sym.reader]; - } - - function isReadableStreamDisturbed(stream) { - assert(isReadableStream(stream)); - return !!stream[sym.disturbed]; - } - - function isTransformStream(x) { - return !( - typeof x !== "object" || - x === null || - !(sym.transformStreamController in x) - ); - } - - function isTransformStreamDefaultController( - x, - ) { - return !( - typeof x !== "object" || - x === null || - !(sym.controlledTransformStream in x) - ); - } - - function isWritableStream(x) { - return !( - typeof x !== "object" || - x === null || - !(sym.writableStreamController in x) - ); - } - - function isWritableStreamDefaultController( - x, - ) { - return !( - typeof x !== "object" || - x === null || - !(sym.controlledWritableStream in x) - ); - } - - function isWritableStreamDefaultWriter( - x, - ) { - return !( - typeof x !== "object" || - x === null || - !(sym.ownerWritableStream in x) - ); - } - - function isWritableStreamLocked(stream) { - assert(isWritableStream(stream)); - return stream[sym.writer] !== undefined; - } - - function makeSizeAlgorithmFromSizeFunction( - size, - ) { - if (size === undefined) { - return () => 1; - } - if (typeof size !== "function") { - throw new TypeError("size must be callable."); - } - return (chunk) => { - return size.call(undefined, chunk); - }; - } - - function peekQueueValue(container) { - assert(sym.queue in container && sym.queueTotalSize in container); - assert(container[sym.queue].length); - const [pair] = container[sym.queue]; - return pair.value; - } - - function readableByteStreamControllerShouldCallPull( - controller, - ) { - const stream = controller[sym.controlledReadableByteStream]; - if ( - stream[sym.state] !== "readable" || - controller[sym.closeRequested] || - !controller[sym.started] - ) { - return false; - } - if ( - readableStreamHasDefaultReader(stream) && - readableStreamGetNumReadRequests(stream) > 0 - ) { - return true; - } - // 3.13.25.6 If ! ReadableStreamHasBYOBReader(stream) is true and ! - // ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. - const desiredSize = readableByteStreamControllerGetDesiredSize(controller); - assert(desiredSize !== null); - return desiredSize > 0; - } - - function readableByteStreamControllerCallPullIfNeeded( - controller, - ) { - const shouldPull = readableByteStreamControllerShouldCallPull(controller); - if (!shouldPull) { - return; - } - if (controller[sym.pulling]) { - controller[sym.pullAgain] = true; - return; - } - assert(controller[sym.pullAgain] === false); - controller[sym.pulling] = true; - const pullPromise = controller[sym.pullAlgorithm](); - setPromiseIsHandledToTrue( - pullPromise.then( - () => { - controller[sym.pulling] = false; - if (controller[sym.pullAgain]) { - controller[sym.pullAgain] = false; - readableByteStreamControllerCallPullIfNeeded(controller); - } - }, - (e) => { - readableByteStreamControllerError(controller, e); - }, - ), - ); - } - - function readableByteStreamControllerClearAlgorithms( - controller, - ) { - controller[sym.pullAlgorithm] = undefined; - controller[sym.cancelAlgorithm] = undefined; - } - - function readableByteStreamControllerClose( - controller, - ) { - const stream = controller[sym.controlledReadableByteStream]; - if (controller[sym.closeRequested] || stream[sym.state] !== "readable") { - return; - } - if (controller[sym.queueTotalSize] > 0) { - controller[sym.closeRequested] = true; - return; - } - // 3.13.6.4 If controller.[[pendingPullIntos]] is not empty, (BYOB Support) - readableByteStreamControllerClearAlgorithms(controller); - readableStreamClose(stream); - } - - function readableByteStreamControllerEnqueue( - controller, - chunk, - ) { - const stream = controller[sym.controlledReadableByteStream]; - if (controller[sym.closeRequested] || stream[sym.state] !== "readable") { - return; - } - const { buffer, byteOffset, byteLength } = chunk; - const transferredBuffer = transferArrayBuffer(buffer); - if (readableStreamHasDefaultReader(stream)) { - if (readableStreamGetNumReadRequests(stream) === 0) { - readableByteStreamControllerEnqueueChunkToQueue( - controller, - transferredBuffer, - byteOffset, - byteLength, - ); - } else { - assert(controller[sym.queue].length === 0); - const transferredView = new Uint8Array( - transferredBuffer, - byteOffset, - byteLength, - ); - readableStreamFulfillReadRequest(stream, transferredView, false); - } - // 3.13.9.8 Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true - } else { - assert(!isReadableStreamLocked(stream)); - readableByteStreamControllerEnqueueChunkToQueue( - controller, - transferredBuffer, - byteOffset, - byteLength, - ); - } - readableByteStreamControllerCallPullIfNeeded(controller); - } - - function readableByteStreamControllerEnqueueChunkToQueue( - controller, - buffer, - byteOffset, - byteLength, - ) { - controller[sym.queue].push({ - value: buffer, - offset: byteOffset, - size: byteLength, - }); - controller[sym.queueTotalSize] += byteLength; - } - - function readableByteStreamControllerError( - controller, - e, - ) { - const stream = controller[sym.controlledReadableByteStream]; - if (stream[sym.state] !== "readable") { - return; - } - // 3.13.11.3 Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). - resetQueue(controller); - readableByteStreamControllerClearAlgorithms(controller); - readableStreamError(stream, e); - } - - function readableByteStreamControllerGetDesiredSize( - controller, - ) { - const stream = controller[sym.controlledReadableByteStream]; - const state = stream[sym.state]; - if (state === "errored") { - return null; - } - if (state === "closed") { - return 0; - } - return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; - } - - function readableByteStreamControllerHandleQueueDrain( - controller, - ) { - assert( - controller[sym.controlledReadableByteStream][sym.state] === "readable", - ); - if ( - controller[sym.queueTotalSize] === 0 && controller[sym.closeRequested] - ) { - readableByteStreamControllerClearAlgorithms(controller); - readableStreamClose(controller[sym.controlledReadableByteStream]); - } else { - readableByteStreamControllerCallPullIfNeeded(controller); - } - } - - function readableStreamAddReadRequest( - stream, - ) { - assert(isReadableStreamDefaultReader(stream[sym.reader])); - assert(stream[sym.state] === "readable"); - const promise = getDeferred(); - stream[sym.reader][sym.readRequests].push(promise); - return promise.promise; - } - - function readableStreamCancel( - stream, - reason, - ) { - stream[sym.disturbed] = true; - if (stream[sym.state] === "closed") { - return Promise.resolve(); - } - if (stream[sym.state] === "errored") { - return Promise.reject(stream[sym.storedError]); - } - readableStreamClose(stream); - return stream[sym.readableStreamController][sym.cancelSteps](reason).then( - () => undefined, - ); - } - - function readableStreamClose(stream) { - assert(stream[sym.state] === "readable"); - stream[sym.state] = "closed"; - const reader = stream[sym.reader]; - if (!reader) { - return; - } - if (isReadableStreamDefaultReader(reader)) { - for (const readRequest of reader[sym.readRequests]) { - assert(readRequest.resolve); - readRequest.resolve( - readableStreamCreateReadResult( - undefined, - true, - reader[sym.forAuthorCode], - ), - ); - } - reader[sym.readRequests] = []; - } - const resolve = reader[sym.closedPromise].resolve; - assert(resolve); - resolve(); - } - - function readableStreamCreateReadResult( - value, - done, - forAuthorCode, - ) { - const prototype = forAuthorCode ? Object.prototype : null; - assert(typeof done === "boolean"); - const obj = Object.create(prototype); - Object.defineProperties(obj, { - value: { value, writable: true, enumerable: true, configurable: true }, - done: { - value: done, - writable: true, - enumerable: true, - configurable: true, - }, - }); - return obj; - } - - function readableStreamDefaultControllerCallPullIfNeeded( - controller, - ) { - const shouldPull = readableStreamDefaultControllerShouldCallPull( - controller, - ); - if (!shouldPull) { - return; - } - if (controller[sym.pulling]) { - controller[sym.pullAgain] = true; - return; - } - assert(controller[sym.pullAgain] === false); - controller[sym.pulling] = true; - const pullPromise = controller[sym.pullAlgorithm](); - pullPromise.then( - () => { - controller[sym.pulling] = false; - if (controller[sym.pullAgain]) { - controller[sym.pullAgain] = false; - readableStreamDefaultControllerCallPullIfNeeded(controller); - } - }, - (e) => { - readableStreamDefaultControllerError(controller, e); - }, - ); - } - - function readableStreamDefaultControllerCanCloseOrEnqueue( - controller, - ) { - const state = controller[sym.controlledReadableStream][sym.state]; - return !controller[sym.closeRequested] && state === "readable"; - } - - function readableStreamDefaultControllerClearAlgorithms( - controller, - ) { - controller[sym.pullAlgorithm] = undefined; - controller[sym.cancelAlgorithm] = undefined; - controller[sym.strategySizeAlgorithm] = undefined; - } - - function readableStreamDefaultControllerClose( - controller, - ) { - if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { - return; - } - const stream = controller[sym.controlledReadableStream]; - controller[sym.closeRequested] = true; - if (controller[sym.queue].length === 0) { - readableStreamDefaultControllerClearAlgorithms(controller); - readableStreamClose(stream); - } - } - - function readableStreamDefaultControllerEnqueue( - controller, - chunk, - ) { - if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { - return; - } - const stream = controller[sym.controlledReadableStream]; - if ( - isReadableStreamLocked(stream) && - readableStreamGetNumReadRequests(stream) > 0 - ) { - readableStreamFulfillReadRequest(stream, chunk, false); - } else { - try { - const chunkSize = controller[sym.strategySizeAlgorithm](chunk); - enqueueValueWithSize(controller, chunk, chunkSize); - } catch (err) { - readableStreamDefaultControllerError(controller, err); - throw err; - } - } - readableStreamDefaultControllerCallPullIfNeeded(controller); - } - - function readableStreamDefaultControllerGetDesiredSize( - controller, - ) { - const stream = controller[sym.controlledReadableStream]; - const state = stream[sym.state]; - if (state === "errored") { - return null; - } - if (state === "closed") { - return 0; - } - return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; - } - - function readableStreamDefaultControllerError( - controller, - e, - ) { - const stream = controller[sym.controlledReadableStream]; - if (stream[sym.state] !== "readable") { - return; - } - resetQueue(controller); - readableStreamDefaultControllerClearAlgorithms(controller); - readableStreamError(stream, e); - } - - function readableStreamDefaultControllerHasBackpressure( - controller, - ) { - return readableStreamDefaultControllerShouldCallPull(controller); - } - - function readableStreamDefaultControllerShouldCallPull( - controller, - ) { - const stream = controller[sym.controlledReadableStream]; - if ( - !readableStreamDefaultControllerCanCloseOrEnqueue(controller) || - controller[sym.started] === false - ) { - return false; - } - if ( - isReadableStreamLocked(stream) && - readableStreamGetNumReadRequests(stream) > 0 - ) { - return true; - } - const desiredSize = readableStreamDefaultControllerGetDesiredSize( - controller, - ); - assert(desiredSize !== null); - return desiredSize > 0; - } - - function readableStreamDefaultReaderRead( - reader, - ) { - const stream = reader[sym.ownerReadableStream]; - assert(stream); - stream[sym.disturbed] = true; - if (stream[sym.state] === "closed") { - return Promise.resolve( - readableStreamCreateReadResult( - undefined, - true, - reader[sym.forAuthorCode], - ), - ); - } - if (stream[sym.state] === "errored") { - return Promise.reject(stream[sym.storedError]); - } - assert(stream[sym.state] === "readable"); - return (stream[ - sym.readableStreamController - ])[sym.pullSteps](); - } - - function readableStreamError(stream, e) { - assert(isReadableStream(stream)); - assert(stream[sym.state] === "readable"); - stream[sym.state] = "errored"; - stream[sym.storedError] = e; - const reader = stream[sym.reader]; - if (reader === undefined) { - return; - } - if (isReadableStreamDefaultReader(reader)) { - for (const readRequest of reader[sym.readRequests]) { - assert(readRequest.reject); - readRequest.reject(e); - readRequest.reject = undefined; - readRequest.resolve = undefined; - } - reader[sym.readRequests] = []; - } - // 3.5.6.8 Otherwise, support BYOB Reader - reader[sym.closedPromise].reject(e); - reader[sym.closedPromise].reject = undefined; - reader[sym.closedPromise].resolve = undefined; - setPromiseIsHandledToTrue(reader[sym.closedPromise].promise); - } - - function readableStreamFulfillReadRequest( - stream, - chunk, - done, - ) { - const reader = stream[sym.reader]; - const readRequest = reader[sym.readRequests].shift(); - assert(readRequest.resolve); - readRequest.resolve( - readableStreamCreateReadResult(chunk, done, reader[sym.forAuthorCode]), - ); - } - - function readableStreamGetNumReadRequests( - stream, - ) { - return stream[sym.reader]?.[sym.readRequests].length ?? 0; - } - - function readableStreamHasDefaultReader( - stream, - ) { - const reader = stream[sym.reader]; - return !(reader === undefined || !isReadableStreamDefaultReader(reader)); - } - - function readableStreamPipeTo( - source, - dest, - preventClose, - preventAbort, - preventCancel, - signal, - ) { - assert(isReadableStream(source)); - assert(isWritableStream(dest)); - assert( - typeof preventClose === "boolean" && - typeof preventAbort === "boolean" && - typeof preventCancel === "boolean", - ); - assert(signal === undefined || signal instanceof AbortSignal); - assert(!isReadableStreamLocked(source)); - assert(!isWritableStreamLocked(dest)); - const reader = acquireReadableStreamDefaultReader(source); - const writer = acquireWritableStreamDefaultWriter(dest); - source[sym.disturbed] = true; - let shuttingDown = false; - const promise = getDeferred(); - let abortAlgorithm; - if (signal) { - abortAlgorithm = () => { - const error = new DOMException("Abort signal received.", "AbortSignal"); - const actions = []; - if (!preventAbort) { - actions.push(() => { - if (dest[sym.state] === "writable") { - return writableStreamAbort(dest, error); - } else { - return Promise.resolve(undefined); - } - }); - } - if (!preventCancel) { - actions.push(() => { - if (source[sym.state] === "readable") { - return readableStreamCancel(source, error); - } else { - return Promise.resolve(undefined); - } - }); - } - shutdownWithAction( - () => Promise.all(actions.map((action) => action())), - true, - error, - ); - }; - if (signal.aborted) { - abortAlgorithm(); - return promise.promise; - } - signal.addEventListener("abort", abortAlgorithm); - } - - let currentWrite = Promise.resolve(); - - // At this point, the spec becomes non-specific and vague. Most of the rest - // of this code is based on the reference implementation that is part of the - // specification. This is why the functions are only scoped to this function - // to ensure they don't leak into the spec compliant parts. - - function isOrBecomesClosed( - stream, - promise, - action, - ) { - if (stream[sym.state] === "closed") { - action(); - } else { - setPromiseIsHandledToTrue(promise.then(action)); - } - } - - function isOrBecomesErrored( - stream, - promise, - action, - ) { - if (stream[sym.state] === "errored") { - action(stream[sym.storedError]); - } else { - setPromiseIsHandledToTrue(promise.catch((error) => action(error))); - } - } - - function finalize(isError, error) { - writableStreamDefaultWriterRelease(writer); - readableStreamReaderGenericRelease(reader); - - if (signal) { - signal.removeEventListener("abort", abortAlgorithm); - } - if (isError) { - promise.reject(error); - } else { - promise.resolve(); - } - } - - function waitForWritesToFinish() { - const oldCurrentWrite = currentWrite; - return currentWrite.then(() => - oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined - ); - } - - function shutdownWithAction( - action, - originalIsError, - originalError, - ) { - function doTheRest() { - setPromiseIsHandledToTrue( - action().then( - () => finalize(originalIsError, originalError), - (newError) => finalize(true, newError), - ), - ); - } - - if (shuttingDown) { - return; - } - shuttingDown = true; - - if ( - dest[sym.state] === "writable" && - writableStreamCloseQueuedOrInFlight(dest) === false - ) { - setPromiseIsHandledToTrue(waitForWritesToFinish().then(doTheRest)); - } else { - doTheRest(); - } - } - - function shutdown(isError, error) { - if (shuttingDown) { - return; - } - shuttingDown = true; - - if ( - dest[sym.state] === "writable" && - !writableStreamCloseQueuedOrInFlight(dest) - ) { - setPromiseIsHandledToTrue( - waitForWritesToFinish().then(() => finalize(isError, error)), - ); - } - finalize(isError, error); - } - - function pipeStep() { - if (shuttingDown) { - return Promise.resolve(true); - } - return writer[sym.readyPromise].promise.then(() => { - return readableStreamDefaultReaderRead(reader).then( - ({ value, done }) => { - if (done === true) { - return true; - } - currentWrite = writableStreamDefaultWriterWrite( - writer, - value, - ).then(undefined, () => {}); - return false; - }, - ); - }); - } - - function pipeLoop() { - return new Promise((resolveLoop, rejectLoop) => { - function next(done) { - if (done) { - resolveLoop(undefined); - } else { - setPromiseIsHandledToTrue(pipeStep().then(next, rejectLoop)); - } - } - next(false); - }); - } - - isOrBecomesErrored( - source, - reader[sym.closedPromise].promise, - (storedError) => { - if (!preventAbort) { - shutdownWithAction( - () => writableStreamAbort(dest, storedError), - true, - storedError, - ); - } else { - shutdown(true, storedError); - } - }, - ); - - isOrBecomesErrored( - dest, - writer[sym.closedPromise].promise, - (storedError) => { - if (!preventCancel) { - shutdownWithAction( - () => readableStreamCancel(source, storedError), - true, - storedError, - ); - } else { - shutdown(true, storedError); - } - }, - ); - - isOrBecomesClosed(source, reader[sym.closedPromise].promise, () => { - if (!preventClose) { - shutdownWithAction(() => - writableStreamDefaultWriterCloseWithErrorPropagation(writer) - ); - } - }); - - if ( - writableStreamCloseQueuedOrInFlight(dest) || - dest[sym.state] === "closed" - ) { - const destClosed = new TypeError( - "The destination writable stream closed before all data could be piped to it.", - ); - if (!preventCancel) { - shutdownWithAction( - () => readableStreamCancel(source, destClosed), - true, - destClosed, - ); - } else { - shutdown(true, destClosed); - } - } - - setPromiseIsHandledToTrue(pipeLoop()); - return promise.promise; - } - - function readableStreamReaderGenericCancel( - reader, - reason, - ) { - const stream = reader[sym.ownerReadableStream]; - assert(stream); - return readableStreamCancel(stream, reason); - } - - function readableStreamReaderGenericInitialize( - reader, - stream, - ) { - reader[sym.forAuthorCode] = true; - reader[sym.ownerReadableStream] = stream; - stream[sym.reader] = reader; - if (stream[sym.state] === "readable") { - reader[sym.closedPromise] = getDeferred(); - } else if (stream[sym.state] === "closed") { - reader[sym.closedPromise] = { promise: Promise.resolve() }; - } else { - assert(stream[sym.state] === "errored"); - reader[sym.closedPromise] = { - promise: Promise.reject(stream[sym.storedError]), - }; - setPromiseIsHandledToTrue(reader[sym.closedPromise].promise); - } - } - - function readableStreamReaderGenericRelease( - reader, - ) { - assert(reader[sym.ownerReadableStream]); - assert(reader[sym.ownerReadableStream][sym.reader] === reader); - const closedPromise = reader[sym.closedPromise]; - if (reader[sym.ownerReadableStream][sym.state] === "readable") { - assert(closedPromise.reject); - closedPromise.reject(new TypeError("ReadableStream state is readable.")); - } else { - closedPromise.promise = Promise.reject( - new TypeError("Reading is closed."), - ); - delete closedPromise.reject; - delete closedPromise.resolve; - } - setPromiseIsHandledToTrue(closedPromise.promise); - reader[sym.ownerReadableStream][sym.reader] = undefined; - reader[sym.ownerReadableStream] = undefined; - } - - function readableStreamTee( - stream, - cloneForBranch2, - ) { - assert(isReadableStream(stream)); - assert(typeof cloneForBranch2 === "boolean"); - const reader = acquireReadableStreamDefaultReader(stream); - let reading = false; - let canceled1 = false; - let canceled2 = false; - let reason1 = undefined; - let reason2 = undefined; - /* eslint-disable prefer-const */ - let branch1; - let branch2; - /* eslint-enable prefer-const */ - const cancelPromise = getDeferred(); - const pullAlgorithm = () => { - if (reading) { - return Promise.resolve(); - } - reading = true; - const readPromise = readableStreamDefaultReaderRead(reader).then( - (result) => { - reading = false; - assert(typeof result === "object"); - const { done } = result; - assert(typeof done === "boolean"); - if (done) { - if (!canceled1) { - readableStreamDefaultControllerClose( - branch1[ - sym.readableStreamController - ], - ); - } - if (!canceled2) { - readableStreamDefaultControllerClose( - branch2[ - sym.readableStreamController - ], - ); - } - return; - } - const { value } = result; - const value1 = value; - let value2 = value; - if (!canceled2 && cloneForBranch2) { - value2 = cloneValue(value2); - } - if (!canceled1) { - readableStreamDefaultControllerEnqueue( - branch1[ - sym.readableStreamController - ], - value1, - ); - } - if (!canceled2) { - readableStreamDefaultControllerEnqueue( - branch2[ - sym.readableStreamController - ], - value2, - ); - } - }, - ); - setPromiseIsHandledToTrue(readPromise); - return Promise.resolve(); - }; - const cancel1Algorithm = (reason) => { - canceled1 = true; - reason1 = reason; - if (canceled2) { - const compositeReason = [reason1, reason2]; - const cancelResult = readableStreamCancel(stream, compositeReason); - cancelPromise.resolve(cancelResult); - } - return cancelPromise.promise; - }; - const cancel2Algorithm = (reason) => { - canceled2 = true; - reason2 = reason; - if (canceled1) { - const compositeReason = [reason1, reason2]; - const cancelResult = readableStreamCancel(stream, compositeReason); - cancelPromise.resolve(cancelResult); - } - return cancelPromise.promise; - }; - const startAlgorithm = () => undefined; - branch1 = createReadableStream( - startAlgorithm, - pullAlgorithm, - cancel1Algorithm, - ); - branch2 = createReadableStream( - startAlgorithm, - pullAlgorithm, - cancel2Algorithm, - ); - setPromiseIsHandledToTrue( - reader[sym.closedPromise].promise.catch((r) => { - readableStreamDefaultControllerError( - branch1[ - sym.readableStreamController - ], - r, - ); - readableStreamDefaultControllerError( - branch2[ - sym.readableStreamController - ], - r, - ); - }), - ); - return [branch1, branch2]; - } - - function resetQueue(container) { - assert(sym.queue in container && sym.queueTotalSize in container); - container[sym.queue] = []; - container[sym.queueTotalSize] = 0; - } - - /** An internal function which mimics the behavior of setting the promise to - * handled in JavaScript. In this situation, an assertion failure, which - * shouldn't happen will get thrown, instead of swallowed. */ - function setPromiseIsHandledToTrue(promise) { - promise.then(undefined, (e) => { - if (e && e instanceof AssertionError) { - queueMicrotask(() => { - throw e; - }); - } - }); - } - - function setUpReadableByteStreamController( - stream, - controller, - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - highWaterMark, - autoAllocateChunkSize, - ) { - assert(stream[sym.readableStreamController] === undefined); - if (autoAllocateChunkSize !== undefined) { - assert(Number.isInteger(autoAllocateChunkSize)); - assert(autoAllocateChunkSize >= 0); - } - controller[sym.controlledReadableByteStream] = stream; - controller[sym.pulling] = controller[sym.pullAgain] = false; - controller[sym.byobRequest] = undefined; - controller[sym.queue] = []; - controller[sym.queueTotalSize] = 0; - controller[sym.closeRequested] = controller[sym.started] = false; - controller[sym.strategyHWM] = validateAndNormalizeHighWaterMark( - highWaterMark, - ); - controller[sym.pullAlgorithm] = pullAlgorithm; - controller[sym.cancelAlgorithm] = cancelAlgorithm; - controller[sym.autoAllocateChunkSize] = autoAllocateChunkSize; - // 3.13.26.12 Set controller.[[pendingPullIntos]] to a new empty List. - stream[sym.readableStreamController] = controller; - const startResult = startAlgorithm(); - const startPromise = Promise.resolve(startResult); - setPromiseIsHandledToTrue( - startPromise.then( - () => { - controller[sym.started] = true; - assert(!controller[sym.pulling]); - assert(!controller[sym.pullAgain]); - readableByteStreamControllerCallPullIfNeeded(controller); - }, - (r) => { - readableByteStreamControllerError(controller, r); - }, - ), - ); - } - - function setUpReadableByteStreamControllerFromUnderlyingSource( - stream, - underlyingByteSource, - highWaterMark, - ) { - assert(underlyingByteSource); - const controller = Object.create( - ReadableByteStreamController.prototype, - ); - const startAlgorithm = () => { - return invokeOrNoop(underlyingByteSource, "start", controller); - }; - const pullAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingByteSource, - "pull", - 0, - controller, - ); - setFunctionName(pullAlgorithm, "[[pullAlgorithm]]"); - const cancelAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingByteSource, - "cancel", - 1, - ); - setFunctionName(cancelAlgorithm, "[[cancelAlgorithm]]"); - // 3.13.27.6 Let autoAllocateChunkSize be ? GetV(underlyingByteSource, "autoAllocateChunkSize"). - const autoAllocateChunkSize = undefined; - setUpReadableByteStreamController( - stream, - controller, - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - highWaterMark, - autoAllocateChunkSize, - ); - } - - function setUpReadableStreamDefaultController( - stream, - controller, - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - highWaterMark, - sizeAlgorithm, - ) { - assert(stream[sym.readableStreamController] === undefined); - controller[sym.controlledReadableStream] = stream; - controller[sym.queue] = []; - controller[sym.queueTotalSize] = 0; - controller[sym.started] = controller[sym.closeRequested] = controller[ - sym.pullAgain - ] = controller[sym.pulling] = false; - controller[sym.strategySizeAlgorithm] = sizeAlgorithm; - controller[sym.strategyHWM] = highWaterMark; - controller[sym.pullAlgorithm] = pullAlgorithm; - controller[sym.cancelAlgorithm] = cancelAlgorithm; - stream[sym.readableStreamController] = controller; - const startResult = startAlgorithm(); - const startPromise = Promise.resolve(startResult); - setPromiseIsHandledToTrue( - startPromise.then( - () => { - controller[sym.started] = true; - assert(controller[sym.pulling] === false); - assert(controller[sym.pullAgain] === false); - readableStreamDefaultControllerCallPullIfNeeded(controller); - }, - (r) => { - readableStreamDefaultControllerError(controller, r); - }, - ), - ); - } - - function setUpReadableStreamDefaultControllerFromUnderlyingSource( - stream, - underlyingSource, - highWaterMark, - sizeAlgorithm, - ) { - assert(underlyingSource); - const controller = Object.create( - ReadableStreamDefaultController.prototype, - ); - const startAlgorithm = () => - invokeOrNoop(underlyingSource, "start", controller); - const pullAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingSource, - "pull", - 0, - controller, - ); - setFunctionName(pullAlgorithm, "[[pullAlgorithm]]"); - const cancelAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingSource, - "cancel", - 1, - ); - setFunctionName(cancelAlgorithm, "[[cancelAlgorithm]]"); - setUpReadableStreamDefaultController( - stream, - controller, - startAlgorithm, - pullAlgorithm, - cancelAlgorithm, - highWaterMark, - sizeAlgorithm, - ); - } - - function setUpTransformStreamDefaultController( - stream, - controller, - transformAlgorithm, - flushAlgorithm, - ) { - assert(isTransformStream(stream)); - assert(stream[sym.transformStreamController] === undefined); - controller[sym.controlledTransformStream] = stream; - stream[sym.transformStreamController] = controller; - controller[sym.transformAlgorithm] = transformAlgorithm; - controller[sym.flushAlgorithm] = flushAlgorithm; - } - - function setUpTransformStreamDefaultControllerFromTransformer( - stream, - transformer, - ) { - assert(transformer); - const controller = Object.create( - TransformStreamDefaultController.prototype, - ); - let transformAlgorithm = (chunk) => { - try { - transformStreamDefaultControllerEnqueue( - controller, - // it defaults to no tranformation, so I is assumed to be O - chunk, - ); - } catch (e) { - return Promise.reject(e); - } - return Promise.resolve(); - }; - const transformMethod = transformer.transform; - if (transformMethod) { - if (typeof transformMethod !== "function") { - throw new TypeError("tranformer.transform must be callable."); - } - transformAlgorithm = async (chunk) => - call(transformMethod, transformer, [chunk, controller]); - } - const flushAlgorithm = createAlgorithmFromUnderlyingMethod( - transformer, - "flush", - 0, - controller, - ); - setUpTransformStreamDefaultController( - stream, - controller, - transformAlgorithm, - flushAlgorithm, - ); - } - - function setUpWritableStreamDefaultController( - stream, - controller, - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, - ) { - assert(isWritableStream(stream)); - assert(stream[sym.writableStreamController] === undefined); - controller[sym.controlledWritableStream] = stream; - stream[sym.writableStreamController] = controller; - controller[sym.queue] = []; - controller[sym.queueTotalSize] = 0; - controller[sym.started] = false; - controller[sym.strategySizeAlgorithm] = sizeAlgorithm; - controller[sym.strategyHWM] = highWaterMark; - controller[sym.writeAlgorithm] = writeAlgorithm; - controller[sym.closeAlgorithm] = closeAlgorithm; - controller[sym.abortAlgorithm] = abortAlgorithm; - const backpressure = writableStreamDefaultControllerGetBackpressure( - controller, - ); - writableStreamUpdateBackpressure(stream, backpressure); - const startResult = startAlgorithm(); - const startPromise = Promise.resolve(startResult); - setPromiseIsHandledToTrue( - startPromise.then( - () => { - assert( - stream[sym.state] === "writable" || - stream[sym.state] === "erroring", - ); - controller[sym.started] = true; - writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - }, - (r) => { - assert( - stream[sym.state] === "writable" || - stream[sym.state] === "erroring", - ); - controller[sym.started] = true; - writableStreamDealWithRejection(stream, r); - }, - ), - ); - } - - function setUpWritableStreamDefaultControllerFromUnderlyingSink( - stream, - underlyingSink, - highWaterMark, - sizeAlgorithm, - ) { - assert(underlyingSink); - const controller = Object.create( - WritableStreamDefaultController.prototype, - ); - const startAlgorithm = () => { - return invokeOrNoop(underlyingSink, "start", controller); - }; - const writeAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingSink, - "write", - 1, - controller, - ); - setFunctionName(writeAlgorithm, "[[writeAlgorithm]]"); - const closeAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingSink, - "close", - 0, - ); - setFunctionName(closeAlgorithm, "[[closeAlgorithm]]"); - const abortAlgorithm = createAlgorithmFromUnderlyingMethod( - underlyingSink, - "abort", - 1, - ); - setFunctionName(abortAlgorithm, "[[abortAlgorithm]]"); - setUpWritableStreamDefaultController( - stream, - controller, - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, - ); - } - - function transformStreamDefaultControllerClearAlgorithms( - controller, - ) { - controller[sym.transformAlgorithm] = undefined; - controller[sym.flushAlgorithm] = undefined; - } - - function transformStreamDefaultControllerEnqueue( - controller, - chunk, - ) { - const stream = controller[sym.controlledTransformStream]; - const readableController = stream[sym.readable][ - sym.readableStreamController - ]; - if (!readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) { - throw new TypeError( - "TransformStream's readable controller cannot be closed or enqueued.", - ); - } - try { - readableStreamDefaultControllerEnqueue(readableController, chunk); - } catch (e) { - transformStreamErrorWritableAndUnblockWrite(stream, e); - throw stream[sym.readable][sym.storedError]; - } - const backpressure = readableStreamDefaultControllerHasBackpressure( - readableController, - ); - if (backpressure) { - transformStreamSetBackpressure(stream, true); - } - } - - function transformStreamDefaultControllerError( - controller, - e, - ) { - transformStreamError(controller[sym.controlledTransformStream], e); - } - - function transformStreamDefaultControllerPerformTransform( - controller, - chunk, - ) { - const transformPromise = controller[sym.transformAlgorithm](chunk); - return transformPromise.then(undefined, (r) => { - transformStreamError(controller[sym.controlledTransformStream], r); - throw r; - }); - } - - function transformStreamDefaultSinkAbortAlgorithm( - stream, - reason, - ) { - transformStreamError(stream, reason); - return Promise.resolve(undefined); - } - - function transformStreamDefaultSinkCloseAlgorithm( - stream, - ) { - const readable = stream[sym.readable]; - const controller = stream[sym.transformStreamController]; - const flushPromise = controller[sym.flushAlgorithm](); - transformStreamDefaultControllerClearAlgorithms(controller); - return flushPromise.then( - () => { - if (readable[sym.state] === "errored") { - throw readable[sym.storedError]; - } - const readableController = readable[ - sym.readableStreamController - ]; - if ( - readableStreamDefaultControllerCanCloseOrEnqueue(readableController) - ) { - readableStreamDefaultControllerClose(readableController); - } - }, - (r) => { - transformStreamError(stream, r); - throw readable[sym.storedError]; - }, - ); - } - - function transformStreamDefaultSinkWriteAlgorithm( - stream, - chunk, - ) { - assert(stream[sym.writable][sym.state] === "writable"); - const controller = stream[sym.transformStreamController]; - if (stream[sym.backpressure]) { - const backpressureChangePromise = stream[sym.backpressureChangePromise]; - assert(backpressureChangePromise); - return backpressureChangePromise.promise.then(() => { - const writable = stream[sym.writable]; - const state = writable[sym.state]; - if (state === "erroring") { - throw writable[sym.storedError]; - } - assert(state === "writable"); - return transformStreamDefaultControllerPerformTransform( - controller, - chunk, - ); - }); - } - return transformStreamDefaultControllerPerformTransform(controller, chunk); - } - - function transformStreamDefaultSourcePullAlgorithm( - stream, - ) { - assert(stream[sym.backpressure] === true); - assert(stream[sym.backpressureChangePromise] !== undefined); - transformStreamSetBackpressure(stream, false); - return stream[sym.backpressureChangePromise].promise; - } - - function transformStreamError( - stream, - e, - ) { - readableStreamDefaultControllerError( - stream[sym.readable][ - sym.readableStreamController - ], - e, - ); - transformStreamErrorWritableAndUnblockWrite(stream, e); - } - - function transformStreamDefaultControllerTerminate( - controller, - ) { - const stream = controller[sym.controlledTransformStream]; - const readableController = stream[sym.readable][ - sym.readableStreamController - ]; - readableStreamDefaultControllerClose(readableController); - const error = new TypeError("TransformStream is closed."); - transformStreamErrorWritableAndUnblockWrite(stream, error); - } - - function transformStreamErrorWritableAndUnblockWrite( - stream, - e, - ) { - transformStreamDefaultControllerClearAlgorithms( - stream[sym.transformStreamController], - ); - writableStreamDefaultControllerErrorIfNeeded( - stream[sym.writable][sym.writableStreamController], - e, - ); - if (stream[sym.backpressure]) { - transformStreamSetBackpressure(stream, false); - } - } - - function transformStreamSetBackpressure( - stream, - backpressure, - ) { - assert(stream[sym.backpressure] !== backpressure); - if (stream[sym.backpressureChangePromise] !== undefined) { - stream[sym.backpressureChangePromise].resolve(undefined); - } - stream[sym.backpressureChangePromise] = getDeferred(); - stream[sym.backpressure] = backpressure; - } - - function transferArrayBuffer(buffer) { - assert(!isDetachedBuffer(buffer)); - const transferredIshVersion = buffer.slice(0); - - Object.defineProperty(buffer, "byteLength", { - get() { - return 0; - }, - }); - buffer[sym.isFakeDetached] = true; - - return transferredIshVersion; - } - - function validateAndNormalizeHighWaterMark( - highWaterMark, - ) { - highWaterMark = Number(highWaterMark); - if (Number.isNaN(highWaterMark) || highWaterMark < 0) { - throw new RangeError( - `highWaterMark must be a positive number or Infinity. Received: ${highWaterMark}.`, - ); - } - return highWaterMark; - } - - function writableStreamAbort( - stream, - reason, - ) { - const state = stream[sym.state]; - if (state === "closed" || state === "errored") { - return Promise.resolve(undefined); - } - if (stream[sym.pendingAbortRequest]) { - return stream[sym.pendingAbortRequest].promise.promise; - } - assert(state === "writable" || state === "erroring"); - let wasAlreadyErroring = false; - if (state === "erroring") { - wasAlreadyErroring = true; - reason = undefined; - } - const promise = getDeferred(); - stream[sym.pendingAbortRequest] = { promise, reason, wasAlreadyErroring }; - - if (wasAlreadyErroring === false) { - writableStreamStartErroring(stream, reason); - } - return promise.promise; - } - - function writableStreamAddWriteRequest( - stream, - ) { - assert(isWritableStream(stream)); - assert(stream[sym.state] === "writable"); - const promise = getDeferred(); - stream[sym.writeRequests].push(promise); - return promise.promise; - } - - function writableStreamClose( - stream, - ) { - const state = stream[sym.state]; - if (state === "closed" || state === "errored") { - return Promise.reject( - new TypeError( - "Cannot close an already closed or errored WritableStream.", - ), - ); - } - assert(!writableStreamCloseQueuedOrInFlight(stream)); - const promise = getDeferred(); - stream[sym.closeRequest] = promise; - const writer = stream[sym.writer]; - if (writer && stream[sym.backpressure] && state === "writable") { - writer[sym.readyPromise].resolve(); - writer[sym.readyPromise].resolve = undefined; - writer[sym.readyPromise].reject = undefined; - } - writableStreamDefaultControllerClose(stream[sym.writableStreamController]); - return promise.promise; - } - - function writableStreamCloseQueuedOrInFlight( - stream, - ) { - return !( - stream[sym.closeRequest] === undefined && - stream[sym.inFlightCloseRequest] === undefined - ); - } - - function writableStreamDealWithRejection( - stream, - error, - ) { - const state = stream[sym.state]; - if (state === "writable") { - writableStreamStartErroring(stream, error); - return; - } - assert(state === "erroring"); - writableStreamFinishErroring(stream); - } - - function writableStreamDefaultControllerAdvanceQueueIfNeeded( - controller, - ) { - const stream = controller[sym.controlledWritableStream]; - if (!controller[sym.started]) { - return; - } - if (stream[sym.inFlightWriteRequest]) { - return; - } - const state = stream[sym.state]; - assert(state !== "closed" && state !== "errored"); - if (state === "erroring") { - writableStreamFinishErroring(stream); - return; - } - if (!controller[sym.queue].length) { - return; - } - const writeRecord = peekQueueValue(controller); - if (writeRecord === "close") { - writableStreamDefaultControllerProcessClose(controller); - } else { - writableStreamDefaultControllerProcessWrite( - controller, - writeRecord.chunk, - ); - } - } - - function writableStreamDefaultControllerClearAlgorithms( - controller, - ) { - controller[sym.writeAlgorithm] = undefined; - controller[sym.closeAlgorithm] = undefined; - controller[sym.abortAlgorithm] = undefined; - controller[sym.strategySizeAlgorithm] = undefined; - } - - function writableStreamDefaultControllerClose( - controller, - ) { - enqueueValueWithSize(controller, "close", 0); - writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - } - - function writableStreamDefaultControllerError( - controller, - error, - ) { - const stream = controller[sym.controlledWritableStream]; - assert(stream[sym.state] === "writable"); - writableStreamDefaultControllerClearAlgorithms(controller); - writableStreamStartErroring(stream, error); - } - - function writableStreamDefaultControllerErrorIfNeeded( - controller, - error, - ) { - if (controller[sym.controlledWritableStream][sym.state] === "writable") { - writableStreamDefaultControllerError(controller, error); - } - } - - function writableStreamDefaultControllerGetBackpressure( - controller, - ) { - const desiredSize = writableStreamDefaultControllerGetDesiredSize( - controller, - ); - return desiredSize <= 0; - } - - function writableStreamDefaultControllerGetChunkSize( - controller, - chunk, - ) { - let returnValue; - try { - returnValue = controller[sym.strategySizeAlgorithm](chunk); - } catch (e) { - writableStreamDefaultControllerErrorIfNeeded(controller, e); - return 1; - } - return returnValue; - } - - function writableStreamDefaultControllerGetDesiredSize( - controller, - ) { - return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; - } - - function writableStreamDefaultControllerProcessClose( - controller, - ) { - const stream = controller[sym.controlledWritableStream]; - writableStreamMarkCloseRequestInFlight(stream); - dequeueValue(controller); - assert(controller[sym.queue].length === 0); - const sinkClosePromise = controller[sym.closeAlgorithm](); - writableStreamDefaultControllerClearAlgorithms(controller); - setPromiseIsHandledToTrue( - sinkClosePromise.then( - () => { - writableStreamFinishInFlightClose(stream); - }, - (reason) => { - writableStreamFinishInFlightCloseWithError(stream, reason); - }, - ), - ); - } - - function writableStreamDefaultControllerProcessWrite( - controller, - chunk, - ) { - const stream = controller[sym.controlledWritableStream]; - writableStreamMarkFirstWriteRequestInFlight(stream); - const sinkWritePromise = controller[sym.writeAlgorithm](chunk); - setPromiseIsHandledToTrue( - sinkWritePromise.then( - () => { - writableStreamFinishInFlightWrite(stream); - const state = stream[sym.state]; - assert(state === "writable" || state === "erroring"); - dequeueValue(controller); - if ( - !writableStreamCloseQueuedOrInFlight(stream) && - state === "writable" - ) { - const backpressure = writableStreamDefaultControllerGetBackpressure( - controller, - ); - writableStreamUpdateBackpressure(stream, backpressure); - } - writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - }, - (reason) => { - if (stream[sym.state] === "writable") { - writableStreamDefaultControllerClearAlgorithms(controller); - } - writableStreamFinishInFlightWriteWithError(stream, reason); - }, - ), - ); - } - - function writableStreamDefaultControllerWrite( - controller, - chunk, - chunkSize, - ) { - const writeRecord = { chunk }; - try { - enqueueValueWithSize(controller, writeRecord, chunkSize); - } catch (e) { - writableStreamDefaultControllerErrorIfNeeded(controller, e); - return; - } - const stream = controller[sym.controlledWritableStream]; - if ( - !writableStreamCloseQueuedOrInFlight(stream) && - stream[sym.state] === "writable" - ) { - const backpressure = writableStreamDefaultControllerGetBackpressure( - controller, - ); - writableStreamUpdateBackpressure(stream, backpressure); - } - writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - } - - function writableStreamDefaultWriterAbort( - writer, - reason, - ) { - const stream = writer[sym.ownerWritableStream]; - assert(stream); - return writableStreamAbort(stream, reason); - } - - function writableStreamDefaultWriterClose( - writer, - ) { - const stream = writer[sym.ownerWritableStream]; - assert(stream); - return writableStreamClose(stream); - } - - function writableStreamDefaultWriterCloseWithErrorPropagation( - writer, - ) { - const stream = writer[sym.ownerWritableStream]; - assert(stream); - const state = stream[sym.state]; - if (writableStreamCloseQueuedOrInFlight(stream) || state === "closed") { - return Promise.resolve(); - } - if (state === "errored") { - return Promise.reject(stream[sym.storedError]); - } - assert(state === "writable" || state === "erroring"); - return writableStreamDefaultWriterClose(writer); - } - - function writableStreamDefaultWriterEnsureClosePromiseRejected( - writer, - error, - ) { - if (writer[sym.closedPromise].reject) { - writer[sym.closedPromise].reject(error); - } else { - writer[sym.closedPromise] = { - promise: Promise.reject(error), - }; - } - setPromiseIsHandledToTrue(writer[sym.closedPromise].promise); - } - - function writableStreamDefaultWriterEnsureReadyPromiseRejected( - writer, - error, - ) { - if (writer[sym.readyPromise].reject) { - writer[sym.readyPromise].reject(error); - writer[sym.readyPromise].reject = undefined; - writer[sym.readyPromise].resolve = undefined; - } else { - writer[sym.readyPromise] = { - promise: Promise.reject(error), - }; - } - setPromiseIsHandledToTrue(writer[sym.readyPromise].promise); - } - - function writableStreamDefaultWriterWrite( - writer, - chunk, - ) { - const stream = writer[sym.ownerWritableStream]; - assert(stream); - const controller = stream[sym.writableStreamController]; - assert(controller); - const chunkSize = writableStreamDefaultControllerGetChunkSize( - controller, - chunk, - ); - if (stream !== writer[sym.ownerWritableStream]) { - return Promise.reject("Writer has incorrect WritableStream."); - } - const state = stream[sym.state]; - if (state === "errored") { - return Promise.reject(stream[sym.storedError]); - } - if (writableStreamCloseQueuedOrInFlight(stream) || state === "closed") { - return Promise.reject(new TypeError("The stream is closed or closing.")); - } - if (state === "erroring") { - return Promise.reject(stream[sym.storedError]); - } - assert(state === "writable"); - const promise = writableStreamAddWriteRequest(stream); - writableStreamDefaultControllerWrite(controller, chunk, chunkSize); - return promise; - } - - function writableStreamDefaultWriterGetDesiredSize( - writer, - ) { - const stream = writer[sym.ownerWritableStream]; - const state = stream[sym.state]; - if (state === "errored" || state === "erroring") { - return null; - } - if (state === "closed") { - return 0; - } - return writableStreamDefaultControllerGetDesiredSize( - stream[sym.writableStreamController], - ); - } - - function writableStreamDefaultWriterRelease( - writer, - ) { - const stream = writer[sym.ownerWritableStream]; - assert(stream); - assert(stream[sym.writer] === writer); - const releasedError = new TypeError( - "Writer was released and can no longer be used to monitor the stream's closedness.", - ); - writableStreamDefaultWriterEnsureReadyPromiseRejected( - writer, - releasedError, - ); - writableStreamDefaultWriterEnsureClosePromiseRejected( - writer, - releasedError, - ); - stream[sym.writer] = undefined; - writer[sym.ownerWritableStream] = undefined; - } - - function writableStreamFinishErroring(stream) { - assert(stream[sym.state] === "erroring"); - assert(!writableStreamHasOperationMarkedInFlight(stream)); - stream[sym.state] = "errored"; - stream[sym.writableStreamController][sym.errorSteps](); - const storedError = stream[sym.storedError]; - for (const writeRequest of stream[sym.writeRequests]) { - assert(writeRequest.reject); - writeRequest.reject(storedError); - } - stream[sym.writeRequests] = []; - if (!stream[sym.pendingAbortRequest]) { - writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - return; - } - const abortRequest = stream[sym.pendingAbortRequest]; - assert(abortRequest); - stream[sym.pendingAbortRequest] = undefined; - if (abortRequest.wasAlreadyErroring) { - assert(abortRequest.promise.reject); - abortRequest.promise.reject(storedError); - writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - return; - } - const promise = stream[sym.writableStreamController][sym.abortSteps]( - abortRequest.reason, - ); - setPromiseIsHandledToTrue( - promise.then( - () => { - assert(abortRequest.promise.resolve); - abortRequest.promise.resolve(); - writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - }, - (reason) => { - assert(abortRequest.promise.reject); - abortRequest.promise.reject(reason); - writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - }, - ), - ); - } - - function writableStreamFinishInFlightClose( - stream, - ) { - assert(stream[sym.inFlightCloseRequest]); - stream[sym.inFlightCloseRequest]?.resolve(); - stream[sym.inFlightCloseRequest] = undefined; - const state = stream[sym.state]; - assert(state === "writable" || state === "erroring"); - if (state === "erroring") { - stream[sym.storedError] = undefined; - if (stream[sym.pendingAbortRequest]) { - stream[sym.pendingAbortRequest].promise.resolve(); - stream[sym.pendingAbortRequest] = undefined; - } - } - stream[sym.state] = "closed"; - const writer = stream[sym.writer]; - if (writer) { - writer[sym.closedPromise].resolve(); - } - assert(stream[sym.pendingAbortRequest] === undefined); - assert(stream[sym.storedError] === undefined); - } - - function writableStreamFinishInFlightCloseWithError( - stream, - error, - ) { - assert(stream[sym.inFlightCloseRequest]); - stream[sym.inFlightCloseRequest]?.reject(error); - stream[sym.inFlightCloseRequest] = undefined; - assert( - stream[sym.state] === "writable" || stream[sym.state] === "erroring", - ); - if (stream[sym.pendingAbortRequest]) { - stream[sym.pendingAbortRequest]?.promise.reject(error); - stream[sym.pendingAbortRequest] = undefined; - } - writableStreamDealWithRejection(stream, error); - } - - function writableStreamFinishInFlightWrite( - stream, - ) { - assert(stream[sym.inFlightWriteRequest]); - stream[sym.inFlightWriteRequest].resolve(); - stream[sym.inFlightWriteRequest] = undefined; - } - - function writableStreamFinishInFlightWriteWithError( - stream, - error, - ) { - assert(stream[sym.inFlightWriteRequest]); - stream[sym.inFlightWriteRequest].reject(error); - stream[sym.inFlightWriteRequest] = undefined; - assert( - stream[sym.state] === "writable" || stream[sym.state] === "erroring", - ); - writableStreamDealWithRejection(stream, error); - } - - function writableStreamHasOperationMarkedInFlight( - stream, - ) { - return !( - stream[sym.inFlightWriteRequest] === undefined && - stream[sym.inFlightCloseRequest] === undefined - ); - } - - function writableStreamMarkCloseRequestInFlight( - stream, - ) { - assert(stream[sym.inFlightCloseRequest] === undefined); - assert(stream[sym.closeRequest] !== undefined); - stream[sym.inFlightCloseRequest] = stream[sym.closeRequest]; - stream[sym.closeRequest] = undefined; - } - - function writableStreamMarkFirstWriteRequestInFlight( - stream, - ) { - assert(stream[sym.inFlightWriteRequest] === undefined); - assert(stream[sym.writeRequests].length); - const writeRequest = stream[sym.writeRequests].shift(); - stream[sym.inFlightWriteRequest] = writeRequest; - } - - function writableStreamRejectCloseAndClosedPromiseIfNeeded( - stream, - ) { - assert(stream[sym.state] === "errored"); - if (stream[sym.closeRequest]) { - assert(stream[sym.inFlightCloseRequest] === undefined); - stream[sym.closeRequest].reject(stream[sym.storedError]); - stream[sym.closeRequest] = undefined; - } - const writer = stream[sym.writer]; - if (writer) { - writer[sym.closedPromise].reject(stream[sym.storedError]); - setPromiseIsHandledToTrue(writer[sym.closedPromise].promise); - } - } - - function writableStreamStartErroring( - stream, - reason, - ) { - assert(stream[sym.storedError] === undefined); - assert(stream[sym.state] === "writable"); - const controller = stream[sym.writableStreamController]; - assert(controller); - stream[sym.state] = "erroring"; - stream[sym.storedError] = reason; - const writer = stream[sym.writer]; - if (writer) { - writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); - } - if ( - !writableStreamHasOperationMarkedInFlight(stream) && - controller[sym.started] - ) { - writableStreamFinishErroring(stream); - } - } - - function writableStreamUpdateBackpressure( - stream, - backpressure, - ) { - assert(stream[sym.state] === "writable"); - assert(!writableStreamCloseQueuedOrInFlight(stream)); - const writer = stream[sym.writer]; - if (writer && backpressure !== stream[sym.backpressure]) { - if (backpressure) { - writer[sym.readyPromise] = getDeferred(); - } else { - assert(backpressure === false); - writer[sym.readyPromise].resolve(); - writer[sym.readyPromise].resolve = undefined; - writer[sym.readyPromise].reject = undefined; - } - } - stream[sym.backpressure] = backpressure; - } - /* eslint-enable */ - - window.__bootstrap.streams = { - ReadableStream, - TransformStream, - WritableStream, - isReadableStreamDisturbed, - }; -})(this); diff --git a/cli/rt/20_blob.js b/cli/rt/20_blob.js deleted file mode 100644 index f7309dafb..000000000 --- a/cli/rt/20_blob.js +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { build } = window.__bootstrap.build; - const { ReadableStream } = window.__bootstrap.streams; - - const bytesSymbol = Symbol("bytes"); - - function containsOnlyASCII(str) { - if (typeof str !== "string") { - return false; - } - return /^[\x00-\x7F]*$/.test(str); - } - - function convertLineEndingsToNative(s) { - const nativeLineEnd = build.os == "windows" ? "\r\n" : "\n"; - - let position = 0; - - let collectionResult = collectSequenceNotCRLF(s, position); - - let token = collectionResult.collected; - position = collectionResult.newPosition; - - let result = token; - - while (position < s.length) { - const c = s.charAt(position); - if (c == "\r") { - result += nativeLineEnd; - position++; - if (position < s.length && s.charAt(position) == "\n") { - position++; - } - } else if (c == "\n") { - position++; - result += nativeLineEnd; - } - - collectionResult = collectSequenceNotCRLF(s, position); - - token = collectionResult.collected; - position = collectionResult.newPosition; - - result += token; - } - - return result; - } - - function collectSequenceNotCRLF( - s, - position, - ) { - const start = position; - for ( - let c = s.charAt(position); - position < s.length && !(c == "\r" || c == "\n"); - c = s.charAt(++position) - ); - return { collected: s.slice(start, position), newPosition: position }; - } - - function toUint8Arrays( - blobParts, - doNormalizeLineEndingsToNative, - ) { - const ret = []; - const enc = new TextEncoder(); - for (const element of blobParts) { - if (typeof element === "string") { - let str = element; - if (doNormalizeLineEndingsToNative) { - str = convertLineEndingsToNative(element); - } - ret.push(enc.encode(str)); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - } else if (element instanceof Blob) { - ret.push(element[bytesSymbol]); - } else if (element instanceof Uint8Array) { - ret.push(element); - } else if (element instanceof Uint16Array) { - const uint8 = new Uint8Array(element.buffer); - ret.push(uint8); - } else if (element instanceof Uint32Array) { - const uint8 = new Uint8Array(element.buffer); - ret.push(uint8); - } else if (ArrayBuffer.isView(element)) { - // Convert view to Uint8Array. - const uint8 = new Uint8Array(element.buffer); - ret.push(uint8); - } else if (element instanceof ArrayBuffer) { - // Create a new Uint8Array view for the given ArrayBuffer. - const uint8 = new Uint8Array(element); - ret.push(uint8); - } else { - ret.push(enc.encode(String(element))); - } - } - return ret; - } - - function processBlobParts( - blobParts, - options, - ) { - const normalizeLineEndingsToNative = options.ending === "native"; - // ArrayBuffer.transfer is not yet implemented in V8, so we just have to - // pre compute size of the array buffer and do some sort of static allocation - // instead of dynamic allocation. - const uint8Arrays = toUint8Arrays(blobParts, normalizeLineEndingsToNative); - const byteLength = uint8Arrays - .map((u8) => u8.byteLength) - .reduce((a, b) => a + b, 0); - const ab = new ArrayBuffer(byteLength); - const bytes = new Uint8Array(ab); - let courser = 0; - for (const u8 of uint8Arrays) { - bytes.set(u8, courser); - courser += u8.byteLength; - } - - return bytes; - } - - function getStream(blobBytes) { - // TODO: Align to spec https://fetch.spec.whatwg.org/#concept-construct-readablestream - return new ReadableStream({ - type: "bytes", - start: (controller) => { - controller.enqueue(blobBytes); - controller.close(); - }, - }); - } - - async function readBytes( - reader, - ) { - const chunks = []; - while (true) { - const { done, value } = await reader.read(); - if (!done && value instanceof Uint8Array) { - chunks.push(value); - } else if (done) { - const size = chunks.reduce((p, i) => p + i.byteLength, 0); - const bytes = new Uint8Array(size); - let offs = 0; - for (const chunk of chunks) { - bytes.set(chunk, offs); - offs += chunk.byteLength; - } - return bytes.buffer; - } else { - throw new TypeError("Invalid reader result."); - } - } - } - - // A WeakMap holding blob to byte array mapping. - // Ensures it does not impact garbage collection. - const blobBytesWeakMap = new WeakMap(); - - class Blob { - constructor(blobParts, options) { - if (arguments.length === 0) { - this[bytesSymbol] = new Uint8Array(); - return; - } - - const { ending = "transparent", type = "" } = options ?? {}; - // Normalize options.type. - let normalizedType = type; - if (!containsOnlyASCII(type)) { - normalizedType = ""; - } else { - if (type.length) { - for (let i = 0; i < type.length; ++i) { - const char = type[i]; - if (char < "\u0020" || char > "\u007E") { - normalizedType = ""; - break; - } - } - normalizedType = type.toLowerCase(); - } - } - const bytes = processBlobParts(blobParts, { ending, type }); - // Set Blob object's properties. - this[bytesSymbol] = bytes; - this.size = bytes.byteLength; - this.type = normalizedType; - } - - slice(start, end, contentType) { - return new Blob([this[bytesSymbol].slice(start, end)], { - type: contentType || this.type, - }); - } - - stream() { - return getStream(this[bytesSymbol]); - } - - async text() { - const reader = getStream(this[bytesSymbol]).getReader(); - const decoder = new TextDecoder(); - return decoder.decode(await readBytes(reader)); - } - - arrayBuffer() { - return readBytes(getStream(this[bytesSymbol]).getReader()); - } - } - - window.__bootstrap.blob = { - Blob, - bytesSymbol, - containsOnlyASCII, - blobBytesWeakMap, - }; -})(this); diff --git a/cli/rt/20_headers.js b/cli/rt/20_headers.js deleted file mode 100644 index ccde77e8d..000000000 --- a/cli/rt/20_headers.js +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { DomIterableMixin } = window.__bootstrap.domIterable; - const { requiredArguments } = window.__bootstrap.webUtil; - - // From node-fetch - // Copyright (c) 2016 David Frank. MIT License. - const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; - const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; - - function isHeaders(value) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return value instanceof Headers; - } - - const headersData = Symbol("headers data"); - - // TODO: headerGuard? Investigate if it is needed - // node-fetch did not implement this but it is in the spec - function normalizeParams(name, value) { - name = String(name).toLowerCase(); - value = String(value).trim(); - return [name, value]; - } - - // The following name/value validations are copied from - // https://github.com/bitinn/node-fetch/blob/master/src/headers.js - // Copyright (c) 2016 David Frank. MIT License. - function validateName(name) { - if (invalidTokenRegex.test(name) || name === "") { - throw new TypeError(`${name} is not a legal HTTP header name`); - } - } - - function validateValue(value) { - if (invalidHeaderCharRegex.test(value)) { - throw new TypeError(`${value} is not a legal HTTP header value`); - } - } - - /** Appends a key and value to the header list. - * - * The spec indicates that when a key already exists, the append adds the new - * value onto the end of the existing value. The behaviour of this though - * varies when the key is `set-cookie`. In this case, if the key of the cookie - * already exists, the value is replaced, but if the key of the cookie does not - * exist, and additional `set-cookie` header is added. - * - * The browser specification of `Headers` is written for clients, and not - * servers, and Deno is a server, meaning that it needs to follow the patterns - * expected for servers, of which a `set-cookie` header is expected for each - * unique cookie key, but duplicate cookie keys should not exist. */ - function dataAppend( - data, - key, - value, - ) { - for (let i = 0; i < data.length; i++) { - const [dataKey] = data[i]; - if (key === "set-cookie" && dataKey === "set-cookie") { - const [, dataValue] = data[i]; - const [dataCookieKey] = dataValue.split("="); - const [cookieKey] = value.split("="); - if (dataCookieKey === cookieKey) { - data[i][1] = value; - return; - } - } else { - if (dataKey === key) { - data[i][1] += `, ${value}`; - return; - } - } - } - data.push([key, value]); - } - - /** Gets a value of a key in the headers list. - * - * This varies slightly from spec behaviour in that when the key is `set-cookie` - * the value returned will look like a concatenated value, when in fact, if the - * headers were iterated over, each individual `set-cookie` value is a unique - * entry in the headers list. */ - function dataGet( - data, - key, - ) { - const setCookieValues = []; - for (const [dataKey, value] of data) { - if (dataKey === key) { - if (key === "set-cookie") { - setCookieValues.push(value); - } else { - return value; - } - } - } - if (setCookieValues.length) { - return setCookieValues.join(", "); - } - return undefined; - } - - /** Sets a value of a key in the headers list. - * - * The spec indicates that the value should be replaced if the key already - * exists. The behaviour here varies, where if the key is `set-cookie` the key - * of the cookie is inspected, and if the key of the cookie already exists, - * then the value is replaced. If the key of the cookie is not found, then - * the value of the `set-cookie` is added to the list of headers. - * - * The browser specification of `Headers` is written for clients, and not - * servers, and Deno is a server, meaning that it needs to follow the patterns - * expected for servers, of which a `set-cookie` header is expected for each - * unique cookie key, but duplicate cookie keys should not exist. */ - function dataSet( - data, - key, - value, - ) { - for (let i = 0; i < data.length; i++) { - const [dataKey] = data[i]; - if (dataKey === key) { - // there could be multiple set-cookie headers, but all others are unique - if (key === "set-cookie") { - const [, dataValue] = data[i]; - const [dataCookieKey] = dataValue.split("="); - const [cookieKey] = value.split("="); - if (cookieKey === dataCookieKey) { - data[i][1] = value; - return; - } - } else { - data[i][1] = value; - return; - } - } - } - data.push([key, value]); - } - - function dataDelete(data, key) { - let i = 0; - while (i < data.length) { - const [dataKey] = data[i]; - if (dataKey === key) { - data.splice(i, 1); - } else { - i++; - } - } - } - - function dataHas(data, key) { - for (const [dataKey] of data) { - if (dataKey === key) { - return true; - } - } - return false; - } - - // ref: https://fetch.spec.whatwg.org/#dom-headers - class HeadersBase { - constructor(init) { - if (init === null) { - throw new TypeError( - "Failed to construct 'Headers'; The provided value was not valid", - ); - } else if (isHeaders(init)) { - this[headersData] = [...init]; - } else { - this[headersData] = []; - if (Array.isArray(init)) { - for (const tuple of init) { - // If header does not contain exactly two items, - // then throw a TypeError. - // ref: https://fetch.spec.whatwg.org/#concept-headers-fill - requiredArguments( - "Headers.constructor tuple array argument", - tuple.length, - 2, - ); - - this.append(tuple[0], tuple[1]); - } - } else if (init) { - for (const [rawName, rawValue] of Object.entries(init)) { - this.append(rawName, rawValue); - } - } - } - } - - [Symbol.for("Deno.customInspect")]() { - let length = this[headersData].length; - let output = ""; - for (const [key, value] of this[headersData]) { - const prefix = length === this[headersData].length ? " " : ""; - const postfix = length === 1 ? " " : ", "; - output = output + `${prefix}${key}: ${value}${postfix}`; - length--; - } - return `Headers {${output}}`; - } - - // ref: https://fetch.spec.whatwg.org/#concept-headers-append - append(name, value) { - requiredArguments("Headers.append", arguments.length, 2); - const [newname, newvalue] = normalizeParams(name, value); - validateName(newname); - validateValue(newvalue); - dataAppend(this[headersData], newname, newvalue); - } - - delete(name) { - requiredArguments("Headers.delete", arguments.length, 1); - const [newname] = normalizeParams(name); - validateName(newname); - dataDelete(this[headersData], newname); - } - - get(name) { - requiredArguments("Headers.get", arguments.length, 1); - const [newname] = normalizeParams(name); - validateName(newname); - return dataGet(this[headersData], newname) ?? null; - } - - has(name) { - requiredArguments("Headers.has", arguments.length, 1); - const [newname] = normalizeParams(name); - validateName(newname); - return dataHas(this[headersData], newname); - } - - set(name, value) { - requiredArguments("Headers.set", arguments.length, 2); - const [newname, newvalue] = normalizeParams(name, value); - validateName(newname); - validateValue(newvalue); - dataSet(this[headersData], newname, newvalue); - } - - get [Symbol.toStringTag]() { - return "Headers"; - } - } - - class Headers extends DomIterableMixin(HeadersBase, headersData) {} - - window.__bootstrap.headers = { - Headers, - }; -})(this); diff --git a/cli/rt/20_streams_queuing_strategy.js b/cli/rt/20_streams_queuing_strategy.js deleted file mode 100644 index af32c7d2e..000000000 --- a/cli/rt/20_streams_queuing_strategy.js +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const customInspect = Symbol.for("Deno.customInspect"); - - class CountQueuingStrategy { - constructor({ highWaterMark }) { - this.highWaterMark = highWaterMark; - } - - size() { - return 1; - } - - [customInspect]() { - return `${this.constructor.name} { highWaterMark: ${ - String(this.highWaterMark) - }, size: f }`; - } - } - - Object.defineProperty(CountQueuingStrategy.prototype, "size", { - enumerable: true, - }); - - class ByteLengthQueuingStrategy { - constructor({ highWaterMark }) { - this.highWaterMark = highWaterMark; - } - - size(chunk) { - return chunk.byteLength; - } - - [customInspect]() { - return `${this.constructor.name} { highWaterMark: ${ - String(this.highWaterMark) - }, size: f }`; - } - } - - Object.defineProperty(ByteLengthQueuingStrategy.prototype, "size", { - enumerable: true, - }); - - window.__bootstrap.queuingStrategy = { - CountQueuingStrategy, - ByteLengthQueuingStrategy, - }; -})(this); diff --git a/cli/rt/21_dom_file.js b/cli/rt/21_dom_file.js deleted file mode 100644 index 9d2f7fb6b..000000000 --- a/cli/rt/21_dom_file.js +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const blob = window.__bootstrap.blob; - - class DomFile extends blob.Blob { - constructor( - fileBits, - fileName, - options, - ) { - const { lastModified = Date.now(), ...blobPropertyBag } = options ?? {}; - super(fileBits, blobPropertyBag); - - // 4.1.2.1 Replace any "/" character (U+002F SOLIDUS) - // with a ":" (U + 003A COLON) - this.name = String(fileName).replace(/\u002F/g, "\u003A"); - // 4.1.3.3 If lastModified is not provided, set lastModified to the current - // date and time represented in number of milliseconds since the Unix Epoch. - this.lastModified = lastModified; - } - } - - window.__bootstrap.domFile = { - DomFile, - }; -})(this); diff --git a/cli/rt/21_filereader.js b/cli/rt/21_filereader.js deleted file mode 100644 index ea1ca3e5f..000000000 --- a/cli/rt/21_filereader.js +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const base64 = window.__bootstrap.base64; - const setTimeout = window.__bootstrap.timers.setTimeout; - - // ProgressEvent could also be used in other DOM progress event emits. - // Current use is for FileReader. - class ProgressEvent extends Event { - constructor(type, eventInitDict = {}) { - super(type, eventInitDict); - - this.lengthComputable = eventInitDict?.lengthComputable ?? false; - this.loaded = eventInitDict?.loaded ?? 0; - this.total = eventInitDict?.total ?? 0; - } - } - window.__bootstrap.progressEvent = { - ProgressEvent, - }; - - async function readOperation(fr, blob, readtype) { - // Implementation from https://w3c.github.io/FileAPI/ notes - // And body of deno blob.ts readBytes - - fr.aborting = false; - - // 1. If fr’s state is "loading", throw an InvalidStateError DOMException. - if (fr.readyState === FileReader.LOADING) { - throw new DOMException( - "Invalid FileReader state.", - "InvalidStateError", - ); - } - // 2. Set fr’s state to "loading". - fr.readyState = FileReader.LOADING; - // 3. Set fr’s result to null. - fr.result = null; - // 4. Set fr’s error to null. - fr.error = null; - - // 5. Let stream be the result of calling get stream on blob. - const stream /*: ReadableStream*/ = blob.stream(); - - // 6. Let reader be the result of getting a reader from stream. - const reader = stream.getReader(); - - // 7. Let bytes be an empty byte sequence. - //let bytes = new Uint8Array(); - const chunks /*: Uint8Array[]*/ = []; - - // 8. Let chunkPromise be the result of reading a chunk from stream with reader. - let chunkPromise = reader.read(); - - // 9. Let isFirstChunk be true. - let isFirstChunk = true; - - // 10 in parallel while true - while (!fr.aborting) { - // 1. Wait for chunkPromise to be fulfilled or rejected. - try { - const chunk = await chunkPromise; - - // 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr. - if (isFirstChunk) { - setTimeout(() => { - // fire a progress event for loadstart - const ev = new ProgressEvent("loadstart", {}); - fr.dispatchEvent(ev); - if (fr.onloadstart !== null) { - fr.onloadstart(ev); - } - }, 0); - } - // 3. Set isFirstChunk to false. - isFirstChunk = false; - - // 4. If chunkPromise is fulfilled with an object whose done property is false - // and whose value property is a Uint8Array object, run these steps: - if (!chunk.done && chunk.value instanceof Uint8Array) { - chunks.push(chunk.value); - - // TODO: (only) If roughly 50ms have passed since last progress - { - const size = chunks.reduce((p, i) => p + i.byteLength, 0); - const ev = new ProgressEvent("progress", { - loaded: size, - }); - fr.dispatchEvent(ev); - if (fr.onprogress !== null) { - fr.onprogress(ev); - } - } - - chunkPromise = reader.read(); - } // 5 Otherwise, if chunkPromise is fulfilled with an object whose done property is true, queue a task to run the following steps and abort this algorithm: - else if (chunk.done === true) { - setTimeout(() => { - if (fr.aborting) { - return; - } - - // 1. Set fr’s state to "done". - fr.readyState = FileReader.DONE; - // 2. Let result be the result of package data given bytes, type, blob’s type, and encodingName. - const size = chunks.reduce((p, i) => p + i.byteLength, 0); - const bytes = new Uint8Array(size); - let offs = 0; - for (const chunk of chunks) { - bytes.set(chunk, offs); - offs += chunk.byteLength; - } - switch (readtype.kind) { - case "ArrayBuffer": { - fr.result = bytes.buffer; - break; - } - case "Text": { - const decoder = new TextDecoder(readtype.encoding); - fr.result = decoder.decode(bytes.buffer); - break; - } - case "DataUrl": { - fr.result = "data:application/octet-stream;base64," + - base64.fromByteArray(bytes); - break; - } - } - // 4.2 Fire a progress event called load at the fr. - { - const ev = new ProgressEvent("load", { - lengthComputable: true, - loaded: size, - total: size, - }); - fr.dispatchEvent(ev); - if (fr.onload !== null) { - fr.onload(ev); - } - } - - // 5. If fr’s state is not "loading", fire a progress event called loadend at the fr. - //Note: Event handler for the load or error events could have started another load, if that happens the loadend event for this load is not fired. - if (fr.readyState !== FileReader.LOADING) { - const ev = new ProgressEvent("loadend", { - lengthComputable: true, - loaded: size, - total: size, - }); - fr.dispatchEvent(ev); - if (fr.onloadend !== null) { - fr.onloadend(ev); - } - } - }, 0); - - break; - } - } catch (err) { - if (fr.aborting) { - break; - } - - // chunkPromise rejected - fr.readyState = FileReader.DONE; - fr.error = err; - - { - const ev = new ProgressEvent("error", {}); - fr.dispatchEvent(ev); - if (fr.onerror !== null) { - fr.onerror(ev); - } - } - - //If fr’s state is not "loading", fire a progress event called loadend at fr. - //Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired. - if (fr.readyState !== FileReader.LOADING) { - const ev = new ProgressEvent("loadend", {}); - fr.dispatchEvent(ev); - if (fr.onloadend !== null) { - fr.onloadend(ev); - } - } - - break; - } - } - } - - class FileReader extends EventTarget { - error = null; - onabort = null; - onerror = null; - onload = null; - onloadend = null; - onloadstart = null; - onprogress = null; - - readyState = FileReader.EMPTY; - result = null; - aborting = false; - - constructor() { - super(); - } - - abort() { - // If context object's state is "empty" or if context object's state is "done" set context object's result to null and terminate this algorithm. - if ( - this.readyState === FileReader.EMPTY || - this.readyState === FileReader.DONE - ) { - this.result = null; - return; - } - // If context object's state is "loading" set context object's state to "done" and set context object's result to null. - if (this.readyState === FileReader.LOADING) { - this.readyState = FileReader.DONE; - this.result = null; - } - // If there are any tasks from the context object on the file reading task source in an affiliated task queue, then remove those tasks from that task queue. - // Terminate the algorithm for the read method being processed. - this.aborting = true; - - // Fire a progress event called abort at the context object. - const ev = new ProgressEvent("abort", {}); - this.dispatchEvent(ev); - if (this.onabort !== null) { - this.onabort(ev); - } - - // If context object's state is not "loading", fire a progress event called loadend at the context object. - if (this.readyState !== FileReader.LOADING) { - const ev = new ProgressEvent("loadend", {}); - this.dispatchEvent(ev); - if (this.onloadend !== null) { - this.onloadend(ev); - } - } - } - readAsArrayBuffer(blob) { - readOperation(this, blob, { kind: "ArrayBuffer" }); - } - readAsBinaryString(blob) { - // alias for readAsArrayBuffer - readOperation(this, blob, { kind: "ArrayBuffer" }); - } - readAsDataURL(blob) { - readOperation(this, blob, { kind: "DataUrl" }); - } - readAsText(blob, encoding) { - readOperation(this, blob, { kind: "Text", encoding }); - } - } - - FileReader.EMPTY = 0; - FileReader.LOADING = 1; - FileReader.DONE = 2; - - window.__bootstrap.fileReader = { - FileReader, - }; -})(this); diff --git a/cli/rt/22_form_data.js b/cli/rt/22_form_data.js deleted file mode 100644 index cc656d387..000000000 --- a/cli/rt/22_form_data.js +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const blob = window.__bootstrap.blob; - const domFile = window.__bootstrap.domFile; - const { DomIterableMixin } = window.__bootstrap.domIterable; - const { requiredArguments } = window.__bootstrap.webUtil; - - const dataSymbol = Symbol("data"); - - function parseFormDataValue(value, filename) { - if (value instanceof domFile.DomFile) { - return new domFile.DomFile([value], filename || value.name, { - type: value.type, - lastModified: value.lastModified, - }); - } else if (value instanceof blob.Blob) { - return new domFile.DomFile([value], filename || "blob", { - type: value.type, - }); - } else { - return String(value); - } - } - - class FormDataBase { - [dataSymbol] = []; - - append(name, value, filename) { - requiredArguments("FormData.append", arguments.length, 2); - name = String(name); - this[dataSymbol].push([name, parseFormDataValue(value, filename)]); - } - - delete(name) { - requiredArguments("FormData.delete", arguments.length, 1); - name = String(name); - let i = 0; - while (i < this[dataSymbol].length) { - if (this[dataSymbol][i][0] === name) { - this[dataSymbol].splice(i, 1); - } else { - i++; - } - } - } - - getAll(name) { - requiredArguments("FormData.getAll", arguments.length, 1); - name = String(name); - const values = []; - for (const entry of this[dataSymbol]) { - if (entry[0] === name) { - values.push(entry[1]); - } - } - - return values; - } - - get(name) { - requiredArguments("FormData.get", arguments.length, 1); - name = String(name); - for (const entry of this[dataSymbol]) { - if (entry[0] === name) { - return entry[1]; - } - } - - return null; - } - - has(name) { - requiredArguments("FormData.has", arguments.length, 1); - name = String(name); - return this[dataSymbol].some((entry) => entry[0] === name); - } - - set(name, value, filename) { - requiredArguments("FormData.set", arguments.length, 2); - name = String(name); - - // If there are any entries in the context object’s entry list whose name - // is name, replace the first such entry with entry and remove the others - let found = false; - let i = 0; - while (i < this[dataSymbol].length) { - if (this[dataSymbol][i][0] === name) { - if (!found) { - this[dataSymbol][i][1] = parseFormDataValue(value, filename); - found = true; - } else { - this[dataSymbol].splice(i, 1); - continue; - } - } - i++; - } - - // Otherwise, append entry to the context object’s entry list. - if (!found) { - this[dataSymbol].push([name, parseFormDataValue(value, filename)]); - } - } - - get [Symbol.toStringTag]() { - return "FormData"; - } - } - - class FormData extends DomIterableMixin(FormDataBase, dataSymbol) {} - - window.__bootstrap.formData = { - FormData, - }; -})(this); diff --git a/cli/rt/23_multipart.js b/cli/rt/23_multipart.js deleted file mode 100644 index 25c261b98..000000000 --- a/cli/rt/23_multipart.js +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { Buffer } = window.__bootstrap.buffer; - const { bytesSymbol, Blob } = window.__bootstrap.blob; - const { DomFile } = window.__bootstrap.domFile; - const { getHeaderValueParams } = window.__bootstrap.webUtil; - - const decoder = new TextDecoder(); - const encoder = new TextEncoder(); - const CR = "\r".charCodeAt(0); - const LF = "\n".charCodeAt(0); - - class MultipartBuilder { - constructor(formData, boundary) { - this.formData = formData; - this.boundary = boundary ?? this.#createBoundary(); - this.writer = new Buffer(); - } - - getContentType() { - return `multipart/form-data; boundary=${this.boundary}`; - } - - getBody() { - for (const [fieldName, fieldValue] of this.formData.entries()) { - if (fieldValue instanceof DomFile) { - this.#writeFile(fieldName, fieldValue); - } else this.#writeField(fieldName, fieldValue); - } - - this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`)); - - return this.writer.bytes(); - } - - #createBoundary = () => { - return ( - "----------" + - Array.from(Array(32)) - .map(() => Math.random().toString(36)[2] || 0) - .join("") - ); - }; - - #writeHeaders = (headers) => { - let buf = this.writer.empty() ? "" : "\r\n"; - - buf += `--${this.boundary}\r\n`; - for (const [key, value] of headers) { - buf += `${key}: ${value}\r\n`; - } - buf += `\r\n`; - - this.writer.writeSync(encoder.encode(buf)); - }; - - #writeFileHeaders = ( - field, - filename, - type, - ) => { - const headers = [ - [ - "Content-Disposition", - `form-data; name="${field}"; filename="${filename}"`, - ], - ["Content-Type", type || "application/octet-stream"], - ]; - return this.#writeHeaders(headers); - }; - - #writeFieldHeaders = (field) => { - const headers = [["Content-Disposition", `form-data; name="${field}"`]]; - return this.#writeHeaders(headers); - }; - - #writeField = (field, value) => { - this.#writeFieldHeaders(field); - this.writer.writeSync(encoder.encode(value)); - }; - - #writeFile = (field, value) => { - this.#writeFileHeaders(field, value.name, value.type); - this.writer.writeSync(value[bytesSymbol]); - }; - } - - class MultipartParser { - constructor(body, boundary) { - if (!boundary) { - throw new TypeError("multipart/form-data must provide a boundary"); - } - - this.boundary = `--${boundary}`; - this.body = body; - this.boundaryChars = encoder.encode(this.boundary); - } - - #parseHeaders = (headersText) => { - const headers = new Headers(); - const rawHeaders = headersText.split("\r\n"); - for (const rawHeader of rawHeaders) { - const sepIndex = rawHeader.indexOf(":"); - if (sepIndex < 0) { - continue; // Skip this header - } - const key = rawHeader.slice(0, sepIndex); - const value = rawHeader.slice(sepIndex + 1); - headers.set(key, value); - } - - return { - headers, - disposition: getHeaderValueParams( - headers.get("Content-Disposition") ?? "", - ), - }; - }; - - parse() { - const formData = new FormData(); - let headerText = ""; - let boundaryIndex = 0; - let state = 0; - let fileStart = 0; - - for (let i = 0; i < this.body.length; i++) { - const byte = this.body[i]; - const prevByte = this.body[i - 1]; - const isNewLine = byte === LF && prevByte === CR; - - if (state === 1 || state === 2 || state == 3) { - headerText += String.fromCharCode(byte); - } - if (state === 0 && isNewLine) { - state = 1; - } else if (state === 1 && isNewLine) { - state = 2; - const headersDone = this.body[i + 1] === CR && - this.body[i + 2] === LF; - - if (headersDone) { - state = 3; - } - } else if (state === 2 && isNewLine) { - state = 3; - } else if (state === 3 && isNewLine) { - state = 4; - fileStart = i + 1; - } else if (state === 4) { - if (this.boundaryChars[boundaryIndex] !== byte) { - boundaryIndex = 0; - } else { - boundaryIndex++; - } - - if (boundaryIndex >= this.boundary.length) { - const { headers, disposition } = this.#parseHeaders(headerText); - const content = this.body.subarray( - fileStart, - i - boundaryIndex - 1, - ); - // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata - const filename = disposition.get("filename"); - const name = disposition.get("name"); - - state = 5; - // Reset - boundaryIndex = 0; - headerText = ""; - - if (!name) { - continue; // Skip, unknown name - } - - if (filename) { - const blob = new Blob([content], { - type: headers.get("Content-Type") || "application/octet-stream", - }); - formData.append(name, blob, filename); - } else { - formData.append(name, decoder.decode(content)); - } - } - } else if (state === 5 && isNewLine) { - state = 1; - } - } - - return formData; - } - } - - window.__bootstrap.multipart = { - MultipartBuilder, - MultipartParser, - }; -})(this); diff --git a/cli/rt/24_body.js b/cli/rt/24_body.js deleted file mode 100644 index f755e2bad..000000000 --- a/cli/rt/24_body.js +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const { Blob } = window.__bootstrap.blob; - const { ReadableStream, isReadableStreamDisturbed } = - window.__bootstrap.streams; - const { Buffer } = window.__bootstrap.buffer; - const { - getHeaderValueParams, - hasHeaderValueOf, - isTypedArray, - } = window.__bootstrap.webUtil; - const { MultipartParser } = window.__bootstrap.multipart; - - function validateBodyType(owner, bodySource) { - if (isTypedArray(bodySource)) { - return true; - } else if (bodySource instanceof ArrayBuffer) { - return true; - } else if (typeof bodySource === "string") { - return true; - } else if (bodySource instanceof ReadableStream) { - return true; - } else if (bodySource instanceof FormData) { - return true; - } else if (bodySource instanceof URLSearchParams) { - return true; - } else if (!bodySource) { - return true; // null body is fine - } - throw new Error( - `Bad ${owner.constructor.name} body type: ${bodySource.constructor.name}`, - ); - } - - async function bufferFromStream( - stream, - size, - ) { - const encoder = new TextEncoder(); - const buffer = new Buffer(); - - if (size) { - // grow to avoid unnecessary allocations & copies - buffer.grow(size); - } - - while (true) { - const { done, value } = await stream.read(); - - if (done) break; - - if (typeof value === "string") { - buffer.writeSync(encoder.encode(value)); - } else if (value instanceof ArrayBuffer) { - buffer.writeSync(new Uint8Array(value)); - } else if (value instanceof Uint8Array) { - buffer.writeSync(value); - } else if (!value) { - // noop for undefined - } else { - throw new Error("unhandled type on stream read"); - } - } - - return buffer.bytes().buffer; - } - - function bodyToArrayBuffer(bodySource) { - if (isTypedArray(bodySource)) { - return bodySource.buffer; - } else if (bodySource instanceof ArrayBuffer) { - return bodySource; - } else if (typeof bodySource === "string") { - const enc = new TextEncoder(); - return enc.encode(bodySource).buffer; - } else if (bodySource instanceof ReadableStream) { - throw new Error( - `Can't convert stream to ArrayBuffer (try bufferFromStream)`, - ); - } else if ( - bodySource instanceof FormData || - bodySource instanceof URLSearchParams - ) { - const enc = new TextEncoder(); - return enc.encode(bodySource.toString()).buffer; - } else if (!bodySource) { - return new ArrayBuffer(0); - } - throw new Error( - `Body type not implemented: ${bodySource.constructor.name}`, - ); - } - - const BodyUsedError = - "Failed to execute 'clone' on 'Body': body is already used"; - - class Body { - #contentType = ""; - #size = undefined; - - constructor(_bodySource, meta) { - validateBodyType(this, _bodySource); - this._bodySource = _bodySource; - this.#contentType = meta.contentType; - this.#size = meta.size; - this._stream = null; - } - - get body() { - if (this._stream) { - return this._stream; - } - - if (!this._bodySource) { - return null; - } else if (this._bodySource instanceof ReadableStream) { - this._stream = this._bodySource; - } else { - const buf = bodyToArrayBuffer(this._bodySource); - if (!(buf instanceof ArrayBuffer)) { - throw new Error( - `Expected ArrayBuffer from body`, - ); - } - - this._stream = new ReadableStream({ - start(controller) { - controller.enqueue(buf); - controller.close(); - }, - }); - } - - return this._stream; - } - - get bodyUsed() { - if (this.body && isReadableStreamDisturbed(this.body)) { - return true; - } - return false; - } - - async blob() { - return new Blob([await this.arrayBuffer()], { - type: this.#contentType, - }); - } - - // ref: https://fetch.spec.whatwg.org/#body-mixin - async formData() { - const formData = new FormData(); - if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) { - const params = getHeaderValueParams(this.#contentType); - - // ref: https://tools.ietf.org/html/rfc2046#section-5.1 - const boundary = params.get("boundary"); - const body = new Uint8Array(await this.arrayBuffer()); - const multipartParser = new MultipartParser(body, boundary); - - return multipartParser.parse(); - } else if ( - hasHeaderValueOf(this.#contentType, "application/x-www-form-urlencoded") - ) { - // From https://github.com/github/fetch/blob/master/fetch.js - // Copyright (c) 2014-2016 GitHub, Inc. MIT License - const body = await this.text(); - try { - body - .trim() - .split("&") - .forEach((bytes) => { - if (bytes) { - const split = bytes.split("="); - const name = split.shift().replace(/\+/g, " "); - const value = split.join("=").replace(/\+/g, " "); - formData.append( - decodeURIComponent(name), - decodeURIComponent(value), - ); - } - }); - } catch (e) { - throw new TypeError("Invalid form urlencoded format"); - } - return formData; - } else { - throw new TypeError("Invalid form data"); - } - } - - async text() { - if (typeof this._bodySource === "string") { - return this._bodySource; - } - - const ab = await this.arrayBuffer(); - const decoder = new TextDecoder("utf-8"); - return decoder.decode(ab); - } - - async json() { - const raw = await this.text(); - return JSON.parse(raw); - } - - arrayBuffer() { - if (this._bodySource instanceof ReadableStream) { - return bufferFromStream(this._bodySource.getReader(), this.#size); - } - return bodyToArrayBuffer(this._bodySource); - } - } - - window.__bootstrap.body = { - Body, - BodyUsedError, - }; -})(this); diff --git a/cli/rt/25_request.js b/cli/rt/25_request.js deleted file mode 100644 index 467a66fe9..000000000 --- a/cli/rt/25_request.js +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const body = window.__bootstrap.body; - const { ReadableStream } = window.__bootstrap.streams; - - function byteUpperCase(s) { - return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) { - return c.toUpperCase(); - }); - } - - function normalizeMethod(m) { - const u = byteUpperCase(m); - if ( - u === "DELETE" || - u === "GET" || - u === "HEAD" || - u === "OPTIONS" || - u === "POST" || - u === "PUT" - ) { - return u; - } - return m; - } - - class Request extends body.Body { - constructor(input, init) { - if (arguments.length < 1) { - throw TypeError("Not enough arguments"); - } - - if (!init) { - init = {}; - } - - let b; - - // prefer body from init - if (init.body) { - b = init.body; - } else if (input instanceof Request && input._bodySource) { - if (input.bodyUsed) { - throw TypeError(body.BodyUsedError); - } - b = input._bodySource; - } else if (typeof input === "object" && "body" in input && input.body) { - if (input.bodyUsed) { - throw TypeError(body.BodyUsedError); - } - b = input.body; - } else { - b = ""; - } - - let headers; - - // prefer headers from init - if (init.headers) { - headers = new Headers(init.headers); - } else if (input instanceof Request) { - headers = input.headers; - } else { - headers = new Headers(); - } - - const contentType = headers.get("content-type") || ""; - super(b, { contentType }); - this.headers = headers; - - // readonly attribute ByteString method; - this.method = "GET"; - - // readonly attribute USVString url; - this.url = ""; - - // readonly attribute RequestCredentials credentials; - this.credentials = "omit"; - - if (input instanceof Request) { - if (input.bodyUsed) { - throw TypeError(body.BodyUsedError); - } - this.method = input.method; - this.url = input.url; - this.headers = new Headers(input.headers); - this.credentials = input.credentials; - this._stream = input._stream; - } else if (typeof input === "string") { - this.url = input; - } - - if (init && "method" in init) { - this.method = normalizeMethod(init.method); - } - - if ( - init && - "credentials" in init && - init.credentials && - ["omit", "same-origin", "include"].indexOf(init.credentials) !== -1 - ) { - this.credentials = init.credentials; - } - } - - clone() { - if (this.bodyUsed) { - throw TypeError(body.BodyUsedError); - } - - const iterators = this.headers.entries(); - const headersList = []; - for (const header of iterators) { - headersList.push(header); - } - - let body2 = this._bodySource; - - if (this._bodySource instanceof ReadableStream) { - const tees = this._bodySource.tee(); - this._stream = this._bodySource = tees[0]; - body2 = tees[1]; - } - - return new Request(this.url, { - body: body2, - method: this.method, - headers: new Headers(headersList), - credentials: this.credentials, - }); - } - } - - window.__bootstrap.request = { - Request, - }; -})(this); diff --git a/cli/rt/26_fetch.js b/cli/rt/26_fetch.js deleted file mode 100644 index b337fbe10..000000000 --- a/cli/rt/26_fetch.js +++ /dev/null @@ -1,391 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -((window) => { - const core = window.Deno.core; - const { notImplemented } = window.__bootstrap.util; - const { getHeaderValueParams, isTypedArray } = window.__bootstrap.webUtil; - const { Blob, bytesSymbol: blobBytesSymbol } = window.__bootstrap.blob; - const Body = window.__bootstrap.body; - const { ReadableStream } = window.__bootstrap.streams; - const { MultipartBuilder } = window.__bootstrap.multipart; - const { Headers } = window.__bootstrap.headers; - - function createHttpClient(options) { - return new HttpClient(opCreateHttpClient(options)); - } - - function opCreateHttpClient(args) { - return core.jsonOpSync("op_create_http_client", args); - } - - class HttpClient { - constructor(rid) { - this.rid = rid; - } - close() { - core.close(this.rid); - } - } - - function opFetch(args, body) { - let zeroCopy; - if (body != null) { - zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength); - } - - return core.jsonOpAsync("op_fetch", args, ...(zeroCopy ? [zeroCopy] : [])); - } - - const NULL_BODY_STATUS = [101, 204, 205, 304]; - const REDIRECT_STATUS = [301, 302, 303, 307, 308]; - - const responseData = new WeakMap(); - class Response extends Body.Body { - constructor(body = null, init) { - init = init ?? {}; - - if (typeof init !== "object") { - throw new TypeError(`'init' is not an object`); - } - - const extraInit = responseData.get(init) || {}; - let { type = "default", url = "" } = extraInit; - - let status = init.status === undefined ? 200 : Number(init.status || 0); - let statusText = init.statusText ?? ""; - let headers = init.headers instanceof Headers - ? init.headers - : new Headers(init.headers); - - if (init.status !== undefined && (status < 200 || status > 599)) { - throw new RangeError( - `The status provided (${init.status}) is outside the range [200, 599]`, - ); - } - - // null body status - if (body && NULL_BODY_STATUS.includes(status)) { - throw new TypeError("Response with null body status cannot have body"); - } - - if (!type) { - type = "default"; - } else { - if (type == "error") { - // spec: https://fetch.spec.whatwg.org/#concept-network-error - status = 0; - statusText = ""; - headers = new Headers(); - body = null; - /* spec for other Response types: - https://fetch.spec.whatwg.org/#concept-filtered-response-basic - Please note that type "basic" is not the same thing as "default".*/ - } else if (type == "basic") { - for (const h of headers) { - /* Forbidden Response-Header Names: - https://fetch.spec.whatwg.org/#forbidden-response-header-name */ - if (["set-cookie", "set-cookie2"].includes(h[0].toLowerCase())) { - headers.delete(h[0]); - } - } - } else if (type == "cors") { - /* CORS-safelisted Response-Header Names: - https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name */ - const allowedHeaders = [ - "Cache-Control", - "Content-Language", - "Content-Length", - "Content-Type", - "Expires", - "Last-Modified", - "Pragma", - ].map((c) => c.toLowerCase()); - for (const h of headers) { - /* Technically this is still not standards compliant because we are - supposed to allow headers allowed in the - 'Access-Control-Expose-Headers' header in the 'internal response' - However, this implementation of response doesn't seem to have an - easy way to access the internal response, so we ignore that - header. - TODO(serverhiccups): change how internal responses are handled - so we can do this properly. */ - if (!allowedHeaders.includes(h[0].toLowerCase())) { - headers.delete(h[0]); - } - } - /* TODO(serverhiccups): Once I fix the 'internal response' thing, - these actually need to treat the internal response differently */ - } else if (type == "opaque" || type == "opaqueredirect") { - url = ""; - status = 0; - statusText = ""; - headers = new Headers(); - body = null; - } - } - - const contentType = headers.get("content-type") || ""; - const size = Number(headers.get("content-length")) || undefined; - - super(body, { contentType, size }); - - this.url = url; - this.statusText = statusText; - this.status = extraInit.status || status; - this.headers = headers; - this.redirected = extraInit.redirected || false; - this.type = type; - } - - get ok() { - return 200 <= this.status && this.status < 300; - } - - clone() { - if (this.bodyUsed) { - throw TypeError(Body.BodyUsedError); - } - - const iterators = this.headers.entries(); - const headersList = []; - for (const header of iterators) { - headersList.push(header); - } - - let resBody = this._bodySource; - - if (this._bodySource instanceof ReadableStream) { - const tees = this._bodySource.tee(); - this._stream = this._bodySource = tees[0]; - resBody = tees[1]; - } - - return new Response(resBody, { - status: this.status, - statusText: this.statusText, - headers: new Headers(headersList), - }); - } - - static redirect(url, status) { - if (![301, 302, 303, 307, 308].includes(status)) { - throw new RangeError( - "The redirection status must be one of 301, 302, 303, 307 and 308.", - ); - } - return new Response(null, { - status, - statusText: "", - headers: [["Location", typeof url === "string" ? url : url.toString()]], - }); - } - } - - function sendFetchReq(url, method, headers, body, clientRid) { - let headerArray = []; - if (headers) { - headerArray = Array.from(headers.entries()); - } - - const args = { - method, - url, - headers: headerArray, - clientRid, - }; - - return opFetch(args, body); - } - - async function fetch(input, init) { - let url; - let method = null; - let headers = null; - let body; - let clientRid = null; - let redirected = false; - let remRedirectCount = 20; // TODO: use a better way to handle - - if (typeof input === "string" || input instanceof URL) { - url = typeof input === "string" ? input : input.href; - if (init != null) { - method = init.method || null; - if (init.headers) { - headers = init.headers instanceof Headers - ? init.headers - : new Headers(init.headers); - } else { - headers = null; - } - - // ref: https://fetch.spec.whatwg.org/#body-mixin - // Body should have been a mixin - // but we are treating it as a separate class - if (init.body) { - if (!headers) { - headers = new Headers(); - } - let contentType = ""; - if (typeof init.body === "string") { - body = new TextEncoder().encode(init.body); - contentType = "text/plain;charset=UTF-8"; - } else if (isTypedArray(init.body)) { - body = init.body; - } else if (init.body instanceof ArrayBuffer) { - body = new Uint8Array(init.body); - } else if (init.body instanceof URLSearchParams) { - body = new TextEncoder().encode(init.body.toString()); - contentType = "application/x-www-form-urlencoded;charset=UTF-8"; - } else if (init.body instanceof Blob) { - body = init.body[blobBytesSymbol]; - contentType = init.body.type; - } else if (init.body instanceof FormData) { - let boundary; - if (headers.has("content-type")) { - const params = getHeaderValueParams("content-type"); - boundary = params.get("boundary"); - } - const multipartBuilder = new MultipartBuilder(init.body, boundary); - body = multipartBuilder.getBody(); - contentType = multipartBuilder.getContentType(); - } else { - // TODO: ReadableStream - notImplemented(); - } - if (contentType && !headers.has("content-type")) { - headers.set("content-type", contentType); - } - } - - if (init.client instanceof HttpClient) { - clientRid = init.client.rid; - } - } - } else { - url = input.url; - method = input.method; - headers = input.headers; - - if (input._bodySource) { - body = new DataView(await input.arrayBuffer()); - } - } - - let responseBody; - let responseInit = {}; - while (remRedirectCount) { - const fetchResponse = await sendFetchReq( - url, - method, - headers, - body, - clientRid, - ); - const rid = fetchResponse.bodyRid; - - if ( - NULL_BODY_STATUS.includes(fetchResponse.status) || - REDIRECT_STATUS.includes(fetchResponse.status) - ) { - // We won't use body of received response, so close it now - // otherwise it will be kept in resource table. - core.close(fetchResponse.bodyRid); - responseBody = null; - } else { - responseBody = new ReadableStream({ - type: "bytes", - async pull(controller) { - try { - const result = await core.jsonOpAsync("op_fetch_read", { rid }); - if (!result || !result.chunk) { - controller.close(); - core.close(rid); - } else { - // TODO(ry) This is terribly inefficient. Make this zero-copy. - const chunk = new Uint8Array(result.chunk); - controller.enqueue(chunk); - } - } catch (e) { - controller.error(e); - controller.close(); - core.close(rid); - } - }, - cancel() { - // When reader.cancel() is called - core.close(rid); - }, - }); - } - - responseInit = { - status: 200, - statusText: fetchResponse.statusText, - headers: fetchResponse.headers, - }; - - responseData.set(responseInit, { - redirected, - rid: fetchResponse.bodyRid, - status: fetchResponse.status, - url, - }); - - const response = new Response(responseBody, responseInit); - - if (REDIRECT_STATUS.includes(fetchResponse.status)) { - // We're in a redirect status - switch ((init && init.redirect) || "follow") { - case "error": - responseInit = {}; - responseData.set(responseInit, { - type: "error", - redirected: false, - url: "", - }); - return new Response(null, responseInit); - case "manual": - responseInit = {}; - responseData.set(responseInit, { - type: "opaqueredirect", - redirected: false, - url: "", - }); - return new Response(null, responseInit); - case "follow": - default: - let redirectUrl = response.headers.get("Location"); - if (redirectUrl == null) { - return response; // Unspecified - } - if ( - !redirectUrl.startsWith("http://") && - !redirectUrl.startsWith("https://") - ) { - redirectUrl = new URL(redirectUrl, url).href; - } - url = redirectUrl; - redirected = true; - remRedirectCount--; - } - } else { - return response; - } - } - - responseData.set(responseInit, { - type: "error", - redirected: false, - url: "", - }); - - return new Response(null, responseInit); - } - - window.__bootstrap.fetch = { - fetch, - Response, - HttpClient, - createHttpClient, - }; -})(this); diff --git a/cli/rt/28_filereader.js b/cli/rt/28_filereader.js new file mode 100644 index 000000000..ea1ca3e5f --- /dev/null +++ b/cli/rt/28_filereader.js @@ -0,0 +1,264 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const base64 = window.__bootstrap.base64; + const setTimeout = window.__bootstrap.timers.setTimeout; + + // ProgressEvent could also be used in other DOM progress event emits. + // Current use is for FileReader. + class ProgressEvent extends Event { + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + + this.lengthComputable = eventInitDict?.lengthComputable ?? false; + this.loaded = eventInitDict?.loaded ?? 0; + this.total = eventInitDict?.total ?? 0; + } + } + window.__bootstrap.progressEvent = { + ProgressEvent, + }; + + async function readOperation(fr, blob, readtype) { + // Implementation from https://w3c.github.io/FileAPI/ notes + // And body of deno blob.ts readBytes + + fr.aborting = false; + + // 1. If fr’s state is "loading", throw an InvalidStateError DOMException. + if (fr.readyState === FileReader.LOADING) { + throw new DOMException( + "Invalid FileReader state.", + "InvalidStateError", + ); + } + // 2. Set fr’s state to "loading". + fr.readyState = FileReader.LOADING; + // 3. Set fr’s result to null. + fr.result = null; + // 4. Set fr’s error to null. + fr.error = null; + + // 5. Let stream be the result of calling get stream on blob. + const stream /*: ReadableStream*/ = blob.stream(); + + // 6. Let reader be the result of getting a reader from stream. + const reader = stream.getReader(); + + // 7. Let bytes be an empty byte sequence. + //let bytes = new Uint8Array(); + const chunks /*: Uint8Array[]*/ = []; + + // 8. Let chunkPromise be the result of reading a chunk from stream with reader. + let chunkPromise = reader.read(); + + // 9. Let isFirstChunk be true. + let isFirstChunk = true; + + // 10 in parallel while true + while (!fr.aborting) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const chunk = await chunkPromise; + + // 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr. + if (isFirstChunk) { + setTimeout(() => { + // fire a progress event for loadstart + const ev = new ProgressEvent("loadstart", {}); + fr.dispatchEvent(ev); + if (fr.onloadstart !== null) { + fr.onloadstart(ev); + } + }, 0); + } + // 3. Set isFirstChunk to false. + isFirstChunk = false; + + // 4. If chunkPromise is fulfilled with an object whose done property is false + // and whose value property is a Uint8Array object, run these steps: + if (!chunk.done && chunk.value instanceof Uint8Array) { + chunks.push(chunk.value); + + // TODO: (only) If roughly 50ms have passed since last progress + { + const size = chunks.reduce((p, i) => p + i.byteLength, 0); + const ev = new ProgressEvent("progress", { + loaded: size, + }); + fr.dispatchEvent(ev); + if (fr.onprogress !== null) { + fr.onprogress(ev); + } + } + + chunkPromise = reader.read(); + } // 5 Otherwise, if chunkPromise is fulfilled with an object whose done property is true, queue a task to run the following steps and abort this algorithm: + else if (chunk.done === true) { + setTimeout(() => { + if (fr.aborting) { + return; + } + + // 1. Set fr’s state to "done". + fr.readyState = FileReader.DONE; + // 2. Let result be the result of package data given bytes, type, blob’s type, and encodingName. + const size = chunks.reduce((p, i) => p + i.byteLength, 0); + const bytes = new Uint8Array(size); + let offs = 0; + for (const chunk of chunks) { + bytes.set(chunk, offs); + offs += chunk.byteLength; + } + switch (readtype.kind) { + case "ArrayBuffer": { + fr.result = bytes.buffer; + break; + } + case "Text": { + const decoder = new TextDecoder(readtype.encoding); + fr.result = decoder.decode(bytes.buffer); + break; + } + case "DataUrl": { + fr.result = "data:application/octet-stream;base64," + + base64.fromByteArray(bytes); + break; + } + } + // 4.2 Fire a progress event called load at the fr. + { + const ev = new ProgressEvent("load", { + lengthComputable: true, + loaded: size, + total: size, + }); + fr.dispatchEvent(ev); + if (fr.onload !== null) { + fr.onload(ev); + } + } + + // 5. If fr’s state is not "loading", fire a progress event called loadend at the fr. + //Note: Event handler for the load or error events could have started another load, if that happens the loadend event for this load is not fired. + if (fr.readyState !== FileReader.LOADING) { + const ev = new ProgressEvent("loadend", { + lengthComputable: true, + loaded: size, + total: size, + }); + fr.dispatchEvent(ev); + if (fr.onloadend !== null) { + fr.onloadend(ev); + } + } + }, 0); + + break; + } + } catch (err) { + if (fr.aborting) { + break; + } + + // chunkPromise rejected + fr.readyState = FileReader.DONE; + fr.error = err; + + { + const ev = new ProgressEvent("error", {}); + fr.dispatchEvent(ev); + if (fr.onerror !== null) { + fr.onerror(ev); + } + } + + //If fr’s state is not "loading", fire a progress event called loadend at fr. + //Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired. + if (fr.readyState !== FileReader.LOADING) { + const ev = new ProgressEvent("loadend", {}); + fr.dispatchEvent(ev); + if (fr.onloadend !== null) { + fr.onloadend(ev); + } + } + + break; + } + } + } + + class FileReader extends EventTarget { + error = null; + onabort = null; + onerror = null; + onload = null; + onloadend = null; + onloadstart = null; + onprogress = null; + + readyState = FileReader.EMPTY; + result = null; + aborting = false; + + constructor() { + super(); + } + + abort() { + // If context object's state is "empty" or if context object's state is "done" set context object's result to null and terminate this algorithm. + if ( + this.readyState === FileReader.EMPTY || + this.readyState === FileReader.DONE + ) { + this.result = null; + return; + } + // If context object's state is "loading" set context object's state to "done" and set context object's result to null. + if (this.readyState === FileReader.LOADING) { + this.readyState = FileReader.DONE; + this.result = null; + } + // If there are any tasks from the context object on the file reading task source in an affiliated task queue, then remove those tasks from that task queue. + // Terminate the algorithm for the read method being processed. + this.aborting = true; + + // Fire a progress event called abort at the context object. + const ev = new ProgressEvent("abort", {}); + this.dispatchEvent(ev); + if (this.onabort !== null) { + this.onabort(ev); + } + + // If context object's state is not "loading", fire a progress event called loadend at the context object. + if (this.readyState !== FileReader.LOADING) { + const ev = new ProgressEvent("loadend", {}); + this.dispatchEvent(ev); + if (this.onloadend !== null) { + this.onloadend(ev); + } + } + } + readAsArrayBuffer(blob) { + readOperation(this, blob, { kind: "ArrayBuffer" }); + } + readAsBinaryString(blob) { + // alias for readAsArrayBuffer + readOperation(this, blob, { kind: "ArrayBuffer" }); + } + readAsDataURL(blob) { + readOperation(this, blob, { kind: "DataUrl" }); + } + readAsText(blob, encoding) { + readOperation(this, blob, { kind: "Text", encoding }); + } + } + + FileReader.EMPTY = 0; + FileReader.LOADING = 1; + FileReader.DONE = 2; + + window.__bootstrap.fileReader = { + FileReader, + }; +})(this); diff --git a/cli/rt/99_main.js b/cli/rt/99_main.js index 02834dcf3..8091823bb 100644 --- a/cli/rt/99_main.js +++ b/cli/rt/99_main.js @@ -22,15 +22,10 @@ delete Object.prototype.__proto__; const crypto = window.__bootstrap.crypto; const url = window.__bootstrap.url; const headers = window.__bootstrap.headers; - const queuingStrategy = window.__bootstrap.queuingStrategy; const streams = window.__bootstrap.streams; - const blob = window.__bootstrap.blob; - const domFile = window.__bootstrap.domFile; const progressEvent = window.__bootstrap.progressEvent; const fileReader = window.__bootstrap.fileReader; - const formData = window.__bootstrap.formData; const webSocket = window.__bootstrap.webSocket; - const request = window.__bootstrap.request; const fetch = window.__bootstrap.fetch; const denoNs = window.__bootstrap.denoNs; const denoNsUnstable = window.__bootstrap.denoNsUnstable; @@ -198,22 +193,22 @@ delete Object.prototype.__proto__; // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope const windowOrWorkerGlobalScope = { - Blob: util.nonEnumerable(blob.Blob), + Blob: util.nonEnumerable(fetch.Blob), ByteLengthQueuingStrategy: util.nonEnumerable( - queuingStrategy.ByteLengthQueuingStrategy, + streams.ByteLengthQueuingStrategy, ), CloseEvent: util.nonEnumerable(CloseEvent), CountQueuingStrategy: util.nonEnumerable( - queuingStrategy.CountQueuingStrategy, + streams.CountQueuingStrategy, ), CustomEvent: util.nonEnumerable(CustomEvent), DOMException: util.nonEnumerable(DOMException), ErrorEvent: util.nonEnumerable(ErrorEvent), Event: util.nonEnumerable(Event), EventTarget: util.nonEnumerable(EventTarget), - File: util.nonEnumerable(domFile.DomFile), + File: util.nonEnumerable(fetch.DomFile), FileReader: util.nonEnumerable(fileReader.FileReader), - FormData: util.nonEnumerable(formData.FormData), + FormData: util.nonEnumerable(fetch.FormData), Headers: util.nonEnumerable(headers.Headers), MessageEvent: util.nonEnumerable(MessageEvent), Performance: util.nonEnumerable(performance.Performance), @@ -222,7 +217,7 @@ delete Object.prototype.__proto__; PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure), ProgressEvent: util.nonEnumerable(progressEvent.ProgressEvent), ReadableStream: util.nonEnumerable(streams.ReadableStream), - Request: util.nonEnumerable(request.Request), + Request: util.nonEnumerable(fetch.Request), Response: util.nonEnumerable(fetch.Response), TextDecoder: util.nonEnumerable(TextDecoder), TextEncoder: util.nonEnumerable(TextEncoder), diff --git a/cli/state.rs b/cli/state.rs index 5f2aeb3ff..fdc61cf16 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -21,6 +21,7 @@ use std::cell::Cell; use std::cell::RefCell; use std::collections::HashMap; use std::path::Path; +use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; use std::str; @@ -315,3 +316,13 @@ impl CliState { } } } + +impl deno_fetch::FetchPermissions for CliState { + fn check_net_url(&self, url: &url::Url) -> Result<(), AnyError> { + CliState::check_net_url(self, url) + } + + fn check_read(&self, p: &PathBuf) -> Result<(), AnyError> { + CliState::check_read(self, p) + } +} diff --git a/cli/tests/performance_stats.out b/cli/tests/performance_stats.out index dd1dbe32e..7943486d3 100644 --- a/cli/tests/performance_stats.out +++ b/cli/tests/performance_stats.out @@ -1,5 +1,5 @@ [WILDCARD] -Files: 45 +Files: 46 Nodes: [WILDCARD] Identifiers: [WILDCARD] Symbols: [WILDCARD] diff --git a/cli/tests/unit/blob_test.ts b/cli/tests/unit/blob_test.ts index 7ef9b0125..b1587b6da 100644 --- a/cli/tests/unit/blob_test.ts +++ b/cli/tests/unit/blob_test.ts @@ -61,6 +61,7 @@ unitTest(function blobShouldNotThrowError(): void { assertEquals(hasThrown, false); }); +/* TODO https://github.com/denoland/deno/issues/7540 unitTest(function nativeEndLine(): void { const options = { ending: "native", @@ -69,6 +70,7 @@ unitTest(function nativeEndLine(): void { assertEquals(blob.size, Deno.build.os === "windows" ? 12 : 11); }); +*/ unitTest(async function blobText(): Promise { const blob = new Blob(["Hello World"]); diff --git a/cli/tests/unit/dom_iterable_test.ts b/cli/tests/unit/dom_iterable_test.ts index f4690d5b9..30599b6e6 100644 --- a/cli/tests/unit/dom_iterable_test.ts +++ b/cli/tests/unit/dom_iterable_test.ts @@ -1,4 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +/* TODO https://github.com/denoland/deno/issues/7540 import { unitTest, assert, assertEquals } from "./test_util.ts"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -25,6 +27,7 @@ function setup() { }; } + unitTest(function testDomIterable(): void { const { DomIterable, Base } = setup(); @@ -85,3 +88,4 @@ unitTest(function testDomIterableScope(): void { checkScope(null, window); checkScope(undefined, window); }); +*/ diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index e5abbd144..63a5d1f5b 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -435,6 +435,7 @@ delete Object.prototype.__proto__; ts.libs.push("deno.ns", "deno.window", "deno.worker", "deno.shared_globals"); ts.libMap.set("deno.ns", "lib.deno.ns.d.ts"); ts.libMap.set("deno.web", "lib.deno.web.d.ts"); + ts.libMap.set("deno.fetch", "lib.deno.fetch.d.ts"); ts.libMap.set("deno.window", "lib.deno.window.d.ts"); ts.libMap.set("deno.worker", "lib.deno.worker.d.ts"); ts.libMap.set("deno.shared_globals", "lib.deno.shared_globals.d.ts"); @@ -450,6 +451,10 @@ delete Object.prototype.__proto__; `${ASSETS}/lib.deno.web.d.ts`, ts.ScriptTarget.ESNext, ); + SNAPSHOT_HOST.getSourceFile( + `${ASSETS}/lib.deno.fetch.d.ts`, + ts.ScriptTarget.ESNext, + ); SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.window.d.ts`, ts.ScriptTarget.ESNext, diff --git a/op_crates/fetch/01_fetch_util.js b/op_crates/fetch/01_fetch_util.js new file mode 100644 index 000000000..07f45d821 --- /dev/null +++ b/op_crates/fetch/01_fetch_util.js @@ -0,0 +1,20 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + function requiredArguments( + name, + length, + required, + ) { + if (length < required) { + const errMsg = `${name} requires at least ${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present`; + throw new TypeError(errMsg); + } + } + + window.__bootstrap.fetchUtil = { + requiredArguments, + }; +})(this); diff --git a/op_crates/fetch/03_dom_iterable.js b/op_crates/fetch/03_dom_iterable.js new file mode 100644 index 000000000..bea60b61f --- /dev/null +++ b/op_crates/fetch/03_dom_iterable.js @@ -0,0 +1,77 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { requiredArguments } = window.__bootstrap.fetchUtil; + // const { exposeForTest } = window.__bootstrap.internals; + + function DomIterableMixin( + Base, + dataSymbol, + ) { + // we have to cast `this` as `any` because there is no way to describe the + // Base class in a way where the Symbol `dataSymbol` is defined. So the + // runtime code works, but we do lose a little bit of type safety. + + // Additionally, we have to not use .keys() nor .values() since the internal + // slot differs in type - some have a Map, which yields [K, V] in + // Symbol.iterator, and some have an Array, which yields V, in this case + // [K, V] too as they are arrays of tuples. + + const DomIterable = class extends Base { + *entries() { + for (const entry of this[dataSymbol]) { + yield entry; + } + } + + *keys() { + for (const [key] of this[dataSymbol]) { + yield key; + } + } + + *values() { + for (const [, value] of this[dataSymbol]) { + yield value; + } + } + + forEach( + callbackfn, + thisArg, + ) { + requiredArguments( + `${this.constructor.name}.forEach`, + arguments.length, + 1, + ); + callbackfn = callbackfn.bind( + thisArg == null ? globalThis : Object(thisArg), + ); + for (const [key, value] of this[dataSymbol]) { + callbackfn(value, key, this); + } + } + + *[Symbol.iterator]() { + for (const entry of this[dataSymbol]) { + yield entry; + } + } + }; + + // we want the Base class name to be the name of the class. + Object.defineProperty(DomIterable, "name", { + value: Base.name, + configurable: true, + }); + + return DomIterable; + } + + // exposeForTest("DomIterableMixin", DomIterableMixin); + + window.__bootstrap.domIterable = { + DomIterableMixin, + }; +})(this); diff --git a/op_crates/fetch/11_streams.js b/op_crates/fetch/11_streams.js new file mode 100644 index 000000000..b182a96ed --- /dev/null +++ b/op_crates/fetch/11_streams.js @@ -0,0 +1,3418 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This code closely follows the WHATWG Stream Specification +// See: https://streams.spec.whatwg.org/ +// +// There are some parts that are not fully implemented, and there are some +// comments which point to steps of the specification that are not implemented. + +((window) => { + /* eslint-disable @typescript-eslint/no-explicit-any,require-await */ + + const customInspect = Symbol.for("Deno.customInspect"); + + /** Clone a value in a similar way to structured cloning. It is similar to a + * StructureDeserialize(StructuredSerialize(...)). */ + function cloneValue(value) { + switch (typeof value) { + case "number": + case "string": + case "boolean": + case "undefined": + case "bigint": + return value; + case "object": { + if (objectCloneMemo.has(value)) { + return objectCloneMemo.get(value); + } + if (value === null) { + return value; + } + if (value instanceof Date) { + return new Date(value.valueOf()); + } + if (value instanceof RegExp) { + return new RegExp(value); + } + if (value instanceof SharedArrayBuffer) { + return value; + } + if (value instanceof ArrayBuffer) { + const cloned = cloneArrayBuffer( + value, + 0, + value.byteLength, + ArrayBuffer, + ); + objectCloneMemo.set(value, cloned); + return cloned; + } + if (ArrayBuffer.isView(value)) { + const clonedBuffer = cloneValue(value.buffer); + // Use DataViewConstructor type purely for type-checking, can be a + // DataView or TypedArray. They use the same constructor signature, + // only DataView has a length in bytes and TypedArrays use a length in + // terms of elements, so we adjust for that. + let length; + if (value instanceof DataView) { + length = value.byteLength; + } else { + length = value.length; + } + return new (value.constructor)( + clonedBuffer, + value.byteOffset, + length, + ); + } + if (value instanceof Map) { + const clonedMap = new Map(); + objectCloneMemo.set(value, clonedMap); + value.forEach((v, k) => clonedMap.set(k, cloneValue(v))); + return clonedMap; + } + if (value instanceof Set) { + const clonedSet = new Map(); + objectCloneMemo.set(value, clonedSet); + value.forEach((v, k) => clonedSet.set(k, cloneValue(v))); + return clonedSet; + } + + const clonedObj = {}; + objectCloneMemo.set(value, clonedObj); + const sourceKeys = Object.getOwnPropertyNames(value); + for (const key of sourceKeys) { + clonedObj[key] = cloneValue(value[key]); + } + return clonedObj; + } + case "symbol": + case "function": + default: + throw new DOMException("Uncloneable value in stream", "DataCloneError"); + } + } + + function setFunctionName(fn, value) { + Object.defineProperty(fn, "name", { value, configurable: true }); + } + + class AssertionError extends Error { + constructor(msg) { + super(msg); + this.name = "AssertionError"; + } + } + + function assert(cond, msg = "Assertion failed.") { + if (!cond) { + throw new AssertionError(msg); + } + } + + const sym = { + abortAlgorithm: Symbol("abortAlgorithm"), + abortSteps: Symbol("abortSteps"), + asyncIteratorReader: Symbol("asyncIteratorReader"), + autoAllocateChunkSize: Symbol("autoAllocateChunkSize"), + backpressure: Symbol("backpressure"), + backpressureChangePromise: Symbol("backpressureChangePromise"), + byobRequest: Symbol("byobRequest"), + cancelAlgorithm: Symbol("cancelAlgorithm"), + cancelSteps: Symbol("cancelSteps"), + closeAlgorithm: Symbol("closeAlgorithm"), + closedPromise: Symbol("closedPromise"), + closeRequest: Symbol("closeRequest"), + closeRequested: Symbol("closeRequested"), + controlledReadableByteStream: Symbol( + "controlledReadableByteStream", + ), + controlledReadableStream: Symbol("controlledReadableStream"), + controlledTransformStream: Symbol("controlledTransformStream"), + controlledWritableStream: Symbol("controlledWritableStream"), + disturbed: Symbol("disturbed"), + errorSteps: Symbol("errorSteps"), + flushAlgorithm: Symbol("flushAlgorithm"), + forAuthorCode: Symbol("forAuthorCode"), + inFlightWriteRequest: Symbol("inFlightWriteRequest"), + inFlightCloseRequest: Symbol("inFlightCloseRequest"), + isFakeDetached: Symbol("isFakeDetached"), + ownerReadableStream: Symbol("ownerReadableStream"), + ownerWritableStream: Symbol("ownerWritableStream"), + pendingAbortRequest: Symbol("pendingAbortRequest"), + preventCancel: Symbol("preventCancel"), + pullAgain: Symbol("pullAgain"), + pullAlgorithm: Symbol("pullAlgorithm"), + pulling: Symbol("pulling"), + pullSteps: Symbol("pullSteps"), + queue: Symbol("queue"), + queueTotalSize: Symbol("queueTotalSize"), + readable: Symbol("readable"), + readableStreamController: Symbol("readableStreamController"), + reader: Symbol("reader"), + readRequests: Symbol("readRequests"), + readyPromise: Symbol("readyPromise"), + started: Symbol("started"), + state: Symbol("state"), + storedError: Symbol("storedError"), + strategyHWM: Symbol("strategyHWM"), + strategySizeAlgorithm: Symbol("strategySizeAlgorithm"), + transformAlgorithm: Symbol("transformAlgorithm"), + transformStreamController: Symbol("transformStreamController"), + writableStreamController: Symbol("writableStreamController"), + writeAlgorithm: Symbol("writeAlgorithm"), + writable: Symbol("writable"), + writer: Symbol("writer"), + writeRequests: Symbol("writeRequests"), + }; + class ReadableByteStreamController { + constructor() { + throw new TypeError( + "ReadableByteStreamController's constructor cannot be called.", + ); + } + + get byobRequest() { + return undefined; + } + + get desiredSize() { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + return readableByteStreamControllerGetDesiredSize(this); + } + + close() { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + if (this[sym.closeRequested]) { + throw new TypeError("Closed already requested."); + } + if (this[sym.controlledReadableByteStream][sym.state] !== "readable") { + throw new TypeError( + "ReadableByteStreamController's stream is not in a readable state.", + ); + } + readableByteStreamControllerClose(this); + } + + enqueue(chunk) { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + if (this[sym.closeRequested]) { + throw new TypeError("Closed already requested."); + } + if (this[sym.controlledReadableByteStream][sym.state] !== "readable") { + throw new TypeError( + "ReadableByteStreamController's stream is not in a readable state.", + ); + } + if (!ArrayBuffer.isView(chunk)) { + throw new TypeError( + "You can only enqueue array buffer views when using a ReadableByteStreamController", + ); + } + if (isDetachedBuffer(chunk.buffer)) { + throw new TypeError( + "Cannot enqueue a view onto a detached ArrayBuffer", + ); + } + readableByteStreamControllerEnqueue(this, chunk); + } + + error(error) { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + readableByteStreamControllerError(this, error); + } + + [sym.cancelSteps](reason) { + // 3.11.5.1.1 If this.[[pendingPullIntos]] is not empty, + resetQueue(this); + const result = this[sym.cancelAlgorithm](reason); + readableByteStreamControllerClearAlgorithms(this); + return result; + } + + [sym.pullSteps]() { + const stream = this[sym.controlledReadableByteStream]; + assert(readableStreamHasDefaultReader(stream)); + if (this[sym.queueTotalSize] > 0) { + assert(readableStreamGetNumReadRequests(stream) === 0); + const entry = this[sym.queue].shift(); + assert(entry); + this[sym.queueTotalSize] -= entry.size; + readableByteStreamControllerHandleQueueDrain(this); + const view = new Uint8Array(entry.value, entry.offset, entry.size); + return Promise.resolve( + readableStreamCreateReadResult( + view, + false, + stream[sym.reader][sym.forAuthorCode], + ), + ); + } + // 3.11.5.2.5 If autoAllocateChunkSize is not undefined, + const promise = readableStreamAddReadRequest(stream); + readableByteStreamControllerCallPullIfNeeded(this); + return promise; + } + + [customInspect]() { + return `${this.constructor.name} { byobRequest: ${ + String(this.byobRequest) + }, desiredSize: ${String(this.desiredSize)} }`; + } + } + + class ReadableStreamDefaultController { + constructor() { + throw new TypeError( + "ReadableStreamDefaultController's constructor cannot be called.", + ); + } + + get desiredSize() { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + return readableStreamDefaultControllerGetDesiredSize(this); + } + + close() { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + if (!readableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError( + "ReadableStreamDefaultController cannot close or enqueue.", + ); + } + readableStreamDefaultControllerClose(this); + } + + enqueue(chunk) { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + if (!readableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError("ReadableSteamController cannot enqueue."); + } + return readableStreamDefaultControllerEnqueue(this, chunk); + } + + error(error) { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + readableStreamDefaultControllerError(this, error); + } + + [sym.cancelSteps](reason) { + resetQueue(this); + const result = this[sym.cancelAlgorithm](reason); + readableStreamDefaultControllerClearAlgorithms(this); + return result; + } + + [sym.pullSteps]() { + const stream = this[sym.controlledReadableStream]; + if (this[sym.queue].length) { + const chunk = dequeueValue(this); + if (this[sym.closeRequested] && this[sym.queue].length === 0) { + readableStreamDefaultControllerClearAlgorithms(this); + readableStreamClose(stream); + } else { + readableStreamDefaultControllerCallPullIfNeeded(this); + } + return Promise.resolve( + readableStreamCreateReadResult( + chunk, + false, + stream[sym.reader][sym.forAuthorCode], + ), + ); + } + const pendingPromise = readableStreamAddReadRequest(stream); + readableStreamDefaultControllerCallPullIfNeeded(this); + return pendingPromise; + } + + [customInspect]() { + return `${this.constructor.name} { desiredSize: ${ + String(this.desiredSize) + } }`; + } + } + + class ReadableStreamDefaultReader { + constructor(stream) { + if (!isReadableStream(stream)) { + throw new TypeError("stream is not a ReadableStream."); + } + if (isReadableStreamLocked(stream)) { + throw new TypeError("stream is locked."); + } + readableStreamReaderGenericInitialize(this, stream); + this[sym.readRequests] = []; + } + + get closed() { + if (!isReadableStreamDefaultReader(this)) { + return Promise.reject( + new TypeError("Invalid ReadableStreamDefaultReader."), + ); + } + return ( + this[sym.closedPromise].promise ?? + Promise.reject(new TypeError("Invalid reader.")) + ); + } + + cancel(reason) { + if (!isReadableStreamDefaultReader(this)) { + return Promise.reject( + new TypeError("Invalid ReadableStreamDefaultReader."), + ); + } + if (!this[sym.ownerReadableStream]) { + return Promise.reject(new TypeError("Invalid reader.")); + } + return readableStreamReaderGenericCancel(this, reason); + } + + read() { + if (!isReadableStreamDefaultReader(this)) { + return Promise.reject( + new TypeError("Invalid ReadableStreamDefaultReader."), + ); + } + if (!this[sym.ownerReadableStream]) { + return Promise.reject(new TypeError("Invalid reader.")); + } + return readableStreamDefaultReaderRead(this); + } + + releaseLock() { + if (!isReadableStreamDefaultReader(this)) { + throw new TypeError("Invalid ReadableStreamDefaultReader."); + } + if (this[sym.ownerReadableStream] === undefined) { + return; + } + if (this[sym.readRequests].length) { + throw new TypeError("Cannot release lock with pending read requests."); + } + readableStreamReaderGenericRelease(this); + } + + [customInspect]() { + return `${this.constructor.name} { closed: Promise }`; + } + } + + const AsyncIteratorPrototype = Object + .getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); + + const ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf({ + next() { + if (!isReadableStreamAsyncIterator(this)) { + return Promise.reject( + new TypeError("invalid ReadableStreamAsyncIterator."), + ); + } + const reader = this[sym.asyncIteratorReader]; + if (!reader[sym.ownerReadableStream]) { + return Promise.reject( + new TypeError("reader owner ReadableStream is undefined."), + ); + } + return readableStreamDefaultReaderRead(reader).then((result) => { + assert(typeof result === "object"); + const { done } = result; + assert(typeof done === "boolean"); + if (done) { + readableStreamReaderGenericRelease(reader); + } + const { value } = result; + return readableStreamCreateReadResult(value, done, true); + }); + }, + return( + value, + ) { + if (!isReadableStreamAsyncIterator(this)) { + return Promise.reject( + new TypeError("invalid ReadableStreamAsyncIterator."), + ); + } + const reader = this[sym.asyncIteratorReader]; + if (!reader[sym.ownerReadableStream]) { + return Promise.reject( + new TypeError("reader owner ReadableStream is undefined."), + ); + } + if (reader[sym.readRequests].length) { + return Promise.reject( + new TypeError("reader has outstanding read requests."), + ); + } + if (!this[sym.preventCancel]) { + const result = readableStreamReaderGenericCancel(reader, value); + readableStreamReaderGenericRelease(reader); + return result.then(() => + readableStreamCreateReadResult(value, true, true) + ); + } + readableStreamReaderGenericRelease(reader); + return Promise.resolve( + readableStreamCreateReadResult(value, true, true), + ); + }, + }, AsyncIteratorPrototype); + + class ReadableStream { + constructor( + underlyingSource = {}, + strategy = {}, + ) { + initializeReadableStream(this); + const { size } = strategy; + let { highWaterMark } = strategy; + const { type } = underlyingSource; + + if (underlyingSource.type == "bytes") { + if (size !== undefined) { + throw new RangeError( + `When underlying source is "bytes", strategy.size must be undefined.`, + ); + } + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark ?? 0); + setUpReadableByteStreamControllerFromUnderlyingSource( + this, + underlyingSource, + highWaterMark, + ); + } else if (type === undefined) { + const sizeAlgorithm = makeSizeAlgorithmFromSizeFunction(size); + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark ?? 1); + setUpReadableStreamDefaultControllerFromUnderlyingSource( + this, + underlyingSource, + highWaterMark, + sizeAlgorithm, + ); + } else { + throw new RangeError( + `Valid values for underlyingSource are "bytes" or undefined. Received: "${type}".`, + ); + } + } + + get locked() { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + return isReadableStreamLocked(this); + } + + cancel(reason) { + if (!isReadableStream(this)) { + return Promise.reject(new TypeError("Invalid ReadableStream.")); + } + if (isReadableStreamLocked(this)) { + return Promise.reject( + new TypeError("Cannot cancel a locked ReadableStream."), + ); + } + return readableStreamCancel(this, reason); + } + + getIterator({ + preventCancel, + } = {}) { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + const reader = acquireReadableStreamDefaultReader(this); + const iterator = Object.create(ReadableStreamAsyncIteratorPrototype); + iterator[sym.asyncIteratorReader] = reader; + iterator[sym.preventCancel] = Boolean(preventCancel); + return iterator; + } + + getReader({ mode } = {}) { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + if (mode === undefined) { + return acquireReadableStreamDefaultReader(this, true); + } + mode = String(mode); + // 3.2.5.4.4 If mode is "byob", return ? AcquireReadableStreamBYOBReader(this, true). + throw new RangeError(`Unsupported mode "${mode}"`); + } + + pipeThrough( + { + writable, + readable, + }, + { preventClose, preventAbort, preventCancel, signal } = {}, + ) { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + if (!isWritableStream(writable)) { + throw new TypeError("writable is not a valid WritableStream."); + } + if (!isReadableStream(readable)) { + throw new TypeError("readable is not a valid ReadableStream."); + } + preventClose = Boolean(preventClose); + preventAbort = Boolean(preventAbort); + preventCancel = Boolean(preventCancel); + if (signal && !(signal instanceof AbortSignal)) { + throw new TypeError("Invalid signal."); + } + if (isReadableStreamLocked(this)) { + throw new TypeError("ReadableStream is locked."); + } + if (isWritableStreamLocked(writable)) { + throw new TypeError("writable is locked."); + } + const promise = readableStreamPipeTo( + this, + writable, + preventClose, + preventAbort, + preventCancel, + signal, + ); + setPromiseIsHandledToTrue(promise); + return readable; + } + + pipeTo( + dest, + { preventClose, preventAbort, preventCancel, signal } = {}, + ) { + if (!isReadableStream(this)) { + return Promise.reject(new TypeError("Invalid ReadableStream.")); + } + if (!isWritableStream(dest)) { + return Promise.reject( + new TypeError("dest is not a valid WritableStream."), + ); + } + preventClose = Boolean(preventClose); + preventAbort = Boolean(preventAbort); + preventCancel = Boolean(preventCancel); + if (signal && !(signal instanceof AbortSignal)) { + return Promise.reject(new TypeError("Invalid signal.")); + } + if (isReadableStreamLocked(this)) { + return Promise.reject(new TypeError("ReadableStream is locked.")); + } + if (isWritableStreamLocked(dest)) { + return Promise.reject(new TypeError("dest is locked.")); + } + return readableStreamPipeTo( + this, + dest, + preventClose, + preventAbort, + preventCancel, + signal, + ); + } + + tee() { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + return readableStreamTee(this, false); + } + + [customInspect]() { + return `${this.constructor.name} { locked: ${String(this.locked)} }`; + } + + [Symbol.asyncIterator]( + options = {}, + ) { + return this.getIterator(options); + } + } + + class TransformStream { + constructor( + transformer = {}, + writableStrategy = {}, + readableStrategy = {}, + ) { + const writableSizeFunction = writableStrategy.size; + let writableHighWaterMark = writableStrategy.highWaterMark; + const readableSizeFunction = readableStrategy.size; + let readableHighWaterMark = readableStrategy.highWaterMark; + const writableType = transformer.writableType; + if (writableType !== undefined) { + throw new RangeError( + `Expected transformer writableType to be undefined, received "${ + String(writableType) + }"`, + ); + } + const writableSizeAlgorithm = makeSizeAlgorithmFromSizeFunction( + writableSizeFunction, + ); + if (writableHighWaterMark === undefined) { + writableHighWaterMark = 1; + } + writableHighWaterMark = validateAndNormalizeHighWaterMark( + writableHighWaterMark, + ); + const readableType = transformer.readableType; + if (readableType !== undefined) { + throw new RangeError( + `Expected transformer readableType to be undefined, received "${ + String(readableType) + }"`, + ); + } + const readableSizeAlgorithm = makeSizeAlgorithmFromSizeFunction( + readableSizeFunction, + ); + if (readableHighWaterMark === undefined) { + readableHighWaterMark = 1; + } + readableHighWaterMark = validateAndNormalizeHighWaterMark( + readableHighWaterMark, + ); + const startPromise = getDeferred(); + initializeTransformStream( + this, + startPromise.promise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ); + // the brand check expects this, and the brand check occurs in the following + // but the property hasn't been defined. + Object.defineProperty(this, sym.transformStreamController, { + value: undefined, + writable: true, + configurable: true, + }); + setUpTransformStreamDefaultControllerFromTransformer(this, transformer); + const startResult = invokeOrNoop( + transformer, + "start", + this[sym.transformStreamController], + ); + startPromise.resolve(startResult); + } + + get readable() { + if (!isTransformStream(this)) { + throw new TypeError("Invalid TransformStream."); + } + return this[sym.readable]; + } + + get writable() { + if (!isTransformStream(this)) { + throw new TypeError("Invalid TransformStream."); + } + return this[sym.writable]; + } + + [customInspect]() { + return this.constructor.name; + } + } + + class TransformStreamDefaultController { + constructor() { + throw new TypeError( + "TransformStreamDefaultController's constructor cannot be called.", + ); + } + + get desiredSize() { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + const readableController = this[sym.controlledTransformStream][ + sym.readable + ][sym.readableStreamController]; + return readableStreamDefaultControllerGetDesiredSize( + readableController, + ); + } + + enqueue(chunk) { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + transformStreamDefaultControllerEnqueue(this, chunk); + } + + error(reason) { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + transformStreamDefaultControllerError(this, reason); + } + + terminate() { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + transformStreamDefaultControllerTerminate(this); + } + + [customInspect]() { + return `${this.constructor.name} { desiredSize: ${ + String(this.desiredSize) + } }`; + } + } + + class WritableStreamDefaultController { + constructor() { + throw new TypeError( + "WritableStreamDefaultController's constructor cannot be called.", + ); + } + + error(e) { + if (!isWritableStreamDefaultController(this)) { + throw new TypeError("Invalid WritableStreamDefaultController."); + } + const state = this[sym.controlledWritableStream][sym.state]; + if (state !== "writable") { + return; + } + writableStreamDefaultControllerError(this, e); + } + + [sym.abortSteps](reason) { + const result = this[sym.abortAlgorithm](reason); + writableStreamDefaultControllerClearAlgorithms(this); + return result; + } + + [sym.errorSteps]() { + resetQueue(this); + } + + [customInspect]() { + return `${this.constructor.name} { }`; + } + } + + class WritableStreamDefaultWriter { + constructor(stream) { + if (!isWritableStream(stream)) { + throw new TypeError("Invalid stream."); + } + if (isWritableStreamLocked(stream)) { + throw new TypeError("Cannot create a writer for a locked stream."); + } + this[sym.ownerWritableStream] = stream; + stream[sym.writer] = this; + const state = stream[sym.state]; + if (state === "writable") { + if ( + !writableStreamCloseQueuedOrInFlight(stream) && + stream[sym.backpressure] + ) { + this[sym.readyPromise] = getDeferred(); + } else { + this[sym.readyPromise] = { promise: Promise.resolve() }; + } + this[sym.closedPromise] = getDeferred(); + } else if (state === "erroring") { + this[sym.readyPromise] = { + promise: Promise.reject(stream[sym.storedError]), + }; + setPromiseIsHandledToTrue(this[sym.readyPromise].promise); + this[sym.closedPromise] = getDeferred(); + } else if (state === "closed") { + this[sym.readyPromise] = { promise: Promise.resolve() }; + this[sym.closedPromise] = { promise: Promise.resolve() }; + } else { + assert(state === "errored"); + const storedError = stream[sym.storedError]; + this[sym.readyPromise] = { promise: Promise.reject(storedError) }; + setPromiseIsHandledToTrue(this[sym.readyPromise].promise); + this[sym.closedPromise] = { promise: Promise.reject(storedError) }; + setPromiseIsHandledToTrue(this[sym.closedPromise].promise); + } + } + + get closed() { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + return this[sym.closedPromise].promise; + } + + get desiredSize() { + if (!isWritableStreamDefaultWriter(this)) { + throw new TypeError("Invalid WritableStreamDefaultWriter."); + } + if (!this[sym.ownerWritableStream]) { + throw new TypeError("WritableStreamDefaultWriter has no owner."); + } + return writableStreamDefaultWriterGetDesiredSize(this); + } + + get ready() { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + return this[sym.readyPromise].promise; + } + + abort(reason) { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + if (!this[sym.ownerWritableStream]) { + Promise.reject( + new TypeError("WritableStreamDefaultWriter has no owner."), + ); + } + return writableStreamDefaultWriterAbort(this, reason); + } + + close() { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + const stream = this[sym.ownerWritableStream]; + if (!stream) { + Promise.reject( + new TypeError("WritableStreamDefaultWriter has no owner."), + ); + } + if (writableStreamCloseQueuedOrInFlight(stream)) { + Promise.reject( + new TypeError("Stream is in an invalid state to be closed."), + ); + } + return writableStreamDefaultWriterClose(this); + } + + releaseLock() { + if (!isWritableStreamDefaultWriter(this)) { + throw new TypeError("Invalid WritableStreamDefaultWriter."); + } + const stream = this[sym.ownerWritableStream]; + if (!stream) { + return; + } + assert(stream[sym.writer]); + writableStreamDefaultWriterRelease(this); + } + + write(chunk) { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + if (!this[sym.ownerWritableStream]) { + Promise.reject( + new TypeError("WritableStreamDefaultWriter has no owner."), + ); + } + return writableStreamDefaultWriterWrite(this, chunk); + } + + [customInspect]() { + return `${this.constructor.name} { closed: Promise, desiredSize: ${ + String(this.desiredSize) + }, ready: Promise }`; + } + } + + class WritableStream { + constructor( + underlyingSink = {}, + strategy = {}, + ) { + initializeWritableStream(this); + const size = strategy.size; + let highWaterMark = strategy.highWaterMark ?? 1; + const { type } = underlyingSink; + if (type !== undefined) { + throw new RangeError(`Sink type of "${String(type)}" not supported.`); + } + const sizeAlgorithm = makeSizeAlgorithmFromSizeFunction(size); + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); + setUpWritableStreamDefaultControllerFromUnderlyingSink( + this, + underlyingSink, + highWaterMark, + sizeAlgorithm, + ); + } + + get locked() { + if (!isWritableStream(this)) { + throw new TypeError("Invalid WritableStream."); + } + return isWritableStreamLocked(this); + } + + abort(reason) { + if (!isWritableStream(this)) { + return Promise.reject(new TypeError("Invalid WritableStream.")); + } + if (isWritableStreamLocked(this)) { + return Promise.reject( + new TypeError("Cannot abort a locked WritableStream."), + ); + } + return writableStreamAbort(this, reason); + } + + close() { + if (!isWritableStream(this)) { + return Promise.reject(new TypeError("Invalid WritableStream.")); + } + if (isWritableStreamLocked(this)) { + return Promise.reject( + new TypeError("Cannot abort a locked WritableStream."), + ); + } + if (writableStreamCloseQueuedOrInFlight(this)) { + return Promise.reject( + new TypeError("Cannot close an already closing WritableStream."), + ); + } + return writableStreamClose(this); + } + + getWriter() { + if (!isWritableStream(this)) { + throw new TypeError("Invalid WritableStream."); + } + return acquireWritableStreamDefaultWriter(this); + } + + [customInspect]() { + return `${this.constructor.name} { locked: ${String(this.locked)} }`; + } + } + + function acquireReadableStreamDefaultReader( + stream, + forAuthorCode = false, + ) { + const reader = new ReadableStreamDefaultReader(stream); + reader[sym.forAuthorCode] = forAuthorCode; + return reader; + } + + function acquireWritableStreamDefaultWriter( + stream, + ) { + return new WritableStreamDefaultWriter(stream); + } + + function call( + fn, + v, + args, + ) { + return Function.prototype.apply.call(fn, v, args); + } + + function createAlgorithmFromUnderlyingMethod( + underlyingObject, + methodName, + algoArgCount, + ...extraArgs + ) { + const method = underlyingObject[methodName]; + if (method) { + if (!isCallable(method)) { + throw new TypeError("method is not callable"); + } + if (algoArgCount === 0) { + return async () => call(method, underlyingObject, extraArgs); + } else { + return async (arg) => { + const fullArgs = [arg, ...extraArgs]; + return call(method, underlyingObject, fullArgs); + }; + } + } + return async () => undefined; + } + + function createReadableStream( + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark = 1, + sizeAlgorithm = () => 1, + ) { + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); + const stream = Object.create( + ReadableStream.prototype, + ); + initializeReadableStream(stream); + const controller = Object.create( + ReadableStreamDefaultController.prototype, + ); + setUpReadableStreamDefaultController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + return stream; + } + + function createWritableStream( + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark = 1, + sizeAlgorithm = () => 1, + ) { + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); + const stream = Object.create(WritableStream.prototype); + initializeWritableStream(stream); + const controller = Object.create( + WritableStreamDefaultController.prototype, + ); + setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + return stream; + } + + function dequeueValue(container) { + assert(sym.queue in container && sym.queueTotalSize in container); + assert(container[sym.queue].length); + const pair = container[sym.queue].shift(); + container[sym.queueTotalSize] -= pair.size; + if (container[sym.queueTotalSize] <= 0) { + container[sym.queueTotalSize] = 0; + } + return pair.value; + } + + function enqueueValueWithSize( + container, + value, + size, + ) { + assert(sym.queue in container && sym.queueTotalSize in container); + size = Number(size); + if (!isFiniteNonNegativeNumber(size)) { + throw new RangeError("size must be a finite non-negative number."); + } + container[sym.queue].push({ value, size }); + container[sym.queueTotalSize] += size; + } + + /** Non-spec mechanism to "unwrap" a promise and store it to be resolved + * later. */ + function getDeferred() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve, reject: reject }; + } + + function initializeReadableStream( + stream, + ) { + stream[sym.state] = "readable"; + stream[sym.reader] = stream[sym.storedError] = undefined; + stream[sym.disturbed] = false; + } + + function initializeTransformStream( + stream, + startPromise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ) { + const startAlgorithm = () => startPromise; + const writeAlgorithm = (chunk) => + transformStreamDefaultSinkWriteAlgorithm(stream, chunk); + const abortAlgorithm = (reason) => + transformStreamDefaultSinkAbortAlgorithm(stream, reason); + const closeAlgorithm = () => + transformStreamDefaultSinkCloseAlgorithm(stream); + stream[sym.writable] = createWritableStream( + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + writableHighWaterMark, + writableSizeAlgorithm, + ); + const pullAlgorithm = () => + transformStreamDefaultSourcePullAlgorithm(stream); + const cancelAlgorithm = (reason) => { + transformStreamErrorWritableAndUnblockWrite(stream, reason); + return Promise.resolve(undefined); + }; + stream[sym.readable] = createReadableStream( + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ); + stream[sym.backpressure] = stream[sym.backpressureChangePromise] = + undefined; + transformStreamSetBackpressure(stream, true); + Object.defineProperty(stream, sym.transformStreamController, { + value: undefined, + configurable: true, + }); + } + + function initializeWritableStream( + stream, + ) { + stream[sym.state] = "writable"; + stream[sym.storedError] = stream[sym.writer] = stream[ + sym.writableStreamController + ] = stream[sym.inFlightWriteRequest] = stream[sym.closeRequest] = stream[ + sym.inFlightCloseRequest + ] = stream[sym.pendingAbortRequest] = undefined; + stream[sym.writeRequests] = []; + stream[sym.backpressure] = false; + } + + function invokeOrNoop( + o, + p, + ...args + ) { + assert(o); + const method = o[p]; + if (!method) { + return undefined; + } + return call(method, o, args); + } + + function isCallable(value) { + return typeof value === "function"; + } + + function isDetachedBuffer(value) { + return sym.isFakeDetached in value; + } + + function isFiniteNonNegativeNumber(v) { + return Number.isFinite(v) && (v) >= 0; + } + + function isReadableByteStreamController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledReadableByteStream in x) + ); + } + + function isReadableStream(x) { + return !( + typeof x !== "object" || + x === null || + !(sym.readableStreamController in x) + ); + } + + function isReadableStreamAsyncIterator( + x, + ) { + if (typeof x !== "object" || x === null) { + return false; + } + return sym.asyncIteratorReader in x; + } + + function isReadableStreamDefaultController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledReadableStream in x) + ); + } + + function isReadableStreamDefaultReader( + x, + ) { + return !(typeof x !== "object" || x === null || !(sym.readRequests in x)); + } + + function isReadableStreamLocked(stream) { + assert(isReadableStream(stream)); + return !!stream[sym.reader]; + } + + function isReadableStreamDisturbed(stream) { + assert(isReadableStream(stream)); + return !!stream[sym.disturbed]; + } + + function isTransformStream(x) { + return !( + typeof x !== "object" || + x === null || + !(sym.transformStreamController in x) + ); + } + + function isTransformStreamDefaultController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledTransformStream in x) + ); + } + + function isWritableStream(x) { + return !( + typeof x !== "object" || + x === null || + !(sym.writableStreamController in x) + ); + } + + function isWritableStreamDefaultController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledWritableStream in x) + ); + } + + function isWritableStreamDefaultWriter( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.ownerWritableStream in x) + ); + } + + function isWritableStreamLocked(stream) { + assert(isWritableStream(stream)); + return stream[sym.writer] !== undefined; + } + + function makeSizeAlgorithmFromSizeFunction( + size, + ) { + if (size === undefined) { + return () => 1; + } + if (typeof size !== "function") { + throw new TypeError("size must be callable."); + } + return (chunk) => { + return size.call(undefined, chunk); + }; + } + + function peekQueueValue(container) { + assert(sym.queue in container && sym.queueTotalSize in container); + assert(container[sym.queue].length); + const [pair] = container[sym.queue]; + return pair.value; + } + + function readableByteStreamControllerShouldCallPull( + controller, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if ( + stream[sym.state] !== "readable" || + controller[sym.closeRequested] || + !controller[sym.started] + ) { + return false; + } + if ( + readableStreamHasDefaultReader(stream) && + readableStreamGetNumReadRequests(stream) > 0 + ) { + return true; + } + // 3.13.25.6 If ! ReadableStreamHasBYOBReader(stream) is true and ! + // ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. + const desiredSize = readableByteStreamControllerGetDesiredSize(controller); + assert(desiredSize !== null); + return desiredSize > 0; + } + + function readableByteStreamControllerCallPullIfNeeded( + controller, + ) { + const shouldPull = readableByteStreamControllerShouldCallPull(controller); + if (!shouldPull) { + return; + } + if (controller[sym.pulling]) { + controller[sym.pullAgain] = true; + return; + } + assert(controller[sym.pullAgain] === false); + controller[sym.pulling] = true; + const pullPromise = controller[sym.pullAlgorithm](); + setPromiseIsHandledToTrue( + pullPromise.then( + () => { + controller[sym.pulling] = false; + if (controller[sym.pullAgain]) { + controller[sym.pullAgain] = false; + readableByteStreamControllerCallPullIfNeeded(controller); + } + }, + (e) => { + readableByteStreamControllerError(controller, e); + }, + ), + ); + } + + function readableByteStreamControllerClearAlgorithms( + controller, + ) { + controller[sym.pullAlgorithm] = undefined; + controller[sym.cancelAlgorithm] = undefined; + } + + function readableByteStreamControllerClose( + controller, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if (controller[sym.closeRequested] || stream[sym.state] !== "readable") { + return; + } + if (controller[sym.queueTotalSize] > 0) { + controller[sym.closeRequested] = true; + return; + } + // 3.13.6.4 If controller.[[pendingPullIntos]] is not empty, (BYOB Support) + readableByteStreamControllerClearAlgorithms(controller); + readableStreamClose(stream); + } + + function readableByteStreamControllerEnqueue( + controller, + chunk, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if (controller[sym.closeRequested] || stream[sym.state] !== "readable") { + return; + } + const { buffer, byteOffset, byteLength } = chunk; + const transferredBuffer = transferArrayBuffer(buffer); + if (readableStreamHasDefaultReader(stream)) { + if (readableStreamGetNumReadRequests(stream) === 0) { + readableByteStreamControllerEnqueueChunkToQueue( + controller, + transferredBuffer, + byteOffset, + byteLength, + ); + } else { + assert(controller[sym.queue].length === 0); + const transferredView = new Uint8Array( + transferredBuffer, + byteOffset, + byteLength, + ); + readableStreamFulfillReadRequest(stream, transferredView, false); + } + // 3.13.9.8 Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true + } else { + assert(!isReadableStreamLocked(stream)); + readableByteStreamControllerEnqueueChunkToQueue( + controller, + transferredBuffer, + byteOffset, + byteLength, + ); + } + readableByteStreamControllerCallPullIfNeeded(controller); + } + + function readableByteStreamControllerEnqueueChunkToQueue( + controller, + buffer, + byteOffset, + byteLength, + ) { + controller[sym.queue].push({ + value: buffer, + offset: byteOffset, + size: byteLength, + }); + controller[sym.queueTotalSize] += byteLength; + } + + function readableByteStreamControllerError( + controller, + e, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if (stream[sym.state] !== "readable") { + return; + } + // 3.13.11.3 Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). + resetQueue(controller); + readableByteStreamControllerClearAlgorithms(controller); + readableStreamError(stream, e); + } + + function readableByteStreamControllerGetDesiredSize( + controller, + ) { + const stream = controller[sym.controlledReadableByteStream]; + const state = stream[sym.state]; + if (state === "errored") { + return null; + } + if (state === "closed") { + return 0; + } + return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; + } + + function readableByteStreamControllerHandleQueueDrain( + controller, + ) { + assert( + controller[sym.controlledReadableByteStream][sym.state] === "readable", + ); + if ( + controller[sym.queueTotalSize] === 0 && controller[sym.closeRequested] + ) { + readableByteStreamControllerClearAlgorithms(controller); + readableStreamClose(controller[sym.controlledReadableByteStream]); + } else { + readableByteStreamControllerCallPullIfNeeded(controller); + } + } + + function readableStreamAddReadRequest( + stream, + ) { + assert(isReadableStreamDefaultReader(stream[sym.reader])); + assert(stream[sym.state] === "readable"); + const promise = getDeferred(); + stream[sym.reader][sym.readRequests].push(promise); + return promise.promise; + } + + function readableStreamCancel( + stream, + reason, + ) { + stream[sym.disturbed] = true; + if (stream[sym.state] === "closed") { + return Promise.resolve(); + } + if (stream[sym.state] === "errored") { + return Promise.reject(stream[sym.storedError]); + } + readableStreamClose(stream); + return stream[sym.readableStreamController][sym.cancelSteps](reason).then( + () => undefined, + ); + } + + function readableStreamClose(stream) { + assert(stream[sym.state] === "readable"); + stream[sym.state] = "closed"; + const reader = stream[sym.reader]; + if (!reader) { + return; + } + if (isReadableStreamDefaultReader(reader)) { + for (const readRequest of reader[sym.readRequests]) { + assert(readRequest.resolve); + readRequest.resolve( + readableStreamCreateReadResult( + undefined, + true, + reader[sym.forAuthorCode], + ), + ); + } + reader[sym.readRequests] = []; + } + const resolve = reader[sym.closedPromise].resolve; + assert(resolve); + resolve(); + } + + function readableStreamCreateReadResult( + value, + done, + forAuthorCode, + ) { + const prototype = forAuthorCode ? Object.prototype : null; + assert(typeof done === "boolean"); + const obj = Object.create(prototype); + Object.defineProperties(obj, { + value: { value, writable: true, enumerable: true, configurable: true }, + done: { + value: done, + writable: true, + enumerable: true, + configurable: true, + }, + }); + return obj; + } + + function readableStreamDefaultControllerCallPullIfNeeded( + controller, + ) { + const shouldPull = readableStreamDefaultControllerShouldCallPull( + controller, + ); + if (!shouldPull) { + return; + } + if (controller[sym.pulling]) { + controller[sym.pullAgain] = true; + return; + } + assert(controller[sym.pullAgain] === false); + controller[sym.pulling] = true; + const pullPromise = controller[sym.pullAlgorithm](); + pullPromise.then( + () => { + controller[sym.pulling] = false; + if (controller[sym.pullAgain]) { + controller[sym.pullAgain] = false; + readableStreamDefaultControllerCallPullIfNeeded(controller); + } + }, + (e) => { + readableStreamDefaultControllerError(controller, e); + }, + ); + } + + function readableStreamDefaultControllerCanCloseOrEnqueue( + controller, + ) { + const state = controller[sym.controlledReadableStream][sym.state]; + return !controller[sym.closeRequested] && state === "readable"; + } + + function readableStreamDefaultControllerClearAlgorithms( + controller, + ) { + controller[sym.pullAlgorithm] = undefined; + controller[sym.cancelAlgorithm] = undefined; + controller[sym.strategySizeAlgorithm] = undefined; + } + + function readableStreamDefaultControllerClose( + controller, + ) { + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller[sym.controlledReadableStream]; + controller[sym.closeRequested] = true; + if (controller[sym.queue].length === 0) { + readableStreamDefaultControllerClearAlgorithms(controller); + readableStreamClose(stream); + } + } + + function readableStreamDefaultControllerEnqueue( + controller, + chunk, + ) { + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller[sym.controlledReadableStream]; + if ( + isReadableStreamLocked(stream) && + readableStreamGetNumReadRequests(stream) > 0 + ) { + readableStreamFulfillReadRequest(stream, chunk, false); + } else { + try { + const chunkSize = controller[sym.strategySizeAlgorithm](chunk); + enqueueValueWithSize(controller, chunk, chunkSize); + } catch (err) { + readableStreamDefaultControllerError(controller, err); + throw err; + } + } + readableStreamDefaultControllerCallPullIfNeeded(controller); + } + + function readableStreamDefaultControllerGetDesiredSize( + controller, + ) { + const stream = controller[sym.controlledReadableStream]; + const state = stream[sym.state]; + if (state === "errored") { + return null; + } + if (state === "closed") { + return 0; + } + return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; + } + + function readableStreamDefaultControllerError( + controller, + e, + ) { + const stream = controller[sym.controlledReadableStream]; + if (stream[sym.state] !== "readable") { + return; + } + resetQueue(controller); + readableStreamDefaultControllerClearAlgorithms(controller); + readableStreamError(stream, e); + } + + function readableStreamDefaultControllerHasBackpressure( + controller, + ) { + return readableStreamDefaultControllerShouldCallPull(controller); + } + + function readableStreamDefaultControllerShouldCallPull( + controller, + ) { + const stream = controller[sym.controlledReadableStream]; + if ( + !readableStreamDefaultControllerCanCloseOrEnqueue(controller) || + controller[sym.started] === false + ) { + return false; + } + if ( + isReadableStreamLocked(stream) && + readableStreamGetNumReadRequests(stream) > 0 + ) { + return true; + } + const desiredSize = readableStreamDefaultControllerGetDesiredSize( + controller, + ); + assert(desiredSize !== null); + return desiredSize > 0; + } + + function readableStreamDefaultReaderRead( + reader, + ) { + const stream = reader[sym.ownerReadableStream]; + assert(stream); + stream[sym.disturbed] = true; + if (stream[sym.state] === "closed") { + return Promise.resolve( + readableStreamCreateReadResult( + undefined, + true, + reader[sym.forAuthorCode], + ), + ); + } + if (stream[sym.state] === "errored") { + return Promise.reject(stream[sym.storedError]); + } + assert(stream[sym.state] === "readable"); + return (stream[ + sym.readableStreamController + ])[sym.pullSteps](); + } + + function readableStreamError(stream, e) { + assert(isReadableStream(stream)); + assert(stream[sym.state] === "readable"); + stream[sym.state] = "errored"; + stream[sym.storedError] = e; + const reader = stream[sym.reader]; + if (reader === undefined) { + return; + } + if (isReadableStreamDefaultReader(reader)) { + for (const readRequest of reader[sym.readRequests]) { + assert(readRequest.reject); + readRequest.reject(e); + readRequest.reject = undefined; + readRequest.resolve = undefined; + } + reader[sym.readRequests] = []; + } + // 3.5.6.8 Otherwise, support BYOB Reader + reader[sym.closedPromise].reject(e); + reader[sym.closedPromise].reject = undefined; + reader[sym.closedPromise].resolve = undefined; + setPromiseIsHandledToTrue(reader[sym.closedPromise].promise); + } + + function readableStreamFulfillReadRequest( + stream, + chunk, + done, + ) { + const reader = stream[sym.reader]; + const readRequest = reader[sym.readRequests].shift(); + assert(readRequest.resolve); + readRequest.resolve( + readableStreamCreateReadResult(chunk, done, reader[sym.forAuthorCode]), + ); + } + + function readableStreamGetNumReadRequests( + stream, + ) { + return stream[sym.reader]?.[sym.readRequests].length ?? 0; + } + + function readableStreamHasDefaultReader( + stream, + ) { + const reader = stream[sym.reader]; + return !(reader === undefined || !isReadableStreamDefaultReader(reader)); + } + + function readableStreamPipeTo( + source, + dest, + preventClose, + preventAbort, + preventCancel, + signal, + ) { + assert(isReadableStream(source)); + assert(isWritableStream(dest)); + assert( + typeof preventClose === "boolean" && + typeof preventAbort === "boolean" && + typeof preventCancel === "boolean", + ); + assert(signal === undefined || signal instanceof AbortSignal); + assert(!isReadableStreamLocked(source)); + assert(!isWritableStreamLocked(dest)); + const reader = acquireReadableStreamDefaultReader(source); + const writer = acquireWritableStreamDefaultWriter(dest); + source[sym.disturbed] = true; + let shuttingDown = false; + const promise = getDeferred(); + let abortAlgorithm; + if (signal) { + abortAlgorithm = () => { + const error = new DOMException("Abort signal received.", "AbortSignal"); + const actions = []; + if (!preventAbort) { + actions.push(() => { + if (dest[sym.state] === "writable") { + return writableStreamAbort(dest, error); + } else { + return Promise.resolve(undefined); + } + }); + } + if (!preventCancel) { + actions.push(() => { + if (source[sym.state] === "readable") { + return readableStreamCancel(source, error); + } else { + return Promise.resolve(undefined); + } + }); + } + shutdownWithAction( + () => Promise.all(actions.map((action) => action())), + true, + error, + ); + }; + if (signal.aborted) { + abortAlgorithm(); + return promise.promise; + } + signal.addEventListener("abort", abortAlgorithm); + } + + let currentWrite = Promise.resolve(); + + // At this point, the spec becomes non-specific and vague. Most of the rest + // of this code is based on the reference implementation that is part of the + // specification. This is why the functions are only scoped to this function + // to ensure they don't leak into the spec compliant parts. + + function isOrBecomesClosed( + stream, + promise, + action, + ) { + if (stream[sym.state] === "closed") { + action(); + } else { + setPromiseIsHandledToTrue(promise.then(action)); + } + } + + function isOrBecomesErrored( + stream, + promise, + action, + ) { + if (stream[sym.state] === "errored") { + action(stream[sym.storedError]); + } else { + setPromiseIsHandledToTrue(promise.catch((error) => action(error))); + } + } + + function finalize(isError, error) { + writableStreamDefaultWriterRelease(writer); + readableStreamReaderGenericRelease(reader); + + if (signal) { + signal.removeEventListener("abort", abortAlgorithm); + } + if (isError) { + promise.reject(error); + } else { + promise.resolve(); + } + } + + function waitForWritesToFinish() { + const oldCurrentWrite = currentWrite; + return currentWrite.then(() => + oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined + ); + } + + function shutdownWithAction( + action, + originalIsError, + originalError, + ) { + function doTheRest() { + setPromiseIsHandledToTrue( + action().then( + () => finalize(originalIsError, originalError), + (newError) => finalize(true, newError), + ), + ); + } + + if (shuttingDown) { + return; + } + shuttingDown = true; + + if ( + dest[sym.state] === "writable" && + writableStreamCloseQueuedOrInFlight(dest) === false + ) { + setPromiseIsHandledToTrue(waitForWritesToFinish().then(doTheRest)); + } else { + doTheRest(); + } + } + + function shutdown(isError, error) { + if (shuttingDown) { + return; + } + shuttingDown = true; + + if ( + dest[sym.state] === "writable" && + !writableStreamCloseQueuedOrInFlight(dest) + ) { + setPromiseIsHandledToTrue( + waitForWritesToFinish().then(() => finalize(isError, error)), + ); + } + finalize(isError, error); + } + + function pipeStep() { + if (shuttingDown) { + return Promise.resolve(true); + } + return writer[sym.readyPromise].promise.then(() => { + return readableStreamDefaultReaderRead(reader).then( + ({ value, done }) => { + if (done === true) { + return true; + } + currentWrite = writableStreamDefaultWriterWrite( + writer, + value, + ).then(undefined, () => {}); + return false; + }, + ); + }); + } + + function pipeLoop() { + return new Promise((resolveLoop, rejectLoop) => { + function next(done) { + if (done) { + resolveLoop(undefined); + } else { + setPromiseIsHandledToTrue(pipeStep().then(next, rejectLoop)); + } + } + next(false); + }); + } + + isOrBecomesErrored( + source, + reader[sym.closedPromise].promise, + (storedError) => { + if (!preventAbort) { + shutdownWithAction( + () => writableStreamAbort(dest, storedError), + true, + storedError, + ); + } else { + shutdown(true, storedError); + } + }, + ); + + isOrBecomesErrored( + dest, + writer[sym.closedPromise].promise, + (storedError) => { + if (!preventCancel) { + shutdownWithAction( + () => readableStreamCancel(source, storedError), + true, + storedError, + ); + } else { + shutdown(true, storedError); + } + }, + ); + + isOrBecomesClosed(source, reader[sym.closedPromise].promise, () => { + if (!preventClose) { + shutdownWithAction(() => + writableStreamDefaultWriterCloseWithErrorPropagation(writer) + ); + } + }); + + if ( + writableStreamCloseQueuedOrInFlight(dest) || + dest[sym.state] === "closed" + ) { + const destClosed = new TypeError( + "The destination writable stream closed before all data could be piped to it.", + ); + if (!preventCancel) { + shutdownWithAction( + () => readableStreamCancel(source, destClosed), + true, + destClosed, + ); + } else { + shutdown(true, destClosed); + } + } + + setPromiseIsHandledToTrue(pipeLoop()); + return promise.promise; + } + + function readableStreamReaderGenericCancel( + reader, + reason, + ) { + const stream = reader[sym.ownerReadableStream]; + assert(stream); + return readableStreamCancel(stream, reason); + } + + function readableStreamReaderGenericInitialize( + reader, + stream, + ) { + reader[sym.forAuthorCode] = true; + reader[sym.ownerReadableStream] = stream; + stream[sym.reader] = reader; + if (stream[sym.state] === "readable") { + reader[sym.closedPromise] = getDeferred(); + } else if (stream[sym.state] === "closed") { + reader[sym.closedPromise] = { promise: Promise.resolve() }; + } else { + assert(stream[sym.state] === "errored"); + reader[sym.closedPromise] = { + promise: Promise.reject(stream[sym.storedError]), + }; + setPromiseIsHandledToTrue(reader[sym.closedPromise].promise); + } + } + + function readableStreamReaderGenericRelease( + reader, + ) { + assert(reader[sym.ownerReadableStream]); + assert(reader[sym.ownerReadableStream][sym.reader] === reader); + const closedPromise = reader[sym.closedPromise]; + if (reader[sym.ownerReadableStream][sym.state] === "readable") { + assert(closedPromise.reject); + closedPromise.reject(new TypeError("ReadableStream state is readable.")); + } else { + closedPromise.promise = Promise.reject( + new TypeError("Reading is closed."), + ); + delete closedPromise.reject; + delete closedPromise.resolve; + } + setPromiseIsHandledToTrue(closedPromise.promise); + reader[sym.ownerReadableStream][sym.reader] = undefined; + reader[sym.ownerReadableStream] = undefined; + } + + function readableStreamTee( + stream, + cloneForBranch2, + ) { + assert(isReadableStream(stream)); + assert(typeof cloneForBranch2 === "boolean"); + const reader = acquireReadableStreamDefaultReader(stream); + let reading = false; + let canceled1 = false; + let canceled2 = false; + let reason1 = undefined; + let reason2 = undefined; + /* eslint-disable prefer-const */ + let branch1; + let branch2; + /* eslint-enable prefer-const */ + const cancelPromise = getDeferred(); + const pullAlgorithm = () => { + if (reading) { + return Promise.resolve(); + } + reading = true; + const readPromise = readableStreamDefaultReaderRead(reader).then( + (result) => { + reading = false; + assert(typeof result === "object"); + const { done } = result; + assert(typeof done === "boolean"); + if (done) { + if (!canceled1) { + readableStreamDefaultControllerClose( + branch1[ + sym.readableStreamController + ], + ); + } + if (!canceled2) { + readableStreamDefaultControllerClose( + branch2[ + sym.readableStreamController + ], + ); + } + return; + } + const { value } = result; + const value1 = value; + let value2 = value; + if (!canceled2 && cloneForBranch2) { + value2 = cloneValue(value2); + } + if (!canceled1) { + readableStreamDefaultControllerEnqueue( + branch1[ + sym.readableStreamController + ], + value1, + ); + } + if (!canceled2) { + readableStreamDefaultControllerEnqueue( + branch2[ + sym.readableStreamController + ], + value2, + ); + } + }, + ); + setPromiseIsHandledToTrue(readPromise); + return Promise.resolve(); + }; + const cancel1Algorithm = (reason) => { + canceled1 = true; + reason1 = reason; + if (canceled2) { + const compositeReason = [reason1, reason2]; + const cancelResult = readableStreamCancel(stream, compositeReason); + cancelPromise.resolve(cancelResult); + } + return cancelPromise.promise; + }; + const cancel2Algorithm = (reason) => { + canceled2 = true; + reason2 = reason; + if (canceled1) { + const compositeReason = [reason1, reason2]; + const cancelResult = readableStreamCancel(stream, compositeReason); + cancelPromise.resolve(cancelResult); + } + return cancelPromise.promise; + }; + const startAlgorithm = () => undefined; + branch1 = createReadableStream( + startAlgorithm, + pullAlgorithm, + cancel1Algorithm, + ); + branch2 = createReadableStream( + startAlgorithm, + pullAlgorithm, + cancel2Algorithm, + ); + setPromiseIsHandledToTrue( + reader[sym.closedPromise].promise.catch((r) => { + readableStreamDefaultControllerError( + branch1[ + sym.readableStreamController + ], + r, + ); + readableStreamDefaultControllerError( + branch2[ + sym.readableStreamController + ], + r, + ); + }), + ); + return [branch1, branch2]; + } + + function resetQueue(container) { + assert(sym.queue in container && sym.queueTotalSize in container); + container[sym.queue] = []; + container[sym.queueTotalSize] = 0; + } + + /** An internal function which mimics the behavior of setting the promise to + * handled in JavaScript. In this situation, an assertion failure, which + * shouldn't happen will get thrown, instead of swallowed. */ + function setPromiseIsHandledToTrue(promise) { + promise.then(undefined, (e) => { + if (e && e instanceof AssertionError) { + queueMicrotask(() => { + throw e; + }); + } + }); + } + + function setUpReadableByteStreamController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + autoAllocateChunkSize, + ) { + assert(stream[sym.readableStreamController] === undefined); + if (autoAllocateChunkSize !== undefined) { + assert(Number.isInteger(autoAllocateChunkSize)); + assert(autoAllocateChunkSize >= 0); + } + controller[sym.controlledReadableByteStream] = stream; + controller[sym.pulling] = controller[sym.pullAgain] = false; + controller[sym.byobRequest] = undefined; + controller[sym.queue] = []; + controller[sym.queueTotalSize] = 0; + controller[sym.closeRequested] = controller[sym.started] = false; + controller[sym.strategyHWM] = validateAndNormalizeHighWaterMark( + highWaterMark, + ); + controller[sym.pullAlgorithm] = pullAlgorithm; + controller[sym.cancelAlgorithm] = cancelAlgorithm; + controller[sym.autoAllocateChunkSize] = autoAllocateChunkSize; + // 3.13.26.12 Set controller.[[pendingPullIntos]] to a new empty List. + stream[sym.readableStreamController] = controller; + const startResult = startAlgorithm(); + const startPromise = Promise.resolve(startResult); + setPromiseIsHandledToTrue( + startPromise.then( + () => { + controller[sym.started] = true; + assert(!controller[sym.pulling]); + assert(!controller[sym.pullAgain]); + readableByteStreamControllerCallPullIfNeeded(controller); + }, + (r) => { + readableByteStreamControllerError(controller, r); + }, + ), + ); + } + + function setUpReadableByteStreamControllerFromUnderlyingSource( + stream, + underlyingByteSource, + highWaterMark, + ) { + assert(underlyingByteSource); + const controller = Object.create( + ReadableByteStreamController.prototype, + ); + const startAlgorithm = () => { + return invokeOrNoop(underlyingByteSource, "start", controller); + }; + const pullAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingByteSource, + "pull", + 0, + controller, + ); + setFunctionName(pullAlgorithm, "[[pullAlgorithm]]"); + const cancelAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingByteSource, + "cancel", + 1, + ); + setFunctionName(cancelAlgorithm, "[[cancelAlgorithm]]"); + // 3.13.27.6 Let autoAllocateChunkSize be ? GetV(underlyingByteSource, "autoAllocateChunkSize"). + const autoAllocateChunkSize = undefined; + setUpReadableByteStreamController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + autoAllocateChunkSize, + ); + } + + function setUpReadableStreamDefaultController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + sizeAlgorithm, + ) { + assert(stream[sym.readableStreamController] === undefined); + controller[sym.controlledReadableStream] = stream; + controller[sym.queue] = []; + controller[sym.queueTotalSize] = 0; + controller[sym.started] = controller[sym.closeRequested] = controller[ + sym.pullAgain + ] = controller[sym.pulling] = false; + controller[sym.strategySizeAlgorithm] = sizeAlgorithm; + controller[sym.strategyHWM] = highWaterMark; + controller[sym.pullAlgorithm] = pullAlgorithm; + controller[sym.cancelAlgorithm] = cancelAlgorithm; + stream[sym.readableStreamController] = controller; + const startResult = startAlgorithm(); + const startPromise = Promise.resolve(startResult); + setPromiseIsHandledToTrue( + startPromise.then( + () => { + controller[sym.started] = true; + assert(controller[sym.pulling] === false); + assert(controller[sym.pullAgain] === false); + readableStreamDefaultControllerCallPullIfNeeded(controller); + }, + (r) => { + readableStreamDefaultControllerError(controller, r); + }, + ), + ); + } + + function setUpReadableStreamDefaultControllerFromUnderlyingSource( + stream, + underlyingSource, + highWaterMark, + sizeAlgorithm, + ) { + assert(underlyingSource); + const controller = Object.create( + ReadableStreamDefaultController.prototype, + ); + const startAlgorithm = () => + invokeOrNoop(underlyingSource, "start", controller); + const pullAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSource, + "pull", + 0, + controller, + ); + setFunctionName(pullAlgorithm, "[[pullAlgorithm]]"); + const cancelAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSource, + "cancel", + 1, + ); + setFunctionName(cancelAlgorithm, "[[cancelAlgorithm]]"); + setUpReadableStreamDefaultController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + } + + function setUpTransformStreamDefaultController( + stream, + controller, + transformAlgorithm, + flushAlgorithm, + ) { + assert(isTransformStream(stream)); + assert(stream[sym.transformStreamController] === undefined); + controller[sym.controlledTransformStream] = stream; + stream[sym.transformStreamController] = controller; + controller[sym.transformAlgorithm] = transformAlgorithm; + controller[sym.flushAlgorithm] = flushAlgorithm; + } + + function setUpTransformStreamDefaultControllerFromTransformer( + stream, + transformer, + ) { + assert(transformer); + const controller = Object.create( + TransformStreamDefaultController.prototype, + ); + let transformAlgorithm = (chunk) => { + try { + transformStreamDefaultControllerEnqueue( + controller, + // it defaults to no tranformation, so I is assumed to be O + chunk, + ); + } catch (e) { + return Promise.reject(e); + } + return Promise.resolve(); + }; + const transformMethod = transformer.transform; + if (transformMethod) { + if (typeof transformMethod !== "function") { + throw new TypeError("tranformer.transform must be callable."); + } + transformAlgorithm = async (chunk) => + call(transformMethod, transformer, [chunk, controller]); + } + const flushAlgorithm = createAlgorithmFromUnderlyingMethod( + transformer, + "flush", + 0, + controller, + ); + setUpTransformStreamDefaultController( + stream, + controller, + transformAlgorithm, + flushAlgorithm, + ); + } + + function setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ) { + assert(isWritableStream(stream)); + assert(stream[sym.writableStreamController] === undefined); + controller[sym.controlledWritableStream] = stream; + stream[sym.writableStreamController] = controller; + controller[sym.queue] = []; + controller[sym.queueTotalSize] = 0; + controller[sym.started] = false; + controller[sym.strategySizeAlgorithm] = sizeAlgorithm; + controller[sym.strategyHWM] = highWaterMark; + controller[sym.writeAlgorithm] = writeAlgorithm; + controller[sym.closeAlgorithm] = closeAlgorithm; + controller[sym.abortAlgorithm] = abortAlgorithm; + const backpressure = writableStreamDefaultControllerGetBackpressure( + controller, + ); + writableStreamUpdateBackpressure(stream, backpressure); + const startResult = startAlgorithm(); + const startPromise = Promise.resolve(startResult); + setPromiseIsHandledToTrue( + startPromise.then( + () => { + assert( + stream[sym.state] === "writable" || + stream[sym.state] === "erroring", + ); + controller[sym.started] = true; + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, + (r) => { + assert( + stream[sym.state] === "writable" || + stream[sym.state] === "erroring", + ); + controller[sym.started] = true; + writableStreamDealWithRejection(stream, r); + }, + ), + ); + } + + function setUpWritableStreamDefaultControllerFromUnderlyingSink( + stream, + underlyingSink, + highWaterMark, + sizeAlgorithm, + ) { + assert(underlyingSink); + const controller = Object.create( + WritableStreamDefaultController.prototype, + ); + const startAlgorithm = () => { + return invokeOrNoop(underlyingSink, "start", controller); + }; + const writeAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSink, + "write", + 1, + controller, + ); + setFunctionName(writeAlgorithm, "[[writeAlgorithm]]"); + const closeAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSink, + "close", + 0, + ); + setFunctionName(closeAlgorithm, "[[closeAlgorithm]]"); + const abortAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSink, + "abort", + 1, + ); + setFunctionName(abortAlgorithm, "[[abortAlgorithm]]"); + setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + } + + function transformStreamDefaultControllerClearAlgorithms( + controller, + ) { + controller[sym.transformAlgorithm] = undefined; + controller[sym.flushAlgorithm] = undefined; + } + + function transformStreamDefaultControllerEnqueue( + controller, + chunk, + ) { + const stream = controller[sym.controlledTransformStream]; + const readableController = stream[sym.readable][ + sym.readableStreamController + ]; + if (!readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) { + throw new TypeError( + "TransformStream's readable controller cannot be closed or enqueued.", + ); + } + try { + readableStreamDefaultControllerEnqueue(readableController, chunk); + } catch (e) { + transformStreamErrorWritableAndUnblockWrite(stream, e); + throw stream[sym.readable][sym.storedError]; + } + const backpressure = readableStreamDefaultControllerHasBackpressure( + readableController, + ); + if (backpressure) { + transformStreamSetBackpressure(stream, true); + } + } + + function transformStreamDefaultControllerError( + controller, + e, + ) { + transformStreamError(controller[sym.controlledTransformStream], e); + } + + function transformStreamDefaultControllerPerformTransform( + controller, + chunk, + ) { + const transformPromise = controller[sym.transformAlgorithm](chunk); + return transformPromise.then(undefined, (r) => { + transformStreamError(controller[sym.controlledTransformStream], r); + throw r; + }); + } + + function transformStreamDefaultSinkAbortAlgorithm( + stream, + reason, + ) { + transformStreamError(stream, reason); + return Promise.resolve(undefined); + } + + function transformStreamDefaultSinkCloseAlgorithm( + stream, + ) { + const readable = stream[sym.readable]; + const controller = stream[sym.transformStreamController]; + const flushPromise = controller[sym.flushAlgorithm](); + transformStreamDefaultControllerClearAlgorithms(controller); + return flushPromise.then( + () => { + if (readable[sym.state] === "errored") { + throw readable[sym.storedError]; + } + const readableController = readable[ + sym.readableStreamController + ]; + if ( + readableStreamDefaultControllerCanCloseOrEnqueue(readableController) + ) { + readableStreamDefaultControllerClose(readableController); + } + }, + (r) => { + transformStreamError(stream, r); + throw readable[sym.storedError]; + }, + ); + } + + function transformStreamDefaultSinkWriteAlgorithm( + stream, + chunk, + ) { + assert(stream[sym.writable][sym.state] === "writable"); + const controller = stream[sym.transformStreamController]; + if (stream[sym.backpressure]) { + const backpressureChangePromise = stream[sym.backpressureChangePromise]; + assert(backpressureChangePromise); + return backpressureChangePromise.promise.then(() => { + const writable = stream[sym.writable]; + const state = writable[sym.state]; + if (state === "erroring") { + throw writable[sym.storedError]; + } + assert(state === "writable"); + return transformStreamDefaultControllerPerformTransform( + controller, + chunk, + ); + }); + } + return transformStreamDefaultControllerPerformTransform(controller, chunk); + } + + function transformStreamDefaultSourcePullAlgorithm( + stream, + ) { + assert(stream[sym.backpressure] === true); + assert(stream[sym.backpressureChangePromise] !== undefined); + transformStreamSetBackpressure(stream, false); + return stream[sym.backpressureChangePromise].promise; + } + + function transformStreamError( + stream, + e, + ) { + readableStreamDefaultControllerError( + stream[sym.readable][ + sym.readableStreamController + ], + e, + ); + transformStreamErrorWritableAndUnblockWrite(stream, e); + } + + function transformStreamDefaultControllerTerminate( + controller, + ) { + const stream = controller[sym.controlledTransformStream]; + const readableController = stream[sym.readable][ + sym.readableStreamController + ]; + readableStreamDefaultControllerClose(readableController); + const error = new TypeError("TransformStream is closed."); + transformStreamErrorWritableAndUnblockWrite(stream, error); + } + + function transformStreamErrorWritableAndUnblockWrite( + stream, + e, + ) { + transformStreamDefaultControllerClearAlgorithms( + stream[sym.transformStreamController], + ); + writableStreamDefaultControllerErrorIfNeeded( + stream[sym.writable][sym.writableStreamController], + e, + ); + if (stream[sym.backpressure]) { + transformStreamSetBackpressure(stream, false); + } + } + + function transformStreamSetBackpressure( + stream, + backpressure, + ) { + assert(stream[sym.backpressure] !== backpressure); + if (stream[sym.backpressureChangePromise] !== undefined) { + stream[sym.backpressureChangePromise].resolve(undefined); + } + stream[sym.backpressureChangePromise] = getDeferred(); + stream[sym.backpressure] = backpressure; + } + + function transferArrayBuffer(buffer) { + assert(!isDetachedBuffer(buffer)); + const transferredIshVersion = buffer.slice(0); + + Object.defineProperty(buffer, "byteLength", { + get() { + return 0; + }, + }); + buffer[sym.isFakeDetached] = true; + + return transferredIshVersion; + } + + function validateAndNormalizeHighWaterMark( + highWaterMark, + ) { + highWaterMark = Number(highWaterMark); + if (Number.isNaN(highWaterMark) || highWaterMark < 0) { + throw new RangeError( + `highWaterMark must be a positive number or Infinity. Received: ${highWaterMark}.`, + ); + } + return highWaterMark; + } + + function writableStreamAbort( + stream, + reason, + ) { + const state = stream[sym.state]; + if (state === "closed" || state === "errored") { + return Promise.resolve(undefined); + } + if (stream[sym.pendingAbortRequest]) { + return stream[sym.pendingAbortRequest].promise.promise; + } + assert(state === "writable" || state === "erroring"); + let wasAlreadyErroring = false; + if (state === "erroring") { + wasAlreadyErroring = true; + reason = undefined; + } + const promise = getDeferred(); + stream[sym.pendingAbortRequest] = { promise, reason, wasAlreadyErroring }; + + if (wasAlreadyErroring === false) { + writableStreamStartErroring(stream, reason); + } + return promise.promise; + } + + function writableStreamAddWriteRequest( + stream, + ) { + assert(isWritableStream(stream)); + assert(stream[sym.state] === "writable"); + const promise = getDeferred(); + stream[sym.writeRequests].push(promise); + return promise.promise; + } + + function writableStreamClose( + stream, + ) { + const state = stream[sym.state]; + if (state === "closed" || state === "errored") { + return Promise.reject( + new TypeError( + "Cannot close an already closed or errored WritableStream.", + ), + ); + } + assert(!writableStreamCloseQueuedOrInFlight(stream)); + const promise = getDeferred(); + stream[sym.closeRequest] = promise; + const writer = stream[sym.writer]; + if (writer && stream[sym.backpressure] && state === "writable") { + writer[sym.readyPromise].resolve(); + writer[sym.readyPromise].resolve = undefined; + writer[sym.readyPromise].reject = undefined; + } + writableStreamDefaultControllerClose(stream[sym.writableStreamController]); + return promise.promise; + } + + function writableStreamCloseQueuedOrInFlight( + stream, + ) { + return !( + stream[sym.closeRequest] === undefined && + stream[sym.inFlightCloseRequest] === undefined + ); + } + + function writableStreamDealWithRejection( + stream, + error, + ) { + const state = stream[sym.state]; + if (state === "writable") { + writableStreamStartErroring(stream, error); + return; + } + assert(state === "erroring"); + writableStreamFinishErroring(stream); + } + + function writableStreamDefaultControllerAdvanceQueueIfNeeded( + controller, + ) { + const stream = controller[sym.controlledWritableStream]; + if (!controller[sym.started]) { + return; + } + if (stream[sym.inFlightWriteRequest]) { + return; + } + const state = stream[sym.state]; + assert(state !== "closed" && state !== "errored"); + if (state === "erroring") { + writableStreamFinishErroring(stream); + return; + } + if (!controller[sym.queue].length) { + return; + } + const writeRecord = peekQueueValue(controller); + if (writeRecord === "close") { + writableStreamDefaultControllerProcessClose(controller); + } else { + writableStreamDefaultControllerProcessWrite( + controller, + writeRecord.chunk, + ); + } + } + + function writableStreamDefaultControllerClearAlgorithms( + controller, + ) { + controller[sym.writeAlgorithm] = undefined; + controller[sym.closeAlgorithm] = undefined; + controller[sym.abortAlgorithm] = undefined; + controller[sym.strategySizeAlgorithm] = undefined; + } + + function writableStreamDefaultControllerClose( + controller, + ) { + enqueueValueWithSize(controller, "close", 0); + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + + function writableStreamDefaultControllerError( + controller, + error, + ) { + const stream = controller[sym.controlledWritableStream]; + assert(stream[sym.state] === "writable"); + writableStreamDefaultControllerClearAlgorithms(controller); + writableStreamStartErroring(stream, error); + } + + function writableStreamDefaultControllerErrorIfNeeded( + controller, + error, + ) { + if (controller[sym.controlledWritableStream][sym.state] === "writable") { + writableStreamDefaultControllerError(controller, error); + } + } + + function writableStreamDefaultControllerGetBackpressure( + controller, + ) { + const desiredSize = writableStreamDefaultControllerGetDesiredSize( + controller, + ); + return desiredSize <= 0; + } + + function writableStreamDefaultControllerGetChunkSize( + controller, + chunk, + ) { + let returnValue; + try { + returnValue = controller[sym.strategySizeAlgorithm](chunk); + } catch (e) { + writableStreamDefaultControllerErrorIfNeeded(controller, e); + return 1; + } + return returnValue; + } + + function writableStreamDefaultControllerGetDesiredSize( + controller, + ) { + return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; + } + + function writableStreamDefaultControllerProcessClose( + controller, + ) { + const stream = controller[sym.controlledWritableStream]; + writableStreamMarkCloseRequestInFlight(stream); + dequeueValue(controller); + assert(controller[sym.queue].length === 0); + const sinkClosePromise = controller[sym.closeAlgorithm](); + writableStreamDefaultControllerClearAlgorithms(controller); + setPromiseIsHandledToTrue( + sinkClosePromise.then( + () => { + writableStreamFinishInFlightClose(stream); + }, + (reason) => { + writableStreamFinishInFlightCloseWithError(stream, reason); + }, + ), + ); + } + + function writableStreamDefaultControllerProcessWrite( + controller, + chunk, + ) { + const stream = controller[sym.controlledWritableStream]; + writableStreamMarkFirstWriteRequestInFlight(stream); + const sinkWritePromise = controller[sym.writeAlgorithm](chunk); + setPromiseIsHandledToTrue( + sinkWritePromise.then( + () => { + writableStreamFinishInFlightWrite(stream); + const state = stream[sym.state]; + assert(state === "writable" || state === "erroring"); + dequeueValue(controller); + if ( + !writableStreamCloseQueuedOrInFlight(stream) && + state === "writable" + ) { + const backpressure = writableStreamDefaultControllerGetBackpressure( + controller, + ); + writableStreamUpdateBackpressure(stream, backpressure); + } + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, + (reason) => { + if (stream[sym.state] === "writable") { + writableStreamDefaultControllerClearAlgorithms(controller); + } + writableStreamFinishInFlightWriteWithError(stream, reason); + }, + ), + ); + } + + function writableStreamDefaultControllerWrite( + controller, + chunk, + chunkSize, + ) { + const writeRecord = { chunk }; + try { + enqueueValueWithSize(controller, writeRecord, chunkSize); + } catch (e) { + writableStreamDefaultControllerErrorIfNeeded(controller, e); + return; + } + const stream = controller[sym.controlledWritableStream]; + if ( + !writableStreamCloseQueuedOrInFlight(stream) && + stream[sym.state] === "writable" + ) { + const backpressure = writableStreamDefaultControllerGetBackpressure( + controller, + ); + writableStreamUpdateBackpressure(stream, backpressure); + } + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + + function writableStreamDefaultWriterAbort( + writer, + reason, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + return writableStreamAbort(stream, reason); + } + + function writableStreamDefaultWriterClose( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + return writableStreamClose(stream); + } + + function writableStreamDefaultWriterCloseWithErrorPropagation( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + const state = stream[sym.state]; + if (writableStreamCloseQueuedOrInFlight(stream) || state === "closed") { + return Promise.resolve(); + } + if (state === "errored") { + return Promise.reject(stream[sym.storedError]); + } + assert(state === "writable" || state === "erroring"); + return writableStreamDefaultWriterClose(writer); + } + + function writableStreamDefaultWriterEnsureClosePromiseRejected( + writer, + error, + ) { + if (writer[sym.closedPromise].reject) { + writer[sym.closedPromise].reject(error); + } else { + writer[sym.closedPromise] = { + promise: Promise.reject(error), + }; + } + setPromiseIsHandledToTrue(writer[sym.closedPromise].promise); + } + + function writableStreamDefaultWriterEnsureReadyPromiseRejected( + writer, + error, + ) { + if (writer[sym.readyPromise].reject) { + writer[sym.readyPromise].reject(error); + writer[sym.readyPromise].reject = undefined; + writer[sym.readyPromise].resolve = undefined; + } else { + writer[sym.readyPromise] = { + promise: Promise.reject(error), + }; + } + setPromiseIsHandledToTrue(writer[sym.readyPromise].promise); + } + + function writableStreamDefaultWriterWrite( + writer, + chunk, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + const controller = stream[sym.writableStreamController]; + assert(controller); + const chunkSize = writableStreamDefaultControllerGetChunkSize( + controller, + chunk, + ); + if (stream !== writer[sym.ownerWritableStream]) { + return Promise.reject("Writer has incorrect WritableStream."); + } + const state = stream[sym.state]; + if (state === "errored") { + return Promise.reject(stream[sym.storedError]); + } + if (writableStreamCloseQueuedOrInFlight(stream) || state === "closed") { + return Promise.reject(new TypeError("The stream is closed or closing.")); + } + if (state === "erroring") { + return Promise.reject(stream[sym.storedError]); + } + assert(state === "writable"); + const promise = writableStreamAddWriteRequest(stream); + writableStreamDefaultControllerWrite(controller, chunk, chunkSize); + return promise; + } + + function writableStreamDefaultWriterGetDesiredSize( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + const state = stream[sym.state]; + if (state === "errored" || state === "erroring") { + return null; + } + if (state === "closed") { + return 0; + } + return writableStreamDefaultControllerGetDesiredSize( + stream[sym.writableStreamController], + ); + } + + function writableStreamDefaultWriterRelease( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + assert(stream[sym.writer] === writer); + const releasedError = new TypeError( + "Writer was released and can no longer be used to monitor the stream's closedness.", + ); + writableStreamDefaultWriterEnsureReadyPromiseRejected( + writer, + releasedError, + ); + writableStreamDefaultWriterEnsureClosePromiseRejected( + writer, + releasedError, + ); + stream[sym.writer] = undefined; + writer[sym.ownerWritableStream] = undefined; + } + + function writableStreamFinishErroring(stream) { + assert(stream[sym.state] === "erroring"); + assert(!writableStreamHasOperationMarkedInFlight(stream)); + stream[sym.state] = "errored"; + stream[sym.writableStreamController][sym.errorSteps](); + const storedError = stream[sym.storedError]; + for (const writeRequest of stream[sym.writeRequests]) { + assert(writeRequest.reject); + writeRequest.reject(storedError); + } + stream[sym.writeRequests] = []; + if (!stream[sym.pendingAbortRequest]) { + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const abortRequest = stream[sym.pendingAbortRequest]; + assert(abortRequest); + stream[sym.pendingAbortRequest] = undefined; + if (abortRequest.wasAlreadyErroring) { + assert(abortRequest.promise.reject); + abortRequest.promise.reject(storedError); + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const promise = stream[sym.writableStreamController][sym.abortSteps]( + abortRequest.reason, + ); + setPromiseIsHandledToTrue( + promise.then( + () => { + assert(abortRequest.promise.resolve); + abortRequest.promise.resolve(); + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, + (reason) => { + assert(abortRequest.promise.reject); + abortRequest.promise.reject(reason); + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, + ), + ); + } + + function writableStreamFinishInFlightClose( + stream, + ) { + assert(stream[sym.inFlightCloseRequest]); + stream[sym.inFlightCloseRequest]?.resolve(); + stream[sym.inFlightCloseRequest] = undefined; + const state = stream[sym.state]; + assert(state === "writable" || state === "erroring"); + if (state === "erroring") { + stream[sym.storedError] = undefined; + if (stream[sym.pendingAbortRequest]) { + stream[sym.pendingAbortRequest].promise.resolve(); + stream[sym.pendingAbortRequest] = undefined; + } + } + stream[sym.state] = "closed"; + const writer = stream[sym.writer]; + if (writer) { + writer[sym.closedPromise].resolve(); + } + assert(stream[sym.pendingAbortRequest] === undefined); + assert(stream[sym.storedError] === undefined); + } + + function writableStreamFinishInFlightCloseWithError( + stream, + error, + ) { + assert(stream[sym.inFlightCloseRequest]); + stream[sym.inFlightCloseRequest]?.reject(error); + stream[sym.inFlightCloseRequest] = undefined; + assert( + stream[sym.state] === "writable" || stream[sym.state] === "erroring", + ); + if (stream[sym.pendingAbortRequest]) { + stream[sym.pendingAbortRequest]?.promise.reject(error); + stream[sym.pendingAbortRequest] = undefined; + } + writableStreamDealWithRejection(stream, error); + } + + function writableStreamFinishInFlightWrite( + stream, + ) { + assert(stream[sym.inFlightWriteRequest]); + stream[sym.inFlightWriteRequest].resolve(); + stream[sym.inFlightWriteRequest] = undefined; + } + + function writableStreamFinishInFlightWriteWithError( + stream, + error, + ) { + assert(stream[sym.inFlightWriteRequest]); + stream[sym.inFlightWriteRequest].reject(error); + stream[sym.inFlightWriteRequest] = undefined; + assert( + stream[sym.state] === "writable" || stream[sym.state] === "erroring", + ); + writableStreamDealWithRejection(stream, error); + } + + function writableStreamHasOperationMarkedInFlight( + stream, + ) { + return !( + stream[sym.inFlightWriteRequest] === undefined && + stream[sym.inFlightCloseRequest] === undefined + ); + } + + function writableStreamMarkCloseRequestInFlight( + stream, + ) { + assert(stream[sym.inFlightCloseRequest] === undefined); + assert(stream[sym.closeRequest] !== undefined); + stream[sym.inFlightCloseRequest] = stream[sym.closeRequest]; + stream[sym.closeRequest] = undefined; + } + + function writableStreamMarkFirstWriteRequestInFlight( + stream, + ) { + assert(stream[sym.inFlightWriteRequest] === undefined); + assert(stream[sym.writeRequests].length); + const writeRequest = stream[sym.writeRequests].shift(); + stream[sym.inFlightWriteRequest] = writeRequest; + } + + function writableStreamRejectCloseAndClosedPromiseIfNeeded( + stream, + ) { + assert(stream[sym.state] === "errored"); + if (stream[sym.closeRequest]) { + assert(stream[sym.inFlightCloseRequest] === undefined); + stream[sym.closeRequest].reject(stream[sym.storedError]); + stream[sym.closeRequest] = undefined; + } + const writer = stream[sym.writer]; + if (writer) { + writer[sym.closedPromise].reject(stream[sym.storedError]); + setPromiseIsHandledToTrue(writer[sym.closedPromise].promise); + } + } + + function writableStreamStartErroring( + stream, + reason, + ) { + assert(stream[sym.storedError] === undefined); + assert(stream[sym.state] === "writable"); + const controller = stream[sym.writableStreamController]; + assert(controller); + stream[sym.state] = "erroring"; + stream[sym.storedError] = reason; + const writer = stream[sym.writer]; + if (writer) { + writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); + } + if ( + !writableStreamHasOperationMarkedInFlight(stream) && + controller[sym.started] + ) { + writableStreamFinishErroring(stream); + } + } + + function writableStreamUpdateBackpressure( + stream, + backpressure, + ) { + assert(stream[sym.state] === "writable"); + assert(!writableStreamCloseQueuedOrInFlight(stream)); + const writer = stream[sym.writer]; + if (writer && backpressure !== stream[sym.backpressure]) { + if (backpressure) { + writer[sym.readyPromise] = getDeferred(); + } else { + assert(backpressure === false); + writer[sym.readyPromise].resolve(); + writer[sym.readyPromise].resolve = undefined; + writer[sym.readyPromise].reject = undefined; + } + } + stream[sym.backpressure] = backpressure; + } + /* eslint-enable */ + + class CountQueuingStrategy { + constructor({ highWaterMark }) { + this.highWaterMark = highWaterMark; + } + + size() { + return 1; + } + + [customInspect]() { + return `${this.constructor.name} { highWaterMark: ${ + String(this.highWaterMark) + }, size: f }`; + } + } + + Object.defineProperty(CountQueuingStrategy.prototype, "size", { + enumerable: true, + }); + + class ByteLengthQueuingStrategy { + constructor({ highWaterMark }) { + this.highWaterMark = highWaterMark; + } + + size(chunk) { + return chunk.byteLength; + } + + [customInspect]() { + return `${this.constructor.name} { highWaterMark: ${ + String(this.highWaterMark) + }, size: f }`; + } + } + + Object.defineProperty(ByteLengthQueuingStrategy.prototype, "size", { + enumerable: true, + }); + + window.__bootstrap.streams = { + ReadableStream, + TransformStream, + WritableStream, + isReadableStreamDisturbed, + CountQueuingStrategy, + ByteLengthQueuingStrategy, + }; +})(this); diff --git a/op_crates/fetch/20_headers.js b/op_crates/fetch/20_headers.js new file mode 100644 index 000000000..c2ae72864 --- /dev/null +++ b/op_crates/fetch/20_headers.js @@ -0,0 +1,256 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { DomIterableMixin } = window.__bootstrap.domIterable; + const { requiredArguments } = window.__bootstrap.fetchUtil; + + // From node-fetch + // Copyright (c) 2016 David Frank. MIT License. + const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; + const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + + function isHeaders(value) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return value instanceof Headers; + } + + const headersData = Symbol("headers data"); + + // TODO: headerGuard? Investigate if it is needed + // node-fetch did not implement this but it is in the spec + function normalizeParams(name, value) { + name = String(name).toLowerCase(); + value = String(value).trim(); + return [name, value]; + } + + // The following name/value validations are copied from + // https://github.com/bitinn/node-fetch/blob/master/src/headers.js + // Copyright (c) 2016 David Frank. MIT License. + function validateName(name) { + if (invalidTokenRegex.test(name) || name === "") { + throw new TypeError(`${name} is not a legal HTTP header name`); + } + } + + function validateValue(value) { + if (invalidHeaderCharRegex.test(value)) { + throw new TypeError(`${value} is not a legal HTTP header value`); + } + } + + /** Appends a key and value to the header list. + * + * The spec indicates that when a key already exists, the append adds the new + * value onto the end of the existing value. The behaviour of this though + * varies when the key is `set-cookie`. In this case, if the key of the cookie + * already exists, the value is replaced, but if the key of the cookie does not + * exist, and additional `set-cookie` header is added. + * + * The browser specification of `Headers` is written for clients, and not + * servers, and Deno is a server, meaning that it needs to follow the patterns + * expected for servers, of which a `set-cookie` header is expected for each + * unique cookie key, but duplicate cookie keys should not exist. */ + function dataAppend( + data, + key, + value, + ) { + for (let i = 0; i < data.length; i++) { + const [dataKey] = data[i]; + if (key === "set-cookie" && dataKey === "set-cookie") { + const [, dataValue] = data[i]; + const [dataCookieKey] = dataValue.split("="); + const [cookieKey] = value.split("="); + if (dataCookieKey === cookieKey) { + data[i][1] = value; + return; + } + } else { + if (dataKey === key) { + data[i][1] += `, ${value}`; + return; + } + } + } + data.push([key, value]); + } + + /** Gets a value of a key in the headers list. + * + * This varies slightly from spec behaviour in that when the key is `set-cookie` + * the value returned will look like a concatenated value, when in fact, if the + * headers were iterated over, each individual `set-cookie` value is a unique + * entry in the headers list. */ + function dataGet( + data, + key, + ) { + const setCookieValues = []; + for (const [dataKey, value] of data) { + if (dataKey === key) { + if (key === "set-cookie") { + setCookieValues.push(value); + } else { + return value; + } + } + } + if (setCookieValues.length) { + return setCookieValues.join(", "); + } + return undefined; + } + + /** Sets a value of a key in the headers list. + * + * The spec indicates that the value should be replaced if the key already + * exists. The behaviour here varies, where if the key is `set-cookie` the key + * of the cookie is inspected, and if the key of the cookie already exists, + * then the value is replaced. If the key of the cookie is not found, then + * the value of the `set-cookie` is added to the list of headers. + * + * The browser specification of `Headers` is written for clients, and not + * servers, and Deno is a server, meaning that it needs to follow the patterns + * expected for servers, of which a `set-cookie` header is expected for each + * unique cookie key, but duplicate cookie keys should not exist. */ + function dataSet( + data, + key, + value, + ) { + for (let i = 0; i < data.length; i++) { + const [dataKey] = data[i]; + if (dataKey === key) { + // there could be multiple set-cookie headers, but all others are unique + if (key === "set-cookie") { + const [, dataValue] = data[i]; + const [dataCookieKey] = dataValue.split("="); + const [cookieKey] = value.split("="); + if (cookieKey === dataCookieKey) { + data[i][1] = value; + return; + } + } else { + data[i][1] = value; + return; + } + } + } + data.push([key, value]); + } + + function dataDelete(data, key) { + let i = 0; + while (i < data.length) { + const [dataKey] = data[i]; + if (dataKey === key) { + data.splice(i, 1); + } else { + i++; + } + } + } + + function dataHas(data, key) { + for (const [dataKey] of data) { + if (dataKey === key) { + return true; + } + } + return false; + } + + // ref: https://fetch.spec.whatwg.org/#dom-headers + class HeadersBase { + constructor(init) { + if (init === null) { + throw new TypeError( + "Failed to construct 'Headers'; The provided value was not valid", + ); + } else if (isHeaders(init)) { + this[headersData] = [...init]; + } else { + this[headersData] = []; + if (Array.isArray(init)) { + for (const tuple of init) { + // If header does not contain exactly two items, + // then throw a TypeError. + // ref: https://fetch.spec.whatwg.org/#concept-headers-fill + requiredArguments( + "Headers.constructor tuple array argument", + tuple.length, + 2, + ); + + this.append(tuple[0], tuple[1]); + } + } else if (init) { + for (const [rawName, rawValue] of Object.entries(init)) { + this.append(rawName, rawValue); + } + } + } + } + + [Symbol.for("Deno.customInspect")]() { + let length = this[headersData].length; + let output = ""; + for (const [key, value] of this[headersData]) { + const prefix = length === this[headersData].length ? " " : ""; + const postfix = length === 1 ? " " : ", "; + output = output + `${prefix}${key}: ${value}${postfix}`; + length--; + } + return `Headers {${output}}`; + } + + // ref: https://fetch.spec.whatwg.org/#concept-headers-append + append(name, value) { + requiredArguments("Headers.append", arguments.length, 2); + const [newname, newvalue] = normalizeParams(name, value); + validateName(newname); + validateValue(newvalue); + dataAppend(this[headersData], newname, newvalue); + } + + delete(name) { + requiredArguments("Headers.delete", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + dataDelete(this[headersData], newname); + } + + get(name) { + requiredArguments("Headers.get", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + return dataGet(this[headersData], newname) ?? null; + } + + has(name) { + requiredArguments("Headers.has", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + return dataHas(this[headersData], newname); + } + + set(name, value) { + requiredArguments("Headers.set", arguments.length, 2); + const [newname, newvalue] = normalizeParams(name, value); + validateName(newname); + validateValue(newvalue); + dataSet(this[headersData], newname, newvalue); + } + + get [Symbol.toStringTag]() { + return "Headers"; + } + } + + class Headers extends DomIterableMixin(HeadersBase, headersData) {} + + window.__bootstrap.headers = { + Headers, + }; +})(this); diff --git a/op_crates/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js new file mode 100644 index 000000000..4b31110d6 --- /dev/null +++ b/op_crates/fetch/26_fetch.js @@ -0,0 +1,1390 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + // provided by "deno_web" + const { URLSearchParams } = window.__bootstrap.url; + + const { requiredArguments } = window.__bootstrap.fetchUtil; + const { ReadableStream, isReadableStreamDisturbed } = + window.__bootstrap.streams; + const { DomIterableMixin } = window.__bootstrap.domIterable; + const { Headers } = window.__bootstrap.headers; + + // FIXME(bartlomieju): stubbed out, needed in blob + const build = { + os: "", + }; + + const MAX_SIZE = 2 ** 32 - 2; + + // `off` is the offset into `dst` where it will at which to begin writing values + // from `src`. + // Returns the number of bytes copied. + function copyBytes(src, dst, off = 0) { + const r = dst.byteLength - off; + if (src.byteLength > r) { + src = src.subarray(0, r); + } + dst.set(src, off); + return src.byteLength; + } + + class Buffer { + #buf = null; // contents are the bytes buf[off : len(buf)] + #off = 0; // read at buf[off], write at buf[buf.byteLength] + + constructor(ab) { + if (ab == null) { + this.#buf = new Uint8Array(0); + return; + } + + this.#buf = new Uint8Array(ab); + } + + bytes(options = { copy: true }) { + if (options.copy === false) return this.#buf.subarray(this.#off); + return this.#buf.slice(this.#off); + } + + empty() { + return this.#buf.byteLength <= this.#off; + } + + get length() { + return this.#buf.byteLength - this.#off; + } + + get capacity() { + return this.#buf.buffer.byteLength; + } + + reset() { + this.#reslice(0); + this.#off = 0; + } + + #tryGrowByReslice = (n) => { + const l = this.#buf.byteLength; + if (n <= this.capacity - l) { + this.#reslice(l + n); + return l; + } + return -1; + }; + + #reslice = (len) => { + if (!(len <= this.#buf.buffer.byteLength)) { + throw new Error("assert"); + } + this.#buf = new Uint8Array(this.#buf.buffer, 0, len); + }; + + writeSync(p) { + const m = this.#grow(p.byteLength); + return copyBytes(p, this.#buf, m); + } + + write(p) { + const n = this.writeSync(p); + return Promise.resolve(n); + } + + #grow = (n) => { + const m = this.length; + // If buffer is empty, reset to recover space. + if (m === 0 && this.#off !== 0) { + this.reset(); + } + // Fast: Try to grow by means of a reslice. + const i = this.#tryGrowByReslice(n); + if (i >= 0) { + return i; + } + const c = this.capacity; + if (n <= Math.floor(c / 2) - m) { + // We can slide things down instead of allocating a new + // ArrayBuffer. We only need m+n <= c to slide, but + // we instead let capacity get twice as large so we + // don't spend all our time copying. + copyBytes(this.#buf.subarray(this.#off), this.#buf); + } else if (c + n > MAX_SIZE) { + throw new Error("The buffer cannot be grown beyond the maximum size."); + } else { + // Not enough space anywhere, we need to allocate. + const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE)); + copyBytes(this.#buf.subarray(this.#off), buf); + this.#buf = buf; + } + // Restore this.#off and len(this.#buf). + this.#off = 0; + this.#reslice(Math.min(m + n, MAX_SIZE)); + return m; + }; + + grow(n) { + if (n < 0) { + throw Error("Buffer.grow: negative count"); + } + const m = this.#grow(n); + this.#reslice(m); + } + } + + function isTypedArray(x) { + return ArrayBuffer.isView(x) && !(x instanceof DataView); + } + + function hasHeaderValueOf(s, value) { + return new RegExp(`^${value}[\t\s]*;?`).test(s); + } + + function getHeaderValueParams(value) { + const params = new Map(); + // Forced to do so for some Map constructor param mismatch + value + .split(";") + .slice(1) + .map((s) => s.trim().split("=")) + .filter((arr) => arr.length > 1) + .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")]) + .forEach(([k, v]) => params.set(k, v)); + return params; + } + + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + const CR = "\r".charCodeAt(0); + const LF = "\n".charCodeAt(0); + + const dataSymbol = Symbol("data"); + const bytesSymbol = Symbol("bytes"); + + function containsOnlyASCII(str) { + if (typeof str !== "string") { + return false; + } + return /^[\x00-\x7F]*$/.test(str); + } + + function convertLineEndingsToNative(s) { + const nativeLineEnd = build.os == "windows" ? "\r\n" : "\n"; + + let position = 0; + + let collectionResult = collectSequenceNotCRLF(s, position); + + let token = collectionResult.collected; + position = collectionResult.newPosition; + + let result = token; + + while (position < s.length) { + const c = s.charAt(position); + if (c == "\r") { + result += nativeLineEnd; + position++; + if (position < s.length && s.charAt(position) == "\n") { + position++; + } + } else if (c == "\n") { + position++; + result += nativeLineEnd; + } + + collectionResult = collectSequenceNotCRLF(s, position); + + token = collectionResult.collected; + position = collectionResult.newPosition; + + result += token; + } + + return result; + } + + function collectSequenceNotCRLF( + s, + position, + ) { + const start = position; + for ( + let c = s.charAt(position); + position < s.length && !(c == "\r" || c == "\n"); + c = s.charAt(++position) + ); + return { collected: s.slice(start, position), newPosition: position }; + } + + function toUint8Arrays( + blobParts, + doNormalizeLineEndingsToNative, + ) { + const ret = []; + const enc = new TextEncoder(); + for (const element of blobParts) { + if (typeof element === "string") { + let str = element; + if (doNormalizeLineEndingsToNative) { + str = convertLineEndingsToNative(element); + } + ret.push(enc.encode(str)); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + } else if (element instanceof Blob) { + ret.push(element[bytesSymbol]); + } else if (element instanceof Uint8Array) { + ret.push(element); + } else if (element instanceof Uint16Array) { + const uint8 = new Uint8Array(element.buffer); + ret.push(uint8); + } else if (element instanceof Uint32Array) { + const uint8 = new Uint8Array(element.buffer); + ret.push(uint8); + } else if (ArrayBuffer.isView(element)) { + // Convert view to Uint8Array. + const uint8 = new Uint8Array(element.buffer); + ret.push(uint8); + } else if (element instanceof ArrayBuffer) { + // Create a new Uint8Array view for the given ArrayBuffer. + const uint8 = new Uint8Array(element); + ret.push(uint8); + } else { + ret.push(enc.encode(String(element))); + } + } + return ret; + } + + function processBlobParts( + blobParts, + options, + ) { + const normalizeLineEndingsToNative = options.ending === "native"; + // ArrayBuffer.transfer is not yet implemented in V8, so we just have to + // pre compute size of the array buffer and do some sort of static allocation + // instead of dynamic allocation. + const uint8Arrays = toUint8Arrays(blobParts, normalizeLineEndingsToNative); + const byteLength = uint8Arrays + .map((u8) => u8.byteLength) + .reduce((a, b) => a + b, 0); + const ab = new ArrayBuffer(byteLength); + const bytes = new Uint8Array(ab); + let courser = 0; + for (const u8 of uint8Arrays) { + bytes.set(u8, courser); + courser += u8.byteLength; + } + + return bytes; + } + + function getStream(blobBytes) { + // TODO: Align to spec https://fetch.spec.whatwg.org/#concept-construct-readablestream + return new ReadableStream({ + type: "bytes", + start: (controller) => { + controller.enqueue(blobBytes); + controller.close(); + }, + }); + } + + async function readBytes( + reader, + ) { + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (!done && value instanceof Uint8Array) { + chunks.push(value); + } else if (done) { + const size = chunks.reduce((p, i) => p + i.byteLength, 0); + const bytes = new Uint8Array(size); + let offs = 0; + for (const chunk of chunks) { + bytes.set(chunk, offs); + offs += chunk.byteLength; + } + return bytes.buffer; + } else { + throw new TypeError("Invalid reader result."); + } + } + } + + // A WeakMap holding blob to byte array mapping. + // Ensures it does not impact garbage collection. + // const blobBytesWeakMap = new WeakMap(); + + class Blob { + constructor(blobParts, options) { + if (arguments.length === 0) { + this[bytesSymbol] = new Uint8Array(); + return; + } + + const { ending = "transparent", type = "" } = options ?? {}; + // Normalize options.type. + let normalizedType = type; + if (!containsOnlyASCII(type)) { + normalizedType = ""; + } else { + if (type.length) { + for (let i = 0; i < type.length; ++i) { + const char = type[i]; + if (char < "\u0020" || char > "\u007E") { + normalizedType = ""; + break; + } + } + normalizedType = type.toLowerCase(); + } + } + const bytes = processBlobParts(blobParts, { ending, type }); + // Set Blob object's properties. + this[bytesSymbol] = bytes; + this.size = bytes.byteLength; + this.type = normalizedType; + } + + slice(start, end, contentType) { + return new Blob([this[bytesSymbol].slice(start, end)], { + type: contentType || this.type, + }); + } + + stream() { + return getStream(this[bytesSymbol]); + } + + async text() { + const reader = getStream(this[bytesSymbol]).getReader(); + const decoder = new TextDecoder(); + return decoder.decode(await readBytes(reader)); + } + + arrayBuffer() { + return readBytes(getStream(this[bytesSymbol]).getReader()); + } + } + + class DomFile extends Blob { + constructor( + fileBits, + fileName, + options, + ) { + const { lastModified = Date.now(), ...blobPropertyBag } = options ?? {}; + super(fileBits, blobPropertyBag); + + // 4.1.2.1 Replace any "/" character (U+002F SOLIDUS) + // with a ":" (U + 003A COLON) + this.name = String(fileName).replace(/\u002F/g, "\u003A"); + // 4.1.3.3 If lastModified is not provided, set lastModified to the current + // date and time represented in number of milliseconds since the Unix Epoch. + this.lastModified = lastModified; + } + } + + function parseFormDataValue(value, filename) { + if (value instanceof DomFile) { + return new DomFile([value], filename || value.name, { + type: value.type, + lastModified: value.lastModified, + }); + } else if (value instanceof Blob) { + return new DomFile([value], filename || "blob", { + type: value.type, + }); + } else { + return String(value); + } + } + + class FormDataBase { + [dataSymbol] = []; + + append(name, value, filename) { + requiredArguments("FormData.append", arguments.length, 2); + name = String(name); + this[dataSymbol].push([name, parseFormDataValue(value, filename)]); + } + + delete(name) { + requiredArguments("FormData.delete", arguments.length, 1); + name = String(name); + let i = 0; + while (i < this[dataSymbol].length) { + if (this[dataSymbol][i][0] === name) { + this[dataSymbol].splice(i, 1); + } else { + i++; + } + } + } + + getAll(name) { + requiredArguments("FormData.getAll", arguments.length, 1); + name = String(name); + const values = []; + for (const entry of this[dataSymbol]) { + if (entry[0] === name) { + values.push(entry[1]); + } + } + + return values; + } + + get(name) { + requiredArguments("FormData.get", arguments.length, 1); + name = String(name); + for (const entry of this[dataSymbol]) { + if (entry[0] === name) { + return entry[1]; + } + } + + return null; + } + + has(name) { + requiredArguments("FormData.has", arguments.length, 1); + name = String(name); + return this[dataSymbol].some((entry) => entry[0] === name); + } + + set(name, value, filename) { + requiredArguments("FormData.set", arguments.length, 2); + name = String(name); + + // If there are any entries in the context object’s entry list whose name + // is name, replace the first such entry with entry and remove the others + let found = false; + let i = 0; + while (i < this[dataSymbol].length) { + if (this[dataSymbol][i][0] === name) { + if (!found) { + this[dataSymbol][i][1] = parseFormDataValue(value, filename); + found = true; + } else { + this[dataSymbol].splice(i, 1); + continue; + } + } + i++; + } + + // Otherwise, append entry to the context object’s entry list. + if (!found) { + this[dataSymbol].push([name, parseFormDataValue(value, filename)]); + } + } + + get [Symbol.toStringTag]() { + return "FormData"; + } + } + + class FormData extends DomIterableMixin(FormDataBase, dataSymbol) {} + + class MultipartBuilder { + constructor(formData, boundary) { + this.formData = formData; + this.boundary = boundary ?? this.#createBoundary(); + this.writer = new Buffer(); + } + + getContentType() { + return `multipart/form-data; boundary=${this.boundary}`; + } + + getBody() { + for (const [fieldName, fieldValue] of this.formData.entries()) { + if (fieldValue instanceof DomFile) { + this.#writeFile(fieldName, fieldValue); + } else this.#writeField(fieldName, fieldValue); + } + + this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`)); + + return this.writer.bytes(); + } + + #createBoundary = () => { + return ( + "----------" + + Array.from(Array(32)) + .map(() => Math.random().toString(36)[2] || 0) + .join("") + ); + }; + + #writeHeaders = (headers) => { + let buf = this.writer.empty() ? "" : "\r\n"; + + buf += `--${this.boundary}\r\n`; + for (const [key, value] of headers) { + buf += `${key}: ${value}\r\n`; + } + buf += `\r\n`; + + // FIXME(Bartlomieju): this should use `writeSync()` + this.writer.write(encoder.encode(buf)); + }; + + #writeFileHeaders = ( + field, + filename, + type, + ) => { + const headers = [ + [ + "Content-Disposition", + `form-data; name="${field}"; filename="${filename}"`, + ], + ["Content-Type", type || "application/octet-stream"], + ]; + return this.#writeHeaders(headers); + }; + + #writeFieldHeaders = (field) => { + const headers = [["Content-Disposition", `form-data; name="${field}"`]]; + return this.#writeHeaders(headers); + }; + + #writeField = (field, value) => { + this.#writeFieldHeaders(field); + this.writer.writeSync(encoder.encode(value)); + }; + + #writeFile = (field, value) => { + this.#writeFileHeaders(field, value.name, value.type); + this.writer.writeSync(value[bytesSymbol]); + }; + } + + class MultipartParser { + constructor(body, boundary) { + if (!boundary) { + throw new TypeError("multipart/form-data must provide a boundary"); + } + + this.boundary = `--${boundary}`; + this.body = body; + this.boundaryChars = encoder.encode(this.boundary); + } + + #parseHeaders = (headersText) => { + const headers = new Headers(); + const rawHeaders = headersText.split("\r\n"); + for (const rawHeader of rawHeaders) { + const sepIndex = rawHeader.indexOf(":"); + if (sepIndex < 0) { + continue; // Skip this header + } + const key = rawHeader.slice(0, sepIndex); + const value = rawHeader.slice(sepIndex + 1); + headers.set(key, value); + } + + return { + headers, + disposition: getHeaderValueParams( + headers.get("Content-Disposition") ?? "", + ), + }; + }; + + parse() { + const formData = new FormData(); + let headerText = ""; + let boundaryIndex = 0; + let state = 0; + let fileStart = 0; + + for (let i = 0; i < this.body.length; i++) { + const byte = this.body[i]; + const prevByte = this.body[i - 1]; + const isNewLine = byte === LF && prevByte === CR; + + if (state === 1 || state === 2 || state == 3) { + headerText += String.fromCharCode(byte); + } + if (state === 0 && isNewLine) { + state = 1; + } else if (state === 1 && isNewLine) { + state = 2; + const headersDone = this.body[i + 1] === CR && + this.body[i + 2] === LF; + + if (headersDone) { + state = 3; + } + } else if (state === 2 && isNewLine) { + state = 3; + } else if (state === 3 && isNewLine) { + state = 4; + fileStart = i + 1; + } else if (state === 4) { + if (this.boundaryChars[boundaryIndex] !== byte) { + boundaryIndex = 0; + } else { + boundaryIndex++; + } + + if (boundaryIndex >= this.boundary.length) { + const { headers, disposition } = this.#parseHeaders(headerText); + const content = this.body.subarray( + fileStart, + i - boundaryIndex - 1, + ); + // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata + const filename = disposition.get("filename"); + const name = disposition.get("name"); + + state = 5; + // Reset + boundaryIndex = 0; + headerText = ""; + + if (!name) { + continue; // Skip, unknown name + } + + if (filename) { + const blob = new Blob([content], { + type: headers.get("Content-Type") || "application/octet-stream", + }); + formData.append(name, blob, filename); + } else { + formData.append(name, decoder.decode(content)); + } + } + } else if (state === 5 && isNewLine) { + state = 1; + } + } + + return formData; + } + } + + function validateBodyType(owner, bodySource) { + if (isTypedArray(bodySource)) { + return true; + } else if (bodySource instanceof ArrayBuffer) { + return true; + } else if (typeof bodySource === "string") { + return true; + } else if (bodySource instanceof ReadableStream) { + return true; + } else if (bodySource instanceof FormData) { + return true; + } else if (bodySource instanceof URLSearchParams) { + return true; + } else if (!bodySource) { + return true; // null body is fine + } + throw new Error( + `Bad ${owner.constructor.name} body type: ${bodySource.constructor.name}`, + ); + } + + async function bufferFromStream( + stream, + size, + ) { + const encoder = new TextEncoder(); + const buffer = new Buffer(); + + if (size) { + // grow to avoid unnecessary allocations & copies + buffer.grow(size); + } + + while (true) { + const { done, value } = await stream.read(); + + if (done) break; + + if (typeof value === "string") { + buffer.writeSync(encoder.encode(value)); + } else if (value instanceof ArrayBuffer) { + buffer.writeSync(new Uint8Array(value)); + } else if (value instanceof Uint8Array) { + buffer.writeSync(value); + } else if (!value) { + // noop for undefined + } else { + throw new Error("unhandled type on stream read"); + } + } + + return buffer.bytes().buffer; + } + + function bodyToArrayBuffer(bodySource) { + if (isTypedArray(bodySource)) { + return bodySource.buffer; + } else if (bodySource instanceof ArrayBuffer) { + return bodySource; + } else if (typeof bodySource === "string") { + const enc = new TextEncoder(); + return enc.encode(bodySource).buffer; + } else if (bodySource instanceof ReadableStream) { + throw new Error( + `Can't convert stream to ArrayBuffer (try bufferFromStream)`, + ); + } else if ( + bodySource instanceof FormData || + bodySource instanceof URLSearchParams + ) { + const enc = new TextEncoder(); + return enc.encode(bodySource.toString()).buffer; + } else if (!bodySource) { + return new ArrayBuffer(0); + } + throw new Error( + `Body type not implemented: ${bodySource.constructor.name}`, + ); + } + + const BodyUsedError = + "Failed to execute 'clone' on 'Body': body is already used"; + + class Body { + #contentType = ""; + #size = undefined; + + constructor(_bodySource, meta) { + validateBodyType(this, _bodySource); + this._bodySource = _bodySource; + this.#contentType = meta.contentType; + this.#size = meta.size; + this._stream = null; + } + + get body() { + if (this._stream) { + return this._stream; + } + + if (!this._bodySource) { + return null; + } else if (this._bodySource instanceof ReadableStream) { + this._stream = this._bodySource; + } else { + const buf = bodyToArrayBuffer(this._bodySource); + if (!(buf instanceof ArrayBuffer)) { + throw new Error( + `Expected ArrayBuffer from body`, + ); + } + + this._stream = new ReadableStream({ + start(controller) { + controller.enqueue(buf); + controller.close(); + }, + }); + } + + return this._stream; + } + + get bodyUsed() { + if (this.body && isReadableStreamDisturbed(this.body)) { + return true; + } + return false; + } + + async blob() { + return new Blob([await this.arrayBuffer()], { + type: this.#contentType, + }); + } + + // ref: https://fetch.spec.whatwg.org/#body-mixin + async formData() { + const formData = new FormData(); + if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) { + const params = getHeaderValueParams(this.#contentType); + + // ref: https://tools.ietf.org/html/rfc2046#section-5.1 + const boundary = params.get("boundary"); + const body = new Uint8Array(await this.arrayBuffer()); + const multipartParser = new MultipartParser(body, boundary); + + return multipartParser.parse(); + } else if ( + hasHeaderValueOf(this.#contentType, "application/x-www-form-urlencoded") + ) { + // From https://github.com/github/fetch/blob/master/fetch.js + // Copyright (c) 2014-2016 GitHub, Inc. MIT License + const body = await this.text(); + try { + body + .trim() + .split("&") + .forEach((bytes) => { + if (bytes) { + const split = bytes.split("="); + const name = split.shift().replace(/\+/g, " "); + const value = split.join("=").replace(/\+/g, " "); + formData.append( + decodeURIComponent(name), + decodeURIComponent(value), + ); + } + }); + } catch (e) { + throw new TypeError("Invalid form urlencoded format"); + } + return formData; + } else { + throw new TypeError("Invalid form data"); + } + } + + async text() { + if (typeof this._bodySource === "string") { + return this._bodySource; + } + + const ab = await this.arrayBuffer(); + const decoder = new TextDecoder("utf-8"); + return decoder.decode(ab); + } + + async json() { + const raw = await this.text(); + return JSON.parse(raw); + } + + arrayBuffer() { + if (this._bodySource instanceof ReadableStream) { + return bufferFromStream(this._bodySource.getReader(), this.#size); + } + return bodyToArrayBuffer(this._bodySource); + } + } + + function createHttpClient(options) { + return new HttpClient(opCreateHttpClient(options)); + } + + function opCreateHttpClient(args) { + return core.jsonOpSync("op_create_http_client", args); + } + + class HttpClient { + constructor(rid) { + this.rid = rid; + } + close() { + core.close(this.rid); + } + } + + function opFetch(args, body) { + let zeroCopy; + if (body != null) { + zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength); + } + + return core.jsonOpAsync("op_fetch", args, ...(zeroCopy ? [zeroCopy] : [])); + } + + const NULL_BODY_STATUS = [101, 204, 205, 304]; + const REDIRECT_STATUS = [301, 302, 303, 307, 308]; + + function byteUpperCase(s) { + return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) { + return c.toUpperCase(); + }); + } + + function normalizeMethod(m) { + const u = byteUpperCase(m); + if ( + u === "DELETE" || + u === "GET" || + u === "HEAD" || + u === "OPTIONS" || + u === "POST" || + u === "PUT" + ) { + return u; + } + return m; + } + + class Request extends Body { + constructor(input, init) { + if (arguments.length < 1) { + throw TypeError("Not enough arguments"); + } + + if (!init) { + init = {}; + } + + let b; + + // prefer body from init + if (init.body) { + b = init.body; + } else if (input instanceof Request && input._bodySource) { + if (input.bodyUsed) { + throw TypeError(BodyUsedError); + } + b = input._bodySource; + } else if (typeof input === "object" && "body" in input && input.body) { + if (input.bodyUsed) { + throw TypeError(BodyUsedError); + } + b = input.body; + } else { + b = ""; + } + + let headers; + + // prefer headers from init + if (init.headers) { + headers = new Headers(init.headers); + } else if (input instanceof Request) { + headers = input.headers; + } else { + headers = new Headers(); + } + + const contentType = headers.get("content-type") || ""; + super(b, { contentType }); + this.headers = headers; + + // readonly attribute ByteString method; + this.method = "GET"; + + // readonly attribute USVString url; + this.url = ""; + + // readonly attribute RequestCredentials credentials; + this.credentials = "omit"; + + if (input instanceof Request) { + if (input.bodyUsed) { + throw TypeError(BodyUsedError); + } + this.method = input.method; + this.url = input.url; + this.headers = new Headers(input.headers); + this.credentials = input.credentials; + this._stream = input._stream; + } else if (typeof input === "string") { + this.url = input; + } + + if (init && "method" in init) { + this.method = normalizeMethod(init.method); + } + + if ( + init && + "credentials" in init && + init.credentials && + ["omit", "same-origin", "include"].indexOf(init.credentials) !== -1 + ) { + this.credentials = init.credentials; + } + } + + clone() { + if (this.bodyUsed) { + throw TypeError(BodyUsedError); + } + + const iterators = this.headers.entries(); + const headersList = []; + for (const header of iterators) { + headersList.push(header); + } + + let body2 = this._bodySource; + + if (this._bodySource instanceof ReadableStream) { + const tees = this._bodySource.tee(); + this._stream = this._bodySource = tees[0]; + body2 = tees[1]; + } + + return new Request(this.url, { + body: body2, + method: this.method, + headers: new Headers(headersList), + credentials: this.credentials, + }); + } + } + + const responseData = new WeakMap(); + class Response extends Body { + constructor(body = null, init) { + init = init ?? {}; + + if (typeof init !== "object") { + throw new TypeError(`'init' is not an object`); + } + + const extraInit = responseData.get(init) || {}; + let { type = "default", url = "" } = extraInit; + + let status = init.status === undefined ? 200 : Number(init.status || 0); + let statusText = init.statusText ?? ""; + let headers = init.headers instanceof Headers + ? init.headers + : new Headers(init.headers); + + if (init.status !== undefined && (status < 200 || status > 599)) { + throw new RangeError( + `The status provided (${init.status}) is outside the range [200, 599]`, + ); + } + + // null body status + if (body && NULL_BODY_STATUS.includes(status)) { + throw new TypeError("Response with null body status cannot have body"); + } + + if (!type) { + type = "default"; + } else { + if (type == "error") { + // spec: https://fetch.spec.whatwg.org/#concept-network-error + status = 0; + statusText = ""; + headers = new Headers(); + body = null; + /* spec for other Response types: + https://fetch.spec.whatwg.org/#concept-filtered-response-basic + Please note that type "basic" is not the same thing as "default".*/ + } else if (type == "basic") { + for (const h of headers) { + /* Forbidden Response-Header Names: + https://fetch.spec.whatwg.org/#forbidden-response-header-name */ + if (["set-cookie", "set-cookie2"].includes(h[0].toLowerCase())) { + headers.delete(h[0]); + } + } + } else if (type == "cors") { + /* CORS-safelisted Response-Header Names: + https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name */ + const allowedHeaders = [ + "Cache-Control", + "Content-Language", + "Content-Length", + "Content-Type", + "Expires", + "Last-Modified", + "Pragma", + ].map((c) => c.toLowerCase()); + for (const h of headers) { + /* Technically this is still not standards compliant because we are + supposed to allow headers allowed in the + 'Access-Control-Expose-Headers' header in the 'internal response' + However, this implementation of response doesn't seem to have an + easy way to access the internal response, so we ignore that + header. + TODO(serverhiccups): change how internal responses are handled + so we can do this properly. */ + if (!allowedHeaders.includes(h[0].toLowerCase())) { + headers.delete(h[0]); + } + } + /* TODO(serverhiccups): Once I fix the 'internal response' thing, + these actually need to treat the internal response differently */ + } else if (type == "opaque" || type == "opaqueredirect") { + url = ""; + status = 0; + statusText = ""; + headers = new Headers(); + body = null; + } + } + + const contentType = headers.get("content-type") || ""; + const size = Number(headers.get("content-length")) || undefined; + + super(body, { contentType, size }); + + this.url = url; + this.statusText = statusText; + this.status = extraInit.status || status; + this.headers = headers; + this.redirected = extraInit.redirected || false; + this.type = type; + } + + get ok() { + return 200 <= this.status && this.status < 300; + } + + clone() { + if (this.bodyUsed) { + throw TypeError(BodyUsedError); + } + + const iterators = this.headers.entries(); + const headersList = []; + for (const header of iterators) { + headersList.push(header); + } + + let resBody = this._bodySource; + + if (this._bodySource instanceof ReadableStream) { + const tees = this._bodySource.tee(); + this._stream = this._bodySource = tees[0]; + resBody = tees[1]; + } + + return new Response(resBody, { + status: this.status, + statusText: this.statusText, + headers: new Headers(headersList), + }); + } + + static redirect(url, status) { + if (![301, 302, 303, 307, 308].includes(status)) { + throw new RangeError( + "The redirection status must be one of 301, 302, 303, 307 and 308.", + ); + } + return new Response(null, { + status, + statusText: "", + headers: [["Location", typeof url === "string" ? url : url.toString()]], + }); + } + } + + function sendFetchReq(url, method, headers, body, clientRid) { + let headerArray = []; + if (headers) { + headerArray = Array.from(headers.entries()); + } + + const args = { + method, + url, + headers: headerArray, + clientRid, + }; + + return opFetch(args, body); + } + + async function fetch(input, init) { + let url; + let method = null; + let headers = null; + let body; + let clientRid = null; + let redirected = false; + let remRedirectCount = 20; // TODO: use a better way to handle + + if (typeof input === "string" || input instanceof URL) { + url = typeof input === "string" ? input : input.href; + if (init != null) { + method = init.method || null; + if (init.headers) { + headers = init.headers instanceof Headers + ? init.headers + : new Headers(init.headers); + } else { + headers = null; + } + + // ref: https://fetch.spec.whatwg.org/#body-mixin + // Body should have been a mixin + // but we are treating it as a separate class + if (init.body) { + if (!headers) { + headers = new Headers(); + } + let contentType = ""; + if (typeof init.body === "string") { + body = new TextEncoder().encode(init.body); + contentType = "text/plain;charset=UTF-8"; + } else if (isTypedArray(init.body)) { + body = init.body; + } else if (init.body instanceof ArrayBuffer) { + body = new Uint8Array(init.body); + } else if (init.body instanceof URLSearchParams) { + body = new TextEncoder().encode(init.body.toString()); + contentType = "application/x-www-form-urlencoded;charset=UTF-8"; + } else if (init.body instanceof Blob) { + body = init.body[bytesSymbol]; + contentType = init.body.type; + } else if (init.body instanceof FormData) { + let boundary; + if (headers.has("content-type")) { + const params = getHeaderValueParams("content-type"); + boundary = params.get("boundary"); + } + const multipartBuilder = new MultipartBuilder( + init.body, + boundary, + ); + body = multipartBuilder.getBody(); + contentType = multipartBuilder.getContentType(); + } else { + // TODO: ReadableStream + throw new Error("Not implemented"); + } + if (contentType && !headers.has("content-type")) { + headers.set("content-type", contentType); + } + } + + if (init.client instanceof HttpClient) { + clientRid = init.client.rid; + } + } + } else { + url = input.url; + method = input.method; + headers = input.headers; + + if (input._bodySource) { + body = new DataView(await input.arrayBuffer()); + } + } + + let responseBody; + let responseInit = {}; + while (remRedirectCount) { + const fetchResponse = await sendFetchReq( + url, + method, + headers, + body, + clientRid, + ); + const rid = fetchResponse.bodyRid; + + if ( + NULL_BODY_STATUS.includes(fetchResponse.status) || + REDIRECT_STATUS.includes(fetchResponse.status) + ) { + // We won't use body of received response, so close it now + // otherwise it will be kept in resource table. + core.close(fetchResponse.bodyRid); + responseBody = null; + } else { + responseBody = new ReadableStream({ + type: "bytes", + async pull(controller) { + try { + const result = await core.jsonOpAsync("op_fetch_read", { rid }); + if (!result || !result.chunk) { + controller.close(); + core.close(rid); + } else { + // TODO(ry) This is terribly inefficient. Make this zero-copy. + const chunk = new Uint8Array(result.chunk); + controller.enqueue(chunk); + } + } catch (e) { + controller.error(e); + controller.close(); + core.close(rid); + } + }, + cancel() { + // When reader.cancel() is called + core.close(rid); + }, + }); + } + + responseInit = { + status: 200, + statusText: fetchResponse.statusText, + headers: fetchResponse.headers, + }; + + responseData.set(responseInit, { + redirected, + rid: fetchResponse.bodyRid, + status: fetchResponse.status, + url, + }); + + const response = new Response(responseBody, responseInit); + + if (REDIRECT_STATUS.includes(fetchResponse.status)) { + // We're in a redirect status + switch ((init && init.redirect) || "follow") { + case "error": + responseInit = {}; + responseData.set(responseInit, { + type: "error", + redirected: false, + url: "", + }); + return new Response(null, responseInit); + case "manual": + responseInit = {}; + responseData.set(responseInit, { + type: "opaqueredirect", + redirected: false, + url: "", + }); + return new Response(null, responseInit); + case "follow": + default: + let redirectUrl = response.headers.get("Location"); + if (redirectUrl == null) { + return response; // Unspecified + } + if ( + !redirectUrl.startsWith("http://") && + !redirectUrl.startsWith("https://") + ) { + redirectUrl = new URL(redirectUrl, url).href; + } + url = redirectUrl; + redirected = true; + remRedirectCount--; + } + } else { + return response; + } + } + + responseData.set(responseInit, { + type: "error", + redirected: false, + url: "", + }); + + return new Response(null, responseInit); + } + + window.__bootstrap.fetch = { + Blob, + DomFile, + FormData, + fetch, + Request, + Response, + HttpClient, + createHttpClient, + }; +})(this); diff --git a/op_crates/fetch/Cargo.toml b/op_crates/fetch/Cargo.toml new file mode 100644 index 000000000..66c03ee37 --- /dev/null +++ b/op_crates/fetch/Cargo.toml @@ -0,0 +1,19 @@ +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_fetch" +version = "0.1.0" +edition = "2018" +description = "fetch Web API" +authors = ["the Deno authors"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.57.0", path = "../../core" } +reqwest = { version = "0.10.8", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli"] } +serde = { version = "1.0.116", features = ["derive"] } \ No newline at end of file diff --git a/op_crates/fetch/lib.deno_fetch.d.ts b/op_crates/fetch/lib.deno_fetch.d.ts new file mode 100644 index 000000000..fcc2fc919 --- /dev/null +++ b/op_crates/fetch/lib.deno_fetch.d.ts @@ -0,0 +1,636 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, no-var */ + +/// +/// + +interface DomIterable { + keys(): IterableIterator; + values(): IterableIterator; + entries(): IterableIterator<[K, V]>; + [Symbol.iterator](): IterableIterator<[K, V]>; + forEach( + callback: (value: V, key: K, parent: this) => void, + thisArg?: any, + ): void; +} + +interface ReadableStreamReadDoneResult { + done: true; + value?: T; +} + +interface ReadableStreamReadValueResult { + done: false; + value: T; +} + +type ReadableStreamReadResult = + | ReadableStreamReadValueResult + | ReadableStreamReadDoneResult; + +interface ReadableStreamDefaultReader { + readonly closed: Promise; + cancel(reason?: any): Promise; + read(): Promise>; + releaseLock(): void; +} + +interface ReadableStreamReader { + cancel(): Promise; + read(): Promise>; + releaseLock(): void; +} + +interface ReadableByteStreamControllerCallback { + (controller: ReadableByteStreamController): void | PromiseLike; +} + +interface UnderlyingByteSource { + autoAllocateChunkSize?: number; + cancel?: ReadableStreamErrorCallback; + pull?: ReadableByteStreamControllerCallback; + start?: ReadableByteStreamControllerCallback; + type: "bytes"; +} + +interface UnderlyingSource { + cancel?: ReadableStreamErrorCallback; + pull?: ReadableStreamDefaultControllerCallback; + start?: ReadableStreamDefaultControllerCallback; + type?: undefined; +} + +interface ReadableStreamErrorCallback { + (reason: any): void | PromiseLike; +} + +interface ReadableStreamDefaultControllerCallback { + (controller: ReadableStreamDefaultController): void | PromiseLike; +} + +interface ReadableStreamDefaultController { + readonly desiredSize: number | null; + close(): void; + enqueue(chunk: R): void; + error(error?: any): void; +} + +interface ReadableByteStreamController { + readonly byobRequest: undefined; + readonly desiredSize: number | null; + close(): void; + enqueue(chunk: ArrayBufferView): void; + error(error?: any): void; +} + +interface PipeOptions { + preventAbort?: boolean; + preventCancel?: boolean; + preventClose?: boolean; + signal?: AbortSignal; +} + +interface QueuingStrategySizeCallback { + (chunk: T): number; +} + +interface QueuingStrategy { + highWaterMark?: number; + size?: QueuingStrategySizeCallback; +} + +/** This Streams API interface provides a built-in byte length queuing strategy + * that can be used when constructing streams. */ +declare class CountQueuingStrategy implements QueuingStrategy { + constructor(options: { highWaterMark: number }); + highWaterMark: number; + size(chunk: any): 1; +} + +declare class ByteLengthQueuingStrategy + implements QueuingStrategy { + constructor(options: { highWaterMark: number }); + highWaterMark: number; + size(chunk: ArrayBufferView): number; +} + +/** This Streams API interface represents a readable stream of byte data. The + * Fetch API offers a concrete instance of a ReadableStream through the body + * property of a Response object. */ +interface ReadableStream { + readonly locked: boolean; + cancel(reason?: any): Promise; + getIterator(options?: { preventCancel?: boolean }): AsyncIterableIterator; + // getReader(options: { mode: "byob" }): ReadableStreamBYOBReader; + getReader(): ReadableStreamDefaultReader; + pipeThrough( + { + writable, + readable, + }: { + writable: WritableStream; + readable: ReadableStream; + }, + options?: PipeOptions, + ): ReadableStream; + pipeTo(dest: WritableStream, options?: PipeOptions): Promise; + tee(): [ReadableStream, ReadableStream]; + [Symbol.asyncIterator](options?: { + preventCancel?: boolean; + }): AsyncIterableIterator; +} + +declare var ReadableStream: { + prototype: ReadableStream; + new ( + underlyingSource: UnderlyingByteSource, + strategy?: { highWaterMark?: number; size?: undefined }, + ): ReadableStream; + new ( + underlyingSource?: UnderlyingSource, + strategy?: QueuingStrategy, + ): ReadableStream; +}; + +interface WritableStreamDefaultControllerCloseCallback { + (): void | PromiseLike; +} + +interface WritableStreamDefaultControllerStartCallback { + (controller: WritableStreamDefaultController): void | PromiseLike; +} + +interface WritableStreamDefaultControllerWriteCallback { + (chunk: W, controller: WritableStreamDefaultController): + | void + | PromiseLike< + void + >; +} + +interface WritableStreamErrorCallback { + (reason: any): void | PromiseLike; +} + +interface UnderlyingSink { + abort?: WritableStreamErrorCallback; + close?: WritableStreamDefaultControllerCloseCallback; + start?: WritableStreamDefaultControllerStartCallback; + type?: undefined; + write?: WritableStreamDefaultControllerWriteCallback; +} + +/** This Streams API interface provides a standard abstraction for writing + * streaming data to a destination, known as a sink. This object comes with + * built-in backpressure and queuing. */ +declare class WritableStream { + constructor( + underlyingSink?: UnderlyingSink, + strategy?: QueuingStrategy, + ); + readonly locked: boolean; + abort(reason?: any): Promise; + close(): Promise; + getWriter(): WritableStreamDefaultWriter; +} + +/** This Streams API interface represents a controller allowing control of a + * WritableStream's state. When constructing a WritableStream, the underlying + * sink is given a corresponding WritableStreamDefaultController instance to + * manipulate. */ +interface WritableStreamDefaultController { + error(error?: any): void; +} + +/** This Streams API interface is the object returned by + * WritableStream.getWriter() and once created locks the < writer to the + * WritableStream ensuring that no other streams can write to the underlying + * sink. */ +interface WritableStreamDefaultWriter { + readonly closed: Promise; + readonly desiredSize: number | null; + readonly ready: Promise; + abort(reason?: any): Promise; + close(): Promise; + releaseLock(): void; + write(chunk: W): Promise; +} + +declare class TransformStream { + constructor( + transformer?: Transformer, + writableStrategy?: QueuingStrategy, + readableStrategy?: QueuingStrategy, + ); + readonly readable: ReadableStream; + readonly writable: WritableStream; +} + +interface TransformStreamDefaultController { + readonly desiredSize: number | null; + enqueue(chunk: O): void; + error(reason?: any): void; + terminate(): void; +} + +interface Transformer { + flush?: TransformStreamDefaultControllerCallback; + readableType?: undefined; + start?: TransformStreamDefaultControllerCallback; + transform?: TransformStreamDefaultControllerTransformCallback; + writableType?: undefined; +} + +interface TransformStreamDefaultControllerCallback { + (controller: TransformStreamDefaultController): void | PromiseLike; +} + +interface TransformStreamDefaultControllerTransformCallback { + ( + chunk: I, + controller: TransformStreamDefaultController, + ): void | PromiseLike; +} + +type BlobPart = BufferSource | Blob | string; + +interface BlobPropertyBag { + type?: string; + ending?: "transparent" | "native"; +} + +/** A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. */ +interface Blob { + readonly size: number; + readonly type: string; + arrayBuffer(): Promise; + slice(start?: number, end?: number, contentType?: string): Blob; + stream(): ReadableStream; + text(): Promise; +} + +declare const Blob: { + prototype: Blob; + new (blobParts?: BlobPart[], options?: BlobPropertyBag): Blob; +}; + +interface FilePropertyBag extends BlobPropertyBag { + lastModified?: number; +} + +/** Provides information about files and allows JavaScript in a web page to + * access their content. */ +interface File extends Blob { + readonly lastModified: number; + readonly name: string; +} + +declare const File: { + prototype: File; + new (fileBits: BlobPart[], fileName: string, options?: FilePropertyBag): File; +}; + +type FormDataEntryValue = File | string; + +/** Provides a way to easily construct a set of key/value pairs representing + * form fields and their values, which can then be easily sent using the + * XMLHttpRequest.send() method. It uses the same format a form would use if the + * encoding type were set to "multipart/form-data". */ +interface FormData extends DomIterable { + append(name: string, value: string | Blob, fileName?: string): void; + delete(name: string): void; + get(name: string): FormDataEntryValue | null; + getAll(name: string): FormDataEntryValue[]; + has(name: string): boolean; + set(name: string, value: string | Blob, fileName?: string): void; +} + +declare const FormData: { + prototype: FormData; + // TODO(ry) FormData constructor is non-standard. + // new(form?: HTMLFormElement): FormData; + new (): FormData; +}; + +interface Body { + /** A simple getter used to expose a `ReadableStream` of the body contents. */ + readonly body: ReadableStream | null; + /** Stores a `Boolean` that declares whether the body has been used in a + * response yet. + */ + readonly bodyUsed: boolean; + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with an `ArrayBuffer`. + */ + arrayBuffer(): Promise; + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with a `Blob`. + */ + blob(): Promise; + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with a `FormData` object. + */ + formData(): Promise; + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with the result of parsing the body text as JSON. + */ + json(): Promise; + /** Takes a `Response` stream and reads it to completion. It returns a promise + * that resolves with a `USVString` (text). + */ + text(): Promise; +} + +type HeadersInit = Headers | string[][] | Record; + +/** This Fetch API interface allows you to perform various actions on HTTP + * request and response headers. These actions include retrieving, setting, + * adding to, and removing. A Headers object has an associated header list, + * which is initially empty and consists of zero or more name and value pairs. + *  You can add to this using methods like append() (see Examples.) In all + * methods of this interface, header names are matched by case-insensitive byte + * sequence. */ +interface Headers { + append(name: string, value: string): void; + delete(name: string): void; + get(name: string): string | null; + has(name: string): boolean; + set(name: string, value: string): void; + forEach( + callbackfn: (value: string, key: string, parent: Headers) => void, + thisArg?: any, + ): void; +} + +interface Headers extends DomIterable { + /** Appends a new value onto an existing header inside a `Headers` object, or + * adds the header if it does not already exist. + */ + append(name: string, value: string): void; + /** Deletes a header from a `Headers` object. */ + delete(name: string): void; + /** Returns an iterator allowing to go through all key/value pairs + * contained in this Headers object. The both the key and value of each pairs + * are ByteString objects. + */ + entries(): IterableIterator<[string, string]>; + /** Returns a `ByteString` sequence of all the values of a header within a + * `Headers` object with a given name. + */ + get(name: string): string | null; + /** Returns a boolean stating whether a `Headers` object contains a certain + * header. + */ + has(name: string): boolean; + /** Returns an iterator allowing to go through all keys contained in + * this Headers object. The keys are ByteString objects. + */ + keys(): IterableIterator; + /** Sets a new value for an existing header inside a Headers object, or adds + * the header if it does not already exist. + */ + set(name: string, value: string): void; + /** Returns an iterator allowing to go through all values contained in + * this Headers object. The values are ByteString objects. + */ + values(): IterableIterator; + forEach( + callbackfn: (value: string, key: string, parent: this) => void, + thisArg?: any, + ): void; + /** The Symbol.iterator well-known symbol specifies the default + * iterator for this Headers object + */ + [Symbol.iterator](): IterableIterator<[string, string]>; +} + +declare const Headers: { + prototype: Headers; + new (init?: HeadersInit): Headers; +}; + +type RequestInfo = Request | string; +type RequestCache = + | "default" + | "force-cache" + | "no-cache" + | "no-store" + | "only-if-cached" + | "reload"; +type RequestCredentials = "include" | "omit" | "same-origin"; +type RequestMode = "cors" | "navigate" | "no-cors" | "same-origin"; +type RequestRedirect = "error" | "follow" | "manual"; +type ReferrerPolicy = + | "" + | "no-referrer" + | "no-referrer-when-downgrade" + | "origin" + | "origin-when-cross-origin" + | "same-origin" + | "strict-origin" + | "strict-origin-when-cross-origin" + | "unsafe-url"; +type BodyInit = + | Blob + | BufferSource + | FormData + | URLSearchParams + | ReadableStream + | string; +type RequestDestination = + | "" + | "audio" + | "audioworklet" + | "document" + | "embed" + | "font" + | "image" + | "manifest" + | "object" + | "paintworklet" + | "report" + | "script" + | "sharedworker" + | "style" + | "track" + | "video" + | "worker" + | "xslt"; + +interface RequestInit { + /** + * A BodyInit object or null to set request's body. + */ + body?: BodyInit | null; + /** + * A string indicating how the request will interact with the browser's cache + * to set request's cache. + */ + cache?: RequestCache; + /** + * A string indicating whether credentials will be sent with the request + * always, never, or only when sent to a same-origin URL. Sets request's + * credentials. + */ + credentials?: RequestCredentials; + /** + * A Headers object, an object literal, or an array of two-item arrays to set + * request's headers. + */ + headers?: HeadersInit; + /** + * A cryptographic hash of the resource to be fetched by request. Sets + * request's integrity. + */ + integrity?: string; + /** + * A boolean to set request's keepalive. + */ + keepalive?: boolean; + /** + * A string to set request's method. + */ + method?: string; + /** + * A string to indicate whether the request will use CORS, or will be + * restricted to same-origin URLs. Sets request's mode. + */ + mode?: RequestMode; + /** + * A string indicating whether request follows redirects, results in an error + * upon encountering a redirect, or returns the redirect (in an opaque + * fashion). Sets request's redirect. + */ + redirect?: RequestRedirect; + /** + * A string whose value is a same-origin URL, "about:client", or the empty + * string, to set request's referrer. + */ + referrer?: string; + /** + * A referrer policy to set request's referrerPolicy. + */ + referrerPolicy?: ReferrerPolicy; + /** + * An AbortSignal to set request's signal. + */ + signal?: AbortSignal | null; + /** + * Can only be null. Used to disassociate request from any Window. + */ + window?: any; +} + +/** This Fetch API interface represents a resource request. */ +interface Request extends Body { + /** + * Returns the cache mode associated with request, which is a string + * indicating how the request will interact with the browser's cache when + * fetching. + */ + readonly cache: RequestCache; + /** + * Returns the credentials mode associated with request, which is a string + * indicating whether credentials will be sent with the request always, never, + * or only when sent to a same-origin URL. + */ + readonly credentials: RequestCredentials; + /** + * Returns the kind of resource requested by request, e.g., "document" or "script". + */ + readonly destination: RequestDestination; + /** + * Returns a Headers object consisting of the headers associated with request. + * Note that headers added in the network layer by the user agent will not be + * accounted for in this object, e.g., the "Host" header. + */ + readonly headers: Headers; + /** + * Returns request's subresource integrity metadata, which is a cryptographic + * hash of the resource being fetched. Its value consists of multiple hashes + * separated by whitespace. [SRI] + */ + readonly integrity: string; + /** + * Returns a boolean indicating whether or not request is for a history + * navigation (a.k.a. back-forward navigation). + */ + readonly isHistoryNavigation: boolean; + /** + * Returns a boolean indicating whether or not request is for a reload + * navigation. + */ + readonly isReloadNavigation: boolean; + /** + * Returns a boolean indicating whether or not request can outlive the global + * in which it was created. + */ + readonly keepalive: boolean; + /** + * Returns request's HTTP method, which is "GET" by default. + */ + readonly method: string; + /** + * Returns the mode associated with request, which is a string indicating + * whether the request will use CORS, or will be restricted to same-origin + * URLs. + */ + readonly mode: RequestMode; + /** + * Returns the redirect mode associated with request, which is a string + * indicating how redirects for the request will be handled during fetching. A + * request will follow redirects by default. + */ + readonly redirect: RequestRedirect; + /** + * Returns the referrer of request. Its value can be a same-origin URL if + * explicitly set in init, the empty string to indicate no referrer, and + * "about:client" when defaulting to the global's default. This is used during + * fetching to determine the value of the `Referer` header of the request + * being made. + */ + readonly referrer: string; + /** + * Returns the referrer policy associated with request. This is used during + * fetching to compute the value of the request's referrer. + */ + readonly referrerPolicy: ReferrerPolicy; + /** + * Returns the signal associated with request, which is an AbortSignal object + * indicating whether or not request has been aborted, and its abort event + * handler. + */ + readonly signal: AbortSignal; + /** + * Returns the URL of request as a string. + */ + readonly url: string; + clone(): Request; +} + +declare const Request: { + prototype: Request; + new (input: RequestInfo, init?: RequestInit): Request; +}; + +declare const Response: { + prototype: Response; + new (body?: BodyInit | null, init?: ResponseInit): Response; + error(): Response; + redirect(url: string, status?: number): Response; +}; + +/** Fetch a resource from the network. It returns a Promise that resolves to the + * Response to that request, whether it is successful or not. + * + * const response = await fetch("http://my.json.host/data.json"); + * console.log(response.status); // e.g. 200 + * console.log(response.statusText); // e.g. "OK" + * const jsonData = await response.json(); + */ +declare function fetch( + input: Request | URL | string, + init?: RequestInit, +): Promise; diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs new file mode 100644 index 000000000..e386431b5 --- /dev/null +++ b/op_crates/fetch/lib.rs @@ -0,0 +1,266 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::bad_resource_id; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::futures; +use deno_core::js_check; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use deno_core::url; +use deno_core::url::Url; +use deno_core::BufVec; +use deno_core::JsRuntime; +use deno_core::OpState; +use deno_core::ZeroCopyBuf; +use reqwest::header::HeaderMap; +use reqwest::header::HeaderName; +use reqwest::header::HeaderValue; +use reqwest::header::USER_AGENT; +use reqwest::redirect::Policy; +use reqwest::Client; +use reqwest::Method; +use reqwest::Response; +use serde::Deserialize; +use std::cell::RefCell; +use std::convert::From; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::path::PathBuf; +use std::rc::Rc; + +pub fn init(isolate: &mut JsRuntime) { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let files = vec![ + manifest_dir.join("01_fetch_util.js"), + manifest_dir.join("03_dom_iterable.js"), + manifest_dir.join("11_streams.js"), + manifest_dir.join("20_headers.js"), + manifest_dir.join("26_fetch.js"), + ]; + // TODO(nayeemrmn): https://github.com/rust-lang/cargo/issues/3946 to get the + // workspace root. + let display_root = manifest_dir.parent().unwrap().parent().unwrap(); + for file in files { + println!("cargo:rerun-if-changed={}", file.display()); + let display_path = file.strip_prefix(display_root).unwrap(); + let display_path_str = display_path.display().to_string(); + js_check(isolate.execute( + &("deno:".to_string() + &display_path_str.replace('\\', "/")), + &std::fs::read_to_string(&file).unwrap(), + )); + } +} + +pub trait FetchPermissions { + fn check_net_url(&self, url: &Url) -> Result<(), AnyError>; + fn check_read(&self, p: &PathBuf) -> Result<(), AnyError>; +} + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_fetch.d.ts") +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct FetchArgs { + method: Option, + url: String, + headers: Vec<(String, String)>, + client_rid: Option, +} + +pub async fn op_fetch( + state: Rc>, + args: Value, + data: BufVec, +) -> Result +where + FP: FetchPermissions + 'static, +{ + let args: FetchArgs = serde_json::from_value(args)?; + let url = args.url; + + let client = if let Some(rid) = args.client_rid { + let state_ = state.borrow(); + let r = state_ + .resource_table + .get::(rid) + .ok_or_else(bad_resource_id)?; + r.client.clone() + } else { + let state_ = state.borrow(); + let client = state_.borrow::(); + client.clone() + }; + + let method = match args.method { + Some(method_str) => Method::from_bytes(method_str.as_bytes())?, + None => Method::GET, + }; + + let url_ = url::Url::parse(&url)?; + + // Check scheme before asking for net permission + let scheme = url_.scheme(); + if scheme != "http" && scheme != "https" { + return Err(type_error(format!("scheme '{}' not supported", scheme))); + } + + { + let state_ = state.borrow(); + // TODO(ry) The Rc below is a hack because we store Rc in OpState. + // Ideally it could be removed. + let permissions = state_.borrow::>(); + permissions.check_net_url(&url_)?; + } + + let mut request = client.request(method, url_); + + match data.len() { + 0 => {} + 1 => request = request.body(Vec::from(&*data[0])), + _ => panic!("Invalid number of arguments"), + } + + for (key, value) in args.headers { + let name = HeaderName::from_bytes(key.as_bytes()).unwrap(); + let v = HeaderValue::from_str(&value).unwrap(); + request = request.header(name, v); + } + //debug!("Before fetch {}", url); + + let res = request.send().await?; + + //debug!("Fetch response {}", url); + let status = res.status(); + let mut res_headers = Vec::new(); + for (key, val) in res.headers().iter() { + res_headers.push((key.to_string(), val.to_str().unwrap().to_owned())); + } + + let rid = state + .borrow_mut() + .resource_table + .add("httpBody", Box::new(res)); + + Ok(json!({ + "bodyRid": rid, + "status": status.as_u16(), + "statusText": status.canonical_reason().unwrap_or(""), + "headers": res_headers + })) +} + +pub async fn op_fetch_read( + state: Rc>, + args: Value, + _data: BufVec, +) -> Result { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct Args { + rid: u32, + } + + let args: Args = serde_json::from_value(args)?; + let rid = args.rid; + + use futures::future::poll_fn; + use futures::ready; + use futures::FutureExt; + let f = poll_fn(move |cx| { + let mut state = state.borrow_mut(); + let response = state + .resource_table + .get_mut::(rid as u32) + .ok_or_else(bad_resource_id)?; + + let mut chunk_fut = response.chunk().boxed_local(); + let r = ready!(chunk_fut.poll_unpin(cx))?; + if let Some(chunk) = r { + Ok(json!({ "chunk": &*chunk })).into() + } else { + Ok(json!({ "chunk": null })).into() + } + }); + f.await + /* + // I'm programming this as I want it to be programmed, even though it might be + // incorrect, normally we would use poll_fn here. We need to make this await pattern work. + let chunk = response.chunk().await?; + if let Some(chunk) = chunk { + // TODO(ry) This is terribly inefficient. Make this zero-copy. + Ok(json!({ "chunk": &*chunk })) + } else { + Ok(json!({ "chunk": null })) + } + */ +} + +struct HttpClientResource { + client: Client, +} + +impl HttpClientResource { + fn new(client: Client) -> Self { + Self { client } + } +} + +pub fn op_create_http_client( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result +where + FP: FetchPermissions + 'static, +{ + #[derive(Deserialize, Default, Debug)] + #[serde(rename_all = "camelCase")] + #[serde(default)] + struct CreateHttpClientOptions { + ca_file: Option, + } + + let args: CreateHttpClientOptions = serde_json::from_value(args)?; + + if let Some(ca_file) = args.ca_file.clone() { + // TODO(ry) The Rc below is a hack because we store Rc in OpState. + // Ideally it could be removed. + let permissions = state.borrow::>(); + permissions.check_read(&PathBuf::from(ca_file))?; + } + + let client = create_http_client(args.ca_file.as_deref()).unwrap(); + + let rid = state + .resource_table + .add("httpClient", Box::new(HttpClientResource::new(client))); + Ok(json!(rid)) +} + +/// Create new instance of async reqwest::Client. This client supports +/// proxies and doesn't follow redirects. +fn create_http_client(ca_file: Option<&str>) -> Result { + let mut headers = HeaderMap::new(); + // TODO(ry) set the verison correctly. + headers.insert(USER_AGENT, format!("Deno/{}", "x.x.x").parse().unwrap()); + let mut builder = Client::builder() + .redirect(Policy::none()) + .default_headers(headers) + .use_rustls_tls(); + + if let Some(ca_file) = ca_file { + let mut buf = Vec::new(); + File::open(ca_file)?.read_to_end(&mut buf)?; + let cert = reqwest::Certificate::from_pem(&buf)?; + builder = builder.add_root_certificate(cert); + } + + builder + .build() + .map_err(|_| deno_core::error::generic_error("Unable to build http client")) +} diff --git a/std/http/file_server_test.ts b/std/http/file_server_test.ts index faaf0b9d1..a6fdb4e1b 100644 --- a/std/http/file_server_test.ts +++ b/std/http/file_server_test.ts @@ -197,7 +197,7 @@ Deno.test("contentType", async () => { (response.body as Deno.File).close(); }); -/* +/* TODO https://github.com/denoland/deno/issues/7540 Deno.test("file_server running as library", async function (): Promise { await startFileServerAsLibrary(); try { -- cgit v1.2.3