Skip to main content

cratefield_runtime_native/
runtime.rs

1//! The `Native` runtime builder (issue #19): the counterpart of
2//! `cratefield_runtime_cloudflare::Cloudflare` for a self-hosted binary.
3//!
4//! ```ignore
5//! # use std::sync::Arc;
6//! # fn db() -> Arc<dyn cratefield_core::Database> { unimplemented!() }
7//! # fn rate_limiter() -> Arc<dyn cratefield_core::RateLimiter> { unimplemented!() }
8//! # fn kv() -> Arc<dyn cratefield_core::KeyValue> { unimplemented!() }
9//! # fn mailer() -> Arc<dyn cratefield_core::Mailer> { unimplemented!() }
10//! use cratefield_runtime_native::Native;
11//!
12//! let runtime = Native::new()
13//!     .db_arc(db())                       // Postgres (adapter-postgres) or Sqlite
14//!     .rate_limiter_arc(rate_limiter())   // RedisRateLimiter, optional
15//!     .kv_arc(kv())                       // RedisKv, optional
16//!     .mailer_arc(mailer());              // Resend::from_env(..), as on Workers
17//! # let _runtime: Native = runtime;
18//! ```
19
20use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22
23use cratefield_core::{
24    Blob, Captcha, Database, HarnessConfig, KeyValue, Mailer, Port, Ports, RateLimiter, Runtime,
25    UlidIdGen,
26};
27
28use crate::config::EnvConfig;
29use crate::ports::{ReqwestClient, SpawnDefer, TokioClock};
30
31static WARNED_SIGNER: AtomicBool = AtomicBool::new(false);
32
33fn warn_once(flag: &AtomicBool, message: &str) {
34    if !flag.load(Ordering::Relaxed) {
35        flag.store(true, Ordering::Relaxed);
36        tracing::warn!("{message}");
37    }
38}
39
40/// The native runtime. Adapters are passed in as instances (the same
41/// pattern as the Cloudflare runtime's `.mailer(..)`/`.captcha(..)`):
42/// a Postgres or SQLite [`Database`](cratefield_core::Database) from the
43/// matching adapter crate, a [`RedisRateLimiter`](crate::RedisRateLimiter)
44/// and [`RedisKv`](crate::RedisKv) over a shared Redis connection
45/// manager, `Resend`/`Turnstile` unchanged from the Workers path.
46///
47/// Config — including `HARNESS_SECRET` for the `Signer` port — is read
48/// from `std::env` through the same [`cratefield_core::Config`] trait
49/// ([`EnvConfig`]), so module keys behave identically on both runtimes.
50#[derive(Clone, Default)]
51pub struct Native {
52    db: Option<Arc<dyn Database>>,
53    rate_limiter: Option<Arc<dyn RateLimiter>>,
54    kv: Option<Arc<dyn KeyValue>>,
55    blob: Option<Arc<dyn Blob>>,
56    mailer: Option<Arc<dyn Mailer>>,
57    captcha: Option<Arc<dyn Captcha>>,
58}
59
60impl Native {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// The `Database` port: `Postgres` (`cratefield-adapter-postgres`) or
66    /// `SqliteDatabase` (`cratefield-adapter-sqlite`) — any `Database`
67    /// implementation.
68    #[must_use]
69    pub fn db(mut self, db: impl Database + 'static) -> Self {
70        self.db = Some(Arc::new(db));
71        self
72    }
73
74    /// `db` for an already-shared database.
75    #[must_use]
76    pub fn db_arc(mut self, db: Arc<dyn Database>) -> Self {
77        self.db = Some(db);
78        self
79    }
80
81    /// The `RateLimiter` port: [`RedisRateLimiter`](crate::RedisRateLimiter) in this crate, or a
82    /// test fake.
83    #[must_use]
84    pub fn rate_limiter(mut self, limiter: impl RateLimiter + 'static) -> Self {
85        self.rate_limiter = Some(Arc::new(limiter));
86        self
87    }
88
89    /// `rate_limiter` for an already-shared limiter.
90    #[must_use]
91    pub fn rate_limiter_arc(mut self, limiter: Arc<dyn RateLimiter>) -> Self {
92        self.rate_limiter = Some(limiter);
93        self
94    }
95
96    /// The `KeyValue` port: [`RedisKv`](crate::RedisKv) in this crate, or a test fake.
97    #[must_use]
98    pub fn kv(mut self, kv: impl KeyValue + 'static) -> Self {
99        self.kv = Some(Arc::new(kv));
100        self
101    }
102
103    /// `kv` for an already-shared store.
104    #[must_use]
105    pub fn kv_arc(mut self, kv: Arc<dyn KeyValue>) -> Self {
106        self.kv = Some(kv);
107        self
108    }
109
110    /// The `Blob` port: a directory store ([`crate::DirBlob`]) or an
111    /// S3-compatible adapter.
112    #[must_use]
113    pub fn blob_arc(mut self, blob: Arc<dyn Blob>) -> Self {
114        self.blob = Some(blob);
115        self
116    }
117
118    #[must_use]
119    pub fn mailer(mut self, mailer: impl Mailer + 'static) -> Self {
120        self.mailer = Some(Arc::new(mailer));
121        self
122    }
123
124    #[must_use]
125    pub fn mailer_arc(mut self, mailer: Arc<dyn Mailer>) -> Self {
126        self.mailer = Some(mailer);
127        self
128    }
129
130    #[must_use]
131    pub fn captcha(mut self, captcha: impl Captcha + 'static) -> Self {
132        self.captcha = Some(Arc::new(captcha));
133        self
134    }
135
136    #[must_use]
137    pub fn captcha_arc(mut self, captcha: Arc<dyn Captcha>) -> Self {
138        self.captcha = Some(captcha);
139        self
140    }
141
142    /// Resolves the port bundle once per process from the builder and
143    /// `std::env`: config, `Signer` (from `HARNESS_SECRET` — warn once
144    /// and leave unset when missing, exactly like the Cloudflare
145    /// runtime), `HttpClient` over reqwest, `Clock` over the system
146    /// clock, `IdGen`, and `Defer` over `tokio::spawn`.
147    ///
148    /// Unlike the Workers runtime — which resolves bindings per event —
149    /// a native process builds one bundle and shares it: every adapter
150    /// here is an `Arc`, so `clone_ports` snapshots cheaply.
151    #[must_use]
152    pub fn ports(&self) -> Ports {
153        let config: Arc<dyn cratefield_core::Config> = Arc::new(EnvConfig);
154        let mut ports = Ports::with_config(Arc::clone(&config));
155
156        ports.db.clone_from(&self.db);
157        ports.rate_limiter.clone_from(&self.rate_limiter);
158        ports.kv.clone_from(&self.kv);
159        ports.blob.clone_from(&self.blob);
160        ports.mailer.clone_from(&self.mailer);
161        ports.captcha.clone_from(&self.captcha);
162
163        match HarnessConfig::from_config(&*config) {
164            Ok(parsed) => ports.signer = Some(Arc::new(parsed.signer())),
165            Err(_) => warn_once(
166                &WARNED_SIGNER,
167                "HARNESS_SECRET missing or invalid: Signer port not provided",
168            ),
169        }
170
171        ports.http = Some(Arc::new(ReqwestClient::new()));
172        ports.clock = Some(Arc::new(TokioClock));
173        ports.id_gen = Some(Arc::new(UlidIdGen));
174        ports.defer = Some(Arc::new(SpawnDefer));
175        ports
176    }
177}
178
179impl Runtime for Native {
180    /// The static set used by `Harness::build`. `Signer` is included:
181    /// its presence is validated at `ports()` time (and by `fz doctor`),
182    /// mirroring the Cloudflare runtime.
183    fn provides(&self) -> Vec<Port> {
184        let mut provided = vec![
185            Port::Signer,
186            Port::HttpClient,
187            Port::Clock,
188            Port::IdGen,
189            Port::Defer,
190        ];
191        if self.db.is_some() {
192            provided.push(Port::Db);
193        }
194        if self.rate_limiter.is_some() {
195            provided.push(Port::RateLimiter);
196        }
197        if self.kv.is_some() {
198            provided.push(Port::KeyValue);
199        }
200        if self.blob.is_some() {
201            provided.push(Port::Blob);
202        }
203        if self.mailer.is_some() {
204            provided.push(Port::Mailer);
205        }
206        if self.captcha.is_some() {
207            provided.push(Port::Captcha);
208        }
209        provided
210    }
211}
212
213/// Snapshots a [`Ports`] bundle by cloning every `Arc` (core's `Ports`
214/// is not `Clone`; every field is a shared handle, so this is the whole
215/// job). Used to give the cron scheduler the same adapters the HTTP
216/// router serves with.
217pub(crate) fn clone_ports(ports: &Ports) -> Ports {
218    let mut snapshot = Ports::with_config(Arc::clone(&ports.config));
219    snapshot.db.clone_from(&ports.db);
220    snapshot.mailer.clone_from(&ports.mailer);
221    snapshot.captcha.clone_from(&ports.captcha);
222    snapshot.rate_limiter.clone_from(&ports.rate_limiter);
223    snapshot.signer.clone_from(&ports.signer);
224    snapshot.kv.clone_from(&ports.kv);
225    snapshot.blob.clone_from(&ports.blob);
226    snapshot.http.clone_from(&ports.http);
227    snapshot.clock.clone_from(&ports.clock);
228    snapshot.id_gen.clone_from(&ports.id_gen);
229    snapshot.defer.clone_from(&ports.defer);
230    snapshot
231}