Skip to content

Commit 42e516a

Browse files
committed
Rewrite Book.loadFromFetch() using async/await.
1 parent 44e16b0 commit 42e516a

File tree

3 files changed

+79
-87
lines changed

3 files changed

+79
-87
lines changed

code/book-binder.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export class BookBinder extends EventTarget {
220220
* @param {string} fileNameOrUri The filename or URI. Must end in a file extension that can be
221221
* used to guess what type of book this is.
222222
* @param {ArrayBuffer} ab The initial ArrayBuffer to start the unarchiving process.
223-
* @param {number} totalExpectedSize Thee total expected size of the archived book in bytes.
223+
* @param {number} totalExpectedSize The total expected size of the archived book in bytes.
224224
* @returns {Promise<BookBinder>} A Promise that will resolve with a BookBinder.
225225
*/
226226
export function createBookBinderAsync(fileNameOrUri, ab, totalExpectedSize) {

code/book.js

Lines changed: 55 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ export class Book extends EventTarget {
321321
* Starts a fetch and progressively loads in the book.
322322
* @returns {Promise<Book>} A Promise that returns this book when all bytes have been fed to it.
323323
*/
324-
loadFromFetch() {
324+
async loadFromFetch() {
325325
if (!this.#needsLoading) {
326326
throw 'Cannot try to load via Fetch when the Book is already loading or loaded';
327327
}
@@ -332,88 +332,62 @@ export class Book extends EventTarget {
332332
this.#needsLoading = false;
333333
this.dispatchEvent(new BookLoadingStartedEvent(this));
334334

335-
return fetch(this.#request).then(response => {
336-
337-
let pendingChunks = [];
338-
const reader = response.body.getReader();
339-
/**
340-
* This function receives a chunk. On the first chunk, it starts the async process of creating
341-
* a BookBinder (analyzing the bytes and loading a script to instantiate the binder). Until
342-
* the binder is created, all chunks are stored in the pendingChunks array. Once the binder
343-
* object is created, all pending chunks are processed in order. On the last chunk, send the
344-
* BookLoadingComplete event.
345-
* @returns {Promise<Book>}
346-
*/
347-
const readAndProcessNextChunk = () => {
348-
reader.read().then(({ done, value }) => {
349-
if (Params['debugFetch'] === 'true') {
350-
let str = `debugFetch: readAndProcessNextChunk(), `;
351-
if (done) { str += 'done'; }
352-
else { str += `buffer.byteLength=${value.buffer.byteLength}`; }
353-
console.log(str);
354-
}
355-
if (!done) {
356-
// value is a chunk of the file as a Uint8Array.
357-
if (!this.#bookBinder) {
358-
// It is possible for #startBookBinding() to not set #bookBinder immediately because
359-
// the binder script or the unarchiving script needs to load and be connected.
360-
if (this.#startedBinding) {
361-
if (Params['debugFetch'] === 'true') {
362-
console.log(`debugFetch: Found a pending chunk of length ${value.buffer.byteLength}`);
363-
}
364-
pendingChunks.push(value.buffer);
365-
return readAndProcessNextChunk();
366-
}
367-
if (Params['debugFetch'] === 'true') {
368-
console.log(`debugFetch: Got first chunk of length ${value.buffer.byteLength}`);
369-
}
370-
// Else if binding has not started, start the book binding process (asynchronously).
371-
return this.#startBookBinding(this.#name, value.buffer, this.#expectedSize).then(() => {
372-
if (!this.#bookBinder) {
373-
throw `bookBinder was not set after startBookBinding()`;
374-
}
375-
// After this point, #bookBinder is set.
376-
if (Params['debugFetch'] === 'true') {
377-
console.log(`debugFetch: Book Binder created`);
378-
}
379-
if (pendingChunks.length > 0) {
380-
for (const chunk of pendingChunks) {
381-
this.appendBytes(chunk);
382-
this.#bookBinder.appendBytes(chunk);
383-
if (Params['debugFetch'] === 'true') {
384-
console.log(`debugFetch: Processed chunk of length ${chunk.byteLength}`);
385-
}
386-
}
387-
pendingChunks = [];
388-
}
389-
return readAndProcessNextChunk();
390-
})
391-
} // if (!this.#bookBinder)
392-
393-
if (Params['debugFetch'] === 'true') {
394-
console.log(`debugFetch: Got a chunk after binding of length ${value.buffer.byteLength}`);
395-
}
396-
397-
// Process the chunk.
398-
this.appendBytes(value.buffer);
399-
this.#bookBinder.appendBytes(value.buffer);
400-
401-
return readAndProcessNextChunk();
402-
} else {
403-
if (Params['debugFetch'] === 'true') {
404-
console.log(`debugFetch: Got to the end, done fetching book chunks`);
405-
}
406-
this.#finishedLoading = true;
407-
this.dispatchEvent(new BookLoadingCompleteEvent(this));
408-
return this;
409-
}
410-
});
411-
};
412-
return readAndProcessNextChunk();
413-
}).catch(e => {
335+
/** @type {Response} */
336+
let response;
337+
try {
338+
response = await fetch(this.#request);
339+
} catch (e) {
414340
console.error(`Error from fetch: ${e}`);
415341
throw e;
416-
});
342+
}
343+
344+
// =============================================================================================
345+
// Option 1: Readable code, fetching chunk by chunk using await.
346+
const reader = response.body.getReader();
347+
348+
/**
349+
* Reads one chunk at a time.
350+
* @returns {Promise<ArrayBuffer | null>}
351+
*/
352+
const getOneChunk = async () => {
353+
const { done, value } = await reader.read();
354+
if (!done) return value.buffer;
355+
return null;
356+
};
357+
358+
const firstChunk = await getOneChunk();
359+
if (!firstChunk) {
360+
throw `Could not get one chunk from fetch()`;
361+
}
362+
let bytesTotal = firstChunk.byteLength;
363+
364+
// Asynchronously wait for the BookBinder and its implementation to be connected.
365+
await this.#startBookBinding(this.#name, firstChunk, this.#expectedSize);
366+
367+
// Read out all subsequent chunks.
368+
/** @type {ArrayBuffer | null} */
369+
let nextChunk;
370+
while (nextChunk = await getOneChunk()) {
371+
bytesTotal += nextChunk.byteLength;
372+
this.appendBytes(nextChunk);
373+
this.#bookBinder.appendBytes(nextChunk);
374+
}
375+
376+
377+
// =============================================================================================
378+
// Option 2: The XHR way (grab all bytes and only then start book binding).
379+
// const ab = await response.arrayBuffer();
380+
// bytesTotal = ab.byteLength;
381+
// await this.#startBookBinding(this.#name, ab, this.#expectedSize);
382+
383+
// Send out BookLoadingComplete event and return this book.
384+
this.#finishedLoading = true;
385+
this.dispatchEvent(new BookLoadingCompleteEvent(this));
386+
if (Params['debugFetch'] === 'true') {
387+
console.log(`debugFetch: ArrayBuffers were total length ${bytesTotal}`);
388+
}
389+
390+
return this;
417391
}
418392

419393
/**

code/epub/epub-book-binder.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,36 +57,50 @@ export class EPUBBookBinder extends BookBinder {
5757
/**
5858
* A map of all files in the archive, keyed by its full path in the archive with the value
5959
* being the raw ArrayBuffer.
60-
* @private {Map<string, Uint8Array>}
60+
* @type {Map<string, Uint8Array>}
61+
* @private
6162
*/
6263
this.fileMap_ = new Map();
6364

64-
/** @private {string} */
65+
/**
66+
* @type {string}
67+
* @private
68+
*/
6569
this.opfFilename_ = undefined;
6670

6771
/**
6872
* Maps the id of each manifest item to its file reference.
69-
* @private {Map<string, FileRef>}
73+
* @type {Map<string, FileRef>}
74+
* @private
7075
*/
7176
this.manifestFileMap_ = new Map();
7277

7378
/**
7479
* The ordered list of reading items.
75-
* @private {Array<FileRef>}
80+
* @type {Array<FileRef>}
81+
* @private
7682
*/
7783
this.spineRefs_ = [];
7884
}
7985

8086
/** @override */
8187
beforeStart_() {
88+
if (Params['debugFetch'] === 'true') {
89+
console.log(`EPubBookBinder.beforeStart_()`);
90+
}
8291
let firstFile = true;
92+
let numExtractions = 0;
8393
this.unarchiver.onExtract(evt => {
94+
numExtractions++;
8495
/** @type {import('../bitjs/archive/decompress.js').UnarchivedFile} */
8596
const theFile = evt.unarchivedFile;
8697
if (Params['debugFetch'] === 'true' && this.fileMap_.has(theFile.filename)) {
8798
// TODO: How does it get multiple extract events for the same file?
88-
debugger;
99+
console.error(`debugFetch: Received an EXTRACT event for ${theFile.filename}, but already have that file!`);
100+
return;
89101
}
102+
103+
// This is a new file. Add it to the map.
90104
this.fileMap_.set(theFile.filename, theFile.fileData);
91105
if (Params['debugFetch'] === 'true') {
92106
console.log(`debugFetch: Extracted file ${theFile.filename} of size ${theFile.fileData.byteLength}`);
@@ -99,6 +113,10 @@ export class EPUBBookBinder extends BookBinder {
99113
}
100114
});
101115
this.unarchiver.addEventListener(UnarchiveEventType.FINISH, evt => {
116+
if (Params['debugFetch'] === 'true') {
117+
console.log(`debugFetch: Received UnarchiveEventType.FINISH event with ${numExtractions} extractions`);
118+
}
119+
102120
this.setUnarchiveComplete();
103121
this.dispatchEvent(new BookProgressEvent(this));
104122
this.parseContainer_();

0 commit comments

Comments
 (0)