Skip to main content

tuwunel_service/transaction_ids/
mod.rs

1//! Durable transaction-response deduplication.
2//!
3//! Opaque response bytes are keyed by user, optional device, and transaction
4//! identifier. Room-send operations add a domain tag, room, and event type so
5//! independent send scopes cannot replay one another's responses.
6
7use std::sync::Arc;
8
9use ruma::{DeviceId, RoomId, TransactionId, UserId};
10use tuwunel_core::{Result, implement};
11use tuwunel_database::{Handle, Map};
12
13/// Persistent transaction-response lookup service.
14///
15/// Responses remain stored across process restarts and have no service-level
16/// expiry. Adding a response for an existing key replaces the stored bytes.
17/// Lookup and insertion are separate operations; the service does not reserve a
18/// transaction identifier against concurrent execution.
19pub struct Service {
20	db: Data,
21}
22
23struct Data {
24	userdevicetxnid_response: Arc<Map>,
25}
26
27type Key<'a> = (&'a UserId, Option<&'a DeviceId>, &'a TransactionId);
28type RoomKey<'a> = (
29	&'a UserId,
30	Option<&'a DeviceId>,
31	&'a TransactionId,
32	&'static str,
33	&'a RoomId,
34	&'a str,
35);
36
37const ROOM_SEND_TAG: &str = "room-send";
38
39impl crate::Service for Service {
40	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
41		Ok(Arc::new(Self {
42			db: Data {
43				userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
44			},
45		}))
46	}
47
48	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
49}
50
51#[implement(Service)]
52/// Records a response under the legacy transaction scope.
53///
54/// The key contains the user, optional device, and transaction identifier.
55/// Writing the same key again replaces its opaque response bytes while
56/// preserving the original non-room key layout.
57pub fn add_txnid(
58	&self,
59	user_id: &UserId,
60	device_id: Option<&DeviceId>,
61	txn_id: &TransactionId,
62	data: &[u8],
63) {
64	let key = txnid_key(user_id, device_id, txn_id);
65
66	self.db
67		.userdevicetxnid_response
68		.put_raw(key, data);
69}
70
71#[implement(Service)]
72/// Looks up a response under the legacy transaction scope.
73///
74/// The key contains the user, optional device, and transaction identifier.
75/// A missing row remains a database not-found error, and a hit returns the
76/// stored opaque bytes through a database handle.
77pub async fn existing_txnid(
78	&self,
79	user_id: &UserId,
80	device_id: Option<&DeviceId>,
81	txn_id: &TransactionId,
82) -> Result<Handle<'_>> {
83	let key = txnid_key(user_id, device_id, txn_id);
84
85	self.db.userdevicetxnid_response.qry(&key).await
86}
87
88#[implement(Service)]
89/// Records a response under the scoped room-send transaction key.
90///
91/// A domain tag, room, and event type extend the legacy key so unrelated send
92/// scopes cannot alias. Writing the same complete key again replaces its
93/// opaque response bytes.
94pub fn add_room_txnid(
95	&self,
96	user_id: &UserId,
97	device_id: Option<&DeviceId>,
98	txn_id: &TransactionId,
99	room_id: &RoomId,
100	event_type: &str,
101	data: &[u8],
102) {
103	let key = room_txnid_key(user_id, device_id, txn_id, room_id, event_type);
104
105	self.db
106		.userdevicetxnid_response
107		.put_raw(key, data);
108}
109
110#[implement(Service)]
111/// Looks up a response under the scoped room-send transaction key.
112///
113/// A domain tag, room, and event type extend the legacy key so unrelated send
114/// scopes cannot alias. A missing row remains a database not-found error, and
115/// a hit returns the stored opaque bytes through a database handle.
116pub async fn existing_room_txnid(
117	&self,
118	user_id: &UserId,
119	device_id: Option<&DeviceId>,
120	txn_id: &TransactionId,
121	room_id: &RoomId,
122	event_type: &str,
123) -> Result<Handle<'_>> {
124	let key = room_txnid_key(user_id, device_id, txn_id, room_id, event_type);
125
126	self.db.userdevicetxnid_response.qry(&key).await
127}
128
129fn txnid_key<'a>(
130	user_id: &'a UserId,
131	device_id: Option<&'a DeviceId>,
132	txn_id: &'a TransactionId,
133) -> Key<'a> {
134	(user_id, device_id, txn_id)
135}
136
137fn room_txnid_key<'a>(
138	user_id: &'a UserId,
139	device_id: Option<&'a DeviceId>,
140	txn_id: &'a TransactionId,
141	room_id: &'a RoomId,
142	event_type: &'a str,
143) -> RoomKey<'a> {
144	(user_id, device_id, txn_id, ROOM_SEND_TAG, room_id, event_type)
145}
146
147#[cfg(test)]
148mod tests;