Thank you for being patient! We're working hard on resolving the issue
LonaSdk.Client is the entry point for all SDK operations. It manages
authentication, sheet handles, and provides access to sheet and row data.
import { LonaSdk, SheetPlatform } from "@tento-lona";
const client = new LonaSdk.Client({
accessToken: "lona_pat_...", // personal access token
storage: localStorage, // preferences persistence
platform: SheetPlatform.TEST, // UI invalidation callbacks
});
storage and platform are required. For headless/test environments use
SheetPlatform.TEST and an in-memory storage:
const client = new LonaSdk.Client({
accessToken: "lona_pat_...",
storage: { getItem: () => null, setItem: () => {}, removeItem: () => {} },
platform: SheetPlatform.TEST,
});
client.open(key)Opens a sheet by its key and returns a SheetHandle — a wrapper around the
sheet's rows, tree, and metadata.
const sheet = await client.open("your-sheet-id");
client.open({ ephemeral })Create a local sheet without any server call. Useful for previews or
offline-first workflows. The ephemeral data must conform to
Network.UserSheetWithRows:
import { DateTime, Utc } from "@tento-chrono";
const now = DateTime.utc.now();
const sheet = await client.open({
ephemeral: {
id: "ephemeral-1",
title: "Draft Sheet",
creatorId: "local",
createdAt: now,
lastUpdatedAt: now,
role: "owner",
rows: [
{
id: "row-1",
type: "number",
label: "Weight",
ownerId: "local",
timeScale: "day",
position: { n: 1, d: 1 },
},
],
},
});
// Work with the sheet locally...
const weight = sheet.row({ label: "Weight" });
// Later, persist to the server:
await sheet.promote();
client.rows.open(key)Opens a row by its key without sheet context. Supports cross-user keys for rows shared via per-row ACL grants.
// Own row by lookup key
const row = await client.rows.open(":~garmin:sleep");
// Cross-user row (requires ACL grant)
const row = await client.rows.open("[alice@example.com]:~garmin:sleep");
if (row) {
const cells = await row.cells(range);
}
Returns null if the row doesn't exist or the user lacks access.
client.sheets.list()Returns all sheets accessible to the authenticated user.
const sheets = await client.sheets.list();
for (const sheet of sheets) {
console.log(`${sheet.id} — ${sheet.title}`);
}
client.sheets.create(attrs)Create a new sheet with optional initial rows.
const created = await client.sheets.create({
title: "My Sheet",
rows: [
{ type: "number", label: "Weight" },
{ type: "text", label: "Notes" },
],
});
client.sheets.delete(key)Delete a sheet by key.
await client.sheets.delete("your-sheet-id");
Returned by client.open(). Holds the sheet metadata and row tree.
| Property | Type | Description |
|---|---|---|
sheet.id | Uuid | Sheet UUID |
sheet.title | string | null | Sheet title |
sheet.rows | RowsAccessor | Row lookup and operations |
sheet.cells | CellAccessor | Cell fetch, cache, and subscriptions |
sheet.editable | boolean | Whether current user can edit |
sheet.row(query)Look up a row by ID, label, type, or lookup key. Returns a Row, or
null if not found.
const row = sheet.row({ label: "Weight" });
const row = sheet.row({ lookupKey: ":~calendar" });
const row = sheet.row({ type: "number" });
const row = sheet.row("row-id-string");
sheet.tree()The resolved row tree with parent/child relationships.
const tree = sheet.tree();
for (const node of tree.nodes) {
console.log(node.id, node.children.length);
}
sheet.dispose()Tear down all subscriptions and clear cached data.
sheet.dispose();
Accessed via sheet.cells. Manages per-row cell data with caching,
progressive loading, and change subscriptions.
sheet.cells.fetch(range)Bulk fetch cells for all rows in a date range. Populates the cache.
import { NaiveDate } from "@tento-chrono";
const start = NaiveDate.parse("2026-01-01");
const end = NaiveDate.parse("2026-03-31");
await sheet.cells.fetch(new NaiveDate.Range(start, end));
sheet.cells.get(rowId, date)Get a single cell (from cache, or fetches if not cached).
const cell = await sheet.cells.get(rowId, "2026-01-15");
if (cell) console.log(cell.data);
sheet.cells.getCached(rowId, date)Synchronous read from cache. Returns null if not cached.
const cell = sheet.cells.getCached(rowId, "2026-01-15");
sheet.cells.getRange(rowId)Get all cached cells for a row.
const cells = sheet.cells.getRange(rowId);
for (const cell of cells) {
console.log(cell.date, cell.data);
}
sheet.cells.set(rowId, date, data)Write a cell value. Optimistic local update + server sync.
await sheet.cells.set(rowId, "2026-01-15", 74.2);
sheet.cells.cell(rowId, date)Progressive cell handle for sync-aware loading. Returns a CellHandle
that tracks the loading lifecycle:
const cell = sheet.cells.cell(rowId, "2026-01-15");
cell.onLoading(() => showShimmer());
cell.onData((data) => render(data));
CellHandle is also awaitable:
const data = await sheet.cells.cell(rowId, "2026-01-15");
sheet.cells.onChange(cb)Subscribe to cell data changes. Returns an unsubscribe function.
const unsub = sheet.cells.onChange((staleRowIds) => {
for (const id of staleRowIds) rebindRow(id);
});
sheet.cells.isStale(rowId)Check if a row's cached data is stale.
sheet.cells.ensureFresh(rowId, range)Re-evaluate formulas or re-fetch if stale.
Accessed via sheet.rows. Provides the fluent API for listing, querying,
and adding rows.
sheet.rows.listAll rows in the sheet as Row[].
for (const row of sheet.rows.list) {
console.log(row.label, row.type);
}
sheet.rows.add(attrs)Add a new row to the sheet. Returns a Row.
const row = await sheet.rows.add({
type: "number",
label: "Weight",
});
sheet.rows.get(query)Find a row by query. Same query syntax as sheet.row().
const row = sheet.rows.get({ label: "Weight" });
SheetPlatform provides infrastructure and layout queries to the SDK.
It does not handle data change notifications — those are handled by
InvalidationSink (see below).
const client = new LonaSdk.Client({
accessToken: "lona_pat_...",
storage: localStorage,
platform: {
installDebounce(baseUrl, accessCode) { /* TypedApi backend */ },
getMaxHeight: () => window.innerHeight,
getMaxWidth: () => window.innerWidth,
},
});
| Callback | Required | Description |
|---|---|---|
installDebounce(baseUrl, accessCode) | yes | Install the TypedApi debounce backend |
getMaxHeight() | yes | Viewport height for layout calculations |
getMaxWidth() | no | Sidebar width calculation |
onFrozenRowOverflow(tried, max, maxHeight) | no | Frozen rows exceed viewport |
onFrozenRowFit() | no | Frozen rows fit again |
themeBackend | no | Theme CSS application |
For tests and headless environments, use the built-in stub:
import { SheetPlatform } from "@tento-lona";
SheetPlatform.TEST
// installDebounce + getMaxHeight: () => 800, getMaxWidth: () => 1820
UI feedback (confirmations, warnings, undo) is handled by
RowsAccessor.FeedbackDelegate, passed via ClientConfig.feedback:
const client = new LonaSdk.Client({
accessToken: "lona_pat_...",
storage: localStorage,
platform: SheetPlatform.TEST,
feedback: {
async confirmDelete(message) {
return window.confirm(message);
},
showUndo(message, undo) {
showToast(message, { action: undo });
},
warn(message, description) {
showToast(message, { description });
},
},
});
| Callback | Returns | Description |
|---|---|---|
confirmDelete(message) | Promise<boolean> | Confirm destructive action. Return false to cancel |
showUndo(message, undo) | void | Show undo notification with async restore callback |
warn(message, description?) | void | Show a warning |
Defaults to FeedbackDelegate.NOOP (auto-confirms deletes, ignores
warnings) for tests and headless environments.
The SDK uses a typed event system for change notifications. When a mutation
occurs, the SDK emits an Invalidation event through the InvalidationSink.
Consumers subscribe to these events to update their UI.
Pass a sink when creating the client:
const client = new LonaSdk.Client({
accessToken: "lona_pat_...",
storage: localStorage,
platform: SheetPlatform.TEST,
invalidation: mySink,
});
The sink interface:
interface InvalidationSink {
emit(event: Invalidation): void;
subscribe(cb: (event: Invalidation) => void): () => void;
}
InvalidationSink.NOOP silently drops all events (the default for tests).
Every invalidation event carries an Actor identifying its origin. This
enables components to suppress redundant re-renders from their own
optimistic updates.
import { Actor } from "@tento-lona";
const MY_ACTOR = new Actor("settings-panel");
// Pass actor when mutating
await row.update({ label: "New Name" }, MY_ACTOR);
// Filter by actor when consuming
sink.subscribe((event) => {
if (event.actor === MY_ACTOR) return; // I made this change — skip
rebind(event);
});
SDK stores have static .ACTOR fields (e.g., SheetRow.Client.ACTOR,
EventsStore.ACTOR, CalendarClient.ACTOR) used as defaults when no
caller actor is provided.
Every event extends Invalidation with an actor, a path string, and
an eq() method for deduplication.
Singleton events (no entity payload):
| Event | Path | Emitted by |
|---|---|---|
TimezoneChange | user.preferences.timezone | PreferencesClient |
ThemeChange | user.preferences.theme | ThemeStore |
CalendarPreferencesChanged | user.preferences.calendar | CalendarPreferences |
LayoutChanged | ui.layout | SheetRowTree, RowHeightPreferences |
WindowResize | ui.viewport | App layer |
FrozenRowsChanged | sheets.*.numFrozenRows | SheetsAccessor |
SheetStructureChanged | sheets.*.structure | SheetHandle, RowsAccessor |
SheetListChanged | sheets | SheetsAccessor |
CalendarVisibilityChange | calendars.visibility | CalendarClient |
NewRowInserted | sheets.*.rows | RowsAccessor, SheetHandle |
ViewportsInvalidated | calendars.events | EventsStore |
TasksChanged | tasks | App layer |
Scoped events (carry entity ID):
| Event | Constructor | Path |
|---|---|---|
RowDataChanged | (actor, rowId, field?) | rows.{rowId} or rows.{rowId}.{field} |
RowHeightChanged | (actor, rowId) | rows.{rowId}.heightPx |
RowLegendChanged | (actor, rowId) | rows.{rowId}.label |
SheetMetadataChanged | (actor, sheetId) | sheets.{sheetId} |
ColumnDataChanged | (actor, column) | rows.*.cells.{column} |
TimepointsChanged | (actor, range) | calendars.events.{range} |
Every event carries a hierarchical path that identifies what changed. Paths mirror the REST API structure:
user.preferences.timezone
user.preferences.theme
rows.{rowId}
rows.{rowId}.label
rows.{rowId}.heightPx
rows.{rowId}.attributes.{key}
sheets.{sheetId}
sheets.{sheetId}.structure
calendars.visibility
calendars.events
row.update() emits fine-grained events for each changed field:
await row.update({ label: "Weight", heightPx: 64 });
// Emits:
// RowLegendChanged(actor, rowId) path: rows.{id}.label
// RowHeightChanged(actor, rowId) path: rows.{id}.heightPx
Attribute updates emit per-key events:
await row.update({ attributes: { color: "blue", visible: true } });
// Emits:
// RowDataChanged(actor, rowId, "attributes.color")
// RowDataChanged(actor, rowId, "attributes.visible")
Direct subscription with instanceof matching:
const unsubscribe = sink.subscribe((event) => {
if (event instanceof RowLegendChanged) {
rebindLegend(event.rowId);
}
if (event instanceof RowDataChanged && event.field === "formula") {
recomputeFormula(event.rowId);
}
});
For complex UIs, use InvalidationSinkImpl to declare path-to-effect rules:
import { InvalidationSinkImpl } from "@tento-lona";
const sink = new InvalidationSinkImpl<MyEffect>()
.when("rows.*.heightPx", MyEffect.Layout)
.when("rows.*.label", MyEffect.LegendRow)
.when("rows.*", MyEffect.CellContentRow)
.when("user.preferences.timezone", MyEffect.CellContentAll)
.when("calendars.events", MyEffect.CellContentRange);
sink.onEffect((event) => {
for (const effect of sink.resolve(event)) {
applyEffect(effect, event);
}
});
* matches a single path segment (entity ID). Parent paths subsume
children — rows.* matches rows.{id}.label.
When you call row.update({ label: "New Name" }, myActor):
RowLegendChanged(myActor, rowId) through the sinkevent.actor === myActor)The public SDK mainly exposes two row identity concepts:
RowKey for a stable row referenceRowLocalId for in-memory tree and UI identityimport { RowKey } from "@tento-lona";
const key = RowKey.lookup("revenue");
const rowId = key.resolve();
Use RowKey at boundaries such as configuration, persistence, or named
lookups. Use RowLocalId once the row is part of the current session and you
need a Map key, tree node id, or invalidation target.
Lookup, virtual, and system keys are user-scoped. If your app switches users
in a long-lived client, call client.setCurrentUser(userId) before resolving
those keys.
See Row Keys and Row Identity for the full overview.