From ee9ae97aa03aeb0dd347b559b46401d3253d31b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 7 Jun 2020 17:33:12 +0200 Subject: [PATCH 1/4] operate on blob parts --- index.js | 163 +++++++++++++++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 114 insertions(+), 51 deletions(-) diff --git a/index.js b/index.js index 6dd75a0..db886f5 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,39 @@ -// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js -// (MIT licensed) - -const {Readable: ReadableStream} = require('stream'); +const {Readable} = require('stream'); const wm = new WeakMap(); +async function * read(parts) { + for (const part of parts) { + if ('stream' in part) { + yield * part.stream(); + } else { + yield part; + } + } +} + +const isBlob = object => { + return ( + typeof object === 'object' && + typeof object.stream === 'function' && + typeof object.constructor === 'function' && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ); +}; + class Blob { + /** + * The Blob() constructor returns a new Blob object. The content + * of the blob consists of the concatenation of the values given + * in the parameter array. + * + * @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts + * @param {{ type?: string }} [options] + */ constructor(blobParts = [], options = {type: ''}) { - const buffers = []; let size = 0; - blobParts.forEach(element => { + const parts = blobParts.map(element => { let buffer; if (element instanceof Buffer) { buffer = element; @@ -18,89 +41,129 @@ class Blob { buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); } else if (element instanceof ArrayBuffer) { buffer = Buffer.from(element); - } else if (element instanceof Blob) { - buffer = wm.get(element).buffer; + } else if (isBlob(element)) { + buffer = element; } else { buffer = Buffer.from(typeof element === 'string' ? element : String(element)); } - size += buffer.length; - buffers.push(buffer); + size += buffer.length || buffer.size || 0; + return buffer; }); - const buffer = Buffer.concat(buffers, size); - const type = options.type === undefined ? '' : String(options.type).toLowerCase(); wm.set(this, { type: /[^\u0020-\u007E]/.test(type) ? '' : type, size, - buffer + parts }); } + /** + * The Blob interface's size property returns the + * size of the Blob in bytes. + */ get size() { return wm.get(this).size; } + /** + * The type property of a Blob object returns the MIME type of the file. + */ get type() { return wm.get(this).type; } - text() { - return Promise.resolve(wm.get(this).buffer.toString()); + /** + * The text() method in the Blob interface returns a Promise + * that resolves with a string containing the contents of + * the blob, interpreted as UTF-8. + * + * @return {Promise} + */ + async text() { + return Buffer.from(await this.arrayBuffer()).toString(); } - arrayBuffer() { - const buf = wm.get(this).buffer; - const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); - return Promise.resolve(ab); + /** + * The arrayBuffer() method in the Blob interface returns a + * Promise that resolves with the contents of the blob as + * binary data contained in an ArrayBuffer. + * + * @return {Promise} + */ + async arrayBuffer() { + const data = new Uint8Array(this.size); + let offset = 0; + for await (const chunk of this.stream()) { + data.set(chunk, offset); + offset += chunk.length; + } + + return data.buffer; } + /** + * The Blob interface's stream() method is difference from native + * and uses node streams instead of whatwg streams. + * + * @returns {Readable} Node readable stream + */ stream() { - const readable = new ReadableStream(); - readable._read = () => { }; - readable.push(wm.get(this).buffer); - readable.push(null); - return readable; + return Readable.from(read(wm.get(this).parts)); } + /** + * @returns {string} + */ toString() { return '[object Blob]'; } - slice(...args) { + /** + * The Blob interface's slice() method creates and returns a + * new Blob object which contains data from a subset of the + * blob on which it's called. + * + * @param {number} [start] + * @param {number} [end] + * @param {string} [contentType] + */ + slice(start = 0, end = this.size, type = '') { const {size} = this; - const start = args[0]; - const end = args[1]; - let relativeStart; - let relativeEnd; + 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); - if (start === undefined) { - relativeStart = 0; // - } else if (start < 0) { - relativeStart = Math.max(size + start, 0); // - } else { - relativeStart = Math.min(start, size); + const span = Math.max(relativeEnd - relativeStart, 0); + const parts = wm.get(this).parts.values(); + const blobParts = []; + let added = 0; + + for (const part of parts) { + 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 { + const chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); + blobParts.push(chunk); + added += size; + relativeStart = 0; // All next sequental parts should start at 0 + + // don't add the overflow to new blobParts + if (added >= span) { + break; + } + } } - if (end === undefined) { - relativeEnd = size; // - } else if (end < 0) { - relativeEnd = Math.max(size + end, 0); // - } else { - relativeEnd = Math.min(end, size); - } + const blob = new Blob([], {type}); + Object.assign(wm.get(blob), {size: span, parts: blobParts}); - const span = Math.max(relativeEnd - relativeStart, 0); - const slicedBuffer = wm.get(this).buffer.slice( - relativeStart, - relativeStart + span - ); - const blob = new Blob([], {type: args[2]}); - const _ = wm.get(blob); - _.buffer = slicedBuffer; return blob; } } diff --git a/package.json b/package.json index cb0f11d..3564f87 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "node-fetch" ], "engines": { - "node": ">=6" + "node": "^10.17.0" }, "author": "David Frank", "license": "MIT", From 297a1a2f1776e5c004eee54be67aea728fd7f207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 7 Jun 2020 20:25:54 +0200 Subject: [PATCH 2/4] jsdoc --- index.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index db886f5..9667998 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,8 @@ const {Readable} = require('stream'); +/** + * @type {WeakMap { return ( typeof object === 'object' && @@ -35,7 +43,7 @@ class Blob { const parts = blobParts.map(element => { let buffer; - if (element instanceof Buffer) { + if (Buffer.isBuffer(element)) { buffer = element; } else if (ArrayBuffer.isView(element)) { buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); @@ -114,13 +122,6 @@ class Blob { return Readable.from(read(wm.get(this).parts)); } - /** - * @returns {string} - */ - toString() { - return '[object Blob]'; - } - /** * The Blob interface's slice() method creates and returns a * new Blob object which contains data from a subset of the From 1b6f7cdd7383d8c03c0c9da597b8644d2fe73d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 7 Jun 2020 20:28:19 +0200 Subject: [PATCH 3/4] specify parts type --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 9667998..8d99b37 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ const {Readable} = require('stream'); /** - * @type {WeakMap} */ const wm = new WeakMap(); From aa801ae2f9d0f1af6e47e1613e976353bac41b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 7 Jun 2020 20:57:12 +0200 Subject: [PATCH 4/4] Back to 100% (discovered a small bugg) --- index.js | 2 +- test.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 8d99b37..5ad1e05 100644 --- a/index.js +++ b/index.js @@ -144,7 +144,7 @@ class Blob { for (const part of parts) { const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; - if (relativeStart && size < relativeStart) { + if (relativeStart && size <= relativeStart) { // Skip the beginning and change the relative // start & end position as we skip the unwanted parts relativeStart -= size; diff --git a/test.js b/test.js index 115835f..8f56b48 100644 --- a/test.js +++ b/test.js @@ -113,6 +113,11 @@ test('Blob slice(0, -1)', async t => { t.is(await blob.text(), 'abcdefg'); }); +test('throw away unwanted parts', async t => { + const blob = new Blob(['a', 'b', 'c']).slice(1, 2); + t.is(await blob.text(), 'b'); +}); + test('Blob works with node-fetch Response.blob()', async t => { const data = 'a=1'; const type = 'text/plain';