2021-04-06 06:55:05 -04:00
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference no-default-lib="true" />
/// <reference path="../../core/lib.deno_core.d.ts" />
2021-07-06 10:20:21 -04:00
/// <reference path="../../core/internal.d.ts" />
2021-04-06 06:55:05 -04:00
/// <reference path="../webidl/internal.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference lib="esnext" />
"use strict" ;
( ( window ) => {
const webidl = window . _ _bootstrap . webidl ;
2021-06-05 17:10:07 -04:00
const { forgivingBase64Encode } = window . _ _bootstrap . infra ;
const { decode , TextDecoder } = window . _ _bootstrap . encoding ;
2021-04-08 09:05:08 -04:00
const { parseMimeType } = window . _ _bootstrap . mimesniff ;
2021-07-03 15:32:28 -04:00
const { DOMException } = window . _ _bootstrap . domException ;
2021-07-06 10:20:21 -04:00
const {
ArrayPrototypeJoin ,
ArrayPrototypeMap ,
ArrayPrototypePush ,
ArrayPrototypeReduce ,
FunctionPrototypeCall ,
Map ,
MapPrototypeGet ,
MapPrototypeSet ,
ObjectDefineProperty ,
StringFromCodePoint ,
Symbol ,
SymbolToStringTag ,
TypedArrayPrototypeSet ,
TypeError ,
Uint8Array ,
} = window . _ _bootstrap . primordials ;
2021-04-06 06:55:05 -04:00
const state = Symbol ( "[[state]]" ) ;
const result = Symbol ( "[[result]]" ) ;
const error = Symbol ( "[[error]]" ) ;
const aborted = Symbol ( "[[aborted]]" ) ;
class FileReader extends EventTarget {
2021-07-06 10:20:21 -04:00
get [ SymbolToStringTag ] ( ) {
2021-04-14 16:49:16 -04:00
return "FileReader" ;
}
2021-04-06 06:55:05 -04:00
/** @type {"empty" | "loading" | "done"} */
[ state ] = "empty" ;
/** @type {null | string | ArrayBuffer} */
[ result ] = null ;
/** @type {null | DOMException} */
[ error ] = null ;
2021-07-14 06:08:42 -04:00
/** @type {null | {aborted: boolean}} */
[ aborted ] = null ;
2021-04-06 06:55:05 -04:00
2021-04-28 10:08:51 -04:00
/ * *
2021-04-06 06:55:05 -04:00
* @ param { Blob } blob
2021-04-08 09:05:08 -04:00
* @ param { { kind : "ArrayBuffer" | "Text" | "DataUrl" | "BinaryString" , encoding ? : string } } readtype
2021-04-06 06:55:05 -04:00
* /
2021-05-27 19:33:11 -04:00
# readOperation ( blob , readtype ) {
2021-04-06 06:55:05 -04:00
// 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 ;
2021-07-14 06:08:42 -04:00
// We set this[aborted] to a new object, and keep track of it in a
// separate variable, so if a new read operation starts while there are
// remaining tasks from a previous aborted operation, the new operation
// will run while the tasks from the previous one are still aborted.
const abortedState = this [ aborted ] = { aborted : false } ;
2021-04-06 06:55:05 -04:00
// 5. Let stream be the result of calling get stream on blob.
const stream /*: ReadableStream<ArrayBufferView>*/ = 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
2021-04-08 09:05:08 -04:00
( async ( ) => {
2021-07-14 06:08:42 -04:00
while ( ! abortedState . aborted ) {
2021-04-08 09:05:08 -04:00
// 1. Wait for chunkPromise to be fulfilled or rejected.
try {
const chunk = await chunkPromise ;
2021-07-14 06:08:42 -04:00
if ( abortedState . aborted ) return ;
2021-04-08 09:05:08 -04:00
// 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr.
if ( isFirstChunk ) {
// TODO(lucacasonato): this is wrong, should be HTML "queue a task"
queueMicrotask ( ( ) => {
2021-07-14 06:08:42 -04:00
if ( abortedState . aborted ) return ;
2021-04-08 09:05:08 -04:00
// fire a progress event for loadstart
const ev = new ProgressEvent ( "loadstart" , { } ) ;
this . dispatchEvent ( ev ) ;
2021-04-06 06:55:05 -04:00
} ) ;
}
2021-04-08 09:05:08 -04:00
// 3. Set isFirstChunk to false.
isFirstChunk = false ;
2021-04-06 06:55:05 -04:00
2021-04-08 09:05:08 -04:00
// 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 ) {
2021-07-06 10:20:21 -04:00
ArrayPrototypePush ( chunks , chunk . value ) ;
2021-04-08 09:05:08 -04:00
// TODO(bartlomieju): (only) If roughly 50ms have passed since last progress
{
2021-07-06 10:20:21 -04:00
const size = ArrayPrototypeReduce (
chunks ,
( p , i ) => p + i . byteLength ,
0 ,
) ;
2021-04-08 09:05:08 -04:00
const ev = new ProgressEvent ( "progress" , {
loaded : size ,
} ) ;
// TODO(lucacasonato): this is wrong, should be HTML "queue a task"
queueMicrotask ( ( ) => {
2021-07-14 06:08:42 -04:00
if ( abortedState . aborted ) return ;
2021-04-08 09:05:08 -04:00
this . dispatchEvent ( ev ) ;
} ) ;
2021-04-06 06:55:05 -04:00
}
2021-04-08 09:05:08 -04:00
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 ) {
// TODO(lucacasonato): this is wrong, should be HTML "queue a task"
queueMicrotask ( ( ) => {
2021-07-14 06:08:42 -04:00
if ( abortedState . aborted ) return ;
2021-04-08 09:05:08 -04:00
// 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.
2021-07-06 10:20:21 -04:00
const size = ArrayPrototypeReduce (
chunks ,
( p , i ) => p + i . byteLength ,
0 ,
) ;
2021-04-08 09:05:08 -04:00
const bytes = new Uint8Array ( size ) ;
let offs = 0 ;
for ( const chunk of chunks ) {
2021-07-06 10:20:21 -04:00
TypedArrayPrototypeSet ( bytes , chunk , offs ) ;
2021-04-08 09:05:08 -04:00
offs += chunk . byteLength ;
2021-04-06 06:55:05 -04:00
}
2021-04-08 09:05:08 -04:00
switch ( readtype . kind ) {
case "ArrayBuffer" : {
this [ result ] = bytes . buffer ;
break ;
}
case "BinaryString" :
2021-07-06 10:20:21 -04:00
this [ result ] = ArrayPrototypeJoin (
ArrayPrototypeMap (
[ ... new Uint8Array ( bytes . buffer ) ] ,
( v ) => StringFromCodePoint ( v ) ,
) ,
"" ,
) ;
2021-04-08 09:05:08 -04:00
break ;
case "Text" : {
let decoder = undefined ;
if ( readtype . encoding ) {
try {
decoder = new TextDecoder ( readtype . encoding ) ;
} catch {
// don't care about the error
}
}
if ( decoder === undefined ) {
const mimeType = parseMimeType ( blob . type ) ;
if ( mimeType ) {
2021-07-06 10:20:21 -04:00
const charset = MapPrototypeGet (
mimeType . parameters ,
"charset" ,
) ;
2021-04-08 09:05:08 -04:00
if ( charset ) {
try {
decoder = new TextDecoder ( charset ) ;
} catch {
// don't care about the error
}
}
}
}
if ( decoder === undefined ) {
decoder = new TextDecoder ( ) ;
}
this [ result ] = decode ( bytes , decoder . encoding ) ;
break ;
}
case "DataUrl" : {
const mediaType = blob . type || "application/octet-stream" ;
this [ result ] = ` data: ${ mediaType } ;base64, ${
2021-06-05 17:10:07 -04:00
forgivingBase64Encode ( bytes )
2021-04-08 09:05:08 -04:00
} ` ;
break ;
}
2021-04-06 06:55:05 -04:00
}
2021-04-08 09:05:08 -04:00
// 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 ) ;
2021-04-06 06:55:05 -04:00
}
2021-04-08 09:05:08 -04:00
// 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 ) {
// TODO(lucacasonato): this is wrong, should be HTML "queue a task"
queueMicrotask ( ( ) => {
2021-07-14 06:08:42 -04:00
if ( abortedState . aborted ) return ;
2021-04-08 09:05:08 -04:00
// chunkPromise rejected
this [ state ] = "done" ;
this [ error ] = err ;
2021-04-06 06:55:05 -04:00
{
2021-04-08 09:05:08 -04:00
const ev = new ProgressEvent ( "error" , { } ) ;
2021-04-06 06:55:05 -04:00
this . dispatchEvent ( ev ) ;
}
2021-04-08 09:05:08 -04:00
//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.
2021-04-06 06:55:05 -04:00
if ( this [ state ] !== "loading" ) {
2021-04-08 09:05:08 -04:00
const ev = new ProgressEvent ( "loadend" , { } ) ;
2021-04-06 06:55:05 -04:00
this . dispatchEvent ( ev ) ;
}
} ) ;
break ;
}
}
2021-04-08 09:05:08 -04:00
} ) ( ) ;
2021-05-27 19:33:11 -04:00
}
2021-04-06 06:55:05 -04:00
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.
2021-07-14 06:08:42 -04:00
if ( this [ aborted ] !== null ) {
this [ aborted ] . aborted = true ;
}
2021-04-06 06:55:05 -04:00
// 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
2021-04-08 09:05:08 -04:00
this . # readOperation ( blob , { kind : "BinaryString" } ) ;
2021-04-06 06:55:05 -04:00
}
/** @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" } ) ;
}
2021-04-28 10:08:51 -04:00
/ * *
2021-04-06 06:55:05 -04:00
* @ 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 } ) ;
}
}
2021-06-07 04:04:10 -04:00
webidl . configurePrototype ( FileReader ) ;
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( FileReader , "EMPTY" , {
2021-04-08 09:05:08 -04:00
writable : false ,
enumerable : true ,
configurable : false ,
value : 0 ,
} ) ;
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( FileReader , "LOADING" , {
2021-04-08 09:05:08 -04:00
writable : false ,
enumerable : true ,
configurable : false ,
value : 1 ,
} ) ;
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( FileReader , "DONE" , {
2021-04-08 09:05:08 -04:00
writable : false ,
enumerable : true ,
configurable : false ,
value : 2 ,
} ) ;
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( FileReader . prototype , "EMPTY" , {
2021-04-08 09:05:08 -04:00
writable : false ,
enumerable : true ,
configurable : false ,
value : 0 ,
} ) ;
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( FileReader . prototype , "LOADING" , {
2021-04-08 09:05:08 -04:00
writable : false ,
enumerable : true ,
configurable : false ,
value : 1 ,
} ) ;
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( FileReader . prototype , "DONE" , {
2021-04-08 09:05:08 -04:00
writable : false ,
enumerable : true ,
configurable : false ,
value : 2 ,
} ) ;
2021-04-06 06:55:05 -04:00
const handlerSymbol = Symbol ( "eventHandlers" ) ;
function makeWrappedHandler ( handler ) {
function wrappedHandler ( ... args ) {
if ( typeof wrappedHandler . handler !== "function" ) {
return ;
}
2021-07-06 10:20:21 -04:00
return FunctionPrototypeCall ( wrappedHandler . handler , this , ... args ) ;
2021-04-06 06:55:05 -04:00
}
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
2021-07-06 10:20:21 -04:00
ObjectDefineProperty ( emitter , ` on ${ name } ` , {
2021-04-06 06:55:05 -04:00
get ( ) {
2021-07-06 10:20:21 -04:00
const maybeMap = this [ handlerSymbol ] ;
if ( ! maybeMap ) return null ;
return MapPrototypeGet ( maybeMap , name ) ? . handler ? ? null ;
2021-04-06 06:55:05 -04:00
} ,
set ( value ) {
if ( ! this [ handlerSymbol ] ) {
this [ handlerSymbol ] = new Map ( ) ;
}
2021-07-06 10:20:21 -04:00
let handlerWrapper = MapPrototypeGet ( this [ handlerSymbol ] , name ) ;
2021-04-06 06:55:05 -04:00
if ( handlerWrapper ) {
handlerWrapper . handler = value ;
} else {
handlerWrapper = makeWrappedHandler ( value ) ;
this . addEventListener ( name , handlerWrapper ) ;
}
2021-07-06 10:20:21 -04:00
MapPrototypeSet ( this [ handlerSymbol ] , name , handlerWrapper ) ;
2021-04-06 06:55:05 -04:00
} ,
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 ) ;