Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
98437f1
docs: spec for coalescing tile fetches through deck.gl getTileData
kylebarron May 12, 2026
163bdf4
docs(spec): per-origin concurrency limiter (gating only)
kylebarron May 19, 2026
fb20134
feat(geotiff): Semaphore primitive with signal-aware acquire
kylebarron May 19, 2026
93a391b
feat(geotiff): ConcurrencyLimiter interface + PerOriginSemaphore
kylebarron May 19, 2026
d9d8612
feat(geotiff): limitFetch helper
kylebarron May 19, 2026
7282713
feat(geotiff): export ConcurrencyLimiter type and PerOriginSemaphore …
kylebarron May 19, 2026
c09b508
feat(geotiff): concurrencyLimiter option on GeoTIFF.fromUrl
kylebarron May 19, 2026
62a13f1
feat(deck.gl-geotiff): default PerOriginSemaphore + fetchGeoTIFF option
kylebarron May 19, 2026
9fdc43f
feat(deck.gl-geotiff): COGLayer.concurrencyLimiter prop + default
kylebarron May 19, 2026
c71dcd8
feat(deck.gl-geotiff): MultiCOGLayer.concurrencyLimiter prop + default
kylebarron May 19, 2026
46a763e
docs(geotiff): docstrings on Semaphore internals (PR #557 review)
kylebarron May 19, 2026
ee60fef
refactor(geotiff): limitFetch → LimiterMiddleware class (PR #557 review)
kylebarron May 19, 2026
db6b69f
cleaner
kylebarron May 19, 2026
e413fc9
feat(geotiff): dynamic priority on ConcurrencyLimiter / Semaphore
kylebarron May 19, 2026
8d58e10
feat(geotiff): thread getPriority through GeoTIFF.fromUrl + fetchGeoTIFF
kylebarron May 19, 2026
8afac6e
feat(deck.gl-geotiff): MosaicLayer passes distance-from-viewport-cent…
kylebarron May 19, 2026
85c5e46
feat(examples/naip-mosaic): thread getPriority through getCachedGeoTIFF
kylebarron May 19, 2026
d1abbf0
fix(examples/naip-mosaic): actually install the limiter so priority c…
kylebarron May 19, 2026
b99a551
feat(deck.gl-geotiff): MosaicLayer concurrencyLimiter prop
kylebarron May 19, 2026
5515739
feat(geotiff): gate header fetches through the concurrency limiter
kylebarron May 19, 2026
a4b9777
refactor(examples/naip-mosaic): forward MosaicLayer opts straight to …
kylebarron May 19, 2026
4c6703b
docs(spec): cover dynamic priority + LimiterMiddleware + MosaicLayer
kylebarron May 19, 2026
6134596
Merge remote-tracking branch 'origin/main' into kyle/getTileData-coal…
kylebarron May 20, 2026
13ce197
feat(geotiff): export GeoTIFFFromUrlOptions; fix stale gating docstring
kylebarron May 21, 2026
acd46d6
fix(deck.gl-geotiff): address PR #557 review round 2
kylebarron May 21, 2026
6d8d241
fix(geotiff): thread signal into getTileSize header reads
kylebarron May 21, 2026
45d13c8
options object
kylebarron May 21, 2026
7e881b3
fix(deck.gl-geotiff): drop debounceTime from MosaicLayer
kylebarron May 21, 2026
7b18d1b
shorter comment
kylebarron May 21, 2026
860abac
refactor(deck.gl-geotiff): simplify COG layer AbortController to fina…
kylebarron May 21, 2026
0d57f73
refactor(deck.gl-geotiff): extract createGetPriorityCallback helper
kylebarron May 21, 2026
7ba675b
refactor(deck.gl-geotiff): extract openCogSources helper
kylebarron May 21, 2026
eb4a2b3
remove comment
kylebarron May 21, 2026
10638e7
Merge remote-tracking branch 'origin/main' into kyle/getTileData-coal…
kylebarron May 21, 2026
2668b82
fix(geotiff): gate via source wrapper so abort-while-queued works
kylebarron May 22, 2026
e5c3e5c
test(geotiff): abort a queued fromUrl header read end-to-end
kylebarron May 22, 2026
c7d2d18
fix(deck.gl-geotiff): don't forward undefined maxRequests from Mosaic…
kylebarron May 22, 2026
1c7b382
refactor(deck.gl-geotiff): omit undefined when forwarding TileLayerProps
kylebarron May 22, 2026
63eecbf
cleaner comment
kylebarron May 22, 2026
a655b4b
concise
kylebarron May 22, 2026
92e65ca
concsie
kylebarron May 22, 2026
3d825a5
concise
kylebarron May 22, 2026
db7cff3
concise
kylebarron May 22, 2026
07efc3e
move up abort signal check
kylebarron May 22, 2026
26435d1
doc comment
kylebarron May 22, 2026
1316bc8
fix(deck.gl-geotiff,geotiff): address round-2 review nits
kylebarron May 22, 2026
369a280
cleaner options
kylebarron May 22, 2026
38dd4d5
concise
kylebarron May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions dev-docs/specs/2026-05-19-concurrency-limiter-design.md

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions examples/naip-mosaic/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
LinearRescale,
} from "@developmentseed/deck.gl-raster/gpu-modules";
import colormapsPngUrl from "@developmentseed/deck.gl-raster/gpu-modules/colormaps.png";
import type { Overview } from "@developmentseed/geotiff";
import type { GeoTIFFFromUrlOptions, Overview } from "@developmentseed/geotiff";
import { GeoTIFF } from "@developmentseed/geotiff";
import type { Device, Texture } from "@luma.gl/core";
import type { ShaderModule } from "@luma.gl/shadertools";
Expand Down Expand Up @@ -85,10 +85,13 @@ type TextureDataT = {
*/
const geotiffCache = new Map<string, Promise<GeoTIFF>>();

function getCachedGeoTIFF(url: string, signal?: AbortSignal): Promise<GeoTIFF> {
function getCachedGeoTIFF(
url: string,
opts: GeoTIFFFromUrlOptions,
): Promise<GeoTIFF> {
let promise = geotiffCache.get(url);
if (!promise) {
promise = GeoTIFF.fromUrl(url, { signal }).catch((err) => {
promise = GeoTIFF.fromUrl(url, opts).catch((err) => {
geotiffCache.delete(url);
throw err;
});
Expand Down Expand Up @@ -351,7 +354,6 @@ export default function App() {
async function wrappedFetchSTACItems() {
try {
const data = STAC_DATA as unknown as STACFeatureCollection;
(window as any).data = data;
setStacItems(data.features);
} catch (err) {
console.error("Error fetching STAC items:", err);
Expand Down Expand Up @@ -410,8 +412,8 @@ export default function App() {
// the MosaicLayer's TileLayer cache so we can keep cheap header metadata
// around indefinitely without pinning every parent tile (and its inner
// COGLayer's in-flight tile requests) in memory.
getSource: async (source, { signal }) =>
getCachedGeoTIFF(source.assets.image.href, signal),
getSource: async (source, opts) =>
getCachedGeoTIFF(source.assets.image.href, opts),
renderSource: (source, { data, signal }) => {
const url = source.assets.image.href;
return new COGLayer<TextureDataT>({
Expand All @@ -434,8 +436,9 @@ export default function App() {
signal,
});
},
// Smaller cache for MosaicLayer cache, since it caches full COGLayer
// instances
// Disable the MosaicLayer tile cache: each cached tile is a full
// COGLayer instance, and opened GeoTIFFs are already kept in the
// module-level `geotiffCache`, so there's nothing cheap to retain here.
maxCacheSize: 0,
// @ts-expect-error beforeId is injected by @deck.gl/mapbox; LayerProps
// doesn't know about it.
Expand Down
55 changes: 51 additions & 4 deletions packages/deck.gl-geotiff/src/cog-layer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UpdateParameters } from "@deck.gl/core";
import type { LayerContext, UpdateParameters } from "@deck.gl/core";
import type {
MinimalTileData,
GetTileDataOptions as RasterTileGetTileDataOptions,
Expand All @@ -7,7 +7,12 @@ import type {
RenderTileResult,
} from "@developmentseed/deck.gl-raster";
import { RasterTileLayer } from "@developmentseed/deck.gl-raster";
import type { DecoderPool, GeoTIFF, Overview } from "@developmentseed/geotiff";
import type {
ConcurrencyLimiter,
DecoderPool,
GeoTIFF,
Overview,
} from "@developmentseed/geotiff";
import { defaultDecoderPool } from "@developmentseed/geotiff";
import type { EpsgResolver, ProjectionDefinition } from "@developmentseed/proj";
import {
Expand All @@ -18,6 +23,7 @@ import {
} from "@developmentseed/proj";
import type { Texture } from "@luma.gl/core";
import proj4 from "proj4";
import { DEFAULT_CONCURRENCY_LIMITER } from "./default-concurrency-limiter.js";
import { fetchGeoTIFF, getGeographicBounds } from "./geotiff/geotiff.js";
import type { TextureDataT } from "./geotiff/render-pipeline.js";
import { inferRenderPipeline } from "./geotiff/render-pipeline.js";
Expand Down Expand Up @@ -133,6 +139,19 @@ export type COGLayerProps<DataT extends MinimalTileData = DefaultDataT> = Omit<
* automatically aborted.
*/
signal?: AbortSignal;

/**
* Caps concurrent HTTP requests for this layer's source fetches.
*
* Defaults to a maximum of 6 concurrent requests per origin, which aligns
* with browser limits of 6 HTTP/1.1 requests per origin. If your sources
* support HTTP/2 or HTTP/3, you may want to increase this limit or disable
* it entirely by passing `null`.
*
* Ignored when `geotiff` is a pre-opened `GeoTIFF` instance — wire the
* limiter via {@link GeoTIFF.fromUrl} at construction time instead.
*/
concurrencyLimiter?: ConcurrencyLimiter | null;
};

/**
Expand All @@ -150,17 +169,27 @@ export class COGLayer<
static override defaultProps = {
...RasterTileLayer.defaultProps,
epsgResolver,
concurrencyLimiter: DEFAULT_CONCURRENCY_LIMITER,
} as typeof RasterTileLayer.defaultProps;

declare state: {
geotiff?: GeoTIFF;
tilesetDescriptor?: RasterTilesetDescriptor;
defaultGetTileData?: COGLayerProps<TextureDataT>["getTileData"];
defaultRenderTile?: COGLayerProps<TextureDataT>["renderTile"];
/** Aborts the in-flight header read when the `geotiff` prop changes or the
* layer is removed
*/
abortController?: AbortController;
Comment thread
kylebarron marked this conversation as resolved.
};

override initializeState(): void {
this.setState({});
this.setState({ abortController: new AbortController() });
}

override finalizeState(context: LayerContext): void {
this.state.abortController?.abort();
super.finalizeState(context);
}

override updateState(params: UpdateParameters<this>) {
Expand Down Expand Up @@ -189,13 +218,31 @@ export class COGLayer<
}

async _parseGeoTIFF(): Promise<void> {
const geotiff = await fetchGeoTIFF(this.props.geotiff);
const signal = this.state.abortController?.signal;

let geotiff: GeoTIFF;
try {
geotiff = await fetchGeoTIFF(this.props.geotiff, {
concurrencyLimiter: this.props.concurrencyLimiter,
signal,
});
} catch (err) {
// Layer removed mid-open (finalizeState aborted the signal); drop it.
if (signal?.aborted) {
return;
}
throw err;
}
const crs = geotiff.crs;
const sourceProjection =
typeof crs === "number"
? await this.props.epsgResolver!(crs)
: parseWkt(crs);

if (signal?.aborted) {
return;
}

// @ts-expect-error - proj4 typings are incomplete and don't support
// wkt-parser input
const converter4326 = proj4(sourceProjection, "EPSG:4326");
Expand Down
12 changes: 12 additions & 0 deletions packages/deck.gl-geotiff/src/default-concurrency-limiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PerOriginSemaphore } from "@developmentseed/geotiff";

/**
* Shared default concurrency limiter for every COGLayer / MultiCOGLayer that
* doesn't override its `concurrencyLimiter` prop. A single module-level
* `PerOriginSemaphore({ maxRequests: 6 })` so two layers fetching from the
* same origin (e.g. the same S3 bucket) share *one* HTTP/1.1 connection
* pool. The cap matches Chrome's default per-origin HTTP/1.1 limit.
*/
export const DEFAULT_CONCURRENCY_LIMITER = new PerOriginSemaphore({
maxRequests: 6,
});
13 changes: 11 additions & 2 deletions packages/deck.gl-geotiff/src/geotiff/geotiff.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Utilities for interacting with a GeoTIFF

import type { RasterArray } from "@developmentseed/geotiff";
import type {
ConcurrencyLimiter,
Priority,
RasterArray,
} from "@developmentseed/geotiff";
import { GeoTIFF } from "@developmentseed/geotiff";
import type { Converter } from "proj4";

Expand Down Expand Up @@ -54,9 +58,14 @@ export function addAlphaChannel(rgbImage: RasterArray): RasterArray {

export async function fetchGeoTIFF(
input: GeoTIFF | string | URL | ArrayBuffer,
options: {
concurrencyLimiter?: ConcurrencyLimiter | null;
getPriority?: () => Priority;
signal?: AbortSignal;
} = {},
): Promise<GeoTIFF> {
if (typeof input === "string" || input instanceof URL) {
return await GeoTIFF.fromUrl(input);
return await GeoTIFF.fromUrl(input, options);
}

if (input instanceof ArrayBuffer) {
Expand Down
1 change: 1 addition & 0 deletions packages/deck.gl-geotiff/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type {
GetTileDataOptions,
} from "./cog-layer.js";
export { COGLayer } from "./cog-layer.js";
export { DEFAULT_CONCURRENCY_LIMITER } from "./default-concurrency-limiter.js";
export { addAlphaChannel } from "./geotiff/geotiff.js";
export * as texture from "./geotiff/texture.js";
export type { MosaicLayerProps } from "./mosaic-layer/mosaic-layer.js";
Expand Down
Loading