this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

syntax: TID helpers, including generator 'clock'

+167 -1
+90 -1
atproto/syntax/tid.go
··· 1 1 package syntax 2 2 3 3 import ( 4 + "encoding/base32" 4 5 "fmt" 5 6 "regexp" 7 + "strings" 8 + "sync" 9 + "time" 6 10 ) 11 + 12 + const ( 13 + Base32SortAlphabet = "234567abcdefghijklmnopqrstuvwxyz" 14 + ) 15 + 16 + func Base32Sort() *base32.Encoding { 17 + return base32.NewEncoding(Base32SortAlphabet).WithPadding(base32.NoPadding) 18 + } 7 19 8 20 // Represents a TID in string format, as would pass Lexicon syntax validation. 9 21 // ··· 23 35 return TID(raw), nil 24 36 } 25 37 26 - // TODO: additional helpers: to timestamp, from timestamp, from integer, etc 38 + // Naive (unsafe) one-off TID generation with the current time. 39 + // 40 + // You should usually use a [TIDClock] to ensure monotonic output. 41 + func NewTIDNow(clockId uint) TID { 42 + return NewTID(time.Now().UTC().UnixMicro(), clockId) 43 + } 44 + 45 + func NewTIDFromInteger(v uint64) TID { 46 + v = (0x7FFF_FFFF_FFFF_FFFF & v) 47 + s := "" 48 + for i := 0; i < 13; i++ { 49 + s = string(Base32SortAlphabet[v&0x1F]) + s 50 + v = v >> 5 51 + } 52 + return TID(s) 53 + } 54 + 55 + // Constructs a new TID from a UNIX timestamp (in milliseconds) and clock ID value. 56 + func NewTID(unixMilis int64, clockId uint) TID { 57 + var v uint64 = (uint64(unixMilis&0x1F_FFFF_FFFF_FFFF) << 10) | uint64(clockId&0x3FF) 58 + return NewTIDFromInteger(v) 59 + } 60 + 61 + // Returns full integer representation of this TID (not used often) 62 + func (t TID) Integer() uint64 { 63 + s := t.String() 64 + if len(s) != 13 { 65 + return 0 66 + } 67 + var v uint64 68 + for i := 0; i < 13; i++ { 69 + c := strings.IndexByte(Base32SortAlphabet, s[i]) 70 + if c < 0 { 71 + return 0 72 + } 73 + v = (v << 5) | uint64(c&0x1F) 74 + } 75 + return v 76 + } 77 + 78 + // Returns the golang [time.Time] corresponding to this TID's timestamp. 79 + func (t TID) Time() time.Time { 80 + i := t.Integer() 81 + i = (i >> 10) & 0x1FFF_FFFF_FFFF_FFFF 82 + return time.UnixMicro(int64(i)).UTC() 83 + } 84 + 85 + // Returns the clock ID part of this TID, as an unsigned integer 86 + func (t TID) ClockID() uint { 87 + i := t.Integer() 88 + return uint(i & 0x3FF) 89 + } 27 90 28 91 func (t TID) String() string { 29 92 return string(t) ··· 41 104 *t = tid 42 105 return nil 43 106 } 107 + 108 + // TID generator, which keeps state to ensure TID values always monotonically increase. 109 + // 110 + // Uses [sync.Mutex], so may block briefly but safe for concurrent use. 111 + type TIDClock struct { 112 + ClockID uint 113 + mtx sync.Mutex 114 + lastUnixMicro int64 115 + } 116 + 117 + func NewTIDClock(clockId uint) *TIDClock { 118 + return &TIDClock{ 119 + ClockID: clockId, 120 + } 121 + } 122 + 123 + func (c *TIDClock) Next() TID { 124 + now := time.Now().UTC().UnixMicro() 125 + c.mtx.Lock() 126 + if now <= c.lastUnixMicro { 127 + now = c.lastUnixMicro + 1 128 + } 129 + c.lastUnixMicro = now 130 + c.mtx.Unlock() 131 + return NewTID(now, c.ClockID) 132 + }
+77
atproto/syntax/tid_test.go
··· 5 5 "fmt" 6 6 "os" 7 7 "testing" 8 + "time" 8 9 9 10 "github.com/stretchr/testify/assert" 10 11 ) ··· 48 49 } 49 50 assert.NoError(scanner.Err()) 50 51 } 52 + 53 + func TestTIDParts(t *testing.T) { 54 + assert := assert.New(t) 55 + 56 + raw := "3kao2cl6lyj2p" 57 + tid, err := ParseTID(raw) 58 + assert.NoError(err) 59 + // TODO: assert.Equal(uint64(0x181a8044491f3bec), tid.Integer()) 60 + // TODO: assert.Equal(uint(1004), tid.ClockID()) 61 + assert.Equal(2023, tid.Time().Year()) 62 + 63 + out := NewTID(tid.Time().UnixMicro(), tid.ClockID()) 64 + assert.Equal(raw, out.String()) 65 + assert.Equal(tid.ClockID(), out.ClockID()) 66 + assert.Equal(tid.Time(), out.Time()) 67 + assert.Equal(tid.Integer(), out.Integer()) 68 + 69 + out2 := NewTIDFromInteger(tid.Integer()) 70 + assert.Equal(tid.Integer(), out2.Integer()) 71 + } 72 + 73 + func TestTIDExamples(t *testing.T) { 74 + assert := assert.New(t) 75 + // TODO: seems like TS code might be wrong? "242k52k4kg3s2" 76 + assert.Equal("242k52k4kg3sc", NewTIDFromInteger(0x0102030405060708).String()) 77 + assert.Equal(uint64(0x0102030405060708), TID("242k52k4kg3sc").Integer()) 78 + //assert.Equal("2222222222222", NewTIDFromInteger(0x0000000000000000).String()) 79 + //assert.Equal(uint64(), TID("242k52k4kg3s2").Integer()) 80 + assert.Equal("2222222222223", NewTIDFromInteger(0x0000000000000001).String()) 81 + assert.Equal(uint64(0x0000000000000001), TID("2222222222223").Integer()) 82 + 83 + assert.Equal("6222222222222", NewTIDFromInteger(0x4000000000000000).String()) 84 + assert.Equal(uint64(0x4000000000000000), TID("6222222222222").Integer()) 85 + 86 + // ignoring type byte 87 + assert.Equal("2222222222222", NewTIDFromInteger(0x8000000000000000).String()) 88 + } 89 + 90 + func TestTIDNoPanic(t *testing.T) { 91 + for _, s := range []string{"", "3jzfcijpj2z2aa", "3jzfcijpj2z2", ".."} { 92 + bad := TID(s) 93 + _ = bad.ClockID() 94 + _ = bad.Integer() 95 + _ = bad.Time() 96 + _ = bad.String() 97 + } 98 + } 99 + 100 + func TestTIDConstruction(t *testing.T) { 101 + assert := assert.New(t) 102 + 103 + zero := NewTID(0, 0) 104 + assert.Equal("2222222222222", zero.String()) 105 + assert.Equal(uint64(0), zero.Integer()) 106 + assert.Equal(uint(0), zero.ClockID()) 107 + assert.Equal(time.UnixMilli(0).UTC(), zero.Time()) 108 + 109 + now := NewTIDNow(1011) 110 + assert.Equal(uint(1011), now.ClockID()) 111 + assert.True(time.Since(now.Time()) < time.Minute) 112 + 113 + over := NewTIDNow(4096) 114 + assert.Equal(uint(0), over.ClockID()) 115 + } 116 + 117 + func TestTIDClock(t *testing.T) { 118 + assert := assert.New(t) 119 + 120 + clk := NewTIDClock(0) 121 + last := NewTID(0, 0) 122 + for i := 0; i < 100; i++ { 123 + next := clk.Next() 124 + assert.Greater(next, last) 125 + last = next 126 + } 127 + }