Skip to content

Commit 091a579

Browse files
jasnelldanielleadams
authored andcommitted
buffer: add Blob.prototype.stream method and other cleanups
Adds the `stream()` method to get a `ReadableStream` for the `Blob`. Also makes some other improvements to get the implementation closer to the API standard definition. Signed-off-by: James M Snell <[email protected]> PR-URL: #39693 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Stephen Belanger <[email protected]>
1 parent 0dc167a commit 091a579

File tree

3 files changed

+145
-31
lines changed

3 files changed

+145
-31
lines changed

doc/api/buffer.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,15 +507,24 @@ added: v15.7.0
507507
Creates and returns a new `Blob` containing a subset of this `Blob` objects
508508
data. The original `Blob` is not altered.
509509

510+
### `blob.stream()`
511+
<!-- YAML
512+
added: REPLACEME
513+
-->
514+
515+
* Returns: {ReadableStream}
516+
517+
Returns a new `ReadableStream` that allows the content of the `Blob` to be read.
518+
510519
### `blob.text()`
511520
<!-- YAML
512521
added: v15.7.0
513522
-->
514523

515524
* Returns: {Promise}
516525

517-
Returns a promise that resolves the contents of the `Blob` decoded as a UTF-8
518-
string.
526+
Returns a promise that fulfills with the contents of the `Blob` decoded as a
527+
UTF-8 string.
519528

520529
### `blob.type`
521530
<!-- YAML

lib/internal/blob.js

Lines changed: 116 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ const {
55
MathMax,
66
MathMin,
77
ObjectDefineProperty,
8-
ObjectSetPrototypeOf,
98
PromiseResolve,
9+
PromiseReject,
10+
PromisePrototypeFinally,
11+
ReflectConstruct,
1012
RegExpPrototypeTest,
1113
StringPrototypeToLowerCase,
1214
Symbol,
@@ -16,14 +18,14 @@ const {
1618
} = primordials;
1719

1820
const {
19-
createBlob,
21+
createBlob: _createBlob,
2022
FixedSizeBlobCopyJob,
2123
} = internalBinding('buffer');
2224

2325
const { TextDecoder } = require('internal/encoding');
2426

2527
const {
26-
JSTransferable,
28+
makeTransferable,
2729
kClone,
2830
kDeserialize,
2931
} = require('internal/worker/js_transferable');
@@ -44,6 +46,7 @@ const {
4446
AbortError,
4547
codes: {
4648
ERR_INVALID_ARG_TYPE,
49+
ERR_INVALID_THIS,
4750
ERR_BUFFER_TOO_LARGE,
4851
}
4952
} = require('internal/errors');
@@ -56,17 +59,27 @@ const {
5659
const kHandle = Symbol('kHandle');
5760
const kType = Symbol('kType');
5861
const kLength = Symbol('kLength');
62+
const kArrayBufferPromise = Symbol('kArrayBufferPromise');
5963

6064
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
6165

6266
let Buffer;
67+
let ReadableStream;
6368

6469
function lazyBuffer() {
6570
if (Buffer === undefined)
6671
Buffer = require('buffer').Buffer;
6772
return Buffer;
6873
}
6974

75+
function lazyReadableStream(options) {
76+
if (ReadableStream === undefined) {
77+
ReadableStream =
78+
require('internal/webstreams/readablestream').ReadableStream;
79+
}
80+
return new ReadableStream(options);
81+
}
82+
7083
function isBlob(object) {
7184
return object?.[kHandle] !== undefined;
7285
}
@@ -89,16 +102,7 @@ function getSource(source, encoding) {
89102
return [source.byteLength, source];
90103
}
91104

92-
class InternalBlob extends JSTransferable {
93-
constructor(handle, length, type = '') {
94-
super();
95-
this[kHandle] = handle;
96-
this[kType] = type;
97-
this[kLength] = length;
98-
}
99-
}
100-
101-
class Blob extends JSTransferable {
105+
class Blob {
102106
constructor(sources = [], options = {}) {
103107
emitExperimentalWarning('buffer.Blob');
104108
if (sources === null ||
@@ -120,13 +124,15 @@ class Blob extends JSTransferable {
120124
if (!isUint32(length))
121125
throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF);
122126

123-
super();
124-
this[kHandle] = createBlob(sources_, length);
127+
this[kHandle] = _createBlob(sources_, length);
125128
this[kLength] = length;
126129

127130
type = `${type}`;
128131
this[kType] = RegExpPrototypeTest(disallowedTypeCharacters, type) ?
129132
'' : StringPrototypeToLowerCase(type);
133+
134+
// eslint-disable-next-line no-constructor-return
135+
return makeTransferable(this);
130136
}
131137

132138
[kInspect](depth, options) {
@@ -150,7 +156,7 @@ class Blob extends JSTransferable {
150156
const length = this[kLength];
151157
return {
152158
data: { handle, type, length },
153-
deserializeInfo: 'internal/blob:InternalBlob'
159+
deserializeInfo: 'internal/blob:ClonedBlob'
154160
};
155161
}
156162

@@ -160,11 +166,35 @@ class Blob extends JSTransferable {
160166
this[kLength] = length;
161167
}
162168

163-
get type() { return this[kType]; }
169+
/**
170+
* @readonly
171+
* @type {string}
172+
*/
173+
get type() {
174+
if (!isBlob(this))
175+
throw new ERR_INVALID_THIS('Blob');
176+
return this[kType];
177+
}
164178

165-
get size() { return this[kLength]; }
179+
/**
180+
* @readonly
181+
* @type {number}
182+
*/
183+
get size() {
184+
if (!isBlob(this))
185+
throw new ERR_INVALID_THIS('Blob');
186+
return this[kLength];
187+
}
166188

189+
/**
190+
* @param {number} [start]
191+
* @param {number} [end]
192+
* @param {string} [contentType]
193+
* @returns {Blob}
194+
*/
167195
slice(start = 0, end = this[kLength], contentType = '') {
196+
if (!isBlob(this))
197+
throw new ERR_INVALID_THIS('Blob');
168198
if (start < 0) {
169199
start = MathMax(this[kLength] + start, 0);
170200
} else {
@@ -188,49 +218,106 @@ class Blob extends JSTransferable {
188218

189219
const span = MathMax(end - start, 0);
190220

191-
return new InternalBlob(
192-
this[kHandle].slice(start, start + span), span, contentType);
221+
return createBlob(
222+
this[kHandle].slice(start, start + span),
223+
span,
224+
contentType);
193225
}
194226

195-
async arrayBuffer() {
227+
/**
228+
* @returns {Promise<ArrayBuffer>}
229+
*/
230+
arrayBuffer() {
231+
if (!isBlob(this))
232+
return PromiseReject(new ERR_INVALID_THIS('Blob'));
233+
234+
// If there's already a promise in flight for the content,
235+
// reuse it, but only once. After the cached promise resolves
236+
// it will be cleared, allowing it to be garbage collected
237+
// as soon as possible.
238+
if (this[kArrayBufferPromise])
239+
return this[kArrayBufferPromise];
240+
196241
const job = new FixedSizeBlobCopyJob(this[kHandle]);
197242

198243
const ret = job.run();
244+
245+
// If the job returns a value immediately, the ArrayBuffer
246+
// was generated synchronously and should just be returned
247+
// directly.
199248
if (ret !== undefined)
200249
return PromiseResolve(ret);
201250

202251
const {
203252
promise,
204253
resolve,
205-
reject
254+
reject,
206255
} = createDeferredPromise();
256+
207257
job.ondone = (err, ab) => {
208258
if (err !== undefined)
209259
return reject(new AbortError());
210260
resolve(ab);
211261
};
262+
this[kArrayBufferPromise] =
263+
PromisePrototypeFinally(
264+
promise,
265+
() => this[kArrayBufferPromise] = undefined);
212266

213-
return promise;
267+
return this[kArrayBufferPromise];
214268
}
215269

270+
/**
271+
*
272+
* @returns {Promise<string>}
273+
*/
216274
async text() {
275+
if (!isBlob(this))
276+
throw new ERR_INVALID_THIS('Blob');
277+
217278
const dec = new TextDecoder();
218279
return dec.decode(await this.arrayBuffer());
219280
}
281+
282+
/**
283+
* @returns {ReadableStream}
284+
*/
285+
stream() {
286+
if (!isBlob(this))
287+
throw new ERR_INVALID_THIS('Blob');
288+
289+
const self = this;
290+
return new lazyReadableStream({
291+
async start(controller) {
292+
const ab = await self.arrayBuffer();
293+
controller.enqueue(new Uint8Array(ab));
294+
controller.close();
295+
}
296+
});
297+
}
298+
}
299+
300+
function ClonedBlob() {
301+
return makeTransferable(ReflectConstruct(function() {}, [], Blob));
302+
}
303+
ClonedBlob.prototype[kDeserialize] = () => {};
304+
305+
function createBlob(handle, length, type = '') {
306+
return makeTransferable(ReflectConstruct(function() {
307+
this[kHandle] = handle;
308+
this[kType] = type;
309+
this[kLength] = length;
310+
}, [], Blob));
220311
}
221312

222313
ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
223314
configurable: true,
224315
value: 'Blob',
225316
});
226317

227-
InternalBlob.prototype.constructor = Blob;
228-
ObjectSetPrototypeOf(
229-
InternalBlob.prototype,
230-
Blob.prototype);
231-
232318
module.exports = {
233319
Blob,
234-
InternalBlob,
320+
ClonedBlob,
321+
createBlob,
235322
isBlob,
236323
};

test/parallel/test-blob.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,21 @@ assert.throws(() => new Blob({}), {
198198
'Blob { size: 0, type: \'\' }');
199199
assert.strictEqual(inspect(b, { depth: -1 }), '[Blob]');
200200
}
201+
202+
{
203+
// The Blob has to be over a specific size for the data to
204+
// be copied asynchronously..
205+
const b = new Blob(['hello', 'there'.repeat(820)]);
206+
assert.strictEqual(b.arrayBuffer(), b.arrayBuffer());
207+
b.arrayBuffer().then(common.mustCall());
208+
}
209+
210+
(async () => {
211+
const b = new Blob(['hello']);
212+
const reader = b.stream().getReader();
213+
let res = await reader.read();
214+
assert.strictEqual(res.value.byteLength, 5);
215+
assert(!res.done);
216+
res = await reader.read();
217+
assert(res.done);
218+
})().then(common.mustCall());

0 commit comments

Comments
 (0)