From 3eef15ba0b56dd53547be3bede4d6d26871d4e02 Mon Sep 17 00:00:00 2001 From: nobody Date: Fri, 12 Dec 2025 22:44:01 -0800 Subject: [PATCH] refactor: improve explanation on readme --- typescript/README.md | 92 +++++++++++++++++++++++++++-------------- typescript/package.json | 2 +- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/typescript/README.md b/typescript/README.md index b70fd2c..df58ed4 100644 --- a/typescript/README.md +++ b/typescript/README.md @@ -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. \ No newline at end of file +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 diff --git a/typescript/package.json b/typescript/package.json index 67a2e5d..5a782ca 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@peoplesgrocers/lseq", - "version": "0.99.0", + "version": "0.99.1", "description": "L-SEQ algorithm implementation for fractional indexing and list CRDTs", "main": "dist/index.js", "types": "dist/index.d.ts",