hunt
hunt
CDCloudflare Developers
Created by hunt on 2/19/2025 in #workers-help
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?
7 replies