Skip to content

Latest commit

 

History

History
332 lines (239 loc) · 8.97 KB

File metadata and controls

332 lines (239 loc) · 8.97 KB

@endo/eventual-send

Eventual send: a uniform async messaging API for local and remote objects.

Overview

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

Shim

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".

Importing

import { E } from '@endo/eventual-send';

Core API

E(target).method(...args)

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);  // 18

Key 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.

E.get(target).property

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;  // 5000

Useful for accessing properties on remote objects or promises.

E.sendOnly(target).method(...args)

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 eventually

When 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.

E.when(promiseOrValue, onFulfilled?, onRejected?)

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.

E.resolve(value)

Convert a value to a handled promise:

const promise = E.resolve(value);
// promise is a HandledPromise wrapping value

Usually not needed directly; E() handles this automatically.

Promise Pipelining

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 result

Without 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 end

This 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

Why Eventual Send?

Eventual send provides four key benefits:

1. Uniform API

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.

2. Message Ordering

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 guaranteed

This simplifies reasoning about concurrency.

3. Pipeline Optimization

As shown above, eliminates round trips in distributed systems.

4. Future-Proof Code

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/machine

Integration with Exo

Exos (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 receive

Even 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

HandledPromise

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 in Tests

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

Integration with Endo Packages

  • 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.

Background

This package implements the ECMAScript eventual-send proposal, which provides native language support for eventual send operations.

See Also