jskelet 0.1.4 → 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 +6 -0
- package/docs/09-dev-araclari.md +27 -5
- package/docs/en/09-dev-tools.md +26 -3
- package/package.json +1 -1
- package/src/client/devtools/overlay.js +111 -46
- package/src/server/create-app.js +8 -1
- package/src/server/dev/devtools.js +106 -15
- package/src/server/dev/socket.js +157 -0
package/CHANGELOG.md
CHANGED
|
@@ -47,6 +47,12 @@ one is listed under a **Breaking** heading.
|
|
|
47
47
|
|
|
48
48
|
### Changed
|
|
49
49
|
|
|
50
|
+
- The dev tools panel is now fed over a WebSocket (`<devBasePath>/ws`) instead of
|
|
51
|
+
polling `/stats` every two seconds. The server pushes statistics as they change
|
|
52
|
+
and sends live reload and CSS hot-swap events over the same connection, so an
|
|
53
|
+
open tab no longer keeps hitting the server while the panel is closed. No new
|
|
54
|
+
dependency is involved; if the socket cannot be opened, the panel falls back to
|
|
55
|
+
the previous SSE plus polling path.
|
|
50
56
|
- `notFound()` is no longer served as a 404 when a transient upstream failure
|
|
51
57
|
(`429`, `5xx`, network error) happened during the same render. The page is
|
|
52
58
|
retried first and, if upstream is still failing, responds with an uncached
|
package/docs/09-dev-araclari.md
CHANGED
|
@@ -111,9 +111,9 @@ Restart satırı değişen dosyayı ya da sayısını gösterir:
|
|
|
111
111
|
|
|
112
112
|
## CSS hot-swap ve tam yenileme
|
|
113
113
|
|
|
114
|
-
Dev sunucusu `.jskelet/manifest.json` dosyasını izler ve
|
|
115
|
-
(`<devBasePath>/
|
|
116
|
-
|
|
114
|
+
Dev sunucusu `.jskelet/manifest.json` dosyasını izler ve olayları canlı kanal
|
|
115
|
+
(`<devBasePath>/ws`) üzerinden tarayıcıya yayınlar. Manifest her build turunda
|
|
116
|
+
yeniden yazıldığı için değişiklik tespiti manifest üzerinden yapılır.
|
|
117
117
|
|
|
118
118
|
| Değişen | Davranış |
|
|
119
119
|
| --- | --- |
|
|
@@ -131,6 +131,27 @@ Sunucu yeniden başladığında overlay bunu **boot kimliğinden** anlar: her s
|
|
|
131
131
|
kendine özgü bir `boot` değeri yayınlar, overlay değişikliği görüp "restarted"
|
|
132
132
|
bilgisini gösterir ve kendi durumunu sıfırlamaz.
|
|
133
133
|
|
|
134
|
+
## Canlı kanal
|
|
135
|
+
|
|
136
|
+
Overlay'e giden her şey — istatistikler, live reload ve CSS hot-swap olayları —
|
|
137
|
+
tek bir WebSocket üzerinden gelir (`<devBasePath>/ws`). Panel eskiden
|
|
138
|
+
istatistikleri iki saniyede bir çekiyordu; açık her sekme, panel kapalıyken bile
|
|
139
|
+
sunucuya sürekli istek atıyordu. Artık sunucu değişiklik oldukça iter: bir istek
|
|
140
|
+
ya da hata kaydedildiğinde (120 ms birleştirilerek), ısıtma sürerken saniyede
|
|
141
|
+
bir, geri kalan zamanda yalnızca uptime/bellek tazelensin diye dört saniyede bir.
|
|
142
|
+
Bağlı panel yoksa hiçbir şey hesaplanmaz.
|
|
143
|
+
|
|
144
|
+
El sıkışma HTTP `upgrade` olayında geçtiği ve o olay middleware zincirine hiç
|
|
145
|
+
uğramadığı için kanal `listen` sonrası doğrudan sunucuya bağlanır
|
|
146
|
+
(`attachDevSocket`). Sunucu tarafı `ws` gibi bir bağımlılık kullanmaz: yalnızca
|
|
147
|
+
sunucu→istemci metin çerçevesi yazmak ve istemcinin ping/close çerçevelerini
|
|
148
|
+
yanıtlamak gerekiyor.
|
|
149
|
+
|
|
150
|
+
Soket hiç açılamazsa (araya giren bir proxy WebSocket'i geçirmiyor olabilir)
|
|
151
|
+
overlay eski yola düşer: `/events` SSE akışı + `/stats` yoklaması. Soket kurulup
|
|
152
|
+
sonra düşerse — yani sunucu yeniden başlıyorsa — yarım saniyede bir yeniden
|
|
153
|
+
bağlanır ve gösterge bu sırada "bağlantı yok" der.
|
|
154
|
+
|
|
134
155
|
## Devtools overlay
|
|
135
156
|
|
|
136
157
|
Sağ altta yüzen bir baloncuk; `Alt+D` ile açılır, `Esc` ya da karartma alanına
|
|
@@ -236,8 +257,9 @@ Rapor katmanı yalnızca development'ta yüklenir, üretim çıktısına hiç gi
|
|
|
236
257
|
| --- | --- | --- |
|
|
237
258
|
| `/overlay.js` | GET | Overlay script'i |
|
|
238
259
|
| `/logo.png` | GET | Overlay logosu |
|
|
239
|
-
| `/
|
|
240
|
-
| `/
|
|
260
|
+
| `/ws` | GET (upgrade) | Canlı kanal: istatistikler, live reload ve CSS hot-swap olayları |
|
|
261
|
+
| `/events` | GET | SSE: yalnızca WebSocket kurulamazsa kullanılan yedek olay akışı |
|
|
262
|
+
| `/stats` | GET | Anlık istatistikler; aynı yedek yolun veri ucu |
|
|
241
263
|
| `/report` | GET | Rapor sayfası (HTML) |
|
|
242
264
|
| `/report.js` | GET | Rapor sayfasının script'i |
|
|
243
265
|
| `/report/data` | GET | Raporun tek veri kaynağı (JSON) |
|
package/docs/en/09-dev-tools.md
CHANGED
|
@@ -116,7 +116,7 @@ If `JSKELET_VERBOSE=1` is set, all files are listed when more than one changed.
|
|
|
116
116
|
## CSS hot-swap and full reload
|
|
117
117
|
|
|
118
118
|
The dev server watches `.jskelet/manifest.json` and broadcasts events to the
|
|
119
|
-
browser over
|
|
119
|
+
browser over the live channel (`<devBasePath>/ws`). Since the manifest is
|
|
120
120
|
rewritten on every build round, change detection is done through the manifest.
|
|
121
121
|
|
|
122
122
|
| What changed | Behavior |
|
|
@@ -134,6 +134,28 @@ When the server restarts, the overlay figures it out from the **boot id**: every
|
|
|
134
134
|
process broadcasts a unique `boot` value, the overlay sees the change, shows the
|
|
135
135
|
"restarted" note and does not reset its own state.
|
|
136
136
|
|
|
137
|
+
## The live channel
|
|
138
|
+
|
|
139
|
+
Everything the overlay shows — statistics, live reload and CSS hot-swap events —
|
|
140
|
+
arrives over a single WebSocket (`<devBasePath>/ws`). The panel used to poll for
|
|
141
|
+
statistics every two seconds, so every open tab kept hitting the server even
|
|
142
|
+
while the panel was closed. Now the server pushes as things change: when a
|
|
143
|
+
request or an error is recorded (coalesced over 120 ms), once per second while
|
|
144
|
+
prewarming runs, and every four seconds otherwise so uptime and memory stay
|
|
145
|
+
fresh. Nothing is computed when no panel is connected.
|
|
146
|
+
|
|
147
|
+
The handshake happens on the HTTP `upgrade` event, and that event never reaches
|
|
148
|
+
the middleware chain, so the channel is attached straight to the server after
|
|
149
|
+
`listen` (`attachDevSocket`). The server side pulls in no dependency such as
|
|
150
|
+
`ws`: all it needs is to write server-to-client text frames and to answer the
|
|
151
|
+
client's ping/close frames.
|
|
152
|
+
|
|
153
|
+
If the socket cannot be opened at all (a proxy in between may not pass WebSocket
|
|
154
|
+
through), the overlay falls back to the old path: the `/events` SSE stream plus
|
|
155
|
+
polling `/stats`. If the socket opens and later drops — that is, the server is
|
|
156
|
+
restarting — it reconnects every half second and the indicator reads
|
|
157
|
+
"server restarting…" in the meantime.
|
|
158
|
+
|
|
137
159
|
## Devtools overlay
|
|
138
160
|
|
|
139
161
|
A floating bubble in the bottom right; opened with `Alt+D`, closed with `Esc` or
|
|
@@ -240,8 +262,9 @@ Under `brand.devBasePath` (default `/__jskelet/dev`):
|
|
|
240
262
|
| --- | --- | --- |
|
|
241
263
|
| `/overlay.js` | GET | The overlay script |
|
|
242
264
|
| `/logo.png` | GET | The overlay logo |
|
|
243
|
-
| `/
|
|
244
|
-
| `/
|
|
265
|
+
| `/ws` | GET (upgrade) | Live channel: statistics, live reload and CSS hot-swap events |
|
|
266
|
+
| `/events` | GET | SSE: the fallback event stream, used only when WebSocket cannot be established |
|
|
267
|
+
| `/stats` | GET | Current statistics; the data endpoint of that same fallback |
|
|
245
268
|
| `/report` | GET | The report page (HTML) |
|
|
246
269
|
| `/report.js` | GET | The report page's script |
|
|
247
270
|
| `/report/data` | GET | The report's single data source (JSON) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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/server/create-app.js
CHANGED
|
@@ -168,7 +168,14 @@ export async function startServer(options = {}) {
|
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
return new Promise((resolve) => {
|
|
171
|
-
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
|
+
|
|
172
179
|
// Bu satırın biçimi sözleşme: `jskelet dev` sunucunun hazır olduğunu
|
|
173
180
|
// buradan anlar ve özet satırını ona göre basar.
|
|
174
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
|
+
}
|