/**
* BeNomad BeMap JavaScript API — RecoverablePromiseCache (v2.0)
*
* A drop-in replacement for pmtiles' `SharedPromiseCache` that FIXES the
* permanent-hole bug: the stock cache stores the header/directory *promise* and,
* when that promise REJECTS (a timeout / network blip / aborted read), leaves the
* rejected promise cached forever. Every dependent tile then awaits that cached
* rejection → a blank region until a full page reload. (`invalidate()` only runs
* on an `EtagMismatch`, never on a timeout/error.)
*
* This wrapper evicts a cache entry as soon as its promise rejects, so the NEXT
* read re-fetches instead of replaying the failure — self-healing without a
* reload. It is **transparent**: success, in-flight de-duplication, LRU pruning
* and `invalidate()` are all inherited unchanged (it delegates to a real
* `SharedPromiseCache`); the ONLY behavioural change is on a *failed* read.
*
* Composition (not subclassing) is deliberate: the bundled `pmtiles` is an ES6
* UMD build whose `SharedPromiseCache` is an ES6 `class`, which cannot be
* `Function.apply`-ed for the ES5 subclass pattern this codebase uses. Composing
* a Cache-shaped object that delegates to an internal `SharedPromiseCache` (and
* deletes its internal entry on reject) is the portable, spec-matching form.
*
* Wire it via `new pmtiles.PMTiles(archiveUrl, new bemap.RecoverablePromiseCache())`
* + `protocol.add(...)` (see `bemap.MapLibreMap` — gated on `opts.recoverableCache`,
* default on). `bemap.MapLibreMap` does this automatically for the tiles archive.
*
* @since 2.0.0
* @public
* @constructor
* @param {Number} [maxCacheEntries] Forwarded to `SharedPromiseCache` (default
* its own default, currently 100). Optional — omit for stock sizing.
* @param {Boolean} [prefetch=true] Forwarded to `SharedPromiseCache`.
*/
bemap.RecoverablePromiseCache = function (maxCacheEntries, prefetch) {
if (typeof pmtiles === 'undefined' || typeof pmtiles.SharedPromiseCache !== 'function') {
throw new Error('bemap.RecoverablePromiseCache: pmtiles.SharedPromiseCache is unavailable — load the pmtiles library before constructing this cache.');
}
// Delegate to a real SharedPromiseCache (forward sizing knobs when given).
this._inner = (maxCacheEntries != null)
? new pmtiles.SharedPromiseCache(maxCacheEntries, prefetch !== false)
: new pmtiles.SharedPromiseCache();
};
/**
* Page-level default for whether `bemap.MapLibreMap` wires this recoverable
* cache into the pmtiles archive. `true` (self-healing) by default;
* `bemap.MapLibreMap`'s `opts.recoverableCache: false` sets it to `false`, in
* which case pmtiles uses its stock (poisoning) `SharedPromiseCache`. Mirrors
* `bemap.RangeFetchPolicy.defaultMode`.
* @public
* @since 2.0.0
* @type {Boolean}
*/
bemap.RecoverablePromiseCache.enabled = true;
/**
* Attach a one-shot eviction to `promise`: when it REJECTS, delete `key` from
* the inner cache's Map — but ONLY if the entry currently at `key` is still the
* SAME entry we observed (a later success or an LRU prune may have replaced it).
* That identity guard prevents evicting a good entry that happens to share a key.
* Never throws; a miss (unknown key / changed pmtiles internals) is a safe no-op
* that simply leaves stock behaviour in place.
* @private
*/
function _evictOnReject(inner, key, promise) {
if (!inner || !inner.cache || typeof inner.cache.get !== 'function' ||
key == null || !promise || typeof promise.then !== 'function') {
return promise;
}
var entry = inner.cache.get(key); // the `{ lastUsed, data }` record just set (or a cache hit)
if (entry) {
promise.then(null, function () {
if (inner.cache.get(key) === entry) inner.cache.delete(key);
});
}
return promise;
}
/**
* @public
* @param {Object} source pmtiles `Source` (has `getKey()` + `getBytes()`).
* @return {Promise} the header promise; evicted from cache if it rejects.
*/
bemap.RecoverablePromiseCache.prototype.getHeader = function (source) {
var inner = this._inner;
var p = inner.getHeader(source);
// Header cache key is exactly `source.getKey()`.
var key = (source && typeof source.getKey === 'function') ? source.getKey() : null;
return _evictOnReject(inner, key, p);
};
/**
* @public
* @return {Promise} the directory promise; evicted from cache if it rejects.
*/
bemap.RecoverablePromiseCache.prototype.getDirectory = function (source, offset, length, header) {
var inner = this._inner;
var p = inner.getDirectory(source, offset, length, header);
// Directory cache key format (pmtiles): `<sourceKey>|<etag>|<offset>|<length>`.
// If pmtiles ever changes this the key simply won't match → no-op (safe).
var key = ((source && typeof source.getKey === 'function') ? source.getKey() : '') +
'|' + ((header && header.etag) || '') + '|' + offset + '|' + length;
return _evictOnReject(inner, key, p);
};
/** Delegate: invalidate an entry on `EtagMismatch` (inherited behaviour). @public */
bemap.RecoverablePromiseCache.prototype.invalidate = function () {
return this._inner.invalidate.apply(this._inner, arguments);
};
/** Delegate: LRU prune (inherited behaviour). @public */
bemap.RecoverablePromiseCache.prototype.prune = function () {
if (typeof this._inner.prune === 'function') return this._inner.prune.apply(this._inner, arguments);
};