atagen

macltok

public
Star0
Watch0

Commit 1fe480c16a3c

tarball

atagen <boss@atagen.co>2026-09-27 01:42 UTC

10 files changed+194-99
engine: make the executor portable

Spawn, sleep and timeout go through matrix_sdk's executor, which is tokio natively and spawn_local with gloo timers on wasm, so the engine no longer assumes tokio's runtime. TaskSet stands in for JoinSet and aborts what's left on drop like JoinSet did, or a dead room actor would leave its redactions running and their callers would never hear Interrupted. Tasks catch their own panics so each comes back with its context, since joining through the sdk executor gives no task id to look it up by, and upload forwarders are pruned by their closed ready channel since the wasm JoinHandle can't tell a finished task from a live one.
Modified file: Cargo.toml+1 additions−1 deletions
@@ -21,7 +21,7 @@ serde = { version = "1.0.219", features = ["derive", "rc"], optional = true }
2121 thiserror = "2.0.16"
2222 mime = "0.3.17"
2323 sha2 = "0.10.9"
24-tokio = { version = "1.47.1", features = ["rt", "sync", "time"] }
24+tokio = { version = "1.47.1", features = ["macros", "sync"] }
2525 tracing = "0.1.41"
2626 url = { version = "2.5.7", optional = true }
2727
Modified file: src/encryption.rs+4 additions−3 deletions
@@ -1,6 +1,7 @@
11 use std::sync::Arc;
22
33 use futures_util::StreamExt;
4+use matrix_sdk::executor::{JoinHandle, spawn};
45 use matrix_sdk::{
56 Client,
67 encryption::{
@@ -14,7 +15,7 @@ use matrix_sdk::{
1415 ruma::events::key::verification::VerificationMethod,
1516 };
1617 use ruma::{OwnedDeviceId, UserId};
17-use tokio::{sync::Mutex, task::JoinHandle};
18+use tokio::sync::Mutex;
1819
1920 use crate::{
2021 EncryptionState, Error, ErrorKind, Event, KeyBackupState, RecoveryState, VerificationAction,
@@ -266,7 +267,7 @@ impl VerificationController {
266267 Error::with_source(ErrorKind::Interrupted, "event receiver has closed", source)
267268 })?;
268269 let active = self.active.clone();
269- self.task = Some(tokio::spawn(async move {
270+ self.task = Some(spawn(async move {
270271 follow_request(request, changes, active, sender).await;
271272 }));
272273 Ok(())
@@ -311,7 +312,7 @@ impl VerificationController {
311312 }
312313 }
313314 let active = self.active.clone();
314- self.task = Some(tokio::spawn(async move {
315+ self.task = Some(spawn(async move {
315316 follow_request(request, changes, active, sender).await;
316317 }));
317318 }
Added file: src/engine/executor.rs+82 additions
@@ -0,0 +1,82 @@
1+use std::future::Future;
2+
3+use futures_util::{StreamExt, stream::FuturesUnordered};
4+use matrix_sdk::SendOutsideWasm;
5+pub(super) use matrix_sdk::executor::{AbortHandle, JoinError, JoinHandle, spawn};
6+
7+#[cfg(not(target_family = "wasm"))]
8+pub(super) type EngineBoxFuture<'a, T> = futures_util::future::BoxFuture<'a, T>;
9+#[cfg(target_family = "wasm")]
10+pub(super) type EngineBoxFuture<'a, T> = futures_util::future::LocalBoxFuture<'a, T>;
11+
12+#[cfg(not(target_family = "wasm"))]
13+pub(super) fn is_panic(error: &JoinError) -> bool {
14+ error.is_panic()
15+}
16+
17+/// the sdk's wasm join error has no payload, though a panic there aborts the instance anyway
18+#[cfg(target_family = "wasm")]
19+pub(super) fn is_panic(error: &JoinError) -> bool {
20+ !error.is_cancelled()
21+}
22+
23+#[cfg(not(target_family = "wasm"))]
24+pub(super) fn resume_unwind(error: JoinError) -> ! {
25+ std::panic::resume_unwind(error.into_panic())
26+}
27+
28+#[cfg(target_family = "wasm")]
29+pub(super) fn resume_unwind(error: JoinError) -> ! {
30+ panic!("{error}")
31+}
32+
33+/// a `JoinSet` that runs on wasm too, and like it aborts whatever's left when dropped
34+pub(super) struct TaskSet<T: 'static> {
35+ handles: FuturesUnordered<JoinHandle<T>>,
36+}
37+
38+impl<T: 'static> TaskSet<T> {
39+ pub(super) fn new() -> Self {
40+ Self {
41+ handles: FuturesUnordered::new(),
42+ }
43+ }
44+
45+ pub(super) fn spawn(
46+ &mut self,
47+ task: impl Future<Output = T> + SendOutsideWasm + 'static,
48+ ) -> AbortHandle
49+ where
50+ T: SendOutsideWasm,
51+ {
52+ let handle = spawn(task);
53+ let abort = handle.abort_handle();
54+ self.handles.push(handle);
55+ abort
56+ }
57+
58+ pub(super) fn is_empty(&self) -> bool {
59+ self.handles.is_empty()
60+ }
61+
62+ pub(super) async fn join_next(&mut self) -> Option<Result<T, JoinError>> {
63+ self.handles.next().await
64+ }
65+
66+ fn abort_all(&self) {
67+ for handle in &self.handles {
68+ handle.abort();
69+ }
70+ }
71+
72+ pub(super) async fn shutdown(&mut self) {
73+ self.abort_all();
74+ while self.handles.next().await.is_some() {}
75+ }
76+}
77+
78+impl<T: 'static> Drop for TaskSet<T> {
79+ fn drop(&mut self) {
80+ self.abort_all();
81+ }
82+}
Modified file: src/engine/media.rs+15 additions−10 deletions
@@ -9,10 +9,10 @@ use std::{
99
1010 use futures_util::{
1111 FutureExt, StreamExt,
12- future::{BoxFuture, Shared, WeakShared},
12+ future::{Shared, WeakShared},
1313 };
1414 use matrix_sdk::{
15- Client, Room,
15+ Client, Room, SendOutsideWasm, SyncOutsideWasm,
1616 media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
1717 room::reply::{EnforceThread, Reply as SdkReply},
1818 ruma::{
@@ -31,6 +31,7 @@ use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
3131 use ruma::MxcUri;
3232 use tokio::sync::Mutex;
3333
34+use super::executor::EngineBoxFuture;
3435 use crate::{
3536 AttachmentUpload, Error, ErrorKind, Media, MediaId, MediaKind, MediaSize, TimelineId,
3637 normalise::{MediaRegistration, MediaScope},
@@ -42,7 +43,7 @@ const DOWNLOAD_IDLE: Duration = Duration::from_secs(60);
4243 /// the sdk client gives up on any request after thirty seconds, far too soon for a video
4344 const DOWNLOAD_TOTAL: Duration = Duration::from_secs(60 * 60);
4445
45-type FlightFuture = BoxFuture<'static, Result<Media, Error>>;
46+type FlightFuture = EngineBoxFuture<'static, Result<Media, Error>>;
4647 type Flight = Shared<FlightFuture>;
4748
4849 /// owns everything needed to turn a media id or an mxc uri into bytes, so the
@@ -188,7 +189,7 @@ impl MediaService {
188189 match flights.get(&key).and_then(WeakShared::upgrade) {
189190 Some(flight) => flight,
190191 None => {
191- let flight: Flight = AssertUnwindSafe(fetch(
192+ let prepared = AssertUnwindSafe(fetch(
192193 self.client.clone(),
193194 request,
194195 mimetype,
@@ -199,9 +200,9 @@ impl MediaService {
199200 result.unwrap_or_else(|_| {
200201 Err(Error::new(ErrorKind::Other, "media fetch panicked"))
201202 })
202- })
203- .boxed()
204- .shared();
203+ });
204+ let boxed: FlightFuture = Box::pin(prepared);
205+ let flight: Flight = boxed.shared();
205206 if let Some(weak) = flight.downgrade() {
206207 flights.insert(key, weak);
207208 }
@@ -335,7 +336,9 @@ async fn cached_media(
335336 client: &Client,
336337 request: &MediaRequestParameters,
337338 ) -> Result<Option<Vec<u8>>, Error> {
338- fn cache_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error {
339+ fn cache_failed(
340+ source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
341+ ) -> Error {
339342 Error::with_source(ErrorKind::Store, "reading media cache failed", source)
340343 }
341344 client
@@ -356,7 +359,9 @@ fn maybe_decrypt(
356359 let MediaSource::Encrypted(file) = &request.source else {
357360 return Ok(bytes);
358361 };
359- fn decrypt_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error {
362+ fn decrypt_failed(
363+ source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
364+ ) -> Error {
360365 Error::with_source(
361366 ErrorKind::MediaUnavailable,
362367 "media decryption failed",
@@ -447,7 +452,7 @@ async fn download_bounded(
447452 let mut stream = response.bytes_stream();
448453 let mut bytes = Vec::new();
449454 loop {
450- let chunk = tokio::time::timeout(DOWNLOAD_IDLE, stream.next())
455+ let chunk = matrix_sdk::timeout::timeout(stream.next(), DOWNLOAD_IDLE)
451456 .await
452457 .map_err(|_| Error::new(ErrorKind::Network, "media download stalled"))?;
453458 let Some(chunk) = chunk else { break };
Modified file: src/engine/mod.rs+51 additions−43 deletions
@@ -1,4 +1,5 @@
11 mod commands;
2+mod executor;
23 mod media;
34 mod mutations;
45 mod room;
@@ -8,23 +9,22 @@ mod watchers;
89 use std::{
910 collections::{HashMap, VecDeque},
1011 future::Future,
12+ panic::AssertUnwindSafe,
1113 sync::Arc,
1214 };
1315
14-use futures_util::future::BoxFuture;
15-use matrix_sdk::{Client, SessionChange};
16+use futures_util::FutureExt;
17+use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
1618 use matrix_sdk_ui::sync_service::SyncService;
1719 use ruma::OwnedRoomId;
1820 use thiserror::Error as ThisError;
19-use tokio::{
20- sync::{Notify, broadcast, mpsc, watch},
21- task::{AbortHandle, Id as TaskId, JoinError, JoinSet},
22-};
21+use tokio::sync::{Notify, broadcast, mpsc, watch};
2322
2423 pub(crate) use media::MediaService;
2524
2625 use self::{
2726 commands::Job,
27+ executor::{EngineBoxFuture, TaskSet},
2828 mutations::{Mutation, MutationJob},
2929 room::{ActorNote, ActorRequest, RoomActor},
3030 timeline::{SessionContext, Supervisor},
@@ -143,36 +143,49 @@ impl TaskContext {
143143 }
144144 }
145145
146+/// tasks catch their own panics, since the sdk's wasm join error drops the payload
147+fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
148+ payload
149+ .downcast_ref::<&str>()
150+ .map(ToString::to_string)
151+ .or_else(|| payload.downcast_ref::<String>().cloned())
152+ .unwrap_or_else(|| "task panicked with a non-string payload".to_owned())
153+}
154+
146155 struct TaskRegistry {
147- tasks: JoinSet<()>,
148- contexts: HashMap<TaskId, TaskContext>,
156+ tasks: TaskSet<(TaskContext, Option<String>)>,
149157 }
150158
151159 impl TaskRegistry {
152160 fn spawn(
153161 &mut self,
154162 context: TaskContext,
155- task: impl Future<Output = ()> + Send + 'static,
156- ) -> AbortHandle {
157- let handle = self.tasks.spawn(task);
158- self.contexts.insert(handle.id(), context);
159- handle
160- }
161-
162- async fn join_next(&mut self) -> Option<(TaskContext, Result<(), JoinError>)> {
163- let result = self.tasks.join_next_with_id().await?;
164- let id = match &result {
165- Ok((id, ())) => *id,
166- Err(error) => error.id(),
167- };
168- let context = self.contexts.remove(&id)?;
169- Some((context, result.map(drop)))
163+ task: impl Future<Output = ()> + SendOutsideWasm + 'static,
164+ ) {
165+ self.tasks.spawn(async move {
166+ let panic = AssertUnwindSafe(task)
167+ .catch_unwind()
168+ .await
169+ .err()
170+ .map(panic_message);
171+ (context, panic)
172+ });
173+ }
174+
175+ fn is_empty(&self) -> bool {
176+ self.tasks.is_empty()
177+ }
178+
179+ async fn join_next(&mut self) -> Option<(TaskContext, Option<String>)> {
180+ loop {
181+ if let Ok(done) = self.tasks.join_next().await? {
182+ return Some(done);
183+ }
184+ }
170185 }
171186
172187 async fn shutdown(&mut self) {
173- self.tasks.abort_all();
174- while self.tasks.join_next().await.is_some() {}
175- self.contexts.clear();
188+ self.tasks.shutdown().await;
176189 }
177190 }
178191
@@ -230,7 +243,7 @@ pub struct Engine {
230243 encryption: mpsc::Sender<Job<EncryptionCommand>>,
231244 queues: Option<Queues>,
232245 /// one progress forwarder per room that has uploaded, and whether it's subscribed yet
233- uploads: HashMap<OwnedRoomId, (AbortHandle, watch::Receiver<bool>)>,
246+ uploads: HashMap<OwnedRoomId, watch::Receiver<bool>>,
234247 /// the client can outlive us, so the handler has to come off it when we stop
235248 verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
236249 stop: Arc<Notify>,
@@ -283,8 +296,7 @@ impl Engine {
283296 config,
284297 media,
285298 tasks: TaskRegistry {
286- tasks: JoinSet::new(),
287- contexts: HashMap::new(),
299+ tasks: TaskSet::new(),
288300 },
289301 rooms: HashMap::new(),
290302 next_generation: 0,
@@ -309,7 +321,7 @@ impl Engine {
309321
310322 /// boxed because the engine's future is deep enough to blow the compiler's query depth
311323 /// in every crate that spawns it, which isn't something a client should have to fix
312- pub fn run(self) -> BoxFuture<'static, Result<(), EngineError>> {
324+ pub fn run(self) -> EngineBoxFuture<'static, Result<(), EngineError>> {
313325 Box::pin(self.run_inner())
314326 }
315327
@@ -346,13 +358,10 @@ impl Engine {
346358 break;
347359 }
348360 () = self.stop.notified() => break,
349- Some((context, result)) = self.tasks.join_next(), if !self.tasks.tasks.is_empty() => {
350- let Err(error) = result else { continue };
351- if !error.is_panic() {
352- continue;
353- }
361+ Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
362+ let Some(message) = panic else { continue };
354363 if let TaskContext::Command(label) = context {
355- tracing::error!(label, %error, "command task panicked");
364+ tracing::error!(label, panic = %message, "command task panicked");
356365 continue;
357366 }
358367 failure = Some(EngineError::TaskPanicked { task: context.label() });
@@ -392,7 +401,7 @@ impl Engine {
392401 self.verification_handler = Some(self.verification_handler(incoming));
393402 let client = &self.client;
394403 let events = || self.port.event_sender();
395- let watchers: [(&'static str, BoxFuture<'static, ()>); 11] = [
404+ let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 11] = [
396405 (
397406 "upload failure watcher",
398407 Box::pin(watchers::upload_failures(
@@ -606,7 +615,7 @@ impl Engine {
606615 &mut self,
607616 label: &'static str,
608617 reply: Responder,
609- work: impl Future<Output = Result<(), Error>> + Send + 'static,
618+ work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
610619 ) {
611620 self.tasks.spawn(TaskContext::Command(label), async move {
612621 reply.done(work.await);
@@ -630,18 +639,17 @@ impl Engine {
630639 }
631640
632641 fn upload_forwarder(&mut self, room: &matrix_sdk::Room) -> watch::Receiver<bool> {
633- self.uploads
634- .retain(|_, (forwarder, _)| !forwarder.is_finished());
635- if let Some((_, ready)) = self.uploads.get(room.room_id()) {
642+ self.uploads.retain(|_, ready| ready.has_changed().is_ok());
643+ if let Some(ready) = self.uploads.get(room.room_id()) {
636644 return ready.clone();
637645 }
638646 let (subscribed, ready) = watch::channel(false);
639- let forwarder = self.tasks.spawn(
647+ self.tasks.spawn(
640648 TaskContext::Command("upload watcher"),
641649 watchers::room_uploads(room.clone(), self.port.event_sender(), subscribed),
642650 );
643651 self.uploads
644- .insert(room.room_id().to_owned(), (forwarder, ready.clone()));
652+ .insert(room.room_id().to_owned(), ready.clone());
645653 ready
646654 }
647655
Modified file: src/engine/mutations.rs+4 additions−3 deletions
@@ -19,9 +19,10 @@ use matrix_sdk::{
1919 },
2020 };
2121 use ruma::{OwnedRoomId, OwnedUserId};
22-use tokio::{sync::mpsc, task::JoinSet};
22+use tokio::sync::mpsc;
2323
2424 use super::commands::{Job, guarded};
25+use super::executor::TaskSet;
2526 use crate::{
2627 CommandOutput, CreateRoomOptions, Error, ErrorKind, PowerLevelChanges, RoomCommand, RoomPreset,
2728 RoomVisibility,
@@ -56,7 +57,7 @@ pub(super) async fn run_scheduler(
5657 let fresh_dms = FreshDms::default();
5758 let mut waiting = HashMap::<Key, VecDeque<MutationJob>>::new();
5859 let mut running = HashSet::<Key>::new();
59- let mut tasks = JoinSet::<Key>::new();
60+ let mut tasks = TaskSet::<Key>::new();
6061 let mut queued = 0_usize;
6162 let mut independent = 0_u64;
6263 let mut receiving = true;
@@ -150,7 +151,7 @@ fn key(mutation: &Mutation, server: Option<&ruma::ServerName>) -> Option<Key> {
150151 type FreshDms = Arc<std::sync::Mutex<HashMap<OwnedUserId, OwnedRoomId>>>;
151152
152153 fn spawn(
153- tasks: &mut JoinSet<Key>,
154+ tasks: &mut TaskSet<Key>,
154155 client: &Client,
155156 fresh_dms: &FreshDms,
156157 key: Key,
Modified file: src/engine/room.rs+11 additions−14 deletions
@@ -3,11 +3,9 @@ use std::collections::{HashMap, VecDeque};
33 use matrix_sdk::{Room, RoomState};
44 use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineFocus, TimelineReadReceiptTracking};
55 use ruma::{OwnedEventId, OwnedRoomId};
6-use tokio::{
7- sync::mpsc,
8- task::{AbortHandle, JoinSet},
9-};
6+use tokio::sync::mpsc;
107
8+use super::executor::{AbortHandle, JoinError, TaskSet, is_panic, resume_unwind};
119 use super::timeline::{SessionContext, TaskGuard, TimelineSession};
1210 use crate::{
1311 Error, ErrorKind, Event, TimelineCommand, TimelineId, TypingState, transport::Responder,
@@ -100,7 +98,7 @@ impl RoomActor {
10098 let mut timelines = Timelines {
10199 room: room_timeline,
102100 threads: Threads::new(self.requests.max_capacity()),
103- work: JoinSet::new(),
101+ work: TaskSet::new(),
104102 };
105103 let close = self.serve_open(&mut timelines).await;
106104 if close.is_none() {
@@ -150,7 +148,7 @@ impl RoomActor {
150148 }
151149 Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
152150 Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
153- if let Err(error) = done && error.is_panic() {
151+ if let Err(error) = done && is_panic(&error) {
154152 tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
155153 }
156154 }
@@ -209,7 +207,7 @@ async fn watch_typing(
209207 struct Timelines {
210208 room: TimelineSession,
211209 threads: Threads,
212- work: JoinSet<()>,
210+ work: TaskSet<()>,
213211 }
214212
215213 impl Timelines {
@@ -262,7 +260,7 @@ impl Timelines {
262260
263261 async fn thread_built(
264262 &mut self,
265- built: Result<Built, tokio::task::JoinError>,
263+ built: Result<Built, JoinError>,
266264 room_id: &OwnedRoomId,
267265 context: &SessionContext,
268266 ) {
@@ -324,7 +322,7 @@ struct OpeningThread {
324322 struct Threads {
325323 open: HashMap<OwnedEventId, TimelineSession>,
326324 opening: HashMap<OwnedEventId, OpeningThread>,
327- builds: JoinSet<Built>,
325+ builds: TaskSet<Built>,
328326 next_generation: u64,
329327 queued: usize,
330328 capacity: usize,
@@ -335,7 +333,7 @@ impl Threads {
335333 Self {
336334 open: HashMap::new(),
337335 opening: HashMap::new(),
338- builds: JoinSet::new(),
336+ builds: TaskSet::new(),
339337 next_generation: 0,
340338 queued: 0,
341339 capacity,
@@ -403,7 +401,7 @@ impl Threads {
403401 /// `None` when the build belongs to a thread that was closed while it ran
404402 fn built(
405403 &mut self,
406- built: Result<Built, tokio::task::JoinError>,
404+ built: Result<Built, JoinError>,
407405 ) -> Option<(
408406 OwnedEventId,
409407 Queued,
@@ -412,7 +410,7 @@ impl Threads {
412410 let (root, generation, timeline) = match built {
413411 Ok(built) => built,
414412 Err(error) if error.is_cancelled() => return None,
415- Err(error) => std::panic::resume_unwind(error.into_panic()),
413+ Err(error) => resume_unwind(error),
416414 };
417415 if self
418416 .opening
@@ -443,8 +441,7 @@ impl Threads {
443441 }
444442
445443 async fn close(&mut self) {
446- self.builds.abort_all();
447- while self.builds.join_next().await.is_some() {}
444+ self.builds.shutdown().await;
448445 self.opening.clear();
449446 for (_, session) in self.open.drain() {
450447 session.close().await;
Modified file: src/engine/timeline.rs+10 additions−11 deletions
@@ -1,6 +1,7 @@
1-use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc};
1+use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, sync::Arc};
22
33 use futures_util::{FutureExt, StreamExt};
4+use matrix_sdk::SendOutsideWasm;
45 use matrix_sdk::{
56 room::edit::EditedContent,
67 room::reply::{EnforceThread, Reply as SdkReply},
@@ -16,11 +17,9 @@ use matrix_sdk::{
1617 };
1718 use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};
1819 use ruma::{OwnedEventId, OwnedUserId};
19-use tokio::{
20- sync::mpsc,
21- task::{JoinHandle, JoinSet},
22-};
20+use tokio::sync::mpsc;
2321
22+use super::executor::{EngineBoxFuture, JoinHandle, TaskSet, spawn};
2423 use crate::{
2524 Error, ErrorKind, Event, EventRef, HistoryState, MessageContent, MessageEditContent,
2625 MessageMentions, RichText, TimelineCommand, TimelineId,
@@ -44,10 +43,10 @@ impl Supervisor {
4443 pub(super) fn spawn(
4544 &self,
4645 label: &'static str,
47- task: impl Future<Output = ()> + Send + 'static,
46+ task: impl Future<Output = ()> + SendOutsideWasm + 'static,
4847 ) -> JoinHandle<()> {
4948 let panics = self.0.clone();
50- tokio::spawn(async move {
49+ spawn(async move {
5150 if AssertUnwindSafe(task).catch_unwind().await.is_err() {
5251 let _ = panics.send(label);
5352 }
@@ -166,11 +165,11 @@ impl TimelineSession {
166165 &self,
167166 command: TimelineCommand,
168167 reply: Responder,
169- work: &mut JoinSet<()>,
168+ work: &mut TaskSet<()>,
170169 ) {
171170 let timeline = self.timeline.clone();
172171 let thread_root = self.id.thread_root().cloned();
173- let background: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> = match command {
172+ let background: EngineBoxFuture<'static, Result<(), Error>> = match command {
174173 TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),
175174 // network-bound, so it runs beside the actor instead of queueing the room behind it
176175 TimelineCommand::Redact { target, reason, .. } => Box::pin(async move {
@@ -331,7 +330,7 @@ fn item_id(target: EventRef) -> TimelineEventItemId {
331330 struct ReplyFetcher {
332331 timeline: Arc<Timeline>,
333332 asked: HashSet<OwnedEventId>,
334- fetches: JoinSet<()>,
333+ fetches: TaskSet<()>,
335334 }
336335
337336 impl ReplyFetcher {
@@ -369,7 +368,7 @@ async fn start_watcher(
369368 let mut replies = ReplyFetcher {
370369 timeline: timeline.clone(),
371370 asked: HashSet::new(),
372- fetches: JoinSet::new(),
371+ fetches: TaskSet::new(),
373372 };
374373 replies.fetch(normalised.missing_replies);
375374 let id = id.clone();
Modified file: src/engine/watchers.rs+7 additions−11 deletions
@@ -1,7 +1,4 @@
1-use std::{
2- sync::Arc,
3- time::{Duration, Instant},
4-};
1+use std::{sync::Arc, time::Duration};
52
63 use futures_util::StreamExt;
74 use matrix_sdk::{
@@ -10,11 +7,10 @@ use matrix_sdk::{
107 send_queue::{LocalEchoContent, RoomSendQueueUpdate},
118 };
129 use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
13-use tokio::{
14- sync::{Notify, broadcast::error::RecvError, watch},
15- task::JoinSet,
16-};
10+use ruma::time::Instant;
11+use tokio::sync::{Notify, broadcast::error::RecvError, watch};
1712
13+use super::executor::TaskSet;
1814 use crate::{
1915 ConnectionState, Event, encryption::encryption_state, error::classify_sdk_error,
2016 transport::EventSender,
@@ -106,7 +102,7 @@ pub(super) async fn connection(
106102 }
107103 if restart {
108104 sync.stop().await;
109- tokio::time::sleep(backoff).await;
105+ matrix_sdk::sleep::sleep(backoff).await;
110106 backoff = (backoff * 2).min(SYNC_RESTART_MAX);
111107 sync.start().await;
112108 }
@@ -119,7 +115,7 @@ pub(super) async fn send_queue_errors(
119115 mut errors: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueRoomError>,
120116 online: watch::Receiver<ConnectionState>,
121117 ) {
122- let mut retries = JoinSet::new();
118+ let mut retries = TaskSet::new();
123119 loop {
124120 let error = tokio::select! {
125121 error = errors.recv() => error,
@@ -130,7 +126,7 @@ pub(super) async fn send_queue_errors(
130126 let client = client.clone();
131127 let online = online.clone();
132128 retries.spawn(async move {
133- tokio::time::sleep(SEND_QUEUE_RETRY).await;
129+ matrix_sdk::sleep::sleep(SEND_QUEUE_RETRY).await;
134130 if *online.borrow() == ConnectionState::Online
135131 && let Some(room) = client.get_room(&error.room_id)
136132 {
Modified file: src/error.rs+9 additions−3 deletions
@@ -1,7 +1,7 @@
11 use std::sync::Arc;
22
33 use matrix_sdk::{
4- HttpError, RefreshTokenError,
4+ HttpError, RefreshTokenError, SendOutsideWasm, SyncOutsideWasm,
55 authentication::oauth::error::{
66 OAuthAuthorizationCodeError, OAuthDiscoveryError, OAuthError, OAuthTokenRevocationError,
77 RequestTokenError,
@@ -17,6 +17,12 @@ use serde::{Deserialize, Serialize};
1717
1818 use crate::InteractiveAuthChallenge;
1919
20+/// the sdk's `SendOutsideWasm` isn't an auto trait, so it can't go in a trait object
21+#[cfg(not(target_family = "wasm"))]
22+type SourceError = dyn std::error::Error + Send + Sync;
23+#[cfg(target_family = "wasm")]
24+type SourceError = dyn std::error::Error;
25+
2026 /// the one thing that goes wrong, for commands and queries alike. match on `kind`, show `message`
2127 #[derive(Clone, Debug, thiserror::Error)]
2228 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -29,7 +35,7 @@ pub struct Error {
2935 pub challenge: Option<Box<InteractiveAuthChallenge>>,
3036 #[source]
3137 #[cfg_attr(feature = "serde", serde(skip))]
32- source: Option<Arc<dyn std::error::Error + Send + Sync>>,
38+ source: Option<Arc<SourceError>>,
3339 }
3440
3541 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -75,7 +81,7 @@ impl Error {
7581 pub(crate) fn with_source(
7682 kind: ErrorKind,
7783 message: impl Into<String>,
78- source: impl std::error::Error + Send + Sync + 'static,
84+ source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
7985 ) -> Self {
8086 Self {
8187 source: Some(Arc::new(source)),