@@ -445,17 +445,23 @@ async function addArcGISFeatureLayerAsGeoJson(
445445
446446 const bounds = arcgisExtentToBounds ( layerInfo . extent ) ;
447447 if ( bounds && options . zoomTo !== false ) app . fitBounds ?.( bounds ) ;
448- if ( map ) startArcGISViewportLoader ( id , map , queryUrl , options , layerInfo ) ;
448+ if ( map ) startArcGISViewportLoader ( id , map , queryUrl , options , ( ) => Promise . resolve ( layerInfo ) ) ;
449449 return id ;
450450}
451451
452- /** Keep one FeatureServer layer synchronized with the settled map viewport. */
452+ /**
453+ * Keep one FeatureServer layer synchronized with the settled map viewport.
454+ *
455+ * `resolveLayerInfo` is a thunk rather than a value so the restore path can
456+ * register a loader before it has the service metadata: the fetch then happens
457+ * on the first query, and a failed one is retried by the next.
458+ */
453459function startArcGISViewportLoader (
454460 layerId : string ,
455461 map : maplibregl . Map ,
456462 queryUrl : string ,
457463 options : ArcGISLayerOptions ,
458- layerInfo : ArcGISFeatureLayerInfo ,
464+ resolveLayerInfo : ( ) => Promise < ArcGISFeatureLayerInfo > ,
459465) : void {
460466 let abort : AbortController | null = null ;
461467 let requestSequence = 0 ;
@@ -488,6 +494,13 @@ function startArcGISViewportLoader(
488494 abort = controller ;
489495 loader . abort = controller ;
490496 const sequence = ++ requestSequence ;
497+ let layerInfo : ArcGISFeatureLayerInfo ;
498+ try {
499+ layerInfo = await resolveLayerInfo ( ) ;
500+ } catch ( error ) {
501+ if ( sequence !== requestSequence ) return currentArcGISLayerGeojson ( layerId ) ;
502+ throw error ;
503+ }
491504 const envelopes = arcgisViewportEnvelopes ( map . getBounds ( ) ) ;
492505 // One bucket per envelope, so a viewport split across the antimeridian
493506 // publishes both halves together instead of each replacing the other.
@@ -579,6 +592,10 @@ export function reloadArcGISViewportLayer(layerId: string): Promise<FeatureColle
579592 * view when it was saved: panning fetches nothing, and a refresh falls back to
580593 * the unbounded download the viewport path exists to avoid.
581594 *
595+ * Loaders are registered synchronously, before the service metadata they need
596+ * is fetched, so there is no window in which a refresh finds the layer
597+ * unbound.
598+ *
582599 * @param app - The host app API, for the map the loaders bind to.
583600 */
584601export function restoreArcGISViewportLayers ( app : GeoLibreAppAPI ) : void {
@@ -608,21 +625,23 @@ export function restoreArcGISViewportLayers(app: GeoLibreAppAPI): void {
608625 pageSize : typeof source . pageSize === "number" ? source . pageSize : undefined ,
609626 sourceType : "url" ,
610627 } ;
611- const layerId = layer . id ;
612628 // Re-read the service metadata rather than trusting a stored copy, the same
613629 // reason refreshArcGISFeatureLayer does: paging capabilities and
614- // `maxRecordCount` are the service's to change between sessions.
615- void fetchArcGISJson < ArcGISFeatureLayerInfo > (
616- trimTrailingSlash ( queryUrl ) . replace ( / \/ q u e r y $ / i, "" ) ,
617- options ,
618- undefined ,
619- )
620- . then ( ( layerInfo ) => {
621- // The layer may have been removed while the metadata was in flight.
622- if ( ! useAppStore . getState ( ) . layers . some ( ( entry ) => entry . id === layerId ) ) return ;
623- startArcGISViewportLoader ( layerId , map , queryUrl , options , layerInfo ) ;
624- } )
625- . catch ( ( error : unknown ) => handleArcGISViewportError ( layerId , error ) ) ;
630+ // `maxRecordCount` are the service's to change between sessions. Fetched
631+ // lazily on the first query and memoized, with a failure clearing the
632+ // memo — so a blocked or flaky reopen retries on the next pan or refresh
633+ // instead of leaving the layer permanently unbound.
634+ let pending : Promise < ArcGISFeatureLayerInfo > | null = null ;
635+ const resolveLayerInfo = ( ) : Promise < ArcGISFeatureLayerInfo > =>
636+ ( pending ??= fetchArcGISJson < ArcGISFeatureLayerInfo > (
637+ trimTrailingSlash ( queryUrl ) . replace ( / \/ q u e r y $ / i, "" ) ,
638+ options ,
639+ undefined ,
640+ ) . catch ( ( error : unknown ) => {
641+ pending = null ;
642+ throw error ;
643+ } ) ) ;
644+ startArcGISViewportLoader ( layer . id , map , queryUrl , options , resolveLayerInfo ) ;
626645 }
627646}
628647
@@ -1241,7 +1260,7 @@ async function planArcGISPaging(
12411260 supportsOrderBy : layerInfo . advancedQueryCapabilities ?. supportsOrderBy !== false ,
12421261 // Spatial counts can be as expensive as fetching the first page on large
12431262 // polygon services. Start rendering immediately for viewport queries.
1244- total : request . params ? null : await fetchArcGISFeatureCount ( queryUrl , params , request . signal ) ,
1263+ total : request . params ? null : await fetchArcGISFeatureCount ( queryUrl , params ) ,
12451264 } ;
12461265}
12471266
@@ -1275,13 +1294,16 @@ function positiveInteger(value: number | undefined): number | null {
12751294 * condition. Best-effort: a service that will not answer `returnCountOnly`
12761295 * still pages fine, so any failure resolves to `null` rather than throwing.
12771296 *
1297+ * Takes no abort signal, and needs none: {@link planArcGISPaging} skips the
1298+ * count entirely for a viewport query (the only cancellable caller), because a
1299+ * spatial count can cost as much as the first page.
1300+ *
12781301 * @param queryUrl - The layer's `/query` endpoint.
1279- * @param token - The access token to send, if any .
1302+ * @param params - The query params every request in the plan shares .
12801303 */
12811304async function fetchArcGISFeatureCount (
12821305 queryUrl : string ,
12831306 params : Record < string , string | undefined > ,
1284- signal ?: AbortSignal ,
12851307) : Promise < number | null > {
12861308 try {
12871309 const response = await fetch (
@@ -1291,15 +1313,13 @@ async function fetchArcGISFeatureCount(
12911313 returnCountOnly : "true" ,
12921314 where : "1=1" ,
12931315 } ) ,
1294- { signal } ,
12951316 ) ;
12961317 if ( ! response . ok ) return null ;
12971318 const json = ( await response . json ( ) ) as { count ?: unknown } ;
12981319 return typeof json . count === "number" && Number . isFinite ( json . count ) && json . count >= 0
12991320 ? json . count
13001321 : null ;
1301- } catch ( error ) {
1302- if ( signal ?. aborted ) throw error ;
1322+ } catch {
13031323 return null ;
13041324 }
13051325}
0 commit comments