Skip to content

Commit

Permalink
worker: add markAsUncloneable api
Browse files Browse the repository at this point in the history
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: nodejs#55234
Refs: nodejs#55178
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
  • Loading branch information
jazelly authored and tpoisseau committed Nov 21, 2024
1 parent c76352d commit 29d6579
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 0 deletions.
32 changes: 32 additions & 0 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,38 @@ isMarkedAsUntransferable(pooledBuffer); // Returns true.

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)`

<!-- YAML
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/worker/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ const {
oninit: onInitSymbol,
no_message_symbol: noMessageSymbol,
} = internalBinding('symbols');
const {
privateSymbols: {
transfer_mode_private_symbol,
},
constants: {
kCloneable,
},
} = internalBinding('util');
const {
MessagePort,
MessageChannel,
Expand Down Expand Up @@ -447,13 +455,21 @@ ObjectDefineProperties(BroadcastChannel.prototype, {
defineEventHandler(BroadcastChannel.prototype, 'message');
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 = {
drainMessagePort,
messageTypes,
kPort,
kIncrementsPortRef,
kWaitingStreams,
kStdioWantsMoreDataCallback,
markAsUncloneable,
moveMessagePortToContext,
MessagePort,
MessageChannel,
Expand Down
2 changes: 2 additions & 0 deletions lib/worker_threads.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
const {
MessagePort,
MessageChannel,
markAsUncloneable,
moveMessagePortToContext,
receiveMessageOnPort,
BroadcastChannel,
Expand All @@ -31,6 +32,7 @@ module.exports = {
isMainThread,
MessagePort,
MessageChannel,
markAsUncloneable,
markAsUntransferable,
isMarkedAsUntransferable,
moveMessagePortToContext,
Expand Down
70 changes: 70 additions & 0 deletions test/parallel/test-worker-message-mark-as-uncloneable.js
Original file line number Diff line number Diff line change
@@ -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`);
}

0 comments on commit 29d6579

Please sign in to comment.