{"record":{"id":"c83f5b5066c8c3d4","repo":"Crosstalk-Solutions/project-nomad","slug":"download-already-in-progress-for-url-url","errorCode":null,"errorMessage":"Download already in progress for URL ${url}","messagePattern":"Download already in progress for URL (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"admin/app/services/map_service.ts","lineNumber":266,"sourceCode":"          logger.info(\n            `[MapService] Kept prior ${parsed.resource_id} file (reason: ${decision.reason})`\n          )\n        }\n      } catch (error) {\n        logger.error(`[MapService] Failed to create InstalledResource for ${filename}:`, error)\n      }\n    }\n  }\n\n  async downloadRemote(url: string): Promise<{ filename: string; jobId?: string }> {\n    const parsed = new URL(url)\n    if (!parsed.pathname.endsWith('.pmtiles')) {\n      throw new Error(`Invalid PMTiles file URL: ${url}. URL must end with .pmtiles`)\n    }\n\n    const existing = await RunDownloadJob.getActiveByUrl(url)\n    if (existing) {\n      throw new Error(`Download already in progress for URL ${url}`)\n    }\n\n    const filename = url.split('/').pop()\n    if (!filename) {\n      throw new Error('Could not determine filename from URL')\n    }\n\n    const filepath = join(process.cwd(), this.mapStoragePath, 'pmtiles', filename)\n\n\n    // First, ensure base assets are present - regions depend 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":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/app/services/map_service.ts#L248-L284","documentation":"Concurrency guard in MapService.downloadRemote: before starting a new download job it queries RunDownloadJob.getActiveByUrl(url) and throws if an active job for the same URL already exists, preventing duplicate concurrent downloads of the same PMTiles file.","triggerScenarios":"Calling downloadRemote twice for the same URL while the first job is still running — double-clicked submit, retried request, or two admins triggering the import simultaneously.","commonSituations":"Frontend retry/backoff resubmitting before completion, webhook/poll-driven triggers racing, stale 'active' job records stuck in the DB after a crashed worker blocking future downloads.","solutions":["Wait for or monitor the existing job (its id is tracked in RunDownloadJob) instead of starting a new one","Disable/retry-guard the submit button while a job is pending","If no job is actually running, mark the stale RunDownloadJob record as failed/completed so the guard clears","Idempotently attach to the existing job at the API layer and return its jobId"],"exampleFix":"// before\nawait mapService.downloadRemote(url) // second click: throws\n\n// after\nconst active = await runDownloadJobService.getByUrl(url)\nif (active) return { jobId: active.id } // attach to existing\nreturn mapService.downloadRemote(url)","handlingStrategy":"fallback","validationCode":"const active = await runDownloadJobRepo.findOne({ where: { url, status: In(['active','running']) } })\nif (active) return { jobId: active.id, alreadyRunning: true } // attach, don't error\nreturn mapService.downloadRemote(url)","typeGuard":"const isDuplicateDownloadError = (e: unknown): boolean =>\n  (e as Error).message.startsWith('Download already in progress')","tryCatchPattern":"try {\n  await mapService.downloadRemote(url)\n} catch (e) {\n  if ((e as Error).message.startsWith('Download already in progress')) {\n    const job = await runDownloadJobService.getActiveByUrl(url)\n    return { jobId: job?.id, status: 'in-progress' } // fallback: join existing job\n  }\n  throw e\n}","preventionTips":["Debounce/disable the submit button while a job is pending","Return the existing job id from the API instead of erroring (idempotent design)","Reconcile stale 'active' job rows on worker startup via a heartbeat/timeout"],"tags":["concurrency","duplicate-request","job-scheduling","maps"],"backgroundTag":"duplicate-job-in-progress","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}