feat(crdt): add @robonen/crdt package
Hand-built CRDT primitives: Lamport clock + version vectors, op-log, LWW register/map, RGA sequence, fractional indexing, marks store, sync encode, and doc/replica. Includes eslint flat config + composite tsconfig.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { keyBetween, keysBetween } from '..';
|
||||
|
||||
function seeded(seed: number): () => number {
|
||||
let state = seed >>> 0;
|
||||
return () => {
|
||||
state = (state * 1664525 + 1013904223) >>> 0;
|
||||
return state / 0xFFFFFFFF;
|
||||
};
|
||||
}
|
||||
|
||||
describe('keyBetween', () => {
|
||||
it('produces a key strictly between its bounds', () => {
|
||||
const a = keyBetween(null, null);
|
||||
const b = keyBetween(a, null);
|
||||
expect(b > a).toBe(true);
|
||||
const mid = keyBetween(a, b);
|
||||
expect(mid > a && mid < b).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects an inverted range', () => {
|
||||
expect(() => keyBetween('b', 'a')).toThrow();
|
||||
});
|
||||
|
||||
it('keysBetween returns n ascending keys within the bounds', () => {
|
||||
const keys = keysBetween('a', 'b', 5);
|
||||
expect(keys).toHaveLength(5);
|
||||
for (let i = 1; i < keys.length; i++)
|
||||
expect(keys[i - 1]! < keys[i]!).toBe(true);
|
||||
expect(keys[0]! > 'a' && keys[keys.length - 1]! < 'b').toBe(true);
|
||||
});
|
||||
|
||||
it('stays strictly ordered under 300 random insertions', () => {
|
||||
const rng = seeded(7);
|
||||
const keys: string[] = [keyBetween(null, null)];
|
||||
|
||||
for (let i = 0; i < 300; i++) {
|
||||
const pos = Math.floor(rng() * (keys.length + 1));
|
||||
const lower = pos > 0 ? keys[pos - 1]! : null;
|
||||
const upper = pos < keys.length ? keys[pos]! : null;
|
||||
keys.splice(pos, 0, keyBetween(lower, upper));
|
||||
}
|
||||
|
||||
for (let i = 1; i < keys.length; i++)
|
||||
expect(keys[i - 1]! < keys[i]!).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Fractional indexing: generate string "order keys" so an element can be placed
|
||||
* strictly between two neighbors with a single key, and re-ordered (moved) by
|
||||
* just changing its key — without touching anything else. The digit alphabet is
|
||||
* ASCII-ascending, so JavaScript string comparison matches digit order.
|
||||
*/
|
||||
const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
function midpoint(a: string, b: string): string {
|
||||
if (b !== '' && a >= b)
|
||||
throw new Error(`fractional-index: lower '${a}' must be < upper '${b}'`);
|
||||
|
||||
let result = '';
|
||||
let i = 0;
|
||||
let upper = b;
|
||||
|
||||
for (;;) {
|
||||
const x = i < a.length ? DIGITS.indexOf(a[i]!) : 0;
|
||||
const y = upper !== '' && i < upper.length ? DIGITS.indexOf(upper[i]!) : DIGITS.length;
|
||||
|
||||
if (x === y) {
|
||||
result += DIGITS[x]!;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const mid = x + Math.floor((y - x) / 2);
|
||||
if (mid !== x)
|
||||
return result + DIGITS[mid]!;
|
||||
|
||||
// Digits are adjacent — keep the lower digit and open the upper bound.
|
||||
result += DIGITS[x]!;
|
||||
i += 1;
|
||||
upper = '';
|
||||
}
|
||||
}
|
||||
|
||||
/** A key strictly between `lower` and `upper` (`null` = open bound). */
|
||||
export function keyBetween(lower: string | null, upper: string | null): string {
|
||||
return midpoint(lower ?? '', upper ?? '');
|
||||
}
|
||||
|
||||
/** `n` keys strictly between `lower` and `upper`, in ascending order. */
|
||||
export function keysBetween(lower: string | null, upper: string | null, n: number): string[] {
|
||||
if (n <= 0)
|
||||
return [];
|
||||
if (n === 1)
|
||||
return [keyBetween(lower, upper)];
|
||||
|
||||
const mid = keyBetween(lower, upper);
|
||||
const left = keysBetween(lower, mid, Math.floor((n - 1) / 2));
|
||||
const right = keysBetween(mid, upper, n - 1 - left.length);
|
||||
return [...left, mid, ...right];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './fractional-index';
|
||||
Reference in New Issue
Block a user