@basemaps/lambda-tiler 6.32.1 → 6.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/build/__tests__/xyz.util.d.ts +1 -1
- package/build/__tests__/xyz.util.d.ts.map +1 -1
- package/build/__tests__/xyz.util.js +2 -2
- package/build/__tests__/xyz.util.js.map +1 -1
- package/build/arcgis/__tests__/arcgis.style.json.test.d.ts +2 -0
- package/build/arcgis/__tests__/arcgis.style.json.test.d.ts.map +1 -0
- package/build/arcgis/__tests__/arcgis.style.json.test.js +128 -0
- package/build/arcgis/__tests__/arcgis.style.json.test.js.map +1 -0
- package/build/arcgis/__tests__/vector.tiler.server.test.d.ts +2 -0
- package/build/arcgis/__tests__/vector.tiler.server.test.d.ts.map +1 -0
- package/build/arcgis/__tests__/vector.tiler.server.test.js +47 -0
- package/build/arcgis/__tests__/vector.tiler.server.test.js.map +1 -0
- package/build/arcgis/arcgis.info.d.ts +3 -0
- package/build/arcgis/arcgis.info.d.ts.map +1 -0
- package/build/arcgis/arcgis.info.js +25 -0
- package/build/arcgis/arcgis.info.js.map +1 -0
- package/build/arcgis/arcgis.style.json.d.ts +9 -0
- package/build/arcgis/arcgis.style.json.d.ts.map +1 -0
- package/build/arcgis/arcgis.style.json.js +73 -0
- package/build/arcgis/arcgis.style.json.js.map +1 -0
- package/build/arcgis/vector.tile.server.d.ts +8 -0
- package/build/arcgis/vector.tile.server.d.ts.map +1 -0
- package/build/arcgis/vector.tile.server.js +71 -0
- package/build/arcgis/vector.tile.server.js.map +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +22 -8
- package/build/index.js.map +1 -1
- package/build/routes/__tests__/fonts.test.js +14 -26
- package/build/routes/__tests__/fonts.test.js.map +1 -1
- package/build/routes/__tests__/sprites.test.js +7 -1
- package/build/routes/__tests__/sprites.test.js.map +1 -1
- package/build/routes/__tests__/xyz.test.js +1 -2
- package/build/routes/__tests__/xyz.test.js.map +1 -1
- package/build/routes/fonts.d.ts +0 -2
- package/build/routes/fonts.d.ts.map +1 -1
- package/build/routes/fonts.js +3 -66
- package/build/routes/fonts.js.map +1 -1
- package/build/routes/sprites.d.ts.map +1 -1
- package/build/routes/sprites.js +3 -29
- package/build/routes/sprites.js.map +1 -1
- package/build/routes/tile.xyz.vector.js +2 -2
- package/build/routes/tile.xyz.vector.js.map +1 -1
- package/build/util/assets.provider.d.ts +27 -0
- package/build/util/assets.provider.d.ts.map +1 -0
- package/build/util/assets.provider.js +56 -0
- package/build/util/assets.provider.js.map +1 -0
- package/build/util/config.cache.d.ts +16 -0
- package/build/util/config.cache.d.ts.map +1 -0
- package/build/util/config.cache.js +41 -0
- package/build/util/config.cache.js.map +1 -0
- package/build/util/response.d.ts +3 -1
- package/build/util/response.d.ts.map +1 -1
- package/build/util/response.js +2 -0
- package/build/util/response.js.map +1 -1
- package/build/util/source.cache.d.ts +1 -0
- package/build/util/source.cache.d.ts.map +1 -1
- package/build/util/source.cache.js +2 -1
- package/build/util/source.cache.js.map +1 -1
- package/build/util/swapping.lru.d.ts +1 -0
- package/build/util/swapping.lru.d.ts.map +1 -1
- package/build/util/swapping.lru.js +7 -0
- package/build/util/swapping.lru.js.map +1 -1
- package/build/util/validate.d.ts +0 -5
- package/build/util/validate.d.ts.map +1 -1
- package/build/util/validate.js +1 -23
- package/build/util/validate.js.map +1 -1
- package/dist/index.js +52 -52
- package/dist/node_modules/.package-lock.json +4 -4
- package/dist/node_modules/node-abi/abi_registry.json +8 -1
- package/dist/node_modules/node-abi/package.json +1 -1
- package/dist/package-lock.json +8 -8
- package/dist/package.json +1 -1
- package/package.json +4 -4
- package/src/__tests__/xyz.util.ts +7 -2
- package/src/arcgis/__tests__/arcgis.style.json.test.ts +153 -0
- package/src/arcgis/__tests__/vector.tiler.server.test.ts +61 -0
- package/src/arcgis/arcgis.info.ts +26 -0
- package/src/arcgis/arcgis.style.json.ts +81 -0
- package/src/arcgis/vector.tile.server.ts +78 -0
- package/src/index.ts +25 -8
- package/src/routes/__tests__/fonts.test.ts +14 -29
- package/src/routes/__tests__/sprites.test.ts +7 -2
- package/src/routes/__tests__/xyz.test.ts +2 -2
- package/src/routes/fonts.ts +4 -64
- package/src/routes/sprites.ts +4 -27
- package/src/routes/tile.xyz.vector.ts +2 -2
- package/src/util/assets.provider.ts +67 -0
- package/src/util/config.cache.ts +44 -0
- package/src/util/response.ts +4 -1
- package/src/util/source.cache.ts +2 -1
- package/src/util/swapping.lru.ts +7 -0
- package/src/util/validate.ts +1 -27
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,8 +2,9 @@ import { Env } from '@basemaps/shared';
|
|
|
2
2
|
import { fsa } from '@chunkd/fs';
|
|
3
3
|
import o from 'ospec';
|
|
4
4
|
import { handler } from '../../index.js';
|
|
5
|
+
import { assetProvider } from '../../util/assets.provider.js';
|
|
5
6
|
import { mockRequest } from '../../__tests__/xyz.util.js';
|
|
6
|
-
import { fontList
|
|
7
|
+
import { fontList } from '../fonts.js';
|
|
7
8
|
import { FsMemory } from './memory.fs.js';
|
|
8
9
|
|
|
9
10
|
o.spec('/v1/fonts', () => {
|
|
@@ -11,52 +12,30 @@ o.spec('/v1/fonts', () => {
|
|
|
11
12
|
o.before(() => {
|
|
12
13
|
fsa.register('memory://', memory);
|
|
13
14
|
});
|
|
15
|
+
const assetLocation = process.env[Env.AssetLocation];
|
|
14
16
|
|
|
15
17
|
o.beforeEach(() => {
|
|
16
18
|
process.env[Env.AssetLocation] = 'memory://';
|
|
19
|
+
assetProvider.set('memory://');
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
o.afterEach(() => {
|
|
20
|
-
|
|
23
|
+
assetProvider.set(assetLocation);
|
|
21
24
|
memory.files.clear();
|
|
22
25
|
});
|
|
23
26
|
|
|
24
|
-
o('should
|
|
25
|
-
await Promise.all([
|
|
26
|
-
fsa.write('memory://fonts/Roboto Thin/0-255.pbf', Buffer.from('')),
|
|
27
|
-
fsa.write('memory://fonts/Roboto Thin/256-512.pbf', Buffer.from('')),
|
|
28
|
-
fsa.write('memory://fonts/Roboto Black/0-255.pbf', Buffer.from('')),
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
const fonts = await getFonts('memory://fonts/');
|
|
32
|
-
o(fonts).deepEquals(['Roboto Black', 'Roboto Thin']);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
o('should return empty list if no fonts found', async () => {
|
|
36
|
-
const res = await fontList(mockRequest('/v1/fonts.json'));
|
|
37
|
-
o(res.status).equals(200);
|
|
38
|
-
o(res.body).equals('[]');
|
|
39
|
-
o(res.header('etag')).notEquals(undefined);
|
|
40
|
-
o(res.header('cache-control')).equals('public, max-age=604800, stale-while-revalidate=86400');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
o('should return 404 if no assets defined', async () => {
|
|
44
|
-
delete process.env[Env.AssetLocation];
|
|
27
|
+
o('should return 404 if no font found', async () => {
|
|
45
28
|
const res = await fontList(mockRequest('/v1/fonts.json'));
|
|
46
29
|
o(res.status).equals(404);
|
|
47
30
|
});
|
|
48
31
|
|
|
49
32
|
o('should return a list of fonts found', async () => {
|
|
50
|
-
await
|
|
51
|
-
fsa.write('memory://fonts/Roboto Thin/0-255.pbf', Buffer.from('')),
|
|
52
|
-
fsa.write('memory://fonts/Roboto Thin/256-512.pbf', Buffer.from('')),
|
|
53
|
-
fsa.write('memory://fonts/Roboto Black/0-255.pbf', Buffer.from('')),
|
|
54
|
-
]);
|
|
33
|
+
await fsa.write('memory://fonts.json', Buffer.from(JSON.stringify(['Roboto Black', 'Roboto Thin'])));
|
|
55
34
|
const res = await fontList(mockRequest('/v1/fonts.json'));
|
|
56
35
|
o(res.status).equals(200);
|
|
57
36
|
o(res.header('content-type')).equals('application/json');
|
|
58
37
|
o(res.header('content-encoding')).equals(undefined);
|
|
59
|
-
o(res.
|
|
38
|
+
o(res._body?.toString()).equals(JSON.stringify(['Roboto Black', 'Roboto Thin']));
|
|
60
39
|
});
|
|
61
40
|
|
|
62
41
|
o('should get the correct font', async () => {
|
|
@@ -83,4 +62,10 @@ o.spec('/v1/fonts', () => {
|
|
|
83
62
|
o(res255.header('etag')).notEquals(undefined);
|
|
84
63
|
o(res255.header('cache-control')).equals('public, max-age=604800, stale-while-revalidate=86400');
|
|
85
64
|
});
|
|
65
|
+
|
|
66
|
+
o('should return 404 if no asset location set', async () => {
|
|
67
|
+
assetProvider.set(undefined);
|
|
68
|
+
const res = await fontList(mockRequest('/v1/fonts.json'));
|
|
69
|
+
o(res.status).equals(404);
|
|
70
|
+
});
|
|
86
71
|
});
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import { Env } from '@basemaps/shared';
|
|
2
2
|
import { fsa } from '@chunkd/fs';
|
|
3
3
|
import o from 'ospec';
|
|
4
|
+
import { createSandbox } from 'sinon';
|
|
4
5
|
import { gunzipSync, gzipSync } from 'zlib';
|
|
5
6
|
import { handler } from '../../index.js';
|
|
7
|
+
import { assetProvider } from '../../util/assets.provider.js';
|
|
6
8
|
import { mockRequest } from '../../__tests__/xyz.util.js';
|
|
7
9
|
import { FsMemory } from './memory.fs.js';
|
|
8
10
|
|
|
9
11
|
o.spec('/v1/sprites', () => {
|
|
10
12
|
const memory = new FsMemory();
|
|
13
|
+
const sandbox = createSandbox();
|
|
11
14
|
o.before(() => {
|
|
12
15
|
fsa.register('memory://', memory);
|
|
13
16
|
});
|
|
17
|
+
const assetLocation = process.env[Env.AssetLocation];
|
|
14
18
|
|
|
15
19
|
o.beforeEach(() => {
|
|
16
20
|
process.env[Env.AssetLocation] = 'memory://';
|
|
21
|
+
assetProvider.set('memory://');
|
|
17
22
|
});
|
|
18
23
|
|
|
19
24
|
o.afterEach(() => {
|
|
20
|
-
|
|
25
|
+
assetProvider.set(assetLocation);
|
|
21
26
|
memory.files.clear();
|
|
27
|
+
sandbox.restore();
|
|
22
28
|
});
|
|
23
|
-
|
|
24
29
|
o('should return 404 if no assets defined', async () => {
|
|
25
30
|
delete process.env[Env.AssetLocation];
|
|
26
31
|
const res404 = await handler.router.handle(mockRequest('/v1/sprites/topographic.json'));
|
|
@@ -63,7 +63,8 @@ o.spec('/v1/tiles', () => {
|
|
|
63
63
|
|
|
64
64
|
['png', 'webp', 'jpeg', 'avif'].forEach((fmt) => {
|
|
65
65
|
o(`should 200 with empty ${fmt} if a tile is out of bounds`, async () => {
|
|
66
|
-
|
|
66
|
+
o.timeout(1_000);
|
|
67
|
+
|
|
67
68
|
const res = await handler.router.handle(
|
|
68
69
|
mockRequest(`/v1/tiles/aerial/global-mercator/0/0/0.${fmt}`, 'get', Api.header),
|
|
69
70
|
);
|
|
@@ -71,7 +72,6 @@ o.spec('/v1/tiles', () => {
|
|
|
71
72
|
o(res.header('content-type')).equals(`image/${fmt}`);
|
|
72
73
|
o(res.header('etag')).notEquals(undefined);
|
|
73
74
|
o(res.header('cache-control')).equals('public, max-age=604800, stale-while-revalidate=86400');
|
|
74
|
-
// o(rasterMock.calls.length).equals(1);
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
package/src/routes/fonts.ts
CHANGED
|
@@ -1,76 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fsa } from '@chunkd/fs';
|
|
3
|
-
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
1
|
+
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
4
2
|
import path from 'path';
|
|
5
|
-
import {
|
|
6
|
-
import { Etag } from '../util/etag.js';
|
|
7
|
-
import { NotFound, NotModified } from '../util/response.js';
|
|
3
|
+
import { assetProvider } from '../util/assets.provider.js';
|
|
8
4
|
|
|
9
5
|
interface FontGet {
|
|
10
6
|
Params: { fontStack: string; range: string };
|
|
11
7
|
}
|
|
12
8
|
|
|
13
9
|
export async function fontGet(req: LambdaHttpRequest<FontGet>): Promise<LambdaHttpResponse> {
|
|
14
|
-
const assetLocation = Env.get(Env.AssetLocation);
|
|
15
|
-
if (assetLocation == null) return NotFound();
|
|
16
|
-
|
|
17
10
|
const targetFile = path.join('fonts', req.params.fontStack, req.params.range) + '.pbf';
|
|
18
|
-
|
|
19
|
-
return serveFromCotar(req, assetLocation, targetFile, 'application/x-protobuf');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const filePath = fsa.join(assetLocation, targetFile);
|
|
24
|
-
const buf = await fsa.read(filePath);
|
|
25
|
-
|
|
26
|
-
const cacheKey = Etag.key(buf);
|
|
27
|
-
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
28
|
-
|
|
29
|
-
const response = LambdaHttpResponse.ok().buffer(buf, 'application/x-protobuf');
|
|
30
|
-
response.header(HttpHeader.ETag, cacheKey);
|
|
31
|
-
response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
|
|
32
|
-
if (isGzip(buf)) response.header(HttpHeader.ContentEncoding, 'gzip');
|
|
33
|
-
return response;
|
|
34
|
-
} catch (e: any) {
|
|
35
|
-
if (e.code === 404) return NotFound();
|
|
36
|
-
throw e;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Get the unique name of folders is a path that contain .pbf files */
|
|
41
|
-
export async function getFonts(fontPath: string): Promise<string[]> {
|
|
42
|
-
const fonts = new Set<string>();
|
|
43
|
-
|
|
44
|
-
// TODO use {recursive: false}
|
|
45
|
-
for await (const font of fsa.list(fontPath)) {
|
|
46
|
-
if (!font.endsWith('.pbf')) continue;
|
|
47
|
-
const dirName = path.basename(path.dirname(font)); // TODO this only works for /a/b.pbf and not /a/b/c.pbf
|
|
48
|
-
if (dirName.includes('/')) continue;
|
|
49
|
-
fonts.add(dirName);
|
|
50
|
-
}
|
|
51
|
-
// Ensure the fonts are alphabetical
|
|
52
|
-
return [...fonts].sort();
|
|
11
|
+
return assetProvider.serve(req, targetFile, 'application/x-protobuf');
|
|
53
12
|
}
|
|
54
13
|
|
|
55
14
|
export async function fontList(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
|
|
56
|
-
|
|
57
|
-
if (assetLocation == null) return NotFound();
|
|
58
|
-
|
|
59
|
-
if (assetLocation.endsWith('.tar.co')) return serveFromCotar(req, assetLocation, 'fonts.json', 'application/json');
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const filePath = fsa.join(assetLocation, '/fonts');
|
|
63
|
-
const fonts = await getFonts(filePath);
|
|
64
|
-
|
|
65
|
-
const cacheKey = Etag.key(fonts);
|
|
66
|
-
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
67
|
-
|
|
68
|
-
const response = LambdaHttpResponse.ok().buffer(JSON.stringify(fonts), 'application/json');
|
|
69
|
-
response.header(HttpHeader.ETag, cacheKey);
|
|
70
|
-
response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
|
|
71
|
-
return response;
|
|
72
|
-
} catch (e: any) {
|
|
73
|
-
if (e.code === 404) return NotFound();
|
|
74
|
-
throw e;
|
|
75
|
-
}
|
|
15
|
+
return assetProvider.serve(req, 'fonts.json', 'application/json');
|
|
76
16
|
}
|
package/src/routes/sprites.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { Env } from '@basemaps/shared';
|
|
2
1
|
import { fsa } from '@chunkd/fs';
|
|
3
2
|
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
import { NotFound
|
|
6
|
-
import {
|
|
7
|
-
import { Etag } from '../util/etag.js';
|
|
3
|
+
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
4
|
+
import { NotFound } from '../util/response.js';
|
|
5
|
+
import { assetProvider } from '../util/assets.provider.js';
|
|
8
6
|
|
|
9
7
|
interface SpriteGet {
|
|
10
8
|
Params: {
|
|
@@ -17,31 +15,10 @@ Extensions.set('.png', 'image/png');
|
|
|
17
15
|
Extensions.set('.json', 'application/json');
|
|
18
16
|
|
|
19
17
|
export async function spriteGet(req: LambdaHttpRequest<SpriteGet>): Promise<LambdaHttpResponse> {
|
|
20
|
-
const assetLocation = Env.get(Env.AssetLocation);
|
|
21
|
-
if (assetLocation == null) return NotFound();
|
|
22
|
-
|
|
23
18
|
const extension = path.extname(req.params.spriteName);
|
|
24
19
|
const mimeType = Extensions.get(extension);
|
|
25
20
|
if (mimeType == null) return NotFound();
|
|
26
21
|
|
|
27
22
|
const targetFile = fsa.join('sprites', req.params.spriteName);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const filePath = fsa.join(assetLocation, targetFile);
|
|
32
|
-
req.set('target', filePath);
|
|
33
|
-
|
|
34
|
-
const buf = await fsa.read(filePath);
|
|
35
|
-
const cacheKey = Etag.key(buf);
|
|
36
|
-
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
37
|
-
|
|
38
|
-
const response = LambdaHttpResponse.ok().buffer(buf, mimeType);
|
|
39
|
-
response.header(HttpHeader.ETag, cacheKey);
|
|
40
|
-
response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
|
|
41
|
-
if (isGzip(buf)) response.header(HttpHeader.ContentEncoding, 'gzip');
|
|
42
|
-
return response;
|
|
43
|
-
} catch (e: any) {
|
|
44
|
-
if (e.code === 404) return NotFound();
|
|
45
|
-
throw e;
|
|
46
|
-
}
|
|
23
|
+
return assetProvider.serve(req, targetFile, mimeType);
|
|
47
24
|
}
|
|
@@ -3,7 +3,7 @@ import { GoogleTms, VectorFormat } from '@basemaps/geo';
|
|
|
3
3
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
4
4
|
import { isGzip } from '../util/cotar.serve.js';
|
|
5
5
|
import { Etag } from '../util/etag.js';
|
|
6
|
-
import { NotFound, NotModified } from '../util/response.js';
|
|
6
|
+
import { NoContent, NotFound, NotModified } from '../util/response.js';
|
|
7
7
|
import { CoSources } from '../util/source.cache.js';
|
|
8
8
|
import { TileXyz } from '../util/validate.js';
|
|
9
9
|
|
|
@@ -34,7 +34,7 @@ export const tileXyzVector = {
|
|
|
34
34
|
|
|
35
35
|
req.timer.start('cotar:tile');
|
|
36
36
|
const tile = await cotar.get(tilePath);
|
|
37
|
-
if (tile == null) return
|
|
37
|
+
if (tile == null) return NoContent();
|
|
38
38
|
req.timer.end('cotar:tile');
|
|
39
39
|
|
|
40
40
|
const tileBuffer = Buffer.from(tile);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { fsa } from '@chunkd/fs';
|
|
2
|
+
import { LambdaHttpResponse, LambdaHttpRequest, HttpHeader } from '@linzjs/lambda';
|
|
3
|
+
import { isGzip } from './cotar.serve.js';
|
|
4
|
+
import { Etag } from './etag.js';
|
|
5
|
+
import { NotFound, NotModified } from './response.js';
|
|
6
|
+
import { CoSources } from './source.cache.js';
|
|
7
|
+
|
|
8
|
+
export class AssetProvider {
|
|
9
|
+
/**
|
|
10
|
+
* Assets can be ready from the following locations.
|
|
11
|
+
*
|
|
12
|
+
* /home/blacha/config/build/assets # Local File
|
|
13
|
+
* /home/blacha/config/build/assets.tar.co # Local Cotar
|
|
14
|
+
* s3://linz-baesmaps/assets/ # Remote location
|
|
15
|
+
* s3://linz-basemaps/assets/assets-b4ff211a.tar.co # Remote Cotar
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Path of the assets location */
|
|
19
|
+
path: string | undefined;
|
|
20
|
+
|
|
21
|
+
set(path?: string): void {
|
|
22
|
+
this.path = path;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(fileName: string): Promise<Buffer | null> {
|
|
26
|
+
if (this.path == null) return null;
|
|
27
|
+
// get assets file from cotar
|
|
28
|
+
if (this.path.endsWith('.tar.co')) return await this.getFromCotar(this.path, fileName);
|
|
29
|
+
|
|
30
|
+
// get assets file for directory
|
|
31
|
+
try {
|
|
32
|
+
const filePath = fsa.join(this.path, fileName);
|
|
33
|
+
return await fsa.read(filePath);
|
|
34
|
+
} catch (e: any) {
|
|
35
|
+
if (e.code === 404) return null;
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getFromCotar(path: string, fileName: string): Promise<Buffer | null> {
|
|
41
|
+
const cotar = await CoSources.getCotar(path);
|
|
42
|
+
const data = await cotar.get(fileName);
|
|
43
|
+
return data ? Buffer.from(data) : data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load a assets from local path or cotar returning the file back as a LambdaResponse
|
|
48
|
+
*
|
|
49
|
+
* This will also set two headers
|
|
50
|
+
* - Content-Encoding if the file starts with gzip magic
|
|
51
|
+
* - Content-Type from the parameter contentType
|
|
52
|
+
*/
|
|
53
|
+
async serve(req: LambdaHttpRequest, file: string, contentType: string): Promise<LambdaHttpResponse> {
|
|
54
|
+
const buf = await assetProvider.get(file);
|
|
55
|
+
if (buf == null) return NotFound();
|
|
56
|
+
const cacheKey = Etag.key(buf);
|
|
57
|
+
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
58
|
+
|
|
59
|
+
const response = LambdaHttpResponse.ok().buffer(buf, contentType);
|
|
60
|
+
response.header(HttpHeader.ETag, cacheKey);
|
|
61
|
+
response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
|
|
62
|
+
if (isGzip(buf)) response.header(HttpHeader.ContentEncoding, 'gzip');
|
|
63
|
+
return response;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const assetProvider = new AssetProvider();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ConfigBundled, ConfigProviderMemory } from '@basemaps/config';
|
|
2
|
+
import { fsa } from '@chunkd/fs';
|
|
3
|
+
import { SwappingLru } from './swapping.lru.js';
|
|
4
|
+
|
|
5
|
+
class LruConfig {
|
|
6
|
+
configProvider: Promise<ConfigProviderMemory>;
|
|
7
|
+
|
|
8
|
+
constructor(config: Promise<ConfigBundled>) {
|
|
9
|
+
this.configProvider = config.then((c) => {
|
|
10
|
+
const configProvider = ConfigProviderMemory.fromJson(c);
|
|
11
|
+
configProvider.createVirtualTileSets();
|
|
12
|
+
return configProvider;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get size(): number {
|
|
17
|
+
// Return size 1 for the config and cache the number of configs based on size number.
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ConfigCache {
|
|
23
|
+
cache: SwappingLru<LruConfig>;
|
|
24
|
+
constructor(maxSize: number) {
|
|
25
|
+
this.cache = new SwappingLru<LruConfig>(maxSize);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getConfig(location: string): Promise<ConfigProviderMemory | null> {
|
|
29
|
+
const existing = this.cache.get(location)?.configProvider;
|
|
30
|
+
if (existing != null) return existing;
|
|
31
|
+
try {
|
|
32
|
+
const configJson = fsa.readJson<ConfigBundled>(location);
|
|
33
|
+
const config = new LruConfig(configJson);
|
|
34
|
+
this.cache.set(location, config);
|
|
35
|
+
return config.configProvider;
|
|
36
|
+
} catch (e: any) {
|
|
37
|
+
if (e.code === 404) return Promise.resolve(null);
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Cache 20 configs(Around 500KB each)*/
|
|
44
|
+
export const CachedConfig = new ConfigCache(20);
|
package/src/util/response.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { LambdaHttpResponse } from '@linzjs/lambda';
|
|
1
|
+
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
2
2
|
|
|
3
3
|
export const NotFound = (): LambdaHttpResponse => new LambdaHttpResponse(404, 'Not Found');
|
|
4
4
|
export const NotModified = (): LambdaHttpResponse => new LambdaHttpResponse(304, 'Not modified');
|
|
5
|
+
export const NoContent = (): LambdaHttpResponse => new LambdaHttpResponse(204, 'No Content');
|
|
6
|
+
export const OkResponse = (req: LambdaHttpRequest): LambdaHttpResponse =>
|
|
7
|
+
new LambdaHttpResponse(200, 'ok').json({ id: req.id, correlationId: req.correlationId, message: 'ok' });
|
package/src/util/source.cache.ts
CHANGED
package/src/util/swapping.lru.ts
CHANGED
|
@@ -6,6 +6,7 @@ export class SwappingLru<T extends { size: number }> {
|
|
|
6
6
|
hits = 0;
|
|
7
7
|
misses = 0;
|
|
8
8
|
resets = 0;
|
|
9
|
+
clears = 0;
|
|
9
10
|
|
|
10
11
|
_lastCheckedAt = -1;
|
|
11
12
|
|
|
@@ -37,6 +38,7 @@ export class SwappingLru<T extends { size: number }> {
|
|
|
37
38
|
clear(): void {
|
|
38
39
|
this.cacheA.clear();
|
|
39
40
|
this.cacheB.clear();
|
|
41
|
+
this.clears++;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
set(id: string, tiff: T): void {
|
|
@@ -50,6 +52,11 @@ export class SwappingLru<T extends { size: number }> {
|
|
|
50
52
|
if (this.maxSize <= 0) return;
|
|
51
53
|
if (this.currentSize <= this.maxSize) return;
|
|
52
54
|
this.resets++;
|
|
55
|
+
// Paranoia if we are resetting too often something is wrong, reset the entire cache.
|
|
56
|
+
if (this.resets > 100) {
|
|
57
|
+
this.resets = 0;
|
|
58
|
+
return this.clear();
|
|
59
|
+
}
|
|
53
60
|
this.cacheB = this.cacheA;
|
|
54
61
|
this.cacheA = new Map();
|
|
55
62
|
}
|
package/src/util/validate.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { ImageFormat, TileMatrixSet, TileMatrixSets, VectorFormat } from '@basemaps/geo';
|
|
2
|
-
import { Const, Projection } from '@basemaps/shared';
|
|
2
|
+
import { Const, isValidApiKey, Projection } from '@basemaps/shared';
|
|
3
3
|
import { getImageFormat } from '@basemaps/tiler';
|
|
4
4
|
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
5
|
-
import * as ulid from 'ulid';
|
|
6
5
|
import { TileXyzGet } from '../routes/tile.xyz';
|
|
7
6
|
|
|
8
7
|
export interface TileXyz {
|
|
@@ -16,31 +15,6 @@ export interface TileMatrixRequest {
|
|
|
16
15
|
Params: { tileMatrix?: string };
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const OneHourMs = 60 * 60 * 1000;
|
|
20
|
-
const OneDayMs = 24 * OneHourMs;
|
|
21
|
-
const MaxApiAgeMs = 91 * OneDayMs;
|
|
22
|
-
|
|
23
|
-
export interface ApiKeyStatus {
|
|
24
|
-
valid: boolean;
|
|
25
|
-
message: 'ok' | 'malformed' | 'missing' | 'expired';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isValidApiKey(apiKey?: string | null): ApiKeyStatus {
|
|
29
|
-
if (apiKey == null) return { valid: false, message: 'missing' };
|
|
30
|
-
if (!apiKey.startsWith('c') && !apiKey.startsWith('d')) return { valid: false, message: 'malformed' };
|
|
31
|
-
const ulidId = apiKey.slice(1).toUpperCase();
|
|
32
|
-
try {
|
|
33
|
-
const ulidTime = ulid.decodeTime(ulidId);
|
|
34
|
-
if (apiKey.startsWith('d')) return { valid: true, message: 'ok' };
|
|
35
|
-
|
|
36
|
-
if (Date.now() - ulidTime > MaxApiAgeMs) return { valid: false, message: 'expired' };
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return { valid: false, message: 'malformed' };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { valid: true, message: 'ok' };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
18
|
export const Validate = {
|
|
45
19
|
/**
|
|
46
20
|
* Validate that the api key exists and is valid
|