uploadthing
Version:
Learn more: [docs.uploadthing.com](https://docs.uploadthing.com)
287 lines (284 loc) • 11 kB
JavaScript
import { version } from "../dist/package-Beb-iarE.js";
import { createDeferred, createUTReporter, generateTraceHeaders } from "../dist/ut-reporter-C8446w-v.js";
import { logDeprecationWarning } from "../dist/deprecations-pLmw6Ytd.js";
import * as Arr from "effect/Array";
import * as Micro$1 from "effect/Micro";
import * as Micro from "effect/Micro";
import { FetchContext, UploadAbortedError, UploadAbortedError as UploadAbortedError$1, UploadPausedError, UploadPausedError as UploadPausedError$1, UploadThingError, allowedContentTextLabelGenerator, bytesToFileSize, createIdentityProxy, fetchEff, fileSizeToBytes, generateClientDropzoneAccept, generateMimeTypes, generatePermittedFileTypes, matchFileType, objectKeys, resolveMaybeUrlArg } from "@uploadthing/shared";
import { unsafeCoerce } from "effect/Function";
import { hasProperty, isRecord } from "effect/Predicate";
//#region src/_internal/upload-browser.ts
const uploadWithProgress = (file, rangeStart, presigned, opts) => Micro$1.async((resume) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", presigned.url, true);
xhr.setRequestHeader("Range", `bytes=${rangeStart}-`);
xhr.setRequestHeader("x-uploadthing-version", version);
xhr.setRequestHeader("b3", opts.traceHeaders.b3);
xhr.setRequestHeader("traceparent", opts.traceHeaders.traceparent);
xhr.responseType = "json";
let previousLoaded = 0;
xhr.upload.addEventListener("progress", ({ loaded }) => {
const delta = loaded - previousLoaded;
opts.onUploadProgress?.({
loaded,
delta
});
previousLoaded = loaded;
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300 && isRecord(xhr.response)) if (hasProperty(xhr.response, "error")) resume(new UploadThingError({
code: "UPLOAD_FAILED",
message: String(xhr.response.error),
data: xhr.response
}));
else resume(Micro$1.succeed(xhr.response));
else resume(new UploadThingError({
code: "UPLOAD_FAILED",
message: `XHR failed ${xhr.status} ${xhr.statusText}`,
data: xhr.response
}));
});
xhr.addEventListener("error", () => {
resume(new UploadThingError({ code: "UPLOAD_FAILED" }));
});
const formData = new FormData();
/**
* iOS/React Native FormData handling requires special attention:
*
* Issue: In React Native, iOS crashes with "attempt to insert nil object" when appending File directly
* to FormData. This happens because iOS tries to create NSDictionary from the file object and expects
* specific structure {uri, type, name}.
*
*
* Note: Don't try to use Blob or modify File object - iOS specifically needs plain object
* with these properties to create valid NSDictionary.
*/
if ("uri" in file) formData.append("file", {
uri: file.uri,
type: file.type,
name: file.name,
...rangeStart > 0 && { range: rangeStart }
});
else formData.append("file", rangeStart > 0 ? file.slice(rangeStart) : file);
xhr.send(formData);
return Micro$1.sync(() => xhr.abort());
});
const uploadFile = (file, presigned, opts) => fetchEff(presigned.url, {
method: "HEAD",
headers: opts.traceHeaders
}).pipe(Micro$1.map(({ headers }) => parseInt(headers.get("x-ut-range-start") ?? "0", 10)), Micro$1.tap((start) => opts.onUploadProgress?.({
delta: start,
loaded: start
})), Micro$1.flatMap((start) => uploadWithProgress(file, start, presigned, {
traceHeaders: opts.traceHeaders,
onUploadProgress: (progressEvent) => opts.onUploadProgress?.({
delta: progressEvent.delta,
loaded: progressEvent.loaded + start
})
})), Micro$1.map(unsafeCoerce), Micro$1.map((uploadResponse) => ({
name: file.name,
size: file.size,
key: presigned.key,
lastModified: file.lastModified,
serverData: uploadResponse.serverData,
get url() {
logDeprecationWarning("`file.url` is deprecated and will be removed in uploadthing v9. Use `file.ufsUrl` instead.");
return uploadResponse.url;
},
get appUrl() {
logDeprecationWarning("`file.appUrl` is deprecated and will be removed in uploadthing v9. Use `file.ufsUrl` instead.");
return uploadResponse.appUrl;
},
ufsUrl: uploadResponse.ufsUrl,
customId: presigned.customId,
type: file.type,
fileHash: uploadResponse.fileHash
})));
const uploadFilesInternal = (endpoint, opts) => {
const traceHeaders = generateTraceHeaders();
const reportEventToUT = createUTReporter({
endpoint: String(endpoint),
package: opts.package,
url: opts.url,
headers: opts.headers,
traceHeaders
});
const totalSize = opts.files.reduce((acc, f) => acc + f.size, 0);
let totalLoaded = 0;
return Micro$1.flatMap(reportEventToUT("upload", {
input: "input" in opts ? opts.input : null,
files: opts.files.map((f) => ({
name: f.name,
size: f.size,
type: f.type,
lastModified: f.lastModified
}))
}), (presigneds) => Micro$1.forEach(presigneds, (presigned, i) => Micro$1.flatMap(Micro$1.sync(() => opts.onUploadBegin?.({ file: opts.files[i].name })), () => uploadFile(opts.files[i], presigned, {
traceHeaders,
onUploadProgress: (ev) => {
totalLoaded += ev.delta;
opts.onUploadProgress?.({
file: opts.files[i],
progress: ev.loaded / opts.files[i].size * 100,
loaded: ev.loaded,
delta: ev.delta,
totalLoaded,
totalProgress: totalLoaded / totalSize
});
}
})), { concurrency: 6 }));
};
//#endregion
//#region src/client.ts
const version$1 = version;
/**
* Validate that a file is of a valid type given a route config
* @public
*/
const isValidFileType = (file, routeConfig) => Micro.runSync(matchFileType(file, objectKeys(routeConfig)).pipe(Micro.map((type) => file.type.includes(type)), Micro.orElseSucceed(() => false)));
/**
* Validate that a file is of a valid size given a route config
* @public
*/
const isValidFileSize = (file, routeConfig) => Micro.runSync(matchFileType(file, objectKeys(routeConfig)).pipe(Micro.flatMap((type) => fileSizeToBytes(routeConfig[type].maxFileSize)), Micro.map((maxFileSize) => file.size <= maxFileSize), Micro.orElseSucceed(() => false)));
/**
* Generate a typed uploader for a given FileRouter
* @public
*/
const genUploader = (initOpts) => {
const routeRegistry = createIdentityProxy();
const controllableUpload = async (slug, opts) => {
const uploads = new Map();
const endpoint = typeof slug === "function" ? slug(routeRegistry) : slug;
const traceHeaders = generateTraceHeaders();
const utReporter = createUTReporter({
endpoint: String(endpoint),
package: initOpts?.package ?? "uploadthing/client",
url: resolveMaybeUrlArg(initOpts?.url),
headers: opts.headers,
traceHeaders
});
const fetchFn = initOpts?.fetch ?? window.fetch;
const presigneds = await Micro.runPromise(utReporter("upload", {
input: "input" in opts ? opts.input : null,
files: opts.files.map((f) => ({
name: f.name,
size: f.size,
type: f.type,
lastModified: f.lastModified
}))
}).pipe(Micro.provideService(FetchContext, fetchFn)));
const totalSize = opts.files.reduce((acc, f) => acc + f.size, 0);
let totalLoaded = 0;
const uploadEffect = (file, presigned) => uploadFile(file, presigned, {
traceHeaders,
onUploadProgress: (progressEvent) => {
totalLoaded += progressEvent.delta;
opts.onUploadProgress?.({
...progressEvent,
file,
progress: Math.round(progressEvent.loaded / file.size * 100),
totalLoaded,
totalProgress: Math.round(totalLoaded / totalSize * 100)
});
}
}).pipe(Micro.provideService(FetchContext, fetchFn));
for (const [i, p] of presigneds.entries()) {
const file = opts.files[i];
if (!file) continue;
const deferred = createDeferred();
uploads.set(file, {
deferred,
presigned: p
});
Micro.runPromiseExit(uploadEffect(file, p), { signal: deferred.ac.signal }).then((result) => {
if (result._tag === "Success") return deferred.resolve(result.value);
else if (result.cause._tag === "Interrupt") throw new UploadPausedError$1();
throw Micro.causeSquash(result.cause);
}).catch((err) => {
if (err instanceof UploadPausedError$1) return;
deferred.reject(err);
});
}
/**
* Pause an ongoing upload
* @param file The file upload you want to pause. Can be omitted to pause all files
*/
const pauseUpload = (file) => {
const files = Arr.ensure(file ?? opts.files);
for (const file$1 of files) {
const upload = uploads.get(file$1);
if (!upload) return;
if (upload.deferred.ac.signal.aborted) throw new UploadAbortedError$1();
upload.deferred.ac.abort();
}
};
/**
* Resume a paused upload
* @param file The file upload you want to resume. Can be omitted to resume all files
*/
const resumeUpload = (file) => {
const files = Arr.ensure(file ?? opts.files);
for (const file$1 of files) {
const upload = uploads.get(file$1);
if (!upload) throw "No upload found";
upload.deferred.ac = new AbortController();
Micro.runPromiseExit(uploadEffect(file$1, upload.presigned), { signal: upload.deferred.ac.signal }).then((result) => {
if (result._tag === "Success") return upload.deferred.resolve(result.value);
else if (result.cause._tag === "Interrupt") throw new UploadPausedError$1();
throw Micro.causeSquash(result.cause);
}).catch((err) => {
if (err instanceof UploadPausedError$1) return;
upload.deferred.reject(err);
});
}
};
/**
* Wait for an upload to complete
* @param file The file upload you want to wait for. Can be omitted to wait for all files
*/
const done = async (file) => {
const promises = [];
const files = Arr.ensure(file ?? opts.files);
for (const file$1 of files) {
const upload = uploads.get(file$1);
if (!upload) throw "No upload found";
promises.push(upload.deferred.promise);
}
const results = await Promise.all(promises);
return file ? results[0] : results;
};
return {
pauseUpload,
resumeUpload,
done
};
};
/**
* One step upload function that both requests presigned URLs
* and then uploads the files to UploadThing
*/
const typedUploadFiles = (slug, opts) => {
const endpoint = typeof slug === "function" ? slug(routeRegistry) : slug;
const fetchFn = initOpts?.fetch ?? window.fetch;
return uploadFilesInternal(endpoint, {
...opts,
skipPolling: {},
url: resolveMaybeUrlArg(initOpts?.url),
package: initOpts?.package ?? "uploadthing/client",
input: opts.input
}).pipe(Micro.provideService(FetchContext, fetchFn), (effect) => Micro.runPromiseExit(effect, opts.signal && { signal: opts.signal })).then((exit) => {
if (exit._tag === "Success") return exit.value;
else if (exit.cause._tag === "Interrupt") throw new UploadAbortedError$1();
throw Micro.causeSquash(exit.cause);
});
};
return {
uploadFiles: typedUploadFiles,
createUpload: controllableUpload,
routeRegistry
};
};
//#endregion
export { UploadAbortedError, UploadPausedError, allowedContentTextLabelGenerator, bytesToFileSize, genUploader, generateClientDropzoneAccept, generateMimeTypes, generatePermittedFileTypes, isValidFileSize, isValidFileType, version$1 as version };
//# sourceMappingURL=index.js.map