atagen

macltok

public
Star0
Watch0
d9b96952ec6003585d618d852b489c48fc386d5d
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
harness/web/src/lib.rsRust209 lines7.0 KB
1
//! a browser-side slice of macltok, login through to a live sync, exported for
2
//! wasm-bindgen so both the worker tests and the size measurement reach the same code
3
4
use std::{pin::pin, sync::Arc};
5
6
use futures_channel::{mpsc, oneshot};
7
use futures_util::{
8
    StreamExt,
9
    future::{Either, select},
10
};
11
use gloo_timers::future::TimeoutFuture;
12
use macltok::{
13
    ClientHandle, EncryptedSessionStore, EngineConfig, Error, Event, EventStream, MessageContent,
14
    PasswordLogin, RoomMembership, SendState, Session, StoreConfig, TimelineContent, TimelineDiff,
15
    TimelineId, TimelineItem, imbl::Vector,
16
};
17
use wasm_bindgen::prelude::*;
18
19
const PHASE_TIMEOUT_MS: f64 = 30_000.0;
20
21
/// logs in, then proves a real sliding sync by finding the seeded room, reading the
22
/// seeded message out of its timeline, and seeing a message of its own come back from
23
/// the server
24
#[wasm_bindgen]
25
pub async fn smoke(
26
    homeserver: String,
27
    username: String,
28
    password: String,
29
    store_name: String,
30
    room_name: String,
31
    seeded: String,
32
) -> Result<(), JsError> {
33
    let sessions = Arc::new(EncryptedSessionStore::indexeddb(
34
        StoreConfig::indexeddb(store_name.clone(), "macltok-web"),
35
    ));
36
    let login = PasswordLogin::new(homeserver, username, password).allow_insecure_loopback(true);
37
    let session = Session::login_password(login, sessions).await?;
38
    let (engine, handle, events) = session.build(EngineConfig::default()).await?;
39
    let (panicked, panics) = mpsc::unbounded();
40
    macltok::on_panic(move |message| {
41
        let _ = panicked.unbounded_send(message.to_owned());
42
    });
43
    let mut events = Watched { events, panics };
44
    let (done, running) = oneshot::channel();
45
    wasm_bindgen_futures::spawn_local(async move {
46
        let _ = done.send(engine.run().await);
47
    });
48
49
    let mut phase = Phase::new("the seeded room to sync");
50
    let (_never, mut idle) = oneshot::channel();
51
    let room_id = loop {
52
        let rooms = handle.rooms().await;
53
        if let Some(room) = rooms
54
            .into_iter()
55
            .find(|room| room.membership == RoomMembership::Joined && room.name == room_name)
56
        {
57
            break room.room_id;
58
        }
59
        while !matches!(
60
            phase.next(&mut events, &mut idle).await?,
61
            Event::RoomsChanged
62
        ) {}
63
    };
64
    let timeline_id = TimelineId::from(room_id.clone());
65
66
    let mut phase = Phase::new("the seeded message in the opened timeline");
67
    let mut opening = command(
68
        &handle,
69
        |handle| async move { handle.open_room(room_id).await },
70
    );
71
    let mut items = None;
72
    while !contains(&items, &seeded, false) {
73
        let event = phase.next(&mut events, &mut opening).await?;
74
        track(&timeline_id, event, &mut items)?;
75
    }
76
77
    let sent = format!("{seeded}, answered from {store_name}");
78
    let mut phase = Phase::new("the sent message to come back from the server");
79
    let content = MessageContent::text(sent.clone());
80
    let target = timeline_id.clone();
81
    let mut sending = command(&handle, |handle| async move {
82
        handle.send_message(target, content).await
83
    });
84
    while !contains(&items, &sent, true) {
85
        let event = phase.next(&mut events, &mut sending).await?;
86
        track(&timeline_id, event, &mut items)?;
87
    }
88
89
    handle.shutdown().await?;
90
    while events.events.recv().await.is_some() {}
91
    running
92
        .await
93
        .map_err(|_| JsError::new("engine task vanished"))??;
94
    Ok(())
95
}
96
97
/// answers only arrive while the event stream is drained, so commands run beside it
98
fn command<F>(
99
    handle: &ClientHandle,
100
    run: impl FnOnce(ClientHandle) -> F,
101
) -> oneshot::Receiver<Result<(), Error>>
102
where
103
    F: Future<Output = Result<(), Error>> + 'static,
104
{
105
    let (answer, answered) = oneshot::channel();
106
    let pending = run(handle.clone());
107
    wasm_bindgen_futures::spawn_local(async move {
108
        let _ = answer.send(pending.await);
109
    });
110
    answered
111
}
112
113
/// a panicking engine task goes silent rather than stopping, and a phase would otherwise wait
114
/// out its whole timeout
115
struct Watched {
116
    events: EventStream,
117
    panics: mpsc::UnboundedReceiver<String>,
118
}
119
120
struct Phase {
121
    what: &'static str,
122
    deadline: f64,
123
}
124
125
impl Phase {
126
    fn new(what: &'static str) -> Self {
127
        Self {
128
            what,
129
            deadline: js_sys::Date::now() + PHASE_TIMEOUT_MS,
130
        }
131
    }
132
133
    async fn next(
134
        &mut self,
135
        watched: &mut Watched,
136
        pending: &mut oneshot::Receiver<Result<(), Error>>,
137
    ) -> Result<Event, JsError> {
138
        let left = (self.deadline - js_sys::Date::now()).max(0.0);
139
        let timeout = TimeoutFuture::new(left as u32);
140
        let panicked = watched.panics.next();
141
        let outcome = match select(pin!(watched.events.recv()), select(timeout, panicked)).await {
142
            Either::Left((event, _)) => Some(event),
143
            Either::Right((Either::Left(_), _)) => None,
144
            Either::Right((Either::Right((message, _)), _)) => {
145
                return Err(JsError::new(&format!(
146
                    "panicked while waiting for {}, {}",
147
                    self.what,
148
                    message.unwrap_or_default()
149
                )));
150
            }
151
        };
152
        if let Ok(Some(Err(error))) = pending.try_recv() {
153
            return Err(JsError::new(&format!("{} failed, {error}", self.what)));
154
        }
155
        match outcome {
156
            Some(Some(Event::Stopped) | None) => Err(JsError::new(&format!(
157
                "engine stopped while waiting for {}",
158
                self.what
159
            ))),
160
            Some(Some(Event::SessionSaveFailed { message })) => Err(JsError::new(&format!(
161
                "saving the session failed while waiting for {}, {message}",
162
                self.what
163
            ))),
164
            Some(Some(event)) => Ok(event),
165
            None => Err(JsError::new(&format!(
166
                "timed out after {}s waiting for {}",
167
                PHASE_TIMEOUT_MS / 1000.0,
168
                self.what
169
            ))),
170
        }
171
    }
172
}
173
174
fn track(
175
    timeline_id: &TimelineId,
176
    event: Event,
177
    items: &mut Option<Vector<TimelineItem>>,
178
) -> Result<(), JsError> {
179
    match event {
180
        Event::TimelineReset {
181
            timeline_id: id,
182
            items: snapshot,
183
        } if &id == timeline_id => *items = Some(snapshot),
184
        Event::TimelineChanged {
185
            timeline_id: id,
186
            diffs,
187
        } if &id == timeline_id => {
188
            if let Some(items) = items {
189
                TimelineDiff::try_apply_all(diffs, items)
190
                    .map_err(|error| JsError::new(&format!("timeline diff misfit, {error:?}")))?;
191
            }
192
        }
193
        _ => {}
194
    }
195
    Ok(())
196
}
197
198
fn contains(items: &Option<Vector<TimelineItem>>, body: &str, remote_echo: bool) -> bool {
199
    items.iter().flatten().any(|item| {
200
        let TimelineItem::Event(event) = item else {
201
            return false;
202
        };
203
        let TimelineContent::Message(message) = &event.content else {
204
            return false;
205
        };
206
        message.body == body
207
            && (!remote_echo || (event.event_id.is_some() && event.send_state == SendState::Sent))
208
    })
209
}