## Q1. Why can't `create-interval` (returning a stream of tick events) be its own component? **Because a component-model stream needs a producer *task*, and a producer task only advances while its own component *instance* has a live async task pumping that instance's event loop. A component that only exposes short-lived exports has no such pump.** Mechanism, step by step: 1. `create-interval` must **return** the stream's readable end to the caller so the caller can subscribe. Returning the reader = the export call **completes**. 2. To keep emitting ticks, something must loop: `await` the clock (`wasi:clocks/monotonic-clock@0.3.0` `wait-until`, an async **host** import), write a tick, repeat. That loop is a **task**. 3. In wit-bindgen 0.58 the only ways to start such a task are: - `spawn_local` — attaches the future to the **current** task; it is dropped when that task ends. - `start_task` — a **new top-level** task; it is torn down when the export invocation that created it returns. 4. Either way, the moment `create-interval` returns (which it must, to hand back the reader), the helpers instance has **no remaining live task**, so the producer is deleted. When the producer's still-pending `wait-until` later completes, wasmtime tries to re-enter the deleted task (`host_task_reenter_caller` → `lower_result_and_exit_call`) and traps with **`resource not present`**. ### Evidence (validated probes) - `tmp/probe2`: helpers **sync** `create() -> stream` + `start_task` producer → `resource not present`. - `tmp/probe2`: helpers `create` made **`async`** + `start_task` → still `resource not present`. - `tmp/probe2`: enabling wit-bindgen `inter-task-wakeup` → error merely changes to `resource is of another type`; still broken. - `tmp/async_probe`: producer moved **into the client's long-lived `run()`** (`spawn_local`), writing a client-local `stream` that a subscriber reads → **works**: `subscriber: got tick 0/1/2`, exit 0. ### The precise rule **A component with no long-lived async export cannot host a background task.** `wasm-helpers` is such a component (it only ever receives short calls: `create-interval` / `wait-tick` / `send-log-message`). The **client** is not — its `run()` ends in `std::future::pending().await` and stays alive, so tasks it spawns keep being polled. Therefore interval timing must be driven from the client instance, one of: - **`wait-tick`** — helpers exposes `wait-tick: async func(ns)`; the client loops `loop { wait_tick(ns).await; runnable(); }`. Simplest; no stream object. - **client-local `create_interval` (static library, same instance as `run()`)** — a linked Rust function that does `let (tx, rx) = wit_stream::new(); spawn_local(producer(tx)); rx`, called from `run()`, returning a stream the client subscribes to. Preserves the `create-interval -> stream` abstraction; validated in `tmp/async_probe`. - Caveat: `wit_stream::new::()` needs a generated per-type stream vtable, which only exists if the client's WIT declares a `stream` somewhere. A purely-internal stream therefore requires a stream to appear in the client world (e.g. a dummy `export …: func() -> stream`), or reuse an existing stream type. This is the one bit of friction that makes `wait-tick` simpler for the immediate fix. Two requirements for the static-library form: 1. `create_interval` must be **called from within the async `run()` context** so `spawn_local` has a current task to attach to (calling it from a sync entry with no active task panics). 2. The client imports the **host clock** (`wasi:clocks/monotonic-clock`) directly, or a thin `wait-tick` from helpers; `wasm-helpers` can still provide **DLT** as a component. --- ## Q2. Would it work if `create-interval` were an async export in a separate component that never completes (always returns pending)? **No.** An always-pending async function never returns, so it can **never hand the stream's readable end back to the caller**. The client's `let rx = create_interval(ns).await;` would block forever and never reach its subscribe loop — a deadlock, with zero ticks delivered. Keeping the helpers task alive is useless if the client can never obtain the reader. The *only* way a separate-component async export could both (a) give the caller the reader and (b) keep producing is the component-model **"return-early-then-continue"** pattern: use the canonical-ABI `task.return` to hand back the reader while the task keeps running. wit-bindgen 0.58's high-level bindings do **not** expose this — an `async fn -> T` *couples* "return the value" with "complete the task": when the function returns the reader, the task is done and the producer stops. There is no supported way in this toolchain to express "return the reader now, keep the producer task alive" from a separate component. **Conclusion:** neither a spawned background task nor an always-pending async export lets a *separate guest component* implement `create-interval`. The producer must live in the client instance (the `wait-tick` loop, or a client-local static-library `create_interval`) — **or in the host** (see Q3, which is the clean way to keep `create-interval` external and swappable).