jskelet 0.1.3 → 0.1.5
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 +31 -6
- package/docs/06-cache.md +65 -12
- package/docs/07-yapilandirma.md +20 -2
- package/docs/09-dev-araclari.md +27 -5
- package/docs/en/06-caching.md +70 -15
- package/docs/en/07-configuration.md +21 -2
- package/docs/en/09-dev-tools.md +26 -3
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +111 -46
- package/src/config/defaults.js +14 -0
- package/src/config/index.js +24 -3
- package/src/server/create-app.js +14 -1
- package/src/server/dev/devtools.js +106 -15
- package/src/server/dev/socket.js +157 -0
- package/src/server/render.js +111 -38
- package/src/server/upstream-tracking.js +103 -13
|
@@ -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/defaults.js
CHANGED
|
@@ -88,6 +88,20 @@ export const DEFAULT_PREWARM = {
|
|
|
88
88
|
*/
|
|
89
89
|
export const DEFAULT_HTML_CACHE_MAX_ENTRIES = 500;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* `notFound()` geçici bir upstream hatasına denk geldiğinde sayfanın kaç kez
|
|
93
|
+
* daha denenmesi gerektiği.
|
|
94
|
+
*
|
|
95
|
+
* Varsayılan tek deneme: maliyeti upstream'e binen ikinci bir istek turu, ama
|
|
96
|
+
* alternatifi var olan bir sayfayı 404 olarak servis etmek — arama motoru için
|
|
97
|
+
* geçici bir rate limit'in kalıcı kayba dönüşmesi. `attempts: 0` tekrarı
|
|
98
|
+
* kapatır ve doğrudan önbelleğe girmeyen 503'e düşer.
|
|
99
|
+
*/
|
|
100
|
+
export const DEFAULT_TRANSIENT_RETRY = {
|
|
101
|
+
attempts: 1,
|
|
102
|
+
delayMs: 300,
|
|
103
|
+
};
|
|
104
|
+
|
|
91
105
|
/**
|
|
92
106
|
* Upstream veri önbelleği.
|
|
93
107
|
*
|
package/src/config/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
DEFAULT_PREWARM_SKIP,
|
|
39
39
|
DEFAULT_SECURITY,
|
|
40
40
|
DEFAULT_STATIC,
|
|
41
|
+
DEFAULT_TRANSIENT_RETRY,
|
|
41
42
|
} from "./defaults.js";
|
|
42
43
|
|
|
43
44
|
/** Framework paketinin kökü — kendi şablonlarına ve varlıklarına erişir. */
|
|
@@ -68,6 +69,8 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
68
69
|
* @property {{ pattern: CompiledPattern, seconds: number }[]} html
|
|
69
70
|
* @property {number} htmlMaxEntries HTML önbelleğinin girdi sınırı.
|
|
70
71
|
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
72
|
+
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
73
|
+
* @property {{ attempts: number, delayMs: number }} transientRetry
|
|
71
74
|
* @property {Record<string, unknown>} prewarm
|
|
72
75
|
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
73
76
|
* @property {Record<string, unknown>} brand
|
|
@@ -206,7 +209,9 @@ function normalizePriority(raw) {
|
|
|
206
209
|
/**
|
|
207
210
|
* @param {unknown} raw
|
|
208
211
|
* @returns {{ html: ResolvedConfig["html"], htmlMaxEntries: number,
|
|
209
|
-
* data: Record<string, unknown>,
|
|
212
|
+
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
213
|
+
* transientRetry: { attempts: number, delayMs: number },
|
|
214
|
+
* prewarm: Record<string, unknown>,
|
|
210
215
|
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
211
216
|
*/
|
|
212
217
|
function normalizeCache(raw) {
|
|
@@ -230,6 +235,13 @@ function normalizeCache(raw) {
|
|
|
230
235
|
? Math.floor(maxEntries)
|
|
231
236
|
: DEFAULT_HTML_CACHE_MAX_ENTRIES,
|
|
232
237
|
data: { ...DEFAULT_DATA_CACHE, ...(raw?.data ?? {}) },
|
|
238
|
+
// Otomatik upstream izleme kapatılabilir olmalı: `fetch`i kendisi saran
|
|
239
|
+
// bir uygulama (ölçüm, retry, circuit breaker) çakışma yaşayabilir.
|
|
240
|
+
trackUpstream: raw?.trackUpstream !== false,
|
|
241
|
+
transientRetry:
|
|
242
|
+
raw?.transientRetry === false
|
|
243
|
+
? { attempts: 0, delayMs: 0 }
|
|
244
|
+
: { ...DEFAULT_TRANSIENT_RETRY, ...(raw?.transientRetry ?? {}) },
|
|
233
245
|
// Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
|
|
234
246
|
// ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
|
|
235
247
|
prewarm,
|
|
@@ -440,8 +452,15 @@ export async function loadConfig(options = {}) {
|
|
|
440
452
|
section("cache"),
|
|
441
453
|
]);
|
|
442
454
|
|
|
443
|
-
const {
|
|
444
|
-
|
|
455
|
+
const {
|
|
456
|
+
html,
|
|
457
|
+
htmlMaxEntries,
|
|
458
|
+
data,
|
|
459
|
+
trackUpstream,
|
|
460
|
+
transientRetry,
|
|
461
|
+
prewarm,
|
|
462
|
+
prewarmPriority,
|
|
463
|
+
} = normalizeCache(cache);
|
|
445
464
|
const dirs = resolveDirs(root, source.paths);
|
|
446
465
|
const brand = { ...DEFAULT_BRAND, ...(source.brand ?? {}) };
|
|
447
466
|
|
|
@@ -455,6 +474,8 @@ export async function loadConfig(options = {}) {
|
|
|
455
474
|
html,
|
|
456
475
|
htmlMaxEntries,
|
|
457
476
|
data,
|
|
477
|
+
trackUpstream,
|
|
478
|
+
transientRetry,
|
|
458
479
|
prewarm,
|
|
459
480
|
prewarmPriority,
|
|
460
481
|
brand,
|
package/src/server/create-app.js
CHANGED
|
@@ -34,6 +34,7 @@ import { registerRoutes } from "./router.js";
|
|
|
34
34
|
import { renderNotFound } from "./render.js";
|
|
35
35
|
import { renderStatusPage, statusFromError } from "./status-page.js";
|
|
36
36
|
import { startPrewarm } from "./prewarm.js";
|
|
37
|
+
import { trackUpstreamFetch } from "./upstream-tracking.js";
|
|
37
38
|
import { isNotFoundError, isRedirectError } from "../http/control-flow.js";
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -45,6 +46,11 @@ export async function createApp(options = {}) {
|
|
|
45
46
|
await loadConfig(options);
|
|
46
47
|
const config = getConfig();
|
|
47
48
|
|
|
49
|
+
// Upstream hatalarının izlenmesi route'lardan önce kurulmalı: sarmalayıcı
|
|
50
|
+
// yalnızca render bağlamı içindeki `fetch` çağrılarına bakar, ama bağlamın
|
|
51
|
+
// ilk kurulduğu istek de kapsanmalı.
|
|
52
|
+
if (config.trackUpstream) trackUpstreamFetch();
|
|
53
|
+
|
|
48
54
|
const app = express();
|
|
49
55
|
|
|
50
56
|
app.disable("x-powered-by");
|
|
@@ -162,7 +168,14 @@ export async function startServer(options = {}) {
|
|
|
162
168
|
});
|
|
163
169
|
|
|
164
170
|
return new Promise((resolve) => {
|
|
165
|
-
const server = app.listen(port, host, () => {
|
|
171
|
+
const server = app.listen(port, host, async () => {
|
|
172
|
+
// Dev panelinin canlı kanalı: el sıkışma `upgrade` olayında geçtiği
|
|
173
|
+
// için middleware zincirine değil, doğrudan sunucuya bağlanır.
|
|
174
|
+
if (process.env.NODE_ENV === "development") {
|
|
175
|
+
const { attachDevSocket } = await import("./dev/devtools.js");
|
|
176
|
+
attachDevSocket(server);
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
// Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
|
|
167
180
|
// buradan anlar ve özet satırını ona göre basar.
|
|
168
181
|
console.log(
|
|
@@ -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,75 @@ 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) ve ısıtma ilerlemesi bir olay
|
|
240
|
+
* üretmiyor; onlar için düşük frekanslı bir kalp atışı gerekiyor. Isıtma
|
|
241
|
+
* sürerken sayaç akıcı görünsün diye sıklaşır.
|
|
242
|
+
*
|
|
243
|
+
* Bağlı panel yokken hiçbir şey hesaplanmaz.
|
|
244
|
+
*/
|
|
245
|
+
function startHeartbeat() {
|
|
246
|
+
let tick = 0;
|
|
247
|
+
|
|
248
|
+
const timer = setInterval(() => {
|
|
249
|
+
if (!socketCount()) return;
|
|
250
|
+
|
|
251
|
+
tick += 1;
|
|
252
|
+
if (!prewarmProgress.active && tick % 4 !== 0) return;
|
|
253
|
+
|
|
254
|
+
broadcastSocket(statsPayload());
|
|
255
|
+
}, 1000);
|
|
256
|
+
|
|
257
|
+
timer.unref?.();
|
|
258
|
+
}
|
|
259
|
+
|
|
188
260
|
/* ------------------------------------------------------------ live reload */
|
|
189
261
|
|
|
190
262
|
/** @type {Set<import('express').Response>} */
|
|
@@ -198,8 +270,14 @@ function send(res, payload) {
|
|
|
198
270
|
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
199
271
|
}
|
|
200
272
|
|
|
201
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Canlı yenileme olayları. Panel normalde WebSocket üzerinden dinler; SSE
|
|
275
|
+
* yalnızca soket kurulamadığında devreye giren yedek yol.
|
|
276
|
+
*
|
|
277
|
+
* @param {object} payload
|
|
278
|
+
*/
|
|
202
279
|
function broadcast(payload) {
|
|
280
|
+
broadcastSocket(payload);
|
|
203
281
|
for (const client of clients) send(client, payload);
|
|
204
282
|
}
|
|
205
283
|
|
|
@@ -285,22 +363,10 @@ function router() {
|
|
|
285
363
|
req.on("close", () => clients.delete(res));
|
|
286
364
|
});
|
|
287
365
|
|
|
366
|
+
// WebSocket kurulamadığında panelin düştüğü yedek uç.
|
|
288
367
|
api.get("/stats", (req, res) => {
|
|
289
|
-
const usage = process.memoryUsage();
|
|
290
368
|
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
|
-
});
|
|
369
|
+
res.json(statsPayload());
|
|
304
370
|
});
|
|
305
371
|
|
|
306
372
|
// Detaylı rapor: kendi sayfası, script'i ve veri ucu.
|
|
@@ -364,6 +430,7 @@ function router() {
|
|
|
364
430
|
errors.length = 0;
|
|
365
431
|
requests.length = 0;
|
|
366
432
|
persist();
|
|
433
|
+
pushStats();
|
|
367
434
|
res.json({ ok: true });
|
|
368
435
|
});
|
|
369
436
|
|
|
@@ -382,6 +449,30 @@ export function mountDevtools(app) {
|
|
|
382
449
|
trackServerFetch();
|
|
383
450
|
watchManifest();
|
|
384
451
|
startVersionCheck();
|
|
452
|
+
startHeartbeat();
|
|
385
453
|
app.use(timing());
|
|
386
454
|
app.use(brand.devBasePath, router());
|
|
387
455
|
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Canlı kanalı HTTP sunucusuna bağlar.
|
|
459
|
+
*
|
|
460
|
+
* Express uygulamasına takılamıyor: WebSocket el sıkışması `upgrade` olayında
|
|
461
|
+
* geçiyor ve o olay middleware zincirine hiç uğramıyor. Bu yüzden `listen`
|
|
462
|
+
* sonrası ayrı bir adım.
|
|
463
|
+
*
|
|
464
|
+
* @param {import('node:http').Server} server
|
|
465
|
+
*/
|
|
466
|
+
export function attachDevSocket(server) {
|
|
467
|
+
const endpoint = `${getConfig().brand.devBasePath}/ws`;
|
|
468
|
+
|
|
469
|
+
server.on("upgrade", (req, socket) => {
|
|
470
|
+
// Uygulamanın kendi WebSocket uçları olabilir; yalnızca bizimkini alırız.
|
|
471
|
+
if ((req.url ?? "").split("?")[0] !== endpoint) return;
|
|
472
|
+
|
|
473
|
+
upgradeToSocket(req, socket, (send) => {
|
|
474
|
+
send({ type: "hello", boot: BOOT_ID });
|
|
475
|
+
send(statsPayload());
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev araçlarının canlı kanalı: küçük bir WebSocket sunucusu.
|
|
3
|
+
*
|
|
4
|
+
* Overlay eskiden istatistikleri iki saniyede bir `GET /stats` ile çekiyordu;
|
|
5
|
+
* panel kapalıyken bile her sekme sürekli istek atıyor, dev sunucusunun istek
|
|
6
|
+
* günlüğünü de kirletiyordu. Artık sunucu değişiklik oldukça (istek, hata,
|
|
7
|
+
* ısıtma ilerlemesi, CSS/JS değişimi) tek bağlantı üzerinden itiyor.
|
|
8
|
+
*
|
|
9
|
+
* Bağımlılık eklemiyoruz: yalnızca sunucu→istemci metin çerçevesi yazmak ve
|
|
10
|
+
* istemcinin ping/close çerçevelerini anlamak gerekiyor; bu da RFC 6455'in
|
|
11
|
+
* küçük bir alt kümesi. `permessage-deflate` yok, parça (fragment) yok —
|
|
12
|
+
* gönderdiğimiz paketler için ikisi de gereksiz.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
/** RFC 6455'te sabitlenmiş el sıkışma tuzu. */
|
|
17
|
+
const GUID = "258EAFA5-E914-47DA-95CA-5AB0DC85B39A";
|
|
18
|
+
|
|
19
|
+
/** @type {Set<import('node:net').Socket>} */
|
|
20
|
+
const sockets = new Set();
|
|
21
|
+
|
|
22
|
+
/** @param {string} key */
|
|
23
|
+
function accept(key) {
|
|
24
|
+
return createHash("sha1")
|
|
25
|
+
.update(key + GUID)
|
|
26
|
+
.digest("base64");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tek parçalı metin çerçevesi. Maskeleme yalnızca istemci→sunucu yönünde
|
|
31
|
+
* zorunlu olduğu için burada yok.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} text
|
|
34
|
+
* @returns {Buffer}
|
|
35
|
+
*/
|
|
36
|
+
function frame(text) {
|
|
37
|
+
const payload = Buffer.from(text, "utf8");
|
|
38
|
+
const length = payload.length;
|
|
39
|
+
|
|
40
|
+
/** @type {Buffer} */
|
|
41
|
+
let header;
|
|
42
|
+
if (length < 126) {
|
|
43
|
+
header = Buffer.from([0x81, length]);
|
|
44
|
+
} else if (length < 65536) {
|
|
45
|
+
header = Buffer.alloc(4);
|
|
46
|
+
header[0] = 0x81;
|
|
47
|
+
header[1] = 126;
|
|
48
|
+
header.writeUInt16BE(length, 2);
|
|
49
|
+
} else {
|
|
50
|
+
header = Buffer.alloc(10);
|
|
51
|
+
header[0] = 0x81;
|
|
52
|
+
header[1] = 127;
|
|
53
|
+
header.writeBigUInt64BE(BigInt(length), 2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Buffer.concat([header, payload]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* İstemciden gelen çerçeveleri ayıklar. İçerik bizi ilgilendirmiyor; amaç
|
|
61
|
+
* kapanışı ve ping'i doğru yanıtlamak, aksi hâlde tarayıcı bağlantıyı
|
|
62
|
+
* "protokol hatası" sayıp kapatıyor.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('node:net').Socket} socket
|
|
65
|
+
*/
|
|
66
|
+
function readFrames(socket) {
|
|
67
|
+
let buffer = Buffer.alloc(0);
|
|
68
|
+
|
|
69
|
+
socket.on("data", (chunk) => {
|
|
70
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
71
|
+
|
|
72
|
+
while (buffer.length >= 2) {
|
|
73
|
+
const opcode = buffer[0] & 0x0f;
|
|
74
|
+
const masked = (buffer[1] & 0x80) !== 0;
|
|
75
|
+
let length = buffer[1] & 0x7f;
|
|
76
|
+
let offset = 2;
|
|
77
|
+
|
|
78
|
+
if (length === 126) {
|
|
79
|
+
if (buffer.length < offset + 2) return;
|
|
80
|
+
length = buffer.readUInt16BE(offset);
|
|
81
|
+
offset += 2;
|
|
82
|
+
} else if (length === 127) {
|
|
83
|
+
if (buffer.length < offset + 8) return;
|
|
84
|
+
length = Number(buffer.readBigUInt64BE(offset));
|
|
85
|
+
offset += 8;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (masked) offset += 4;
|
|
89
|
+
if (buffer.length < offset + length) return;
|
|
90
|
+
|
|
91
|
+
buffer = buffer.subarray(offset + length);
|
|
92
|
+
|
|
93
|
+
// 0x8 close, 0x9 ping. Veri çerçeveleri yok sayılır: istemcinin bize
|
|
94
|
+
// söyleyeceği bir şey yok, kanal tek yönlü kullanılıyor.
|
|
95
|
+
if (opcode === 0x8) {
|
|
96
|
+
socket.end(Buffer.from([0x88, 0x00]));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (opcode === 0x9) socket.write(Buffer.from([0x8a, 0x00]));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* HTTP upgrade isteğini WebSocket bağlantısına çevirir.
|
|
106
|
+
*
|
|
107
|
+
* @param {import('node:http').IncomingMessage} req
|
|
108
|
+
* @param {import('node:net').Socket} socket
|
|
109
|
+
* @param {(send: (payload: object) => void) => void} onOpen
|
|
110
|
+
* Bağlantı kurulunca çağrılır; ilk paketi göndermek için kullanılır.
|
|
111
|
+
*/
|
|
112
|
+
export function upgradeToSocket(req, socket, onOpen) {
|
|
113
|
+
const key = req.headers["sec-websocket-key"];
|
|
114
|
+
if (!key) {
|
|
115
|
+
socket.destroy();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
socket.write(
|
|
120
|
+
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
121
|
+
"Upgrade: websocket\r\n" +
|
|
122
|
+
"Connection: Upgrade\r\n" +
|
|
123
|
+
`Sec-WebSocket-Accept: ${accept(String(key))}\r\n\r\n`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Küçük paketleri geciktirmenin anlamı yok; panel anlık görünmeli.
|
|
127
|
+
socket.setNoDelay(true);
|
|
128
|
+
// Bağlantı hataları (sekme kapanması, ağ) süreci düşürmemeli.
|
|
129
|
+
socket.on("error", () => socket.destroy());
|
|
130
|
+
socket.on("close", () => sockets.delete(socket));
|
|
131
|
+
|
|
132
|
+
readFrames(socket);
|
|
133
|
+
sockets.add(socket);
|
|
134
|
+
|
|
135
|
+
onOpen((payload) => {
|
|
136
|
+
if (!socket.destroyed) socket.write(frame(JSON.stringify(payload)));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Bağlı tüm panellere gönderir.
|
|
142
|
+
* @param {object} payload
|
|
143
|
+
*/
|
|
144
|
+
export function broadcastSocket(payload) {
|
|
145
|
+
if (!sockets.size) return;
|
|
146
|
+
|
|
147
|
+
const packet = frame(JSON.stringify(payload));
|
|
148
|
+
for (const socket of sockets) {
|
|
149
|
+
if (socket.destroyed) sockets.delete(socket);
|
|
150
|
+
else socket.write(packet);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @returns {number} açık panel sayısı */
|
|
155
|
+
export function socketCount() {
|
|
156
|
+
return sockets.size;
|
|
157
|
+
}
|