{"record":{"id":"e27412a3018fbbc9","repo":"koala73/worldmonitor","slug":"seed-unavailable-key-list-webcams","errorCode":null,"errorMessage":"Seed unavailable: ${key}","messagePattern":"Seed unavailable: (.+?)","errorType":"exception","errorClass":"SeedUnavailableError","httpStatus":503,"severity":"error","filePath":"server/worldmonitor/webcam/v1/list-webcams.ts","lineNumber":150,"sourceCode":"    } else {\n      const centerLon = (qW + qE) / 2;\n      const widthKm = equirectangularWidthKm(qS, qN, qW, qE);\n      ids = await geoSearchByBoxStrict(geoKey, centerLon, centerLat, widthKm, heightKm, MAX_RESULTS, true);\n    }\n\n    if (ids.length === 0) {\n      const empty: ListWebcamsResponse = { webcams: [], clusters: [], totalInView: 0 };\n      await setCachedJson(cacheKey, empty, RESPONSE_CACHE_TTL);\n      return empty;\n    }\n\n    // Fetch metadata\n    const metaMap = await getHashFieldsBatchStrict(metaKey, ids, true);\n    const webcams: Array<{ webcamId: string; title: string; lat: number; lng: number; category: string; country: string }> = [];\n\n    for (const id of ids) {\n      const raw = metaMap.get(id);\n      if (!raw) throw new SeedUnavailableError(metaKey);\n      try {\n        const meta = JSON.parse(raw);\n        webcams.push({\n          webcamId: id,\n          title: meta.title || '',\n          lat: meta.lat || 0,\n          lng: meta.lng || 0,\n          category: meta.category || 'other',\n          country: meta.country || '',\n        });\n      } catch { throw new SeedUnavailableError(metaKey); }\n    }\n\n    const cellSize = getClusterCellSize(zoom);\n    const { singles, clusters } = clusterWebcams(webcams, cellSize);\n\n    const result: ListWebcamsResponse = {\n      webcams: singles,","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/webcam/v1/list-webcams.ts#L132-L168","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: throws on any missing metadata\nconst raw = metaMap.get(id);\nif (!raw) throw new SeedUnavailableError(metaKey);\n// after: skip and log missing entries\nconst raw = metaMap.get(id);\nif (!raw) { logger.warn('missing webcam metadata', { id, metaKey }); continue; }","handlingStrategy":"try-catch","validationCode":"// verify all indexed IDs have metadata before calling\nconst ids = await redis.smembers(idsKey);\nconst missing = ids.filter(id => !metaMap.get(id));","typeGuard":null,"tryCatchPattern":"try {\n  return await listWebcams(ctx, req);\n} catch (e) {\n  if (e instanceof SeedUnavailableError) {\n    await triggerWebcamReseed();\n    return { webcams: [], clusters: [], seedStatus: 'reseeding' };\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["redis","seed-data","missing-metadata"],"backgroundTag":"record-not-found","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}