Skip to content

Commit 775b34b

Browse files
jasnellruyadorno
authored andcommitted
buffer: introduce Blob
The `Blob` object is an immutable data buffer. This is a first step towards alignment with the `Blob` Web API. Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #36811 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent fada6b0 commit 775b34b

13 files changed

+1031
-0
lines changed

doc/api/buffer.md

+114
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,119 @@ for (const b of buf) {
287287
Additionally, the [`buf.values()`][], [`buf.keys()`][], and
288288
[`buf.entries()`][] methods can be used to create iterators.
289289

290+
## Class: `Blob`
291+
<!-- YAML
292+
added: REPLACEME
293+
-->
294+
295+
> Stability: 1 - Experimental
296+
297+
A [`Blob`][] encapsulates immutable, raw data that can be safely shared across
298+
multiple worker threads.
299+
300+
### `new buffer.Blob([sources[, options]])`
301+
<!-- YAML
302+
added: REPLACEME
303+
-->
304+
305+
* `sources` {string[]|ArrayBuffer[]|TypedArray[]|DataView[]|Blob[]} An array
306+
of string, {ArrayBuffer}, {TypedArray}, {DataView}, or {Blob} objects, or
307+
any mix of such objects, that will be stored within the `Blob`.
308+
* `options` {Object}
309+
* `encoding` {string} The character encoding to use for string sources.
310+
**Default**: `'utf8'`.
311+
* `type` {string} The Blob content-type. The intent is for `type` to convey
312+
the MIME media type of the data, however no validation of the type format
313+
is performed.
314+
315+
Creates a new `Blob` object containing a concatenation of the given sources.
316+
317+
{ArrayBuffer}, {TypedArray}, {DataView}, and {Buffer} sources are copied into
318+
the 'Blob' and can therefore be safely modified after the 'Blob' is created.
319+
320+
String sources are also copied into the `Blob`.
321+
322+
### `blob.arrayBuffer()`
323+
<!-- YAML
324+
added: REPLACEME
325+
-->
326+
327+
* Returns: {Promise}
328+
329+
Returns a promise that fulfills with an {ArrayBuffer} containing a copy of
330+
the `Blob` data.
331+
332+
### `blob.size`
333+
<!-- YAML
334+
added: REPLACEME
335+
-->
336+
337+
The total size of the `Blob` in bytes.
338+
339+
### `blob.slice([start, [end, [type]]])`
340+
<!-- YAML
341+
added: REPLACEME
342+
-->
343+
344+
* `start` {number} The starting index.
345+
* `end` {number} The ending index.
346+
* `type` {string} The content-type for the new `Blob`
347+
348+
Creates and returns a new `Blob` containing a subset of this `Blob` objects
349+
data. The original `Blob` is not alterered.
350+
351+
### `blob.text()`
352+
<!-- YAML
353+
added: REPLACEME
354+
-->
355+
356+
* Returns: {Promise}
357+
358+
Returns a promise that resolves the contents of the `Blob` decoded as a UTF-8
359+
string.
360+
361+
### `blob.type`
362+
<!-- YAML
363+
added: REPLACEME
364+
-->
365+
366+
* Type: {string}
367+
368+
The content-type of the `Blob`.
369+
370+
### `Blob` objects and `MessageChannel`
371+
372+
Once a {Blob} object is created, it can be sent via `MessagePort` to multiple
373+
destinations without transfering or immediately copying the data. The data
374+
contained by the `Blob` is copied only when the `arrayBuffer()` or `text()`
375+
methods are called.
376+
377+
```js
378+
const { Blob } = require('buffer');
379+
const blob = new Blob(['hello there']);
380+
const { setTimeout: delay } = require('timers/promises');
381+
382+
const mc1 = new MessageChannel();
383+
const mc2 = new MessageChannel();
384+
385+
mc1.port1.onmessage = async ({ data }) => {
386+
console.log(await data.arrayBuffer());
387+
mc1.port1.close();
388+
};
389+
390+
mc2.port1.onmessage = async ({ data }) => {
391+
await delay(1000);
392+
console.log(await data.arrayBuffer());
393+
mc2.port1.close();
394+
};
395+
396+
mc1.port2.postMessage(blob);
397+
mc2.port2.postMessage(blob);
398+
399+
// The Blob is still usable after posting.
400+
data.text().then(console.log);
401+
```
402+
290403
## Class: `Buffer`
291404

292405
The `Buffer` class is a global type for dealing with binary data directly.
@@ -3389,6 +3502,7 @@ introducing security vulnerabilities into an application.
33893502
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
33903503
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
33913504
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
3505+
[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
33923506
[`Buffer.alloc()`]: #buffer_static_method_buffer_alloc_size_fill_encoding
33933507
[`Buffer.allocUnsafe()`]: #buffer_static_method_buffer_allocunsafe_size
33943508
[`Buffer.allocUnsafeSlow()`]: #buffer_static_method_buffer_allocunsafeslow_size

lib/buffer.js

+5
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ const {
115115
createUnsafeBuffer
116116
} = require('internal/buffer');
117117

118+
const {
119+
Blob,
120+
} = require('internal/blob');
121+
118122
FastBuffer.prototype.constructor = Buffer;
119123
Buffer.prototype = FastBuffer.prototype;
120124
addBufferPrototypeMethods(Buffer.prototype);
@@ -1210,6 +1214,7 @@ if (internalBinding('config').hasIntl) {
12101214
}
12111215

12121216
module.exports = {
1217+
Blob,
12131218
Buffer,
12141219
SlowBuffer,
12151220
transcode,

lib/internal/blob.js

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
'use strict';
2+
3+
const {
4+
ArrayFrom,
5+
ObjectSetPrototypeOf,
6+
Promise,
7+
PromiseResolve,
8+
RegExpPrototypeTest,
9+
StringPrototypeToLowerCase,
10+
Symbol,
11+
SymbolIterator,
12+
Uint8Array,
13+
} = primordials;
14+
15+
const {
16+
createBlob,
17+
FixedSizeBlobCopyJob,
18+
} = internalBinding('buffer');
19+
20+
const {
21+
JSTransferable,
22+
kClone,
23+
kDeserialize,
24+
} = require('internal/worker/js_transferable');
25+
26+
const {
27+
isAnyArrayBuffer,
28+
isArrayBufferView,
29+
} = require('internal/util/types');
30+
31+
const {
32+
customInspectSymbol: kInspect,
33+
emitExperimentalWarning,
34+
} = require('internal/util');
35+
const { inspect } = require('internal/util/inspect');
36+
37+
const {
38+
AbortError,
39+
codes: {
40+
ERR_INVALID_ARG_TYPE,
41+
ERR_BUFFER_TOO_LARGE,
42+
ERR_OUT_OF_RANGE,
43+
}
44+
} = require('internal/errors');
45+
46+
const {
47+
validateObject,
48+
validateString,
49+
validateUint32,
50+
isUint32,
51+
} = require('internal/validators');
52+
53+
const kHandle = Symbol('kHandle');
54+
const kType = Symbol('kType');
55+
const kLength = Symbol('kLength');
56+
57+
let Buffer;
58+
59+
function deferred() {
60+
let res, rej;
61+
const promise = new Promise((resolve, reject) => {
62+
res = resolve;
63+
rej = reject;
64+
});
65+
return { promise, resolve: res, reject: rej };
66+
}
67+
68+
function lazyBuffer() {
69+
if (Buffer === undefined)
70+
Buffer = require('buffer').Buffer;
71+
return Buffer;
72+
}
73+
74+
function isBlob(object) {
75+
return object?.[kHandle] !== undefined;
76+
}
77+
78+
function getSource(source, encoding) {
79+
if (isBlob(source))
80+
return [source.size, source[kHandle]];
81+
82+
if (typeof source === 'string') {
83+
source = lazyBuffer().from(source, encoding);
84+
} else if (isAnyArrayBuffer(source)) {
85+
source = new Uint8Array(source);
86+
} else if (!isArrayBufferView(source)) {
87+
throw new ERR_INVALID_ARG_TYPE(
88+
'source',
89+
[
90+
'string',
91+
'ArrayBuffer',
92+
'SharedArrayBuffer',
93+
'Buffer',
94+
'TypedArray',
95+
'DataView'
96+
],
97+
source);
98+
}
99+
100+
// We copy into a new Uint8Array because the underlying
101+
// BackingStores are going to be detached and owned by
102+
// the Blob. We also don't want to have to worry about
103+
// byte offsets.
104+
source = new Uint8Array(source);
105+
return [source.byteLength, source];
106+
}
107+
108+
class InternalBlob extends JSTransferable {
109+
constructor(handle, length, type = '') {
110+
super();
111+
this[kHandle] = handle;
112+
this[kType] = type;
113+
this[kLength] = length;
114+
}
115+
}
116+
117+
class Blob extends JSTransferable {
118+
constructor(sources = [], options) {
119+
emitExperimentalWarning('buffer.Blob');
120+
if (sources === null ||
121+
typeof sources[SymbolIterator] !== 'function' ||
122+
typeof sources === 'string') {
123+
throw new ERR_INVALID_ARG_TYPE('sources', 'Iterable', sources);
124+
}
125+
if (options !== undefined)
126+
validateObject(options, 'options');
127+
const {
128+
encoding = 'utf8',
129+
type = '',
130+
} = { ...options };
131+
132+
let length = 0;
133+
const sources_ = ArrayFrom(sources, (source) => {
134+
const { 0: len, 1: src } = getSource(source, encoding);
135+
length += len;
136+
return src;
137+
});
138+
139+
// This is a MIME media type but we're not actively checking the syntax.
140+
// But, to be fair, neither does Chrome.
141+
validateString(type, 'options.type');
142+
143+
if (!isUint32(length))
144+
throw new ERR_BUFFER_TOO_LARGE(0xFFFFFFFF);
145+
146+
super();
147+
this[kHandle] = createBlob(sources_, length);
148+
this[kLength] = length;
149+
this[kType] = RegExpPrototypeTest(/[^\u{0020}-\u{007E}]/u, type) ?
150+
'' : StringPrototypeToLowerCase(type);
151+
}
152+
153+
[kInspect](depth, options) {
154+
if (depth < 0)
155+
return this;
156+
157+
const opts = {
158+
...options,
159+
depth: options.depth == null ? null : options.depth - 1
160+
};
161+
162+
return `Blob ${inspect({
163+
size: this.size,
164+
type: this.type,
165+
}, opts)}`;
166+
}
167+
168+
[kClone]() {
169+
const handle = this[kHandle];
170+
const type = this[kType];
171+
const length = this[kLength];
172+
return {
173+
data: { handle, type, length },
174+
deserializeInfo: 'internal/blob:InternalBlob'
175+
};
176+
}
177+
178+
[kDeserialize]({ handle, type, length }) {
179+
this[kHandle] = handle;
180+
this[kType] = type;
181+
this[kLength] = length;
182+
}
183+
184+
get type() { return this[kType]; }
185+
186+
get size() { return this[kLength]; }
187+
188+
slice(start = 0, end = (this[kLength]), type = this[kType]) {
189+
validateUint32(start, 'start');
190+
if (end < 0) end = this[kLength] + end;
191+
validateUint32(end, 'end');
192+
validateString(type, 'type');
193+
if (end < start)
194+
throw new ERR_OUT_OF_RANGE('end', 'greater than start', end);
195+
if (end > this[kLength])
196+
throw new ERR_OUT_OF_RANGE('end', 'less than or equal to length', end);
197+
return new InternalBlob(
198+
this[kHandle].slice(start, end),
199+
end - start, type);
200+
}
201+
202+
async arrayBuffer() {
203+
const job = new FixedSizeBlobCopyJob(this[kHandle]);
204+
205+
const ret = job.run();
206+
if (ret !== undefined)
207+
return PromiseResolve(ret);
208+
209+
const {
210+
promise,
211+
resolve,
212+
reject
213+
} = deferred();
214+
job.ondone = (err, ab) => {
215+
if (err !== undefined)
216+
return reject(new AbortError());
217+
resolve(ab);
218+
};
219+
220+
return promise;
221+
}
222+
223+
async text() {
224+
const dec = new TextDecoder();
225+
return dec.decode(await this.arrayBuffer());
226+
}
227+
}
228+
229+
InternalBlob.prototype.constructor = Blob;
230+
ObjectSetPrototypeOf(
231+
InternalBlob.prototype,
232+
Blob.prototype);
233+
234+
module.exports = {
235+
Blob,
236+
InternalBlob,
237+
isBlob,
238+
};

0 commit comments

Comments
 (0)