Files
claudemesh/apps/web/src/utils.ts
Alejandro Gutiérrez d3163a5bff feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:19:32 +01:00

151 lines
4.8 KiB
TypeScript

/**
* Attempts to share content using the Web Share API, falls back to download if unavailable.
*
* @param data - Either a URL-based share (with optional filename) or a Blob-based share (requires filename)
*
* @example
* // Share/download a URL
* await shareOrDownload({ url: 'https://example.com/file.pdf', filename: 'report.pdf' });
*
* @example
* // Share/download a Blob
* const blob = new Blob(['Hello'], { type: 'text/plain' });
* await shareOrDownload({ blob, filename: 'hello.txt' });
*/
export async function shareOrDownload(
data: { url: string; filename?: string } | { blob: Blob; filename: string }
): Promise<void> {
if ("url" in data) {
// URL-based sharing/download
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (navigator.share) {
try {
await navigator.share({ url: data.url });
return;
} catch {
// Fall through to download
}
}
// Download fallback
const link = document.createElement("a");
link.href = data.url;
link.download = data.filename ?? "download";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
// Blob-based sharing/download
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
navigator.share &&
navigator.canShare({ files: [new File([data.blob], data.filename)] })
) {
try {
await navigator.share({
files: [
new File([data.blob], data.filename, { type: data.blob.type }),
],
});
return;
} catch {
// Fall through to download
}
}
// Download fallback
const url = URL.createObjectURL(data.blob);
const link = document.createElement("a");
link.href = url;
link.download = data.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
/**
* Wraps an async function to handle form submissions, preventing default and propagation
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function onPromise<T extends (...args: any[]) => Promise<unknown>>(
handler: T
): (...args: Parameters<T>) => void {
return (...args) => {
const event = args[0] as { preventDefault?: () => void; stopPropagation?: () => void } | undefined;
if (event?.preventDefault) {
event.preventDefault();
event.stopPropagation?.();
}
void handler(...args);
};
}
/**
* Upload a file with retry logic
* Overloaded to support both direct upload function and storage path-based upload
*/
export async function uploadWithRetry(
fileOrOptions: File | { path: string; file: File },
uploadFn?: (file: File) => Promise<string>,
options?: { maxRetries?: number; delayMs?: number }
): Promise<string> {
const { maxRetries = 3, delayMs = 1000 } = options ?? {};
let lastError: Error | undefined;
// Handle object-style call (path + file) - upload using fetch to presigned URL
if (typeof fileOrOptions === "object" && "path" in fileOrOptions) {
const { path, file } = fileOrOptions;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Get presigned URL from API
const response = await fetch("/api/storage/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path, contentType: file.type }),
});
if (!response.ok) throw new Error("Failed to get upload URL");
const { url, publicUrl } = (await response.json()) as {
url: string;
publicUrl?: string;
};
// Upload file to presigned URL
const uploadResponse = await fetch(url, {
method: "PUT",
body: file,
headers: { "Content-Type": file.type },
});
if (!uploadResponse.ok) throw new Error("Upload failed");
return publicUrl ?? path;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs * (attempt + 1)));
}
}
}
throw lastError ?? new Error("Upload failed after retries");
}
// Handle function-style call (file + uploadFn)
const file = fileOrOptions;
if (!uploadFn) {
throw new Error("uploadFn is required when passing a File directly");
}
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await uploadFn(file);
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs * (attempt + 1)));
}
}
}
throw lastError ?? new Error("Upload failed after retries");
}