@lotics/app-sdk 0.66.1 → 0.67.0

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/AGENTS.md CHANGED
@@ -15,7 +15,7 @@ signature; open the file.**
15
15
  | Doc | Read it for |
16
16
  |---|---|
17
17
  | [docs/queries.md](./docs/queries.md) | **The query engine authoring reference** — AST node kinds, per-field-type operator support, filters/params/pruning, free-text search, combining tables (join/union/link/`unnest`/`record_id`), shaping (aggregates, date buckets, windows), runtime refinement bounds, limits & the efficiency playbook. |
18
- | [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, data discipline, the search-as-you-type + record-picker patterns. |
18
+ | [docs/data_fetching.md](./docs/data_fetching.md) | The three read hooks (`useQuery`/`useInfiniteQuery`/`usePaginatedQuery`), the `QueryRow` shape (projected columns `unknown`; `__source_record_id`/`__source_table_id` typed but optional), cell readers (`row.*`, `readSelect`, `readMembers`, `readLinks`, `readFiles`, `readLocked`), `useFieldOptions`, caching — **arrival revalidates** (a re-mount renders cache *and* refreshes it in the background, `loading` never flips) — data discipline, the search-as-you-type + record-picker patterns. |
19
19
  | [docs/mutations.md](./docs/mutations.md) | `useWorkflow` (the ONLY write path), the `WorkflowResult` resolve-never-throw contract, typed inputs, diff-before-update, locked records, `useOptimistic`, `useNewRecord` (client-minted `rec_*` id so a new-record surface never remounts on its first save), read-after-write ordering (a re-read must not overtake an in-flight write). |
20
20
  | [docs/workflows.md](./docs/workflows.md) | **The workflow-BODY authoring reference** — the JS subset a `src/workflows/<alias>.ts` body may use: the parse-at-save/never-execute model, opaque `fld_*`/`opt_*` keys, expression sources + link descent, every step form (tool call, `agent`, waits, `validate`, `return`), the accepted sugar and its canonical lowering, helpers + callback rules, record-write surfaces, the traps, the bright line, and the verify loop — `check` (the only local gate: the app's own `npm run typecheck` never sees a body) → `dry_run_workflow` (static green is not a run) → `set`. |
21
21
  | [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs (**a bearer credential for the bytes** — never logged, reported, or persisted), workflow-generated files, **naming a zip's entries** (`{ id, name }` per file — a file name, never a path), preview pairing, filter operators, the server-side delivery bounds. **Uploads declare a `fidelity`** (`standard` / `high` / `original`) — the app picks how much of the image survives storage; use `high` whenever text must stay legible. |
package/dist/src/hooks.js CHANGED
@@ -31,19 +31,24 @@ export function useWorkflow(alias) {
31
31
  // rows (no retry loop that masks the error), and honor the focus/reconnect
32
32
  // opt-out.
33
33
  //
34
- // `revalidateIfStale: false` SWR defaults this to `true`, and its initial
35
- // -revalidation decision is `isUndefined(data) || revalidateIfStale`, so every
36
- // re-mount of a screen that already had rows re-queried the backend. An app
37
- // that navigates between screens (or re-opens a drawer) paid a full query set
38
- // each time for data it was already showing. A cold mount still fetches
39
- // (`isUndefined(data)` short-circuits), focus/reconnect still refresh, and the
40
- // host's `refetchQueries` poke still forces a refresh after a chat turn mutates
41
- // records so freshness keeps every path it had except "re-mounted".
34
+ // `revalidateIfStale` is LEFT AT SWR's DEFAULT (`true`): re-mounting a screen
35
+ // renders its cached rows AND re-reads them. Pinning it `false` saved a re-query
36
+ // per navigation and cost every screen a hand-rolled arrival refetch where
37
+ // forgetting is SILENT (no error, no empty state, just last visit's data until a
38
+ // hard reload), which is not a mistake an author gets told about once and learns.
39
+ //
40
+ // The default costs one BACKGROUND request per re-mount of a warm key and
41
+ // nothing visible: SWR's `isLoading` is true only on an initial load with no
42
+ // cached data, so nothing blanks to a skeleton, and `dedupingInterval` collapses
43
+ // a burst of navigation into one fetch.
44
+ //
45
+ // `useAppContext` (viewer.ts) still pins it off — the app's identity is read
46
+ // once at boot and cannot change under the reader. `useInfiniteQuery` is the
47
+ // one hook this does not reach; see its own note.
42
48
  function swrConfig(revalidateOnFocus) {
43
49
  return {
44
50
  revalidateOnFocus,
45
51
  revalidateOnReconnect: revalidateOnFocus,
46
- revalidateIfStale: false,
47
52
  shouldRetryOnError: false,
48
53
  };
49
54
  }
@@ -152,7 +157,20 @@ export function useInfiniteQuery(alias, params, opts) {
152
157
  sort,
153
158
  filter,
154
159
  });
155
- }, { revalidateFirstPage: false, ...swrConfig(revalidateOnFocus) });
160
+ },
161
+ // `revalidateFirstPage: false` — `loadMore()` fetches the next page and only
162
+ // that page. SWR's default re-fetches page 1 on every load-more, costing a
163
+ // request per scroll and reshuffling the top of the feed under a reader
164
+ // looking further down it (pinned by the keyset test).
165
+ //
166
+ // THE COST, stated because it is otherwise invisible: this hook does not
167
+ // arrival-revalidate the way the other two do. SWRInfinite decides per page
168
+ // and never consults `revalidateIfStale`, and neither lever that would change
169
+ // that is cheap — `revalidateFirstPage` pays per load-more, `revalidateOnMount`
170
+ // refetches EVERY loaded page. A cold key still fetches, focus/reconnect and
171
+ // `refetch()` still refresh; a feed that must be fresh on arrival calls
172
+ // `refetch()`.
173
+ { revalidateFirstPage: false, ...swrConfig(revalidateOnFocus) });
156
174
  // SWRInfinite leaves an in-flight page slot `undefined` until it resolves —
157
175
  // operate on resolved pages only so a page being appended never crashes the
158
176
  // flatten or skews the counts.
@@ -67,14 +67,18 @@ server validates system conditions by `type` and never reads `field_key` on them
67
67
  - **Cache identity** is the tuple `(alias, params, pageSize, sort, filter)` (+ the page index for
68
68
  the paged hooks). Object *contents* are hashed, not references — passing a fresh inline
69
69
  `{ status: "open" }` each render is the same key; you never need to memoize params.
70
- - The cache **survives unmount/remount**: returning to a screen renders the cached rows instantly
71
- and **sends no request**. Identical concurrent reads dedupe to one request.
72
- - **A re-mount is not a refresh event.** Freshness comes from window focus / tab return / network
73
- reconnect (`revalidateOnFocus`, default on), the host's post-chat-turn refetch poke, and explicit
74
- `refetch()`. Re-mounting a screen you already loaded is none of those, so it re-uses the cache.
75
- A **cold** key (never loaded, or a new `(alias, params, pageSize, sort, filter)` tuple) always
76
- fetches this only affects keys that already hold rows.
77
- After a write the user is watching for, call `refetch()`; never rely on navigation to refresh.
70
+ - The cache **survives unmount/remount**, and **arrival is a refresh event**: returning to a screen
71
+ renders the cached rows instantly *and* revalidates them in the background, so a list reflects what
72
+ another screen changed while you were away. Identical concurrent reads dedupe to one request.
73
+ Freshness comes from four places — arrival, window focus / tab return / network reconnect
74
+ (`revalidateOnFocus`, default on), the host's post-chat-turn poke, and explicit `refetch()`.
75
+ **Still call `refetch()` after a write the user is watching for**: arrival covers navigation, not a
76
+ mutation made on the screen you are already standing on.
77
+ - **`useInfiniteQuery` does not arrival-revalidate** the one exception. A warm feed re-mounted
78
+ refetches nothing; a cold key still fetches, and focus / reconnect / the chat poke still refresh.
79
+ Re-fetching page 1 on arrival would cost a request on every `loadMore()` and reshuffle the top of
80
+ the feed under a reader scrolling further down it, and re-fetching every loaded page grows without
81
+ bound. **A feed that must be fresh on arrival calls `refetch()`.**
78
82
  - **`loading`** is `true` only on the *initial* load of a key — a request is in flight and there
79
83
  are no rows yet. It stays `false` during background revalidation of a key that already has rows,
80
84
  so consumers never blank loaded data to a spinner on refetch. A key *change* (new params, sort,
@@ -89,10 +89,10 @@ name**, not the field key. Each `FieldOptions`:
89
89
 
90
90
  ### Caching & freshness
91
91
 
92
- Field config is slow-changing, so the hook fetches once per alias and does **not** revalidate on
93
- window focus or reconnect. The option set is resolved live from field config at fetch time — an
94
- option added, renamed, or recolored in the table flows through on the next fetch (a remount, a full
95
- reload, or an explicit `refetch()`), with no app redeploy. `opts.enabled` defers the fetch (e.g.
92
+ Field config is slow-changing, so the hook does **not** revalidate on window focus or reconnect — it
93
+ re-reads on arrival (a remount) like the query hooks, and on an explicit `refetch()`. The option set
94
+ is resolved live from field config at fetch time, so an option added, renamed, or recolored in the
95
+ table flows through on the next fetch, with no app redeploy. `opts.enabled` defers the fetch (e.g.
96
96
  until an edit drawer opens). State: `{ fields, loading, isValidating, error, refetch }`.
97
97
 
98
98
  ### Coloring a stored value (the `byKey` idiom)
package/docs/queries.md CHANGED
@@ -780,6 +780,19 @@ template as derived nodes, in this order: `filter` (narrow) → `sort` (order)
780
780
  - **`count: true`** — returns `{ total }` only: a single-row COUNT over the *filtered* set,
781
781
  ignoring sort/limit/offset. Drives "Page 1 of N".
782
782
 
783
+ **A terminal aggregate cannot be refined — plan the total accordingly.** Because refinement wraps
784
+ *around* the template over its **output** columns, a query that has already collapsed (a `group`
785
+ with an empty `by` and a `sum`, say) has no source column left for the filter to name, so a facet
786
+ the list narrows by 400s against the total beside it. `window` is not a way out either: the frame
787
+ aggregate is computed before the refinement wraps, so the number describes the unfiltered set.
788
+ Both alternatives are worse than they look — baking the facet into the aggregate stops it being a
789
+ runtime facet (a filter TREE is not a scalar param), and leaving the total unfiltered puts a count
790
+ that follows the facet next to a sum that does not. **Project the summed column plus the columns
791
+ the facet can name, fetch under the same runtime `filter`, and total in the browser** — correct and
792
+ facet-aware, but it ships every matching row, so it holds only where the scope is already small
793
+ (one member's own records, a single project). A workspace-wide total that must follow a runtime
794
+ facet has no server-side shape today.
795
+
783
796
  The SDK hooks map onto this directly (`dist/src/hooks.d.ts` for exact signatures): `useQuery`
784
797
  sends `limit: pageSize, offset: 0` (a cap, not pagination) plus `opts.sort`/`opts.filter`;
785
798
  `useInfiniteQuery` pages by **keyset** (`keyset: true` + the prior `next_cursor`), so its scroll
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.66.1",
3
+ "version": "0.67.0",
4
4
  "description": "Runtime SDK for Lotics custom-code apps \u2014 typed hooks, postMessage bridge, mount entry point",
5
5
  "type": "module",
6
6
  "exports": {