1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//! Display rows as stored on session tree nodes.
//!
//! A node stores the model messages it adds (`transition`) and the transcript
//! rows it shows (`display_messages`). The two are usually the same messages.
//! Version 5 files store such a row as a reference, `{"model":3}`: "message 3
//! of this node's own transition". Only rows that differ from every message
//! the node stores keep full content, `{"message":{...}}`; user input rewritten
//! for the model and background results are the usual cases.
//!
//! References never leave their node, so readers resolve them from one record:
//! the tree at insert time, and search evidence from the raw JSON record.
//! Version 1-4 rows always carry `timestamp` and `message`; older binaries
//! reject version 5 headers, so they never see a reference.
use rho_providers::model::Message;
use serde::{Deserialize, Serialize, Serializer};
use super::persistence::StoredDisplayMessage;
/// First session version whose node display rows may reference model messages.
pub(super) const MODEL_REF_VERSION: u32 = 5;
/// How a session file stores display rows; fixed by its header version.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum DisplayRowFormat {
/// Versions 1-4: every row repeats its timestamp and full message.
Inline,
/// Version 5: rows identical to a message of the same node reference it.
ModelRefs,
}
impl DisplayRowFormat {
pub(super) fn for_version(version: u32) -> Self {
if version >= MODEL_REF_VERSION {
Self::ModelRefs
} else {
Self::Inline
}
}
}
/// One stored display row.
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(try_from = "RawDisplayRow<String, Message>")]
pub(crate) enum StoredDisplayRow {
/// The message at this index of the node's own model messages (the
/// appended history of a delta, the full history of a snapshot), shown
/// at the node timestamp.
Model(usize),
/// Full content. A `None` timestamp means the node timestamp.
Inline {
timestamp: Option<String>,
message: Message,
},
}
impl StoredDisplayRow {
/// Stores `tail` for a node whose transition holds `model`, at `timestamp`.
///
/// Rows normally follow model order, so the search starts after the last
/// match and wraps once; a repeated message may resolve to any equal copy.
pub(crate) fn encode(
tail: &[Message],
model: &[Message],
format: DisplayRowFormat,
timestamp: &str,
) -> Vec<Self> {
if format == DisplayRowFormat::Inline {
return tail
.iter()
.map(|message| Self::Inline {
timestamp: Some(timestamp.to_owned()),
message: message.clone(),
})
.collect();
}
let mut next = 0;
tail.iter()
.map(|message| {
let found = model[next..]
.iter()
.position(|candidate| candidate == message)
.map(|offset| next + offset)
.or_else(|| {
model[..next]
.iter()
.position(|candidate| candidate == message)
});
match found {
Some(index) => {
next = index + 1;
Self::Model(index)
}
None => Self::Inline {
timestamp: None,
message: message.clone(),
},
}
})
.collect()
}
/// Resolves stored rows into full display messages. Errors name the
/// node so a malformed or mis-versioned record fails the load loudly.
pub(crate) fn resolve(
rows: Vec<Self>,
model: &[Message],
format: DisplayRowFormat,
timestamp: &str,
node_id: &str,
) -> anyhow::Result<Vec<StoredDisplayMessage>> {
rows.into_iter()
.map(|row| match (row, format) {
(
Self::Inline {
timestamp: Some(timestamp),
message,
},
_,
) => Ok(StoredDisplayMessage { timestamp, message }),
(
Self::Inline {
timestamp: None,
message,
},
DisplayRowFormat::ModelRefs,
) => Ok(StoredDisplayMessage {
timestamp: timestamp.to_owned(),
message,
}),
(Self::Model(index), DisplayRowFormat::ModelRefs) => {
let message = model.get(index).ok_or_else(|| {
anyhow::anyhow!(
"node '{node_id}' display row references model message {index}, \
but the node stores {}",
model.len()
)
})?;
Ok(StoredDisplayMessage {
timestamp: timestamp.to_owned(),
message: message.clone(),
})
}
(
Self::Inline {
timestamp: None, ..
}
| Self::Model(_),
DisplayRowFormat::Inline,
) => {
anyhow::bail!(
"node '{node_id}' display row needs a timestamp and message before \
session version {MODEL_REF_VERSION}"
)
}
})
.collect()
}
}
/// JSON shape shared by both variants; exactly one of `model` and `message`.
/// Generic so saves serialize borrowed content. Missing `Option` fields
/// deserialize as `None`.
#[derive(Deserialize, Serialize)]
struct RawDisplayRow<T, M> {
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<M>,
}
impl TryFrom<RawDisplayRow<String, Message>> for StoredDisplayRow {
type Error = String;
fn try_from(row: RawDisplayRow<String, Message>) -> Result<Self, Self::Error> {
match row {
RawDisplayRow {
timestamp: None,
model: Some(index),
message: None,
} => Ok(Self::Model(index)),
RawDisplayRow {
timestamp,
model: None,
message: Some(message),
} => Ok(Self::Inline { timestamp, message }),
_ => Err(
"display row needs exactly one of `model` or `message`, and model rows take no timestamp"
.into(),
),
}
}
}
impl Serialize for StoredDisplayRow {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Model(index) => RawDisplayRow::<&str, &Message> {
timestamp: None,
model: Some(*index),
message: None,
},
Self::Inline { timestamp, message } => RawDisplayRow {
timestamp: timestamp.as_deref(),
model: None,
message: Some(message),
},
}
.serialize(serializer)
}
}
#[cfg(test)]
#[path = "display_rows_tests.rs"]
mod tests;