fork of indigo with slightly nicer lexgen
at main 3.5 kB view raw
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}