@transloadit/convex 0.0.3 → 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 +114 -134
- package/dist/client/index.d.ts +24 -13
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +14 -3
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/dataModel.d.ts +1 -1
- package/dist/component/_generated/server.d.ts +1 -1
- package/dist/component/apiUtils.d.ts +26 -6
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +48 -38
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +7 -9
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +74 -18
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +4 -6
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +0 -7
- 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 +201 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +674 -94
- 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 +4 -4
- 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 +36 -6
- package/src/client/index.ts +73 -7
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +1 -1
- package/src/component/apiUtils.test.ts +166 -2
- package/src/component/apiUtils.ts +96 -64
- package/src/component/lib.test.ts +170 -4
- package/src/component/lib.ts +113 -25
- package/src/component/schema.ts +0 -10
- package/src/debug/index.ts +84 -0
- package/src/react/index.test.tsx +340 -0
- package/src/react/index.tsx +1089 -179
- 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 +1 -1
- package/src/test/nodeModules.ts +19 -0
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,191 @@
|
|
|
1
|
+
import { isAssemblyTerminal, } from "@transloadit/zod/v3/assemblyStatus";
|
|
1
2
|
import { useAction, useQuery } from "convex/react";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { Upload } from "tus-js-client";
|
|
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;
|
|
113
|
+
};
|
|
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) {
|
|
160
|
+
try {
|
|
161
|
+
await runUpload();
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
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;
|
|
173
|
+
}
|
|
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
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* @deprecated Prefer `useTransloaditUpload` (single + multi-file) for new code.
|
|
188
|
+
*/
|
|
4
189
|
export function useTransloaditTusUpload(createAssembly) {
|
|
5
190
|
const create = useAction(createAssembly);
|
|
6
191
|
const [state, setState] = useState({
|
|
@@ -8,89 +193,165 @@ export function useTransloaditTusUpload(createAssembly) {
|
|
|
8
193
|
progress: 0,
|
|
9
194
|
error: null,
|
|
10
195
|
});
|
|
11
|
-
const upload = useCallback(async (file, options) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
196
|
+
const upload = useCallback(async (file, options) => uploadWithTransloaditTus(create, file, options, {
|
|
197
|
+
onStateChange: setState,
|
|
198
|
+
}), [create]);
|
|
199
|
+
const reset = useCallback(() => {
|
|
200
|
+
setState({ isUploading: false, progress: 0, error: null });
|
|
201
|
+
}, []);
|
|
202
|
+
return useMemo(() => ({
|
|
203
|
+
upload,
|
|
204
|
+
reset,
|
|
205
|
+
isUploading: state.isUploading,
|
|
206
|
+
progress: state.progress,
|
|
207
|
+
error: state.error,
|
|
208
|
+
}), [state.error, state.isUploading, state.progress, upload, reset]);
|
|
209
|
+
}
|
|
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,
|
|
217
|
+
progress: 0,
|
|
218
|
+
error: null,
|
|
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),
|
|
23
314
|
});
|
|
24
|
-
const data = assembly.data;
|
|
25
|
-
options.onAssemblyCreated?.(assembly);
|
|
26
|
-
const tusUrl = (typeof data.tus_url === "string" && data.tus_url) ||
|
|
27
|
-
(typeof data.tusUrl === "string" && data.tusUrl) ||
|
|
28
|
-
"";
|
|
29
|
-
if (!tusUrl) {
|
|
30
|
-
throw new Error("Transloadit response missing tus_url for resumable upload");
|
|
31
|
-
}
|
|
32
|
-
const assemblyUrl = (typeof data.assembly_ssl_url === "string" &&
|
|
33
|
-
data.assembly_ssl_url) ||
|
|
34
|
-
(typeof data.assembly_url === "string" && data.assembly_url) ||
|
|
35
|
-
(typeof data.assemblyUrl === "string" && data.assemblyUrl) ||
|
|
36
|
-
"";
|
|
37
|
-
if (!assemblyUrl) {
|
|
38
|
-
throw new Error("Transloadit response missing assembly_url for resumable upload");
|
|
39
|
-
}
|
|
40
|
-
const metadata = {
|
|
41
|
-
filename: file.name,
|
|
42
|
-
...options.metadata,
|
|
43
|
-
};
|
|
44
|
-
if (file.type) {
|
|
45
|
-
metadata.filetype = file.type;
|
|
46
|
-
}
|
|
47
|
-
if (!metadata.fieldname) {
|
|
48
|
-
metadata.fieldname = options.fieldName ?? "file";
|
|
49
|
-
}
|
|
50
|
-
if (!metadata.assembly_url) {
|
|
51
|
-
metadata.assembly_url = assemblyUrl;
|
|
52
|
-
}
|
|
53
|
-
const getStatus = (error) => error.originalResponse?.getStatus &&
|
|
54
|
-
typeof error.originalResponse.getStatus === "function"
|
|
55
|
-
? error.originalResponse.getStatus()
|
|
56
|
-
: 0;
|
|
57
|
-
const retryDelays = options.retryDelays
|
|
58
|
-
? [...options.retryDelays]
|
|
59
|
-
: [1000, 5000, 15000, 30000];
|
|
60
|
-
const rateLimitRetryDelays = options.rateLimitRetryDelays
|
|
61
|
-
? [...options.rateLimitRetryDelays]
|
|
62
|
-
: [20_000, 40_000, 80_000];
|
|
63
|
-
const shouldRetry = (error) => {
|
|
64
|
-
const status = getStatus(error);
|
|
65
|
-
if (!status)
|
|
66
|
-
return true;
|
|
67
|
-
if (status === 409 || status === 423)
|
|
68
|
-
return true;
|
|
69
|
-
return status < 400 || status >= 500;
|
|
70
|
-
};
|
|
71
315
|
let uploadUrl = null;
|
|
72
316
|
let rateLimitAttempt = 0;
|
|
317
|
+
let uploader = null;
|
|
73
318
|
const runUpload = () => new Promise((resolve, reject) => {
|
|
74
|
-
|
|
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;
|
|
75
330
|
const uploadOptions = {
|
|
76
|
-
endpoint
|
|
331
|
+
endpoint,
|
|
77
332
|
metadata,
|
|
78
333
|
retryDelays,
|
|
79
334
|
uploadDataDuringCreation: options.uploadDataDuringCreation ?? false,
|
|
80
335
|
onUploadUrlAvailable: () => {
|
|
81
|
-
uploadUrl =
|
|
336
|
+
uploadUrl = currentUploader.url;
|
|
82
337
|
},
|
|
83
338
|
onShouldRetry: (error, retryAttempt) => options.onShouldRetry?.(error, retryAttempt) ??
|
|
84
339
|
shouldRetry(error),
|
|
85
340
|
onProgress: (bytesUploaded, bytesTotal) => {
|
|
341
|
+
perFileBytes.set(index, {
|
|
342
|
+
uploaded: bytesUploaded,
|
|
343
|
+
total: bytesTotal,
|
|
344
|
+
});
|
|
86
345
|
const progress = Math.round((bytesUploaded / bytesTotal) * 100);
|
|
87
|
-
|
|
88
|
-
|
|
346
|
+
options.onFileProgress?.(file, progress);
|
|
347
|
+
updateOverallProgress();
|
|
89
348
|
},
|
|
90
349
|
onError: (error) => {
|
|
350
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
91
351
|
reject(error);
|
|
92
352
|
},
|
|
93
353
|
onSuccess: () => {
|
|
354
|
+
abortController.signal.removeEventListener("abort", onAbort);
|
|
94
355
|
resolve();
|
|
95
356
|
},
|
|
96
357
|
};
|
|
@@ -111,8 +372,14 @@ export function useTransloaditTusUpload(createAssembly) {
|
|
|
111
372
|
uploadOptions.removeFingerprintOnSuccess =
|
|
112
373
|
options.removeFingerprintOnSuccess;
|
|
113
374
|
}
|
|
114
|
-
|
|
115
|
-
uploader
|
|
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
|
+
}
|
|
116
383
|
});
|
|
117
384
|
while (true) {
|
|
118
385
|
try {
|
|
@@ -120,6 +387,9 @@ export function useTransloaditTusUpload(createAssembly) {
|
|
|
120
387
|
break;
|
|
121
388
|
}
|
|
122
389
|
catch (error) {
|
|
390
|
+
if (cancelled) {
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
123
393
|
const status = getStatus(error);
|
|
124
394
|
if (status === 429 &&
|
|
125
395
|
rateLimitAttempt < rateLimitRetryDelays.length) {
|
|
@@ -131,25 +401,263 @@ export function useTransloaditTusUpload(createAssembly) {
|
|
|
131
401
|
throw error;
|
|
132
402
|
}
|
|
133
403
|
}
|
|
134
|
-
|
|
135
|
-
|
|
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;
|
|
136
498
|
}
|
|
137
499
|
catch (error) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
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;
|
|
141
508
|
}
|
|
142
509
|
}, [create]);
|
|
510
|
+
const cancel = useCallback(() => {
|
|
511
|
+
cancelRef.current?.();
|
|
512
|
+
}, []);
|
|
143
513
|
const reset = useCallback(() => {
|
|
514
|
+
cancelRef.current?.();
|
|
515
|
+
cancelRef.current = null;
|
|
516
|
+
setAssemblyId(null);
|
|
517
|
+
setAssemblyData(null);
|
|
144
518
|
setState({ isUploading: false, progress: 0, error: null });
|
|
145
519
|
}, []);
|
|
146
|
-
|
|
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 {
|
|
147
541
|
upload,
|
|
542
|
+
cancel,
|
|
148
543
|
reset,
|
|
149
544
|
isUploading: state.isUploading,
|
|
150
545
|
progress: state.progress,
|
|
151
546
|
error: state.error,
|
|
152
|
-
|
|
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
|
+
};
|
|
153
661
|
}
|
|
154
662
|
export function useAssemblyStatus(getStatus, assemblyId) {
|
|
155
663
|
return useQuery(getStatus, { assemblyId });
|
|
@@ -157,47 +665,119 @@ export function useAssemblyStatus(getStatus, assemblyId) {
|
|
|
157
665
|
export function useAssemblyStatusWithPolling(getStatus, refreshAssembly, assemblyId, options) {
|
|
158
666
|
const status = useQuery(getStatus, { assemblyId });
|
|
159
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]);
|
|
160
680
|
useEffect(() => {
|
|
161
681
|
if (!assemblyId)
|
|
162
682
|
return;
|
|
163
683
|
const intervalMs = options?.pollIntervalMs ?? 5000;
|
|
164
684
|
if (intervalMs <= 0)
|
|
165
685
|
return;
|
|
166
|
-
const
|
|
167
|
-
|
|
686
|
+
const shouldKeepPolling = () => {
|
|
687
|
+
const shouldContinue = shouldContinueRef.current?.();
|
|
688
|
+
if (shouldContinue === false)
|
|
168
689
|
return false;
|
|
169
|
-
if (!
|
|
170
|
-
return
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
175
698
|
};
|
|
176
|
-
if (
|
|
699
|
+
if (!shouldKeepPolling())
|
|
177
700
|
return;
|
|
178
701
|
let cancelled = false;
|
|
702
|
+
let intervalId = null;
|
|
703
|
+
let inFlight = false;
|
|
179
704
|
const tick = async () => {
|
|
180
705
|
if (cancelled)
|
|
181
706
|
return;
|
|
182
|
-
|
|
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
|
+
}
|
|
183
728
|
};
|
|
184
|
-
|
|
185
|
-
const id = setInterval(() => {
|
|
729
|
+
intervalId = setInterval(() => {
|
|
186
730
|
void tick();
|
|
187
731
|
}, intervalMs);
|
|
732
|
+
void tick();
|
|
188
733
|
return () => {
|
|
189
734
|
cancelled = true;
|
|
190
|
-
|
|
735
|
+
if (intervalId)
|
|
736
|
+
clearInterval(intervalId);
|
|
191
737
|
};
|
|
192
|
-
}, [
|
|
193
|
-
assemblyId,
|
|
194
|
-
options?.pollIntervalMs,
|
|
195
|
-
options?.stopOnTerminal,
|
|
196
|
-
refresh,
|
|
197
|
-
status,
|
|
198
|
-
]);
|
|
738
|
+
}, [assemblyId, options?.pollIntervalMs, options?.stopOnTerminal, refresh]);
|
|
199
739
|
return status;
|
|
200
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
|
+
}
|
|
201
781
|
export function useTransloaditFiles(listResults, args) {
|
|
202
782
|
return useQuery(listResults, args);
|
|
203
783
|
}
|