Files
PMTiles/serverless/cloudflare/worker.ts
2022-07-21 18:52:05 +08:00

164 lines
3.8 KiB
TypeScript

import { PMTiles, Source } from "../../js";
interface Env {
BUCKET: R2Bucket;
PMTILES_PATH: string | undefined;
}
interface CacheEntry {
lastUsed: number;
buffer: DataView;
}
class KeyNotFoundError extends Error {
constructor(message: string) {
super(message);
}
}
export class LRUCache {
entries: Map<string, CacheEntry>;
counter: number;
constructor() {
this.entries = new Map<string, CacheEntry>();
this.counter = 0;
}
async get(
bucket: R2Bucket,
key: string,
offset: number,
length: number
): Promise<[boolean, DataView]> {
let cacheKey = key + ":" + offset + "-" + length;
let val = this.entries.get(cacheKey);
if (val) {
val.lastUsed = this.counter++;
return [true, val.buffer];
}
let resp = await bucket.get(key, {
range: { offset: offset, length: length },
});
if (!resp) {
throw new KeyNotFoundError("Key not found");
}
let a = await (resp as R2ObjectBody).arrayBuffer();
let d = new DataView(a);
this.entries.set(cacheKey, {
lastUsed: this.counter++,
buffer: d,
});
if (this.entries.size > 128) {
let minUsed = Infinity;
let minKey = undefined;
this.entries.forEach((val, key) => {
if (val.lastUsed < minUsed) {
minUsed = val.lastUsed;
minKey = key;
}
});
if (minKey) this.entries.delete(minKey);
}
return [false, d];
}
}
let worker_cache = new LRUCache();
export const pmtiles_path = (p: string | undefined, name: string): string => {
if (p) {
return p.replace("{name}", name);
}
return name + ".pmtiles";
};
const TILE = new RegExp(
/^\/([0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(\d+)\/(\d+)\/(\d+).pbf$/
);
export default {
async fetch(
request: Request,
env: Env,
context: ExecutionContext
): Promise<Response> {
let url = new URL(request.url);
let match = url.pathname.match(TILE)!;
let subrequests = 1;
if (match) {
let name = match[1];
let z = +match[2];
let x = +match[3];
let y = +match[4];
class TempSource {
getKey() {
return "";
}
async getBytes(offset: number, length: number) {
let result = await worker_cache.get(
env.BUCKET,
pmtiles_path(env.PMTILES_PATH, name),
offset,
length
);
if (!result[0]) subrequests++;
return result[1];
}
}
let source = new TempSource();
let p = new PMTiles(source);
try {
let metadata = await p.metadata();
if (z < metadata.minzoom || z > metadata.maxzoom) {
return new Response("Tile not found", { status: 404 });
}
let entry = await p.getZxy(z, x, y);
if (entry) {
let tile = await env.BUCKET.get(
pmtiles_path(env.PMTILES_PATH, name),
{
range: { offset: entry.offset, length: entry.length },
}
);
let headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Content-Type", "application/x-protobuf");
headers.set("X-Pmap-Subrequests", subrequests.toString());
if (metadata.compression === "gzip") {
headers.set("Content-Encoding", "gzip");
}
return new Response((tile as R2ObjectBody).body, {
headers: headers,
encodeBody: "manual",
} as any);
} else {
return new Response(undefined, { status: 204 });
}
} catch (e) {
if (e instanceof KeyNotFoundError) {
return new Response("Archive not found", { status: 404 });
} else {
throw e;
}
}
}
return new Response("Invalid tile URL", { status: 400 });
},
};