Skip to content

Commit df37c10

Browse files
jasnellMylesBorins
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 Backport-PR-URL: #39704 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 223494c commit df37c10

13 files changed

+1014
-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.
@@ -3397,6 +3510,7 @@ introducing security vulnerabilities into an application.
33973510
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
33983511
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
33993512
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
3513+
[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
34003514
[`Buffer.alloc()`]: #buffer_static_method_buffer_alloc_size_fill_encoding
34013515
[`Buffer.allocUnsafe()`]: #buffer_static_method_buffer_allocunsafe_size
34023516
[`Buffer.allocUnsafeSlow()`]: #buffer_static_method_buffer_allocunsafeslow_size

lib/buffer.js

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

119+
const {
120+
Blob,
121+
} = require('internal/blob');
122+
119123
FastBuffer.prototype.constructor = Buffer;
120124
Buffer.prototype = FastBuffer.prototype;
121125
addBufferPrototypeMethods(Buffer.prototype);
@@ -1259,6 +1263,7 @@ function atob(input) {
12591263
}
12601264

12611265
module.exports = {
1266+
Blob,
12621267
Buffer,
12631268
SlowBuffer,
12641269
transcode,

lib/internal/blob.js

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

0 commit comments

Comments
 (0)