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..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 @@ -84,6 +84,24 @@ export class MosaicTileset2D extends Tileset2D { this.getItems = getItems; } + /** + * 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; + 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", () => { 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;