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.
Files changed (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +2 -0
  3. package/docs/02-mimari.md +1 -0
  4. package/docs/04-render-ve-sablonlar.md +9 -3
  5. package/docs/06-cache.md +53 -9
  6. package/docs/07-yapilandirma.md +1208 -1191
  7. package/docs/08-build.md +2 -1
  8. package/docs/10-dagitim.md +16 -6
  9. package/docs/12-panel-ve-oturum.md +2 -1
  10. package/docs/en/02-architecture.md +2 -1
  11. package/docs/en/04-rendering.md +9 -3
  12. package/docs/en/06-caching.md +54 -9
  13. package/docs/en/07-configuration.md +24 -9
  14. package/docs/en/08-build.md +2 -1
  15. package/docs/en/10-deployment.md +17 -6
  16. package/docs/en/12-dashboards-and-sessions.md +2 -1
  17. package/package.json +1 -1
  18. package/src/config/defaults.js +541 -518
  19. package/src/config/index.js +1500 -1456
  20. package/src/init.mjs +2 -0
  21. package/src/server/cache-blob.js +70 -0
  22. package/src/server/cache-control.js +45 -0
  23. package/src/server/data-cache.js +118 -27
  24. package/src/server/disk-cache.js +233 -0
  25. package/src/server/html-cache.js +90 -16
  26. package/src/server/image-optimizer.js +95 -2
  27. package/src/server/logs/file-sink.js +159 -32
  28. package/src/server/logs/pipeline.js +10 -3
  29. package/src/server/middleware/static-precompressed.js +31 -10
  30. package/src/server/og-image.js +17 -4
  31. package/src/server/prewarm.js +25 -1
  32. package/src/server/redis.js +31 -12
  33. package/src/server/render.js +910 -910
  34. package/types/config/defaults.d.ts +21 -1
  35. package/types/config/index.d.ts +14 -0
  36. package/types/server/cache-blob.d.ts +13 -0
  37. package/types/server/cache-control.d.ts +28 -0
  38. package/types/server/data-cache.d.ts +9 -0
  39. package/types/server/disk-cache.d.ts +36 -0
  40. package/types/server/html-cache.d.ts +26 -3
  41. package/types/server/logs/file-sink.d.ts +16 -5
  42. package/types/server/og-image.d.ts +5 -0
  43. package/types/server/redis.d.ts +2 -1
@@ -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
- const DEFAULT_CACHE =
22
- "public, max-age=0, s-maxage=86400, stale-while-revalidate=604800";
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
- res.setHeader("Cache-Control", cacheControl);
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));
@@ -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 (isHtmlCacheFresh(target.path, requestForHost(target.host))) continue;
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
  }
@@ -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
- const raw = await client.get(key);
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 === null ? null : JSON.parse(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
- /** @type {string} */
313
- let payload;
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", error);
333
+ noteFailure("serialize", new Error("not JSON"));
320
334
  return;
321
335
  }
322
336
 
323
- Promise.resolve(client.set(key, payload, "PX", Math.ceil(ttlMs)))
324
- .then(noteSuccess)
325
- .catch((error) => noteFailure("set", error));
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
  /**