@lotics/app-sdk 0.51.2 → 0.51.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ai.md +1 -1
- package/docs/data_fetching.md +55 -14
- package/docs/queries.md +22 -13
- package/docs/runtime.md +8 -4
- package/package.json +1 -1
package/docs/ai.md
CHANGED
|
@@ -121,7 +121,7 @@ Both settle the in-flight `run()` promise cleanly with `undefined` — a stop is
|
|
|
121
121
|
|
|
122
122
|
### Runs survive dropped connections
|
|
123
123
|
|
|
124
|
-
The run's lifetime is decoupled from the stream: the server drives it to completion and persists the result even if the connection drops. The hook reads the run id from the stream's start; if the connection then fails, it **polls the persisted run to completion** (every 2.5 s, bounded at 11 minutes) and resolves with the settled result instead of surfacing a network error — a
|
|
124
|
+
The run's lifetime is decoupled from the stream: the server drives it to completion and persists the result even if the connection drops. The hook reads the run id from the stream's start; if the connection then fails, it **polls the persisted run to completion** (every 2.5 s, bounded at 11 minutes) and resolves with the settled result instead of surfacing a network error — so a run that settles within that 11-minute poll window survives a flaky connection. A run still going when the poll deadline passes (the hard cap is 20 min) is **not** recovered on the client: the poll gives up and the drop surfaces as an error, though the result is still persisted server-side. Only when no run id was ever received (the run never started) does the failure reject before any polling.
|
|
125
125
|
|
|
126
126
|
**Warning:** on this recovery path the hook adopts the *persisted* run's output — and a **free-text** run is persisted with its final answer text as the output. So after a connection-drop recovery, a free-text agent's `output` can carry the answer **string** (the one place the "never a stray string" rule doesn't hold), while `text` keeps only what streamed before the drop. If you consume free-text agents, guard with `typeof output === "string"` on the resolved value.
|
|
127
127
|
|
package/docs/data_fetching.md
CHANGED
|
@@ -96,24 +96,39 @@ server validates system conditions by `type` and never reads `field_key` on them
|
|
|
96
96
|
| `query execution failed` | The query failed at the database. Deliberately generic — database internals are never sent to the client. | The app author diagnoses from the platform's server logs; the app surfaces the message. |
|
|
97
97
|
| A specific validation message | e.g. an un-projected `field_key` in runtime `sort`/`filter`, invalid params, an unknown alias. | Fix the call site — these are contract violations, not transient. |
|
|
98
98
|
|
|
99
|
-
## Pagination
|
|
99
|
+
## Pagination — two models
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
Reads paginate two different ways, and the difference is load-bearing:
|
|
102
|
+
|
|
103
|
+
- **`useInfiniteQuery` uses keyset (seek) pagination.** Each page seeks past the previous page's
|
|
104
|
+
opaque cursor instead of counting an offset, so a deep scroll stays O(page) and — the reason it
|
|
105
|
+
matters — **never skips or duplicates a row as the set shifts** under concurrent inserts/deletes.
|
|
106
|
+
The cursor is internal: the hook sends it and reads the next one back; you never see it. The
|
|
107
|
+
server keysets a single sortable key (the runtime `sort`, or the record-id default) and falls
|
|
108
|
+
back to offset transparently for a multi-key sort it can't seek. So an infinite feed needs **no
|
|
109
|
+
dedupe** — key rows by `__source_record_id` for stable React keys, not to guard against repeats.
|
|
110
|
+
- **`useQuery`, `usePaginatedQuery`, and manual `rpc("query", { limit, offset })` use offset
|
|
111
|
+
pagination** — count `offset` rows, skip them, return the next page. Two consequences a keyset
|
|
112
|
+
scroll doesn't have: deep pages cost more, and pages shift under concurrent writes.
|
|
113
|
+
|
|
114
|
+
Shared by both:
|
|
104
115
|
|
|
105
116
|
- **Row cap: 10,000.** No single query response returns more than 10,000 rows; a larger `limit`
|
|
106
117
|
(or an omitted `pageSize` on `useQuery`) is clamped to it. To read a bigger result set, page
|
|
107
118
|
through it.
|
|
119
|
+
- **Always give a paginated query a deterministic order.** With no `ORDER BY` (neither in the
|
|
120
|
+
template nor runtime `sort`), row order is unspecified — offset pages may overlap or skip rows
|
|
121
|
+
even without concurrent writes, and keyset falls back to the record-id order. Sort by a stable
|
|
122
|
+
column (unique where possible).
|
|
123
|
+
|
|
124
|
+
Offset-only (the `usePaginatedQuery` numbered pages and the `useQuery` / manual `rpc` cap paths):
|
|
125
|
+
|
|
108
126
|
- **Deep pages cost more.** `offset: n` makes the server produce and discard `n` rows before the
|
|
109
127
|
page — page 200 is materially slower than page 2. Prefer narrowing filters over deep paging.
|
|
110
128
|
- **Pages can shift under concurrent writes.** A row inserted/deleted between two page fetches
|
|
111
|
-
shifts every later offset — a row can appear on two consecutive pages or fall between them.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- **Always give a paginated query a deterministic order.** With no `ORDER BY` (neither in the
|
|
115
|
-
template nor runtime `sort`), row order is unspecified and pages may overlap or skip rows even
|
|
116
|
-
without concurrent writes. Sort by a stable column (unique where possible).
|
|
129
|
+
shifts every later offset — a row can appear on two consecutive pages or fall between them. If
|
|
130
|
+
you concatenate offset pages yourself, key rows by `__source_record_id` (never by array index)
|
|
131
|
+
and dedupe if the list must be exact.
|
|
117
132
|
- **`count: true` counts the filtered set**, ignoring sort/limit/offset — `usePaginatedQuery`
|
|
118
133
|
issues it automatically. The count and the page are separate requests, so under concurrent
|
|
119
134
|
writes `total` can briefly disagree with what paging finds.
|
|
@@ -134,16 +149,22 @@ decode cells with the readers below.
|
|
|
134
149
|
const { rows, loadMore, hasMore, loadingMore } = useInfiniteQuery("feed", {}, { pageSize: 30 });
|
|
135
150
|
```
|
|
136
151
|
|
|
137
|
-
The first render loads one page; `loadMore()` appends the next, accumulating into `rows`.
|
|
152
|
+
The first render loads one page; `loadMore()` appends the next, accumulating into `rows`. Paging is
|
|
153
|
+
**keyset (seek)** — appended pages never skip or duplicate a row as the set shifts (the pagination
|
|
154
|
+
models above).
|
|
138
155
|
|
|
139
|
-
- `hasMore` is `true`
|
|
140
|
-
of `pageSize` costs one final
|
|
156
|
+
- `hasMore` is `true` while the last page came back **full** (a non-null cursor came back) — so a
|
|
157
|
+
total that is an exact multiple of `pageSize` costs one final short fetch before `hasMore` turns
|
|
158
|
+
`false`.
|
|
141
159
|
- `loadMore()` is a no-op while `loadingMore` is `true` or when `hasMore` is `false` — safe to wire
|
|
142
160
|
directly to a scroll sentinel.
|
|
143
161
|
- Changing any part of the result-set identity (`params`, `sort`, `filter`, `pageSize`) starts a
|
|
144
162
|
fresh accumulation from the first page.
|
|
145
163
|
- `refetch()` revalidates every loaded page. Appending a page does **not** re-fetch earlier pages,
|
|
146
|
-
so a long-lived list mixes page snapshots taken at different times (
|
|
164
|
+
so a long-lived list still mixes page snapshots taken at different times (an edit between two
|
|
165
|
+
page loads shows old and new side by side) — but keyset guarantees no row is skipped or repeated.
|
|
166
|
+
- **Standalone (public) apps:** `loadMore()` can't advance past page one — the cursor is dropped in
|
|
167
|
+
transit (see [Standalone transport](#standalone-public-transport)).
|
|
147
168
|
|
|
148
169
|
### `usePaginatedQuery`
|
|
149
170
|
|
|
@@ -168,6 +189,26 @@ page cursor and fetches two things: the current page, and a `count` over the fil
|
|
|
168
189
|
`keepPreviousData`) — gate any skeleton on `loading && rows.length === 0`, never on `loading`
|
|
169
190
|
alone, or every page click collapses the table.
|
|
170
191
|
|
|
192
|
+
## Standalone (public) transport
|
|
193
|
+
|
|
194
|
+
An app served standalone at `<slug>.lotics.app` (a public share with no Lotics host — see
|
|
195
|
+
[runtime](./runtime.md)) reaches the query endpoint through a thinner transport that forwards only
|
|
196
|
+
`alias`, `params`, `limit`, and `offset`. It **silently drops** `sort`, `filter`, `count`, and the
|
|
197
|
+
keyset `cursor`. Design a public app around this:
|
|
198
|
+
|
|
199
|
+
- **Runtime `sort` / `filter` are ignored** — every hook's `opts.sort` / `opts.filter` is dropped,
|
|
200
|
+
so a standalone app can't order or narrow a query at the call site. Bake ordering and scoping
|
|
201
|
+
into the query **template** (or drive them through declared `params`), not the refinement options.
|
|
202
|
+
- **`usePaginatedQuery.total` stays `undefined`** — the `count` request never resolves, so
|
|
203
|
+
`totalPages` never lands and `hasMore` falls back to "the current page came back full". A
|
|
204
|
+
numbered "Page 1 of N" control has no N; render it defensively, or prefer `useInfiniteQuery`.
|
|
205
|
+
- **`useInfiniteQuery.loadMore()` never advances past the first page** — with the cursor dropped,
|
|
206
|
+
no `next_cursor` comes back, so `hasMore` is `false` after page one. For a standalone browse,
|
|
207
|
+
size the template `limit` (or a `params`-driven page) to return the whole set in one fetch.
|
|
208
|
+
|
|
209
|
+
The embedded product host and the `lotics app dev` forwarder pass all of these through — this
|
|
210
|
+
caveat is standalone-only.
|
|
211
|
+
|
|
171
212
|
## Decoding query cells
|
|
172
213
|
|
|
173
214
|
`useQuery` rows are `Record<string, unknown>`. The server serializes each output column by type;
|
package/docs/queries.md
CHANGED
|
@@ -708,17 +708,24 @@ template as derived nodes, in this order: `filter` (narrow) → `sort` (order)
|
|
|
708
708
|
it. Record-link membership (`has_any_of` by id) **works** here; so does `is_current_member`
|
|
709
709
|
on a member column. Traversals / `locked` / `current_member` do not — bake those into the
|
|
710
710
|
template.
|
|
711
|
-
- **`sort`** — `[{ field_key, order
|
|
712
|
-
|
|
711
|
+
- **`sort`** — `[{ field_key, order }]` over output columns — the typed `QuerySortKey` the hooks
|
|
712
|
+
accept. The wire sort node also honors `blank_position?: "top" | "bottom"` (blanks default to
|
|
713
|
+
bottom), but that field is **not** on the typed hook option; reach it only through a raw
|
|
714
|
+
`rpc("query", { sort })` call.
|
|
715
|
+
- **`limit` / `offset` / keyset `cursor`** — pagination. `useQuery` / `usePaginatedQuery` page by
|
|
716
|
+
`offset` (`limit` clamped to the 10,000-row cap, §10); `useInfiniteQuery` opts into **keyset
|
|
717
|
+
(seek)** by sending `keyset: true` + the prior page's `cursor`, and the server returns the next
|
|
718
|
+
`next_cursor` (null on the last page). Seek stays O(page) and never skips/duplicates a row as the
|
|
719
|
+
set shifts, falling back to offset for a multi-key sort it can't seek.
|
|
713
720
|
- **`count: true`** — returns `{ total }` only: a single-row COUNT over the *filtered* set,
|
|
714
721
|
ignoring sort/limit/offset. Drives "Page 1 of N".
|
|
715
722
|
|
|
716
723
|
The SDK hooks map onto this directly (`dist/src/hooks.d.ts` for exact signatures): `useQuery`
|
|
717
724
|
sends `limit: pageSize, offset: 0` (a cap, not pagination) plus `opts.sort`/`opts.filter`;
|
|
718
|
-
`useInfiniteQuery` pages by `
|
|
719
|
-
|
|
720
|
-
independent of page and sort, so paging and
|
|
721
|
-
`(params, filter)` resets to page 0 and recounts.
|
|
725
|
+
`useInfiniteQuery` pages by **keyset** (`keyset: true` + the prior `next_cursor`), so its scroll
|
|
726
|
+
never skips or duplicates a row; `usePaginatedQuery` owns the page cursor, pages by `offset`, and
|
|
727
|
+
issues a `count` keyed on `(alias, params, filter)` — independent of page and sort, so paging and
|
|
728
|
+
re-sorting never recount, while changing `(params, filter)` resets to page 0 and recounts.
|
|
722
729
|
|
|
723
730
|
Build `filter` from UI column-filters with `columnFilterToConditions` (`@lotics/ui`); prefer
|
|
724
731
|
`useFieldOptions` for a select filter's option set.
|
|
@@ -730,11 +737,12 @@ silently ignored there and `count` never resolves (`usePaginatedQuery.total` sta
|
|
|
730
737
|
`undefined`). A standalone app must bake ordering/scoping into the template (or params) rather
|
|
731
738
|
than rely on runtime refinement.
|
|
732
739
|
|
|
733
|
-
**Pagination semantics
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
740
|
+
**Pagination semantics.** `useQuery` / `usePaginatedQuery` and manual `rpc("query", { limit,
|
|
741
|
+
offset })` are **offset** — a deep page costs the server the full skipped prefix (page 400 of a
|
|
742
|
+
25-row pager scans ~10,000 rows before returning 25), and pages can shift under concurrent writes
|
|
743
|
+
(a row inserted before your offset repeats or skips a row across pages). `useInfiniteQuery` is
|
|
744
|
+
**keyset (seek)** — deep scrolls stay O(page) and never skip/duplicate rows. Keep offset browses
|
|
745
|
+
filtered and sorted by a stable key, and don't build numbered UX that walks thousands of pages.
|
|
738
746
|
|
|
739
747
|
---
|
|
740
748
|
|
|
@@ -924,8 +932,9 @@ Consolidated from the sections above — these describe present engine behavior:
|
|
|
924
932
|
- **No `first_value`/`last_value`/`nth_value` navigation** — the `functions` set is `row_number`,
|
|
925
933
|
`rank`, `dense_rank`, `percent_rank`, `cume_dist`, `ntile`, `lag`, `lead` (§8). Frame aggregates
|
|
926
934
|
remain the 10-item OVER-legal subset.
|
|
927
|
-
-
|
|
928
|
-
concurrent writes (
|
|
935
|
+
- **`useQuery` / numbered pagination is offset.** Deep pages cost the full skipped prefix and can
|
|
936
|
+
shift under concurrent writes; `useInfiniteQuery` uses keyset (seek), which stays O(page) and
|
|
937
|
+
never skips or duplicates rows (§9).
|
|
929
938
|
- **No collation control.** Text ORDER BY uses the database default collation —
|
|
930
939
|
locale-specific alphabetical order (e.g. accented-letter ordering) is not configurable.
|
|
931
940
|
- **Select / member / link columns sort by raw JSON** in queries — not by configured option
|
package/docs/runtime.md
CHANGED
|
@@ -169,12 +169,16 @@ surfaces:
|
|
|
169
169
|
| `query`, `field_options`, `workflow`, `members`, `context`, `binding`, `upload`, `urlState.get/set`, `openExternal` | yes | yes | yes |
|
|
170
170
|
| `comments.*` | yes | yes | rejects — `"Comments are available only in embedded apps — a signed-in member is required."` |
|
|
171
171
|
| `agentRun` (streaming, internal to `useAgentRun`) | yes | yes | yes |
|
|
172
|
-
| `
|
|
172
|
+
| `agentRun.get`, `agentRun.cancel` | yes | **no** — the dev forwarder doesn't implement them (`"Unknown RPC op: …"`) | yes |
|
|
173
|
+
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | **no** | yes |
|
|
173
174
|
| `askAi` | yes | **no** — `"Unknown RPC op: askAi"` | rejects — `"askAi is only available when the app runs inside Lotics"` |
|
|
174
175
|
|
|
175
|
-
**Limitation:**
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
**Limitation:** agent-run **history** (`agentRuns`) is not implemented by the
|
|
177
|
+
embedded product host *or* the dev forwarder — both error `"Unknown RPC op:
|
|
178
|
+
agentRuns"`. Don't build a session-log UI on `useAgentRuns`; keep the log in app
|
|
179
|
+
state from live `useAgentRun` results (see [ai](./ai.md)). Server-side *cancel*
|
|
180
|
+
also errors in the dev loop (the local abort of a live stream still works) —
|
|
181
|
+
verify cancel on a deployed app. The standalone `query` transport forwards only
|
|
178
182
|
`alias`/`params`/`limit`/`offset` — runtime `sort`/`filter`/`count` refinement
|
|
179
183
|
is embedded-only (see [data fetching](./data_fetching.md)).
|
|
180
184
|
|