From d38e3822e08a06823f9ad0f8a4dac2f9523dfa70 Mon Sep 17 00:00:00 2001 From: ccbblin <205563055+ccbblin@users.noreply.github.com> Date: Mon, 18 May 2026 08:44:12 +1200 Subject: [PATCH] fix(geo): handle concurrent projection loads --- .../proj/__tests__/projection.loader.test.ts | 27 +++++++++++++++++++ packages/geo/src/proj/projection.loader.ts | 15 +++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 packages/geo/src/proj/__tests__/projection.loader.test.ts diff --git a/packages/geo/src/proj/__tests__/projection.loader.test.ts b/packages/geo/src/proj/__tests__/projection.loader.test.ts new file mode 100644 index 0000000000..327f3173f5 --- /dev/null +++ b/packages/geo/src/proj/__tests__/projection.loader.test.ts @@ -0,0 +1,27 @@ +import assert from 'node:assert'; +import { describe, it, TestContext } from 'node:test'; + +import { PROJJSONDefinition } from 'proj4/dist/lib/core.js'; + +import { Aitm2000Json } from '../json/nzoi/aitm2000.js'; +import { ProjectionLoader } from '../projection.loader.js'; + +/** Arbitrary EPSG code not in ProjJsons and not pre-registered */ +const TestCode: number = 99999; + +describe('ProjectionLoader', () => { + it('should handle concurrent loads for the same code without throwing', async (t: TestContext) => { + t.mock.method( + ProjectionLoader, + 'fetchProjJson', + (): Promise => + Promise.resolve({ ...Aitm2000Json, id: { authority: 'EPSG', code: TestCode } }), + ); + + const [epsgA, epsgB] = await Promise.all([ProjectionLoader.load(TestCode), ProjectionLoader.load(TestCode)]); + + assert.equal(epsgA.code, TestCode); + assert.equal(epsgB.code, TestCode); + assert.equal(epsgA, epsgB); + }); +}); diff --git a/packages/geo/src/proj/projection.loader.ts b/packages/geo/src/proj/projection.loader.ts index 5083e43b48..cc57a4bd2a 100644 --- a/packages/geo/src/proj/projection.loader.ts +++ b/packages/geo/src/proj/projection.loader.ts @@ -11,6 +11,8 @@ export class ProjectionLoader { // Exposed for testing static _fetch = fetch; + private static _inflight = new Map>(); + /** * Initialises an Epsg instance for the given code and ensures that * a corresponding Projection instance is available. @@ -18,9 +20,18 @@ export class ProjectionLoader { * @param code - The code for which to initialise an Epsg and Projection instance. * @returns an Epsg instance */ - static async load(code: number): Promise { - if (Projection.tryGet(code) != null) return Epsg.get(code); + static load(code: number): Promise { + if (Projection.tryGet(code) != null) return Promise.resolve(Epsg.get(code)); + + let promise = this._inflight.get(code); + if (promise == null) { + promise = this._load(code).finally(() => this._inflight.delete(code)); + this._inflight.set(code, promise); + } + return promise; + } + private static async _load(code: number): Promise { let projJson: PROJJSONDefinition; if (UtmZone.isUTMZoneCode(code)) {