Skip to content

Commit 943808b

Browse files
committed
known differences, buffer.Blob support, private stream, more test
1 parent ead8187 commit 943808b

File tree

3 files changed

+117
-55
lines changed

3 files changed

+117
-55
lines changed

README.md

+50-20
Original file line numberDiff line numberDiff line change
@@ -23,49 +23,78 @@ npm install fetch-blob
2323
- internal buffers was replaced with Uint8Arrays
2424
- CommonJS was replaced with ESM
2525
- The node stream returned by calling `blob.stream()` was replaced with a simple generator function that yields Uint8Array (Breaking change)
26+
(Read "Differences from other blobs" for more info.)
2627

27-
The reasoning behind `Blob.prototype.stream()` is that node readable stream
28-
isn't spec compatible with whatwg stream and we didn't want to import a hole whatwg stream polyfill for node
29-
or browserify hole node-stream for browsers and picking any flavor over the other. So we decided to opted out
28+
All of this changes have made it dependency free of any core node modules, so it would be possible to just import it using http-import from a CDN without any bundling
29+
30+
</details>
31+
32+
<details>
33+
<summary>Differences from other Blobs</summary>
34+
35+
- Unlike NodeJS `buffer.Blob` (Added in: v15.7.0) and browser native Blob this polyfilled version can't be sent via PostMessage
36+
- This blob version is more arbitrary, it can be constructed with blob parts that isn't a instance of itself
37+
it has to look and behave as a blob to be accepted as a blob part.
38+
- The benefit of this is that you can create other types of blobs that don't contain any internal data that has to be read in other ways, such as the `BlobDataItem` created in `from.js` that wraps a file path into a blob-like item and read lazily (nodejs plans to [implement this][fs-blobs] as well)
39+
- The `blob.stream()` is the most noticeable differences. It returns a AsyncGeneratorFunction that yields Uint8Arrays
40+
41+
The reasoning behind `Blob.prototype.stream()` is that NodeJS readable stream
42+
isn't spec compatible with whatwg streams and we didn't want to import the hole whatwg stream polyfill for node
43+
or browserify NodeJS streams for the browsers and picking any flavor over the other. So we decided to opted out
3044
of any stream and just implement the bear minium of what both streams have in common which is the asyncIterator
31-
that both yields Uint8Array. It would be redundant to convert anything to whatwg streams and than convert it back to
45+
that both yields Uint8Array. this is the most isomorphic way with the use of `for-await-of` loops.
46+
It would be redundant to convert anything to whatwg streams and than convert it back to
3247
node streams since you work inside of Node.
3348
It will probably stay like this until nodejs get native support for whatwg<sup>[1][https://github.com/nodejs/whatwg-stream]</sup> streams and whatwg stream add the node
3449
equivalent for `Readable.from(iterable)`<sup>[2](https://github.com/whatwg/streams/issues/1018)</sup>
3550

36-
But for now if you really want/need a Node Stream then you can do so using this transformation
51+
But for now if you really need a Node Stream then you can do so using this transformation
3752
```js
3853
import {Readable} from 'stream'
3954
const stream = Readable.from(blob.stream())
4055
```
41-
But if you don't need it to be a stream then you can just use the asyncIterator part of it that both whatwg stream and node stream have in common
56+
But if you don't need it to be a stream then you can just use the asyncIterator part of it that is isomorphic.
4257
```js
4358
for await (const chunk of blob.stream()) {
4459
console.log(chunk) // uInt8Array
4560
}
4661
```
47-
48-
All of this changes have made it dependency free of any core node modules, so it would be possible to just import it using http-import from a CDN without any bundling
49-
62+
If you need to make some feature detection to fix this different behavior
63+
```js
64+
if (Blob.prototype.stream?.constructor?.name === 'AsyncGeneratorFunction') {
65+
// not spec compatible, monkey patch it...
66+
// (Alternative you could extend the Blob and use super.stream())
67+
let orig = Blob.prototype.stream
68+
Blob.prototype.stream = function () {
69+
const iterator = orig.call(this)
70+
return new ReadableStream({
71+
async pull (ctrl) {
72+
const next = await iterator.next()
73+
return next.done ? ctrl.close() : ctrl.enqueue(next.value)
74+
}
75+
})
76+
}
77+
}
78+
```
79+
Possible feature whatwg version: `ReadableStream.from(iterator)`
80+
It's also possible to delete this method and instead use `.slice()` and `.arrayBuffer()` since it has both a public and private stream method
5081
</details>
5182
5283
## Usage
5384
5485
```js
5586
// Ways to import
56-
// (note that it's dependency free ESM package so regular http-import from CDN works too)
57-
import Blob from 'fetch-blob';
58-
import {Blob} from 'fetch-blob';
59-
const {Blob} = await import('fetch-blob');
87+
// (PS it's dependency free ESM package so regular http-import from CDN works too)
88+
import Blob from 'fetch-blob'
89+
import {Blob} from 'fetch-blob'
90+
const {Blob} = await import('fetch-blob')
6091

61-
const blob = new Blob(['hello, world']);
6292

6393
// Ways to read the blob:
94+
const blob = new Blob(['hello, world'])
6495

6596
await blob.text()
66-
6797
await blob.arrayBuffer()
68-
6998
for await (let chunk of blob.stream()) { ... }
7099

71100
// turn the async iterator into a node stream
@@ -85,14 +114,14 @@ npm install fetch-blob domexception
85114
```js
86115
// The default export is sync and use fs.stat to retrieve size & last modified
87116
import blobFromSync from 'fetch-blob/from.js'
88-
import {Blob, blobFrom, blobFromSync} 'fetch-blob/from.js'
117+
import {Blob, blobFrom, blobFromSync} from 'fetch-blob/from.js'
89118

90-
const fsBlob1 = blobFromSync('./2-GiB-file.bin');
91-
const fsBlob2 = await blobFrom('./2-GiB-file.bin');
119+
const fsBlob1 = blobFromSync('./2-GiB-file.bin')
120+
const fsBlob2 = await blobFrom('./2-GiB-file.bin')
92121

93122
// Not a 4 GiB memory snapshot, just holds 3 references
94123
// points to where data is located on the disk
95-
const blob = new Blob([fsBlob1, fsBlob2, 'memory']);
124+
const blob = new Blob([fsBlob1, fsBlob2, 'memory'])
96125
console.log(blob.size) // 4 GiB
97126
```
98127
@@ -106,3 +135,4 @@ See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blo
106135
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
107136
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
108137
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
138+
[fs-blobs]: https://github.com/nodejs/node/issues/37340

index.js

+39-27
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,43 @@
11
// 64 KiB (same size chrome slice theirs blob into Uint8array's)
22
const POOL_SIZE = 65536;
33

4+
/** @param {(Blob | Uint8Array)[]} parts */
5+
async function * toIterator (parts, clone = true) {
6+
for (let part of parts) {
7+
if ('stream' in part) {
8+
yield * part.stream();
9+
} else if (ArrayBuffer.isView(part)) {
10+
if (clone) {
11+
let position = part.byteOffset;
12+
let end = part.byteOffset + part.byteLength;
13+
while (position !== end) {
14+
const size = Math.min(end - position, POOL_SIZE);
15+
const chunk = part.buffer.slice(position, position + size);
16+
yield new Uint8Array(chunk);
17+
position += chunk.byteLength;
18+
}
19+
} else {
20+
yield part;
21+
}
22+
} else {
23+
// For blobs that have arrayBuffer but no stream method (nodes buffer.Blob)
24+
let position = 0;
25+
while (position !== part.size) {
26+
const chunk = part.slice(position, Math.min(part.size, position + POOL_SIZE));
27+
const buffer = await chunk.arrayBuffer();
28+
position += buffer.byteLength;
29+
yield new Uint8Array(buffer);
30+
}
31+
}
32+
}
33+
}
34+
435
export default class Blob {
536

637
/** @type {Array.<(Blob|Uint8Array)>} */
738
#parts = [];
839
#type = '';
940
#size = 0;
10-
#avoidClone = false
1141

1242
/**
1343
* The Blob() constructor returns a new Blob object. The content
@@ -66,12 +96,11 @@ export default class Blob {
6696
* @return {Promise<string>}
6797
*/
6898
async text() {
69-
this.#avoidClone = true
7099
// More optimized than using this.arrayBuffer()
71100
// that requires twice as much ram
72101
const decoder = new TextDecoder();
73102
let str = '';
74-
for await (let part of this.stream()) {
103+
for await (let part of toIterator(this.#parts, false)) {
75104
str += decoder.decode(part, { stream: true });
76105
}
77106
// Remaining
@@ -87,10 +116,9 @@ export default class Blob {
87116
* @return {Promise<ArrayBuffer>}
88117
*/
89118
async arrayBuffer() {
90-
this.#avoidClone = true
91119
const data = new Uint8Array(this.size);
92120
let offset = 0;
93-
for await (const chunk of this.stream()) {
121+
for await (const chunk of toIterator(this.#parts, false)) {
94122
data.set(chunk, offset);
95123
offset += chunk.length;
96124
}
@@ -100,30 +128,12 @@ export default class Blob {
100128

101129
/**
102130
* The Blob stream() implements partial support of the whatwg stream
103-
* by being only async iterable.
131+
* by only being async iterable.
104132
*
105133
* @returns {AsyncGenerator<Uint8Array>}
106134
*/
107135
async * stream() {
108-
for (let part of this.#parts) {
109-
if ('stream' in part) {
110-
yield * part.stream();
111-
} else {
112-
if (this.#avoidClone) {
113-
yield part
114-
} else {
115-
let position = part.byteOffset;
116-
let end = part.byteOffset + part.byteLength;
117-
while (position !== end) {
118-
const size = Math.min(end - position, POOL_SIZE);
119-
const chunk = part.buffer.slice(position, position + size);
120-
yield new Uint8Array(chunk);
121-
position += chunk.byteLength;
122-
}
123-
}
124-
}
125-
}
126-
this.#avoidClone = false
136+
yield * toIterator(this.#parts, true);
127137
}
128138

129139
/**
@@ -187,9 +197,11 @@ export default class Blob {
187197
return (
188198
object &&
189199
typeof object === 'object' &&
190-
typeof object.stream === 'function' &&
191-
object.stream.length === 0 &&
192200
typeof object.constructor === 'function' &&
201+
(
202+
typeof object.stream === 'function' ||
203+
typeof object.arrayBuffer === 'function'
204+
) &&
193205
/^(Blob|File)$/.test(object[Symbol.toStringTag])
194206
);
195207
}

test.js

+28-8
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import fs from 'fs';
22
import test from 'ava';
33
import {Response} from 'node-fetch';
44
import {Readable} from 'stream';
5+
import buffer from 'buffer';
56
import Blob from './index.js';
6-
import blobFrom from './from.js';
7+
import sync, {blobFromSync, blobFrom} from './from.js';
78

89
const license = fs.readFileSync('./LICENSE', 'utf-8');
910

@@ -26,11 +27,12 @@ test('Blob ctor parts', async t => {
2627
new Uint8Array([101]).buffer,
2728
Buffer.from('f'),
2829
new Blob(['g']),
29-
{}
30+
{},
31+
new URLSearchParams('foo')
3032
];
3133

3234
const blob = new Blob(parts);
33-
t.is(await blob.text(), 'abcdefg[object Object]');
35+
t.is(await blob.text(), 'abcdefg[object Object]foo=');
3436
});
3537

3638
test('Blob size', t => {
@@ -149,13 +151,13 @@ test('Blob works with node-fetch Response.text()', async t => {
149151
});
150152

151153
test('blob part backed up by filesystem', async t => {
152-
const blob = blobFrom('./LICENSE');
154+
const blob = blobFromSync('./LICENSE');
153155
t.is(await blob.slice(0, 3).text(), license.slice(0, 3));
154156
t.is(await blob.slice(4, 11).text(), license.slice(4, 11));
155157
});
156158

157159
test('Reading after modified should fail', async t => {
158-
const blob = blobFrom('./LICENSE');
160+
const blob = blobFromSync('./LICENSE');
159161
await new Promise(resolve => {
160162
setTimeout(resolve, 100);
161163
});
@@ -168,13 +170,19 @@ test('Reading after modified should fail', async t => {
168170
});
169171

170172
test('Reading from the stream created by blobFrom', async t => {
171-
const blob = blobFrom('./LICENSE');
173+
const blob = blobFromSync('./LICENSE');
174+
const actual = await blob.text();
175+
t.is(actual, license);
176+
});
177+
178+
test('create a blob from path asynchronous', async t => {
179+
const blob = await blobFrom('./LICENSE');
172180
const actual = await blob.text();
173181
t.is(actual, license);
174182
});
175183

176184
test('Reading empty blobs', async t => {
177-
const blob = blobFrom('./LICENSE').slice(0, 0);
185+
const blob = blobFromSync('./LICENSE').slice(0, 0);
178186
const actual = await blob.text();
179187
t.is(actual, '');
180188
});
@@ -196,7 +204,7 @@ test('Instanceof check returns false for nullish values', t => {
196204
});
197205

198206
/** @see https://github.com/w3c/FileAPI/issues/43 - important to keep boundary value */
199-
test('Dose not lowercase the blob type', t => {
207+
test('Dose not lowercase the blob values', t => {
200208
const type = 'multipart/form-data; boundary=----WebKitFormBoundaryTKqdrVt01qOBltBd';
201209
t.is(new Blob([], {type}).type, type);
202210
});
@@ -234,3 +242,15 @@ test('Can use named import - as well as default', async t => {
234242
const {Blob, default: def} = await import('./index.js');
235243
t.is(Blob, def);
236244
});
245+
246+
test('default from.js exports blobFromSync', t => {
247+
t.is(blobFromSync, sync);
248+
});
249+
250+
if (buffer.Blob) {
251+
test('Can wrap buffer.Blob to a fetch-blob', async t => {
252+
const blob1 = new buffer.Blob(['blob part']);
253+
const blob2 = new Blob([blob1]);
254+
t.is(await blob2.text(), 'blob part');
255+
});
256+
}

0 commit comments

Comments
 (0)