jskelet 0.1.4 → 0.1.6
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 +29 -0
- package/docs/06-cache.md +73 -8
- package/docs/07-yapilandirma.md +15 -5
- package/docs/09-dev-araclari.md +31 -6
- package/docs/11-tasima.md +2 -3
- package/docs/en/06-caching.md +73 -8
- package/docs/en/07-configuration.md +16 -5
- package/docs/en/09-dev-tools.md +30 -4
- package/docs/en/11-migration.md +2 -3
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +111 -46
- package/src/config/index.js +8 -0
- package/src/index.js +1 -0
- package/src/server/cache-deps.js +42 -0
- package/src/server/create-app.js +11 -0
- package/src/server/data-cache.js +23 -10
- package/src/server/dev/devtools.js +101 -15
- package/src/server/dev/socket.js +157 -0
- package/src/server/html-cache.js +312 -9
- package/src/server/prewarm.js +23 -10
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const BASE = "/__jskelet/dev";
|
|
14
|
+
/** Soket kurulamadığında düşülen yedek yoklama sıklığı. */
|
|
14
15
|
const POLL_MS = 2000;
|
|
15
16
|
const MAX_ERRORS = 100;
|
|
16
17
|
|
|
@@ -386,50 +387,112 @@ let restarts = 0;
|
|
|
386
387
|
/* -------------------------------------------------------- canlı yenileme */
|
|
387
388
|
|
|
388
389
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
390
|
+
* Sunucudan gelen her şey tek bir WebSocket üzerinden akar: istatistikler,
|
|
391
|
+
* CSS takası ve yeniden başlatma bildirimi. Eskiden istatistikler iki saniyede
|
|
392
|
+
* bir çekiliyordu; her açık sekme, panel kapalıyken bile sunucuya sürekli
|
|
393
|
+
* istek atıyordu.
|
|
394
|
+
*
|
|
395
|
+
* CSS değiştiğinde sayfa yenilenmez, yalnızca stylesheet yeni sürümle takas
|
|
396
|
+
* edilir. Sunucu yeniden başladığında (boot kimliği değişince) tek sefer tam
|
|
397
|
+
* yenileme yapılır; overlay durumu sekme belleğinde durduğu için panel açık
|
|
398
|
+
* kalmaya devam eder.
|
|
393
399
|
*/
|
|
394
|
-
function
|
|
395
|
-
|
|
400
|
+
function connectSocket() {
|
|
401
|
+
let socket;
|
|
402
|
+
try {
|
|
403
|
+
socket = new WebSocket(
|
|
404
|
+
`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${BASE}/ws`,
|
|
405
|
+
);
|
|
406
|
+
} catch {
|
|
407
|
+
startFallback();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
396
410
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
411
|
+
// Soket hiç açılamazsa (proxy WebSocket'i geçirmiyor olabilir) eski
|
|
412
|
+
// SSE + yoklama yoluna düşülür; dev akışı bir ara katman yüzünden körelmesin.
|
|
413
|
+
let opened = false;
|
|
400
414
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
sessionStorage.setItem(BOOT_KEY, payload.boot);
|
|
415
|
+
socket.addEventListener("open", () => {
|
|
416
|
+
opened = true;
|
|
417
|
+
});
|
|
405
418
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
419
|
+
socket.addEventListener("message", (event) => {
|
|
420
|
+
handleServerMessage(JSON.parse(event.data));
|
|
421
|
+
});
|
|
411
422
|
|
|
412
|
-
|
|
413
|
-
|
|
423
|
+
socket.addEventListener("close", () => {
|
|
424
|
+
if (!opened) {
|
|
425
|
+
startFallback();
|
|
414
426
|
return;
|
|
415
427
|
}
|
|
416
428
|
|
|
417
|
-
|
|
418
|
-
|
|
429
|
+
// Sunucu yeniden başlıyor: gösterge "bağlantı yok"a döner ve kısa aralıkla
|
|
430
|
+
// yeniden denenir. Açılışta gelen `hello` yeniden başlatmayı bildirir.
|
|
431
|
+
if (!offline) {
|
|
432
|
+
offline = true;
|
|
433
|
+
render();
|
|
434
|
+
}
|
|
435
|
+
setTimeout(connectSocket, 500);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Hem soketten hem yedek SSE akışından gelen paketler burada işlenir.
|
|
441
|
+
* @param {{ type: string, boot?: string, href?: string }} payload
|
|
442
|
+
*/
|
|
443
|
+
function handleServerMessage(payload) {
|
|
444
|
+
if (payload.type === "stats") {
|
|
445
|
+
applyStats(payload);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (payload.type === "hello") {
|
|
450
|
+
offline = false;
|
|
451
|
+
const previous = sessionStorage.getItem(BOOT_KEY);
|
|
452
|
+
sessionStorage.setItem(BOOT_KEY, payload.boot);
|
|
453
|
+
|
|
454
|
+
if (previous && previous !== payload.boot) {
|
|
455
|
+
restarts += 1;
|
|
456
|
+
location.reload();
|
|
419
457
|
return;
|
|
420
458
|
}
|
|
421
459
|
|
|
422
|
-
|
|
423
|
-
|
|
460
|
+
bootId = payload.boot;
|
|
461
|
+
render();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (payload.type === "css") {
|
|
466
|
+
swapStylesheet(payload.href);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (payload.type === "reload") location.reload();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* WebSocket kurulamadığında eski yol: SSE + periyodik yoklama. Bir kez
|
|
475
|
+
* başlatılır.
|
|
476
|
+
*/
|
|
477
|
+
let fallbackStarted = false;
|
|
424
478
|
|
|
479
|
+
function startFallback() {
|
|
480
|
+
if (fallbackStarted) return;
|
|
481
|
+
fallbackStarted = true;
|
|
482
|
+
|
|
483
|
+
const source = new EventSource(`${BASE}/events`);
|
|
484
|
+
source.addEventListener("message", (event) =>
|
|
485
|
+
handleServerMessage(JSON.parse(event.data)),
|
|
486
|
+
);
|
|
425
487
|
source.addEventListener("error", () => {
|
|
426
|
-
// Sunucu yeniden başlarken bağlantı düşer; EventSource kendi kendine
|
|
427
|
-
// yeniden bağlanır, biz yalnızca göstergeyi güncelleriz.
|
|
428
488
|
if (!offline) {
|
|
429
489
|
offline = true;
|
|
430
490
|
render();
|
|
431
491
|
}
|
|
432
492
|
});
|
|
493
|
+
|
|
494
|
+
setInterval(pollServer, POLL_MS);
|
|
495
|
+
pollServer();
|
|
433
496
|
}
|
|
434
497
|
|
|
435
498
|
/**
|
|
@@ -446,24 +509,30 @@ function swapStylesheet(href) {
|
|
|
446
509
|
current.after(next);
|
|
447
510
|
}
|
|
448
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Sunucudan gelen istatistik paketini panele işler.
|
|
514
|
+
* @param {object} stats
|
|
515
|
+
*/
|
|
516
|
+
function applyStats(stats) {
|
|
517
|
+
// Süreç kimliği değiştiyse sunucu yeniden başlamıştır. Overlay kapanmaz,
|
|
518
|
+
// yalnızca sayacı artar; günlükler sunucuda kalıcı olduğu için de silinmez.
|
|
519
|
+
if (bootId && stats.boot !== bootId) restarts += 1;
|
|
520
|
+
bootId = stats.boot ?? bootId;
|
|
521
|
+
|
|
522
|
+
offline = false;
|
|
523
|
+
serverStats = stats;
|
|
524
|
+
render();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Yalnızca yedek yolda kullanılır; canlı veri soketten gelir. */
|
|
449
528
|
async function pollServer() {
|
|
529
|
+
if (!fallbackStarted) return;
|
|
530
|
+
|
|
450
531
|
try {
|
|
451
532
|
const response = await fetch(`${BASE}/stats`, { cache: "no-store" });
|
|
452
533
|
if (!response.ok) return;
|
|
453
534
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// Süreç kimliği değiştiyse sunucu yeniden başlamıştır. Overlay kapanmaz,
|
|
457
|
-
// yalnızca sayacı artar; günlükler sunucuda kalıcı olduğu için de silinmez.
|
|
458
|
-
if (bootId && stats.boot !== bootId) restarts += 1;
|
|
459
|
-
bootId = stats.boot ?? bootId;
|
|
460
|
-
|
|
461
|
-
offline = false;
|
|
462
|
-
serverStats = stats;
|
|
463
|
-
render();
|
|
464
|
-
|
|
465
|
-
// Isıtma sürerken sayaç akıcı görünsün diye yoklama sıklaşır.
|
|
466
|
-
if (stats.prewarm?.active) setTimeout(pollServer, 600);
|
|
535
|
+
applyStats(await response.json());
|
|
467
536
|
} catch {
|
|
468
537
|
// Yeniden başlatma penceresi: eldeki veriler korunur, yalnızca durum
|
|
469
538
|
// göstergesi "bağlantı yok"a döner.
|
|
@@ -1763,13 +1832,9 @@ function start() {
|
|
|
1763
1832
|
bind(ensureRoot());
|
|
1764
1833
|
render();
|
|
1765
1834
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
// Panel kapalıyken de rozet güncel kalsın diye sunucu yine yoklanır.
|
|
1770
|
-
pollServer();
|
|
1771
|
-
}, POLL_MS);
|
|
1772
|
-
pollServer();
|
|
1835
|
+
// Panel kapalıyken de rozet güncel kalsın diye kanal her zaman açılır;
|
|
1836
|
+
// maliyeti tek bir bağlantı ve yalnızca değişiklik oldukça gelen paketler.
|
|
1837
|
+
connectSocket();
|
|
1773
1838
|
|
|
1774
1839
|
// Ölçümler oturmadan gönderilmesin; sonra sekmeden ayrılırken güncellenir.
|
|
1775
1840
|
setTimeout(() => sendPageReport(), 3000);
|
package/src/config/index.js
CHANGED
|
@@ -70,6 +70,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
70
70
|
* @property {number} htmlMaxEntries HTML önbelleğinin girdi sınırı.
|
|
71
71
|
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
72
72
|
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
73
|
+
* @property {boolean} trackDependencies Render'ın okuduğu veri anahtarları kaydedilsin mi.
|
|
73
74
|
* @property {{ attempts: number, delayMs: number }} transientRetry
|
|
74
75
|
* @property {Record<string, unknown>} prewarm
|
|
75
76
|
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
@@ -210,6 +211,7 @@ function normalizePriority(raw) {
|
|
|
210
211
|
* @param {unknown} raw
|
|
211
212
|
* @returns {{ html: ResolvedConfig["html"], htmlMaxEntries: number,
|
|
212
213
|
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
214
|
+
* trackDependencies: boolean,
|
|
213
215
|
* transientRetry: { attempts: number, delayMs: number },
|
|
214
216
|
* prewarm: Record<string, unknown>,
|
|
215
217
|
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
@@ -238,6 +240,10 @@ function normalizeCache(raw) {
|
|
|
238
240
|
// Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
|
|
239
241
|
// bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
|
|
240
242
|
trackUpstream: raw?.trackUpstream !== false,
|
|
243
|
+
// Hangi sayfanın hangi veri anahtarını okuduğu kaydedilsin mi.
|
|
244
|
+
// `withDataCache` kullanmayan bir uygulamada kaydedilecek bir şey yok;
|
|
245
|
+
// kapatmak bağlam kurma maliyetini de kaldırır.
|
|
246
|
+
trackDependencies: raw?.trackDependencies !== false,
|
|
241
247
|
transientRetry:
|
|
242
248
|
raw?.transientRetry === false
|
|
243
249
|
? { attempts: 0, delayMs: 0 }
|
|
@@ -457,6 +463,7 @@ export async function loadConfig(options = {}) {
|
|
|
457
463
|
htmlMaxEntries,
|
|
458
464
|
data,
|
|
459
465
|
trackUpstream,
|
|
466
|
+
trackDependencies,
|
|
460
467
|
transientRetry,
|
|
461
468
|
prewarm,
|
|
462
469
|
prewarmPriority,
|
|
@@ -475,6 +482,7 @@ export async function loadConfig(options = {}) {
|
|
|
475
482
|
htmlMaxEntries,
|
|
476
483
|
data,
|
|
477
484
|
trackUpstream,
|
|
485
|
+
trackDependencies,
|
|
478
486
|
transientRetry,
|
|
479
487
|
prewarm,
|
|
480
488
|
prewarmPriority,
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bir render'ın hangi veri anahtarlarını okuduğunu kaydeder.
|
|
3
|
+
*
|
|
4
|
+
* `request-cache.js` ile aynı desen: `AsyncLocalStorage`, bağlam yoksa her şey
|
|
5
|
+
* sessizce devre dışı. Script'ten, cron'dan ya da istek dışı bir yerden yapılan
|
|
6
|
+
* `withDataCache` çağrıları hiçbir şeye yazılmaz.
|
|
7
|
+
*
|
|
8
|
+
* Neden gerekli: HTML önbelleğinin elinde "bu sayfa şu içerikten etkilenir"
|
|
9
|
+
* bilgisi yoktu, dolayısıyla bir içerik güncellendiğinde tek seçenek TTL'i
|
|
10
|
+
* beklemek ya da tüm önbelleği boşaltmaktı. Bağımlılığı uygulamanın elle
|
|
11
|
+
* bildirmesi (tag'lemek) ise en sık yapılan hatayı davet ediyor: detay
|
|
12
|
+
* sayfasını işaretleyip aynı içeriği listeleyen ana sayfayı unutmak.
|
|
13
|
+
*
|
|
14
|
+
* Burada bildirim yok, **gözlem** var: render sırasında fiilen okunan anahtarlar
|
|
15
|
+
* kaydedilir. Ana sayfa o veriyi okuduysa listede olur, okumadıysa olmaz.
|
|
16
|
+
*/
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
+
|
|
19
|
+
/** @type {AsyncLocalStorage<Set<string>>} */
|
|
20
|
+
const storage = new AsyncLocalStorage();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `run`'ı, içindeki `recordDependency()` çağrılarının `deps`'e yazacağı bir
|
|
24
|
+
* bağlamda çalıştırır.
|
|
25
|
+
*
|
|
26
|
+
* @template T
|
|
27
|
+
* @param {Set<string>} deps
|
|
28
|
+
* @param {() => T} run
|
|
29
|
+
* @returns {T}
|
|
30
|
+
*/
|
|
31
|
+
export function collectDependencies(deps, run) {
|
|
32
|
+
return storage.run(deps, run);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Bu render'ın bir veri anahtarını okuduğunu bildirir. Bağlam yoksa no-op.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} key
|
|
39
|
+
*/
|
|
40
|
+
export function recordDependency(key) {
|
|
41
|
+
storage.getStore()?.add(key);
|
|
42
|
+
}
|
package/src/server/create-app.js
CHANGED
|
@@ -167,6 +167,15 @@ export async function startServer(options = {}) {
|
|
|
167
167
|
console.error("[uncaughtException]", error);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
// Dev panelinin canlı kanalı: el sıkışma `upgrade` olayında geçtiği için
|
|
171
|
+
// middleware zincirine değil, doğrudan sunucuya bağlanır. Modül `listen`den
|
|
172
|
+
// önce yüklenir; dinleme başladıktan sonra beklenen bir `await` kalırsa ilk
|
|
173
|
+
// upgrade isteği dinleyici yokken gelip reddedilebiliyor.
|
|
174
|
+
const attachDevSocket =
|
|
175
|
+
process.env.NODE_ENV === "development"
|
|
176
|
+
? (await import("./dev/devtools.js")).attachDevSocket
|
|
177
|
+
: null;
|
|
178
|
+
|
|
170
179
|
return new Promise((resolve) => {
|
|
171
180
|
const server = app.listen(port, host, () => {
|
|
172
181
|
// Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
|
|
@@ -177,5 +186,7 @@ export async function startServer(options = {}) {
|
|
|
177
186
|
startPrewarm({ port });
|
|
178
187
|
resolve(server);
|
|
179
188
|
});
|
|
189
|
+
|
|
190
|
+
attachDevSocket?.(server);
|
|
180
191
|
});
|
|
181
192
|
}
|
package/src/server/data-cache.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { getConfig } from "../config/index.js";
|
|
21
21
|
import { DEFAULT_DATA_CACHE } from "../config/defaults.js";
|
|
22
|
+
import { recordDependency } from "./cache-deps.js";
|
|
23
|
+
import { invalidateHtmlByDependency } from "./html-cache.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* @typedef {{ value: unknown, expiresAt: number, staleUntil: number }} DataEntry
|
|
@@ -144,6 +146,10 @@ function refresh(key, ttlSeconds, producer, options) {
|
|
|
144
146
|
export async function withDataCache(key, ttlSeconds, producer, options = {}) {
|
|
145
147
|
if (!ttlSeconds) return producer();
|
|
146
148
|
|
|
149
|
+
// Bu anahtarı okuyan render, `clearDataCache(key)` çağrıldığında etkilenen
|
|
150
|
+
// sayfalar arasında sayılsın. Render bağlamı yoksa çağrı no-op.
|
|
151
|
+
recordDependency(key);
|
|
152
|
+
|
|
147
153
|
const hit = read(key);
|
|
148
154
|
|
|
149
155
|
if (hit) {
|
|
@@ -202,24 +208,31 @@ export function dataCache(fn, options) {
|
|
|
202
208
|
* Bir anahtarı ya da önek eşleşen tüm anahtarları düşürür. Webhook ile
|
|
203
209
|
* "bu haber güncellendi" bilgisi geldiğinde kullanılır.
|
|
204
210
|
*
|
|
211
|
+
* Düşen anahtarları **render sırasında okumuş** HTML girdileri de bayatlar:
|
|
212
|
+
* uygulamanın ayrıca `invalidateHtmlCache()` çağırması gerekmez ve aynı veriyi
|
|
213
|
+
* gösteren liste sayfalarını unutmak mümkün değildir (bkz. `cache-deps.js`).
|
|
214
|
+
*
|
|
205
215
|
* @param {string} [prefix] Verilmezse tüm önbellek boşaltılır.
|
|
206
216
|
* @returns {number} Silinen girdi sayısı.
|
|
207
217
|
*/
|
|
208
218
|
export function clearDataCache(prefix) {
|
|
219
|
+
/** @type {string[]} */
|
|
220
|
+
const removed = [];
|
|
221
|
+
|
|
209
222
|
if (prefix === undefined) {
|
|
210
|
-
|
|
223
|
+
removed.push(...store.keys());
|
|
211
224
|
store.clear();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
store.delete(key);
|
|
219
|
-
removed += 1;
|
|
225
|
+
} else {
|
|
226
|
+
for (const key of store.keys()) {
|
|
227
|
+
if (key.startsWith(prefix)) {
|
|
228
|
+
store.delete(key);
|
|
229
|
+
removed.push(key);
|
|
230
|
+
}
|
|
220
231
|
}
|
|
221
232
|
}
|
|
222
|
-
|
|
233
|
+
|
|
234
|
+
if (removed.length) invalidateHtmlByDependency(removed);
|
|
235
|
+
return removed.length;
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
/** @returns {number} */
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
trackServerFetch,
|
|
23
23
|
} from "./report.js";
|
|
24
24
|
import { startVersionCheck, versionStatus } from "./version-check.mjs";
|
|
25
|
+
import { broadcastSocket, socketCount, upgradeToSocket } from "./socket.js";
|
|
25
26
|
|
|
26
27
|
/** Overlay dosyaları framework paketinden servis edilir, uygulamadan değil. */
|
|
27
28
|
const DEVTOOLS_DIR = path.join(FRAMEWORK_ROOT, "src", "client", "devtools");
|
|
@@ -115,6 +116,7 @@ export function recordServerError(level, message, extra = {}) {
|
|
|
115
116
|
});
|
|
116
117
|
trim(errors);
|
|
117
118
|
persist();
|
|
119
|
+
pushStats();
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
@@ -177,6 +179,7 @@ function timing() {
|
|
|
177
179
|
requests.push(entry);
|
|
178
180
|
trim(requests);
|
|
179
181
|
persist();
|
|
182
|
+
pushStats();
|
|
180
183
|
// Terminalde canlı istek satırı.
|
|
181
184
|
log.http(entry);
|
|
182
185
|
});
|
|
@@ -185,6 +188,70 @@ function timing() {
|
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
/* ----------------------------------------------------------- istatistikler */
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Overlay'in gösterdiği her şey tek pakette. `GET /stats` ve WebSocket aynı
|
|
195
|
+
* gövdeyi kullanır ki panel hangi yoldan beslenirse beslensin aynı şeyi
|
|
196
|
+
* görsün.
|
|
197
|
+
*
|
|
198
|
+
* @returns {object}
|
|
199
|
+
*/
|
|
200
|
+
function statsPayload() {
|
|
201
|
+
const usage = process.memoryUsage();
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
type: "stats",
|
|
205
|
+
pid: process.pid,
|
|
206
|
+
// Overlay yeniden başlatmayı bu kimlikten anlar; kendi durumunu
|
|
207
|
+
// sıfırlamadan yalnızca "restarted" bilgisini gösterir.
|
|
208
|
+
boot: BOOT_ID,
|
|
209
|
+
uptime: process.uptime(),
|
|
210
|
+
node: process.version,
|
|
211
|
+
version: versionStatus(),
|
|
212
|
+
memory: { rss: usage.rss, heapUsed: usage.heapUsed },
|
|
213
|
+
prewarm: { ...prewarmProgress },
|
|
214
|
+
requests: requests.slice(-25).reverse(),
|
|
215
|
+
errors: errors.slice(-25).reverse(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @type {NodeJS.Timeout | null} */
|
|
220
|
+
let statsTimer = null;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Değişiklikleri panele iter. Bir sayfa yüklemesi arka arkaya birçok kayıt
|
|
224
|
+
* üretiyor (istek + uyarılar); paket başına bir çerçeve yerine kısa bir
|
|
225
|
+
* sessizlikten sonra tek çerçeve gönderilir.
|
|
226
|
+
*/
|
|
227
|
+
function pushStats() {
|
|
228
|
+
if (statsTimer || !socketCount()) return;
|
|
229
|
+
|
|
230
|
+
statsTimer = setTimeout(() => {
|
|
231
|
+
statsTimer = null;
|
|
232
|
+
broadcastSocket(statsPayload());
|
|
233
|
+
}, 120);
|
|
234
|
+
|
|
235
|
+
statsTimer.unref?.();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Zamana bağlı alanlar (uptime, bellek, ısıtma sayacı) bir olay üretmiyor;
|
|
240
|
+
* onlar için sabit bir kalp atışı var. Bilinçli olarak ısıtmadan bağımsız:
|
|
241
|
+
* kanalın temposu bir arka plan işine göre değişirse panel de o işin ritmine
|
|
242
|
+
* bağlanmış olur.
|
|
243
|
+
*
|
|
244
|
+
* Bağlı panel yokken hiçbir şey hesaplanmaz.
|
|
245
|
+
*/
|
|
246
|
+
function startHeartbeat() {
|
|
247
|
+
const timer = setInterval(() => {
|
|
248
|
+
if (!socketCount()) return;
|
|
249
|
+
broadcastSocket(statsPayload());
|
|
250
|
+
}, 2000);
|
|
251
|
+
|
|
252
|
+
timer.unref?.();
|
|
253
|
+
}
|
|
254
|
+
|
|
188
255
|
/* ------------------------------------------------------------ live reload */
|
|
189
256
|
|
|
190
257
|
/** @type {Set<import('express').Response>} */
|
|
@@ -198,8 +265,14 @@ function send(res, payload) {
|
|
|
198
265
|
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
199
266
|
}
|
|
200
267
|
|
|
201
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Canlı yenileme olayları. Panel normalde WebSocket üzerinden dinler; SSE
|
|
270
|
+
* yalnızca soket kurulamadığında devreye giren yedek yol.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} payload
|
|
273
|
+
*/
|
|
202
274
|
function broadcast(payload) {
|
|
275
|
+
broadcastSocket(payload);
|
|
203
276
|
for (const client of clients) send(client, payload);
|
|
204
277
|
}
|
|
205
278
|
|
|
@@ -285,22 +358,10 @@ function router() {
|
|
|
285
358
|
req.on("close", () => clients.delete(res));
|
|
286
359
|
});
|
|
287
360
|
|
|
361
|
+
// WebSocket kurulamadığında panelin düştüğü yedek uç.
|
|
288
362
|
api.get("/stats", (req, res) => {
|
|
289
|
-
const usage = process.memoryUsage();
|
|
290
363
|
res.setHeader("Cache-Control", "no-store");
|
|
291
|
-
res.json(
|
|
292
|
-
pid: process.pid,
|
|
293
|
-
// Overlay yeniden başlatmayı bu kimlikten anlar; kendi durumunu
|
|
294
|
-
// sıfırlamadan yalnızca "restarted" bilgisini gösterir.
|
|
295
|
-
boot: BOOT_ID,
|
|
296
|
-
uptime: process.uptime(),
|
|
297
|
-
node: process.version,
|
|
298
|
-
version: versionStatus(),
|
|
299
|
-
memory: { rss: usage.rss, heapUsed: usage.heapUsed },
|
|
300
|
-
prewarm: { ...prewarmProgress },
|
|
301
|
-
requests: requests.slice(-25).reverse(),
|
|
302
|
-
errors: errors.slice(-25).reverse(),
|
|
303
|
-
});
|
|
364
|
+
res.json(statsPayload());
|
|
304
365
|
});
|
|
305
366
|
|
|
306
367
|
// Detaylı rapor: kendi sayfası, script'i ve veri ucu.
|
|
@@ -364,6 +425,7 @@ function router() {
|
|
|
364
425
|
errors.length = 0;
|
|
365
426
|
requests.length = 0;
|
|
366
427
|
persist();
|
|
428
|
+
pushStats();
|
|
367
429
|
res.json({ ok: true });
|
|
368
430
|
});
|
|
369
431
|
|
|
@@ -382,6 +444,30 @@ export function mountDevtools(app) {
|
|
|
382
444
|
trackServerFetch();
|
|
383
445
|
watchManifest();
|
|
384
446
|
startVersionCheck();
|
|
447
|
+
startHeartbeat();
|
|
385
448
|
app.use(timing());
|
|
386
449
|
app.use(brand.devBasePath, router());
|
|
387
450
|
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Canlı kanalı HTTP sunucusuna bağlar.
|
|
454
|
+
*
|
|
455
|
+
* Express uygulamasına takılamıyor: WebSocket el sıkışması `upgrade` olayında
|
|
456
|
+
* geçiyor ve o olay middleware zincirine hiç uğramıyor. Bu yüzden `listen`
|
|
457
|
+
* sonrası ayrı bir adım.
|
|
458
|
+
*
|
|
459
|
+
* @param {import('node:http').Server} server
|
|
460
|
+
*/
|
|
461
|
+
export function attachDevSocket(server) {
|
|
462
|
+
const endpoint = `${getConfig().brand.devBasePath}/ws`;
|
|
463
|
+
|
|
464
|
+
server.on("upgrade", (req, socket) => {
|
|
465
|
+
// Uygulamanın kendi WebSocket uçları olabilir; yalnızca bizimkini alırız.
|
|
466
|
+
if ((req.url ?? "").split("?")[0] !== endpoint) return;
|
|
467
|
+
|
|
468
|
+
upgradeToSocket(req, socket, (send) => {
|
|
469
|
+
send({ type: "hello", boot: BOOT_ID });
|
|
470
|
+
send(statsPayload());
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|