Crosstalk-Solutions/project-nomad · warning · Error

Download already in progress for URL ${url}

Error message

Download already in progress for URL ${url}

What it means

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.

Source

Thrown at admin/app/services/map_service.ts:266

          logger.info(
            `[MapService] Kept prior ${parsed.resource_id} file (reason: ${decision.reason})`
          )
        }
      } catch (error) {
        logger.error(`[MapService] Failed to create InstalledResource for ${filename}:`, error)
      }
    }
  }

  async downloadRemote(url: string): Promise<{ filename: string; jobId?: string }> {
    const parsed = new URL(url)
    if (!parsed.pathname.endsWith('.pmtiles')) {
      throw new Error(`Invalid PMTiles file URL: ${url}. URL must end with .pmtiles`)
    }

    const existing = await RunDownloadJob.getActiveByUrl(url)
    if (existing) {
      throw new Error(`Download already in progress for URL ${url}`)
    }

    const filename = url.split('/').pop()
    if (!filename) {
      throw new Error('Could not determine filename from URL')
    }

    const filepath = join(process.cwd(), this.mapStoragePath, 'pmtiles', filename)


    // First, ensure base assets are present - regions depend on them
    const baseAssetsExist = await this.ensureBaseAssets()
    if (!baseAssetsExist) {
      throw new Error(
        'Base map assets are missing and could not be downloaded. Please check your connection and try again.'
      )
    }

View on GitHub (pinned to 0bd1c6f4f9)

Solutions

  1. Wait for or monitor the existing job (its id is tracked in RunDownloadJob) instead of starting a new one
  2. Disable/retry-guard the submit button while a job is pending
  3. If no job is actually running, mark the stale RunDownloadJob record as failed/completed so the guard clears
  4. Idempotently attach to the existing job at the API layer and return its jobId

Example fix

// before
await mapService.downloadRemote(url) // second click: throws

// after
const active = await runDownloadJobService.getByUrl(url)
if (active) return { jobId: active.id } // attach to existing
return mapService.downloadRemote(url)
Defensive patterns

Strategy: fallback

Validate before calling

const active = await runDownloadJobRepo.findOne({ where: { url, status: In(['active','running']) } })
if (active) return { jobId: active.id, alreadyRunning: true } // attach, don't error
return mapService.downloadRemote(url)

Type guard

const isDuplicateDownloadError = (e: unknown): boolean =>
  (e as Error).message.startsWith('Download already in progress')

Try / catch

try {
  await mapService.downloadRemote(url)
} catch (e) {
  if ((e as Error).message.startsWith('Download already in progress')) {
    const job = await runDownloadJobService.getActiveByUrl(url)
    return { jobId: job?.id, status: 'in-progress' } // fallback: join existing job
  }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/c83f5b5066c8c3d4. Report an issue: GitHub.