Something went wrong

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

Client Architecture - Lona Docs Log in

Client Architecture

LonaSdk.Client is the entry point for all SDK operations. It manages authentication, sheet handles, and provides access to sheet and row data.

Creating a client

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,
});

Opening a sheet

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();

Opening a row

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.

Listing sheets

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");

SheetHandle

Returned by client.open(). Holds the sheet metadata and row tree.

Properties

PropertyTypeDescription
sheet.idUuidSheet UUID
sheet.titlestring | nullSheet title
sheet.rowsRowsAccessorRow lookup and operations
sheet.cellsCellAccessorCell fetch, cache, and subscriptions
sheet.editablebooleanWhether 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();

CellAccessor

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.

RowsAccessor

Accessed via sheet.rows. Provides the fluent API for listing, querying, and adding rows.

sheet.rows.list

All 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

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,
  },
});
CallbackRequiredDescription
installDebounce(baseUrl, accessCode)yesInstall the TypedApi debounce backend
getMaxHeight()yesViewport height for layout calculations
getMaxWidth()noSidebar width calculation
onFrozenRowOverflow(tried, max, maxHeight)noFrozen rows exceed viewport
onFrozenRowFit()noFrozen rows fit again
themeBackendnoTheme CSS application

For tests and headless environments, use the built-in stub:

import { SheetPlatform } from "@tento-lona";

SheetPlatform.TEST
// installDebounce + getMaxHeight: () => 800, getMaxWidth: () => 1820

FeedbackDelegate

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 });
    },
  },
});
CallbackReturnsDescription
confirmDelete(message)Promise<boolean>Confirm destructive action. Return false to cancel
showUndo(message, undo)voidShow undo notification with async restore callback
warn(message, description?)voidShow a warning

Defaults to FeedbackDelegate.NOOP (auto-confirms deletes, ignores warnings) for tests and headless environments.

Invalidation

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.

InvalidationSink

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

Actor

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.

Event types

Every event extends Invalidation with an actor, a path string, and an eq() method for deduplication.

Singleton events (no entity payload):

EventPathEmitted by
TimezoneChangeuser.preferences.timezonePreferencesClient
ThemeChangeuser.preferences.themeThemeStore
CalendarPreferencesChangeduser.preferences.calendarCalendarPreferences
LayoutChangedui.layoutSheetRowTree, RowHeightPreferences
WindowResizeui.viewportApp layer
FrozenRowsChangedsheets.*.numFrozenRowsSheetsAccessor
SheetStructureChangedsheets.*.structureSheetHandle, RowsAccessor
SheetListChangedsheetsSheetsAccessor
CalendarVisibilityChangecalendars.visibilityCalendarClient
NewRowInsertedsheets.*.rowsRowsAccessor, SheetHandle
ViewportsInvalidatedcalendars.eventsEventsStore
TasksChangedtasksApp layer

Scoped events (carry entity ID):

EventConstructorPath
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}

Resource paths

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

Field-level granularity

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")

Subscribing

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);
  }
});

Path-based effect mapping

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.

Mutation flow

When you call row.update({ label: "New Name" }, myActor):

  1. SDK applies the patch to local state
  2. SDK emits RowLegendChanged(myActor, rowId) through the sink
  3. SDK debounces the server sync
  4. Subscribers re-render (skipping if event.actor === myActor)

Row identity

The public SDK mainly exposes two row identity concepts:

  • RowKey for a stable row reference
  • RowLocalId for in-memory tree and UI identity
import { 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.

See also

  • Sheets — listing, opening, and creating sheets
  • Rows — row types and tree navigation
  • Cells — reading, writing, and cross-user row access
  • API Reference — full method listing