Welcome to the new Golem Cloud Docs! πŸ‘‹
Documentation
Experimental Languages
JavaScript
Worker to Worker Communication

Worker to Worker communication for JavaScript 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 project with golem-cli

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 JavaScript 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 js-default --package-name rpcdemo:component1 component1
$ golem-cli new --example js-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.js 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;
}

Creating the worker client

The worker to be invoked is identified by an URI which consists of the component ID and, in case of durable workers, the worker name. For ephemeral components no worker name needs to be specified.

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 module 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}` })

When calling ephemeral workers, only the component ID is needed in the URI:

const component2Worker = new Component2Api({ value: `urn:worker:${componentId}` })

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 = []
 
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.