@lotics/app-sdk 0.100.1 → 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 +92 -62
- 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 -48
- 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/select.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reader for `select` cells in `useQuery` rows.
|
|
3
|
-
*
|
|
4
|
-
* The server (`backend/lib/select_option_resolver.ts`) rewrites every
|
|
5
|
-
* `select` column from its storage shape (`string[]` of bare `opt_*` keys)
|
|
6
|
-
* into `ResolvedOption[]` before the row reaches the app. Apps used to
|
|
7
|
-
* hardcode an `opt_* → label` map because the SDK didn't expose the resolved
|
|
8
|
-
* shape; this helper makes the right shape the obvious one.
|
|
9
|
-
*
|
|
10
|
-
* If the wire format changes, the resolver and this reader move together.
|
|
11
|
-
*/
|
|
12
|
-
export interface ResolvedOption {
|
|
13
|
-
key: string;
|
|
14
|
-
/** Option display name. Falls back to the key when the option was deleted
|
|
15
|
-
* after the cell was written — surfaces the stale state explicitly. */
|
|
16
|
-
label: string;
|
|
17
|
-
/**
|
|
18
|
-
* Named palette color token (e.g. `"blue"`, `"emerald"`). Populated by
|
|
19
|
-
* `useFieldOptions` (which reads the field config); absent on options read
|
|
20
|
-
* back from a query CELL via `readSelect` — a cell carries only key + label.
|
|
21
|
-
* Pass the resolved option straight to `@lotics/ui`'s `Status`, which
|
|
22
|
-
* degrades a missing/unknown token to a neutral badge.
|
|
23
|
-
*/
|
|
24
|
-
color?: string;
|
|
25
|
-
/**
|
|
26
|
-
* The option's own mark, where its field gives one to every option — the
|
|
27
|
-
* channel it IS (`brand`, a `BrandMark` name) or a glyph (`icon`, a kit icon
|
|
28
|
-
* name). Populated by `useFieldOptions` like `color`, absent on a cell. Pass
|
|
29
|
-
* the option to `@lotics/ui`'s `Status`, which draws it in the dot's place and
|
|
30
|
-
* degrades a name its build does not draw to the dot.
|
|
31
|
-
*/
|
|
32
|
-
mark?: {
|
|
33
|
-
kind: "brand";
|
|
34
|
-
name: string;
|
|
35
|
-
} | {
|
|
36
|
-
kind: "icon";
|
|
37
|
-
name: string;
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Parse a `useQuery` cell value into `ResolvedOption[]`. Returns `[]` for
|
|
42
|
-
* null/undefined/empty cells and for any unexpected shape — callers iterate
|
|
43
|
-
* uniformly without null-checks. A bare-string entry (a column whose source
|
|
44
|
-
* options couldn't be resolved server-side) becomes `{ key, label: key }` so
|
|
45
|
-
* single-value reads still work. Entries that fail the shape check are
|
|
46
|
-
* dropped silently rather than corrupting the array with partial data.
|
|
47
|
-
*/
|
|
48
|
-
export declare function readSelect(value: unknown): ResolvedOption[];
|
package/dist/src/select.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reader for `select` cells in `useQuery` rows.
|
|
3
|
-
*
|
|
4
|
-
* The server (`backend/lib/select_option_resolver.ts`) rewrites every
|
|
5
|
-
* `select` column from its storage shape (`string[]` of bare `opt_*` keys)
|
|
6
|
-
* into `ResolvedOption[]` before the row reaches the app. Apps used to
|
|
7
|
-
* hardcode an `opt_* → label` map because the SDK didn't expose the resolved
|
|
8
|
-
* shape; this helper makes the right shape the obvious one.
|
|
9
|
-
*
|
|
10
|
-
* If the wire format changes, the resolver and this reader move together.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Parse a `useQuery` cell value into `ResolvedOption[]`. Returns `[]` for
|
|
14
|
-
* null/undefined/empty cells and for any unexpected shape — callers iterate
|
|
15
|
-
* uniformly without null-checks. A bare-string entry (a column whose source
|
|
16
|
-
* options couldn't be resolved server-side) becomes `{ key, label: key }` so
|
|
17
|
-
* single-value reads still work. Entries that fail the shape check are
|
|
18
|
-
* dropped silently rather than corrupting the array with partial data.
|
|
19
|
-
*/
|
|
20
|
-
export function readSelect(value) {
|
|
21
|
-
if (!Array.isArray(value))
|
|
22
|
-
return [];
|
|
23
|
-
const out = [];
|
|
24
|
-
for (const entry of value) {
|
|
25
|
-
if (typeof entry === "string") {
|
|
26
|
-
if (entry !== "")
|
|
27
|
-
out.push({ key: entry, label: entry });
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (!entry || typeof entry !== "object")
|
|
31
|
-
continue;
|
|
32
|
-
const obj = entry;
|
|
33
|
-
const key = obj.key;
|
|
34
|
-
if (typeof key !== "string" || key === "")
|
|
35
|
-
continue;
|
|
36
|
-
const label = typeof obj.label === "string" ? obj.label : key;
|
|
37
|
-
out.push({ key, label });
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
package/dist/src/types.d.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App-specific workflow augmentation point.
|
|
3
|
-
*
|
|
4
|
-
* The base SDK ships `AppWorkflows` empty — `useWorkflow(alias)` accepts any
|
|
5
|
-
* string at runtime. Per-app codegen (folded into `lotics app pull`) emits a
|
|
6
|
-
* file that augments this interface with the aliases declared in the app's
|
|
7
|
-
* `package.json` lotics.workflows, giving compile-time autocomplete +
|
|
8
|
-
* "undeclared alias" errors:
|
|
9
|
-
*
|
|
10
|
-
* ```ts
|
|
11
|
-
* // .lotics/app_workflows.d.ts (generated)
|
|
12
|
-
* import "@lotics/app-sdk";
|
|
13
|
-
* declare module "@lotics/app-sdk" {
|
|
14
|
-
* interface AppWorkflows {
|
|
15
|
-
* "issueInvoiceStorageDrop": { record_id: string };
|
|
16
|
-
* "issueInvoiceReuseLift": Record<string, never>;
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* The value type is the workflow's declared input shape — `Record<string, never>`
|
|
22
|
-
* for a workflow that takes no typed inputs, the typed object otherwise.
|
|
23
|
-
*/
|
|
24
|
-
export interface AppWorkflows {
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* App-specific workflow-RESULT augmentation point. Same pattern as AppWorkflows,
|
|
28
|
-
* but maps each alias to the type of the `data` its workflow returns via
|
|
29
|
-
* `return({ data })` — derived from the alias's declared `outputs` schema.
|
|
30
|
-
*
|
|
31
|
-
* The base SDK ships it empty, so `useWorkflow(alias)` resolves `result.data` as
|
|
32
|
-
* `unknown`. Per-app codegen augments it for aliases that declare `outputs`:
|
|
33
|
-
*
|
|
34
|
-
* ```ts
|
|
35
|
-
* // .lotics/app_workflows.d.ts (generated)
|
|
36
|
-
* declare module "@lotics/app-sdk" {
|
|
37
|
-
* interface AppWorkflowResults {
|
|
38
|
-
* "computeQuote": { total: number; lines: ReadonlyArray<{ name: string; amount: number }> };
|
|
39
|
-
* }
|
|
40
|
-
* }
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
|
-
* An alias absent from this map (no declared `outputs`) gets `result.data: unknown`.
|
|
44
|
-
*/
|
|
45
|
-
export interface AppWorkflowResults {
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* App-specific named-query augmentation point. Same pattern as AppWorkflows.
|
|
49
|
-
*
|
|
50
|
-
* The base SDK ships `AppQueries` empty — `useQuery(alias)` accepts any string
|
|
51
|
-
* at runtime. Per-app codegen (`lotics app pull`) emits a file that augments
|
|
52
|
-
* this interface with the queries declared in the app's `package.json`
|
|
53
|
-
* lotics.queries, mapping each alias to its declared param type:
|
|
54
|
-
*
|
|
55
|
-
* ```ts
|
|
56
|
-
* // .lotics/app_queries.d.ts (generated)
|
|
57
|
-
* import "@lotics/app-sdk";
|
|
58
|
-
* declare module "@lotics/app-sdk" {
|
|
59
|
-
* interface AppQueries {
|
|
60
|
-
* "openOrders": { status: string };
|
|
61
|
-
* "allContainers": Record<string, never>;
|
|
62
|
-
* }
|
|
63
|
-
* }
|
|
64
|
-
* ```
|
|
65
|
-
*
|
|
66
|
-
* The value type is the query's declared param shape — `Record<string, never>`
|
|
67
|
-
* for a query that takes no params, the typed object otherwise.
|
|
68
|
-
*/
|
|
69
|
-
export interface AppQueries {
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* The OUTPUT COLUMN NAMES each query projects — the same codegen, from the same
|
|
73
|
-
* manifest, as a literal union per alias:
|
|
74
|
-
*
|
|
75
|
-
* ```ts
|
|
76
|
-
* declare module "@lotics/app-sdk" {
|
|
77
|
-
* interface AppQueryColumns {
|
|
78
|
-
* "openOrders": "id" | "customer" | "total";
|
|
79
|
-
* }
|
|
80
|
-
* }
|
|
81
|
-
* ```
|
|
82
|
-
*
|
|
83
|
-
* A runtime `filter` / `sort` `field_key` is typed against it, so a column the
|
|
84
|
-
* query does not carry is a `tsc` error instead of the server's request-time
|
|
85
|
-
* refusal. An alias is ABSENT when its names cannot be read off the AST alone
|
|
86
|
-
* (a bare `from_table`); its key stays `string` and the server's check is the
|
|
87
|
-
* only one — a union that could be wrong is worse than none.
|
|
88
|
-
*/
|
|
89
|
-
export interface AppQueryColumns {
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* App-specific AGENT augmentation point — same pattern as `AppWorkflows`, for
|
|
93
|
-
* the streaming agents declared in `package.json` lotics.agents and invoked via
|
|
94
|
-
* `useAgentRun(alias)`. Per-app codegen maps each alias to its declared input
|
|
95
|
-
* shape, so an undeclared alias is a compile-time error and the run input is
|
|
96
|
-
* typed:
|
|
97
|
-
*
|
|
98
|
-
* ```ts
|
|
99
|
-
* declare module "@lotics/app-sdk" {
|
|
100
|
-
* interface AppAgents {
|
|
101
|
-
* "recognize": { image_file_id: string };
|
|
102
|
-
* "edit": { current: Record<string, unknown>; prompt: string };
|
|
103
|
-
* }
|
|
104
|
-
* }
|
|
105
|
-
* ```
|
|
106
|
-
*/
|
|
107
|
-
export interface AppAgents {
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* App-specific agent-RESULT augmentation point — maps each agent alias to the
|
|
111
|
-
* type of its declared `outputs` (the structured result the run emits). Absent
|
|
112
|
-
* ⇒ `run.output` is `unknown`. Same pattern as `AppWorkflowResults`.
|
|
113
|
-
*/
|
|
114
|
-
export interface AppAgentResults {
|
|
115
|
-
}
|
package/dist/src/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-side image compression for app uploads.
|
|
3
|
-
*
|
|
4
|
-
* Phone-camera photos are huge (4–10 MB HEIC/JPEG) and no document needs
|
|
5
|
-
* megapixels. Resize to ≤2000px on the long edge — above the standard vision
|
|
6
|
-
* tier's 1568px so a high-resolution reader keeps real detail, but under the
|
|
7
|
-
* limit that rejects requests carrying more than 20 images — and
|
|
8
|
-
* re-encode as JPEG at q=0.9, which keeps small glyphs (container numbers,
|
|
9
|
-
* invoice lines) legible to a reader and to an extraction agent.
|
|
10
|
-
*
|
|
11
|
-
* HEIC/HEIF / PNG / WebP are converted to JPEG. Non-image files (PDF, etc.)
|
|
12
|
-
* pass through unchanged. Falls through gracefully on any browser API gap
|
|
13
|
-
* — the original file is always returned as a usable upload candidate.
|
|
14
|
-
*
|
|
15
|
-
* Ported from `frontend/lib/upload_file_optimization.ts` so public-app forms
|
|
16
|
-
* get the same mobile-friendly upload behavior as the in-Lotics UI. Kept
|
|
17
|
-
* dependency-free (no logger, no ImageFidelity abstraction) so the SDK ships
|
|
18
|
-
* as a single drop-in.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* How faithful the STORED image must be — a closed ordinal scale, not a taxonomy
|
|
22
|
-
* of subjects, so a caller can read the value and know what it costs.
|
|
23
|
-
* `@lotics/shared/image_policy` carries the same scale and the reasoning.
|
|
24
|
-
*/
|
|
25
|
-
export type ImageFidelity = "original" | "high" | "standard";
|
|
26
|
-
/**
|
|
27
|
-
* What an upload defaults to when the caller says nothing. MIRRORS
|
|
28
|
-
* `@lotics/shared/image_policy` — this package ships dependency-free and cannot
|
|
29
|
-
* import it — and a test fails when the two drift.
|
|
30
|
-
*
|
|
31
|
-
* `high` because the surfaces that do not think about this mostly carry text; a
|
|
32
|
-
* caller collecting in volume declares `"standard"` and pays a fraction as much.
|
|
33
|
-
*/
|
|
34
|
-
export declare const DEFAULT_IMAGE_FIDELITY: ImageFidelity;
|
|
35
|
-
export interface ImagePolicy {
|
|
36
|
-
maxDimensionPx: number;
|
|
37
|
-
jpegQuality: number;
|
|
38
|
-
}
|
|
39
|
-
/** The mirror, exposed so the drift guard can compare it with the shared
|
|
40
|
-
* source of truth. Not part of the package's public surface. */
|
|
41
|
-
export declare const IMAGE_POLICIES_FOR_TEST: Record<"high" | "standard", ImagePolicy>;
|
|
42
|
-
export type OptimizationReason = "optimized" | "skipped_original" | "skipped_unsupported_format" | "skipped_environment_unsupported" | "skipped_decode_unavailable" | "skipped_invalid_dimensions" | "skipped_small_dimensions" | "skipped_canvas_unavailable" | "skipped_canvas_type_mismatch";
|
|
43
|
-
export interface OptimizationResult {
|
|
44
|
-
file: File;
|
|
45
|
-
optimized: boolean;
|
|
46
|
-
reason: OptimizationReason;
|
|
47
|
-
originalSizeBytes: number;
|
|
48
|
-
optimizedSizeBytes: number;
|
|
49
|
-
width: number;
|
|
50
|
-
height: number;
|
|
51
|
-
targetWidth: number;
|
|
52
|
-
targetHeight: number;
|
|
53
|
-
}
|
|
54
|
-
export declare function optimizeImageForUpload(file: File, fidelity?: ImageFidelity): Promise<OptimizationResult>;
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-side image compression for app uploads.
|
|
3
|
-
*
|
|
4
|
-
* Phone-camera photos are huge (4–10 MB HEIC/JPEG) and no document needs
|
|
5
|
-
* megapixels. Resize to ≤2000px on the long edge — above the standard vision
|
|
6
|
-
* tier's 1568px so a high-resolution reader keeps real detail, but under the
|
|
7
|
-
* limit that rejects requests carrying more than 20 images — and
|
|
8
|
-
* re-encode as JPEG at q=0.9, which keeps small glyphs (container numbers,
|
|
9
|
-
* invoice lines) legible to a reader and to an extraction agent.
|
|
10
|
-
*
|
|
11
|
-
* HEIC/HEIF / PNG / WebP are converted to JPEG. Non-image files (PDF, etc.)
|
|
12
|
-
* pass through unchanged. Falls through gracefully on any browser API gap
|
|
13
|
-
* — the original file is always returned as a usable upload candidate.
|
|
14
|
-
*
|
|
15
|
-
* Ported from `frontend/lib/upload_file_optimization.ts` so public-app forms
|
|
16
|
-
* get the same mobile-friendly upload behavior as the in-Lotics UI. Kept
|
|
17
|
-
* dependency-free (no logger, no ImageFidelity abstraction) so the SDK ships
|
|
18
|
-
* as a single drop-in.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* What an upload defaults to when the caller says nothing. MIRRORS
|
|
22
|
-
* `@lotics/shared/image_policy` — this package ships dependency-free and cannot
|
|
23
|
-
* import it — and a test fails when the two drift.
|
|
24
|
-
*
|
|
25
|
-
* `high` because the surfaces that do not think about this mostly carry text; a
|
|
26
|
-
* caller collecting in volume declares `"standard"` and pays a fraction as much.
|
|
27
|
-
*/
|
|
28
|
-
export const DEFAULT_IMAGE_FIDELITY = "high";
|
|
29
|
-
// MIRRORS `@lotics/shared/image_policy` — the SDK ships dependency-free so it
|
|
30
|
-
// cannot import it. Change both together.
|
|
31
|
-
// · standard — bulk visual evidence, NEVER read by a model, so tokens do not
|
|
32
|
-
// enter it: sized purely for upload time and storage across dozens of files.
|
|
33
|
-
// · high — read by a model or a person. Token cost depends on DIMENSION
|
|
34
|
-
// alone, so this buys quality (free in tokens) and keeps the dimension modest.
|
|
35
|
-
const IMAGE_POLICIES = {
|
|
36
|
-
standard: { maxDimensionPx: 1280, jpegQuality: 0.8 },
|
|
37
|
-
high: { maxDimensionPx: 1568, jpegQuality: 0.95 },
|
|
38
|
-
};
|
|
39
|
-
/** The mirror, exposed so the drift guard can compare it with the shared
|
|
40
|
-
* source of truth. Not part of the package's public surface. */
|
|
41
|
-
export const IMAGE_POLICIES_FOR_TEST = IMAGE_POLICIES;
|
|
42
|
-
const CONVERTIBLE_EXTENSION_PATTERN = /\.(heic|heif|png|webp)$/i;
|
|
43
|
-
export async function optimizeImageForUpload(file, fidelity = DEFAULT_IMAGE_FIDELITY) {
|
|
44
|
-
if (fidelity === "original")
|
|
45
|
-
return unchanged(file, "skipped_original");
|
|
46
|
-
const policy = IMAGE_POLICIES[fidelity];
|
|
47
|
-
const format = getOptimizableFormat(file);
|
|
48
|
-
const mimeType = format?.outputMimeType;
|
|
49
|
-
const unsupportedReason = getUnsupportedReason(file, mimeType);
|
|
50
|
-
if (unsupportedReason)
|
|
51
|
-
return unchanged(file, unsupportedReason);
|
|
52
|
-
if (!mimeType || !format)
|
|
53
|
-
return unchanged(file, "skipped_unsupported_format");
|
|
54
|
-
const loaded = await loadImage(file);
|
|
55
|
-
if (loaded.status === "decode_unavailable")
|
|
56
|
-
return unchanged(file, "skipped_decode_unavailable");
|
|
57
|
-
const { source, width, height } = loaded;
|
|
58
|
-
if (width <= 0 || height <= 0) {
|
|
59
|
-
closeSource(source);
|
|
60
|
-
return unchanged(file, "skipped_invalid_dimensions");
|
|
61
|
-
}
|
|
62
|
-
if (Math.max(width, height) <= policy.maxDimensionPx) {
|
|
63
|
-
closeSource(source);
|
|
64
|
-
return unchanged(file, "skipped_small_dimensions", width, height);
|
|
65
|
-
}
|
|
66
|
-
const { width: targetWidth, height: targetHeight } = scale(width, height, policy.maxDimensionPx);
|
|
67
|
-
const canvas = document.createElement("canvas");
|
|
68
|
-
canvas.width = targetWidth;
|
|
69
|
-
canvas.height = targetHeight;
|
|
70
|
-
const ctx = canvas.getContext("2d");
|
|
71
|
-
if (!ctx || typeof canvas.toBlob !== "function") {
|
|
72
|
-
closeSource(source);
|
|
73
|
-
return unchanged(file, "skipped_canvas_unavailable", width, height);
|
|
74
|
-
}
|
|
75
|
-
ctx.imageSmoothingEnabled = true;
|
|
76
|
-
ctx.imageSmoothingQuality = "high";
|
|
77
|
-
ctx.drawImage(source, 0, 0, targetWidth, targetHeight);
|
|
78
|
-
closeSource(source);
|
|
79
|
-
const blob = await canvasToBlob(canvas, mimeType, policy.jpegQuality);
|
|
80
|
-
if (blob.type !== mimeType) {
|
|
81
|
-
return unchanged(file, "skipped_canvas_type_mismatch", width, height, targetWidth, targetHeight);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
file: new File([blob], format.outputFilename, {
|
|
85
|
-
type: mimeType,
|
|
86
|
-
lastModified: file.lastModified,
|
|
87
|
-
}),
|
|
88
|
-
optimized: true,
|
|
89
|
-
reason: "optimized",
|
|
90
|
-
originalSizeBytes: file.size,
|
|
91
|
-
optimizedSizeBytes: blob.size,
|
|
92
|
-
width,
|
|
93
|
-
height,
|
|
94
|
-
targetWidth,
|
|
95
|
-
targetHeight,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
function getUnsupportedReason(file, mimeType) {
|
|
99
|
-
void file;
|
|
100
|
-
if (mimeType === undefined)
|
|
101
|
-
return "skipped_unsupported_format";
|
|
102
|
-
if (typeof window === "undefined" ||
|
|
103
|
-
typeof document === "undefined" ||
|
|
104
|
-
typeof document.createElement !== "function" ||
|
|
105
|
-
typeof URL.createObjectURL !== "function" ||
|
|
106
|
-
typeof URL.revokeObjectURL !== "function" ||
|
|
107
|
-
typeof Image === "undefined") {
|
|
108
|
-
return "skipped_environment_unsupported";
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
function getOptimizableFormat(file) {
|
|
113
|
-
const mimeType = normalizeMimeType(file.type);
|
|
114
|
-
if (!mimeType)
|
|
115
|
-
return undefined;
|
|
116
|
-
if (mimeType === "image/jpeg") {
|
|
117
|
-
return { outputMimeType: "image/jpeg", outputFilename: file.name };
|
|
118
|
-
}
|
|
119
|
-
// PNG / WebP / HEIC / HEIF → JPEG (transparency lost on PNG; acceptable
|
|
120
|
-
// for document uploads where we explicitly opt into lossy compression).
|
|
121
|
-
return { outputMimeType: "image/jpeg", outputFilename: replaceExtensionWithJpeg(file.name) };
|
|
122
|
-
}
|
|
123
|
-
function normalizeMimeType(mimeType) {
|
|
124
|
-
const m = mimeType.toLowerCase();
|
|
125
|
-
if (m === "image/jpeg" || m === "image/jpg")
|
|
126
|
-
return "image/jpeg";
|
|
127
|
-
if (m === "image/heic" || m === "image/heif")
|
|
128
|
-
return m;
|
|
129
|
-
if (m === "image/png" || m === "image/webp")
|
|
130
|
-
return m;
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
function replaceExtensionWithJpeg(filename) {
|
|
134
|
-
if (CONVERTIBLE_EXTENSION_PATTERN.test(filename)) {
|
|
135
|
-
return filename.replace(CONVERTIBLE_EXTENSION_PATTERN, ".jpg");
|
|
136
|
-
}
|
|
137
|
-
return `${filename}.jpg`;
|
|
138
|
-
}
|
|
139
|
-
function unchanged(file, reason, width = 0, height = 0, targetWidth = width, targetHeight = height) {
|
|
140
|
-
return {
|
|
141
|
-
file,
|
|
142
|
-
optimized: false,
|
|
143
|
-
reason,
|
|
144
|
-
originalSizeBytes: file.size,
|
|
145
|
-
optimizedSizeBytes: file.size,
|
|
146
|
-
width,
|
|
147
|
-
height,
|
|
148
|
-
targetWidth,
|
|
149
|
-
targetHeight,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function closeSource(source) {
|
|
153
|
-
if ("close" in source && typeof source.close === "function")
|
|
154
|
-
source.close();
|
|
155
|
-
}
|
|
156
|
-
function scale(width, height, maxDimensionPx) {
|
|
157
|
-
const largest = Math.max(width, height);
|
|
158
|
-
if (largest <= maxDimensionPx)
|
|
159
|
-
return { width, height };
|
|
160
|
-
const factor = maxDimensionPx / largest;
|
|
161
|
-
return {
|
|
162
|
-
width: Math.max(1, Math.round(width * factor)),
|
|
163
|
-
height: Math.max(1, Math.round(height * factor)),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
async function loadImage(file) {
|
|
167
|
-
// Prefer createImageBitmap — off-main-thread decode, more reliable on
|
|
168
|
-
// mobile under memory pressure where Image() silently fails.
|
|
169
|
-
if (typeof createImageBitmap === "function") {
|
|
170
|
-
try {
|
|
171
|
-
const bitmap = await createImageBitmap(file);
|
|
172
|
-
return { status: "decoded", source: bitmap, width: bitmap.width, height: bitmap.height };
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
// unsupported format / corrupt — fall back to Image()
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const objectUrl = URL.createObjectURL(file);
|
|
179
|
-
return new Promise((resolve) => {
|
|
180
|
-
const image = new Image();
|
|
181
|
-
const cleanup = () => {
|
|
182
|
-
image.onload = null;
|
|
183
|
-
image.onerror = null;
|
|
184
|
-
URL.revokeObjectURL(objectUrl);
|
|
185
|
-
};
|
|
186
|
-
image.onload = () => {
|
|
187
|
-
cleanup();
|
|
188
|
-
resolve({ status: "decoded", source: image, width: image.naturalWidth, height: image.naturalHeight });
|
|
189
|
-
};
|
|
190
|
-
image.onerror = () => {
|
|
191
|
-
cleanup();
|
|
192
|
-
resolve({ status: "decode_unavailable" });
|
|
193
|
-
};
|
|
194
|
-
image.src = objectUrl;
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
async function canvasToBlob(canvas, mimeType, jpegQuality) {
|
|
198
|
-
return new Promise((resolve, reject) => {
|
|
199
|
-
canvas.toBlob((blob) => {
|
|
200
|
-
if (!blob) {
|
|
201
|
-
reject(new Error("Canvas export returned no data during upload optimization"));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
resolve(blob);
|
|
205
|
-
}, mimeType, jpegQuality);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Orchestrator for the app-upload pipeline.
|
|
3
|
-
*
|
|
4
|
-
* File → optimize (image only) → request presigned URL →
|
|
5
|
-
* PUT to storage (with retry) → complete → ProcessedFile
|
|
6
|
-
*
|
|
7
|
-
* Each step is a focused module (see `optimize.ts`, `transport.ts`); this
|
|
8
|
-
* file just wires them together so `useFileUpload` callers get one robust
|
|
9
|
-
* "uploaded" promise instead of a bare-fetch happy path.
|
|
10
|
-
*
|
|
11
|
-
* Surface is intentionally minimal: the only knob is an optional
|
|
12
|
-
* `AbortSignal` for caller cancellation. Optimization, retries, and
|
|
13
|
-
* timeouts use platform-internal defaults — same conventions as the
|
|
14
|
-
* in-Lotics direct-upload pipeline.
|
|
15
|
-
*/
|
|
16
|
-
import { type ImageFidelity } from "./optimize.js";
|
|
17
|
-
interface UploadInitResponse {
|
|
18
|
-
upload_url: string;
|
|
19
|
-
file_id: string;
|
|
20
|
-
file_storage_key: string;
|
|
21
|
-
}
|
|
22
|
-
interface CompleteResponseFile {
|
|
23
|
-
id: string;
|
|
24
|
-
filename: string;
|
|
25
|
-
mime_type: string;
|
|
26
|
-
url?: string;
|
|
27
|
-
thumbnail_url?: string;
|
|
28
|
-
preview_url?: string;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Caller injects the RPC primitives so this module stays decoupled from the
|
|
32
|
-
* SDK's auth / bootstrap layer. The pipeline targets a specific app's upload
|
|
33
|
-
* endpoints via the supplied `initUpload` and `completeUpload`.
|
|
34
|
-
*/
|
|
35
|
-
export interface UploadRpc {
|
|
36
|
-
initUpload(input: {
|
|
37
|
-
filename: string;
|
|
38
|
-
mime_type: string;
|
|
39
|
-
file_size: number;
|
|
40
|
-
}): Promise<UploadInitResponse>;
|
|
41
|
-
completeUpload(input: {
|
|
42
|
-
file_id: string;
|
|
43
|
-
file_storage_key: string;
|
|
44
|
-
filename: string;
|
|
45
|
-
}): Promise<{
|
|
46
|
-
file: CompleteResponseFile;
|
|
47
|
-
}>;
|
|
48
|
-
}
|
|
49
|
-
export interface RunUploadPipelineOptions {
|
|
50
|
-
signal?: AbortSignal;
|
|
51
|
-
/** What the surface is collecting — selects the stored image resolution. */
|
|
52
|
-
fidelity?: ImageFidelity;
|
|
53
|
-
}
|
|
54
|
-
export declare function runUploadPipeline(file: File, rpc: UploadRpc, options?: RunUploadPipelineOptions): Promise<CompleteResponseFile>;
|
|
55
|
-
export {};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Orchestrator for the app-upload pipeline.
|
|
3
|
-
*
|
|
4
|
-
* File → optimize (image only) → request presigned URL →
|
|
5
|
-
* PUT to storage (with retry) → complete → ProcessedFile
|
|
6
|
-
*
|
|
7
|
-
* Each step is a focused module (see `optimize.ts`, `transport.ts`); this
|
|
8
|
-
* file just wires them together so `useFileUpload` callers get one robust
|
|
9
|
-
* "uploaded" promise instead of a bare-fetch happy path.
|
|
10
|
-
*
|
|
11
|
-
* Surface is intentionally minimal: the only knob is an optional
|
|
12
|
-
* `AbortSignal` for caller cancellation. Optimization, retries, and
|
|
13
|
-
* timeouts use platform-internal defaults — same conventions as the
|
|
14
|
-
* in-Lotics direct-upload pipeline.
|
|
15
|
-
*/
|
|
16
|
-
import { optimizeImageForUpload, DEFAULT_IMAGE_FIDELITY } from "./optimize.js";
|
|
17
|
-
import { putToStorageWithRetry } from "./transport.js";
|
|
18
|
-
export async function runUploadPipeline(file, rpc, options = {}) {
|
|
19
|
-
const { signal, fidelity = DEFAULT_IMAGE_FIDELITY } = options;
|
|
20
|
-
// 1. Compress images. Non-image files return unchanged. Failures here are
|
|
21
|
-
// intentionally swallowed — the user shouldn't see an upload error
|
|
22
|
-
// because canvas threw; we just upload the original bytes.
|
|
23
|
-
let candidate = file;
|
|
24
|
-
try {
|
|
25
|
-
const optimized = await optimizeImageForUpload(file, fidelity);
|
|
26
|
-
candidate = optimized.file;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// fall through with original file
|
|
30
|
-
}
|
|
31
|
-
if (signal?.aborted)
|
|
32
|
-
throw new Error("Upload aborted");
|
|
33
|
-
// 2. Ask the backend for a presigned upload URL.
|
|
34
|
-
const init = await rpc.initUpload({
|
|
35
|
-
filename: candidate.name,
|
|
36
|
-
mime_type: candidate.type,
|
|
37
|
-
file_size: candidate.size,
|
|
38
|
-
});
|
|
39
|
-
// 3. PUT the bytes with timeout + retry. This is the mobile-network
|
|
40
|
-
// failure point.
|
|
41
|
-
const putResponse = await putToStorageWithRetry(init.upload_url, candidate, signal);
|
|
42
|
-
if (!putResponse.ok) {
|
|
43
|
-
throw new Error(`Storage upload failed (${putResponse.status}). Please check your connection and try again.`);
|
|
44
|
-
}
|
|
45
|
-
// 4. Finalize — the server validates the object and creates the file row.
|
|
46
|
-
const { file: uploaded } = await rpc.completeUpload({
|
|
47
|
-
file_id: init.file_id,
|
|
48
|
-
file_storage_key: init.file_storage_key,
|
|
49
|
-
filename: candidate.name,
|
|
50
|
-
});
|
|
51
|
-
return uploaded;
|
|
52
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transport-layer helpers for the SDK upload pipeline.
|
|
3
|
-
*
|
|
4
|
-
* Two concerns this module owns:
|
|
5
|
-
*
|
|
6
|
-
* 1. **Per-request timeout.** A bare `fetch` hangs forever if the network is
|
|
7
|
-
* unreachable; we wrap it in an `AbortController` so a stuck request fails
|
|
8
|
-
* after `UPLOAD_TIMEOUT_MS` instead of leaving the user staring at a
|
|
9
|
-
* spinner.
|
|
10
|
-
*
|
|
11
|
-
* 2. **Retry with exponential backoff** for the presigned `PUT` to object
|
|
12
|
-
* storage — the single most failure-prone step on mobile networks. We
|
|
13
|
-
* retry on network errors / 5xx / timeouts up to `UPLOAD_PUT_MAX_ATTEMPTS`
|
|
14
|
-
* with 1s → 2s → 4s waits between attempts. We do NOT retry on 4xx
|
|
15
|
-
* (client error, retrying won't help) or on caller-initiated aborts.
|
|
16
|
-
*
|
|
17
|
-
* Mirrors the conventions in `frontend/lib/api_utils.ts` and
|
|
18
|
-
* `frontend/lib/file_upload.ts`, simplified for the SDK (no logger, no
|
|
19
|
-
* correlation headers).
|
|
20
|
-
*/
|
|
21
|
-
export declare const UPLOAD_TIMEOUT_MS: number;
|
|
22
|
-
export declare class UploadTimeoutError extends Error {
|
|
23
|
-
readonly url: string;
|
|
24
|
-
readonly timeoutMs: number;
|
|
25
|
-
readonly name = "UploadTimeoutError";
|
|
26
|
-
constructor(url: string, timeoutMs: number);
|
|
27
|
-
}
|
|
28
|
-
export declare class UploadAbortedError extends Error {
|
|
29
|
-
readonly name = "UploadAbortedError";
|
|
30
|
-
constructor();
|
|
31
|
-
}
|
|
32
|
-
/** `fetch` with timeout + optional external `AbortSignal`. */
|
|
33
|
-
export declare function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response>;
|
|
34
|
-
/**
|
|
35
|
-
* Presigned-PUT to object storage with retry + backoff.
|
|
36
|
-
*
|
|
37
|
-
* Retries network errors, timeouts, and 5xx responses up to
|
|
38
|
-
* `UPLOAD_PUT_MAX_ATTEMPTS` times. A 4xx response is returned to the caller
|
|
39
|
-
* unchanged (retrying a 403/400 won't help; the caller decides). Aborts
|
|
40
|
-
* propagate immediately without retry.
|
|
41
|
-
*/
|
|
42
|
-
export declare function putToStorageWithRetry(uploadUrl: string, file: File, signal: AbortSignal | undefined): Promise<Response>;
|