chiitiler 1.23.0 → 1.23.3
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/README.md +3 -0
- package/dist/cli.js +21 -1
- package/dist/render/warmup.d.ts +9 -0
- package/dist/render/warmup.js +35 -0
- package/dist/source/fs.js +2 -16
- package/dist/source/gcs.js +16 -13
- package/dist/source/path.d.ts +7 -0
- package/dist/source/path.js +21 -0
- package/dist/source/s3.js +21 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,8 +121,11 @@ All options can be set via CLI flag or environment variable.
|
|
|
121
121
|
| `--port <n>` | `CHIITILER_PORT` | `3000` |
|
|
122
122
|
| `--debug` | `CHIITILER_DEBUG` | `false` |
|
|
123
123
|
| `--user-agent <ua>` | `CHIITILER_USER_AGENT` | (none) |
|
|
124
|
+
| `--prewarm` | `CHIITILER_PREWARM` | `false` |
|
|
124
125
|
| — | `CHIITILER_PROCESSES` | `1` (set `0` for all CPUs) |
|
|
125
126
|
|
|
127
|
+
With prewarm enabled, one tile is rendered at startup **before** the server starts listening, so renderer initialization (GL context, sharp/libvips, JIT) doesn't hit the first request. On AWS Lambda with Lambda Web Adapter, the readiness check keeps this inside the INIT phase, which runs with a full CPU boost.
|
|
128
|
+
|
|
126
129
|
### Cache
|
|
127
130
|
|
|
128
131
|
| Flag | Env | Default |
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { initServer } from './server/index.js';
|
|
4
4
|
import * as caches from './cache/index.js';
|
|
5
5
|
import { setUserAgent } from './source/userAgent.js';
|
|
6
|
+
import { prewarm } from './render/warmup.js';
|
|
6
7
|
function parseCacheStrategy(method, options) {
|
|
7
8
|
// command-line option
|
|
8
9
|
if (method === 'memory')
|
|
@@ -71,6 +72,10 @@ function parsePort(port) {
|
|
|
71
72
|
// undefined or invalid
|
|
72
73
|
return 3000;
|
|
73
74
|
}
|
|
75
|
+
function parsePrewarm(prewarmFlag) {
|
|
76
|
+
// command-line option, then env
|
|
77
|
+
return prewarmFlag === true || process.env.CHIITILER_PREWARM === 'true';
|
|
78
|
+
}
|
|
74
79
|
function parseDebug(debug) {
|
|
75
80
|
// command-line option
|
|
76
81
|
if (debug)
|
|
@@ -101,8 +106,9 @@ export function createProgram() {
|
|
|
101
106
|
.option('--gcs-api-endpoint <api-endpoint>', 'gcs api endpoint', '')
|
|
102
107
|
.option('-p, --port <port>', 'port number')
|
|
103
108
|
.option('--user-agent <user-agent>', 'User-Agent header for outbound HTTP requests')
|
|
109
|
+
.option('--prewarm', 'render one tile at startup before listening, to warm up the renderer')
|
|
104
110
|
.option('-D, --debug', 'debug mode')
|
|
105
|
-
.action((options) => {
|
|
111
|
+
.action(async (options) => {
|
|
106
112
|
// env fallback (CHIITILER_USER_AGENT) is handled in userAgent.ts
|
|
107
113
|
if (options.userAgent !== undefined)
|
|
108
114
|
setUserAgent(options.userAgent);
|
|
@@ -130,6 +136,20 @@ export function createProgram() {
|
|
|
130
136
|
console.log(`debug page: http://localhost:${serverOptions.port}/debug`);
|
|
131
137
|
console.log(`editor page: http://localhost:${serverOptions.port}/editor`);
|
|
132
138
|
}
|
|
139
|
+
// warm up BEFORE listening: Lambda Web Adapter polls the
|
|
140
|
+
// readiness check until the port opens, so work done here
|
|
141
|
+
// stays inside the INIT phase (full CPU boost)
|
|
142
|
+
if (parsePrewarm(options.prewarm)) {
|
|
143
|
+
const startedAt = Date.now();
|
|
144
|
+
try {
|
|
145
|
+
await prewarm(serverOptions.cache);
|
|
146
|
+
console.log(`prewarm done in ${Date.now() - startedAt}ms`);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
// 温まらないだけでサーバとしては動けるので起動は続行する
|
|
150
|
+
console.warn(`prewarm failed: ${err}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
133
153
|
const { start } = initServer(serverOptions);
|
|
134
154
|
start();
|
|
135
155
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Cache } from '../cache/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Render one z0 tile ahead of the first request so that renderer
|
|
4
|
+
* initialization happens at startup — on AWS Lambda (with Lambda Web
|
|
5
|
+
* Adapter's readiness check) this moves the work into the INIT phase,
|
|
6
|
+
* which runs with a full CPU boost.
|
|
7
|
+
*/
|
|
8
|
+
declare function prewarm(cache: Cache): Promise<void>;
|
|
9
|
+
export { prewarm };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getRenderedTile } from './index.js';
|
|
2
|
+
// スタイル固有のリソース(タイル/glyphs/sprite)は温まらないが、
|
|
3
|
+
// EGL/GLコンテキスト・Xvfb接続・sharp(libvips)・JITの初期化は共通で効く。
|
|
4
|
+
const MINIMAL_STYLE = {
|
|
5
|
+
version: 8,
|
|
6
|
+
sources: {},
|
|
7
|
+
layers: [
|
|
8
|
+
{
|
|
9
|
+
id: 'background',
|
|
10
|
+
type: 'background',
|
|
11
|
+
paint: { 'background-color': '#ffffff' },
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Render one z0 tile ahead of the first request so that renderer
|
|
17
|
+
* initialization happens at startup — on AWS Lambda (with Lambda Web
|
|
18
|
+
* Adapter's readiness check) this moves the work into the INIT phase,
|
|
19
|
+
* which runs with a full CPU boost.
|
|
20
|
+
*/
|
|
21
|
+
async function prewarm(cache) {
|
|
22
|
+
const sharp = await getRenderedTile({
|
|
23
|
+
stylejson: MINIMAL_STYLE,
|
|
24
|
+
z: 0,
|
|
25
|
+
x: 0,
|
|
26
|
+
y: 0,
|
|
27
|
+
tileSize: 512,
|
|
28
|
+
cache,
|
|
29
|
+
margin: 0,
|
|
30
|
+
ext: 'png',
|
|
31
|
+
quality: 100,
|
|
32
|
+
});
|
|
33
|
+
await sharp.toBuffer(); // force the actual encode
|
|
34
|
+
}
|
|
35
|
+
export { prewarm };
|
package/dist/source/fs.js
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { loadWithDecodedPathFallback } from './path.js';
|
|
2
3
|
async function getFilesystemSource(uri) {
|
|
3
4
|
const path = uri.replace('file://', '');
|
|
4
|
-
const data = await fs.readFile(
|
|
5
|
+
const data = await loadWithDecodedPathFallback(path, (candidate) => fs.readFile(candidate).catch(() => null));
|
|
5
6
|
if (data !== null)
|
|
6
7
|
return data;
|
|
7
|
-
// maplibre-gl-native percent-encodes {fontstack} in glyphs URLs
|
|
8
|
-
// (e.g. "Noto Sans Regular" -> "Noto%20Sans%20Regular"),
|
|
9
|
-
// so retry with the percent-decoded path
|
|
10
|
-
let decodedPath;
|
|
11
|
-
try {
|
|
12
|
-
decodedPath = decodeURIComponent(path);
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
decodedPath = path;
|
|
16
|
-
}
|
|
17
|
-
if (decodedPath !== path) {
|
|
18
|
-
const decodedData = await fs.readFile(decodedPath).catch(() => null);
|
|
19
|
-
if (decodedData !== null)
|
|
20
|
-
return decodedData;
|
|
21
|
-
}
|
|
22
8
|
console.error(`[ERROR]: failed to read ${path}`);
|
|
23
9
|
return null;
|
|
24
10
|
}
|
package/dist/source/gcs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getStorageClient } from '../gcs.js';
|
|
2
|
+
import { loadWithDecodedPathFallback } from './path.js';
|
|
2
3
|
async function getGCSSource(uri) {
|
|
3
4
|
const storageClient = getStorageClient({
|
|
4
5
|
projectId: process.env.CHIITILER_GCS_PROJECT_ID,
|
|
@@ -7,18 +8,20 @@ async function getGCSSource(uri) {
|
|
|
7
8
|
});
|
|
8
9
|
const bucket = uri.replace('gs://', '').split('/')[0];
|
|
9
10
|
const path = uri.replace(`gs://${bucket}/`, '');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
return loadWithDecodedPathFallback(path, async (candidate) => {
|
|
12
|
+
try {
|
|
13
|
+
const file = storageClient.bucket(bucket).file(candidate);
|
|
14
|
+
const [buffer] = await file.download();
|
|
15
|
+
return buffer;
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
// 404: オブジェクトが存在しない。空タイルとして扱わせる
|
|
19
|
+
if (e.code === 404)
|
|
20
|
+
return null;
|
|
21
|
+
// それ以外は有無が不明なのでthrowしてレンダリングを失敗させる
|
|
22
|
+
console.log(e);
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
23
26
|
}
|
|
24
27
|
export { getGCSSource };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a percent-decoded path first, then retry its literal form when the
|
|
3
|
+
* resource is absent. MapLibre Native encodes glyph font stacks before passing
|
|
4
|
+
* their URLs to the source resolver, while object stores treat keys literally.
|
|
5
|
+
*/
|
|
6
|
+
declare function loadWithDecodedPathFallback<T>(path: string, load: (path: string) => Promise<T | null>): Promise<T | null>;
|
|
7
|
+
export { loadWithDecodedPathFallback };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a percent-decoded path first, then retry its literal form when the
|
|
3
|
+
* resource is absent. MapLibre Native encodes glyph font stacks before passing
|
|
4
|
+
* their URLs to the source resolver, while object stores treat keys literally.
|
|
5
|
+
*/
|
|
6
|
+
async function loadWithDecodedPathFallback(path, load) {
|
|
7
|
+
let decodedPath;
|
|
8
|
+
try {
|
|
9
|
+
decodedPath = decodeURIComponent(path);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return load(path);
|
|
13
|
+
}
|
|
14
|
+
if (decodedPath === path)
|
|
15
|
+
return load(path);
|
|
16
|
+
const decodedData = await load(decodedPath);
|
|
17
|
+
if (decodedData !== null)
|
|
18
|
+
return decodedData;
|
|
19
|
+
return load(path);
|
|
20
|
+
}
|
|
21
|
+
export { loadWithDecodedPathFallback };
|
package/dist/source/s3.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
2
|
import { getS3Client } from '../s3.js';
|
|
3
|
+
import { loadWithDecodedPathFallback } from './path.js';
|
|
3
4
|
async function getS3Source(uri) {
|
|
4
5
|
const s3Client = getS3Client({
|
|
5
6
|
region: process.env.CHIITILER_S3_REGION ?? 'us-east-1',
|
|
@@ -8,25 +9,26 @@ async function getS3Source(uri) {
|
|
|
8
9
|
});
|
|
9
10
|
const bucket = uri.replace('s3://', '').split('/')[0];
|
|
10
11
|
const key = uri.replace(`s3://${bucket}/`, '');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
return loadWithDecodedPathFallback(key, async (candidate) => {
|
|
13
|
+
const cmd = new GetObjectCommand({
|
|
14
|
+
Bucket: bucket,
|
|
15
|
+
Key: candidate,
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
const obj = await s3Client.send(cmd);
|
|
19
|
+
if (obj.Body === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
return Buffer.from(await obj.Body.transformToByteArray());
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
// NoSuchKey: オブジェクトが存在しない。空タイルとして扱わせる
|
|
25
|
+
if (e.name === 'NoSuchKey')
|
|
26
|
+
return null;
|
|
27
|
+
// それ以外(スロットリング・ネットワークエラー等)は有無が不明なので
|
|
28
|
+
// throwしてレンダリングを失敗させる
|
|
29
|
+
console.log(e);
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
14
32
|
});
|
|
15
|
-
try {
|
|
16
|
-
const obj = await s3Client.send(cmd);
|
|
17
|
-
if (obj.Body === undefined)
|
|
18
|
-
return null;
|
|
19
|
-
const buf = Buffer.from(await obj.Body.transformToByteArray());
|
|
20
|
-
return buf;
|
|
21
|
-
}
|
|
22
|
-
catch (e) {
|
|
23
|
-
// NoSuchKey: オブジェクトが存在しない。空タイルとして扱わせる
|
|
24
|
-
if (e.name === 'NoSuchKey')
|
|
25
|
-
return null;
|
|
26
|
-
// それ以外(スロットリング・ネットワークエラー等)は有無が不明なので
|
|
27
|
-
// throwしてレンダリングを失敗させる
|
|
28
|
-
console.log(e);
|
|
29
|
-
throw e;
|
|
30
|
-
}
|
|
31
33
|
}
|
|
32
34
|
export { getS3Source };
|