123 lines
3.7 KiB
Go
123 lines
3.7 KiB
Go
package smolid
|
|
|
|
import (
|
|
"encoding/base32"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Taking a reference to time.Now to make testing easier
|
|
var now = time.Now
|
|
|
|
const (
|
|
epoch uint64 = 1735707600000 // 2025-01-01 00:00:00
|
|
timestampShiftOffset = 23 // The timestamp is shifted three bytes to the left
|
|
versionShiftOffset = 21
|
|
versionMask = 0b11 << versionShiftOffset
|
|
|
|
v1TypeShiftOffset = 9
|
|
v1TypeFlag = 0b1 << 20
|
|
v1TypeMask = 0b111 << v1TypeShiftOffset
|
|
v1RandomSpace = 0xfffff // In V1, the least significant two and a half bytes (20 bits) can be random
|
|
v1Version = 0b1 << 21
|
|
)
|
|
|
|
/*
|
|
ID is a 64-bit (8-byte) value intended to be
|
|
- URL-Friendly; short and unobtrusive in its default unpadded base32 string encoding
|
|
- temporally sortable with strong index locality
|
|
- fast-enough and unique-enough for most use cases
|
|
|
|
Field Definitions
|
|
|
|
- Timestamp (41 bits) The most significant 41 bits represent a millisecond-precision timestamp
|
|
The allowed timestamp range is 2025-01-01 00:00:00 - 2094-09-07 15:47:35
|
|
- Version (2 bits) Bits 41-42 are reserved for versioning.
|
|
v1 is 0b01
|
|
- Type Flag (1 bit): Bit 43 serves as a boolean flag. If set, the "Type/Rand" field (Byte 6) is an embedded type
|
|
identifier.
|
|
- Reserved/Random (4 bits): The remaining 4 bits of the 6th byte are currently reserved. In Version 1, these are
|
|
populated with pseudo-random data.
|
|
- Type/Random (7 bits): If the Type Flag is set, this field contains the Type Identifier. Otherwise, it
|
|
is populated with pseudo-random data.
|
|
- Random (9 bits): The remaining byte is dedicated to pseudo-random data to reasonably ensure uniqueness.
|
|
|
|
String Format
|
|
|
|
The string format is base32 with no padding. Canonically the string is lowercased. This decision is purely for
|
|
aesthetics, but the parser is case-insensitive and will accept uppercase base32 strings.
|
|
*/
|
|
type ID struct {
|
|
n uint64
|
|
}
|
|
|
|
func New() ID {
|
|
var id = (uint64(now().UTC().UnixMilli()) - epoch) << timestampShiftOffset // set the timestamp
|
|
id |= v1Version // set the version bit
|
|
id |= rand.Uint64N(v1RandomSpace) // radom-fill the remaining space
|
|
return ID{id}
|
|
}
|
|
|
|
func Nil() ID { return ID{0} }
|
|
|
|
func NewForType(typ byte) ID {
|
|
id := New() // get a new v1 ID
|
|
id.n &^= v1TypeMask // clear the random data in the type space
|
|
id.n |= v1TypeFlag // set the type flag
|
|
id.n |= uint64(typ) << v1TypeShiftOffset // set the type
|
|
return id
|
|
}
|
|
|
|
func FromString(s string) (_ ID, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
var ok bool
|
|
err, ok = r.(error)
|
|
if !ok {
|
|
err = fmt.Errorf("%v", r)
|
|
}
|
|
}
|
|
}()
|
|
|
|
s = strings.ToUpper(s)
|
|
var bs []byte
|
|
bs, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s)
|
|
if err != nil {
|
|
return Nil(), err
|
|
}
|
|
|
|
return ID{binary.BigEndian.Uint64(bs)}, nil
|
|
}
|
|
|
|
func Must(id ID, err error) ID {
|
|
if err != nil {
|
|
panic("couldn't parse id: " + err.Error())
|
|
}
|
|
return id
|
|
}
|
|
|
|
func (id ID) String() string {
|
|
return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(id.Bytes()))
|
|
}
|
|
|
|
func (id ID) Version() int { return int(id.n&versionMask) >> versionShiftOffset }
|
|
|
|
func (id ID) Bytes() []byte {
|
|
var bs = make([]byte, 8)
|
|
binary.BigEndian.PutUint64(bs, id.n)
|
|
return bs
|
|
}
|
|
|
|
func (id ID) Time() time.Time {
|
|
var ms = int64(id.n>>timestampShiftOffset + epoch) // extract the timestamp
|
|
return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)) // fill into a time.Time
|
|
}
|
|
|
|
func (id ID) Type() byte {
|
|
typ := id.n & v1TypeMask
|
|
return byte(typ >> v1TypeShiftOffset)
|
|
}
|