atagen

macltok

public
Star0
Watch0
f5bb08c6a471967d659c8fa95d53691249ced9f5
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: harness/web/src/lib.rs

55a1b8252814atagen2026-09-26 09:08 UTC1
//! a browser-side slice of macltok, login through to a live sync, exported for
7aa42f589ee2atagen2026-09-26 12:30 UTC2
//! wasm-bindgen so both the worker tests and the size measurement reach the same code
55a1b8252814atagen2026-09-26 09:08 UTC3
7aa42f589ee2atagen2026-09-26 12:30 UTC4
use std::{pin::pin, sync::Arc};
7aa42f589ee2atagen2026-09-26 12:30 UTC5
7aa42f589ee2atagen2026-09-26 12:30 UTC6
use futures_channel::{mpsc, oneshot};
7aa42f589ee2atagen2026-09-26 12:30 UTC7
use futures_util::{
7aa42f589ee2atagen2026-09-26 12:30 UTC8
    StreamExt,
7aa42f589ee2atagen2026-09-26 12:30 UTC9
    future::{Either, select},
7aa42f589ee2atagen2026-09-26 12:30 UTC10
};
7aa42f589ee2atagen2026-09-26 12:30 UTC11
use gloo_timers::future::TimeoutFuture;
55a1b8252814atagen2026-09-26 09:08 UTC12
use macltok::{
7aa42f589ee2atagen2026-09-26 12:30 UTC13
    ClientHandle, EncryptedSessionStore, EngineConfig, Error, Event, EventStream, MessageContent,
7aa42f589ee2atagen2026-09-26 12:30 UTC14
    PasswordLogin, RoomMembership, SendState, Session, StoreConfig, TimelineContent, TimelineDiff,
7aa42f589ee2atagen2026-09-26 12:30 UTC15
    TimelineId, TimelineItem, imbl::Vector,
55a1b8252814atagen2026-09-26 09:08 UTC16
};
55a1b8252814atagen2026-09-26 09:08 UTC17
use wasm_bindgen::prelude::*;
55a1b8252814atagen2026-09-26 09:08 UTC18
7aa42f589ee2atagen2026-09-26 12:30 UTC19
const PHASE_TIMEOUT_MS: f64 = 30_000.0;
7aa42f589ee2atagen2026-09-26 12:30 UTC20
7aa42f589ee2atagen2026-09-26 12:30 UTC21
/// logs in, then proves a real sliding sync by finding the seeded room, reading the
7aa42f589ee2atagen2026-09-26 12:30 UTC22
/// seeded message out of its timeline, and seeing a message of its own come back from
7aa42f589ee2atagen2026-09-26 12:30 UTC23
/// the server
55a1b8252814atagen2026-09-26 09:08 UTC24
#[wasm_bindgen]
55a1b8252814atagen2026-09-26 09:08 UTC25
pub async fn smoke(
55a1b8252814atagen2026-09-26 09:08 UTC26
    homeserver: String,
55a1b8252814atagen2026-09-26 09:08 UTC27
    username: String,
55a1b8252814atagen2026-09-26 09:08 UTC28
    password: String,
55a1b8252814atagen2026-09-26 09:08 UTC29
    store_name: String,
7aa42f589ee2atagen2026-09-26 12:30 UTC30
    room_name: String,
7aa42f589ee2atagen2026-09-26 12:30 UTC31
    seeded: String,
55a1b8252814atagen2026-09-26 09:08 UTC32
) -> Result<(), JsError> {
7aa42f589ee2atagen2026-09-26 12:30 UTC33
    let sessions = Arc::new(EncryptedSessionStore::indexeddb(
7aa42f589ee2atagen2026-09-26 12:30 UTC34
        StoreConfig::indexeddb(store_name.clone(), "macltok-web"),
7aa42f589ee2atagen2026-09-26 12:30 UTC35
    ));
9c05b1dd327aatagen2026-10-08 11:27 UTC36
    let login = PasswordLogin::new(homeserver, username, password).allow_insecure_loopback(true);
7aa42f589ee2atagen2026-09-26 12:30 UTC37
    let session = Session::login_password(login, sessions).await?;
7aa42f589ee2atagen2026-09-26 12:30 UTC38
    let (engine, handle, events) = session.build(EngineConfig::default()).await?;
7aa42f589ee2atagen2026-09-26 12:30 UTC39
    let (panicked, panics) = mpsc::unbounded();
7aa42f589ee2atagen2026-09-26 12:30 UTC40
    macltok::on_panic(move |message| {
7aa42f589ee2atagen2026-09-26 12:30 UTC41
        let _ = panicked.unbounded_send(message.to_owned());
7aa42f589ee2atagen2026-09-26 12:30 UTC42
    });
7aa42f589ee2atagen2026-09-26 12:30 UTC43
    let mut events = Watched { events, panics };
55a1b8252814atagen2026-09-26 09:08 UTC44
    let (done, running) = oneshot::channel();
55a1b8252814atagen2026-09-26 09:08 UTC45
    wasm_bindgen_futures::spawn_local(async move {
55a1b8252814atagen2026-09-26 09:08 UTC46
        let _ = done.send(engine.run().await);
55a1b8252814atagen2026-09-26 09:08 UTC47
    });
55a1b8252814atagen2026-09-26 09:08 UTC48
7aa42f589ee2atagen2026-09-26 12:30 UTC49
    let mut phase = Phase::new("the seeded room to sync");
7aa42f589ee2atagen2026-09-26 12:30 UTC50
    let (_never, mut idle) = oneshot::channel();
7aa42f589ee2atagen2026-09-26 12:30 UTC51
    let room_id = loop {
7aa42f589ee2atagen2026-09-26 12:30 UTC52
        let rooms = handle.rooms().await;
7aa42f589ee2atagen2026-09-26 12:30 UTC53
        if let Some(room) = rooms
7aa42f589ee2atagen2026-09-26 12:30 UTC54
            .into_iter()
7aa42f589ee2atagen2026-09-26 12:30 UTC55
            .find(|room| room.membership == RoomMembership::Joined && room.name == room_name)
7aa42f589ee2atagen2026-09-26 12:30 UTC56
        {
7aa42f589ee2atagen2026-09-26 12:30 UTC57
            break room.room_id;
55a1b8252814atagen2026-09-26 09:08 UTC58
        }
7aa42f589ee2atagen2026-09-26 12:30 UTC59
        while !matches!(
7aa42f589ee2atagen2026-09-26 12:30 UTC60
            phase.next(&mut events, &mut idle).await?,
7aa42f589ee2atagen2026-09-26 12:30 UTC61
            Event::RoomsChanged
7aa42f589ee2atagen2026-09-26 12:30 UTC62
        ) {}
7aa42f589ee2atagen2026-09-26 12:30 UTC63
    };
7aa42f589ee2atagen2026-09-26 12:30 UTC64
    let timeline_id = TimelineId::from(room_id.clone());
7aa42f589ee2atagen2026-09-26 12:30 UTC65
7aa42f589ee2atagen2026-09-26 12:30 UTC66
    let mut phase = Phase::new("the seeded message in the opened timeline");
7aa42f589ee2atagen2026-09-26 12:30 UTC67
    let mut opening = command(
7aa42f589ee2atagen2026-09-26 12:30 UTC68
        &handle,
7aa42f589ee2atagen2026-09-26 12:30 UTC69
        |handle| async move { handle.open_room(room_id).await },
7aa42f589ee2atagen2026-09-26 12:30 UTC70
    );
7aa42f589ee2atagen2026-09-26 12:30 UTC71
    let mut items = None;
7aa42f589ee2atagen2026-09-26 12:30 UTC72
    while !contains(&items, &seeded, false) {
7aa42f589ee2atagen2026-09-26 12:30 UTC73
        let event = phase.next(&mut events, &mut opening).await?;
7aa42f589ee2atagen2026-09-26 12:30 UTC74
        track(&timeline_id, event, &mut items)?;
7aa42f589ee2atagen2026-09-26 12:30 UTC75
    }
7aa42f589ee2atagen2026-09-26 12:30 UTC76
7aa42f589ee2atagen2026-09-26 12:30 UTC77
    let sent = format!("{seeded}, answered from {store_name}");
7aa42f589ee2atagen2026-09-26 12:30 UTC78
    let mut phase = Phase::new("the sent message to come back from the server");
7aa42f589ee2atagen2026-09-26 12:30 UTC79
    let content = MessageContent::text(sent.clone());
7aa42f589ee2atagen2026-09-26 12:30 UTC80
    let target = timeline_id.clone();
7aa42f589ee2atagen2026-09-26 12:30 UTC81
    let mut sending = command(&handle, |handle| async move {
7aa42f589ee2atagen2026-09-26 12:30 UTC82
        handle.send_message(target, content).await
7aa42f589ee2atagen2026-09-26 12:30 UTC83
    });
7aa42f589ee2atagen2026-09-26 12:30 UTC84
    while !contains(&items, &sent, true) {
7aa42f589ee2atagen2026-09-26 12:30 UTC85
        let event = phase.next(&mut events, &mut sending).await?;
7aa42f589ee2atagen2026-09-26 12:30 UTC86
        track(&timeline_id, event, &mut items)?;
55a1b8252814atagen2026-09-26 09:08 UTC87
    }
55a1b8252814atagen2026-09-26 09:08 UTC88
55a1b8252814atagen2026-09-26 09:08 UTC89
    handle.shutdown().await?;
7aa42f589ee2atagen2026-09-26 12:30 UTC90
    while events.events.recv().await.is_some() {}
55a1b8252814atagen2026-09-26 09:08 UTC91
    running
55a1b8252814atagen2026-09-26 09:08 UTC92
        .await
55a1b8252814atagen2026-09-26 09:08 UTC93
        .map_err(|_| JsError::new("engine task vanished"))??;
55a1b8252814atagen2026-09-26 09:08 UTC94
    Ok(())
55a1b8252814atagen2026-09-26 09:08 UTC95
}
7aa42f589ee2atagen2026-09-26 12:30 UTC96
7aa42f589ee2atagen2026-09-26 12:30 UTC97
/// answers only arrive while the event stream is drained, so commands run beside it
7aa42f589ee2atagen2026-09-26 12:30 UTC98
fn command<F>(
7aa42f589ee2atagen2026-09-26 12:30 UTC99
    handle: &ClientHandle,
7aa42f589ee2atagen2026-09-26 12:30 UTC100
    run: impl FnOnce(ClientHandle) -> F,
7aa42f589ee2atagen2026-09-26 12:30 UTC101
) -> oneshot::Receiver<Result<(), Error>>
7aa42f589ee2atagen2026-09-26 12:30 UTC102
where
7aa42f589ee2atagen2026-09-26 12:30 UTC103
    F: Future<Output = Result<(), Error>> + 'static,
7aa42f589ee2atagen2026-09-26 12:30 UTC104
{
7aa42f589ee2atagen2026-09-26 12:30 UTC105
    let (answer, answered) = oneshot::channel();
7aa42f589ee2atagen2026-09-26 12:30 UTC106
    let pending = run(handle.clone());
7aa42f589ee2atagen2026-09-26 12:30 UTC107
    wasm_bindgen_futures::spawn_local(async move {
7aa42f589ee2atagen2026-09-26 12:30 UTC108
        let _ = answer.send(pending.await);
7aa42f589ee2atagen2026-09-26 12:30 UTC109
    });
7aa42f589ee2atagen2026-09-26 12:30 UTC110
    answered
7aa42f589ee2atagen2026-09-26 12:30 UTC111
}
7aa42f589ee2atagen2026-09-26 12:30 UTC112
7aa42f589ee2atagen2026-09-26 12:30 UTC113
/// a panicking engine task goes silent rather than stopping, and a phase would otherwise wait
7aa42f589ee2atagen2026-09-26 12:30 UTC114
/// out its whole timeout
7aa42f589ee2atagen2026-09-26 12:30 UTC115
struct Watched {
7aa42f589ee2atagen2026-09-26 12:30 UTC116
    events: EventStream,
7aa42f589ee2atagen2026-09-26 12:30 UTC117
    panics: mpsc::UnboundedReceiver<String>,
7aa42f589ee2atagen2026-09-26 12:30 UTC118
}
7aa42f589ee2atagen2026-09-26 12:30 UTC119
7aa42f589ee2atagen2026-09-26 12:30 UTC120
struct Phase {
7aa42f589ee2atagen2026-09-26 12:30 UTC121
    what: &'static str,
7aa42f589ee2atagen2026-09-26 12:30 UTC122
    deadline: f64,
7aa42f589ee2atagen2026-09-26 12:30 UTC123
}
7aa42f589ee2atagen2026-09-26 12:30 UTC124
7aa42f589ee2atagen2026-09-26 12:30 UTC125
impl Phase {
7aa42f589ee2atagen2026-09-26 12:30 UTC126
    fn new(what: &'static str) -> Self {
7aa42f589ee2atagen2026-09-26 12:30 UTC127
        Self {
7aa42f589ee2atagen2026-09-26 12:30 UTC128
            what,
7aa42f589ee2atagen2026-09-26 12:30 UTC129
            deadline: js_sys::Date::now() + PHASE_TIMEOUT_MS,
7aa42f589ee2atagen2026-09-26 12:30 UTC130
        }
7aa42f589ee2atagen2026-09-26 12:30 UTC131
    }
7aa42f589ee2atagen2026-09-26 12:30 UTC132
7aa42f589ee2atagen2026-09-26 12:30 UTC133
    async fn next(
7aa42f589ee2atagen2026-09-26 12:30 UTC134
        &mut self,
7aa42f589ee2atagen2026-09-26 12:30 UTC135
        watched: &mut Watched,
7aa42f589ee2atagen2026-09-26 12:30 UTC136
        pending: &mut oneshot::Receiver<Result<(), Error>>,
7aa42f589ee2atagen2026-09-26 12:30 UTC137
    ) -> Result<Event, JsError> {
7aa42f589ee2atagen2026-09-26 12:30 UTC138
        let left = (self.deadline - js_sys::Date::now()).max(0.0);
7aa42f589ee2atagen2026-09-26 12:30 UTC139
        let timeout = TimeoutFuture::new(left as u32);
7aa42f589ee2atagen2026-09-26 12:30 UTC140
        let panicked = watched.panics.next();
7aa42f589ee2atagen2026-09-26 12:30 UTC141
        let outcome = match select(pin!(watched.events.recv()), select(timeout, panicked)).await {
7aa42f589ee2atagen2026-09-26 12:30 UTC142
            Either::Left((event, _)) => Some(event),
7aa42f589ee2atagen2026-09-26 12:30 UTC143
            Either::Right((Either::Left(_), _)) => None,
7aa42f589ee2atagen2026-09-26 12:30 UTC144
            Either::Right((Either::Right((message, _)), _)) => {
7aa42f589ee2atagen2026-09-26 12:30 UTC145
                return Err(JsError::new(&format!(
7aa42f589ee2atagen2026-09-26 12:30 UTC146
                    "panicked while waiting for {}, {}",
7aa42f589ee2atagen2026-09-26 12:30 UTC147
                    self.what,
7aa42f589ee2atagen2026-09-26 12:30 UTC148
                    message.unwrap_or_default()
7aa42f589ee2atagen2026-09-26 12:30 UTC149
                )));
7aa42f589ee2atagen2026-09-26 12:30 UTC150
            }
7aa42f589ee2atagen2026-09-26 12:30 UTC151
        };
7aa42f589ee2atagen2026-09-26 12:30 UTC152
        if let Ok(Some(Err(error))) = pending.try_recv() {
7aa42f589ee2atagen2026-09-26 12:30 UTC153
            return Err(JsError::new(&format!("{} failed, {error}", self.what)));
7aa42f589ee2atagen2026-09-26 12:30 UTC154
        }
7aa42f589ee2atagen2026-09-26 12:30 UTC155
        match outcome {
7aa42f589ee2atagen2026-09-26 12:30 UTC156
            Some(Some(Event::Stopped) | None) => Err(JsError::new(&format!(
7aa42f589ee2atagen2026-09-26 12:30 UTC157
                "engine stopped while waiting for {}",
7aa42f589ee2atagen2026-09-26 12:30 UTC158
                self.what
7aa42f589ee2atagen2026-09-26 12:30 UTC159
            ))),
7aa42f589ee2atagen2026-09-26 12:30 UTC160
            Some(Some(Event::SessionSaveFailed { message })) => Err(JsError::new(&format!(
7aa42f589ee2atagen2026-09-26 12:30 UTC161
                "saving the session failed while waiting for {}, {message}",
7aa42f589ee2atagen2026-09-26 12:30 UTC162
                self.what
7aa42f589ee2atagen2026-09-26 12:30 UTC163
            ))),
7aa42f589ee2atagen2026-09-26 12:30 UTC164
            Some(Some(event)) => Ok(event),
7aa42f589ee2atagen2026-09-26 12:30 UTC165
            None => Err(JsError::new(&format!(
7aa42f589ee2atagen2026-09-26 12:30 UTC166
                "timed out after {}s waiting for {}",
7aa42f589ee2atagen2026-09-26 12:30 UTC167
                PHASE_TIMEOUT_MS / 1000.0,
7aa42f589ee2atagen2026-09-26 12:30 UTC168
                self.what
7aa42f589ee2atagen2026-09-26 12:30 UTC169
            ))),
7aa42f589ee2atagen2026-09-26 12:30 UTC170
        }
7aa42f589ee2atagen2026-09-26 12:30 UTC171
    }
7aa42f589ee2atagen2026-09-26 12:30 UTC172
}
7aa42f589ee2atagen2026-09-26 12:30 UTC173
7aa42f589ee2atagen2026-09-26 12:30 UTC174
fn track(
7aa42f589ee2atagen2026-09-26 12:30 UTC175
    timeline_id: &TimelineId,
7aa42f589ee2atagen2026-09-26 12:30 UTC176
    event: Event,
7aa42f589ee2atagen2026-09-26 12:30 UTC177
    items: &mut Option<Vector<TimelineItem>>,
7aa42f589ee2atagen2026-09-26 12:30 UTC178
) -> Result<(), JsError> {
7aa42f589ee2atagen2026-09-26 12:30 UTC179
    match event {
7aa42f589ee2atagen2026-09-26 12:30 UTC180
        Event::TimelineReset {
7aa42f589ee2atagen2026-09-26 12:30 UTC181
            timeline_id: id,
7aa42f589ee2atagen2026-09-26 12:30 UTC182
            items: snapshot,
7aa42f589ee2atagen2026-09-26 12:30 UTC183
        } if &id == timeline_id => *items = Some(snapshot),
7aa42f589ee2atagen2026-09-26 12:30 UTC184
        Event::TimelineChanged {
7aa42f589ee2atagen2026-09-26 12:30 UTC185
            timeline_id: id,
7aa42f589ee2atagen2026-09-26 12:30 UTC186
            diffs,
7aa42f589ee2atagen2026-09-26 12:30 UTC187
        } if &id == timeline_id => {
7aa42f589ee2atagen2026-09-26 12:30 UTC188
            if let Some(items) = items {
7aa42f589ee2atagen2026-09-26 12:30 UTC189
                TimelineDiff::try_apply_all(diffs, items)
7aa42f589ee2atagen2026-09-26 12:30 UTC190
                    .map_err(|error| JsError::new(&format!("timeline diff misfit, {error:?}")))?;
7aa42f589ee2atagen2026-09-26 12:30 UTC191
            }
7aa42f589ee2atagen2026-09-26 12:30 UTC192
        }
7aa42f589ee2atagen2026-09-26 12:30 UTC193
        _ => {}
7aa42f589ee2atagen2026-09-26 12:30 UTC194
    }
7aa42f589ee2atagen2026-09-26 12:30 UTC195
    Ok(())
7aa42f589ee2atagen2026-09-26 12:30 UTC196
}
7aa42f589ee2atagen2026-09-26 12:30 UTC197
7aa42f589ee2atagen2026-09-26 12:30 UTC198
fn contains(items: &Option<Vector<TimelineItem>>, body: &str, remote_echo: bool) -> bool {
7aa42f589ee2atagen2026-09-26 12:30 UTC199
    items.iter().flatten().any(|item| {
7aa42f589ee2atagen2026-09-26 12:30 UTC200
        let TimelineItem::Event(event) = item else {
7aa42f589ee2atagen2026-09-26 12:30 UTC201
            return false;
7aa42f589ee2atagen2026-09-26 12:30 UTC202
        };
7aa42f589ee2atagen2026-09-26 12:30 UTC203
        let TimelineContent::Message(message) = &event.content else {
7aa42f589ee2atagen2026-09-26 12:30 UTC204
            return false;
7aa42f589ee2atagen2026-09-26 12:30 UTC205
        };
7aa42f589ee2atagen2026-09-26 12:30 UTC206
        message.body == body
7aa42f589ee2atagen2026-09-26 12:30 UTC207
            && (!remote_echo || (event.event_id.is_some() && event.send_state == SendState::Sent))
7aa42f589ee2atagen2026-09-26 12:30 UTC208
    })
7aa42f589ee2atagen2026-09-26 12:30 UTC209
}