@lotics/app-sdk 0.49.1 → 0.51.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 +1 -1
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.js +33 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/rpc.d.ts +40 -1
- package/dist/src/rpc.js +43 -0
- package/dist/src/viewer.d.ts +3 -1
- package/dist/src/viewer.js +7 -1
- package/docs/runtime.md +76 -5
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -22,7 +22,7 @@ signature; open the file.**
|
|
|
22
22
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
23
23
|
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming `items` → `AgentRun`) and `askAi` — plus the fields-vs-file razor for choosing between them. |
|
|
24
24
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen. |
|
|
25
|
-
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, and the publish chain for package contributors. |
|
|
25
|
+
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), `getAppBinding` (package apps' runtime `F`/`OPT`/`ROLE` resolution via the generated `.lotics/app_fields.ts`), and the publish chain for package contributors. |
|
|
26
26
|
|
|
27
27
|
## Non-negotiables (each detailed in its doc)
|
|
28
28
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AppConfigValue } from "./rpc.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read the installation's customization config — the first rung of the App
|
|
4
|
+
* Packages customization ladder (see docs/app_packages.md § The customization
|
|
5
|
+
* ladder). A package declares typed config knobs with defaults (labels, feature
|
|
6
|
+
* toggles, theme, column choices); each installation stores the customized values
|
|
7
|
+
* and the app reads them here. Updates flow: editing config in the product
|
|
8
|
+
* changes what this returns, with no re-deploy.
|
|
9
|
+
*
|
|
10
|
+
* `defaults` is the package contract's declared defaults — the canonical fallback
|
|
11
|
+
* (the codegen surface emits them so the call is fully typed). They fill any knob
|
|
12
|
+
* the installation hasn't overridden and provide a flicker-free first paint:
|
|
13
|
+
* before the context resolves the stored map is empty, so the defaults show
|
|
14
|
+
* immediately and the stored values overlay them once resolved.
|
|
15
|
+
*
|
|
16
|
+
* const { config } = useConfig({ "deal.label": "Deal", "show_archived": false });
|
|
17
|
+
* <Text>{config["deal.label"]}</Text>
|
|
18
|
+
*
|
|
19
|
+
* Gate on `loading` only for config-derived layout that must not flash a default.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useConfig<T extends Record<string, AppConfigValue> = Record<string, AppConfigValue>>(defaults?: T): {
|
|
22
|
+
config: T;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useAppContext } from "./viewer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read the installation's customization config — the first rung of the App
|
|
5
|
+
* Packages customization ladder (see docs/app_packages.md § The customization
|
|
6
|
+
* ladder). A package declares typed config knobs with defaults (labels, feature
|
|
7
|
+
* toggles, theme, column choices); each installation stores the customized values
|
|
8
|
+
* and the app reads them here. Updates flow: editing config in the product
|
|
9
|
+
* changes what this returns, with no re-deploy.
|
|
10
|
+
*
|
|
11
|
+
* `defaults` is the package contract's declared defaults — the canonical fallback
|
|
12
|
+
* (the codegen surface emits them so the call is fully typed). They fill any knob
|
|
13
|
+
* the installation hasn't overridden and provide a flicker-free first paint:
|
|
14
|
+
* before the context resolves the stored map is empty, so the defaults show
|
|
15
|
+
* immediately and the stored values overlay them once resolved.
|
|
16
|
+
*
|
|
17
|
+
* const { config } = useConfig({ "deal.label": "Deal", "show_archived": false });
|
|
18
|
+
* <Text>{config["deal.label"]}</Text>
|
|
19
|
+
*
|
|
20
|
+
* Gate on `loading` only for config-derived layout that must not flash a default.
|
|
21
|
+
*/
|
|
22
|
+
export function useConfig(defaults) {
|
|
23
|
+
const ctx = useAppContext();
|
|
24
|
+
const stored = ctx.config;
|
|
25
|
+
// Key the memo on the defaults CONTENT, not identity. An inline literal is a
|
|
26
|
+
// fresh reference each render, so a content hash keeps the result reference
|
|
27
|
+
// stable while values are unchanged AND recomputes when a default value changes
|
|
28
|
+
// (e.g. an i18n label after a locale switch) — which a ref-captured defaults
|
|
29
|
+
// would silently miss. Config maps are tiny, so the stringify cost is trivial.
|
|
30
|
+
const defaultsKey = JSON.stringify(defaults ?? {});
|
|
31
|
+
const config = useMemo(() => ({ ...(defaults ?? {}), ...stored }), [stored, defaultsKey]);
|
|
32
|
+
return { config, loading: !ctx.resolved };
|
|
33
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -21,10 +21,12 @@ export type { UploadedFile, AttachedFile, BaseQueryOptions, QueryOptions, Infini
|
|
|
21
21
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
22
22
|
export type { AppComment, AppCommentFile, CommentsState, UseCommentsArgs, CommentCountsState, UseCommentCountsArgs, } from "./comments.js";
|
|
23
23
|
export { useViewer } from "./viewer.js";
|
|
24
|
+
export { useConfig } from "./config.js";
|
|
25
|
+
export type { AppConfigValue } from "./rpc.js";
|
|
24
26
|
export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
|
|
25
27
|
export type { GeofenceZone, GeoCoords, GeofenceOutcome, GeofenceOptions } from "./geolocation.js";
|
|
26
|
-
export { rpc, isEmbedded } from "./rpc.js";
|
|
27
|
-
export type { RpcOp } from "./rpc.js";
|
|
28
|
+
export { rpc, isEmbedded, getAppBinding } from "./rpc.js";
|
|
29
|
+
export type { RpcOp, AppBinding } from "./rpc.js";
|
|
28
30
|
export { openExternal } from "./open_external.js";
|
|
29
31
|
export { askAi, type AskAiArgs } from "./ask_ai.js";
|
|
30
32
|
export { downloadFile } from "./download.js";
|
package/dist/src/index.js
CHANGED
|
@@ -18,8 +18,9 @@ export { mount } from "./mount.js";
|
|
|
18
18
|
export { useWorkflow, useQuery, useInfiniteQuery, usePaginatedQuery, useFieldOptions, useFileUpload, useAttachments, useMembers, useAgentRun, useAgentRuns, } from "./hooks.js";
|
|
19
19
|
export { useComments, useCommentCounts } from "./comments.js";
|
|
20
20
|
export { useViewer } from "./viewer.js";
|
|
21
|
+
export { useConfig } from "./config.js";
|
|
21
22
|
export { requestGeofencedLocation, isWithinZone } from "./geolocation.js";
|
|
22
|
-
export { rpc, isEmbedded } from "./rpc.js";
|
|
23
|
+
export { rpc, isEmbedded, getAppBinding } from "./rpc.js";
|
|
23
24
|
export { openExternal } from "./open_external.js";
|
|
24
25
|
export { askAi } from "./ask_ai.js";
|
|
25
26
|
export { downloadFile } from "./download.js";
|
package/dist/src/rpc.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
19
19
|
* app → host: { id, op, payload }
|
|
20
20
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
21
21
|
*/
|
|
22
|
-
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "openExternal" | "askAi" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
22
|
+
export type RpcOp = "query" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "members" | "context" | "binding" | "openExternal" | "askAi" | "urlState.get" | "urlState.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
|
|
23
23
|
/** Payload for starting a streaming agent run. */
|
|
24
24
|
export interface AgentRunPayload {
|
|
25
25
|
alias: string;
|
|
@@ -45,6 +45,15 @@ export interface AgentRunHandle {
|
|
|
45
45
|
* The PostHog key/host are not here — they're the public project key, hardcoded
|
|
46
46
|
* in `analytics.ts`.
|
|
47
47
|
*/
|
|
48
|
+
/**
|
|
49
|
+
* A single installation config value. An app package declares typed config knobs
|
|
50
|
+
* (labels, toggles, theme, column choices) with defaults; an installation stores
|
|
51
|
+
* the customized values, which the SDK's `useConfig()` reads. The canonical type
|
|
52
|
+
* lives in `@lotics/shared` (`AppInstallationConfigValue`); the SDK mirrors it
|
|
53
|
+
* here so it stays a zero-internal-dependency published package (the boundary
|
|
54
|
+
* mirror, like `AppContext` itself).
|
|
55
|
+
*/
|
|
56
|
+
export type AppConfigValue = string | number | boolean;
|
|
48
57
|
export interface AppContext {
|
|
49
58
|
app_id: string;
|
|
50
59
|
app_name: string;
|
|
@@ -57,6 +66,13 @@ export interface AppContext {
|
|
|
57
66
|
* app that didn't opt in, and (vacuously) for standalone visitors.
|
|
58
67
|
*/
|
|
59
68
|
comments_enabled: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The installation's customization values — the package's config knobs by
|
|
71
|
+
* alias (see `useConfig`). Empty for a bespoke app (no package) and until the
|
|
72
|
+
* context resolves. Read-only; an app changes config through the product, not
|
|
73
|
+
* the SDK.
|
|
74
|
+
*/
|
|
75
|
+
config: Record<string, AppConfigValue>;
|
|
60
76
|
}
|
|
61
77
|
/**
|
|
62
78
|
* Whether the app is running embedded in a Lotics host (vs. standalone at its
|
|
@@ -94,3 +110,26 @@ export declare function rpcAgentRun(payload: AgentRunPayload, onText: (chunk: st
|
|
|
94
110
|
* `parsed` is the JSON.parse of the body, or `null` if it wasn't JSON.
|
|
95
111
|
*/
|
|
96
112
|
export declare function transportErrorMessage(status: number, parsed: unknown): string;
|
|
113
|
+
/**
|
|
114
|
+
* A package installation's alias→concrete-id maps — what the generated
|
|
115
|
+
* `.lotics/app_fields.ts` of a package project resolves `F`/`OPT`/`ROLE`
|
|
116
|
+
* through at module load. Keys are fully-qualified contract aliases
|
|
117
|
+
* (`entity.field`, `entity.field:opt`, role alias); values are this
|
|
118
|
+
* installation's concrete ids. 404s for a bespoke (non-package) app.
|
|
119
|
+
*/
|
|
120
|
+
export interface AppBinding {
|
|
121
|
+
fields: Record<string, string>;
|
|
122
|
+
options: Record<string, string>;
|
|
123
|
+
roles: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch the installation's binding, once per boot (module-cached — every
|
|
127
|
+
* `app_fields` import shares the same in-flight promise). Called at module
|
|
128
|
+
* load via top-level await, so it must work before `mount()` — and a failure
|
|
129
|
+
* there rejects the whole module graph (blank frame, no ErrorBoundary can
|
|
130
|
+
* catch module evaluation). Two mitigations, mirroring `boot()`:
|
|
131
|
+
* bounded retries absorb a transient transport blip, and a rejection is never
|
|
132
|
+
* cached so a remount/direct caller can retry rather than replaying the same
|
|
133
|
+
* stale failure forever.
|
|
134
|
+
*/
|
|
135
|
+
export declare function getAppBinding(): Promise<AppBinding>;
|
package/dist/src/rpc.js
CHANGED
|
@@ -452,6 +452,8 @@ function rpcStandalone(op, payload) {
|
|
|
452
452
|
return standaloneMembers(payload);
|
|
453
453
|
case "context":
|
|
454
454
|
return standaloneContext();
|
|
455
|
+
case "binding":
|
|
456
|
+
return standaloneBinding();
|
|
455
457
|
case "openExternal":
|
|
456
458
|
return standaloneOpenExternal(payload);
|
|
457
459
|
case "askAi":
|
|
@@ -482,6 +484,46 @@ function rpcStandalone(op, payload) {
|
|
|
482
484
|
function rejectCommentsStandalone() {
|
|
483
485
|
return Promise.reject(new Error("Comments are available only in embedded apps — a signed-in member is required."));
|
|
484
486
|
}
|
|
487
|
+
let bindingPromise;
|
|
488
|
+
/**
|
|
489
|
+
* Fetch the installation's binding, once per boot (module-cached — every
|
|
490
|
+
* `app_fields` import shares the same in-flight promise). Called at module
|
|
491
|
+
* load via top-level await, so it must work before `mount()` — and a failure
|
|
492
|
+
* there rejects the whole module graph (blank frame, no ErrorBoundary can
|
|
493
|
+
* catch module evaluation). Two mitigations, mirroring `boot()`:
|
|
494
|
+
* bounded retries absorb a transient transport blip, and a rejection is never
|
|
495
|
+
* cached so a remount/direct caller can retry rather than replaying the same
|
|
496
|
+
* stale failure forever.
|
|
497
|
+
*/
|
|
498
|
+
export function getAppBinding() {
|
|
499
|
+
if (bindingPromise === undefined) {
|
|
500
|
+
const attempt = fetchBindingWithRetry();
|
|
501
|
+
bindingPromise = attempt;
|
|
502
|
+
attempt.catch(() => {
|
|
503
|
+
if (bindingPromise === attempt)
|
|
504
|
+
bindingPromise = undefined;
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return bindingPromise;
|
|
508
|
+
}
|
|
509
|
+
async function fetchBindingWithRetry() {
|
|
510
|
+
const delays = [500, 1500];
|
|
511
|
+
for (const delay of delays) {
|
|
512
|
+
try {
|
|
513
|
+
return await rpc("binding", {});
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return rpc("binding", {});
|
|
520
|
+
}
|
|
521
|
+
async function standaloneBinding() {
|
|
522
|
+
const { app_id } = await boot();
|
|
523
|
+
return (await apiCall("GET", `/v1/apps/${app_id}/binding`, undefined, {
|
|
524
|
+
appId: app_id,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
485
527
|
async function standaloneMembers(p) {
|
|
486
528
|
const { app_id } = await boot();
|
|
487
529
|
const qs = p.group ? `?group_id=${encodeURIComponent(p.group)}` : "";
|
|
@@ -529,6 +571,7 @@ async function standaloneContext() {
|
|
|
529
571
|
// comments are unavailable regardless of the capability flag.
|
|
530
572
|
member_id: null,
|
|
531
573
|
comments_enabled: info.comments_enabled,
|
|
574
|
+
config: info.config ?? {},
|
|
532
575
|
};
|
|
533
576
|
}
|
|
534
577
|
async function standaloneQuery(p) {
|
package/dist/src/viewer.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { type AppConfigValue } from "./rpc.js";
|
|
1
2
|
/**
|
|
2
3
|
* Read the app's context once, shared across every hook via a stable SWR key.
|
|
3
4
|
* The host (the product iframe, or `lotics app dev`) supplies the signed-in
|
|
4
|
-
* member
|
|
5
|
+
* member, the app's declared capabilities, and the installation's config.
|
|
5
6
|
*/
|
|
6
7
|
export declare function useAppContext(): {
|
|
7
8
|
memberId: string | null;
|
|
8
9
|
commentsEnabled: boolean;
|
|
10
|
+
config: Record<string, AppConfigValue>;
|
|
9
11
|
resolved: boolean;
|
|
10
12
|
};
|
|
11
13
|
/**
|
package/dist/src/viewer.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import useSWR from "swr";
|
|
2
2
|
import { rpc } from "./rpc.js";
|
|
3
|
+
// Stable identity for the not-yet-resolved / bespoke-app case. An inline `{}`
|
|
4
|
+
// fallback would be a fresh reference every render, defeating useConfig's
|
|
5
|
+
// memo (its result would recompute — and re-render consumers — on every
|
|
6
|
+
// render even with unchanged values).
|
|
7
|
+
const EMPTY_CONFIG = {};
|
|
3
8
|
/**
|
|
4
9
|
* Read the app's context once, shared across every hook via a stable SWR key.
|
|
5
10
|
* The host (the product iframe, or `lotics app dev`) supplies the signed-in
|
|
6
|
-
* member
|
|
11
|
+
* member, the app's declared capabilities, and the installation's config.
|
|
7
12
|
*/
|
|
8
13
|
export function useAppContext() {
|
|
9
14
|
const { data } = useSWR("app-context", () => rpc("context", {}), {
|
|
@@ -15,6 +20,7 @@ export function useAppContext() {
|
|
|
15
20
|
return {
|
|
16
21
|
memberId: data?.member_id ?? null,
|
|
17
22
|
commentsEnabled: data?.comments_enabled ?? false,
|
|
23
|
+
config: data?.config ?? EMPTY_CONFIG,
|
|
18
24
|
resolved: data !== undefined,
|
|
19
25
|
};
|
|
20
26
|
}
|
package/docs/runtime.md
CHANGED
|
@@ -166,7 +166,7 @@ surfaces:
|
|
|
166
166
|
|
|
167
167
|
| Op | Embedded (product) | `lotics app dev` | Standalone |
|
|
168
168
|
|---|---|---|---|
|
|
169
|
-
| `query`, `field_options`, `workflow`, `members`, `context`, `upload`, `urlState.get/set`, `openExternal` | yes | yes | yes |
|
|
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
|
| `agentRuns`, `agentRun.get`, `agentRun.cancel` | yes | **no** — the dev forwarder doesn't implement them (`"Unknown RPC op: …"`) | yes |
|
|
@@ -222,10 +222,13 @@ The **streaming** agent-run op is *not* reachable through `rpc()` — its
|
|
|
222
222
|
response is a chunk stream, not a single value; it's internal to `useAgentRun`.
|
|
223
223
|
|
|
224
224
|
`rpc("context", {})` resolves the app's identity: `{ app_id, app_name,
|
|
225
|
-
workspace_id, organization_id, member_id, comments_enabled }`.
|
|
226
|
-
the signed-in member when embedded, `null` standalone
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
workspace_id, organization_id, member_id, comments_enabled, config }`.
|
|
226
|
+
`member_id` is the signed-in member when embedded, `null` standalone; `config`
|
|
227
|
+
is the installation's customization map — read it through
|
|
228
|
+
[`useConfig`](#installation-config--useconfig), not off this raw op. **Limitation:**
|
|
229
|
+
the context type is not exported from the package root — type the result
|
|
230
|
+
yourself via the `rpc<T>` generic (`AppConfigValue`, the config value type, *is*
|
|
231
|
+
exported).
|
|
229
232
|
|
|
230
233
|
## `openExternal()` — open a link in a new tab
|
|
231
234
|
|
|
@@ -366,6 +369,74 @@ install `posthog-js` or call any analytics API from app code.
|
|
|
366
369
|
browsers (e.g. Playwright) are never tracked, so analytics cannot be verified
|
|
367
370
|
through headless automation.
|
|
368
371
|
|
|
372
|
+
## Installation config — `useConfig()`
|
|
373
|
+
|
|
374
|
+
**`useConfig(defaults?)`** → `{ config, loading }` — the App-Packages customization
|
|
375
|
+
knobs for this installation (labels, toggles, theme, column choices), keyed by the
|
|
376
|
+
alias the package contract declares. Exact signature: `dist/src/config.d.ts`.
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
const { config } = useConfig({ board_title: "Tasks", show_archive: false });
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
- **Pass the contract's defaults.** They fill un-overridden knobs and paint
|
|
383
|
+
flicker-free before the context resolves; the installation's stored values
|
|
384
|
+
overlay them (a stored value always wins over the same-key default). `loading`
|
|
385
|
+
is true only until the **one-shot** context RPC settles (it never revalidates)
|
|
386
|
+
— gate on it only for config-derived layout that must not flash a default.
|
|
387
|
+
- Values are `string | number | boolean` (`AppConfigValue`, exported from the
|
|
388
|
+
package root). The returned object is referentially stable across renders while
|
|
389
|
+
the values are unchanged — the merge is memoized, and the defaults are keyed by
|
|
390
|
+
their *content*, so passing a fresh inline `{…}` literal every render is fine
|
|
391
|
+
(no churn); it's safe as a `useEffect`/`useMemo` dependency.
|
|
392
|
+
- **Bespoke apps** (not installed from a package) get `{}` from the host — the
|
|
393
|
+
hook resolves to just the defaults, so shared code needs no package check.
|
|
394
|
+
- **A standalone (public) app behind a password never receives its stored config
|
|
395
|
+
in the client.** The knobs are gated server-side (they can carry business
|
|
396
|
+
terms), and the SDK resolves the app's identity once — before the visitor
|
|
397
|
+
authenticates — so `useConfig` returns the defaults only, even after unlock.
|
|
398
|
+
Design a public, password-gated app to be correct on its defaults alone.
|
|
399
|
+
(Embedded and standalone-*unprotected* apps both get the stored config.)
|
|
400
|
+
- **Read-only.** Config is edited in the product (the installation's settings),
|
|
401
|
+
never written from app code — there is no setter.
|
|
402
|
+
|
|
403
|
+
## Installation binding — `getAppBinding()` (package apps)
|
|
404
|
+
|
|
405
|
+
**`getAppBinding()`** → `Promise<AppBinding>` — a package installation's
|
|
406
|
+
alias→concrete-id maps: `{ fields, options, roles }`, keyed by fully-qualified
|
|
407
|
+
contract aliases (`"tasks.title"`, `"tasks.status:to_do"`, role alias) with this
|
|
408
|
+
workspace's `fld_`/`opt_`/`grp_` ids as values. Exact signature:
|
|
409
|
+
`dist/src/rpc.d.ts`.
|
|
410
|
+
|
|
411
|
+
You normally never call it yourself: the package project's generated
|
|
412
|
+
`.lotics/app_fields.ts` (written by `lotics package new/extract/dev/sync` from
|
|
413
|
+
`contract.json`) calls it once at **module load** via top-level await and
|
|
414
|
+
exports plain-string `F` / `OPT` / `ROLE` maps — the same import surface as a
|
|
415
|
+
bespoke app's baked codegen, but resolved per-installation at runtime:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { OPT } from "../.lotics/app_fields";
|
|
419
|
+
const STATUS_DONE = OPT.TASKS.status.done; // this installation's "opt_…"
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- **One fetch per boot.** The promise is module-cached; every importer shares
|
|
423
|
+
it. The ESM graph awaits it before any dependent module evaluates, so the
|
|
424
|
+
values are ordinary strings everywhere — including module-top-level constants.
|
|
425
|
+
Transient failures are retried (3 attempts, short backoff) and a rejection is
|
|
426
|
+
never cached — a module-load failure rejects the whole graph (blank frame),
|
|
427
|
+
so the fetch absorbs blips rather than bricking the boot on one lost request.
|
|
428
|
+
- **Requires the starter's `build.target: "es2022"`** (top-level await does not
|
|
429
|
+
exist below it). `lotics package extract` refreshes `vite.config.ts` from the
|
|
430
|
+
current starter for exactly this reason.
|
|
431
|
+
- **Fails loud.** A key missing from the binding throws at boot with the alias
|
|
432
|
+
named — the binding is verified complete at install/adopt, so a miss means
|
|
433
|
+
the generated file is stale relative to the installed contract version
|
|
434
|
+
(re-run the codegen, republish).
|
|
435
|
+
- **Bespoke apps 404.** A never-adopted app has no binding; bespoke projects
|
|
436
|
+
keep the baked `lotics app codegen` variant instead. Shared code should not
|
|
437
|
+
call this directly — import from `.lotics/app_fields` and let the project
|
|
438
|
+
kind pick the implementation.
|
|
439
|
+
|
|
369
440
|
## For package contributors
|
|
370
441
|
|
|
371
442
|
Everything below concerns changing `@lotics/app-sdk` itself (in the Lotics
|