@lyleunderwood/filereader-polyfill
Version:
W3C File API specification compliant FileReader polyfill for Node.js environments
148 lines • 5.48 kB
JavaScript
export class FileReader extends EventTarget {
constructor() {
super();
// Instance constants (required by W3C spec)
this.EMPTY = 0;
this.LOADING = 1;
this.DONE = 2;
// Instance properties (readonly per W3C spec, but mutable internally)
this.error = null;
this.readyState = FileReader.EMPTY;
this.result = null;
// Event handlers (W3C spec compliant)
this.onabort = null;
this.onerror = null;
this.onload = null;
this.onloadend = null;
this.onloadstart = null;
this.onprogress = null;
this._abortController = null;
}
// Read methods
readAsArrayBuffer(blob) {
this._read(blob, 'arrayBuffer');
}
readAsText(blob, encoding = 'utf-8') {
this._read(blob, 'text', encoding);
}
readAsDataURL(blob) {
this._read(blob, 'dataURL');
}
readAsBinaryString(blob) {
this._read(blob, 'binaryString');
}
abort() {
if (this.readyState !== FileReader.LOADING) {
return;
}
this.readyState = FileReader.DONE;
this.result = null;
this.error = new DOMException('The operation was aborted.', 'AbortError');
if (this._abortController) {
this._abortController.abort();
}
this._fireEvent('abort');
this._fireEvent('loadend');
}
_read(blob, format, encoding) {
if (this.readyState === FileReader.LOADING) {
throw new DOMException('The FileReader is already loading.', 'InvalidStateError');
}
this._performRead(blob, format, encoding);
}
async _performRead(blob, format, encoding) {
this.readyState = FileReader.LOADING;
this.result = null;
this.error = null;
this._abortController = new AbortController();
this._fireEvent('loadstart');
try {
let totalSize = blob.size;
let loadedSize = 0;
// Use stream-based reading for progress events
const reader = blob.stream().getReader();
const chunks = [];
try {
while (true) {
const { done, value } = await reader.read();
if (this._abortController.signal.aborted) {
return; // Abort was called
}
if (done)
break;
chunks.push(value);
loadedSize += value.length;
// Fire progress event (W3C spec compliant)
const progressEvent = new Event('progress');
progressEvent.lengthComputable = totalSize > 0;
progressEvent.loaded = loadedSize;
progressEvent.total = totalSize;
this._fireProgressEvent(progressEvent);
}
}
finally {
reader.releaseLock();
}
if (this._abortController.signal.aborted) {
return; // Abort was called during reading
}
// Combine all chunks
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
// Format the result based on the requested format
switch (format) {
case 'arrayBuffer':
this.result = combined.buffer.slice(combined.byteOffset, combined.byteOffset + combined.byteLength);
break;
case 'text':
this.result = new TextDecoder(encoding).decode(combined);
break;
case 'dataURL':
const base64 = Buffer.from(combined).toString('base64');
this.result = `data:${blob.type || 'application/octet-stream'};base64,${base64}`;
break;
case 'binaryString':
this.result = Buffer.from(combined).toString('latin1');
break;
}
this.readyState = FileReader.DONE;
this._fireEvent('load');
this._fireEvent('loadend');
}
catch (error) {
if (this._abortController.signal.aborted) {
return; // Abort was called
}
this.readyState = FileReader.DONE;
this.error = error instanceof DOMException ? error : new DOMException(String(error), 'NotReadableError');
this.result = null;
this._fireEvent('error');
this._fireEvent('loadend');
}
}
_fireEvent(type) {
const event = new Event(type);
this.dispatchEvent(event);
// Also call the corresponding handler if set
const handler = this[`on${type}`];
if (typeof handler === 'function') {
handler.call(this, event);
}
}
_fireProgressEvent(event) {
this.dispatchEvent(event);
if (typeof this.onprogress === 'function') {
this.onprogress.call(this, event);
}
}
}
// ReadyState constants (W3C spec compliant)
FileReader.EMPTY = 0;
FileReader.LOADING = 1;
FileReader.DONE = 2;
//# sourceMappingURL=FileReader.js.map