Accepted (revised from float-based ordering)
Blocks within a page (or within a parent block) need a sort order. Reordering blocks (drag-and-drop, indent/outdent) must be efficient.
The original decision was float-based fractional ordering with lazy rebalancing. Architectural review identified that:
- Float precision gives ~52 halvings before exhaustion — sufficient for most use, but the rebalancing algorithm was entirely unspecified (trigger, mechanism, atomicity, UI flickering).
- String-based fractional indexing (as used by Figma) eliminates the rebalancing problem entirely at the cost of variable-length keys.
- The
rocicorp/fractional-indexinglibrary is a production-ready implementation.
String-based fractional indexing using rocicorp/fractional-indexing.
- Each block has an
order: Stringcolumn (stored in CozoDB as String) - Keys are base-62 encoded strings that sort lexicographically
- To insert between two blocks,
generateKeyBetween(a, b)produces a string that sorts between them - Keys can be generated for any position: start, end, or between any two existing keys
- No precision limit — keys grow by ~1 character per insertion in the same gap
import { generateKeyBetween } from 'fractional-indexing';
// Initial blocks
const a = generateKeyBetween(null, null); // 'a0'
const b = generateKeyBetween(a, null); // 'a1'
const c = generateKeyBetween(b, null); // 'a2'
// Insert between a and b
const ab = generateKeyBetween(a, b); // 'a0V'Positive:
- No rebalancing needed — ever
- Insert between any two blocks is O(1): one write, no sibling updates
- Lexicographic ordering works with CozoDB's key comparison
rocicorp/fractional-indexingis small (~2KB), well-tested, zero dependencies- Compatible with future CRDT sync (keys are globally unique when combined with a site ID)
Negative:
- Keys grow with successive insertions in the same gap (~1 char per insertion in base-62)
- In worst case (10,000 insertions at same position), keys reach ~170 characters — negligible storage cost
- Key comparison is O(key_length) instead of O(1) for floats — negligible for typical key lengths
- Human editing patterns produce short keys (typically 2-6 characters)
- Float-based with rebalancing: Works but requires defining trigger threshold, rebalancing algorithm, and handling atomicity/UI-flickering — significant engineering burden for a solved problem.
- Integer with gaps: Frequent rebalancing needed.
- LSEQ: Discredited — interleaving problem when multiple users insert concurrently.
- RGA: Designed for character-level collaborative editing, not block-level reordering. Requires tombstones. Overwheight for this use case.
blocks.order and block_history.order are String type (not Float).