From a616ea8c42991aae72d2442cf84baa1edc1546f5 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 30 Oct 2026 11:18:10 -0400 Subject: [PATCH 1/2] fix(deck.gl-geotiff): call onSourceUnload for MosaicLayer's cached sources when it's removed deck.gl's `Tileset2D.finalize` clears its cache without `onTileUnload`, so removing a MosaicLayer never called `onSourceUnload` for the sources it still held. MosaicTileset2D now unloads every cached item on finalize, as RasterTileset2D does (#728). An item still opening is unloaded once its open finishes, since deck.gl keeps what an aborted open returns anyway. Part of #816. Co-Authored-By: Claude Opus 5.5 --- .../src/mosaic-layer/mosaic-tileset-2d.ts | 17 ++++++++ .../tests/mosaic-layer.test.ts | 39 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts b/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts index 043e40de..835281a1 100644 --- a/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts +++ b/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts @@ -84,6 +84,23 @@ export class MosaicTileset2D extends Tileset2D { this.getItems = getItems; } + /** + * Unloads every cached item, which deck.gl's `finalize` skips (see #728). An + * item still opening is unloaded once its open finishes. + */ + override finalize(): void { + const { onTileUnload } = this.opts; + for (const tile of this.tiles) { + if (tile.isLoading) { + // `data` is a promise until the load finishes + void Promise.resolve(tile.data).then(() => onTileUnload(tile)); + } else { + onTileUnload(tile); + } + } + super.finalize(); + } + /** The Tileset2D cache key for an item. */ override getTileId(tileIndex: TileIndex): string { // `getTileIndices` always returns `ResolvedItem`s, so an `id` is diff --git a/packages/deck.gl-geotiff/tests/mosaic-layer.test.ts b/packages/deck.gl-geotiff/tests/mosaic-layer.test.ts index 4b89fcd7..fa741137 100644 --- a/packages/deck.gl-geotiff/tests/mosaic-layer.test.ts +++ b/packages/deck.gl-geotiff/tests/mosaic-layer.test.ts @@ -248,7 +248,14 @@ function makeMosaicRenderer(viewport: WebMercatorViewport = VIEWPORT) { layerManager.activateViewport(next); }; - return { render, wait, moveTo }; + /** Remove the layer, as an app does by no longer rendering it. */ + const remove = async () => { + layerManager.setLayers([]); + await settle(layerManager); + return report(); + }; + + return { render, wait, moveTo, remove }; } // The tests above reuse one layer instance. Apps create a new MosaicLayer on @@ -455,6 +462,36 @@ describe("MosaicLayer sources", () => { expect(errors).toEqual([]); expect(unloaded).toEqual({ A: { of: "A" }, B: { of: "B" } }); }); + + it("passes every cached item's source to onSourceUnload when the layer is removed", async () => { + const { render, remove } = makeMosaicRenderer(); + await render({ data: [A, B] }); + + const { unloaded, errors } = await remove(); + expect(errors).toEqual([]); + expect(unloaded).toEqual({ A: { of: "A" }, B: { of: "B" } }); + }); + + it("passes the source of an item still opening at removal to onSourceUnload once it arrives", async () => { + const { render, remove, wait } = makeMosaicRenderer(); + let finish!: () => void; + await render({ + data: [A], + // Ignores the abort, as a source cache shared between loads can + getSource: (item) => + new Promise((resolve) => { + finish = () => resolve({ of: item.id }); + }), + }); + + // A copy: the report's records keep filling in + const atRemoval = { ...(await remove()).unloaded }; + finish(); + const { unloaded, errors } = await wait(); + expect(errors).toEqual([]); + expect(atRemoval).toEqual({}); + expect(unloaded).toEqual({ A: { of: "A" } }); + }); }); describe("MosaicLayer fetch priority", () => { From 53581a19b2d9f288b4fe6761b0404f414367d9ca Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Fri, 30 Oct 2026 11:24:22 -0400 Subject: [PATCH 2/2] docs: point both finalize overrides to visgl/deck.gl#10883 Co-Authored-By: Claude Opus 5.5 --- .../deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts | 5 +++-- .../deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts b/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts index 835281a1..948921cb 100644 --- a/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts +++ b/packages/deck.gl-geotiff/src/mosaic-layer/mosaic-tileset-2d.ts @@ -85,8 +85,9 @@ export class MosaicTileset2D extends Tileset2D { } /** - * Unloads every cached item, which deck.gl's `finalize` skips (see #728). An - * item still opening is unloaded once its open finishes. + * Unloads every cached item, which deck.gl's `finalize` skips until + * https://github.com/visgl/deck.gl/pull/10883 lands. An item still opening + * is unloaded once its open finishes. */ override finalize(): void { const { onTileUnload } = this.opts; diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts index aad3daa9..226fb908 100644 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts @@ -263,7 +263,8 @@ export class RasterTileset2D extends Tileset2D { * without calling `onTileUnload`, so GPU resources that `getTileData` made * for those tiles, like textures, would be freed only by garbage collection. * A tile still loading is unloaded once its load finishes, since deck.gl - * keeps the data that `getTileData` returns after an abort. + * keeps the data that `getTileData` returns after an abort. Not needed once + * https://github.com/visgl/deck.gl/pull/10883 lands. */ override finalize(): void { const { onTileUnload } = this.opts;