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