// 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) }