Skip to content

fix(deck.gl-geotiff)!: stop passing renderSource an abort signal - #821

Merged
kylebarron merged 2 commits into
mainfrom
kyle/mosaic-render-source-signal
Oct 30, 2026
Merged

kylebarron merged 2 commits into
mainfrom
kyle/mosaic-render-source-signal

Conversation

@kylebarron

Copy link
Copy Markdown
Member

Note

This PR was written by Claude (Claude Code), not by @kylebarron.

MosaicLayer passed renderSource the signal of the item's open. Both its docs example and the naip-mosaic example passed it on to each COGLayer's signal. That signal can't do what passing it suggests, and it can leave an item blank:

  • It can't stop a layer's tile loads. deck.gl doesn't abort a tile once it has loaded, and renderSource only draws items whose open has finished.
  • It can arrive already aborted. Say an item's open is aborted while more than maxRequests opens are in flight, and its getSource finishes anyway. Then deck.gl keeps the source, together with the aborted signal.
    • naip-mosaic's shared per-URL cache allows that: a later load's abort doesn't reject the promise that an earlier load started.
    • When the item is drawn again, its COGLayer gets the aborted signal, and every tile load aborts at once. The item stays blank for as long as its mosaic tile stays cached.

renderSource now gets only { source }. A COGLayer already aborts its loads that leave the view (#809). It aborts the rest when MosaicLayer evicts its item and the layer is finalized (#728).

Migration

Drop signal from renderSource's options. 0.10 already changes them from { data } to { source }.

// Before
renderSource: (item, { source, signal }) =>
  new COGLayer({ id: item.url, source, signal }),

// After
renderSource: (item, { source }) => new COGLayer({ id: item.url, source }),

Tests

  • The bug, in deck.gl's LayerManager. The test reproduces it step by step:

    1. With maxRequests: 1, item A's open is aborted when A leaves the view while F's starts.
    2. A's getSource finishes anyway.
    3. A comes back into view.

    Before the fix, renderSource got an aborted signal. Now it gets { source }.

  • A type test: renderSource's options are exactly { source?: SourceT }. It fails if signal is added back.

  • The workspace typecheck passes, naip-mosaic included.

  • In Chrome: naip-mosaic draws as before (924 NAIP items over Denver), with no new console messages.

🤖 Written by Claude Code

kylebarron and others added 2 commits October 29, 2026 13:47
MosaicLayer passed `renderSource` the signal of the item's open, and its
docs example and naip-mosaic passed it on to each COGLayer's `signal`.
That signal can't stop the layer's tile loads: deck.gl doesn't abort a
tile once it has loaded, and `renderSource` only draws items whose open
has finished. But it can arrive already aborted.

If an item's open is aborted while more than `maxRequests` opens are in
flight, and its `getSource` finishes anyway, as naip-mosaic's shared
per-URL cache allows, deck.gl keeps the source together with that
aborted signal. When the item is drawn again, every tile load of its
COGLayer aborts at once, and the item stays blank.

`renderSource` now gets only `{ source }`. A COGLayer already aborts its
loads that leave the view (#809), and the rest when MosaicLayer evicts
its item and the layer is finalized (#728).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getSource: async (item, opts) =>
getCachedGeoTIFF(item.assets.image.href, opts),
renderSource: (item, { source, signal }) => {
renderSource: (item, { source }) => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so we only supply signal in getSource? Because renderSource shouldn't be loading headers (that's getSource's job), and then the tile reads happen inside the downstream layer (i.e. COGLayer)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This reply was written by Claude (Claude Code), not by @kylebarron.

Yes, exactly.

  • getSource is the only async work MosaicLayer does for an item: opening its source, which reads the headers. So it's the only place a signal can cancel anything. deck.gl aborts it when it cancels the item's load: when the layer is removed, or when the item leaves view while more than maxRequests opens are in flight.
  • renderSource is synchronous. It only builds layers from the opened source and loads nothing itself.
  • The tile reads happen inside the layer it returns. COGLayer's inner TileLayer gives each tile load its own signal. That signal aborts when the tile leaves the view (feat(deck.gl-geotiff): coalesce COG tile reads by default #809) or the layer is removed (fix(deck.gl-raster): destroy tile textures when a raster layer is removed #728). COGLayer's own signal prop stays, for an app that wants to cancel all of a layer's tile loads itself.

🤖 Written by Claude Code

@kylebarron kylebarron left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅

@kylebarron
kylebarron merged commit 2f2dcc8 into main Oct 30, 2026
3 checks passed
@kylebarron
kylebarron deleted the kyle/mosaic-render-source-signal branch October 30, 2026 15:04
kylebarron added a commit that referenced this pull request Oct 30, 2026
> [!NOTE]
> This PR was written by Claude (Claude Code), not by @kylebarron.

Bumps `version` to `0.10.0-beta.1` in the root `package.json` and in
every `packages/*/package.json`, ready for the first 0.10 beta. The
publish workflow sees `beta` in the version and publishes to npm under
the `beta` dist-tag, so `latest` stays on 0.9.0.

`CHANGELOG.md` gets a `v0.10.0-beta.1` section listing the 19 PRs merged
since v0.9.0.

## Publish `@developmentseed/range-reader` by hand before tagging

`@developmentseed/range-reader` (#806) isn't on npm yet, and
`@developmentseed/geotiff` and `@developmentseed/deck.gl-geotiff` depend
on it. The publish workflow can't create it: it publishes only through
trusted publishing (OIDC), and npm can add a trusted publisher only to a
package that [already
exists](https://docs.npmjs.com/cli/v11/commands/npm-trust). Pushing the
tag first would fail at `range-reader`. `pnpm publish --recursive` stops
at the first failure, so packages published before it in the run would
be out, and the rest, including `geotiff` and `deck.gl-geotiff`, would
not.

Once this is merged, from an up-to-date `main`:

1. Publish it once, as a public package under the `beta` tag. Its
`prepublishOnly` script builds it first.
   ```sh
   cd packages/range-reader
   pnpm publish --tag beta --access public --no-git-checks
   ```
2. Give it the same trusted publisher as the other packages (workflow
`release.yml`, environment `release`), in its settings on npmjs.com or
with:
   ```sh
npm trust github @developmentseed/range-reader --file release.yml --repo
developmentseed/deck.gl-raster --env release --allow-publish
   ```
3. Tag the merge commit `v0.10.0-beta.1` and push the tag. The workflow
skips `range-reader`, since that version is already on npm, and
publishes the other ten packages.

## Breaking changes

- #799: `COGLayer` takes a single `source`: an opened `COGSource`, or
any input of `COGSource.open`. `geotiff` becomes `source`,
`onGeoTIFFLoad` becomes `onSourceLoad`, and `epsgResolver` and
`concurrencyLimiter` move to `COGSource.open`.
- #801: `MultiCOGLayer` does the same with `MultiCOGSource`. `sources: {
red: { url } }` becomes `source: { red: url }`, `MultiCOGSourceConfig`
is removed, and files that don't share a CRS and an extent are rejected.
- #804: `MosaicLayer`'s `sources` becomes deck.gl's `data`, and
`MosaicSource` becomes `MosaicItem`. The callbacks get the item's opened
source as `source`, and `onViewportLoad`'s entries are `{ item, source
}`.
- #821: `renderSource` gets only `{ source }`, without `signal`.
- #812: `ZarrLayer` takes a `ZarrSource`, or a zarrita group or array,
as `source`. `node`, `variable`, `metadata` and `epsgResolver` are
removed (`metadata` becomes `ZarrSource.open`'s `geoZarrMetadata`), and
the type parameters are now `ZarrLayer<DataT, Dtype, Store>`.
- #807: `GeoTIFF.fromUrl` no longer takes `chunkSize` or `cacheSize`.
They're `RangeReader` options now (`minMetadataReadBytes`,
`cache.metadataBytes`).

Two behavior changes aren't marked breaking:

- #809: COG tile reads are coalesced by default, and `COGLayer` and
`MultiCOGLayer` default to `maxRequests: 256` and `debounceTime: 50`.
- #818: changing `updateTriggers.getTileData`, or ZarrLayer's
`selection`, fetches tiles again, and the old tiles are removed at once.

🤖 Written by [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kylebarron added a commit that referenced this pull request Oct 30, 2026
… top of main

Re-applies this PR's change to main's MosaicLayer, which stage 3 and
#821 rewrote: the inner TileLayer is built from `getSubLayerProps()`, and
each layer `renderSource` returns inherits the props it didn't set
itself, such as `opacity`.

`updateTriggers.all`, which `getSubLayerProps()` passes, no longer reaches
the inner TileLayer, as in deck.gl's TerrainLayer: deck.gl's TileLayer
would reload through `Tileset2D.reloadAll`, which leaks sources (#816).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant