Skip to content

Commit 0ae84b1

Browse files
committed
url,buffer: implement URL.createObjectURL
Signed-off-by: James M Snell <[email protected]>
1 parent 51fcc3e commit 0ae84b1

14 files changed

+462
-29
lines changed

doc/api/buffer.md

+14
Original file line numberDiff line numberDiff line change
@@ -4952,6 +4952,20 @@ added: v3.0.0
49524952

49534953
An alias for [`buffer.constants.MAX_STRING_LENGTH`][].
49544954

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

doc/api/url.md

+47
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

+2
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

+57-10
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

@@ -65,18 +67,26 @@ const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
6567

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

6982
function lazyBuffer() {
70-
if (Buffer === undefined)
71-
Buffer = require('buffer').Buffer;
83+
Buffer ??= require('buffer').Buffer;
7284
return Buffer;
7385
}
7486

7587
function lazyReadableStream(options) {
76-
if (ReadableStream === undefined) {
77-
ReadableStream =
78-
require('internal/webstreams/readablestream').ReadableStream;
79-
}
88+
ReadableStream ??=
89+
require('internal/webstreams/readablestream').ReadableStream;
8090
return new ReadableStream(options);
8191
}
8292

@@ -260,15 +270,14 @@ class Blob {
260270
resolve(ab);
261271
};
262272
this[kArrayBufferPromise] =
263-
PromisePrototypeFinally(
273+
SafePromisePrototypeFinally(
264274
promise,
265275
() => this[kArrayBufferPromise] = undefined);
266276

267277
return this[kArrayBufferPromise];
268278
}
269279

270280
/**
271-
*
272281
* @returns {Promise<string>}
273282
*/
274283
async text() {
@@ -315,9 +324,47 @@ ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
315324
value: 'Blob',
316325
});
317326

327+
function resolveObjectURL(url) {
328+
url = `${url}`;
329+
try {
330+
const parsed = new lazyURL(url);
331+
332+
const split = StringPrototypeSplit(parsed.pathname, ':');
333+
334+
if (split.length !== 2)
335+
return;
336+
337+
const {
338+
0: base,
339+
1: id,
340+
} = split;
341+
342+
if (base !== 'nodedata')
343+
return;
344+
345+
const ret = getDataObject(id);
346+
347+
if (ret === undefined)
348+
return;
349+
350+
const {
351+
0: handle,
352+
1: length,
353+
2: type,
354+
} = ret;
355+
356+
if (handle !== undefined)
357+
return createBlob(handle, length, type);
358+
} catch {
359+
// If there's an error, it's ignored and nothing is returned
360+
}
361+
}
362+
318363
module.exports = {
319364
Blob,
320365
ClonedBlob,
321366
createBlob,
322367
isBlob,
368+
kHandle,
369+
resolveObjectURL,
323370
};

lib/internal/url.js

+68-11
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 { 1: id } = StringPrototypeSplit(parsed.pathname, ':');
984+
if (id !== undefined)
985+
revokeDataObject(id);
986+
} catch {
987+
// If there's an error, it's ignored.
988+
}
989+
}
933990
}
934991

935992
ObjectDefineProperties(URL.prototype, {

src/node_binding.cc

+1
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)