UNPKG

@lyleunderwood/filereader-polyfill

Version:

W3C File API specification compliant FileReader polyfill for Node.js environments

148 lines 5.48 kB
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