@howells/stow-client 0.1.0 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.mts CHANGED
@@ -25,11 +25,13 @@
25
25
  * console.log(result.url);
26
26
  * ```
27
27
  */
28
+ /** Error thrown when an SDK request fails. */
28
29
  declare class StowError extends Error {
29
30
  readonly status: number;
30
31
  readonly code?: string;
31
32
  constructor(message: string, status: number, code?: string);
32
33
  }
34
+ /** Configuration for creating a {@link StowClient} instance. */
33
35
  interface StowClientConfig {
34
36
  /**
35
37
  * Your server endpoint that handles presign/confirm requests.
@@ -37,38 +39,57 @@ interface StowClientConfig {
37
39
  */
38
40
  endpoint: string;
39
41
  }
42
+ /** Result returned after a successful upload or dedupe short-circuit. */
40
43
  interface UploadResult {
44
+ contentType: string;
45
+ /** True when upload short-circuited to an existing file via dedupe. */
46
+ deduped?: boolean;
41
47
  key: string;
42
- url: string | null;
43
48
  size: number;
44
- contentType: string;
49
+ url: string | null;
45
50
  }
51
+ /** Progress payload emitted during `uploadFile`. */
46
52
  interface UploadProgress {
47
53
  /** Bytes uploaded so far */
48
54
  loaded: number;
49
- /** Total bytes to upload */
50
- total: number;
51
55
  /** Upload percentage (0-100) */
52
56
  percent: number;
53
57
  /** Current phase of the upload */
54
58
  phase: "presigning" | "uploading" | "confirming";
59
+ /** Total bytes to upload */
60
+ total: number;
55
61
  }
62
+ /** Optional behavior for a single upload operation. */
56
63
  interface UploadOptions {
57
- /** Optional route/folder for organizing files */
58
- route?: string;
59
64
  /** Custom metadata to attach to the file */
60
65
  metadata?: Record<string, string>;
61
66
  /** Progress callback */
62
67
  onProgress?: (progress: UploadProgress) => void;
68
+ /** Optional upload route (server-side configured route/path policy). */
69
+ route?: string;
63
70
  }
71
+ /** Browser SDK for direct-to-R2 uploads via your server's presign/confirm endpoints. */
64
72
  declare class StowClient {
65
73
  private readonly endpoint;
74
+ /**
75
+ * @param config Server upload endpoint base path.
76
+ *
77
+ * The endpoint should expose:
78
+ * - `POST {endpoint}/presign`
79
+ * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign)
80
+ */
66
81
  constructor(config: StowClientConfig | string);
82
+ private endpointUrl;
83
+ private resolveConfirmUrl;
67
84
  /**
68
85
  * Upload a file directly to R2 storage.
69
86
  *
70
87
  * The file bytes never touch your server - they go directly to R2.
71
88
  * Only small JSON requests go through your server for presign/confirm.
89
+ *
90
+ * Dedup behavior:
91
+ * If presign responds with `{ dedupe: true, ... }`, upload skips R2 PUT and
92
+ * confirm entirely, returning the existing file metadata with `deduped: true`.
72
93
  */
73
94
  uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>;
74
95
  /**
@@ -77,6 +98,10 @@ declare class StowClient {
77
98
  private uploadToR2;
78
99
  /**
79
100
  * Upload multiple files with optional concurrency
101
+ *
102
+ * - `concurrency=1` preserves strict sequential ordering.
103
+ * - `concurrency>1` uses a bounded worker pool and stops scheduling new files
104
+ * after the first error.
80
105
  */
81
106
  uploadFiles(files: FileList | File[], options?: UploadOptions & {
82
107
  /** Number of concurrent uploads (default: 3) */
package/dist/index.d.ts CHANGED
@@ -25,11 +25,13 @@
25
25
  * console.log(result.url);
26
26
  * ```
27
27
  */
28
+ /** Error thrown when an SDK request fails. */
28
29
  declare class StowError extends Error {
29
30
  readonly status: number;
30
31
  readonly code?: string;
31
32
  constructor(message: string, status: number, code?: string);
32
33
  }
34
+ /** Configuration for creating a {@link StowClient} instance. */
33
35
  interface StowClientConfig {
34
36
  /**
35
37
  * Your server endpoint that handles presign/confirm requests.
@@ -37,38 +39,57 @@ interface StowClientConfig {
37
39
  */
38
40
  endpoint: string;
39
41
  }
42
+ /** Result returned after a successful upload or dedupe short-circuit. */
40
43
  interface UploadResult {
44
+ contentType: string;
45
+ /** True when upload short-circuited to an existing file via dedupe. */
46
+ deduped?: boolean;
41
47
  key: string;
42
- url: string | null;
43
48
  size: number;
44
- contentType: string;
49
+ url: string | null;
45
50
  }
51
+ /** Progress payload emitted during `uploadFile`. */
46
52
  interface UploadProgress {
47
53
  /** Bytes uploaded so far */
48
54
  loaded: number;
49
- /** Total bytes to upload */
50
- total: number;
51
55
  /** Upload percentage (0-100) */
52
56
  percent: number;
53
57
  /** Current phase of the upload */
54
58
  phase: "presigning" | "uploading" | "confirming";
59
+ /** Total bytes to upload */
60
+ total: number;
55
61
  }
62
+ /** Optional behavior for a single upload operation. */
56
63
  interface UploadOptions {
57
- /** Optional route/folder for organizing files */
58
- route?: string;
59
64
  /** Custom metadata to attach to the file */
60
65
  metadata?: Record<string, string>;
61
66
  /** Progress callback */
62
67
  onProgress?: (progress: UploadProgress) => void;
68
+ /** Optional upload route (server-side configured route/path policy). */
69
+ route?: string;
63
70
  }
71
+ /** Browser SDK for direct-to-R2 uploads via your server's presign/confirm endpoints. */
64
72
  declare class StowClient {
65
73
  private readonly endpoint;
74
+ /**
75
+ * @param config Server upload endpoint base path.
76
+ *
77
+ * The endpoint should expose:
78
+ * - `POST {endpoint}/presign`
79
+ * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign)
80
+ */
66
81
  constructor(config: StowClientConfig | string);
82
+ private endpointUrl;
83
+ private resolveConfirmUrl;
67
84
  /**
68
85
  * Upload a file directly to R2 storage.
69
86
  *
70
87
  * The file bytes never touch your server - they go directly to R2.
71
88
  * Only small JSON requests go through your server for presign/confirm.
89
+ *
90
+ * Dedup behavior:
91
+ * If presign responds with `{ dedupe: true, ... }`, upload skips R2 PUT and
92
+ * confirm entirely, returning the existing file metadata with `deduped: true`.
72
93
  */
73
94
  uploadFile(file: File, options?: UploadOptions): Promise<UploadResult>;
74
95
  /**
@@ -77,6 +98,10 @@ declare class StowClient {
77
98
  private uploadToR2;
78
99
  /**
79
100
  * Upload multiple files with optional concurrency
101
+ *
102
+ * - `concurrency=1` preserves strict sequential ordering.
103
+ * - `concurrency>1` uses a bounded worker pool and stops scheduling new files
104
+ * after the first error.
80
105
  */
81
106
  uploadFiles(files: FileList | File[], options?: UploadOptions & {
82
107
  /** Number of concurrent uploads (default: 3) */
package/dist/index.js CHANGED
@@ -36,18 +36,44 @@ var StowError = class extends Error {
36
36
  };
37
37
  var StowClient = class {
38
38
  endpoint;
39
+ /**
40
+ * @param config Server upload endpoint base path.
41
+ *
42
+ * The endpoint should expose:
43
+ * - `POST {endpoint}/presign`
44
+ * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign)
45
+ */
39
46
  constructor(config) {
40
47
  if (typeof config === "string") {
41
- this.endpoint = config;
48
+ this.endpoint = config.replace(/\/+$/, "");
42
49
  } else {
43
- this.endpoint = config.endpoint;
50
+ this.endpoint = config.endpoint.replace(/\/+$/, "");
51
+ }
52
+ }
53
+ endpointUrl(path) {
54
+ if (!this.endpoint) {
55
+ return path;
56
+ }
57
+ return `${this.endpoint}${path}`;
58
+ }
59
+ resolveConfirmUrl(confirmUrl) {
60
+ if (!confirmUrl) {
61
+ return this.endpointUrl("/confirm");
62
+ }
63
+ if (confirmUrl.startsWith("/") || /^[a-z][a-z0-9+\-.]*:\/\//i.test(confirmUrl)) {
64
+ return confirmUrl;
44
65
  }
66
+ return this.endpointUrl(`/${confirmUrl.replace(/^\/+/, "")}`);
45
67
  }
46
68
  /**
47
69
  * Upload a file directly to R2 storage.
48
70
  *
49
71
  * The file bytes never touch your server - they go directly to R2.
50
72
  * Only small JSON requests go through your server for presign/confirm.
73
+ *
74
+ * Dedup behavior:
75
+ * If presign responds with `{ dedupe: true, ... }`, upload skips R2 PUT and
76
+ * confirm entirely, returning the existing file metadata with `deduped: true`.
51
77
  */
52
78
  async uploadFile(file, options) {
53
79
  const { route, metadata, onProgress } = options || {};
@@ -57,7 +83,7 @@ var StowClient = class {
57
83
  percent: 0,
58
84
  phase: "presigning"
59
85
  });
60
- const presignResponse = await fetch(`${this.endpoint}/presign`, {
86
+ const presignResponse = await fetch(this.endpointUrl("/presign"), {
61
87
  method: "POST",
62
88
  headers: { "Content-Type": "application/json" },
63
89
  body: JSON.stringify({
@@ -77,6 +103,21 @@ var StowClient = class {
77
103
  );
78
104
  }
79
105
  const presign = await presignResponse.json();
106
+ if ("dedupe" in presign && presign.dedupe) {
107
+ onProgress?.({
108
+ loaded: file.size,
109
+ total: file.size,
110
+ percent: 100,
111
+ phase: "confirming"
112
+ });
113
+ return {
114
+ key: presign.key,
115
+ url: presign.url,
116
+ size: presign.size,
117
+ contentType: presign.contentType,
118
+ deduped: true
119
+ };
120
+ }
80
121
  onProgress?.({
81
122
  loaded: 0,
82
123
  total: file.size,
@@ -97,16 +138,19 @@ var StowClient = class {
97
138
  percent: 100,
98
139
  phase: "confirming"
99
140
  });
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
- });
141
+ const confirmResponse = await fetch(
142
+ this.resolveConfirmUrl(presign.confirmUrl),
143
+ {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({
147
+ fileKey: presign.fileKey,
148
+ size: file.size,
149
+ contentType: file.type || "application/octet-stream",
150
+ ...metadata ? { metadata } : {}
151
+ })
152
+ }
153
+ );
110
154
  if (!confirmResponse.ok) {
111
155
  const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" }));
112
156
  throw new StowError(
@@ -162,6 +206,10 @@ var StowClient = class {
162
206
  }
163
207
  /**
164
208
  * Upload multiple files with optional concurrency
209
+ *
210
+ * - `concurrency=1` preserves strict sequential ordering.
211
+ * - `concurrency>1` uses a bounded worker pool and stops scheduling new files
212
+ * after the first error.
165
213
  */
166
214
  async uploadFiles(files, options) {
167
215
  const fileArray = Array.from(files);
package/dist/index.mjs CHANGED
@@ -11,18 +11,44 @@ var StowError = class extends Error {
11
11
  };
12
12
  var StowClient = class {
13
13
  endpoint;
14
+ /**
15
+ * @param config Server upload endpoint base path.
16
+ *
17
+ * The endpoint should expose:
18
+ * - `POST {endpoint}/presign`
19
+ * - `POST {endpoint}/confirm` (or a `confirmUrl` returned from presign)
20
+ */
14
21
  constructor(config) {
15
22
  if (typeof config === "string") {
16
- this.endpoint = config;
23
+ this.endpoint = config.replace(/\/+$/, "");
17
24
  } else {
18
- this.endpoint = config.endpoint;
25
+ this.endpoint = config.endpoint.replace(/\/+$/, "");
26
+ }
27
+ }
28
+ endpointUrl(path) {
29
+ if (!this.endpoint) {
30
+ return path;
31
+ }
32
+ return `${this.endpoint}${path}`;
33
+ }
34
+ resolveConfirmUrl(confirmUrl) {
35
+ if (!confirmUrl) {
36
+ return this.endpointUrl("/confirm");
37
+ }
38
+ if (confirmUrl.startsWith("/") || /^[a-z][a-z0-9+\-.]*:\/\//i.test(confirmUrl)) {
39
+ return confirmUrl;
19
40
  }
41
+ return this.endpointUrl(`/${confirmUrl.replace(/^\/+/, "")}`);
20
42
  }
21
43
  /**
22
44
  * Upload a file directly to R2 storage.
23
45
  *
24
46
  * The file bytes never touch your server - they go directly to R2.
25
47
  * Only small JSON requests go through your server for presign/confirm.
48
+ *
49
+ * Dedup behavior:
50
+ * If presign responds with `{ dedupe: true, ... }`, upload skips R2 PUT and
51
+ * confirm entirely, returning the existing file metadata with `deduped: true`.
26
52
  */
27
53
  async uploadFile(file, options) {
28
54
  const { route, metadata, onProgress } = options || {};
@@ -32,7 +58,7 @@ var StowClient = class {
32
58
  percent: 0,
33
59
  phase: "presigning"
34
60
  });
35
- const presignResponse = await fetch(`${this.endpoint}/presign`, {
61
+ const presignResponse = await fetch(this.endpointUrl("/presign"), {
36
62
  method: "POST",
37
63
  headers: { "Content-Type": "application/json" },
38
64
  body: JSON.stringify({
@@ -52,6 +78,21 @@ var StowClient = class {
52
78
  );
53
79
  }
54
80
  const presign = await presignResponse.json();
81
+ if ("dedupe" in presign && presign.dedupe) {
82
+ onProgress?.({
83
+ loaded: file.size,
84
+ total: file.size,
85
+ percent: 100,
86
+ phase: "confirming"
87
+ });
88
+ return {
89
+ key: presign.key,
90
+ url: presign.url,
91
+ size: presign.size,
92
+ contentType: presign.contentType,
93
+ deduped: true
94
+ };
95
+ }
55
96
  onProgress?.({
56
97
  loaded: 0,
57
98
  total: file.size,
@@ -72,16 +113,19 @@ var StowClient = class {
72
113
  percent: 100,
73
114
  phase: "confirming"
74
115
  });
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
- });
116
+ const confirmResponse = await fetch(
117
+ this.resolveConfirmUrl(presign.confirmUrl),
118
+ {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({
122
+ fileKey: presign.fileKey,
123
+ size: file.size,
124
+ contentType: file.type || "application/octet-stream",
125
+ ...metadata ? { metadata } : {}
126
+ })
127
+ }
128
+ );
85
129
  if (!confirmResponse.ok) {
86
130
  const error = await confirmResponse.json().catch(() => ({ error: "Confirm failed" }));
87
131
  throw new StowError(
@@ -137,6 +181,10 @@ var StowClient = class {
137
181
  }
138
182
  /**
139
183
  * Upload multiple files with optional concurrency
184
+ *
185
+ * - `concurrency=1` preserves strict sequential ordering.
186
+ * - `concurrency>1` uses a bounded worker pool and stops scheduling new files
187
+ * after the first error.
140
188
  */
141
189
  async uploadFiles(files, options) {
142
190
  const fileArray = Array.from(files);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howells/stow-client",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Client-side SDK for Stow file storage",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,17 +29,17 @@
29
29
  "files": [
30
30
  "dist"
31
31
  ],
32
+ "devDependencies": {
33
+ "jsdom": "^28.1.0",
34
+ "tsup": "^8.5.1",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.18",
37
+ "@stow/typescript-config": "0.0.0"
38
+ },
32
39
  "scripts": {
33
40
  "build": "tsup src/index.ts --format cjs,esm --dts",
34
41
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
35
42
  "test": "vitest run",
36
43
  "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
44
  }
45
- }
45
+ }