Skip to content

Commit ade156a

Browse files
committed
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: nodejs#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 d98d193 commit ade156a

13 files changed

+1014
-0
lines changed

doc/api/buffer.md

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

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

284397
The `Buffer` class is a global type for dealing with binary data directly.
@@ -3388,6 +3501,7 @@ introducing security vulnerabilities into an application.
33883501
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
33893502
[WHATWG Encoding Standard]: https://encoding.spec.whatwg.org/
33903503
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
3504+
[`Blob`]: https://developer.mozilla.org/en-US/docs/Web/API/Blob
33913505
[`Buffer.alloc()`]: #buffer_static_method_buffer_alloc_size_fill_encoding
33923506
[`Buffer.allocUnsafe()`]: #buffer_static_method_buffer_allocunsafe_size
33933507
[`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);
@@ -1240,6 +1244,7 @@ function atob(input) {
12401244
}
12411245

12421246
module.exports = {
1247+
Blob,
12431248
Buffer,
12441249
SlowBuffer,
12451250
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)