diff --git a/cli/js/globals.ts b/cli/js/globals.ts index e5b7ff85a0..6eedb9289d 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -18,6 +18,7 @@ import * as urlSearchParams from "./web/url_search_params.ts"; import * as workers from "./web/workers.ts"; import * as performanceUtil from "./web/performance.ts"; import * as request from "./web/request.ts"; +import * as streams from "./web/streams/mod.ts"; // These imports are not exposed and therefore are fine to just import the // symbols required. @@ -226,6 +227,7 @@ export const windowOrWorkerGlobalScopeProperties = { FormData: nonEnumerable(formData.FormData), TextEncoder: nonEnumerable(textEncoding.TextEncoder), TextDecoder: nonEnumerable(textEncoding.TextDecoder), + ReadableStream: nonEnumerable(streams.ReadableStream), Request: nonEnumerable(request.Request), Response: nonEnumerable(fetchTypes.Response), performance: writable(new performanceUtil.Performance()), diff --git a/cli/js/lib.deno.shared_globals.d.ts b/cli/js/lib.deno.shared_globals.d.ts index 565121beab..5fec86311f 100644 --- a/cli/js/lib.deno.shared_globals.d.ts +++ b/cli/js/lib.deno.shared_globals.d.ts @@ -34,6 +34,7 @@ declare interface WindowOrWorkerGlobalScope { FormData: __domTypes.FormDataConstructor; TextEncoder: typeof __textEncoding.TextEncoder; TextDecoder: typeof __textEncoding.TextDecoder; + ReadableStream: __domTypes.ReadableStreamConstructor; Request: __domTypes.RequestConstructor; Response: typeof __fetch.Response; performance: __performanceUtil.Performance; @@ -250,6 +251,7 @@ declare const location: __domTypes.Location; declare const FormData: __domTypes.FormDataConstructor; declare const TextEncoder: typeof __textEncoding.TextEncoder; declare const TextDecoder: typeof __textEncoding.TextDecoder; +declare const ReadableStream: __domTypes.ReadableStreamConstructor; declare const Request: __domTypes.RequestConstructor; declare const Response: typeof __fetch.Response; declare const performance: __performanceUtil.Performance; @@ -282,6 +284,7 @@ declare type Headers = __domTypes.Headers; declare type FormData = __domTypes.FormData; declare type TextEncoder = __textEncoding.TextEncoder; declare type TextDecoder = __textEncoding.TextDecoder; +declare type ReadableStream = __domTypes.ReadableStream; declare type Request = __domTypes.Request; declare type Response = __domTypes.Response; declare type Worker = __workers.Worker; @@ -551,6 +554,27 @@ declare namespace __domTypes { preventClose?: boolean; signal?: AbortSignal; } + export interface UnderlyingSource { + cancel?: ReadableStreamErrorCallback; + pull?: ReadableStreamDefaultControllerCallback; + start?: ReadableStreamDefaultControllerCallback; + type?: undefined; + } + export interface ReadableStreamErrorCallback { + (reason: any): void | PromiseLike; + } + + export interface ReadableStreamDefaultControllerCallback { + (controller: ReadableStreamDefaultController): void | PromiseLike; + } + + export interface ReadableStreamDefaultController { + readonly desiredSize: number; + enqueue(chunk?: R): void; + close(): void; + error(e?: any): void; + } + /** 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. */ @@ -574,6 +598,12 @@ declare namespace __domTypes { */ tee(): [ReadableStream, ReadableStream]; } + + export interface ReadableStreamConstructor { + new (src?: UnderlyingSource): ReadableStream; + prototype: ReadableStream; + } + export interface ReadableStreamReader { cancel(reason: any): Promise; read(): Promise>; @@ -939,6 +969,9 @@ declare namespace __blob { options?: __domTypes.BlobPropertyBag ); slice(start?: number, end?: number, contentType?: string): DenoBlob; + stream(): __domTypes.ReadableStream; + text(): Promise; + arrayBuffer(): Promise; } } diff --git a/cli/js/tests/blob_test.ts b/cli/js/tests/blob_test.ts index b60877dd03..af84c37a35 100644 --- a/cli/js/tests/blob_test.ts +++ b/cli/js/tests/blob_test.ts @@ -1,5 +1,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { unitTest, assert, assertEquals } from "./test_util.ts"; +import { concat } from "../../../std/bytes/mod.ts"; +import { decode } from "../../../std/encoding/utf8.ts"; unitTest(function blobString(): void { const b1 = new Blob(["Hello World"]); @@ -67,4 +69,24 @@ unitTest(function nativeEndLine(): void { assertEquals(blob.size, Deno.build.os === "win" ? 12 : 11); }); -// TODO(qti3e) Test the stored data in a Blob after implementing FileReader API. +unitTest(async function blobText(): Promise { + const blob = new Blob(["Hello World"]); + assertEquals(await blob.text(), "Hello World"); +}); + +unitTest(async function blobStream(): Promise { + const blob = new Blob(["Hello World"]); + const stream = blob.stream(); + assert(stream instanceof ReadableStream); + const reader = stream.getReader(); + let bytes = new Uint8Array(); + const read = async (): Promise => { + const { done, value } = await reader.read(); + if (!done && value) { + bytes = concat(bytes, value); + return read(); + } + }; + await read(); + assertEquals(decode(bytes), "Hello World"); +}); diff --git a/cli/js/web/blob.ts b/cli/js/web/blob.ts index c4e4674ab4..7bdde8e28e 100644 --- a/cli/js/web/blob.ts +++ b/cli/js/web/blob.ts @@ -1,7 +1,8 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import * as domTypes from "./dom_types.ts"; -import { TextEncoder } from "./text_encoding.ts"; +import { TextDecoder, TextEncoder } from "./text_encoding.ts"; import { build } from "../build.ts"; +import { ReadableStream } from "./streams/mod.ts"; export const bytesSymbol = Symbol("bytes"); @@ -114,7 +115,6 @@ function processBlobParts( .reduce((a, b): number => 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); @@ -124,6 +124,48 @@ function processBlobParts( return bytes; } +function getStream(blobBytes: Uint8Array): domTypes.ReadableStream { + return new ReadableStream({ + start: ( + controller: domTypes.ReadableStreamDefaultController + ): void => { + controller.enqueue(blobBytes); + controller.close(); + }, + }) as domTypes.ReadableStream; +} + +async function readBytes( + reader: domTypes.ReadableStreamReader +): Promise { + const chunks: Uint8Array[] = []; + while (true) { + try { + 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 Promise.resolve(bytes); + } else { + return Promise.reject(new TypeError()); + } + } catch (e) { + return Promise.reject(e); + } + } +} + +// A WeakMap holding blob to byte array mapping. +// Ensures it does not impact garbage collection. +export const blobBytesWeakMap = new WeakMap(); + export class DenoBlob implements domTypes.Blob { [bytesSymbol]: Uint8Array; readonly size: number = 0; @@ -167,4 +209,18 @@ export class DenoBlob implements domTypes.Blob { type: contentType || this.type, }); } + + stream(): domTypes.ReadableStream { + return getStream(this[bytesSymbol]); + } + + async text(): Promise { + const reader = getStream(this[bytesSymbol]).getReader(); + const decoder = new TextDecoder(); + return decoder.decode(await readBytes(reader)); + } + + arrayBuffer(): Promise { + return readBytes(getStream(this[bytesSymbol]).getReader()); + } } diff --git a/cli/js/web/dom_types.ts b/cli/js/web/dom_types.ts index 33cda15820..94e26846fa 100644 --- a/cli/js/web/dom_types.ts +++ b/cli/js/web/dom_types.ts @@ -277,6 +277,9 @@ export interface Blob { readonly size: number; readonly type: string; slice(start?: number, end?: number, contentType?: string): Blob; + stream(): ReadableStream; + text(): Promise; + arrayBuffer(): Promise; } export interface Body { @@ -317,6 +320,24 @@ export interface PipeOptions { signal?: AbortSignal; } +export interface UnderlyingSource { + cancel?: ReadableStreamErrorCallback; + pull?: ReadableStreamDefaultControllerCallback; + start?: ReadableStreamDefaultControllerCallback; + type?: undefined; +} +export interface ReadableStreamErrorCallback { + (reason: any): void | PromiseLike; +} + +export interface ReadableStreamDefaultControllerCallback { + (controller: ReadableStreamDefaultController): void | PromiseLike; +} + +export interface ReadableStreamConstructor { + new (source?: UnderlyingSource): ReadableStream; +} + export interface ReadableStream { readonly locked: boolean; cancel(reason?: any): Promise;