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.
import { PinnedBlockStore } from "./pinned-store";
import { isRoutingCell } from "./index";
import type { Block } from "./index";
export interface SavedIndex {
header: Uint8Array;
index: Uint8Array;
}
export interface BlockStore {
available: boolean;
getIndex(id: string): Promise<SavedIndex | undefined>;
putIndex(id: string, value: SavedIndex): Promise<void>;
get(snapshot: string, block: Block): Promise<Uint8Array | undefined>;
put(snapshot: string, block: Block, bytes: Uint8Array): Promise<void>;
keys(snapshot: string): Promise<string[]>;
}
const eligible = (key: string) =>
key.startsWith("hashtree/") ||
key.startsWith("tile/") ||
key.startsWith("raster/") ||
key.startsWith("labels/") ||
key.startsWith("search/") ||
key.startsWith("features/") ||
key.startsWith("boundaries/") ||
key.startsWith("objects/") ||
key.startsWith("query/") ||
key.startsWith("index/") ||
isRoutingCell(key);
const request = <T>(r: IDBRequest<T>) =>
new Promise<T>((resolve, reject) => {
r.onsuccess = () => resolve(r.result);
r.onerror = () => reject(r.error);
});
const done = (tx: IDBTransaction) => {
const completion = new Promise<void>((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = tx.onabort = () => reject(tx.error);
});
void completion.catch(() => {});
return completion;
};
/** Compressed, hash-checked immutable blocks. 128 MiB LRU across all snapshots. */
export class BrowserBlockStore implements BlockStore {
available = true;
private pins = new PinnedBlockStore();
private queue: Promise<unknown> = Promise.resolve();
private db: Promise<IDBDatabase>;
private listeners = new Set<() => void>();
subscribe(listener: () => void) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
private changed() {
for (const listener of this.listeners) listener();
}
/** Drain queued writes, then atomically clear every snapshot and saved index.
* Callers must dispose readers/workers first so they cannot enqueue new writes.
* Unlike best-effort caching, a failed purge must be reported to the caller. */
async clear() {
const job = this.queue.then(async () => {
const db = await this.db;
const tx = db.transaction(["data", "meta", "indexes"], "readwrite");
const end = done(tx);
for (const name of ["data", "meta", "indexes"])
tx.objectStore(name).clear();
await end;
this.available = true;
this.changed();
});
this.queue = job.catch(() => {});
await job;
await this.pins.clear();
}
constructor(readonly limit = 128 * 1024 * 1024) {
this.db = new Promise((resolve, reject) => {
const r = indexedDB.open("atlas-blocks-v1", 1);
r.onupgradeneeded = () => {
for (const name of ["data", "meta", "indexes"])
r.result.createObjectStore(name);
};
r.onsuccess = () => resolve(r.result);
r.onerror = () => reject(r.error);
});
this.db.catch(() => {
this.available = false;
});
}
private async safe<T>(
fn: (db: IDBDatabase) => Promise<T>,
): Promise<T | undefined> {
if (!this.available) return;
try {
return await fn(await this.db);
} catch {
this.available = false;
return;
}
}
async getIndex(id: string) {
return this.safe((db) =>
request<SavedIndex | undefined>(
db.transaction("indexes").objectStore("indexes").get(id),
),
);
}
async putIndex(id: string, value: SavedIndex) {
await this.safe(async (db) => {
const tx = db.transaction("indexes", "readwrite"),
end = done(tx);
tx.objectStore("indexes").put(value, id);
await end;
});
}
private id(snapshot: string, b: Block) {
return `${snapshot}:${b.key}:${b.hash}`;
}
async get(snapshot: string, b: Block) {
const pinned = await this.pins.get(snapshot, b).catch(() => undefined);
if (pinned) return pinned;
if (!eligible(b.key)) return;
return this.safe(async (db) => {
const id = this.id(snapshot, b),
tx = db.transaction(["data", "meta"], "readwrite"),
end = done(tx);
const [bytes, meta] = await Promise.all([
request<Uint8Array | undefined>(tx.objectStore("data").get(id)),
request<any>(tx.objectStore("meta").get(id)),
]);
if (meta) tx.objectStore("meta").put({ ...meta, used: Date.now() }, id);
await end;
return bytes;
});
}
async put(snapshot: string, b: Block, bytes: Uint8Array) {
if (!eligible(b.key) || bytes.length > this.limit) return;
const job = this.queue.then(() =>
this.safe(async (db) => {
const id = this.id(snapshot, b),
tx = db.transaction(["data", "meta"], "readwrite"),
end = done(tx);
const meta = tx.objectStore("meta");
const entries = await request<
Array<{ id: string; bytes: number; used: number }>
>(meta.getAll());
let size = entries.reduce(
(n, e) => n + (e.id === id ? 0 : e.bytes),
bytes.length,
);
for (const e of entries.sort((a, b) => a.used - b.used)) {
if (size <= this.limit) break;
if (e.id === id) continue;
meta.delete(e.id);
tx.objectStore("data").delete(e.id);
size -= e.bytes;
}
tx.objectStore("data").put(bytes, id);
meta.put(
{ id, snapshot, key: b.key, bytes: bytes.length, used: Date.now() },
id,
);
await end;
this.changed();
}),
);
this.queue = job.catch(() => {});
await job;
}
async keys(snapshot: string) {
const pinned = await this.pins.keys(snapshot).catch(() => []);
const cached =
(await this.safe(async (db) => {
const entries = await request<Array<{ snapshot: string; key: string }>>(
db.transaction("meta").objectStore("meta").getAll(),
);
return entries.filter((e) => e.snapshot === snapshot).map((e) => e.key);
})) ?? [];
return [...new Set([...pinned, ...cached])];
}
}
