From a56ae1476e98b5c92c79f88e0e6e1b198b15a358 Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Mon, 21 Apr 2025 12:56:34 -0400 Subject: [PATCH 1/2] Raise RPC limit --- .../docs/workers/runtime-apis/rpc/index.mdx | 178 ++++++++++++------ .../workers/service-binding-rpc-example.mdx | 27 +-- .../service-binding-rpc-functions-example.mdx | 49 ++--- 3 files changed, 161 insertions(+), 93 deletions(-) diff --git a/src/content/docs/workers/runtime-apis/rpc/index.mdx b/src/content/docs/workers/runtime-apis/rpc/index.mdx index 67d00eab82f53a..746367cc23c361 100644 --- a/src/content/docs/workers/runtime-apis/rpc/index.mdx +++ b/src/content/docs/workers/runtime-apis/rpc/index.mdx @@ -4,10 +4,15 @@ title: Remote-procedure call (RPC) head: [] description: The built-in, JavaScript-native RPC system built into Workers and Durable Objects. - --- -import { DirectoryListing, Render, Stream, WranglerConfig } from "~/components" +import { + DirectoryListing, + Render, + Stream, + WranglerConfig, + TypeScriptExample, +} from "~/components"; :::note To use RPC, [define a compatibility date](/workers/configuration/compatibility-dates) of `2024-04-03` or higher, or include `rpc` in your [compatibility flags](/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). @@ -15,8 +20,8 @@ To use RPC, [define a compatibility date](/workers/configuration/compatibility-d Workers provide a built-in, JavaScript-native [RPC (Remote Procedure Call)](https://en.wikipedia.org/wiki/Remote_procedure_call) system, allowing you to: -* Define public methods on your Worker that can be called by other Workers on the same Cloudflare account, via [Service Bindings](/workers/runtime-apis/bindings/service-bindings/rpc) -* Define public methods on [Durable Objects](/durable-objects) that can be called by other workers on the same Cloudflare account that declare a binding to it. +- Define public methods on your Worker that can be called by other Workers on the same Cloudflare account, via [Service Bindings](/workers/runtime-apis/bindings/service-bindings/rpc) +- Define public methods on [Durable Objects](/durable-objects) that can be called by other workers on the same Cloudflare account that declare a binding to it. The RPC system is designed to feel as similar as possible to calling a JavaScript function in the same Worker. In most cases, you should be able to write code in the same way you would if everything was in a single Worker. @@ -42,11 +47,11 @@ As an exception to Structured Clone, application-defined classes (or objects wit The RPC system also supports a number of types that are not Structured Cloneable, including: -* Functions, which are replaced by stubs that call back to the sender. -* Application-defined classes that extend `RpcTarget`, which are similarly replaced by stubs. -* [ReadableStream](/workers/runtime-apis/streams/readablestream/) and [WriteableStream](/workers/runtime-apis/streams/writablestream/), with automatic streaming flow control. -* [Request](/workers/runtime-apis/request/) and [Response](/workers/runtime-apis/response/), for conveniently representing HTTP messages. -* RPC stubs themselves, even if the stub was received from a third Worker. +- Functions, which are replaced by stubs that call back to the sender. +- Application-defined classes that extend `RpcTarget`, which are similarly replaced by stubs. +- [ReadableStream](/workers/runtime-apis/streams/readablestream/) and [WriteableStream](/workers/runtime-apis/streams/writablestream/), with automatic streaming flow control. +- [Request](/workers/runtime-apis/request/) and [Response](/workers/runtime-apis/response/), for conveniently representing HTTP messages. +- RPC stubs themselves, even if the stub was received from a third Worker. ## Functions @@ -77,38 +82,40 @@ main = "./src/counter.js" -```js + + +```ts import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers"; class Counter extends RpcTarget { - #value = 0; + #value = 0; - increment(amount) { - this.#value += amount; - return this.#value; - } + increment(amount: number) { + this.#value += amount; + return this.#value; + } - get value() { - return this.#value; - } + get value() { + return this.#value; + } } export class CounterService extends WorkerEntrypoint { - async newCounter() { - return new Counter(); - } + async newCounter() { + return new Counter(); + } } export default { - fetch() { - return new Response("ok") - } -} + fetch() { + return new Response("ok"); + }, +}; ``` -The method `increment` can be called directly by the client, as can the public property `value`: - + +The method `increment` can be called directly by the client, as can the public property `value`: @@ -122,22 +129,26 @@ services = [ -```js + + +```ts export default { - async fetch(request, env) { - using counter = await env.COUNTER_SERVICE.newCounter(); + async fetch(request: Request, env: Env) { + using counter = await env.COUNTER_SERVICE.newCounter(); - await counter.increment(2); // returns 2 - await counter.increment(1); // returns 3 - await counter.increment(-5); // returns -2 + await counter.increment(2); // returns 2 + await counter.increment(1); // returns 3 + await counter.increment(-5); // returns -2 - const count = await counter.value; // returns -2 + const count = await counter.value; // returns -2 - return new Response(count); - } -} + return new Response(count); + }, +}; ``` + + :::note Refer to [Explicit Resource Management](/workers/runtime-apis/rpc/lifecycle) to learn more about the `using` declaration shown in the example above. @@ -161,59 +172,75 @@ Classes which do not inherit `RpcTarget` cannot be sent over RPC at all. This di When you call an RPC method and get back an object, it's common to immediately call a method on the object: -```js + + +```ts // Two round trips. using counter = await env.COUNTER_SERVICE.getCounter(); await counter.increment(); ``` + + But consider the case where the Worker service that you are calling may be far away across the network, as in the case of [Smart Placement](/workers/runtime-apis/bindings/service-bindings/#smart-placement) or [Durable Objects](/durable-objects). The code above makes two round trips, once when calling `getCounter()`, and again when calling `.increment()`. We'd like to avoid this. With most RPC systems, the only way to avoid the problem would be to combine the two calls into a single "batch" call, perhaps called `getCounterAndIncrement()`. However, this makes the interface worse. You wouldn't design a local interface this way. Workers RPC allows a different approach: You can simply omit the first `await`: -```js + + +```ts // Only one round trip! Note the missing `await`. using promiseForCounter = env.COUNTER_SERVICE.getCounter(); await promiseForCounter.increment(); ``` + + In this code, `getCounter()` returns a promise for a counter. Normally, the only thing you would do with a promise is `await` it. However, Workers RPC promises are special: they also allow you to initiate speculative calls on the future result of the promise. These calls are sent to the server immediately, without waiting for the initial call to complete. Thus, multiple chained calls can be completed in a single round trip. How does this work? The promise returned by an RPC is not a real JavaScript `Promise`. Instead, it is a custom ["Thenable"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables). It has a `.then()` method like `Promise`, which allows it to be used in all the places where you'd use a normal `Promise`. For instance, you can `await` it. But, in addition to that, an RPC promise also acts like a stub. Calling any method name on the promise forms a speculative call on the promise's eventual result. This is known as "promise pipelining". This works when calling properties of objects returned by RPC methods as well. For example: -```js + + +```ts import { WorkerEntrypoint } from "cloudflare:workers"; export class MyService extends WorkerEntrypoint { - async foo() { - return { - bar: { - baz: () => "qux" - } - } - } + async foo() { + return { + bar: { + baz: () => "qux", + }, + }; + } } ``` -```js + + + + +```ts export default { - async fetch(request, env) { - using foo = env.MY_SERVICE.foo(); - let baz = await foo.bar.baz(); - return new Response(baz); - } -} + async fetch(request, env) { + using foo = env.MY_SERVICE.foo(); + let baz = await foo.bar.baz(); + return new Response(baz); + }, +}; ``` + + If the initial RPC ends up throwing an exception, then any pipelined calls will also fail with the same exception ## ReadableStream, WriteableStream, Request and Response -You can send and receive [`ReadableStream`](/workers/runtime-apis/streams/readablestream/), [`WriteableStream`](/workers/runtime-apis/streams/writablestream/), [`Request`](/workers/runtime-apis/request/), and [`Response`](/workers/runtime-apis/response/) using RPC methods. When doing so, bytes in the body are automatically streamed with appropriate flow control. +You can send and receive [`ReadableStream`](/workers/runtime-apis/streams/readablestream/), [`WriteableStream`](/workers/runtime-apis/streams/writablestream/), [`Request`](/workers/runtime-apis/request/), and [`Response`](/workers/runtime-apis/response/) using RPC methods. When doing so, bytes in the body are automatically streamed with appropriate flow control. This allows you to send messages over RPC which are larger than [the typical 32 MiB limit](#limitations). Only [byte-oriented streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_byte_streams) (streams with an underlying byte source of `type: "bytes"`) are supported. @@ -223,11 +250,15 @@ In all cases, ownership of the stream is transferred to the recipient. The sende A stub received over RPC from one Worker can be forwarded over RPC to another Worker. -```js + + +```ts using counter = env.COUNTER_SERVICE.getCounter(); await env.ANOTHER_SERVICE.useCounter(counter); ``` + + Here, three different workers are involved: 1. The calling Worker (we'll call this the "introducer") @@ -244,7 +275,11 @@ Currently, this proxying only lasts until the end of the Workers' execution cont In this video, we explore how Cloudflare Workers support Remote Procedure Calls (RPC) to simplify communication between Workers. Learn how to implement RPC in your JavaScript applications and build serverless solutions with ease. Whether you're managing microservices or optimizing web architecture, this tutorial will show you how to quickly set up and use Cloudflare Workers for RPC calls. By the end of this video, you'll understand how to call functions between Workers, pass functions as arguments, and implement user authentication with Cloudflare Workers. - + ## More Details @@ -252,5 +287,30 @@ In this video, we explore how Cloudflare Workers support Remote Procedure Calls ## Limitations -* [Smart Placement](/workers/configuration/smart-placement/) is currently ignored when making RPC calls. If Smart Placement is enabled for Worker A, and Worker B declares a [Service Binding](/workers/runtime-apis/bindings) to it, when Worker B calls Worker A via RPC, Worker A will run locally, on the same machine. -* The maximum serialized RPC limit is 1 MB. Consider using [`ReadableStream`](/workers/runtime-apis/streams/readablestream/) when returning more data. \ No newline at end of file +- [Smart Placement](/workers/configuration/smart-placement/) is currently ignored when making RPC calls. If Smart Placement is enabled for Worker A, and Worker B declares a [Service Binding](/workers/runtime-apis/bindings) to it, when Worker B calls Worker A via RPC, Worker A will run locally, on the same machine. + +- The maximum serialized RPC limit is 32 MiB. Consider using [`ReadableStream`](/workers/runtime-apis/streams/readablestream/) when returning more data. + + + + ```ts + export class MyService extends WorkerEntrypoint { + async foo() { + // Although this works, it puts a lot of memory pressure on the isolate. + // If possible, streaming the data from its original source is much preferred and would yield better performance. + // If you must buffer the data into memory, consider chunking it into smaller pieces if possible. + + const sizeInBytes = 33 * 1024 * 1024; // 33 MiB + const arr = new Uint8Array(sizeInBytes); + + return new ReadableStream({ + start(controller) { + controller.enqueue(arr); + controller.close(); + }, + }); + } + } + ``` + + diff --git a/src/content/partials/workers/service-binding-rpc-example.mdx b/src/content/partials/workers/service-binding-rpc-example.mdx index b5088e2c512149..2dd828f2018eb9 100644 --- a/src/content/partials/workers/service-binding-rpc-example.mdx +++ b/src/content/partials/workers/service-binding-rpc-example.mdx @@ -1,9 +1,8 @@ --- {} - --- -import { WranglerConfig } from "~/components"; +import { WranglerConfig, TypeScriptExample } from "~/components"; For example, if Worker B implements the public method `add(a, b)`: @@ -16,19 +15,25 @@ main = "./src/workerB.js" + + ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { - async fetch() { return new Response("Hello from Worker B"); } + async fetch() { + return new Response("Hello from Worker B"); + } - add(a, b) { return a + b; } + add(a: number, b: number) { + return a + b; + } } ``` -Worker A can declare a [binding](/workers/runtime-apis/bindings) to Worker B: - + +Worker A can declare a [binding](/workers/runtime-apis/bindings) to Worker B: @@ -46,9 +51,9 @@ Making it possible for Worker A to call the `add()` method from Worker B: ```js export default { - async fetch(request, env) { - const result = await env.WORKER_B.add(1, 2); - return new Response(result); - } -} + async fetch(request, env) { + const result = await env.WORKER_B.add(1, 2); + return new Response(result); + }, +}; ``` diff --git a/src/content/partials/workers/service-binding-rpc-functions-example.mdx b/src/content/partials/workers/service-binding-rpc-functions-example.mdx index 109309d78a7aa4..9f3f2c6982c5f2 100644 --- a/src/content/partials/workers/service-binding-rpc-functions-example.mdx +++ b/src/content/partials/workers/service-binding-rpc-functions-example.mdx @@ -1,9 +1,8 @@ --- {} - --- -import { WranglerConfig } from "~/components"; +import { WranglerConfig, TypeScriptExample } from "~/components"; Consider the following two Workers, connected via a [Service Binding](/workers/runtime-apis/bindings/service-bindings/rpc). The counter service provides the RPC method `newCounter()`, which returns a function: @@ -16,26 +15,28 @@ main = "./src/counterService.js" + + ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { - async fetch() { return new Response("Hello from counter-service"); } - - async newCounter() { - let value = 0; - return (increment = 0) => { - value += increment; - return value; - } - } + async fetch() { + return new Response("Hello from counter-service"); + } + + async newCounter() { + let value = 0; + return (increment = 0) => { + value += increment; + return value; + }; + } } ``` This function can then be called by the client Worker: - - ```toml @@ -48,19 +49,21 @@ services = [ -```js +```ts export default { - async fetch(request, env) { - using f = await env.COUNTER_SERVICE.newCounter(); - await f(2); // returns 2 - await f(1); // returns 3 - const count = await f(-5); // returns -2 - - return new Response(count); - } -} + async fetch(request: Request, env: Env) { + using f = await env.COUNTER_SERVICE.newCounter(); + await f(2); // returns 2 + await f(1); // returns 3 + const count = await f(-5); // returns -2 + + return new Response(count); + }, +}; ``` + + :::note Refer to [Explicit Resource Management](/workers/runtime-apis/rpc/lifecycle) to learn more about the `using` declaration shown in the example above. From 062ce41c7ba661ba4d4ee9e5694cd683219977ab Mon Sep 17 00:00:00 2001 From: Greg Brimble Date: Fri, 25 Apr 2025 10:38:09 -0400 Subject: [PATCH 2/2] Fix TypeScriptExample --- .../partials/workers/service-binding-rpc-example.mdx | 8 ++++++-- .../workers/service-binding-rpc-functions-example.mdx | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/content/partials/workers/service-binding-rpc-example.mdx b/src/content/partials/workers/service-binding-rpc-example.mdx index 2dd828f2018eb9..809a72f0de229c 100644 --- a/src/content/partials/workers/service-binding-rpc-example.mdx +++ b/src/content/partials/workers/service-binding-rpc-example.mdx @@ -17,7 +17,7 @@ main = "./src/workerB.js" -```js +```ts import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { @@ -49,7 +49,9 @@ services = [ Making it possible for Worker A to call the `add()` method from Worker B: -```js + + +```ts export default { async fetch(request, env) { const result = await env.WORKER_B.add(1, 2); @@ -57,3 +59,5 @@ export default { }, }; ``` + + diff --git a/src/content/partials/workers/service-binding-rpc-functions-example.mdx b/src/content/partials/workers/service-binding-rpc-functions-example.mdx index 9f3f2c6982c5f2..47f03e31cee615 100644 --- a/src/content/partials/workers/service-binding-rpc-functions-example.mdx +++ b/src/content/partials/workers/service-binding-rpc-functions-example.mdx @@ -17,7 +17,7 @@ main = "./src/counterService.js" -```js +```ts import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { @@ -35,6 +35,8 @@ export default class extends WorkerEntrypoint { } ``` + + This function can then be called by the client Worker: @@ -49,6 +51,8 @@ services = [ + + ```ts export default { async fetch(request: Request, env: Env) {