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// write_once appends an event whose ID covers only what it says, not when it73// was said. Re-running an interrupted strip therefore records the same fact74// instead of a second one.75pub fn write_once(store &storage.Store, event Event) !Event {76	mut record := event77	if record.created_at == 0 {78		record.created_at = time.now().unix()79	}80	if record.machine == '' {81		record.machine = machine.name()82	}83	record.id = storage.hash_hex('${record.kind}:${record.save_id}:${record.path}'.bytes())[..12]84	insert(store, record)!85	return record86}8788// receive stores an event exactly as another machine recorded it, ID and all.89// Re-deriving the ID here would disagree with `write_once` for strips, and the90// event would come back as new on every steal. Reports whether it was new.91pub fn receive(store &storage.Store, event Event) !bool {92	insert(store, event)!93	return store.db.changes() > 094}9596fn next_timestamp(store &storage.Store) !i64 {97	now := time.now().unix()98	mut stmt := store.db.prepare('SELECT MAX(created_at) FROM events')!99	defer {100		stmt.finalize()101	}102	if !stmt.step()! {103		return now104	}105	newest := stmt.int_or_none(0) or { return now }106	if newest >= now {107		return newest + 1108	}109	return now110}111112fn insert(store &storage.Store, record Event) ! {113	mut stmt := store.db.prepare('INSERT OR IGNORE INTO events114		(id, kind, save_id, path, reason, created_at, machine) VALUES (?, ?, ?, ?, ?, ?, ?)')!115	defer {116		stmt.finalize()117	}118	stmt.bind_text(1, record.id)!119	stmt.bind_text(2, record.kind)!120	bind_optional(stmt, 3, record.save_id)!121	bind_optional(stmt, 4, record.path)!122	bind_optional(stmt, 5, record.reason)!123	stmt.bind_int(6, record.created_at)!124	stmt.bind_text(7, record.machine)!125	stmt.step()!126}127128pub fn flag(store &storage.Store, save_id string, reason string) !Event {129	return write(store, Event{130		kind:    kind_flag131		save_id: save_id132		reason:  reason133	})!134}135136pub fn clear_flag(store &storage.Store, save_id string) !Event {137	return write(store, Event{138		kind:    kind_clear139		save_id: save_id140	})!141}142143// strip records that a path was removed from a save (or from every save, when144// save_id is empty).145pub fn strip(store &storage.Store, save_id string, path string) !Event {146	return write_once(store, Event{147		kind:    kind_strip148		save_id: save_id149		path:    path150	})!151}152153pub fn event_id(event Event) string {154	mut sb := strings.Builder{}155	put(mut sb, 'kind', event.kind)156	put(mut sb, 'save', event.save_id)157	put(mut sb, 'path', event.path)158	put(mut sb, 'reason', event.reason)159	put(mut sb, 'created', event.created_at.str())160	put(mut sb, 'machine', event.machine)161	return storage.hash_hex(sb.str().bytes())[..12]162}163164fn put(mut sb strings.Builder, name string, value string) {165	sb.write_string('${name}:${value.len}:')166	sb.write_string(value)167	sb.write_string('\n')168}169170fn bind_optional(stmt &storage.Stmt, index int, value string) ! {171	if value == '' {172		stmt.bind_null(index)!173	} else {174		stmt.bind_text(index, value)!175	}176}177178// all returns every event, oldest first.179pub fn all(store &storage.Store) ![]Event {180	mut stmt := store.db.prepare('${select_event} ORDER BY created_at, id')!181	defer {182		stmt.finalize()183	}184	mut out := []Event{}185	for stmt.step()! {186		out << read_event(stmt)187	}188	return out189}190191// latest_flags derives the flag state of every save that has ever been flagged192// or cleared. Newest wins, ties broken by ID so the answer is the same on every193// machine.194pub fn latest_flags(store &storage.Store) !map[string]Flag {195	mut stmt := store.db.prepare('${select_event} WHERE kind IN (?, ?) AND save_id IS NOT NULL196		ORDER BY created_at, id')!197	defer {198		stmt.finalize()199	}200	stmt.bind_text(1, kind_flag)!201	stmt.bind_text(2, kind_clear)!202	mut flags := map[string]Flag{}203	for stmt.step()! {204		event := read_event(stmt)205		flags[event.save_id] = Flag{206			save_id: event.save_id207			flagged: event.kind == kind_flag208			reason:  event.reason209			at:      event.created_at210		}211	}212	return flags213}214215pub fn flag_state(store &storage.Store, save_id string) !Flag {216	flags := latest_flags(store)!217	return flags[save_id] or {218		Flag{219			save_id: save_id220		}221	}222}223224// flagged_ids is what the log and the history view mark.225pub fn flagged_ids(store &storage.Store) ![]string {226	flags := latest_flags(store)!227	mut ids := []string{}228	for save_id, state in flags {229		if state.flagged {230			ids << save_id231		}232	}233	ids.sort()234	return ids235}236237pub fn strip_events(store &storage.Store) ![]Event {238	mut stmt := store.db.prepare('${select_event} WHERE kind = ? ORDER BY created_at, id')!239	defer {240		stmt.finalize()241	}242	stmt.bind_text(1, kind_strip)!243	mut out := []Event{}244	for stmt.step()! {245		out << read_event(stmt)246	}247	return out248}