Thank you for being patient! We're working hard on resolving the issue
Cells hold date-indexed data for each row. Each cell has a date key and a JSON value.
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");
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);
}
await row.setCell("2026-01-15", 74.2);
Only writable rows (not formula or builtin types) support setCell().
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);
}
}
| Format | Example | Description |
|---|---|---|
:lookupKey | :~garmin:sleep | Builtin row (current user) |
[user@email]:lookupKey | [alice@example.com]:~garmin:sleep | Cross-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.
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();
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
RangeCellHandleRangeCellHandle 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.
:~ 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:
Client see the same cells without duplicate
fetches (SheetsAccessor owns the shared canonicalCells store).CanonicalBackend, which coordinates provider
sync (weather, Google, Garmin, Whoop, …) and caches the normalized
result.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.
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.
| Property | Type | Description |
|---|---|---|
date | string | Cell date ("YYYY-MM-DD") |
endDate | string | undefined | End date (range cells) |
data | unknown | Cell value |
When a row's data comes from an external provider (calendar, weather), the
server syncs it asynchronously. SyncState tracks this progress:
| Property | Type | Description |
|---|---|---|
status | string | "pending", "running", "complete", or "failed" |
keys | Map<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 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.
When fetchCells returns sync keys for a row:
pending → running → complete/failedThe poll uses adaptive timing: fast polling (500ms) for the first 5 seconds, then exponential backoff up to 5s intervals.
For debug observability, the backend exposes syncPollState as a
LiveData.Readable<ReadonlyMap<string, SyncPollEntry>>:
| Property | Type | Description |
|---|---|---|
syncKey | string | The sync key being polled |
status | SyncStatus | Current poll status |
start | DateTime<Utc> | When polling started |
end | DateTime<Utc> | null | When polling ended (null if active) |
stage | string | null | Sync stage ("viewport", "near", "full") |
Rows operate on time scales that determine cell granularity:
| Scale | Description |
|---|---|
"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).
:~)