Skip to content

Commit fd4f45e

Browse files
authored
Operate on blob parts (byte sequence) (#44)
* operate on blob parts * jsdoc * specify parts type * Back to 100% (discovered a small bug)
1 parent 9dc0747 commit fd4f45e

File tree

3 files changed

+125
-56
lines changed

3 files changed

+125
-56
lines changed

index.js

Lines changed: 119 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,170 @@
1-
// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js
2-
// (MIT licensed)
3-
4-
const {Readable: ReadableStream} = require('stream');
1+
const {Readable} = require('stream');
52

3+
/**
4+
* @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>}
5+
*/
66
const wm = new WeakMap();
77

8+
async function * read(parts) {
9+
for (const part of parts) {
10+
if ('stream' in part) {
11+
yield * part.stream();
12+
} else {
13+
yield part;
14+
}
15+
}
16+
}
17+
18+
/**
19+
* @template T
20+
* @param {T} object
21+
* @returns {T is Blob}
22+
*/
23+
const isBlob = object => {
24+
return (
25+
typeof object === 'object' &&
26+
typeof object.stream === 'function' &&
27+
typeof object.constructor === 'function' &&
28+
/^(Blob|File)$/.test(object[Symbol.toStringTag])
29+
);
30+
};
31+
832
class Blob {
33+
/**
34+
* The Blob() constructor returns a new Blob object. The content
35+
* of the blob consists of the concatenation of the values given
36+
* in the parameter array.
37+
*
38+
* @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts
39+
* @param {{ type?: string }} [options]
40+
*/
941
constructor(blobParts = [], options = {type: ''}) {
10-
const buffers = [];
1142
let size = 0;
1243

13-
blobParts.forEach(element => {
44+
const parts = blobParts.map(element => {
1445
let buffer;
15-
if (element instanceof Buffer) {
46+
if (Buffer.isBuffer(element)) {
1647
buffer = element;
1748
} else if (ArrayBuffer.isView(element)) {
1849
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
1950
} else if (element instanceof ArrayBuffer) {
2051
buffer = Buffer.from(element);
21-
} else if (element instanceof Blob) {
22-
buffer = wm.get(element).buffer;
52+
} else if (isBlob(element)) {
53+
buffer = element;
2354
} else {
2455
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
2556
}
2657

27-
size += buffer.length;
28-
buffers.push(buffer);
58+
size += buffer.length || buffer.size || 0;
59+
return buffer;
2960
});
3061

31-
const buffer = Buffer.concat(buffers, size);
32-
3362
const type = options.type === undefined ? '' : String(options.type).toLowerCase();
3463

3564
wm.set(this, {
3665
type: /[^\u0020-\u007E]/.test(type) ? '' : type,
3766
size,
38-
buffer
67+
parts
3968
});
4069
}
4170

71+
/**
72+
* The Blob interface's size property returns the
73+
* size of the Blob in bytes.
74+
*/
4275
get size() {
4376
return wm.get(this).size;
4477
}
4578

79+
/**
80+
* The type property of a Blob object returns the MIME type of the file.
81+
*/
4682
get type() {
4783
return wm.get(this).type;
4884
}
4985

50-
text() {
51-
return Promise.resolve(wm.get(this).buffer.toString());
86+
/**
87+
* The text() method in the Blob interface returns a Promise
88+
* that resolves with a string containing the contents of
89+
* the blob, interpreted as UTF-8.
90+
*
91+
* @return {Promise<string>}
92+
*/
93+
async text() {
94+
return Buffer.from(await this.arrayBuffer()).toString();
5295
}
5396

54-
arrayBuffer() {
55-
const buf = wm.get(this).buffer;
56-
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
57-
return Promise.resolve(ab);
58-
}
97+
/**
98+
* The arrayBuffer() method in the Blob interface returns a
99+
* Promise that resolves with the contents of the blob as
100+
* binary data contained in an ArrayBuffer.
101+
*
102+
* @return {Promise<ArrayBuffer>}
103+
*/
104+
async arrayBuffer() {
105+
const data = new Uint8Array(this.size);
106+
let offset = 0;
107+
for await (const chunk of this.stream()) {
108+
data.set(chunk, offset);
109+
offset += chunk.length;
110+
}
59111

60-
stream() {
61-
const readable = new ReadableStream();
62-
readable._read = () => { };
63-
readable.push(wm.get(this).buffer);
64-
readable.push(null);
65-
return readable;
112+
return data.buffer;
66113
}
67114

68-
toString() {
69-
return '[object Blob]';
115+
/**
116+
* The Blob interface's stream() method is difference from native
117+
* and uses node streams instead of whatwg streams.
118+
*
119+
* @returns {Readable} Node readable stream
120+
*/
121+
stream() {
122+
return Readable.from(read(wm.get(this).parts));
70123
}
71124

72-
slice(...args) {
125+
/**
126+
* The Blob interface's slice() method creates and returns a
127+
* new Blob object which contains data from a subset of the
128+
* blob on which it's called.
129+
*
130+
* @param {number} [start]
131+
* @param {number} [end]
132+
* @param {string} [contentType]
133+
*/
134+
slice(start = 0, end = this.size, type = '') {
73135
const {size} = this;
74136

75-
const start = args[0];
76-
const end = args[1];
77-
let relativeStart;
78-
let relativeEnd;
137+
let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
138+
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);
79139

80-
if (start === undefined) {
81-
relativeStart = 0; //
82-
} else if (start < 0) {
83-
relativeStart = Math.max(size + start, 0); //
84-
} else {
85-
relativeStart = Math.min(start, size);
140+
const span = Math.max(relativeEnd - relativeStart, 0);
141+
const parts = wm.get(this).parts.values();
142+
const blobParts = [];
143+
let added = 0;
144+
145+
for (const part of parts) {
146+
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size;
147+
if (relativeStart && size <= relativeStart) {
148+
// Skip the beginning and change the relative
149+
// start & end position as we skip the unwanted parts
150+
relativeStart -= size;
151+
relativeEnd -= size;
152+
} else {
153+
const chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
154+
blobParts.push(chunk);
155+
added += size;
156+
relativeStart = 0; // All next sequental parts should start at 0
157+
158+
// don't add the overflow to new blobParts
159+
if (added >= span) {
160+
break;
161+
}
162+
}
86163
}
87164

88-
if (end === undefined) {
89-
relativeEnd = size; //
90-
} else if (end < 0) {
91-
relativeEnd = Math.max(size + end, 0); //
92-
} else {
93-
relativeEnd = Math.min(end, size);
94-
}
165+
const blob = new Blob([], {type});
166+
Object.assign(wm.get(blob), {size: span, parts: blobParts});
95167

96-
const span = Math.max(relativeEnd - relativeStart, 0);
97-
const slicedBuffer = wm.get(this).buffer.slice(
98-
relativeStart,
99-
relativeStart + span
100-
);
101-
const blob = new Blob([], {type: args[2]});
102-
const _ = wm.get(blob);
103-
_.buffer = slicedBuffer;
104168
return blob;
105169
}
106170
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"node-fetch"
2020
],
2121
"engines": {
22-
"node": ">=6"
22+
"node": "^10.17.0"
2323
},
2424
"author": "David Frank",
2525
"license": "MIT",

test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ test('Blob slice(0, -1)', async t => {
113113
t.is(await blob.text(), 'abcdefg');
114114
});
115115

116+
test('throw away unwanted parts', async t => {
117+
const blob = new Blob(['a', 'b', 'c']).slice(1, 2);
118+
t.is(await blob.text(), 'b');
119+
});
120+
116121
test('Blob works with node-fetch Response.blob()', async t => {
117122
const data = 'a=1';
118123
const type = 'text/plain';

0 commit comments

Comments
 (0)