Repository navigation
Fix tile cancellation #602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,14 +34,25 @@ export class TileCache<D> extends EventTarget { | |
| protected mapIdsByTileUrl: Map<string, Set<string>> = new Map() | ||
| protected tileUrlsByMapId: Map<string, Set<string>> = new Map() | ||
|
|
||
| protected tilesFetchingCount = 0 | ||
| protected tileRemoveQueue: { | ||
| tileUrl: string | ||
| mapId: string | ||
| }[] = [] | ||
|
|
||
| protected fetchableTiles: FetchableTile[] = [] | ||
|
|
||
| /** | ||
| * The tiles in flight. Deleting returns true only for the first caller, so a | ||
| * tile is counted out exactly once no matter how it ends: fetched, failed, or | ||
| * removed while still fetching. | ||
| */ | ||
| #tilesFetching: Set<string> = new Set() | ||
|
|
||
| #waitingForTiles: Set<{ | ||
| resolve: () => void | ||
| reject: (error: Error) => void | ||
| }> = new Set() | ||
|
|
||
| #boundTileFetched = this.tileFetched.bind(this) | ||
| #boundTileFetchError = this.tileFetchError.bind(this) | ||
| #boundTilesFromSpriteTile = this.tilesFromSpriteTile.bind(this) | ||
|
|
@@ -211,26 +222,16 @@ export class TileCache<D> extends EventTarget { | |
|
|
||
| /** | ||
| * Returns a promise that resolves when all requested tiles are loaded. | ||
| * This could happen immidiately, in case there are no ongoing requests and the tilesFetchingCount is zero, | ||
| * or in a while, when the count reaches zero and the ALLREQUESTEDTILESLOADED event is fired. | ||
| * This could happen immidiately, in case there are no ongoing requests, | ||
| * or in a while, when the last one finishes and ALLREQUESTEDTILESLOADED is fired. | ||
| */ | ||
| async allRequestedTilesLoaded(): Promise<void> { | ||
| return new Promise((resolve) => { | ||
| if (this.finished) { | ||
| resolve() | ||
| } else { | ||
| const listener = () => { | ||
| this.removeEventListener( | ||
| WarpedMapEventType.ALLREQUESTEDTILESLOADED, | ||
| listener | ||
| ) | ||
| resolve() | ||
| } | ||
| this.addEventListener( | ||
| WarpedMapEventType.ALLREQUESTEDTILESLOADED, | ||
| listener | ||
| ) | ||
| } | ||
| if (this.finished) { | ||
| return | ||
| } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| this.#waitingForTiles.add({ resolve, reject }) | ||
| }) | ||
| } | ||
|
|
||
|
|
@@ -268,7 +269,11 @@ export class TileCache<D> extends EventTarget { | |
| this.tilesByTileUrl = new Map() | ||
| this.mapIdsByTileUrl = new Map() | ||
| this.tileUrlsByMapId = new Map() | ||
| this.tilesFetchingCount = 0 | ||
| this.#tilesFetching = new Set() | ||
|
|
||
| this.#stopWaitingForTiles( | ||
| new Error('Tile cache was cleared while waiting for tiles') | ||
| ) | ||
| } | ||
|
|
||
| destroy() { | ||
|
|
@@ -310,7 +315,7 @@ export class TileCache<D> extends EventTarget { | |
| // This is an async function that we are not awaiting to continue | ||
| // The results are handled inside the tile using events | ||
| cacheableTile.fetch() | ||
| this.updateTilesFetchingCount(1) | ||
| this.startFetching(cacheableTile.fetchableTile.tileUrl) | ||
| } | ||
|
|
||
| // Directly add cached tiles created from sprites | ||
|
|
@@ -335,6 +340,14 @@ export class TileCache<D> extends EventTarget { | |
| } | ||
|
|
||
| protected delayedRemoveCacheableTileForMapId(tileUrl: string, mapId: string) { | ||
| const cacheableTile = this.tilesByTileUrl.get(tileUrl) | ||
|
|
||
| // No pixels yet to keep, so queueing only pays for the rest of a download. | ||
| if (cacheableTile && !cacheableTile.isCachedTile()) { | ||
| this.removeCacheableTileForMapId(tileUrl, mapId) | ||
| return | ||
| } | ||
|
|
||
|
Comment on lines
342
to
+350
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -nP -C8 '\.prune\s*\(' --type=ts packages/render/src
rg -nP -C6 'pruneInfoByMapId|MapPruneInfo' --type=ts packages/render/srcRepository: allmaps/allmaps Length of output: 9536 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- TileCache structure ---'
ast-grep outline packages/render/src/tilecache/TileCache.ts
printf '%s\n' '--- TileCache prune and removal logic ---'
sed -n '220,390p' packages/render/src/tilecache/TileCache.ts
printf '%s\n' '--- CacheableTile lifecycle and pruning ---'
ast-grep outline packages/render/src/tilecache/CacheableTile.ts
sed -n '1,220p' packages/render/src/tilecache/CacheableTile.ts
printf '%s\n' '--- pruneTileCache callers and renderer timing ---'
rg -n -P -C10 'pruneTileCache|requestAnimationFrame|setTimeout|setInterval|render\(' packages/render/src/renderers packages/render/src | head -n 500Repository: allmaps/allmaps Length of output: 40128 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- remaining TileCache removal and fetch handlers ---'
sed -n '380,620p' packages/render/src/tilecache/TileCache.ts
printf '%s\n' '--- BaseRenderer request and render flow ---'
sed -n '650,745p' packages/render/src/renderers/BaseRenderer.ts
sed -n '1080,1180p' packages/render/src/renderers/BaseRenderer.ts
printf '%s\n' '--- BaseRenderer render call sites and event handlers ---'
rg -n -P -C12 'render\(|changed\(|TILEFETCHED|TILEFETCHERROR|MAPTILELOADED' packages/render/src/renderers/BaseRenderer.ts packages/render/src/renderers/WebGL2Renderer.ts packages/render/src/renderers/CanvasRenderer.ts
printf '%s\n' '--- TileCache tests and queue assertions ---'
rg -n -P -C8 'tileRemoveQueue|delayedRemove|prune\(|abort|requestFetchableTiles' packages/render/test packages/render/src --glob '*.{ts,tsx}' 2>/dev/null | head -n 600Repository: allmaps/allmaps Length of output: 50371 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- BaseRenderer render preparation and scheduling ---'
ast-grep outline packages/render/src/renderers/BaseRenderer.ts
sed -n '250,430p' packages/render/src/renderers/BaseRenderer.ts
sed -n '430,650p' packages/render/src/renderers/BaseRenderer.ts
sed -n '1180,1345p' packages/render/src/renderers/BaseRenderer.ts
printf '%s\n' '--- TileCache request implementation ---'
sed -n '1,230p' packages/render/src/tilecache/TileCache.ts
printf '%s\n' '--- focused tile-cache tests ---'
sed -n '1,180p' packages/render/test/tile-cache.test.ts
printf '%s\n' '--- focused renderer scheduling symbols ---'
rg -n 'prepareRenderInternal|thrott|requestAnimationFrame|setTimeout|render\(' packages/render/src/renderers/BaseRenderer.ts packages/render/src/renderers/WebGL2Renderer.ts packages/render/src/renderers/CanvasRenderer.tsRepository: allmaps/allmaps Length of output: 31016 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- WebGL2Renderer throttle configuration and preparation ---'
sed -n '200,380p' packages/render/src/renderers/WebGL2Renderer.ts
sed -n '440,535p' packages/render/src/renderers/WebGL2Renderer.ts
printf '%s\n' '--- renderer option defaults and buffer relationships ---'
rg -n -P -C8 'requestViewportBufferRatio|pruneViewportBufferRatio|overviewRequestViewportBufferRatio|overviewPruneViewportBufferRatio|anticipateInteraction' packages/render/src packages/render/test --glob '*.{ts,tsx}'
printf '%s\n' '--- all TileCache prune tests and recent diff metadata ---'
rg -n -P -C12 'prune\(|pruned while|delayedRemove|tileRemoveQueue|in.flight|in-flight' packages/render/test --glob '*.{ts,tsx}'
git diff --statRepository: allmaps/allmaps Length of output: 26645 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
base = Path('packages/render/src/renderers/BaseRenderer.ts').read_text()
webgl = Path('packages/render/src/renderers/WebGL2Renderer.ts').read_text()
cache = Path('packages/render/src/tilecache/TileCache.ts').read_text()
def required(text, pattern, label):
match = re.search(pattern, text, re.S)
if not match:
raise SystemExit(f'missing {label}')
return match
defaults = required(
base,
r'anticipateInteraction:\s*false.*?'
r'requestViewportBufferRatio:\s*([0-9.]+).*?'
r'overviewRequestViewportBufferRatio:\s*([0-9.]+).*?'
r'pruneViewportBufferRatio:\s*([0-9.]+).*?'
r'overviewPruneViewportBufferRatio:\s*([0-9.]+)',
'base buffer defaults',
)
print('BaseRenderer defaults:', defaults.groups())
webgl_default = required(
webgl,
r'defaultSpecificWebGL2RenderOptions\s*=\s*\{.*?'
r'anticipateInteraction:\s*(true|false)',
'WebGL anticipateInteraction default',
)
print('WebGL2Renderer anticipateInteraction:', webgl_default.group(1))
prepare = required(
webgl,
r'`#throttledPrepareRenderInternal`\s*=\s*throttle\(\s*'
r'this\.\`#prepareRenderInternal`\.bind\(this\),\s*'
r'([A-Z0-9_]+)',
'prepare throttle',
)
print('WebGL2Renderer prepare throttle constant:', prepare.group(1))
request_pos = base.index('this.tileCache.requestFetchableTiles(allRequestedTilesForViewport)')
prune_pos = base.index('this.pruneTileCache(mapsInViewportForOverviewPrune)')
print('request-before-prune:', request_pos < prune_pos)
immediate_branch = required(
cache,
r'if \(cacheableTile && !cacheableTile\.isCachedTile\(\)\) \{\s*'
r'this\.removeCacheableTileForMapId\(tileUrl, mapId\)',
'immediate in-flight removal',
)
removal = required(
cache,
r'if \(!mapIds\.size\) \{\s*'
r'if \(this\.stopFetching\(tileUrl\)\) \{\s*'
r'cacheableTile\.abort\(\)',
'abort on final map removal',
)
print('immediate branch and abort-on-last-map:', bool(immediate_branch and removal))
print('Conclusion: current requested tiles are associated before prune; an in-flight tile that prune removes is aborted and absent for a later request.')
PYRepository: allmaps/allmaps Length of output: 379 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact throttle values and removal branch ---'
rg -n -C3 'THROTTLE_PREPARE_RENDER_WAIT_MS|THROTTLE_PREPARE_RENDER_OPTIONS|THROTTLE_CHANGED_WAIT_MS' packages/render/src/renderers/WebGL2Renderer.ts
sed -n '398,425p' packages/render/src/tilecache/TileCache.ts
printf '%s\n' '--- minimal read-only invariant check ---'
python3 - <<'PY'
from pathlib import Path
cache = Path('packages/render/src/tilecache/TileCache.ts').read_text()
base = Path('packages/render/src/renderers/BaseRenderer.ts').read_text()
checks = {
'request precedes prune': base.index(
'this.tileCache.requestFetchableTiles(allRequestedTilesForViewport)'
) < base.index(
'this.pruneTileCache(mapsInViewportForOverviewPrune)'
),
'in-flight branch removes immediately': (
'if (cacheableTile && !cacheableTile.isCachedTile())' in cache
and 'this.removeCacheableTileForMapId(tileUrl, mapId)' in cache
),
'last-map removal aborts': (
'if (!mapIds.size)' in cache
and 'if (this.stopFetching(tileUrl))' in cache
and 'cacheableTile.abort()' in cache
),
}
for name, result in checks.items():
print(f'{name}: {result}')
if not all(checks.values()):
raise SystemExit(1)
PYRepository: allmaps/allmaps Length of output: 1780 Retain in-flight tiles until completion or failure.
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving this as is. Prune runs with So a re-fetch needs a large pan out past the 16x ring and straight back, which is the trade the comment describes. |
||
| if ( | ||
| this.tileRemoveQueue.some( | ||
| (tile) => tile.tileUrl === tileUrl && tile.mapId === mapId | ||
|
|
@@ -375,15 +388,14 @@ export class TileCache<D> extends EventTarget { | |
| const mapIds = this.removeMapIdForTileurl(/sitelet?url=https%3A%2F%2Fgithub.com%2Fallmaps%2Fallmaps%2Fpull%2F602%2FmapId%2C%2520tileUrl) | ||
| this.removeTileUrlForMapId(tileUrl, mapId) | ||
|
|
||
| // If there are no other maps for this tile and it's still fetching, | ||
| // abort the fetch and delete the tile from the cache. | ||
| // No other map wants this tile, so it leaves the cache. | ||
| if (!mapIds.size) { | ||
| if (!cacheableTile.isCachedTile()) { | ||
| // Cancel fetch if tile is still being fetched | ||
| // Still fetching means the download is live, so stop it. | ||
| if (this.stopFetching(tileUrl)) { | ||
| cacheableTile.abort() | ||
| this.updateTilesFetchingCount(-1) | ||
| } | ||
|
|
||
| this.removeEventListenersFromCacheableTile(cacheableTile) | ||
| this.tilesByTileUrl.delete(tileUrl) | ||
| } | ||
|
|
||
|
|
@@ -402,7 +414,7 @@ export class TileCache<D> extends EventTarget { | |
| } | ||
| const { tileUrl } = event.data | ||
|
|
||
| this.updateTilesFetchingCount(-1) | ||
| this.stopFetching(tileUrl) | ||
|
|
||
| for (const mapId of this.mapIdsByTileUrl.get(tileUrl) || []) { | ||
| this.dispatchEvent( | ||
|
|
@@ -439,11 +451,7 @@ export class TileCache<D> extends EventTarget { | |
| } | ||
| const { tileUrl } = event.data | ||
|
|
||
| // A failed fetch must decrement the in-flight count just like a successful | ||
| // one (see tileFetched), otherwise tilesFetchingCount never reaches zero. | ||
| if (this.tilesByTileUrl.has(tileUrl)) { | ||
| this.updateTilesFetchingCount(-1) | ||
| } | ||
| this.stopFetching(tileUrl) | ||
|
|
||
| const mapIds = [ | ||
| ...new Set([ | ||
|
|
@@ -577,23 +585,47 @@ export class TileCache<D> extends EventTarget { | |
| } | ||
|
|
||
| get finished() { | ||
| return this.tilesFetchingCount === 0 | ||
| return this.#tilesFetching.size === 0 | ||
| } | ||
|
|
||
| protected updateTilesFetchingCount(delta: number) { | ||
| const previousTilesFetchingCount = this.tilesFetchingCount | ||
| this.tilesFetchingCount += delta | ||
| protected startFetching(tileUrl: string) { | ||
| const previousCount = this.#tilesFetching.size | ||
| this.#tilesFetching.add(tileUrl) | ||
|
|
||
| if (previousTilesFetchingCount === 0 && this.tilesFetchingCount > 0) { | ||
| if (previousCount === 0 && this.#tilesFetching.size > 0) { | ||
| this.dispatchEvent( | ||
| new WarpedMapEvent(WarpedMapEventType.REQUESTEDTILESLOADING) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** False if it had already stopped, so each tile is counted out once. */ | ||
| protected stopFetching(tileUrl: string) { | ||
| if (!this.#tilesFetching.delete(tileUrl)) { | ||
| return false | ||
| } | ||
|
|
||
| if (this.tilesFetchingCount === 0) { | ||
| if (this.#tilesFetching.size === 0) { | ||
| this.dispatchEvent( | ||
| new WarpedMapEvent(WarpedMapEventType.ALLREQUESTEDTILESLOADED) | ||
| ) | ||
| this.#stopWaitingForTiles() | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| /** With an error, so a cleared cache never reads as loaded. */ | ||
| #stopWaitingForTiles(error?: Error) { | ||
| const waiting = this.#waitingForTiles | ||
| this.#waitingForTiles = new Set() | ||
|
|
||
| for (const { resolve, reject } of waiting) { | ||
| if (error) { | ||
| reject(error) | ||
| } else { | ||
| resolve() | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,42 +4,56 @@ import { fetchUrl } from '@allmaps/stdlib' | |
|
|
||
| import type { FetchFn } from '@allmaps/types' | ||
|
|
||
| const fetchAndGetImageDataWorker = { | ||
| export const abortControllers = new Map<string, AbortController>() | ||
|
|
||
| export const fetchAndGetImageDataWorker = { | ||
| async getImageData( | ||
| tileUrl: string, | ||
| onAbort: () => void, // Define as a no-arguments function | ||
| fetchFn: FetchFn | undefined, | ||
| width: number, | ||
| height: number | ||
| ): Promise<ImageData> { | ||
| const workerAbortController = new AbortController() | ||
| const abortController = new AbortController() | ||
| const { signal } = abortController | ||
| abortControllers.set(tileUrl, abortController) | ||
|
Comment on lines
+16
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find all callers of getImageData and the tile lifecycle that can re-request a URL.
rg -nP -C6 '\bgetImageData\s*\(' --type=ts packages/render/src
# Check whether a tile URL can be re-added while a previous fetch is unsettled.
rg -nP -C8 'addCacheableTile|requestFetchableTile\b|tilesByTileUrl\.(set|delete)' packages/render/src/tilecache/TileCache.tsRepository: allmaps/allmaps Length of output: 9392 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- fetch-and-get-image-data.ts ---'
cat -n packages/render/src/workers/fetch-and-get-image-data.ts
printf '%s\n' '--- CacheableWorkerImageDataTile.ts ---'
cat -n packages/render/src/tilecache/CacheableWorkerImageDataTile.ts
printf '%s\n' '--- worker pool and worker API references ---'
rg -n -C5 'WorkerPool|fetchAndGetImageDataWorker|abortControllers|\.abort\(' packages/render/src/workers packages/render/src/tilecache --type=ts
printf '%s\n' '--- TileCache lifecycle around removal and cancellation ---'
sed -n '240,430p' packages/render/src/tilecache/TileCache.tsRepository: allmaps/allmaps Length of output: 29860 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- PoolWorkers.ts ---'
cat -n packages/render/src/workers/PoolWorkers.ts
printf '%s\n' '--- WorkerPool construction and cache wiring ---'
rg -n -C8 'new WorkerPool|createFactory\(|CacheableWorkerImageDataTile' packages/render/src --type=ts
printf '%s\n' '--- all getImageData and worker abort call sites ---'
rg -n -C4 '\.getImageData\(|\.abort\(this\.fetchableTile\.tileUrl\)|fetchAndGetImageDataWorker' packages --type=ts
printf '%s\n' '--- package tests for worker/tile concurrency ---'
rg -n -C5 'CacheableWorkerImageDataTile|PoolWorkers|getImageData|abortControllers' packages/render --glob '*.{test,spec}.{ts,tsx,js}'Repository: allmaps/allmaps Length of output: 50371 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- relevant worker-pool configuration ---'
rg -n -C3 'POOL_SIZE|new WorkerPool|`#workerPool`|workerPool' packages/render/src/renderers/WebGL2Renderer.ts packages/render/src --type=ts | head -120
printf '%s\n' '--- direct concurrency test ---'
sed -n '335,375p' packages/render/test/fetch-and-get-image-data.test.ts
printf '%s\n' '--- concise cache/renderer construction references ---'
rg -n 'new TileCache|new WebGL2Renderer|CacheableWorkerImageDataTile.createFactory|new WorkerPool' packages/render/src packages/render/test --type=ts
printf '%s\n' '--- deterministic WorkerPool simulation ---'
python3 - <<'PY'
from pathlib import Path
import re
source = Path('packages/render/src/workers/PoolWorkers.ts').read_text()
size = 4
outstanding = [0] * size
assignments = []
for request in range(size + 1):
min_index = min(range(len(outstanding)), key=lambda i: outstanding[i])
outstanding[min_index] += 1
assignments.append(min_index)
print('acquire assignments for five overlapping requests with pool size four:', assignments)
print('same-worker overlap:', len(set(assignments)) < len(assignments))
print('pool acquire increments outstanding:', bool(re.search(r'`#outstanding`\\[minIndex\\]\\+\\+', source)))
print('pool release occurs only in caller finally:', 'release(index)' in Path('packages/render/src/tilecache/CacheableWorkerImageDataTile.ts').read_text())
PYRepository: allmaps/allmaps Length of output: 11435 Use request-scoped cancellation for overlapping tile requests.
Key the registry by a request ID and pass that ID to 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct in principle, leaving it. Two live The compare before delete in the |
||
|
|
||
| // Connect the abort signal with a listener | ||
| onAbort() | ||
| try { | ||
| const response = await fetchurl(/sitelet?url=https%3A%2F%2Fgithub.com%2Fallmaps%2Fallmaps%2Fpull%2F602%2FtileUrl%2C%2520%257B%2520signal%2520%257D%2C%2520fetchFn%253C%2Fspan%253E) | ||
|
|
||
| const response = await fetchUrl( | ||
| tileUrl, | ||
| { | ||
| signal: workerAbortController.signal | ||
| }, | ||
| fetchFn | ||
| ) | ||
| const blob = await response.blob() | ||
| signal.throwIfAborted() | ||
|
|
||
| const blob = await response.blob() | ||
| const imageBitmap = await createImageBitmap(blob, 0, 0, width, height) | ||
|
|
||
| const imageBitmap = await createImageBitmap(blob, 0, 0, width, height) | ||
| try { | ||
| signal.throwIfAborted() | ||
|
|
||
| const canvas = new OffscreenCanvas(width, height) | ||
| const context = canvas.getContext('2d') | ||
| const canvas = new OffscreenCanvas(width, height) | ||
| const context = canvas.getContext('2d') | ||
|
|
||
| if (!context) { | ||
| throw new Error('Could not create OffscreenCanvas context') | ||
| } | ||
| if (!context) { | ||
| throw new Error('Could not create OffscreenCanvas context') | ||
| } | ||
|
|
||
| context.drawImage(imageBitmap, 0, 0) | ||
| const imageData = context.getImageData(0, 0, width, height) | ||
| context.drawImage(imageBitmap, 0, 0) | ||
| const imageData = context.getImageData(0, 0, width, height) | ||
|
|
||
| return transfer(imageData, [imageData.data.buffer]) | ||
| } finally { | ||
| imageBitmap.close() | ||
| } | ||
| } finally { | ||
| // A later fetch for this url may own the entry now; deleting it then | ||
| // would leave that fetch unabortable. | ||
| if (abortControllers.get(tileUrl) === abortController) { | ||
| abortControllers.delete(tileUrl) | ||
| } | ||
| } | ||
| }, | ||
|
|
||
| return transfer(imageData, [imageData.data.buffer]) | ||
| /** Runs while getImageData is still awaiting: the worker is idle on I/O. */ | ||
| abort(tileUrl: string): void { | ||
| abortControllers.get(tileUrl)?.abort() | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
clear()resets the set without emittingALLREQUESTEDTILESLOADED, so pending waiters never resolve.allRequestedTilesLoaded()at lines 223-241 resolves either immediately or on theALLREQUESTEDTILESLOADEDevent. If a caller awaits that promise andclear()then runs,finishedbecomestruebut no event is dispatched. The awaited promise stays pending forever.Emit the completion event from
clear()when the set was not empty, or resolve waiters explicitly.🐛 Proposed fix
The test "clear announces nothing" in
packages/render/test/tile-cache.test.tslines 131-141 asserts the current behavior, so confirm which contract you want before changing it.📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, but by rejecting rather than emitting.
Dispatching
ALLREQUESTEDTILESLOADEDwould tell every listener the tiles are ready when they were discarded, and leaflet, openlayers and warpedmaplayer all forward that event.allRequestedTilesLoaded()now keeps its own settlers andclear()rejects them, so a waiter learns the difference between loaded and thrown away.clear announces nothingstill holds, andwaiting for all tiles rejects when the cache is clearedcovers the rest.