---
url: /guide/forwarding.md
---
# Forwarding

`forward(commands, send)` registers commands whose handlers run in another process. Each forwarder
sends the command's name and args through `send` and returns the outcome that comes back. The app
supplies `send`: a fetch, an IPC call, a socket message.

The two processes share the commands' descriptions, in a file that holds data only:

```ts
import type { Commands } from '@ufo-soft/caelus'

export const commands = {
  increment: { description: 'Add one', parameters: [] },
  decrement: { description: 'Subtract one', parameters: [] },
  reset: { description: 'Back to zero', parameters: [] },
} satisfies Commands

```

## The sending process

```ts
import { Registry, failed, forward } from '@ufo-soft/caelus'
import type { Send } from '@ufo-soft/caelus'

import { commands } from './descriptions.js'

const send: Send = async (name, args) => {
  try {
    const response = await fetch('http://localhost:3000/invoke', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ name, args }),
    })
    return response.ok ? await response.json() : failed(`${response.status} from the server`)
  } catch (error) {
    return failed(error instanceof Error ? error.message : String(error))
  }
}

const registry = new Registry()
registry.register(forward(commands, send))
```

`send` has the type `Send`: `(name, args) => Promise<Outcome>`. A transport failure should come back
as a `failed` outcome, as above. A `send` that throws rejects the promise `invoke` returns.

Any surface works on a registry of forwarders. As a CLI:

```ts
import { cli } from '@ufo-soft/caelus-cli'

process.exitCode = await cli(registry, process.argv.slice(2), 'counter')
```

```txt
$ counter increment
1
$ counter increment
2
$ counter decrement
1
```

With the server down, the forwarder's `failed` outcome becomes the CLI's error:

```txt
$ counter increment
fetch failed
```

## The receiving process

The other process binds handlers to the same descriptions and invokes its own registry with what
arrives:

```ts
import { createServer } from 'node:http'
import { text } from 'node:stream/consumers'
import { Registry, bind, failed, ok } from '@ufo-soft/caelus'

import { commands } from './descriptions.js'

let count = 0
const registry = new Registry()
registry.register(
  bind(commands, {
    increment: () => ok(++count),
    decrement: () => ok(--count),
    reset: () => ok((count = 0)),
  }),
)

// POST /invoke with { name, args }; the reply is the outcome as JSON
createServer(async (request, response) => {
  if (request.method !== 'POST' || request.url !== '/invoke') return void response.writeHead(404).end()
  const body = await text(request)
    .then(JSON.parse)
    .catch(() => null)
  const outcome =
    typeof body?.name === 'string'
      ? await registry.invoke(body.name, body.args ?? {})
      : failed('expected a JSON body { name, args }')
  response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(outcome))
}).listen(3000)

```

The receiving end is a trust boundary. Check the shape of what arrives before invoking, as above.
The registry checks only that the name exists: the handlers check their own args.

An outcome is plain data, so it crosses the boundary as JSON. Data a handler returns has to survive
that trip: no functions, class instances, or `Date` objects that the other side expects back as
they were.

## Mixing local and forwarded commands

One registry can hold both. A page can forward its data commands to a server and register a
command that only touches the page, such as clearing a log, with a local handler. Every surface sees
one list of commands either way.

See [the examples](../architecture/examples.md) for forwarding over Electron IPC and over a REST
client.
