jskelet 0.6.2 → 0.6.4
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 +32 -0
- package/README.md +2 -0
- package/docs/02-mimari.md +1 -0
- package/docs/04-render-ve-sablonlar.md +9 -3
- package/docs/06-cache.md +53 -9
- package/docs/07-yapilandirma.md +1208 -1191
- package/docs/08-build.md +2 -1
- package/docs/10-dagitim.md +16 -6
- package/docs/12-panel-ve-oturum.md +2 -1
- package/docs/en/02-architecture.md +2 -1
- package/docs/en/04-rendering.md +9 -3
- package/docs/en/06-caching.md +54 -9
- package/docs/en/07-configuration.md +24 -9
- package/docs/en/08-build.md +2 -1
- package/docs/en/10-deployment.md +17 -6
- package/docs/en/12-dashboards-and-sessions.md +2 -1
- package/package.json +1 -1
- package/src/config/defaults.js +541 -518
- package/src/config/index.js +1500 -1456
- package/src/init.mjs +2 -0
- package/src/server/cache-blob.js +70 -0
- package/src/server/cache-control.js +45 -0
- package/src/server/data-cache.js +118 -27
- package/src/server/disk-cache.js +233 -0
- package/src/server/html-cache.js +90 -16
- package/src/server/image-optimizer.js +95 -2
- package/src/server/logs/file-sink.js +159 -32
- package/src/server/logs/pipeline.js +10 -3
- package/src/server/middleware/static-precompressed.js +31 -10
- package/src/server/og-image.js +17 -4
- package/src/server/prewarm.js +25 -1
- package/src/server/redis.js +31 -12
- package/src/server/render.js +910 -910
- package/types/config/defaults.d.ts +21 -1
- package/types/config/index.d.ts +14 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/og-image.d.ts +5 -0
- package/types/server/redis.d.ts +2 -1
package/src/server/og-image.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { tryImportFromApp } from "../build/resolve-peer.mjs";
|
|
12
12
|
import { getConfig } from "../config/index.js";
|
|
13
13
|
import { isNotFoundError } from "../http/control-flow.js";
|
|
14
|
+
import { setEdgeCacheHeaders } from "./cache-control.js";
|
|
14
15
|
|
|
15
16
|
/** @type {((input: Buffer, opts?: object) => import('sharp').Sharp) | null | undefined} */
|
|
16
17
|
let sharpModule;
|
|
@@ -18,8 +19,12 @@ let sharpModule;
|
|
|
18
19
|
/** Sosyal kartlar için yaygın boyut (Facebook / X / LinkedIn). */
|
|
19
20
|
export const OG_SIZE = Object.freeze({ width: 1200, height: 630 });
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* OG süreleri HTML TTL'ye bağlanmaz. Yalnızca `s-maxage` kalkar; edge
|
|
24
|
+
* `CDN-Cache-Control` üzerinde aynı pencereyi görür.
|
|
25
|
+
*/
|
|
26
|
+
const OG_EDGE_MAX_AGE = 86400;
|
|
27
|
+
const OG_EDGE_STALE = 604800;
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* @typedef {object} OgCardOptions
|
|
@@ -255,17 +260,25 @@ export async function ogImage(options = {}) {
|
|
|
255
260
|
|
|
256
261
|
/**
|
|
257
262
|
* Express yanıtına OG görseli basar.
|
|
263
|
+
*
|
|
264
|
+
* Varsayılan edge penceresi 86400 / 604800'tür ve HTML TTL'ye bağlı değildir.
|
|
265
|
+
* `cacheControl` verilirse yalnızca `Cache-Control` yazılır;
|
|
266
|
+
* `CDN-Cache-Control` basılmaz.
|
|
267
|
+
*
|
|
258
268
|
* @param {import('express').Response} res
|
|
259
269
|
* @param {OgImageOptions} [options]
|
|
260
270
|
* @returns {Promise<OgImageResult>}
|
|
261
271
|
*/
|
|
262
272
|
export async function sendOgImage(res, options = {}) {
|
|
263
273
|
const result = await ogImage(options);
|
|
264
|
-
const cacheControl = options.cacheControl ?? DEFAULT_CACHE;
|
|
265
274
|
|
|
266
275
|
res.status(200);
|
|
267
276
|
res.setHeader("Content-Type", result.contentType);
|
|
268
|
-
|
|
277
|
+
if (options.cacheControl != null) {
|
|
278
|
+
res.setHeader("Cache-Control", options.cacheControl);
|
|
279
|
+
} else {
|
|
280
|
+
setEdgeCacheHeaders(res, OG_EDGE_MAX_AGE, OG_EDGE_STALE);
|
|
281
|
+
}
|
|
269
282
|
res.setHeader("Content-Length", String(result.body.length));
|
|
270
283
|
// Kazıyıcılar ve CDN'ler için boyut ipucu (meta ile de verilir).
|
|
271
284
|
res.setHeader("X-Og-Width", String(result.width));
|
package/src/server/prewarm.js
CHANGED
|
@@ -23,6 +23,7 @@ import { getRequestContext } from "../http/request-context.js";
|
|
|
23
23
|
import { publicHost } from "./cache-vary.js";
|
|
24
24
|
import {
|
|
25
25
|
isHtmlCacheFresh,
|
|
26
|
+
isHtmlCacheKeyFresh,
|
|
26
27
|
startEarlyExpirySweep,
|
|
27
28
|
takeInvalidatedPaths,
|
|
28
29
|
takeInvalidatedTargets,
|
|
@@ -107,6 +108,16 @@ const CLASSIC_PREWARM_ENV = [
|
|
|
107
108
|
*/
|
|
108
109
|
const visitWarmSettings = { concurrency: null, rps: null };
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Süre dolumu ısıtması klasik turun `rps: 0` (sınırsız) ayarını kullanmaz.
|
|
113
|
+
* Bir TTL uçurumu tek seferde yüzlerce render başlatmasın.
|
|
114
|
+
*/
|
|
115
|
+
const EXPIRY_WARM_CONCURRENCY = 1;
|
|
116
|
+
const EXPIRY_WARM_RPS = 2;
|
|
117
|
+
|
|
118
|
+
/** @type {{ concurrency: number | null, rps: number | null }} */
|
|
119
|
+
const expiryWarmSettings = { concurrency: null, rps: null };
|
|
120
|
+
|
|
110
121
|
/**
|
|
111
122
|
* Isıtmanın canlı durumu. Dev araçları bunu okuyup ilerlemeyi gösterir;
|
|
112
123
|
* üretimde kimse okumazsa da maliyeti bir nesnedir.
|
|
@@ -208,6 +219,15 @@ function num(value, fallback) {
|
|
|
208
219
|
* @returns {number}
|
|
209
220
|
*/
|
|
210
221
|
function setting(envKey, configKey, fallback) {
|
|
222
|
+
if (
|
|
223
|
+
expiryWarmSettings.concurrency != null &&
|
|
224
|
+
configKey === "concurrency"
|
|
225
|
+
) {
|
|
226
|
+
return expiryWarmSettings.concurrency;
|
|
227
|
+
}
|
|
228
|
+
if (expiryWarmSettings.rps != null && configKey === "rps") {
|
|
229
|
+
return expiryWarmSettings.rps;
|
|
230
|
+
}
|
|
211
231
|
if (
|
|
212
232
|
visitWarmSettings.concurrency != null &&
|
|
213
233
|
configKey === "concurrency"
|
|
@@ -1015,11 +1035,13 @@ async function drainExpiryWarm() {
|
|
|
1015
1035
|
if (!targets.length) return;
|
|
1016
1036
|
|
|
1017
1037
|
expiryDraining = true;
|
|
1038
|
+
expiryWarmSettings.concurrency = EXPIRY_WARM_CONCURRENCY;
|
|
1039
|
+
expiryWarmSettings.rps = EXPIRY_WARM_RPS;
|
|
1018
1040
|
try {
|
|
1019
1041
|
/** @type {Map<string, string[]>} */
|
|
1020
1042
|
const byHost = new Map();
|
|
1021
1043
|
for (const target of targets) {
|
|
1022
|
-
if (
|
|
1044
|
+
if (isHtmlCacheKeyFresh(target.key)) continue;
|
|
1023
1045
|
const host = target.host || "";
|
|
1024
1046
|
const list = byHost.get(host);
|
|
1025
1047
|
if (list) list.push(target.path);
|
|
@@ -1053,6 +1075,8 @@ async function drainExpiryWarm() {
|
|
|
1053
1075
|
} catch (error) {
|
|
1054
1076
|
console.error("[prewarm] expiry warm failed", error);
|
|
1055
1077
|
} finally {
|
|
1078
|
+
expiryWarmSettings.concurrency = null;
|
|
1079
|
+
expiryWarmSettings.rps = null;
|
|
1056
1080
|
expiryDraining = false;
|
|
1057
1081
|
}
|
|
1058
1082
|
}
|
package/src/server/redis.js
CHANGED
|
@@ -22,6 +22,7 @@ import crypto from "node:crypto";
|
|
|
22
22
|
import { DEFAULT_REDIS } from "../config/defaults.js";
|
|
23
23
|
import { tryImportFromApp } from "../build/resolve-peer.mjs";
|
|
24
24
|
import { getBuildId } from "./assets.js";
|
|
25
|
+
import { decodeCacheValue, encodeCacheValue } from "./cache-blob.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* @typedef {import('../config/index.js').RedisConfig} RedisConfig
|
|
@@ -289,18 +290,34 @@ export async function redisGetJson(key) {
|
|
|
289
290
|
if (!usable()) return null;
|
|
290
291
|
|
|
291
292
|
try {
|
|
292
|
-
|
|
293
|
+
// `get` dizge döndürür ve ikili gövdeyi bozar. `getBuffer` yoksa (eski
|
|
294
|
+
// sahte istemci) düz JSON hâlâ `get` ile okunur.
|
|
295
|
+
const raw =
|
|
296
|
+
typeof client.getBuffer === "function" ? await client.getBuffer(key) : await client.get(key);
|
|
293
297
|
noteSuccess();
|
|
294
|
-
return raw
|
|
298
|
+
return raw == null ? null : decodeCacheValue(raw);
|
|
295
299
|
} catch (error) {
|
|
296
300
|
noteFailure("get", error);
|
|
297
301
|
return null;
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
304
|
|
|
305
|
+
/**
|
|
306
|
+
* @param {string} key
|
|
307
|
+
* @param {string | Buffer} payload
|
|
308
|
+
* @param {number} ttlMs
|
|
309
|
+
* @returns {Promise<void>}
|
|
310
|
+
*/
|
|
311
|
+
function writeRaw(key, payload, ttlMs) {
|
|
312
|
+
return Promise.resolve(client.set(key, payload, "PX", Math.ceil(ttlMs)))
|
|
313
|
+
.then(noteSuccess)
|
|
314
|
+
.catch((error) => noteFailure("set", error));
|
|
315
|
+
}
|
|
316
|
+
|
|
301
317
|
/**
|
|
302
318
|
* Ateşle-unut yazma. İsteğin yanıt yolunda beklenmez: HTML zaten L1'e
|
|
303
|
-
* yazıldı, Redis kopyası yalnızca diğer node'lar için.
|
|
319
|
+
* yazıldı, Redis kopyası yalnızca diğer node'lar için. 1 KB ve üstü gövdeler
|
|
320
|
+
* brotli ile yazılır; okuma düz JSON'u da kabul eder.
|
|
304
321
|
*
|
|
305
322
|
* @param {string} key
|
|
306
323
|
* @param {unknown} value
|
|
@@ -309,20 +326,22 @@ export async function redisGetJson(key) {
|
|
|
309
326
|
export function redisSetJson(key, value, ttlMs) {
|
|
310
327
|
if (!usable() || !(ttlMs > 0)) return;
|
|
311
328
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
payload = JSON.stringify(value);
|
|
316
|
-
} catch (error) {
|
|
329
|
+
const encoded = encodeCacheValue(value);
|
|
330
|
+
if (encoded === null) {
|
|
317
331
|
// Serileştirilemeyen değer (döngüsel referans, BigInt) sessizce atlanır;
|
|
318
332
|
// L1 kopyası çalışmaya devam eder.
|
|
319
|
-
noteFailure("serialize",
|
|
333
|
+
noteFailure("serialize", new Error("not JSON"));
|
|
320
334
|
return;
|
|
321
335
|
}
|
|
322
336
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
337
|
+
// Küçük gövde senkron döner ki testler `set`'ten hemen sonra kaydı okusun.
|
|
338
|
+
// Büyük gövdenin brotli'si MISS yanıtını bekletmez.
|
|
339
|
+
if (typeof encoded === "string" || Buffer.isBuffer(encoded)) {
|
|
340
|
+
writeRaw(key, encoded, ttlMs);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
encoded.then((payload) => writeRaw(key, payload, ttlMs));
|
|
326
345
|
}
|
|
327
346
|
|
|
328
347
|
/**
|