Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Cells - Lona Docs Log in

Cells

Cells hold date-indexed data for each row. Each cell has a date key and a JSON value.

Reading cells

Bulk fetch via sheet handle

sheet.cells.fetch(range) loads all rows' cells in one request and caches them. Read from cache with sheet.cells.getCached() or sheet.cells.getRange():

import { NaiveDate } from "@tento-chrono";

const range = new NaiveDate.Range(
  NaiveDate.fromYmd1(2026, 1, 1),
  NaiveDate.fromYmd1(2026, 3, 31),
);

// Bulk fetch — populates cache for all rows
await sheet.cells.fetch(range);

// Read from cache (sync, no network)
const weightRow = sheet.row({ label: "Weight" });
const cells = sheet.cells.getRange(weightRow.id);
for (const cell of cells) {
  console.log(cell.date, cell.data);
}

// Single cell lookup
const cell = sheet.cells.getCached(weightRow.id, "2026-01-15");

Per-row fetch

row.cells(range) fetches cells for a single row:

const row = sheet.rows.list.find(r => r.label === "Weight");
const start = NaiveDate.fromYmd1(2026, 1, 1);
const end = NaiveDate.fromYmd1(2026, 3, 31);

const cells = await row.cells(new NaiveDate.Range(start, end));
for (const cell of cells) {
  console.log(cell.date, cell.data);
}

Writing cells

await row.setCell("2026-01-15", 74.2);

Only writable rows (not formula or builtin types) support setCell().

Cross-user row access

Open any row by key with client.rows.open(), including rows owned by other users. Cross-user access requires a per-row ACL grant from an administrator.

// Access another user's Garmin sleep data
const row = await client.rows.open("[alice@example.com]:~garmin:sleep");
if (row) {
  const cells = await row.cells(range);
  for (const cell of cells) {
    console.log(cell.date, cell.data);
  }
}

Row key formats

FormatExampleDescription
:lookupKey:~garmin:sleepBuiltin row (current user)
[user@email]:lookupKey[alice@example.com]:~garmin:sleepCross-user row
r_<hex>r_a1b2c3d4...Row by UUID

Cross-user keys return null when the authenticated user has no ACL grant for the target row.

Subscribing to cell changes

sheet.cells.onChange() notifies when cell data is updated (after a fetch or write):

const unsubscribe = sheet.cells.onChange((staleRowIds) => {
  for (const rowId of staleRowIds) {
    rebindRow(rowId);
  }
});

// Later:
unsubscribe();

Progressive loading with row.cell(date)

Some rows (e.g., calendar, weather) trigger a server-side sync when fetched. row.cell(date) returns a CellHandle with two callbacks:

const cell = row.cell("2026-01-15");

// Show a shimmer while data loads
cell.onLoading(() => {
  showShimmer();
});

// Render data when it arrives (may re-fire on sync updates)
cell.onData((data) => {
  render(data);
});

// Clean up when done
cell.dispose();

For plugins that only want final data (after all sync completes), use onDataFull instead of onData:

cell.onDataFull((data) => {
  renderFinal(data);
});

CellHandle is also awaitable for simple use cases — it resolves when sync completes:

const data = await row.cell("2026-01-15");
console.log(data); // CellEntry | null

Progressive loading over a range: RangeCellHandle

RangeCellHandle is the range-oriented cousin of CellHandle. Use it when a plugin needs every cell in a date range with the same onLoading → onData × N → onDataFull progression:

import { RangeCellHandle } from "@tento-lona/sheets";
import { NaiveDate } from "@tento-chrono";

const range = new NaiveDate.Range(
  NaiveDate.fromYmd1(2026, 1, 1),
  NaiveDate.fromYmd1(2026, 3, 31),
);

const handle = new RangeCellHandle({
  readCache: () => sheet.cells.getRangeCached(row.id, range),
  fetch: () => row.cells(range),
});

handle
  .onLoading(() => showShimmer())
  .onData((entries) => renderPartial(entries))
  .onDataFull((entries) => renderFinal(entries));

// Or await for simple cases
const all = await handle;

// Cancel if the component unmounts mid-fetch
handle.dispose();

Cache hits resolve synchronously (via queueMicrotask), so plugins don't flash a shimmer when data is already loaded — canonical :~-keyed rows whose cells are in Sheet.canonicalCells paint immediately.

Canonical routing (:~ rows)

Rows whose lookupKey begins with :~:~weather:{locationId}, :~google:{authId}:calendar:{calendarId}, etc. — resolve cells from the canonical cell store instead of per-sheet sourceCellData. This means:

  • Multiple sheets sharing a Client see the same cells without duplicate fetches (SheetsAccessor owns the shared canonicalCells store).
  • Reads are routed through CanonicalBackend, which coordinates provider sync (weather, Google, Garmin, Whoop, …) and caches the normalized result.
  • Writes for canonical rows are rejected — these rows are read-only from the client's perspective; mutations happen via provider sync.

Consumers don't need to care which path runs. row.cells(range) and row.cell(date) work the same way on user and canonical rows; the routing is transparent.

Encoding decoders

Wire cells for calendar events, tasks, weather, and scheduling slots come in as versioned JSON (event_v1, task_v1, weather_v1, slot_v1). The SDK decodes them into canonical objects before facades see them:

import {
  EVENT_V1, TASK_V1, WEATHER_V1, SLOT_V1,
} from "@tento-lona/sheets";

You rarely call these directly — they're registered on the EncodingRegistry at client construction and invoked by NormalizingBackend on read. But if you need a one-off decode of a wire cell (tests, introspection), each decoder is a pure function.

CellEntry

PropertyTypeDescription
datestringCell date ("YYYY-MM-DD")
endDatestring | undefinedEnd date (range cells)
dataunknownCell value

SyncState

When a row's data comes from an external provider (calendar, weather), the server syncs it asynchronously. SyncState tracks this progress:

PropertyTypeDescription
statusstring"pending", "running", "complete", or "failed"
keysMap<string, SyncKeyStatus>Per-key sync status

Each SyncKeyStatus has a status and optional stage field for multi-stage providers (e.g., weather uses viewport, near, full).

Sync key format

Sync keys encode the provider, resource, date range, and stage:

sync:shared::~weather:2550002:20260225_20260514:@viewport

The @stage suffix identifies which sync stage a key belongs to. Legacy keys use _stage1 format instead.

Sync poll lifecycle

When fetchCells returns sync keys for a row:

  1. Terminal dedup — keys whose poll already completed are skipped
  2. Active dedup — keys already being polled are not restarted
  3. New keys start polling the server for status updates
  4. Each poll transitions through pendingrunningcomplete/failed
  5. When all keys reach a terminal state, the row's sync state clears

The poll uses adaptive timing: fast polling (500ms) for the first 5 seconds, then exponential backoff up to 5s intervals.

SyncPollEntry

For debug observability, the backend exposes syncPollState as a LiveData.Readable<ReadonlyMap<string, SyncPollEntry>>:

PropertyTypeDescription
syncKeystringThe sync key being polled
statusSyncStatusCurrent poll status
startDateTime<Utc>When polling started
endDateTime<Utc> | nullWhen polling ended (null if active)
stagestring | nullSync stage ("viewport", "near", "full")

Time scales

Rows operate on time scales that determine cell granularity:

ScaleDescription
"day"One cell per day
"week"One cell per week
"month"One cell per month
"year"One cell per year

A row's time scale determines how its data maps to the sheet's column grid:

console.log(row.timeScale); // "day", "week", "month", "year", or "custom"

When a row's time scale differs from the sheet's, cells may stack (multiple values in one column) or span (one value across columns).

See also