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.
Markers are written in TypeScript on the method itself:
| Marker | What it declares |
|---|---|
@Remote | A 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.
A marked method becomes callable once its owner is bound:
1. A descriptor under an unsupported version is refused, and a second binding on the same service is refused too.A call names the service it is for, the peer that made it, and a cancellation signal:
ctx. invocation is the call in progress: it names the service being used, the peer (the caller), and the signal. Outside a Remote call it is undefined.signal: AbortSignal parameter to cooperate with cancellation. The runtime injects the signal; it never travels as a sent argument. React to it to stop your work when the caller goes away.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.
ctx. invocation. uplink(). It is available once per call; asking for it a second time throws.All sides share the same lossless value checks, and a value that cannot round-trip unchanged is refused:
null, strings, booleans, finite numbers (but not -0), plain objects with enumerable fields, and dense arrays.NaN, infinities, -0, class instances and other non-plain objects, symbol keys, non-enumerable fields, sparse arrays, and cycles.Uint8Array) is allowed only in a single result of a unary call, directly or nested inside it. Parameters and stream items stay JSON only.A call answers with a result envelope: { ok: true, value } on success, { ok: false, error } on failure.
RemoteError type carrying a stable code of the form <domain>/ <reason>.gateway/ bad-request when the call does not satisfy the endpoint, and it can list the issues that describe what was wrong; gateway/ cancelled when the caller aborted; and gateway/ internal for a failure the layer could not classify.code, never by an instance check on the error's class.<namespace>/ <method>; a registered invocation also carries a full id of the form <package>#<namespace>/ <method>._, $, . and -. A lone . or . . is refused, and a # inside a name is refused because # separates the package part of an identity. A name outside those characters is refused with must contain only RPC endpoint segment characters.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.
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.
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.
Inside the method, ctx. 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.
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.
No. An item the caller sends before the stream has opened is queued, and it is delivered once the stream opens.
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.
Leaving a stream loop early cancels the stream, and items that were never read are dropped; they are not delivered later.
By the failure's code, which has the stable form <domain>/, never by a class check. The universal codes are gateway/, gateway/ and gateway/.
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.