- name
- resonate-basic-ephemeral-world-usage-typescript
- description
- Core patterns for using Resonate Client APIs in the Ephemeral World - initialization, registration, top-level invocations, promise management, and dependency injection. Covers connecting to a Resonate Server, running on Postgres with no server process (PostgresNetwork), and token authentication - the constructor takes `token`, not `auth`, and silently ignores unknown options. Use this for application entry points and orchestration code outside of durable functions.
- license
- Apache-2.0
# Resonate Basic Ephemeral World Usage
> **SDK version:** This skill reflects `@resonatehq/sdk` v0.11.4 (current on npm).
>
> **Two execution engines (v0.11.0+):** The SDK now ships two engines. This skill documents the **generator engine** (imported from `@resonatehq/sdk`), which uses `function*` / `yield*` and is the basis of all existing Resonate examples. An **async/await engine** (imported from `@resonatehq/sdk/async`) was added in v0.11.0 and is documented in `resonate-async-await-engine-typescript`.
## Overview
The **Ephemeral World** is where your application code lives that is NOT inside generator functions. This is where you:
- Initialize the Resonate Client
- Register functions
- Start top-level executions
- Manage promises externally
- Set dependencies
**Key distinction:** Ephemeral World code uses `resonate` (the Client). Durable World code uses `ctx` (the Context).
## Mental Model
```
┌─────────────────────────────────────────┐
│ EPHEMERAL WORLD (Stateless) │
│ │
│ • Resonate Client initialization │
│ • Function registration │
│ • Top-level run/rpc calls │
│ • Promise management (create/resolve) │
│ • Dependency injection │
│ │
│ Uses: resonate.run(), resonate.rpc() │
└─────────────────────────────────────────┘
↓ invokes ↓
┌─────────────────────────────────────────┐
│ DURABLE WORLD (Stateful) │
│ │
│ • Generator functions with yield* │
│ • Context APIs for sub-invocations │
│ • Durable coordination and recovery │
│ │
│ Uses: ctx.run(), ctx.rpc() │
└─────────────────────────────────────────┘
```
## Core Rule
**You CANNOT use Context APIs in the ephemeral world, and you CANNOT use Client APIs in durable functions.**
## Initialization Patterns
### Local Development Mode (Zero Dependencies)
```ts
import { Resonate } from "@resonatehq/sdk";
// No URL = local in-memory mode
const resonate = new Resonate();
```
**Key behaviors:**
- In-memory storage (no persistence across restarts)
- No external dependencies required
- Perfect for testing and development
- **CRITICAL:** When using `new Resonate()` without a URL, function arguments are wrapped in an array
### Connected to Resonate Server
```ts
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate({
url: "http://localhost:8001",
group: "workers",
token: process.env.RESONATE_TOKEN, // JWT bearer token, if the server is secured
});
```
**When to use:**
- Multiple worker processes
- Persistence across restarts
- Distributed execution
- Production deployments
> **There is no basic-auth option on current releases.** The constructor accepts `token` (a JWT bearer token) and nothing else auth-related — there is no `auth`, `username`, or `password` field, and no `RESONATE_USERNAME` / `RESONATE_PASSWORD` environment variable. Passing an unknown option does **not** throw, so credentials handed to a field that does not exist are silently dropped and the client sends unauthenticated requests.
>
> Basic auth did exist through `0.9.6`, where `auth: { username, password }` sent an `Authorization: Basic` header that the server of that era validated. The `0.10.0` networking rewrite removed it with no deprecation warning. If you are upgrading from `0.9.x`, a working `auth` block does not fail loudly — it stops authenticating.
### Postgres Instead of a Server
The SDK can also run durable execution directly on Postgres, with no Resonate Server process. Apply the `resonate.sql` schema from [resonate-pg](https://github.com/resonatehq/resonate-pg) to a Postgres 16+ database, install the `pg` peer dependency, and pass a `PostgresNetwork` as the `network` option:
```ts
import { Resonate } from "@resonatehq/sdk";
import { PostgresNetwork } from "@resonatehq/sdk/postgres";
const network = new PostgresNetwork({
connectionString: process.env.DATABASE_URL ?? "postgres://localhost:5432/mydb",
group: "workers",
});
const resonate = new Resonate({ network });
```
Works with both engines — pass the same `network` to the `Resonate` class from `@resonatehq/sdk/async`.
**Key behavior to know:** with no server process, due timers are advanced by the workers themselves. Durable sleeps and timeouts progress only while at least one worker is connected; with every worker down they suspend until one reconnects.
### Environment Variables
```bash
export RESONATE_URL="http://localhost:8001"
export RESONATE_TOKEN="<jwt>"
```
```ts
const resonate = new Resonate(); // Picks up env vars automatically
```
**Resolution order:**
1. Constructor arguments (highest priority)
2. Environment variables
3. Built-in defaults (local mode)
## Registration Patterns
### Correct: Register with Function Reference
```ts
function* myWorkflow(ctx: Context, arg: string) {
return `Hello ${arg}`;
}
// ✅ CORRECT
const stub = resonate.register(myWorkflow);
```
### Wrong: Custom String Names
```ts
// ❌ WRONG - Don't use custom string names
resonate.register("my_workflow", myWorkflow);
// ❌ WRONG - Don't use snake_case
resonate.register("db_get_user", dbGetUser);
```
**Why this matters:**
- Resonate uses function names for routing
- Custom names break RPC resolution
- Snake_case violates naming conventions
### Using the Returned Stub
```ts
const workflowStub = resonate.register(myWorkflow);
// Later, invoke directly via stub
const result = await workflowStub.run("execution-1", "World");
```
## Top-Level Invocation Patterns
### Run (Local Execution)
```ts
// Blocks until result is ready
const result = await resonate.run(
"execution-id",
myWorkflow,
"arg1",
"arg2"
);
```
**Use when:**
- Running in the same process
- Need the result immediately
### Begin Run (Non-Blocking Local)
```ts
// Returns immediately with handle
const handle = await resonate.beginRun(
"execution-id",
myWorkflow,
"arg1"
);
// Do other work...
// Get result later
const result = await handle.result();
```
**Use when:**
- Starting work but not waiting immediately
- Running multiple executions concurrently
### RPC (Remote Execution)
```ts
// Blocks until remote execution completes
const result = await resonate.rpc(
"execution-id",
"myWorkflow", // String name!
"arg1",
resonate.options({
target: "poll://any@workers"
})
);
```
**Critical differences from run:**
- Uses string function name (not function reference)
- Requires target specification
- Executes in different process/group
### Begin RPC (Non-Blocking Remote)
```ts
const handle = await resonate.beginRpc(
"execution-id",
"myWorkflow",
"arg1",
resonate.options({
target: "poll://any@workers"
})
);
const result = await handle.result();
```
## Options Pattern
```ts
await resonate.run(
"execution-id",
myWorkflow,
"arg1",
resonate.options({
timeout: 60_000, // 60 seconds in ms
tags: { userId: "123" },
version: 1
})
);
```
**Available options:**
- `timeout`: Max execution time in milliseconds
- `tags`: Metadata for filtering/searching
- `version`: Function version for schema evolution
- `target`: RPC routing (poll://any@group-name)
## Promise Management
### Create Promise
```ts
await resonate.promises.create(
"approval-123",
Date.now() + 30000 // timeout 30s from now
);
```
### Get Promise
```ts
const promise = await resonate.promises.get("approval-123");
```
### Resolve Promise (Human-in-the-Loop)
```ts
// Elsewhere (webhook, UI, CLI):
await resonate.promises.resolve("approval-123", {
data: JSON.stringify({
approved: true,
approver: "alice@example.com",
}),
});
```
### Reject Promise
```ts
await resonate.promises.reject("approval-123", {
data: JSON.stringify({
reason: "Insufficient funds",
}),
});
```
### Cancel Promise
```ts
await resonate.promises.cancel("approval-123");
```
> **Migration note (v0.10.0 → v0.10.1+):** The single `resonate.promises.settle(id, state, value)` method was split into `resolve`, `reject`, and `cancel` in v0.10.1. The old `settle()` is private in v0.10.2 and remains private in v0.11.4. Update any code that calls `.settle()` directly.
## Dependency Injection
### Set Dependencies
```ts
import { createClient } from "@supabase/supabase-js";
const db = createClient(url, key);
// Set in ephemeral world
resonate.setDependency("db", db);
resonate.setDependency("config", { apiKey: "..." });
```
**Rules:**
- Only set dependencies in ephemeral world
- Set before registering functions that use them
- Pass non-serializable objects (DB connections, etc.)
### Access in Durable World
```ts
function* myWorkflow(ctx: Context, userId: string) {
// Get in durable world
const db = ctx.getDependency("db");
const user = yield* ctx.run(async (_ctx, id) => {
return await db.from("users").select().eq("id", id).single();
}, userId);
return user;
}
```
## Scheduling
```ts
const schedule = await resonate.schedule(
"daily-report",
"0 8 * * *", // Every day at 8am
generateReport,
"arg1"
);
// Later, delete the schedule
await schedule.delete();
```
## Subscription (Get Existing Execution)
```ts
// Subscribe to existing execution
const handle = await resonate.get("execution-id");
// Wait for result
const result = await handle.result();
// Check if done
const isDone = await handle.done();
```
**Use when:**
- Checking status of long-running executions
- Reconnecting to executions after restart
- Monitoring from external systems
## Common Pitfalls
### 1. Mixing Ephemeral and Durable APIs
```ts
// ❌ WRONG - Using ctx in ephemeral world
async function main() {
const result = await ctx.run(myFunc); // ctx doesn't exist here!
}
// ✅ CORRECT
async function main() {
const result = await resonate.run("id", myFunc);
}
```
### 2. Using Client APIs Inside Generators
```ts
// ❌ WRONG
function* myWorkflow(ctx: Context) {
const result = await resonate.run("id", otherFunc); // Wrong API!
}
// ✅ CORRECT
function* myWorkflow(ctx: Context) {
const result = yield* ctx.run(otherFunc);
}
```
### 3. Missing Target on RPC
```ts
// ❌ WRONG - RPC needs target
await resonate.rpc("id", "funcName", "arg");
// ✅ CORRECT
await resonate.rpc(
"id",
"funcName",
"arg",
resonate.options({ target: "poll://any@workers" })
);
```
### 4. Forgetting In-Memory Mode Wraps Args in Array
```ts
// In-memory mode (no URL)
const resonate = new Resonate();
function* workflow(ctx: Context, state: MyState) {
// ❌ WRONG - state will be [MyState], not MyState!
console.log(state.someField); // undefined!
}
// ✅ CORRECT - Handle array wrapping in local mode
function* workflow(ctx: Context, state: any) {
const actualState = Array.isArray(state) ? state[0] : state;
console.log(actualState.someField); // Works!
}
```
### 5. Using Async Instead of Generators
```ts
// ❌ WRONG - Durable functions must be generators
async function* workflow(ctx: Context) { }
// ✅ CORRECT
function* workflow(ctx: Context) { }
```
## Complete Example
```ts
import "dotenv/config";
import { Resonate, type Context } from "@resonatehq/sdk";
// Initialize client (ephemeral world)
const resonate = new Resonate({
url: process.env.RESONATE_URL,
group: "workers"
});
// Set dependencies (ephemeral world)
resonate.setDependency("apiKey", process.env.API_KEY);
// Register functions (ephemeral world)
const workflowStub = resonate.register(myWorkflow);
// Start execution (ephemeral world)
async function main() {
const handle = await resonate.beginRun(
"workflow-1",
myWorkflow,
{ userId: "123" }
);
console.log("Started:", handle.id);
const result = await handle.result();
console.log("Result:", result);
View on GitHub