//! Events: what Koh learns about saves that already exist. //! //! Save records are write-once, so a flag is not stored on the save. It is //! appended as a small event that syncs alongside saves, and a save's flag //! state is always derived from the newest event about it — never stored twice. //! A strip event is what makes `strip` visible to other machines. module events import strings import time import src.machine import src.storage pub const kind_flag = 'flag' pub const kind_clear = 'clear' pub const kind_strip = 'strip' // An event about one save. pub struct Event { pub mut: id string kind string save_id string path string reason string created_at i64 machine string } // Flag is the derived answer to "is this save flagged, and why". pub struct Flag { pub: save_id string flagged bool reason string at i64 } const select_event = 'SELECT id, kind, save_id, path, reason, created_at, machine FROM events' fn read_event(stmt &storage.Stmt) Event { return Event{ id: stmt.text(0) kind: stmt.text(1) save_id: stmt.text_or_none(2) or { '' } path: stmt.text_or_none(3) or { '' } reason: stmt.text_or_none(4) or { '' } created_at: stmt.int(5) machine: stmt.text(6) } } // write appends an event. The ID is the first 12 hex characters of the Blake3 // hash of the record, so two machines that record the same fact agree. pub fn write(store &storage.Store, event Event) !Event { mut record := event if record.created_at == 0 { // Events are ordered by time, and flag/clear can land in the same // second. Bumping past the newest event keeps "newest wins" true, and // keeps the ordering the same on every machine. record.created_at = next_timestamp(store)! } if record.machine == '' { record.machine = machine.name() } record.id = event_id(record) insert(store, record)! return record } // receive stores an event exactly as another machine recorded it, ID and all. // An ID re-derived here that disagreed with the sender's would make the event // look new on every steal. Reports whether it was new. pub fn receive(store &storage.Store, event Event) !bool { insert(store, event)! return store.db.changes() > 0 } // next_timestamp is now, or one second past the newest event when that is not earlier. fn next_timestamp(store &storage.Store) !i64 { now := time.now().unix() mut stmt := store.db.prepare('SELECT MAX(created_at) FROM events')! defer { stmt.finalize() } if !stmt.step()! { return now } newest := stmt.int_or_none(0) or { return now } if newest >= now { return newest + 1 } return now } // insert ignores an ID already stored, which is how receive tells a repeat from a new event. fn insert(store &storage.Store, record Event) ! { mut stmt := store.db.prepare('INSERT OR IGNORE INTO events (id, kind, save_id, path, reason, created_at, machine) VALUES (?, ?, ?, ?, ?, ?, ?)')! defer { stmt.finalize() } stmt.bind_text(1, record.id)! stmt.bind_text(2, record.kind)! bind_optional(stmt, 3, record.save_id)! bind_optional(stmt, 4, record.path)! bind_optional(stmt, 5, record.reason)! stmt.bind_int(6, record.created_at)! stmt.bind_text(7, record.machine)! stmt.step()! } pub fn flag(store &storage.Store, save_id string, reason string) !Event { return write(store, Event{ kind: kind_flag save_id: save_id reason: reason })! } pub fn clear_flag(store &storage.Store, save_id string) !Event { return write(store, Event{ kind: kind_clear save_id: save_id })! } // strip records that a path was removed from a save (or from every save, when // save_id is empty). Each strip is its own event, even of a path stripped // before: a machine that applied the first must still hear about the second. pub fn strip(store &storage.Store, save_id string, path string) !Event { return write(store, Event{ kind: kind_strip save_id: save_id path: path })! } // valid_strip is a strip event Doctor may act on. Doctor reports an invalid one // and leaves the saves alone (spec §7.6.2). pub fn valid_strip(event Event) bool { return event.kind == kind_strip && event.path != '' && event.created_at >= 0 } // event_id hashes each field with its length, so two different facts cannot spell the same input. pub fn event_id(event Event) string { mut sb := strings.Builder{} put(mut sb, 'kind', event.kind) put(mut sb, 'save', event.save_id) put(mut sb, 'path', event.path) put(mut sb, 'reason', event.reason) put(mut sb, 'created', event.created_at.str()) put(mut sb, 'machine', event.machine) return storage.hash_hex(sb.str().bytes())[..12] } fn put(mut sb strings.Builder, name string, value string) { sb.write_string('${name}:${value.len}:') sb.write_string(value) sb.write_string('\n') } fn bind_optional(stmt &storage.Stmt, index int, value string) ! { if value == '' { stmt.bind_null(index)! } else { stmt.bind_text(index, value)! } } // all returns every event, oldest first. pub fn all(store &storage.Store) ![]Event { mut stmt := store.db.prepare('${select_event} ORDER BY created_at, id')! defer { stmt.finalize() } mut out := []Event{} for stmt.step()! { out << read_event(stmt) } return out } // latest_flags derives the flag state of every save that has ever been flagged // or cleared. Newest wins, ties broken by ID so the answer is the same on every // machine. pub fn latest_flags(store &storage.Store) !map[string]Flag { mut stmt := store.db.prepare('${select_event} WHERE kind IN (?, ?) AND save_id IS NOT NULL ORDER BY created_at, id')! defer { stmt.finalize() } stmt.bind_text(1, kind_flag)! stmt.bind_text(2, kind_clear)! mut flags := map[string]Flag{} for stmt.step()! { event := read_event(stmt) flags[event.save_id] = Flag{ save_id: event.save_id flagged: event.kind == kind_flag reason: event.reason at: event.created_at } } return flags } // flag_state is latest_flags for one save; a save never flagged reads as not flagged. pub fn flag_state(store &storage.Store, save_id string) !Flag { flags := latest_flags(store)! return flags[save_id] or { Flag{ save_id: save_id } } } // strip_events lists every strip event, whole-history or one save, oldest first. pub fn strip_events(store &storage.Store) ![]Event { mut stmt := store.db.prepare('${select_event} WHERE kind = ? ORDER BY created_at, id')! defer { stmt.finalize() } stmt.bind_text(1, kind_strip)! mut out := []Event{} for stmt.step()! { out << read_event(stmt) } return out }