1use std::sync::Arc;
23use async_trait::async_trait;
4use core_crypto::prelude::{ConversationId, Obfuscated};
5#[cfg(target_family = "wasm")]
6use js_sys::{Promise, Uint8Array};
7#[cfg(target_family = "wasm")]
8use log::kv;
9#[cfg(target_family = "wasm")]
10use wasm_bindgen::prelude::*;
11#[cfg(target_family = "wasm")]
12use wasm_bindgen_futures::JsFuture;
1314use crate::{CoreCrypto, CoreCryptoError, CoreCryptoResult};
1516#[cfg(not(target_family = "wasm"))]
17#[derive(Debug, thiserror::Error, uniffi::Error)]
18#[uniffi(flat_error)]
19pub enum EpochChangedReportingError {
20#[error("panic or otherwise unexpected error from foreign code")]
21Ffi(#[from] uniffi::UnexpectedUniFFICallbackError),
22}
2324/// An `EpochObserver` is notified whenever a conversation's epoch changes.
25#[cfg(not(target_family = "wasm"))]
26#[uniffi::export(with_foreign)]
27#[async_trait]
28pub trait EpochObserver: Send + Sync {
29/// This function will be called every time a conversation's epoch changes.
30 ///
31 /// The `epoch` parameter is the new epoch.
32 ///
33 /// <div class="warning">
34 /// This function must not block! Foreign implementors of this inteface can
35 /// spawn a task indirecting the notification, or (unblocking) send the notification
36 /// on some kind of channel, or anything else, as long as the operation completes
37 /// quickly.
38 /// </div>
39 ///
40 /// Though the signature includes an error type, that error is only present because
41 /// it is required by `uniffi` in order to handle panics. This function should suppress
42 /// and ignore internal errors instead of propagating them, to the maximum extent possible.
43async fn epoch_changed(
44&self,
45 conversation_id: ConversationId,
46 epoch: u64,
47 ) -> Result<(), EpochChangedReportingError>;
48}
4950/// This shim bridges the public `EpochObserver` interface with the internal one defined by `core-crypto`.
51///
52/// This is slightly unfortunate, as it introduces an extra layer of indirection before a change notice can
53/// actually reach its foreign target. However, the orphan rule prevents us from just tying the two traits
54/// together directly, so this is the straightforward way to accomplish that.
55#[cfg(not(target_family = "wasm"))]
56struct ObserverShim(Arc<dyn EpochObserver>);
5758#[cfg(not(target_family = "wasm"))]
59#[async_trait]
60impl core_crypto::mls::EpochObserver for ObserverShim {
61async fn epoch_changed(&self, conversation_id: ConversationId, epoch: u64) {
62if let Err(err) = self.0.epoch_changed(conversation_id.clone(), epoch).await {
63// we don't _care_ if an error is thrown by the the notification function, per se,
64 // but this would probably be useful information for downstream debugging efforts
65log::warn!(
66 conversation_id = Obfuscated::new(&conversation_id),
67 epoch,
68 err = log::kv::Value::from_dyn_error(&err);
69"caught an error when attempting to notify the epoch observer of an epoch change"
70);
71 }
72 }
73}
7475#[cfg(not(target_family = "wasm"))]
76#[uniffi::export]
77impl CoreCrypto {
78/// Add an epoch observer to this client.
79 ///
80 /// This function should be called 0 or 1 times in a client's lifetime.
81 /// If called when an epoch observer already exists, this will return an error.
82pub async fn register_epoch_observer(&self, epoch_observer: Arc<dyn EpochObserver>) -> CoreCryptoResult<()> {
83let shim = Arc::new(ObserverShim(epoch_observer));
84self.inner
85 .register_epoch_observer(shim)
86 .await
87.map_err(CoreCryptoError::generic())
88 }
89}
9091/// An `EpochObserver` is notified whenever a conversation's epoch changes.
92#[cfg(target_family = "wasm")]
93#[wasm_bindgen]
94pub struct EpochObserver {
95 this_context: JsValue,
96 epoch_changed: js_sys::Function,
97}
9899#[cfg(target_family = "wasm")]
100// SAFETY: we promise that we're only ever using this in a single-threaded context
101unsafe impl Send for EpochObserver {}
102#[cfg(target_family = "wasm")]
103// SAFETY: we promise that we're only ever using this in a single-threaded context
104unsafe impl Sync for EpochObserver {}
105106#[cfg(target_family = "wasm")]
107#[wasm_bindgen]
108impl EpochObserver {
109/// Create a new Epoch Observer.
110 ///
111 /// This function should be hidden on the JS side of things! The JS bindings should have an `interface EpochObserver`
112 /// which has the method defined, and the bindings themselves should destructure an instance implementing that
113 /// interface appropriately to construct this.
114 ///
115 /// - `this_context` is the instance itself, which will be bound to `this` within the function bodies
116 /// - `epoch_changed`: A function of the form `(conversation_id: Uint8Array, epoch: bigint) -> Promise<void>`.
117 /// Called every time a conversation's epoch changes.
118#[wasm_bindgen(constructor)]
119pub fn new(this_context: JsValue, epoch_changed: js_sys::Function) -> CoreCryptoResult<Self> {
120// we can't do much type-checking here unfortunately, but we can at least validate that the incoming functions have the right length
121if epoch_changed.length() != 2 {
122return Err(CoreCryptoError::ad_hoc(format!(
123"`epoch_changed` must accept 2 arguments but accepts {}",
124 epoch_changed.length()
125 )));
126 }
127Ok(Self {
128 this_context,
129 epoch_changed,
130 })
131 }
132}
133134#[cfg(target_family = "wasm")]
135impl EpochObserver {
136/// Call the JS `epoch_observed` function
137 ///
138 /// This blocks if the JS side of things blocks.
139 ///
140 /// This is extracted as its own function instead of being implemented inline within the
141 /// `impl EpochObserver for EpochObserver` block mostly to consolidate error-handling.
142async fn epoch_changed(&self, conversation_id: ConversationId, epoch: u64) -> Result<(), JsValue> {
143let converation_id = Uint8Array::from(conversation_id.as_slice());
144145let promise = self
146.epoch_changed
147 .call2(&self.this_context, &converation_id.into(), &epoch.into())?
148.dyn_into::<Promise>()?;
149// we don't actually care what the result of executing the notification promise is; we'll ignore it if it exists
150JsFuture::from(promise).await?;
151Ok(())
152 }
153}
154155#[cfg(target_family = "wasm")]
156#[async_trait(?Send)]
157impl core_crypto::mls::EpochObserver for EpochObserver {
158async fn epoch_changed(&self, conversation_id: ConversationId, epoch: u64) {
159if let Err(err) = self.epoch_changed(conversation_id.clone(), epoch).await {
160// we don't _care_ if an error is thrown by the the notification function, per se,
161 // but this would probably be useful information for downstream debugging efforts
162log::warn!(
163 conversation_id = Obfuscated::new(&conversation_id),
164 epoch,
165 err = LoggableJsValue(err);
166"caught an error when attempting to notify the epoch observer of an epoch change"
167);
168 }
169 }
170}
171172#[cfg(target_family = "wasm")]
173#[wasm_bindgen]
174impl CoreCrypto {
175/// Add an epoch observer to this client.
176 ///
177 /// This function should be called 0 or 1 times in a client's lifetime.
178 /// If called when an epoch observer already exists, this will return an error.
179pub async fn register_epoch_observer(&self, epoch_observer: EpochObserver) -> CoreCryptoResult<()> {
180self.inner
181 .register_epoch_observer(Arc::new(epoch_observer))
182 .await
183.map_err(CoreCryptoError::generic())
184 }
185}
186187#[cfg(target_family = "wasm")]
188struct LoggableJsValue(JsValue);
189190#[cfg(target_family = "wasm")]
191impl kv::ToValue for LoggableJsValue {
192fn to_value(&self) -> kv::Value<'_> {
193// can't get a borrowed str from `JsValue`, so can't directly
194 // convert into a string; oh well; fallback should catch it
195if let Some(f) = self.0.as_f64() {
196return f.into();
197 }
198if let Some(b) = self.0.as_bool() {
199return b.into();
200 }
201if self.0.is_null() || self.0.is_undefined() {
202return kv::Value::null();
203 }
204 kv::Value::from_debug(&self.0)
205 }
206}