Skip to content

Commit 5fda4a1

Browse files
jazellytargos
authored andcommitted
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: #55234 Refs: #55178 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com> Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
1 parent b36f8c2 commit 5fda4a1

File tree

4 files changed

+120
-0
lines changed

4 files changed

+120
-0
lines changed

doc/api/worker_threads.md

+32
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,38 @@ isMarkedAsUntransferable(pooledBuffer); // Returns true.
194194

195195
There is no equivalent to this API in browsers.
196196

197+
## `worker.markAsUncloneable(object)`
198+
199+
<!-- YAML
200+
added: REPLACEME
201+
-->
202+
203+
* `object` {any} Any arbitrary JavaScript value.
204+
205+
Mark an object as not cloneable. If `object` is used as [`message`](#event-message) in
206+
a [`port.postMessage()`][] call, an error is thrown. This is a no-op if `object` is a
207+
primitive value.
208+
209+
This has no effect on `ArrayBuffer`, or any `Buffer` like objects.
210+
211+
This operation cannot be undone.
212+
213+
```js
214+
const { markAsUncloneable } = require('node:worker_threads');
215+
216+
const anyObject = { foo: 'bar' };
217+
markAsUncloneable(anyObject);
218+
const { port1 } = new MessageChannel();
219+
try {
220+
// This will throw an error, because anyObject is not cloneable.
221+
port1.postMessage(anyObject)
222+
} catch (error) {
223+
// error.name === 'DataCloneError'
224+
}
225+
```
226+
227+
There is no equivalent to this API in browsers.
228+
197229
## `worker.moveMessagePortToContext(port, contextifiedSandbox)`
198230

199231
<!-- YAML

lib/internal/worker/io.js

+16
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ const {
2929
oninit: onInitSymbol,
3030
no_message_symbol: noMessageSymbol,
3131
} = internalBinding('symbols');
32+
const {
33+
privateSymbols: {
34+
transfer_mode_private_symbol,
35+
},
36+
constants: {
37+
kCloneable,
38+
},
39+
} = internalBinding('util');
3240
const {
3341
MessagePort,
3442
MessageChannel,
@@ -447,13 +455,21 @@ ObjectDefineProperties(BroadcastChannel.prototype, {
447455
defineEventHandler(BroadcastChannel.prototype, 'message');
448456
defineEventHandler(BroadcastChannel.prototype, 'messageerror');
449457

458+
function markAsUncloneable(obj) {
459+
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) {
460+
return;
461+
}
462+
obj[transfer_mode_private_symbol] &= ~kCloneable;
463+
}
464+
450465
module.exports = {
451466
drainMessagePort,
452467
messageTypes,
453468
kPort,
454469
kIncrementsPortRef,
455470
kWaitingStreams,
456471
kStdioWantsMoreDataCallback,
472+
markAsUncloneable,
457473
moveMessagePortToContext,
458474
MessagePort,
459475
MessageChannel,

lib/worker_threads.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
const {
1414
MessagePort,
1515
MessageChannel,
16+
markAsUncloneable,
1617
moveMessagePortToContext,
1718
receiveMessageOnPort,
1819
BroadcastChannel,
@@ -31,6 +32,7 @@ module.exports = {
3132
isMainThread,
3233
MessagePort,
3334
MessageChannel,
35+
markAsUncloneable,
3436
markAsUntransferable,
3537
isMarkedAsUntransferable,
3638
moveMessagePortToContext,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const { markAsUncloneable } = require('node:worker_threads');
6+
const { mustCall } = require('../common');
7+
8+
const expectedErrorName = 'DataCloneError';
9+
10+
// Uncloneables cannot be cloned during message posting
11+
{
12+
const anyObject = { foo: 'bar' };
13+
markAsUncloneable(anyObject);
14+
const { port1 } = new MessageChannel();
15+
assert.throws(() => port1.postMessage(anyObject), {
16+
constructor: DOMException,
17+
name: expectedErrorName,
18+
code: 25,
19+
}, `Should throw ${expectedErrorName} when posting uncloneables`);
20+
}
21+
22+
// Uncloneables cannot be cloned during structured cloning
23+
{
24+
class MockResponse extends Response {
25+
constructor() {
26+
super();
27+
markAsUncloneable(this);
28+
}
29+
}
30+
structuredClone(MockResponse.prototype);
31+
32+
markAsUncloneable(MockResponse.prototype);
33+
const r = new MockResponse();
34+
assert.throws(() => structuredClone(r), {
35+
constructor: DOMException,
36+
name: expectedErrorName,
37+
code: 25,
38+
}, `Should throw ${expectedErrorName} when cloning uncloneables`);
39+
}
40+
41+
// markAsUncloneable cannot affect ArrayBuffer
42+
{
43+
const pooledBuffer = new ArrayBuffer(8);
44+
const { port1, port2 } = new MessageChannel();
45+
markAsUncloneable(pooledBuffer);
46+
port1.postMessage(pooledBuffer);
47+
port2.on('message', mustCall((value) => {
48+
assert.deepStrictEqual(value, pooledBuffer);
49+
port2.close(mustCall());
50+
}));
51+
}
52+
53+
// markAsUncloneable can affect Node.js built-in object like Blob
54+
{
55+
const cloneableBlob = new Blob();
56+
const { port1, port2 } = new MessageChannel();
57+
port1.postMessage(cloneableBlob);
58+
port2.on('message', mustCall((value) => {
59+
assert.deepStrictEqual(value, cloneableBlob);
60+
port2.close(mustCall());
61+
}));
62+
63+
const uncloneableBlob = new Blob();
64+
markAsUncloneable(uncloneableBlob);
65+
assert.throws(() => port1.postMessage(uncloneableBlob), {
66+
constructor: DOMException,
67+
name: expectedErrorName,
68+
code: 25,
69+
}, `Should throw ${expectedErrorName} when cloning uncloneables`);
70+
}

0 commit comments

Comments
 (0)