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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.1.4",
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
- const ok = await runBuild();
114
- if (!ok) {
115
- console.error("❌ Build failed — fix errors and save again");
115
+ if (building) {
116
+ buildPending = true;
116
117
  return;
117
118
  }
118
- await startAppServer();
119
- // Give the app server a moment to bind its port
120
- await Bun.sleep(200);
121
- broadcastReload();
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.innerHTML+=${safeJsonStringify(head)}</script>`;
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) {
@@ -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
- console.log("Shutting down...");
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
- // Force exit if stop hangs
423
- setTimeout(() => process.exit(1), 10_000);
461
+ setTimeout(() => process.exit(1), 5_000);
424
462
  }
425
463
 
426
464
  process.on("SIGTERM", shutdown);