From d28c2c9c0c8412bc5f72feaf96e9f92c7a59a1de Mon Sep 17 00:00:00 2001 From: Matt Moyles Date: Thu, 15 Oct 2026 13:09:16 -0400 Subject: [PATCH] fix(maps): provision the world basemap when a map collection is downloaded The base assets and world.pmtiles were only fetched on a visit to /maps or /settings/maps. Easy setup downloads map collections without opening either page, so a box that went offline afterwards had regional extracts but a grey map everywhere else (#1030). downloadCollection() now starts ensureBaseAssets() in the background whenever it queues a download. It isn't awaited: a failure is logged and the lazy path on /maps and the Download Base Map action still apply. The in-flight guards for the tarball download and the world extract are now static. Controllers get a fresh MapService per request, so the per-instance guard never deduplicated across requests, and easy setup sends one download-collection request per selected collection. Fixes #1030 --- admin/app/services/map_service.ts | 38 +++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/admin/app/services/map_service.ts b/admin/app/services/map_service.ts index dae68a2f..4ce5262a 100644 --- a/admin/app/services/map_service.ts +++ b/admin/app/services/map_service.ts @@ -79,7 +79,11 @@ export class MapService implements IMapService { private readonly baseDirPath = join(process.cwd(), this.mapStoragePath) private baseAssetsExistCache: boolean | null = null private worldBasemapReady = false - private worldBasemapInFlight: Promise | null = null + // Process-wide rather than per instance: controllers get a fresh MapService per request, + // so easy setup's parallel collection downloads, or a /maps visit landing at the same + // moment, would otherwise each download the tarball and extract the basemap. + private static baseAssetFilesInFlight: Promise | null = null + private static worldBasemapInFlight: Promise | null = null async listRegions() { const files = (await this.listAllMapStorageItems()).filter( @@ -147,6 +151,14 @@ export class MapService implements IMapService { if (toDownload.length === 0) return null + // Collections are downloaded while online (often from easy setup, before anyone opens + // /maps), so provision the base assets and world basemap now too. Otherwise a box that + // goes offline next shows a grey map outside the downloaded regions (#1030). Not + // awaited: a failure leaves the lazy path on /maps and the Download Base Map action. + this.ensureBaseAssets().catch((err) => { + logger.warn(`[MapService] Base map provisioning for collection ${slug} failed: ${err}`) + }) + const downloadFilenames: string[] = [] for (const resource of toDownload) { @@ -440,18 +452,30 @@ export class MapService implements IMapService { */ private async ensureWorldBasemap(): Promise { if (this.worldBasemapReady) return - if (this.worldBasemapInFlight) return this.worldBasemapInFlight - this.worldBasemapInFlight = this._setupWorldBasemap().finally(() => { - this.worldBasemapInFlight = null - }) - return this.worldBasemapInFlight + if (!MapService.worldBasemapInFlight) { + MapService.worldBasemapInFlight = this._setupWorldBasemap().finally(() => { + MapService.worldBasemapInFlight = null + }) + } + // _setupWorldBasemap() throws on failure, so reaching here means the file is in place, + // including when another instance ran the extract. + await MapService.worldBasemapInFlight + this.worldBasemapReady = true } /** Download the style/sprite/font base assets if missing. Does not touch the world basemap. */ private async ensureBaseAssetFiles(): Promise { const exists = await this.checkBaseAssetsExist() if (exists) return true - return this.downloadBaseAssets() + if (!MapService.baseAssetFilesInFlight) { + MapService.baseAssetFilesInFlight = this.downloadBaseAssets().finally(() => { + MapService.baseAssetFilesInFlight = null + }) + } + const downloaded = await MapService.baseAssetFilesInFlight + // This instance cached `false` above; downloadBaseAssets() only updates its own instance. + if (downloaded) this.baseAssetsExistCache = true + return downloaded } private async _setupWorldBasemap(): Promise {