///|
/// An ordered collection of normalized events for deterministic timeline queries.
pub(all) struct Timeline {
events : Array[NormalizedEvent]
} derive(Debug, Eq, ToJson)
///|
/// A query window that cannot be evaluated because its start follows its end.
pub(all) suberror TimelineError {
InvalidWindow
} derive(Debug, Eq)
///|
fn compare_timeline_events(
left : NormalizedEvent,
right : NormalizedEvent,
) -> Int {
match (left.timestamp, right.timestamp) {
(Some(left_time), Some(right_time)) =>
compare_timestamps(left_time, right_time)
(Some(_), None) => -1
(None, Some(_)) => 1
(None, None) => 0
}
}
///|
fn merge_timeline_events(
left : Array[NormalizedEvent],
right : Array[NormalizedEvent],
) -> Array[NormalizedEvent] {
let merged : Array[NormalizedEvent] = []
for left_index = 0, right_index = 0; left_index < left.length() &&
right_index < right.length(); {
if compare_timeline_events(left[left_index], right[right_index]) <= 0 {
merged.push(left[left_index])
continue left_index + 1, right_index
} else {
merged.push(right[right_index])
continue left_index, right_index + 1
}
} nobreak {
for index in left_index.. Array[NormalizedEvent] {
if events.length() <= 1 {
events
} else {
let midpoint = events.length() / 2
let left : Array[NormalizedEvent] = []
let right : Array[NormalizedEvent] = []
for index, event in events {
if index < midpoint {
left.push(event)
} else {
right.push(event)
}
}
merge_timeline_events(
sort_timeline_events(left),
sort_timeline_events(right),
)
}
}
///|
/// Builds a stable timeline ordered by UTC time; events without timestamps come last.
/// Equal timestamps retain their input order.
///
/// # Example
/// ```mbt check
/// test {
/// let event = NormalizedEvent::{
/// timestamp: None,
/// severity: None,
/// source_id: "svc/api",
/// raw_content: "timestamp unavailable",
/// }
/// let timeline = build_timeline([event])
/// assert_eq(timeline.events.length(), 1)
/// }
/// ```
pub fn build_timeline(events : Array[NormalizedEvent]) -> Timeline {
let sorted = sort_timeline_events(events)
let copy : Array[NormalizedEvent] = []
for event in sorted {
copy.push(event)
}
{ events: copy, }
}
///|
/// Keeps only events whose normalized source identifier matches `source_id`.
///
/// # Example
/// ```mbt check
/// test {
/// let event = NormalizedEvent::{
/// timestamp: None,
/// severity: None,
/// source_id: "svc/api",
/// raw_content: "entry",
/// }
/// let timeline = build_timeline([event])
/// let filtered = filter_timeline_by_source(timeline, "svc/api")
/// assert_eq(filtered.events.length(), 1)
/// }
/// ```
pub fn filter_timeline_by_source(
timeline : Timeline,
source_id : String,
) -> Timeline {
let events : Array[NormalizedEvent] = []
for event in timeline.events {
if event.source_id == source_id {
events.push(event)
}
}
{ events, }
}
///|
/// Returns events with timestamps inside the inclusive UTC window, preserving timeline order.
/// Events without timestamps are omitted.
///
/// # Example
/// ```mbt check
/// test {
/// let time = NormalizedTimestamp::{
/// epoch_day: 1,
/// second_of_day: 10,
/// nanosecond: 0,
/// raw: "start",
/// }
/// let event = NormalizedEvent::{
/// timestamp: Some(time),
/// severity: None,
/// source_id: "svc/api",
/// raw_content: "entry",
/// }
/// let timeline = build_timeline([event])
/// let result = query_timeline_window(timeline, time, time)
/// assert_eq(result.events.length(), 1)
/// }
/// ```
pub fn query_timeline_window(
timeline : Timeline,
start : NormalizedTimestamp,
end : NormalizedTimestamp,
) -> Timeline raise TimelineError {
if compare_timestamps(start, end) > 0 {
raise TimelineError::InvalidWindow
}
let events : Array[NormalizedEvent] = []
for event in timeline.events {
match event.timestamp {
Some(timestamp) =>
if compare_timestamps(timestamp, start) >= 0 &&
compare_timestamps(timestamp, end) <= 0 {
events.push(event)
}
None => ()
}
}
{ events, }
}