Returning an RpcTarget from a WorkerEntrypoint shows `never` for method return types

This goes off the back of a question I asked in another channel. (if you scroll down there's some more context you can piece together) To give the TL;DR, I need to expose the methods on a durable object through an entrypoint so that I can call them from a different worker. A really neat way to do that is to define an RpcTarget on the durable object which then gets returned through a method defined in the entrypoint. Here's the repro i've been working on This works if you're fine ignoring types. If you do care about types (which I do), there's a problem with the signature of the function when calling the DO's rpc target from the consuming worker. If you try and pass anything more complex than a scalar value from the rpc target and then again through the entrypoint, it'll set the return type to never on the consuming worker which isn't desired (i.e. instead of () => { foo: string }, it returns () => never example) I figured out that it's because the Disposable type that gets attached to the return object on the function when you're pulling the target off of the DO isn't serializable, so when you try and return it from the entrypoint. I can confirm this is the case by adding Disposable as a type to Rpc.Serializable in the workers-types declaration file which gives me a return type that looks closer to what i'm expecting:
complexReturnType: () => Promise<{
a: string;
b: string;
[Symbol.dispose]: Rpc.Stub<() => void>;
} & Disposable> & {
a: Promise<string>;
b: Promise<string>;
}
complexReturnType: () => Promise<{
a: string;
b: string;
[Symbol.dispose]: Rpc.Stub<() => void>;
} & Disposable> & {
a: Promise<string>;
b: Promise<string>;
}
...but why would there be a need to serialize Disposable if the stub is just being passed onto the consuming worker? the way the types are setup would indicate that when returning the rpc target from the DO, a target is being pinned to both the DO and the parent worker
// 1. this is how the consuming worker sees the type. outer stub is the stub returned by the worker entrypoint, the inner one is the one returned by the DO
Rpc.Stub<Rpc.Stub<{ getComplexType: () => { foo: string } }>>
// 2. this is the "actualized type" after computing the inner Rpc.Stub
Rpc.Stub<{
complexReturnType: () => Promise<{
a: string;
b: string;
} & Disposable> & {
...;
}
}>
// 3. uh oh, the Disposable in the return type isn't serializable, so we should reject the return type
{ getComplexType: () => never }
// 1. this is how the consuming worker sees the type. outer stub is the stub returned by the worker entrypoint, the inner one is the one returned by the DO
Rpc.Stub<Rpc.Stub<{ getComplexType: () => { foo: string } }>>
// 2. this is the "actualized type" after computing the inner Rpc.Stub
Rpc.Stub<{
complexReturnType: () => Promise<{
a: string;
b: string;
} & Disposable> & {
...;
}
}>
// 3. uh oh, the Disposable in the return type isn't serializable, so we should reject the return type
{ getComplexType: () => never }
so a couple of questions come to mind: (1) is it something wrong with the way I'm passing the target back from the worker entrypoint? (2) does this mean that two targets are being created and upheld when accessing something on the target? (3) would a change to the types package where there's a check so it wont try and "re-stubify" objects it already knows are stubs be inline with how stubs are orchestrated?
GitHub
GitHub - hntrl/durable-object-rpc-repro at target-chaining-type-error
Contribute to hntrl/durable-object-rpc-repro development by creating an account on GitHub.
GitHub
durable-object-rpc-repro/packages/workerB/src/worker.ts at b110e2d3...
Contribute to hntrl/durable-object-rpc-repro development by creating an account on GitHub.
6 Replies
1984 Ford Laser
Very interesting stuff here, will follow developments. Not sure if this at all helps: https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#disposers-and-rpctarget-classes
Cloudflare Docs
Workers RPC — Lifecycle · Cloudflare Workers docs
Memory management, resource management, and the lifecycle of RPC stubs.
1984 Ford Laser
And this higher in that page
No description
Elliot Hesp
Elliot Hesp3d ago
Yeah this is a known problem and super annoying. Related issue https://github.com/cloudflare/workerd/issues/3042
GitHub
RPC Response type not compatible with Worker · Issue #3042 · clou...
Given the following worker code: export default { async fetch(request, env, ctx): Promise<Response> { const id = env.MY_DO.idFromName('foo'); const stub = env.MY_DO.get(id); const foo...
Elliot Hesp
Elliot Hesp3d ago
My solution is to always return a simple function with basic types which internally calls the complex and functions
hunt
huntOP2d ago
oh interesting. I would've assumed that stub types would take on a different form for the reserved methods like fetch, as in it skips using RPC entirely since the signature of stub.fetch and DurableObject.fetch isn't one to one (there's some transformations in between that cloudflare takes). Regardless it's still an issue if you return a response object from a non-fetch RPC method https://developers.cloudflare.com/durable-objects/api/base/
Cloudflare Docs
Durable Object Base Class · Cloudflare Durable Objects docs
The DurableObject base class is an abstract class which all Durable Objects inherit from. This base class provides a set of optional methods, frequently referred to as handler methods, which can respond to events, for example a webSocketMessage when using the WebSocket Hibernation API. To provide a concrete example, here is a Durable Object MyDu...
hunt
huntOP2d ago
Maybe complex types is a bad way to describe what i'm returning -- the issue i'm running into is if the object i'm returning is anything but a simple value like a string or a number (like any key-value object { foo: "bar" }) then it isn't serializable.

Did you find this page helpful?