@edgestore/react 0.1.6 → 0.2.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 CHANGED
@@ -98,7 +98,7 @@ export default function RootLayout({
98
98
 
99
99
  ### Upload file
100
100
 
101
- You can use the `useEdgeStore` hook to access typesafe frontend client and use it to upload files.
101
+ You can use the `useEdgeStore` hook to access a typesafe frontend client and use it to upload files.
102
102
 
103
103
  ```tsx {1, 6, 19-28}
104
104
  import * as React from 'react';
@@ -144,7 +144,7 @@ export default function Page() {
144
144
  By passing the `replaceTargetUrl` option, you can replace an existing file with a new one.
145
145
  It will automatically delete the old file after the upload is complete.
146
146
 
147
- You can also just upload the file using the same file name, but in that case, you might still see the old file for a while because of the CDN cache.
147
+ You can also upload the file using the same file name, but in that case, you might still see the old file for a while because of the CDN cache.
148
148
 
149
149
  ```tsx
150
150
  const res = await edgestore.publicFiles.upload({
@@ -10,12 +10,30 @@ export type Prettify<TType> = {
10
10
  } & {};
11
11
  export type BucketFunctions<TRouter extends AnyRouter> = {
12
12
  [K in keyof TRouter['buckets']]: {
13
+ /**
14
+ * Upload a file to the bucket
15
+ *
16
+ * @example
17
+ * await edgestore.myBucket.upload({
18
+ * file: file,
19
+ * signal: abortController.signal, // if you want to be able to cancel the ongoing upload
20
+ * onProgressChange: (progress) => { console.log(progress) }, // if you want to show the progress of the upload
21
+ * input: {...} // if the bucket has an input schema
22
+ * options: {
23
+ * manualFileName: file.name, // if you want to use a custom file name
24
+ * replaceTargetUrl: url, // if you want to replace an existing file
25
+ * temporary: true, // if you want to delete the file after 24 hours
26
+ * }
27
+ * })
28
+ */
13
29
  upload: (params: z.infer<TRouter['buckets'][K]['_def']['input']> extends never ? {
14
30
  file: File;
31
+ signal?: AbortSignal;
15
32
  onProgressChange?: OnProgressChangeHandler;
16
33
  options?: UploadOptions;
17
34
  } : {
18
35
  file: File;
36
+ signal?: AbortSignal;
19
37
  input: z.infer<TRouter['buckets'][K]['_def']['input']>;
20
38
  onProgressChange?: OnProgressChangeHandler;
21
39
  options?: UploadOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"createNextProxy.d.ts","sourceRoot":"","sources":["../src/createNextProxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createNextProxy.d.ts","sourceRoot":"","sources":["../src/createNextProxy.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAExB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,CAAC;AAK7B;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,KAAK,IAAI;KAC3B,CAAC,IAAI,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAE7B,GAAG,EAAE,CAAC;AAEP,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,SAAS,IAAI;KACtD,CAAC,IAAI,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG;QAC/B;;;;;;;;;;;;;;;WAeG;QACH,MAAM,EAAE,CACN,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,SAAS,KAAK,GACjE;YACE,IAAI,EAAE,IAAI,CAAC;YACX,MAAM,CAAC,EAAE,WAAW,CAAC;YACrB,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;YAC3C,OAAO,CAAC,EAAE,aAAa,CAAC;SACzB,GACD;YACE,IAAI,EAAE,IAAI,CAAC;YACX,MAAM,CAAC,EAAE,WAAW,CAAC;YACrB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YACvD,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;YAC3C,OAAO,CAAC,EAAE,aAAa,CAAC;SACzB,KACF,OAAO,CACV,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,SAAS,OAAO,GACjD;YACE,GAAG,EAAE,MAAM,CAAC;YACZ,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;YAC5B,IAAI,EAAE,MAAM,CAAC;YACb,UAAU,EAAE,IAAI,CAAC;YACjB,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,IAAI,EAAE,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,SAAS,EAAE,QAAQ,CACjB,MAAM,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CACnD,EAAE,CAAC;SACL,GACD;YACE,GAAG,EAAE,MAAM,CAAC;YACZ,IAAI,EAAE,MAAM,CAAC;YACb,UAAU,EAAE,IAAI,CAAC;YACjB,QAAQ,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,IAAI,EAAE,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,SAAS,EAAE,QAAQ,CACjB,MAAM,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CACnD,EAAE,CAAC;SACL,CACN,CAAC;QACF,aAAa,EAAE,CAAC,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1D,MAAM,EAAE,CAAC,MAAM,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACpD;CACF,CAAC;AAEF,KAAK,uBAAuB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAE1D,wBAAgB,eAAe,CAAC,OAAO,SAAS,SAAS,EAAE,EACzD,OAAO,EACP,iBAAiB,EACjB,oBAAwB,GACzB,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,4BAiEA"}
@@ -0,0 +1,3 @@
1
+ export { EdgeStoreApiClientError } from '@edgestore/shared';
2
+ export { UploadAbortedError } from '../libs/errors/uploadAbortedError';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC"}
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var shared = require('@edgestore/shared');
6
+ var uploadAbortedError = require('../uploadAbortedError-fbfcc57b.js');
7
+
8
+
9
+
10
+ Object.defineProperty(exports, 'EdgeStoreApiClientError', {
11
+ enumerable: true,
12
+ get: function () { return shared.EdgeStoreApiClientError; }
13
+ });
14
+ exports.UploadAbortedError = uploadAbortedError.UploadAbortedError;
@@ -0,0 +1,2 @@
1
+ export { EdgeStoreApiClientError } from '@edgestore/shared';
2
+ export { U as UploadAbortedError } from '../uploadAbortedError-e1379bb0.mjs';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var React = require('react');
6
6
  var shared = require('@edgestore/shared');
7
+ var uploadAbortedError = require('./uploadAbortedError-fbfcc57b.js');
7
8
 
8
9
  function _interopNamespace(e) {
9
10
  if (e && e.__esModule) return e;
@@ -52,15 +53,29 @@ function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5
52
53
  upload: async (params)=>{
53
54
  try {
54
55
  params.onProgressChange?.(0);
56
+ // This handles the case where the user cancels the upload while it's waiting in the queue
57
+ const abortPromise = new Promise((resolve)=>{
58
+ params.signal?.addEventListener('abort', ()=>{
59
+ resolve();
60
+ }, {
61
+ once: true
62
+ });
63
+ });
55
64
  while(uploadingCountRef.current >= maxConcurrentUploads && uploadingCountRef.current > 0){
56
- await new Promise((resolve)=>setTimeout(resolve, 300));
65
+ await Promise.race([
66
+ new Promise((resolve)=>setTimeout(resolve, 300)),
67
+ abortPromise
68
+ ]);
69
+ if (params.signal?.aborted) {
70
+ throw new uploadAbortedError.UploadAbortedError('File upload aborted');
71
+ }
57
72
  }
58
73
  uploadingCountRef.current++;
59
- const test = await uploadFile(params, {
74
+ const fileInfo = await uploadFile(params, {
60
75
  bucketName: bucketName,
61
76
  apiPath
62
77
  });
63
- return test;
78
+ return fileInfo;
64
79
  } finally{
65
80
  uploadingCountRef.current--;
66
81
  }
@@ -88,12 +103,13 @@ function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5
88
103
  }
89
104
  });
90
105
  }
91
- async function uploadFile({ file, input, onProgressChange, options }, { apiPath, bucketName }) {
106
+ async function uploadFile({ file, signal, input, onProgressChange, options }, { apiPath, bucketName }) {
92
107
  try {
93
108
  onProgressChange?.(0);
94
109
  const res = await fetch(`${apiPath}/request-upload`, {
95
110
  method: 'POST',
96
111
  credentials: 'include',
112
+ signal: signal,
97
113
  body: JSON.stringify({
98
114
  bucketName,
99
115
  input,
@@ -119,13 +135,19 @@ async function uploadFile({ file, input, onProgressChange, options }, { apiPath,
119
135
  bucketName,
120
136
  multipartInfo: json.multipart,
121
137
  onProgressChange,
138
+ signal,
122
139
  file,
123
140
  apiPath
124
141
  });
125
142
  } else if ('uploadUrl' in json) {
126
143
  // Single part upload
127
144
  // Upload the file to the signed URL and get the progress
128
- await uploadFileInner(file, json.uploadUrl, onProgressChange);
145
+ await uploadFileInner({
146
+ file,
147
+ uploadUrl: json.uploadUrl,
148
+ onProgressChange,
149
+ signal
150
+ });
129
151
  } else {
130
152
  throw new EdgeStoreClientError('An error occurred');
131
153
  }
@@ -139,13 +161,16 @@ async function uploadFile({ file, input, onProgressChange, options }, { apiPath,
139
161
  metadata: json.metadata
140
162
  };
141
163
  } catch (e) {
164
+ if (e instanceof Error && e.name === 'AbortError') {
165
+ throw new uploadAbortedError.UploadAbortedError('File upload aborted');
166
+ }
142
167
  onProgressChange?.(0);
143
168
  throw e;
144
169
  }
145
170
  }
146
171
  /**
147
172
  * Protected files need third-party cookies to work.
148
- * Since third party cookies doesn't work on localhost,
173
+ * Since third party cookies don't work on localhost,
149
174
  * we need to proxy the file through the server.
150
175
  */ function getUrl(url, apiPath) {
151
176
  const mode = typeof process !== 'undefined' ? process.env.NODE_ENV : undefined?.DEV ? 'development' : 'production';
@@ -159,8 +184,13 @@ async function uploadFile({ file, input, onProgressChange, options }, { apiPath,
159
184
  }
160
185
  return url;
161
186
  }
162
- const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
187
+ async function uploadFileInner(props) {
188
+ const { file, uploadUrl, onProgressChange, signal } = props;
163
189
  const promise = new Promise((resolve, reject)=>{
190
+ if (signal?.aborted) {
191
+ reject(new uploadAbortedError.UploadAbortedError('File upload aborted'));
192
+ return;
193
+ }
164
194
  const request = new XMLHttpRequest();
165
195
  request.open('PUT', uploadUrl);
166
196
  // This is for Azure provider. Specifies the blob type
@@ -179,35 +209,45 @@ const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
179
209
  reject(new Error('Error uploading file'));
180
210
  });
181
211
  request.addEventListener('abort', ()=>{
182
- reject(new Error('File upload aborted'));
212
+ reject(new uploadAbortedError.UploadAbortedError('File upload aborted'));
183
213
  });
184
214
  request.addEventListener('loadend', ()=>{
185
215
  // Return the ETag header (needed to complete multipart upload)
186
216
  resolve(request.getResponseHeader('ETag'));
187
217
  });
218
+ if (signal) {
219
+ signal.addEventListener('abort', ()=>{
220
+ request.abort();
221
+ });
222
+ }
188
223
  request.send(file);
189
224
  });
190
225
  return promise;
191
- };
226
+ }
192
227
  async function multipartUpload(params) {
193
- const { bucketName, multipartInfo, onProgressChange, file, apiPath } = params;
228
+ const { bucketName, multipartInfo, onProgressChange, file, signal, apiPath } = params;
194
229
  const { partSize, parts, totalParts, uploadId, key } = multipartInfo;
195
230
  const uploadingParts = [];
196
231
  const uploadPart = async (params)=>{
197
232
  const { part, chunk } = params;
198
233
  const { uploadUrl } = part;
199
- const eTag = await uploadFileInner(chunk, uploadUrl, (progress)=>{
200
- const uploadingPart = uploadingParts.find((p)=>p.partNumber === part.partNumber);
201
- if (uploadingPart) {
202
- uploadingPart.progress = progress;
203
- } else {
204
- uploadingParts.push({
205
- partNumber: part.partNumber,
206
- progress
207
- });
234
+ const eTag = await uploadFileInner({
235
+ file: chunk,
236
+ uploadUrl,
237
+ signal,
238
+ onProgressChange: (progress)=>{
239
+ const uploadingPart = uploadingParts.find((p)=>p.partNumber === part.partNumber);
240
+ if (uploadingPart) {
241
+ uploadingPart.progress = progress;
242
+ } else {
243
+ uploadingParts.push({
244
+ partNumber: part.partNumber,
245
+ progress
246
+ });
247
+ }
248
+ const totalProgress = Math.round(uploadingParts.reduce((acc, p)=>acc + p.progress * 100, 0) / totalParts) / 100;
249
+ onProgressChange?.(totalProgress);
208
250
  }
209
- const totalProgress = Math.round(uploadingParts.reduce((acc, p)=>acc + p.progress * 100, 0) / totalParts) / 100;
210
- onProgressChange?.(totalProgress);
211
251
  });
212
252
  if (!eTag) {
213
253
  throw new EdgeStoreClientError('Could not get ETag from multipart response');
@@ -285,6 +325,9 @@ async function queuedPromises({ items, fn, maxParallel, maxRetries = 0 }) {
285
325
  try {
286
326
  return await func();
287
327
  } catch (error) {
328
+ if (error instanceof uploadAbortedError.UploadAbortedError) {
329
+ throw error;
330
+ }
288
331
  if (retries > 0) {
289
332
  await new Promise((resolve)=>setTimeout(resolve, 5000));
290
333
  return executeWithRetry(func, retries - 1);
@@ -296,7 +339,7 @@ async function queuedPromises({ items, fn, maxParallel, maxRetries = 0 }) {
296
339
  const semaphore = {
297
340
  count: maxParallel,
298
341
  async wait () {
299
- // If we've reached our maximum concurrency or it's the last item, wait
342
+ // If we've reached our maximum concurrency, or it's the last item, wait
300
343
  while(this.count <= 0)await new Promise((resolve)=>setTimeout(resolve, 500));
301
344
  this.count--;
302
345
  },
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { EdgeStoreApiClientError } from '@edgestore/shared';
3
+ import { U as UploadAbortedError } from './uploadAbortedError-e1379bb0.mjs';
3
4
 
4
5
  class EdgeStoreClientError extends Error {
5
6
  constructor(message){
@@ -28,15 +29,29 @@ function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5
28
29
  upload: async (params)=>{
29
30
  try {
30
31
  params.onProgressChange?.(0);
32
+ // This handles the case where the user cancels the upload while it's waiting in the queue
33
+ const abortPromise = new Promise((resolve)=>{
34
+ params.signal?.addEventListener('abort', ()=>{
35
+ resolve();
36
+ }, {
37
+ once: true
38
+ });
39
+ });
31
40
  while(uploadingCountRef.current >= maxConcurrentUploads && uploadingCountRef.current > 0){
32
- await new Promise((resolve)=>setTimeout(resolve, 300));
41
+ await Promise.race([
42
+ new Promise((resolve)=>setTimeout(resolve, 300)),
43
+ abortPromise
44
+ ]);
45
+ if (params.signal?.aborted) {
46
+ throw new UploadAbortedError('File upload aborted');
47
+ }
33
48
  }
34
49
  uploadingCountRef.current++;
35
- const test = await uploadFile(params, {
50
+ const fileInfo = await uploadFile(params, {
36
51
  bucketName: bucketName,
37
52
  apiPath
38
53
  });
39
- return test;
54
+ return fileInfo;
40
55
  } finally{
41
56
  uploadingCountRef.current--;
42
57
  }
@@ -64,12 +79,13 @@ function createNextProxy({ apiPath, uploadingCountRef, maxConcurrentUploads = 5
64
79
  }
65
80
  });
66
81
  }
67
- async function uploadFile({ file, input, onProgressChange, options }, { apiPath, bucketName }) {
82
+ async function uploadFile({ file, signal, input, onProgressChange, options }, { apiPath, bucketName }) {
68
83
  try {
69
84
  onProgressChange?.(0);
70
85
  const res = await fetch(`${apiPath}/request-upload`, {
71
86
  method: 'POST',
72
87
  credentials: 'include',
88
+ signal: signal,
73
89
  body: JSON.stringify({
74
90
  bucketName,
75
91
  input,
@@ -95,13 +111,19 @@ async function uploadFile({ file, input, onProgressChange, options }, { apiPath,
95
111
  bucketName,
96
112
  multipartInfo: json.multipart,
97
113
  onProgressChange,
114
+ signal,
98
115
  file,
99
116
  apiPath
100
117
  });
101
118
  } else if ('uploadUrl' in json) {
102
119
  // Single part upload
103
120
  // Upload the file to the signed URL and get the progress
104
- await uploadFileInner(file, json.uploadUrl, onProgressChange);
121
+ await uploadFileInner({
122
+ file,
123
+ uploadUrl: json.uploadUrl,
124
+ onProgressChange,
125
+ signal
126
+ });
105
127
  } else {
106
128
  throw new EdgeStoreClientError('An error occurred');
107
129
  }
@@ -115,13 +137,16 @@ async function uploadFile({ file, input, onProgressChange, options }, { apiPath,
115
137
  metadata: json.metadata
116
138
  };
117
139
  } catch (e) {
140
+ if (e instanceof Error && e.name === 'AbortError') {
141
+ throw new UploadAbortedError('File upload aborted');
142
+ }
118
143
  onProgressChange?.(0);
119
144
  throw e;
120
145
  }
121
146
  }
122
147
  /**
123
148
  * Protected files need third-party cookies to work.
124
- * Since third party cookies doesn't work on localhost,
149
+ * Since third party cookies don't work on localhost,
125
150
  * we need to proxy the file through the server.
126
151
  */ function getUrl(url, apiPath) {
127
152
  const mode = typeof process !== 'undefined' ? process.env.NODE_ENV : import.meta.env?.DEV ? 'development' : 'production';
@@ -135,8 +160,13 @@ async function uploadFile({ file, input, onProgressChange, options }, { apiPath,
135
160
  }
136
161
  return url;
137
162
  }
138
- const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
163
+ async function uploadFileInner(props) {
164
+ const { file, uploadUrl, onProgressChange, signal } = props;
139
165
  const promise = new Promise((resolve, reject)=>{
166
+ if (signal?.aborted) {
167
+ reject(new UploadAbortedError('File upload aborted'));
168
+ return;
169
+ }
140
170
  const request = new XMLHttpRequest();
141
171
  request.open('PUT', uploadUrl);
142
172
  // This is for Azure provider. Specifies the blob type
@@ -155,35 +185,45 @@ const uploadFileInner = async (file, uploadUrl, onProgressChange)=>{
155
185
  reject(new Error('Error uploading file'));
156
186
  });
157
187
  request.addEventListener('abort', ()=>{
158
- reject(new Error('File upload aborted'));
188
+ reject(new UploadAbortedError('File upload aborted'));
159
189
  });
160
190
  request.addEventListener('loadend', ()=>{
161
191
  // Return the ETag header (needed to complete multipart upload)
162
192
  resolve(request.getResponseHeader('ETag'));
163
193
  });
194
+ if (signal) {
195
+ signal.addEventListener('abort', ()=>{
196
+ request.abort();
197
+ });
198
+ }
164
199
  request.send(file);
165
200
  });
166
201
  return promise;
167
- };
202
+ }
168
203
  async function multipartUpload(params) {
169
- const { bucketName, multipartInfo, onProgressChange, file, apiPath } = params;
204
+ const { bucketName, multipartInfo, onProgressChange, file, signal, apiPath } = params;
170
205
  const { partSize, parts, totalParts, uploadId, key } = multipartInfo;
171
206
  const uploadingParts = [];
172
207
  const uploadPart = async (params)=>{
173
208
  const { part, chunk } = params;
174
209
  const { uploadUrl } = part;
175
- const eTag = await uploadFileInner(chunk, uploadUrl, (progress)=>{
176
- const uploadingPart = uploadingParts.find((p)=>p.partNumber === part.partNumber);
177
- if (uploadingPart) {
178
- uploadingPart.progress = progress;
179
- } else {
180
- uploadingParts.push({
181
- partNumber: part.partNumber,
182
- progress
183
- });
210
+ const eTag = await uploadFileInner({
211
+ file: chunk,
212
+ uploadUrl,
213
+ signal,
214
+ onProgressChange: (progress)=>{
215
+ const uploadingPart = uploadingParts.find((p)=>p.partNumber === part.partNumber);
216
+ if (uploadingPart) {
217
+ uploadingPart.progress = progress;
218
+ } else {
219
+ uploadingParts.push({
220
+ partNumber: part.partNumber,
221
+ progress
222
+ });
223
+ }
224
+ const totalProgress = Math.round(uploadingParts.reduce((acc, p)=>acc + p.progress * 100, 0) / totalParts) / 100;
225
+ onProgressChange?.(totalProgress);
184
226
  }
185
- const totalProgress = Math.round(uploadingParts.reduce((acc, p)=>acc + p.progress * 100, 0) / totalParts) / 100;
186
- onProgressChange?.(totalProgress);
187
227
  });
188
228
  if (!eTag) {
189
229
  throw new EdgeStoreClientError('Could not get ETag from multipart response');
@@ -261,6 +301,9 @@ async function queuedPromises({ items, fn, maxParallel, maxRetries = 0 }) {
261
301
  try {
262
302
  return await func();
263
303
  } catch (error) {
304
+ if (error instanceof UploadAbortedError) {
305
+ throw error;
306
+ }
264
307
  if (retries > 0) {
265
308
  await new Promise((resolve)=>setTimeout(resolve, 5000));
266
309
  return executeWithRetry(func, retries - 1);
@@ -272,7 +315,7 @@ async function queuedPromises({ items, fn, maxParallel, maxRetries = 0 }) {
272
315
  const semaphore = {
273
316
  count: maxParallel,
274
317
  async wait () {
275
- // If we've reached our maximum concurrency or it's the last item, wait
318
+ // If we've reached our maximum concurrency, or it's the last item, wait
276
319
  while(this.count <= 0)await new Promise((resolve)=>setTimeout(resolve, 500));
277
320
  this.count--;
278
321
  },
@@ -0,0 +1,4 @@
1
+ export declare class UploadAbortedError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ //# sourceMappingURL=uploadAbortedError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadAbortedError.d.ts","sourceRoot":"","sources":["../../../src/libs/errors/uploadAbortedError.ts"],"names":[],"mappings":"AAAA,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B"}
@@ -1,2 +1,7 @@
1
- export { EdgeStoreApiClientError } from '@edgestore/shared';
1
+ import { EdgeStoreApiClientError as DeprecatedEdgeStoreApiClientError } from '@edgestore/shared';
2
+ /**
3
+ * @deprecated import from `@edgestore/react/errors` instead.
4
+ */
5
+ export declare class EdgeStoreApiClientError extends DeprecatedEdgeStoreApiClientError {
6
+ }
2
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shared/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,IAAI,iCAAiC,EAAE,MAAM,mBAAmB,CAAC;AAEjG;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,iCAAiC;CAAG"}
@@ -4,9 +4,10 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var shared = require('@edgestore/shared');
6
6
 
7
+ // TODO: delete this file on next major release (moved to "errors")
8
+ /**
9
+ * @deprecated import from `@edgestore/react/errors` instead.
10
+ */ class EdgeStoreApiClientError extends shared.EdgeStoreApiClientError {
11
+ }
7
12
 
8
-
9
- Object.defineProperty(exports, 'EdgeStoreApiClientError', {
10
- enumerable: true,
11
- get: function () { return shared.EdgeStoreApiClientError; }
12
- });
13
+ exports.EdgeStoreApiClientError = EdgeStoreApiClientError;
@@ -1 +1,9 @@
1
- export { EdgeStoreApiClientError } from '@edgestore/shared';
1
+ import { EdgeStoreApiClientError as EdgeStoreApiClientError$1 } from '@edgestore/shared';
2
+
3
+ // TODO: delete this file on next major release (moved to "errors")
4
+ /**
5
+ * @deprecated import from `@edgestore/react/errors` instead.
6
+ */ class EdgeStoreApiClientError extends EdgeStoreApiClientError$1 {
7
+ }
8
+
9
+ export { EdgeStoreApiClientError };
@@ -0,0 +1,8 @@
1
+ class UploadAbortedError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UploadAbortedError';
5
+ }
6
+ }
7
+
8
+ export { UploadAbortedError as U };
@@ -0,0 +1,8 @@
1
+ class UploadAbortedError extends Error {
2
+ constructor(message){
3
+ super(message);
4
+ this.name = 'UploadAbortedError';
5
+ }
6
+ }
7
+
8
+ export { UploadAbortedError as U };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ class UploadAbortedError extends Error {
4
+ constructor(message){
5
+ super(message);
6
+ this.name = 'UploadAbortedError';
7
+ }
8
+ }
9
+
10
+ exports.UploadAbortedError = UploadAbortedError;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export declare function getDownloadUrl(url: string, name?: string): string;
8
8
  /**
9
- * This will format the file size to a human readable format.
9
+ * This will format the file size to a human-readable format.
10
10
  *
11
11
  * @example 1024 => 1 KB
12
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,UAIxD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,UAa5C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,UAIxD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,UAO5C"}
@@ -13,21 +13,15 @@ Object.defineProperty(exports, '__esModule', { value: true });
13
13
  return urlObj.toString();
14
14
  }
15
15
  /**
16
- * This will format the file size to a human readable format.
16
+ * This will format the file size to a human-readable format.
17
17
  *
18
18
  * @example 1024 => 1 KB
19
19
  */ function formatFileSize(bytes) {
20
- if (!bytes) {
21
- return '0 Bytes';
22
- }
23
- bytes = Number(bytes);
24
- if (bytes === 0) {
25
- return '0 Bytes';
26
- }
20
+ if (!bytes) return '0 B';
27
21
  const k = 1024;
28
22
  const dm = 2;
29
23
  const sizes = [
30
- 'Bytes',
24
+ 'B',
31
25
  'KB',
32
26
  'MB',
33
27
  'GB',
@@ -9,21 +9,15 @@
9
9
  return urlObj.toString();
10
10
  }
11
11
  /**
12
- * This will format the file size to a human readable format.
12
+ * This will format the file size to a human-readable format.
13
13
  *
14
14
  * @example 1024 => 1 KB
15
15
  */ function formatFileSize(bytes) {
16
- if (!bytes) {
17
- return '0 Bytes';
18
- }
19
- bytes = Number(bytes);
20
- if (bytes === 0) {
21
- return '0 Bytes';
22
- }
16
+ if (!bytes) return '0 B';
23
17
  const k = 1024;
24
18
  const dm = 2;
25
19
  const sizes = [
26
- 'Bytes',
20
+ 'B',
27
21
  'KB',
28
22
  'MB',
29
23
  'GB',
@@ -0,0 +1 @@
1
+ export * from '../dist/errors';
@@ -0,0 +1 @@
1
+ module.exports = require('../dist/errors');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgestore/react",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Upload files with ease from React/Next.js",
5
5
  "homepage": "https://edgestore.dev",
6
6
  "repository": "https://github.com/edgestorejs/edgestore.git",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/shared/index.mjs",
42
42
  "require": "./dist/shared/index.js",
43
43
  "default": "./dist/shared/index.js"
44
+ },
45
+ "./errors": {
46
+ "import": "./dist/errors/index.mjs",
47
+ "require": "./dist/errors/index.js",
48
+ "default": "./dist/errors/index.js"
44
49
  }
45
50
  },
46
51
  "files": [
@@ -50,6 +55,7 @@
50
55
  "package.json",
51
56
  "utils",
52
57
  "shared",
58
+ "errors",
53
59
  "!**/*.test.*"
54
60
  ],
55
61
  "private": false,
@@ -60,7 +66,7 @@
60
66
  "dependencies": {
61
67
  "@aws-sdk/client-s3": "^3.294.0",
62
68
  "@aws-sdk/s3-request-presigner": "^3.294.0",
63
- "@edgestore/shared": "0.1.6",
69
+ "@edgestore/shared": "0.2.0",
64
70
  "@panva/hkdf": "^1.0.4",
65
71
  "cookie": "^0.5.0",
66
72
  "jose": "^4.13.1",
@@ -83,5 +89,5 @@
83
89
  "typescript": "^5.1.6",
84
90
  "zod": "3.21.4"
85
91
  },
86
- "gitHead": "c9d1d53a21038b4c0c3b9df685d2cac9cabc88ae"
92
+ "gitHead": "a5ecda9ba69eaada4624556dd9422ce49e5dfb66"
87
93
  }
@@ -8,6 +8,7 @@ import {
8
8
  import { type z } from 'zod';
9
9
  import EdgeStoreClientError from './libs/errors/EdgeStoreClientError';
10
10
  import { handleError } from './libs/errors/handleError';
11
+ import { UploadAbortedError } from './libs/errors/uploadAbortedError';
11
12
 
12
13
  /**
13
14
  * @internal
@@ -20,15 +21,33 @@ export type Prettify<TType> = {
20
21
 
21
22
  export type BucketFunctions<TRouter extends AnyRouter> = {
22
23
  [K in keyof TRouter['buckets']]: {
24
+ /**
25
+ * Upload a file to the bucket
26
+ *
27
+ * @example
28
+ * await edgestore.myBucket.upload({
29
+ * file: file,
30
+ * signal: abortController.signal, // if you want to be able to cancel the ongoing upload
31
+ * onProgressChange: (progress) => { console.log(progress) }, // if you want to show the progress of the upload
32
+ * input: {...} // if the bucket has an input schema
33
+ * options: {
34
+ * manualFileName: file.name, // if you want to use a custom file name
35
+ * replaceTargetUrl: url, // if you want to replace an existing file
36
+ * temporary: true, // if you want to delete the file after 24 hours
37
+ * }
38
+ * })
39
+ */
23
40
  upload: (
24
41
  params: z.infer<TRouter['buckets'][K]['_def']['input']> extends never
25
42
  ? {
26
43
  file: File;
44
+ signal?: AbortSignal;
27
45
  onProgressChange?: OnProgressChangeHandler;
28
46
  options?: UploadOptions;
29
47
  }
30
48
  : {
31
49
  file: File;
50
+ signal?: AbortSignal;
32
51
  input: z.infer<TRouter['buckets'][K]['_def']['input']>;
33
52
  onProgressChange?: OnProgressChangeHandler;
34
53
  options?: UploadOptions;
@@ -80,18 +99,37 @@ export function createNextProxy<TRouter extends AnyRouter>({
80
99
  upload: async (params) => {
81
100
  try {
82
101
  params.onProgressChange?.(0);
102
+
103
+ // This handles the case where the user cancels the upload while it's waiting in the queue
104
+ const abortPromise = new Promise<void>((resolve) => {
105
+ params.signal?.addEventListener(
106
+ 'abort',
107
+ () => {
108
+ resolve();
109
+ },
110
+ { once: true },
111
+ );
112
+ });
113
+
83
114
  while (
84
115
  uploadingCountRef.current >= maxConcurrentUploads &&
85
116
  uploadingCountRef.current > 0
86
117
  ) {
87
- await new Promise((resolve) => setTimeout(resolve, 300));
118
+ await Promise.race([
119
+ new Promise((resolve) => setTimeout(resolve, 300)),
120
+ abortPromise,
121
+ ]);
122
+ if (params.signal?.aborted) {
123
+ throw new UploadAbortedError('File upload aborted');
124
+ }
88
125
  }
126
+
89
127
  uploadingCountRef.current++;
90
- const test = await uploadFile(params, {
128
+ const fileInfo = await uploadFile(params, {
91
129
  bucketName: bucketName as string,
92
130
  apiPath,
93
131
  });
94
- return test;
132
+ return fileInfo;
95
133
  } finally {
96
134
  uploadingCountRef.current--;
97
135
  }
@@ -123,11 +161,13 @@ export function createNextProxy<TRouter extends AnyRouter>({
123
161
  async function uploadFile(
124
162
  {
125
163
  file,
164
+ signal,
126
165
  input,
127
166
  onProgressChange,
128
167
  options,
129
168
  }: {
130
169
  file: File;
170
+ signal?: AbortSignal;
131
171
  input?: object;
132
172
  onProgressChange?: OnProgressChangeHandler;
133
173
  options?: UploadOptions;
@@ -145,6 +185,7 @@ async function uploadFile(
145
185
  const res = await fetch(`${apiPath}/request-upload`, {
146
186
  method: 'POST',
147
187
  credentials: 'include',
188
+ signal: signal,
148
189
  body: JSON.stringify({
149
190
  bucketName,
150
191
  input,
@@ -170,13 +211,19 @@ async function uploadFile(
170
211
  bucketName,
171
212
  multipartInfo: json.multipart,
172
213
  onProgressChange,
214
+ signal,
173
215
  file,
174
216
  apiPath,
175
217
  });
176
218
  } else if ('uploadUrl' in json) {
177
219
  // Single part upload
178
220
  // Upload the file to the signed URL and get the progress
179
- await uploadFileInner(file, json.uploadUrl, onProgressChange);
221
+ await uploadFileInner({
222
+ file,
223
+ uploadUrl: json.uploadUrl,
224
+ onProgressChange,
225
+ signal,
226
+ });
180
227
  } else {
181
228
  throw new EdgeStoreClientError('An error occurred');
182
229
  }
@@ -192,6 +239,9 @@ async function uploadFile(
192
239
  metadata: json.metadata as any,
193
240
  };
194
241
  } catch (e) {
242
+ if (e instanceof Error && e.name === 'AbortError') {
243
+ throw new UploadAbortedError('File upload aborted');
244
+ }
195
245
  onProgressChange?.(0);
196
246
  throw e;
197
247
  }
@@ -199,7 +249,7 @@ async function uploadFile(
199
249
 
200
250
  /**
201
251
  * Protected files need third-party cookies to work.
202
- * Since third party cookies doesn't work on localhost,
252
+ * Since third party cookies don't work on localhost,
203
253
  * we need to proxy the file through the server.
204
254
  */
205
255
  function getUrl(url: string, apiPath: string) {
@@ -221,12 +271,19 @@ function getUrl(url: string, apiPath: string) {
221
271
  return url;
222
272
  }
223
273
 
224
- const uploadFileInner = async (
225
- file: File | Blob,
226
- uploadUrl: string,
227
- onProgressChange?: OnProgressChangeHandler,
228
- ) => {
274
+ async function uploadFileInner(props: {
275
+ file: File | Blob;
276
+ uploadUrl: string;
277
+ onProgressChange?: OnProgressChangeHandler;
278
+ signal?: AbortSignal;
279
+ }) {
280
+ const { file, uploadUrl, onProgressChange, signal } = props;
229
281
  const promise = new Promise<string | null>((resolve, reject) => {
282
+ if (signal?.aborted) {
283
+ reject(new UploadAbortedError('File upload aborted'));
284
+ return;
285
+ }
286
+
230
287
  const request = new XMLHttpRequest();
231
288
  request.open('PUT', uploadUrl);
232
289
  // This is for Azure provider. Specifies the blob type
@@ -245,17 +302,23 @@ const uploadFileInner = async (
245
302
  reject(new Error('Error uploading file'));
246
303
  });
247
304
  request.addEventListener('abort', () => {
248
- reject(new Error('File upload aborted'));
305
+ reject(new UploadAbortedError('File upload aborted'));
249
306
  });
250
307
  request.addEventListener('loadend', () => {
251
308
  // Return the ETag header (needed to complete multipart upload)
252
309
  resolve(request.getResponseHeader('ETag'));
253
310
  });
254
311
 
312
+ if (signal) {
313
+ signal.addEventListener('abort', () => {
314
+ request.abort();
315
+ });
316
+ }
317
+
255
318
  request.send(file);
256
319
  });
257
320
  return promise;
258
- };
321
+ }
259
322
 
260
323
  async function multipartUpload(params: {
261
324
  bucketName: string;
@@ -265,9 +328,11 @@ async function multipartUpload(params: {
265
328
  >['multipart'];
266
329
  onProgressChange: OnProgressChangeHandler | undefined;
267
330
  file: File;
331
+ signal: AbortSignal | undefined;
268
332
  apiPath: string;
269
333
  }) {
270
- const { bucketName, multipartInfo, onProgressChange, file, apiPath } = params;
334
+ const { bucketName, multipartInfo, onProgressChange, file, signal, apiPath } =
335
+ params;
271
336
  const { partSize, parts, totalParts, uploadId, key } = multipartInfo;
272
337
  const uploadingParts: {
273
338
  partNumber: number;
@@ -279,24 +344,29 @@ async function multipartUpload(params: {
279
344
  }) => {
280
345
  const { part, chunk } = params;
281
346
  const { uploadUrl } = part;
282
- const eTag = await uploadFileInner(chunk, uploadUrl, (progress) => {
283
- const uploadingPart = uploadingParts.find(
284
- (p) => p.partNumber === part.partNumber,
285
- );
286
- if (uploadingPart) {
287
- uploadingPart.progress = progress;
288
- } else {
289
- uploadingParts.push({
290
- partNumber: part.partNumber,
291
- progress,
292
- });
293
- }
294
- const totalProgress =
295
- Math.round(
296
- uploadingParts.reduce((acc, p) => acc + p.progress * 100, 0) /
297
- totalParts,
298
- ) / 100;
299
- onProgressChange?.(totalProgress);
347
+ const eTag = await uploadFileInner({
348
+ file: chunk,
349
+ uploadUrl,
350
+ signal,
351
+ onProgressChange: (progress) => {
352
+ const uploadingPart = uploadingParts.find(
353
+ (p) => p.partNumber === part.partNumber,
354
+ );
355
+ if (uploadingPart) {
356
+ uploadingPart.progress = progress;
357
+ } else {
358
+ uploadingParts.push({
359
+ partNumber: part.partNumber,
360
+ progress,
361
+ });
362
+ }
363
+ const totalProgress =
364
+ Math.round(
365
+ uploadingParts.reduce((acc, p) => acc + p.progress * 100, 0) /
366
+ totalParts,
367
+ ) / 100;
368
+ onProgressChange?.(totalProgress);
369
+ },
300
370
  });
301
371
  if (!eTag) {
302
372
  throw new EdgeStoreClientError(
@@ -424,6 +494,9 @@ async function queuedPromises<TType, TRes>({
424
494
  try {
425
495
  return await func();
426
496
  } catch (error) {
497
+ if (error instanceof UploadAbortedError) {
498
+ throw error;
499
+ }
427
500
  if (retries > 0) {
428
501
  await new Promise((resolve) => setTimeout(resolve, 5000));
429
502
  return executeWithRetry(func, retries - 1);
@@ -436,7 +509,7 @@ async function queuedPromises<TType, TRes>({
436
509
  const semaphore = {
437
510
  count: maxParallel,
438
511
  async wait() {
439
- // If we've reached our maximum concurrency or it's the last item, wait
512
+ // If we've reached our maximum concurrency, or it's the last item, wait
440
513
  while (this.count <= 0)
441
514
  await new Promise((resolve) => setTimeout(resolve, 500));
442
515
  this.count--;
@@ -0,0 +1,2 @@
1
+ export { EdgeStoreApiClientError } from '@edgestore/shared';
2
+ export { UploadAbortedError } from '../libs/errors/uploadAbortedError';
@@ -0,0 +1,6 @@
1
+ export class UploadAbortedError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'UploadAbortedError';
5
+ }
6
+ }
@@ -1 +1,7 @@
1
- export { EdgeStoreApiClientError } from '@edgestore/shared';
1
+ // TODO: delete this file on next major release (moved to "errors")
2
+ import { EdgeStoreApiClientError as DeprecatedEdgeStoreApiClientError } from '@edgestore/shared';
3
+
4
+ /**
5
+ * @deprecated import from `@edgestore/react/errors` instead.
6
+ */
7
+ export class EdgeStoreApiClientError extends DeprecatedEdgeStoreApiClientError {}
@@ -11,21 +11,15 @@ export function getDownloadUrl(url: string, name?: string) {
11
11
  }
12
12
 
13
13
  /**
14
- * This will format the file size to a human readable format.
14
+ * This will format the file size to a human-readable format.
15
15
  *
16
16
  * @example 1024 => 1 KB
17
17
  */
18
18
  export function formatFileSize(bytes?: number) {
19
- if (!bytes) {
20
- return '0 Bytes';
21
- }
22
- bytes = Number(bytes);
23
- if (bytes === 0) {
24
- return '0 Bytes';
25
- }
19
+ if (!bytes) return '0 B';
26
20
  const k = 1024;
27
21
  const dm = 2;
28
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
22
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
29
23
  const i = Math.floor(Math.log(bytes) / Math.log(k));
30
24
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
31
25
  }