44use core:: cell:: { LazyCell , OnceCell , RefCell } ;
55use core:: ops:: Deref ;
66use spacetimedb_lib:: bsatn;
7+ use std:: rc:: Rc ;
78
89#[ cfg( feature = "unstable" ) ]
910mod client_visibility_filter;
@@ -888,8 +889,6 @@ pub struct ViewContext {
888889/// number generation.
889890///
890891/// Implements the `DbContext` trait for accessing views into a database.
891- /// Currently, being this generic is only meaningful in clients,
892- /// as `ReducerContext` is the only implementor of `DbContext` within modules.
893892#[ non_exhaustive]
894893pub struct ReducerContext {
895894 /// The `Identity` of the client that invoked the reducer.
@@ -904,6 +903,8 @@ pub struct ReducerContext {
904903 /// including `init` and scheduled reducers.
905904 pub connection_id : Option < ConnectionId > ,
906905
906+ sender_auth : AuthCtx ,
907+
907908 /// Allows accessing the local database attached to a module.
908909 ///
909910 /// This slightly strange type appears to have no methods, but that is misleading.
@@ -942,8 +943,6 @@ pub struct ReducerContext {
942943 /// See the [`#[table]`](macro@crate::table) macro for more information.
943944 pub db : Local ,
944945
945- sender_auth : AuthCtx ,
946-
947946 #[ cfg( feature = "rand08" ) ]
948947 rng : std:: cell:: OnceCell < StdbRng > ,
949948}
@@ -964,16 +963,12 @@ impl ReducerContext {
964963
965964 #[ doc( hidden) ]
966965 fn new ( db : Local , sender : Identity , connection_id : Option < ConnectionId > , timestamp : Timestamp ) -> Self {
967- let sender_auth = match connection_id {
968- Some ( cid) => AuthCtx :: from_connection_id ( cid) ,
969- None => AuthCtx :: internal ( ) ,
970- } ;
971966 Self {
972967 db,
973968 sender,
974969 timestamp,
975970 connection_id,
976- sender_auth,
971+ sender_auth : AuthCtx :: from_connection_id_opt ( connection_id ) ,
977972 #[ cfg( feature = "rand08" ) ]
978973 rng : std:: cell:: OnceCell :: new ( ) ,
979974 }
@@ -1011,6 +1006,57 @@ impl ReducerContext {
10111006 }
10121007}
10131008
1009+ /// The context that an anonymous transaction
1010+ /// in [`ProcedureContext::with_transaction`] is provided with.
1011+ ///
1012+ /// Includes information about the client starting the transaction
1013+ /// and the time of the procedure/reducer,
1014+ /// as well as a view into the module's database.
1015+ ///
1016+ /// If the crate was compiled with the `rand` feature, also includes faculties for random
1017+ /// number generation.
1018+ ///
1019+ /// Implements the `DbContext` trait for accessing views into a database.
1020+ pub struct TxContext ( ReducerContext ) ;
1021+
1022+ impl AsRef < ReducerContext > for TxContext {
1023+ fn as_ref ( & self ) -> & ReducerContext {
1024+ & self . 0
1025+ }
1026+ }
1027+
1028+ impl Deref for TxContext {
1029+ type Target = ReducerContext ;
1030+
1031+ fn deref ( & self ) -> & Self :: Target {
1032+ & self . 0
1033+ }
1034+ }
1035+
1036+ /// Values which knows whether they signify an ok state as opposed to error.
1037+ pub trait IsOk {
1038+ /// Returns whether the current state of `self` is "ok".
1039+ fn is_ok ( & self ) -> bool ;
1040+ }
1041+
1042+ impl IsOk for ( ) {
1043+ fn is_ok ( & self ) -> bool {
1044+ true
1045+ }
1046+ }
1047+
1048+ impl < T > IsOk for Option < T > {
1049+ fn is_ok ( & self ) -> bool {
1050+ self . is_some ( )
1051+ }
1052+ }
1053+
1054+ impl < T , E > IsOk for Result < T , E > {
1055+ fn is_ok ( & self ) -> bool {
1056+ self . is_ok ( )
1057+ }
1058+ }
1059+
10141060/// The context that any procedure is provided with.
10151061///
10161062/// Each procedure must accept `&mut ProcedureContext` as its first argument.
@@ -1074,6 +1120,65 @@ impl ProcedureContext {
10741120 let new_time = Timestamp :: from_micros_since_unix_epoch ( new_time) ;
10751121 self . timestamp = new_time;
10761122 }
1123+
1124+ /// Acquire a mutable transaction
1125+ /// and execute `body` with read-write access to the database.
1126+ ///
1127+ /// When `body().is_ok()`,
1128+ /// the transaction will be committed and its mutations persisted.
1129+ /// When `!body().is_ok()`,
1130+ /// the transaction will be rolled back and its mutations discarded.
1131+ ///
1132+ /// Regardless of the transaction's success or failure,
1133+ /// the return value of `body` is not persisted to the commitlog
1134+ /// or broadcast to subscribed clients.
1135+ /// Clients attribute mutations performed by this transaction to `Event::UnknownTransaction`.
1136+ ///
1137+ /// If the transaction fails to commit after `body` returns,
1138+ /// e.g., due to a conflict with a concurrent transaction,
1139+ /// this method will re-invoke `body` with a new transaction in order to retry.
1140+ /// This is done once. On the second failure, a panic will occur.
1141+ pub fn with_transaction < R : IsOk > ( & mut self , body : impl Fn ( & TxContext ) -> R ) -> R {
1142+ let run = || {
1143+ // Start the transaction.
1144+ let timestamp = sys:: procedure:: procedure_start_mut_transaction ( ) . expect (
1145+ "holding `&mut ProcedureContext`, so should not be in a tx already; called manually elsewhere?" ,
1146+ ) ;
1147+ let timestamp = Timestamp :: from_micros_since_unix_epoch ( timestamp) ;
1148+
1149+ // We've resumed, so do the work.
1150+ let tx = ReducerContext :: new ( Local { } , self . sender , self . connection_id , timestamp) ;
1151+ let tx = TxContext ( tx) ;
1152+ body ( & tx)
1153+ } ;
1154+
1155+ let mut res = run ( ) ;
1156+ let abort = || {
1157+ sys:: procedure:: procedure_abort_mut_transaction ( )
1158+ . expect ( "should have a pending mutable anon tx as `procedure_start_mut_transaction` preceded" )
1159+ } ;
1160+
1161+ // Commit or roll back?
1162+ if res. is_ok ( ) {
1163+ if sys:: procedure:: procedure_commit_mut_transaction ( ) . is_err ( ) {
1164+ log:: warn!( "committing anonymous transaction failed" ) ;
1165+
1166+ // NOTE(procedure,centril): there's no actual guarantee that `body`
1167+ // does the exact same as the time before, as the timestamps differ
1168+ // and due to interior mutability.
1169+ res = run ( ) ;
1170+ if res. is_ok ( ) {
1171+ sys:: procedure:: procedure_commit_mut_transaction ( ) . expect ( "transaction retry failed again" )
1172+ } else {
1173+ abort ( ) ;
1174+ }
1175+ }
1176+ } else {
1177+ abort ( ) ;
1178+ }
1179+
1180+ res
1181+ }
10771182}
10781183
10791184/// A handle on a database with a particular table schema.
@@ -1102,9 +1207,16 @@ impl DbContext for ReducerContext {
11021207 }
11031208}
11041209
1105- // `ProcedureContext` is *not* a `DbContext`. We will add a `TxContext`
1106- // which can be obtained from `ProcedureContext::start_tx`,
1107- // and that will be a `DbContext`.
1210+ impl DbContext for TxContext {
1211+ type DbView = Local ;
1212+
1213+ fn db ( & self ) -> & Self :: DbView {
1214+ & self . db
1215+ }
1216+ }
1217+
1218+ // `ProcedureContext` is *not* a `DbContext`
1219+ // but a `TxContext` derived from it is.
11081220
11091221/// Allows accessing the local database attached to the module.
11101222///
@@ -1125,18 +1237,25 @@ pub struct JwtClaims {
11251237}
11261238
11271239/// Authentication information for the caller of a reducer.
1240+ #[ derive( Clone ) ]
11281241pub struct AuthCtx {
11291242 is_internal : bool ,
11301243 // NOTE(jsdt): cannot directly use a `LazyCell` without making this struct generic,
11311244 // which would cause `ReducerContext` to become generic as well.
1132- jwt : Box < dyn Deref < Target = Option < JwtClaims > > > ,
1245+ jwt : Rc < dyn Deref < Target = Option < JwtClaims > > > ,
11331246}
11341247
11351248impl AuthCtx {
1249+ /// Creates an [`AuthCtx`] both for cases where there's a [`ConnectionId`]
1250+ /// and for when there isn't.
1251+ fn from_connection_id_opt ( conn_id : Option < ConnectionId > ) -> Self {
1252+ conn_id. map ( Self :: from_connection_id) . unwrap_or_else ( Self :: internal)
1253+ }
1254+
11361255 fn new ( is_internal : bool , jwt_fn : impl FnOnce ( ) -> Option < JwtClaims > + ' static ) -> Self {
11371256 AuthCtx {
11381257 is_internal,
1139- jwt : Box :: new ( LazyCell :: new ( jwt_fn) ) ,
1258+ jwt : Rc :: new ( LazyCell :: new ( jwt_fn) ) ,
11401259 }
11411260 }
11421261
0 commit comments