*/ public function offers(int $needsGb, int $limit = 6): array { $usable = (int) ceil($needsGb * 1.25); // RAID 1 headroom plus the host's own footprint return collect($this->feed()) ->map(function (array $s) { $disks = $s['serverDiskData'] ?? []; $flash = array_sum($disks['nvme'] ?? []) + array_sum($disks['sata'] ?? []); return [ 'cpu' => (string) ($s['cpu'] ?? ''), 'ram' => (int) ($s['ram_size'] ?? 0), // The feed sends this as a LIST of disk descriptions // ("2x SSD M.2 NVMe 512 GB"), not a string. Casting an // array to string is a warning and the word "Array". 'disks' => trim(implode(', ', array_map( fn ($d) => is_scalar($d) ? (string) $d : '', (array) ($s['hdd_hr'] ?? []) ))), // Half, because the pair is a mirror. A machine's usable // space is not the sum of its disks on a platform that // sells "your data survives a disk dying". 'flash_gb' => (int) floor($flash / 2), 'ecc' => (bool) ($s['is_ecc'] ?? false), 'datacenter' => (string) ($s['datacenter'] ?? ''), 'price' => (float) ($s['price'] ?? 0), ]; }) // All-flash only, and ECC only. Both are product decisions, not // preferences: one VM per customer means every database lives on // the same disks, and ECC is part of what the backups promise. ->filter(fn (array $s) => $s['flash_gb'] >= $usable && $s['ecc'] && $s['ram'] >= 32) ->sortBy('price') ->take($limit) ->values() ->all(); } /** @return array> */ private function feed(): array { return Cache::remember('server-market', now()->addMinutes(self::TTL_MINUTES), function () { try { $response = Http::timeout(8)->get(self::FEED); return $response->successful() ? ($response->json('server') ?? []) : []; } catch (Throwable) { // An outside list being unreachable is not a reason for the // capacity page to fail. It shows the queue either way; the // prices are the convenience. return []; } }); } }