From 4b522c804db8155f74a734c95ed962d996b2c692 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Wed, 18 Jun 2025 23:33:02 -0400 Subject: Include historical events in the boot response. The returned events are all events up to and including the `resume_point` in the same response. If combined with the events from `/api/events?resume_point=x`, using the same `resume_point`, the client will have a complete event history, less any events from histories that have been purged. --- docs/api/boot.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) (limited to 'docs/api/boot.md') diff --git a/docs/api/boot.md b/docs/api/boot.md index 7e3dda3..1f6e619 100644 --- a/docs/api/boot.md +++ b/docs/api/boot.md @@ -21,8 +21,8 @@ sequenceDiagram Client initialization serves three purposes: - It confirms that the client's [identity token](./authentication.md) is valid, and tells the client what user that token is associated with. -- It provides an initial snapshot of the state of the service. -- It provides a resume point for the [event stream](./events.md), which allows clients to consume events starting from the moment the snapshot was created. +- It provides an initial event collection. +- It provides a resume point for the [event stream](./events.md), which allows clients to consume events starting after the initial event collection. ## `GET /api/boot` @@ -43,6 +43,56 @@ This endpoint will respond with a status of }, "resume_point": 1312, "heartbeat": 30, + "events": [ + { + "type": "user", + "event": "created", + "at": "2025-04-14T23:58:10.421901Z", + "id": "U1234abcd", + "name": "example username" + }, + { + "type": "channel", + "event": "created", + "at": "2025-04-14T23:58:11.421901Z", + "id": "C1234abcd", + "name": "nonsense and such" + }, + { + "type": "message", + "event": "sent", + "at": "2024-09-27T23:19:10.208147Z", + "channel": "C1234abcd", + "sender": "U1234abcd", + "id": "M1312acab", + "body": "beep" + }, + { + "type": "message", + "event": "sent", + "at": "2025-06-19T15:14:40.431627Z", + "channel": "Ccfdryfdb4krpy77", + "sender": "U888j6fyc8ccrnkf", + "id": "Mc6jk823wjc82734", + "body": "test" + }, + { + "type": "channel", + "event": "created", + "at": "2025-06-19T15:14:44.764263Z", + "id": "C2d9y6wckph3n36x", + "name": "noob" + }, + { + "type": "message", + "event": "sent", + "at": "2025-06-19T15:29:47.376455Z", + "channel": "Ccfdryfdb4krpy77", + "sender": "U888j6fyc8ccrnkf", + "id": "M3twnj7rfk2ph744", + "body": "test" + } + ], "users": [ { "id": "U1234abcd", @@ -75,10 +125,14 @@ The response will include the following fields: | `user` | object | The details of the caller's identity. | | `resume_point` | integer | A resume point for [events](./events.md), such that the event stream will begin immediately after the included snapshot. | | `heartbeat` | integer | The [heartbeat timeout](./events.md#heartbeat-events), in seconds, for events. | +| `events` | array of object | The events on the server up to the resume point. | | `users` | array of object | A snapshot of the users present in the service. | | `channels` | array of object | A snapshot of the channels present in the service. | | `messages` | array of object | A snapshot of the messages present in the service. | +Each element of the +`events` object is an event, as described in [Events](./events.md). Events are provided in the same order as they would appear in the event stream response. + The `user` object will include the following fields: | Field | Type | Description | -- cgit v1.2.3 From 7778cdf0c495a04f4f5f3f85b78348c8037a5771 Mon Sep 17 00:00:00 2001 From: Owen Jacobson Date: Fri, 20 Jun 2025 19:47:46 -0400 Subject: Remove the snapshot fields from `/api/boot`. Clients now _must_ construct their state from the event stream; it is no longer possible for them to delegate that work to the server. --- docs/api/boot.md | 50 ------------------------------------- src/boot/app.rs | 30 +++++----------------- src/boot/handlers/boot/test.rs | 22 +++------------- src/boot/mod.rs | 5 +--- src/channel/handlers/create/test.rs | 10 +++++++- src/channel/handlers/delete/test.rs | 12 ++++++++- src/channel/history.rs | 6 ----- src/message/handlers/delete/test.rs | 10 +++++++- src/message/history.rs | 6 ----- src/user/history.rs | 8 +----- 10 files changed, 41 insertions(+), 118 deletions(-) (limited to 'docs/api/boot.md') diff --git a/docs/api/boot.md b/docs/api/boot.md index 1f6e619..f6e6dc2 100644 --- a/docs/api/boot.md +++ b/docs/api/boot.md @@ -92,28 +92,6 @@ This endpoint will respond with a status of "id": "M3twnj7rfk2ph744", "body": "test" } - ], - "users": [ - { - "id": "U1234abcd", - "name": "example username" - } - ], - "channels": [ - { - "at": "2025-04-14T23:58:11.421901Z", - "name": "nonsense and such", - "id": "C1234abcd" - } - ], - "messages": [ - { - "at": "2024-09-27T23:19:10.208147Z", - "channel": "C1234abcd", - "sender": "U1234abcd", - "id": "M1312acab", - "body": "beep" - } ] } ``` @@ -126,9 +104,6 @@ The response will include the following fields: | `resume_point` | integer | A resume point for [events](./events.md), such that the event stream will begin immediately after the included snapshot. | | `heartbeat` | integer | The [heartbeat timeout](./events.md#heartbeat-events), in seconds, for events. | | `events` | array of object | The events on the server up to the resume point. | -| `users` | array of object | A snapshot of the users present in the service. | -| `channels` | array of object | A snapshot of the channels present in the service. | -| `messages` | array of object | A snapshot of the messages present in the service. | Each element of the `events` object is an event, as described in [Events](./events.md). Events are provided in the same order as they would appear in the event stream response. @@ -139,28 +114,3 @@ The `user` object will include the following fields: | :----- | :----- | :--------------------------------------- | | `name` | string | The name of the caller's login identity. | | `id` | string | The ID of the caller's login identity. | - -Each element of the `users` array describes a distinct user, and will include the following fields: - -| Field | Type | Description | -| :----- | :----- | :----------------------------------------------------------------------------------------------------------------------------------- | -| `name` | string | The name for the user. | -| `id` | string | A unique identifier for the user. This can be used to associate the user with other events, or to make API calls targeting the user. | - -Each element of the `channels` array describes a distinct channel, and will include the following fields: - -| Field | Type | Description | -| :----- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| `at` | timestamp | The moment the channel was created. | -| `name` | string | The name for the channel. | -| `id` | string | A unique identifier for the channel. This can be used to associate the channel with other events, or to make API calls targeting the channel. | - -Each element of the `messages` array describes a distinct message, and will include the following fields: - -| Field | Type | Description | -| :-------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | -| `at` | timestamp | The moment the message was sent. | -| `channel` | string | The ID of the channel the message was sent to. | -| `sender` | string | The ID of the user that sent the message. | -| `id` | string | A unique identifier for the message. This can be used to associate the message with other events, or to make API calls targeting the message. | -| `body` | string | The text of the message. | diff --git a/src/boot/app.rs b/src/boot/app.rs index 690bcf4..89eec12 100644 --- a/src/boot/app.rs +++ b/src/boot/app.rs @@ -23,42 +23,27 @@ impl<'a> Boot<'a> { let mut tx = self.db.begin().await?; let resume_point = tx.sequence().current().await?; - let user_histories = tx.users().all(resume_point).await?; - let channel_histories = tx.channels().all(resume_point).await?; - let message_histories = tx.messages().all(resume_point).await?; + let users = tx.users().all(resume_point).await?; + let channels = tx.channels().all(resume_point).await?; + let messages = tx.messages().all(resume_point).await?; tx.commit().await?; - let users = user_histories - .iter() - .filter_map(|user| user.as_of(resume_point)) - .collect(); - - let channels = channel_histories - .iter() - .filter_map(|channel| channel.as_of(resume_point)) - .collect(); - - let messages = message_histories - .iter() - .filter_map(|message| message.as_of(resume_point)) - .collect(); - - let user_events = user_histories + let user_events = users .iter() .map(user::History::events) .kmerge_by(Sequence::merge) .filter(Sequence::up_to(resume_point)) .map(Event::from); - let channel_events = channel_histories + let channel_events = channels .iter() .map(channel::History::events) .kmerge_by(Sequence::merge) .filter(Sequence::up_to(resume_point)) .map(Event::from); - let message_events = message_histories + let message_events = messages .iter() .map(message::History::events) .kmerge_by(Sequence::merge) @@ -72,9 +57,6 @@ impl<'a> Boot<'a> { Ok(Snapshot { resume_point, - users, - channels, - messages, events, }) } diff --git a/src/boot/handlers/boot/test.rs b/src/boot/handlers/boot/test.rs index d68618e..1e590a7 100644 --- a/src/boot/handlers/boot/test.rs +++ b/src/boot/handlers/boot/test.rs @@ -25,8 +25,6 @@ async fn includes_users() { .await .expect("boot always succeeds"); - assert!(response.snapshot.users.contains(&spectator)); - let created = response .snapshot .events @@ -48,8 +46,6 @@ async fn includes_channels() { .await .expect("boot always succeeds"); - assert!(response.snapshot.channels.contains(&channel)); - let created = response .snapshot .events @@ -73,8 +69,6 @@ async fn includes_messages() { .await .expect("boot always succeeds"); - assert!(response.snapshot.messages.contains(&message)); - let sent = response .snapshot .events @@ -87,7 +81,7 @@ async fn includes_messages() { } #[tokio::test] -async fn excludes_expired_messages() { +async fn includes_expired_messages() { let app = fixtures::scratch_app().await; let sender = fixtures::user::create(&app, &fixtures::ancient()).await; let channel = fixtures::channel::create(&app, &fixtures::ancient()).await; @@ -104,8 +98,6 @@ async fn excludes_expired_messages() { .await .expect("boot always succeeds"); - assert!(!response.snapshot.messages.contains(&expired_message)); - let sent = response .snapshot .events @@ -131,7 +123,7 @@ async fn excludes_expired_messages() { } #[tokio::test] -async fn excludes_deleted_messages() { +async fn includes_deleted_messages() { let app = fixtures::scratch_app().await; let sender = fixtures::user::create(&app, &fixtures::now()).await; let channel = fixtures::channel::create(&app, &fixtures::now()).await; @@ -147,8 +139,6 @@ async fn excludes_deleted_messages() { .await .expect("boot always succeeds"); - assert!(!response.snapshot.messages.contains(&deleted_message)); - let sent = response .snapshot .events @@ -174,7 +164,7 @@ async fn excludes_deleted_messages() { } #[tokio::test] -async fn excludes_expired_channels() { +async fn includes_expired_channels() { let app = fixtures::scratch_app().await; let expired_channel = fixtures::channel::create(&app, &fixtures::ancient()).await; @@ -188,8 +178,6 @@ async fn excludes_expired_channels() { .await .expect("boot always succeeds"); - assert!(!response.snapshot.channels.contains(&expired_channel)); - let created = response .snapshot .events @@ -215,7 +203,7 @@ async fn excludes_expired_channels() { } #[tokio::test] -async fn excludes_deleted_channels() { +async fn includes_deleted_channels() { let app = fixtures::scratch_app().await; let deleted_channel = fixtures::channel::create(&app, &fixtures::now()).await; @@ -229,8 +217,6 @@ async fn excludes_deleted_channels() { .await .expect("boot always succeeds"); - assert!(!response.snapshot.channels.contains(&deleted_channel)); - let created = response .snapshot .events diff --git a/src/boot/mod.rs b/src/boot/mod.rs index 148e87d..e0d35d9 100644 --- a/src/boot/mod.rs +++ b/src/boot/mod.rs @@ -1,4 +1,4 @@ -use crate::{channel::Channel, event::Event, event::Sequence, message::Message, user::User}; +use crate::{event::Event, event::Sequence}; pub mod app; pub mod handlers; @@ -6,8 +6,5 @@ pub mod handlers; #[derive(serde::Serialize)] pub struct Snapshot { pub resume_point: Sequence, - pub users: Vec, - pub channels: Vec, - pub messages: Vec, pub events: Vec, } diff --git a/src/channel/handlers/create/test.rs b/src/channel/handlers/create/test.rs index 595a879..31bb778 100644 --- a/src/channel/handlers/create/test.rs +++ b/src/channel/handlers/create/test.rs @@ -2,6 +2,7 @@ use std::future; use axum::extract::{Json, State}; use futures::stream::StreamExt as _; +use itertools::Itertools; use crate::{ channel::app, @@ -33,7 +34,14 @@ async fn new_channel() { // Verify the semantics let snapshot = app.boot().snapshot().await.expect("boot always succeeds"); - assert!(snapshot.channels.iter().any(|channel| channel == &response)); + let created = snapshot + .events + .into_iter() + .filter_map(fixtures::event::channel) + .filter_map(fixtures::event::channel::created) + .exactly_one() + .expect("only one channel has been created"); + assert_eq!(response, created.channel); let channel = app .channels() diff --git a/src/channel/handlers/delete/test.rs b/src/channel/handlers/delete/test.rs index b1e42ea..99c19db 100644 --- a/src/channel/handlers/delete/test.rs +++ b/src/channel/handlers/delete/test.rs @@ -1,4 +1,5 @@ use axum::extract::{Path, State}; +use itertools::Itertools; use crate::{channel::app, test::fixtures}; @@ -28,7 +29,16 @@ pub async fn valid_channel() { // Verify the semantics let snapshot = app.boot().snapshot().await.expect("boot always succeeds"); - assert!(!snapshot.channels.contains(&channel)); + let created = snapshot + .events + .into_iter() + .filter_map(fixtures::event::channel) + .filter_map(fixtures::event::channel::created) + .exactly_one() + .expect("only one channel has been created"); + // We don't expect `channel` to match the event exactly, as the name will have been + // tombstoned and the channel given a `deleted_at` date. + assert_eq!(channel.id, created.channel.id); } #[tokio::test] diff --git a/src/channel/history.rs b/src/channel/history.rs index faf6a0e..7f18e45 100644 --- a/src/channel/history.rs +++ b/src/channel/history.rs @@ -27,12 +27,6 @@ impl History { self.channel.clone() } - pub fn as_of(&self, resume_point: Sequence) -> Option { - self.events() - .filter(Sequence::up_to(resume_point)) - .collect() - } - // Snapshot of this channel as of all events recorded in this history. pub fn as_snapshot(&self) -> Option { self.events().collect() diff --git a/src/message/handlers/delete/test.rs b/src/message/handlers/delete/test.rs index 15aa2c2..f567eb7 100644 --- a/src/message/handlers/delete/test.rs +++ b/src/message/handlers/delete/test.rs @@ -1,4 +1,5 @@ use axum::extract::{Path, State}; +use itertools::Itertools; use crate::{message::app, test::fixtures}; @@ -29,7 +30,14 @@ pub async fn delete_message() { // Verify the semantics let snapshot = app.boot().snapshot().await.expect("boot always succeeds"); - assert!(!snapshot.messages.contains(&message)); + let deleted = snapshot + .events + .into_iter() + .filter_map(fixtures::event::message) + .filter_map(fixtures::event::message::deleted) + .exactly_one() + .expect("only one message has been deleted"); + assert_eq!(response.id, deleted.id) } #[tokio::test] diff --git a/src/message/history.rs b/src/message/history.rs index 1a72c08..7585e1c 100644 --- a/src/message/history.rs +++ b/src/message/history.rs @@ -27,12 +27,6 @@ impl History { self.message.clone() } - pub fn as_of(&self, resume_point: Sequence) -> Option { - self.events() - .filter(Sequence::up_to(resume_point)) - .collect() - } - // Snapshot of this message as of all events recorded in this history. pub fn as_snapshot(&self) -> Option { self.events().collect() diff --git a/src/user/history.rs b/src/user/history.rs index ae7a561..72e0aee 100644 --- a/src/user/history.rs +++ b/src/user/history.rs @@ -2,7 +2,7 @@ use super::{ Id, User, event::{Created, Event}, }; -use crate::event::{Instant, Sequence}; +use crate::event::Instant; #[derive(Clone, Debug, Eq, PartialEq)] pub struct History { @@ -24,12 +24,6 @@ impl History { self.user.clone() } - pub fn as_of(&self, resume_point: Sequence) -> Option { - self.events() - .filter(Sequence::up_to(resume_point)) - .collect() - } - // Snapshot of this user, as of all events recorded in this history. pub fn as_snapshot(&self) -> Option { self.events().collect() -- cgit v1.2.3