@howells/stow-client 0.1.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @howells/stow-client
2
+
3
+ Client-side SDK for [Stow](https://stow.sh) file storage. Upload files directly from the browser with progress tracking.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @howells/stow-client
9
+ # or
10
+ pnpm add @howells/stow-client
11
+ # or
12
+ yarn add @howells/stow-client
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { StowClient } from "@howells/stow-client";
19
+
20
+ const stow = new StowClient("/api/upload");
21
+
22
+ // Upload a file from a file input
23
+ const fileInput = document.querySelector('input[type="file"]');
24
+ const file = fileInput.files[0];
25
+
26
+ const result = await stow.uploadFile(file, {
27
+ onProgress: (progress) => {
28
+ console.log(`Upload: ${progress.percent}%`);
29
+ },
30
+ });
31
+
32
+ console.log(result.url);
33
+ ```
34
+
35
+ ## Important: Server Endpoint Required
36
+
37
+ This SDK uploads files through your server endpoint, not directly to Stow. Your server should use `@howells/stow-server` to handle the actual upload. This keeps your API key secure.
38
+
39
+ ```
40
+ Browser → Your Server (/api/upload) → Stow
41
+ ```
42
+
43
+ See `@howells/stow-next` for easy Next.js integration.
44
+
45
+ ## API Reference
46
+
47
+ ### `new StowClient(config)`
48
+
49
+ Create a new client instance.
50
+
51
+ ```typescript
52
+ // Simple: just pass the endpoint
53
+ const stow = new StowClient("/api/upload");
54
+
55
+ // With config object
56
+ const stow = new StowClient({
57
+ endpoint: "/api/upload",
58
+ });
59
+ ```
60
+
61
+ ### `uploadFile(file, options?)`
62
+
63
+ Upload a single file with progress tracking.
64
+
65
+ ```typescript
66
+ const result = await stow.uploadFile(file, {
67
+ route: "avatars", // optional folder/route
68
+ onProgress: (progress) => {
69
+ console.log(`${progress.loaded}/${progress.total} bytes (${progress.percent}%)`);
70
+ },
71
+ });
72
+
73
+ // Result:
74
+ // {
75
+ // key: "bucket-id/avatars/abc123.jpg",
76
+ // url: "https://stow.sh/files/bucket-id/avatars/abc123.jpg",
77
+ // size: 12345,
78
+ // contentType: "image/jpeg"
79
+ // }
80
+ ```
81
+
82
+ ### `uploadFiles(files, options?)`
83
+
84
+ Upload multiple files sequentially.
85
+
86
+ ```typescript
87
+ const results = await stow.uploadFiles(fileInput.files, {
88
+ route: "gallery",
89
+ onProgress: (progress) => {
90
+ console.log(`Current file: ${progress.percent}%`);
91
+ },
92
+ onFileComplete: (result, index) => {
93
+ console.log(`File ${index + 1} complete:`, result.url);
94
+ },
95
+ onFileError: (error, file, index) => {
96
+ console.error(`File ${index + 1} failed:`, error.message);
97
+ },
98
+ });
99
+ ```
100
+
101
+ ## Error Handling
102
+
103
+ ```typescript
104
+ import { StowError } from "@howells/stow-client";
105
+
106
+ try {
107
+ await stow.uploadFile(file);
108
+ } catch (error) {
109
+ if (error instanceof StowError) {
110
+ if (error.code === "ABORTED") {
111
+ console.log("Upload was cancelled");
112
+ } else {
113
+ console.error(`Upload failed: ${error.message}`);
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## TypeScript
120
+
121
+ Full TypeScript support:
122
+
123
+ ```typescript
124
+ import type {
125
+ StowClientConfig,
126
+ UploadResult,
127
+ UploadProgress,
128
+ UploadOptions,
129
+ } from "@howells/stow-client";
130
+ ```
131
+
132
+ ## Why XHR?
133
+
134
+ This SDK uses XMLHttpRequest instead of fetch because **fetch doesn't support upload progress events**. XHR's `upload.onprogress` is the only way to show real upload progress to users.
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Stow Client SDK
3
+ *
4
+ * Client-side SDK for browser environments.
5
+ * Uploads files directly to R2 storage, bypassing your server for file bytes.
6
+ *
7
+ * Flow:
8
+ * 1. Client requests presigned URL from your server
9
+ * 2. Client uploads directly to R2 (your server never sees the file bytes!)
10
+ * 3. Client confirms upload via your server
11
+ *
12
+ * IMPORTANT: This SDK requires a server endpoint that handles presign/confirm.
13
+ * Never expose your API key to the client - use @howells/stow-server on your backend.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { StowClient } from "@howells/stow-client";
18
+ *
19
+ * const stow = new StowClient("/api/stow");
20
+ *
21
+ * // Upload a file - goes directly to R2, not through your server!
22
+ * const result = await stow.uploadFile(file, {
23
+ * onProgress: (p) => console.log(`${p.percent}%`),
24
+ * });
25
+ * console.log(result.url);
26
+ * ```
27
+ */
28
+ declare class StowError extends Error {
29
+ readonly status: number;
30
+ readonly code?: string;
31
+ constructor(message: string, status: number, code?: string);
32
+ }
33
+ interface StowClientConfig {
34
+ /**
35
+ * Your server endpoint that handles presign/confirm requests.
36
+ * This endpoint should use @howells/stow-server internally.
37
+ */
38
+ endpoint: string;
39
+ }
40
+ interface UploadResult {
41
+ key: string;
42
+ url: string | null;
43
+ size: number;
44
+ contentType: string;
45
+ }
46
+ interface UploadProgress {
47
+ /** Bytes uploaded so far */
48
+ loaded: number;
49
+ /** Total bytes to upload */
50
+ total: number;
51
+ /** Upload percentage (0-100) */
52
+ percent: number;
53
+ /** Current phase of the upload */
54
+ phase: "presigning" | "uploading" | "confirming";
55
+ }
56
+ interface UploadOptions {
57
+ /** Optional route/folder for organizing files */
58
+ route?: string;
59
+ /** Custom metadata to attach to the file */
60
+ metadata?: Record<string, string>;
61
+ /** Progress callback */
62
+ onProgress?: (progress: UploadProgress) => void;
63
+ }
64
+ declare class StowClient {
65
+ private readonly endpoint;
66
+ constructor(config: StowClientConfig | string);
67
+ /**
68
+ * Upload a file directly to R2 storage.
69
+ *
70
+ * The file bytes never touch your server - they go directly to R2.
71
+ * Only small JSON requests go through your server for presign/confirm.
72
+ */
73
+ uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>;
74
+ /**
75
+ * Upload file directly to R2 using presigned URL with progress tracking
76
+ */
77
+ private uploadToR2;
78
+ /**
79
+ * Upload multiple files with optional concurrency
80
+ */
81
+ uploadFiles(files: FileList | File[], options?: UploadOptions & {
82
+ /** Number of concurrent uploads (default: 3) */
83
+ concurrency?: number;
84
+ /** Called when a file completes */
85
+ onFileComplete?: (result: UploadResult, index: number) => void;
86
+ /** Called when a file fails */
87
+ onFileError?: (error: Error, file: File, index: number) => void;
88
+ }): Promise<UploadResult[]>;
89
+ }
90
+
91
+ export { StowClient, type StowClientConfig, StowError, type UploadOptions, type UploadProgress, type UploadResult };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Stow Client SDK
3
+ *
4
+ * Client-side SDK for browser environments.
5
+ * Uploads files directly to R2 storage, bypassing your server for file bytes.
6
+ *
7
+ * Flow:
8
+ * 1. Client requests presigned URL from your server
9
+ * 2. Client uploads directly to R2 (your server never sees the file bytes!)
10
+ * 3. Client confirms upload via your server
11
+ *
12
+ * IMPORTANT: This SDK requires a server endpoint that handles presign/confirm.
13
+ * Never expose your API key to the client - use @howells/stow-server on your backend.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { StowClient } from "@howells/stow-client";
18
+ *
19
+ * const stow = new StowClient("/api/stow");
20
+ *
21
+ * // Upload a file - goes directly to R2, not through your server!
22
+ * const result = await stow.uploadFile(file, {
23
+ * onProgress: (p) => console.log(`${p.percent}%`),
24
+ * });
25
+ * console.log(result.url);
26
+ * ```
27
+ */
28
+ declare class StowError extends Error {
29
+ readonly status: number;
30
+ readonly code?: string;
31
+ constructor(message: string, status: number, code?: string);
32
+ }
33
+ interface StowClientConfig {
34
+ /**
35
+ * Your server endpoint that handles presign/confirm requests.
36
+ * This endpoint should use @howells/stow-server internally.
37
+ */
38
+ endpoint: string;
39
+ }
40
+ interface UploadResult {
41
+ key: string;
42
+ url: string | null;
43
+ size: number;
44
+ contentType: string;
45
+ }
46
+ interface UploadProgress {
47
+ /** Bytes uploaded so far */
48
+ loaded: number;
49
+ /** Total bytes to upload */
50
+ total: number;
51
+ /** Upload percentage (0-100) */
52
+ percent: number;
53
+ /** Current phase of the upload */
54
+ phase: "presigning" | "uploading" | "confirming";
55
+ }
56
+ interface UploadOptions {
57
+ /** Optional route/folder for organizing files */
58
+ route?: string;
59
+ /** Custom metadata to attach to the file */
60
+ metadata?: Record<string, string>;
61
+ /** Progress callback */
62
+ onProgress?: (progress: UploadProgress) => void;
63
+ }
64
+ declare class StowClient {
65
+ private readonly endpoint;
66
+ constructor(config: StowClientConfig | string);
67
+ /**
68
+ * Upload a file directly to R2 storage.
69
+ *
70
+ * The file bytes never touch your server - they go directly to R2.
71
+ * Only small JSON requests go through your server for presign/confirm.
72
+ */
73
+ uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>;
74
+ /**
75
+ * Upload file directly to R2 using presigned URL with progress tracking
76
+ */
77
+ private uploadToR2;
78
+ /**
79
+ * Upload multiple files with optional concurrency
80
+ */
81
+ uploadFiles(files: FileList | File[], options?: UploadOptions & {
82
+ /** Number of concurrent uploads (default: 3) */
83
+ concurrency?: number;
84
+ /** Called when a file completes */
85
+ onFileComplete?: (result: UploadResult, index: number) => void;
86
+ /** Called when a file fails */
87
+ onFileError?: (error: Error, file: File, index: number) => void;
88
+ }): Promise<UploadResult[]>;
89
+ }
90
+
91
+ export { StowClient, type StowClientConfig, StowError, type UploadOptions, type UploadProgress, type UploadResult };
package/dist/index.js ADDED
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ StowClient: () => StowClient,
24
+ StowError: () => StowError
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var StowError = class extends Error {
28
+ status;
29
+ code;
30
+ constructor(message, status, code) {
31
+ super(message);
32
+ this.name = "StowError";
33
+ this.status = status;
34
+ this.code = code;
35
+ }
36
+ };
37
+ var StowClient = class {
38
+ endpoint;
39
+ constructor(config) {
40
+ if (typeof config === "string") {
41
+ this.endpoint = config;
42
+ } else {
43
+ this.endpoint = config.endpoint;
44
+ }
45
+ }
46
+ /**
47
+ * Upload a file directly to R2 storage.
48
+ *
49
+ * The file bytes never touch your server - they go directly to R2.
50
+ * Only small JSON requests go through your server for presign/confirm.
51
+ */
52
+ async uploadFile(file, options) {
53
+ const { route, metadata, onProgress } = options || {};
54
+ onProgress?.({
55
+ loaded: 0,
56
+ total: file.size,
57
+ percent: 0,
58
+ phase: "presigning"
59
+ });
60
+ const presignResponse = await fetch(`${this.endpoint}/presign`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({
64
+ filename: file.name,
65
+ contentType: file.type || "application/octet-stream",
66
+ size: file.size,
67
+ route,
68
+ ...metadata ? { metadata } : {}
69
+ })
70
+ });
71
+ if (!presignResponse.ok) {
72
+ const error = await presignResponse.json().catch(() => ({ error: "Presign failed" }));
73
+ throw new StowError(
74
+ error.error || "Presign failed",
75
+ presignResponse.status,
76
+ error.code
77
+ );
78
+ }
79
+ const presign = await presignResponse.json();
80
+ onProgress?.({
81
+ loaded: 0,
82
+ total: file.size,
83
+ percent: 0,
84
+ phase: "uploading"
85
+ });
86
+ await this.uploadToR2(presign.uploadUrl, file, (loaded) => {
87
+ onProgress?.({
88
+ loaded,
89
+ total: file.size,
90
+ percent: Math.round(loaded / file.size * 100),
91
+ phase: "uploading"
92
+ });
93
+ });
94
+ onProgress?.({
95
+ loaded: file.size,
96
+ total: file.size,
97
+ percent: 100,
98
+ phase: "confirming"
99
+ });
100
+ const confirmResponse = await fetch(`${this.endpoint}/confirm`, {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({
104
+ fileKey: presign.fileKey,
105
+ size: file.size,
106
+ contentType: file.type || "application/octet-stream",
107
+ ...metadata ? { metadata } : {}
108
+ })
109
+ });
110
+ if (!confirmResponse.ok) {
111
+ const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" }));
112
+ throw new StowError(
113
+ error.error || "Confirm failed",
114
+ confirmResponse.status,
115
+ error.code
116
+ );
117
+ }
118
+ const result = await confirmResponse.json();
119
+ return {
120
+ key: result.key,
121
+ url: result.url,
122
+ size: result.size,
123
+ contentType: result.contentType
124
+ };
125
+ }
126
+ /**
127
+ * Upload file directly to R2 using presigned URL with progress tracking
128
+ */
129
+ uploadToR2(uploadUrl, file, onProgress) {
130
+ return new Promise((resolve, reject) => {
131
+ const xhr = new XMLHttpRequest();
132
+ xhr.upload.addEventListener("progress", (event) => {
133
+ if (event.lengthComputable && onProgress) {
134
+ onProgress(event.loaded);
135
+ }
136
+ });
137
+ xhr.addEventListener("load", () => {
138
+ if (xhr.status >= 200 && xhr.status < 300) {
139
+ resolve();
140
+ } else {
141
+ reject(
142
+ new StowError(
143
+ `R2 upload failed with status ${xhr.status}`,
144
+ xhr.status
145
+ )
146
+ );
147
+ }
148
+ });
149
+ xhr.addEventListener("error", () => {
150
+ reject(new StowError("Network error during R2 upload", 0));
151
+ });
152
+ xhr.addEventListener("abort", () => {
153
+ reject(new StowError("R2 upload aborted", 0, "ABORTED"));
154
+ });
155
+ xhr.open("PUT", uploadUrl);
156
+ xhr.setRequestHeader(
157
+ "Content-Type",
158
+ file.type || "application/octet-stream"
159
+ );
160
+ xhr.send(file);
161
+ });
162
+ }
163
+ /**
164
+ * Upload multiple files with optional concurrency
165
+ */
166
+ async uploadFiles(files, options) {
167
+ const fileArray = Array.from(files);
168
+ const concurrency = Math.max(1, options?.concurrency ?? 3);
169
+ if (concurrency === 1) {
170
+ const results2 = [];
171
+ for (let i = 0; i < fileArray.length; i++) {
172
+ try {
173
+ const result = await this.uploadFile(fileArray[i], options);
174
+ results2.push(result);
175
+ options?.onFileComplete?.(result, i);
176
+ } catch (error) {
177
+ options?.onFileError?.(
178
+ error instanceof Error ? error : new Error("Upload failed"),
179
+ fileArray[i],
180
+ i
181
+ );
182
+ throw error;
183
+ }
184
+ }
185
+ return results2;
186
+ }
187
+ const results = new Array(fileArray.length);
188
+ let nextIndex = 0;
189
+ let firstError = null;
190
+ const worker = async () => {
191
+ while (nextIndex < fileArray.length && !firstError) {
192
+ const i = nextIndex++;
193
+ try {
194
+ const result = await this.uploadFile(fileArray[i], options);
195
+ results[i] = result;
196
+ options?.onFileComplete?.(result, i);
197
+ } catch (error) {
198
+ const err = error instanceof Error ? error : new Error("Upload failed");
199
+ options?.onFileError?.(err, fileArray[i], i);
200
+ if (!firstError) {
201
+ firstError = { error: err, file: fileArray[i], index: i };
202
+ }
203
+ }
204
+ }
205
+ };
206
+ const workers = Array.from(
207
+ { length: Math.min(concurrency, fileArray.length) },
208
+ () => worker()
209
+ );
210
+ await Promise.all(workers);
211
+ if (firstError !== null) {
212
+ throw firstError.error;
213
+ }
214
+ return results;
215
+ }
216
+ };
217
+ // Annotate the CommonJS export names for ESM import in node:
218
+ 0 && (module.exports = {
219
+ StowClient,
220
+ StowError
221
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,195 @@
1
+ // src/index.ts
2
+ var StowError = class extends Error {
3
+ status;
4
+ code;
5
+ constructor(message, status, code) {
6
+ super(message);
7
+ this.name = "StowError";
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ };
12
+ var StowClient = class {
13
+ endpoint;
14
+ constructor(config) {
15
+ if (typeof config === "string") {
16
+ this.endpoint = config;
17
+ } else {
18
+ this.endpoint = config.endpoint;
19
+ }
20
+ }
21
+ /**
22
+ * Upload a file directly to R2 storage.
23
+ *
24
+ * The file bytes never touch your server - they go directly to R2.
25
+ * Only small JSON requests go through your server for presign/confirm.
26
+ */
27
+ async uploadFile(file, options) {
28
+ const { route, metadata, onProgress } = options || {};
29
+ onProgress?.({
30
+ loaded: 0,
31
+ total: file.size,
32
+ percent: 0,
33
+ phase: "presigning"
34
+ });
35
+ const presignResponse = await fetch(`${this.endpoint}/presign`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({
39
+ filename: file.name,
40
+ contentType: file.type || "application/octet-stream",
41
+ size: file.size,
42
+ route,
43
+ ...metadata ? { metadata } : {}
44
+ })
45
+ });
46
+ if (!presignResponse.ok) {
47
+ const error = await presignResponse.json().catch(() => ({ error: "Presign failed" }));
48
+ throw new StowError(
49
+ error.error || "Presign failed",
50
+ presignResponse.status,
51
+ error.code
52
+ );
53
+ }
54
+ const presign = await presignResponse.json();
55
+ onProgress?.({
56
+ loaded: 0,
57
+ total: file.size,
58
+ percent: 0,
59
+ phase: "uploading"
60
+ });
61
+ await this.uploadToR2(presign.uploadUrl, file, (loaded) => {
62
+ onProgress?.({
63
+ loaded,
64
+ total: file.size,
65
+ percent: Math.round(loaded / file.size * 100),
66
+ phase: "uploading"
67
+ });
68
+ });
69
+ onProgress?.({
70
+ loaded: file.size,
71
+ total: file.size,
72
+ percent: 100,
73
+ phase: "confirming"
74
+ });
75
+ const confirmResponse = await fetch(`${this.endpoint}/confirm`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({
79
+ fileKey: presign.fileKey,
80
+ size: file.size,
81
+ contentType: file.type || "application/octet-stream",
82
+ ...metadata ? { metadata } : {}
83
+ })
84
+ });
85
+ if (!confirmResponse.ok) {
86
+ const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" }));
87
+ throw new StowError(
88
+ error.error || "Confirm failed",
89
+ confirmResponse.status,
90
+ error.code
91
+ );
92
+ }
93
+ const result = await confirmResponse.json();
94
+ return {
95
+ key: result.key,
96
+ url: result.url,
97
+ size: result.size,
98
+ contentType: result.contentType
99
+ };
100
+ }
101
+ /**
102
+ * Upload file directly to R2 using presigned URL with progress tracking
103
+ */
104
+ uploadToR2(uploadUrl, file, onProgress) {
105
+ return new Promise((resolve, reject) => {
106
+ const xhr = new XMLHttpRequest();
107
+ xhr.upload.addEventListener("progress", (event) => {
108
+ if (event.lengthComputable && onProgress) {
109
+ onProgress(event.loaded);
110
+ }
111
+ });
112
+ xhr.addEventListener("load", () => {
113
+ if (xhr.status >= 200 && xhr.status < 300) {
114
+ resolve();
115
+ } else {
116
+ reject(
117
+ new StowError(
118
+ `R2 upload failed with status ${xhr.status}`,
119
+ xhr.status
120
+ )
121
+ );
122
+ }
123
+ });
124
+ xhr.addEventListener("error", () => {
125
+ reject(new StowError("Network error during R2 upload", 0));
126
+ });
127
+ xhr.addEventListener("abort", () => {
128
+ reject(new StowError("R2 upload aborted", 0, "ABORTED"));
129
+ });
130
+ xhr.open("PUT", uploadUrl);
131
+ xhr.setRequestHeader(
132
+ "Content-Type",
133
+ file.type || "application/octet-stream"
134
+ );
135
+ xhr.send(file);
136
+ });
137
+ }
138
+ /**
139
+ * Upload multiple files with optional concurrency
140
+ */
141
+ async uploadFiles(files, options) {
142
+ const fileArray = Array.from(files);
143
+ const concurrency = Math.max(1, options?.concurrency ?? 3);
144
+ if (concurrency === 1) {
145
+ const results2 = [];
146
+ for (let i = 0; i < fileArray.length; i++) {
147
+ try {
148
+ const result = await this.uploadFile(fileArray[i], options);
149
+ results2.push(result);
150
+ options?.onFileComplete?.(result, i);
151
+ } catch (error) {
152
+ options?.onFileError?.(
153
+ error instanceof Error ? error : new Error("Upload failed"),
154
+ fileArray[i],
155
+ i
156
+ );
157
+ throw error;
158
+ }
159
+ }
160
+ return results2;
161
+ }
162
+ const results = new Array(fileArray.length);
163
+ let nextIndex = 0;
164
+ let firstError = null;
165
+ const worker = async () => {
166
+ while (nextIndex < fileArray.length && !firstError) {
167
+ const i = nextIndex++;
168
+ try {
169
+ const result = await this.uploadFile(fileArray[i], options);
170
+ results[i] = result;
171
+ options?.onFileComplete?.(result, i);
172
+ } catch (error) {
173
+ const err = error instanceof Error ? error : new Error("Upload failed");
174
+ options?.onFileError?.(err, fileArray[i], i);
175
+ if (!firstError) {
176
+ firstError = { error: err, file: fileArray[i], index: i };
177
+ }
178
+ }
179
+ }
180
+ };
181
+ const workers = Array.from(
182
+ { length: Math.min(concurrency, fileArray.length) },
183
+ () => worker()
184
+ );
185
+ await Promise.all(workers);
186
+ if (firstError !== null) {
187
+ throw firstError.error;
188
+ }
189
+ return results;
190
+ }
191
+ };
192
+ export {
193
+ StowClient,
194
+ StowError
195
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@howells/stow-client",
3
+ "version": "0.1.0",
4
+ "description": "Client-side SDK for Stow file storage",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/howells/stow.git",
9
+ "directory": "packages/stow-client"
10
+ },
11
+ "homepage": "https://stow.sh",
12
+ "keywords": [
13
+ "stow",
14
+ "file-storage",
15
+ "upload",
16
+ "presigned-url",
17
+ "sdk"
18
+ ],
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.mjs",
26
+ "require": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsup src/index.ts --format cjs,esm --dts",
34
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest"
37
+ },
38
+ "devDependencies": {
39
+ "@stow/typescript-config": "workspace:*",
40
+ "jsdom": "^26.0.0",
41
+ "tsup": "^8.0.0",
42
+ "typescript": "^5.0.0",
43
+ "vitest": "^4.0.0"
44
+ }
45
+ }