toeverything/AFFiNE · error · Error

a should be smaller than b

Error message

a should be smaller than b

What it means

generateFractionalIndexingKeyBetween(a, b) builds an ordering key strictly between a and b (used for drag-to-reorder). It asserts its own contract: when both neighbors are non-null, a must compare strictly less than b as strings. Passing an inverted or equal pair violates the ordering invariant and throws immediately.

Source

Thrown at packages/common/infra/src/utils/fractional-indexing.ts:33

export function generateFractionalIndexingKeyBetween(
  a: string | null,
  b: string | null
) {
  const randomSize = 32;
  function postfix(length: number = randomSize) {
    const chars =
      '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
    const values = new Uint8Array(length);
    crypto.getRandomValues(values);
    let result = '';
    for (let i = 0; i < length; i++) {
      result += chars.charAt(values[i] % chars.length);
    }
    return result;
  }

  if (a !== null && b !== null && a >= b) {
    throw new Error('a should be smaller than b');
  }

  // get the subkey in full key
  // e.g.
  // a0xxxx -> a
  // a0x0xxxx -> a0x
  function subkey(key: string | null) {
    if (key === null) {
      return null;
    }
    if (key.length <= randomSize + 1) {
      // no subkey
      return key;
    }
    const splitAt = key.substring(0, key.length - randomSize - 1);
    return splitAt;
  }

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Check the call site: the first argument must be the key of the row ABOVE the drop position (smaller), the second the row BELOW (larger). For moveUp vs moveDown make sure the row pair is not inverted.
  2. Handle boundary moves: when moving to the very top pass (null, firstKey); to the very bottom pass (lastKey, null) — null is allowed and avoids equality/inversion traps.
  3. If a === b can occur (duplicate keys), de-duplicate or regenerate keys before computing the middle key.
  4. Unit-test reorder with adjacent rows, equal keys, and both directions.

Example fix

// before (moveUp branch inverted the pair)
const key = generateFractionalIndexingKeyBetween(below.key, above.key);

// after
const key =
  above.key === null
    ? generateFractionalIndexingKeyBetween(null, below.key) // move to top
    : generateFractionalIndexingKeyBetween(above.key, below.key);
Defensive patterns

Strategy: validation

Validate before calling

function safeMiddleKey(a: string | null, b: string | null): string {
  if (a !== null && b !== null && a >= b) {
    // inputs inverted or equal — pick boundary semantics instead of throwing
    return a === b ? generateFractionalIndexingKeyBetween(a, null) : generateFractionalIndexingKeyBetween(b, a);
  }
  return generateFractionalIndexingKeyBetween(a, b);
}

Type guard

const isOrdered = (a: string | null, b: string | null): boolean => a === null || b === null || a < b;
// assert isOrdered(aboveKey, belowKey) before generating

Try / catch

try { return generateFractionalIndexingKeyBetween(prev, next); } catch (e) { if (e instanceof Error && e.message === 'a should be smaller than b') { /* swap or null-out the inverted bound, then retry */ } throw e; }

Prevention

When it happens

Trigger: Calling generateFractionalIndexingKeyBetween(highKey, lowKey) after swapping the neighbor arguments; passing the same key for both a and b (a >= b includes equality); reorder logic that reads the wrong 'before/after' indices, e.g. when moving a block upward and fetching rows in descending order.

Common situations: Drag-and-drop reorder handlers where the moveUp branch accidentally passes (below, above) instead of (above, below); duplicate ordering keys in existing data making a === b; passing full keys from a different generator (keys must come from this function, per its doc comment).

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/400b73591cae2fae. Report an issue: GitHub.