lseq/golang/lseq.go

293 lines
6.8 KiB
Go
Raw Permalink Normal View History

2025-12-12 23:18:05 -08:00
// Package lseq provides an implementation of LSEQ, a CRDT-friendly sequence
// allocation algorithm that generates lexicographically sortable identifiers.
package lseq
import (
"errors"
"math"
"strings"
)
// Alphabet is the 64-character set used for encoding sort keys.
// Characters are ordered so that string comparison matches numeric comparison.
const Alphabet = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
// maxLevelExponent caps level values at 64^8 for JavaScript float compatibility.
const maxLevelExponent = 8
// maxValueForLevel returns the maximum value for a given level (0-indexed).
// Level 0: 64^1 - 1 = 63
// Level 1: 64^2 - 1 = 4095
// Level 7+: 64^8 - 1 (capped for JS compatibility)
func maxValueForLevel(level int) uint64 {
exp := level + 1
if exp > maxLevelExponent {
exp = maxLevelExponent
}
return uint64(math.Pow(64, float64(exp))) - 1
}
// charsForLevel returns the number of characters needed to encode a value at this level.
func charsForLevel(level int) int {
chars := level + 1
if chars > maxLevelExponent {
chars = maxLevelExponent
}
return chars
}
// idToNumbers parses a sort key string into its numeric components.
func idToNumbers(id string) ([]uint64, error) {
if id == "" {
return nil, nil
}
numbers := make([]uint64, 0)
pos := 0
level := 0
for pos < len(id) {
numChars := charsForLevel(level)
if pos+numChars > len(id) {
return nil, errors.New("invalid sort key length")
}
var value uint64
for i := 0; i < numChars; i++ {
char := id[pos+i]
digit := strings.IndexByte(Alphabet, char)
if digit == -1 {
return nil, errors.New("invalid character in sort key")
}
value = value*64 + uint64(digit)
}
numbers = append(numbers, value)
pos += numChars
level++
}
return numbers, nil
}
// numbersToID encodes numeric components into a sort key string.
func numbersToID(numbers []uint64) string {
var result strings.Builder
for level, value := range numbers {
numChars := charsForLevel(level)
chars := make([]byte, numChars)
v := value
for i := numChars - 1; i >= 0; i-- {
chars[i] = Alphabet[v%64]
v /= 64
}
result.Write(chars)
}
return result.String()
}
// LSEQ allocates sort keys between existing keys using the LSEQ algorithm.
type LSEQ struct {
strategies []bool
random func() float64
}
// NewLSEQ creates a new LSEQ allocator with the given random number generator.
// The random function should return values in [0, 1).
// If random is nil, a default implementation using math/rand is NOT provided;
// the caller must supply their own RNG for deterministic behavior.
func NewLSEQ(random func() float64) *LSEQ {
l := &LSEQ{
strategies: make([]bool, 1),
random: random,
}
l.strategies[0] = random() < 0.5
return l
}
// genRange returns a random integer in [min, max).
func (l *LSEQ) genRange(min, max uint64) uint64 {
return min + uint64(l.random()*float64(max-min))
}
// Alloc allocates a new sort key between before and after.
// If before is nil, allocates at the beginning.
// If after is nil, allocates at the end.
func (l *LSEQ) Alloc(before, after *string) string {
var p, q []uint64
var err error
if before != nil {
p, err = idToNumbers(*before)
if err != nil {
p = nil
}
}
if after != nil {
q, err = idToNumbers(*after)
if err != nil {
q = nil
}
}
depth := 0
result := make([]uint64, 0)
for {
var pVal uint64
if depth < len(p) {
pVal = p[depth]
}
var qUpper *uint64
if depth < len(q) {
qUpper = &q[depth]
}
levelMax := maxValueForLevel(depth)
minAlloc := pVal + 1
var maxAlloc uint64
if qUpper != nil {
if *qUpper > 0 {
maxAlloc = *qUpper - 1
} else {
maxAlloc = 0
}
} else {
maxAlloc = levelMax
}
if minAlloc <= maxAlloc {
rangeSize := maxAlloc - minAlloc + 1
offset := l.genRange(0, rangeSize)
var newValue uint64
if l.strategies[depth] {
newValue = minAlloc + offset
} else {
newValue = maxAlloc - offset
}
result = append(result, newValue)
return numbersToID(result)
}
result = append(result, pVal)
depth++
if depth >= len(l.strategies) {
l.strategies = append(l.strategies, l.random() < 0.5)
}
}
}
// CompareLSEQ compares two LSEQ sort keys.
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
func CompareLSEQ(a, b string) int {
return strings.Compare(a, b)
}
// EvenSpacingIterator generates evenly spaced positions for bulk allocation.
type EvenSpacingIterator struct {
remainingItems int
spaceSize uint64
nextItem uint64
stepSizeInteger uint64
stepSizeError float64
errorAccumulator float64
}
// usableSpace contains (64^k - 1) values for k from 1 to 8.
// Position 0 is reserved (nothing can sort before all-zeros).
var usableSpace = []uint64{
64 - 1, // 64^1 - 1
4096 - 1, // 64^2 - 1
262144 - 1, // 64^3 - 1
16777216 - 1, // 64^4 - 1
1073741824 - 1, // 64^5 - 1
68719476736 - 1, // 64^6 - 1
4398046511104 - 1, // 64^7 - 1
281474976710656 - 1, // 64^8 - 1
}
// ErrTooManyItems is returned when the requested number of items exceeds capacity.
var ErrTooManyItems = errors.New("too many items to allocate")
// NewEvenSpacingIterator creates an iterator for evenly spacing totalItems positions.
// Returns the level k and the iterator, or an error if totalItems is too large.
func NewEvenSpacingIterator(totalItems int) (k int, iter *EvenSpacingIterator, err error) {
if totalItems == 0 {
return 0, nil, ErrTooManyItems
}
k = 0
var spaceSize uint64
for i, size := range usableSpace {
if size >= uint64(totalItems) {
k = i + 1
spaceSize = size
break
}
}
if k == 0 {
return 0, nil, ErrTooManyItems
}
stepSize := float64(spaceSize) / float64(totalItems)
stepSizeInteger := uint64(stepSize)
stepSizeError := stepSize - float64(stepSizeInteger)
iter = &EvenSpacingIterator{
remainingItems: totalItems,
spaceSize: spaceSize,
nextItem: 1,
stepSizeInteger: stepSizeInteger,
stepSizeError: stepSizeError,
errorAccumulator: 0,
}
return k, iter, nil
}
// Next returns the next position and true, or 0 and false if exhausted.
func (e *EvenSpacingIterator) Next() (uint64, bool) {
if e.remainingItems == 0 {
return 0, false
}
if e.nextItem > e.spaceSize {
return 0, false
}
currentPosition := e.nextItem
e.remainingItems--
e.nextItem += e.stepSizeInteger
e.errorAccumulator += e.stepSizeError
if e.errorAccumulator >= 1.0 {
e.nextItem++
e.errorAccumulator -= 1.0
}
return currentPosition, true
}
// PositionToKey converts a position within a level-k space to a sort key string.
// Creates a k-level key where levels 0 through k-2 are 0, and level k-1 contains the position.
func PositionToKey(k int, position uint64) string {
result := make([]uint64, 0, k)
for i := 0; i < k-1; i++ {
result = append(result, 0)
}
if k > 0 {
result = append(result, position)
}
return numbersToID(result)
}