Skip to content

Add platform abstraction layer with traits and RuntimeServices#545

Open
prk-Jr wants to merge 9 commits intomainfrom
feature/edgezero-pr2-platform-traits
Open

Add platform abstraction layer with traits and RuntimeServices#545
prk-Jr wants to merge 9 commits intomainfrom
feature/edgezero-pr2-platform-traits

Conversation

@prk-Jr
Copy link
Collaborator

@prk-Jr prk-Jr commented Mar 20, 2026

Summary

  • Introduces trusted-server-core::platform module with six platform-neutral traits (PlatformConfigStore, PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient, PlatformGeo) enabling future non-Fastly adapter support
  • Defines ClientInfo (extract-once plain data struct), PlatformError enum, and RuntimeServices aggregate that threads all platform services into route_request
  • Moves GeoInfo to platform/types as platform-neutral data and adds a geo_from_fastly mapping function; wires full Fastly implementations in the adapter crate

Note: trusted-server-core still depends on the fastly crate directly (via geo.rs, backend.rs, fastly_storage.rs). These are legacy call-sites that will be migrated in follow-up PRs as part of #483. The new platform/ module is intentionally incremental.

Changes

File Change
crates/trusted-server-core/src/platform/mod.rs New module: RuntimeServices, re-exports, object-safety assertions, unit tests
crates/trusted-server-core/src/platform/traits.rs New: six platform traits with async_trait + WASM-conditional ?Send
crates/trusted-server-core/src/platform/types.rs New: ClientInfo, GeoInfo (moved here from geo.rs); service fields pub(crate) with public getters and new() constructor
crates/trusted-server-core/src/platform/error.rs New: PlatformError enum
crates/trusted-server-core/src/platform/http.rs New: PlatformHttpClient request/response types
crates/trusted-server-core/src/geo.rs Refactored: delegates to platform/types, adds geo_from_fastly
crates/trusted-server-core/src/backend.rs Updated to use PlatformBackend trait
crates/trusted-server-core/src/fastly_storage.rs Adds PlatformKvStore / PlatformConfigStore impl wiring
crates/trusted-server-adapter-fastly/src/platform.rs New: Fastly implementations of all platform traits; uses try_open/try_get throughout
crates/trusted-server-adapter-fastly/src/main.rs Constructs RuntimeServices, threads into route_request; KV store failure degrades gracefully
crates/trusted-server-adapter-fastly/Cargo.toml Adds async-trait, futures dependencies
crates/trusted-server-core/Cargo.toml Adds async-trait dependency

Closes

Closes #483

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

prk-Jr added 5 commits March 18, 2026 16:54
Rename crates/common → crates/trusted-server-core and crates/fastly →
crates/trusted-server-adapter-fastly following the EdgeZero naming
convention. Add EdgeZero workspace dependencies pinned to rev 170b74b.
Update all references across docs, CI workflows, scripts, agent files,
and configuration.
Introduces trusted-server-core::platform with PlatformConfigStore,
PlatformSecretStore, PlatformKvStore, PlatformBackend, PlatformHttpClient,
and PlatformGeo traits alongside ClientInfo, PlatformError, and
RuntimeServices. Wires the Fastly adapter implementations and threads
RuntimeServices into route_request. Moves GeoInfo to platform/types as
platform-neutral data and adds geo_from_fastly for field mapping.
@prk-Jr prk-Jr self-assigned this Mar 20, 2026
prk-Jr added 4 commits March 20, 2026 21:53
- Defer KV store opening: replace early error return with a local
  UnavailableKvStore fallback so routes that do not need synthetic ID
  access succeed when the KV store is missing or temporarily unavailable
- Use ConfigStore::try_open + try_get and SecretStore::try_get throughout
  FastlyPlatformConfigStore and FastlyPlatformSecretStore to honour the
  Result contract instead of panicking on open/lookup failure
- Encapsulate RuntimeServices service fields as pub(crate) with public
  getter methods (config_store, secret_store, backend, http_client, geo)
  and a pub new() constructor; adapter updated to use new()
- Reference #487 in FastlyPlatformHttpClient stub (PR 6 implements it)
- Remove unused KvPage re-export from platform/mod.rs
- Use super::KvHandle shorthand in RuntimeServices::kv_handle()
Copy link
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Solid architectural direction — this PR lays the groundwork for decoupling trusted-server-core from the Fastly SDK. The trait design, object-safety assertions, and graceful KV degradation are well done. A few issues to address before merge, mostly around constructor ergonomics and a question about Send bounds.

Blocking

🔧 wrench

  • RuntimeServices::new takes 7 args with lint suppression: At the boundary today, will exceed it when creative_store/counter_store are added per the TODO. Use a builder pattern instead. (crates/trusted-server-core/src/platform/types.rs:103)
  • PR description overstates decoupling: trusted-server-core still depends on fastly directly (geo.rs, backend.rs, fastly_storage.rs, settings.rs). The description says this "enables future non-Fastly adapter support" — should clarify this is step N of M and the fastly dep removal comes in a follow-up.

❓ question

  • PlatformPendingRequest is !Send — intentional?: Box<dyn Any> is !Send, while PlatformHttpClient: Send + Sync. Will this asymmetry hold for future adapters using tokio::spawn? (crates/trusted-server-core/src/platform/http.rs:64)

Non-blocking

🤔 thinking

  • PlatformBackendSpec duplicates BackendConfig fields: Both types have scheme, host, port, certificate_check, first_byte_timeout — one owns, the other borrows. Every ensure call allocates owned Strings immediately borrowed. Consider sharing the type.
  • Asymmetric key semantics on PlatformConfigStore: get takes store_name, put/delete take store_id. Newtype wrappers would make misuse a compile error. (crates/trusted-server-core/src/platform/traits.rs:19)

♻️ refactor

  • geo_from_fastly should live in adapter, not core: Imports fastly::geo::Geo in core, tying it to Fastly. Being pub means new code could accidentally depend on it. (crates/trusted-server-core/src/geo.rs:10)
  • UnavailableKvStore should live in core, not adapter: Platform-neutral fallback that any future adapter would duplicate. (crates/trusted-server-adapter-fastly/src/platform.rs:276)

⛏ nitpick

  • Prefer attach_with over attach(format!(...)): Avoids allocation unless the report is inspected. Appears throughout platform.rs.

🌱 seedling

  • No Debug impl for RuntimeServices: A manual Debug impl printing client_info while omitting service handles would help debugging.

CI Status

  • fmt: PASS
  • clippy: PASS
  • rust tests: PASS
  • js tests: PASS

/// Adapter crates should call this constructor rather than using struct
/// literal syntax so that any future invariants on the struct are enforced
/// in one place.
#[allow(clippy::too_many_arguments)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrenchRuntimeServices::new takes 7 arguments and suppresses the lint

CLAUDE.md says "Functions should never take more than 7 arguments — use a struct instead." This constructor is at the boundary and uses #[allow(clippy::too_many_arguments)]. More importantly, the TODO at line 82-85 mentions adding creative_store, counter_store — which will push past 7.

Fix: Use a builder pattern:

RuntimeServices::builder()
    .config_store(config_store)
    .secret_store(secret_store)
    .kv_store(kv_store)
    .backend(backend)
    .http_client(http_client)
    .geo(geo)
    .client_info(client_info)
    .build()

This also resolves the "struct literal vs constructor" concern in the doc comment at line 98-101.

///
/// The core stores this as an opaque support type. Adapter implementations can
/// recover their concrete runtime handle through [`Self::downcast`].
pub struct PlatformPendingRequest {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

questionPlatformPendingRequest uses Box<dyn Any> which is !Send

PlatformPendingRequest stores Box<dyn Any> (!Send), while RuntimeServices stores Arc<dyn PlatformHttpClient> requiring Send + Sync. The trait contract says PlatformHttpClient: Send + Sync but async methods' futures are !Send (via #[async_trait(?Send)]).

Is this intentional asymmetry? Will it hold for future adapters (e.g., an Axum adapter using tokio::spawn)?

///
/// Returns [`PlatformError::ConfigStore`] when the key does not exist or
/// the store cannot be opened.
fn get(&self, store_name: &str, key: &str) -> Result<String, Report<PlatformError>>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingget vs put/delete have asymmetric key semantics

get takes store_name (edge-visible name) while put/delete take store_id (management API identifier). This is documented but is a footgun — someone will pass a store_name to put or vice versa.

Consider newtype wrappers (StoreName(String) / StoreId(String)) to make misuse a compile error.

//! blocks for construction and response header injection.

use fastly::geo::geo_lookup;
use fastly::geo::{geo_lookup, Geo};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactorgeo_from_fastly should live in the adapter crate, not core

This function imports fastly::geo::Geo in core, tying the core to Fastly. Being pub means new code could accidentally depend on it. Should move to trusted-server-adapter-fastly in the follow-up PR.

/// Every method returns [`KvError::Unavailable`], ensuring that handlers
/// which call [`RuntimeServices::kv_handle`] receive a typed error rather than
/// a panic. Routes that do not touch the KV store are unaffected.
pub(crate) struct UnavailableKvStore;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactorUnavailableKvStore should live in core, not the adapter

This is a platform-neutral fallback (all methods return KvError::Unavailable). Any future adapter would need the same stub. Move to trusted-server-core::platform to avoid duplication across adapters.

impl PlatformConfigStore for FastlyPlatformConfigStore {
fn get(&self, store_name: &str, key: &str) -> Result<String, Report<PlatformError>> {
let store = ConfigStore::try_open(store_name).map_err(|e| {
Report::new(PlatformError::ConfigStore)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick — Prefer attach_with over attach(format!(...))

attach_with(|| format!(...)) avoids the allocation unless the report is actually inspected. This pattern appears throughout this file.

// Current:
.attach(format!("failed to open config store '{store_name}': {e}"))
// Preferred:
.attach_with(|| format!("failed to open config store '{store_name}': {e}"))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define platform traits, ClientInfo, PlatformError, RuntimeServices

2 participants