Skip to content

Commit deab33a

Browse files
Update async order docs to show concurrent execution pattern
- Update CLAUDE.md and README.md with improved async order handling examples showing how to return unresolved Promises for parallel execution instead of awaiting sequentially - Add tests for the documented patterns: mixed order types with type-based dispatch and error handling for unknown order types Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7994fc1 commit deab33a

File tree

3 files changed

+222
-32
lines changed

3 files changed

+222
-32
lines changed

CLAUDE.md

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -583,23 +583,32 @@ function fetch(url: string): Promise<any> {
583583
const data = await fetch("/api/users");
584584
```
585585

586-
JavaScript handles these orders and fulfills them:
586+
JavaScript handles orders by returning unresolved Promises immediately, enabling parallel async operations:
587587

588588
```javascript
589-
async function handleOrders(orders) {
590-
const responses = [];
591-
for (const order of orders) {
592-
const { id, payload } = order;
593-
// payload contains { type: "fetch", url: "..." }
594-
595-
// Simulate async operation with setTimeout
596-
await new Promise(r => setTimeout(r, 100));
597-
598-
// Mock response based on payload
599-
const result = { data: "mock" };
600-
responses.push({ id, result });
589+
// Handle orders by returning unresolved Promises immediately.
590+
// This enables parallel async operations - each order gets a Promise that
591+
// resolves after async work, but execution continues immediately.
592+
function handleOrders(orderIds) {
593+
for (const orderId of orderIds) {
594+
const payload = runner.get_order_payload(orderId);
595+
596+
// Create unresolved Promise and fulfill order immediately
597+
const promiseHandle = runner.create_promise();
598+
runner.set_order_result(orderId, promiseHandle);
599+
600+
// Schedule resolution based on order type (runs concurrently!)
601+
if (payload.type === "fetch") {
602+
fetch(payload.url)
603+
.then(r => r.json())
604+
.then(data => runner.resolve_promise(promiseHandle, toHandle(data)));
605+
} else if (payload.type === "timeout") {
606+
setTimeout(() => runner.resolve_promise(promiseHandle, undefined), payload.ms);
607+
} else {
608+
runner.reject_promise(promiseHandle, `Unknown type: ${payload.type}`);
609+
}
601610
}
602-
return responses;
611+
// Return immediately - don't wait for async operations
603612
}
604613
```
605614

README.md

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,17 @@ assert_eq!(joined.as_str(), Some("admin, developer"));
219219

220220
### Async/Await with Orders
221221

222-
For async operations, the interpreter pauses with pending "orders" that the host fulfills:
222+
For async operations, the interpreter pauses with pending "orders" that the host fulfills. The host examines the order payload to determine what operation to perform:
223223

224224
```rust
225-
use tsrun::{Interpreter, StepResult, OrderResponse, RuntimeValue, api};
225+
use tsrun::{Interpreter, StepResult, OrderResponse, RuntimeValue, JsError, api};
226226

227227
let mut interp = Interpreter::new();
228228

229229
// Code that uses the order system for async I/O
230230
interp.prepare(r#"
231231
import { order } from "tsrun:host";
232-
const response = await order({ url: "/api/users" });
232+
const response = await order({ type: "fetch", url: "/api/users" });
233233
response.data
234234
"#, Some("/main.ts".into()))?;
235235

@@ -239,14 +239,33 @@ loop {
239239
StepResult::Suspended { pending, cancelled } => {
240240
let mut responses = Vec::new();
241241
for order in pending {
242-
// Examine order.payload to determine what to do
243-
// Create response value
244-
let response = api::create_response_object(&mut interp, &serde_json::json!({
245-
"data": [{"id": 1, "name": "Alice"}]
246-
}))?;
242+
// Extract order type from payload to dispatch
243+
let order_type = api::get_property(order.payload.value(), "type")
244+
.ok()
245+
.and_then(|v| v.as_str().map(String::from));
246+
247+
let result = match order_type.as_deref() {
248+
Some("fetch") => {
249+
let url = api::get_property(order.payload.value(), "url")
250+
.ok()
251+
.and_then(|v| v.as_str().map(String::from))
252+
.unwrap_or_default();
253+
// In real code: perform actual HTTP fetch here
254+
api::create_response_object(&mut interp, &serde_json::json!({
255+
"data": [{"id": 1, "name": "Alice"}],
256+
"url": url
257+
}))
258+
}
259+
Some("timeout") => {
260+
// In real code: sleep for the requested duration
261+
Ok(RuntimeValue::unguarded(tsrun::JsValue::Undefined))
262+
}
263+
_ => Err(JsError::type_error("Unknown order type")),
264+
};
265+
247266
responses.push(OrderResponse {
248267
id: order.id,
249-
result: Ok(response),
268+
result,
250269
});
251270
}
252271
interp.fulfill_orders(responses);
@@ -381,15 +400,32 @@ const [user, posts] = await Promise.all([
381400
```javascript
382401
// In the step loop, handle STEP_SUSPENDED status:
383402
if (result.status === STEP_SUSPENDED()) {
384-
const orders = runner.get_pending_orders();
385-
// orders = [{ id: 1, payload: { type: "fetch", url: "/api/users/1" } }, ...]
386-
387-
const responses = await Promise.all(orders.map(async order => {
388-
const data = await realFetch(order.payload.url);
389-
return { id: order.id, result: data };
390-
}));
391-
392-
runner.fulfill_orders(responses);
403+
const orderIds = runner.get_pending_order_ids();
404+
405+
for (const orderId of orderIds) {
406+
// Read payload now (before fulfilling)
407+
const payload = runner.get_order_payload(orderId);
408+
409+
// Create an unresolved Promise - enables concurrent execution
410+
const promiseHandle = runner.create_promise();
411+
412+
// Fulfill the order immediately with this Promise
413+
runner.set_order_result(orderId, promiseHandle);
414+
415+
// Schedule async work based on order type - all run concurrently!
416+
if (payload.type === "fetch") {
417+
fetch(payload.url)
418+
.then(r => r.json())
419+
.then(data => runner.resolve_promise(promiseHandle, toHandle(data)));
420+
} else if (payload.type === "timeout") {
421+
setTimeout(() => {
422+
runner.resolve_promise(promiseHandle, runner.create_undefined());
423+
}, payload.ms);
424+
} else {
425+
runner.reject_promise(promiseHandle, `Unknown order type: ${payload.type}`);
426+
}
427+
}
428+
runner.commit_fulfillments();
393429
}
394430
```
395431

tests/interpreter/orders.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2193,3 +2193,148 @@ fn test_three_way_race_cancels_two_losers() {
21932193
cancelled
21942194
);
21952195
}
2196+
2197+
// ═══════════════════════════════════════════════════════════════════════════════
2198+
// Documentation Example Pattern Tests
2199+
// These tests demonstrate the patterns shown in README.md and CLAUDE.md:
2200+
// - Multiple order types with type-based dispatch
2201+
// - Returning unresolved Promises for concurrent execution
2202+
// - Error handling for unknown order types
2203+
// ═══════════════════════════════════════════════════════════════════════════════
2204+
2205+
#[test]
2206+
fn test_concurrent_mixed_order_types() {
2207+
// Demonstrates the pattern from documentation examples:
2208+
// 1. Multiple order types (fetch, timeout)
2209+
// 2. Host checks payload.type to dispatch
2210+
// 3. Returns unresolved Promises for concurrent execution
2211+
let mut interp = create_test_interp();
2212+
2213+
// Create Promises upfront for concurrent resolution
2214+
let fetch_promise = api::create_promise(&mut interp);
2215+
let timeout_promise = api::create_promise(&mut interp);
2216+
2217+
let result = run_with_globals(
2218+
&mut interp,
2219+
r#"
2220+
import { order } from "tsrun:host";
2221+
2222+
// Issue two orders with different types
2223+
const fetchPromise = order({ type: "fetch", url: "/api/users" });
2224+
const timeoutPromise = order({ type: "timeout", ms: 100 });
2225+
2226+
// Wait for both concurrently
2227+
const [data, _] = await Promise.all([fetchPromise, timeoutPromise]);
2228+
data.name;
2229+
"#,
2230+
);
2231+
2232+
// First order: fetch
2233+
let StepResult::Suspended { pending, .. } = result else {
2234+
panic!("Expected Suspended for fetch order");
2235+
};
2236+
assert_eq!(pending.len(), 1);
2237+
2238+
// Host checks type to dispatch
2239+
let payload = pending[0].payload.value();
2240+
let order_type = get_string_prop(payload, "type");
2241+
assert_eq!(order_type, Some("fetch".into()));
2242+
2243+
// Return unresolved Promise immediately
2244+
interp.fulfill_orders(vec![OrderResponse {
2245+
id: pending[0].id,
2246+
result: Ok(RuntimeValue::unguarded(fetch_promise.value().clone())),
2247+
}]);
2248+
let result = run_to_completion(&mut interp).unwrap();
2249+
2250+
// Second order: timeout
2251+
let StepResult::Suspended { pending, .. } = result else {
2252+
panic!("Expected Suspended for timeout order");
2253+
};
2254+
assert_eq!(pending.len(), 1);
2255+
2256+
// Host checks type to dispatch
2257+
let payload = pending[0].payload.value();
2258+
let order_type = get_string_prop(payload, "type");
2259+
assert_eq!(order_type, Some("timeout".into()));
2260+
let ms = get_number_prop(payload, "ms");
2261+
assert_eq!(ms, Some(100.0));
2262+
2263+
// Return unresolved Promise immediately
2264+
interp.fulfill_orders(vec![OrderResponse {
2265+
id: pending[0].id,
2266+
result: Ok(RuntimeValue::unguarded(timeout_promise.value().clone())),
2267+
}]);
2268+
let result = run_to_completion(&mut interp).unwrap();
2269+
2270+
// Now awaiting Promise.all - host can resolve concurrently
2271+
let StepResult::Suspended { pending, .. } = result else {
2272+
panic!("Expected Suspended for Promise.all");
2273+
};
2274+
assert!(pending.is_empty());
2275+
2276+
// Simulate concurrent resolution (timeout resolves first, then fetch)
2277+
api::resolve_promise(
2278+
&mut interp,
2279+
&timeout_promise,
2280+
RuntimeValue::unguarded(JsValue::Undefined),
2281+
)
2282+
.unwrap();
2283+
2284+
let fetch_data = api::create_response_object(&mut interp, &json!({ "name": "Alice" })).unwrap();
2285+
api::resolve_promise(&mut interp, &fetch_promise, fetch_data).unwrap();
2286+
2287+
let result = run_to_completion(&mut interp).unwrap();
2288+
2289+
let StepResult::Complete(value) = result else {
2290+
panic!("Expected Complete");
2291+
};
2292+
assert_eq!(*value, JsValue::String("Alice".into()));
2293+
}
2294+
2295+
#[test]
2296+
fn test_unknown_order_type_rejection() {
2297+
// Test that unknown order types can be rejected by the host
2298+
let mut interp = create_test_interp();
2299+
2300+
let result = run_with_globals(
2301+
&mut interp,
2302+
r#"
2303+
import { order } from "tsrun:host";
2304+
2305+
try {
2306+
const result = await order({ type: "unknown_type" });
2307+
"success: " + result;
2308+
} catch (e) {
2309+
"error: " + e;
2310+
}
2311+
"#,
2312+
);
2313+
2314+
let StepResult::Suspended { pending, .. } = result else {
2315+
panic!("Expected Suspended");
2316+
};
2317+
2318+
// Host checks type - unknown type, return error
2319+
let order_type = get_string_prop(pending[0].payload.value(), "type");
2320+
assert_eq!(order_type, Some("unknown_type".into()));
2321+
2322+
// Reject with error for unknown type
2323+
interp.fulfill_orders(vec![OrderResponse {
2324+
id: pending[0].id,
2325+
result: Err(tsrun::JsError::type_error(
2326+
"Unknown order type: unknown_type",
2327+
)),
2328+
}]);
2329+
let result = run_to_completion(&mut interp).unwrap();
2330+
2331+
let StepResult::Complete(value) = result else {
2332+
panic!("Expected Complete with error");
2333+
};
2334+
let result_str = value.as_str().expect("Expected string result");
2335+
assert!(
2336+
result_str.contains("Unknown order type"),
2337+
"Expected error message, got: {}",
2338+
result_str
2339+
);
2340+
}

0 commit comments

Comments
 (0)