From 1838f89486c55cc308264706d289a6d0ec77f57d Mon Sep 17 00:00:00 2001 From: Nick K Date: Mon, 27 Sep 2021 23:26:01 +0300 Subject: [PATCH 01/10] Use private class fields in parts initialization directly. --- index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index a97500c..c1cc5de 100644 --- a/index.js +++ b/index.js @@ -58,8 +58,6 @@ const _Blob = class Blob { * @param {{ type?: string }} [options] */ constructor(blobParts = [], options = {}) { - const parts = []; - let size = 0; if (typeof blobParts !== 'object') { throw new TypeError(`Failed to construct 'Blob': parameter 1 is not an iterable object.`); } @@ -82,15 +80,13 @@ const _Blob = class Blob { part = new TextEncoder().encode(element); } - size += ArrayBuffer.isView(part) ? part.byteLength : part.size; - parts.push(part); + this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size; + this.#parts.push(part); } const type = options.type === undefined ? '' : String(options.type); this.#type = /^[\x20-\x7E]*$/.test(type) ? type : ''; - this.#size = size; - this.#parts = parts; } /** From 81c73fbb096ed8ad926714cee96bde7d868c2948 Mon Sep 17 00:00:00 2001 From: Nick K Date: Mon, 27 Sep 2021 23:44:52 +0300 Subject: [PATCH 02/10] Move Blob.slice implementation into a generator function. --- index.js | 85 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/index.js b/index.js index c1cc5de..d0677be 100644 --- a/index.js +++ b/index.js @@ -42,6 +42,48 @@ async function * toIterator (parts, clone = true) { } } +/** + * @param {Blob | NodeBlob | Uint8Array} blobParts + * @param {number} blobSize + * @param {number} start + * @param {number} end + */ +function* sliceBlob(blobParts, blobSize, start, end) { + let relativeStart = start < 0 ? Math.max(blobSize + start, 0) : Math.min(start, blobSize); + let relativeEnd = end < 0 ? Math.max(blobSize + end, 0) : Math.min(end, blobSize); + + const span = Math.max(relativeEnd - relativeStart, 0); + + let added = 0; + for (const part of blobParts) { + if (added >= span) { + break; + } + + const partSize = ArrayBuffer.isView(part) ? part.byteLength : part.size; + if (relativeStart && partSize <= relativeStart) { + // Skip the beginning and change the relative + // start & end position as we skip the unwanted parts + relativeStart -= partSize; + relativeEnd -= partSize; + } else { + let chunk; + if (ArrayBuffer.isView(part)) { + chunk = part.subarray(relativeStart, Math.min(partSize, relativeEnd)); + added += chunk.byteLength; + } else { + chunk = part.slice(relativeStart, Math.min(partSize, relativeEnd)); + added += chunk.size; + } + + relativeEnd -= partSize; + relativeStart = 0; // All next sequential parts should start at 0 + + yield chunk; + } + } +} + const _Blob = class Blob { /** @type {Array.<(Blob|Uint8Array)>} */ @@ -169,48 +211,7 @@ const _Blob = class Blob { * @param {string} [type] */ slice(start = 0, end = this.size, type = '') { - const {size} = this; - - let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); - let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); - - const span = Math.max(relativeEnd - relativeStart, 0); - const parts = this.#parts; - const blobParts = []; - let added = 0; - - for (const part of parts) { - // don't add the overflow to new blobParts - if (added >= span) { - break; - } - - const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; - if (relativeStart && size <= relativeStart) { - // Skip the beginning and change the relative - // start & end position as we skip the unwanted parts - relativeStart -= size; - relativeEnd -= size; - } else { - let chunk - if (ArrayBuffer.isView(part)) { - chunk = part.subarray(relativeStart, Math.min(size, relativeEnd)); - added += chunk.byteLength - } else { - chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); - added += chunk.size - } - relativeEnd -= size; - blobParts.push(chunk); - relativeStart = 0; // All next sequential parts should start at 0 - } - } - - const blob = new Blob([], {type: String(type).toLowerCase()}); - blob.#size = span; - blob.#parts = blobParts; - - return blob; + return new Blob(sliceBlob(this.#parts, this.size, start, end), {type}) } get [Symbol.toStringTag]() { From 238c2fb445986e4317099e6071ed0f7c23a59af8 Mon Sep 17 00:00:00 2001 From: Nick K Date: Mon, 27 Sep 2021 23:51:44 +0300 Subject: [PATCH 03/10] Move encoder initialization outside the loop --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index d0677be..6cd89a4 100644 --- a/index.js +++ b/index.js @@ -110,6 +110,7 @@ const _Blob = class Blob { if (options === null) options = {}; + const encoder = new TextEncoder() for (const element of blobParts) { let part; if (ArrayBuffer.isView(element)) { @@ -119,7 +120,7 @@ const _Blob = class Blob { } else if (element instanceof Blob) { part = element; } else { - part = new TextEncoder().encode(element); + part = encoder.encode(element); } this.#size += ArrayBuffer.isView(part) ? part.byteLength : part.size; From 56838bffd4621668960ea8d900dc0fd97dcf6d0b Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 28 Sep 2021 00:01:00 +0300 Subject: [PATCH 04/10] Improve errors reported by Blob constructor. --- index.js | 12 ++++++++---- test.js | 12 +++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 6cd89a4..ba3efcd 100644 --- a/index.js +++ b/index.js @@ -100,12 +100,16 @@ const _Blob = class Blob { * @param {{ type?: string }} [options] */ constructor(blobParts = [], options = {}) { - if (typeof blobParts !== 'object') { - throw new TypeError(`Failed to construct 'Blob': parameter 1 is not an iterable object.`); - } + if (typeof blobParts !== "object" || blobParts === null) { + throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'); + } + + if (typeof blobParts[Symbol.iterator] !== "function") { + throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.'); + } if (typeof options !== 'object' && typeof options !== 'function') { - throw new TypeError(`Failed to construct 'Blob': parameter 2 cannot convert to dictionary.`); + throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.'); } if (options === null) options = {}; diff --git a/test.js b/test.js index 0a9688b..33e2c6f 100644 --- a/test.js +++ b/test.js @@ -58,7 +58,17 @@ test('Blob ctor reads blob parts from object with @@iterator', async t => { }); test('Blob ctor throws a string', t => { - t.throws(() => new Blob('abc')); + t.throws(() => new Blob('abc'), { + instanceOf: TypeError, + message: 'Failed to construct \'Blob\': The provided value cannot be converted to a sequence.' + }); +}); + +test('Blob ctor throws an error for an object that does not have @@iterable method', t => { + t.throws(() => new Blob({}), { + instanceOf: TypeError, + message: 'Failed to construct \'Blob\': The object must have a callable @@iterator property.' + }); }); test('Blob ctor threats Uint8Array as a sequence', async t => { From 355a7891c8ff9a67f2261c452d343de9a32f4f47 Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 28 Sep 2021 00:19:42 +0300 Subject: [PATCH 05/10] Simulate WebIDL type casting for lastModified in File constructor. --- file.js | 8 ++++++-- test.js | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/file.js b/file.js index e324d8b..e16e299 100644 --- a/file.js +++ b/file.js @@ -17,8 +17,12 @@ const _File = class File extends Blob { if (options === null) options = {}; - const modified = Number(options.lastModified); - this.#lastModified = Number.isNaN(modified) ? Date.now() : modified + // Simulate WebIDL type casting for NaN value in lastModified option. + const lastModified = options.lastModified === undefined ? Date.now() : Number(options.lastModified); + if (!Number.isNaN(lastModified)) { + this.#lastModified = lastModified; + } + this.#name = String(fileName); } diff --git a/test.js b/test.js index 33e2c6f..5db89c3 100644 --- a/test.js +++ b/test.js @@ -365,6 +365,25 @@ test('new File(,,{lastModified: new Date()})', t => { t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms }); +test('new File(,,{lastModified: null})', t => { + const mod = new File([], '', {lastModified: null}).lastModified; + t.is(mod, 0); +}); + +test("Interpretes NaN value in lastModified option as 0", t => { + t.plan(3); + + const values = ['Not a Number', [], {}]; + + // I can't really see anything about this in the spec, + // but this is how browsers handle type casting for this option... + for (const lastModified of values) { + const file = new File(['Some content'], 'file.txt', {lastModified}); + + t.is(file.lastModified, 0); + } +}); + test('new File(,,{}) sets current time', t => { const mod = new File([], '').lastModified - Date.now(); t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms From 05f23d9c5fa2878e5106a36c7350c65c8a396cc5 Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 28 Sep 2021 00:38:37 +0300 Subject: [PATCH 06/10] Add cancel method for Blob.stream() underlying source. --- index.js | 4 ++++ test.js | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ba3efcd..d1bec41 100644 --- a/index.js +++ b/index.js @@ -202,6 +202,10 @@ const _Blob = class Blob { async pull(ctrl) { const chunk = await it.next(); chunk.done ? ctrl.close() : ctrl.enqueue(chunk.value); + }, + + async cancel() { + await it.return() } }) } diff --git a/test.js b/test.js index 5db89c3..e8b6112 100644 --- a/test.js +++ b/test.js @@ -133,6 +133,20 @@ test('Blob stream()', async t => { } }); +test('Blob stream() can be cancelled', async t => { + const stream = new Blob(['Some content']).stream(); + + // Cancel the stream before start reading, or this will throw an error + await stream.cancel(); + + const iterator = stream[Symbol.asyncIterator](); + + const {done, value: chunk} = await iterator.next(); + + t.true(done); + t.is(chunk, undefined); +}); + test('Blob toString()', t => { const data = 'a=1'; const type = 'text/plain'; @@ -365,12 +379,17 @@ test('new File(,,{lastModified: new Date()})', t => { t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms }); +test('new File(,,{lastModified: undefined})', t => { + const mod = new File([], '', {lastModified: undefined}).lastModified - Date.now(); + t.true(mod <= 0 && mod >= -20); // Close to tolerance: 0.020ms +}); + test('new File(,,{lastModified: null})', t => { const mod = new File([], '', {lastModified: null}).lastModified; t.is(mod, 0); }); -test("Interpretes NaN value in lastModified option as 0", t => { +test('Interpretes NaN value in lastModified option as 0', t => { t.plan(3); const values = ['Not a Number', [], {}]; From 39ce808183aa1f5bf5159d6c9794311757305400 Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 28 Sep 2021 01:00:21 +0300 Subject: [PATCH 07/10] Fix indentation inconsistency. --- index.js | 72 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index d1bec41..42db054 100644 --- a/index.js +++ b/index.js @@ -50,38 +50,38 @@ async function * toIterator (parts, clone = true) { */ function* sliceBlob(blobParts, blobSize, start, end) { let relativeStart = start < 0 ? Math.max(blobSize + start, 0) : Math.min(start, blobSize); - let relativeEnd = end < 0 ? Math.max(blobSize + end, 0) : Math.min(end, blobSize); - - const span = Math.max(relativeEnd - relativeStart, 0); - - let added = 0; - for (const part of blobParts) { - if (added >= span) { - break; - } - - const partSize = ArrayBuffer.isView(part) ? part.byteLength : part.size; - if (relativeStart && partSize <= relativeStart) { - // Skip the beginning and change the relative - // start & end position as we skip the unwanted parts - relativeStart -= partSize; - relativeEnd -= partSize; - } else { - let chunk; - if (ArrayBuffer.isView(part)) { - chunk = part.subarray(relativeStart, Math.min(partSize, relativeEnd)); - added += chunk.byteLength; - } else { - chunk = part.slice(relativeStart, Math.min(partSize, relativeEnd)); - added += chunk.size; - } - - relativeEnd -= partSize; - relativeStart = 0; // All next sequential parts should start at 0 - - yield chunk; - } - } + let relativeEnd = end < 0 ? Math.max(blobSize + end, 0) : Math.min(end, blobSize); + + const span = Math.max(relativeEnd - relativeStart, 0); + + let added = 0; + for (const part of blobParts) { + if (added >= span) { + break; + } + + const partSize = ArrayBuffer.isView(part) ? part.byteLength : part.size; + if (relativeStart && partSize <= relativeStart) { + // Skip the beginning and change the relative + // start & end position as we skip the unwanted parts + relativeStart -= partSize; + relativeEnd -= partSize; + } else { + let chunk; + if (ArrayBuffer.isView(part)) { + chunk = part.subarray(relativeStart, Math.min(partSize, relativeEnd)); + added += chunk.byteLength; + } else { + chunk = part.slice(relativeStart, Math.min(partSize, relativeEnd)); + added += chunk.size; + } + + relativeEnd -= partSize; + relativeStart = 0; // All next sequential parts should start at 0 + + yield chunk; + } + } } const _Blob = class Blob { @@ -101,12 +101,12 @@ const _Blob = class Blob { */ constructor(blobParts = [], options = {}) { if (typeof blobParts !== "object" || blobParts === null) { - throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'); - } + throw new TypeError('Failed to construct \'Blob\': The provided value cannot be converted to a sequence.'); + } if (typeof blobParts[Symbol.iterator] !== "function") { - throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.'); - } + throw new TypeError('Failed to construct \'Blob\': The object must have a callable @@iterator property.'); + } if (typeof options !== 'object' && typeof options !== 'function') { throw new TypeError('Failed to construct \'Blob\': parameter 2 cannot convert to dictionary.'); From e0f5895bc199fccdf92f8caa01df2e93dc43cee1 Mon Sep 17 00:00:00 2001 From: Nick K Date: Sat, 16 Oct 2021 17:29:47 +0300 Subject: [PATCH 08/10] Revert changes for Blob.slice() to previous approach. --- index.js | 87 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/index.js b/index.js index 42db054..8701a89 100644 --- a/index.js +++ b/index.js @@ -42,48 +42,6 @@ async function * toIterator (parts, clone = true) { } } -/** - * @param {Blob | NodeBlob | Uint8Array} blobParts - * @param {number} blobSize - * @param {number} start - * @param {number} end - */ -function* sliceBlob(blobParts, blobSize, start, end) { - let relativeStart = start < 0 ? Math.max(blobSize + start, 0) : Math.min(start, blobSize); - let relativeEnd = end < 0 ? Math.max(blobSize + end, 0) : Math.min(end, blobSize); - - const span = Math.max(relativeEnd - relativeStart, 0); - - let added = 0; - for (const part of blobParts) { - if (added >= span) { - break; - } - - const partSize = ArrayBuffer.isView(part) ? part.byteLength : part.size; - if (relativeStart && partSize <= relativeStart) { - // Skip the beginning and change the relative - // start & end position as we skip the unwanted parts - relativeStart -= partSize; - relativeEnd -= partSize; - } else { - let chunk; - if (ArrayBuffer.isView(part)) { - chunk = part.subarray(relativeStart, Math.min(partSize, relativeEnd)); - added += chunk.byteLength; - } else { - chunk = part.slice(relativeStart, Math.min(partSize, relativeEnd)); - added += chunk.size; - } - - relativeEnd -= partSize; - relativeStart = 0; // All next sequential parts should start at 0 - - yield chunk; - } - } -} - const _Blob = class Blob { /** @type {Array.<(Blob|Uint8Array)>} */ @@ -219,8 +177,49 @@ const _Blob = class Blob { * @param {number} [end] * @param {string} [type] */ - slice(start = 0, end = this.size, type = '') { - return new Blob(sliceBlob(this.#parts, this.size, start, end), {type}) + slice(start = 0, end = this.size, type = '') { + const {size} = this; + + let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); + let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); + + const span = Math.max(relativeEnd - relativeStart, 0); + const parts = this.#parts; + const blobParts = []; + let added = 0; + + for (const part of parts) { + // don't add the overflow to new blobParts + if (added >= span) { + break; + } + + const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; + if (relativeStart && size <= relativeStart) { + // Skip the beginning and change the relative + // start & end position as we skip the unwanted parts + relativeStart -= size; + relativeEnd -= size; + } else { + let chunk + if (ArrayBuffer.isView(part)) { + chunk = part.subarray(relativeStart, Math.min(size, relativeEnd)); + added += chunk.byteLength + } else { + chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); + added += chunk.size + } + relativeEnd -= size; + blobParts.push(chunk); + relativeStart = 0; // All next sequential parts should start at 0 + } + } + + const blob = new Blob([], {type: String(type).toLowerCase()}); + blob.#size = span; + blob.#parts = blobParts; + + return blob; } get [Symbol.toStringTag]() { From d7892e99d4f8d7bc913f53fb41d2273d0157e93a Mon Sep 17 00:00:00 2001 From: Nick K Date: Sat, 16 Oct 2021 17:43:03 +0300 Subject: [PATCH 09/10] Code style fix for slice method. --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 8701a89..50e5584 100644 --- a/index.js +++ b/index.js @@ -177,7 +177,7 @@ const _Blob = class Blob { * @param {number} [end] * @param {string} [type] */ - slice(start = 0, end = this.size, type = '') { + slice(start = 0, end = this.size, type = '') { const {size} = this; let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); From 5e57899886b9c57d93bacd62466bf5f34fd55641 Mon Sep 17 00:00:00 2001 From: Nick K Date: Sun, 17 Oct 2021 15:38:31 +0300 Subject: [PATCH 10/10] Use getReader in blob stream cancellation test instead of Symbol.asyncIterator --- test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index e8b6112..d7582ed 100644 --- a/test.js +++ b/test.js @@ -139,9 +139,9 @@ test('Blob stream() can be cancelled', async t => { // Cancel the stream before start reading, or this will throw an error await stream.cancel(); - const iterator = stream[Symbol.asyncIterator](); + const reader = stream.getReader(); - const {done, value: chunk} = await iterator.next(); + const {done, value: chunk} = await reader.read(); t.true(done); t.is(chunk, undefined);