293 lines
6.8 KiB
Go
293 lines
6.8 KiB
Go
|
|
// 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)
|
||
|
|
}
|