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 | undefinedPer-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).