lib.rsannotatedlib.rssource601 lines · 22.8 KB · raw
1//! The archive: every request lmjtfy has had answered, and the questions
2//! people asked. These are the messages the Worker and the archive's Durable
3//! Object exchange. No I/O and no clock.
4//!
5//! The rule (the user, 2026-10-02): the same request is never sent to the
6//! same model twice. A request is the exact body, and it is kept with its
7//! response under the model it went to. Asking again returns that response.
8//! The archive is also what makes the call, so two visitors asking the same
9//! new thing at once share one request.
10#![forbid(unsafe_code)]
11
12use ask::{Sent, Wanted};
13use serde::{Deserialize, Serialize};
14
15pub mod event;
16pub use event::{Event, Origin, Seen as Report};
17
18/// What the Worker asks the archive.
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
20pub enum Ask {
21    /// Jev's answer to `wanted` about `input`. `request` is the body the
22    /// Worker prepared and shows on the page; the archive prepares it again
23    /// from `wanted`, and refuses if the two differ.
24    Jev { input: String, wanted: Wanted, request: String, #[serde(default)] pick: Pick },
25    /// The LLM's reply to `request`, from `model`.
26    Llm { model: String, request: String, #[serde(default)] pick: Pick },
27    /// `input` was asked and got `answers`, one per question Jev answered.
28    /// `llm` is whether an LLM had to be asked; `listed` is whether the
29    /// question may be shown on the public feed. `who` is the asking
30    /// browser for this question only (`Asker`): a browser that asked it
31    /// before is not counted again. `None` when the browser sent no id.
32    /// `browser` is that browser's id itself, kept beside `who` so the
33    /// owner can see whose row it is.
34    Asked {
35        input: String,
36        answers: Vec<Answer>,
37        llm: bool,
38        listed: bool,
39        #[serde(default)]
40        who: Option<Asker>,
41        #[serde(default)]
42        browser: String,
43    },
44    /// Someone cloned or pulled `repo` (`lmjtfy.git`) through the site.
45    /// Counted, and told to every open page.
46    Fetched { repo: String, fetch: Fetch },
47    /// `who` thinks Jev's answer to `input` right (`Up`) or wrong (`Down`).
48    /// The vote is for the answer as it is kept now; the same vote again
49    /// takes it back. Answered with the `Rating` after it.
50    Rate {
51        input: String,
52        who: Asker,
53        vote: Vote,
54        #[serde(default)]
55        browser: String,
56    },
57    /// The votes on Jev's answer to `input` as it is kept now, and `who`'s.
58    Rating { input: String, who: Option<Asker> },
59    /// What the home page shows from the archive.
60    Home,
61    /// The next `FEED` listed questions asked before `after`, newest first:
62    /// the feed scrolled to its end.
63    Older { after: Cursor },
64    /// What Jev said to `input`, if it was ever asked and answered. Nothing
65    /// is sent to find out.
66    Answer { input: String },
67    /// Something happened, to be kept whole (`event.rs`).
68    Event(Box<Event>),
69}
70
71/// The archive object's name: there is one, for the whole site. A Worker
72/// that binds the archive (the site's, the admin's) reaches it by this.
73pub const OBJECT: &str = "everything";
74
75/// The path of the archive's door for the owner's admin backend. The site's
76/// own Worker never sends a request there; the admin's Worker, which binds
77/// the same object from the owner's account, does.
78pub const ADMIN: &str = "/admin";
79
80/// What the admin backend asks the archive.
81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82pub enum Admin {
83    /// Rows of the archive's tables: one statement that only reads
84    /// (`reads_only`), with a value for each `?`.
85    Select {
86        sql: String,
87        #[serde(default)]
88        values: Vec<serde_json::Value>,
89    },
90    /// The owner's say on whether `input` may be shown on the feed: yes, no,
91    /// or `None` to leave it to the rules again. Jev's own verdict
92    /// (`asked.listed`) is kept beside it, untouched.
93    Moderate { input: String, listed: Option<bool> },
94}
95
96/// What the archive answers the admin backend.
97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
98pub enum Answered {
99    Rows { columns: Vec<String>, rows: Vec<Vec<serde_json::Value>> },
100    Done,
101    Refused(String),
102}
103
104/// Whether `sql` is one statement that can only read. The admin's door
105/// changes the archive through `Admin`'s own messages, never through SQL:
106/// the schema changes only by a migration, and a response that was kept is
107/// never edited. Text in quotes is not looked at, so a question that says
108/// "delete" can still be searched for; pass such text as a value.
109pub fn reads_only(sql: &str) -> bool {
110    let mut bare = String::new();
111    let mut quoted = None;
112    for c in sql.chars() {
113        match quoted {
114            Some(quote) if c == quote => quoted = None,
115            Some(_) => {}
116            None if c == '\'' || c == '"' => quoted = Some(c),
117            None => bare.push(c.to_ascii_lowercase()),
118        }
119    }
120    let bare = bare.trim().trim_end_matches(';').trim_end();
121    let words: Vec<&str> = bare.split(|c: char| !c.is_ascii_alphanumeric() && c != '_').filter(|word| !word.is_empty()).collect();
122    const WRITES: [&str; 13] = ["insert", "update", "delete", "replace", "drop", "alter", "create", "attach", "detach", "pragma", "vacuum", "reindex", "analyze"];
123    quoted.is_none() && !bare.contains(';') && matches!(words.first(), Some(&"select") | Some(&"with")) && !words.iter().any(|word| WRITES.contains(word))
124}
125
126/// Which of a request's kept responses an ask wants. A request is sent
127/// again only when a visitor asks for that (`Fresh`, the page's ↻); every
128/// response it ever got is kept, numbered from 1, newest last.
129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
130pub enum Pick {
131    /// The newest kept, or sent if there is none.
132    #[default]
133    Latest,
134    /// Sent now, whatever is kept, and kept as the newest.
135    Fresh,
136    /// That one, if kept; the newest kept otherwise.
137    Version(u32),
138    /// The newest kept, and never sent: `Called::NotKept` if there is none.
139    /// What a page that is stepping through old versions uses for the calls
140    /// after the one it stepped, so looking costs nothing.
141    KeptOnly,
142}
143
144/// Which version of each call a page wants, by `call_id`: the page's
145/// `$pins` signal, `id:3;id:fresh`, later entries winning. A call it does
146/// not name gets the newest kept.
147#[derive(Clone, Debug, Default, PartialEq, Eq)]
148pub struct Pins(pub Vec<(String, Pick)>);
149
150impl Pins {
151    pub fn parse(text: &str) -> Pins {
152        let mut pins: Vec<(String, Pick)> = Vec::new();
153        for entry in text.split(';') {
154            let Some((id, what)) = entry.trim().split_once(':') else { continue };
155            if id.len() != 12 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
156                continue;
157            }
158            let pick = match what {
159                "fresh" => Pick::Fresh,
160                number => match number.parse::<u32>() {
161                    Ok(version) if version > 0 => Pick::Version(version),
162                    _ => continue,
163                },
164            };
165            pins.retain(|(seen, _)| seen != id);
166            pins.push((id.to_owned(), pick));
167        }
168        Pins(pins)
169    }
170
171    /// Stepping through old versions, and asking nothing new: a version is
172    /// pinned and nothing is to be sent again. Then a call that is not pinned
173    /// is only read, never sent, so looking costs nothing.
174    pub fn browsing(&self) -> bool {
175        self.0.iter().any(|(_, pick)| matches!(pick, Pick::Version(_))) && !self.0.iter().any(|(_, pick)| *pick == Pick::Fresh)
176    }
177
178    /// What to ask the archive for, for the call `id`.
179    pub fn pick(&self, id: &str) -> Pick {
180        match self.0.iter().find(|(seen, _)| seen == id) {
181            Some((_, pick)) => *pick,
182            None if self.browsing() => Pick::KeptOnly,
183            None => Pick::Latest,
184        }
185    }
186}
187
188/// A call, as the page names it for ↻ and ◀ ▶: the first 12 hex digits of
189/// the SHA-256 of where it went and what was sent.
190pub fn call_id(sent_to: &str, request: &str) -> String {
191    use sha2::{Digest, Sha256};
192    let digest = Sha256::new().chain_update(sent_to.as_bytes()).chain_update([0]).chain_update(request.as_bytes()).finalize();
193    digest.iter().take(6).map(|byte| format!("{byte:02x}")).collect()
194}
195
196/// A response the archive holds, and the call that got it.
197#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
198pub struct Record {
199    /// The response body exactly as it arrived.
200    pub response: String,
201    pub request_id: Option<String>,
202    pub attempts: u32,
203    /// The round trip of the call that was sent, in milliseconds.
204    pub took_ms: f64,
205    /// When that call was answered, in Unix milliseconds.
206    pub answered_ms: f64,
207    /// Whether this ask is the one that sent it.
208    pub sent_now: bool,
209    /// Which of the request's kept responses this is, from 1, and how many
210    /// there are.
211    #[serde(default = "first")]
212    pub version: u32,
213    #[serde(default = "first")]
214    pub versions: u32,
215}
216
217fn first() -> u32 {
218    1
219}
220
221impl Record {
222    pub fn sent(&self) -> Sent {
223        if self.sent_now { Sent::Now } else { Sent::Before { at_ms: self.answered_ms } }
224    }
225
226    /// The same record, as someone who did not send it sees it.
227    pub fn kept(mut self) -> Self {
228        self.sent_now = false;
229        self
230    }
231}
232
233/// A browser, for one question: the hex SHA-256 of the browser's random id
234/// and the question. It says whether this browser asked this question
235/// before, and nothing else: two questions from one browser give unrelated
236/// values, and the id cannot be had back from one.
237#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
238pub struct Asker(pub String);
239
240/// A browser's view of Jev's answer.
241#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
242pub enum Vote {
243    Up,
244    Down,
245}
246
247/// The votes on one answer, and the asking browser's own, if it voted.
248#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
249pub struct Rating {
250    pub up: u32,
251    pub down: u32,
252    pub mine: Option<Vote>,
253}
254
255/// Which shared budget refused a call.
256#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
257pub enum Pot {
258    Jev,
259    Llm,
260}
261
262/// What became of a call.
263#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
264pub enum Called {
265    Answered(Record),
266    /// Sent, or not sendable, and there is no response. Nothing is kept, so
267    /// the same request may be sent again.
268    Failed { error: String, request_id: Option<String>, took_ms: f64 },
269    /// Not sent: today's budget for it is used up.
270    Spent(Pot),
271    /// Not sent, because the ask was `Pick::KeptOnly` and nothing is kept.
272    NotKept,
273}
274
275/// What Jev said to one question, as it is kept for the feed and for link
276/// previews: the answer in a few words, and the numbers behind it.
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278pub struct Answer {
279    /// What the page prints large: "No?", "Pepperoni.", "28%.".
280    pub headline: String,
281    pub detail: Detail,
282}
283
284/// The numbers behind an answer, by the type of question it answered.
285#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
286pub enum Detail {
287    /// Kept before details were. Only the headline is known.
288    Unknown,
289    /// A yes-or-no: the probability of yes.
290    Noul { p_yes: f64 },
291    /// A how-likely, which Jev answers as a Noul about the thing itself: the
292    /// probability that it is so.
293    Chance { p: f64 },
294    /// A pick: every option with its probability, most likely first.
295    Choice { confidence: f64, options: Vec<(String, f64)> },
296    /// A how-much: every level with its probability, lowest first.
297    Score { score: f64, confidence: f64, levels: Vec<(String, f64)> },
298}
299
300impl Answer {
301    /// The type and the numbers in one line, as the page prints them under
302    /// an answer. `None` when only the headline was kept.
303    pub fn note(&self) -> Option<String> {
304        Some(match &self.detail {
305            Detail::Unknown => return None,
306            Detail::Noul { p_yes } => format!("Noul · p(yes) = {p_yes:.2}"),
307            Detail::Chance { p } => format!("Noul · p = {p:.2} · Jev thinks that's {}", ask::likelihood(*p)),
308            Detail::Choice { confidence, .. } => format!("Choice · confidence {confidence:.2}"),
309            Detail::Score { score, confidence, levels } => {
310                format!("Score · {score:.1} of {} · confidence {confidence:.2}", levels.len().saturating_sub(1))
311            }
312        })
313    }
314
315    /// The `most` likeliest options or levels, likeliest first, for a line
316    /// of text. Empty for a Noul, whose one number is in the note.
317    pub fn spread(&self, most: usize) -> Vec<(&str, f64)> {
318        let mut spread: Vec<(&str, f64)> = match &self.detail {
319            Detail::Choice { options, .. } => options.iter().map(|(label, p)| (label.as_str(), *p)).collect(),
320            Detail::Score { levels, .. } => levels.iter().map(|(label, p)| (label.as_str(), *p)).collect(),
321            _ => Vec::new(),
322        };
323        spread.sort_by(|a, b| b.1.total_cmp(&a.1));
324        spread.truncate(most);
325        spread
326    }
327
328    /// Everything in one line: the headline, the note and the spread.
329    pub fn line(&self) -> String {
330        let mut parts = vec![self.headline.clone()];
331        parts.extend(self.note());
332        let spread: Vec<String> = self.spread(3).iter().map(|(label, p)| format!("{label} {:.0}%", p * 100.0)).collect();
333        if !spread.is_empty() {
334            parts.push(spread.join(", "));
335        }
336        parts.join(" · ")
337    }
338}
339
340/// A kept answer as it is stored: with its numbers, or, from before they
341/// were kept, as the headline alone.
342#[derive(Deserialize)]
343#[serde(untagged)]
344enum Stored {
345    Whole(Answer),
346    Headline(String),
347}
348
349/// Reads the answers a question was stored with, old form or new.
350pub fn stored(text: &str) -> Vec<Answer> {
351    let stored: Vec<Stored> = serde_json::from_str(text).unwrap_or_default();
352    stored
353        .into_iter()
354        .map(|stored| match stored {
355            Stored::Whole(answer) => answer,
356            Stored::Headline(headline) => Answer { headline, detail: Detail::Unknown },
357        })
358        .collect()
359}
360
361/// A question somebody asked, and what Jev said. Nothing about who asked.
362#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
363pub struct Entry {
364    pub input: String,
365    pub answers: Vec<Answer>,
366    /// When it was last asked.
367    pub asked_ms: f64,
368    /// How many times it has been asked and answered.
369    pub times: u32,
370}
371
372impl Entry {
373    /// The answers in a few words each, as the feed prints them.
374    pub fn headlines(&self) -> String {
375        let headlines: Vec<&str> = self.answers.iter().map(|answer| answer.headline.as_str()).collect();
376        headlines.join(" ")
377    }
378}
379
380/// How many questions the feed shows as asked lately, and as most asked.
381pub const FEED: u32 = 50;
382pub const MOST: u32 = 5;
383
384/// The archive in numbers.
385#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
386pub struct Stats {
387    /// Different questions answered.
388    pub questions: u32,
389    /// Times a question was asked and answered, repeats included.
390    pub asks: u32,
391    /// Of the different questions, those Jev answered with no LLM.
392    pub no_llm: u32,
393    /// Requests that went out and were answered.
394    pub sent: u32,
395    /// Requests that did not go out, because their response was kept.
396    pub kept: u32,
397    /// Clones and pulls of the code through the site.
398    #[serde(default)]
399    pub clones: u32,
400    #[serde(default)]
401    pub pulls: u32,
402}
403
404impl Stats {
405    /// The share of questions answered with no LLM, as a whole percentage.
406    pub fn no_llm_percent(&self) -> Option<u32> {
407        (self.questions > 0).then(|| (f64::from(self.no_llm) * 100.0 / f64::from(self.questions)).round() as u32)
408    }
409}
410
411/// What the home page shows from the archive. The lists hold only questions
412/// that may be shown publicly; the numbers count every question.
413#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
414pub struct Home {
415    /// Newest first.
416    pub lately: Vec<Entry>,
417    /// Asked more than once, most asked first.
418    pub most: Vec<Entry>,
419    pub stats: Stats,
420}
421
422/// Where the feed was scrolled to: the last question shown. Questions are
423/// listed newest first, ties broken by the question itself, so the next page
424/// starts strictly after this one and none is shown twice or skipped.
425#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
426pub struct Cursor {
427    pub asked_ms: f64,
428    pub input: String,
429}
430
431impl Entry {
432    /// Where the feed is once this entry is the last shown.
433    pub fn cursor(&self) -> Cursor {
434        Cursor { asked_ms: self.asked_ms, input: self.input.clone() }
435    }
436}
437
438/// What a fetch through the clone proxy was. Git's upload-pack request
439/// names the commits wanted; a pull also names the ones it has.
440#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
441pub enum Fetch {
442    Clone,
443    Pull,
444}
445
446impl Fetch {
447    /// Its name in the archive's `counts`, for one repository.
448    pub fn counted(self, repo: &str) -> String {
449        match self {
450            Fetch::Clone => format!("clone:{repo}"),
451            Fetch::Pull => format!("pull:{repo}"),
452        }
453    }
454}
455
456/// What the archive pushes to every open page over its socket, as JSON:
457/// `{"online": 3}` or `{"toast": "<html>"}`.
458#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
459#[serde(rename_all = "lowercase")]
460pub enum Live {
461    /// Pages open now.
462    Online(u32),
463    /// Something happened, as markup to show for a moment.
464    Toast(String),
465    /// The build the site is now (`LMJTFY_BUILD`), told to each page as it
466    /// connects. A page built from another offers a reload: a deploy
467    /// restarts the object, every page reconnects, and each hears this.
468    Build(String),
469    /// Elements that changed, each with an `id`: a page that has an element
470    /// with that id replaces it. An element with `data-prepend="<id>"`
471    /// instead puts its children at the top of that element, each removing
472    /// any element with its id first: a question asked again moves to the
473    /// top of the feed, and what a page has scrolled in below stays. The
474    /// activity feeds stay current this way, with no refresh.
475    Patch(String),
476}
477
478/// Where an open page connected from, as Cloudflare placed the request: a
479/// country code and a city, either unknown. It is kept on the page's socket
480/// while it is open, for the online list.
481#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
482pub struct Place {
483    pub country: Option<String>,
484    pub city: Option<String>,
485}
486
487impl Place {
488    /// From what the Worker passed on, kept only if it looks like a place:
489    /// a country is two letters, a city a short line of text.
490    pub fn new(country: Option<&str>, city: Option<&str>) -> Place {
491        let country = country
492            .map(str::trim)
493            .filter(|code| code.len() == 2 && code.bytes().all(|b| b.is_ascii_alphabetic()))
494            .map(str::to_ascii_uppercase)
495            // Cloudflare's own codes for Tor and for unknown are not places.
496            .filter(|code| code != "T1" && code != "XX");
497        let city = city
498            .map(str::trim)
499            .filter(|city| !city.is_empty() && city.chars().count() <= 64 && !city.chars().any(char::is_control))
500            .map(str::to_owned);
501        Place { country, city }
502    }
503
504    /// The country's flag: its two letters as regional indicator symbols.
505    pub fn flag(&self) -> Option<String> {
506        let code = self.country.as_deref()?;
507        code.chars().map(|letter| char::from_u32(0x1F1E6 + (letter as u32 - 'A' as u32))).collect()
508    }
509
510    /// `Austin, US`, `US`, or `somewhere`.
511    pub fn label(&self) -> String {
512        match (&self.city, &self.country) {
513            (Some(city), Some(country)) => format!("{city}, {country}"),
514            (Some(city), None) => city.clone(),
515            (None, Some(country)) => country.clone(),
516            (None, None) => "somewhere".to_owned(),
517        }
518    }
519}
520
521/// A header value as the Worker percent-encoded it (`+` for a space), back
522/// to text. `None` if it is not UTF-8 or a `%` is not followed by two hex
523/// digits.
524pub fn percent_decoded(text: &str) -> Option<String> {
525    let mut bytes = Vec::with_capacity(text.len());
526    let mut rest = text.bytes();
527    while let Some(byte) = rest.next() {
528        match byte {
529            b'+' => bytes.push(b' '),
530            b'%' => {
531                let hex = [rest.next()?, rest.next()?];
532                bytes.push(u8::from_str_radix(std::str::from_utf8(&hex).ok()?, 16).ok()?);
533            }
534            byte => bytes.push(byte),
535        }
536    }
537    String::from_utf8(bytes).ok()
538}
539
540/// What the archive keeps on an open page's socket: where it is, and whether
541/// a Worker that passes places on sent it. A page that reconnects while a
542/// deploy is still reaching Cloudflare's edge can come through the Worker
543/// from before, which passes no place; `passed` is false for it, and the
544/// archive asks it to reconnect once the deploy has settled.
545#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
546pub struct Seen {
547    pub place: Place,
548    pub passed: bool,
549    /// When it connected, in Unix milliseconds.
550    pub at_ms: f64,
551    /// The row of `events` that says it connected, which the row that says
552    /// it left copies who it was from.
553    #[serde(default)]
554    pub event: Option<f64>,
555}
556
557/// How long after connecting an unplaced page is asked to reconnect: long
558/// enough for a deploy to reach the whole edge.
559pub const REPLACE_AFTER_MS: f64 = 10_000.0;
560
561impl Seen {
562    /// Whether this page should reconnect to be placed: it came through a
563    /// Worker that passes no place, long enough ago. A socket with nothing
564    /// kept on it (`None`) was accepted by code from before places.
565    pub fn stale(seen: Option<&Seen>, now_ms: f64) -> bool {
566        seen.is_none_or(|seen| !seen.passed && now_ms - seen.at_ms >= REPLACE_AFTER_MS)
567    }
568}
569
570/// How many open pages are in each place, most first, then by name.
571pub fn places(open: impl IntoIterator<Item = Place>) -> Vec<(Place, u32)> {
572    let mut counted: std::collections::BTreeMap<Place, u32> = std::collections::BTreeMap::new();
573    for place in open {
574        *counted.entry(place).or_default() += 1;
575    }
576    let mut counted: Vec<(Place, u32)> = counted.into_iter().collect();
577    counted.sort_by(|(a, n), (b, m)| m.cmp(n).then_with(|| a.label().cmp(&b.label())));
578    counted
579}
580
581/// What the archive says back.
582#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
583pub enum Told {
584    Called(Called),
585    Noted,
586    Home(Home),
587    Answer(Option<Entry>),
588    Older(Vec<Entry>),
589    /// `None` if the question was never answered, so there is nothing to
590    /// vote on.
591    Rating(Option<Rating>),
592}
593
594/// A moment as `YYYY-MM-DD HH:MM UTC`.
595pub fn when(unix_ms: f64) -> String {
596    let minutes = (unix_ms / 60_000.0).floor().max(0.0) as u64;
597    format!("{} {:02}:{:02} UTC", budget::date(budget::day(unix_ms)), minutes / 60 % 24, minutes % 60)
598}
599
600#[cfg(test)]
601mod tests;