{"record":{"id":"6b4c05e1f5fc20e2","repo":"Crosstalk-Solutions/project-nomad","slug":"download-already-in-progress-for-url-info-url","errorCode":null,"errorMessage":"Download already in progress for URL ${info.url}","messagePattern":"Download already in progress for URL (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"admin/app/services/map_service.ts","lineNumber":656,"sourceCode":"    const latest = sorted[0]\n\n    const dateStr = latest.key.replace('.pmtiles', '')\n    const date = `${dateStr.slice(0, 4)}-${dateStr.slice(4, 6)}-${dateStr.slice(6, 8)}`\n\n    return {\n      url: `${PROTOMAPS_BUILD_BASE_URL}/${latest.key}`,\n      date,\n      size: latest.size,\n      key: latest.key,\n    }\n  }\n\n  async downloadGlobalMap(): Promise<{ filename: string; jobId?: string }> {\n    const info = await this.getGlobalMapInfo()\n\n    const existing = await RunDownloadJob.getByUrl(info.url)\n    if (existing) {\n      throw new Error(`Download already in progress for URL ${info.url}`)\n    }\n\n    const basePath = resolve(join(this.baseDirPath, 'pmtiles'))\n    const filepath = resolve(join(basePath, info.key))\n\n    // Prevent path traversal — resolved path must stay within the storage directory\n    if (!filepath.startsWith(basePath + sep)) {\n      throw new Error('Invalid filename')\n    }\n\n    // First, ensure base assets are present - the global map depends on them\n    const baseAssetsExist = await this.ensureBaseAssets()\n    if (!baseAssetsExist) {\n      throw new Error(\n        'Base map assets are missing and could not be downloaded. Please check your connection and try again.'\n      )\n    }\n","sourceCodeStart":638,"sourceCodeEnd":674,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/app/services/map_service.ts#L638-L674","documentation":"downloadGlobalMap resolves the latest world basemap info and checks RunDownloadJob.getByUrl for an existing job on that URL. If one exists (in any non-terminal state), it throws this guard error to prevent duplicate concurrent downloads of the same global map.","triggerScenarios":"Invoking downloadGlobalMap twice before the first job finishes — e.g. double-clicking a UI button, two admins triggering it, or a retry while the original job is still queued/running.","commonSituations":"Frontend missing debounce/disable-after-submit; cron or startup hook racing with a manual trigger; a stuck job row in run_download_jobs (crashed worker) that never completes, permanently blocking new downloads.","solutions":["Check the job status for that URL (RunDownloadJob.getByUrl) and wait for it to finish instead of retrying","Debounce/disable the trigger in the UI until the job completes","If a previous job is stuck (worker died), cancel/complete the stale job record so a new download can start"],"exampleFix":"// before\nawait mapService.downloadGlobalMap() // throws if already queued\n\n// after\nconst existing = await RunDownloadJob.getByUrl(latestUrl)\nif (existing) { await pollJob(existing.id); return }\nawait mapService.downloadGlobalMap()","handlingStrategy":"validation","validationCode":"const info = await mapService.getGlobalMapInfo()\nconst existing = await RunDownloadJob.getByUrl(info.url)\nif (existing) { console.log('already queued:', existing.id); return existing }\nawait mapService.downloadGlobalMap()","typeGuard":null,"tryCatchPattern":"try { await mapService.downloadGlobalMap() } catch (e) { if (e instanceof Error && e.message.startsWith('Download already in progress')) { /* treat as success/no-op: job already queued */ return } throw e }","preventionTips":["Disable submit buttons until the current job resolves","Poll job status by URL before triggering a new download","Clean up stale job records from dead workers so the guard cannot deadlock"],"tags":["concurrency","duplicate-download","job-queue"],"backgroundTag":"duplicate-operation-guard","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}