SOURCE / PINNED RELEASE
Made of little things.
Atlas map viewer
- Release
- 1d2807e80b44…
- Author-recorded commit
- 43810ed2b368…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-4757dbabc86
Archive hash verified: 8db6876d96c460ac…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/** Weighted LRU. A caller may pin its active working set; any overage is explicit.
* Weights are bytes for buffers, conservative estimates for JS objects. */
export class BudgetCache<K, V> {
private items = new Map<K, { value: V; bytes: number }>();
private protectedKeys = new Set<K>();
bytes = 0;
evictions = 0;
constructor(
readonly budget: number,
private weight: (value: V, key: K) => number,
private dispose?: (value: V, key: K) => void,
) {
if (!Number.isSafeInteger(budget) || budget < 0)
throw new Error("Invalid cache budget");
}
get size() {
return this.items.size;
}
get stats() {
return {
bytes: this.bytes,
budget: this.budget,
entries: this.size,
evictions: this.evictions,
overage: Math.max(0, this.bytes - this.budget),
};
}
has(key: K) {
return this.items.has(key);
}
get(key: K): V | undefined {
const entry = this.items.get(key);
if (!entry) return;
this.items.delete(key);
this.items.set(key, entry);
return entry.value;
}
set(key: K, value: V) {
const bytes = this.weight(value, key);
if (!Number.isSafeInteger(bytes) || bytes < 0)
throw new Error("Invalid cache weight");
this.delete(key);
if (bytes > this.budget && !this.protectedKeys.has(key)) {
this.dispose?.(value, key);
this.evictions++;
return this;
}
this.items.set(key, { value, bytes });
this.bytes += bytes;
this.trim();
return this;
}
delete(key: K) {
const entry = this.items.get(key);
if (!entry) return false;
this.items.delete(key);
this.bytes -= entry.bytes;
this.dispose?.(entry.value, key);
return true;
}
protect(keys: Iterable<K>) {
this.protectedKeys = new Set(keys);
this.trim();
}
private trim() {
for (const key of this.items.keys()) {
if (this.bytes <= this.budget) break;
if (!this.protectedKeys.has(key)) {
this.delete(key);
this.evictions++;
}
}
}
clear() {
for (const key of this.items.keys()) this.delete(key);
}
*entries(): IterableIterator<[K, V]> {
for (const [k, v] of this.items) yield [k, v.value];
}
*values(): IterableIterator<V> {
for (const v of this.items.values()) yield v.value;
}
keys() {
return this.items.keys();
}
[Symbol.iterator]() {
return this.entries();
}
}
export const DEFAULT_DECODED_BUDGETS = {
map: 32 * 1024 * 1024,
labels: 8 * 1024 * 1024,
routing: 16 * 1024 * 1024,
semantic: 16 * 1024 * 1024,
directory: 1024 * 1024,
};
export type CacheDomain = keyof typeof DEFAULT_DECODED_BUDGETS;
function domain(key: string): CacheDomain {
if (key.startsWith("tile/")) return "map";
if (key.startsWith("labels/")) return "labels";
if (key.startsWith("routing/")) return "routing";
if (key.startsWith("index/")) return "directory";
return "semantic";
}
/** Independent budgets keep a routing/search operation from evicting the map. */
export class DecodedCache {
private groups: Record<CacheDomain, BudgetCache<string, Uint8Array>>;
constructor(budgets: Partial<typeof DEFAULT_DECODED_BUDGETS> = {}) {
const limits = { ...DEFAULT_DECODED_BUDGETS, ...budgets };
this.groups = Object.fromEntries(
Object.entries(limits).map(([k, v]) => [
k,
new BudgetCache<string, Uint8Array>(v, (bytes) => bytes.byteLength),
]),
) as typeof this.groups;
}
get stats() {
return Object.fromEntries(
Object.entries(this.groups).map(([k, c]) => [k, c.stats]),
) as Record<CacheDomain, BudgetCache<string, Uint8Array>["stats"]>;
}
get size() {
return Object.values(this.groups).reduce((n, c) => n + c.size, 0);
}
get(key: string) {
return this.groups[domain(key)].get(key);
}
has(key: string) {
return this.groups[domain(key)].has(key);
}
set(key: string, bytes: Uint8Array) {
this.groups[domain(key)].set(key, bytes);
}
delete(key: string) {
return this.groups[domain(key)].delete(key);
}
clear() {
for (const c of Object.values(this.groups)) c.clear();
}
*[Symbol.iterator](): IterableIterator<[string, Uint8Array]> {
for (const c of Object.values(this.groups)) yield* c;
}
}
