refactor: improve explanation on readme
This commit is contained in:
parent
ca0fc09a01
commit
3eef15ba0b
2 changed files with 63 additions and 31 deletions
|
|
@ -1,6 +1,18 @@
|
|||
# @peoplesgrocers/lseq
|
||||
|
||||
TypeScript implementation of the L-SEQ algorithm for fractional indexing and list CRDTs.
|
||||
TypeScript implementation of the L-SEQ algorithm for fractional indexing and
|
||||
list CRDTs.
|
||||
|
||||
|
||||
The library handles edge cases that break other more naive implementations of
|
||||
fractional indexing. With this library you can always insert before the first
|
||||
item or after the last item, indefinitely.
|
||||
|
||||
This is exactly what you need to build realtime collaborative apps with
|
||||
ordering for lists or trees of items. Users can reorder or insert at arbitrary
|
||||
positions but in practice people really like moving items to the top or end of
|
||||
a list. So don't crash accept random crashes from other libraries when Alice
|
||||
moves yet another element to the front of the list.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -11,51 +23,71 @@ npm install @peoplesgrocers/lseq
|
|||
## Usage
|
||||
|
||||
```typescript
|
||||
import { LSEQ, compareLSEQ } from '@peoplesgrocers/lseq';
|
||||
import { LSEQ } from '@peoplesgrocers/lseq';
|
||||
|
||||
// Create a new L-SEQ instance
|
||||
const lseq = new LSEQ();
|
||||
|
||||
// Allocate identifiers
|
||||
const id1 = lseq.alloc(null, null); // First identifier
|
||||
const id2 = lseq.alloc(id1, null); // After id1
|
||||
const id3 = lseq.alloc(id1, id2); // Between id1 and id2
|
||||
const first = lseq.alloc(null, null);
|
||||
const second = lseq.alloc(first, null);
|
||||
const between = lseq.alloc(first, second);
|
||||
|
||||
// Sort identifiers
|
||||
const ids = [id3, id1, id2];
|
||||
ids.sort(compareLSEQ);
|
||||
console.log(ids); // [id1, id3, id2] - properly ordered
|
||||
|
||||
// Custom random function (useful for deterministic testing)
|
||||
const deterministicLSEQ = new LSEQ(() => 0.5);
|
||||
// String comparison gives correct order
|
||||
[second, first, between].sort(); // [first, between, second]
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `LSEQ`
|
||||
### `new LSEQ(random?: () => number)`
|
||||
|
||||
#### `constructor(random?: () => number)`
|
||||
Create an allocator. Accepts an optional random function for deterministic testing.
|
||||
|
||||
Creates a new L-SEQ instance.
|
||||
### `lseq.alloc(before: string | null, after: string | null): string`
|
||||
|
||||
- `random`: Optional custom random function (defaults to `Math.random`)
|
||||
Allocate a key between `before` and `after`.
|
||||
|
||||
#### `alloc(before: string | null, after: string | null): string`
|
||||
|
||||
Allocates a new identifier between two existing identifiers.
|
||||
|
||||
- `before`: The identifier that should come before the new one (or `null` for beginning)
|
||||
- `after`: The identifier that should come after the new one (or `null` for end)
|
||||
- Returns: A new identifier that sorts between `before` and `after`
|
||||
- `lseq.alloc(null, null)` — first key in an empty list
|
||||
- `lseq.alloc(null, first)` — insert at head
|
||||
- `lseq.alloc(last, null)` — insert at tail
|
||||
- `lseq.alloc(a, b)` — insert between a and b
|
||||
|
||||
### `compareLSEQ(a: string, b: string): number`
|
||||
|
||||
Compares two L-SEQ identifiers for sorting.
|
||||
Compare two keys. Returns -1, 0, or 1.
|
||||
|
||||
- Returns: `-1` if `a < b`, `1` if `a > b`, `0` if `a === b`
|
||||
### `EvenSpacingIterator`
|
||||
|
||||
## How it works
|
||||
Generate evenly-spaced keys for bulk initialization:
|
||||
|
||||
L-SEQ generates identifiers using a base-64 alphabet that maintains lexicographic ordering. Each identifier is a sequence of characters from this alphabet, and new identifiers are generated by finding space between existing ones at different depths.
|
||||
```typescript
|
||||
import { EvenSpacingIterator } from '@peoplesgrocers/lseq';
|
||||
|
||||
The algorithm uses alternating allocation strategies (bias toward min or max) at different depths to avoid degenerative cases and maintain good performance characteristics.
|
||||
const { k, iterator } = EvenSpacingIterator.create(1000);
|
||||
const keys = [];
|
||||
for (const position of iterator) {
|
||||
keys.push(EvenSpacingIterator.positionToKey(k, position));
|
||||
}
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
Keys are base-64 encoded strings using the base64 url-safe alphabet
|
||||
`-0-9A-Z_a-z`. There is a compact binary representation inside. Reasonablt
|
||||
efficient and no serialization issues with JSON, databases, or URLs.
|
||||
|
||||
Standard string comparison produces correct ordering. Keys work in SQL `ORDER
|
||||
BY`, database indexes, and any language with string sorting.
|
||||
|
||||
## Cross-language support
|
||||
|
||||
I wrote matching implementations for Rust, Golang, and Python; validated
|
||||
against a shared conformance test suite with fuzzing. If your backend isn't
|
||||
TypeScript, you can still allocate and work with these keys natively. This
|
||||
makes LSEQ a reasonable building block for your application's data model.
|
||||
|
||||
## References
|
||||
|
||||
- [LSEQ: an Adaptive Structure for Sequences in Distributed Collaborative Editing](https://hal.science/hal-00921633/document) (Nédelec et al., 2013)
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue