store.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. //! Storage abstraction separating the pure decision logic from IO.
  2. //!
  3. //! The [`Store`] trait composes four focused sub-traits:
  4. //! - [`AccountStore`] — account CRUD and versioning
  5. //! - [`PostingStore`] — posting reads and lifecycle transitions
  6. //! - [`TransferStore`] — transfer persistence and queries
  7. //! - [`SagaStore`] — saga state for crash recovery
  8. use async_trait::async_trait;
  9. use kuatia_types::{
  10. Account, AccountId, AssetId, Envelope, EnvelopeId, Journal, JournalId, Posting, PostingId,
  11. PostingStatus, Receipt,
  12. };
  13. use crate::error::StoreError;
  14. use crate::events::EventStore;
  15. /// Pairs a committed transfer with its receipt.
  16. #[derive(Debug, Clone)]
  17. pub struct EnvelopeRecord {
  18. /// The envelope that was committed.
  19. pub envelope: Envelope,
  20. /// The receipt proving commitment.
  21. pub receipt: Receipt,
  22. /// Unix milliseconds when this record was created.
  23. pub created_at: i64,
  24. }
  25. /// Pagination and filtering parameters for posting queries.
  26. #[derive(Debug, Clone)]
  27. pub struct PostingQuery {
  28. /// Filter to postings owned by this account.
  29. pub account: AccountId,
  30. /// Filter by asset.
  31. pub asset: Option<AssetId>,
  32. /// Filter by posting status.
  33. pub status: Option<PostingStatus>,
  34. /// Max results to return.
  35. pub limit: Option<u32>,
  36. /// Number of results to skip.
  37. pub offset: Option<u32>,
  38. }
  39. /// Pagination and filtering parameters for transfer queries.
  40. #[derive(Debug, Clone, Default)]
  41. pub struct TransferQuery {
  42. /// Filter to transfers involving this account.
  43. pub account: Option<AccountId>,
  44. /// Inclusive lower bound (unix millis).
  45. pub from_ts: Option<i64>,
  46. /// Exclusive upper bound (unix millis).
  47. pub to_ts: Option<i64>,
  48. /// Filter by journal.
  49. pub journal: Option<JournalId>,
  50. /// Max results to return.
  51. pub limit: Option<u32>,
  52. /// Number of results to skip.
  53. pub offset: Option<u32>,
  54. }
  55. /// A page of results with total count for pagination.
  56. #[derive(Debug, Clone)]
  57. pub struct Page<T> {
  58. /// The items in this page.
  59. pub items: Vec<T>,
  60. /// Total number of matching items (before pagination).
  61. pub total: u64,
  62. }
  63. // ---------------------------------------------------------------------------
  64. // Sub-traits
  65. // ---------------------------------------------------------------------------
  66. /// Account persistence: create, version, query.
  67. #[async_trait]
  68. pub trait AccountStore: Send + Sync {
  69. /// Fetch a single account by id.
  70. async fn get_account(&self, id: &AccountId) -> Result<Account, StoreError>;
  71. /// Fetch multiple accounts by id.
  72. async fn get_accounts(&self, ids: &[AccountId]) -> Result<Vec<Account>, StoreError>;
  73. /// Persist a new account (version 1).
  74. async fn create_account(&self, account: Account) -> Result<(), StoreError>;
  75. /// Append a new version to an existing account.
  76. async fn append_account_version(&self, account: Account) -> Result<(), StoreError>;
  77. /// Return the full version history for an account.
  78. async fn get_account_history(&self, id: &AccountId) -> Result<Vec<Account>, StoreError>;
  79. /// List all accounts (latest version of each).
  80. async fn list_accounts(&self) -> Result<Vec<Account>, StoreError>;
  81. }
  82. /// Posting persistence: reads and lifecycle transitions.
  83. #[async_trait]
  84. pub trait PostingStore: Send + Sync {
  85. /// Fetch postings by their ids.
  86. async fn get_postings(&self, ids: &[PostingId]) -> Result<Vec<Posting>, StoreError>;
  87. /// Return postings owned by an account, optionally filtered by asset and/or status.
  88. async fn get_postings_by_account(
  89. &self,
  90. account: &AccountId,
  91. asset: Option<&AssetId>,
  92. status: Option<PostingStatus>,
  93. ) -> Result<Vec<Posting>, StoreError>;
  94. /// Reserve postings: Active → PendingInactive.
  95. /// Atomic: if any posting is not Active, the entire batch fails.
  96. async fn reserve_postings(&self, ids: &[PostingId]) -> Result<(), StoreError>;
  97. /// Release postings back from reservation.
  98. /// - PendingInactive → Active
  99. /// - Active → no-op (already released)
  100. /// - Inactive → fail (void posting cannot be released)
  101. /// Atomic: if any posting is Inactive, the entire batch fails.
  102. async fn release_postings(&self, ids: &[PostingId]) -> Result<(), StoreError>;
  103. /// Deactivate postings and insert newly created postings.
  104. async fn finalize_postings(
  105. &self,
  106. deactivate: &[PostingId],
  107. create: &[Posting],
  108. ) -> Result<(), StoreError>;
  109. /// Query postings with filtering and pagination.
  110. async fn query_postings(&self, query: &PostingQuery) -> Result<Page<Posting>, StoreError> {
  111. let all = self
  112. .get_postings_by_account(
  113. &query.account,
  114. query.asset.as_ref(),
  115. query.status,
  116. )
  117. .await?;
  118. let total = all.len() as u64;
  119. let offset = query.offset.unwrap_or(0) as usize;
  120. let limit = query.limit.unwrap_or(u32::MAX) as usize;
  121. let items = all.into_iter().skip(offset).take(limit).collect();
  122. Ok(Page { items, total })
  123. }
  124. }
  125. /// Transfer persistence: store and query committed transfers.
  126. #[async_trait]
  127. pub trait TransferStore: Send + Sync {
  128. /// Fetch a transfer record by its content-addressed id.
  129. async fn get_transfer(&self, id: &EnvelopeId) -> Result<Option<EnvelopeRecord>, StoreError>;
  130. /// Persist a committed transfer and its receipt.
  131. async fn store_transfer(&self, record: EnvelopeRecord) -> Result<(), StoreError>;
  132. /// Return all transfers involving the given account.
  133. async fn get_transfers_for_account(
  134. &self,
  135. account: &AccountId,
  136. ) -> Result<Vec<EnvelopeRecord>, StoreError>;
  137. /// Query transfers with filtering and pagination.
  138. async fn query_transfers(
  139. &self,
  140. query: &TransferQuery,
  141. ) -> Result<Page<EnvelopeRecord>, StoreError> {
  142. // Default in-memory implementation
  143. let all = if let Some(ref account) = query.account {
  144. self.get_transfers_for_account(account).await?
  145. } else {
  146. return Err(StoreError::Internal(
  147. "query_transfers requires account filter in default implementation".into(),
  148. ));
  149. };
  150. let filtered: Vec<EnvelopeRecord> = all
  151. .into_iter()
  152. .filter(|r| {
  153. if let Some(from) = query.from_ts
  154. && r.created_at < from
  155. {
  156. return false;
  157. }
  158. if let Some(to) = query.to_ts
  159. && r.created_at >= to
  160. {
  161. return false;
  162. }
  163. if let Some(journal) = query.journal
  164. && r.envelope.journal() != journal
  165. {
  166. return false;
  167. }
  168. true
  169. })
  170. .collect();
  171. let total = filtered.len() as u64;
  172. let offset = query.offset.unwrap_or(0) as usize;
  173. let limit = query.limit.unwrap_or(u32::MAX) as usize;
  174. let items = filtered.into_iter().skip(offset).take(limit).collect();
  175. Ok(Page { items, total })
  176. }
  177. }
  178. /// Saga state persistence for crash recovery.
  179. #[async_trait]
  180. pub trait SagaStore: Send + Sync {
  181. /// Persist a saga execution state.
  182. async fn save_saga(&self, id: &i64, data: Vec<u8>) -> Result<(), StoreError>;
  183. /// Load all pending (incomplete) saga states.
  184. async fn list_pending_sagas(&self) -> Result<Vec<(i64, Vec<u8>)>, StoreError>;
  185. /// Delete a completed saga state.
  186. async fn delete_saga(&self, id: &i64) -> Result<(), StoreError>;
  187. }
  188. /// Journal persistence.
  189. #[async_trait]
  190. pub trait JournalStore: Send + Sync {
  191. /// Create a new journal.
  192. async fn create_journal(&self, journal: Journal) -> Result<(), StoreError>;
  193. /// Fetch a journal by id.
  194. async fn get_journal(&self, id: &JournalId) -> Result<Journal, StoreError>;
  195. /// List all journals.
  196. async fn list_journals(&self) -> Result<Vec<Journal>, StoreError>;
  197. }
  198. // ---------------------------------------------------------------------------
  199. // Composite trait
  200. // ---------------------------------------------------------------------------
  201. /// Async storage abstraction composing all sub-traits.
  202. pub trait Store:
  203. AccountStore + PostingStore + TransferStore + SagaStore + EventStore + JournalStore
  204. {
  205. }
  206. impl<T: AccountStore + PostingStore + TransferStore + SagaStore + EventStore + JournalStore> Store
  207. for T
  208. {
  209. }