Repository navigation
Userland transferable/cloneable objects #37080
Description
@addaleax @nodejs/n-api @nodejs/workers ...
Currently there is no way for userland code to define their own cloneable/transferable objects that work with the MessageChannel/MessagePort API.
For instance,
class Foo {
a = 1;
}
const foo = new Foo();
const mc = new MessageChannel();
mc.port1.onmessage = ({ data }) => console.log(data); // outputs {}
mc.port2.postMessage(foo);The structured clone algorithm takes the Foo object and clones it as an ordinary JavaScript object that has lost information.
With Node.js built in objects we are able to get around this because at the native layer, node::BaseObject is a "host object" in v8 terms, allowing us to delegate serialization and deserialization to our own provider. We use this, for instance, to transfer KeyObject, X509Certificate, and FileHandle objects in a way that just works.
The node::BaseObject base class has a set of APIs that allow an object to declare itself as being cloneable or transferable, along with functions that provide the serialization and deserialization delegates. For JavaScript objects, we provide the JSTransferable base class that derives from node::BaseObject. The JavaScript KeyObject, X509Certificate and FileHandle objects derive from JSTransferable. On deserialization of a cloned or transfered object, JSTransferable will parse information out of the serialized image and use that to require() and create the implementation class on the receiving side. It all just works.
The challenge, however, is that user code cannot use either node::BaseObject or JSTransferable.
On the JavaScript side, JSTransferable is internal only, and is not exposed to userland code. Even if it was exposed to userland code, it wouldn't work because the internal require() function it uses during deserialization only sees the Node.js built in modules and will not resolve userland code. Also, it uses require() and would not work with ESM modules.
On the C++ side, the node-api napi_define_class() does not define the JavaScript/C++ object wrapper in the same way as what we use internally with BaseObject. Specifically, node-api wrapper objects are not seen as host objects by v8 and therefore will use the default structured clone algorithm to clone only the enumerable JavaScript object properties on the wrapper object. Because these objects would need to provide their own serialization/deserialization logic it's not clear if napi_define_class() can be modified in a backwards compatible way such that the objects can be host objects.
What I'd like to be able to achieve is efficiently and easily creating both JavaScript classes and native wrapper objects that can be cloned/transferred intact over MessagePort but it's not clear at all exactly how we should do it.
There is an argument that can be made for Just Using ArrayBuffers(tm). Specifically, have user code serialize it's objects into an ArrayBuffer, then just transfer/copy that ArrayBuffer over the MessagePort and manually deserialize on the other side. That's obviously far less efficient. For comparison, consider how we handle other native objects like KeyObject and X509Certificate. For those, we are able to clone the JavaScript wrapper while avoiding the cost of copying and reallocating the underlying native data structures. The custom serialization/deserialization/copy is far less efficient.
I'm opening this issue to see if we can discuss and identify an approach to implementing this that would work, as the fix is not clear and touches on several subsystems (node-api, messaging, workers, etc).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriaged
- StatusShow more project fieldsDone
Activity
targos commentedon Jan 26, 2021
Should we try to think about what would be the best API from the user's perspective first (without checking if it's implementable at all)?
mhdawson commentedon Jan 26, 2021
@targos that seems like a good starting point and then go from there towards something we can implement.
jasnell commentedon Jan 26, 2021
To start looking at the API, here are examples of how this is implemented currently inside core...
First, from the JavaScript side:
Then, on the C++ side
For the JavaScript-side, from a userland perspective, what I'd like to have is something very similar, except perhaps without actually requiring the
extends JSTransferableinheritance.When an instance of
Foois passed in topostMessage(), it would be fantastic if the fact that it exposes[kClone]and[kDeserialize]functions would have it be recognized as a host object (using aBaseObjectdelegate).For the node-api side, I'd like something similar, using a
napi_define_transferable_class()method to define a wrap object class that is cloneable or transferable, and having that Just Work(tm) with the wrapper class exposing functions similar to GetTransferMode/CloneForMessaging...There would obviously be some details to work through in that but having this should give a start.
jasnell commentedon Jan 26, 2021
On the javascript side, another option is a Symbol based delegate approach...
addaleax commentedon Jan 26, 2021
So … a Symbol-based approach is something that I would consider ideal as well, and I would have picked it for Node.js core if it was technically feasible. However, without support from V8, this is effectively a non-starter – as it stands, the only way to give an object special treatment for transfer is to make it inherit from a C++-backed object.
It’s also not obvious how we would deal with this in terms of how to deserialize – one of the reasons why we only support
require()with internal methods is that those are the only ones that we can assume to be present in all execution contexts without relying on external factors (because userlandrequire()depends on the module from which it was called, or at the very least a specific on-disk layout).On the native side, this is easier from a technical point of view, and providing a C++-style API for this seems like something that would be doable to some degree. For a N-API-based approach, I feel like things become a bit less clear, in particular because N-API uses
napi_envas its main gateway for communicating with the Node.js source code – butnapi_envis bound to a specificv8::Context/v8::Isolate/add-on instance. How one for the target environment would be created seems to be a tricky question (for example: if we automatically were to create anapi_env, how would thenapi_(get|set)_instance_datafor that look like? What would its lifetime be?).There’s also the added difficulty of dealing with addon lifetimes in general – i.e. we’ll need to ensure that the addon isn’t unloaded before the transferred object has reached its target environment.
So, lots of challenges here, and a potentially wide API surface – we could do something here if we want to, but maybe it makes sense to first have a conversation with the V8 team about what we could do in terms of adding Symbol-based transferable support?
jasnell commentedon Jan 26, 2021
+1 to this for sure. /cc @nodejs/v8
jimmywarting commentedon Mar 10, 2021
Don't you quite often extend your class on something else?
Maybe symbol would be a better alternative?
jasnell commentedon Mar 10, 2021
Yes, the inheritance is an issue. A symbol approach would be ideal but won't work until v8 supports it.
marjakh commentedon Mar 11, 2021
Hi from v8,
If I understand this correctly, you'd like to have a special Symbol you can install on an object, and when that object gets transferred, some user-defined JS will be called, and that will then return something transferable; on the other side that transferable is given to your other function for deserialization. But you're not looking into extending transferability e.g., in a way that would allow you to transfer functions and have them magically work. Is that roughly correct?
Extending the V8 API via a Symbol like that sounds possible ( cc @camillobruni for API).
jimmywarting commentedon Mar 11, 2021
I would like for this to work in browser also with postMessages.
There are related work done here:
https://github.com/littledan/serializable-objects
https://github.com/Microsoft/napajs/blob/master/docs/api/transport.md
it's in Deno's Q1 2021 plans denoland/deno#3557 & denoland/deno#9458
i thought napajs sounds cool
Means that you can clone/serialize mostly anything with a json rest API, putting things in indexedDB/localStorage for instances.
marjakh commentedon Mar 11, 2021
Taking a step back, I'll ask a fundamental(ly stupid) question: Since Node implements the MessageChannel.postMessage function, why can't that function inspect the incoming object and see if it has the special Symbol installed? Why would it need to be plugged into V8?
29 remaining items
github-actions commentedon Oct 29, 2025
There has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the never-staleIssues and PRs exempt from automated stale handling.
label or close this issue if it should be closed. If not, the issue will be automatically closed 6 months after the last non-automated comment.
For more information on how the project manages feature requests, please consult the feature request management document.
saico-0ps commentedon Oct 29, 2025
github-actions commentedon Jul 20, 2026
This issue has been marked as stale due to 90 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.
avivkeller commentedon Aug 9, 2026
This issue slipped through the cracks because our previous stale bot only tracked issues and couldn't catch all the issues.
Our new stale bot flagged this, and would have closed it shortly after RenderATL, but I'm just doing it a bit early so
maintainer's can focus on new code-and-learn PRs during the event.
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.