@lotics/app-sdk 0.100.0 → 0.101.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 +32 -47
- package/dist/agent_stream.d.ts +131 -0
- package/dist/ask_ai.d.ts +27 -0
- package/dist/attachments.d.ts +58 -0
- package/dist/chunk-ARV5FAU5.js +1132 -0
- package/dist/comments.d.ts +89 -0
- package/dist/error_report.d.ts +9 -0
- package/dist/folder_pick.d.ts +8 -0
- package/dist/geolocation.d.ts +42 -0
- package/dist/hooks.d.ts +251 -0
- package/dist/{src/index.d.ts → index.d.ts} +13 -22
- package/dist/index.js +31309 -0
- package/dist/index.js.LEGAL.txt +11 -0
- package/dist/members.d.ts +32 -0
- package/dist/mock.d.ts +37 -0
- package/dist/mount.d.ts +19 -0
- package/dist/new_record.d.ts +37 -0
- package/dist/open_app.d.ts +12 -0
- package/dist/open_external.d.ts +10 -0
- package/dist/overlay.d.ts +25 -0
- package/dist/queries.d.ts +231 -0
- package/dist/recording.d.ts +47 -0
- package/dist/recording_state.d.ts +43 -0
- package/dist/rename_file.d.ts +13 -0
- package/dist/router.d.ts +10 -0
- package/dist/router.js +97 -0
- package/dist/row.d.ts +87 -0
- package/dist/rpc.d.ts +114 -0
- package/dist/select.d.ts +24 -0
- package/dist/shared_types.d.ts +8 -0
- package/dist/store.d.ts +43 -0
- package/dist/types.d.ts +36 -0
- package/dist/upload/optimize.d.ts +30 -0
- package/dist/upload/pipeline.d.ts +36 -0
- package/dist/upload/transport.d.ts +19 -0
- package/dist/url_params.d.ts +55 -0
- package/dist/use_recents.d.ts +15 -0
- package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
- package/dist/viewer.d.ts +41 -0
- package/dist/written.d.ts +77 -0
- package/docs/ai.md +74 -133
- package/docs/data_fetching.md +209 -290
- package/docs/files.md +61 -51
- package/docs/members_and_options.md +93 -63
- package/docs/mutations.md +135 -205
- package/docs/navigation_and_state.md +26 -35
- package/docs/queries.md +144 -207
- package/docs/recipes.md +21 -45
- package/docs/runtime.md +74 -137
- package/docs/security.md +8 -11
- package/docs/workflows.md +189 -174
- package/package.json +27 -28
- package/dist/src/agent_stream.d.ts +0 -200
- package/dist/src/agent_stream.js +0 -314
- package/dist/src/ask_ai.d.ts +0 -40
- package/dist/src/ask_ai.js +0 -35
- package/dist/src/attachments.d.ts +0 -68
- package/dist/src/attachments.js +0 -93
- package/dist/src/comments.d.ts +0 -127
- package/dist/src/comments.js +0 -192
- package/dist/src/download.js +0 -54
- package/dist/src/geolocation.d.ts +0 -64
- package/dist/src/geolocation.js +0 -96
- package/dist/src/hooks.d.ts +0 -781
- package/dist/src/hooks.js +0 -860
- package/dist/src/index.js +0 -34
- package/dist/src/members.d.ts +0 -105
- package/dist/src/members.js +0 -62
- package/dist/src/mock.d.ts +0 -118
- package/dist/src/mock.js +0 -124
- package/dist/src/mount.d.ts +0 -47
- package/dist/src/mount.js +0 -34
- package/dist/src/new_record.d.ts +0 -74
- package/dist/src/new_record.js +0 -117
- package/dist/src/open_app.d.ts +0 -15
- package/dist/src/open_app.js +0 -18
- package/dist/src/open_external.d.ts +0 -16
- package/dist/src/open_external.js +0 -19
- package/dist/src/recording.d.ts +0 -59
- package/dist/src/recording.js +0 -30
- package/dist/src/recording_state.d.ts +0 -59
- package/dist/src/recording_state.js +0 -94
- package/dist/src/router.d.ts +0 -17
- package/dist/src/router.js +0 -144
- package/dist/src/row.d.ts +0 -159
- package/dist/src/row.js +0 -254
- package/dist/src/rpc.d.ts +0 -207
- package/dist/src/rpc.js +0 -904
- package/dist/src/select.d.ts +0 -34
- package/dist/src/select.js +0 -40
- package/dist/src/types.d.ts +0 -115
- package/dist/src/types.js +0 -1
- package/dist/src/upload/optimize.d.ts +0 -54
- package/dist/src/upload/optimize.js +0 -207
- package/dist/src/upload/pipeline.d.ts +0 -55
- package/dist/src/upload/pipeline.js +0 -52
- package/dist/src/upload/transport.d.ts +0 -42
- package/dist/src/upload/transport.js +0 -128
- package/dist/src/url_params.d.ts +0 -93
- package/dist/src/url_params.js +0 -215
- package/dist/src/use_optimistic.d.ts +0 -27
- package/dist/src/use_optimistic.js +0 -27
- package/dist/src/use_recents.d.ts +0 -19
- package/dist/src/use_recents.js +0 -71
- package/dist/src/use_url_state.js +0 -73
- package/dist/src/viewer.d.ts +0 -26
- package/dist/src/viewer.js +0 -47
- /package/dist/{src/download.d.ts → download.d.ts} +0 -0
package/dist/src/new_record.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
3
|
-
const ID_LENGTH = 12;
|
|
4
|
-
/**
|
|
5
|
-
* Mint a record id locally, in the shape the platform mints.
|
|
6
|
-
*
|
|
7
|
-
* The generator is restated here rather than imported because the canonical one
|
|
8
|
-
* lives in a package that is not published, and this one is. The duplication is
|
|
9
|
-
* safe by construction rather than by discipline: the server validates the shape on
|
|
10
|
-
* every write, so a client that drifted would be rejected loudly at the first call
|
|
11
|
-
* instead of quietly persisting a malformed primary key.
|
|
12
|
-
*
|
|
13
|
-
* Uses `crypto.getRandomValues` — available in every browser this SDK runs in — and
|
|
14
|
-
* rejection-samples so each character is uniformly drawn from the alphabet. A plain
|
|
15
|
-
* `% 62` over bytes would bias the first 8 characters, which is a poor property for
|
|
16
|
-
* something used as a primary key.
|
|
17
|
-
*/
|
|
18
|
-
export function newRecordId() {
|
|
19
|
-
const max = 256 - (256 % ID_ALPHABET.length);
|
|
20
|
-
let out = "";
|
|
21
|
-
const buf = new Uint8Array(ID_LENGTH * 2);
|
|
22
|
-
while (out.length < ID_LENGTH) {
|
|
23
|
-
crypto.getRandomValues(buf);
|
|
24
|
-
for (let i = 0; i < buf.length && out.length < ID_LENGTH; i++) {
|
|
25
|
-
if (buf[i] < max)
|
|
26
|
-
out += ID_ALPHABET[buf[i] % ID_ALPHABET.length];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return `rec_${out}`;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* A record that does not exist yet, named before it does.
|
|
33
|
-
*
|
|
34
|
-
* The problem this removes: when the server mints the id, a surface editing a new
|
|
35
|
-
* record has nothing to identify it by until the first write returns. Everything
|
|
36
|
-
* keyed on that id — the route, the drawer, a list selection — therefore changes
|
|
37
|
-
* identity mid-edit, which React resolves by remounting the surface the user is
|
|
38
|
-
* typing into. Minting the id locally makes it stable from the first render, so the
|
|
39
|
-
* create stops being an event the UI has to survive.
|
|
40
|
-
*
|
|
41
|
-
* Creation still happens on the first write, not on mount: a surface the user opens
|
|
42
|
-
* and abandons should leave nothing behind.
|
|
43
|
-
*
|
|
44
|
-
* The transport is the caller's. Like `useOptimistic`, this hook has no idea how the
|
|
45
|
-
* app persists anything — it takes `create` and `update` thunks and owns only the id
|
|
46
|
-
* and the ordering, which is the part that is easy to get wrong:
|
|
47
|
-
*
|
|
48
|
-
* - Two blur-saves fired before the first resolves must not both create. That is a
|
|
49
|
-
* duplicate record, and it is the failure this exists to prevent.
|
|
50
|
-
* - A save arriving mid-create must wait for it, or it updates a row that is not
|
|
51
|
-
* there yet.
|
|
52
|
-
* - A create that FAILS must not latch. Otherwise every later save updates a record
|
|
53
|
-
* that was never written, and the user's work goes nowhere while looking saved.
|
|
54
|
-
*
|
|
55
|
-
* ```tsx
|
|
56
|
-
* const { id, save } = useNewRecord({
|
|
57
|
-
* create: (id, patch) => createCustomer({ record_id: id, ...patch }),
|
|
58
|
-
* update: (id, patch) => updateCustomer({ record_id: id, ...patch }),
|
|
59
|
-
* onCreated: (id) => select(id), // it exists now — the list re-reads itself
|
|
60
|
-
* });
|
|
61
|
-
* <InlineText onBlur={(name) => save({ name })} />
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export function useNewRecord(opts) {
|
|
65
|
-
const idRef = useRef("");
|
|
66
|
-
if (idRef.current === "")
|
|
67
|
-
idRef.current = newRecordId();
|
|
68
|
-
// Callbacks are read through a ref so `save` keeps a stable identity across
|
|
69
|
-
// renders — it is typically handed to an `onBlur`, and a new function every render
|
|
70
|
-
// would re-bind every field on every keystroke.
|
|
71
|
-
//
|
|
72
|
-
// Updated in an effect rather than during render: React only sanctions writing a
|
|
73
|
-
// ref while rendering for one-time initialisation (as `idRef` above does), because
|
|
74
|
-
// a render that is discarded would otherwise leave the ref holding props that never
|
|
75
|
-
// committed. `save` runs from event handlers, which are always after commit, so the
|
|
76
|
-
// effect is early enough.
|
|
77
|
-
const optsRef = useRef(opts);
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
optsRef.current = opts;
|
|
80
|
-
});
|
|
81
|
-
/** The in-flight or settled create. `null` means the record does not exist yet. */
|
|
82
|
-
const createdRef = useRef(null);
|
|
83
|
-
/** Tail of the write chain, so saves apply in the order the user made them. */
|
|
84
|
-
const queueRef = useRef(Promise.resolve());
|
|
85
|
-
const save = useCallback((patch) => {
|
|
86
|
-
const run = async () => {
|
|
87
|
-
const id = idRef.current;
|
|
88
|
-
const { create, update, onCreated } = optsRef.current;
|
|
89
|
-
if (createdRef.current === null) {
|
|
90
|
-
const attempt = (async () => {
|
|
91
|
-
await create(id, patch);
|
|
92
|
-
})();
|
|
93
|
-
createdRef.current = attempt;
|
|
94
|
-
try {
|
|
95
|
-
await attempt;
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
// Unlatch. The record was not written, so the next save has to be free to
|
|
99
|
-
// create it — leaving the promise in place would send every later write to
|
|
100
|
-
// a row that does not exist.
|
|
101
|
-
createdRef.current = null;
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
onCreated?.(id);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
await createdRef.current;
|
|
108
|
-
await update(id, patch);
|
|
109
|
-
};
|
|
110
|
-
// Chained on both settle paths: one failed save must not strand the ones behind
|
|
111
|
-
// it, but they still have to run in order.
|
|
112
|
-
const result = queueRef.current.then(run, run);
|
|
113
|
-
queueRef.current = result.catch(() => undefined);
|
|
114
|
-
return result;
|
|
115
|
-
}, []);
|
|
116
|
-
return { id: idRef.current, save };
|
|
117
|
-
}
|
package/dist/src/open_app.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open a record's screen in the sibling app that owns it — the cross-app hop.
|
|
3
|
-
* The host owns app routing and lands the viewer on `route` inside `appId`,
|
|
4
|
-
* same tab, chrome kept. `route` is the target app's own in-app path, exactly
|
|
5
|
-
* as its router declares it (`/` for its register).
|
|
6
|
-
*
|
|
7
|
-
* ```tsx
|
|
8
|
-
* import { openApp } from "@lotics/app-sdk";
|
|
9
|
-
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
10
|
-
* ```
|
|
11
|
-
*
|
|
12
|
-
* Standalone (`<slug>.lotics.app`) has no sibling apps, so the call rejects
|
|
13
|
-
* there — gate the control on `isEmbedded()`.
|
|
14
|
-
*/
|
|
15
|
-
export declare function openApp(appId: string, route?: string): Promise<void>;
|
package/dist/src/open_app.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { rpc } from "./rpc.js";
|
|
2
|
-
/**
|
|
3
|
-
* Open a record's screen in the sibling app that owns it — the cross-app hop.
|
|
4
|
-
* The host owns app routing and lands the viewer on `route` inside `appId`,
|
|
5
|
-
* same tab, chrome kept. `route` is the target app's own in-app path, exactly
|
|
6
|
-
* as its router declares it (`/` for its register).
|
|
7
|
-
*
|
|
8
|
-
* ```tsx
|
|
9
|
-
* import { openApp } from "@lotics/app-sdk";
|
|
10
|
-
* await openApp(PLANNING_APP_ID, `/plan/${record.id}`);
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* Standalone (`<slug>.lotics.app`) has no sibling apps, so the call rejects
|
|
14
|
-
* there — gate the control on `isEmbedded()`.
|
|
15
|
-
*/
|
|
16
|
-
export function openApp(appId, route = "/") {
|
|
17
|
-
return rpc("openApp", { app_id: appId, route });
|
|
18
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open an external URL in a new tab.
|
|
3
|
-
*
|
|
4
|
-
* The embedded app iframe is sandboxed without `allow-popups`, so a direct
|
|
5
|
-
* `window.open` from app code is silently dropped. This routes the open to
|
|
6
|
-
* whoever can actually perform it: the un-sandboxed host frame (embedded) or
|
|
7
|
-
* the app's own top-level page (public). The URL is scheme-validated
|
|
8
|
-
* (`http`/`https` only) at the point it opens — a non-string or disallowed
|
|
9
|
-
* scheme rejects.
|
|
10
|
-
*
|
|
11
|
-
* ```tsx
|
|
12
|
-
* import { openExternal } from "@lotics/app-sdk";
|
|
13
|
-
* await openExternal(invoiceUrl);
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
export declare function openExternal(url: string): Promise<void>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { rpc } from "./rpc.js";
|
|
2
|
-
/**
|
|
3
|
-
* Open an external URL in a new tab.
|
|
4
|
-
*
|
|
5
|
-
* The embedded app iframe is sandboxed without `allow-popups`, so a direct
|
|
6
|
-
* `window.open` from app code is silently dropped. This routes the open to
|
|
7
|
-
* whoever can actually perform it: the un-sandboxed host frame (embedded) or
|
|
8
|
-
* the app's own top-level page (public). The URL is scheme-validated
|
|
9
|
-
* (`http`/`https` only) at the point it opens — a non-string or disallowed
|
|
10
|
-
* scheme rejects.
|
|
11
|
-
*
|
|
12
|
-
* ```tsx
|
|
13
|
-
* import { openExternal } from "@lotics/app-sdk";
|
|
14
|
-
* await openExternal(invoiceUrl);
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
export function openExternal(url) {
|
|
18
|
-
return rpc("openExternal", { url });
|
|
19
|
-
}
|
package/dist/src/recording.d.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { type RecordingState } from "./recording_state.js";
|
|
2
|
-
import type { AppWorkflows } from "./types.js";
|
|
3
|
-
/** The platform fills a receiving workflow's `recording` input; the app passes
|
|
4
|
-
* the rest of what the workflow declares. */
|
|
5
|
-
type ActInputsOf<K extends keyof AppWorkflows & string> = AppWorkflows[K] extends Record<string, unknown> ? Omit<AppWorkflows[K], "recording"> : Record<string, unknown>;
|
|
6
|
-
export interface UseRecording<I> {
|
|
7
|
-
/**
|
|
8
|
-
* Whether this surface can record. False standalone, in a host that
|
|
9
|
-
* predates recording, on an instance without transcription, and until the
|
|
10
|
-
* host's context has answered. A control that starts a recording is drawn
|
|
11
|
-
* only when this is true.
|
|
12
|
-
*/
|
|
13
|
-
available: boolean;
|
|
14
|
-
/** The most recent recording this app started through this alias. */
|
|
15
|
-
state: RecordingState;
|
|
16
|
-
/** Whether a recording this app started is live, through any alias. The
|
|
17
|
-
* host records one at a time, so a start while this is true is refused. */
|
|
18
|
-
busy: boolean;
|
|
19
|
-
/**
|
|
20
|
-
* Ask the host to record. The host opens its own capture dialog, and nothing
|
|
21
|
-
* records until the member presses Start there. Resolves `{ started: true }`
|
|
22
|
-
* once capture runs, `{ started: false }` when the member closed the dialog;
|
|
23
|
-
* rejects, with the reason, when the host refuses (another recording is
|
|
24
|
-
* running, the inputs fail the workflow's contract, recording is unavailable).
|
|
25
|
-
* When the recording is transcribed, the host runs the alias's workflow with
|
|
26
|
-
* these inputs plus `recording`.
|
|
27
|
-
*/
|
|
28
|
-
start: {} extends I ? (inputs?: I) => Promise<{
|
|
29
|
-
started: boolean;
|
|
30
|
-
}> : (inputs: I) => Promise<{
|
|
31
|
-
started: boolean;
|
|
32
|
-
}>;
|
|
33
|
-
/** Stop this alias's live recording. Resolves whether or not one was live. */
|
|
34
|
-
stop: () => Promise<void>;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Record a call, a visit, a meeting — and file it through a workflow.
|
|
38
|
-
*
|
|
39
|
-
* `alias` names a workflow that declares the platform's `recording` input; the
|
|
40
|
-
* host records as the signed-in member and, once the transcript is ready, runs
|
|
41
|
-
* that workflow once with the app's inputs and the recording. Its write reaches
|
|
42
|
-
* the screen like any workflow's.
|
|
43
|
-
*
|
|
44
|
-
* ```tsx
|
|
45
|
-
* const visit = useRecording("log_visit");
|
|
46
|
-
* if (!visit.available) return null;
|
|
47
|
-
* if (visit.state.phase === "live" && visit.state.inputs.site === siteId) {
|
|
48
|
-
* return <Button onPress={() => visit.stop()}>Stop</Button>;
|
|
49
|
-
* }
|
|
50
|
-
* return (
|
|
51
|
-
* <Button disabled={visit.busy} onPress={() => visit.start({ site: siteId })}>
|
|
52
|
-
* Record the visit
|
|
53
|
-
* </Button>
|
|
54
|
-
* );
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export declare function useRecording<K extends keyof AppWorkflows & string>(alias: K): UseRecording<ActInputsOf<K>>;
|
|
58
|
-
export declare function useRecording(alias: string): UseRecording<Record<string, unknown>>;
|
|
59
|
-
export {};
|
package/dist/src/recording.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
-
import { rpc } from "./rpc.js";
|
|
3
|
-
import { getMockRecordings } from "./mock.js";
|
|
4
|
-
import { useAppContext } from "./viewer.js";
|
|
5
|
-
import { anyRecordingLive, recordingStateOf, subscribeRecordings, } from "./recording_state.js";
|
|
6
|
-
const NO_MOCKS = {};
|
|
7
|
-
export function useRecording(alias) {
|
|
8
|
-
const { recordingEnabled } = useAppContext();
|
|
9
|
-
const mocks = getMockRecordings() ?? NO_MOCKS;
|
|
10
|
-
const mocked = mocks[alias];
|
|
11
|
-
const hostState = useSyncExternalStore(subscribeRecordings, () => recordingStateOf(alias));
|
|
12
|
-
const hostBusy = useSyncExternalStore(subscribeRecordings, anyRecordingLive);
|
|
13
|
-
const start = useCallback(async (inputs) => {
|
|
14
|
-
if (getMockRecordings()?.[alias])
|
|
15
|
-
return { started: true };
|
|
16
|
-
return rpc("recording.start", { alias, inputs: inputs ?? {} });
|
|
17
|
-
}, [alias]);
|
|
18
|
-
const stop = useCallback(async () => {
|
|
19
|
-
if (getMockRecordings()?.[alias])
|
|
20
|
-
return;
|
|
21
|
-
await rpc("recording.stop", { alias });
|
|
22
|
-
}, [alias]);
|
|
23
|
-
return {
|
|
24
|
-
available: mocked !== undefined || recordingEnabled,
|
|
25
|
-
state: mocked ?? hostState,
|
|
26
|
-
busy: hostBusy || Object.values(mocks).some((state) => state.phase === "live"),
|
|
27
|
-
start,
|
|
28
|
-
stop,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the host says about the recordings THIS app started, one per workflow
|
|
3
|
-
* alias: the most recently started recording through that alias.
|
|
4
|
-
*
|
|
5
|
-
* A leaf module, because two sides write it and one reads it: the bridge
|
|
6
|
-
* listener in `rpc.ts` applies the host's `recordingChanged` push, the context
|
|
7
|
-
* fetch in `viewer.ts` applies the snapshot a newly loaded frame starts from,
|
|
8
|
-
* and `useRecording` reads it. Both writes arrive on the one postMessage channel
|
|
9
|
-
* in the order the host sent them, so the later arrival is always the newer
|
|
10
|
-
* state.
|
|
11
|
-
*/
|
|
12
|
-
/** The act's own inputs the recording was started with — what `start` was
|
|
13
|
-
* passed, echoed back so a screen drawing one act per row can tell which row
|
|
14
|
-
* the recording is about. */
|
|
15
|
-
export type RecordingInputs = Record<string, unknown>;
|
|
16
|
-
/**
|
|
17
|
-
* One recording, from the host's side. `idle`: nothing of this alias's is
|
|
18
|
-
* running or waiting. `live`: capturing. `interrupted`: the tab closed mid-call
|
|
19
|
-
* and the member has not yet resumed or finished it from the recording bar.
|
|
20
|
-
* `processing`: stopped, the transcript is on its way. `filed`: the workflow
|
|
21
|
-
* ran and succeeded. `failed`: transcription failed, the platform refused to
|
|
22
|
-
* file (the member lost access, the alias or its declaration changed), or the
|
|
23
|
-
* run failed; the recording is kept, and the member retries from the recording
|
|
24
|
-
* bar.
|
|
25
|
-
*/
|
|
26
|
-
export type RecordingState = {
|
|
27
|
-
phase: "idle";
|
|
28
|
-
} | {
|
|
29
|
-
phase: "live";
|
|
30
|
-
elapsed_seconds: number;
|
|
31
|
-
inputs: RecordingInputs;
|
|
32
|
-
} | {
|
|
33
|
-
phase: "interrupted";
|
|
34
|
-
elapsed_seconds: number;
|
|
35
|
-
inputs: RecordingInputs;
|
|
36
|
-
} | {
|
|
37
|
-
phase: "processing";
|
|
38
|
-
inputs: RecordingInputs;
|
|
39
|
-
} | {
|
|
40
|
-
phase: "filed";
|
|
41
|
-
execution_id: string;
|
|
42
|
-
inputs: RecordingInputs;
|
|
43
|
-
} | {
|
|
44
|
-
phase: "failed";
|
|
45
|
-
message: string;
|
|
46
|
-
inputs: RecordingInputs;
|
|
47
|
-
};
|
|
48
|
-
export declare const IDLE_RECORDING: RecordingState;
|
|
49
|
-
export declare function subscribeRecordings(listener: () => void): () => void;
|
|
50
|
-
export declare function recordingStateOf(alias: string): RecordingState;
|
|
51
|
-
export declare function anyRecordingLive(): boolean;
|
|
52
|
-
/** The host's push for one alias. */
|
|
53
|
-
export declare function applyRecordingChange(alias: string, state: RecordingState): void;
|
|
54
|
-
/** The snapshot `context` carries: every alias it omits is idle. */
|
|
55
|
-
export declare function replaceRecordings(snapshot: Record<string, RecordingState>): void;
|
|
56
|
-
/** A host message's `state`, or null when it is not one. */
|
|
57
|
-
export declare function readRecordingState(raw: unknown): RecordingState | null;
|
|
58
|
-
/** A context's `recordings`, keeping each well-formed entry. */
|
|
59
|
-
export declare function readRecordingSnapshot(raw: unknown): Record<string, RecordingState>;
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the host says about the recordings THIS app started, one per workflow
|
|
3
|
-
* alias: the most recently started recording through that alias.
|
|
4
|
-
*
|
|
5
|
-
* A leaf module, because two sides write it and one reads it: the bridge
|
|
6
|
-
* listener in `rpc.ts` applies the host's `recordingChanged` push, the context
|
|
7
|
-
* fetch in `viewer.ts` applies the snapshot a newly loaded frame starts from,
|
|
8
|
-
* and `useRecording` reads it. Both writes arrive on the one postMessage channel
|
|
9
|
-
* in the order the host sent them, so the later arrival is always the newer
|
|
10
|
-
* state.
|
|
11
|
-
*/
|
|
12
|
-
export const IDLE_RECORDING = { phase: "idle" };
|
|
13
|
-
const states = new Map();
|
|
14
|
-
const listeners = new Set();
|
|
15
|
-
function emit() {
|
|
16
|
-
for (const listener of listeners)
|
|
17
|
-
listener();
|
|
18
|
-
}
|
|
19
|
-
export function subscribeRecordings(listener) {
|
|
20
|
-
listeners.add(listener);
|
|
21
|
-
return () => {
|
|
22
|
-
listeners.delete(listener);
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
export function recordingStateOf(alias) {
|
|
26
|
-
return states.get(alias) ?? IDLE_RECORDING;
|
|
27
|
-
}
|
|
28
|
-
export function anyRecordingLive() {
|
|
29
|
-
for (const state of states.values())
|
|
30
|
-
if (state.phase === "live")
|
|
31
|
-
return true;
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
/** The host's push for one alias. */
|
|
35
|
-
export function applyRecordingChange(alias, state) {
|
|
36
|
-
if (state.phase === "idle")
|
|
37
|
-
states.delete(alias);
|
|
38
|
-
else
|
|
39
|
-
states.set(alias, state);
|
|
40
|
-
emit();
|
|
41
|
-
}
|
|
42
|
-
/** The snapshot `context` carries: every alias it omits is idle. */
|
|
43
|
-
export function replaceRecordings(snapshot) {
|
|
44
|
-
states.clear();
|
|
45
|
-
for (const [alias, state] of Object.entries(snapshot)) {
|
|
46
|
-
if (state.phase !== "idle")
|
|
47
|
-
states.set(alias, state);
|
|
48
|
-
}
|
|
49
|
-
emit();
|
|
50
|
-
}
|
|
51
|
-
function isRecord(value) {
|
|
52
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
53
|
-
}
|
|
54
|
-
/** A host message's `state`, or null when it is not one. */
|
|
55
|
-
export function readRecordingState(raw) {
|
|
56
|
-
if (!isRecord(raw))
|
|
57
|
-
return null;
|
|
58
|
-
if (raw.phase === "idle")
|
|
59
|
-
return IDLE_RECORDING;
|
|
60
|
-
const inputs = raw.inputs;
|
|
61
|
-
if (!isRecord(inputs))
|
|
62
|
-
return null;
|
|
63
|
-
switch (raw.phase) {
|
|
64
|
-
case "live":
|
|
65
|
-
case "interrupted":
|
|
66
|
-
return typeof raw.elapsed_seconds === "number"
|
|
67
|
-
? { phase: raw.phase, elapsed_seconds: raw.elapsed_seconds, inputs }
|
|
68
|
-
: null;
|
|
69
|
-
case "processing":
|
|
70
|
-
return { phase: "processing", inputs };
|
|
71
|
-
case "filed":
|
|
72
|
-
return typeof raw.execution_id === "string"
|
|
73
|
-
? { phase: "filed", execution_id: raw.execution_id, inputs }
|
|
74
|
-
: null;
|
|
75
|
-
case "failed":
|
|
76
|
-
return typeof raw.message === "string" ? { phase: "failed", message: raw.message, inputs } : null;
|
|
77
|
-
default:
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/** A context's `recordings`, keeping each well-formed entry. */
|
|
82
|
-
export function readRecordingSnapshot(raw) {
|
|
83
|
-
if (!isRecord(raw))
|
|
84
|
-
return {};
|
|
85
|
-
const entries = Object.entries(raw).flatMap(([alias, value]) => {
|
|
86
|
-
const state = readRecordingState(value);
|
|
87
|
-
if (state === null) {
|
|
88
|
-
console.error(`The host's context carried a malformed recording for "${alias}"; it reads as idle.`, value);
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
return [[alias, state]];
|
|
92
|
-
});
|
|
93
|
-
return Object.fromEntries(entries);
|
|
94
|
-
}
|
package/dist/src/router.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type RouteObject } from "react-router";
|
|
2
|
-
/**
|
|
3
|
-
* The not-found screen's words. The SDK ships no locale, so an app whose reader
|
|
4
|
-
* does not read English passes its own — from `@lotics/ui`'s locale where the app
|
|
5
|
-
* uses the kit, from its own strings otherwise.
|
|
6
|
-
*/
|
|
7
|
-
export interface NotFoundWords {
|
|
8
|
-
/** Names the address that has no screen. Takes the path so the words may put
|
|
9
|
-
* it anywhere the language needs it. */
|
|
10
|
-
message: (path: string) => string;
|
|
11
|
-
/** The label of the link back to the first screen. */
|
|
12
|
-
firstScreen: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function AppRouter({ routes, notFound }: {
|
|
15
|
-
routes: RouteObject[];
|
|
16
|
-
notFound?: NotFoundWords;
|
|
17
|
-
}): import("react").JSX.Element;
|
package/dist/src/router.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* In-app routing for custom-code apps — `AppRouter` lets an app use react-router
|
|
4
|
-
* normally (`useNavigate`, `useParams`, `<Link>`) while its screens become real,
|
|
5
|
-
* addressable URLs. The app owns its OWN url (a plain browser history) in both
|
|
6
|
-
* modes; the host url only ever *mirrors* the screen, it never drives the router:
|
|
7
|
-
*
|
|
8
|
-
* - **Standalone** (the app's own origin): the page's own browser history — real
|
|
9
|
-
* path URLs, native browser back/forward, deep-link/refresh via the app host's
|
|
10
|
-
* SPA fallback.
|
|
11
|
-
* - **Embedded** (inside the Lotics host): the app drives the IFRAME's own url
|
|
12
|
-
* (the iframe is same-origin to itself) via `pushState`. The user sees the
|
|
13
|
-
* host's address bar, never the iframe's, and the host never sees the iframe's
|
|
14
|
-
* url — so it never navigates and never remounts the iframe. (Reflecting the
|
|
15
|
-
* screen into the host url with a *navigation* is what used to remount the app
|
|
16
|
-
* iframe and reload the whole app on every in-app navigation.) The iframe's
|
|
17
|
-
* history still participates in the session history, so browser Back/Forward
|
|
18
|
-
* walk app screens (then leave the app).
|
|
19
|
-
*
|
|
20
|
-
* To stay shareable + refresh-survivable, `AppRouter` *mirrors* the current
|
|
21
|
-
* screen into the host url under `_loc` via `setUrlParams` — a non-remounting
|
|
22
|
-
* `history.replaceState` on the host, never a navigation. The READ half is the
|
|
23
|
-
* host's job: on (re)load it bakes `_loc` into the iframe src, so the app boots
|
|
24
|
-
* *directly* at the saved screen. There's deliberately no async seed here —
|
|
25
|
-
* that would race the app's own first navigation; the iframe's initial url is
|
|
26
|
-
* the source of truth, set synchronously by the host.
|
|
27
|
-
*
|
|
28
|
-
* Shipped as a separate entry (`@lotics/app-sdk/router`) so apps that don't route
|
|
29
|
-
* never pull react-router into their bundle:
|
|
30
|
-
*
|
|
31
|
-
* import { AppRouter } from "@lotics/app-sdk/router";
|
|
32
|
-
* export default function App() {
|
|
33
|
-
* return <AppRouter routes={[
|
|
34
|
-
* { path: "/", element: <List /> },
|
|
35
|
-
* { path: "/item/:id", element: <Detail /> },
|
|
36
|
-
* ]} />;
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
|
-
* An address none of the routes claim renders the SDK's not-found screen rather
|
|
40
|
-
* than nothing — see {@link NotFoundScreen} and {@link withNotFound}. Its words
|
|
41
|
-
* are the `notFound` prop ({@link NotFoundWords}), because an app's reader reads
|
|
42
|
-
* the app's language and the SDK ships no locale.
|
|
43
|
-
*/
|
|
44
|
-
import { useEffect } from "react";
|
|
45
|
-
import { BrowserRouter, Link, useLocation, useRoutes, } from "react-router";
|
|
46
|
-
import { isEmbedded, setUrlParams } from "./rpc.js";
|
|
47
|
-
/** Host query key carrying the app's current screen, so it's shareable and the
|
|
48
|
-
* host can restore it on refresh. */
|
|
49
|
-
const LOC_KEY = "_loc";
|
|
50
|
-
/** The host handshake param the host puts on the iframe src — present on the
|
|
51
|
-
* initial url only, never part of a route, so it's stripped from the mirror. */
|
|
52
|
-
const HOST_KEY = "lotics_host";
|
|
53
|
-
function screenHref(loc) {
|
|
54
|
-
const search = new URLSearchParams(loc.search);
|
|
55
|
-
search.delete(HOST_KEY);
|
|
56
|
-
const qs = search.toString();
|
|
57
|
-
return loc.pathname + (qs ? `?${qs}` : "") + loc.hash;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Embedded only: mirror the current screen into the host url under `_loc` — a
|
|
61
|
-
* non-remounting `setUrlParams` → `history.replaceState`, never a navigation, so
|
|
62
|
-
* it never reloads the app. Write-only by design: the host reads `_loc` back and
|
|
63
|
-
* bakes it into the iframe src on (re)load, so the app already boots at the saved
|
|
64
|
-
* screen — no async read here, hence no seed-vs-navigation race. Standalone needs
|
|
65
|
-
* none of this — the app's own url already IS the screen.
|
|
66
|
-
*/
|
|
67
|
-
function HostScreenMirror() {
|
|
68
|
-
const location = useLocation();
|
|
69
|
-
const href = screenHref(location);
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
void setUrlParams({ [LOC_KEY]: href });
|
|
72
|
-
}, [href]);
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
function RoutedRoutes({ routes }) {
|
|
76
|
-
return useRoutes(routes);
|
|
77
|
-
}
|
|
78
|
-
/*
|
|
79
|
-
* Shaped like the kit's `RegionState` — a centred message and one destination —
|
|
80
|
-
* and written in its tokens with literal fallbacks, so it takes the app's theme
|
|
81
|
-
* where `@lotics/ui/styles.css` is loaded and stays legible where it is not. It
|
|
82
|
-
* cannot BE `RegionState`: the SDK ships no kit component.
|
|
83
|
-
*/
|
|
84
|
-
const NOT_FOUND_ROOT = {
|
|
85
|
-
display: "flex",
|
|
86
|
-
flexDirection: "column",
|
|
87
|
-
alignItems: "center",
|
|
88
|
-
justifyContent: "center",
|
|
89
|
-
gap: "var(--lotics-space-8, 8px)",
|
|
90
|
-
paddingBlock: "var(--lotics-space-48, 48px)",
|
|
91
|
-
paddingInline: "var(--lotics-space-16, 16px)",
|
|
92
|
-
textAlign: "center",
|
|
93
|
-
fontFamily: "var(--font-sans, system-ui, sans-serif)",
|
|
94
|
-
};
|
|
95
|
-
const NOT_FOUND_MESSAGE = {
|
|
96
|
-
margin: 0,
|
|
97
|
-
fontSize: "var(--lotics-text-sm, 14px)",
|
|
98
|
-
color: "var(--lotics-ink-muted, #71717a)",
|
|
99
|
-
};
|
|
100
|
-
const NOT_FOUND_LINK = {
|
|
101
|
-
fontSize: "var(--lotics-text-sm, 14px)",
|
|
102
|
-
color: "var(--lotics-accent, #2563eb)",
|
|
103
|
-
};
|
|
104
|
-
const NOT_FOUND_ENGLISH = {
|
|
105
|
-
message: (path) => `No screen at ${path}`,
|
|
106
|
-
firstScreen: "Go to the first screen",
|
|
107
|
-
};
|
|
108
|
-
/**
|
|
109
|
-
* What an app shows at an address none of its routes claim. Without it
|
|
110
|
-
* `useRoutes` matches nothing and the page renders EMPTY — no message and no
|
|
111
|
-
* console error — so a stale link or a typo reads as a crash.
|
|
112
|
-
*/
|
|
113
|
-
function NotFoundScreen({ home, words }) {
|
|
114
|
-
const { pathname } = useLocation();
|
|
115
|
-
return (_jsxs("div", { style: NOT_FOUND_ROOT, children: [
|
|
116
|
-
_jsx("p", { style: NOT_FOUND_MESSAGE, children: words.message(pathname) }), _jsx(Link, { to: home, style: NOT_FOUND_LINK, children: words.firstScreen })
|
|
117
|
-
] }));
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* The catch-all, appended at EVERY level of the tree: a route with `children`
|
|
121
|
-
* is a layout, and a catch-all among those children is what keeps that layout's
|
|
122
|
-
* shell on screen instead of swapping the whole page for the message. An app
|
|
123
|
-
* that declares its own `*` still wins — react-router ranks equal matches by
|
|
124
|
-
* declaration order and ours is appended last.
|
|
125
|
-
*/
|
|
126
|
-
function withNotFound(routes, element) {
|
|
127
|
-
return [
|
|
128
|
-
...routes.map((route) => route.children === undefined
|
|
129
|
-
? route
|
|
130
|
-
: { ...route, children: withNotFound(route.children, element) }),
|
|
131
|
-
{ path: "*", element },
|
|
132
|
-
];
|
|
133
|
-
}
|
|
134
|
-
/** The screen the not-found sends the reader back to — the first one declared. */
|
|
135
|
-
function firstScreenPath(routes) {
|
|
136
|
-
const first = routes.find((route) => route.path !== undefined && route.path !== "*");
|
|
137
|
-
return first?.path ?? "/";
|
|
138
|
-
}
|
|
139
|
-
export function AppRouter({ routes, notFound = NOT_FOUND_ENGLISH, }) {
|
|
140
|
-
// `isEmbedded()` reads the `?lotics_host=` the host puts on the iframe src, so
|
|
141
|
-
// it's known synchronously at first render.
|
|
142
|
-
const embedded = isEmbedded();
|
|
143
|
-
return (_jsxs(BrowserRouter, { children: [embedded ? _jsx(HostScreenMirror, {}) : null, _jsx(RoutedRoutes, { routes: withNotFound(routes, _jsx(NotFoundScreen, { home: firstScreenPath(routes), words: notFound })) })] }));
|
|
144
|
-
}
|