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
- Re-run the webcam seeder so every indexed ID has a metadata hash field.
- Write IDs and metadata atomically (same pipeline/transaction) in the seeder.
- Align TTLs so metadata never outlives (or is outlived by) the ID set.
- 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
- Seed metadata and ID set together in one atomic write.
- Match TTLs between the ID set and metadata hash.
- Alert when the ID-set cardinality exceeds the metadata field count.
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)