jskelet 0.7.1 → 0.7.2

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.
@@ -39,6 +39,17 @@ const imageCacheIndex = new Map();
39
39
  /** @type {number} */
40
40
  let imageCacheBytes = 0;
41
41
 
42
+ /** Aynı url+w+q için eşzamanlı optimize birleşir. @type {Map<string, Promise<void>>} */
43
+ const optimizeInflight = new Map();
44
+
45
+ /**
46
+ * DNS güven kontrolü kısa TTL'li cache — her hop'ta tekrar lookup olmasın.
47
+ * @type {Map<string, { ok: boolean, until: number }>}
48
+ */
49
+ const dnsSafeCache = new Map();
50
+ const DNS_SAFE_TTL_MS = 60_000;
51
+ const MAX_DNS_SAFE = 500;
52
+
42
53
  const MAX_REDIRECTS = 5;
43
54
 
44
55
  /**
@@ -172,16 +183,27 @@ export function isRemoteUrlShapeAllowed(url, allowHosts) {
172
183
  */
173
184
  export async function assertResolvedHostSafe(hostname) {
174
185
  if (isBlockedAddress(hostname)) return false;
186
+
187
+ const cached = dnsSafeCache.get(hostname);
188
+ if (cached && Date.now() < cached.until) return cached.ok;
189
+
190
+ /** @type {boolean} */
191
+ let ok = false;
175
192
  try {
176
193
  const results = await dns.lookup(hostname, { all: true, verbatim: true });
177
- if (!results.length) return false;
178
- for (const { address } of results) {
179
- if (isBlockedAddress(address)) return false;
194
+ if (results.length) {
195
+ ok = results.every(({ address }) => !isBlockedAddress(address));
180
196
  }
181
- return true;
182
197
  } catch {
183
- return false;
198
+ ok = false;
199
+ }
200
+
201
+ if (dnsSafeCache.size >= MAX_DNS_SAFE) {
202
+ const oldest = dnsSafeCache.keys().next().value;
203
+ if (oldest !== undefined) dnsSafeCache.delete(oldest);
184
204
  }
205
+ dnsSafeCache.set(hostname, { ok, until: Date.now() + DNS_SAFE_TTL_MS });
206
+ return ok;
185
207
  }
186
208
 
187
209
  /**
@@ -355,32 +377,23 @@ async function handleOptimize(req, res, remote) {
355
377
  return;
356
378
  }
357
379
 
358
- const upstream = await fetchUpstream(allowed.href, remote);
359
- if (!upstream.ok) {
360
- res.status(502).type("text").send("Upstream image fetch failed");
380
+ let pending = optimizeInflight.get(key);
381
+ if (!pending) {
382
+ pending = materializeOptimized(allowed.href, filePath, width, quality, remote).finally(
383
+ () => {
384
+ optimizeInflight.delete(key);
385
+ },
386
+ );
387
+ optimizeInflight.set(key, pending);
388
+ }
389
+ await pending;
390
+
391
+ if (imageCacheIndex.has(filePath) || fs.existsSync(filePath)) {
392
+ sendCached(res, filePath, remote.cacheMaxAge);
361
393
  return;
362
394
  }
363
395
 
364
- const sharp = sharpModule.default;
365
- const buffer = await sharp(upstream.buffer, { failOn: "none" })
366
- .rotate()
367
- .resize({
368
- width,
369
- withoutEnlargement: true,
370
- fit: "inside",
371
- })
372
- .webp({ quality, effort: 4 })
373
- .toBuffer();
374
-
375
- // Atomik yaz: yarım dosya immutable cache'e düşmesin.
376
- const tmp = `${filePath}.${process.pid}.tmp`;
377
- await fs.promises.writeFile(tmp, buffer);
378
- await fs.promises.rename(tmp, filePath);
379
- // Yeni dosya için yer aç: en eski düşer. Az önce yazılan, tek başına
380
- // tavanı aşıyorsa kalır; bir sonraki yazım onu düşürür. Silme, bu
381
- // yanıtın `sendFile`'ından önce olmasın.
382
- noteImageCacheFile(filePath, buffer.length);
383
- sendCached(res, filePath, remote.cacheMaxAge);
396
+ res.status(502).type("text").send("Upstream image fetch failed");
384
397
  } catch (error) {
385
398
  console.warn("[images.remote] optimize failed:", error);
386
399
  // Bozuk kaynakta sayfa boş kalmasın: orijinale düş.
@@ -388,6 +401,37 @@ async function handleOptimize(req, res, remote) {
388
401
  }
389
402
  }
390
403
 
404
+ /**
405
+ * @param {string} href
406
+ * @param {string} filePath
407
+ * @param {number} width
408
+ * @param {number} quality
409
+ * @param {import('../config/index.js').ImagesRemoteConfig} remote
410
+ * @returns {Promise<void>}
411
+ */
412
+ async function materializeOptimized(href, filePath, width, quality, remote) {
413
+ if (imageCacheIndex.has(filePath) || fs.existsSync(filePath)) return;
414
+
415
+ const upstream = await fetchUpstream(href, remote);
416
+ if (!upstream.ok) return;
417
+
418
+ const sharp = /** @type {NonNullable<typeof sharpModule>} */ (sharpModule).default;
419
+ const buffer = await sharp(upstream.buffer, { failOn: "none" })
420
+ .rotate()
421
+ .resize({
422
+ width,
423
+ withoutEnlargement: true,
424
+ fit: "inside",
425
+ })
426
+ .webp({ quality, effort: 4 })
427
+ .toBuffer();
428
+
429
+ const tmp = `${filePath}.${process.pid}.tmp`;
430
+ await fs.promises.writeFile(tmp, buffer);
431
+ await fs.promises.rename(tmp, filePath);
432
+ noteImageCacheFile(filePath, buffer.length);
433
+ }
434
+
391
435
  /**
392
436
  * Redirect'leri elle takip eder; her hop allowlist + DNS private kontrolünden geçer.
393
437
  *
@@ -22,7 +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
+ import { decodeCacheValueAsync, encodeCacheValue } from "./cache-blob.js";
26
26
 
27
27
  /**
28
28
  * @typedef {import('../config/index.js').RedisConfig} RedisConfig
@@ -295,7 +295,7 @@ export async function redisGetJson(key) {
295
295
  const raw =
296
296
  typeof client.getBuffer === "function" ? await client.getBuffer(key) : await client.get(key);
297
297
  noteSuccess();
298
- return raw == null ? null : decodeCacheValue(raw);
298
+ return raw == null ? null : await decodeCacheValueAsync(raw);
299
299
  } catch (error) {
300
300
  noteFailure("get", error);
301
301
  return null;
@@ -58,16 +58,22 @@ const isDev = process.env.NODE_ENV === "development";
58
58
  * taraması dosya sistemine dokunduğu için bir kez yapılır; config yüklenmeden
59
59
  * hesaplanamaz, bu yüzden ilk render'da kurulur.
60
60
  *
61
- * @type {{ helpers: Record<string, unknown>, options: Record<string, unknown>,
61
+ * Sayfa `.jsk` şablonları lazy yüklenir; layout + `components/*.jsk` eager.
62
+ *
63
+ * @typedef {{ helpers: Record<string, unknown>, options: Record<string, unknown>,
62
64
  * viewsDir: string, viewRoots: string[], layout: string,
63
65
  * compiled: Map<string, (data: object, helpers: object) => string>,
66
+ * loading: Map<string, Promise<(data: object, helpers: object) => string | null>>,
67
+ * manifest: Record<string, string>, templatesDir: string, bust: string,
64
68
  * layoutRender: ((data: object, helpers: object) => string) | null
65
- * } | null}
69
+ * }} RenderEngine
66
70
  */
71
+
72
+ /** @type {RenderEngine | null} */
67
73
  let engine = null;
68
74
 
69
75
  /**
70
- * @returns {Promise<NonNullable<typeof engine>>}
76
+ * @returns {Promise<RenderEngine>}
71
77
  */
72
78
  async function getEngine() {
73
79
  if (engine) return engine;
@@ -104,14 +110,11 @@ async function getEngine() {
104
110
  if (!(pascal in helpers)) helpers[pascal] = value;
105
111
  }
106
112
 
107
- const { compiled, layoutRender: appLayoutRender } = await loadCompiledTemplates(
108
- config,
109
- helpers,
110
- );
113
+ const loaded = await loadCompiledTemplates(config, helpers);
111
114
 
112
115
  // Yalnızca framework varsayılan layout'una düşüldüğünde hazır modülü kullan;
113
116
  // uygulamanın kendi layout.jsk'si derlenmemişse sessizce framework'e kayma.
114
- let layoutRender = appLayoutRender;
117
+ let layoutRender = loaded.layoutRender;
115
118
  if (!layoutRender) {
116
119
  const fwLayout = path.join(FRAMEWORK_ROOT, "src", "templates", "layout.jsk");
117
120
  if (path.resolve(config.layout) === path.resolve(fwLayout)) {
@@ -125,7 +128,11 @@ async function getEngine() {
125
128
  viewRoots,
126
129
  layout: config.layout,
127
130
  helpers,
128
- compiled,
131
+ compiled: loaded.compiled,
132
+ loading: new Map(),
133
+ manifest: loaded.manifest,
134
+ templatesDir: loaded.templatesDir,
135
+ bust: loaded.bust,
129
136
  layoutRender,
130
137
  options: {
131
138
  // `include('partials/header')` gibi çağrılar views kökünden çözülür.
@@ -141,11 +148,48 @@ async function getEngine() {
141
148
  }
142
149
 
143
150
  /**
144
- * Derlenmiş `.jsk` modüllerini yükler. İstek anında parse/compile yok.
151
+ * Manifest'ten tek bir derlenmiş view yükler. Include'lar modül grafiğinde
152
+ * statik import ile gelir.
153
+ *
154
+ * @param {RenderEngine} eng
155
+ * @param {string} viewId
156
+ * @returns {Promise<((data: object, helpers: object) => string) | null>}
157
+ */
158
+ async function ensureCompiledView(eng, viewId) {
159
+ const hit = eng.compiled.get(viewId);
160
+ if (hit) return hit;
161
+
162
+ const rel = eng.manifest[viewId];
163
+ if (!rel) return null;
164
+
165
+ const pending = eng.loading.get(viewId);
166
+ if (pending) return pending;
167
+
168
+ const task = (async () => {
169
+ const file = path.join(eng.templatesDir, rel);
170
+ try {
171
+ const mod = await import(pathToFileURL(file).href + eng.bust);
172
+ if (typeof mod.render !== "function") return null;
173
+ eng.compiled.set(viewId, mod.render);
174
+ return mod.render;
175
+ } catch {
176
+ return null;
177
+ } finally {
178
+ eng.loading.delete(viewId);
179
+ }
180
+ })();
181
+
182
+ eng.loading.set(viewId, task);
183
+ return task;
184
+ }
185
+
186
+ /**
187
+ * Layout + `components/*.jsk` eager; sayfa şablonları ilk `renderView`'da.
145
188
  *
146
189
  * @param {import('../config/index.js').ResolvedConfig} config
147
190
  * @param {Record<string, unknown>} helpers
148
- * @returns {Promise<{ compiled: Map<string, Function>, layoutRender: Function | null }>}
191
+ * @returns {Promise<{ compiled: Map<string, Function>, layoutRender: Function | null,
192
+ * manifest: Record<string, string>, templatesDir: string, bust: string }>}
149
193
  */
150
194
  async function loadCompiledTemplates(config, helpers) {
151
195
  /** @type {Map<string, (data: object, helpers: object) => string>} */
@@ -154,16 +198,26 @@ async function loadCompiledTemplates(config, helpers) {
154
198
  const manifestPath = path.join(templatesDir, "manifest.json");
155
199
 
156
200
  if (!fs.existsSync(manifestPath)) {
157
- return { compiled, layoutRender: null };
201
+ return {
202
+ compiled,
203
+ layoutRender: null,
204
+ manifest: {},
205
+ templatesDir,
206
+ bust: "",
207
+ };
158
208
  }
159
209
 
160
210
  /** @type {Record<string, string>} */
161
211
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
162
212
  const bust = isDev ? `?t=${Date.now()}` : "";
163
213
 
164
- // Paralel import — sıralı await cold start'ı şişiriyordu.
214
+ const eagerIds = Object.keys(manifest).filter(
215
+ (id) => id === "layout" || id.startsWith("components/"),
216
+ );
217
+
165
218
  const entries = await Promise.all(
166
- Object.entries(manifest).map(async ([viewId, rel]) => {
219
+ eagerIds.map(async (viewId) => {
220
+ const rel = manifest[viewId];
167
221
  const file = path.join(templatesDir, rel);
168
222
  try {
169
223
  const mod = await import(pathToFileURL(file).href + bust);
@@ -178,7 +232,6 @@ async function loadCompiledTemplates(config, helpers) {
178
232
  if (!mod || typeof mod.render !== "function") continue;
179
233
  compiled.set(viewId, mod.render);
180
234
 
181
- // `components/*.jsk` → helpers.PascalName
182
235
  if (viewId.startsWith("components/")) {
183
236
  const base = viewId.slice("components/".length).split("/").pop() ?? "";
184
237
  const pascal = base
@@ -193,7 +246,24 @@ async function loadCompiledTemplates(config, helpers) {
193
246
  }
194
247
 
195
248
  const layoutRender = compiled.get("layout") ?? null;
196
- return { compiled, layoutRender };
249
+ return { compiled, layoutRender, manifest, templatesDir, bust };
250
+ }
251
+
252
+ /**
253
+ * Listen öncesi: motor + asset manifest ısınır (sayfa prewarm değil).
254
+ *
255
+ * @returns {Promise<void>}
256
+ */
257
+ export async function warmRenderEngine() {
258
+ await getEngine();
259
+ hasAsset("app.css");
260
+ hasAsset("main.js");
261
+ try {
262
+ const { getBuildId } = await import("./assets.js");
263
+ getBuildId();
264
+ } catch {
265
+ /* yoksa sessiz */
266
+ }
197
267
  }
198
268
 
199
269
  /**
@@ -232,8 +302,13 @@ export function resetRenderEngine() {
232
302
  * @returns {Promise<string>}
233
303
  */
234
304
  export async function renderView(view, data = {}) {
235
- const { helpers, options, compiled, viewRoots, viewsDir } = await getEngine();
236
- const renderFn = compiled.get(view);
305
+ const eng = await getEngine();
306
+ const { helpers, options, viewRoots, viewsDir } = eng;
307
+
308
+ let renderFn = eng.compiled.get(view) ?? null;
309
+ if (!renderFn && eng.manifest[view]) {
310
+ renderFn = await ensureCompiledView(eng, view);
311
+ }
237
312
  if (renderFn) {
238
313
  return renderFn({ ...data }, helpers);
239
314
  }
@@ -256,17 +331,31 @@ export async function renderPage(page) {
256
331
  const { helpers, options, layout, layoutRender } = await getEngine();
257
332
  const config = getConfig();
258
333
 
259
- const metadata = {
334
+ // Gövde `page.metadata` ile başlar (hook beklemez). Head/layout birleşik
335
+ // metadata'yı alır — siteName/titleTemplate gibi hook alanları layout'ta.
336
+ const pageMeta = page.metadata ?? {};
337
+ const metadataPromise = (async () => ({
260
338
  ...(await hook("metadata", {}, page)),
261
- ...(page.metadata ?? {}),
262
- };
339
+ ...pageMeta,
340
+ }))();
263
341
 
264
- // Layout bağlamı ve gövde paralel üretilir: navigasyon çoğu projede
265
- // upstream'den geliyor ve gövde render'ıyla sırayla beklemek her sayfaya
266
- // gereksiz gecikme ekliyor.
267
- const [body, context] = await Promise.all([
268
- renderView(page.view, { ...(page.data ?? {}), metadata }),
269
- hook("layoutContext", {}, { pathname: page.pathname ?? "", metadata }),
342
+ const bodyPromise = renderView(page.view, {
343
+ ...(page.data ?? {}),
344
+ metadata: pageMeta,
345
+ });
346
+
347
+ // metadata → layoutContext zinciri gövdeyle paralel.
348
+ const layoutPromise = metadataPromise.then(async (metadata) => {
349
+ const context = await hook("layoutContext", {}, {
350
+ pathname: page.pathname ?? "",
351
+ metadata,
352
+ });
353
+ return { metadata, context };
354
+ });
355
+
356
+ const [body, { metadata, context }] = await Promise.all([
357
+ bodyPromise,
358
+ layoutPromise,
270
359
  ]);
271
360
 
272
361
  // Layout local'leri. `.jsk` `(data, helpers)` aldığı için helpers'ı data'ya
@@ -694,31 +783,28 @@ async function sendHtml(req, res, body, encoded, options = {}) {
694
783
  const encoding =
695
784
  req.method === "HEAD" ? null : negotiateEncoding(req.headers["accept-encoding"]);
696
785
 
697
- if (!encoding || !encoded) {
786
+ if (!encoding) {
698
787
  if (options.etag === false) {
699
- // Sıkıştırma middleware'i devreye girerse Content-Length'i kendisi
700
- // kaldırır; girmezse doğru uzunlukla gider.
701
788
  res.setHeader("Content-Length", String(Buffer.byteLength(body)));
702
789
  res.end(req.method === "HEAD" ? undefined : body);
703
790
  return;
704
791
  }
705
-
706
792
  res.send(body);
707
793
  return;
708
794
  }
709
795
 
710
- let buffer = encoded.get(encoding);
796
+ // Public cache haritası veya private/fragment: HTML'i burada sıkıştır —
797
+ // middleware'in ikinci kez buffer'lamasını ve private yolda q=5 maliyetini kes.
798
+ let buffer = encoded?.get(encoding);
711
799
  if (!buffer) {
712
800
  buffer = await encodeText(body, encoding);
713
- // Ham HTML kalır; sıkıştırılmış kopya tek kodlamadır. Diğer istemci
714
- // gelince eski buffer düşer, bayt bütçesi iki gövdeyi birden saymaz.
715
- rememberHtmlEncoding(encoded, encoding, buffer);
801
+ if (encoded) rememberHtmlEncoding(encoded, encoding, buffer);
716
802
  }
717
803
 
718
804
  res.setHeader("Content-Encoding", encoding);
719
805
  res.setHeader("Vary", "Accept-Encoding");
720
806
  res.setHeader("Content-Length", String(buffer.length));
721
- res.end(buffer);
807
+ res.end(req.method === "HEAD" ? undefined : buffer);
722
808
  }
723
809
 
724
810
  /**
@@ -770,12 +856,47 @@ async function produce(controller, ctx) {
770
856
  if (!first.transient.length) return { html: await renderNotFound(), status: 404 };
771
857
 
772
858
  // Buraya gelindiyse `notFound()` veri gelmediği için çağrılmış. **Var olan
773
- // bir sayfayı** 404 olarak servis etmek en kötü sonuç: arama motoru geçici
774
- // bir rate limit'i kalıcı bir kayıp sanar. Bu yüzden sayfa yeniden denenir —
775
- // ısıtma günlükleri gösteriyor ki aynı yol saniyeler sonra 200 dönüyor.
776
- const { attempts, delayMs } = transientRetry();
859
+ // bir sayfayı** 404 olarak servis etmek en kötü sonuç. Varsayılan: hemen
860
+ // 503 dön, retry'yi arka planda çalıştır (data cache ısınır). `background:
861
+ // false` eski senkron retry davranışını geri getirir.
862
+ const { attempts, delayMs, background } = transientRetry();
777
863
  let failures = first.transient;
778
864
 
865
+ if (background && attempts > 0) {
866
+ const retryDetail =
867
+ `returned notFound() while upstream is failing ` +
868
+ `(${summarizeFailures(failures)}), retrying in background (${attempts}×)`;
869
+ if (!suppressForPrewarm(retryDetail)) {
870
+ console.warn(`[render] ${ctx.pathname} ${retryDetail}`);
871
+ }
872
+
873
+ void (async () => {
874
+ for (let round = 1; round <= attempts; round += 1) {
875
+ await sleep(delayMs * round);
876
+ try {
877
+ const retried = await withUpstreamTracking(() =>
878
+ withRequestCache(() => attempt(controller, ctx)),
879
+ );
880
+ if ("page" in retried || !retried.transient.length) return;
881
+ failures = retried.transient;
882
+ } catch (error) {
883
+ console.warn(
884
+ `[render] ${ctx.pathname} background retry failed`,
885
+ error instanceof Error ? error.message : error,
886
+ );
887
+ return;
888
+ }
889
+ }
890
+ })();
891
+
892
+ return {
893
+ html: await renderStatusPage(503),
894
+ status: 503,
895
+ degraded: true,
896
+ retryAfter: RETRY_AFTER_SECONDS,
897
+ };
898
+ }
899
+
779
900
  for (let round = 1; round <= attempts; round += 1) {
780
901
  const retryDetail =
781
902
  `returned notFound() while upstream is failing ` +
@@ -784,29 +905,20 @@ async function produce(controller, ctx) {
784
905
  console.warn(`[render] ${ctx.pathname} ${retryDetail}`);
785
906
  }
786
907
 
787
- // Beklemeden tekrar denemek rate limit'e girmiş bir API'de aynı 429'u
788
- // getirir; kısa bekleme hem pencerenin dönmesine şans verir hem de
789
- // fırtınayı büyütmez.
790
908
  await sleep(delayMs * round);
791
909
 
792
- // Her deneme kendi upstream ve istek içi cache bağlamında çalışır: ilk
793
- // turun hataları ikinci turun kararını kirletmesin ve memoize edilmiş
794
- // boş cevaplar tekrar kullanılmasın.
795
910
  const retried = await withUpstreamTracking(() =>
796
911
  withRequestCache(() => attempt(controller, ctx)),
797
912
  );
798
913
 
799
914
  if ("page" in retried) return retried.page;
800
915
  if (!retried.transient.length) {
801
- // Bu kez upstream sağlam cevap verdi ve "yok" dedi: gerçek 404.
802
916
  return { html: await renderNotFound(), status: 404 };
803
917
  }
804
918
 
805
919
  failures = retried.transient;
806
920
  }
807
921
 
808
- // Denemeler tükendi. 404 yerine 503: önbelleğe girmez, `Retry-After` taşır
809
- // ve bir sonraki istek gerçek içeriği üretebilir.
810
922
  const outageDetail =
811
923
  `could not be produced, upstream is still failing ` +
812
924
  `(${summarizeFailures(failures)}), serving an uncached 503 instead of a 404`;
@@ -830,11 +942,10 @@ async function produce(controller, ctx) {
830
942
  const RETRY_AFTER_SECONDS = 30;
831
943
 
832
944
  /**
833
- * Tekrar denemenin maliyeti upstream'e binen ikinci bir istek turu; bu yüzden
834
- * varsayılan tek deneme ve kısa bekleme. Rate limit fırtınasında toplam yük
835
- * iki katına çıkabilir, ama alternatifi var olan sayfaları 404'e düşürmek.
945
+ * Varsayılan: arka plan retry + hemen 503 (TTFB'yi sleep ile şişirmez).
946
+ * `background: false` → eski senkron döngü.
836
947
  *
837
- * @returns {{ attempts: number, delayMs: number }}
948
+ * @returns {{ attempts: number, delayMs: number, background: boolean }}
838
949
  */
839
950
  function transientRetry() {
840
951
  const raw = /** @type {any} */ (getConfig().transientRetry ?? {});
@@ -844,6 +955,7 @@ function transientRetry() {
844
955
  return {
845
956
  attempts: Number.isFinite(attempts) && attempts >= 0 ? Math.floor(attempts) : 1,
846
957
  delayMs: Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 300,
958
+ background: raw.background !== false,
847
959
  };
848
960
  }
849
961
 
@@ -16,6 +16,8 @@
16
16
  * `data-island-idle` görünür olsa bile sayfa sakinleşene (load + boş zaman)
17
17
  * kadar bekletir. İlk ekranda görünen ama kritik olmayan ağır modüller
18
18
  * (ör. grafik kütüphanesi çeken mini grafik) LCP ile yarışmasın diye.
19
+ * Gizli (zero-box) host'larda da aynı yol kullanılır — `hydrate()` onları
20
+ * `mountWhenReady` ile bağlar, hemen `mountIsland` çağırmaz.
19
21
  *
20
22
  * `data-island-props` JSON ise parse edilip props olarak geçer.
21
23
  */
@@ -175,6 +175,11 @@ export declare const DATA_CACHE_MAX_ENTRIES_CEILING = 20000;
175
175
  export declare const DEFAULT_TRANSIENT_RETRY: {
176
176
  attempts: number;
177
177
  delayMs: number;
178
+ /**
179
+ * `true` (varsayılan): ilk transient `notFound`ta hemen 503; retry arka
180
+ * planda data cache'i ısıtır. `false`: yanıt öncesi senkron retry (eski).
181
+ */
182
+ background: boolean;
178
183
  };
179
184
  /**
180
185
  * Upstream veri önbelleği.
@@ -236,9 +241,8 @@ export declare const DEFAULT_UPSTREAM_LIMIT: {
236
241
  * invalidation'ı bütün node'lara yaymak. Tek instance çalışan bir kurulumda
237
242
  * kazanç neredeyse yok; bu yüzden `enabled` varsayılan olarak kapalı.
238
243
  *
239
- * `storeEncoded` kapalı, çünkü sıkıştırılmış gövdeleri de paylaşmak girdi
240
- * başına boyutu iki-üç katına çıkarır ve brotli'yi yeniden üretmek Redis'ten
241
- * indirmekten çoğu zaman daha ucuz.
244
+ * `storeEncoded` açık: soğuk replica L2 HIT'te yeniden brotli ödemesin.
245
+ * Boyut artar; tek encoding (br) tutulduğu için maliyet sınırlıdır.
242
246
  */
243
247
  export declare const DEFAULT_REDIS: {
244
248
  enabled: boolean;
@@ -251,7 +255,7 @@ export declare const DEFAULT_REDIS: {
251
255
  html: boolean;
252
256
  /** Veri önbelleği paylaşılsın mı. */
253
257
  data: boolean;
254
- /** Brotli/gzip gövdeleri de paylaşılsın mı. */
258
+ /** Brotli gövdeleri de paylaşılsın mı (yazma sonrası preencode). */
255
259
  storeEncoded: boolean;
256
260
  /** pub/sub üzerinden invalidation yayını. */
257
261
  events: boolean;
@@ -147,6 +147,7 @@ export type ResolvedConfig = {
147
147
  transientRetry: {
148
148
  attempts: number;
149
149
  delayMs: number;
150
+ background?: boolean;
150
151
  };
151
152
  /**
152
153
  * Opsiyonel Redis ikinci kademesi.
package/types/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * controller'lar bu yüzeyi kullanır. Alt yollardan (`jskelet/server`)
6
6
  * ithal etmek de mümkün; buradaki liste "kararlı" sayılan yüzeydir.
7
7
  */
8
- export { route, fragment, renderPage, renderView, renderNotFound, } from "./server/render.js";
8
+ export { route, fragment, renderPage, renderView, renderNotFound, warmRenderEngine, resetRenderEngine, } from "./server/render.js";
9
9
  export { renderStatusPage, statusFromError } from "./server/status-page.js";
10
10
  export { createApp, startServer } from "./server/create-app.js";
11
11
  export { notFound, redirect, permanentRedirect, seeOther, isNotFoundError, isRedirectError, NotFoundError, RedirectError, } from "./http/control-flow.js";
@@ -7,7 +7,15 @@
7
7
  */
8
8
  export declare function encodeCacheValue(value: unknown): string | Buffer | Promise<string | Buffer> | null;
9
9
  /**
10
+ * Senkron çözücü — küçük test / senkron yollar için. Hot path
11
+ * `decodeCacheValueAsync` kullanır (event loop'u brotli ile bloklamaz).
12
+ *
10
13
  * @param {Buffer | string} raw
11
14
  * @returns {unknown}
12
15
  */
13
16
  export declare function decodeCacheValue(raw: Buffer | string): unknown;
17
+ /**
18
+ * @param {Buffer | string} raw
19
+ * @returns {Promise<unknown>}
20
+ */
21
+ export declare function decodeCacheValueAsync(raw: Buffer | string): Promise<unknown>;
@@ -3,6 +3,7 @@ export type DataEntry = {
3
3
  expiresAt: number;
4
4
  staleUntil: number;
5
5
  bytes: number;
6
+ produceMs: number;
6
7
  };
7
8
  /**
8
9
  * Süreç ömrü boyunca biriken sayaçlar.
@@ -50,10 +50,8 @@ export type HtmlEntry = {
50
50
  */
51
51
  export declare function earlyRefreshLeadMs(produceMs: number, ttlMs: number): number;
52
52
  /**
53
- * Sıkıştırılmış gövde `install()`'dan sonra, ilk brotli/gzip yanıtında
54
- * girdinin `encoded` haritasına eklenir. Sayacı delta ile büyütmek, o sıra
55
- * LRU'dan düşmüş bir haritaya yazınca bir daha inmeyen bir artık bırakır;
56
- * store'dan yeniden okumak o artığı taşımaz.
53
+ * Bayt sayacını store'dan yeniden hesaplar. Testler elle `encoded` doldurunca
54
+ * çağırır; hot path `rememberHtmlEncoding` delta kullanır.
57
55
  *
58
56
  * @returns {void}
59
57
  */
@@ -202,7 +200,8 @@ export declare function isHtmlCacheFresh(pathname: string, req?: {
202
200
  export declare function isHtmlCacheKeyFresh(key: string): boolean;
203
201
  /**
204
202
  * Girdide en fazla bir sıkıştırılmış gövde durur. Yeni kodlama eskisinin
205
- * yerini alır; ham HTML kalır. Bayt sayacı `noteHtmlCacheGrowth` ile işlenir.
203
+ * yerini alır; ham HTML kalır. Bayt sayacı delta ile güncellenir — harita
204
+ * LRU'dan düşmüşse (store'da yok) sayaç oynatılmaz.
206
205
  *
207
206
  * @param {Map<string, Buffer>} encoded
208
207
  * @param {string} encoding
@@ -1,3 +1,22 @@
1
+ export type RenderEngine = {
2
+ helpers: Record<string, unknown>;
3
+ options: Record<string, unknown>;
4
+ viewsDir: string;
5
+ viewRoots: string[];
6
+ layout: string;
7
+ compiled: Map<string, (data: object, helpers: object) => string>;
8
+ loading: Map<string, Promise<(data: object, helpers: object) => string | null>>;
9
+ manifest: Record<string, string>;
10
+ templatesDir: string;
11
+ bust: string;
12
+ layoutRender: ((data: object, helpers: object) => string) | null;
13
+ };
14
+ /**
15
+ * Listen öncesi: motor + asset manifest ısınır (sayfa prewarm değil).
16
+ *
17
+ * @returns {Promise<void>}
18
+ */
19
+ export declare function warmRenderEngine(): Promise<void>;
1
20
  /**
2
21
  * Uygulama geliştirirken bileşen dosyaları değişince kayıt yenilenmeli.
3
22
  * Dev sunucusu süreci yeniden başlattığı için normalde gerekmez; gömülü