File
Blob: src/workerd/api/export-loopback.h
| 1 | // Copyright (c) 2017-2025 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #pragma once |
| 6 | |
| 7 | #include "actor.h" |
| 8 | #include "http.h" |
| 9 | |
| 10 | #include <workerd/io/io-channels.h> |
| 11 | |
| 12 | namespace workerd::api { |
| 13 | |
| 14 | // LoopbackServiceStub is the type of a property of `ctx.exports` which points back at a stateless |
| 15 | // (non-actor) entrypoint of this Worker. It can be used as a regular Fetcher to make calls to that |
| 16 | // entrypoint with empty props. It can also be invoked as a function in order to specialize it with |
| 17 | // props and make it available for RPC. |
| 18 | class LoopbackServiceStub: public Fetcher { |
| 19 | public: |
| 20 | // Loopback services are always represented by numbered subrequest channels. |
| 21 | explicit LoopbackServiceStub(uint channel) |
| 22 | : Fetcher(channel, RequiresHostAndProtocol::YES, /*isInHouse=*/true), |
| 23 | channel(channel) {} |
| 24 | |
| 25 | struct Options { |
| 26 | jsg::Optional<jsg::JsRef<jsg::JsObject>> props; |
| 27 | |
| 28 | JSG_STRUCT(props); |
| 29 | }; |
| 30 | |
| 31 | struct OptionsWithVersion { |
| 32 | struct Version { |
| 33 | jsg::Optional<kj::Maybe<kj::String>> cohort; |
| 34 | |
| 35 | JSG_STRUCT(cohort); |
| 36 | }; |
| 37 | |
| 38 | jsg::Optional<jsg::JsRef<jsg::JsObject>> props; |
| 39 | jsg::Optional<Version> version; |
| 40 | |
| 41 | JSG_STRUCT(props, version); |
| 42 | }; |
| 43 | |
| 44 | // Create a specialized Fetcher which can be passed over RPC. |
| 45 | jsg::Ref<Fetcher> callImpl(jsg::Lock& js, |
| 46 | jsg::Optional<jsg::JsRef<jsg::JsObject>> propsMaybe, |
| 47 | jsg::Optional<OptionsWithVersion::Version> versionMaybe); |
| 48 | |
| 49 | jsg::Ref<Fetcher> call(jsg::Lock& js, Options options) { |
| 50 | return callImpl(js, kj::mv(options.props), kj::none); |
| 51 | } |
| 52 | |
| 53 | jsg::Ref<Fetcher> callWithVersion(jsg::Lock& js, OptionsWithVersion options) { |
| 54 | return callImpl(js, kj::mv(options.props), kj::mv(options.version)); |
| 55 | } |
| 56 | |
| 57 | // Note that `LoopbackServiceStub` is intentionally NOT serializable, unlike its parent class |
| 58 | // Fetcher. We want people to explicitly specialize the entrypoint with props before sending |
| 59 | // it off to other services. |
| 60 | |
| 61 | JSG_RESOURCE_TYPE(LoopbackServiceStub, CompatibilityFlags::Reader flags) { |
| 62 | JSG_INHERIT(Fetcher); |
| 63 | |
| 64 | if (flags.getEnableVersionApi()) { |
| 65 | JSG_CALLABLE(callWithVersion); |
| 66 | } else { |
| 67 | JSG_CALLABLE(call); |
| 68 | } |
| 69 | |
| 70 | JSG_TS_ROOT(); |
| 71 | |
| 72 | if (flags.getEnableVersionApi()) { |
| 73 | JSG_TS_OVERRIDE( |
| 74 | type LoopbackServiceStub< |
| 75 | T extends Rpc.WorkerEntrypointBranded | undefined = undefined |
| 76 | > = Fetcher<T> & |
| 77 | ( T extends CloudflareWorkersModule.WorkerEntrypoint<any, infer Props> |
| 78 | ? (opts: {props?: Props, version?: { cohort?: string | null }}) => Fetcher<T> |
| 79 | : (opts: {props?: any, version?: { cohort?: string | null }}) => Fetcher<T>); |
| 80 | ); |
| 81 | } else { |
| 82 | JSG_TS_OVERRIDE( |
| 83 | type LoopbackServiceStub< |
| 84 | T extends Rpc.WorkerEntrypointBranded | undefined = undefined |
| 85 | > = Fetcher<T> & |
| 86 | ( T extends CloudflareWorkersModule.WorkerEntrypoint<any, infer Props> |
| 87 | ? (opts: {props?: Props}) => Fetcher<T> |
| 88 | : (opts: {props?: any}) => Fetcher<T>); |
| 89 | ); |
| 90 | } |
| 91 | |
| 92 | // LoopbackForExport takes the type of an exported value and evaluates to the appropriate |
| 93 | // loopback stub for that export. |
| 94 | JSG_TS_DEFINE( |
| 95 | type LoopbackForExport< |
| 96 | T extends |
| 97 | | (new (...args: any[]) => Rpc.EntrypointBranded) |
| 98 | | ExportedHandler<any, any, any> |
| 99 | | undefined = undefined |
| 100 | > = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? LoopbackServiceStub<InstanceType<T>> |
| 101 | : T extends new (...args: any[]) => Rpc.DurableObjectBranded ? LoopbackDurableObjectClass<InstanceType<T>> |
| 102 | : T extends ExportedHandler<any, any, any> ? LoopbackServiceStub<undefined> |
| 103 | : undefined; |
| 104 | ); |
| 105 | } |
| 106 | |
| 107 | private: |
| 108 | uint channel; |
| 109 | }; |
| 110 | |
| 111 | // Similar to LoopbackServiceStub, but for actor classes. |
| 112 | // |
| 113 | // Specifically, this is used for actor classes that do *not* have any storage configured. If you |
| 114 | // simply export a class extending `DurableObject` but you don't configure storage for it, it shows |
| 115 | // up in `ctx.exports` as this type. This can be used to create a Durable Object facet. |
| 116 | class LoopbackDurableObjectClass: public DurableObjectClass { |
| 117 | public: |
| 118 | LoopbackDurableObjectClass(uint channel): DurableObjectClass(channel), channel(channel) {} |
| 119 | |
| 120 | struct Options { |
| 121 | jsg::Optional<jsg::JsRef<jsg::JsObject>> props; |
| 122 | |
| 123 | JSG_STRUCT(props); |
| 124 | }; |
| 125 | |
| 126 | // Create a specialized DurableObjectClass which can be passed over RPC. |
| 127 | jsg::Ref<DurableObjectClass> call(jsg::Lock& js, Options options); |
| 128 | |
| 129 | JSG_RESOURCE_TYPE(LoopbackDurableObjectClass) { |
| 130 | JSG_INHERIT(DurableObjectClass); |
| 131 | JSG_CALLABLE(call); |
| 132 | |
| 133 | JSG_TS_ROOT(); |
| 134 | JSG_TS_OVERRIDE( |
| 135 | type LoopbackDurableObjectClass< |
| 136 | T extends |
| 137 | | Rpc.DurableObjectBranded |
| 138 | | undefined = undefined |
| 139 | > = DurableObjectClass<T> & |
| 140 | ( T extends CloudflareWorkersModule.DurableObject<any, infer Props> |
| 141 | ? (opts: {props?: Props}) => DurableObjectClass<T> |
| 142 | : (opts: {props?: any}) => DurableObjectClass<T>); |
| 143 | ); |
| 144 | } |
| 145 | |
| 146 | private: |
| 147 | uint channel; |
| 148 | }; |
| 149 | |
| 150 | // LoopbackDurableObjectNamespace is similar to LoopbackDurableObjectClass, but used when the |
| 151 | // class has storage configured. In this case, we want a binding that behaves *both* like a |
| 152 | // LoopbackDurableObjectClass *and* like a DurableObjectNamespace binding. Easy enough, we'll |
| 153 | // inherit DurableObjectNamespace, but also make the binding invokable as a function like |
| 154 | // LoopbackDurableObjectClass. |
| 155 | class LoopbackDurableObjectNamespace: public DurableObjectNamespace { |
| 156 | public: |
| 157 | LoopbackDurableObjectNamespace(uint nsChannel, |
| 158 | kj::Own<ActorIdFactory> idFactory, |
| 159 | jsg::Ref<LoopbackDurableObjectClass> loopbackClass) |
| 160 | : DurableObjectNamespace(nsChannel, kj::mv(idFactory)), |
| 161 | loopbackClass(kj::mv(loopbackClass)) {} |
| 162 | |
| 163 | // getClass() accessor for use from C++ only. |
| 164 | LoopbackDurableObjectClass& getClass() { |
| 165 | return *loopbackClass.get(); |
| 166 | } |
| 167 | |
| 168 | // Invoking the binding creates a specialization of the class -- not the namespace. |
| 169 | jsg::Ref<DurableObjectClass> call(jsg::Lock& js, LoopbackDurableObjectClass::Options options) { |
| 170 | return loopbackClass->call(js, kj::mv(options)); |
| 171 | } |
| 172 | |
| 173 | // If `DurableObjectNamespace` ever becomes serializable, we actually don't want to block |
| 174 | // serialization here, the way we want to for `LoopbackDurableObjectClass`, because actually |
| 175 | // serializing the loopback namespace would mean serializing the namespace stub, *not* the |
| 176 | // class stub. They are different things, and you might want to serialize either one. |
| 177 | |
| 178 | JSG_RESOURCE_TYPE(LoopbackDurableObjectNamespace) { |
| 179 | JSG_INHERIT(DurableObjectNamespace); |
| 180 | JSG_CALLABLE(call); |
| 181 | } |
| 182 | |
| 183 | private: |
| 184 | jsg::Ref<LoopbackDurableObjectClass> loopbackClass; |
| 185 | }; |
| 186 | |
| 187 | // Like LoopbackDurableObjectNamespace, but for colo-local (ephemeral) actor namespaces. |
| 188 | class LoopbackColoLocalActorNamespace: public ColoLocalActorNamespace { |
| 189 | public: |
| 190 | LoopbackColoLocalActorNamespace( |
| 191 | uint nsChannel, jsg::Ref<LoopbackDurableObjectClass> loopbackClass) |
| 192 | : ColoLocalActorNamespace(nsChannel), |
| 193 | loopbackClass(kj::mv(loopbackClass)) {} |
| 194 | |
| 195 | // getClass() accessor for use from C++ only. |
| 196 | LoopbackDurableObjectClass& getClass() { |
| 197 | return *loopbackClass.get(); |
| 198 | } |
| 199 | |
| 200 | // Invoking the binding creates a specialization of the class -- not the namespace. |
| 201 | jsg::Ref<DurableObjectClass> call(jsg::Lock& js, LoopbackDurableObjectClass::Options options) { |
| 202 | return loopbackClass->call(js, kj::mv(options)); |
| 203 | } |
| 204 | |
| 205 | JSG_RESOURCE_TYPE(LoopbackColoLocalActorNamespace) { |
| 206 | JSG_INHERIT(ColoLocalActorNamespace); |
| 207 | JSG_CALLABLE(call); |
| 208 | } |
| 209 | |
| 210 | private: |
| 211 | jsg::Ref<LoopbackDurableObjectClass> loopbackClass; |
| 212 | }; |
| 213 | |
| 214 | #define EW_EXPORT_LOOPBACK_ISOLATE_TYPES \ |
| 215 | api::LoopbackServiceStub, api::LoopbackServiceStub::Options, \ |
| 216 | api::LoopbackServiceStub::OptionsWithVersion, \ |
| 217 | api::LoopbackServiceStub::OptionsWithVersion::Version, api::LoopbackDurableObjectClass, \ |
| 218 | api::LoopbackDurableObjectClass::Options, api::LoopbackDurableObjectNamespace, \ |
| 219 | api::LoopbackColoLocalActorNamespace |
| 220 | |
| 221 | } // namespace workerd::api |