The pending value holds the stream sequence and the small fields the waits read; the take-in reads the message from the forge bridge's stream by sequence. A message the stream no longer holds is said (take-in.pending lost, indexer_pending) and never taken in silently. indexer_take_in_forge takes one forge message in by its sequence. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
260 lines
11 KiB
Go
260 lines
11 KiB
Go
// Package takein is the indexer's take-in contract (novox/hq ADR 0633 §1, §5): the events it publishes on its own stream
|
|
// `take-in` while a source revision waits for the indexer it names or a delivery group is gathered, their keys, and the
|
|
// answer of its tool indexer_pending. The indexer writes them; the watchman and the view read them. It is the indexer's
|
|
// own package: one module owns these types, so they live with it and not in mesh-sdk (the operator, 2026-11-04).
|
|
package takein
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Merge is one merge a take-in event or a wait holds: the repository as `<owner>/<name>`, its pull request and the
|
|
// merge commit the forge made of it.
|
|
type Merge struct {
|
|
Repository string `json:"repository"`
|
|
Number int `json:"number"`
|
|
Merge string `json:"merge"`
|
|
}
|
|
|
|
// checkRepository refuses a repository that is not `<owner>/<name>`: one slash, neither part empty.
|
|
func checkRepository(repository string) error {
|
|
owner, name, ok := strings.Cut(repository, "/")
|
|
if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
|
|
return fmt.Errorf("delivery: the repository %q is not <owner>/<name>, with exactly one slash", repository)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decode reads a value of delivery.Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk
|
|
// delivery's values are read.
|
|
func decode(data []byte, into any) error {
|
|
var probe struct {
|
|
V *int `json:"v"`
|
|
}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return fmt.Errorf("delivery: value is not JSON: %w", err)
|
|
}
|
|
if probe.V == nil || *probe.V != delivery.Version {
|
|
return fmt.Errorf("%w: this reader reads %d", delivery.ErrUnknownVersion, delivery.Version)
|
|
}
|
|
if err := json.Unmarshal(data, into); err != nil {
|
|
return fmt.Errorf("delivery: value does not fit its shape: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The take-in events (novox/hq ADR 0633 §1, §5): the indexer's ordinary events, `take-in.pending` and `take-in.taken`, on
|
|
// the path its other events take (the module's event subject; its manifest's emits), with no stream of its own. A wait
|
|
// starts with `take-in.pending` and ends with `take-in.taken`, whoever ended it (the named indexer, a revert, a hand
|
|
// take-in or a drop); the wait's key is in the body (TakeIn.Key), since an emitted event names one event and a key is
|
|
// no event name. A reader that starts later asks the waits of indexer_pending, not a replay.
|
|
const (
|
|
TakeInPending = "pending"
|
|
TakeInTaken = "taken"
|
|
// TakeInPendingEvent and TakeInTakenEvent are the two events' names, relative to the indexer.
|
|
TakeInPendingEvent = "take-in." + TakeInPending
|
|
TakeInTakenEvent = "take-in." + TakeInTaken
|
|
)
|
|
|
|
// What waits in a take-in event: a source revision for the indexer build it names, or a delivery group for its end.
|
|
const (
|
|
TakeInRevision = "revision"
|
|
TakeInGroup = "group"
|
|
)
|
|
|
|
// PendingKey is the key of one held merge in the indexer's pending state, and the key a revision's take-in events
|
|
// carry (its oldest merge's): `m.<the merge's stream sequence, 20 digits>`, so the key order is the merge order.
|
|
func PendingKey(seq uint64) string { return fmt.Sprintf("m.%020d", seq) }
|
|
|
|
// GroupTakeInKey is the key a group's take-in events carry while it is gathered: `g.<group>`, with every character a
|
|
// bus subject cannot carry (a space, `*`, `>`) written `_`. The event's body names the group exactly.
|
|
func GroupTakeInKey(group string) string {
|
|
return "g." + strings.Map(func(r rune) rune {
|
|
if r == '*' || r == '>' || r <= ' ' || r == 0x7f {
|
|
return '_'
|
|
}
|
|
return r
|
|
}, group)
|
|
}
|
|
|
|
// TakeInEvent is the name of the take-in event of the given kind: `take-in.<pending|taken>`.
|
|
func TakeInEvent(kind string) (string, error) {
|
|
if kind != TakeInPending && kind != TakeInTaken {
|
|
return "", fmt.Errorf("delivery: the take-in kind %q is not pending or taken", kind)
|
|
}
|
|
return "take-in." + kind, nil
|
|
}
|
|
|
|
// ParseTakeInEvent is the inverse of TakeInEvent: the kind of a take-in event's name.
|
|
func ParseTakeInEvent(event string) (kind string, err error) {
|
|
k, ok := strings.CutPrefix(event, "take-in.")
|
|
if !ok {
|
|
return "", fmt.Errorf("delivery: %q is not a take-in event, take-in.<pending|taken>", event)
|
|
}
|
|
if _, err := TakeInEvent(k); err != nil {
|
|
return "", fmt.Errorf("delivery: %q is not a take-in event: %w", event, err)
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
func checkTakeInKey(key string) error {
|
|
if !strings.HasPrefix(key, "m.") && !strings.HasPrefix(key, "g.") {
|
|
return fmt.Errorf("delivery: the take-in key %q is neither m.<sequence> nor g.<group>", key)
|
|
}
|
|
for _, tok := range strings.Split(key, ".") {
|
|
if tok == "" || strings.ContainsAny(tok, "*> \t\r\n") {
|
|
return fmt.Errorf("delivery: the take-in key %q is not a bus subject's tokens", key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TakeIn is the body of a take-in event. Kind is the event's kind and Key its key. What says what waits: a revision
|
|
// (Key its oldest merge's PendingKey, WaitsFor the indexer build it names) or a group being gathered (Key its
|
|
// GroupTakeInKey, Members every approved member with its head, Merges the members held so far). Merges are the merges
|
|
// it holds, oldest first; Since is when the wait began. A `taken` event names the same once the wait ended; By and Why
|
|
// name a hand act (a hand take-in, or a drop, Dropped), and are carried by nothing else.
|
|
type TakeIn struct {
|
|
V int `json:"v"`
|
|
Kind string `json:"kind"`
|
|
Key string `json:"key"`
|
|
What string `json:"what"`
|
|
Merges []Merge `json:"merges"`
|
|
Group string `json:"group,omitempty"`
|
|
Members []delivery.Head `json:"members,omitempty"`
|
|
WaitsFor string `json:"waits-for,omitempty"`
|
|
Since time.Time `json:"since"`
|
|
By string `json:"by,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
Dropped bool `json:"dropped,omitempty"`
|
|
// Lost are the keys of merges whose message the forge bridge's stream no longer holds: they are not taken in, and the
|
|
// revision waits until indexer_drop_pending drops them (issue 1585).
|
|
Lost []string `json:"lost,omitempty"`
|
|
}
|
|
|
|
func (t TakeIn) check() error {
|
|
if _, err := TakeInEvent(t.Kind); err != nil {
|
|
return err
|
|
}
|
|
if err := checkTakeInKey(t.Key); err != nil {
|
|
return err
|
|
}
|
|
if len(t.Merges) == 0 {
|
|
return fmt.Errorf("delivery: the take-in event %s names no merge", t.Key)
|
|
}
|
|
for _, m := range t.Merges {
|
|
if err := checkRepository(m.Repository); err != nil {
|
|
return err
|
|
}
|
|
if m.Merge == "" {
|
|
return fmt.Errorf("delivery: the take-in event %s names a merge of %s without its commit", t.Key, m.Repository)
|
|
}
|
|
}
|
|
if t.Since.IsZero() {
|
|
return fmt.Errorf("delivery: the take-in event %s does not say since when it waits", t.Key)
|
|
}
|
|
switch t.What {
|
|
case TakeInRevision:
|
|
if !strings.HasPrefix(t.Key, "m.") {
|
|
return fmt.Errorf("delivery: a revision's take-in key is its oldest merge's m.<sequence>, not %q", t.Key)
|
|
}
|
|
if t.Kind == TakeInPending && t.WaitsFor == "" {
|
|
return fmt.Errorf("delivery: a pending revision %s names the indexer build it waits for", t.Key)
|
|
}
|
|
case TakeInGroup:
|
|
if t.Group == "" {
|
|
return fmt.Errorf("delivery: a group's take-in event %s names its group", t.Key)
|
|
}
|
|
if t.Key != GroupTakeInKey(t.Group) {
|
|
return fmt.Errorf("delivery: the take-in key of the group %q is %s, not %s", t.Group, GroupTakeInKey(t.Group), t.Key)
|
|
}
|
|
if t.WaitsFor != "" {
|
|
return fmt.Errorf("delivery: a group being gathered waits for its end, not for an indexer build")
|
|
}
|
|
default:
|
|
return fmt.Errorf("delivery: a take-in event's what %q is not revision or group", t.What)
|
|
}
|
|
if t.Kind != TakeInTaken && (t.By != "" || t.Why != "" || t.Dropped) {
|
|
return fmt.Errorf("delivery: only a taken event names the hand act that ended a wait")
|
|
}
|
|
if (t.Why != "" || t.Dropped) && t.By == "" {
|
|
return fmt.Errorf("delivery: a hand act's take-in event names who acted (by)")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeTakeIn is t as JSON at Version; it refuses what its kind and what do not carry.
|
|
func EncodeTakeIn(t TakeIn) ([]byte, error) {
|
|
if err := t.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
t.V = delivery.Version
|
|
return json.Marshal(t)
|
|
}
|
|
|
|
// DecodeTakeIn reads a take-in event's body; it refuses an unknown version (ErrUnknownVersion) and what EncodeTakeIn
|
|
// refuses.
|
|
func DecodeTakeIn(data []byte) (TakeIn, error) {
|
|
var t TakeIn
|
|
if err := decode(data, &t); err != nil {
|
|
return TakeIn{}, err
|
|
}
|
|
if err := t.check(); err != nil {
|
|
return TakeIn{}, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// SameTakeIn says whether two take-in bodies are the same value.
|
|
func SameTakeIn(a, b TakeIn) bool { return reflect.DeepEqual(a, b) }
|
|
|
|
// PendingWait is one wait the indexer's tool indexer_pending answers (novox/hq ADR 0633 §5), as the view reads it to
|
|
// name a module waiting for its revision's indexer. Key and What are a take-in event's: a revision under its oldest
|
|
// merge's key, waiting for the indexer build WaitsFor (empty while it is worked out), or a group being gathered under
|
|
// its group key, with Members every approved member and Merges those held so far. A revision that waits behind a
|
|
// group being gathered names that group's key in Behind. Keys are the pending keys of its merges, in Merges' order (indexer_drop_pending drops one). Holds are the modules it holds back: every module of its
|
|
// merges' trees. Said is the last take-in event said of it (`take-in.<kind>`, of its Key) and SaidAt when.
|
|
type PendingWait struct {
|
|
Key string `json:"key"`
|
|
What string `json:"what"`
|
|
Merges []Merge `json:"merges"`
|
|
Keys []string `json:"keys"`
|
|
Group string `json:"group,omitempty"`
|
|
Members []delivery.Head `json:"members,omitempty"`
|
|
Behind string `json:"behind,omitempty"`
|
|
WaitsFor string `json:"waits-for,omitempty"`
|
|
Since time.Time `json:"since"`
|
|
Holds []string `json:"holds"`
|
|
Said string `json:"said,omitempty"`
|
|
SaidAt time.Time `json:"said-at,omitzero"`
|
|
// Lost are the keys of its merges whose message the forge bridge's stream no longer holds (TakeIn.Lost).
|
|
Lost []string `json:"lost,omitempty"`
|
|
}
|
|
|
|
// PendingAnswer is indexer_pending's answer: every wait, oldest first, and the pending keys that do not read.
|
|
type PendingAnswer struct {
|
|
Waits []PendingWait `json:"waits"`
|
|
Unread []string `json:"unread,omitempty"`
|
|
}
|
|
|
|
// DecodePendingAnswer reads indexer_pending's answer, refusing a wait that is not a revision or a group, or names no
|
|
// key.
|
|
func DecodePendingAnswer(data []byte) (PendingAnswer, error) {
|
|
var a PendingAnswer
|
|
if err := json.Unmarshal(data, &a); err != nil {
|
|
return a, fmt.Errorf("delivery: not an answer of indexer_pending: %w", err)
|
|
}
|
|
for _, w := range a.Waits {
|
|
if w.Key == "" || (w.What != TakeInRevision && w.What != TakeInGroup) {
|
|
return a, fmt.Errorf("delivery: indexer_pending answers a wait %q of %q, not a revision or a group", w.Key, w.What)
|
|
}
|
|
}
|
|
return a, nil
|
|
}
|