1package domain
2
3import (
4 "encoding/hex"
5 "errors"
6 "fmt"
7 "io"
8 "slices"
9 "strings"
10 "unicode/utf8"
11)
12
13// IncarnationID permanently distinguishes uses of the same session name.
14type IncarnationID [16]byte
15
16// CheckpointRef identifies one immutable snapshot manifest.
17type CheckpointRef struct {
18 Generation uint64
19 ManifestDigest [32]byte
20}
21
22// CatalogueTabRecord is the durable ordered metadata for one stopped tab.
23// StableID is empty for records migrated from the legacy TabNames-only format.
24type CatalogueTabRecord struct {
25 StableID TabStableID
26 Name string
27}
28
29// CatalogueRecord is the durable source of truth for one named session.
30type CatalogueRecord struct {
31 Name string
32 IncarnationID IncarnationID
33 Cwd string
34 CreatedAt int64
35 UpdatedAt int64
36 LastUsedSeq uint64
37 // TabNames remains the compatibility metadata consumed by older recovery
38 // paths. TabRecords is authoritative when present and retains trailing
39 // unnamed tabs instead of silently trimming them.
40 TabNames []string
41 TabRecords []CatalogueTabRecord
42 Committed *CheckpointRef
43 DegradedReason string
44}
45
46// Equal reports canonical value equality between checkpoint references.
47func (r *CheckpointRef) Equal(other *CheckpointRef) bool {
48 if r == nil || other == nil {
49 return r == nil && other == nil
50 }
51 return *r == *other
52}
53
54// Equal reports canonical value equality, including ordered tab names and
55// pointed-to checkpoint values rather than pointer identity.
56func (r CatalogueRecord) Equal(other CatalogueRecord) bool {
57 return r.Name == other.Name && r.IncarnationID == other.IncarnationID && r.Cwd == other.Cwd &&
58 r.CreatedAt == other.CreatedAt && r.UpdatedAt == other.UpdatedAt && r.LastUsedSeq == other.LastUsedSeq &&
59 slices.Equal(r.TabNames, other.TabNames) && slices.Equal(r.TabRecords, other.TabRecords) && r.Committed.Equal(other.Committed) &&
60 r.DegradedReason == other.DegradedReason
61}
62
63// CatalogueMetadataUpdate changes mutable runtime metadata for one catalogue
64// incarnation without carrying authority-owned recovery or checkpoint fields.
65type CatalogueMetadataUpdate struct {
66 Name string
67 IncarnationID IncarnationID
68 Cwd *string
69 UpdatedAt *int64
70 LastUsedSeq *uint64
71 TabNames *[]string
72 TabRecords *[]CatalogueTabRecord
73}
74
75// MetadataUpdate returns a complete mutable-metadata update for r. Catalogue
76// implementations retain all recovery and checkpoint fields they own.
77func (r CatalogueRecord) MetadataUpdate() CatalogueMetadataUpdate {
78 cwd, updatedAt, lastUsedSeq := r.Cwd, r.UpdatedAt, r.LastUsedSeq
79 tabNames := append([]string(nil), r.TabNames...)
80 tabRecords := append([]CatalogueTabRecord(nil), r.TabRecords...)
81 return CatalogueMetadataUpdate{
82 Name: r.Name, IncarnationID: r.IncarnationID,
83 Cwd: &cwd, UpdatedAt: &updatedAt, LastUsedSeq: &lastUsedSeq, TabNames: &tabNames, TabRecords: &tabRecords,
84 }
85}
86
87// NewIncarnationID reads a new non-zero identity from r.
88func NewIncarnationID(r io.Reader) (IncarnationID, error) {
89 var id IncarnationID
90 if _, err := io.ReadFull(r, id[:]); err != nil {
91 return id, err
92 }
93 if id == (IncarnationID{}) {
94 return id, errors.New("zero incarnation ID")
95 }
96 return id, nil
97}
98
99func (id IncarnationID) String() string {
100 return hex.EncodeToString(id[:])
101}
102
103// MarshalText emits the canonical lowercase hexadecimal identity.
104func (id IncarnationID) MarshalText() ([]byte, error) {
105 if id == (IncarnationID{}) {
106 return nil, errors.New("zero incarnation ID")
107 }
108 return []byte(id.String()), nil
109}
110
111// UnmarshalText accepts only a canonical lowercase non-zero identity.
112func (id *IncarnationID) UnmarshalText(text []byte) error {
113 if len(text) != 32 || strings.ToLower(string(text)) != string(text) {
114 return errors.New("invalid incarnation ID")
115 }
116 var decoded IncarnationID
117 if _, err := hex.Decode(decoded[:], text); err != nil {
118 return fmt.Errorf("invalid incarnation ID: %w", err)
119 }
120 if decoded == (IncarnationID{}) {
121 return errors.New("zero incarnation ID")
122 }
123 *id = decoded
124 return nil
125}
126
127// Validate rejects catalogue states that cannot be restored deterministically.
128func (r CatalogueRecord) Validate() error {
129 if err := ValidateSessionName(r.Name); err != nil {
130 return err
131 }
132 if r.IncarnationID == (IncarnationID{}) {
133 return errors.New("zero catalogue incarnation ID")
134 }
135
136 for _, tab := range r.TabRecords {
137 if tab.StableID != "" {
138 if err := ValidateTabStableID(tab.StableID); err != nil {
139 return errors.New("invalid catalogue tab metadata")
140 }
141 }
142 if !utf8.ValidString(tab.Name) {
143 return errors.New("invalid catalogue tab metadata")
144 }
145 }
146 if r.DegradedReason != "" && r.Committed == nil {
147 return errors.New("broken session has no committed checkpoint")
148 }
149 if r.Committed != nil {
150 if r.Committed.Generation == 0 {
151 return errors.New("zero checkpoint generation")
152 }
153 if r.Committed.ManifestDigest == ([32]byte{}) {
154 return errors.New("zero checkpoint manifest digest")
155 }
156 }
157 return nil
158}