Thank you for being patient! We're working hard on resolving the issue
Each provider declares a host-supplied trait that its row bodies call into for any work that touches host-shaped state.
<Provider>BackendDeps methods speak only:
Cell, Owner, WriteOutput.&str, Uuid, serde_json::Value,
Range<NaiveDate>, etc.If the row body would need to query the host's database by row
uuid, the right move is add a method to BackendDeps. The
host implements the method against its real types and converts at
the boundary.
pub trait GarminBackendDeps: Send + Sync {
fn persist_cells<'a>(
&'a self,
row_id: Uuid,
cells: Vec<Cell>,
) -> BoxFuture<'a, anyhow::Result<()>>;
fn recent_log_cells<'a>(
&'a self,
owner: &'a Owner,
lookup_key: &'a str,
) -> BoxFuture<'a, anyhow::Result<Vec<Cell>>>;
}
The Row body in tento-garmin/src/connector/sleep/mod.rs walks
date ranges, calls the Garmin SDK, builds Vec<Cell>, and calls
ctx.deps.persist_cells(...). Sync logic lives in the
provider crate.
This is the default pattern. Use it when:
Row::sync.pub trait GoogleBackendDeps: Send + Sync {
fn run_calendar_sync(&self, owner: &Owner, auth_id: &str, …) -> …;
fn create_calendar_event(&self, …) -> …;
fn update_calendar_event(&self, …) -> …;
fn delete_calendar_event(&self, …) -> …;
fn recent_log_cells(&self, …) -> …;
}
Google's Row::sync body just calls
ctx.deps.run_calendar_sync(...). The actual sync logic lives
host-side because the EventDb / sync-token / multi-calendar
state machinery touches host data heavily and didn't decompose
cleanly along Cell boundaries.
Use this pattern only when:
events + calendars + watches).persist_cells shape.If you find yourself reaching for "rich deps" early, double-check
whether you can split the sync into a Row::sync body that emits
Vec<Cell> plus a host-side post-processor. The thin pattern is
worth fighting for — it keeps the provider crate independently
useful.
The trait is always <Provider>BackendDeps (singular Backend,
plural Deps). Live examples:
tento-garmin/src/connector/mod.rs::GarminBackendDepstento-whoop/src/connector/mod.rs::WhoopBackendDepstento-google/src/connector/mod.rs::GoogleBackendDepstento-weather/src/connector/mod.rs::WeatherBackendDepsWeather has a second trait WeatherLogsDeps for the :logs
sub-row — fine to split when one row family needs different host
seams than another.
The trait should not provide default method implementations. If a
seam has a sensible no-op default, that suggests the row doesn't
need that seam at all — just don't call it. Forcing the host to
write unimplemented!() is better than giving them a silent
no-op.
Each <Provider>BackendDeps is registered into the host's
[IntegrationDeps] registry — a TypeId-keyed map populated
once at boot:
let mut integrations = lona_rows::IntegrationDeps::new();
integrations.register::<dyn WeatherBackendDeps>(weather_deps);
integrations.register::<dyn StravaBackendDeps>(strava_deps);
// ... one register call per provider, no central deps.rs edit
The dispatcher resolves <R::Integration as Integration>::Deps to its registered &Arc<dyn ...> at
runtime via IntegrationDeps::expect. Missing wirings panic
at boot via IntegrationDeps::ensure_all_registered::<RowKeys>(),
called once in service init — so a forgotten registration fails
fast with the named trait, not silently in the background.
Adding a new provider therefore requires zero edits to
lona-rows/src/deps.rs or any other central file in the host;
only the provider's own crate plus one host-side register
line is touched.