@transloadit/convex 0.0.1 → 0.0.3
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 +162 -79
- package/dist/client/index.d.ts +92 -63
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +57 -30
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +35 -15
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/apiUtils.d.ts +13 -4
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +22 -12
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +69 -45
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +154 -77
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +9 -9
- package/dist/component/schema.js +3 -3
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +23 -25
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +129 -88
- package/dist/react/index.js.map +1 -1
- 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/package.json +27 -15
- package/src/client/index.ts +72 -35
- package/src/client/types.ts +12 -11
- package/src/component/_generated/component.ts +44 -13
- package/src/component/apiUtils.test.ts +29 -0
- package/src/component/apiUtils.ts +52 -26
- package/src/component/lib.test.ts +73 -3
- package/src/component/lib.ts +220 -97
- package/src/component/schema.ts +3 -3
- package/src/react/index.tsx +193 -150
- package/src/test/index.ts +10 -0
package/src/react/index.tsx
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
import { useAction, useQuery } from "convex/react";
|
|
2
2
|
import type { FunctionReference } from "convex/server";
|
|
3
|
-
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { Upload } from "tus-js-client";
|
|
5
5
|
|
|
6
|
-
export type GenerateUploadParamsFn = FunctionReference<
|
|
7
|
-
"action",
|
|
8
|
-
"public",
|
|
9
|
-
{
|
|
10
|
-
templateId?: string;
|
|
11
|
-
steps?: unknown;
|
|
12
|
-
fields?: unknown;
|
|
13
|
-
notifyUrl?: string;
|
|
14
|
-
numExpectedUploadFiles?: number;
|
|
15
|
-
expires?: string;
|
|
16
|
-
additionalParams?: unknown;
|
|
17
|
-
userId?: string;
|
|
18
|
-
},
|
|
19
|
-
{ params: string; signature: string; url: string }
|
|
20
|
-
>;
|
|
21
|
-
|
|
22
6
|
export type CreateAssemblyFn = FunctionReference<
|
|
23
7
|
"action",
|
|
24
8
|
"public",
|
|
@@ -49,6 +33,13 @@ export type ListResultsFn = FunctionReference<
|
|
|
49
33
|
Array<unknown>
|
|
50
34
|
>;
|
|
51
35
|
|
|
36
|
+
export type RefreshAssemblyFn = FunctionReference<
|
|
37
|
+
"action",
|
|
38
|
+
"public",
|
|
39
|
+
{ assemblyId: string },
|
|
40
|
+
{ assemblyId: string; ok?: string; status?: string; resultCount: number }
|
|
41
|
+
>;
|
|
42
|
+
|
|
52
43
|
export interface UploadOptions {
|
|
53
44
|
templateId?: string;
|
|
54
45
|
steps?: Record<string, unknown>;
|
|
@@ -66,124 +57,22 @@ export interface UploadState {
|
|
|
66
57
|
error: Error | null;
|
|
67
58
|
}
|
|
68
59
|
|
|
69
|
-
export interface FormUploadOptions extends UploadOptions {
|
|
70
|
-
fileField?: string;
|
|
71
|
-
onProgress?: (progress: number) => void;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
60
|
export interface TusUploadOptions extends UploadOptions {
|
|
75
61
|
metadata?: Record<string, string>;
|
|
62
|
+
fieldName?: string;
|
|
76
63
|
chunkSize?: number;
|
|
77
64
|
retryDelays?: number[];
|
|
65
|
+
onShouldRetry?: (error: unknown, retryAttempt: number) => boolean;
|
|
66
|
+
rateLimitRetryDelays?: number[];
|
|
67
|
+
overridePatchMethod?: boolean;
|
|
68
|
+
uploadDataDuringCreation?: boolean;
|
|
69
|
+
storeFingerprintForResuming?: boolean;
|
|
70
|
+
removeFingerprintOnSuccess?: boolean;
|
|
78
71
|
onProgress?: (progress: number) => void;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
params: { params: string; signature: string; url: string },
|
|
84
|
-
options: FormUploadOptions,
|
|
85
|
-
): Promise<Record<string, unknown>> {
|
|
86
|
-
const formData = new FormData();
|
|
87
|
-
formData.append("params", params.params);
|
|
88
|
-
formData.append("signature", params.signature);
|
|
89
|
-
formData.append(options.fileField ?? "file", file);
|
|
90
|
-
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const xhr = new XMLHttpRequest();
|
|
93
|
-
xhr.open("POST", params.url, true);
|
|
94
|
-
|
|
95
|
-
xhr.upload.onprogress = (event) => {
|
|
96
|
-
if (!event.lengthComputable) return;
|
|
97
|
-
const progress = Math.round((event.loaded / event.total) * 100);
|
|
98
|
-
options.onProgress?.(progress);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
xhr.onload = () => {
|
|
102
|
-
try {
|
|
103
|
-
const response = JSON.parse(xhr.responseText) as Record<
|
|
104
|
-
string,
|
|
105
|
-
unknown
|
|
106
|
-
>;
|
|
107
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
108
|
-
resolve(response);
|
|
109
|
-
} else {
|
|
110
|
-
reject(
|
|
111
|
-
new Error(
|
|
112
|
-
`Transloadit upload failed (${xhr.status}): ${JSON.stringify(response)}`,
|
|
113
|
-
),
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
} catch (error) {
|
|
117
|
-
reject(error);
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
xhr.onerror = () => {
|
|
122
|
-
reject(new Error("Transloadit upload failed"));
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
xhr.send(formData);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function useTransloaditUpload(
|
|
130
|
-
generateUploadParams: GenerateUploadParamsFn,
|
|
131
|
-
) {
|
|
132
|
-
const generate = useAction(generateUploadParams);
|
|
133
|
-
const [state, setState] = useState<UploadState>({
|
|
134
|
-
isUploading: false,
|
|
135
|
-
progress: 0,
|
|
136
|
-
error: null,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const upload = useCallback(
|
|
140
|
-
async (file: File, options: FormUploadOptions) => {
|
|
141
|
-
setState({ isUploading: true, progress: 0, error: null });
|
|
142
|
-
try {
|
|
143
|
-
const params = await generate({
|
|
144
|
-
templateId: options.templateId,
|
|
145
|
-
steps: options.steps,
|
|
146
|
-
fields: options.fields,
|
|
147
|
-
notifyUrl: options.notifyUrl,
|
|
148
|
-
numExpectedUploadFiles: options.numExpectedUploadFiles,
|
|
149
|
-
expires: options.expires,
|
|
150
|
-
additionalParams: options.additionalParams,
|
|
151
|
-
userId: options.userId,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const response = await uploadViaForm(file, params, {
|
|
155
|
-
...options,
|
|
156
|
-
onProgress: (progress) => {
|
|
157
|
-
setState((prev) => ({ ...prev, progress }));
|
|
158
|
-
options.onProgress?.(progress);
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
setState({ isUploading: false, progress: 100, error: null });
|
|
163
|
-
return response;
|
|
164
|
-
} catch (error) {
|
|
165
|
-
const err = error instanceof Error ? error : new Error("Upload failed");
|
|
166
|
-
setState({ isUploading: false, progress: 0, error: err });
|
|
167
|
-
throw err;
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
[generate],
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const reset = useCallback(() => {
|
|
174
|
-
setState({ isUploading: false, progress: 0, error: null });
|
|
175
|
-
}, []);
|
|
176
|
-
|
|
177
|
-
return useMemo(
|
|
178
|
-
() => ({
|
|
179
|
-
upload,
|
|
180
|
-
reset,
|
|
181
|
-
isUploading: state.isUploading,
|
|
182
|
-
progress: state.progress,
|
|
183
|
-
error: state.error,
|
|
184
|
-
}),
|
|
185
|
-
[state.error, state.isUploading, state.progress, upload, reset],
|
|
186
|
-
);
|
|
72
|
+
onAssemblyCreated?: (assembly: {
|
|
73
|
+
assemblyId: string;
|
|
74
|
+
data: Record<string, unknown>;
|
|
75
|
+
}) => void;
|
|
187
76
|
}
|
|
188
77
|
|
|
189
78
|
export function useTransloaditTusUpload(createAssembly: CreateAssemblyFn) {
|
|
@@ -211,6 +100,7 @@ export function useTransloaditTusUpload(createAssembly: CreateAssemblyFn) {
|
|
|
211
100
|
});
|
|
212
101
|
|
|
213
102
|
const data = assembly.data as Record<string, unknown>;
|
|
103
|
+
options.onAssemblyCreated?.(assembly);
|
|
214
104
|
const tusUrl =
|
|
215
105
|
(typeof data.tus_url === "string" && data.tus_url) ||
|
|
216
106
|
(typeof data.tusUrl === "string" && data.tusUrl) ||
|
|
@@ -222,33 +112,132 @@ export function useTransloaditTusUpload(createAssembly: CreateAssemblyFn) {
|
|
|
222
112
|
);
|
|
223
113
|
}
|
|
224
114
|
|
|
115
|
+
const assemblyUrl =
|
|
116
|
+
(typeof data.assembly_ssl_url === "string" &&
|
|
117
|
+
data.assembly_ssl_url) ||
|
|
118
|
+
(typeof data.assembly_url === "string" && data.assembly_url) ||
|
|
119
|
+
(typeof data.assemblyUrl === "string" && data.assemblyUrl) ||
|
|
120
|
+
"";
|
|
121
|
+
|
|
122
|
+
if (!assemblyUrl) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
"Transloadit response missing assembly_url for resumable upload",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
225
128
|
const metadata: Record<string, string> = {
|
|
226
129
|
filename: file.name,
|
|
227
|
-
filetype: file.type,
|
|
228
130
|
...options.metadata,
|
|
229
131
|
};
|
|
132
|
+
if (file.type) {
|
|
133
|
+
metadata.filetype = file.type;
|
|
134
|
+
}
|
|
135
|
+
if (!metadata.fieldname) {
|
|
136
|
+
metadata.fieldname = options.fieldName ?? "file";
|
|
137
|
+
}
|
|
138
|
+
if (!metadata.assembly_url) {
|
|
139
|
+
metadata.assembly_url = assemblyUrl;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type RetryError = {
|
|
143
|
+
originalResponse?: {
|
|
144
|
+
getStatus?: () => number;
|
|
145
|
+
getHeader?: (header: string) => string | undefined;
|
|
146
|
+
} | null;
|
|
147
|
+
};
|
|
230
148
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
149
|
+
const getStatus = (error: RetryError) =>
|
|
150
|
+
error.originalResponse?.getStatus &&
|
|
151
|
+
typeof error.originalResponse.getStatus === "function"
|
|
152
|
+
? error.originalResponse.getStatus()
|
|
153
|
+
: 0;
|
|
154
|
+
|
|
155
|
+
const retryDelays = options.retryDelays
|
|
156
|
+
? [...options.retryDelays]
|
|
157
|
+
: [1000, 5000, 15000, 30000];
|
|
158
|
+
const rateLimitRetryDelays = options.rateLimitRetryDelays
|
|
159
|
+
? [...options.rateLimitRetryDelays]
|
|
160
|
+
: [20_000, 40_000, 80_000];
|
|
161
|
+
|
|
162
|
+
const shouldRetry = (error: RetryError) => {
|
|
163
|
+
const status = getStatus(error);
|
|
164
|
+
if (!status) return true;
|
|
165
|
+
if (status === 409 || status === 423) return true;
|
|
166
|
+
return status < 400 || status >= 500;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
let uploadUrl: string | null = null;
|
|
170
|
+
let rateLimitAttempt = 0;
|
|
171
|
+
|
|
172
|
+
const runUpload = () =>
|
|
173
|
+
new Promise<void>((resolve, reject) => {
|
|
174
|
+
let uploader: Upload;
|
|
175
|
+
const uploadOptions: ConstructorParameters<typeof Upload>[1] = {
|
|
176
|
+
endpoint: tusUrl,
|
|
177
|
+
metadata,
|
|
178
|
+
retryDelays,
|
|
179
|
+
uploadDataDuringCreation:
|
|
180
|
+
options.uploadDataDuringCreation ?? false,
|
|
181
|
+
onUploadUrlAvailable: () => {
|
|
182
|
+
uploadUrl = uploader.url;
|
|
183
|
+
},
|
|
184
|
+
onShouldRetry: (error, retryAttempt) =>
|
|
185
|
+
options.onShouldRetry?.(error, retryAttempt) ??
|
|
186
|
+
shouldRetry(error),
|
|
187
|
+
onProgress: (bytesUploaded, bytesTotal) => {
|
|
188
|
+
const progress = Math.round((bytesUploaded / bytesTotal) * 100);
|
|
189
|
+
setState((prev) => ({ ...prev, progress }));
|
|
190
|
+
options.onProgress?.(progress);
|
|
191
|
+
},
|
|
192
|
+
onError: (error) => {
|
|
193
|
+
reject(error);
|
|
194
|
+
},
|
|
195
|
+
onSuccess: () => {
|
|
196
|
+
resolve();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (options.chunkSize !== undefined) {
|
|
201
|
+
uploadOptions.chunkSize = options.chunkSize;
|
|
202
|
+
}
|
|
203
|
+
if (uploadUrl) {
|
|
204
|
+
uploadOptions.uploadUrl = uploadUrl;
|
|
205
|
+
}
|
|
206
|
+
if (options.overridePatchMethod !== undefined) {
|
|
207
|
+
uploadOptions.overridePatchMethod = options.overridePatchMethod;
|
|
208
|
+
}
|
|
209
|
+
if (options.storeFingerprintForResuming !== undefined) {
|
|
210
|
+
uploadOptions.storeFingerprintForResuming =
|
|
211
|
+
options.storeFingerprintForResuming;
|
|
212
|
+
}
|
|
213
|
+
if (options.removeFingerprintOnSuccess !== undefined) {
|
|
214
|
+
uploadOptions.removeFingerprintOnSuccess =
|
|
215
|
+
options.removeFingerprintOnSuccess;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
uploader = new Upload(file, uploadOptions);
|
|
219
|
+
|
|
220
|
+
uploader.start();
|
|
248
221
|
});
|
|
249
222
|
|
|
250
|
-
|
|
251
|
-
|
|
223
|
+
while (true) {
|
|
224
|
+
try {
|
|
225
|
+
await runUpload();
|
|
226
|
+
break;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const status = getStatus(error as RetryError);
|
|
229
|
+
if (
|
|
230
|
+
status === 429 &&
|
|
231
|
+
rateLimitAttempt < rateLimitRetryDelays.length
|
|
232
|
+
) {
|
|
233
|
+
const delay = rateLimitRetryDelays[rateLimitAttempt] ?? 0;
|
|
234
|
+
rateLimitAttempt += 1;
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
252
241
|
|
|
253
242
|
setState({ isUploading: false, progress: 100, error: null });
|
|
254
243
|
return assembly;
|
|
@@ -284,6 +273,60 @@ export function useAssemblyStatus(
|
|
|
284
273
|
return useQuery(getStatus, { assemblyId });
|
|
285
274
|
}
|
|
286
275
|
|
|
276
|
+
export function useAssemblyStatusWithPolling(
|
|
277
|
+
getStatus: GetAssemblyStatusFn,
|
|
278
|
+
refreshAssembly: RefreshAssemblyFn,
|
|
279
|
+
assemblyId: string,
|
|
280
|
+
options?: { pollIntervalMs?: number; stopOnTerminal?: boolean },
|
|
281
|
+
) {
|
|
282
|
+
const status = useQuery(getStatus, { assemblyId });
|
|
283
|
+
const refresh = useAction(refreshAssembly);
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (!assemblyId) return;
|
|
287
|
+
const intervalMs = options?.pollIntervalMs ?? 5000;
|
|
288
|
+
if (intervalMs <= 0) return;
|
|
289
|
+
|
|
290
|
+
const isTerminal = () => {
|
|
291
|
+
if (!options?.stopOnTerminal) return false;
|
|
292
|
+
if (!status || typeof status !== "object") return false;
|
|
293
|
+
const ok =
|
|
294
|
+
"ok" in status && typeof status.ok === "string" ? status.ok : "";
|
|
295
|
+
return (
|
|
296
|
+
ok === "ASSEMBLY_COMPLETED" ||
|
|
297
|
+
ok === "ASSEMBLY_FAILED" ||
|
|
298
|
+
ok === "ASSEMBLY_CANCELED"
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (isTerminal()) return;
|
|
303
|
+
|
|
304
|
+
let cancelled = false;
|
|
305
|
+
const tick = async () => {
|
|
306
|
+
if (cancelled) return;
|
|
307
|
+
await refresh({ assemblyId });
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
void tick();
|
|
311
|
+
const id = setInterval(() => {
|
|
312
|
+
void tick();
|
|
313
|
+
}, intervalMs);
|
|
314
|
+
|
|
315
|
+
return () => {
|
|
316
|
+
cancelled = true;
|
|
317
|
+
clearInterval(id);
|
|
318
|
+
};
|
|
319
|
+
}, [
|
|
320
|
+
assemblyId,
|
|
321
|
+
options?.pollIntervalMs,
|
|
322
|
+
options?.stopOnTerminal,
|
|
323
|
+
refresh,
|
|
324
|
+
status,
|
|
325
|
+
]);
|
|
326
|
+
|
|
327
|
+
return status;
|
|
328
|
+
}
|
|
329
|
+
|
|
287
330
|
export function useTransloaditFiles(
|
|
288
331
|
listResults: ListResultsFn,
|
|
289
332
|
args: { assemblyId: string; stepName?: string; limit?: number },
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { convexTest } from "convex-test";
|
|
4
|
+
import schema from "../component/schema.js";
|
|
5
|
+
|
|
6
|
+
export const modules = import.meta.glob("../component/**/*.*s");
|
|
7
|
+
|
|
8
|
+
export function createTransloaditTest() {
|
|
9
|
+
return convexTest(schema, modules);
|
|
10
|
+
}
|