Worker to Worker communication for TypeScript components
See the Worker to Worker communication page for a general overview of how workers can invoke each other in Golem.
Tooling setup
Make sure that all required tools are installed according to the setup page, including the Worker to Worker communication parts.
Setup with using example RPC TypeScript project
The golem-cli new
command can be used to generate an example project which contains multiple components that are calling each other.
It also has higher level build scripts for adding, connecting, deploying and testing components.
To create such an example use the following command:
$ golem-cli new --example ts-multi-rpc --package-name golem:demo ts-example
The command will create a new Golem project in the ts-example
directory, and print short, language-specific instructions on how to build the project. It also contains documentation in the generated README.md
with more details about managing components and worker to worker communication.
In case you prefer manually setting up your project, or if you want to learn more about how golem-cli stubgen
works, see the next section.
Setup with directly using golem-cli stubgen
A project building two Golem components where one can call the other can be set up with the following primary steps:
Create components
First create two TypeScript components using the techniques described in the defining components page. The two components should be in two separate directories, enclosed by a root directory for the whole project.
It is important to use a different package name for each component, otherwise the stub generator will fail.
This can be done by using the --package-name
parameter of golem-cli new
for example --package-name rpcdemo:component1
and --package-name rpcdemo:component2
.
It is also important to use different exported interface names in the component WIT worlds, otherwise there will be name clashes in the generated bindings.
In this document we will use the golem-cli new
command for creating the components:
$ golem-cli new --example ts-default --package-name rpcdemo:component1 component1
$ golem-cli new --example ts-default --package-name rpcdemo:component2 component2
Then to avoid naming problems, we rename the exported interfaces in both component's wit definition.
component1/wit/component1.wit
:
package rpcdemo:component1;
// Renamed from api to component1-api
interface component1-api {
// ...
}
world component1 {
// ...
// Renamed from api to component1-api
export component1-api;
}
component2/wit/component2.wit
:
package rpcdemo:component2;
// Renamed from api to component2-api
interface component2-api {
// ...
}
world component2 {
// ...
// Renamed from api to component2-api
export component2-api;
}
Updating the main.ts
component implementations is also needed based on the above.
To initialize dependencies, use npm install
in both component's directory.
Generate RPC client stub definition and component
All components that we want to call from another component will require client stub definitions and components.
Let's create one for component2 using golem-cli stubgen build
:
$ golem-cli stubgen build --source-wit-root component2/wit --dest-wasm component2-stub/component2_stub.wasm --dest-wit-root component2-stub/wit
The above command will place the generated WIT definitions and WASM component into the component2-stub
folder:
$ tree component2-stub
component2-stub
βββ component2_stub.wasm // stub component
βββ wit
βββ _stub.wit // stub definition
βββ deps
βββ ...
.
.
.
Add the stub dependency to the caller project
Now we add the generated stub definition to the caller project with the golem-cli stubgen add-stub-dependency
golem-cli stubgen add-stub-dependency --stub-wit-root component2-stub/wit --dest-wit-root component1/wit --overwrite
After this step the stub definitions will be present in the component1/wit/deps
directory:
$ tree component1/wit
component1/wit
βββ component1.wit
βββ deps
.
.
.
βββ rpcdemo_component2 // component2 definition
β βββ component2.wit
βββ rpcdemo_component2-stub // component2 stub definition
β βββ _stub.wit
.
.
.
Import the stub definition in the caller world
To make the stub definition available for binding generation we have to import it in the callers world. To do so we have to
edit the component1/wit/component1.wit
file:
package rpcdemo:component1;
interface api {
// ...
}
world component1 {
// ... other imports
// Add the following import for the client stub:
import rpcdemo:component2-stub/stub-component2;
export api;
}
Regenerate bindings
To actually be able to use the imported stub we regenerate our bindings, which can done in the component1
directory with:
$ npm run stub
# the above will execute: jco stubgen wit -o src/generated
Creating the worker client
The worker to be invoked is identified by an URI which consists of the component ID and the worker name.
If the worker pointed by the URI does not exists, it is going to be created automatically and it inherits the environment variables of the caller.
A common pattern is to use environment variables to configure the deployed component's component ID and use that to construct the URI. See the defining components page for more information about passing configuration values to workers.
The following code snippet gets the target component's id from an environment variable, and constructs the URI for the target worker with a fixed worker name target-worker
:
// Import the generated binding, which includes the remote client stub
import { Component2Api } from "rpcdemo:component2-stub/stub-component2"
// Import getEnvironment for environment variable access
import { getEnvironment } from "wasi:cli/environment@0.2.0"
// We get the component ID from environment variable, because it depends on the deploy
const componentId = (() => {
const envVar = getEnvironment().find(([key]) => key == "COMPONENT2_ID")
if (envVar === undefined) {
throw new Error("Missing COMPONENT2_ID")
}
return envVar[1]
})()
const workerName = "target-worker"
// Create new worker client with an URN constructed from the component ID and worker name
const component2Worker = new Component2Api({ value: `urn:worker:${componentId}/${workerName}` })
Calling worker client functions
For each exported function in the target component, the generated stub contains two exported variants:
- A blocking variant, prefixed with
Blocking
, which does not return until the remote worker finished processing the call. - A non-blocking variant, which returns a pollable result immediately (unless for remote functions without any return value, in which case it just triggers the invocation but does not return anything)
An example blocking call, using the client created in the previous step:
// Example for a blocking call, this will wait until the other worker finished
component2Worker.blockingAdd(BigInt(3))
And a non-blocking one, which assumes two worker clients, and uses the low level WASI poll API:
const future1 = component2Worker1.get()
const future2 = component2Worker2.get()
const poll1 = future1.subscribe()
const poll2 = future1.subscribe()
let futures = [future1, future2]
let polls = [poll1, poll2]
let results: BigInt[] = []
while (polls.length != 0) {
// Poll returns the indices of the finished future poll handles
const ready = poll(polls)
for (let idx of ready) {
const result = futures[idx]?.get()
if (result === undefined) {
throw new Error("Missing result for future")
}
results[idx] = result
futures.splice(idx, 1)
results.splice(idx, 1)
}
}
Composing the stub client component into the caller component
Before we can deploy our built component we also have to compose the stub client's component into the caller's one.
Let's build our component in the component1
directory:
npm run componentize
Then compose the components:
golem-cli stubgen compose --source-wasm out/component1.wasm --stub-wasm ../component2-stub/component2_stub.wasm --dest-wasm component1-with-component2-stub.wasm
component1-with-component2-stub.wasm
is now ready to be deployed to golem.