From 00e63306cbcc295a87ba662f9f63311a3c6c49ce Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Tue, 6 Apr 2021 12:55:05 +0200 Subject: [PATCH] refactor: add deno_file op crate (#10019) Also enables WPT for FileReader. --- Cargo.lock | 8 + cli/build.rs | 6 + cli/dts/lib.deno.shared_globals.d.ts | 1 + cli/main.rs | 3 +- cli/tsc.rs | 1 + op_crates/fetch/internal.d.ts | 10 - op_crates/fetch/lib.deno_fetch.d.ts | 36 -- op_crates/fetch/lib.rs | 4 - .../{fetch/21_file.js => file/01_file.js} | 2 +- op_crates/file/02_filereader.js | 334 ++++++++++++++++++ op_crates/file/Cargo.toml | 17 + op_crates/file/README.md | 5 + op_crates/file/internal.d.ts | 18 + op_crates/file/lib.deno_file.d.ts | 40 +++ op_crates/file/lib.rs | 22 ++ op_crates/web/21_filereader.js | 261 -------------- op_crates/web/internal.d.ts | 6 + op_crates/web/lib.rs | 4 - runtime/Cargo.toml | 2 + runtime/build.rs | 1 + runtime/lib.rs | 1 + test_util/wpt | 2 +- tools/wpt/expectation.json | 7 +- 23 files changed, 470 insertions(+), 321 deletions(-) rename op_crates/{fetch/21_file.js => file/01_file.js} (99%) create mode 100644 op_crates/file/02_filereader.js create mode 100644 op_crates/file/Cargo.toml create mode 100644 op_crates/file/README.md create mode 100644 op_crates/file/internal.d.ts create mode 100644 op_crates/file/lib.deno_file.d.ts create mode 100644 op_crates/file/lib.rs delete mode 100644 op_crates/web/21_filereader.js diff --git a/Cargo.lock b/Cargo.lock index ef2d7a2725..f5af15f8c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,13 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "deno_file" +version = "0.1.0" +dependencies = [ + "deno_core", +] + [[package]] name = "deno_lint" version = "0.2.19" @@ -643,6 +650,7 @@ dependencies = [ "deno_core", "deno_crypto", "deno_fetch", + "deno_file", "deno_url", "deno_web", "deno_webgpu", diff --git a/cli/build.rs b/cli/build.rs index 6606242e2c..1b1e913d28 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -11,6 +11,7 @@ use deno_core::RuntimeOptions; use deno_runtime::deno_console; use deno_runtime::deno_crypto; use deno_runtime::deno_fetch; +use deno_runtime::deno_file; use deno_runtime::deno_url; use deno_runtime::deno_web; use deno_runtime::deno_webgpu; @@ -66,6 +67,7 @@ fn create_compiler_snapshot( op_crate_libs.insert("deno.console", deno_console::get_declaration()); op_crate_libs.insert("deno.url", deno_url::get_declaration()); op_crate_libs.insert("deno.web", deno_web::get_declaration()); + op_crate_libs.insert("deno.file", deno_file::get_declaration()); op_crate_libs.insert("deno.fetch", deno_fetch::get_declaration()); op_crate_libs.insert("deno.webgpu", deno_webgpu::get_declaration()); op_crate_libs.insert("deno.websocket", deno_websocket::get_declaration()); @@ -270,6 +272,10 @@ fn main() { "cargo:rustc-env=DENO_WEB_LIB_PATH={}", deno_web::get_declaration().display() ); + println!( + "cargo:rustc-env=DENO_FILE_LIB_PATH={}", + deno_file::get_declaration().display() + ); println!( "cargo:rustc-env=DENO_FETCH_LIB_PATH={}", deno_fetch::get_declaration().display() diff --git a/cli/dts/lib.deno.shared_globals.d.ts b/cli/dts/lib.deno.shared_globals.d.ts index 84272caeb3..d96d068309 100644 --- a/cli/dts/lib.deno.shared_globals.d.ts +++ b/cli/dts/lib.deno.shared_globals.d.ts @@ -6,6 +6,7 @@ /// /// /// +/// /// /// /// diff --git a/cli/main.rs b/cli/main.rs index 6833665c15..e9e29c9db7 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -275,11 +275,12 @@ fn print_cache_info( pub fn get_types(unstable: bool) -> String { let mut types = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", crate::tsc::DENO_NS_LIB, crate::tsc::DENO_CONSOLE_LIB, crate::tsc::DENO_URL_LIB, crate::tsc::DENO_WEB_LIB, + crate::tsc::DENO_FILE_LIB, crate::tsc::DENO_FETCH_LIB, crate::tsc::DENO_WEBGPU_LIB, crate::tsc::DENO_WEBSOCKET_LIB, diff --git a/cli/tsc.rs b/cli/tsc.rs index dd97cb6b76..7c54905c50 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -32,6 +32,7 @@ pub static DENO_NS_LIB: &str = include_str!("dts/lib.deno.ns.d.ts"); pub static DENO_CONSOLE_LIB: &str = include_str!(env!("DENO_CONSOLE_LIB_PATH")); pub static DENO_URL_LIB: &str = include_str!(env!("DENO_URL_LIB_PATH")); pub static DENO_WEB_LIB: &str = include_str!(env!("DENO_WEB_LIB_PATH")); +pub static DENO_FILE_LIB: &str = include_str!(env!("DENO_FILE_LIB_PATH")); pub static DENO_FETCH_LIB: &str = include_str!(env!("DENO_FETCH_LIB_PATH")); pub static DENO_WEBGPU_LIB: &str = include_str!(env!("DENO_WEBGPU_LIB_PATH")); pub static DENO_WEBSOCKET_LIB: &str = diff --git a/op_crates/fetch/internal.d.ts b/op_crates/fetch/internal.d.ts index a474d499cb..e02bc6ed2b 100644 --- a/op_crates/fetch/internal.d.ts +++ b/op_crates/fetch/internal.d.ts @@ -19,16 +19,6 @@ declare namespace globalThis { Headers: typeof Headers; }; - declare var file: { - Blob: typeof Blob & { - [globalThis.__bootstrap.file._byteSequence]: Uint8Array; - }; - _byteSequence: unique symbol; - File: typeof File & { - [globalThis.__bootstrap.file._byteSequence]: Uint8Array; - }; - }; - declare var streams: { ReadableStream: typeof ReadableStream; isReadableStreamDisturbed(stream: ReadableStream): boolean; diff --git a/op_crates/fetch/lib.deno_fetch.d.ts b/op_crates/fetch/lib.deno_fetch.d.ts index 7d06fe691d..af21d8c447 100644 --- a/op_crates/fetch/lib.deno_fetch.d.ts +++ b/op_crates/fetch/lib.deno_fetch.d.ts @@ -287,42 +287,6 @@ interface TransformStreamDefaultControllerTransformCallback { ): void | PromiseLike; } -type BlobPart = BufferSource | Blob | string; - -interface BlobPropertyBag { - type?: string; - endings?: "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. */ -declare class Blob { - constructor(blobParts?: BlobPart[], options?: BlobPropertyBag); - - readonly size: number; - readonly type: string; - arrayBuffer(): Promise; - slice(start?: number, end?: number, contentType?: string): Blob; - stream(): ReadableStream; - text(): Promise; -} - -interface FilePropertyBag extends BlobPropertyBag { - lastModified?: number; -} - -/** Provides information about files and allows JavaScript in a web page to - * access their content. */ -declare class File extends Blob { - constructor( - fileBits: BlobPart[], - fileName: string, - options?: FilePropertyBag, - ); - - readonly lastModified: number; - readonly name: string; -} - type FormDataEntryValue = File | string; /** Provides a way to easily construct a set of key/value pairs representing diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs index 1d36bfc09d..32a5a01407 100644 --- a/op_crates/fetch/lib.rs +++ b/op_crates/fetch/lib.rs @@ -68,10 +68,6 @@ pub fn init(isolate: &mut JsRuntime) { "deno:op_crates/fetch/20_headers.js", include_str!("20_headers.js"), ), - ( - "deno:op_crates/fetch/21_file.js", - include_str!("21_file.js"), - ), ( "deno:op_crates/fetch/26_fetch.js", include_str!("26_fetch.js"), diff --git a/op_crates/fetch/21_file.js b/op_crates/file/01_file.js similarity index 99% rename from op_crates/fetch/21_file.js rename to op_crates/file/01_file.js index 4a052576cb..23886fbd51 100644 --- a/op_crates/fetch/21_file.js +++ b/op_crates/file/01_file.js @@ -6,7 +6,7 @@ /// /// /// -/// +/// /// "use strict"; diff --git a/op_crates/file/02_filereader.js b/op_crates/file/02_filereader.js new file mode 100644 index 0000000000..e398b23df3 --- /dev/null +++ b/op_crates/file/02_filereader.js @@ -0,0 +1,334 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// +/// +/// +/// +/// +/// +/// +/// + +"use strict"; + +((window) => { + const webidl = window.__bootstrap.webidl; + const base64 = window.__bootstrap.base64; + + const state = Symbol("[[state]]"); + const result = Symbol("[[result]]"); + const error = Symbol("[[error]]"); + const aborted = Symbol("[[aborted]]"); + + class FileReader extends EventTarget { + /** @type {"empty" | "loading" | "done"} */ + [state] = "empty"; + /** @type {null | string | ArrayBuffer} */ + [result] = null; + /** @type {null | DOMException} */ + [error] = null; + + [aborted] = false; + + /** + * @param {Blob} blob + * @param {{kind: "ArrayBuffer" | "Text" | "DataUrl", encoding?: string}} readtype + */ + #readOperation = async (blob, readtype) => { + // 1. If fr’s state is "loading", throw an InvalidStateError DOMException. + if (this[state] === "loading") { + throw new DOMException( + "Invalid FileReader state.", + "InvalidStateError", + ); + } + // 2. Set fr’s state to "loading". + this[state] = "loading"; + // 3. Set fr’s result to null. + this[result] = null; + // 4. Set fr’s error to null. + this[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. + /** @type {Uint8Array[]} */ + const chunks = []; + + // 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 (!this[aborted]) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const chunk = await chunkPromise; + if (this[aborted]) return; + + // 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr. + if (isFirstChunk) { + queueMicrotask(() => { + // fire a progress event for loadstart + const ev = new ProgressEvent("loadstart", {}); + this.dispatchEvent(ev); + }); + } + // 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(bartlomieju): (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, + }); + this.dispatchEvent(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) { + queueMicrotask(() => { + // 1. Set fr’s state to "done". + this[state] = "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": { + this[result] = bytes.buffer; + break; + } + case "Text": { + const decoder = new TextDecoder(readtype.encoding); + this[result] = decoder.decode(bytes.buffer); + break; + } + case "DataUrl": { + this[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, + }); + this.dispatchEvent(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 (this[state] !== "loading") { + const ev = new ProgressEvent("loadend", { + lengthComputable: true, + loaded: size, + total: size, + }); + this.dispatchEvent(ev); + } + }); + + break; + } + } catch (err) { + if (this[aborted]) return; + + // chunkPromise rejected + this[state] = "done"; + this[error] = err; + + { + const ev = new ProgressEvent("error", {}); + this.dispatchEvent(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 (this[state] !== "loading") { + const ev = new ProgressEvent("loadend", {}); + this.dispatchEvent(ev); + } + + break; + } + } + }; + + static EMPTY = 0; + static LOADING = 1; + static DONE = 2; + + constructor() { + super(); + this[webidl.brand] = webidl.brand; + } + + /** @returns {number} */ + get readyState() { + webidl.assertBranded(this, FileReader); + switch (this[state]) { + case "empty": + return FileReader.EMPTY; + case "loading": + return FileReader.LOADING; + case "done": + return FileReader.DONE; + default: + throw new TypeError("Invalid state"); + } + } + + get result() { + webidl.assertBranded(this, FileReader); + return this[result]; + } + + get error() { + webidl.assertBranded(this, FileReader); + return this[error]; + } + + abort() { + webidl.assertBranded(this, FileReader); + // 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[state] === "empty" || + this[state] === "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[state] === "loading") { + this[state] = "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[aborted] = true; + + // Fire a progress event called abort at the context object. + const ev = new ProgressEvent("abort", {}); + this.dispatchEvent(ev); + + // If context object's state is not "loading", fire a progress event called loadend at the context object. + if (this[state] !== "loading") { + const ev = new ProgressEvent("loadend", {}); + this.dispatchEvent(ev); + } + } + + /** @param {Blob} blob */ + readAsArrayBuffer(blob) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsArrayBuffer' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + this.#readOperation(blob, { kind: "ArrayBuffer" }); + } + + /** @param {Blob} blob */ + readAsBinaryString(blob) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + // alias for readAsArrayBuffer + this.#readOperation(blob, { kind: "ArrayBuffer" }); + } + + /** @param {Blob} blob */ + readAsDataURL(blob) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + // alias for readAsArrayBuffer + this.#readOperation(blob, { kind: "DataUrl" }); + } + + /** + * @param {Blob} blob + * @param {string} [encoding] + */ + readAsText(blob, encoding) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + if (encoding !== undefined) { + encoding = webidl.converters["DOMString"](encoding, { + prefix, + context: "Argument 2", + }); + } + // alias for readAsArrayBuffer + this.#readOperation(blob, { kind: "Text", encoding }); + } + } + + const handlerSymbol = Symbol("eventHandlers"); + + function makeWrappedHandler(handler) { + function wrappedHandler(...args) { + if (typeof wrappedHandler.handler !== "function") { + return; + } + return wrappedHandler.handler.call(this, ...args); + } + wrappedHandler.handler = handler; + return wrappedHandler; + } + // TODO(benjamingr) reuse when we can reuse code between web crates + function defineEventHandler(emitter, name) { + // HTML specification section 8.1.5.1 + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[handlerSymbol]?.get(name)?.handler; + }, + set(value) { + if (!this[handlerSymbol]) { + this[handlerSymbol] = new Map(); + } + let handlerWrapper = this[handlerSymbol]?.get(name); + if (handlerWrapper) { + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + } + this[handlerSymbol].set(name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + defineEventHandler(FileReader.prototype, "error"); + defineEventHandler(FileReader.prototype, "loadstart"); + defineEventHandler(FileReader.prototype, "load"); + defineEventHandler(FileReader.prototype, "loadend"); + defineEventHandler(FileReader.prototype, "progress"); + defineEventHandler(FileReader.prototype, "abort"); + + window.__bootstrap.fileReader = { + FileReader, + }; +})(this); diff --git a/op_crates/file/Cargo.toml b/op_crates/file/Cargo.toml new file mode 100644 index 0000000000..54476c783f --- /dev/null +++ b/op_crates/file/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_file" +version = "0.1.0" +edition = "2018" +description = "File API implementation for Deno" +authors = ["the Deno authors"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.83.0", path = "../../core" } diff --git a/op_crates/file/README.md b/op_crates/file/README.md new file mode 100644 index 0000000000..c421bf0047 --- /dev/null +++ b/op_crates/file/README.md @@ -0,0 +1,5 @@ +# deno_file + +This crate implements the File API. + +Spec: https://w3c.github.io/FileAPI diff --git a/op_crates/file/internal.d.ts b/op_crates/file/internal.d.ts new file mode 100644 index 0000000000..91a61d811f --- /dev/null +++ b/op_crates/file/internal.d.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +/// +/// + +declare namespace globalThis { + declare namespace __bootstrap { + declare var file: { + Blob: typeof Blob & { + [globalThis.__bootstrap.file._byteSequence]: Uint8Array; + }; + _byteSequence: unique symbol; + File: typeof File & { + [globalThis.__bootstrap.file._byteSequence]: Uint8Array; + }; + }; + } +} diff --git a/op_crates/file/lib.deno_file.d.ts b/op_crates/file/lib.deno_file.d.ts new file mode 100644 index 0000000000..a907c3f509 --- /dev/null +++ b/op_crates/file/lib.deno_file.d.ts @@ -0,0 +1,40 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +/// +/// + +type BlobPart = BufferSource | Blob | string; + +interface BlobPropertyBag { + type?: string; + endings?: "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. */ +declare class Blob { + constructor(blobParts?: BlobPart[], options?: BlobPropertyBag); + + readonly size: number; + readonly type: string; + arrayBuffer(): Promise; + slice(start?: number, end?: number, contentType?: string): Blob; + stream(): ReadableStream; + text(): Promise; +} + +interface FilePropertyBag extends BlobPropertyBag { + lastModified?: number; +} + +/** Provides information about files and allows JavaScript in a web page to + * access their content. */ +declare class File extends Blob { + constructor( + fileBits: BlobPart[], + fileName: string, + options?: FilePropertyBag, + ); + + readonly lastModified: number; + readonly name: string; +} diff --git a/op_crates/file/lib.rs b/op_crates/file/lib.rs new file mode 100644 index 0000000000..c7e690433c --- /dev/null +++ b/op_crates/file/lib.rs @@ -0,0 +1,22 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::JsRuntime; +use std::path::PathBuf; + +/// Load and execute the javascript code. +pub fn init(isolate: &mut JsRuntime) { + let files = vec![ + ("deno:op_crates/file/01_file.js", include_str!("01_file.js")), + ( + "deno:op_crates/file/02_filereader.js", + include_str!("02_filereader.js"), + ), + ]; + for (url, source_code) in files { + isolate.execute(url, source_code).unwrap(); + } +} + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_file.d.ts") +} diff --git a/op_crates/web/21_filereader.js b/op_crates/web/21_filereader.js deleted file mode 100644 index 30fff33d86..0000000000 --- a/op_crates/web/21_filereader.js +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -"use strict"; - -((window) => { - const base64 = window.__bootstrap.base64; - - 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) { - queueMicrotask(() => { - // fire a progress event for loadstart - const ev = new ProgressEvent("loadstart", {}); - fr.dispatchEvent(ev); - }); - } - // 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(bartlomieju): (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); - } - - 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) { - queueMicrotask(() => { - 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); - } - - // 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); - } - }); - - 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’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); - } - - break; - } - } - } - - class FileReader extends EventTarget { - error = 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 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); - } - } - 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; - - const handlerSymbol = Symbol("eventHandlers"); - - function makeWrappedHandler(handler) { - function wrappedHandler(...args) { - if (typeof wrappedHandler.handler !== "function") { - return; - } - return wrappedHandler.handler.call(this, ...args); - } - wrappedHandler.handler = handler; - return wrappedHandler; - } - // TODO(benjamingr) reuse when we can reuse code between web crates - function defineEventHandler(emitter, name) { - // HTML specification section 8.1.5.1 - Object.defineProperty(emitter, `on${name}`, { - get() { - return this[handlerSymbol]?.get(name)?.handler; - }, - set(value) { - if (!this[handlerSymbol]) { - this[handlerSymbol] = new Map(); - } - let handlerWrapper = this[handlerSymbol]?.get(name); - if (handlerWrapper) { - handlerWrapper.handler = value; - } else { - handlerWrapper = makeWrappedHandler(value); - this.addEventListener(name, handlerWrapper); - } - this[handlerSymbol].set(name, handlerWrapper); - }, - configurable: true, - enumerable: true, - }); - } - defineEventHandler(FileReader.prototype, "error"); - defineEventHandler(FileReader.prototype, "loadstart"); - defineEventHandler(FileReader.prototype, "load"); - defineEventHandler(FileReader.prototype, "loadend"); - defineEventHandler(FileReader.prototype, "progress"); - defineEventHandler(FileReader.prototype, "abort"); - - window.__bootstrap.fileReader = { - FileReader, - }; -})(this); diff --git a/op_crates/web/internal.d.ts b/op_crates/web/internal.d.ts index 458f4a173c..521563810b 100644 --- a/op_crates/web/internal.d.ts +++ b/op_crates/web/internal.d.ts @@ -12,5 +12,11 @@ declare namespace globalThis { declare var location: { getLocationHref(): string | undefined; }; + + declare var base64: { + byteLength(b64: string): number; + toByteArray(b64: string): Uint8Array; + fromByteArray(uint8: Uint8Array): string; + }; } } diff --git a/op_crates/web/lib.rs b/op_crates/web/lib.rs index af7a7cebc4..8ee944d74c 100644 --- a/op_crates/web/lib.rs +++ b/op_crates/web/lib.rs @@ -30,10 +30,6 @@ pub fn init(isolate: &mut JsRuntime) { "deno:op_crates/web/12_location.js", include_str!("12_location.js"), ), - ( - "deno:op_crates/web/21_filereader.js", - include_str!("21_filereader.js"), - ), ]; for (url, source_code) in files { isolate.execute(url, source_code).unwrap(); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a6f9a80ff6..22293f0a4f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -22,6 +22,7 @@ deno_core = { path = "../core", version = "0.83.0" } deno_console = { path = "../op_crates/console", version = "0.2.1" } deno_crypto = { path = "../op_crates/crypto", version = "0.16.1" } deno_fetch = { path = "../op_crates/fetch", version = "0.24.1" } +deno_file = { path = "../op_crates/file", version = "0.1.0" } deno_web = { path = "../op_crates/web", version = "0.32.1" } deno_url = { path = "../op_crates/url", version = "0.2.1" } deno_webidl = { path = "../op_crates/webidl", version = "0.2.1" } @@ -37,6 +38,7 @@ deno_core = { path = "../core", version = "0.83.0" } deno_console = { path = "../op_crates/console", version = "0.2.1" } deno_crypto = { path = "../op_crates/crypto", version = "0.16.1" } deno_fetch = { path = "../op_crates/fetch", version = "0.24.1" } +deno_file = { path = "../op_crates/file", version = "0.1.0" } deno_web = { path = "../op_crates/web", version = "0.32.1" } deno_url = { path = "../op_crates/url", version = "0.2.1" } deno_webidl = { path = "../op_crates/webidl", version = "0.2.1" } diff --git a/runtime/build.rs b/runtime/build.rs index 9172816d82..d7d8cb78fe 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -17,6 +17,7 @@ fn create_snapshot( deno_console::init(&mut js_runtime); deno_url::init(&mut js_runtime); deno_web::init(&mut js_runtime); + deno_file::init(&mut js_runtime); deno_fetch::init(&mut js_runtime); deno_websocket::init(&mut js_runtime); deno_crypto::init(&mut js_runtime); diff --git a/runtime/lib.rs b/runtime/lib.rs index 1606e9dfed..b45010c4d7 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -5,6 +5,7 @@ pub use deno_console; pub use deno_crypto; pub use deno_fetch; +pub use deno_file; pub use deno_url; pub use deno_web; pub use deno_webgpu; diff --git a/test_util/wpt b/test_util/wpt index 681d273a49..f897da0087 160000 --- a/test_util/wpt +++ b/test_util/wpt @@ -1 +1 @@ -Subproject commit 681d273a49e7b5228394285b0c017f1b4c0d33b0 +Subproject commit f897da00871cf39366bc2f0ceec051c65bc75703 diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json index abbe8cdfc9..2ebdc10114 100644 --- a/tools/wpt/expectation.json +++ b/tools/wpt/expectation.json @@ -358,7 +358,7 @@ ], "patched-global.any.js": true, "reentrant-strategies.any.js": true, - "tee.any.js": true, + "tee.any.js": false, "templated.any.js": [ "ReadableStream (empty) reader: canceling via the stream should fail" ] @@ -784,7 +784,8 @@ }, "file": { "File-constructor.any.js": true - } + }, + "fileReader.any.js": true }, "html": { "webappapis": { @@ -805,4 +806,4 @@ } } } -} +} \ No newline at end of file