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
Status
Awaiting Triage
Activity