This example demonstrates Chapter 5 of the UMA book: the runtime layer around a pure service. It shows how contracts, adapter binding, deterministic event ordering, and lifecycle metadata work together around a small network-fetching service. The validated reader path is the native Rust cloud host smoke flow, with a TypeScript reference runtime kept in parity for the core lab scenarios. That validated path is hermetic: it resolves a checked-in fixture through the host adapter instead of opening a localhost server.
- Previous: Chapter 4: Feature Flag Evaluator
- Next: Chapter 6: UMA Portability Lab
- pure service logic stays separate from host capabilities
- the runtime selects and records the
network.fetchadapter binding - validation can stop execution before side effects happen
- event ordering and lifecycle metadata make runtime behavior auditable
- Rust 1.77 or newer
cargojqfor the guided lab checks and golden-fixture comparisons- Optional:
npmif you want to inspect the illustrative browser host scaffolding
- Validated path:
./scripts/smoke_runtime_labs.sh - Main implementation: Rust workspace rooted at
Cargo.toml - Secondary implementation: TypeScript reference runtime in
ts/ - Guided reader labs:
./scripts/list_labs.shand./scripts/run_lab.sh - Implementation parity check:
./scripts/compare_impls.sh - Optional paths: browser and edge host sketches remain illustrative, not validated quick-starts
./scripts/list_labs.sh
./scripts/run_lab.sh lab1-cloud-golden-path
./scripts/run_lab.sh lab2-header-validation-fail-fast
./scripts/run_lab.sh lab3-adapter-binding-and-wrappers
./scripts/run_lab.sh lab4-rust-ts-parity
./scripts/smoke_runtime_labs.shExpected log signals:
Integration test passed: output matches golden fixture.
Building native runtime CLI...
Running the service in cloud host via native CLI...
Chapter 5 smoke run completed successfully.
Use this order if you just finished Chapter 5 and want the best learning path:
./scripts/list_labs.sh./scripts/run_lab.sh lab1-cloud-golden-path./scripts/run_lab.sh lab2-header-validation-fail-fast./scripts/run_lab.sh lab3-adapter-binding-and-wrappers./scripts/run_lab.sh lab4-rust-ts-parity
Expected satisfaction point:
- by the end of lab 3, you should be able to explain how the runtime validates input, chooses an adapter implementation, and records that decision without changing the pure service logic
You should leave this lab able to explain:
- what belongs in the service crate versus what belongs in the runtime layer
- why runtime validation should fail fast before side effects happen
- how lifecycle metadata proves which adapter path actually ran
- how a TypeScript reference runtime can model the same Chapter 5 behavior while Rust remains the validated default path
The most important signals are:
- the deterministic event sequence in
output.events - the
network.fetchbinding recorded inlifecycle.bindings - the final
lifecycle.state
You got value from the Chapter 5 lab if you can explain all three of these points after running it:
- the service logic normalized a post, but the runtime owned validation, fetch orchestration, and lifecycle recording
- invalid headers stopped the run before any
fetch_requestevent happened - enabling retry/cache wrappers changed the binding record without changing the normalized service output
contracts/, JSON contracts for the service, runtime policy, adapter capability, and metadata schemaservice/, pure normalization logic and service-facing API typesruntime/, runtime orchestration, adapter binding, event bus, lifecycle record, and native CLI entrypointts/, TypeScript reference runtime kept in parity with the core Chapter 5 scenariosadapters/, capability adapter definitions plus illustrative TS/browser scaffoldinghosts/, cloud and edge host shimstests/, fixtures and integration scriptsscripts/, guided Chapter 5 lab helpers
The service accepts input like this:
{
"request": {
"url": "uma-fixture://sample-post",
"headers": {
"accept": "application/json"
}
},
"runId": "demo-001"
}On success it emits a normalized post plus a deterministic event log:
{
"normalizedPost": {
"id": 1,
"userId": 1,
"title": "<string>",
"body": "<string>"
},
"events": [
{ "t": "0", "type": "start", "data": { "runId": "demo-001" } },
{ "t": "1", "type": "fetch_request", "data": { "url": "<string>" } },
{ "t": "2", "type": "fetch_response", "data": { "status": 200 } },
{ "t": "3", "type": "normalized", "data": { "id": 1 } },
{ "t": "4", "type": "end", "data": {} }
]
}See labs/README.md for the guided Chapter 5 lab notes.
Runs the validated cloud host path and compares the output against the checked-in golden fixture.
Feeds an invalid header into the native CLI path and proves that validation stops the run before any fetch happens.
Enables the retry and cache wrappers and verifies that the runtime binding record changes to cache-retry-host-fetch.
Runs the Rust and TypeScript implementations against the validated Chapter 5 scenarios and compares their summarized runtime behavior.
If you want the lower-level commands instead of the guided labs:
cargo test --locked --workspace
bash hosts/cloud/run.sh
bash tests/integration/run_cloud.shThe runtime depends on one capability, network.fetch, described in adapter.network.contract.json.
The lifecycle record persists which implementation satisfied that capability.
policy.runtime.json documents the intended adapter-selection and observability behavior for the sample. The current runtime does not fully parse this policy file yet; in this example it acts as the declared runtime contract rather than a fully interpreted policy engine.
metadata.schema.json defines the persisted lifecycle record shape, including:
- service identity
- policy reference
- capability bindings
- event log
- final state
- logical clock
The runtime supports a few environment variables for the lab:
| Variable | Description |
|---|---|
UMA_ENABLE_RETRY |
Wraps the selected adapter with RetryAdapter |
UMA_ENABLE_CACHE |
Wraps the selected adapter with CacheAdapter |
UMA_POLICY_PATH |
Not implemented yet; would point the runtime to a custom policy file |
The browser and edge files remain illustrative sketches. They are useful as reference material for where a JS/Wasm binding layer would go, but they are not part of the validated quick-start path.
- tests/integration/run_browser.md explains the browser scaffold
- tests/integration/run_edge.sh fails fast with guidance instead of pretending the edge path is turnkey
cargo test --locked --workspaceverifies the service and runtime cratesnpm test --prefix tsverifies the TypeScript reference runtime./scripts/smoke_runtime_labs.shis the validated Chapter 5 smoke path./scripts/compare_impls.shchecks Rust/TypeScript parity for the core labsbash tests/integration/run_cloud.shcompares the cloud output against the golden fixtureruntime/src/tests.rscovers runtime determinism, fail-fast validation, adapter wrapping, and parse-error handling
- If
jqis missing, the guided labs and golden comparison scripts will fail early with an explicit message. - If you want to explore the browser scaffold, install dependencies under
adapters/network/ts-host/, but treat that path as illustrative rather than validated. - If you see network differences on your machine, stick to the validated
uma-fixture://sample-postflow instead of external endpoints.
- The validated Chapter 5 path is still Rust-first. The TypeScript runtime is a parity/reference implementation for the chapter concepts, while the browser and edge host files remain illustrative sketches around the same runtime model.
- The runtime is deliberately deterministic: no timers or random values influence event ordering.
- The logical clock increments once per emitted event so host behavior is easy to compare.
- Did the labs make the runtime layer responsibilities more obvious than the raw code alone?
- Did the failure-path lab clearly show why validation belongs before adapter execution?
- Did the binding record make the capability indirection feel concrete instead of abstract?
If this hands-on worked, you should finish it with three concrete gains:
- you can point to the exact event sequence that proves the runtime behaved deterministically
- you can explain why invalid headers stop before fetch rather than after it
- you can show where the lifecycle record captures the actual adapter decision the runtime made