bosia 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/package.json +1 -1
- package/src/core/client/App.svelte +3 -0
- package/src/core/client/router.svelte.ts +4 -0
- package/src/core/dev.ts +22 -7
- package/src/core/html.ts +1 -1
- package/src/core/server.ts +42 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
|
@@ -86,6 +86,9 @@
|
|
|
86
86
|
layoutData = result?.layoutData ?? [];
|
|
87
87
|
routeParams = result?.pageData?.params ?? match.params;
|
|
88
88
|
|
|
89
|
+
// Scroll to top on forward navigation (not on popstate/back-forward)
|
|
90
|
+
if (router.isPush) window.scrollTo(0, 0);
|
|
91
|
+
|
|
89
92
|
// Update document title and meta description from server metadata
|
|
90
93
|
if (result?.metadata) {
|
|
91
94
|
if (result.metadata.title) document.title = result.metadata.title;
|
|
@@ -8,6 +8,8 @@ import { clientRoutes } from "bosia:routes";
|
|
|
8
8
|
export const router = new class Router {
|
|
9
9
|
currentRoute = $state(typeof window !== "undefined" ? window.location.pathname + window.location.search + window.location.hash : "/");
|
|
10
10
|
params = $state<Record<string, string>>({});
|
|
11
|
+
/** True when navigation was triggered by a link click / navigate() call, false on popstate (back/forward). */
|
|
12
|
+
isPush = $state(true);
|
|
11
13
|
|
|
12
14
|
navigate(path: string) {
|
|
13
15
|
if (this.currentRoute === path) return;
|
|
@@ -17,6 +19,7 @@ export const router = new class Router {
|
|
|
17
19
|
window.location.href = path;
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
22
|
+
this.isPush = true;
|
|
20
23
|
this.currentRoute = path;
|
|
21
24
|
if (typeof history !== "undefined") {
|
|
22
25
|
history.pushState({}, "", path);
|
|
@@ -41,6 +44,7 @@ export const router = new class Router {
|
|
|
41
44
|
|
|
42
45
|
// Browser back/forward
|
|
43
46
|
window.addEventListener("popstate", () => {
|
|
47
|
+
this.isPush = false;
|
|
44
48
|
this.currentRoute = window.location.pathname + window.location.search + window.location.hash;
|
|
45
49
|
});
|
|
46
50
|
}
|
package/src/core/dev.ts
CHANGED
|
@@ -108,17 +108,32 @@ async function startAppServer() {
|
|
|
108
108
|
// ─── Build & Restart ──────────────────────────────────────
|
|
109
109
|
|
|
110
110
|
let buildTimer: ReturnType<typeof setTimeout> | null = null;
|
|
111
|
+
let building = false;
|
|
112
|
+
let buildPending = false;
|
|
111
113
|
|
|
112
114
|
async function buildAndRestart() {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
console.error("❌ Build failed — fix errors and save again");
|
|
115
|
+
if (building) {
|
|
116
|
+
buildPending = true;
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
building = true;
|
|
120
|
+
try {
|
|
121
|
+
const ok = await runBuild();
|
|
122
|
+
if (!ok) {
|
|
123
|
+
console.error("❌ Build failed — fix errors and save again");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await startAppServer();
|
|
127
|
+
// Give the app server a moment to bind its port
|
|
128
|
+
await Bun.sleep(200);
|
|
129
|
+
broadcastReload();
|
|
130
|
+
} finally {
|
|
131
|
+
building = false;
|
|
132
|
+
}
|
|
133
|
+
if (buildPending) {
|
|
134
|
+
buildPending = false;
|
|
135
|
+
buildAndRestart();
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
function scheduleBuild() {
|
package/src/core/html.ts
CHANGED
|
@@ -173,7 +173,7 @@ export function buildHtmlTail(
|
|
|
173
173
|
): string {
|
|
174
174
|
let out = `<script>document.getElementById('__bs__').remove()</script>`;
|
|
175
175
|
out += `\n<div id="app">${body}</div>`;
|
|
176
|
-
if (head) out += `\n<script>document.head.
|
|
176
|
+
if (head) out += `\n<script>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
|
|
177
177
|
if (csr) {
|
|
178
178
|
const publicEnv = getPublicDynamicEnv();
|
|
179
179
|
if (Object.keys(publicEnv).length > 0) {
|
package/src/core/server.ts
CHANGED
|
@@ -108,6 +108,9 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
108
108
|
|
|
109
109
|
// Health check endpoint — for load balancers and orchestrators
|
|
110
110
|
if (path === "/_health") {
|
|
111
|
+
if (shuttingDown) {
|
|
112
|
+
return Response.json({ status: "shutting_down" }, { status: 503 });
|
|
113
|
+
}
|
|
111
114
|
const { timestamp, timezone } = getServerTime();
|
|
112
115
|
return Response.json({ status: "ok", timestamp, timezone });
|
|
113
116
|
}
|
|
@@ -303,6 +306,15 @@ const SECURITY_HEADERS: Record<string, string> = {
|
|
|
303
306
|
};
|
|
304
307
|
|
|
305
308
|
async function handleRequest(request: Request, url: URL): Promise<Response> {
|
|
309
|
+
// Reject new non-health requests during shutdown
|
|
310
|
+
if (shuttingDown && url.pathname !== "/_health") {
|
|
311
|
+
return new Response("Service Unavailable", {
|
|
312
|
+
status: 503,
|
|
313
|
+
headers: { "Retry-After": "5" },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
inFlight++;
|
|
306
318
|
try {
|
|
307
319
|
// Handle CORS preflight before CSRF check (OPTIONS is CSRF-exempt)
|
|
308
320
|
if (CORS_CONFIG && request.method === "OPTIONS") {
|
|
@@ -338,6 +350,11 @@ async function handleRequest(request: Request, url: URL): Promise<Response> {
|
|
|
338
350
|
if (isDev) console.error("Unhandled request error:", err);
|
|
339
351
|
else console.error("Unhandled request error:", (err as Error).message ?? err);
|
|
340
352
|
return Response.json({ error: "Internal Server Error" }, { status: 500 });
|
|
353
|
+
} finally {
|
|
354
|
+
inFlight--;
|
|
355
|
+
if (shuttingDown && inFlight === 0 && drainResolve) {
|
|
356
|
+
drainResolve();
|
|
357
|
+
}
|
|
341
358
|
}
|
|
342
359
|
}
|
|
343
360
|
|
|
@@ -367,6 +384,12 @@ if (BODY_SIZE_LIMIT === 0) {
|
|
|
367
384
|
console.log(`📦 Body size limit: ${BODY_SIZE_LIMIT} bytes`);
|
|
368
385
|
}
|
|
369
386
|
|
|
387
|
+
// ─── Graceful Shutdown State ──────────────────────────────
|
|
388
|
+
|
|
389
|
+
let shuttingDown = false;
|
|
390
|
+
let inFlight = 0;
|
|
391
|
+
let drainResolve: (() => void) | null = null;
|
|
392
|
+
|
|
370
393
|
// ─── Elysia App ───────────────────────────────────────────
|
|
371
394
|
|
|
372
395
|
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : (isDev ? 9001 : 9000);
|
|
@@ -416,11 +439,26 @@ app.listen(PORT, () => {
|
|
|
416
439
|
if (!isDev) console.log(`⬡ Bosia server running at http://localhost:${PORT}`);
|
|
417
440
|
});
|
|
418
441
|
|
|
419
|
-
function shutdown() {
|
|
420
|
-
|
|
442
|
+
async function shutdown() {
|
|
443
|
+
if (shuttingDown) return;
|
|
444
|
+
shuttingDown = true;
|
|
445
|
+
console.log("⏳ Shutting down — draining in-flight requests...");
|
|
446
|
+
|
|
447
|
+
if (inFlight > 0) {
|
|
448
|
+
await Promise.race([
|
|
449
|
+
new Promise<void>(r => { drainResolve = r; }),
|
|
450
|
+
Bun.sleep(10_000),
|
|
451
|
+
]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (inFlight > 0) {
|
|
455
|
+
console.warn(`⚠️ Force shutdown with ${inFlight} request(s) still in flight`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log("✅ All requests drained");
|
|
458
|
+
}
|
|
459
|
+
|
|
421
460
|
app.stop().then(() => process.exit(0));
|
|
422
|
-
|
|
423
|
-
setTimeout(() => process.exit(1), 10_000);
|
|
461
|
+
setTimeout(() => process.exit(1), 5_000);
|
|
424
462
|
}
|
|
425
463
|
|
|
426
464
|
process.on("SIGTERM", shutdown);
|