simplify 2

This commit is contained in:
Andras Schmelczer 2026-09-22 08:00:25 +01:00
commit 82929d7479
4 changed files with 503 additions and 1126 deletions

View file

@ -86,13 +86,11 @@ content/file-manifest version IDs are the event IDs that created them. Rejected
requests and retries of accepted requests append no events. Timestamps never
determine event ordering.
The WebSocket handshake retains `lastSeenVaultUpdateId`. The server sends
`{type: "vaultEvents", headEventId, events}` in increasing order, including events
originated by that client. Each connection drains the database through one sender.
In-memory notifications only wake it; polling covers missed wakes and requests
interrupted after commit. Reconnect with the last durably processed event ID. The
HTTP replay endpoint uses the same history, so losing the final notification is
recoverable.
WebSocket messages are `{type: "vaultChanged"}` hints. Clients fetch ordered
history through HTTP from their last incorporated event ID. The server compares
its current history checkpoint after broadcasts and periodically, so missed wakes
and requests interrupted after commit still trigger catchup. Reconnecting sends a
fresh hint without replaying events over the socket. Cursor messages are unchanged.
A fresh client reads `/vault-snapshot`, applies it durably, then replays after its
watermark. Successful HTTP responses include `X-Vault-Link-History`, containing
an event ID and a random event-incarnation token. Clients persist and echo this

View file

@ -1,16 +1,13 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::app_state::database::models::{DeviceId, DocumentId, EventBatch, VaultUpdateId};
use crate::app_state::database::models::{DeviceId, DocumentId, VaultUpdateId};
#[derive(TS, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WebSocketHandshake {
pub token: String,
pub device_id: DeviceId,
#[ts(as = "Option<i32>")]
pub last_seen_vault_update_id: Option<VaultUpdateId>,
}
#[derive(TS, Deserialize, Clone, Debug)]
@ -67,6 +64,6 @@ pub enum WebSocketClientMessage {
#[serde(rename_all = "camelCase", tag = "type")]
#[ts(export)]
pub enum WebSocketServerMessage {
VaultEvents(EventBatch),
VaultChanged,
CursorPositions(CursorPositionFromServer),
}

View file

@ -20,4 +20,4 @@ pub const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Info;
pub const DEFAULT_MERGEABLE_FILE_EXTENSIONS: &[&str] = &["md", "txt"];
pub const SUPPORTED_API_VERSION: u32 = 4;
pub const SUPPORTED_API_VERSION: u32 = 5;

View file

@ -69,26 +69,21 @@ async fn websocket(
let device = authenticated.handshake.device_id;
let session = state.cursors.register_connection(&vault, &device).await;
let mut after = authenticated
.handshake
.last_seen_vault_update_id
.unwrap_or(0);
let mut last_checkpoint = None;
let mut notifications = state.broadcasts.get_receiver(vault.clone()).await;
let mut timer = tokio::time::interval(Duration::from_secs(2));
timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
info!("WebSocket connected to vault {vault}");
// Exactly one sender drains durable events in commit order. In-memory
// notifications are only wakeups, so lag and commit/notify crashes are safe.
// HTTP owns event replay. Poll the cheap checkpoint to recover missed
// broadcasts, including a commit followed by an interrupted notification.
let result = async {
loop {
let batch = state.database.events_after(&vault, after).await.map_err(server_error)?;
if !batch.events.is_empty() {
let head = batch.end_event_id.unwrap_or(batch.head_event_id);
send_update_over_websocket(&WebSocketServerMessage::VaultEvents(batch), &mut sender).await?;
after = head;
let checkpoint = state.database.history_checkpoint(&vault).await.map_err(server_error)?;
if last_checkpoint.as_ref() != Some(&checkpoint) {
send_update_over_websocket(&WebSocketServerMessage::VaultChanged, &mut sender).await?;
last_checkpoint = Some(checkpoint);
}
tokio::select! {
@ -99,7 +94,7 @@ async fn websocket(
send_update_over_websocket(&WebSocketServerMessage::CursorPositions(positions), &mut sender).await?;
},
Err(RecvError::Lagged(_)) => {}, // Drain the durable log on the next iteration.
Err(RecvError::Lagged(_)) => {}, // Check the durable head on the next iteration.
Err(RecvError::Closed) => break,
},