Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/buffer.md14
-rw-r--r--doc/api/url.md47
-rw-r--r--lib/buffer.js2
-rw-r--r--lib/internal/blob.js94
-rw-r--r--lib/internal/url.js79
-rw-r--r--src/node_binding.cc1
-rw-r--r--src/node_blob.cc166
-rw-r--r--src/node_blob.h56
-rw-r--r--src/node_buffer.cc5
-rw-r--r--src/node_external_reference.h1
-rw-r--r--src/node_snapshotable.cc1
-rw-r--r--src/node_snapshotable.h3
-rw-r--r--test/parallel/test-blob-createobjecturl.js48
-rw-r--r--test/parallel/test-bootstrap-modules.js1
14 files changed, 482 insertions, 36 deletions
diff --git a/doc/api/buffer.md b/doc/api/buffer.md
index 7106d5c6b36..f419be1181a 100644
--- a/doc/api/buffer.md
+++ b/doc/api/buffer.md
@@ -4952,6 +4952,20 @@ added: v3.0.0
An alias for [`buffer.constants.MAX_STRING_LENGTH`][].
+### `buffer.resolveObjectURL(id)`
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to
+ `URL.createObjectURL()`.
+* Returns: {Blob}
+
+Resolves a `'blob:nodedata:...'` an associated {Blob} object registered using
+a prior call to `URL.createObjectURL()`.
+
### `buffer.transcode(source, fromEnc, toEnc)`
<!-- YAML
added: v7.1.0
diff --git a/doc/api/url.md b/doc/api/url.md
index 725084523df..d4b212f515d 100644
--- a/doc/api/url.md
+++ b/doc/api/url.md
@@ -608,6 +608,53 @@ console.log(JSON.stringify(myURLs));
// Prints ["https://www.example.com/","https://test.example.org/"]
```
+#### `URL.createObjectURL(blob)`
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `blob` {Blob}
+* Returns: {string}
+
+Creates a `'blob:nodedata:...'` URL string that represents the given {Blob}
+object and can be used to retrieve the `Blob` later.
+
+```js
+const {
+ Blob,
+ resolveObjectURL,
+} = require('buffer');
+
+const blob = new Blob(['hello']);
+const id = URL.createObjectURL(blob);
+
+// later...
+
+const otherBlob = resolveObjectURL(id);
+console.log(otherBlob.size);
+```
+
+The data stored by the registered {Blob} will be retained in memory until
+`URL.revokeObjectURL()` is called to remove it.
+
+`Blob` objects are registered within the current thread. If using Worker
+Threads, `Blob` objects registered within one Worker will not be available
+to other workers or the main thread.
+
+#### `URL.revokeObjectURL(id)`
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `id` {string} A `'blob:nodedata:...` URL string returned by a prior call to
+ `URL.createObjectURL()`.
+
+Removes the stored {Blob} identified by the given ID.
+
### Class: `URLSearchParams`
<!-- YAML
added:
diff --git a/lib/buffer.js b/lib/buffer.js
index 278a67cbbfb..1f4f0a2e89d 100644
--- a/lib/buffer.js
+++ b/lib/buffer.js
@@ -120,6 +120,7 @@ const {
const {
Blob,
+ resolveObjectURL,
} = require('internal/blob');
FastBuffer.prototype.constructor = Buffer;
@@ -1239,6 +1240,7 @@ function atob(input) {
module.exports = {
Blob,
+ resolveObjectURL,
Buffer,
SlowBuffer,
transcode,
diff --git a/lib/internal/blob.js b/lib/internal/blob.js
index d69c9c17980..3c6e841acda 100644
--- a/lib/internal/blob.js
+++ b/lib/internal/blob.js
@@ -7,10 +7,11 @@ const {
ObjectDefineProperty,
PromiseResolve,
PromiseReject,
- PromisePrototypeFinally,
+ SafePromisePrototypeFinally,
ReflectConstruct,
RegExpPrototypeTest,
StringPrototypeToLowerCase,
+ StringPrototypeSplit,
Symbol,
SymbolIterator,
SymbolToStringTag,
@@ -20,7 +21,8 @@ const {
const {
createBlob: _createBlob,
FixedSizeBlobCopyJob,
-} = internalBinding('buffer');
+ getDataObject,
+} = internalBinding('blob');
const { TextDecoder } = require('internal/encoding');
@@ -57,26 +59,37 @@ const {
} = require('internal/validators');
const kHandle = Symbol('kHandle');
+const kState = Symbol('kState');
const kType = Symbol('kType');
const kLength = Symbol('kLength');
const kArrayBufferPromise = Symbol('kArrayBufferPromise');
+const kMaxChunkSize = 65536;
+
const disallowedTypeCharacters = /[^\u{0020}-\u{007E}]/u;
let Buffer;
let ReadableStream;
+let URL;
+
+
+// Yes, lazy loading is annoying but because of circular
+// references between the url, internal/blob, and buffer
+// modules, lazy loading here makes sure that things work.
+
+function lazyURL(id) {
+ URL ??= require('internal/url').URL;
+ return new URL(id);
+}
function lazyBuffer() {
- if (Buffer === undefined)
- Buffer = require('buffer').Buffer;
+ Buffer ??= require('buffer').Buffer;
return Buffer;
}
function lazyReadableStream(options) {
- if (ReadableStream === undefined) {
- ReadableStream =
- require('internal/webstreams/readablestream').ReadableStream;
- }
+ ReadableStream ??=
+ require('internal/webstreams/readablestream').ReadableStream;
return new ReadableStream(options);
}
@@ -232,9 +245,9 @@ class Blob {
return PromiseReject(new ERR_INVALID_THIS('Blob'));
// If there's already a promise in flight for the content,
- // reuse it, but only once. After the cached promise resolves
- // it will be cleared, allowing it to be garbage collected
- // as soon as possible.
+ // reuse it, but only while it's in flight. After the cached
+ // promise resolves it will be cleared, allowing it to be
+ // garbage collected as soon as possible.
if (this[kArrayBufferPromise])
return this[kArrayBufferPromise];
@@ -260,7 +273,7 @@ class Blob {
resolve(ab);
};
this[kArrayBufferPromise] =
- PromisePrototypeFinally(
+ SafePromisePrototypeFinally(
promise,
() => this[kArrayBufferPromise] = undefined);
@@ -268,7 +281,6 @@ class Blob {
}
/**
- *
* @returns {Promise<string>}
*/
async text() {
@@ -288,10 +300,20 @@ class Blob {
const self = this;
return new lazyReadableStream({
- async start(controller) {
- const ab = await self.arrayBuffer();
- controller.enqueue(new Uint8Array(ab));
- controller.close();
+ async start() {
+ this[kState] = await self.arrayBuffer();
+ },
+
+ pull(controller) {
+ if (this[kState].byteLength <= kMaxChunkSize) {
+ controller.enqueue(new Uint8Array(this[kState]));
+ controller.close();
+ this[kState] = undefined;
+ } else {
+ const slice = this[kState].slice(0, kMaxChunkSize);
+ this[kState] = this[kState].slice(kMaxChunkSize);
+ controller.enqueue(new Uint8Array(slice));
+ }
}
});
}
@@ -315,9 +337,47 @@ ObjectDefineProperty(Blob.prototype, SymbolToStringTag, {
value: 'Blob',
});
+function resolveObjectURL(url) {
+ url = `${url}`;
+ try {
+ const parsed = new lazyURL(url);
+
+ const split = StringPrototypeSplit(parsed.pathname, ':');
+
+ if (split.length !== 2)
+ return;
+
+ const {
+ 0: base,
+ 1: id,
+ } = split;
+
+ if (base !== 'nodedata')
+ return;
+
+ const ret = getDataObject(id);
+
+ if (ret === undefined)
+ return;
+
+ const {
+ 0: handle,
+ 1: length,
+ 2: type,
+ } = ret;
+
+ if (handle !== undefined)
+ return createBlob(handle, length, type);
+ } catch {
+ // If there's an error, it's ignored and nothing is returned
+ }
+}
+
module.exports = {
Blob,
ClonedBlob,
createBlob,
isBlob,
+ kHandle,
+ resolveObjectURL,
};
diff --git a/lib/internal/url.js b/lib/internal/url.js
index 0749e07d6e7..043b7311a2c 100644
--- a/lib/internal/url.js
+++ b/lib/internal/url.js
@@ -42,17 +42,20 @@ const {
const { getConstructorOf, removeColors } = require('internal/util');
const {
- ERR_ARG_NOT_ITERABLE,
- ERR_INVALID_ARG_TYPE,
- ERR_INVALID_ARG_VALUE,
- ERR_INVALID_FILE_URL_HOST,
- ERR_INVALID_FILE_URL_PATH,
- ERR_INVALID_THIS,
- ERR_INVALID_TUPLE,
- ERR_INVALID_URL,
- ERR_INVALID_URL_SCHEME,
- ERR_MISSING_ARGS
-} = require('internal/errors').codes;
+ codes: {
+ ERR_ARG_NOT_ITERABLE,
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_INVALID_FILE_URL_HOST,
+ ERR_INVALID_FILE_URL_PATH,
+ ERR_INVALID_THIS,
+ ERR_INVALID_TUPLE,
+ ERR_INVALID_URL,
+ ERR_INVALID_URL_SCHEME,
+ ERR_MISSING_ARGS,
+ ERR_NO_CRYPTO,
+ },
+} = require('internal/errors');
const {
CHAR_AMPERSAND,
CHAR_BACKWARD_SLASH,
@@ -100,6 +103,11 @@ const {
kSchemeStart
} = internalBinding('url');
+const {
+ storeDataObject,
+ revokeDataObject,
+} = internalBinding('blob');
+
const context = Symbol('context');
const cannotBeBase = Symbol('cannot-be-base');
const cannotHaveUsernamePasswordPort =
@@ -108,6 +116,24 @@ const special = Symbol('special');
const searchParams = Symbol('query');
const kFormat = Symbol('format');
+let blob;
+let cryptoRandom;
+
+function lazyBlob() {
+ blob ??= require('internal/blob');
+ return blob;
+}
+
+function lazyCryptoRandom() {
+ try {
+ cryptoRandom ??= require('internal/crypto/random');
+ } catch {
+ // If Node.js built without crypto support, we'll fall
+ // through here and handle it later.
+ }
+ return cryptoRandom;
+}
+
// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
const IteratorPrototype = ObjectGetPrototypeOf(
ObjectGetPrototypeOf([][SymbolIterator]())
@@ -930,6 +956,37 @@ class URL {
toJSON() {
return this[kFormat]({});
}
+
+ static createObjectURL(obj) {
+ const cryptoRandom = lazyCryptoRandom();
+ if (cryptoRandom === undefined)
+ throw new ERR_NO_CRYPTO();
+
+ // Yes, lazy loading is annoying but because of circular
+ // references between the url, internal/blob, and buffer
+ // modules, lazy loading here makes sure that things work.
+ const blob = lazyBlob();
+ if (!blob.isBlob(obj))
+ throw new ERR_INVALID_ARG_TYPE('obj', 'Blob', obj);
+
+ const id = cryptoRandom.randomUUID();
+
+ storeDataObject(id, obj[blob.kHandle], obj.size, obj.type);
+
+ return `blob:nodedata:${id}`;
+ }
+
+ static revokeObjectURL(url) {
+ url = `${url}`;
+ try {
+ const parsed = new URL(url);
+ const split = StringPrototypeSplit(parsed.pathname, ':');
+ if (split.length === 2)
+ revokeDataObject(split[1]);
+ } catch {
+ // If there's an error, it's ignored.
+ }
+ }
}
ObjectDefineProperties(URL.prototype, {
diff --git a/src/node_binding.cc b/src/node_binding.cc
index b5e42af7951..3c9776e655d 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -40,6 +40,7 @@
// __attribute__((constructor)) like mechanism in GCC.
#define NODE_BUILTIN_STANDARD_MODULES(V) \
V(async_wrap) \
+ V(blob) \
V(block_list) \
V(buffer) \
V(cares_wrap) \
diff --git a/src/node_blob.cc b/src/node_blob.cc
index 0b18bbce138..c583d5e0a93 100644
--- a/src/node_blob.cc
+++ b/src/node_blob.cc
@@ -26,12 +26,26 @@ using v8::Local;
using v8::MaybeLocal;
using v8::Number;
using v8::Object;
+using v8::String;
using v8::Uint32;
using v8::Undefined;
using v8::Value;
-void Blob::Initialize(Environment* env, v8::Local<v8::Object> target) {
+void Blob::Initialize(
+ Local<Object> target,
+ Local<Value> unused,
+ Local<Context> context,
+ void* priv) {
+ Environment* env = Environment::GetCurrent(context);
+
+ BlobBindingData* const binding_data =
+ env->AddBindingData<BlobBindingData>(context, target);
+ if (binding_data == nullptr) return;
+
env->SetMethod(target, "createBlob", New);
+ env->SetMethod(target, "storeDataObject", StoreDataObject);
+ env->SetMethod(target, "getDataObject", GetDataObject);
+ env->SetMethod(target, "revokeDataObject", RevokeDataObject);
FixedSizeBlobCopyJob::Initialize(env, target);
}
@@ -220,6 +234,78 @@ std::unique_ptr<worker::TransferData> Blob::CloneForMessaging() const {
return std::make_unique<BlobTransferData>(store_, length_);
}
+void Blob::StoreDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ BlobBindingData* binding_data =
+ Environment::GetBindingData<BlobBindingData>(args);
+
+ CHECK(args[0]->IsString()); // ID key
+ CHECK(Blob::HasInstance(env, args[1])); // Blob
+ CHECK(args[2]->IsUint32()); // Length
+ CHECK(args[3]->IsString()); // Type
+
+ Utf8Value key(env->isolate(), args[0]);
+ Blob* blob;
+ ASSIGN_OR_RETURN_UNWRAP(&blob, args[1]);
+
+ size_t length = args[2].As<Uint32>()->Value();
+ Utf8Value type(env->isolate(), args[3]);
+
+ binding_data->store_data_object(
+ std::string(*key, key.length()),
+ BlobBindingData::StoredDataObject(
+ BaseObjectPtr<Blob>(blob),
+ length,
+ std::string(*type, type.length())));
+}
+
+void Blob::RevokeDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) {
+ BlobBindingData* binding_data =
+ Environment::GetBindingData<BlobBindingData>(args);
+
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsString()); // ID key
+
+ Utf8Value key(env->isolate(), args[0]);
+
+ binding_data->revoke_data_object(std::string(*key, key.length()));
+}
+
+void Blob::GetDataObject(const v8::FunctionCallbackInfo<v8::Value>& args) {
+ BlobBindingData* binding_data =
+ Environment::GetBindingData<BlobBindingData>(args);
+
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsString());
+
+ Utf8Value key(env->isolate(), args[0]);
+
+ BlobBindingData::StoredDataObject stored =
+ binding_data->get_data_object(std::string(*key, key.length()));
+ if (stored.blob) {
+ Local<Value> type;
+ if (!String::NewFromUtf8(
+ env->isolate(),
+ stored.type.c_str(),
+ v8::NewStringType::kNormal,
+ static_cast<int>(stored.type.length())).ToLocal(&type)) {
+ return;
+ }
+
+ Local<Value> values[] = {
+ stored.blob->object(),
+ Uint32::NewFromUnsigned(env->isolate(), stored.length),
+ type
+ };
+
+ args.GetReturnValue().Set(
+ Array::New(
+ env->isolate(),
+ values,
+ arraysize(values)));
+ }
+}
+
FixedSizeBlobCopyJob::FixedSizeBlobCopyJob(
Environment* env,
Local<Object> object,
@@ -328,10 +414,88 @@ void FixedSizeBlobCopyJob::RegisterExternalReferences(
registry->Register(Run);
}
+void BlobBindingData::StoredDataObject::MemoryInfo(
+ MemoryTracker* tracker) const {
+ tracker->TrackField("blob", blob);
+ tracker->TrackFieldWithSize("type", type.length());
+}
+
+BlobBindingData::StoredDataObject::StoredDataObject(
+ const BaseObjectPtr<Blob>& blob_,
+ size_t length_,
+ const std::string& type_)
+ : blob(blob_),
+ length(length_),
+ type(type_) {}
+
+BlobBindingData::BlobBindingData(Environment* env, Local<Object> wrap)
+ : SnapshotableObject(env, wrap, type_int) {
+ MakeWeak();
+}
+
+void BlobBindingData::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("data_objects", data_objects_);
+}
+
+void BlobBindingData::store_data_object(
+ const std::string& uuid,
+ const BlobBindingData::StoredDataObject& object) {
+ data_objects_[uuid] = object;
+}
+
+void BlobBindingData::revoke_data_object(const std::string& uuid) {
+ CHECK_NE(data_objects_.find(uuid), data_objects_.end());
+ data_objects_.erase(uuid);
+ CHECK_EQ(data_objects_.find(uuid), data_objects_.end());
+}
+
+BlobBindingData::StoredDataObject BlobBindingData::get_data_object(
+ const std::string& uuid) {
+ auto entry = data_objects_.find(uuid);
+ if (entry == data_objects_.end())
+ return BlobBindingData::StoredDataObject {};
+ return entry->second;
+}
+
+void BlobBindingData::Deserialize(
+ Local<Context> context,
+ Local<Object> holder,
+ int index,
+ InternalFieldInfo* info) {
+ DCHECK_EQ(index, BaseObject::kSlot);
+ HandleScope scope(context->GetIsolate());
+ Environment* env = Environment::GetCurrent(context);
+ BlobBindingData* binding =
+ env->AddBindingData<BlobBindingData>(context, holder);
+ CHECK_NOT_NULL(binding);
+}
+
+void BlobBindingData::PrepareForSerialization(
+ Local<Context> context,
+ v8::SnapshotCreator* creator) {
+ // Stored blob objects are not actually persisted.
+}
+
+InternalFieldInfo* BlobBindingData::Serialize(int index) {
+ DCHECK_EQ(index, BaseObject::kSlot);
+ InternalFieldInfo* info = InternalFieldInfo::New(type());
+ return info;
+}
+
+constexpr FastStringKey BlobBindingData::type_name;
+
void Blob::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Blob::New);
registry->Register(Blob::ToArrayBuffer);
registry->Register(Blob::ToSlice);
+ registry->Register(Blob::StoreDataObject);
+ registry->Register(Blob::GetDataObject);
+ registry->Register(Blob::RevokeDataObject);
+
+ FixedSizeBlobCopyJob::RegisterExternalReferences(registry);
}
} // namespace node
+
+NODE_MODULE_CONTEXT_AWARE_INTERNAL(blob, node::Blob::Initialize);
+NODE_MODULE_EXTERNAL_REFERENCE(blob, node::Blob::RegisterExternalReferences);
diff --git a/src/node_blob.h b/src/node_blob.h
index 9d6178996c8..61f4da655c9 100644
--- a/src/node_blob.h
+++ b/src/node_blob.h
@@ -8,9 +8,12 @@
#include "env.h"
#include "memory_tracker.h"
#include "node_internals.h"
+#include "node_snapshotable.h"
#include "node_worker.h"
#include "v8.h"
+#include <string>
+#include <unordered_map>
#include <vector>
namespace node {
@@ -25,11 +28,19 @@ class Blob : public BaseObject {
public:
static void RegisterExternalReferences(
ExternalReferenceRegistry* registry);
- static void Initialize(Environment* env, v8::Local<v8::Object> target);
+
+ static void Initialize(
+ v8::Local<v8::Object> target,
+ v8::Local<v8::Value> unused,
+ v8::Local<v8::Context> context,
+ void* priv);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ToArrayBuffer(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ToSlice(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void StoreDataObject(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void GetDataObject(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void RevokeDataObject(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
@@ -131,6 +142,49 @@ class FixedSizeBlobCopyJob : public AsyncWrap, public ThreadPoolWork {
size_t length_ = 0;
};
+class BlobBindingData : public SnapshotableObject {
+ public:
+ explicit BlobBindingData(Environment* env, v8::Local<v8::Object> wrap);
+
+ SERIALIZABLE_OBJECT_METHODS()
+
+ static constexpr FastStringKey type_name{"node::BlobBindingData"};
+ static constexpr EmbedderObjectType type_int =
+ EmbedderObjectType::k_blob_binding_data;
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_SELF_SIZE(BlobBindingData)
+ SET_MEMORY_INFO_NAME(BlobBindingData)
+
+ struct StoredDataObject : public MemoryRetainer {
+ BaseObjectPtr<Blob> blob;
+ size_t length;
+ std::string type;
+
+ StoredDataObject() = default;
+
+ StoredDataObject(
+ const BaseObjectPtr<Blob>& blob_,
+ size_t length_,
+ const std::string& type_);
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_SELF_SIZE(StoredDataObject)
+ SET_MEMORY_INFO_NAME(StoredDataObject)
+ };
+
+ void store_data_object(
+ const std::string& uuid,
+ const StoredDataObject& object);
+
+ void revoke_data_object(const std::string& uuid);
+
+ StoredDataObject get_data_object(const std::string& uuid);
+
+ private:
+ std::unordered_map<std::string, StoredDataObject> data_objects_;
+};
+
} // namespace node
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/node_buffer.cc b/src/node_buffer.cc
index b5651b5e325..962a4a11aab 100644
--- a/src/node_buffer.cc
+++ b/src/node_buffer.cc
@@ -1266,8 +1266,6 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "utf8Write", StringWrite<UTF8>);
env->SetMethod(target, "getZeroFillToggle", GetZeroFillToggle);
-
- Blob::Initialize(env, target);
}
} // anonymous namespace
@@ -1311,9 +1309,6 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(DetachArrayBuffer);
registry->Register(CopyArrayBuffer);
-
- Blob::RegisterExternalReferences(registry);
- FixedSizeBlobCopyJob::RegisterExternalReferences(registry);
}
} // namespace Buffer
diff --git a/src/node_external_reference.h b/src/node_external_reference.h
index 094558783f7..8d2de64ad43 100644
--- a/src/node_external_reference.h
+++ b/src/node_external_reference.h
@@ -49,6 +49,7 @@ class ExternalReferenceRegistry {
#define EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \
V(async_wrap) \
V(binding) \
+ V(blob) \
V(buffer) \
V(contextify) \
V(credentials) \
diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc
index 35e0ed3f6df..6e284bb66a0 100644
--- a/src/node_snapshotable.cc
+++ b/src/node_snapshotable.cc
@@ -5,6 +5,7 @@
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
+#include "node_blob.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_file.h"
diff --git a/src/node_snapshotable.h b/src/node_snapshotable.h
index 38da68f6d28..ceb84fc9bf0 100644
--- a/src/node_snapshotable.h
+++ b/src/node_snapshotable.h
@@ -15,7 +15,8 @@ struct SnapshotData;
#define SERIALIZABLE_OBJECT_TYPES(V) \
V(fs_binding_data, fs::BindingData) \
- V(v8_binding_data, v8_utils::BindingData)
+ V(v8_binding_data, v8_utils::BindingData) \
+ V(blob_binding_data, BlobBindingData)
enum class EmbedderObjectType : uint8_t {
k_default = 0,
diff --git a/test/parallel/test-blob-createobjecturl.js b/test/parallel/test-blob-createobjecturl.js
new file mode 100644
index 00000000000..a8fd377dd3e
--- /dev/null
+++ b/test/parallel/test-blob-createobjecturl.js
@@ -0,0 +1,48 @@
+// Flags: --no-warnings
+'use strict';
+
+const common = require('../common');
+
+// Because registering a Blob URL requires generating a random
+// UUID, it can only be done if crypto support is enabled.
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const {
+ URL,
+} = require('url');
+
+const {
+ Blob,
+ resolveObjectURL,
+} = require('buffer');
+
+const assert = require('assert');
+
+(async () => {
+ const blob = new Blob(['hello']);
+ const id = URL.createObjectURL(blob);
+ assert.strictEqual(typeof id, 'string');
+ const otherBlob = resolveObjectURL(id);
+ assert.strictEqual(otherBlob.size, 5);
+ assert.strictEqual(
+ Buffer.from(await otherBlob.arrayBuffer()).toString(),
+ 'hello');
+ URL.revokeObjectURL(id);
+ assert.strictEqual(resolveObjectURL(id), undefined);
+
+ // Leaving a Blob registered should not cause an assert
+ // when Node.js exists
+ URL.createObjectURL(new Blob());
+
+})().then(common.mustCall());
+
+['not a url', undefined, 1, 'blob:nodedata:1:wrong', {}].forEach((i) => {
+ assert.strictEqual(resolveObjectURL(i), undefined);
+});
+
+[undefined, 1, '', false, {}].forEach((i) => {
+ assert.throws(() => URL.createObjectURL(i), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ });
+});
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index d02f0c71860..37cdbca3300 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -130,6 +130,7 @@ const expectedModules = new Set([
'NativeModule internal/vm/module',
'NativeModule internal/worker/io',
'NativeModule internal/worker/js_transferable',
+ 'Internal Binding blob',
'NativeModule internal/blob',
'NativeModule async_hooks',
'NativeModule path',