1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! Offline-first local storage and background synchronization.
//!
//! This module gives occasionally-connected apps (the primary consumer is
//! the in-process Tauri mobile shell, issue #1508) a local, in-process
//! SQLite store plus a sync engine that reconciles it with a remote Autumn
//! app backed by PostgreSQL.
//!
//! # Architecture
//!
//! - [`SyncStore`] — the local store. App data lives in it as JSON payloads
//! keyed by `(collection, pk)`. Every write also journals a pending change
//! in the same SQLite transaction (write-through change tracking), and
//! deletes are recorded as tombstone rows so they replicate.
//! - [`SyncEngine`] — the client sync loop: push pending changes, then pull
//! rows newer than the local cursor. At-least-once with server-side
//! dedup; safe to retry; tolerates being offline indefinitely.
//! - [`server`] — an [`axum::Router`] with `POST /push` + `GET /pull`,
//! mountable on the remote app via `AppBuilder::nest("/sync", ...)`,
//! persisting into Postgres shadow tables (or an in-memory backend for
//! tests and demos).
//! - [`ConflictResolver`] — pluggable conflict policy, applied server-side
//! when a pushed change was based on a stale version. The default is
//! last-write-wins on the conflicting writes' `updated_at`, with the
//! device id as a deterministic tiebreak.
//!
//! Ordering is server-authoritative: the server assigns every accepted
//! change a monotonically increasing version from one global sequence, and
//! clients pull "rows with version greater than my cursor". Device clocks
//! never order the change feed.
//!
//! Server-side data is partitioned per tenant/principal by a scope key
//! ([`SyncScope`]) derived from the **authenticated** request — never
//! client-supplied. The default [`server::router`] is single-tenant (every
//! request shares one constant "global" scope); multi-user deployments
//! mount [`server::scoped_router`] and have their auth middleware insert a
//! `SyncScope` derived from the authenticated user.
//!
//! # Client wiring
//!
//! ```rust,no_run
//! use std::time::Duration;
//! use autumn_web::sync::{SyncConfig, SyncEngine, SyncStore};
//!
//! # async fn wire() -> Result<(), autumn_web::sync::SyncError> {
//! // Open (or create) the local store, e.g. inside the app's data dir.
//! let store = SyncStore::open("/data/app/sync.db")?;
//!
//! // Reads and writes work fully offline; every write is journaled.
//! store.put(
//! "notes",
//! "6b3f2c1e-5f43-4a67-9d31-2f4f6a8e9b10", // client-generated UUID pk
//! &serde_json::json!({ "title": "works offline" }),
//! )?;
//! let notes: Vec<(String, serde_json::Value)> = store.list("notes")?;
//!
//! // Sync in the background whenever connectivity allows.
//! let engine = SyncEngine::new(store, SyncConfig::new("https://example.com/sync"));
//! let _task = engine.spawn_background(Duration::from_secs(30));
//! # Ok(())
//! # }
//! ```
//!
//! The server side mounts in one line — see [`server::router`].
//!
//! # Scope
//!
//! Existing diesel `#[repository]` models are **not** transparently
//! offline. Data the app wants available offline goes through the
//! [`SyncStore`] API and is mirrored to the remote shadow tables.
pub use ;
pub use ;
pub use ;
pub use ;
pub use SyncStore;
/// Errors produced by the sync store, engine, and server backends.