Files
mesh-sdk/go/log.go
T
jochen eaed934629
mesh/merge-gate pass: builds mesh-sdk, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript), go/merge-check.sh (go)
mesh/delivery delivered
go: a module appends to and reads its log through the runtime, so its record outlives a bucket's history (hq ADR 0297)
2026-10-11 19:16:09 +02:00

124 lines
4.2 KiB
Go

package stdio
import (
"encoding/json"
"fmt"
"time"
)
// Log — a module's record of operations on the bus, kept as long as the log is (novox/hq ADR 0297). Not the
// current value: that is state, and a log's state is its fold. Each entry is one operation on one key, its
// sequence in the log is its identity, and the order of the log is the order of the operations. A module
// declares each log it owns in its manifest (`logs`) and names it here as it named it there; a log is its
// owner's alone, and the runtime refuses, with the reason, one it did not issue.
// LogEntry is one operation on one key, as the log holds it.
type LogEntry struct {
Sequence uint64 `json:"sequence"`
Key string `json:"key"`
Value json.RawMessage `json:"value"`
At time.Time `json:"at"`
}
// LogPage is one page of a read: its entries, in order, the sequence to read on from (0 when nothing
// follows) and the log's last sequence when it was read.
type LogPage struct {
Entries []LogEntry `json:"entries"`
Next uint64 `json:"next"`
Last uint64 `json:"last"`
}
// LogInfo is how much a log holds: its entries and bytes, its cap in bytes, and its first and last sequence.
type LogInfo struct {
Entries uint64 `json:"entries"`
Bytes uint64 `json:"bytes"`
MaxBytes uint64 `json:"max_bytes"`
First uint64 `json:"first"`
Last uint64 `json:"last"`
}
// KeptLog is one log this module owns.
type KeptLog struct{ name string }
// Log is a log this module owns, by its local name.
func Log(name string) KeptLog { return KeptLog{name: name} }
const logTimeout = 30 * time.Second
func (l KeptLog) ask(verb string, params map[string]any) (json.RawMessage, error) {
c, err := live()
if err != nil {
return nil, err
}
params["log"] = l.name
return c.ask("mesh/log."+verb, params, logTimeout)
}
func (l KeptLog) append(params map[string]any) (uint64, error) {
raw, err := l.ask("append", params)
if err != nil {
return 0, err
}
var appended struct {
Sequence uint64 `json:"sequence"`
}
return appended.Sequence, json.Unmarshal(raw, &appended)
}
// Append writes an entry for key, whatever came before it, and returns its sequence. A key is one or more
// names of letters, digits, `-` and `_`, joined by `.`; a value is JSON of at most 256 KiB. A full log
// refuses the entry: it never drops an older one to make room.
func (l KeptLog) Append(key string, value any) (uint64, error) {
return l.append(map[string]any{"key": key, "value": value})
}
// AppendAt writes an entry for key only if the key's last entry is still at expect — the sequence the caller
// read, 0 for a key with no entry yet — and returns its sequence. When another entry for the key came in
// between, nothing is written and the error is ErrChanged, as for a lost State update: read again and
// decide again.
func (l KeptLog) AppendAt(key string, value any, expect uint64) (uint64, error) {
return l.append(map[string]any{"key": key, "value": value, "expect": expect})
}
// Read is the entries from the sequence from on, oldest first, at most limit of them (0 is the runtime's
// page, 100; it gives at most 1000). keys narrows it: a key, with `*` for one name and `>` for the rest;
// empty is every key. Read on from the page's Next until it is 0.
func (l KeptLog) Read(from uint64, keys string, limit int) (*LogPage, error) {
if limit < 0 {
return nil, fmt.Errorf("a read of %s asks for at least no entries: limit %d", l.name, limit)
}
params := map[string]any{"from": from}
if keys != "" {
params["keys"] = keys
}
if limit > 0 {
params["limit"] = limit
}
raw, err := l.ask("read", params)
if err != nil {
return nil, err
}
var p LogPage
return &p, json.Unmarshal(raw, &p)
}
// Last is a key's last entry, or nil when the key has none.
func (l KeptLog) Last(key string) (*LogEntry, error) {
raw, err := l.ask("last", map[string]any{"key": key})
if err != nil || len(raw) == 0 || string(raw) == "null" {
return nil, err
}
var e LogEntry
return &e, json.Unmarshal(raw, &e)
}
// Info is how much the log holds and how much it may.
func (l KeptLog) Info() (*LogInfo, error) {
raw, err := l.ask("info", map[string]any{})
if err != nil {
return nil, err
}
var i LogInfo
return &i, json.Unmarshal(raw, &i)
}