import { COMPUTE_BASELINE_IOPS, COMPUTE_MAX_IOPS, computeInstanceAddonVariantIdSchema, } from 'shared-data' import { ComputeInstanceAddonVariantId, ComputeInstanceSize, InfraInstanceSize, } from './DiskManagement.types' import { DISK_LIMITS, DISK_PRICING, DiskType, PLAN_DETAILS } from './ui/DiskManagement.constants' import { ProjectDetail } from '@/data/projects/project-detail-query' import { PlanId, ProjectAddonVariantMeta } from '@/data/subscriptions/types' import { INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from '@/lib/constants' // Included disk size only applies to primary, not replicas export const calculateDiskSizePrice = ({ planId, oldSize, oldStorageType, newSize, newStorageType, numReplicas = 0, }: { planId: string oldSize: number oldStorageType: DiskType newSize: number newStorageType: DiskType numReplicas?: number }) => { const oldPricePerGB = DISK_PRICING[oldStorageType]?.storage ?? 0 const newPricePerGB = DISK_PRICING[newStorageType]?.storage ?? 0 const { includedDiskGB } = PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] const oldPrice = Math.max(oldSize - includedDiskGB[oldStorageType], 0) * oldPricePerGB const oldPriceReplica = oldSize * 1.25 * oldPricePerGB const newPrice = Math.max(newSize - includedDiskGB[newStorageType], 0) * newPricePerGB const newPriceReplica = newSize * 1.25 * newPricePerGB return { oldPrice: (oldPrice + numReplicas * oldPriceReplica).toFixed(2), newPrice: (newPrice + numReplicas * newPriceReplica).toFixed(2), } } export const calculateComputeSizePrice = ({ availableOptions, oldComputeSize, newComputeSize, plan, }: { availableOptions: { identifier: string price: number }[] oldComputeSize: string newComputeSize: string plan: PlanId }) => { let _oldComputeSize = oldComputeSize if (plan !== 'free' && oldComputeSize === 'ci_nano') { /** * override the old compute size to micro if the plan is not free * this is to handle the case in which nano compute is a paid entity */ _oldComputeSize = 'ci_micro' } const oldPrice = availableOptions?.find((x) => x.identifier === _oldComputeSize)?.price ?? 0 const newPrice = availableOptions?.find((x) => x.identifier === newComputeSize)?.price ?? 0 const oldPriceMonthly = oldPrice * 720 const newPriceMonthly = newPrice * 720 return { oldPrice: oldPriceMonthly.toFixed(2), newPrice: newPriceMonthly.toFixed(2), } } // Included IOPS applies to both primary and replicas export const calculateIOPSPrice = ({ oldStorageType, oldProvisionedIOPS, newStorageType, newProvisionedIOPS, numReplicas = 0, }: { oldStorageType: DiskType oldProvisionedIOPS: number newStorageType: DiskType newProvisionedIOPS: number numReplicas?: number }) => { if (newStorageType === DiskType.GP3) { const oldChargeableIOPS = Math.max( 0, oldProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops ) const newChargeableIOPS = Math.max( 0, newProvisionedIOPS - DISK_LIMITS[DiskType.GP3].includedIops ) const oldPrice = oldChargeableIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0) const newPrice = newChargeableIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0) return { oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2), newPrice: (newPrice * (1 + numReplicas)).toFixed(2), } } else { const oldPrice = oldStorageType === 'gp3' ? (oldProvisionedIOPS - DISK_LIMITS[oldStorageType].includedIops) * DISK_PRICING[oldStorageType].iops : oldProvisionedIOPS * (DISK_PRICING[oldStorageType]?.iops ?? 0) const newPrice = newProvisionedIOPS * (DISK_PRICING[newStorageType]?.iops ?? 0) return { oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2), newPrice: (newPrice * (1 + numReplicas)).toFixed(2), } } } // This is only applicable for GP3 storage type, no need to consider IO2 at all // Also assumes that disk size is > 400 GB (separate requirement to update throughput) // Also, included throughput applies to both primary and replicas export const calculateThroughputPrice = ({ storageType, newThroughput, oldThroughput, numReplicas = 0, }: { storageType: DiskType newThroughput: number oldThroughput: number numReplicas?: number }) => { if (storageType === DiskType.GP3 && newThroughput) { const oldChargeableThroughput = Math.max( 0, oldThroughput - DISK_LIMITS[DiskType.GP3].includedThroughput ) const newChargeableThroughput = Math.max( 0, newThroughput - DISK_LIMITS[DiskType.GP3].includedThroughput ) const oldPrice = oldChargeableThroughput * DISK_PRICING[DiskType.GP3].throughput const newPrice = newChargeableThroughput * DISK_PRICING[DiskType.GP3].throughput return { oldPrice: (oldPrice * (1 + numReplicas)).toFixed(2), newPrice: (newPrice * (1 + numReplicas)).toFixed(2), } } return { oldPrice: '0.00', newPrice: '0.00' } } export type ComputeAddonVariant = { identifier: ComputeInstanceAddonVariantId name: string price_description: string price: number price_interval: 'hourly' | 'monthly' price_type: string meta?: ProjectAddonVariantMeta } type AvailableAddon = { type: string variants: Array<{ identifier: string name: string price_description: string price: number price_interval: 'hourly' | 'monthly' price_type: string meta?: unknown }> } const isProjectAddonVariantMeta = (meta: unknown): meta is ProjectAddonVariantMeta => { if (typeof meta !== 'object' || meta === null) return false const obj = meta as Record // Validate supported_cloud_providers is an array if present (used at line 200) if ('supported_cloud_providers' in obj && !Array.isArray(obj.supported_cloud_providers)) { return false } // Check for at least one expected property to ensure it's likely a real ProjectAddonVariantMeta const hasExpectedProperty = 'cpu_cores' in obj || 'memory_gb' in obj || 'cpu_dedicated' in obj || 'baseline_disk_io_mbs' in obj || 'max_disk_io_mbs' in obj || 'connections_direct' in obj || 'connections_pooler' in obj || 'backup_duration_days' in obj || 'supported_cloud_providers' in obj return hasExpectedProperty } export function getAvailableComputeOptions( availableAddons: AvailableAddon[], projectCloudProvider?: string ) { const computeAddon = availableAddons.find((addon) => addon.type === 'compute_instance') const computeOptions: ComputeAddonVariant[] = computeAddon?.variants.flatMap((option) => { const parsedId = computeInstanceAddonVariantIdSchema.safeParse(option.identifier) if (!parsedId.success) return [] if (projectCloudProvider && isProjectAddonVariantMeta(option.meta)) { const isSupported = !option.meta.supported_cloud_providers || option.meta.supported_cloud_providers.includes(projectCloudProvider) if (!isSupported) return [] } return [ { ...option, identifier: parsedId.data, meta: isProjectAddonVariantMeta(option.meta) ? option.meta : undefined, }, ] }) ?? [] function hasMicroOptionFromApi() { return (computeAddon?.variants ?? []).some((variant) => variant.identifier === 'ci_micro') } // Backwards comp until API is deployed if (!hasMicroOptionFromApi()) { // Unshift to push to start of array computeOptions.unshift({ identifier: 'ci_micro', name: 'Micro', price_description: '$0.01344/hour (~$10/month)', price: 0.01344, price_interval: 'hourly', price_type: 'usage', meta: { cpu_cores: INSTANCE_MICRO_SPECS.cpu_cores, cpu_dedicated: INSTANCE_MICRO_SPECS.cpu_dedicated, memory_gb: INSTANCE_MICRO_SPECS.memory_gb, baseline_disk_io_mbs: INSTANCE_MICRO_SPECS.baseline_disk_io_mbs, max_disk_io_mbs: INSTANCE_MICRO_SPECS.max_disk_io_mbs, connections_direct: INSTANCE_MICRO_SPECS.connections_direct, connections_pooler: INSTANCE_MICRO_SPECS.connections_pooler, } as ProjectAddonVariantMeta, }) } computeOptions.unshift({ identifier: 'ci_nano', name: 'Nano', price_description: '$0/hour (~$0/month)', price: 0, price_interval: 'hourly', price_type: 'usage', // @ts-ignore API types it as Record meta: { cpu_cores: INSTANCE_NANO_SPECS.cpu_cores, cpu_dedicated: INSTANCE_NANO_SPECS.cpu_dedicated, memory_gb: INSTANCE_NANO_SPECS.memory_gb, baseline_disk_io_mbs: INSTANCE_NANO_SPECS.baseline_disk_io_mbs, max_disk_io_mbs: INSTANCE_NANO_SPECS.max_disk_io_mbs, connections_direct: INSTANCE_NANO_SPECS.connections_direct, connections_pooler: INSTANCE_NANO_SPECS.connections_pooler, } as ProjectAddonVariantMeta, }) return computeOptions } export const calculateMaxIopsAllowedForDiskSizeWithGp3 = (totalSize: number) => { return Math.max(3000, Math.min(500 * totalSize, 16000)) } export const calculateDiskSizeRequiredForIopsWithGp3 = (iops: number) => { return Math.max(1, Math.ceil(iops / 500)) } export const calculateMaxIopsAllowedForDiskSizeWithio2 = (totalSize: number) => { return Math.min(500 * totalSize, 256000) } export const calculateDiskSizeRequiredForIopsWithIo2 = (iops: number) => { return Math.max(4, Math.ceil(iops / 1000)) } export const calculateMaxThroughput = (iops: number) => { return Math.min(0.256 * iops, 1000) } export const calculateIopsRequiredForThroughput = (throughput: number) => { return Math.max(125, Math.ceil(throughput / 0.256)) } export const calculateBaselineIopsForComputeSize = (computeSize: string): number => { const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize) if (!parsed.success) return 0 return COMPUTE_BASELINE_IOPS[parsed.data] ?? 0 } export const calculateMaxIopsForComputeSize = (computeSize: string): number => { const parsed = computeInstanceAddonVariantIdSchema.safeParse(computeSize) if (!parsed.success) return 0 return COMPUTE_MAX_IOPS[parsed.data] ?? 0 } export const calculateComputeSizeRequiredForIops = ( iops: number ): ComputeInstanceAddonVariantId | undefined => { type ComputeSizeMax = { size: ComputeInstanceAddonVariantId; maxIops: number } const computeSizes: ComputeSizeMax[] = Object.entries(COMPUTE_MAX_IOPS) .map((entry) => { const [size, maxIops] = entry const parsedSize = computeInstanceAddonVariantIdSchema.safeParse(size) if (!parsedSize.success) return undefined return { size: parsedSize.data, maxIops: Number(maxIops) } }) .filter((value): value is ComputeSizeMax => value !== undefined) .sort((a, b) => a.maxIops - b.maxIops) for (const { size, maxIops } of computeSizes) { if (iops <= maxIops) { return size } } const fallbackSize = computeSizes[computeSizes.length - 1]?.size if (!fallbackSize) return undefined return fallbackSize } export const calculateDiskSizeRequiredForIops = (provisionedIOPS: number): number | undefined => { if (!provisionedIOPS) { console.error('IOPS is required') return undefined } if (isNaN(provisionedIOPS) || provisionedIOPS < 0) { console.error('IOPS must be a non-negative number') return undefined } if (provisionedIOPS > 256000) { console.error('Maximum allowed IOPS is 256000') return undefined } return Math.max(1, Math.ceil(provisionedIOPS / 1000)) } export const formatComputeName = (compute: string) => { return compute.toUpperCase().replace('CI_', '') } const infraToAddonVariant: Record = { pico: 'ci_nano', nano: 'ci_nano', micro: 'ci_micro', small: 'ci_small', medium: 'ci_medium', large: 'ci_large', xlarge: 'ci_xlarge', '2xlarge': 'ci_2xlarge', '4xlarge': 'ci_4xlarge', '8xlarge': 'ci_8xlarge', '12xlarge': 'ci_12xlarge', '16xlarge': 'ci_16xlarge', '24xlarge': 'ci_24xlarge', '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory', '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu', '24xlarge_high_memory': 'ci_24xlarge_high_memory', '48xlarge': 'ci_48xlarge', '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory', '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu', '48xlarge_high_memory': 'ci_48xlarge_high_memory', } const isInfraInstanceSize = (value: string): value is InfraInstanceSize => Object.prototype.hasOwnProperty.call(infraToAddonVariant, value) export const mapComputeSizeNameToAddonVariantId = ( computeSize: ProjectDetail['infra_compute_size'] ): ComputeInstanceAddonVariantId => { const fallback: InfraInstanceSize = 'nano' const matchedSize = computeSize && isInfraInstanceSize(computeSize) ? computeSize : undefined const sizeKey = matchedSize ?? fallback return infraToAddonVariant[sizeKey] } const addonVariantToComputeSize: Record = { ci_nano: 'Nano', ci_micro: 'Micro', ci_small: 'Small', ci_medium: 'Medium', ci_large: 'Large', ci_xlarge: 'XL', ci_2xlarge: '2XL', ci_4xlarge: '4XL', ci_8xlarge: '8XL', ci_12xlarge: '12XL', ci_16xlarge: '16XL', ci_24xlarge: '24XL', ci_24xlarge_optimized_memory: '24XL - Optimized Memory', ci_24xlarge_optimized_cpu: '24XL - Optimized CPU', ci_24xlarge_high_memory: '24XL - High Memory', ci_48xlarge: '48XL', ci_48xlarge_optimized_memory: '48XL - Optimized Memory', ci_48xlarge_optimized_cpu: '48XL - Optimized CPU', ci_48xlarge_high_memory: '48XL - High Memory', } export const mapAddOnVariantIdToComputeSize = ( addonVariantId: ComputeInstanceAddonVariantId = 'ci_nano' ): ComputeInstanceSize => { const parsed = computeInstanceAddonVariantIdSchema.safeParse(addonVariantId) if (!parsed.success) return addonVariantToComputeSize.ci_nano return addonVariantToComputeSize[parsed.data] } export const formatNumber = (num: number): string => { return num.toLocaleString('en-US') } export const showMicroUpgrade = (plan: PlanId, infraComputeSize: InfraInstanceSize): boolean => { return plan !== 'free' && infraComputeSize === 'nano' } export function hasBurstableIO(): boolean { return false; }