worker: add markAsUncloneable api

External modules need a way to decorate their objects so that node can
recognize it as a host object for serialization process. Exposing a way
for turning off instead of turning on is much safer.

PR-URL: https://github.com/nodejs/node/pull/55234
Refs: https://github.com/nodejs/node/pull/55178
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
This commit is contained in:
Jason Zhang 2024-10-04 16:39:37 +09:30 committed by GitHub
parent b2161d3a13
commit d2ad9b4fb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 120 additions and 0 deletions

View File

@ -194,6 +194,38 @@ isMarkedAsUntransferable(pooledBuffer); // Returns true.
There is no equivalent to this API in browsers. There is no equivalent to this API in browsers.
## `worker.markAsUncloneable(object)`
<!-- YAML
added: REPLACEME
-->
* `object` {any} Any arbitrary JavaScript value.
Mark an object as not cloneable. If `object` is used as [`message`](#event-message) in
a [`port.postMessage()`][] call, an error is thrown. This is a no-op if `object` is a
primitive value.
This has no effect on `ArrayBuffer`, or any `Buffer` like objects.
This operation cannot be undone.
```js
const { markAsUncloneable } = require('node:worker_threads');
const anyObject = { foo: 'bar' };
markAsUncloneable(anyObject);
const { port1 } = new MessageChannel();
try {
// This will throw an error, because anyObject is not cloneable.
port1.postMessage(anyObject)
} catch (error) {
// error.name === 'DataCloneError'
}
```
There is no equivalent to this API in browsers.
## `worker.moveMessagePortToContext(port, contextifiedSandbox)` ## `worker.moveMessagePortToContext(port, contextifiedSandbox)`
<!-- YAML <!-- YAML

View File

@ -29,6 +29,14 @@ const {
oninit: onInitSymbol, oninit: onInitSymbol,
no_message_symbol: noMessageSymbol, no_message_symbol: noMessageSymbol,
} = internalBinding('symbols'); } = internalBinding('symbols');
const {
privateSymbols: {
transfer_mode_private_symbol,
},
constants: {
kCloneable,
},
} = internalBinding('util');
const { const {
MessagePort, MessagePort,
MessageChannel, MessageChannel,
@ -447,6 +455,13 @@ ObjectDefineProperties(BroadcastChannel.prototype, {
defineEventHandler(BroadcastChannel.prototype, 'message'); defineEventHandler(BroadcastChannel.prototype, 'message');
defineEventHandler(BroadcastChannel.prototype, 'messageerror'); defineEventHandler(BroadcastChannel.prototype, 'messageerror');
function markAsUncloneable(obj) {
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) {
return;
}
obj[transfer_mode_private_symbol] &= ~kCloneable;
}
module.exports = { module.exports = {
drainMessagePort, drainMessagePort,
messageTypes, messageTypes,
@ -454,6 +469,7 @@ module.exports = {
kIncrementsPortRef, kIncrementsPortRef,
kWaitingStreams, kWaitingStreams,
kStdioWantsMoreDataCallback, kStdioWantsMoreDataCallback,
markAsUncloneable,
moveMessagePortToContext, moveMessagePortToContext,
MessagePort, MessagePort,
MessageChannel, MessageChannel,

View File

@ -13,6 +13,7 @@ const {
const { const {
MessagePort, MessagePort,
MessageChannel, MessageChannel,
markAsUncloneable,
moveMessagePortToContext, moveMessagePortToContext,
receiveMessageOnPort, receiveMessageOnPort,
BroadcastChannel, BroadcastChannel,
@ -31,6 +32,7 @@ module.exports = {
isMainThread, isMainThread,
MessagePort, MessagePort,
MessageChannel, MessageChannel,
markAsUncloneable,
markAsUntransferable, markAsUntransferable,
isMarkedAsUntransferable, isMarkedAsUntransferable,
moveMessagePortToContext, moveMessagePortToContext,

View File

@ -0,0 +1,70 @@
'use strict';
require('../common');
const assert = require('assert');
const { markAsUncloneable } = require('node:worker_threads');
const { mustCall } = require('../common');
const expectedErrorName = 'DataCloneError';
// Uncloneables cannot be cloned during message posting
{
const anyObject = { foo: 'bar' };
markAsUncloneable(anyObject);
const { port1 } = new MessageChannel();
assert.throws(() => port1.postMessage(anyObject), {
constructor: DOMException,
name: expectedErrorName,
code: 25,
}, `Should throw ${expectedErrorName} when posting uncloneables`);
}
// Uncloneables cannot be cloned during structured cloning
{
class MockResponse extends Response {
constructor() {
super();
markAsUncloneable(this);
}
}
structuredClone(MockResponse.prototype);
markAsUncloneable(MockResponse.prototype);
const r = new MockResponse();
assert.throws(() => structuredClone(r), {
constructor: DOMException,
name: expectedErrorName,
code: 25,
}, `Should throw ${expectedErrorName} when cloning uncloneables`);
}
// markAsUncloneable cannot affect ArrayBuffer
{
const pooledBuffer = new ArrayBuffer(8);
const { port1, port2 } = new MessageChannel();
markAsUncloneable(pooledBuffer);
port1.postMessage(pooledBuffer);
port2.on('message', mustCall((value) => {
assert.deepStrictEqual(value, pooledBuffer);
port2.close(mustCall());
}));
}
// markAsUncloneable can affect Node.js built-in object like Blob
{
const cloneableBlob = new Blob();
const { port1, port2 } = new MessageChannel();
port1.postMessage(cloneableBlob);
port2.on('message', mustCall((value) => {
assert.deepStrictEqual(value, cloneableBlob);
port2.close(mustCall());
}));
const uncloneableBlob = new Blob();
markAsUncloneable(uncloneableBlob);
assert.throws(() => port1.postMessage(uncloneableBlob), {
constructor: DOMException,
name: expectedErrorName,
code: 25,
}, `Should throw ${expectedErrorName} when cloning uncloneables`);
}