Eventual send: a uniform async messaging API for local and remote objects.
The @endo/eventual-send package provides the E() proxy for asynchronous
message passing.
Whether an object is in the same vat, a different vat, or across a network,
E() provides a consistent API that always returns promises.
This enables:
- Uniform communication: Same code for local and remote objects
- Promise pipelining: Chain operations without waiting for resolution
- Message ordering: Preserve message order per target
- Future-proof code: Local code works when migrated to distributed systems
Eventual send relies on an Endo environment.
Programs running in an existing Endo platform like an Agoric smart contract or
an Endo plugin do not need to do anything special to set up HardenedJS,
HandledPromise and related shims.
To construct an environment suitable for Eventual Send requires the
HandledPromise shim:
import '@agoric/eventual-send/shim.js';The shim ensures that every instance of Eventual Send can recognize every other instance's handled promises. This is how we mitigate, what we call, "eval twins".
import { E } from '@endo/eventual-send';Eventual send: invoke a method, returning a promise for the result.
import { E } from '@endo/eventual-send';
const counter = makeCounter(10);
// Send message, get promise
const resultP = E(counter).increment(5);
const result = await resultP; // 15
// Works even if counter is a promise
const counterP = Promise.resolve(counter);
const result2 = await E(counterP).increment(3); // 18Key property: Works uniformly whether the target is:
- A local object
- A local promise for an object
- A remote presence in another vat
- A promise for a remote presence
All calls return promises, even for local objects, ensuring consistent async behavior throughout your codebase.
Eventual get: retrieve a property, returning a promise for its value.
const config = harden({
timeout: 5000,
retries: 3
});
const timeoutP = E.get(config).timeout;
const timeout = await timeoutP; // 5000Useful for accessing properties on remote objects or promises.
Fire-and-forget: send a message without waiting for or receiving the result.
Returns undefined immediately.
const logger = makeLogger();
// Send log message, don't wait for result
E.sendOnly(logger).log('Event occurred');
// Continues immediately, logging happens eventuallyWhen to use:
- Don't need the return value
- Want to optimize latency (no promise creation)
- Logging, notifications, fire-and-forget operations
Note: You won't get errors if the method fails.
Use regular E() if you need error handling.
Shorthand for promise handling with turn tracking:
E.when(
E(counter).getValue(),
value => console.log('Value:', value),
error => console.error('Error:', error)
);
// Equivalent to:
E(counter).getValue().then(
value => console.log('Value:', value),
error => console.error('Error:', error)
);Primarily useful in contexts that need explicit turn tracking for debugging.
Convert a value to a handled promise:
const promise = E.resolve(value);
// promise is a HandledPromise wrapping valueUsually not needed directly; E() handles this automatically.
One of the most powerful features is promise pipelining: the ability to send messages to promises before they resolve.
import { E } from '@endo/eventual-send';
// All of these send immediately - no waiting!
const mintP = E(bootstrap).getMint();
const purseP = E(mintP).makePurse();
const paymentP = E(purseP).withdraw(100);
await E(receiverPurse).deposit(100, paymentP);
// Only wait at the end for the final resultWithout pipelining, you'd need to await each step:
// Without pipelining: 4 round trips
const mint = await bootstrap.getMint(); // wait
const purse = await mint.makePurse(); // wait
const payment = await purse.withdraw(100); // wait
await receiverPurse.deposit(100, payment); // wait
// With pipelining: messages sent immediately, only wait at endThis can dramatically reduce latency in distributed systems by eliminating round trips.
How it works:
- Messages to unresolved promises are queued
- When the promise resolves, queued messages are delivered in order
- Each message returns a new promise that resolves when the operation completes
Eventual send provides four key benefits:
The same code works whether the target is local or remote:
// This code works identically whether counter is:
// - A local object
// - In a different vat on the same machine
// - On a different machine across the network
const result = await E(counter).increment(5);Write local code, deploy distributed, no changes needed.
Messages to the same target are delivered and processed in send order:
E(counter).increment(1); // executed first
E(counter).increment(2); // executed second
E(counter).increment(3); // executed third
// Order is guaranteedThis simplifies reasoning about concurrency.
As shown above, eliminates round trips in distributed systems.
Code written with E() works locally today and distributed tomorrow:
// Works in development (local)
const result = await E(service).getData();
// Same code works in production (distributed)
// No changes needed when service moves to another vat/machineExos (from @endo/exo) are the ideal targets for eventual send:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { E } from '@endo/eventual-send';
const CounterI = M.interface('Counter', {
increment: M.call(M.number()).returns(M.number())
});
const counter = makeExo('Counter', CounterI, {
increment(n) {
return count += n;
}
});
// E() provides async wrapper
const resultP = E(counter).increment(5);
// The InterfaceGuard validates n is a number
// Even if counter is remote, validation happens on receiveEven for local exos, using E() provides benefits:
- Consistent async behavior throughout your codebase
- Turn-based execution prevents reentrancy bugs
- Error isolation via promise rejection
- Future-proof code that works when distributed
Under the hood, E() uses HandledPromise, a Promise subclass that supports
handler-based dispatch:
import { HandledPromise } from '@endo/eventual-send';
// HandledPromise extends native Promise
const hp = new HandledPromise((resolve, reject, resolveWithPresence) => {
// Three ways to settle the promise
resolve(value); // Normal resolution
reject(reason); // Rejection
resolveWithPresence(h); // Resolve with a remote presence
}, handler);
// Handler intercepts operations
const handler = {
get(target, prop) { /* ... */ },
applyMethod(target, verb, args) { /* ... */ }
};Most users don't need to use HandledPromise directly.
The E() proxy provides the ergonomic interface.
Use E() even in unit tests for consistency:
import test from 'ava';
import { E } from '@endo/eventual-send';
test('counter increments correctly', async t => {
const counter = makeCounter(0);
// Use E() even though counter is local
const result = await E(counter).increment(5);
t.is(result, 5);
});Benefits:
- Tests mirror production code
- Async behavior is tested
- Easy to mock remote objects
- Same code works for both local and remote targets
- Foundation: @endo/pass-style - What can be sent as arguments
- Validation: @endo/patterns - Describe method signatures with InterfaceGuards
- Defensive Objects: @endo/exo - Exos are ideal targets
for
E() - Network Transport: @endo/captp - Real network communication using CapTP
Complete Tutorial: See Message Passing for a comprehensive guide showing how eventual-send works with pass-style, patterns, and exo to enable safe distributed computing.
This package implements the ECMAScript eventual-send proposal, which provides native language support for eventual send operations.
- ECMAScript eventual-send proposal
- Concurrency Among Strangers - Mark S. Miller's thesis on eventual send
- @endo/captp - Cap'n Proto RPC implementation for network transport