diff options
Diffstat (limited to 'apps/web/src/services/storage/quota.ts')
| -rw-r--r-- | apps/web/src/services/storage/quota.ts | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/apps/web/src/services/storage/quota.ts b/apps/web/src/services/storage/quota.ts new file mode 100644 index 00000000..1eb6028e --- /dev/null +++ b/apps/web/src/services/storage/quota.ts @@ -0,0 +1,145 @@ +const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; + +export const STORAGE_HEADROOM_RESERVE_BYTES = 50 * 1024 * 1024; + +export interface StorageQuotaStatus { + quotaBytes: number | null; + usageBytes: number | null; + headroomBytes: number | null; + availableBytes: number | null; +} + +export interface StorageCapacityCheckResult { + canStore: boolean; + reason: "enough-space" | "insufficient-space" | "estimate-unavailable"; + availableBytes: number | null; +} + +function normalizeByteValue({ value }: { value: unknown }): number | null { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return null; + } + + return value; +} + +export function formatStorageBytes({ bytes }: { bytes: number }): string { + if (!Number.isFinite(bytes) || bytes <= 0) { + return "0 B"; + } + + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < BYTE_UNITS.length - 1) { + value /= 1024; + unitIndex += 1; + } + + const precision = value >= 10 || unitIndex === 0 ? 0 : 1; + return `${value.toFixed(precision)} ${BYTE_UNITS[unitIndex]}`; +} + +export async function readStorageQuotaStatus(): Promise<StorageQuotaStatus> { + if ( + typeof navigator === "undefined" || + !navigator.storage || + typeof navigator.storage.estimate !== "function" + ) { + return { + quotaBytes: null, + usageBytes: null, + headroomBytes: null, + availableBytes: null, + }; + } + + const estimate = await navigator.storage.estimate(); + const quotaBytes = normalizeByteValue({ value: estimate.quota }); + const usageBytes = normalizeByteValue({ value: estimate.usage }); + + if (quotaBytes === null || usageBytes === null) { + return { + quotaBytes, + usageBytes, + headroomBytes: null, + availableBytes: null, + }; + } + + const headroomBytes = Math.max(quotaBytes - usageBytes, 0); + const availableBytes = Math.max( + headroomBytes - STORAGE_HEADROOM_RESERVE_BYTES, + 0, + ); + + return { + quotaBytes, + usageBytes, + headroomBytes, + availableBytes, + }; +} + +export function evaluateStorageCapacity({ + requiredBytes, + quotaStatus, +}: { + requiredBytes: number; + quotaStatus: StorageQuotaStatus; +}): StorageCapacityCheckResult { + if (quotaStatus.availableBytes === null) { + return { + canStore: true, + reason: "estimate-unavailable", + availableBytes: null, + }; + } + + if (requiredBytes > quotaStatus.availableBytes) { + return { + canStore: false, + reason: "insufficient-space", + availableBytes: quotaStatus.availableBytes, + }; + } + + return { + canStore: true, + reason: "enough-space", + availableBytes: quotaStatus.availableBytes, + }; +} + +export class StorageQuotaExceededError extends Error { + requiredBytes: number; + + constructor({ requiredBytes }: { requiredBytes: number }) { + super( + `Not enough browser storage to save a ${formatStorageBytes({ bytes: requiredBytes })} file.`, + ); + + this.name = "StorageQuotaExceededError"; + this.requiredBytes = requiredBytes; + } +} + +export function isStorageQuotaExceededError({ + error, +}: { + error: unknown; +}): boolean { + if (error instanceof StorageQuotaExceededError) { + return true; + } + + if (!(error instanceof Error)) { + return false; + } + + return ( + error.name === "QuotaExceededError" || + error.name === "NS_ERROR_DOM_QUOTA_REACHED" || + error.message.toLowerCase().includes("quota") + ); +} |
