jskelet 0.7.0 → 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.
- package/CHANGELOG.md +56 -0
- package/docs/04-render-ve-sablonlar.md +20 -9
- package/docs/05-islands.md +8 -4
- package/docs/06-cache.md +18 -16
- package/docs/07-yapilandirma.md +8 -7
- package/docs/en/04-rendering.md +21 -9
- package/docs/en/05-islands.md +8 -4
- package/docs/en/06-caching.md +18 -16
- package/docs/en/07-configuration.md +8 -6
- package/package.json +1 -1
- package/src/build/build.mjs +23 -8
- package/src/build/tasks/precompress.mjs +51 -11
- package/src/client/registry.js +13 -3
- package/src/compile/codegen.js +70 -4
- package/src/compile/compile-all.js +18 -3
- package/src/compile/index.js +6 -1
- package/src/config/defaults.js +9 -5
- package/src/config/index.js +3 -3
- package/src/http/request-cache.js +19 -1
- package/src/index.js +2 -0
- package/src/server/assets.js +51 -22
- package/src/server/cache-blob.js +33 -0
- package/src/server/create-app.js +5 -0
- package/src/server/data-cache.js +77 -11
- package/src/server/disk-cache.js +2 -2
- package/src/server/head-hints.js +21 -9
- package/src/server/html-cache.js +169 -39
- package/src/server/image-optimizer.js +72 -28
- package/src/server/redis.js +2 -2
- package/src/server/render.js +183 -60
- package/src/templates/layout.jsk +4 -4
- package/src/templates/layout.render.js +2 -21
- package/src/views/components/loader.js +11 -6
- package/src/views/helpers/html.js +13 -5
- package/src/views/helpers/tags.js +21 -4
- package/types/client/registry.d.ts +2 -0
- package/types/compile/codegen.d.ts +9 -4
- package/types/compile/compile-all.d.ts +3 -1
- package/types/compile/index.d.ts +1 -1
- package/types/config/defaults.d.ts +8 -4
- package/types/config/index.d.ts +1 -0
- package/types/index.d.ts +1 -1
- package/types/server/cache-blob.d.ts +8 -0
- package/types/server/data-cache.d.ts +1 -0
- package/types/server/html-cache.d.ts +4 -5
- package/types/server/render.d.ts +19 -0
package/src/compile/codegen.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AST → ESM kaynak metni. `eval` / `new Function` yok; çıktı diskteki
|
|
3
3
|
* `.mjs` dosyasına yazılır ve runtime'da normal `import` ile yüklenir.
|
|
4
|
+
*
|
|
5
|
+
* Statik HTML parçaları bitişik `s +=` yerine tek literal'de birleşir;
|
|
6
|
+
* şablon girintisi (satır sonu içeren salt-whitespace) derlemede düşer.
|
|
7
|
+
* `pre` / `textarea` / `script` / `style` / `code` içinde whitespace korunur.
|
|
4
8
|
*/
|
|
5
9
|
import { codegenExpr } from "./expr.js";
|
|
6
10
|
import { CompileError } from "./errors.js";
|
|
@@ -10,6 +14,9 @@ import { CompileError } from "./errors.js";
|
|
|
10
14
|
* @typedef {import('./parse.js').AttrNode} AttrNode
|
|
11
15
|
*/
|
|
12
16
|
|
|
17
|
+
/** İçinde girinti sıkıştırması yapılmayan etiketler. */
|
|
18
|
+
const PRESERVE_WS = new Set(["pre", "textarea", "script", "style", "code"]);
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* @param {AstNode[]} ast
|
|
15
22
|
* @param {{ viewId: string, file?: string, knownComponents?: Set<string> }} options
|
|
@@ -49,6 +56,21 @@ export function toPascalCase(name) {
|
|
|
49
56
|
.join("");
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Şablon girintisini düşür: satır sonu içeren salt-whitespace → boş;
|
|
61
|
+
* yalnızca boşluk/tab → tek boşluk (inline aralık). Koruma alanı dışında.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} value
|
|
64
|
+
* @param {boolean} preserve
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function collapseTemplateWhitespace(value, preserve) {
|
|
68
|
+
if (preserve || !value) return value;
|
|
69
|
+
if (!/^[\t\n\r\f\v ]+$/.test(value)) return value;
|
|
70
|
+
if (/[\n\r]/.test(value)) return "";
|
|
71
|
+
return " ";
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
class CodegenContext {
|
|
53
75
|
/**
|
|
54
76
|
* @param {{ viewId: string, file?: string, knownComponents?: Set<string> }} options
|
|
@@ -71,6 +93,10 @@ class CodegenContext {
|
|
|
71
93
|
this.locals = new Set();
|
|
72
94
|
/** Çocuk gövdesinde `s` yerine kullanılacak değişken. */
|
|
73
95
|
this.outVar = "s";
|
|
96
|
+
/** Bitişik statik parçalar — flush edilene kadar birikir. */
|
|
97
|
+
this.staticBuf = "";
|
|
98
|
+
/** `pre` vb. iç içe derinliği. */
|
|
99
|
+
this.preserveWs = 0;
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
/**
|
|
@@ -81,9 +107,28 @@ class CodegenContext {
|
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
/**
|
|
110
|
+
* Birikmiş statik HTML'i tek `+=` olarak yazar.
|
|
111
|
+
* @returns {void}
|
|
112
|
+
*/
|
|
113
|
+
flushStatic() {
|
|
114
|
+
if (!this.staticBuf) return;
|
|
115
|
+
this.line(`${this.outVar} += ${JSON.stringify(this.staticBuf)};`);
|
|
116
|
+
this.staticBuf = "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} text
|
|
121
|
+
*/
|
|
122
|
+
appendStatic(text) {
|
|
123
|
+
if (text) this.staticBuf += text;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Dinamik ifade — önce statik tamponu boşaltır.
|
|
84
128
|
* @param {string} exprJs
|
|
85
129
|
*/
|
|
86
130
|
append(exprJs) {
|
|
131
|
+
this.flushStatic();
|
|
87
132
|
this.line(`${this.outVar} += ${exprJs};`);
|
|
88
133
|
}
|
|
89
134
|
|
|
@@ -105,9 +150,14 @@ class CodegenContext {
|
|
|
105
150
|
*/
|
|
106
151
|
emit(node) {
|
|
107
152
|
switch (node.type) {
|
|
108
|
-
case "Text":
|
|
109
|
-
|
|
153
|
+
case "Text": {
|
|
154
|
+
const value = collapseTemplateWhitespace(
|
|
155
|
+
node.value,
|
|
156
|
+
this.preserveWs > 0,
|
|
157
|
+
);
|
|
158
|
+
if (value) this.appendStatic(value);
|
|
110
159
|
break;
|
|
160
|
+
}
|
|
111
161
|
case "Interp": {
|
|
112
162
|
const expr = codegenExpr(node.expr, this.locals);
|
|
113
163
|
if (node.raw) this.append(`(${expr} ?? "")`);
|
|
@@ -139,17 +189,21 @@ class CodegenContext {
|
|
|
139
189
|
*/
|
|
140
190
|
emitElement(node) {
|
|
141
191
|
const bound = node.attrs.some((a) => a.bound);
|
|
192
|
+
const preserve = PRESERVE_WS.has(node.name.toLowerCase());
|
|
193
|
+
|
|
142
194
|
if (bound) {
|
|
143
195
|
this.append(
|
|
144
196
|
`"<${node.name}" + helpers.attrs({${this.attrsObject(node.attrs)}}) + ">"`,
|
|
145
197
|
);
|
|
146
198
|
} else {
|
|
147
|
-
this.
|
|
199
|
+
this.appendStatic(`<${node.name}${staticAttrString(node.attrs)}>`);
|
|
148
200
|
}
|
|
149
201
|
|
|
150
202
|
if (!node.selfClosing) {
|
|
203
|
+
if (preserve) this.preserveWs += 1;
|
|
151
204
|
this.emitNodes(node.children);
|
|
152
|
-
this.
|
|
205
|
+
if (preserve) this.preserveWs -= 1;
|
|
206
|
+
this.appendStatic(`</${node.name}>`);
|
|
153
207
|
}
|
|
154
208
|
}
|
|
155
209
|
|
|
@@ -186,13 +240,18 @@ class CodegenContext {
|
|
|
186
240
|
|
|
187
241
|
if (node.children.length) {
|
|
188
242
|
const tmp = this.nextTemp();
|
|
243
|
+
this.flushStatic();
|
|
189
244
|
this.line(`{`);
|
|
190
245
|
this.indent += 1;
|
|
191
246
|
this.line(`let ${tmp} = "";`);
|
|
192
247
|
const prevOut = this.outVar;
|
|
248
|
+
const prevStatic = this.staticBuf;
|
|
193
249
|
this.outVar = tmp;
|
|
250
|
+
this.staticBuf = "";
|
|
194
251
|
this.emitNodes(node.children);
|
|
252
|
+
this.flushStatic();
|
|
195
253
|
this.outVar = prevOut;
|
|
254
|
+
this.staticBuf = prevStatic;
|
|
196
255
|
const propsWithChildren = props
|
|
197
256
|
? `${props}, children: ${tmp}`
|
|
198
257
|
: `children: ${tmp}`;
|
|
@@ -209,14 +268,17 @@ class CodegenContext {
|
|
|
209
268
|
*/
|
|
210
269
|
emitIf(node) {
|
|
211
270
|
const test = codegenExpr(node.test, this.locals);
|
|
271
|
+
this.flushStatic();
|
|
212
272
|
this.line(`if (${test}) {`);
|
|
213
273
|
this.indent += 1;
|
|
214
274
|
this.emitNodes(node.consequent);
|
|
275
|
+
this.flushStatic();
|
|
215
276
|
this.indent -= 1;
|
|
216
277
|
if (node.alternate.length) {
|
|
217
278
|
this.line(`} else {`);
|
|
218
279
|
this.indent += 1;
|
|
219
280
|
this.emitNodes(node.alternate);
|
|
281
|
+
this.flushStatic();
|
|
220
282
|
this.indent -= 1;
|
|
221
283
|
}
|
|
222
284
|
this.line(`}`);
|
|
@@ -228,6 +290,7 @@ class CodegenContext {
|
|
|
228
290
|
emitEach(node) {
|
|
229
291
|
const list = codegenExpr(node.list, this.locals);
|
|
230
292
|
const idx = node.indexName ?? `__i${++this.temp}`;
|
|
293
|
+
this.flushStatic();
|
|
231
294
|
this.line(`{`);
|
|
232
295
|
this.indent += 1;
|
|
233
296
|
this.line(`const __list = ${list} ?? [];`);
|
|
@@ -239,6 +302,7 @@ class CodegenContext {
|
|
|
239
302
|
this.locals.add(idx);
|
|
240
303
|
if (node.indexName) this.locals.add(node.indexName);
|
|
241
304
|
this.emitNodes(node.children);
|
|
305
|
+
this.flushStatic();
|
|
242
306
|
this.locals = prev;
|
|
243
307
|
this.indent -= 1;
|
|
244
308
|
this.line(`}`);
|
|
@@ -259,6 +323,8 @@ class CodegenContext {
|
|
|
259
323
|
|
|
260
324
|
/** @returns {string} */
|
|
261
325
|
finish() {
|
|
326
|
+
this.flushStatic();
|
|
327
|
+
|
|
262
328
|
const imports = [...this.includes]
|
|
263
329
|
.map((id) => {
|
|
264
330
|
const alias = includeAlias(id);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
6
7
|
import { parseTemplate } from "./parse.js";
|
|
7
8
|
import { codegen, normalizeIncludeId } from "./codegen.js";
|
|
8
9
|
import { CompileError } from "./errors.js";
|
|
@@ -97,21 +98,35 @@ export async function compileAll(config, _options = {}) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/**
|
|
100
|
-
* Manifest yoksa veya herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
101
|
+
* Manifest yoksa veya (dev'de) herhangi bir `.jsk` daha yeniyse yeniden derler.
|
|
102
|
+
* Production'da manifest varsa mtime taraması atlanır — `jskelet build` CI
|
|
103
|
+
* güvencesi; ilk istekte yüzlerce `statSync` cold start'ı şişirmesin.
|
|
101
104
|
* Dev sunucu restart'ında build watch kaçırmış olsa bile şablonlar güncel kalır.
|
|
102
105
|
*
|
|
103
106
|
* @param {{ root: string, dirs: Record<string, string> }} config
|
|
104
107
|
* @returns {Promise<{ count: number, rebuilt: boolean }>}
|
|
105
108
|
*/
|
|
106
109
|
export async function ensureTemplatesCompiled(config) {
|
|
110
|
+
const outDir = path.join(config.dirs.generated, "templates");
|
|
111
|
+
const manifestPath = path.join(outDir, "manifest.json");
|
|
112
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
113
|
+
|
|
114
|
+
// Prod'da manifest varsa kaynak ağacını hiç gezme — cold start FS maliyeti.
|
|
115
|
+
if (isProd && fs.existsSync(manifestPath)) {
|
|
116
|
+
try {
|
|
117
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
118
|
+
return { count: Object.keys(manifest).length, rebuilt: false };
|
|
119
|
+
} catch {
|
|
120
|
+
return { count: 0, rebuilt: false };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
const viewRoots = getViewRoots(config);
|
|
108
125
|
const jskFiles = discoverJskFiles(viewRoots);
|
|
109
126
|
if (jskFiles.size === 0) {
|
|
110
127
|
return { count: 0, rebuilt: false };
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
const outDir = path.join(config.dirs.generated, "templates");
|
|
114
|
-
const manifestPath = path.join(outDir, "manifest.json");
|
|
115
130
|
let needs = !fs.existsSync(manifestPath);
|
|
116
131
|
|
|
117
132
|
if (!needs) {
|
package/src/compile/index.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
export { CompileError, indexToLocation } from "./errors.js";
|
|
5
5
|
export { parseExpr, codegenExpr } from "./expr.js";
|
|
6
6
|
export { parseTemplate, isComponentTag } from "./parse.js";
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
codegen,
|
|
9
|
+
toPascalCase,
|
|
10
|
+
normalizeIncludeId,
|
|
11
|
+
collapseTemplateWhitespace,
|
|
12
|
+
} from "./codegen.js";
|
|
8
13
|
export {
|
|
9
14
|
getViewRoots,
|
|
10
15
|
getComponentDirs,
|
package/src/config/defaults.js
CHANGED
|
@@ -209,6 +209,11 @@ export const DATA_CACHE_MAX_ENTRIES_CEILING = 20_000;
|
|
|
209
209
|
export const DEFAULT_TRANSIENT_RETRY = {
|
|
210
210
|
attempts: 1,
|
|
211
211
|
delayMs: 300,
|
|
212
|
+
/**
|
|
213
|
+
* `true` (varsayılan): ilk transient `notFound`ta hemen 503; retry arka
|
|
214
|
+
* planda data cache'i ısıtır. `false`: yanıt öncesi senkron retry (eski).
|
|
215
|
+
*/
|
|
216
|
+
background: true,
|
|
212
217
|
};
|
|
213
218
|
|
|
214
219
|
/**
|
|
@@ -273,9 +278,8 @@ export const DEFAULT_UPSTREAM_LIMIT = {
|
|
|
273
278
|
* invalidation'ı bütün node'lara yaymak. Tek instance çalışan bir kurulumda
|
|
274
279
|
* kazanç neredeyse yok; bu yüzden `enabled` varsayılan olarak kapalı.
|
|
275
280
|
*
|
|
276
|
-
* `storeEncoded`
|
|
277
|
-
*
|
|
278
|
-
* indirmekten çoğu zaman daha ucuz.
|
|
281
|
+
* `storeEncoded` açık: soğuk replica L2 HIT'te yeniden brotli ödemesin.
|
|
282
|
+
* Boyut artar; tek encoding (br) tutulduğu için maliyet sınırlıdır.
|
|
279
283
|
*/
|
|
280
284
|
export const DEFAULT_REDIS = {
|
|
281
285
|
enabled: false,
|
|
@@ -288,8 +292,8 @@ export const DEFAULT_REDIS = {
|
|
|
288
292
|
html: true,
|
|
289
293
|
/** Veri önbelleği paylaşılsın mı. */
|
|
290
294
|
data: true,
|
|
291
|
-
/** Brotli
|
|
292
|
-
storeEncoded:
|
|
295
|
+
/** Brotli gövdeleri de paylaşılsın mı (yazma sonrası preencode). */
|
|
296
|
+
storeEncoded: true,
|
|
293
297
|
/** pub/sub üzerinden invalidation yayını. */
|
|
294
298
|
events: true,
|
|
295
299
|
/**
|
package/src/config/index.js
CHANGED
|
@@ -140,7 +140,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
140
140
|
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
141
141
|
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
142
142
|
* @property {boolean} trackDependencies Render'ın okuduğu veri anahtarları kaydedilsin mi.
|
|
143
|
-
* @property {{ attempts: number, delayMs: number }} transientRetry
|
|
143
|
+
* @property {{ attempts: number, delayMs: number, background?: boolean }} transientRetry
|
|
144
144
|
* @property {RedisConfig} redis Opsiyonel Redis ikinci kademesi.
|
|
145
145
|
* @property {typeof DEFAULT_UPSTREAM_LIMIT} upstream Upstream hız freni.
|
|
146
146
|
* @property {LogsConfig} logs Kalıcı log sink'leri (dosya + S3).
|
|
@@ -348,7 +348,7 @@ function normalizeRedis(raw) {
|
|
|
348
348
|
keyPrefix: String(source.keyPrefix ?? DEFAULT_REDIS.keyPrefix),
|
|
349
349
|
html: source.html !== false,
|
|
350
350
|
data: source.data !== false,
|
|
351
|
-
storeEncoded: source.storeEncoded
|
|
351
|
+
storeEncoded: source.storeEncoded !== false,
|
|
352
352
|
events: source.events !== false,
|
|
353
353
|
commandTimeoutMs:
|
|
354
354
|
Number.isFinite(timeout) && timeout > 0
|
|
@@ -738,7 +738,7 @@ function normalizeVary(raw) {
|
|
|
738
738
|
* staleWhileRevalidate: number,
|
|
739
739
|
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
740
740
|
* trackDependencies: boolean,
|
|
741
|
-
* transientRetry: { attempts: number, delayMs: number },
|
|
741
|
+
* transientRetry: { attempts: number, delayMs: number, background?: boolean },
|
|
742
742
|
* redis: RedisConfig,
|
|
743
743
|
* upstream: typeof DEFAULT_UPSTREAM_LIMIT,
|
|
744
744
|
* cloudflare: typeof DEFAULT_CLOUDFLARE,
|
|
@@ -18,6 +18,24 @@ export function withRequestCache(run) {
|
|
|
18
18
|
return storage.run(new Map(), run);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Sık görülen ilkel argümanlarda `JSON.stringify` atlanır.
|
|
23
|
+
*
|
|
24
|
+
* @param {unknown[]} args
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function argsKey(args) {
|
|
28
|
+
if (args.length === 0) return "";
|
|
29
|
+
if (args.length === 1) {
|
|
30
|
+
const value = args[0];
|
|
31
|
+
const type = typeof value;
|
|
32
|
+
if (value == null) return `\0${value}`;
|
|
33
|
+
if (type === "string") return `\0s:${value}`;
|
|
34
|
+
if (type === "number" || type === "boolean") return `\0${type}:${value}`;
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(args);
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/**
|
|
22
40
|
* @param {F} fn
|
|
23
41
|
* @returns {F}
|
|
@@ -34,7 +52,7 @@ export function cache(fn) {
|
|
|
34
52
|
contextStore.set(fn, perFn);
|
|
35
53
|
}
|
|
36
54
|
|
|
37
|
-
const key =
|
|
55
|
+
const key = argsKey(args);
|
|
38
56
|
if (perFn.has(key)) return perFn.get(key);
|
|
39
57
|
|
|
40
58
|
const result = fn(...args);
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,8 @@ export {
|
|
|
11
11
|
renderPage,
|
|
12
12
|
renderView,
|
|
13
13
|
renderNotFound,
|
|
14
|
+
warmRenderEngine,
|
|
15
|
+
resetRenderEngine,
|
|
14
16
|
} from "./server/render.js";
|
|
15
17
|
export { renderStatusPage, statusFromError } from "./server/status-page.js";
|
|
16
18
|
export { createApp, startServer } from "./server/create-app.js";
|
package/src/server/assets.js
CHANGED
|
@@ -151,6 +151,55 @@ export function getBuildId() {
|
|
|
151
151
|
/** @type {Record<string, OptimizedImage> | null} */
|
|
152
152
|
let images = null;
|
|
153
153
|
|
|
154
|
+
/** Prefix uygulanmış kopya — her `optimizedImage` çağrısında map/spread yok. */
|
|
155
|
+
/** @type {Record<string, OptimizedImage> | null} */
|
|
156
|
+
let imagesPrefixed = null;
|
|
157
|
+
|
|
158
|
+
/** @type {string | null} */
|
|
159
|
+
let imagesPrefix = null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `images.json` yükler; CDN prefix varsa varyant URL'lerini bir kez materialize eder.
|
|
163
|
+
* @returns {Record<string, OptimizedImage>}
|
|
164
|
+
*/
|
|
165
|
+
function loadImages() {
|
|
166
|
+
const prefix = assetPrefix();
|
|
167
|
+
if (!isDev && images && imagesPrefix === prefix) {
|
|
168
|
+
return imagesPrefixed ?? images;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @type {Record<string, OptimizedImage>} */
|
|
172
|
+
let raw = {};
|
|
173
|
+
try {
|
|
174
|
+
const file = path.join(getConfig().dirs.generated, "images.json");
|
|
175
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8")).images ?? {};
|
|
176
|
+
} catch {
|
|
177
|
+
raw = {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
images = raw;
|
|
181
|
+
imagesPrefix = prefix;
|
|
182
|
+
|
|
183
|
+
if (!prefix) {
|
|
184
|
+
imagesPrefixed = raw;
|
|
185
|
+
return raw;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @type {Record<string, OptimizedImage>} */
|
|
189
|
+
const prefixed = {};
|
|
190
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
191
|
+
prefixed[key] = {
|
|
192
|
+
...entry,
|
|
193
|
+
variants: entry.variants.map((variant) => ({
|
|
194
|
+
...variant,
|
|
195
|
+
url: applyAssetPrefix(prefix, variant.url),
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
imagesPrefixed = prefixed;
|
|
200
|
+
return prefixed;
|
|
201
|
+
}
|
|
202
|
+
|
|
154
203
|
/**
|
|
155
204
|
* `build/tasks/images.mjs` çıktısı: kaynak yolundan webp varyantlarına.
|
|
156
205
|
* Build çalışmadıysa boş kalır ve görseller orijinalleriyle servis edilir.
|
|
@@ -159,29 +208,9 @@ let images = null;
|
|
|
159
208
|
* @returns {OptimizedImage | undefined}
|
|
160
209
|
*/
|
|
161
210
|
export function optimizedImage(src) {
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const file = path.join(getConfig().dirs.generated, "images.json");
|
|
165
|
-
images = JSON.parse(fs.readFileSync(file, "utf8")).images ?? {};
|
|
166
|
-
} catch {
|
|
167
|
-
images = {};
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
211
|
+
const table = loadImages();
|
|
171
212
|
if (!src) return undefined;
|
|
172
|
-
|
|
173
|
-
if (!entry) return undefined;
|
|
174
|
-
|
|
175
|
-
const prefix = assetPrefix();
|
|
176
|
-
if (!prefix) return entry;
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
...entry,
|
|
180
|
-
variants: entry.variants.map((variant) => ({
|
|
181
|
-
...variant,
|
|
182
|
-
url: applyAssetPrefix(prefix, variant.url),
|
|
183
|
-
})),
|
|
184
|
-
};
|
|
213
|
+
return table[src];
|
|
185
214
|
}
|
|
186
215
|
|
|
187
216
|
/** @type {{ url: string, ids: Set<string> } | null} */
|
package/src/server/cache-blob.js
CHANGED
|
@@ -53,6 +53,9 @@ export function encodeCacheValue(value) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
+
* Senkron çözücü — küçük test / senkron yollar için. Hot path
|
|
57
|
+
* `decodeCacheValueAsync` kullanır (event loop'u brotli ile bloklamaz).
|
|
58
|
+
*
|
|
56
59
|
* @param {Buffer | string} raw
|
|
57
60
|
* @returns {unknown}
|
|
58
61
|
*/
|
|
@@ -68,3 +71,33 @@ export function decodeCacheValue(raw) {
|
|
|
68
71
|
const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : raw;
|
|
69
72
|
return JSON.parse(text);
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Buffer | string} raw
|
|
77
|
+
* @returns {Promise<unknown>}
|
|
78
|
+
*/
|
|
79
|
+
export function decodeCacheValueAsync(raw) {
|
|
80
|
+
if (
|
|
81
|
+
Buffer.isBuffer(raw) &&
|
|
82
|
+
raw.length > CACHE_MAGIC.length &&
|
|
83
|
+
raw.subarray(0, CACHE_MAGIC.length).equals(CACHE_MAGIC)
|
|
84
|
+
) {
|
|
85
|
+
const body = raw.subarray(CACHE_MAGIC.length);
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
zlib.brotliDecompress(body, (error, out) => {
|
|
88
|
+
if (error) {
|
|
89
|
+
reject(error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
resolve(JSON.parse(out.toString("utf8")));
|
|
94
|
+
} catch (parseError) {
|
|
95
|
+
reject(parseError);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : raw;
|
|
102
|
+
return Promise.resolve(JSON.parse(text));
|
|
103
|
+
}
|
package/src/server/create-app.js
CHANGED
|
@@ -247,6 +247,11 @@ export async function startServer(options = {}) {
|
|
|
247
247
|
|
|
248
248
|
const app = await createApp(options);
|
|
249
249
|
|
|
250
|
+
// İlk istek TTFB'sine binmesin: şablon motoru + asset manifest burada ısınır.
|
|
251
|
+
// Sayfa prewarm değildir.
|
|
252
|
+
const { warmRenderEngine } = await import("./render.js");
|
|
253
|
+
await warmRenderEngine();
|
|
254
|
+
|
|
250
255
|
// Varsayılan `::`, `0.0.0.0` değil: ikisi de "tüm arayüzler" demek, ama
|
|
251
256
|
// yalnızca IPv6 soketi çift yığın çalışır ve `localhost`un `::1`e çözüldüğü
|
|
252
257
|
// durumu da kapsar. Tarayıcılar `localhost` için önce `::1` deniyor; sıradan
|
package/src/server/data-cache.js
CHANGED
|
@@ -27,7 +27,7 @@ import { getConfig } from "../config/index.js";
|
|
|
27
27
|
import { DATA_CACHE_BYTE_BUDGET, DEFAULT_DATA_CACHE } from "../config/defaults.js";
|
|
28
28
|
import { recordDependency } from "./cache-deps.js";
|
|
29
29
|
import { diskDrop, diskDropMatching, diskGetJson, diskSetJson, diskShares } from "./disk-cache.js";
|
|
30
|
-
import { invalidateHtmlByDependency } from "./html-cache.js";
|
|
30
|
+
import { earlyRefreshLeadMs, invalidateHtmlByDependency } from "./html-cache.js";
|
|
31
31
|
import {
|
|
32
32
|
cacheKey,
|
|
33
33
|
onCacheEvent,
|
|
@@ -39,9 +39,12 @@ import {
|
|
|
39
39
|
} from "./redis.js";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* @typedef {{ value: unknown, expiresAt: number, staleUntil: number, bytes: number
|
|
42
|
+
* @typedef {{ value: unknown, expiresAt: number, staleUntil: number, bytes: number,
|
|
43
|
+
* produceMs: number }} DataEntry
|
|
43
44
|
*/
|
|
44
45
|
|
|
46
|
+
const DEFAULT_PRODUCE_MS = 500;
|
|
47
|
+
|
|
45
48
|
/** @type {Map<string, DataEntry>} */
|
|
46
49
|
const store = new Map();
|
|
47
50
|
|
|
@@ -60,6 +63,14 @@ let byteBudgetOverride = null;
|
|
|
60
63
|
/** @type {Map<string, Promise<unknown>>} */
|
|
61
64
|
const inflight = new Map();
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* L2 miss kısa belleği — boş Redis/disk turunu tekrarlamamak için.
|
|
68
|
+
* @type {Map<string, number>}
|
|
69
|
+
*/
|
|
70
|
+
const sharedMissUntil = new Map();
|
|
71
|
+
const SHARED_MISS_TTL_MS = 2_000;
|
|
72
|
+
const MAX_SHARED_MISS = 2_000;
|
|
73
|
+
|
|
63
74
|
/**
|
|
64
75
|
* Süreç ömrü boyunca biriken sayaçlar.
|
|
65
76
|
*
|
|
@@ -106,9 +117,25 @@ function settings() {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @param {DataEntry} entry
|
|
122
|
+
* @param {number} [now]
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
function isEarly(entry, now = Date.now()) {
|
|
126
|
+
if (now >= entry.expiresAt) return false;
|
|
127
|
+
// `storedAt` yok; orijinal TTL = bayat penceresi / staleFactor.
|
|
128
|
+
const staleFactor = settings().staleFactor;
|
|
129
|
+
const staleWindow = Math.max(0, entry.staleUntil - entry.expiresAt);
|
|
130
|
+
const originalTtl =
|
|
131
|
+
staleFactor > 0 ? staleWindow / staleFactor : Math.max(staleWindow, entry.produceMs * 4);
|
|
132
|
+
const lead = earlyRefreshLeadMs(entry.produceMs ?? DEFAULT_PRODUCE_MS, originalTtl);
|
|
133
|
+
return now >= entry.expiresAt - lead;
|
|
134
|
+
}
|
|
135
|
+
|
|
109
136
|
/**
|
|
110
137
|
* @param {string} key
|
|
111
|
-
* @returns {{ value: unknown, stale: boolean } | null}
|
|
138
|
+
* @returns {{ value: unknown, stale: boolean, early: boolean } | null}
|
|
112
139
|
*/
|
|
113
140
|
function read(key) {
|
|
114
141
|
const entry = store.get(key);
|
|
@@ -124,7 +151,12 @@ function read(key) {
|
|
|
124
151
|
store.delete(key);
|
|
125
152
|
store.set(key, entry);
|
|
126
153
|
|
|
127
|
-
|
|
154
|
+
const stale = now >= entry.expiresAt;
|
|
155
|
+
return {
|
|
156
|
+
value: entry.value,
|
|
157
|
+
stale,
|
|
158
|
+
early: !stale && isEarly(entry, now),
|
|
159
|
+
};
|
|
128
160
|
}
|
|
129
161
|
|
|
130
162
|
/**
|
|
@@ -200,8 +232,9 @@ function install(key, entry) {
|
|
|
200
232
|
* @param {unknown} value
|
|
201
233
|
* @param {number} ttlSeconds
|
|
202
234
|
* @param {number} staleFactor
|
|
235
|
+
* @param {number} [produceMs]
|
|
203
236
|
*/
|
|
204
|
-
function write(key, value, ttlSeconds, staleFactor) {
|
|
237
|
+
function write(key, value, ttlSeconds, staleFactor, produceMs = DEFAULT_PRODUCE_MS) {
|
|
205
238
|
const now = Date.now();
|
|
206
239
|
const ttl = ttlSeconds * 1000;
|
|
207
240
|
|
|
@@ -211,8 +244,12 @@ function write(key, value, ttlSeconds, staleFactor) {
|
|
|
211
244
|
expiresAt: now + ttl,
|
|
212
245
|
staleUntil: now + ttl + ttl * staleFactor,
|
|
213
246
|
bytes: valueBytes(value),
|
|
247
|
+
produceMs:
|
|
248
|
+
Number.isFinite(produceMs) && produceMs >= 0 ? Math.round(produceMs) : DEFAULT_PRODUCE_MS,
|
|
214
249
|
};
|
|
215
250
|
|
|
251
|
+
sharedMissUntil.delete(key);
|
|
252
|
+
|
|
216
253
|
// Bütçeye sığmayan değer Redis'e de yazılmaz: bir sonraki istek onu geri
|
|
217
254
|
// alıp yine reddeder. Çağıran sonuç yine `producer`'ın döndürdüğüdür.
|
|
218
255
|
if (!install(key, entry)) return;
|
|
@@ -236,9 +273,25 @@ function write(key, value, ttlSeconds, staleFactor) {
|
|
|
236
273
|
*/
|
|
237
274
|
function promote(key, entry) {
|
|
238
275
|
if (!entry.bytes) entry.bytes = valueBytes(entry.value);
|
|
276
|
+
if (!Number.isFinite(entry.produceMs) || entry.produceMs < 0) {
|
|
277
|
+
entry.produceMs = DEFAULT_PRODUCE_MS;
|
|
278
|
+
}
|
|
279
|
+
sharedMissUntil.delete(key);
|
|
239
280
|
install(key, entry);
|
|
240
281
|
}
|
|
241
282
|
|
|
283
|
+
/**
|
|
284
|
+
* @param {string} key
|
|
285
|
+
* @returns {void}
|
|
286
|
+
*/
|
|
287
|
+
function rememberSharedMiss(key) {
|
|
288
|
+
if (sharedMissUntil.size >= MAX_SHARED_MISS) {
|
|
289
|
+
const oldest = sharedMissUntil.keys().next().value;
|
|
290
|
+
if (oldest !== undefined) sharedMissUntil.delete(oldest);
|
|
291
|
+
}
|
|
292
|
+
sharedMissUntil.set(key, Date.now() + SHARED_MISS_TTL_MS);
|
|
293
|
+
}
|
|
294
|
+
|
|
242
295
|
/**
|
|
243
296
|
* Paylaşımlı kademeden okur.
|
|
244
297
|
*
|
|
@@ -254,13 +307,24 @@ async function readShared(key) {
|
|
|
254
307
|
const onDisk = diskShares("data");
|
|
255
308
|
if (!onRedis && !onDisk) return null;
|
|
256
309
|
|
|
310
|
+
const missUntil = sharedMissUntil.get(key);
|
|
311
|
+
if (missUntil !== undefined) {
|
|
312
|
+
if (Date.now() < missUntil) return null;
|
|
313
|
+
sharedMissUntil.delete(key);
|
|
314
|
+
}
|
|
315
|
+
|
|
257
316
|
const entry = onRedis ? await redisGetJson(cacheKey("data", key)) : await diskGetJson("data", key);
|
|
258
|
-
if (!entry || typeof entry.expiresAt !== "number")
|
|
317
|
+
if (!entry || typeof entry.expiresAt !== "number") {
|
|
318
|
+
rememberSharedMiss(key);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
259
321
|
if (Date.now() >= entry.expiresAt) {
|
|
260
322
|
if (onDisk) diskDrop("data", [key]);
|
|
323
|
+
rememberSharedMiss(key);
|
|
261
324
|
return null;
|
|
262
325
|
}
|
|
263
326
|
|
|
327
|
+
sharedMissUntil.delete(key);
|
|
264
328
|
return /** @type {DataEntry} */ (entry);
|
|
265
329
|
}
|
|
266
330
|
|
|
@@ -309,11 +373,13 @@ async function produce(key, ttlSeconds, producer, options, staleFactor) {
|
|
|
309
373
|
}
|
|
310
374
|
|
|
311
375
|
stats.produced += 1;
|
|
376
|
+
const startedAt = Date.now();
|
|
312
377
|
const value = await producer();
|
|
378
|
+
const produceMs = Date.now() - startedAt;
|
|
313
379
|
|
|
314
380
|
const empty = value === undefined || value === null;
|
|
315
381
|
if (!empty || options.storeEmpty === true) {
|
|
316
|
-
write(key, value, ttlSeconds, staleFactor);
|
|
382
|
+
write(key, value, ttlSeconds, staleFactor, produceMs);
|
|
317
383
|
}
|
|
318
384
|
|
|
319
385
|
return value;
|
|
@@ -348,10 +414,9 @@ export async function withDataCache(key, ttlSeconds, producer, options = {}) {
|
|
|
348
414
|
if (hit.stale) stats.stale += 1;
|
|
349
415
|
else stats.hits += 1;
|
|
350
416
|
|
|
351
|
-
// Bayat girdi anında döner; tazeleme arkada yürür
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
if (hit.stale) {
|
|
417
|
+
// Bayat veya erken penceredeki girdi anında döner; tazeleme arkada yürür
|
|
418
|
+
// — HTML cache ile aynı: TTL dolmadan upstream çağrısı bitsin.
|
|
419
|
+
if (hit.stale || hit.early) {
|
|
355
420
|
void refresh(key, ttlSeconds, producer, options).catch((error) => {
|
|
356
421
|
console.error(`[data-cache] background refresh failed: ${key}`, error);
|
|
357
422
|
});
|
|
@@ -445,6 +510,7 @@ function clearLocal(prefix) {
|
|
|
445
510
|
removed.push(...store.keys());
|
|
446
511
|
store.clear();
|
|
447
512
|
storedBytes = 0;
|
|
513
|
+
sharedMissUntil.clear();
|
|
448
514
|
} else {
|
|
449
515
|
for (const key of [...store.keys()]) {
|
|
450
516
|
if (key.startsWith(prefix)) {
|