@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 +2 -2
- package/dist/createNextProxy.d.ts +18 -0
- package/dist/createNextProxy.d.ts.map +1 -1
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +14 -0
- package/dist/errors/index.mjs +2 -0
- package/dist/index.js +65 -22
- package/dist/index.mjs +65 -22
- package/dist/libs/errors/uploadAbortedError.d.ts +4 -0
- package/dist/libs/errors/uploadAbortedError.d.ts.map +1 -0
- package/dist/shared/index.d.ts +6 -1
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +6 -5
- package/dist/shared/index.mjs +9 -1
- package/dist/uploadAbortedError-a628b025.js +8 -0
- package/dist/uploadAbortedError-e1379bb0.mjs +8 -0
- package/dist/uploadAbortedError-fbfcc57b.js +10 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -9
- package/dist/utils/index.mjs +3 -9
- package/errors/index.d.ts +1 -0
- package/errors/index.js +1 -0
- package/package.json +9 -3
- package/src/createNextProxy.ts +105 -32
- package/src/errors/index.ts +2 -0
- package/src/libs/errors/uploadAbortedError.ts +6 -0
- package/src/shared/index.ts +7 -1
- package/src/utils/index.ts +3 -9
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
|
|
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":";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;
|
|
1
|
+
{"version":3,"file":"createNextProxy.d.ts","sourceRoot":"","sources":["../src/createNextProxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;
|
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
|
|
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
|
|
74
|
+
const fileInfo = await uploadFile(params, {
|
|
60
75
|
bucketName: bucketName,
|
|
61
76
|
apiPath
|
|
62
77
|
});
|
|
63
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
uploadingParts.
|
|
205
|
-
|
|
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
|
|
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
|
|
50
|
+
const fileInfo = await uploadFile(params, {
|
|
36
51
|
bucketName: bucketName,
|
|
37
52
|
apiPath
|
|
38
53
|
});
|
|
39
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
uploadingParts.
|
|
181
|
-
|
|
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 @@
|
|
|
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"}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
|
|
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":"
|
|
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"}
|
package/dist/shared/index.js
CHANGED
|
@@ -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;
|
package/dist/shared/index.mjs
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
24
|
+
'B',
|
|
31
25
|
'KB',
|
|
32
26
|
'MB',
|
|
33
27
|
'GB',
|
package/dist/utils/index.mjs
CHANGED
|
@@ -9,21 +9,15 @@
|
|
|
9
9
|
return urlObj.toString();
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* This will format the file size to a human
|
|
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
|
-
'
|
|
20
|
+
'B',
|
|
27
21
|
'KB',
|
|
28
22
|
'MB',
|
|
29
23
|
'GB',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../dist/errors';
|
package/errors/index.js
ADDED
|
@@ -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.
|
|
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.
|
|
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": "
|
|
92
|
+
"gitHead": "a5ecda9ba69eaada4624556dd9422ce49e5dfb66"
|
|
87
93
|
}
|
package/src/createNextProxy.ts
CHANGED
|
@@ -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
|
|
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
|
|
128
|
+
const fileInfo = await uploadFile(params, {
|
|
91
129
|
bucketName: bucketName as string,
|
|
92
130
|
apiPath,
|
|
93
131
|
});
|
|
94
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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 } =
|
|
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(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
uploadingPart
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
progress
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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--;
|
package/src/shared/index.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
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 {}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
|
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 = ['
|
|
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
|
}
|