2023-01-02 16:00:42 -05:00
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
2021-04-06 06:55:05 -04:00
// @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" />
2023-02-07 14:22:46 -05:00
const core = globalThis . Deno . core ;
const ops = core . ops ;
2023-03-08 06:44:54 -05:00
import * as webidl from "ext:deno_webidl/00_webidl.js" ;
2023-02-07 14:22:46 -05:00
const primordials = globalThis . _ _bootstrap . primordials ;
2023-03-08 06:44:54 -05:00
import { forgivingBase64Encode } from "ext:deno_web/00_infra.js" ;
import { EventTarget , ProgressEvent } from "ext:deno_web/02_event.js" ;
import { decode , TextDecoder } from "ext:deno_web/08_text_encoding.js" ;
import { parseMimeType } from "ext:deno_web/01_mimesniff.js" ;
import DOMException from "ext:deno_web/01_dom_exception.js" ;
2023-02-07 14:22:46 -05:00
const {
ArrayPrototypePush ,
ArrayPrototypeReduce ,
FunctionPrototypeCall ,
MapPrototypeGet ,
MapPrototypeSet ,
ObjectDefineProperty ,
queueMicrotask ,
SafeArrayIterator ,
2023-04-14 16:23:28 -04:00
SafeMap ,
2023-02-07 14:22:46 -05:00
Symbol ,
TypedArrayPrototypeSet ,
2023-04-02 13:41:41 -04:00
TypedArrayPrototypeGetBuffer ,
TypedArrayPrototypeGetByteLength ,
TypedArrayPrototypeGetSymbolToStringTag ,
2023-02-07 14:22:46 -05:00
TypeError ,
Uint8Array ,
} = primordials ;
const state = Symbol ( "[[state]]" ) ;
const result = Symbol ( "[[result]]" ) ;
const error = Symbol ( "[[error]]" ) ;
const aborted = Symbol ( "[[aborted]]" ) ;
const handlerSymbol = Symbol ( "eventHandlers" ) ;
class FileReader extends EventTarget {
/** @type {"empty" | "loading" | "done"} */
[ state ] = "empty" ;
/** @type {null | string | ArrayBuffer} */
[ result ] = null ;
/** @type {null | DOMException} */
[ error ] = null ;
/** @type {null | {aborted: boolean}} */
[ aborted ] = null ;
/ * *
* @ param { Blob } blob
* @ param { { kind : "ArrayBuffer" | "Text" | "DataUrl" | "BinaryString" , encoding ? : string } } readtype
* /
# readOperation ( blob , readtype ) {
2023-03-22 21:34:14 -04:00
// 1. If fr's state is "loading", throw an InvalidStateError DOMException.
2023-02-07 14:22:46 -05:00
if ( this [ state ] === "loading" ) {
throw new DOMException (
"Invalid FileReader state." ,
"InvalidStateError" ,
) ;
}
2023-03-22 21:34:14 -04:00
// 2. Set fr's state to "loading".
2023-02-07 14:22:46 -05:00
this [ state ] = "loading" ;
2023-03-22 21:34:14 -04:00
// 3. Set fr's result to null.
2023-02-07 14:22:46 -05:00
this [ result ] = null ;
2023-03-22 21:34:14 -04:00
// 4. Set fr's error to null.
2023-02-07 14:22:46 -05:00
this [ error ] = null ;
// 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 } ;
// 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
( async ( ) => {
while ( ! abortedState . aborted ) {
// 1. Wait for chunkPromise to be fulfilled or rejected.
try {
const chunk = await chunkPromise ;
if ( abortedState . 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 ) {
// TODO(lucacasonato): this is wrong, should be HTML "queue a task"
queueMicrotask ( ( ) => {
if ( abortedState . aborted ) return ;
// 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 &&
2023-04-02 13:41:41 -04:00
TypedArrayPrototypeGetSymbolToStringTag ( chunk . value ) ===
"Uint8Array"
2023-02-07 14:22:46 -05:00
) {
ArrayPrototypePush ( chunks , chunk . value ) ;
// TODO(bartlomieju): (only) If roughly 50ms have passed since last progress
{
const size = ArrayPrototypeReduce (
chunks ,
2023-04-02 13:41:41 -04:00
( p , i ) => p + TypedArrayPrototypeGetByteLength ( i ) ,
2023-02-07 14:22:46 -05:00
0 ,
) ;
const ev = new ProgressEvent ( "progress" , {
loaded : size ,
} ) ;
2021-04-08 09:05:08 -04:00
// 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
2023-02-07 14:22:46 -05: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 ( ( ) => {
if ( abortedState . aborted ) return ;
2023-03-22 21:34:14 -04:00
// 1. Set fr's state to "done".
2023-02-07 14:22:46 -05:00
this [ state ] = "done" ;
2023-03-22 21:34:14 -04:00
// 2. Let result be the result of package data given bytes, type, blob's type, and encodingName.
2023-02-07 14:22:46 -05:00
const size = ArrayPrototypeReduce (
chunks ,
2023-04-02 13:41:41 -04:00
( p , i ) => p + TypedArrayPrototypeGetByteLength ( i ) ,
2023-02-07 14:22:46 -05:00
0 ,
) ;
const bytes = new Uint8Array ( size ) ;
let offs = 0 ;
for ( let i = 0 ; i < chunks . length ; ++ i ) {
const chunk = chunks [ i ] ;
TypedArrayPrototypeSet ( bytes , chunk , offs ) ;
2023-04-02 13:41:41 -04:00
offs += TypedArrayPrototypeGetByteLength ( chunk ) ;
2023-02-07 14:22:46 -05:00
}
switch ( readtype . kind ) {
case "ArrayBuffer" : {
2023-04-02 13:41:41 -04:00
this [ result ] = TypedArrayPrototypeGetBuffer ( bytes ) ;
2023-02-07 14:22:46 -05:00
break ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
case "BinaryString" :
this [ result ] = ops . op _encode _binary _string ( bytes ) ;
break ;
case "Text" : {
let decoder = undefined ;
if ( readtype . encoding ) {
try {
decoder = new TextDecoder ( readtype . encoding ) ;
} catch {
// don't care about the error
2021-04-08 09:05:08 -04:00
}
2023-02-07 14:22:46 -05:00
}
if ( decoder === undefined ) {
const mimeType = parseMimeType ( blob . type ) ;
if ( mimeType ) {
const charset = MapPrototypeGet (
mimeType . parameters ,
"charset" ,
) ;
if ( charset ) {
try {
decoder = new TextDecoder ( charset ) ;
} catch {
// don't care about the error
2021-04-08 09:05:08 -04:00
}
}
}
}
2023-02-07 14:22:46 -05:00
if ( decoder === undefined ) {
decoder = new TextDecoder ( ) ;
2021-04-08 09:05:08 -04:00
}
2023-02-07 14:22:46 -05:00
this [ result ] = decode ( bytes , decoder . encoding ) ;
break ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
case "DataUrl" : {
const mediaType = blob . type || "application/octet-stream" ;
this [ result ] = ` data: ${ mediaType } ;base64, ${
forgivingBase64Encode ( bytes )
} ` ;
break ;
2021-04-08 09:05:08 -04:00
}
2023-02-07 14:22:46 -05:00
}
// 4.2 Fire a progress event called load at the fr.
2021-04-06 06:55:05 -04:00
{
2023-02-07 14:22:46 -05:00
const ev = new ProgressEvent ( "load" , {
lengthComputable : true ,
loaded : size ,
total : size ,
} ) ;
2021-04-06 06:55:05 -04:00
this . dispatchEvent ( ev ) ;
}
2023-03-22 21:34:14 -04:00
// 5. If fr's state is not "loading", fire a progress event called loadend at the fr.
2023-02-07 14:22:46 -05:00
//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.
2021-04-06 06:55:05 -04:00
if ( this [ state ] !== "loading" ) {
2023-02-07 14:22:46 -05:00
const ev = new ProgressEvent ( "loadend" , {
lengthComputable : true ,
loaded : size ,
total : size ,
} ) ;
2021-04-06 06:55:05 -04:00
this . dispatchEvent ( ev ) ;
}
} ) ;
break ;
}
2023-02-07 14:22:46 -05:00
} catch ( err ) {
// TODO(lucacasonato): this is wrong, should be HTML "queue a task"
queueMicrotask ( ( ) => {
if ( abortedState . aborted ) return ;
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
// chunkPromise rejected
this [ state ] = "done" ;
this [ error ] = err ;
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
{
const ev = new ProgressEvent ( "error" , { } ) ;
this . dispatchEvent ( ev ) ;
}
2021-09-12 09:35:05 -04:00
2023-03-22 21:34:14 -04:00
//If fr's state is not "loading", fire a progress event called loadend at fr.
2023-02-07 14:22:46 -05:00
//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 ;
}
}
} ) ( ) ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
# getEventHandlerFor ( name ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
const maybeMap = this [ handlerSymbol ] ;
if ( ! maybeMap ) return null ;
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
return MapPrototypeGet ( maybeMap , name ) ? . handler ? ? null ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
# setEventHandlerFor ( name , value ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
if ( ! this [ handlerSymbol ] ) {
2023-04-14 16:23:28 -04:00
this [ handlerSymbol ] = new SafeMap ( ) ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
let handlerWrapper = MapPrototypeGet ( this [ handlerSymbol ] , name ) ;
if ( handlerWrapper ) {
handlerWrapper . handler = value ;
} else {
handlerWrapper = makeWrappedHandler ( value ) ;
this . addEventListener ( name , handlerWrapper ) ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
MapPrototypeSet ( this [ handlerSymbol ] , name , handlerWrapper ) ;
}
constructor ( ) {
super ( ) ;
this [ webidl . brand ] = webidl . brand ;
}
/** @returns {number} */
get readyState ( ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
switch ( this [ state ] ) {
case "empty" :
return FileReader . EMPTY ;
case "loading" :
return FileReader . LOADING ;
case "done" :
return FileReader . DONE ;
default :
throw new TypeError ( "Invalid state" ) ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
}
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
get result ( ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
return this [ result ] ;
}
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
get error ( ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
return this [ error ] ;
}
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
abort ( ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
// 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 ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
// 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 ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
// 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.
if ( this [ aborted ] !== null ) {
this [ aborted ] . aborted = true ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
// Fire a progress event called abort at the context object.
const ev = new ProgressEvent ( "abort" , { } ) ;
this . dispatchEvent ( ev ) ;
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
// 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 ) ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
/** @param {Blob} blob */
readAsArrayBuffer ( blob ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
const prefix = "Failed to execute 'readAsArrayBuffer' on 'FileReader'" ;
2023-04-12 15:58:57 -04:00
webidl . requiredArguments ( arguments . length , 1 , prefix ) ;
2023-02-07 14:22:46 -05:00
this . # readOperation ( blob , { kind : "ArrayBuffer" } ) ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
/** @param {Blob} blob */
readAsBinaryString ( blob ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'" ;
2023-04-12 15:58:57 -04:00
webidl . requiredArguments ( arguments . length , 1 , prefix ) ;
2023-02-07 14:22:46 -05:00
// alias for readAsArrayBuffer
this . # readOperation ( blob , { kind : "BinaryString" } ) ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
/** @param {Blob} blob */
readAsDataURL ( blob ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
const prefix = "Failed to execute 'readAsDataURL' on 'FileReader'" ;
2023-04-12 15:58:57 -04:00
webidl . requiredArguments ( arguments . length , 1 , prefix ) ;
2023-02-07 14:22:46 -05:00
// alias for readAsArrayBuffer
this . # readOperation ( blob , { kind : "DataUrl" } ) ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
/ * *
* @ param { Blob } blob
* @ param { string } [ encoding ]
* /
readAsText ( blob , encoding = undefined ) {
webidl . assertBranded ( this , FileReaderPrototype ) ;
const prefix = "Failed to execute 'readAsText' on 'FileReader'" ;
2023-04-12 15:58:57 -04:00
webidl . requiredArguments ( arguments . length , 1 , prefix ) ;
2023-02-07 14:22:46 -05:00
if ( encoding !== undefined ) {
encoding = webidl . converters [ "DOMString" ] ( encoding , {
prefix ,
context : "Argument 2" ,
} ) ;
2021-09-12 09:35:05 -04:00
}
2023-02-07 14:22:46 -05:00
// alias for readAsArrayBuffer
this . # readOperation ( blob , { kind : "Text" , encoding } ) ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
get onerror ( ) {
return this . # getEventHandlerFor ( "error" ) ;
}
set onerror ( value ) {
this . # setEventHandlerFor ( "error" , value ) ;
}
2021-09-12 09:35:05 -04:00
2023-02-07 14:22:46 -05:00
get onloadstart ( ) {
return this . # getEventHandlerFor ( "loadstart" ) ;
}
set onloadstart ( value ) {
this . # setEventHandlerFor ( "loadstart" , value ) ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
get onload ( ) {
return this . # getEventHandlerFor ( "load" ) ;
}
set onload ( value ) {
this . # setEventHandlerFor ( "load" , value ) ;
}
get onloadend ( ) {
return this . # getEventHandlerFor ( "loadend" ) ;
}
set onloadend ( value ) {
this . # setEventHandlerFor ( "loadend" , value ) ;
}
get onprogress ( ) {
return this . # getEventHandlerFor ( "progress" ) ;
}
set onprogress ( value ) {
this . # setEventHandlerFor ( "progress" , value ) ;
}
get onabort ( ) {
return this . # getEventHandlerFor ( "abort" ) ;
}
set onabort ( value ) {
this . # setEventHandlerFor ( "abort" , value ) ;
}
}
webidl . configurePrototype ( FileReader ) ;
const FileReaderPrototype = FileReader . prototype ;
ObjectDefineProperty ( FileReader , "EMPTY" , {
writable : false ,
enumerable : true ,
configurable : false ,
value : 0 ,
} ) ;
ObjectDefineProperty ( FileReader , "LOADING" , {
writable : false ,
enumerable : true ,
configurable : false ,
value : 1 ,
} ) ;
ObjectDefineProperty ( FileReader , "DONE" , {
writable : false ,
enumerable : true ,
configurable : false ,
value : 2 ,
} ) ;
ObjectDefineProperty ( FileReader . prototype , "EMPTY" , {
writable : false ,
enumerable : true ,
configurable : false ,
value : 0 ,
} ) ;
ObjectDefineProperty ( FileReader . prototype , "LOADING" , {
writable : false ,
enumerable : true ,
configurable : false ,
value : 1 ,
} ) ;
ObjectDefineProperty ( FileReader . prototype , "DONE" , {
writable : false ,
enumerable : true ,
configurable : false ,
value : 2 ,
} ) ;
function makeWrappedHandler ( handler ) {
function wrappedHandler ( ... args ) {
if ( typeof wrappedHandler . handler !== "function" ) {
return ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
return FunctionPrototypeCall (
wrappedHandler . handler ,
this ,
... new SafeArrayIterator ( args ) ,
) ;
2021-04-06 06:55:05 -04:00
}
2023-02-07 14:22:46 -05:00
wrappedHandler . handler = handler ;
return wrappedHandler ;
}
2021-04-06 06:55:05 -04:00
2023-02-07 14:22:46 -05:00
export { FileReader } ;