From c423d64ac29747243e2c67bf1677450c84570b37 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 6 Jun 2026 15:50:45 -0700 Subject: [PATCH 1/2] chore(tables): own fractional-indexing in-house, drop runtime dep --- .../fractional-indexing.test.ts | 203 +++++++++++++ .../fractional-indexing.ts | 281 ++++++++++++++++++ apps/sim/lib/table/order-key.ts | 12 +- apps/sim/package.json | 2 +- bun.lock | 2 +- 5 files changed, 494 insertions(+), 6 deletions(-) create mode 100644 apps/sim/lib/fractional-indexing/fractional-indexing.test.ts create mode 100644 apps/sim/lib/fractional-indexing/fractional-indexing.ts diff --git a/apps/sim/lib/fractional-indexing/fractional-indexing.test.ts b/apps/sim/lib/fractional-indexing/fractional-indexing.test.ts new file mode 100644 index 0000000000..5a36957e7b --- /dev/null +++ b/apps/sim/lib/fractional-indexing/fractional-indexing.test.ts @@ -0,0 +1,203 @@ +/** + * @vitest-environment node + * + * Differential test: runs the in-house port side by side with the upstream + * `fractional-indexing` package over exhaustive + randomized inputs and asserts + * byte-identical output. The package is kept as a devDependency solely as this + * oracle. Delete this file (and the dep) once we no longer want the comparison. + */ +import { + generateKeyBetween as oracleKeyBetween, + generateNKeysBetween as oracleNKeysBetween, +} from 'fractional-indexing' +import { describe, expect, it } from 'vitest' +import { + generateKeyBetween, + generateNKeysBetween, +} from '@/lib/fractional-indexing/fractional-indexing' + +/** Deterministic LCG (Numerical Recipes constants) — no test-only dependency. */ +function makeRng(seed: number): () => number { + let state = seed >>> 0 + return () => { + state = (Math.imul(state, 1664525) + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +function randInt(rng: () => number, maxExclusive: number): number { + return Math.floor(rng() * maxExclusive) +} + +/** Length of a key's integer part from its head char (a..z → 2..27, A..Z → 27..2). */ +function integerPartLength(head: string): number { + if (head >= 'a' && head <= 'z') return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2 + if (head >= 'A' && head <= 'Z') return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2 + throw new Error(`unexpected head: ${head}`) +} + +/** Compare both impls for `(a, b)`: same return value, or both throw. */ +function expectKeyParity(a: string | null, b: string | null): string | null { + let mine: string | undefined + let mineThrew = false + try { + mine = generateKeyBetween(a, b) + } catch { + mineThrew = true + } + + let theirs: string | undefined + let theirsThrew = false + try { + theirs = oracleKeyBetween(a, b) + } catch { + theirsThrew = true + } + + expect(mineThrew).toBe(theirsThrew) + if (!mineThrew) { + expect(mine).toBe(theirs) + return mine as string + } + return null +} + +describe('fractional-indexing in-house port ≡ upstream', () => { + it('matches known anchor values from the algorithm', () => { + expect(generateKeyBetween(null, null)).toBe('a0') + expect(generateKeyBetween('a0', null)).toBe('a1') + expect(generateKeyBetween(null, 'a0')).toBe('Zz') + expect(generateKeyBetween('a0', 'a1')).toBe('a0V') + // All match the oracle too. + expect(generateKeyBetween(null, null)).toBe(oracleKeyBetween(null, null)) + expect(generateKeyBetween('a0', 'a1')).toBe(oracleKeyBetween('a0', 'a1')) + }) + + it('matches over exhaustive ordered pairs from a fixed key pool', () => { + // Build a sorted pool by chaining appends, then test every ordered pair + // plus both open ends. + const pool: string[] = [] + let last: string | null = null + for (let i = 0; i < 40; i++) { + last = generateKeyBetween(last, null) + pool.push(last) + } + const ends: Array = [null, ...pool] + for (const a of ends) { + for (const b of ends) { + // Only feed ordered, distinct bounds to the "happy path"; the parity + // helper also asserts both throw together for the invalid ones. + expectKeyParity(a, b) + } + } + }) + + it('matches while building a list via random-position inserts', () => { + for (const seed of [1, 7, 42, 1337, 99999]) { + const rng = makeRng(seed) + const keys: string[] = [] + for (let step = 0; step < 400; step++) { + const pos = randInt(rng, keys.length + 1) + const a = pos === 0 ? null : keys[pos - 1] + const b = pos === keys.length ? null : keys[pos] + const key = expectKeyParity(a, b) + expect(key).not.toBeNull() + keys.splice(pos, 0, key as string) + } + // List stayed strictly sorted throughout. + for (let i = 1; i < keys.length; i++) { + expect(keys[i - 1] < keys[i]).toBe(true) + } + } + }) + + it('matches generateNKeysBetween across open/closed ranges and counts', () => { + const rng = makeRng(2024) + for (let trial = 0; trial < 200; trial++) { + // A random ordered (a, b) window inside a freshly built run. + const run = generateNKeysBetween(null, null, 12) + const i = randInt(rng, run.length) + const j = randInt(rng, run.length) + const lo = Math.min(i, j) + const hi = Math.max(i, j) + const n = randInt(rng, 8) + + const a = run[lo] + const b = lo === hi ? null : run[hi] + expect(generateNKeysBetween(a, b, n)).toEqual(oracleNKeysBetween(a, b, n)) + } + // Edge counts and open ends. + expect(generateNKeysBetween(null, null, 0)).toEqual(oracleNKeysBetween(null, null, 0)) + expect(generateNKeysBetween(null, null, 1)).toEqual(oracleNKeysBetween(null, null, 1)) + expect(generateNKeysBetween(null, null, 50)).toEqual(oracleNKeysBetween(null, null, 50)) + expect(generateNKeysBetween('a0', null, 25)).toEqual(oracleNKeysBetween('a0', null, 25)) + expect(generateNKeysBetween(null, 'a0', 25)).toEqual(oracleNKeysBetween(null, 'a0', 25)) + }) + + it('matches across integer-length rollover on long append/prepend runs', () => { + // A long append run forces incrementInteger to roll the integer part + // through multiple heads and lengths (a→…→z→null path); a long prepend run + // exercises decrementInteger symmetrically. The random test above stays in + // head 'a'/'Z' length-2, so these cover the branchy carry/borrow code. + const lengths = new Set() + let appendKey: string | null = null + for (let i = 0; i < 5000; i++) { + const mine = generateKeyBetween(appendKey, null) + expect(mine).toBe(oracleKeyBetween(appendKey, null)) + appendKey = mine + lengths.add(integerPartLength(mine[0])) + } + let prependKey: string | null = null + for (let i = 0; i < 5000; i++) { + const mine = generateKeyBetween(null, prependKey) + expect(mine).toBe(oracleKeyBetween(null, prependKey)) + prependKey = mine + lengths.add(integerPartLength(mine[0])) + } + // Confirm we actually crossed integer-length boundaries (not just length 2). + expect([...lengths].some((l) => l > 2)).toBe(true) + }) + + it('matches deep same-spot inserts (long fractions)', () => { + // Repeatedly inserting between the same two neighbors grows the fraction + // without bound — exercises the recursive midpoint + common-prefix path. + let lo = generateKeyBetween(null, null) + let hi = generateKeyBetween(lo, null) + let maxLen = 0 + for (let i = 0; i < 2000; i++) { + const mine = generateKeyBetween(lo, hi) + expect(mine).toBe(oracleKeyBetween(lo, hi)) + // Alternate which side we keep so the fraction deepens on both ends. + if (i % 2 === 0) hi = mine + else lo = mine + maxLen = Math.max(maxLen, mine.length) + } + expect(maxLen).toBeGreaterThan(10) + }) + + it('throws on invalid keys and inverted bounds in both impls', () => { + const bad: Array<[string | null, string | null]> = [ + ['a1', 'a0'], // inverted + ['a0', 'a0'], // equal + ['', null], // empty key + ['a00', null], // trailing zero in fraction + ['1', null], // invalid head + ] + for (const [a, b] of bad) { + let mineThrew = false + let theirsThrew = false + try { + generateKeyBetween(a, b) + } catch { + mineThrew = true + } + try { + oracleKeyBetween(a, b) + } catch { + theirsThrew = true + } + expect(mineThrew).toBe(true) + expect(mineThrew).toBe(theirsThrew) + } + }) +}) diff --git a/apps/sim/lib/fractional-indexing/fractional-indexing.ts b/apps/sim/lib/fractional-indexing/fractional-indexing.ts new file mode 100644 index 0000000000..7eab3d5912 --- /dev/null +++ b/apps/sim/lib/fractional-indexing/fractional-indexing.ts @@ -0,0 +1,281 @@ +/** + * Fractional indexing — generate ordering strings that sort lexicographically. + * + * In-house port of David Greenspan's algorithm + * (https://observablehq.com/@dgreensp/implementing-fractional-indexing), + * behavior-identical to the `fractional-indexing` npm package (CC0). A key is a + * variable-length base-62 string: between any two keys there is always room for + * another, so inserts never renumber existing rows. The only cost is gradual + * length growth under repeated same-spot inserts. + * + * A key is ``. The integer part's first character + * encodes its own length (`a..z` → 2..27, `A..Z` → 27..2), letting integers + * grow without bound in both directions. The fraction is plain base-62 digits + * with no trailing zero. + */ + +// --------------------------------------------------------------------------- +// Digits +// --------------------------------------------------------------------------- + +/** Default digit alphabet. Must be in ascending character-code order. */ +export const BASE_62_DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + +// --------------------------------------------------------------------------- +// Integer-part helpers +// --------------------------------------------------------------------------- + +/** Length the integer part must have, derived from its first character. */ +function getIntegerLength(head: string): number { + if (head >= 'a' && head <= 'z') { + return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2 + } + if (head >= 'A' && head <= 'Z') { + return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2 + } + throw new Error(`invalid order key head: ${head}`) +} + +function validateInteger(int: string): void { + if (int.length !== getIntegerLength(int[0])) { + throw new Error(`invalid integer part of order key: ${int}`) + } +} + +function getIntegerPart(key: string): string { + const integerPartLength = getIntegerLength(key[0]) + if (integerPartLength > key.length) { + throw new Error(`invalid order key: ${key}`) + } + return key.slice(0, integerPartLength) +} + +function validateOrderKey(key: string, digits: string): void { + if (key === `A${digits[0].repeat(26)}`) { + throw new Error(`invalid order key: ${key}`) + } + // getIntegerPart throws if the head is bad or the key is too short. + const i = getIntegerPart(key) + const f = key.slice(i.length) + if (f.slice(-1) === digits[0]) { + throw new Error(`invalid order key: ${key}`) + } +} + +/** Increment the integer part; returns null past the largest integer. */ +function incrementInteger(x: string, digits: string): string | null { + validateInteger(x) + const [head, ...digs] = x.split('') + let carry = true + for (let i = digs.length - 1; carry && i >= 0; i--) { + const d = digits.indexOf(digs[i]) + 1 + if (d === digits.length) { + digs[i] = digits[0] + } else { + digs[i] = digits[d] + carry = false + } + } + if (carry) { + if (head === 'Z') { + return `a${digits[0]}` + } + if (head === 'z') { + return null + } + const h = String.fromCharCode(head.charCodeAt(0) + 1) + if (h > 'a') { + digs.push(digits[0]) + } else { + digs.pop() + } + return h + digs.join('') + } + return head + digs.join('') +} + +/** Decrement the integer part; returns null past the smallest integer. */ +function decrementInteger(x: string, digits: string): string | null { + validateInteger(x) + const [head, ...digs] = x.split('') + let borrow = true + for (let i = digs.length - 1; borrow && i >= 0; i--) { + const d = digits.indexOf(digs[i]) - 1 + if (d === -1) { + digs[i] = digits.slice(-1) + } else { + digs[i] = digits[d] + borrow = false + } + } + if (borrow) { + if (head === 'a') { + return `Z${digits.slice(-1)}` + } + if (head === 'A') { + return null + } + const h = String.fromCharCode(head.charCodeAt(0) - 1) + if (h < 'Z') { + digs.push(digits.slice(-1)) + } else { + digs.pop() + } + return h + digs.join('') + } + return head + digs.join('') +} + +// --------------------------------------------------------------------------- +// Midpoint +// --------------------------------------------------------------------------- + +/** + * Fraction strictly between `a` and `b` (both without integer parts). `a` may be + * empty; `b` is null (open end) or non-empty and `> a`. No trailing zeros. + */ +function midpoint(a: string, b: string | null | undefined, digits: string): string { + const zero = digits[0] + if (b != null && a >= b) { + throw new Error(`${a} >= ${b}`) + } + if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) { + throw new Error('trailing zero') + } + if (b) { + // Strip the longest common prefix, padding `a` with zeros as we go. `b` + // needs no padding — it can't end before `a` within the common prefix. + let n = 0 + while ((a[n] || zero) === b[n]) { + n++ + } + if (n > 0) { + return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits) + } + } + // First digits (or lack thereof) differ. + const digitA = a ? digits.indexOf(a[0]) : 0 + const digitB = b != null ? digits.indexOf(b[0]) : digits.length + if (digitB - digitA > 1) { + const midDigit = Math.round(0.5 * (digitA + digitB)) + return digits[midDigit] + } + // First digits are consecutive. + if (b && b.length > 1) { + return b.slice(0, 1) + } + // `b` is null or a single digit; recurse into `a`'s tail. + return digits[digitA] + midpoint(a.slice(1), null, digits) +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Returns a key that sorts strictly between `a` and `b`. Either may be null for + * an open end. `a < b` lexicographically when both are non-null. + * + * @throws if `a`/`b` are invalid keys or `a >= b`. + */ +export function generateKeyBetween( + a: string | null | undefined, + b: string | null | undefined, + digits: string = BASE_62_DIGITS +): string { + if (a != null) { + validateOrderKey(a, digits) + } + if (b != null) { + validateOrderKey(b, digits) + } + if (a != null && b != null && a >= b) { + throw new Error(`${a} >= ${b}`) + } + if (a == null) { + if (b == null) { + return `a${digits[0]}` + } + const ib = getIntegerPart(b) + const fb = b.slice(ib.length) + if (ib === `A${digits[0].repeat(26)}`) { + return ib + midpoint('', fb, digits) + } + if (ib < b) { + return ib + } + const res = decrementInteger(ib, digits) + if (res == null) { + throw new Error('cannot decrement any more') + } + return res + } + + if (b == null) { + const ia = getIntegerPart(a) + const fa = a.slice(ia.length) + const i = incrementInteger(ia, digits) + return i == null ? ia + midpoint(fa, null, digits) : i + } + + const ia = getIntegerPart(a) + const fa = a.slice(ia.length) + const ib = getIntegerPart(b) + const fb = b.slice(ib.length) + if (ia === ib) { + return ia + midpoint(fa, fb, digits) + } + const i = incrementInteger(ia, digits) + if (i == null) { + throw new Error('cannot increment any more') + } + if (i < b) { + return i + } + return ia + midpoint(fa, null, digits) +} + +/** + * Returns `n` distinct keys in sorted order, strictly between `a` and `b` (same + * open-end semantics as {@link generateKeyBetween}). When both ends are null, + * returns a contiguous run of "integer" keys. + */ +export function generateNKeysBetween( + a: string | null | undefined, + b: string | null | undefined, + n: number, + digits: string = BASE_62_DIGITS +): string[] { + if (n === 0) { + return [] + } + if (n === 1) { + return [generateKeyBetween(a, b, digits)] + } + if (b == null) { + let c = generateKeyBetween(a, b, digits) + const result = [c] + for (let i = 0; i < n - 1; i++) { + c = generateKeyBetween(c, b, digits) + result.push(c) + } + return result + } + if (a == null) { + let c = generateKeyBetween(a, b, digits) + const result = [c] + for (let i = 0; i < n - 1; i++) { + c = generateKeyBetween(a, c, digits) + result.push(c) + } + result.reverse() + return result + } + const mid = Math.floor(n / 2) + const c = generateKeyBetween(a, b, digits) + return [ + ...generateNKeysBetween(a, c, mid, digits), + c, + ...generateNKeysBetween(c, b, n - mid - 1, digits), + ] +} diff --git a/apps/sim/lib/table/order-key.ts b/apps/sim/lib/table/order-key.ts index 49c6385707..2f701badf9 100644 --- a/apps/sim/lib/table/order-key.ts +++ b/apps/sim/lib/table/order-key.ts @@ -5,12 +5,16 @@ * between two rows mints a key strictly between their keys, so no other row's * key changes — insert and delete become O(1) (no position reshift / recompact). * - * Thin wrapper over `fractional-indexing` (Figma/rocicorp algorithm) so the - * implementation is swappable. Keys never run out (variable-length strings); - * the only cost is gradual length growth under repeated same-spot inserts. + * Thin wrapper over the in-house fractional-indexing port (Figma/rocicorp + * algorithm) so the implementation is swappable. Keys never run out + * (variable-length strings); the only cost is gradual length growth under + * repeated same-spot inserts. */ -import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing' +import { + generateKeyBetween, + generateNKeysBetween, +} from '@/lib/fractional-indexing/fractional-indexing' /** * Returns a key that sorts strictly between `a` and `b`. Pass `null` for an open diff --git a/apps/sim/package.json b/apps/sim/package.json index c9df40e689..dc6efdb181 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -132,7 +132,6 @@ "es-toolkit": "1.45.1", "ffmpeg-static": "5.3.0", "fluent-ffmpeg": "2.1.3", - "fractional-indexing": "3.2.0", "framer-motion": "^12.5.0", "free-email-domains": "1.2.25", "google-auth-library": "10.5.0", @@ -233,6 +232,7 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^4.1.0", "autoprefixer": "10.4.21", + "fractional-indexing": "3.2.0", "jsdom": "^26.0.0", "postcss": "^8", "react-email": "4.3.2", diff --git a/bun.lock b/bun.lock index 2a293215c7..c23a5e5fe2 100644 --- a/bun.lock +++ b/bun.lock @@ -186,7 +186,6 @@ "es-toolkit": "1.45.1", "ffmpeg-static": "5.3.0", "fluent-ffmpeg": "2.1.3", - "fractional-indexing": "3.2.0", "framer-motion": "^12.5.0", "free-email-domains": "1.2.25", "google-auth-library": "10.5.0", @@ -287,6 +286,7 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^4.1.0", "autoprefixer": "10.4.21", + "fractional-indexing": "3.2.0", "jsdom": "^26.0.0", "postcss": "^8", "react-email": "4.3.2", From c40199c1bec78eeb49b69c5246c1ff1189a3f7ac Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 6 Jun 2026 17:03:47 -0700 Subject: [PATCH 2/2] chore(tables): fully remove fractional-indexing dependency and differential test --- .../fractional-indexing.test.ts | 203 ------------------ apps/sim/package.json | 1 - bun.lock | 3 - 3 files changed, 207 deletions(-) delete mode 100644 apps/sim/lib/fractional-indexing/fractional-indexing.test.ts diff --git a/apps/sim/lib/fractional-indexing/fractional-indexing.test.ts b/apps/sim/lib/fractional-indexing/fractional-indexing.test.ts deleted file mode 100644 index 5a36957e7b..0000000000 --- a/apps/sim/lib/fractional-indexing/fractional-indexing.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * @vitest-environment node - * - * Differential test: runs the in-house port side by side with the upstream - * `fractional-indexing` package over exhaustive + randomized inputs and asserts - * byte-identical output. The package is kept as a devDependency solely as this - * oracle. Delete this file (and the dep) once we no longer want the comparison. - */ -import { - generateKeyBetween as oracleKeyBetween, - generateNKeysBetween as oracleNKeysBetween, -} from 'fractional-indexing' -import { describe, expect, it } from 'vitest' -import { - generateKeyBetween, - generateNKeysBetween, -} from '@/lib/fractional-indexing/fractional-indexing' - -/** Deterministic LCG (Numerical Recipes constants) — no test-only dependency. */ -function makeRng(seed: number): () => number { - let state = seed >>> 0 - return () => { - state = (Math.imul(state, 1664525) + 1013904223) >>> 0 - return state / 0x100000000 - } -} - -function randInt(rng: () => number, maxExclusive: number): number { - return Math.floor(rng() * maxExclusive) -} - -/** Length of a key's integer part from its head char (a..z → 2..27, A..Z → 27..2). */ -function integerPartLength(head: string): number { - if (head >= 'a' && head <= 'z') return head.charCodeAt(0) - 'a'.charCodeAt(0) + 2 - if (head >= 'A' && head <= 'Z') return 'Z'.charCodeAt(0) - head.charCodeAt(0) + 2 - throw new Error(`unexpected head: ${head}`) -} - -/** Compare both impls for `(a, b)`: same return value, or both throw. */ -function expectKeyParity(a: string | null, b: string | null): string | null { - let mine: string | undefined - let mineThrew = false - try { - mine = generateKeyBetween(a, b) - } catch { - mineThrew = true - } - - let theirs: string | undefined - let theirsThrew = false - try { - theirs = oracleKeyBetween(a, b) - } catch { - theirsThrew = true - } - - expect(mineThrew).toBe(theirsThrew) - if (!mineThrew) { - expect(mine).toBe(theirs) - return mine as string - } - return null -} - -describe('fractional-indexing in-house port ≡ upstream', () => { - it('matches known anchor values from the algorithm', () => { - expect(generateKeyBetween(null, null)).toBe('a0') - expect(generateKeyBetween('a0', null)).toBe('a1') - expect(generateKeyBetween(null, 'a0')).toBe('Zz') - expect(generateKeyBetween('a0', 'a1')).toBe('a0V') - // All match the oracle too. - expect(generateKeyBetween(null, null)).toBe(oracleKeyBetween(null, null)) - expect(generateKeyBetween('a0', 'a1')).toBe(oracleKeyBetween('a0', 'a1')) - }) - - it('matches over exhaustive ordered pairs from a fixed key pool', () => { - // Build a sorted pool by chaining appends, then test every ordered pair - // plus both open ends. - const pool: string[] = [] - let last: string | null = null - for (let i = 0; i < 40; i++) { - last = generateKeyBetween(last, null) - pool.push(last) - } - const ends: Array = [null, ...pool] - for (const a of ends) { - for (const b of ends) { - // Only feed ordered, distinct bounds to the "happy path"; the parity - // helper also asserts both throw together for the invalid ones. - expectKeyParity(a, b) - } - } - }) - - it('matches while building a list via random-position inserts', () => { - for (const seed of [1, 7, 42, 1337, 99999]) { - const rng = makeRng(seed) - const keys: string[] = [] - for (let step = 0; step < 400; step++) { - const pos = randInt(rng, keys.length + 1) - const a = pos === 0 ? null : keys[pos - 1] - const b = pos === keys.length ? null : keys[pos] - const key = expectKeyParity(a, b) - expect(key).not.toBeNull() - keys.splice(pos, 0, key as string) - } - // List stayed strictly sorted throughout. - for (let i = 1; i < keys.length; i++) { - expect(keys[i - 1] < keys[i]).toBe(true) - } - } - }) - - it('matches generateNKeysBetween across open/closed ranges and counts', () => { - const rng = makeRng(2024) - for (let trial = 0; trial < 200; trial++) { - // A random ordered (a, b) window inside a freshly built run. - const run = generateNKeysBetween(null, null, 12) - const i = randInt(rng, run.length) - const j = randInt(rng, run.length) - const lo = Math.min(i, j) - const hi = Math.max(i, j) - const n = randInt(rng, 8) - - const a = run[lo] - const b = lo === hi ? null : run[hi] - expect(generateNKeysBetween(a, b, n)).toEqual(oracleNKeysBetween(a, b, n)) - } - // Edge counts and open ends. - expect(generateNKeysBetween(null, null, 0)).toEqual(oracleNKeysBetween(null, null, 0)) - expect(generateNKeysBetween(null, null, 1)).toEqual(oracleNKeysBetween(null, null, 1)) - expect(generateNKeysBetween(null, null, 50)).toEqual(oracleNKeysBetween(null, null, 50)) - expect(generateNKeysBetween('a0', null, 25)).toEqual(oracleNKeysBetween('a0', null, 25)) - expect(generateNKeysBetween(null, 'a0', 25)).toEqual(oracleNKeysBetween(null, 'a0', 25)) - }) - - it('matches across integer-length rollover on long append/prepend runs', () => { - // A long append run forces incrementInteger to roll the integer part - // through multiple heads and lengths (a→…→z→null path); a long prepend run - // exercises decrementInteger symmetrically. The random test above stays in - // head 'a'/'Z' length-2, so these cover the branchy carry/borrow code. - const lengths = new Set() - let appendKey: string | null = null - for (let i = 0; i < 5000; i++) { - const mine = generateKeyBetween(appendKey, null) - expect(mine).toBe(oracleKeyBetween(appendKey, null)) - appendKey = mine - lengths.add(integerPartLength(mine[0])) - } - let prependKey: string | null = null - for (let i = 0; i < 5000; i++) { - const mine = generateKeyBetween(null, prependKey) - expect(mine).toBe(oracleKeyBetween(null, prependKey)) - prependKey = mine - lengths.add(integerPartLength(mine[0])) - } - // Confirm we actually crossed integer-length boundaries (not just length 2). - expect([...lengths].some((l) => l > 2)).toBe(true) - }) - - it('matches deep same-spot inserts (long fractions)', () => { - // Repeatedly inserting between the same two neighbors grows the fraction - // without bound — exercises the recursive midpoint + common-prefix path. - let lo = generateKeyBetween(null, null) - let hi = generateKeyBetween(lo, null) - let maxLen = 0 - for (let i = 0; i < 2000; i++) { - const mine = generateKeyBetween(lo, hi) - expect(mine).toBe(oracleKeyBetween(lo, hi)) - // Alternate which side we keep so the fraction deepens on both ends. - if (i % 2 === 0) hi = mine - else lo = mine - maxLen = Math.max(maxLen, mine.length) - } - expect(maxLen).toBeGreaterThan(10) - }) - - it('throws on invalid keys and inverted bounds in both impls', () => { - const bad: Array<[string | null, string | null]> = [ - ['a1', 'a0'], // inverted - ['a0', 'a0'], // equal - ['', null], // empty key - ['a00', null], // trailing zero in fraction - ['1', null], // invalid head - ] - for (const [a, b] of bad) { - let mineThrew = false - let theirsThrew = false - try { - generateKeyBetween(a, b) - } catch { - mineThrew = true - } - try { - oracleKeyBetween(a, b) - } catch { - theirsThrew = true - } - expect(mineThrew).toBe(true) - expect(mineThrew).toBe(theirsThrew) - } - }) -}) diff --git a/apps/sim/package.json b/apps/sim/package.json index dc6efdb181..29c196333e 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -232,7 +232,6 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^4.1.0", "autoprefixer": "10.4.21", - "fractional-indexing": "3.2.0", "jsdom": "^26.0.0", "postcss": "^8", "react-email": "4.3.2", diff --git a/bun.lock b/bun.lock index c23a5e5fe2..eb65c533e9 100644 --- a/bun.lock +++ b/bun.lock @@ -286,7 +286,6 @@ "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^4.1.0", "autoprefixer": "10.4.21", - "fractional-indexing": "3.2.0", "jsdom": "^26.0.0", "postcss": "^8", "react-email": "4.3.2", @@ -2500,8 +2499,6 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "fractional-indexing": ["fractional-indexing@3.2.0", "", {}, "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ=="], - "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], "free-email-domains": ["free-email-domains@1.2.25", "", {}, "sha512-Uf2rJUjo/agIgQzt6od9XcHrR6rfIMD6TwsNVSJVJCHzjPWWsqjCb+EaQ2VVVY9M55+JB3V0k6ru5sHTGx/ZfA=="],