koala73/worldmonitor · error · SeedUnavailableError

Seed unavailable

Error message

Seed unavailable: ${key}

What it means

SeedUnavailableError thrown by listWebcams when a webcam ID from the Redis ID set has no corresponding metadata record in the strict hash-fields batch read (getHashFieldsBatchStrict with strict=true). Every ID must resolve to metadata; any missing entry aborts the whole listing with the metadata key named in the error.

Solutions

  1. Re-run the webcam seeder so every indexed ID has a metadata hash field.
  2. Write IDs and metadata atomically (same pipeline/transaction) in the seeder.
  3. Align TTLs so metadata never outlives (or is outlived by) the ID set.
  4. If partial results are acceptable, use non-strict mode or skip missing IDs with a logged warning instead of throwing.

Example fix

// before: throws on any missing metadata
const raw = metaMap.get(id);
if (!raw) throw new SeedUnavailableError(metaKey);
// after: skip and log missing entries
const raw = metaMap.get(id);
if (!raw) { logger.warn('missing webcam metadata', { id, metaKey }); continue; }
Defensive patterns

Strategy: try-catch

Validate before calling

// verify all indexed IDs have metadata before calling
const ids = await redis.smembers(idsKey);
const missing = ids.filter(id => !metaMap.get(id));

Try / catch

try {
  return await listWebcams(ctx, req);
} catch (e) {
  if (e instanceof SeedUnavailableError) {
    await triggerWebcamReseed();
    return { webcams: [], clusters: [], seedStatus: 'reseeding' };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling ListWebcams when the webcam ID index contains an ID whose metadata (metaKey hash field) was deleted, expired, or never seeded — the strict mode turns the missing field into a hard error.

Common situations: Partial seeder run (IDs written before metadata); TTL expiry of metadata but not IDs; Redis flush/re-provision; an ID added by another writer that doesn't follow the seeding contract.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at server/worldmonitor/webcam/v1/list-webcams.ts:150

    } else {
      const centerLon = (qW + qE) / 2;
      const widthKm = equirectangularWidthKm(qS, qN, qW, qE);
      ids = await geoSearchByBoxStrict(geoKey, centerLon, centerLat, widthKm, heightKm, MAX_RESULTS, true);
    }

    if (ids.length === 0) {
      const empty: ListWebcamsResponse = { webcams: [], clusters: [], totalInView: 0 };
      await setCachedJson(cacheKey, empty, RESPONSE_CACHE_TTL);
      return empty;
    }

    // Fetch metadata
    const metaMap = await getHashFieldsBatchStrict(metaKey, ids, true);
    const webcams: Array<{ webcamId: string; title: string; lat: number; lng: number; category: string; country: string }> = [];

    for (const id of ids) {
      const raw = metaMap.get(id);
      if (!raw) throw new SeedUnavailableError(metaKey);
      try {
        const meta = JSON.parse(raw);
        webcams.push({
          webcamId: id,
          title: meta.title || '',
          lat: meta.lat || 0,
          lng: meta.lng || 0,
          category: meta.category || 'other',
          country: meta.country || '',
        });
      } catch { throw new SeedUnavailableError(metaKey); }
    }

    const cellSize = getClusterCellSize(zoom);
    const { singles, clusters } = clusterWebcams(webcams, cellSize);

    const result: ListWebcamsResponse = {
      webcams: singles,

View on GitHub (pinned to e586b8b4b8)