1package syntax
2
3import (
4 "encoding/base32"
5 "errors"
6 "regexp"
7 "strings"
8 "sync"
9 "time"
10)
11
12const (
13 Base32SortAlphabet = "234567abcdefghijklmnopqrstuvwxyz"
14)
15
16func Base32Sort() *base32.Encoding {
17 return base32.NewEncoding(Base32SortAlphabet).WithPadding(base32.NoPadding)
18}
19
20// Represents a TID in string format, as would pass Lexicon syntax validation.
21//
22// Always use [ParseTID] instead of wrapping strings directly, especially when working with network input.
23//
24// Syntax specification: https://atproto.com/specs/record-key
25type TID string
26
27var tidRegex = regexp.MustCompile(`^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$`)
28
29func ParseTID(raw string) (TID, error) {
30 if raw == "" {
31 return "", errors.New("expected TID, got empty string")
32 }
33 if len(raw) != 13 {
34 return "", errors.New("TID is wrong length (expected 13 chars)")
35 }
36 if !tidRegex.MatchString(raw) {
37 return "", errors.New("TID syntax didn't validate via regex")
38 }
39 return TID(raw), nil
40}
41
42// Naive (unsafe) one-off TID generation with the current time.
43//
44// You should usually use a [TIDClock] to ensure monotonic output.
45func NewTIDNow(clockId uint) TID {
46 return NewTID(time.Now().UTC().UnixMicro(), clockId)
47}
48
49func NewTIDFromInteger(v uint64) TID {
50 v = (0x7FFF_FFFF_FFFF_FFFF & v)
51 s := ""
52 for i := 0; i < 13; i++ {
53 s = string(Base32SortAlphabet[v&0x1F]) + s
54 v = v >> 5
55 }
56 return TID(s)
57}
58
59// Constructs a new TID from a UNIX timestamp (in milliseconds) and clock ID value.
60func NewTID(unixMicros int64, clockId uint) TID {
61 v := (uint64(unixMicros&0x1F_FFFF_FFFF_FFFF) << 10) | uint64(clockId&0x3FF)
62 return NewTIDFromInteger(v)
63}
64
65// Constructs a new TID from a [time.Time] and clock ID value
66func NewTIDFromTime(ts time.Time, clockId uint) TID {
67 return NewTID(ts.UTC().UnixMicro(), clockId)
68}
69
70// Returns full integer representation of this TID (not used often)
71func (t TID) Integer() uint64 {
72 s := t.String()
73 if len(s) != 13 {
74 return 0
75 }
76 var v uint64
77 for i := 0; i < 13; i++ {
78 c := strings.IndexByte(Base32SortAlphabet, s[i])
79 if c < 0 {
80 return 0
81 }
82 v = (v << 5) | uint64(c&0x1F)
83 }
84 return v
85}
86
87// Returns the golang [time.Time] corresponding to this TID's timestamp.
88func (t TID) Time() time.Time {
89 i := t.Integer()
90 i = (i >> 10) & 0x1FFF_FFFF_FFFF_FFFF
91 return time.UnixMicro(int64(i)).UTC()
92}
93
94// Returns the clock ID part of this TID, as an unsigned integer
95func (t TID) ClockID() uint {
96 i := t.Integer()
97 return uint(i & 0x3FF)
98}
99
100func (t TID) String() string {
101 return string(t)
102}
103
104func (t TID) MarshalText() ([]byte, error) {
105 return []byte(t.String()), nil
106}
107
108func (t *TID) UnmarshalText(text []byte) error {
109 tid, err := ParseTID(string(text))
110 if err != nil {
111 return err
112 }
113 *t = tid
114 return nil
115}
116
117// TID generator, which keeps state to ensure TID values always monotonically increase.
118//
119// Uses [sync.Mutex], so may block briefly but safe for concurrent use.
120type TIDClock struct {
121 ClockID uint
122 mtx sync.Mutex
123 lastUnixMicro int64
124}
125
126func NewTIDClock(clockId uint) TIDClock {
127 return TIDClock{
128 ClockID: clockId,
129 }
130}
131
132func ClockFromTID(t TID) TIDClock {
133 um := t.Integer()
134 um = (um >> 10) & 0x1FFF_FFFF_FFFF_FFFF
135 return TIDClock{
136 ClockID: t.ClockID(),
137 lastUnixMicro: int64(um),
138 }
139}
140
141func (c *TIDClock) Next() TID {
142 now := time.Now().UTC().UnixMicro()
143 c.mtx.Lock()
144 if now <= c.lastUnixMicro {
145 now = c.lastUnixMicro + 1
146 }
147 c.lastUnixMicro = now
148 c.mtx.Unlock()
149 return NewTID(now, c.ClockID)
150}