@lotics/app-sdk 0.100.1 → 0.101.1

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.
Files changed (108) hide show
  1. package/AGENTS.md +32 -47
  2. package/dist/agent_stream.d.ts +131 -0
  3. package/dist/ask_ai.d.ts +27 -0
  4. package/dist/attachments.d.ts +58 -0
  5. package/dist/chunk-ARV5FAU5.js +1132 -0
  6. package/dist/comments.d.ts +89 -0
  7. package/dist/error_report.d.ts +9 -0
  8. package/dist/folder_pick.d.ts +8 -0
  9. package/dist/geolocation.d.ts +42 -0
  10. package/dist/hooks.d.ts +251 -0
  11. package/dist/{src/index.d.ts → index.d.ts} +13 -22
  12. package/dist/index.js +31331 -0
  13. package/dist/index.js.LEGAL.txt +11 -0
  14. package/dist/members.d.ts +32 -0
  15. package/dist/mock.d.ts +37 -0
  16. package/dist/mount.d.ts +19 -0
  17. package/dist/new_record.d.ts +37 -0
  18. package/dist/open_app.d.ts +12 -0
  19. package/dist/open_external.d.ts +10 -0
  20. package/dist/overlay.d.ts +25 -0
  21. package/dist/queries.d.ts +231 -0
  22. package/dist/recording.d.ts +47 -0
  23. package/dist/recording_state.d.ts +43 -0
  24. package/dist/rename_file.d.ts +13 -0
  25. package/dist/router.d.ts +10 -0
  26. package/dist/router.js +97 -0
  27. package/dist/row.d.ts +87 -0
  28. package/dist/rpc.d.ts +114 -0
  29. package/dist/select.d.ts +24 -0
  30. package/dist/shared_types.d.ts +8 -0
  31. package/dist/store.d.ts +43 -0
  32. package/dist/types.d.ts +36 -0
  33. package/dist/upload/optimize.d.ts +30 -0
  34. package/dist/upload/pipeline.d.ts +36 -0
  35. package/dist/upload/transport.d.ts +19 -0
  36. package/dist/url_params.d.ts +55 -0
  37. package/dist/use_recents.d.ts +15 -0
  38. package/dist/{src/use_url_state.d.ts → use_url_state.d.ts} +0 -2
  39. package/dist/viewer.d.ts +41 -0
  40. package/dist/written.d.ts +79 -0
  41. package/docs/ai.md +74 -133
  42. package/docs/data_fetching.md +209 -290
  43. package/docs/files.md +61 -51
  44. package/docs/members_and_options.md +92 -62
  45. package/docs/mutations.md +136 -205
  46. package/docs/navigation_and_state.md +26 -35
  47. package/docs/queries.md +144 -207
  48. package/docs/recipes.md +21 -45
  49. package/docs/runtime.md +74 -137
  50. package/docs/security.md +8 -11
  51. package/docs/workflows.md +189 -174
  52. package/package.json +27 -28
  53. package/dist/src/agent_stream.d.ts +0 -200
  54. package/dist/src/agent_stream.js +0 -314
  55. package/dist/src/ask_ai.d.ts +0 -40
  56. package/dist/src/ask_ai.js +0 -35
  57. package/dist/src/attachments.d.ts +0 -68
  58. package/dist/src/attachments.js +0 -93
  59. package/dist/src/comments.d.ts +0 -127
  60. package/dist/src/comments.js +0 -192
  61. package/dist/src/download.js +0 -54
  62. package/dist/src/geolocation.d.ts +0 -64
  63. package/dist/src/geolocation.js +0 -96
  64. package/dist/src/hooks.d.ts +0 -781
  65. package/dist/src/hooks.js +0 -860
  66. package/dist/src/index.js +0 -34
  67. package/dist/src/members.d.ts +0 -105
  68. package/dist/src/members.js +0 -62
  69. package/dist/src/mock.d.ts +0 -118
  70. package/dist/src/mock.js +0 -124
  71. package/dist/src/mount.d.ts +0 -47
  72. package/dist/src/mount.js +0 -34
  73. package/dist/src/new_record.d.ts +0 -74
  74. package/dist/src/new_record.js +0 -117
  75. package/dist/src/open_app.d.ts +0 -15
  76. package/dist/src/open_app.js +0 -18
  77. package/dist/src/open_external.d.ts +0 -16
  78. package/dist/src/open_external.js +0 -19
  79. package/dist/src/recording.d.ts +0 -59
  80. package/dist/src/recording.js +0 -30
  81. package/dist/src/recording_state.d.ts +0 -59
  82. package/dist/src/recording_state.js +0 -94
  83. package/dist/src/router.d.ts +0 -17
  84. package/dist/src/router.js +0 -144
  85. package/dist/src/row.d.ts +0 -159
  86. package/dist/src/row.js +0 -254
  87. package/dist/src/rpc.d.ts +0 -207
  88. package/dist/src/rpc.js +0 -904
  89. package/dist/src/select.d.ts +0 -48
  90. package/dist/src/select.js +0 -40
  91. package/dist/src/types.d.ts +0 -115
  92. package/dist/src/types.js +0 -1
  93. package/dist/src/upload/optimize.d.ts +0 -54
  94. package/dist/src/upload/optimize.js +0 -207
  95. package/dist/src/upload/pipeline.d.ts +0 -55
  96. package/dist/src/upload/pipeline.js +0 -52
  97. package/dist/src/upload/transport.d.ts +0 -42
  98. package/dist/src/upload/transport.js +0 -128
  99. package/dist/src/url_params.d.ts +0 -93
  100. package/dist/src/url_params.js +0 -215
  101. package/dist/src/use_optimistic.d.ts +0 -27
  102. package/dist/src/use_optimistic.js +0 -27
  103. package/dist/src/use_recents.d.ts +0 -19
  104. package/dist/src/use_recents.js +0 -71
  105. package/dist/src/use_url_state.js +0 -73
  106. package/dist/src/viewer.d.ts +0 -26
  107. package/dist/src/viewer.js +0 -47
  108. /package/dist/{src/download.d.ts → download.d.ts} +0 -0
@@ -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[];
@@ -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
- }
@@ -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>;