| 1 | //! Live repository pages: every event a repository page shows is passed to |
| 2 | //! the notify service as a small change (`g1t_contracts::live`), which |
| 3 | //! tells the people watching that repository's pages. One call per batch |
| 4 | //! (more for a very busy one); nothing written in the repository goes |
| 5 | //! along, and who may see it is notify's to keep: a tab watches a |
| 6 | //! repository only after the site checked its person may read it. |
| 7 | //! |
| 8 | //! A work item's events and comments go to every repository it is linked |
| 9 | //! to (`repoIds`), not only its origin, which it may not have, and to its |
| 10 | //! workspace's Work pages (`workspace_changed`), which hear of it with or |
| 11 | //! without a repository. |
| 12 | |
| 13 | use g1t_contracts::events::Event; |
| 14 | use g1t_contracts::live::{ |
| 15 | MAX_CHANGES, REPO_CHANGED, RepoChange, RepoChangedArgs, WORKSPACE_CHANGED, WorkspaceChangedArgs, change_of, workspace_changes_of, |
| 16 | }; |
| 17 | use worker::{Fetcher, Result}; |
| 18 | |
| 19 | /// The changes an event makes: one for its repository, and one for each |
| 20 | /// other repository it names in `repoIds`. |
| 21 | pub(crate) fn changes_for(event: &Event) -> Vec<RepoChange> { |
| 22 | let mut changes: Vec<RepoChange> = change_of(event).into_iter().collect(); |
| 23 | for repo_id in event.data["repoIds"].as_array().into_iter().flatten().filter_map(|id| id.as_str()) { |
| 24 | if repo_id.is_empty() || changes.iter().any(|change| change.repo_id == repo_id) { |
| 25 | continue; |
| 26 | } |
| 27 | let linked = Event { repo_id: Some(repo_id.to_owned()), ..event.clone() }; |
| 28 | changes.extend(change_of(&linked)); |
| 29 | } |
| 30 | changes |
| 31 | } |
| 32 | |
| 33 | /// A batch's changes, in order, in calls of at most [`MAX_CHANGES`]. |
| 34 | pub(crate) fn changes_of(events: &[Event]) -> Vec<Vec<RepoChange>> { |
| 35 | let changes: Vec<RepoChange> = events.iter().flat_map(changes_for).collect(); |
| 36 | changes.chunks(MAX_CHANGES).map(<[RepoChange]>::to_vec).collect() |
| 37 | } |
| 38 | |
| 39 | /// Tells notify of the batch's changes. Logged and left when it cannot be |
| 40 | /// reached: pages then catch up by themselves, and nothing else waits on it. |
| 41 | pub(crate) async fn tell(notify: &Fetcher, events: &[Event]) { |
| 42 | for changes in changes_of(events) { |
| 43 | let told: Result<serde_json::Value> = g1t_kit::call(notify, REPO_CHANGED, &RepoChangedArgs { changes }).await; |
| 44 | if let Err(error) = told { |
| 45 | worker::console_error!("events: live pages not told: {error}"); |
| 46 | } |
| 47 | } |
| 48 | for changes in workspace_changes_of(events) { |
| 49 | let told: Result<serde_json::Value> = g1t_kit::call(notify, WORKSPACE_CHANGED, &WorkspaceChangedArgs { changes }).await; |
| 50 | if let Err(error) = told { |
| 51 | worker::console_error!("events: live Work pages not told: {error}"); |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | #[cfg(test)] |
| 57 | mod tests { |
| 58 | use super::*; |
| 59 | use serde_json::json; |
| 60 | |
| 61 | fn event(kind: &str, repo_id: Option<&str>, data: serde_json::Value) -> Event { |
| 62 | Event { |
| 63 | id: "evt_1".into(), |
| 64 | kind: kind.into(), |
| 65 | source: "work".into(), |
| 66 | time: "2026-10-11T10:00:00Z".into(), |
| 67 | repo_id: repo_id.map(Into::into), |
| 68 | actor: None, |
| 69 | data, |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | #[test] |
| 74 | fn a_work_item_changes_every_repository_it_is_linked_to() { |
| 75 | let data = json!({ "itemId": "wki_1", "workspace": "acme", "number": 40, "repoIds": ["rep_1", "rep_2"] }); |
| 76 | let repos = |event: &Event| changes_for(event).into_iter().map(|change| (change.repo_id, change.number)).collect::<Vec<_>>(); |
| 77 | let with_origin = event("work_item.updated", Some("rep_1"), data.clone()); |
| 78 | assert_eq!(repos(&with_origin), vec![("rep_1".to_owned(), Some(40)), ("rep_2".to_owned(), Some(40))]); |
| 79 | // Filed in the workspace: only the repositories it is linked to. |
| 80 | let without = event("work_item.linked", None, data); |
| 81 | assert_eq!(repos(&without).len(), 2); |
| 82 | // What changes no page stays quiet. |
| 83 | assert!(changes_for(&event("audit.recorded", None, json!({ "repoIds": ["rep_1"] }))).is_empty()); |
| 84 | } |
| 85 | } |