Files
mesh-sdk/go/buscalls/buscalls.go
T
jschoubben f8bd12e4b7
mesh/delivery merged: source.main moved to novox/mesh-sdk@2341e8e98377
mesh/delivery-group group feat/bus-state merged: every member is merged
mesh/check pass f8bd12e4b7b1 on 7c015b652d36 | unit 6460 run, 262 kept, 27 skipped | integration 6639 run, 264 kept, 35 skipped | over bound: unit, in…
Encode each part of an index key, so no two keys meet and every part is a bus token (issue 1966 S4)
A raw value holding "." collided with a longer path, and a space is refused
by the bus: every byte outside [A-Za-z0-9_-] of a field value or an instance
path part is written =XX (review round 1 of issue 1966 B1).
2026-11-03 15:28:43 +01:00

383 lines
13 KiB
Go

// Package buscalls is the contract of the calls a bundle makes on its own bus state through the tool runner, over the
// bundle's pipe (novox/hq ADR 0595 §9, ADR 0597 §10, design 25 § 12.5): the object store's, a stream's read-back and
// fill, a work queue's size and dead letters, and the ledger's. Each call names an entry by the local name its module
// asks for it by in `requires`; the tool runner serves it from the module's membership's bindings, its own entries
// only. Only the shapes and the method names are here: what the tool runner refuses, and why, is its own.
//
// It is a package of its own, imported only by the modules that use these calls and by the tool runner, so a change
// here rebuilds those and not the module library's importers (ADR 0595 §9). It imports nothing of mesh-sdk.
package buscalls
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// MetaCall is where in a `tools/call`'s `_meta` the tool runner hands a bundle the id of the tool call in flight: the id
// ApplyRequest.Call names, so the runner stamps the caller it proved for that call as the operation's author (novox/hq
// ADR 0597 §7). The runner writes `_meta`; a caller writes only `arguments`, so no caller can name another call.
const MetaCall = "mesh/call"
// The methods a bundle asks the tool runner, one per call.
const (
ObjectsPut = "mesh/objects.put"
ObjectsGet = "mesh/objects.get"
ObjectsList = "mesh/objects.list"
ObjectsDelete = "mesh/objects.delete"
ObjectsInfo = "mesh/objects.info"
ObjectsWatch = "mesh/objects.watch"
StreamRead = "mesh/stream.read"
StreamFill = "mesh/stream.fill"
WorkQueueSize = "mesh/workqueue.size"
WorkQueueDead = "mesh/workqueue.dead"
LedgerApply = "mesh/ledger.apply"
LedgerGet = "mesh/ledger.get"
LedgerHistory = "mesh/ledger.history"
LedgerWatch = "mesh/ledger.watch"
LedgerQuery = "mesh/ledger.query"
// The ledger's repairs, which the tool runner also serves as tools of every module owning a ledger, its own
// ledgers only (design 25 § 12.5).
LedgerRebuild = "mesh/ledger.rebuild"
LedgerReindex = "mesh/ledger.reindex"
LedgerSnapshot = "mesh/ledger.snapshot"
LedgerLag = "mesh/ledger.lag"
)
// The methods the tool runner sends the bundle for a watch it asked, each naming the watch by its id, as `mesh/log`
// does for a log.
const (
ObjectsWatched = "mesh/objects"
LedgerWatched = "mesh/ledger"
)
// Fill is how much an entry holds against its cap: its messages (or objects), its bytes and its cap in bytes.
type Fill struct {
Count uint64 `json:"count"`
Bytes uint64 `json:"bytes"`
MaxBytes uint64 `json:"max_bytes"`
}
// Done is the answer of a call that answers nothing but that it was done.
type Done struct{}
// --- the object store
// ObjectsPutRequest stores one object whole under its name, replacing one of that name.
type ObjectsPutRequest struct {
Objects string `json:"objects"`
Name string `json:"name"`
Data []byte `json:"data"`
Description string `json:"description,omitempty"`
}
// ObjectsNameRequest names one object of an object store: get, delete.
type ObjectsNameRequest struct {
Objects string `json:"objects"`
Name string `json:"name"`
}
// ObjectsRequest names an object store: list, info.
type ObjectsRequest struct {
Objects string `json:"objects"`
}
// ObjectsWatchRequest asks the tool runner to send every object stored or deleted, as ObjectsChange under the watch's id.
type ObjectsWatchRequest struct {
Objects string `json:"objects"`
Watch string `json:"watch"`
}
// ObjectInfo is one object as its store holds it: put's answer, and each of list's.
type ObjectInfo struct {
Name string `json:"name"`
Size uint64 `json:"size"`
Chunks uint32 `json:"chunks"`
Digest string `json:"digest"`
Description string `json:"description,omitempty"`
Modified time.Time `json:"modified"`
Deleted bool `json:"deleted,omitempty"`
}
// ObjectData is get's answer: the object's info and its bytes.
type ObjectData struct {
Info ObjectInfo `json:"info"`
Data []byte `json:"data"`
}
// ObjectsListed is list's answer: every object the store holds.
type ObjectsListed struct {
Objects []ObjectInfo `json:"objects"`
}
// ObjectsChange is one object stored or deleted, sent under ObjectsWatched; Current says it was there when the watch
// began.
type ObjectsChange struct {
Watch string `json:"watch"`
Object ObjectInfo `json:"object"`
Current bool `json:"current"`
}
// --- a stream
// StreamReadRequest reads a module's own stream from a sequence on, at most Limit messages, Subjects narrowing it.
type StreamReadRequest struct {
Stream string `json:"stream"`
From uint64 `json:"from"`
Subjects string `json:"subjects,omitempty"`
Limit int `json:"limit,omitempty"`
}
// StreamRequest names a stream: fill.
type StreamRequest struct {
Stream string `json:"stream"`
}
// StreamMessage is one message of a stream.
type StreamMessage struct {
Sequence uint64 `json:"sequence"`
Subject string `json:"subject"`
Body json.RawMessage `json:"body"`
At time.Time `json:"at"`
}
// StreamPage is read's answer: the messages in order, the sequence to read on from (0 when nothing follows) and the
// stream's last sequence when it was read.
type StreamPage struct {
Messages []StreamMessage `json:"messages"`
Next uint64 `json:"next"`
Last uint64 `json:"last"`
}
// StreamFilled is fill's answer: the stream's fill and its first and last sequence.
type StreamFilled struct {
Fill
First uint64 `json:"first"`
Last uint64 `json:"last"`
}
// --- a work queue
// WorkQueueRequest names a work queue: size.
type WorkQueueRequest struct {
Queue string `json:"queue"`
}
// WorkQueueDeadRequest asks a work queue's dead letters, at most Limit of them.
type WorkQueueDeadRequest struct {
Queue string `json:"queue"`
Limit int `json:"limit,omitempty"`
}
// WorkQueueSized is size's answer: the messages waiting to be taken, against the queue's cap.
type WorkQueueSized struct {
Fill
}
// DeadLetter is one message delivered as often as its queue allows and never settled.
type DeadLetter struct {
Sequence uint64 `json:"sequence"`
Subject string `json:"subject"`
Deliveries int `json:"deliveries"`
Body json.RawMessage `json:"body,omitempty"`
At time.Time `json:"at"`
}
// WorkQueueDeadLetters is dead's answer.
type WorkQueueDeadLetters struct {
Dead []DeadLetter `json:"dead"`
}
// --- the ledger (ADR 0597)
// ApplyRequest applies one operation to one instance of a ledger. Call is the id of the tool call the operation
// serves, as the tool runner handed it under MetaCall, whose caller the tool runner proved and stamps as the author
// (ADR 0597 §7); empty, the operation carries the owning module's account alone. An author the bundle writes into Body
// is never taken as the author.
type ApplyRequest struct {
Ledger string `json:"ledger"`
Instance string `json:"instance"`
Operation string `json:"operation"`
Body json.RawMessage `json:"body,omitempty"`
Call string `json:"call,omitempty"`
}
// Applied is apply's answer: the operation was taken, with its sequence in the instance's history; StateBehind says
// the state could not follow because its bucket is full, and catches up at the next apply after room is made (ADR
// 0597 §6).
type Applied struct {
Taken bool `json:"taken"`
Sequence uint64 `json:"sequence"`
StateBehind bool `json:"state_behind,omitempty"`
}
// Author is who an operation is by, as `apply` stamped it (ADR 0597 §7): the owning module's account, and the caller
// of the tool call it served, as the tool runner proved it.
type Author struct {
Account string `json:"account"`
Caller string `json:"caller,omitempty"`
}
// InstanceRequest names one instance of a ledger: get, snapshot.
type InstanceRequest struct {
Ledger string `json:"ledger"`
Instance string `json:"instance"`
}
// InstanceState is get's answer: the instance's state and the sequence of the last operation it holds.
type InstanceState struct {
Instance string `json:"instance"`
State json.RawMessage `json:"state"`
Sequence uint64 `json:"sequence"`
}
// HistoryRequest reads an instance's operations from a sequence on, at most Limit of them.
type HistoryRequest struct {
Ledger string `json:"ledger"`
Instance string `json:"instance"`
From uint64 `json:"from,omitempty"`
Limit int `json:"limit,omitempty"`
}
// Operation is one operation as the history keeps it.
type Operation struct {
Sequence uint64 `json:"sequence"`
Instance string `json:"instance"`
Operation string `json:"operation"`
Body json.RawMessage `json:"body,omitempty"`
Author Author `json:"author"`
At time.Time `json:"at"`
}
// HistoryPage is history's answer: the operations oldest first, the sequence to read on from (0 when nothing
// follows) and the instance's last sequence when it was read.
type HistoryPage struct {
Operations []Operation `json:"operations"`
Next uint64 `json:"next"`
Last uint64 `json:"last"`
}
// LedgerWatchRequest asks the tool runner to send every change of the instances Instances names (an instance, with
// `*` for one name and `>` for the rest) as InstanceChange under the watch's id: the current states first.
type LedgerWatchRequest struct {
Ledger string `json:"ledger"`
Instances string `json:"instances"`
Watch string `json:"watch"`
}
// InstanceChange is one instance's new state, sent under LedgerWatched; Current says it was the state when the watch
// began.
type InstanceChange struct {
Watch string `json:"watch"`
Instance string `json:"instance"`
State json.RawMessage `json:"state"`
Sequence uint64 `json:"sequence"`
Current bool `json:"current"`
}
// QueryRequest reads a declared index of a ledger: the entries whose key Key names (a key, with `*` for one name and
// `>` for the rest; empty is every key), at most Limit of them.
type QueryRequest struct {
Ledger string `json:"ledger"`
Index string `json:"index"`
Key string `json:"key,omitempty"`
Limit int `json:"limit,omitempty"`
}
// IndexEntry is the value of one key of a declared index (IndexKey): the instance it points at, and the sequence of
// the operation it was written from. The owner's watcher and the tool runner's reindex both write it so.
type IndexEntry struct {
Instance string `json:"instance"`
Sequence uint64 `json:"sequence"`
}
// IndexKey is the key of an instance in a declared index: the values of the index's fields, in its order, then the
// parts of the instance's path, all joined by ".". Each value and each path part is encoded (keyToken): every byte
// outside [A-Za-z0-9_-] is written "=" and two upper-case hex digits ("." is "=2E", " " is "=20", "=" is "=3D"), so two
// different keys never meet and every part is a token the bus takes. `17950.listing.a17` is the key of price 17950
// and instance `listing/a17`. The owner's watcher and the tool runner's reindex both write it, so a reindex leaves the
// index as the watcher keeps it.
func IndexKey(fields []string, instance string) string {
parts := make([]string, 0, len(fields)+2)
for _, f := range fields {
parts = append(parts, keyToken(f))
}
for _, p := range strings.Split(instance, "/") {
parts = append(parts, keyToken(p))
}
return strings.Join(parts, ".")
}
// keyToken is v with every byte outside [A-Za-z0-9_-] written "=" and two upper-case hex digits (IndexKey).
func keyToken(v string) string {
var b strings.Builder
for i := 0; i < len(v); i++ {
c := v[i]
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-' {
b.WriteByte(c)
continue
}
fmt.Fprintf(&b, "=%02X", c)
}
return b.String()
}
// QueryAnswer is query's answer.
type QueryAnswer struct {
Entries []IndexEntry `json:"entries"`
}
// RebuildFrom is what a rebuild derives the state from: the history, or the snapshot and the operations after it.
const (
RebuildFromHistory = "history"
RebuildFromSnapshot = "snapshot"
)
// RebuildRequest derives an instance's state again (every instance's when Instance is empty), From the history or the
// snapshot.
type RebuildRequest struct {
Ledger string `json:"ledger"`
Instance string `json:"instance,omitempty"`
From string `json:"from,omitempty"`
}
// ReindexRequest builds a declared index again from the history.
type ReindexRequest struct {
Ledger string `json:"ledger"`
Index string `json:"index"`
}
// LedgerRequest names a ledger: lag.
type LedgerRequest struct {
Ledger string `json:"ledger"`
}
// Repaired is the answer of rebuild, reindex and snapshot: how many instances (or index entries) it wrote.
type Repaired struct {
Written int `json:"written"`
}
// InstanceLag is one instance whose state or snapshot is behind its history: the sequences each holds.
type InstanceLag struct {
Instance string `json:"instance"`
History uint64 `json:"history"`
State uint64 `json:"state"`
Snapshot uint64 `json:"snapshot"`
}
// IndexLag is how far one declared index is behind the history.
type IndexLag struct {
Index string `json:"index"`
Behind uint64 `json:"behind"`
}
// Lagged is lag's answer: every instance and index behind its history; none when nothing is behind.
type Lagged struct {
Instances []InstanceLag `json:"instances,omitempty"`
Indexes []IndexLag `json:"indexes,omitempty"`
}