jskelet 0.1.1 → 0.1.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.
Files changed (66) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +129 -2
  3. package/README.md +21 -7
  4. package/bin/jskelet.mjs +6 -6
  5. package/docs/03-routing.md +48 -9
  6. package/docs/04-render-ve-sablonlar.md +2 -2
  7. package/docs/05-islands.md +59 -6
  8. package/docs/06-cache.md +240 -26
  9. package/docs/07-yapilandirma.md +108 -7
  10. package/docs/08-build.md +4 -4
  11. package/docs/09-dev-araclari.md +5 -0
  12. package/docs/12-panel-ve-oturum.md +384 -0
  13. package/docs/README.md +25 -2
  14. package/docs/en/01-getting-started.md +292 -0
  15. package/docs/en/02-architecture.md +305 -0
  16. package/docs/en/03-routing.md +493 -0
  17. package/docs/en/04-rendering.md +504 -0
  18. package/docs/en/05-islands.md +492 -0
  19. package/docs/en/06-caching.md +640 -0
  20. package/docs/en/07-configuration.md +789 -0
  21. package/docs/en/08-build.md +383 -0
  22. package/docs/en/09-dev-tools.md +314 -0
  23. package/docs/en/10-deployment.md +332 -0
  24. package/docs/en/11-migration.md +360 -0
  25. package/docs/en/12-dashboards-and-sessions.md +392 -0
  26. package/docs/en/README.md +112 -0
  27. package/package.json +4 -2
  28. package/src/build/build.mjs +1 -1
  29. package/src/build/tasks/client.mjs +2 -2
  30. package/src/build/tasks/fonts.mjs +3 -3
  31. package/src/build/tasks/icons.mjs +1 -1
  32. package/src/build/tasks/images.mjs +2 -2
  33. package/src/client/devtools/overlay.js +196 -164
  34. package/src/client/devtools/report.js +96 -96
  35. package/src/client/form.js +192 -0
  36. package/src/client/index.js +10 -1
  37. package/src/client/registry.js +78 -4
  38. package/src/client/swap.js +188 -0
  39. package/src/config/defaults.js +83 -0
  40. package/src/config/index.js +129 -18
  41. package/src/config/pattern.js +1 -1
  42. package/src/dev-server.mjs +1 -1
  43. package/src/http/control-flow.js +16 -1
  44. package/src/http/cookies.js +257 -0
  45. package/src/http/request-context.js +162 -0
  46. package/src/index.js +26 -2
  47. package/src/init.mjs +32 -31
  48. package/src/log.mjs +8 -2
  49. package/src/logo.png +0 -0
  50. package/src/runtime/alias-hooks.mjs +1 -1
  51. package/src/server/assets.js +1 -1
  52. package/src/server/create-app.js +12 -4
  53. package/src/server/data-cache.js +244 -0
  54. package/src/server/dev/devtools.js +6 -2
  55. package/src/server/dev/report.js +8 -1
  56. package/src/server/dev/version-check.mjs +139 -0
  57. package/src/server/head-hints.js +1 -1
  58. package/src/server/html-cache.js +32 -6
  59. package/src/server/middleware/csrf.js +134 -0
  60. package/src/server/prewarm.js +164 -19
  61. package/src/server/render.js +256 -20
  62. package/src/server/router.js +14 -7
  63. package/src/server/status-page.js +1 -1
  64. package/src/version.mjs +9 -4
  65. package/src/views/components/loader.js +1 -1
  66. package/src/views/helpers/tags.js +53 -1
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Form gönderimini sayfa yenilemeden yürütür — ama yenilemeyi de bozmadan.
3
+ *
4
+ * Sözleşme bilinçli olarak "progressive enhancement": form normal bir
5
+ * `<form method="post" action="…">`. JS yüklenmemişse, hata verdiyse ya da
6
+ * kullanıcı kapattıysa tarayıcı formu kendisi gönderir ve sunucu yönlendirme
7
+ * ile cevap verir. Bu dosya yalnızca aradaki tam sayfa turunu kaldırıyor.
8
+ *
9
+ * Sunucudan üç cevaptan biri beklenir:
10
+ * - yönlendirme → `location.assign` ile izlenir (başarılı mutasyon)
11
+ * - 4xx + parça → formun yerine takılır (alan bazlı doğrulama hataları)
12
+ * - 2xx + parça → hedef bölgeye takılır (güncellenen liste, kart)
13
+ *
14
+ * CSRF token'ı `csrfField()` tarafından basılan gizli alanda; `FormData`
15
+ * onu kendiliğinden taşıdığı için burada ek bir iş yok.
16
+ */
17
+ import { hydrate, unmount } from "./registry.js";
18
+
19
+ /**
20
+ * @param {HTMLFormElement} form
21
+ * @param {boolean} busy
22
+ */
23
+ function setBusy(form, busy) {
24
+ form.toggleAttribute("data-submitting", busy);
25
+
26
+ for (const element of form.elements) {
27
+ if (
28
+ element instanceof HTMLButtonElement ||
29
+ (element instanceof HTMLInputElement && element.type === "submit")
30
+ ) {
31
+ element.disabled = busy;
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {HTMLElement} element
38
+ * @param {string} html
39
+ */
40
+ function replaceContent(element, html) {
41
+ unmount(element);
42
+ element.innerHTML = html;
43
+ hydrate(element);
44
+ }
45
+
46
+ /**
47
+ * @param {HTMLFormElement} form
48
+ * @param {string} html
49
+ */
50
+ function replaceForm(form, html) {
51
+ const holder = document.createElement("div");
52
+ holder.innerHTML = html;
53
+
54
+ const replacement = holder.firstElementChild;
55
+ if (!replacement) return;
56
+
57
+ unmount(form);
58
+ form.replaceWith(replacement);
59
+ hydrate(replacement);
60
+
61
+ // Doğrulama hatasında odak ilk hatalı alana gitmeli; klavye kullanıcısı
62
+ // aksi hâlde hatanın nerede olduğunu bulmak için baştan geziyor.
63
+ const invalid = replacement.querySelector("[aria-invalid='true'], [data-field-error]");
64
+ if (invalid instanceof HTMLElement) invalid.focus();
65
+ }
66
+
67
+ /**
68
+ * @param {HTMLFormElement} form
69
+ * @param {HTMLElement | null} submitter
70
+ */
71
+ async function submitForm(form, submitter) {
72
+ setBusy(form, true);
73
+
74
+ const method = (form.method || "post").toUpperCase();
75
+ const action = form.action || location.href;
76
+ const data = new FormData(form);
77
+
78
+ // Gönderen butonun `name`/`value`'su `FormData`'ya girmez; hangi eylemin
79
+ // tetiklendiğini ayırt eden formlar (kaydet / sil) buna güveniyor.
80
+ if (
81
+ (submitter instanceof HTMLButtonElement ||
82
+ submitter instanceof HTMLInputElement) &&
83
+ submitter.name
84
+ ) {
85
+ data.append(submitter.name, submitter.value);
86
+ }
87
+
88
+ try {
89
+ const isGet = method === "GET";
90
+ const query = new URLSearchParams();
91
+ if (isGet) {
92
+ for (const [key, value] of data.entries()) {
93
+ if (typeof value === "string") query.append(key, value);
94
+ }
95
+ }
96
+
97
+ const response = await fetch(isGet ? `${action.split("?")[0]}?${query}` : action, {
98
+ method,
99
+ body: isGet ? null : data,
100
+ headers: { "X-Requested-With": "fragment" },
101
+ credentials: "same-origin",
102
+ });
103
+
104
+ if (response.redirected) {
105
+ location.assign(response.url);
106
+ return;
107
+ }
108
+
109
+ const html = await response.text();
110
+ const target = form.dataset.target
111
+ ? document.querySelector(form.dataset.target)
112
+ : null;
113
+
114
+ if (!response.ok || !target) {
115
+ replaceForm(form, html);
116
+ } else {
117
+ replaceContent(/** @type {HTMLElement} */ (target), html);
118
+ form.reset();
119
+ }
120
+
121
+ form.dispatchEvent(
122
+ new CustomEvent("jskelet:submitted", {
123
+ bubbles: true,
124
+ detail: { ok: response.ok, status: response.status },
125
+ }),
126
+ );
127
+ } catch (error) {
128
+ console.error("[form] submit failed", error);
129
+ // Ağ hatasında formu kilitli bırakmıyoruz: kullanıcı tekrar denesin.
130
+ form.dispatchEvent(
131
+ new CustomEvent("jskelet:submit-error", { bubbles: true, detail: { error } }),
132
+ );
133
+ } finally {
134
+ setBusy(form, false);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * @param {SubmitEvent} event
140
+ * @param {HTMLFormElement} form
141
+ */
142
+ function onSubmit(event, form) {
143
+ // Tarayıcının kendi doğrulaması geçmediyse hiç ağa çıkmıyoruz; hata
144
+ // balonlarını da tarayıcı gösterir.
145
+ if (!form.checkValidity()) return;
146
+ if (form.hasAttribute("data-submitting")) {
147
+ event.preventDefault();
148
+ return;
149
+ }
150
+
151
+ event.preventDefault();
152
+ void submitForm(form, /** @type {HTMLElement | null} */ (event.submitter));
153
+ }
154
+
155
+ /**
156
+ * Tek bir formu bağlar.
157
+ *
158
+ * `data-target` verilirse başarılı yanıtın gövdesi o bölgeye takılır; yoksa
159
+ * yanıt formun kendisini değiştirir (doğrulama hatalarının geldiği yol).
160
+ *
161
+ * @param {HTMLFormElement} form
162
+ * @returns {() => void} Dinleyiciyi söken fonksiyon.
163
+ */
164
+ export function enhanceForm(form) {
165
+ /** @param {Event} event */
166
+ const listener = (event) => onSubmit(/** @type {SubmitEvent} */ (event), form);
167
+
168
+ form.addEventListener("submit", listener);
169
+ return () => form.removeEventListener("submit", listener);
170
+ }
171
+
172
+ /**
173
+ * `data-enhance` taşıyan formları tek bir delege dinleyiciyle bağlar.
174
+ * `submit` olayı köpürdüğü için sonradan DOM'a giren (fragment ile gelen)
175
+ * formlar da kapsanır ve her takastan sonra yeniden bağlama gerekmez.
176
+ *
177
+ * @param {ParentNode} [root]
178
+ * @returns {() => void}
179
+ */
180
+ export function startForms(root = document) {
181
+ /** @param {Event} event */
182
+ const listener = (event) => {
183
+ const form = event.target;
184
+ if (!(form instanceof HTMLFormElement)) return;
185
+ if (!("enhance" in form.dataset)) return;
186
+
187
+ onSubmit(/** @type {SubmitEvent} */ (event), form);
188
+ };
189
+
190
+ root.addEventListener("submit", listener);
191
+ return () => root.removeEventListener("submit", listener);
192
+ }
@@ -11,7 +11,16 @@
11
11
  *
12
12
  * start();
13
13
  */
14
- export { register, registerAll, hydrate, observeDocument, start } from "./registry.js";
14
+ export {
15
+ register,
16
+ registerAll,
17
+ hydrate,
18
+ unmount,
19
+ observeDocument,
20
+ start,
21
+ } from "./registry.js";
22
+ export { swap, startSwapLinks } from "./swap.js";
23
+ export { enhanceForm, startForms } from "./form.js";
15
24
  export { createStore } from "./store.js";
16
25
  export {
17
26
  debounce,
@@ -28,6 +28,18 @@ const loaders = new Map();
28
28
  /** @type {WeakMap<HTMLElement, Set<string>>} */
29
29
  const mounted = new WeakMap();
30
30
 
31
+ /**
32
+ * `mount()` bir temizlik fonksiyonu döndürebiliyor; DOM'dan çıkan island'ın
33
+ * dinleyicilerini ve zamanlayıcılarını sökmek için saklanır.
34
+ *
35
+ * WeakMap: sökülmeden çöpe giden bir element (ör. tüm sayfa gezinmesi)
36
+ * burada referans bırakmasın. Alt ağacı gezme işi DOM üzerinden yapılıyor,
37
+ * bu yüzden ayrı bir element listesine gerek yok.
38
+ *
39
+ * @type {WeakMap<HTMLElement, (() => void)[]>}
40
+ */
41
+ const cleanups = new WeakMap();
42
+
31
43
  /**
32
44
  * @param {string} name
33
45
  * @param {() => Promise<{ mount: MountFn }>} loader
@@ -55,7 +67,7 @@ function readProps(element) {
55
67
  return JSON.parse(raw);
56
68
  } catch (error) {
57
69
  console.error(
58
- `[island] ${element.dataset.island}: geçersiz data-island-props`,
70
+ `[island] ${element.dataset.island}: invalid data-island-props`,
59
71
  error,
60
72
  );
61
73
  return {};
@@ -92,7 +104,7 @@ async function mountIsland(element) {
92
104
 
93
105
  const loader = loaders.get(name);
94
106
  if (!loader) {
95
- console.warn(`[island] kayıtlı değil: ${name}`);
107
+ console.warn(`[island] not registered: ${name}`);
96
108
  return;
97
109
  }
98
110
 
@@ -100,10 +112,72 @@ async function mountIsland(element) {
100
112
 
101
113
  try {
102
114
  const module = await loader();
103
- module.mount(element, readProps(element));
115
+ const cleanup = module.mount(element, readProps(element));
116
+
117
+ if (typeof cleanup === "function") {
118
+ // Island bağlanırken DOM'dan çıkarılmış olabilir (hızlı bir fragment
119
+ // takası). Temizliği saklamak yerine hemen çalıştırmak gerekiyor;
120
+ // aksi hâlde artık görünmeyen bir elementin dinleyicileri hiç sökülmez.
121
+ if (element.isConnected) {
122
+ const list = cleanups.get(element) ?? [];
123
+ list.push(cleanup);
124
+ cleanups.set(element, list);
125
+ } else {
126
+ runCleanups(element, cleanup);
127
+ }
128
+ }
129
+
104
130
  element.dataset.islandReady = "true";
105
131
  } catch (error) {
106
- console.error(`[island] ${name} yüklenemedi`, error);
132
+ console.error(`[island] ${name} failed to load`, error);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * @param {HTMLElement} element
138
+ * @param {() => void} [extra] Henüz saklanmamış temizlik.
139
+ */
140
+ function runCleanups(element, extra) {
141
+ const stored = cleanups.get(element) ?? [];
142
+ cleanups.delete(element);
143
+ mounted.delete(element);
144
+ delete element.dataset.islandReady;
145
+
146
+ for (const cleanup of extra ? [...stored, extra] : stored) {
147
+ try {
148
+ cleanup();
149
+ } catch (error) {
150
+ console.error(`[island] ${element.dataset.island} cleanup failed`, error);
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Bir alt ağaçtaki island'ları söker.
157
+ *
158
+ * Fragment takasında çağrılması zorunlu: `innerHTML` ile değiştirilen bir
159
+ * bölgenin island'ları DOM'dan çıkar ama `document`/`window` üzerine
160
+ * kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder. Birkaç
161
+ * takastan sonra aynı olay birden fazla kez işlenmeye başlar.
162
+ *
163
+ * Sökülen element yeniden bağlanabilir hâle gelir: `mounted` kaydı da
164
+ * temizlenir, böylece aynı düğüm tekrar DOM'a girerse `hydrate()` onu
165
+ * yeniden görür.
166
+ *
167
+ * @param {ParentNode} [root] Kökün kendisi de island olabilir.
168
+ */
169
+ export function unmount(root = document) {
170
+ const element = /** @type {HTMLElement} */ (root);
171
+
172
+ if (element.dataset?.island) {
173
+ lazyObserver?.unobserve(element);
174
+ runCleanups(element);
175
+ }
176
+
177
+ for (const node of root.querySelectorAll?.("[data-island]") ?? []) {
178
+ const child = /** @type {HTMLElement} */ (node);
179
+ lazyObserver?.unobserve(child);
180
+ runCleanups(child);
107
181
  }
108
182
  }
109
183
 
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Sunucudan gelen bir parçayı DOM'a takar.
3
+ *
4
+ * Dashboard'un can damarı bu: tablo sayfası, filtre sonucu, yeniden hesaplanan
5
+ * kart — hepsi sunucuda render edilip yerine konur, böylece işaretlemenin tek
6
+ * kaynağı sunucuda kalır ve istemci ikinci bir şablon taşımaz.
7
+ *
8
+ * Taşıma katmanından bağımsız. Verinin ne zaman değiştiğini nasıl öğrendiğin
9
+ * (SSE, WebSocket, aralıklı sorgu, kullanıcı tıklaması) uygulamanın kararı;
10
+ * burada yalnızca "şu parçayı tazele" adımı var.
11
+ *
12
+ * Sırası önemli olan üç iş yapıyor:
13
+ * 1. eski alt ağacın island'larını söker (yoksa dinleyici sızar),
14
+ * 2. içeriği değiştirir,
15
+ * 3. yeni alt ağacı hidre eder ve odağı kaybolmuşsa geri getirir.
16
+ */
17
+ import { hydrate, unmount } from "./registry.js";
18
+
19
+ /**
20
+ * @param {Element | string} target
21
+ * @returns {HTMLElement | null}
22
+ */
23
+ function resolveTarget(target) {
24
+ if (typeof target === "string") {
25
+ return /** @type {HTMLElement | null} */ (document.querySelector(target));
26
+ }
27
+ return /** @type {HTMLElement} */ (target);
28
+ }
29
+
30
+ /**
31
+ * Odak takas edilen bölgenin içindeyse kaybolur ve klavye kullanıcısı
32
+ * sayfanın başına düşer. Yeni içerikte aynı `id`'li bir element varsa oraya,
33
+ * yoksa bölgenin kendisine dönüyoruz.
34
+ *
35
+ * @param {HTMLElement} element
36
+ * @param {string | null} previousId
37
+ */
38
+ function restoreFocus(element, previousId) {
39
+ if (previousId) {
40
+ const next = element.querySelector(`#${CSS.escape(previousId)}`);
41
+ if (next instanceof HTMLElement) {
42
+ next.focus();
43
+ return;
44
+ }
45
+ }
46
+
47
+ if (!element.hasAttribute("tabindex")) {
48
+ element.setAttribute("tabindex", "-1");
49
+ }
50
+ element.focus({ preventScroll: true });
51
+ }
52
+
53
+ /**
54
+ * @typedef {object} SwapOptions
55
+ * @property {string} [method] Varsayılan `GET`.
56
+ * @property {BodyInit | null} [body]
57
+ * @property {Record<string, string>} [headers]
58
+ * @property {AbortSignal} [signal]
59
+ * @property {boolean} [history] `true` → adres çubuğu `url` ile güncellenir.
60
+ * @property {"replace" | "append"} [mode] Varsayılan `replace`.
61
+ */
62
+
63
+ /**
64
+ * @param {Element | string} target Takas edilecek bölge.
65
+ * @param {string} url Parçayı döndüren uç.
66
+ * @param {SwapOptions} [options]
67
+ * @returns {Promise<boolean>} Başarılıysa `true`; hata durumunda mevcut
68
+ * içerik korunur ve `false` döner.
69
+ */
70
+ export async function swap(target, url, options = {}) {
71
+ const element = resolveTarget(target);
72
+ if (!element) {
73
+ console.warn(`[swap] target not found: ${String(target)}`);
74
+ return false;
75
+ }
76
+
77
+ const activeId =
78
+ document.activeElement instanceof HTMLElement &&
79
+ element.contains(document.activeElement)
80
+ ? document.activeElement.id || null
81
+ : null;
82
+
83
+ element.setAttribute("aria-busy", "true");
84
+
85
+ try {
86
+ const response = await fetch(url, {
87
+ method: options.method ?? "GET",
88
+ body: options.body ?? null,
89
+ headers: {
90
+ // Sunucu tam sayfa yerine parça döndürmeyi seçebilsin.
91
+ "X-Requested-With": "fragment",
92
+ ...(options.headers ?? {}),
93
+ },
94
+ // Oturum cookie'si olmadan kişiye özel parça gelmez.
95
+ credentials: "same-origin",
96
+ signal: options.signal,
97
+ });
98
+
99
+ // Sunucu yönlendirdiyse (ör. oturum düştü, login'e gidiliyor) parça değil
100
+ // sayfa beklenir; alt ağaca bir login formu takmak anlamsız olurdu.
101
+ if (response.redirected && response.url !== new URL(url, location.href).href) {
102
+ location.assign(response.url);
103
+ return false;
104
+ }
105
+
106
+ if (!response.ok) {
107
+ element.dispatchEvent(
108
+ new CustomEvent("jskelet:swap-error", {
109
+ bubbles: true,
110
+ detail: { url, status: response.status },
111
+ }),
112
+ );
113
+ return false;
114
+ }
115
+
116
+ const html = await response.text();
117
+
118
+ if (options.mode === "append") {
119
+ const holder = document.createElement("div");
120
+ holder.innerHTML = html;
121
+ const added = [...holder.childNodes];
122
+ element.append(...added);
123
+ for (const node of added) {
124
+ if (node instanceof HTMLElement) hydrate(node);
125
+ }
126
+ } else {
127
+ unmount(element);
128
+ element.innerHTML = html;
129
+ hydrate(element);
130
+ restoreFocus(element, activeId);
131
+ }
132
+
133
+ if (options.history) {
134
+ history.pushState({}, "", url);
135
+ }
136
+
137
+ element.dispatchEvent(
138
+ new CustomEvent("jskelet:swap", { bubbles: true, detail: { url } }),
139
+ );
140
+
141
+ return true;
142
+ } catch (error) {
143
+ // İptal edilen istek hata değil: kullanıcı hızlı yazıyor ya da sayfadan
144
+ // ayrıldı.
145
+ if (!(error instanceof DOMException && error.name === "AbortError")) {
146
+ console.error(`[swap] ${url}`, error);
147
+ element.dispatchEvent(
148
+ new CustomEvent("jskelet:swap-error", {
149
+ bubbles: true,
150
+ detail: { url, error },
151
+ }),
152
+ );
153
+ }
154
+ return false;
155
+ } finally {
156
+ element.removeAttribute("aria-busy");
157
+ }
158
+ }
159
+
160
+ /**
161
+ * `data-swap` taşıyan bağlantı ve butonları otomatik bağlar.
162
+ *
163
+ * <a href="/_fragment/rows?page=2" data-swap="#rows" data-swap-history>…</a>
164
+ *
165
+ * Tek bir delege dinleyici: sonradan DOM'a giren bağlantılar da çalışır ve
166
+ * her takastan sonra yeniden bağlama gerekmez.
167
+ *
168
+ * @param {ParentNode} [root]
169
+ * @returns {() => void} Dinleyiciyi söken fonksiyon.
170
+ */
171
+ export function startSwapLinks(root = document) {
172
+ /** @param {Event} event */
173
+ const onClick = (event) => {
174
+ const origin = /** @type {HTMLElement | null} */ (event.target);
175
+ const trigger = origin?.closest?.("[data-swap]");
176
+ if (!(trigger instanceof HTMLElement)) return;
177
+
178
+ const selector = trigger.dataset.swap;
179
+ const url = trigger.getAttribute("href") ?? trigger.dataset.swapUrl;
180
+ if (!selector || !url) return;
181
+
182
+ event.preventDefault();
183
+ void swap(selector, url, { history: "swapHistory" in trigger.dataset });
184
+ };
185
+
186
+ root.addEventListener("click", onClick);
187
+ return () => root.removeEventListener("click", onClick);
188
+ }
@@ -55,6 +55,55 @@ export const DEFAULT_PREWARM = {
55
55
  concurrency: 4,
56
56
  /** İki tur arasında beklenen süre: upstream'e ani yük binmesin. */
57
57
  delayMs: 0,
58
+ /**
59
+ * Saniyedeki en fazla ısıtma isteği. 0 → sınırsız (yalnızca `concurrency`
60
+ * frenler). Upstream'i kota sınırının altında tutmanın en doğrudan yolu bu:
61
+ * paralellik ne kadar yükselse de tur bu hızın üstüne çıkmaz.
62
+ */
63
+ rps: 0,
64
+ /**
65
+ * Tekrar turundan önce beklenen süre. Rate limit pencereleri saniye
66
+ * mertebesinde; hemen tekrar denemek aynı 429'u almak demek.
67
+ */
68
+ retryDelayMs: 2000,
69
+ /**
70
+ * Liste `max`'tan uzunsa periyodik turlar kaldığı yerden devam eder.
71
+ * Böylece 10.000 yolluk bir site tek turda değil, turlar boyunca ısınır.
72
+ * `priority` eşleşen yollar her turda ısıtıldığı için rotasyon yalnızca
73
+ * kuyruğu dolaşır.
74
+ */
75
+ rotate: true,
76
+ /**
77
+ * Isıtma sırasını belirleyen desenler. String (`/haber/:slug`) ya da
78
+ * `RegExp` kabul eder; önce yazılan önce ısınır.
79
+ * @type {(string | RegExp)[]}
80
+ */
81
+ priority: [],
82
+ };
83
+
84
+ /**
85
+ * HTML önbelleğinin girdi sınırı. 500 girdi ortalama bir sayfa boyutunda
86
+ * yaklaşık 100-200 MB tutar; uzun kuyruklu siteler bunu yükseltmek yerine
87
+ * veri önbelleğine yaslanmalı (bkz. `DEFAULT_DATA_CACHE`).
88
+ */
89
+ export const DEFAULT_HTML_CACHE_MAX_ENTRIES = 500;
90
+
91
+ /**
92
+ * Upstream veri önbelleği.
93
+ *
94
+ * HTML önbelleğinden bilinçli olarak çok daha büyük: JSON, aynı sayfanın
95
+ * HTML'ine göre onlarca kat küçük. Uzun kuyruğu (on binlerce haber/etiket)
96
+ * HTML olarak tutmak imkânsız, verisini tutmak ise ucuz — ve API kotasını
97
+ * koruyan katman burası.
98
+ */
99
+ export const DEFAULT_DATA_CACHE = {
100
+ maxEntries: 10000,
101
+ /**
102
+ * TTL dolduktan sonra girdinin kaç TTL boyunca daha kullanılabileceği.
103
+ * HTML'deki 1 katsayısından yüksek: bayat veri, eksik sayfadan iyidir ve
104
+ * upstream düştüğünde tek elde kalan şey budur.
105
+ */
106
+ staleFactor: 10,
58
107
  };
59
108
 
60
109
  /** Oturuma bağlı sayfalar ısıtılmaz; uygulama kendi listesini verebilir. */
@@ -86,6 +135,40 @@ export const DEFAULT_NAVIGATION = {
86
135
  */
87
136
  export const DEFAULT_NAVIGATION_EXCLUDE = ["/api/*", "/_fragment/*"];
88
137
 
138
+ /**
139
+ * Güvenlik ayarları.
140
+ *
141
+ * `trustProxy` varsayılan olarak açık, çünkü JSkelet uygulamaları neredeyse
142
+ * her zaman bir ters proxy arkasında koşuyor ve doğru protokol/IP buna bağlı.
143
+ * Ama doğrudan internete açık bir sunucuda bu, istemcinin `X-Forwarded-For`
144
+ * uydurabilmesi demek — rate limit ve audit log girdiğinde kapatılmalı.
145
+ *
146
+ * `csrf.enabled` açık: framework gövdeyi kendisi ayrıştırdığı için bu yüzey
147
+ * onun sorumluluğu. Kontrol yalnızca **çapraz site olduğu belli** istekleri
148
+ * reddeder (`Origin` uyuşmuyor ya da `Sec-Fetch-Site: cross-site`); başlık
149
+ * hiç yoksa geçer, böylece webhook ve sunucudan sunucuya çağrılar bozulmaz.
150
+ *
151
+ * `csrf.token` kapalı: çift gönderim token'ı `Origin` göndermeyen eski
152
+ * tarayıcılar için ikinci katman ve formlara `csrfField()` eklenmesini
153
+ * gerektiriyor, yani açılması bilinçli bir karar olmalı.
154
+ */
155
+ export const DEFAULT_SECURITY = {
156
+ trustProxy: true,
157
+ /** @type {string | null} */
158
+ cookieSecret: null,
159
+ csrf: {
160
+ enabled: true,
161
+ token: false,
162
+ /** Ek olarak kabul edilen origin'ler (ör. ayrı bir admin alan adı). */
163
+ allowedOrigins: /** @type {string[]} */ ([]),
164
+ /** Kontrolden muaf yollar — webhook uçları buraya yazılır. */
165
+ exclude: /** @type {string[]} */ ([]),
166
+ cookieName: "csrf_token",
167
+ fieldName: "_csrf",
168
+ headerName: "x-csrf-token",
169
+ },
170
+ };
171
+
89
172
  /**
90
173
  * Markalama. Header adı ve dev overlay yolu tek yerden değişsin diye
91
174
  * config'ten okunur — fork eden proje kendi adını verebilir.