document structure

The root CRDT document structure, per-sheet structure, ID system, metadata, and initialization defaults.

Root-Level Shared Types

A Calcillo document is a Y.Doc with only 3 named shared types:

Key Yjs Type Description
sheetOrder Y.Array<SheetId> Sheet tab order (presentation sequence)
sheets Y.Map<YSheetStructure> All sheets keyed by SheetId
meta Y.Map Document metadata
Minimal root structure

Unlike Prezillo’s 12 or Ideallo’s 5 top-level shared types, Calcillo uses only 3. The complexity lives inside each sheet’s self-contained structure, which keeps the root document clean and enables efficient per-sheet synchronization.

Accessing the Document

import * as Y from 'yjs'

const yDoc = new Y.Doc()

// Access root shared types
const sheetOrder = yDoc.getArray('sheetOrder')  // SheetId ordering
const sheets     = yDoc.getMap('sheets')         // YSheetStructure entries
const meta       = yDoc.getMap('meta')           // Metadata

Per-Sheet Structure (YSheetStructure)

Each sheet is a self-contained Y.Map with 14 named sub-types:

Sub-type Yjs Type Description
name Y.Text Sheet name (collaborative text editing)
rowOrder Y.Array<RowId> Stable row ordering
colOrder Y.Array<ColId> Stable column ordering
rows Y.Map<Y.Map<Cell>> Cell data: rows[rowId][colId] = Cell
merges Y.Map<MergeInfo> Merged cell ranges, keyed by "${startRow}_${startCol}"
borders Y.Map<BorderInfo> Per-cell border info, keyed by "${rowId}_${colId}"
hyperlinks Y.Map<HyperlinkInfo> Per-cell hyperlinks, keyed by "${rowId}_${colId}"
validations Y.Map<ValidationRule> Data validation rules, keyed by unique validation ID
conditionalFormats Y.Array<ConditionalFormat> Ordered conditional formatting rules
hiddenRows Y.Map<boolean> rowId → true for hidden rows
hiddenCols Y.Map<boolean> colId → true for hidden columns
rowHeights Y.Map<number> Custom row heights in pixels (absent = default)
colWidths Y.Map<number> Custom column widths in pixels (absent = default)
frozen Y.Map<string|number> Freeze pane configuration

Accessing Sheet Sub-Types

// Get a sheet by ID
const sheet = sheets.get(sheetId) as Y.Map<unknown>

// Access sub-types
const name     = sheet.get('name') as Y.Text
const rowOrder = sheet.get('rowOrder') as Y.Array<string>
const colOrder = sheet.get('colOrder') as Y.Array<string>
const rows     = sheet.get('rows') as Y.Map<Y.Map<Cell>>
const merges   = sheet.get('merges') as Y.Map<MergeInfo>
const borders  = sheet.get('borders') as Y.Map<BorderInfo>

// Access a specific cell
const rowMap = rows.get(rowId) as Y.Map<Cell>
const cell = rowMap?.get(colId) as Cell | undefined
Per-sheet encapsulation

Each sheet manages its own rows, columns, merges, borders, and all other features independently. There are no cross-sheet references within the CRDT – formulas referencing other sheets are stored as plain strings and resolved by the calculation engine at runtime.

ID System

Calcillo uses 3 branded types with variable-length IDs, all encoded as base64url characters:

Branded Type Length Entropy Used In
SheetId 12 chars 72 bits sheets map keys, sheetOrder entries
RowId 9 chars 54 bits rowOrder entries, rows map keys, composite keys
ColId 5 chars 30 bits colOrder entries, inner rows map keys, composite keys

All IDs are generated client-side using crypto.getRandomValues() and encoded as base64url (A-Z, a-z, 0-9, -, _).

Why variable-length IDs?

Sheets use standard 72-bit IDs like other Cloudillo apps. Row and column IDs use shorter lengths because spreadsheets contain many more rows and columns than typical document entities, and the shorter IDs reduce CRDT storage overhead. The entropy levels are chosen to keep collision probability negligible for practical spreadsheet sizes.

Composite Key Convention

Several per-sheet maps use composite keys formed by joining two IDs with an underscore:

Map Key Format Example
merges "${startRowId}_${startColId}" "aB3x_Qm7k_Xk2nR"
borders "${rowId}_${colId}" "aB3x_Qm7k_Xk2nR"
hyperlinks "${rowId}_${colId}" "aB3x_Qm7k_Xk2nR"

This convention provides a deterministic, unique key for cell-position-based features without requiring additional ID generation.

Metadata

The meta map stores document-level settings as individual key-value entries:

Key Type Default Description
initialized boolean true Set during initialization, prevents re-initialization
name string "Untitled Spreadsheet" Document name

The meta map is compatible with Cloudillo’s cloudillo.init() initialization system – the initialized flag is checked before setting up default content.

Document Initialization

When a new empty document is created, the following defaults are established in a single Yjs transaction:

yDoc.transact(() => {
    // 1. Mark as initialized
    meta.set('initialized', true)
    meta.set('name', 'Untitled Spreadsheet')

    // 2. Create default sheet
    const sheetId = generateSheetId()  // 12 base64url chars
    sheetOrder.push([sheetId])

    // 3. Set up sheet structure
    const sheet = new Y.Map()
    sheets.set(sheetId, sheet)

    const name = new Y.Text()
    name.insert(0, 'Sheet 1')
    sheet.set('name', name)

    // 4. Generate 26 columns (A-Z)
    const colOrder = new Y.Array()
    for (let i = 0; i < 26; i++) {
        colOrder.push([generateColId()])  // 5 base64url chars each
    }
    sheet.set('colOrder', colOrder)

    // 5. Generate 100 rows
    const rowOrder = new Y.Array()
    for (let i = 0; i < 100; i++) {
        rowOrder.push([generateRowId()])  // 9 base64url chars each
    }
    sheet.set('rowOrder', rowOrder)

    // 6. Initialize empty sub-type maps
    sheet.set('rows', new Y.Map())
    sheet.set('merges', new Y.Map())
    sheet.set('borders', new Y.Map())
    sheet.set('hyperlinks', new Y.Map())
    sheet.set('validations', new Y.Map())
    sheet.set('conditionalFormats', new Y.Array())
    sheet.set('hiddenRows', new Y.Map())
    sheet.set('hiddenCols', new Y.Map())
    sheet.set('rowHeights', new Y.Map())
    sheet.set('colWidths', new Y.Map())
    sheet.set('frozen', new Y.Map())
})

The default sheet starts with 26 columns (corresponding to A-Z) and 100 rows. All cell data maps start empty – cells are created on first edit (sparse storage).