mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
use bundled place boundaries and shared OpenFreeMap tiles before a guarded Nominatim fallback preserve multipart shapes, holes and cancellation, and keep pending annotations visible while ground sampling finishes across frames keep non-Latin name distinctions when selecting provider results, and retain an outline anchor only when it lies in filled geometry
541 lines
16 KiB
JavaScript
541 lines
16 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createSharedDecodedTileCache,
|
|
createVectorTileSource,
|
|
} from './vectorTiles.js';
|
|
const meta = { tiles: ['https://tiles.example/{z}/{x}/{y}.pbf'] };
|
|
const box = { south: 30.267, north: 30.268, west: -97.744, east: -97.743 };
|
|
const options = {
|
|
tileJsonUrl: 'https://tiles.example/index.json',
|
|
allowedOrigin: 'https://tiles.example',
|
|
decode: () => [],
|
|
};
|
|
|
|
for (const failure of ['503', 'network', 'timeout'])
|
|
test(`TileJSON ${failure} retries after cooldown and caches only success`, async () => {
|
|
let calls = 0,
|
|
time = 0;
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
now: () => time,
|
|
metadataCooldownMs: 5000,
|
|
fetchImpl: async () => {
|
|
calls++;
|
|
if (calls > 1) return Response.json(meta);
|
|
if (failure === '503') return new Response('', { status: 503 });
|
|
throw failure === 'timeout'
|
|
? new DOMException('timeout', 'TimeoutError')
|
|
: new TypeError('network');
|
|
},
|
|
});
|
|
await assert.rejects(source.getMetadata(), { retryable: true });
|
|
await assert.rejects(source.getMetadata(), { retryable: true });
|
|
assert.equal(calls, 1);
|
|
time = 5001;
|
|
assert.equal((await source.getMetadata()).template, meta.tiles[0]);
|
|
await source.getMetadata();
|
|
assert.equal(calls, 2);
|
|
source.clear();
|
|
await source.getMetadata();
|
|
assert.equal(calls, 3);
|
|
});
|
|
for (const invalid of [
|
|
{ tiles: [] },
|
|
{ tiles: ['https://evil.example/{z}/{x}/{y}'] },
|
|
])
|
|
test('invalid TileJSON is permanent only until clear', async () => {
|
|
let calls = 0;
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
fetchImpl: async () => {
|
|
calls++;
|
|
return Response.json(calls === 1 ? invalid : meta);
|
|
},
|
|
});
|
|
await assert.rejects(source.getMetadata(), { retryable: false });
|
|
await assert.rejects(source.getMetadata(), { retryable: false });
|
|
assert.equal(calls, 1);
|
|
source.clear();
|
|
assert.equal((await source.getMetadata()).template, meta.tiles[0]);
|
|
});
|
|
for (const declared of [true, false])
|
|
test(`oversized ${declared ? 'declared' : 'streaming'} tile cancels body and aborts request before release`, async () => {
|
|
let cancelled = 0,
|
|
signal;
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
template: meta.tiles[0],
|
|
maxResponseBytes: 8,
|
|
fetchImpl: async (_, opts) => {
|
|
signal = opts.signal;
|
|
return new Response(
|
|
new ReadableStream({
|
|
pull(c) {
|
|
c.enqueue(new Uint8Array(16));
|
|
},
|
|
cancel() {
|
|
cancelled++;
|
|
},
|
|
}),
|
|
{ headers: declared ? { 'content-length': '16' } : {} },
|
|
);
|
|
},
|
|
});
|
|
await assert.rejects(source.fetchBounds(box, { zoom: 14 }), {
|
|
code: 'RESPONSE_TOO_LARGE',
|
|
});
|
|
assert.equal(cancelled, 1);
|
|
assert.equal(signal.aborted, true);
|
|
assert.equal(source.getStats().cacheEntries, 0);
|
|
});
|
|
|
|
test('overlapping callers share XYZ; cancelling one keeps the other alive', async () => {
|
|
let releaseBody,
|
|
reads = 0,
|
|
underlying;
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
template: meta.tiles[0],
|
|
fetchImpl: async (_, { signal }) => {
|
|
reads++;
|
|
underlying = signal;
|
|
await new Promise((resolve) => {
|
|
releaseBody = resolve;
|
|
});
|
|
return new Response(new Uint8Array([1]));
|
|
},
|
|
});
|
|
const a = new AbortController(),
|
|
b = new AbortController();
|
|
const first = source.fetchBounds(box, { zoom: 12, signal: a.signal });
|
|
const rejection = assert.rejects(first, { name: 'AbortError' });
|
|
const second = source.fetchBounds(box, { zoom: 12, signal: b.signal });
|
|
while (!releaseBody) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
a.abort();
|
|
await rejection;
|
|
assert.equal(underlying.aborted, false);
|
|
releaseBody();
|
|
assert.equal((await second).tiles.length, 1);
|
|
assert.equal(reads, 1);
|
|
await source.fetchBounds(box, { zoom: 12 });
|
|
assert.equal(reads, 1);
|
|
});
|
|
|
|
test('final subscriber cancellation aborts the owned request; completed tiles publish before a straggler', async () => {
|
|
let owned;
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
template: meta.tiles[0],
|
|
fetchImpl: (_, { signal }) =>
|
|
new Promise((resolve, reject) => {
|
|
owned = signal;
|
|
signal.addEventListener('abort', () => reject(signal.reason));
|
|
}),
|
|
});
|
|
const controller = new AbortController();
|
|
const pending = source.fetchBounds(box, {
|
|
zoom: 12,
|
|
signal: controller.signal,
|
|
});
|
|
const rejection = assert.rejects(pending, { name: 'AbortError' });
|
|
while (!owned) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
controller.abort();
|
|
await rejection;
|
|
assert.equal(owned.aborted, true);
|
|
let release,
|
|
published = 0,
|
|
requests = 0;
|
|
const incremental = createVectorTileSource({
|
|
...options,
|
|
template: meta.tiles[0],
|
|
fetchImpl: async () => {
|
|
if (++requests === 2)
|
|
await new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
return new Response(new Uint8Array([1]));
|
|
},
|
|
});
|
|
const complete = incremental.fetchBounds(
|
|
{ ...box, east: -97.7 },
|
|
{ zoom: 12, onTile: () => published++ },
|
|
);
|
|
while (!published) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
assert.equal(published, 1);
|
|
assert.equal(typeof release, 'function');
|
|
release();
|
|
await complete;
|
|
assert.equal(published, 2);
|
|
});
|
|
|
|
test('polygon-selected tiles keep nearest-first order and cached revisits issue zero requests', async () => {
|
|
const urls = [];
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
template: 'https://tiles.example/{z}/{x}/{y}.pbf',
|
|
tileJsonUrl: undefined,
|
|
fetchImpl: async (url) => {
|
|
urls.push(url);
|
|
return new Response(new Uint8Array([1]));
|
|
},
|
|
});
|
|
const tiles = [
|
|
{ z: 12, x: 935, y: 1685 },
|
|
{ z: 12, x: 936, y: 1685 },
|
|
];
|
|
await source.fetchBounds(box, { zoom: 12, tiles });
|
|
assert.ok(urls[0].endsWith('/12/935/1685.pbf'));
|
|
assert.ok(urls[1].endsWith('/12/936/1685.pbf'));
|
|
await source.fetchBounds(box, { zoom: 12, tiles: [...tiles].reverse() });
|
|
assert.equal(urls.length, 2);
|
|
await assert.rejects(
|
|
source.fetchBounds(box, {
|
|
zoom: 12,
|
|
tiles: Array.from({ length: 17 }, (_, i) => ({
|
|
z: 12,
|
|
x: 935 + i,
|
|
y: 1685,
|
|
})),
|
|
}),
|
|
{ code: 'TILE_VIEW_TOO_WIDE' },
|
|
);
|
|
assert.equal(
|
|
urls.length,
|
|
2,
|
|
'over-budget selection is rejected before fetching',
|
|
);
|
|
});
|
|
|
|
test('a configured tileJsonUrl sets the tile origin, and a foreign tiles[0] is still refused', async () => {
|
|
// Without an explicit allowedOrigin the mirror's own TileJSON is honoured…
|
|
const mirrored = createVectorTileSource({
|
|
tileJsonUrl: 'http://127.0.0.1:12348/planet',
|
|
decode: () => [],
|
|
fetchImpl: async () =>
|
|
Response.json({ tiles: ['http://127.0.0.1:12348/p/{z}/{x}/{y}.pbf'] }),
|
|
});
|
|
assert.equal(
|
|
(await mirrored.getMetadata()).template,
|
|
'http://127.0.0.1:12348/p/{z}/{x}/{y}.pbf',
|
|
);
|
|
|
|
// …while a TileJSON that names a different host is refused rather than
|
|
// silently fetched, which is what the origin check is for.
|
|
const foreign = createVectorTileSource({
|
|
tileJsonUrl: 'http://127.0.0.1:12348/planet',
|
|
decode: () => [],
|
|
fetchImpl: async () =>
|
|
Response.json({
|
|
tiles: ['https://tiles.example/planet/{z}/{x}/{y}.pbf'],
|
|
}),
|
|
});
|
|
await assert.rejects(foreign.getMetadata(), { retryable: false });
|
|
});
|
|
|
|
test('an explicit allowedOrigin still wins over the one derived from tileJsonUrl', async () => {
|
|
const source = createVectorTileSource({
|
|
tileJsonUrl: 'http://127.0.0.1:12348/planet',
|
|
allowedOrigin: 'https://tiles.example',
|
|
decode: () => [],
|
|
fetchImpl: async () =>
|
|
Response.json({
|
|
tiles: ['https://tiles.example/planet/{z}/{x}/{y}.pbf'],
|
|
}),
|
|
});
|
|
assert.equal(
|
|
(await source.getMetadata()).template,
|
|
'https://tiles.example/planet/{z}/{x}/{y}.pbf',
|
|
);
|
|
});
|
|
|
|
test('shared decoded tiles are isolated from consumer mutation', async () => {
|
|
const sharedDecodedCache = createSharedDecodedTileCache();
|
|
let decodes = 0;
|
|
let requests = 0;
|
|
const decode = () => {
|
|
decodes++;
|
|
return { roads: [{ name: 'Congress Avenue', coordinates: [[-97, 30]] }] };
|
|
};
|
|
const common = {
|
|
...options,
|
|
template: meta.tiles[0],
|
|
sharedDecodedCache,
|
|
decode,
|
|
selectDecoded: (value) => value.roads,
|
|
fetchImpl: async () => {
|
|
requests++;
|
|
return new Response(new Uint8Array([1, 2, 3]));
|
|
},
|
|
};
|
|
const first = createVectorTileSource(common);
|
|
const second = createVectorTileSource(common);
|
|
const tiles = [{ z: 14, x: 3743, y: 6745 }];
|
|
const firstResult = await first.fetchBounds(box, { zoom: 14, tiles });
|
|
firstResult.tiles[0][0].name = 'mutated';
|
|
firstResult.tiles[0][0].coordinates[0][0] = 0;
|
|
|
|
const secondResult = await second.fetchBounds(box, { zoom: 14, tiles });
|
|
assert.equal(secondResult.tiles[0][0].name, 'Congress Avenue');
|
|
assert.equal(secondResult.tiles[0][0].coordinates[0][0], -97);
|
|
assert.equal(requests, 1);
|
|
assert.equal(decodes, 1);
|
|
});
|
|
|
|
test('compatible consumers share one in-flight fetch and survive one consumer clearing', async () => {
|
|
const sharedDecodedCache = createSharedDecodedTileCache();
|
|
let requests = 0;
|
|
let decodes = 0;
|
|
let releaseResponse;
|
|
let requestStarted;
|
|
const responseReady = new Promise((resolve) => {
|
|
releaseResponse = resolve;
|
|
});
|
|
const started = new Promise((resolve) => {
|
|
requestStarted = resolve;
|
|
});
|
|
const decode = () => {
|
|
decodes++;
|
|
return { roads: ['road'], outlines: ['outline'] };
|
|
};
|
|
const common = {
|
|
...options,
|
|
template: meta.tiles[0],
|
|
sharedDecodedCache,
|
|
decode,
|
|
fetchImpl: async () => {
|
|
requests++;
|
|
requestStarted();
|
|
await responseReady;
|
|
return new Response(new Uint8Array([1, 2, 3]));
|
|
},
|
|
};
|
|
const roads = createVectorTileSource({
|
|
...common,
|
|
selectDecoded: (value) => value.roads,
|
|
});
|
|
const outlines = createVectorTileSource({
|
|
...common,
|
|
selectDecoded: (value) => value.outlines,
|
|
});
|
|
const tiles = [{ z: 14, x: 3743, y: 6745 }];
|
|
const roadResult = roads.fetchBounds(box, { zoom: 14, tiles });
|
|
const roadRejected = assert.rejects(roadResult, { name: 'AbortError' });
|
|
await started;
|
|
const outlineResult = outlines.fetchBounds(box, { zoom: 14, tiles });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
roads.clear();
|
|
releaseResponse();
|
|
await roadRejected;
|
|
assert.deepEqual((await outlineResult).tiles, [['outline']]);
|
|
assert.equal(requests, 1);
|
|
assert.equal(decodes, 1);
|
|
});
|
|
|
|
test('queued shared hits preserve sibling ownership and cannot refill after clear', async (t) => {
|
|
const sharedDecodedCache = createSharedDecodedTileCache();
|
|
let requests = 0,
|
|
decodes = 0,
|
|
selections = 0;
|
|
const geometry = [
|
|
{
|
|
name: 'Congress Avenue',
|
|
coordinates: [
|
|
[-97, 30],
|
|
[-97, 31],
|
|
],
|
|
},
|
|
];
|
|
const common = {
|
|
...options,
|
|
template: meta.tiles[0],
|
|
sharedDecodedCache,
|
|
decode: () => {
|
|
decodes++;
|
|
return { roads: geometry };
|
|
},
|
|
selectDecoded: (value) => {
|
|
selections++;
|
|
return value.roads;
|
|
},
|
|
fetchImpl: async () => {
|
|
requests++;
|
|
return new Response(new Uint8Array([1, 2, 3]));
|
|
},
|
|
};
|
|
const tiles = [{ z: 14, x: 3743, y: 6745 }];
|
|
await createVectorTileSource(common).fetchBounds(box, { zoom: 14, tiles });
|
|
selections = 0;
|
|
const scheduler = Object.getOwnPropertyDescriptor(globalThis, 'scheduler');
|
|
let queued = Promise.withResolvers();
|
|
let resume;
|
|
Object.defineProperty(globalThis, 'scheduler', {
|
|
configurable: true,
|
|
value: {
|
|
yield: () =>
|
|
new Promise((resolve) => {
|
|
resume = resolve;
|
|
queued.resolve();
|
|
}),
|
|
},
|
|
});
|
|
t.after(() => {
|
|
resume?.();
|
|
if (scheduler) Object.defineProperty(globalThis, 'scheduler', scheduler);
|
|
else delete globalThis.scheduler;
|
|
});
|
|
const source = createVectorTileSource(common);
|
|
const controller = new AbortController();
|
|
const cancelled = source.fetchBounds(box, {
|
|
zoom: 14,
|
|
tiles,
|
|
signal: controller.signal,
|
|
});
|
|
const rejected = assert.rejects(cancelled, { name: 'AbortError' });
|
|
const sibling = source.fetchBounds(box, { zoom: 14, tiles });
|
|
await queued.promise;
|
|
controller.abort();
|
|
await rejected;
|
|
resume();
|
|
assert.deepEqual((await sibling).tiles, [geometry]);
|
|
assert.equal(selections, 1);
|
|
assert.deepEqual(source.getStats(), {
|
|
tilesFetched: 0,
|
|
cacheEntries: 1,
|
|
cacheBytes: 12,
|
|
});
|
|
|
|
selections = 0;
|
|
queued = Promise.withResolvers();
|
|
const cleared = createVectorTileSource(common);
|
|
let published = 0;
|
|
const pending = cleared.fetchBounds(box, {
|
|
zoom: 14,
|
|
tiles,
|
|
onTile: () => published++,
|
|
});
|
|
const clearRejected = assert.rejects(pending, { name: 'AbortError' });
|
|
await queued.promise;
|
|
cleared.clear();
|
|
resume();
|
|
await clearRejected;
|
|
assert.equal(selections, 0);
|
|
assert.equal(published, 0);
|
|
assert.deepEqual(cleared.getStats(), {
|
|
tilesFetched: 0,
|
|
cacheEntries: 0,
|
|
cacheBytes: 0,
|
|
});
|
|
|
|
queued = Promise.withResolvers();
|
|
const fresh = cleared.fetchBounds(box, { zoom: 14, tiles });
|
|
await queued.promise;
|
|
resume();
|
|
assert.deepEqual((await fresh).tiles, [geometry]);
|
|
assert.equal(requests, 1);
|
|
assert.equal(decodes, 1);
|
|
});
|
|
|
|
test('shared ownership recharges materialized projections and evicts over budget', () => {
|
|
const shared = createSharedDecodedTileCache({ maxCacheBytes: 100 });
|
|
let resize;
|
|
let releases = 0;
|
|
const decoded = {
|
|
bindSharedCacheEntry(update) {
|
|
resize = update;
|
|
},
|
|
releaseSharedCacheEntry() {
|
|
releases++;
|
|
},
|
|
};
|
|
const entry = shared.set('tile', decoded, 12);
|
|
assert.deepEqual(shared.stats(), { entries: 1, bytes: 12 });
|
|
resize(80);
|
|
assert.deepEqual(shared.stats(), { entries: 1, bytes: 80 });
|
|
resize(101);
|
|
assert.deepEqual(shared.stats(), { entries: 0, bytes: 0 });
|
|
assert.equal(entry.size, 101);
|
|
assert.equal(releases, 1);
|
|
shared.clear();
|
|
assert.equal(releases, 1);
|
|
});
|
|
|
|
test('shared clear releases entries and prevents a pending loader from refilling', async () => {
|
|
const shared = createSharedDecodedTileCache();
|
|
let releases = 0;
|
|
shared.set(
|
|
'resident',
|
|
{
|
|
releaseSharedCacheEntry() {
|
|
releases++;
|
|
},
|
|
},
|
|
12,
|
|
);
|
|
shared.clear();
|
|
assert.deepEqual(shared.stats(), { entries: 0, bytes: 0 });
|
|
assert.equal(releases, 1);
|
|
|
|
let loaderStarted;
|
|
let resolveLoader;
|
|
const started = new Promise((resolve) => {
|
|
loaderStarted = resolve;
|
|
});
|
|
const loaderResult = new Promise((resolve) => {
|
|
resolveLoader = resolve;
|
|
});
|
|
const pending = shared.load('late', null, async () => {
|
|
loaderStarted();
|
|
return loaderResult;
|
|
});
|
|
await started;
|
|
resolveLoader({ value: {}, size: 16 });
|
|
shared.clear();
|
|
await assert.rejects(pending, { name: 'AbortError' });
|
|
assert.deepEqual(shared.stats(), { entries: 0, bytes: 0 });
|
|
});
|
|
|
|
test('shared decoded ownership is isolated by decoder identity', async () => {
|
|
const sharedDecodedCache = createSharedDecodedTileCache();
|
|
let requests = 0;
|
|
const common = {
|
|
...options,
|
|
template: meta.tiles[0],
|
|
sharedDecodedCache,
|
|
fetchImpl: async () => {
|
|
requests++;
|
|
return new Response(new Uint8Array([1]));
|
|
},
|
|
};
|
|
const a = createVectorTileSource({ ...common, decode: () => ['a'] });
|
|
const b = createVectorTileSource({ ...common, decode: () => ['b'] });
|
|
const tiles = [{ z: 14, x: 3743, y: 6745 }];
|
|
assert.deepEqual((await a.fetchBounds(box, { zoom: 14, tiles })).tiles, [
|
|
['a'],
|
|
]);
|
|
assert.deepEqual((await b.fetchBounds(box, { zoom: 14, tiles })).tiles, [
|
|
['b'],
|
|
]);
|
|
assert.equal(requests, 2);
|
|
});
|
|
|
|
test('clear prevents an old generation from starting tile I/O', async () => {
|
|
let requests = 0;
|
|
const source = createVectorTileSource({
|
|
...options,
|
|
template: meta.tiles[0],
|
|
fetchImpl: async () => {
|
|
requests++;
|
|
return new Response(new Uint8Array([1]));
|
|
},
|
|
});
|
|
await source.getMetadata();
|
|
const pending = source.fetchBounds(box, {
|
|
zoom: 14,
|
|
tiles: [{ z: 14, x: 3743, y: 6745 }],
|
|
});
|
|
source.clear();
|
|
await assert.rejects(pending, { name: 'AbortError' });
|
|
assert.equal(requests, 0);
|
|
});
|