serverless js: make formatting consistent

This commit is contained in:
Brandon Liu
2023-03-13 14:29:01 +08:00
parent 118049200f
commit cdda65a7eb
2 changed files with 380 additions and 380 deletions

View File

@@ -1,16 +1,16 @@
import { Readable } from "stream"; import { Readable } from "stream";
import { import {
Context, Context,
APIGatewayProxyResult, APIGatewayProxyResult,
APIGatewayProxyEventV2, APIGatewayProxyEventV2,
} from "aws-lambda"; } from "aws-lambda";
import { import {
PMTiles, PMTiles,
ResolvedValueCache, ResolvedValueCache,
RangeResponse, RangeResponse,
Source, Source,
Compression, Compression,
TileType, TileType,
} from "../../../js/index"; } from "../../../js/index";
import https from "https"; import https from "https";
@@ -21,23 +21,23 @@ import { NodeHttpHandler } from "@aws-sdk/node-http-handler";
// the region should default to the same one as the function // the region should default to the same one as the function
const s3client = new S3Client({ const s3client = new S3Client({
requestHandler: new NodeHttpHandler({ requestHandler: new NodeHttpHandler({
connectionTimeout: 500, connectionTimeout: 500,
socketTimeout: 500, socketTimeout: 500,
}), }),
}); });
async function nativeDecompress( async function nativeDecompress(
buf: ArrayBuffer, buf: ArrayBuffer,
compression: Compression compression: Compression
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
if (compression === Compression.None || compression === Compression.Unknown) { if (compression === Compression.None || compression === Compression.Unknown) {
return buf; return buf;
} else if (compression === Compression.Gzip) { } else if (compression === Compression.Gzip) {
return zlib.gunzipSync(buf); return zlib.gunzipSync(buf);
} else { } else {
throw Error("Compression method not supported"); throw Error("Compression method not supported");
} }
} }
// Lambda needs to run with 512MB, empty function takes about 70 // Lambda needs to run with 512MB, empty function takes about 70
@@ -45,219 +45,219 @@ const CACHE = new ResolvedValueCache(undefined, undefined, nativeDecompress);
// duplicated code below // duplicated code below
export const pmtiles_path = (name: string, setting?: string): string => { export const pmtiles_path = (name: string, setting?: string): string => {
if (setting) { if (setting) {
return setting.replaceAll("{name}", name); return setting.replaceAll("{name}", name);
} }
return name + ".pmtiles"; return name + ".pmtiles";
}; };
const TILE = const TILE =
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/; /^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
export const tile_path = ( export const tile_path = (
path: string, path: string,
setting?: string setting?: string
): { ): {
ok: boolean; ok: boolean;
name: string; name: string;
tile: [number, number, number]; tile: [number, number, number];
ext: string; ext: string;
} => { } => {
let pattern = TILE; let pattern = TILE;
if (setting) { if (setting) {
// escape regex // escape regex
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&"); setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)"); setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
setting = setting.replace("{z}", "(?<Z>\\d+)"); setting = setting.replace("{z}", "(?<Z>\\d+)");
setting = setting.replace("{x}", "(?<X>\\d+)"); setting = setting.replace("{x}", "(?<X>\\d+)");
setting = setting.replace("{y}", "(?<Y>\\d+)"); setting = setting.replace("{y}", "(?<Y>\\d+)");
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)"); setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
pattern = new RegExp(setting); pattern = new RegExp(setting);
} }
let match = path.match(pattern); let match = path.match(pattern);
if (match) { if (match) {
const g = match.groups!; const g = match.groups!;
return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT }; return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT };
} }
return { ok: false, name: "", tile: [0, 0, 0], ext: "" }; return { ok: false, name: "", tile: [0, 0, 0], ext: "" };
}; };
class S3Source implements Source { class S3Source implements Source {
archive_name: string; archive_name: string;
constructor(archive_name: string) { constructor(archive_name: string) {
this.archive_name = archive_name; this.archive_name = archive_name;
} }
getKey() { getKey() {
return this.archive_name; return this.archive_name;
} }
async getBytes(offset: number, length: number): Promise<RangeResponse> { async getBytes(offset: number, length: number): Promise<RangeResponse> {
const resp = await s3client.send( const resp = await s3client.send(
new GetObjectCommand({ new GetObjectCommand({
Bucket: process.env.BUCKET!, Bucket: process.env.BUCKET!,
Key: pmtiles_path(this.archive_name, process.env.PMTILES_PATH), Key: pmtiles_path(this.archive_name, process.env.PMTILES_PATH),
Range: "bytes=" + offset + "-" + (offset + length - 1), Range: "bytes=" + offset + "-" + (offset + length - 1),
}) })
); );
const arr = await resp.Body!.transformToByteArray(); const arr = await resp.Body!.transformToByteArray();
return { return {
data: arr.buffer, data: arr.buffer,
etag: resp.ETag, etag: resp.ETag,
expires: resp.Expires?.toISOString(), expires: resp.Expires?.toISOString(),
cacheControl: resp.CacheControl, cacheControl: resp.CacheControl,
}; };
} }
} }
interface Headers { interface Headers {
[key: string]: string; [key: string]: string;
} }
const apiResp = ( const apiResp = (
statusCode: number, statusCode: number,
body: string, body: string,
isBase64Encoded = false, isBase64Encoded = false,
headers: Headers = {} headers: Headers = {}
): APIGatewayProxyResult => { ): APIGatewayProxyResult => {
return { return {
statusCode: statusCode, statusCode: statusCode,
body: body, body: body,
headers: headers, headers: headers,
isBase64Encoded: isBase64Encoded, isBase64Encoded: isBase64Encoded,
}; };
}; };
// Assumes event is a API Gateway V2 or Lambda Function URL formatted dict // Assumes event is a API Gateway V2 or Lambda Function URL formatted dict
// and returns API Gateway V2 / Lambda Function dict responses // and returns API Gateway V2 / Lambda Function dict responses
// Does not work with CloudFront events/Lambda@Edge; see README // Does not work with CloudFront events/Lambda@Edge; see README
export const handlerRaw = async ( export const handlerRaw = async (
event: APIGatewayProxyEventV2, event: APIGatewayProxyEventV2,
context: Context, context: Context,
tilePostprocess?: (a: ArrayBuffer, t: TileType) => ArrayBuffer tilePostprocess?: (a: ArrayBuffer, t: TileType) => ArrayBuffer
): Promise<APIGatewayProxyResult> => { ): Promise<APIGatewayProxyResult> => {
var path; var path;
var is_api_gateway; var is_api_gateway;
if (event.pathParameters) { if (event.pathParameters) {
is_api_gateway = true; is_api_gateway = true;
if (event.pathParameters.proxy) { if (event.pathParameters.proxy) {
path = "/" + event.pathParameters.proxy; path = "/" + event.pathParameters.proxy;
} else { } else {
return apiResp(500, "Proxy integration missing tile_path parameter"); return apiResp(500, "Proxy integration missing tile_path parameter");
} }
} else { } else {
path = event.rawPath; path = event.rawPath;
} }
if (!path) { if (!path) {
return apiResp(500, "Invalid event configuration"); return apiResp(500, "Invalid event configuration");
} }
var headers: Headers = {}; var headers: Headers = {};
// TODO: metadata and TileJSON // TODO: metadata and TileJSON
if (process.env.CORS) { if (process.env.CORS) {
headers["Access-Control-Allow-Origin"] = process.env.CORS; headers["Access-Control-Allow-Origin"] = process.env.CORS;
} }
const { ok, name, tile, ext } = tile_path(path, process.env.TILE_PATH); const { ok, name, tile, ext } = tile_path(path, process.env.TILE_PATH);
if (!ok) { if (!ok) {
return apiResp(400, "Invalid tile URL", false, headers); return apiResp(400, "Invalid tile URL", false, headers);
} }
const source = new S3Source(name); const source = new S3Source(name);
const p = new PMTiles(source, CACHE, nativeDecompress); const p = new PMTiles(source, CACHE, nativeDecompress);
try { try {
const header = await p.getHeader(); const header = await p.getHeader();
if (tile[0] < header.minZoom || tile[0] > header.maxZoom) { if (tile[0] < header.minZoom || tile[0] > header.maxZoom) {
return apiResp(404, "", false, headers); return apiResp(404, "", false, headers);
} }
for (const pair of [ for (const pair of [
[TileType.Mvt, "mvt"], [TileType.Mvt, "mvt"],
[TileType.Png, "png"], [TileType.Png, "png"],
[TileType.Jpeg, "jpg"], [TileType.Jpeg, "jpg"],
[TileType.Webp, "webp"], [TileType.Webp, "webp"],
]) { ]) {
if (header.tileType === pair[0] && ext !== pair[1]) { if (header.tileType === pair[0] && ext !== pair[1]) {
if (header.tileType == TileType.Mvt && ext === "pbf") { if (header.tileType == TileType.Mvt && ext === "pbf") {
// allow this for now. Eventually we will delete this in favor of .mvt // allow this for now. Eventually we will delete this in favor of .mvt
continue; continue;
} }
return apiResp( return apiResp(
400, 400,
"Bad request: archive has type ." + pair[1], "Bad request: archive has type ." + pair[1],
false, false,
headers headers
); );
} }
} }
const tile_result = await p.getZxy(tile[0], tile[1], tile[2]); const tile_result = await p.getZxy(tile[0], tile[1], tile[2]);
if (tile_result) { if (tile_result) {
switch (header.tileType) { switch (header.tileType) {
case TileType.Mvt: case TileType.Mvt:
// part of the list of Cloudfront compressible types. // part of the list of Cloudfront compressible types.
headers["Content-Type"] = "application/vnd.mapbox-vector-tile"; headers["Content-Type"] = "application/vnd.mapbox-vector-tile";
break; break;
case TileType.Png: case TileType.Png:
headers["Content-Type"] = "image/png"; headers["Content-Type"] = "image/png";
break; break;
case TileType.Jpeg: case TileType.Jpeg:
headers["Content-Type"] = "image/jpeg"; headers["Content-Type"] = "image/jpeg";
break; break;
case TileType.Webp: case TileType.Webp:
headers["Content-Type"] = "image/webp"; headers["Content-Type"] = "image/webp";
break; break;
} }
let data = tile_result.data; let data = tile_result.data;
if (tilePostprocess) { if (tilePostprocess) {
data = tilePostprocess(data, header.tileType); data = tilePostprocess(data, header.tileType);
} }
if (is_api_gateway) { if (is_api_gateway) {
// this is wasted work, but we need to force API Gateway to interpret the Lambda response as binary // this is wasted work, but we need to force API Gateway to interpret the Lambda response as binary
// without depending on clients sending matching Accept: headers in the request. // without depending on clients sending matching Accept: headers in the request.
const recompressed_data = zlib.gzipSync(data); const recompressed_data = zlib.gzipSync(data);
headers["Content-Encoding"] = "gzip"; headers["Content-Encoding"] = "gzip";
return apiResp( return apiResp(
200, 200,
Buffer.from(recompressed_data).toString("base64"), Buffer.from(recompressed_data).toString("base64"),
true, true,
headers headers
); );
} else { } else {
// returns uncompressed response // returns uncompressed response
return apiResp( return apiResp(
200, 200,
Buffer.from(data).toString("base64"), Buffer.from(data).toString("base64"),
true, true,
headers headers
); );
} }
} else { } else {
return apiResp(204, "", false, headers); return apiResp(204, "", false, headers);
} }
} catch (e) { } catch (e) {
if ((e as Error).name === "AccessDenied") { if ((e as Error).name === "AccessDenied") {
return apiResp(403, "Bucket access unauthorized", false, headers); return apiResp(403, "Bucket access unauthorized", false, headers);
} }
throw e; throw e;
} }
return apiResp(404, "Invalid URL", false, headers); return apiResp(404, "Invalid URL", false, headers);
}; };
export const handler = async ( export const handler = async (
event: APIGatewayProxyEventV2, event: APIGatewayProxyEventV2,
context: Context context: Context
): Promise<APIGatewayProxyResult> => { ): Promise<APIGatewayProxyResult> => {
return handlerRaw(event, context); return handlerRaw(event, context);
}; };

View File

@@ -5,240 +5,240 @@
*/ */
import { import {
PMTiles, PMTiles,
Source, Source,
RangeResponse, RangeResponse,
ResolvedValueCache, ResolvedValueCache,
TileType, TileType,
Compression, Compression,
} from "../../../js/index"; } from "../../../js/index";
interface Env { interface Env {
BUCKET: R2Bucket; BUCKET: R2Bucket;
ALLOWED_ORIGINS?: string; ALLOWED_ORIGINS?: string;
PMTILES_PATH?: string; PMTILES_PATH?: string;
TILE_PATH?: string; TILE_PATH?: string;
CACHE_MAX_AGE?: number; CACHE_MAX_AGE?: number;
} }
class KeyNotFoundError extends Error { class KeyNotFoundError extends Error {
constructor(message: string) { constructor(message: string) {
super(message); super(message);
} }
} }
export const pmtiles_path = (name: string, setting?: string): string => { export const pmtiles_path = (name: string, setting?: string): string => {
if (setting) { if (setting) {
return setting.replaceAll("{name}", name); return setting.replaceAll("{name}", name);
} }
return name + ".pmtiles"; return name + ".pmtiles";
}; };
const TILE = const TILE =
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/; /^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
export const tile_path = ( export const tile_path = (
path: string, path: string,
setting?: string setting?: string
): { ): {
ok: boolean; ok: boolean;
name: string; name: string;
tile: [number, number, number]; tile: [number, number, number];
ext: string; ext: string;
} => { } => {
let pattern = TILE; let pattern = TILE;
if (setting) { if (setting) {
// escape regex // escape regex
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&"); setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)"); setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
setting = setting.replace("{z}", "(?<Z>\\d+)"); setting = setting.replace("{z}", "(?<Z>\\d+)");
setting = setting.replace("{x}", "(?<X>\\d+)"); setting = setting.replace("{x}", "(?<X>\\d+)");
setting = setting.replace("{y}", "(?<Y>\\d+)"); setting = setting.replace("{y}", "(?<Y>\\d+)");
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)"); setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
pattern = new RegExp(setting); pattern = new RegExp(setting);
} }
let match = path.match(pattern); let match = path.match(pattern);
if (match) { if (match) {
const g = match.groups!; const g = match.groups!;
return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT }; return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT };
} }
return { ok: false, name: "", tile: [0, 0, 0], ext: "" }; return { ok: false, name: "", tile: [0, 0, 0], ext: "" };
}; };
async function nativeDecompress( async function nativeDecompress(
buf: ArrayBuffer, buf: ArrayBuffer,
compression: Compression compression: Compression
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
if (compression === Compression.None || compression === Compression.Unknown) { if (compression === Compression.None || compression === Compression.Unknown) {
return buf; return buf;
} else if (compression === Compression.Gzip) { } else if (compression === Compression.Gzip) {
let stream = new Response(buf).body!; let stream = new Response(buf).body!;
let result = stream.pipeThrough(new DecompressionStream("gzip")); let result = stream.pipeThrough(new DecompressionStream("gzip"));
return new Response(result).arrayBuffer(); return new Response(result).arrayBuffer();
} else { } else {
throw Error("Compression method not supported"); throw Error("Compression method not supported");
} }
} }
const CACHE = new ResolvedValueCache(25, undefined, nativeDecompress); const CACHE = new ResolvedValueCache(25, undefined, nativeDecompress);
class R2Source implements Source { class R2Source implements Source {
env: Env; env: Env;
archive_name: string; archive_name: string;
constructor(env: Env, archive_name: string) { constructor(env: Env, archive_name: string) {
this.env = env; this.env = env;
this.archive_name = archive_name; this.archive_name = archive_name;
} }
getKey() { getKey() {
return this.archive_name; return this.archive_name;
} }
async getBytes(offset: number, length: number): Promise<RangeResponse> { async getBytes(offset: number, length: number): Promise<RangeResponse> {
const resp = await this.env.BUCKET.get( const resp = await this.env.BUCKET.get(
pmtiles_path(this.archive_name, this.env.PMTILES_PATH), pmtiles_path(this.archive_name, this.env.PMTILES_PATH),
{ {
range: { offset: offset, length: length }, range: { offset: offset, length: length },
} }
); );
if (!resp) { if (!resp) {
throw new KeyNotFoundError("Archive not found"); throw new KeyNotFoundError("Archive not found");
} }
const o = resp as R2ObjectBody; const o = resp as R2ObjectBody;
const a = await o.arrayBuffer(); const a = await o.arrayBuffer();
return { return {
data: a, data: a,
etag: o.etag, etag: o.etag,
cacheControl: o.httpMetadata?.cacheControl, cacheControl: o.httpMetadata?.cacheControl,
expires: o.httpMetadata?.cacheExpiry?.toISOString(), expires: o.httpMetadata?.cacheExpiry?.toISOString(),
}; };
} }
} }
export default { export default {
async fetch( async fetch(
request: Request, request: Request,
env: Env, env: Env,
ctx: ExecutionContext ctx: ExecutionContext
): Promise<Response> { ): Promise<Response> {
if (request.method.toUpperCase() === "POST") if (request.method.toUpperCase() === "POST")
return new Response(undefined, { status: 405 }); return new Response(undefined, { status: 405 });
const url = new URL(request.url); const url = new URL(request.url);
const { ok, name, tile, ext } = tile_path(url.pathname, env.TILE_PATH); const { ok, name, tile, ext } = tile_path(url.pathname, env.TILE_PATH);
const cache = caches.default; const cache = caches.default;
if (ok) { if (ok) {
let allowed_origin = ""; let allowed_origin = "";
if (typeof env.ALLOWED_ORIGINS !== "undefined") { if (typeof env.ALLOWED_ORIGINS !== "undefined") {
for (let o of env.ALLOWED_ORIGINS.split(",")) { for (let o of env.ALLOWED_ORIGINS.split(",")) {
if (o === request.headers.get("Origin") || o === "*") { if (o === request.headers.get("Origin") || o === "*") {
allowed_origin = o; allowed_origin = o;
} }
} }
} }
let cached = await cache.match(request.url); let cached = await cache.match(request.url);
if (cached) { if (cached) {
let resp_headers = new Headers(cached.headers); let resp_headers = new Headers(cached.headers);
if (allowed_origin) if (allowed_origin)
resp_headers.set("Access-Control-Allow-Origin", allowed_origin); resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
resp_headers.set("Vary", "Origin"); resp_headers.set("Vary", "Origin");
return new Response(cached.body, { return new Response(cached.body, {
headers: resp_headers, headers: resp_headers,
status: cached.status, status: cached.status,
}); });
} }
const cacheableResponse = ( const cacheableResponse = (
body: ArrayBuffer | string | undefined, body: ArrayBuffer | string | undefined,
cacheable_headers: Headers, cacheable_headers: Headers,
status: number status: number
) => { ) => {
cacheable_headers.set( cacheable_headers.set(
"Cache-Control", "Cache-Control",
"max-age=" + (env.CACHE_MAX_AGE | 86400) "max-age=" + (env.CACHE_MAX_AGE | 86400)
); );
let cacheable = new Response(body, { let cacheable = new Response(body, {
headers: cacheable_headers, headers: cacheable_headers,
status: status, status: status,
}); });
// normalize HEAD requests // normalize HEAD requests
ctx.waitUntil(cache.put(request.url, cacheable)); ctx.waitUntil(cache.put(request.url, cacheable));
let resp_headers = new Headers(cacheable_headers); let resp_headers = new Headers(cacheable_headers);
if (allowed_origin) if (allowed_origin)
resp_headers.set("Access-Control-Allow-Origin", allowed_origin); resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
resp_headers.set("Vary", "Origin"); resp_headers.set("Vary", "Origin");
return new Response(body, { headers: resp_headers, status: status }); return new Response(body, { headers: resp_headers, status: status });
}; };
const cacheable_headers = new Headers(); const cacheable_headers = new Headers();
const source = new R2Source(env, name); const source = new R2Source(env, name);
const p = new PMTiles(source, CACHE, nativeDecompress); const p = new PMTiles(source, CACHE, nativeDecompress);
try { try {
const p_header = await p.getHeader(); const p_header = await p.getHeader();
if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) { if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) {
return cacheableResponse(undefined, cacheable_headers, 404); return cacheableResponse(undefined, cacheable_headers, 404);
} }
for (const pair of [ for (const pair of [
[TileType.Mvt, "mvt"], [TileType.Mvt, "mvt"],
[TileType.Png, "png"], [TileType.Png, "png"],
[TileType.Jpeg, "jpg"], [TileType.Jpeg, "jpg"],
[TileType.Webp, "webp"], [TileType.Webp, "webp"],
]) { ]) {
if (p_header.tileType === pair[0] && ext !== pair[1]) { if (p_header.tileType === pair[0] && ext !== pair[1]) {
if (p_header.tileType == TileType.Mvt && ext === "pbf") { if (p_header.tileType == TileType.Mvt && ext === "pbf") {
// allow this for now. Eventually we will delete this in favor of .mvt // allow this for now. Eventually we will delete this in favor of .mvt
continue; continue;
} }
return cacheableResponse( return cacheableResponse(
"Bad request: archive has type ." + pair[1], "Bad request: archive has type ." + pair[1],
cacheable_headers, cacheable_headers,
400 400
); );
} }
} }
const tiledata = await p.getZxy(tile[0], tile[1], tile[2]); const tiledata = await p.getZxy(tile[0], tile[1], tile[2]);
switch (p_header.tileType) { switch (p_header.tileType) {
case TileType.Mvt: case TileType.Mvt:
cacheable_headers.set("Content-Type", "application/x-protobuf"); cacheable_headers.set("Content-Type", "application/x-protobuf");
break; break;
case TileType.Png: case TileType.Png:
cacheable_headers.set("Content-Type", "image/png"); cacheable_headers.set("Content-Type", "image/png");
break; break;
case TileType.Jpeg: case TileType.Jpeg:
cacheable_headers.set("Content-Type", "image/jpeg"); cacheable_headers.set("Content-Type", "image/jpeg");
break; break;
case TileType.Webp: case TileType.Webp:
cacheable_headers.set("Content-Type", "image/webp"); cacheable_headers.set("Content-Type", "image/webp");
break; break;
} }
if (tiledata) { if (tiledata) {
return cacheableResponse(tiledata.data, cacheable_headers, 200); return cacheableResponse(tiledata.data, cacheable_headers, 200);
} else { } else {
return cacheableResponse(undefined, cacheable_headers, 204); return cacheableResponse(undefined, cacheable_headers, 204);
} }
} catch (e) { } catch (e) {
if (e instanceof KeyNotFoundError) { if (e instanceof KeyNotFoundError) {
return cacheableResponse("Archive not found", cacheable_headers, 404); return cacheableResponse("Archive not found", cacheable_headers, 404);
} else { } else {
throw e; throw e;
} }
} }
} }
// TODO: metadata responses, tileJSON // TODO: metadata responses, tileJSON
return new Response("Invalid URL", { status: 404 }); return new Response("Invalid URL", { status: 404 });
}, },
}; };