@transloadit/convex 0.0.2 → 0.0.4
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 +184 -121
- package/dist/client/index.d.ts +100 -60
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +69 -31
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/component.d.ts +35 -15
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/dataModel.d.ts +1 -1
- package/dist/component/_generated/server.d.ts +1 -1
- package/dist/component/apiUtils.d.ts +36 -7
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +60 -40
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +71 -49
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +206 -73
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +11 -13
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +3 -10
- package/dist/component/schema.js.map +1 -1
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/index.js +49 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/react/index.d.ts +213 -17
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +726 -105
- package/dist/react/index.js.map +1 -1
- package/dist/shared/assemblyUrls.d.ts +10 -0
- package/dist/shared/assemblyUrls.d.ts.map +1 -0
- package/dist/shared/assemblyUrls.js +26 -0
- package/dist/shared/assemblyUrls.js.map +1 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +10 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/pollAssembly.d.ts +12 -0
- package/dist/shared/pollAssembly.d.ts.map +1 -0
- package/dist/shared/pollAssembly.js +50 -0
- package/dist/shared/pollAssembly.js.map +1 -0
- package/dist/shared/resultTypes.d.ts +37 -0
- package/dist/shared/resultTypes.d.ts.map +1 -0
- package/dist/shared/resultTypes.js +2 -0
- package/dist/shared/resultTypes.js.map +1 -0
- package/dist/shared/resultUtils.d.ts +4 -0
- package/dist/shared/resultUtils.d.ts.map +1 -0
- package/dist/shared/resultUtils.js +69 -0
- package/dist/shared/resultUtils.js.map +1 -0
- package/dist/shared/tusUpload.d.ts +13 -0
- package/dist/shared/tusUpload.d.ts.map +1 -0
- package/dist/shared/tusUpload.js +32 -0
- package/dist/shared/tusUpload.js.map +1 -0
- package/dist/test/index.d.ts +65 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +8 -0
- package/dist/test/index.js.map +1 -0
- package/dist/test/nodeModules.d.ts +2 -0
- package/dist/test/nodeModules.d.ts.map +1 -0
- package/dist/test/nodeModules.js +19 -0
- package/dist/test/nodeModules.js.map +1 -0
- package/package.json +53 -15
- package/src/client/index.ts +141 -38
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +44 -13
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +1 -1
- package/src/component/apiUtils.test.ts +195 -2
- package/src/component/apiUtils.ts +124 -66
- package/src/component/lib.test.ts +243 -7
- package/src/component/lib.ts +302 -90
- package/src/component/schema.ts +3 -13
- package/src/debug/index.ts +84 -0
- package/src/react/index.test.tsx +340 -0
- package/src/react/index.tsx +1105 -152
- package/src/react/uploadWithTus.test.tsx +192 -0
- package/src/shared/assemblyUrls.test.ts +71 -0
- package/src/shared/assemblyUrls.ts +59 -0
- package/src/shared/errors.ts +23 -0
- package/src/shared/pollAssembly.ts +65 -0
- package/src/shared/resultTypes.ts +44 -0
- package/src/shared/resultUtils.test.ts +29 -0
- package/src/shared/resultUtils.ts +71 -0
- package/src/shared/tusUpload.ts +59 -0
- package/src/test/index.ts +10 -0
- package/src/test/nodeModules.ts +19 -0
package/dist/react/index.js
CHANGED
|
@@ -1,76 +1,201 @@
|
|
|
1
|
+
import { isAssemblyTerminal, } from "@transloadit/zod/v3/assemblyStatus";
|
|
1
2
|
import { useAction, useQuery } from "convex/react";
|
|
2
|
-
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { Upload } from "tus-js-client";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
import { getAssemblyStage, parseAssemblyStatus, } from "../shared/assemblyUrls.js";
|
|
6
|
+
import { transloaditError } from "../shared/errors.js";
|
|
7
|
+
import { pollAssembly } from "../shared/pollAssembly.js";
|
|
8
|
+
import { buildTusUploadConfig } from "../shared/tusUpload.js";
|
|
9
|
+
export async function uploadWithAssembly(createAssembly, uppy, options) {
|
|
10
|
+
const files = uppy.getFiles();
|
|
11
|
+
if (files.length === 0) {
|
|
12
|
+
throw transloaditError("upload", "No files provided for upload");
|
|
13
|
+
}
|
|
14
|
+
const args = {
|
|
15
|
+
...(options.createAssemblyArgs ?? {}),
|
|
16
|
+
fileCount: options.fileCount ?? files.length,
|
|
17
|
+
};
|
|
18
|
+
const assembly = await createAssembly(args);
|
|
19
|
+
options.onAssemblyCreated?.(assembly);
|
|
20
|
+
const tusPlugin = uppy.getPlugin("Tus");
|
|
21
|
+
if (!tusPlugin) {
|
|
22
|
+
throw transloaditError("upload", 'Uppy Tus plugin is required. Call uppy.use(Tus, { endpoint: "" }) before uploadWithAssembly.');
|
|
23
|
+
}
|
|
24
|
+
let tusEndpoint = null;
|
|
25
|
+
const addRequestId = options.addRequestId ?? true;
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (!file.data ||
|
|
28
|
+
typeof Blob === "undefined" ||
|
|
29
|
+
!(file.data instanceof Blob)) {
|
|
30
|
+
throw transloaditError("upload", "Uppy file is missing binary data for upload");
|
|
31
|
+
}
|
|
32
|
+
const uploadFile = file.data instanceof File
|
|
33
|
+
? file.data
|
|
34
|
+
: new File([file.data], file.name ?? "file", {
|
|
35
|
+
type: file.data.type || file.type,
|
|
36
|
+
});
|
|
37
|
+
const { endpoint, metadata } = buildTusUploadConfig(assembly.data, uploadFile, {
|
|
38
|
+
fieldName: options.fieldName,
|
|
39
|
+
metadata: options.metadata,
|
|
40
|
+
});
|
|
41
|
+
if (!tusEndpoint) {
|
|
42
|
+
tusEndpoint = endpoint;
|
|
43
|
+
}
|
|
44
|
+
uppy.setFileMeta(file.id, metadata);
|
|
45
|
+
uppy.setFileState(file.id, {
|
|
46
|
+
tus: {
|
|
47
|
+
...(file.tus ?? {}),
|
|
48
|
+
endpoint,
|
|
49
|
+
addRequestId,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (tusPlugin && "setOptions" in tusPlugin && tusEndpoint) {
|
|
54
|
+
tusPlugin.setOptions?.({ endpoint: tusEndpoint, addRequestId });
|
|
55
|
+
}
|
|
56
|
+
const uploadResult = await uppy.upload();
|
|
57
|
+
if (!uploadResult) {
|
|
58
|
+
throw transloaditError("upload", "Uppy upload did not return a result");
|
|
59
|
+
}
|
|
60
|
+
return { assembly, uploadResult };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Low-level tus upload helper. Prefer `useTransloaditUpload` for new code.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* Low-level tus upload helper. Prefer `useTransloaditUpload` for new code.
|
|
67
|
+
*/
|
|
68
|
+
export async function uploadWithTransloaditTus(createAssembly, file, options, events = {}) {
|
|
69
|
+
let currentState = {
|
|
70
|
+
isUploading: true,
|
|
71
|
+
progress: 0,
|
|
72
|
+
error: null,
|
|
73
|
+
};
|
|
74
|
+
const emitState = (next) => {
|
|
75
|
+
currentState = next;
|
|
76
|
+
events.onStateChange?.(next);
|
|
77
|
+
};
|
|
78
|
+
emitState(currentState);
|
|
79
|
+
try {
|
|
80
|
+
const assembly = await createAssembly({
|
|
81
|
+
templateId: options.templateId,
|
|
82
|
+
steps: options.steps,
|
|
83
|
+
fields: options.fields,
|
|
84
|
+
notifyUrl: options.notifyUrl,
|
|
85
|
+
numExpectedUploadFiles: options.numExpectedUploadFiles ?? 1,
|
|
86
|
+
expires: options.expires,
|
|
87
|
+
additionalParams: options.additionalParams,
|
|
88
|
+
userId: options.userId,
|
|
89
|
+
});
|
|
90
|
+
const data = assembly.data;
|
|
91
|
+
options.onAssemblyCreated?.(assembly);
|
|
92
|
+
const { endpoint, metadata } = buildTusUploadConfig(data, file, {
|
|
93
|
+
fieldName: options.fieldName,
|
|
94
|
+
metadata: options.metadata,
|
|
95
|
+
});
|
|
96
|
+
const getStatus = (error) => error.originalResponse?.getStatus &&
|
|
97
|
+
typeof error.originalResponse.getStatus === "function"
|
|
98
|
+
? error.originalResponse.getStatus()
|
|
99
|
+
: 0;
|
|
100
|
+
const retryDelays = options.retryDelays
|
|
101
|
+
? [...options.retryDelays]
|
|
102
|
+
: [1000, 5000, 15000, 30000];
|
|
103
|
+
const rateLimitRetryDelays = options.rateLimitRetryDelays
|
|
104
|
+
? [...options.rateLimitRetryDelays]
|
|
105
|
+
: [20_000, 40_000, 80_000];
|
|
106
|
+
const shouldRetry = (error) => {
|
|
107
|
+
const status = getStatus(error);
|
|
108
|
+
if (!status)
|
|
109
|
+
return true;
|
|
110
|
+
if (status === 409 || status === 423)
|
|
111
|
+
return true;
|
|
112
|
+
return status < 400 || status >= 500;
|
|
17
113
|
};
|
|
18
|
-
|
|
114
|
+
let uploadUrl = null;
|
|
115
|
+
let rateLimitAttempt = 0;
|
|
116
|
+
const runUpload = () => new Promise((resolve, reject) => {
|
|
117
|
+
let uploader;
|
|
118
|
+
const uploadOptions = {
|
|
119
|
+
endpoint,
|
|
120
|
+
metadata,
|
|
121
|
+
retryDelays,
|
|
122
|
+
uploadDataDuringCreation: options.uploadDataDuringCreation ?? false,
|
|
123
|
+
onUploadUrlAvailable: () => {
|
|
124
|
+
uploadUrl = uploader.url;
|
|
125
|
+
},
|
|
126
|
+
onShouldRetry: (error, retryAttempt) => options.onShouldRetry?.(error, retryAttempt) ?? shouldRetry(error),
|
|
127
|
+
onProgress: (bytesUploaded, bytesTotal) => {
|
|
128
|
+
const progress = Math.round((bytesUploaded / bytesTotal) * 100);
|
|
129
|
+
emitState({ isUploading: true, progress, error: null });
|
|
130
|
+
options.onProgress?.(progress);
|
|
131
|
+
},
|
|
132
|
+
onError: (error) => {
|
|
133
|
+
reject(error);
|
|
134
|
+
},
|
|
135
|
+
onSuccess: () => {
|
|
136
|
+
resolve();
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
if (options.chunkSize !== undefined) {
|
|
140
|
+
uploadOptions.chunkSize = options.chunkSize;
|
|
141
|
+
}
|
|
142
|
+
if (uploadUrl) {
|
|
143
|
+
uploadOptions.uploadUrl = uploadUrl;
|
|
144
|
+
}
|
|
145
|
+
if (options.overridePatchMethod !== undefined) {
|
|
146
|
+
uploadOptions.overridePatchMethod = options.overridePatchMethod;
|
|
147
|
+
}
|
|
148
|
+
if (options.storeFingerprintForResuming !== undefined) {
|
|
149
|
+
uploadOptions.storeFingerprintForResuming =
|
|
150
|
+
options.storeFingerprintForResuming;
|
|
151
|
+
}
|
|
152
|
+
if (options.removeFingerprintOnSuccess !== undefined) {
|
|
153
|
+
uploadOptions.removeFingerprintOnSuccess =
|
|
154
|
+
options.removeFingerprintOnSuccess;
|
|
155
|
+
}
|
|
156
|
+
uploader = new Upload(file, uploadOptions);
|
|
157
|
+
uploader.start();
|
|
158
|
+
});
|
|
159
|
+
while (true) {
|
|
19
160
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
resolve(response);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
reject(new Error(`Transloadit upload failed (${xhr.status}): ${JSON.stringify(response)}`));
|
|
26
|
-
}
|
|
161
|
+
await runUpload();
|
|
162
|
+
break;
|
|
27
163
|
}
|
|
28
164
|
catch (error) {
|
|
29
|
-
|
|
165
|
+
const status = getStatus(error);
|
|
166
|
+
if (status === 429 && rateLimitAttempt < rateLimitRetryDelays.length) {
|
|
167
|
+
const delay = rateLimitRetryDelays[rateLimitAttempt] ?? 0;
|
|
168
|
+
rateLimitAttempt += 1;
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
30
173
|
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
174
|
+
}
|
|
175
|
+
emitState({ isUploading: false, progress: 100, error: null });
|
|
176
|
+
return assembly;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const err = error instanceof Error
|
|
180
|
+
? error
|
|
181
|
+
: transloaditError("upload", "Upload failed");
|
|
182
|
+
emitState({ isUploading: false, progress: 0, error: err });
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
37
185
|
}
|
|
38
|
-
|
|
39
|
-
|
|
186
|
+
/**
|
|
187
|
+
* @deprecated Prefer `useTransloaditUpload` (single + multi-file) for new code.
|
|
188
|
+
*/
|
|
189
|
+
export function useTransloaditTusUpload(createAssembly) {
|
|
190
|
+
const create = useAction(createAssembly);
|
|
40
191
|
const [state, setState] = useState({
|
|
41
192
|
isUploading: false,
|
|
42
193
|
progress: 0,
|
|
43
194
|
error: null,
|
|
44
195
|
});
|
|
45
|
-
const upload = useCallback(async (file, options) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const params = await generate({
|
|
49
|
-
templateId: options.templateId,
|
|
50
|
-
steps: options.steps,
|
|
51
|
-
fields: options.fields,
|
|
52
|
-
notifyUrl: options.notifyUrl,
|
|
53
|
-
numExpectedUploadFiles: options.numExpectedUploadFiles,
|
|
54
|
-
expires: options.expires,
|
|
55
|
-
additionalParams: options.additionalParams,
|
|
56
|
-
userId: options.userId,
|
|
57
|
-
});
|
|
58
|
-
const response = await uploadViaForm(file, params, {
|
|
59
|
-
...options,
|
|
60
|
-
onProgress: (progress) => {
|
|
61
|
-
setState((prev) => ({ ...prev, progress }));
|
|
62
|
-
options.onProgress?.(progress);
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
setState({ isUploading: false, progress: 100, error: null });
|
|
66
|
-
return response;
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
const err = error instanceof Error ? error : new Error("Upload failed");
|
|
70
|
-
setState({ isUploading: false, progress: 0, error: err });
|
|
71
|
-
throw err;
|
|
72
|
-
}
|
|
73
|
-
}, [generate]);
|
|
196
|
+
const upload = useCallback(async (file, options) => uploadWithTransloaditTus(create, file, options, {
|
|
197
|
+
onStateChange: setState,
|
|
198
|
+
}), [create]);
|
|
74
199
|
const reset = useCallback(() => {
|
|
75
200
|
setState({ isUploading: false, progress: 0, error: null });
|
|
76
201
|
}, []);
|
|
@@ -82,81 +207,577 @@ export function useTransloaditUpload(generateUploadParams) {
|
|
|
82
207
|
error: state.error,
|
|
83
208
|
}), [state.error, state.isUploading, state.progress, upload, reset]);
|
|
84
209
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Low-level multi-file tus uploader. Prefer `useTransloaditUpload` for new code.
|
|
212
|
+
*/
|
|
213
|
+
export function uploadFilesWithTransloaditTus(createAssembly, files, options) {
|
|
214
|
+
const concurrency = Math.max(1, options.concurrency ?? 3);
|
|
215
|
+
const state = {
|
|
216
|
+
isUploading: true,
|
|
89
217
|
progress: 0,
|
|
90
218
|
error: null,
|
|
91
|
-
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
219
|
+
};
|
|
220
|
+
const results = files.map((file) => ({
|
|
221
|
+
file,
|
|
222
|
+
status: "canceled",
|
|
223
|
+
}));
|
|
224
|
+
const inFlight = new Set();
|
|
225
|
+
const abortController = new AbortController();
|
|
226
|
+
let cancelled = false;
|
|
227
|
+
const emitState = (next) => {
|
|
228
|
+
state.isUploading = next.isUploading;
|
|
229
|
+
state.progress = next.progress;
|
|
230
|
+
state.error = next.error;
|
|
231
|
+
options.onStateChange?.(next);
|
|
232
|
+
};
|
|
233
|
+
const cancel = () => {
|
|
234
|
+
if (cancelled)
|
|
235
|
+
return;
|
|
236
|
+
cancelled = true;
|
|
237
|
+
abortController.abort();
|
|
238
|
+
for (const uploader of inFlight) {
|
|
239
|
+
try {
|
|
240
|
+
uploader.abort(true);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// ignore abort errors
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
if (options.signal) {
|
|
248
|
+
if (options.signal.aborted) {
|
|
249
|
+
cancel();
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
options.signal.addEventListener("abort", cancel, { once: true });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const promise = (async () => {
|
|
256
|
+
if (files.length === 0) {
|
|
257
|
+
throw transloaditError("upload", "No files provided for upload");
|
|
258
|
+
}
|
|
259
|
+
emitState({ ...state });
|
|
260
|
+
const assembly = await createAssembly({
|
|
261
|
+
templateId: options.templateId,
|
|
262
|
+
steps: options.steps,
|
|
263
|
+
fields: options.fields,
|
|
264
|
+
notifyUrl: options.notifyUrl,
|
|
265
|
+
numExpectedUploadFiles: options.numExpectedUploadFiles ?? files.length,
|
|
266
|
+
expires: options.expires,
|
|
267
|
+
additionalParams: options.additionalParams,
|
|
268
|
+
userId: options.userId,
|
|
269
|
+
});
|
|
270
|
+
options.onAssemblyCreated?.(assembly);
|
|
271
|
+
const getStatus = (error) => error.originalResponse?.getStatus &&
|
|
272
|
+
typeof error.originalResponse.getStatus === "function"
|
|
273
|
+
? error.originalResponse.getStatus()
|
|
274
|
+
: 0;
|
|
275
|
+
const retryDelays = options.retryDelays
|
|
276
|
+
? [...options.retryDelays]
|
|
277
|
+
: [1000, 5000, 15000, 30000];
|
|
278
|
+
const rateLimitRetryDelays = options.rateLimitRetryDelays
|
|
279
|
+
? [...options.rateLimitRetryDelays]
|
|
280
|
+
: [20_000, 40_000, 80_000];
|
|
281
|
+
const shouldRetry = (error) => {
|
|
282
|
+
const status = getStatus(error);
|
|
283
|
+
if (!status)
|
|
284
|
+
return true;
|
|
285
|
+
if (status === 409 || status === 423)
|
|
286
|
+
return true;
|
|
287
|
+
return status < 400 || status >= 500;
|
|
288
|
+
};
|
|
289
|
+
const perFileBytes = new Map();
|
|
290
|
+
files.forEach((file, index) => {
|
|
291
|
+
perFileBytes.set(index, { uploaded: 0, total: file.size });
|
|
292
|
+
});
|
|
293
|
+
const updateOverallProgress = () => {
|
|
294
|
+
let totalUploaded = 0;
|
|
295
|
+
let totalBytes = 0;
|
|
296
|
+
for (const { uploaded, total } of perFileBytes.values()) {
|
|
297
|
+
totalUploaded += uploaded;
|
|
298
|
+
totalBytes += total;
|
|
299
|
+
}
|
|
300
|
+
const overall = totalBytes > 0 ? Math.round((totalUploaded / totalBytes) * 100) : 0;
|
|
301
|
+
emitState({ isUploading: true, progress: overall, error: null });
|
|
302
|
+
options.onOverallProgress?.(overall);
|
|
303
|
+
};
|
|
304
|
+
const resolveMetadata = (file) => typeof options.metadata === "function"
|
|
305
|
+
? options.metadata(file)
|
|
306
|
+
: options.metadata;
|
|
307
|
+
const resolveFieldName = (file) => typeof options.fieldName === "function"
|
|
308
|
+
? options.fieldName(file)
|
|
309
|
+
: options.fieldName;
|
|
310
|
+
const uploadFile = async (file, index) => {
|
|
311
|
+
const { endpoint, metadata } = buildTusUploadConfig(assembly.data, file, {
|
|
312
|
+
fieldName: resolveFieldName(file),
|
|
313
|
+
metadata: resolveMetadata(file),
|
|
104
314
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
315
|
+
let uploadUrl = null;
|
|
316
|
+
let rateLimitAttempt = 0;
|
|
317
|
+
let uploader = null;
|
|
318
|
+
const runUpload = () => new Promise((resolve, reject) => {
|
|
319
|
+
if (cancelled) {
|
|
320
|
+
reject(transloaditError("upload", "Upload canceled"));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const onAbort = () => {
|
|
324
|
+
reject(transloaditError("upload", "Upload canceled"));
|
|
325
|
+
};
|
|
326
|
+
abortController.signal.addEventListener("abort", onAbort, {
|
|
327
|
+
once: true,
|
|
328
|
+
});
|
|
329
|
+
let currentUploader;
|
|
330
|
+
const uploadOptions = {
|
|
331
|
+
endpoint,
|
|
120
332
|
metadata,
|
|
121
|
-
|
|
122
|
-
|
|
333
|
+
retryDelays,
|
|
334
|
+
uploadDataDuringCreation: options.uploadDataDuringCreation ?? false,
|
|
335
|
+
onUploadUrlAvailable: () => {
|
|
336
|
+
uploadUrl = currentUploader.url;
|
|
337
|
+
},
|
|
338
|
+
onShouldRetry: (error, retryAttempt) => options.onShouldRetry?.(error, retryAttempt) ??
|
|
339
|
+
shouldRetry(error),
|
|
123
340
|
onProgress: (bytesUploaded, bytesTotal) => {
|
|
341
|
+
perFileBytes.set(index, {
|
|
342
|
+
uploaded: bytesUploaded,
|
|
343
|
+
total: bytesTotal,
|
|
344
|
+
});
|
|
124
345
|
const progress = Math.round((bytesUploaded / bytesTotal) * 100);
|
|
125
|
-
|
|
126
|
-
|
|
346
|
+
options.onFileProgress?.(file, progress);
|
|
347
|
+
updateOverallProgress();
|
|
127
348
|
},
|
|
128
349
|
onError: (error) => {
|
|
350
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
129
351
|
reject(error);
|
|
130
352
|
},
|
|
131
353
|
onSuccess: () => {
|
|
354
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
132
355
|
resolve();
|
|
133
356
|
},
|
|
134
|
-
}
|
|
135
|
-
|
|
357
|
+
};
|
|
358
|
+
if (options.chunkSize !== undefined) {
|
|
359
|
+
uploadOptions.chunkSize = options.chunkSize;
|
|
360
|
+
}
|
|
361
|
+
if (uploadUrl) {
|
|
362
|
+
uploadOptions.uploadUrl = uploadUrl;
|
|
363
|
+
}
|
|
364
|
+
if (options.overridePatchMethod !== undefined) {
|
|
365
|
+
uploadOptions.overridePatchMethod = options.overridePatchMethod;
|
|
366
|
+
}
|
|
367
|
+
if (options.storeFingerprintForResuming !== undefined) {
|
|
368
|
+
uploadOptions.storeFingerprintForResuming =
|
|
369
|
+
options.storeFingerprintForResuming;
|
|
370
|
+
}
|
|
371
|
+
if (options.removeFingerprintOnSuccess !== undefined) {
|
|
372
|
+
uploadOptions.removeFingerprintOnSuccess =
|
|
373
|
+
options.removeFingerprintOnSuccess;
|
|
374
|
+
}
|
|
375
|
+
currentUploader = new Upload(file, uploadOptions);
|
|
376
|
+
uploader = currentUploader;
|
|
377
|
+
inFlight.add(currentUploader);
|
|
378
|
+
currentUploader.start();
|
|
379
|
+
}).finally(() => {
|
|
380
|
+
if (uploader) {
|
|
381
|
+
inFlight.delete(uploader);
|
|
382
|
+
}
|
|
136
383
|
});
|
|
137
|
-
|
|
138
|
-
|
|
384
|
+
while (true) {
|
|
385
|
+
try {
|
|
386
|
+
await runUpload();
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (cancelled) {
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
const status = getStatus(error);
|
|
394
|
+
if (status === 429 &&
|
|
395
|
+
rateLimitAttempt < rateLimitRetryDelays.length) {
|
|
396
|
+
const delay = rateLimitRetryDelays[rateLimitAttempt] ?? 0;
|
|
397
|
+
rateLimitAttempt += 1;
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
let nextIndex = 0;
|
|
406
|
+
const errors = [];
|
|
407
|
+
const worker = async () => {
|
|
408
|
+
while (true) {
|
|
409
|
+
if (cancelled)
|
|
410
|
+
return;
|
|
411
|
+
const index = nextIndex;
|
|
412
|
+
nextIndex += 1;
|
|
413
|
+
if (index >= files.length)
|
|
414
|
+
return;
|
|
415
|
+
const file = files[index];
|
|
416
|
+
try {
|
|
417
|
+
await uploadFile(file, index);
|
|
418
|
+
results[index] = { file, status: "success" };
|
|
419
|
+
options.onFileComplete?.(file);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (cancelled) {
|
|
423
|
+
results[index] = { file, status: "canceled" };
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const err = error instanceof Error
|
|
427
|
+
? error
|
|
428
|
+
: transloaditError("upload", "Upload failed");
|
|
429
|
+
results[index] = { file, status: "error", error: err };
|
|
430
|
+
errors.push(err);
|
|
431
|
+
options.onFileError?.(file, err);
|
|
432
|
+
if (options.failFast ?? false) {
|
|
433
|
+
cancel();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, worker));
|
|
440
|
+
if (cancelled) {
|
|
441
|
+
const error = transloaditError("upload", "Upload canceled");
|
|
442
|
+
error.results = {
|
|
443
|
+
assemblyId: assembly.assemblyId,
|
|
444
|
+
data: assembly.data,
|
|
445
|
+
files: results,
|
|
446
|
+
};
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
const hasErrors = results.some((result) => result.status === "error");
|
|
450
|
+
const resultPayload = {
|
|
451
|
+
assemblyId: assembly.assemblyId,
|
|
452
|
+
data: assembly.data,
|
|
453
|
+
files: results,
|
|
454
|
+
};
|
|
455
|
+
if (hasErrors) {
|
|
456
|
+
const error = transloaditError("upload", `Failed to upload ${errors.length} file${errors.length === 1 ? "" : "s"}`);
|
|
457
|
+
error.results =
|
|
458
|
+
resultPayload;
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
emitState({ isUploading: false, progress: 100, error: null });
|
|
462
|
+
return resultPayload;
|
|
463
|
+
})();
|
|
464
|
+
return { promise, cancel };
|
|
465
|
+
}
|
|
466
|
+
export function useTransloaditUpload(options) {
|
|
467
|
+
const create = useAction(options.createAssembly);
|
|
468
|
+
const refresh = useAction(options.refreshAssembly);
|
|
469
|
+
const [state, setState] = useState({
|
|
470
|
+
isUploading: false,
|
|
471
|
+
progress: 0,
|
|
472
|
+
error: null,
|
|
473
|
+
});
|
|
474
|
+
const [assemblyId, setAssemblyId] = useState(null);
|
|
475
|
+
const [assemblyData, setAssemblyData] = useState(null);
|
|
476
|
+
const cancelRef = useRef(null);
|
|
477
|
+
const upload = useCallback(async (files, uploadOptions) => {
|
|
478
|
+
const resolved = files instanceof FileList
|
|
479
|
+
? Array.from(files)
|
|
480
|
+
: Array.isArray(files)
|
|
481
|
+
? files
|
|
482
|
+
: [files];
|
|
483
|
+
const controller = uploadFilesWithTransloaditTus(create, resolved, {
|
|
484
|
+
...uploadOptions,
|
|
485
|
+
onStateChange: setState,
|
|
486
|
+
onAssemblyCreated: (assembly) => {
|
|
487
|
+
setAssemblyId(assembly.assemblyId);
|
|
488
|
+
setAssemblyData(assembly.data);
|
|
489
|
+
uploadOptions.onAssemblyCreated?.(assembly);
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
cancelRef.current = controller.cancel;
|
|
493
|
+
try {
|
|
494
|
+
const result = await controller.promise;
|
|
495
|
+
setAssemblyId(result.assemblyId);
|
|
496
|
+
setAssemblyData(result.data);
|
|
497
|
+
return result;
|
|
139
498
|
}
|
|
140
499
|
catch (error) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
500
|
+
const resolvedError = error instanceof Error
|
|
501
|
+
? error
|
|
502
|
+
: transloaditError("upload", "Upload failed");
|
|
503
|
+
setState({ isUploading: false, progress: 0, error: resolvedError });
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
cancelRef.current = null;
|
|
144
508
|
}
|
|
145
509
|
}, [create]);
|
|
510
|
+
const cancel = useCallback(() => {
|
|
511
|
+
cancelRef.current?.();
|
|
512
|
+
}, []);
|
|
146
513
|
const reset = useCallback(() => {
|
|
514
|
+
cancelRef.current?.();
|
|
515
|
+
cancelRef.current = null;
|
|
516
|
+
setAssemblyId(null);
|
|
517
|
+
setAssemblyData(null);
|
|
147
518
|
setState({ isUploading: false, progress: 0, error: null });
|
|
148
519
|
}, []);
|
|
149
|
-
|
|
520
|
+
const assembly = useQuery(options.getStatus, assemblyId ? { assemblyId } : "skip");
|
|
521
|
+
const parsedStatus = useMemo(() => {
|
|
522
|
+
const candidate = assembly && typeof assembly === "object"
|
|
523
|
+
? (assembly.raw ?? assembly)
|
|
524
|
+
: assembly;
|
|
525
|
+
return parseAssemblyStatus(candidate);
|
|
526
|
+
}, [assembly]);
|
|
527
|
+
const results = useQuery(options.listResults, assemblyId ? { assemblyId } : "skip");
|
|
528
|
+
useAssemblyPoller({
|
|
529
|
+
assemblyId,
|
|
530
|
+
status: parsedStatus,
|
|
531
|
+
refresh: async () => {
|
|
532
|
+
if (!assemblyId)
|
|
533
|
+
return;
|
|
534
|
+
await refresh({ assemblyId });
|
|
535
|
+
},
|
|
536
|
+
intervalMs: options.pollIntervalMs ?? 5000,
|
|
537
|
+
shouldContinue: options.shouldContinue,
|
|
538
|
+
onError: options.onError,
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
150
541
|
upload,
|
|
542
|
+
cancel,
|
|
151
543
|
reset,
|
|
152
544
|
isUploading: state.isUploading,
|
|
153
545
|
progress: state.progress,
|
|
154
546
|
error: state.error,
|
|
155
|
-
|
|
547
|
+
assemblyId,
|
|
548
|
+
assemblyData,
|
|
549
|
+
assembly,
|
|
550
|
+
status: parsedStatus,
|
|
551
|
+
results,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
export function useTransloaditUppy(options) {
|
|
555
|
+
const create = useAction(options.createAssembly);
|
|
556
|
+
const refresh = useAction(options.refreshAssembly);
|
|
557
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
558
|
+
const [error, setError] = useState(null);
|
|
559
|
+
const [assemblyId, setAssemblyId] = useState(null);
|
|
560
|
+
const [assemblyData, setAssemblyData] = useState(null);
|
|
561
|
+
const [uploadResult, setUploadResult] = useState(null);
|
|
562
|
+
const assembly = useQuery(options.getStatus, assemblyId ? { assemblyId } : "skip");
|
|
563
|
+
const results = useQuery(options.listResults, assemblyId ? { assemblyId } : "skip");
|
|
564
|
+
const parsedStatus = useMemo(() => {
|
|
565
|
+
const candidate = assembly && typeof assembly === "object"
|
|
566
|
+
? (assembly.raw ?? assembly)
|
|
567
|
+
: assembly;
|
|
568
|
+
return parseAssemblyStatus(candidate);
|
|
569
|
+
}, [assembly]);
|
|
570
|
+
useAssemblyPoller({
|
|
571
|
+
assemblyId,
|
|
572
|
+
status: parsedStatus,
|
|
573
|
+
refresh: async () => {
|
|
574
|
+
if (!assemblyId)
|
|
575
|
+
return;
|
|
576
|
+
await refresh({ assemblyId });
|
|
577
|
+
},
|
|
578
|
+
intervalMs: options.pollIntervalMs ?? 5000,
|
|
579
|
+
shouldContinue: options.shouldContinue,
|
|
580
|
+
onError: options.onError,
|
|
581
|
+
});
|
|
582
|
+
const startUpload = useCallback(async (overrides) => {
|
|
583
|
+
setError(null);
|
|
584
|
+
setIsUploading(true);
|
|
585
|
+
try {
|
|
586
|
+
const files = options.uppy.getFiles();
|
|
587
|
+
if (files.length === 0) {
|
|
588
|
+
throw transloaditError("upload", "No files provided for upload");
|
|
589
|
+
}
|
|
590
|
+
const createAssemblyArgs = {
|
|
591
|
+
...(options.createAssemblyArgs ?? {}),
|
|
592
|
+
...(overrides?.createAssemblyArgs ?? {}),
|
|
593
|
+
};
|
|
594
|
+
const { assembly, uploadResult: result } = await uploadWithAssembly(create, options.uppy, {
|
|
595
|
+
fileCount: overrides?.fileCount ?? options.fileCount ?? files.length,
|
|
596
|
+
fieldName: overrides?.fieldName ?? options.fieldName,
|
|
597
|
+
metadata: overrides?.metadata ?? options.metadata,
|
|
598
|
+
addRequestId: overrides?.addRequestId ?? options.addRequestId,
|
|
599
|
+
createAssemblyArgs,
|
|
600
|
+
onAssemblyCreated: (created) => {
|
|
601
|
+
const typed = created;
|
|
602
|
+
setAssemblyId(typed.assemblyId);
|
|
603
|
+
setAssemblyData(typed.data);
|
|
604
|
+
options.onAssemblyCreated?.(typed);
|
|
605
|
+
overrides?.onAssemblyCreated?.(created);
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
setAssemblyId(assembly.assemblyId);
|
|
609
|
+
setAssemblyData(assembly.data);
|
|
610
|
+
setUploadResult(result);
|
|
611
|
+
options.onUploadResult?.(result);
|
|
612
|
+
setIsUploading(false);
|
|
613
|
+
return { assembly, uploadResult: result };
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
const resolved = err instanceof Error
|
|
617
|
+
? err
|
|
618
|
+
: transloaditError("upload", "Upload failed");
|
|
619
|
+
setError(resolved);
|
|
620
|
+
setIsUploading(false);
|
|
621
|
+
throw resolved;
|
|
622
|
+
}
|
|
623
|
+
}, [
|
|
624
|
+
create,
|
|
625
|
+
options.addRequestId,
|
|
626
|
+
options.createAssemblyArgs,
|
|
627
|
+
options.fieldName,
|
|
628
|
+
options.fileCount,
|
|
629
|
+
options.metadata,
|
|
630
|
+
options.onAssemblyCreated,
|
|
631
|
+
options.onUploadResult,
|
|
632
|
+
options.uppy,
|
|
633
|
+
]);
|
|
634
|
+
const reset = useCallback(() => {
|
|
635
|
+
setIsUploading(false);
|
|
636
|
+
setError(null);
|
|
637
|
+
setAssemblyId(null);
|
|
638
|
+
setAssemblyData(null);
|
|
639
|
+
setUploadResult(null);
|
|
640
|
+
}, []);
|
|
641
|
+
const stage = useMemo(() => {
|
|
642
|
+
if (error)
|
|
643
|
+
return "error";
|
|
644
|
+
if (isUploading)
|
|
645
|
+
return "uploading";
|
|
646
|
+
return parsedStatus ? getAssemblyStage(parsedStatus) : null;
|
|
647
|
+
}, [error, isUploading, parsedStatus]);
|
|
648
|
+
return {
|
|
649
|
+
startUpload,
|
|
650
|
+
reset,
|
|
651
|
+
isUploading,
|
|
652
|
+
error,
|
|
653
|
+
assemblyId,
|
|
654
|
+
assemblyData,
|
|
655
|
+
assembly,
|
|
656
|
+
status: parsedStatus,
|
|
657
|
+
results,
|
|
658
|
+
stage,
|
|
659
|
+
uploadResult,
|
|
660
|
+
};
|
|
156
661
|
}
|
|
157
662
|
export function useAssemblyStatus(getStatus, assemblyId) {
|
|
158
663
|
return useQuery(getStatus, { assemblyId });
|
|
159
664
|
}
|
|
665
|
+
export function useAssemblyStatusWithPolling(getStatus, refreshAssembly, assemblyId, options) {
|
|
666
|
+
const status = useQuery(getStatus, { assemblyId });
|
|
667
|
+
const refresh = useAction(refreshAssembly);
|
|
668
|
+
const statusRef = useRef(status);
|
|
669
|
+
const shouldContinueRef = useRef(options?.shouldContinue);
|
|
670
|
+
const onErrorRef = useRef(options?.onError);
|
|
671
|
+
useEffect(() => {
|
|
672
|
+
statusRef.current = status;
|
|
673
|
+
}, [status]);
|
|
674
|
+
useEffect(() => {
|
|
675
|
+
shouldContinueRef.current = options?.shouldContinue;
|
|
676
|
+
}, [options?.shouldContinue]);
|
|
677
|
+
useEffect(() => {
|
|
678
|
+
onErrorRef.current = options?.onError;
|
|
679
|
+
}, [options?.onError]);
|
|
680
|
+
useEffect(() => {
|
|
681
|
+
if (!assemblyId)
|
|
682
|
+
return;
|
|
683
|
+
const intervalMs = options?.pollIntervalMs ?? 5000;
|
|
684
|
+
if (intervalMs <= 0)
|
|
685
|
+
return;
|
|
686
|
+
const shouldKeepPolling = () => {
|
|
687
|
+
const shouldContinue = shouldContinueRef.current?.();
|
|
688
|
+
if (shouldContinue === false)
|
|
689
|
+
return false;
|
|
690
|
+
if (!options?.stopOnTerminal)
|
|
691
|
+
return true;
|
|
692
|
+
const current = statusRef.current;
|
|
693
|
+
const rawCandidate = current && typeof current === "object"
|
|
694
|
+
? (current.raw ?? current)
|
|
695
|
+
: current;
|
|
696
|
+
const parsed = parseAssemblyStatus(rawCandidate);
|
|
697
|
+
return !(parsed ? isAssemblyTerminal(parsed) : false);
|
|
698
|
+
};
|
|
699
|
+
if (!shouldKeepPolling())
|
|
700
|
+
return;
|
|
701
|
+
let cancelled = false;
|
|
702
|
+
let intervalId = null;
|
|
703
|
+
let inFlight = false;
|
|
704
|
+
const tick = async () => {
|
|
705
|
+
if (cancelled)
|
|
706
|
+
return;
|
|
707
|
+
if (!shouldKeepPolling()) {
|
|
708
|
+
if (intervalId)
|
|
709
|
+
clearInterval(intervalId);
|
|
710
|
+
cancelled = true;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (inFlight)
|
|
714
|
+
return;
|
|
715
|
+
inFlight = true;
|
|
716
|
+
try {
|
|
717
|
+
await refresh({ assemblyId });
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
const resolved = error instanceof Error
|
|
721
|
+
? error
|
|
722
|
+
: transloaditError("polling", "Refresh failed");
|
|
723
|
+
onErrorRef.current?.(resolved);
|
|
724
|
+
}
|
|
725
|
+
finally {
|
|
726
|
+
inFlight = false;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
intervalId = setInterval(() => {
|
|
730
|
+
void tick();
|
|
731
|
+
}, intervalMs);
|
|
732
|
+
void tick();
|
|
733
|
+
return () => {
|
|
734
|
+
cancelled = true;
|
|
735
|
+
if (intervalId)
|
|
736
|
+
clearInterval(intervalId);
|
|
737
|
+
};
|
|
738
|
+
}, [assemblyId, options?.pollIntervalMs, options?.stopOnTerminal, refresh]);
|
|
739
|
+
return status;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* @deprecated Prefer `useAssemblyStatusWithPolling` for public usage.
|
|
743
|
+
*/
|
|
744
|
+
export function useAssemblyPoller(options) {
|
|
745
|
+
const refreshRef = useRef(options.refresh);
|
|
746
|
+
const onErrorRef = useRef(options.onError);
|
|
747
|
+
const shouldContinueRef = useRef(options.shouldContinue);
|
|
748
|
+
const statusRef = useRef(options.status);
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
refreshRef.current = options.refresh;
|
|
751
|
+
}, [options.refresh]);
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
onErrorRef.current = options.onError;
|
|
754
|
+
}, [options.onError]);
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
shouldContinueRef.current = options.shouldContinue;
|
|
757
|
+
}, [options.shouldContinue]);
|
|
758
|
+
useEffect(() => {
|
|
759
|
+
statusRef.current = options.status;
|
|
760
|
+
}, [options.status]);
|
|
761
|
+
useEffect(() => {
|
|
762
|
+
if (!options.assemblyId)
|
|
763
|
+
return;
|
|
764
|
+
const controller = pollAssembly({
|
|
765
|
+
intervalMs: options.intervalMs,
|
|
766
|
+
refresh: () => refreshRef.current(),
|
|
767
|
+
shouldContinue: () => shouldContinueRef.current?.() ?? false,
|
|
768
|
+
isTerminal: () => {
|
|
769
|
+
const current = statusRef.current;
|
|
770
|
+
return current ? isAssemblyTerminal(current) : false;
|
|
771
|
+
},
|
|
772
|
+
onError: (error) => {
|
|
773
|
+
onErrorRef.current?.(error);
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
return () => {
|
|
777
|
+
controller.stop();
|
|
778
|
+
};
|
|
779
|
+
}, [options.assemblyId, options.intervalMs]);
|
|
780
|
+
}
|
|
160
781
|
export function useTransloaditFiles(listResults, args) {
|
|
161
782
|
return useQuery(listResults, args);
|
|
162
783
|
}
|