Skip to content

Commit 97a084b

Browse files
authored
Revamp (#83)
* See CHANGELOG for v3.0.0 * removed get-stream * enabled linter just for test.js & from.js * Any blob part is acceptable (jsdoc) since the default fallback is to cast unknown items into strings * async stat version * import from fs/promise instead * Updated the Readme and code examples * known differences, buffer.Blob support, private stream, more test * require node 14 * use fetch v3
1 parent a8fed4e commit 97a084b

File tree

7 files changed

+320
-118
lines changed

7 files changed

+320
-118
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
matrix:
1616
os: [ubuntu-latest, windows-latest, macOS-latest]
17-
node: ["15", "14", "12", engines]
17+
node: ["16", "15", "14", engines]
1818
exclude:
1919
# On Windows, run tests with only the LTS environments.
2020
- os: windows-latest

CHANGELOG.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
Changelog
22
=========
33

4-
## next
5-
- Fixed a bug where in BlobDataItem when the file was empty (#86)
4+
## v3.0.0
5+
- Changed WeakMap for private field (require node 12)
6+
- Switch to ESM
7+
- blob.stream() return a subset of whatwg stream which is the async iterable
8+
(it no longer return a node stream)
9+
- Reduced the dependency of Buffer by changing to global TextEncoder/Decoder (require node 11)
10+
- Disabled xo since it could understand private fields (#)
11+
- No longer transform the type to lowercase (https://github.com/w3c/FileAPI/issues/43)
12+
This is more loose than strict, keys should be lowercased, but values should not.
13+
It would require a more proper mime type parser - so we just made it loose.
14+
- index.js can now be imported by browser & deno since it no longer depends on any
15+
core node features (but why would you? other environment can benefit from it)
616

717
## v2.1.2
818
- Fixed a bug where `start` in BlobDataItem was undefined (#85)

README.md

+94-13
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,95 @@ A Blob implementation in Node.js, originally from [node-fetch](https://github.co
1313
npm install fetch-blob
1414
```
1515

16+
<details>
17+
<summary>Upgrading from 2x to 3x</summary>
18+
19+
Updating from 2 to 3 should be a breeze since there is not many changes to the blob specification.
20+
The major cause of a major release is coding standards.
21+
- internal WeakMaps was replaced with private fields
22+
- internal Buffer.from was replaced with TextEncoder/Decoder
23+
- internal buffers was replaced with Uint8Arrays
24+
- CommonJS was replaced with ESM
25+
- 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.)
27+
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
44+
of any stream and just implement the bear minium of what both streams have in common which is the asyncIterator
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
47+
node streams since you work inside of Node.
48+
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
49+
equivalent for `Readable.from(iterable)`<sup>[2](https://github.com/whatwg/streams/issues/1018)</sup>
50+
51+
But for now if you really need a Node Stream then you can do so using this transformation
52+
```js
53+
import {Readable} from 'stream'
54+
const stream = Readable.from(blob.stream())
55+
```
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.
57+
```js
58+
for await (const chunk of blob.stream()) {
59+
console.log(chunk) // uInt8Array
60+
}
61+
```
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
81+
</details>
82+
1683
## Usage
1784
1885
```js
19-
const Blob = require('fetch-blob');
20-
const fetch = require('node-fetch');
21-
22-
fetch('https://httpbin.org/post', {
23-
method: 'POST',
24-
body: new Blob(['Hello World'], { type: 'text/plain' })
25-
})
26-
.then(res => res.json());
27-
.then(json => console.log(json));
86+
// Ways to import
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')
91+
92+
93+
// Ways to read the blob:
94+
const blob = new Blob(['hello, world'])
95+
96+
await blob.text()
97+
await blob.arrayBuffer()
98+
for await (let chunk of blob.stream()) { ... }
99+
100+
// turn the async iterator into a node stream
101+
stream.Readable.from(blob.stream())
102+
103+
// turn the async iterator into a whatwg stream (feature)
104+
globalThis.ReadableStream.from(blob.stream())
28105
```
29106
30107
### Blob part backed up by filesystem
@@ -35,13 +112,16 @@ npm install fetch-blob domexception
35112
```
36113
37114
```js
38-
const blobFrom = require('fetch-blob/from.js');
39-
const blob1 = blobFrom('./2-GiB-file.bin');
40-
const blob2 = blobFrom('./2-GiB-file.bin');
115+
// The default export is sync and use fs.stat to retrieve size & last modified
116+
import blobFromSync from 'fetch-blob/from.js'
117+
import {Blob, blobFrom, blobFromSync} from 'fetch-blob/from.js'
118+
119+
const fsBlob1 = blobFromSync('./2-GiB-file.bin')
120+
const fsBlob2 = await blobFrom('./2-GiB-file.bin')
41121

42122
// Not a 4 GiB memory snapshot, just holds 3 references
43123
// points to where data is located on the disk
44-
const blob = new Blob([blob1, blob2]);
124+
const blob = new Blob([fsBlob1, fsBlob2, 'memory'])
45125
console.log(blob.size) // 4 GiB
46126
```
47127
@@ -55,3 +135,4 @@ See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blo
55135
[codecov-url]: https://codecov.io/gh/node-fetch/fetch-blob
56136
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
57137
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
138+
[fs-blobs]: https://github.com/nodejs/node/issues/37340

from.js

+43-29
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,75 @@
1-
const {statSync, createReadStream} = require('fs');
2-
const Blob = require('./index.js');
3-
const DOMException = require('domexception');
1+
import {statSync, createReadStream} from 'fs';
2+
import {stat} from 'fs/promises';
3+
import DOMException from 'domexception';
4+
import Blob from './index.js';
45

56
/**
67
* @param {string} path filepath on the disk
78
* @returns {Blob}
89
*/
9-
function blobFrom(path) {
10-
const {size, mtime} = statSync(path);
11-
const blob = new BlobDataItem({path, size, mtime});
10+
const blobFromSync = path => from(statSync(path), path);
1211

13-
return new Blob([blob]);
14-
}
12+
/**
13+
* @param {string} path filepath on the disk
14+
* @returns {Promise<Blob>}
15+
*/
16+
const blobFrom = path => stat(path).then(stat => from(stat, path));
17+
18+
const from = (stat, path) => new Blob([new BlobDataItem({
19+
path,
20+
size: stat.size,
21+
lastModified: stat.mtimeMs,
22+
start: 0
23+
})]);
1524

1625
/**
1726
* This is a blob backed up by a file on the disk
18-
* with minium requirement
27+
* with minium requirement. Its wrapped around a Blob as a blobPart
28+
* so you have no direct access to this.
1929
*
2030
* @private
2131
*/
2232
class BlobDataItem {
33+
#path;
34+
#start;
35+
2336
constructor(options) {
37+
this.#path = options.path;
38+
this.#start = options.start;
2439
this.size = options.size;
25-
this.path = options.path;
26-
this.start = options.start || 0;
27-
this.mtime = options.mtime;
40+
this.lastModified = options.lastModified
2841
}
2942

30-
// Slicing arguments is first validated and formated
31-
// to not be out of range by Blob.prototype.slice
43+
/**
44+
* Slicing arguments is first validated and formatted
45+
* to not be out of range by Blob.prototype.slice
46+
*/
3247
slice(start, end) {
3348
return new BlobDataItem({
34-
path: this.path,
35-
start,
36-
mtime: this.mtime,
37-
size: end - start
49+
path: this.#path,
50+
lastModified: this.lastModified,
51+
size: end - start,
52+
start
3853
});
3954
}
4055

41-
stream() {
42-
if (statSync(this.path).mtime > this.mtime) {
56+
async * stream() {
57+
const {mtimeMs} = await stat(this.#path)
58+
if (mtimeMs > this.lastModified) {
4359
throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError');
4460
}
45-
46-
if (!this.size) {
47-
return new Blob().stream();
61+
if (this.size) {
62+
yield * createReadStream(this.#path, {
63+
start: this.#start,
64+
end: this.#start + this.size - 1
65+
});
4866
}
49-
50-
return createReadStream(this.path, {
51-
start: this.start,
52-
end: this.start + this.size - 1
53-
});
5467
}
5568

5669
get [Symbol.toStringTag]() {
5770
return 'Blob';
5871
}
5972
}
6073

61-
module.exports = blobFrom;
74+
export default blobFromSync;
75+
export {Blob, blobFrom, blobFromSync};

0 commit comments

Comments
 (0)