events.v

6.4 KB · raw

1//! Events: what Koh learns about saves that already exist.2//!3//! Save records are write-once, so a flag is not stored on the save. It is4//! appended as a small event that syncs alongside saves, and a save's flag5//! state is always derived from the newest event about it — never stored twice.6//! A strip event is what makes `strip` visible to other machines.78module events910import strings11import time12import src.machine13import src.storage1415pub const kind_flag = 'flag'16pub const kind_clear = 'clear'17pub const kind_strip = 'strip'1819// An event about one save.20pub struct Event {21pub mut:22	id         string23	kind       string24	save_id    string25	path       string26	reason     string27	created_at i6428	machine    string29}3031// Flag is the derived answer to "is this save flagged, and why".32pub struct Flag {33pub:34	save_id string35	flagged bool36	reason  string37	at      i6438}3940const select_event = 'SELECT id, kind, save_id, path, reason, created_at, machine FROM events'4142fn read_event(stmt &storage.Stmt) Event {43	return Event{44		id:         stmt.text(0)45		kind:       stmt.text(1)46		save_id:    stmt.text_or_none(2) or { '' }47		path:       stmt.text_or_none(3) or { '' }48		reason:     stmt.text_or_none(4) or { '' }49		created_at: stmt.int(5)50		machine:    stmt.text(6)51	}52}5354// write appends an event. The ID is the first 12 hex characters of the Blake355// hash of the record, so two machines that record the same fact agree.56pub fn write(store &storage.Store, event Event) !Event {57	mut record := event58	if record.created_at == 0 {59		// Events are ordered by time, and flag/clear can land in the same60		// second. Bumping past the newest event keeps "newest wins" true, and61		// keeps the ordering the same on every machine.62		record.created_at = next_timestamp(store)!63	}64	if record.machine == '' {65		record.machine = machine.name()66	}67	record.id = event_id(record)68	insert(store, record)!69	return record70}7172// receive stores an event exactly as another machine recorded it, ID and all.73// An ID re-derived here that disagreed with the sender's would make the event74// look new on every steal. Reports whether it was new.75pub fn receive(store &storage.Store, event Event) !bool {76	insert(store, event)!77	return store.db.changes() > 078}7980// next_timestamp is now, or one second past the newest event when that is not earlier.81fn next_timestamp(store &storage.Store) !i64 {82	now := time.now().unix()83	mut stmt := store.db.prepare('SELECT MAX(created_at) FROM events')!84	defer {85		stmt.finalize()86	}87	if !stmt.step()! {88		return now89	}90	newest := stmt.int_or_none(0) or { return now }91	if newest >= now {92		return newest + 193	}94	return now95}9697// insert ignores an ID already stored, which is how receive tells a repeat from a new event.98fn insert(store &storage.Store, record Event) ! {99	mut stmt := store.db.prepare('INSERT OR IGNORE INTO events100		(id, kind, save_id, path, reason, created_at, machine) VALUES (?, ?, ?, ?, ?, ?, ?)')!101	defer {102		stmt.finalize()103	}104	stmt.bind_text(1, record.id)!105	stmt.bind_text(2, record.kind)!106	bind_optional(stmt, 3, record.save_id)!107	bind_optional(stmt, 4, record.path)!108	bind_optional(stmt, 5, record.reason)!109	stmt.bind_int(6, record.created_at)!110	stmt.bind_text(7, record.machine)!111	stmt.step()!112}113114pub fn flag(store &storage.Store, save_id string, reason string) !Event {115	return write(store, Event{116		kind:    kind_flag117		save_id: save_id118		reason:  reason119	})!120}121122pub fn clear_flag(store &storage.Store, save_id string) !Event {123	return write(store, Event{124		kind:    kind_clear125		save_id: save_id126	})!127}128129// strip records that a path was removed from a save (or from every save, when130// save_id is empty). Each strip is its own event, even of a path stripped131// before: a machine that applied the first must still hear about the second.132pub fn strip(store &storage.Store, save_id string, path string) !Event {133	return write(store, Event{134		kind:    kind_strip135		save_id: save_id136		path:    path137	})!138}139140// valid_strip is a strip event Doctor may act on. Doctor reports an invalid one141// and leaves the saves alone (spec §7.6.2).142pub fn valid_strip(event Event) bool {143	return event.kind == kind_strip && event.path != '' && event.created_at >= 0144}145146// event_id hashes each field with its length, so two different facts cannot spell the same input.147pub fn event_id(event Event) string {148	mut sb := strings.Builder{}149	put(mut sb, 'kind', event.kind)150	put(mut sb, 'save', event.save_id)151	put(mut sb, 'path', event.path)152	put(mut sb, 'reason', event.reason)153	put(mut sb, 'created', event.created_at.str())154	put(mut sb, 'machine', event.machine)155	return storage.hash_hex(sb.str().bytes())[..12]156}157158fn put(mut sb strings.Builder, name string, value string) {159	sb.write_string('${name}:${value.len}:')160	sb.write_string(value)161	sb.write_string('\n')162}163164fn bind_optional(stmt &storage.Stmt, index int, value string) ! {165	if value == '' {166		stmt.bind_null(index)!167	} else {168		stmt.bind_text(index, value)!169	}170}171172// all returns every event, oldest first.173pub fn all(store &storage.Store) ![]Event {174	mut stmt := store.db.prepare('${select_event} ORDER BY created_at, id')!175	defer {176		stmt.finalize()177	}178	mut out := []Event{}179	for stmt.step()! {180		out << read_event(stmt)181	}182	return out183}184185// latest_flags derives the flag state of every save that has ever been flagged186// or cleared. Newest wins, ties broken by ID so the answer is the same on every187// machine.188pub fn latest_flags(store &storage.Store) !map[string]Flag {189	mut stmt := store.db.prepare('${select_event} WHERE kind IN (?, ?) AND save_id IS NOT NULL190		ORDER BY created_at, id')!191	defer {192		stmt.finalize()193	}194	stmt.bind_text(1, kind_flag)!195	stmt.bind_text(2, kind_clear)!196	mut flags := map[string]Flag{}197	for stmt.step()! {198		event := read_event(stmt)199		flags[event.save_id] = Flag{200			save_id: event.save_id201			flagged: event.kind == kind_flag202			reason:  event.reason203			at:      event.created_at204		}205	}206	return flags207}208209// flag_state is latest_flags for one save; a save never flagged reads as not flagged.210pub fn flag_state(store &storage.Store, save_id string) !Flag {211	flags := latest_flags(store)!212	return flags[save_id] or {213		Flag{214			save_id: save_id215		}216	}217}218219// strip_events lists every strip event, whole-history or one save, oldest first.220pub fn strip_events(store &storage.Store) ![]Event {221	mut stmt := store.db.prepare('${select_event} WHERE kind = ? ORDER BY created_at, id')!222	defer {223		stmt.finalize()224	}225	stmt.bind_text(1, kind_strip)!226	mut out := []Event{}227	for stmt.step()! {228		out << read_event(stmt)229	}230	return out231}