koala73/worldmonitor · error

Cable health unavailable

Error message

Cable health unavailable

What it means

fetchCableHealth throws 'Cable health unavailable' inside the circuit-breaker execute callback when the RPC response from InfrastructureServiceClient.getCableHealth fails the isUsableResponse validity check (src/services/cable-health.ts:70). isUsableResponse requires a positive finite generatedAt within 90 minutes, a non-empty object-shaped cables map, and every record with valid lastUpdated/evidence fields — so the throw fires when the backend returns an empty, stale, or structurally invalid snapshot.

Solutions

  1. Verify the infrastructure RPC service returns a fresh snapshot: call getCableHealth directly against getRpcBaseUrl() and inspect generatedAt and cables contents.
  2. Check the cable-health data worker is running and writing valid records (positive generatedAt, populated evidence arrays); re-run its seed/refresh job.
  3. Confirm src/generated clients are up to date with proto (make generate) — schema drift yields default-valued fields that fail isUsableResponse.
  4. Check client clock/system time; a skewed clock invalidates the age window (age < 0 or > 90 min) and rejects otherwise-good data.
  5. If the response is valid but rejected, add logging in isUsableResponse to identify which predicate fails and fix the producer accordingly.

Example fix

// before
const response = await client.getCableHealth({});
if (!isUsableResponse(response)) throw new Error('Cable health unavailable');
return response;
// after
const response = await client.getCableHealth({});
if (!isUsableResponse(response)) {
  console.warn('[cable-health] unusable snapshot:', { generatedAt: response?.generatedAt, cableCount: Object.keys(response?.cables ?? {}).length });
  throw new Error('Cable health unavailable');
}
return response;
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeCableSnapshot(r: unknown): boolean {
  const v = r as any;
  return !!v && Number.isFinite(v.generatedAt) && v.generatedAt > 0
    && v.cables && typeof v.cables === 'object' && !Array.isArray(v.cables);
}

Type guard

function isUsableCableHealth(r: unknown): r is GetCableHealthResponse {
  const v = r as any;
  if (!v || typeof v !== 'object') return false;
  const age = Date.now() - v.generatedAt;
  return Number.isFinite(v.generatedAt) && v.generatedAt > 0 && age >= 0 && age < 90 * 60 * 1000
    && v.cables && typeof v.cables === 'object' && !Array.isArray(v.cables);
}

Try / catch

try {
  const health = await fetchCableHealth();
  renderCableHealth(health);
} catch (err) {
  if (err.message === 'Cable health unavailable') renderCableLayerEmpty();
  else throw err;
}

Prevention

When it happens

Trigger: client.getCableHealth({}) resolves but the response has generatedAt <= 0 or older than 90 minutes, cables missing/not an object, or any cable record lacking valid lastUpdated/evidence entries. Also thrown when breaker.execute itself fails validation so the emptyFallback is returned and the post-check path (line 74-78) finds no retained last-good response.

Common situations: Infrastructure worker down or seeded with zero cable records (empty cables map + generatedAt 0); RPC base URL misconfigured so the client hits a wrong endpoint returning an empty/default proto; clock skew or stale persisted cache making generatedAt appear older than the 90-minute retention window; proto schema drift after regeneration so fields arrive unset.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22). Data as JSON: /api/errors/48b8f28385e4db94. Report an issue: GitHub.

Appendix: source

Thrown at src/services/cable-health.ts:70

        && typeof e.ts === 'number' && Number.isFinite(new Date(e.ts).getTime())));
}

function retainedResponse(): CableHealthResponse | null {
  if (!cachedResponse) return null;
  const age = Date.now() - Date.parse(cachedResponse.generatedAt);
  return age >= 0 && age < MAX_RETAINED_AGE_MS ? cachedResponse : null;
}

// ---- Public API ----

export async function fetchCableHealth(): Promise<CableHealthResponse> {
  const now = Date.now();
  const retained = retainedResponse();
  if (retained && now < cacheExpiry) return retained;

  const resp = await breaker.execute(async () => {
    const response = await client.getCableHealth({});
    if (!isUsableResponse(response)) throw new Error('Cable health unavailable');
    return response;
  }, emptyFallback, { shouldCache: isUsableResponse, staleRefreshMode: 'await' });

  if (!isUsableResponse(resp)) {
    const lastGood = retainedResponse();
    if (lastGood) return lastGood;
    throw new Error('Cable health unavailable');
  }

  const cables: Record<string, CableHealthRecord> = {};
  for (const [id, proto] of Object.entries(resp.cables)) {
    cables[id] = toRecord(proto);
  }

  const result: CableHealthResponse = {
    generatedAt: new Date(resp.generatedAt).toISOString(),
    cables,
  };

View on GitHub (pinned to e586b8b4b8)