pub struct SyncSession { /* private fields */ }Expand description
A device’s live sync endpoint: its append chain, its durable journal, and the pump. See the module docs for the contract sequencing.
Implementations§
Source§impl SyncSession
impl SyncSession
Sourcepub fn open(
device_id: impl Into<String>,
journal_path: &Path,
checkpoint_dir: &Path,
wall: WallClock,
) -> Result<Self, SessionError>
pub fn open( device_id: impl Into<String>, journal_path: &Path, checkpoint_dir: &Path, wall: WallClock, ) -> Result<Self, SessionError>
Open a session over an existing (possibly empty, possibly truncated)
journal. A truncated journal resumes only through its covering
checkpoint (resume_anchored — B4 contract): the checkpoint file
must be present in checkpoint_dir under its content address.
Sourcepub fn with_key_provider(self, provider: Arc<dyn SyncKeyProvider>) -> Self
pub fn with_key_provider(self, provider: Arc<dyn SyncKeyProvider>) -> Self
Turn on E2E: op payloads are encrypted at SyncSession::append and
decrypted at SyncSession::state, keyed per scope audience by
provider (a login-derived crate::crypto::DerivedKeyProvider in the
daemon). Builder so SyncSession::open’s signature stays stable for the
local (cleartext FsRelay) path. NOTE: with a remote relay, checkpoint
state is still cleartext-folded — do not push checkpoints to an untrusted
relay until per-scope checkpoint encryption lands (crypto.rs follow-up).
Sourcepub fn bootstrap(
device_id: impl Into<String>,
journal_path: &Path,
checkpoint_dir: &Path,
relay: &mut dyn Relay,
wall: WallClock,
) -> Result<Self, SessionError>
pub fn bootstrap( device_id: impl Into<String>, journal_path: &Path, checkpoint_dir: &Path, relay: &mut dyn Relay, wall: WallClock, ) -> Result<Self, SessionError>
Open + immediately SyncSession::rebase onto the relay’s latest
checkpoint — the cold-device / returning-straggler entry point
(proposal §“Cold / new device bootstrap”). With no relay checkpoint
(young account) this degrades to a plain open; the first pump
replays from genesis.
Sourcepub fn append(
&mut self,
scope: Scope,
surface: Surface,
payload: Value,
) -> Result<OpRecord, SessionError>
pub fn append( &mut self, scope: Scope, surface: Surface, payload: Value, ) -> Result<OpRecord, SessionError>
Record a local mutation: stamp (hybrid clock), journal (flushed), then hold for push — the op is journal-durable before it can ever be transmitted (B1 MUST). If the journal write fails the device chain is rolled back (rebuilt from the durable ops), so the failed op can never leave a hole for the next append to chain onto.
Sourcepub fn committed_run(
&self,
agent_id: &str,
run_id: &str,
) -> Option<FoldedRecord>
pub fn committed_run( &self, agent_id: &str, run_id: &str, ) -> Option<FoldedRecord>
The fence-independent committed-run idempotency oracle (B5) over
this session’s folded state (checkpoint base + journal tail): “has
run_id already committed for agent_id?” Keep-all and immune to epoch
bumps AND compaction, so it is the CORRECT idempotency lookup — unlike
crate::fold::SyncState::intent, which is the fenced “who holds now”
view and can read None/pending for a run that actually committed. A B6
dispatch fence performs exactly this read before an external side effect.
Sourcepub fn record_intent(
&mut self,
scope: Scope,
intent: &Intent,
) -> Result<Option<OpRecord>, SessionError>
pub fn record_intent( &mut self, scope: Scope, intent: &Intent, ) -> Result<Option<OpRecord>, SessionError>
Record a leased execution Intent (B5) — journal-durable exactly
like any SyncSession::append, so it is durable before it can be
transmitted. The partitioned / ungated path: a zombie that cannot
reach the coordinator still records here, and the fold converges the
ledger deterministically.
Terminal guard (C3): a committed run is terminal. A pending/failed
write for a run already committed (per the fence-independent
SyncSession::committed_run oracle) is a no-op (Ok(None)), so a
failed-over holder writing pending before checking cannot revert the
committed ledger.
This converges the ledger; it is NOT the exactly-once execution gate. The B6 dispatch fence — a linearizable “am I still epoch N?” plus this committed-oracle read before the external effect — is what makes execution single-shot.
Sourcepub fn record_intent_if_current(
&mut self,
scope: Scope,
intent: &Intent,
coordinator: &mut dyn LeaseCoordinator,
) -> Result<Option<OpRecord>, SessionError>
pub fn record_intent_if_current( &mut self, scope: Scope, intent: &Intent, coordinator: &mut dyn LeaseCoordinator, ) -> Result<Option<OpRecord>, SessionError>
Record a leased Intent only if the local epoch is still current
— the best-effort local gate: a linearizable coordinator read confirms
this device still holds the lease at intent.epoch before the op is
journaled. Returns Ok(None) when the coordinator says this device is
no longer the holder at that epoch (skipping a write the fold would fence)
or when the run is already committed (the SyncSession::record_intent
terminal guard).
This is a liveness optimization, NOT the safety gate: a partitioned
zombie that cannot reach the coordinator falls back to
SyncSession::record_intent. Exactly-once execution is the B6 dispatch
fence (this check races a pause-after-check — the Kleppmann residual).
Sourcepub fn pump(
&mut self,
relay: &mut dyn Relay,
) -> Result<PumpReport, SessionError>
pub fn pump( &mut self, relay: &mut dyn Relay, ) -> Result<PumpReport, SessionError>
One reconciliation round: push journal-durable own ops → pull → verify → journal the folds → ack. See the module docs for the contract sequencing; retry-safe at every crash point.
Returns RelayError::FrontierTruncated (wrapped) when the relay
has GC’d past this session’s frontier — call
SyncSession::rebase and pump again.
Sourcepub fn rebase(&mut self, relay: &mut dyn Relay) -> Result<bool, SessionError>
pub fn rebase(&mut self, relay: &mut dyn Relay) -> Result<bool, SessionError>
Re-anchor this session on the relay’s latest checkpoint — the cold
bootstrap / post-eviction re-entry move. Returns true when a
rebase happened.
Sequencing (each step durable before the next depends on it):
checkpoint_get → pull(since = checkpoint frontier) → merge in
every locally-held op the checkpoint does NOT cover (a returning
straggler’s unpushed writes survive) → verify_anchored → save the
checkpoint into the session checkpoint dir → truncate_to (journal
rewritten as the anchored tail, truncation marker stamped) →
resume_anchored. The push cursor resets so the next pump
re-offers every own op in the tail (relay dedups the already-pushed).
Sourcepub fn publish_checkpoint(
&mut self,
relay: &mut dyn Relay,
) -> Result<Option<Checkpoint>, SessionError>
pub fn publish_checkpoint( &mut self, relay: &mut dyn Relay, ) -> Result<Option<Checkpoint>, SessionError>
Compute a checkpoint at the relay’s stable frontier from this
session’s held ops and upload it — the device-computed snapshot the
proposal requires under E2E (“the relay holds ciphertext and cannot
fold”). Call after a pump (so held == relay-known and own ops
are pushed). Returns the uploaded checkpoint, or None when there
is no stable frontier, nothing below it, or this session is itself
anchored on a checkpoint (recompaction over a base is the same
later slice B4 deferred).
Sourcepub fn state(&self) -> SyncState
pub fn state(&self) -> SyncState
The materialized state: fold_onto(base checkpoint, journal tail).
Under E2E (SyncSession::with_key_provider) the tail is ciphertext, so
each op’s payload is decrypted here — after the chain has verified, before
the fold groups on payload["id"]/fold_key. Op identity stays the
cleartext-metadata op_id; only the payload is swapped.
Sourcepub fn state_hash(&self) -> String
pub fn state_hash(&self) -> String
state_hash of SyncSession::state — the divergence invariant
two synced devices must agree on.
pub fn device_id(&self) -> &str
Sourcepub fn base(&self) -> Option<&Checkpoint>
pub fn base(&self) -> Option<&Checkpoint>
The checkpoint this session’s journal is anchored on, if truncated.