Skip to content

Commit 37dda19

Browse files
jasnelldanielleadams
authored andcommitted
url,buffer: implement URL.createObjectURL
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 53cf53c commit 37dda19

14 files changed

+482
-36
lines changed

doc/api/buffer.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4948,6 +4948,20 @@ added: v3.0.0
49484948

49494949
An alias for [`buffer.constants.MAX_STRING_LENGTH`][].
49504950

4951+
### `buffer.resolveObjectURL(id)`
4952+
<!-- YAML
4953+
added: REPLACEME
4954+
-->
4955+
4956+
> Stability: 1 - Experimental
4957+
4958+
* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to
4959+
`URL.createObjectURL()`.
4960+
* Returns: {Blob}
4961+
4962+
Resolves a `'blob:nodedata:...'` an associated {Blob} object registered using
4963+
a prior call to `URL.createObjectURL()`.
4964+
49514965
### `buffer.transcode(source, fromEnc, toEnc)`
49524966
<!-- YAML
49534967
added: v7.1.0

doc/api/url.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,53 @@ console.log(JSON.stringify(myURLs));
608608
// Prints ["https://www.example.com/","https://test.example.org/"]
609609
```
610610

611+
#### `URL.createObjectURL(blob)`
612+
<!-- YAML
613+
added: REPLACEME
614+
-->
615+
616+
> Stability: 1 - Experimental
617+
618+
* `blob` {Blob}
619+
* Returns: {string}
620+
621+
Creates a `'blob:nodedata:...'` URL string that represents the given {Blob}
622+
object and can be used to retrieve the `Blob` later.
623+
624+
```js
625+
const {
626+
Blob,
627+
resolveObjectURL,
628+
} = require('buffer');
629+
630+
const blob = new Blob(['hello']);
631+
const id = URL.createObjectURL(blob);
632+
633+
// later...
634+
635+
const otherBlob = resolveObjectURL(id);
636+
console.log(otherBlob.size);
637+
```
638+
639+
The data stored by the registered {Blob} will be retained in memory until
640+
`URL.revokeObjectURL()` is called to remove it.
641+
642+
`Blob` objects are registered within the current thread. If using Worker
643+
Threads, `Blob` objects registered within one Worker will not be available
644+
to other workers or the main thread.
645+
646+
#### `URL.revokeObjectURL(id)`
647+
<!-- YAML
648+
added: REPLACEME
649+
-->
650+
651+
> Stability: 1 - Experimental
652+
653+
* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to
654+
`URL.createObjectURL()`.
655+
656+
Removes the stored {Blob} identified by the given ID.
657+
611658
### Class: `URLSearchParams`
612659
<!-- YAML
613660
added:

lib/buffer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const {
120120

121121
const {
122122
Blob,
123+
resolveObjectURL,
123124
} = require('internal/blob');
124125

125126
FastBuffer.prototype.constructor = Buffer;
@@ -1239,6 +1240,7 @@ function atob(input) {
12391240

12401241
module.exports = {
12411242
Blob,
1243+
resolveObjectURL,
12421244
Buffer,
12431245
SlowBuffer,
12441246
transcode,

lib/internal/blob.js

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ const {
77
ObjectDefineProperty,
88
PromiseResolve,
99
PromiseReject,
10-
PromisePrototypeFinally,
10+
SafePromisePrototypeFinally,
1111
ReflectConstruct,
1212
RegExpPrototypeTest,
1313
StringPrototypeToLowerCase,
14+
StringPrototypeSplit,
1415
Symbol,
1516
SymbolIterator,
1617
SymbolToStringTag,
@@ -20,7 +21,8 @@ const {
2021
const {
2122
createBlob: _createBlob,
2223
FixedSizeBlobCopyJob,
23-
} = internalBinding('buffer');
24+
getDataObject,
25+
} = internalBinding('blob');
2426

2527
const { TextDecoder } = require('internal/encoding');
2628

@@ -57,26 +59,37 @@ const {
5759
} = require('internal/validators');
5860

5961
const kHandle = Symbol('kHandle');
62+
const kState = Symbol('kState');
6063
const kType = Symbol('kType');
6164
const kLength = Symbol('kLength');
6265
const kArrayBufferPromise = Symbol('kArrayBufferPromise');
6366

67+
const kMaxChunkSize = 65536;
68+
6469
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
6570

6671
let Buffer;
6772
let ReadableStream;
73+
let URL;
74+
75+
76+
// Yes, lazy loading is annoying but because of circular
77+
// references between the url, internal/blob, and buffer
78+
// modules, lazy loading here makes sure that things work.
79+
80+
function lazyURL(id) {
81+
URL ??= require('internal/url').URL;
82+
return new URL(id);
83+
}
6884

6985
function lazyBuffer() {
70-
if (Buffer === undefined)
71-
Buffer = require('buffer').Buffer;
86+
Buffer ??= require('buffer').Buffer;
7287
return Buffer;
7388
}
7489

7590
function lazyReadableStream(options) {
76-
if (ReadableStream === undefined) {
77-
ReadableStream =
78-
require('internal/webstreams/readablestream').ReadableStream;
79-
}
91+
ReadableStream ??=
92+
require('internal/webstreams/readablestream').ReadableStream;
8093
return new ReadableStream(options);
8194
}
8295

@@ -232,9 +245,9 @@ class Blob {
232245
return PromiseReject(new ERR_INVALID_THIS('Blob'));
233246

234247
// 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.
248+
// reuse it, but only while it's in flight. After the cached
249+
// promise resolves it will be cleared, allowing it to be
250+
// garbage collected as soon as possible.
238251
if (this[kArrayBufferPromise])
239252
return this[kArrayBufferPromise];
240253

@@ -260,15 +273,14 @@ class Blob {
260273
resolve(ab);
261274
};
262275
this[kArrayBufferPromise] =
263-
PromisePrototypeFinally(
276+
SafePromisePrototypeFinally(
264277
promise,
265278
() => this[kArrayBufferPromise] = undefined);
266279

267280
return this[kArrayBufferPromise];
268281
}
269282

270283
/**
271-
*
272284
* @returns {Promise<string>}
273285
*/
274286
async text() {
@@ -288,10 +300,20 @@ class Blob {
288300

289301
const self = this;
290302
return new lazyReadableStream({
291-
async start(controller) {
292-
const ab = await self.arrayBuffer();
293-
controller.enqueue(new Uint8Array(ab));
294-
controller.close();
303+
async start() {
304+
this[kState] = await self.arrayBuffer();
305+
},
306+
307+
pull(controller) {
308+
if (this[kState].byteLength <= kMaxChunkSize) {
309+
controller.enqueue(new Uint8Array(this[kState]));
310+
controller.close();
311+
this[kState] = undefined;
312+
} else {
313+
const slice = this[kState].slice(0, kMaxChunkSize);
314+
this[kState] = this[kState].slice(kMaxChunkSize);
315+
controller.enqueue(new Uint8Array(slice));
316+
}
295317
}
296318
});
297319
}
@@ -315,9 +337,47 @@ ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
315337
value: 'Blob',
316338
});
317339

340+
function resolveObjectURL(url) {
341+
url = `${url}`;
342+
try {
343+
const parsed = new lazyURL(url);
344+
345+
const split = StringPrototypeSplit(parsed.pathname, ':');
346+
347+
if (split.length !== 2)
348+
return;
349+
350+
const {
351+
0: base,
352+
1: id,
353+
} = split;
354+
355+
if (base !== 'nodedata')
356+
return;
357+
358+
const ret = getDataObject(id);
359+
360+
if (ret === undefined)
361+
return;
362+
363+
const {
364+
0: handle,
365+
1: length,
366+
2: type,
367+
} = ret;
368+
369+
if (handle !== undefined)
370+
return createBlob(handle, length, type);
371+
} catch {
372+
// If there's an error, it's ignored and nothing is returned
373+
}
374+
}
375+
318376
module.exports = {
319377
Blob,
320378
ClonedBlob,
321379
createBlob,
322380
isBlob,
381+
kHandle,
382+
resolveObjectURL,
323383
};

lib/internal/url.js

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,20 @@ const {
4242

4343
const { getConstructorOf, removeColors } = require('internal/util');
4444
const {
45-
ERR_ARG_NOT_ITERABLE,
46-
ERR_INVALID_ARG_TYPE,
47-
ERR_INVALID_ARG_VALUE,
48-
ERR_INVALID_FILE_URL_HOST,
49-
ERR_INVALID_FILE_URL_PATH,
50-
ERR_INVALID_THIS,
51-
ERR_INVALID_TUPLE,
52-
ERR_INVALID_URL,
53-
ERR_INVALID_URL_SCHEME,
54-
ERR_MISSING_ARGS
55-
} = require('internal/errors').codes;
45+
codes: {
46+
ERR_ARG_NOT_ITERABLE,
47+
ERR_INVALID_ARG_TYPE,
48+
ERR_INVALID_ARG_VALUE,
49+
ERR_INVALID_FILE_URL_HOST,
50+
ERR_INVALID_FILE_URL_PATH,
51+
ERR_INVALID_THIS,
52+
ERR_INVALID_TUPLE,
53+
ERR_INVALID_URL,
54+
ERR_INVALID_URL_SCHEME,
55+
ERR_MISSING_ARGS,
56+
ERR_NO_CRYPTO,
57+
},
58+
} = require('internal/errors');
5659
const {
5760
CHAR_AMPERSAND,
5861
CHAR_BACKWARD_SLASH,
@@ -100,6 +103,11 @@ const {
100103
kSchemeStart
101104
} = internalBinding('url');
102105

106+
const {
107+
storeDataObject,
108+
revokeDataObject,
109+
} = internalBinding('blob');
110+
103111
const context = Symbol('context');
104112
const cannotBeBase = Symbol('cannot-be-base');
105113
const cannotHaveUsernamePasswordPort =
@@ -108,6 +116,24 @@ const special = Symbol('special');
108116
const searchParams = Symbol('query');
109117
const kFormat = Symbol('format');
110118

119+
let blob;
120+
let cryptoRandom;
121+
122+
function lazyBlob() {
123+
blob ??= require('internal/blob');
124+
return blob;
125+
}
126+
127+
function lazyCryptoRandom() {
128+
try {
129+
cryptoRandom ??= require('internal/crypto/random');
130+
} catch {
131+
// If Node.js built without crypto support, we'll fall
132+
// through here and handle it later.
133+
}
134+
return cryptoRandom;
135+
}
136+
111137
// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
112138
const IteratorPrototype = ObjectGetPrototypeOf(
113139
ObjectGetPrototypeOf([][SymbolIterator]())
@@ -930,6 +956,37 @@ class URL {
930956
toJSON() {
931957
return this[kFormat]({});
932958
}
959+
960+
static createObjectURL(obj) {
961+
const cryptoRandom = lazyCryptoRandom();
962+
if (cryptoRandom === undefined)
963+
throw new ERR_NO_CRYPTO();
964+
965+
// Yes, lazy loading is annoying but because of circular
966+
// references between the url, internal/blob, and buffer
967+
// modules, lazy loading here makes sure that things work.
968+
const blob = lazyBlob();
969+
if (!blob.isBlob(obj))
970+
throw new ERR_INVALID_ARG_TYPE('obj', 'Blob', obj);
971+
972+
const id = cryptoRandom.randomUUID();
973+
974+
storeDataObject(id, obj[blob.kHandle], obj.size, obj.type);
975+
976+
return `blob:nodedata:${id}`;
977+
}
978+
979+
static revokeObjectURL(url) {
980+
url = `${url}`;
981+
try {
982+
const parsed = new URL(url);
983+
const split = StringPrototypeSplit(parsed.pathname, ':');
984+
if (split.length === 2)
985+
revokeDataObject(split[1]);
986+
} catch {
987+
// If there's an error, it's ignored.
988+
}
989+
}
933990
}
934991

935992
ObjectDefineProperties(URL.prototype, {

src/node_binding.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
// __attribute__((constructor)) like mechanism in GCC.
4141
#define NODE_BUILTIN_STANDARD_MODULES(V) \
4242
V(async_wrap) \
43+
V(blob) \
4344
V(block_list) \
4445
V(buffer) \
4546
V(cares_wrap) \

0 commit comments

Comments
 (0)