Remote calls for extension authors

An extension package can expose methods that code on the other side calls across the boundary between the host and its clients. You mark those methods as Remote, bind them to a service, and the runtime carries calls, streams and cancellation between the two sides. This page covers that model and the rules that keep every call lossless.

The message-level view of Remote calls and streams is on The Oda Remote API. Oda also defines and runs packages in a chat itself, covered by The host half of a dynamic package and The browser half of a dynamic package.

Mark a method as remote

Markers are written in TypeScript on the method itself:

MarkerWhat it declares
@RemoteA plain call, under the method's own name.
@Remote('name')A call under another name than the method's.
@Remote({ mode: 'stream' })A streaming call.
@RemoteScope('context')A contextual call, with an optional second name.

Only a public non-static instance method with a string name and a concrete implementation can carry a marker: private or static methods, methods without an implementation, and generic methods are refused. Two markers on one method fail with Remote method "<method>" has conflicting invocation markers, and a stream marker whose options are not exactly mode: "stream" fails with Remote options must contain exactly mode: "stream".

Parameters are plain named bindings: a rest parameter or a parameter with a default value is refused, so the shape a call carries is exactly what the method declares. A parameter written as T | undefined still counts as required; only an optional parameter may be left out of a call.

Bind the methods to a service

A marked method becomes callable once its owner is bound:

What a call carries

A call names the service it is for, the peer that made it, and a cancellation signal:

Streams

A streaming method is declared with @Remote({ mode: 'stream' }) and returns an Iterable<Out>, an AsyncIterable<Out> or a RemoteStream<Out, In>. On the calling side a streaming call is a handle with send(item), end() and dispose(): send adds one item to the caller's side of the stream, end closes it so no further items follow, and dispose releases the handle when the caller is done.

Values that can cross

All sides share the same lossless value checks, and a value that cannot round-trip unchanged is refused:

Results and failures

A call answers with a result envelope: { ok: true, value } on success, { ok: false, error } on failure.

How calls are named

Questions, answered

Can I mark any method as remote?

No. A marker needs a public non-static instance method with a string name and a concrete implementation; private or static methods, methods without an implementation, and generic methods are refused. One method takes one marker: two markers fail with Remote method "<method>" has conflicting invocation markers.

How does my method learn it was cancelled?

Cooperative cancellation arrives as the injected signal. Declare a final signal: AbortSignal parameter and react to it; the runtime provides it, and it is never sent as a call argument.

Why was my options object refused?

The only accepted options object is { mode: 'stream' }, and its check is exact: Remote options must contain exactly mode: "stream". Use the plain or named marker when the method does not stream.

Where do I read who called, and which service it was?

Inside the method, ctx.invocation names the service, the peer and the signal. Outside a Remote call the same property is undefined, so code shared with non-Remote paths can check it first.

Can a call carry bytes, and what else can a value contain?

Bytes, as Uint8Array, cross only in a single result of a unary call, directly or nested inside it. Everything else is lossless JSON: null, strings, booleans, finite numbers, plain objects and dense arrays. NaN, infinities, -0, class instances, symbol keys, non-enumerable fields, sparse arrays and cycles are refused on every side.

I sent an item before the stream opened. Was it lost?

No. An item the caller sends before the stream has opened is queued, and it is delivered once the stream opens.

What happens if I ask for the uplink twice?

The uplink is available once per call; a second call to uplink() throws. Read it where the method starts to consume the caller's items.

What happens to items the caller never reads?

Leaving a stream loop early cancels the stream, and items that were never read are dropped; they are not delivered later.

How do I tell two failures apart?

By the failure's code, which has the stable form <domain>/<reason>, never by a class check. The universal codes are gateway/bad-request, gateway/cancelled and gateway/internal.

Why was my endpoint name refused?

A segment may contain only letters, digits, _, $, . and -. A lone . or .., or a # inside a name, is refused; the refusal reads must contain only RPC endpoint segment characters.