spark-ssr 0.3.0 → 0.3.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +20 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spark-ssr",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Zero-config SSR for spark-html on Bun. The HTML template infers everything: filesystem routing, layouts, <spark-ssr> declarative data (SQL, URLs, globs, modules), auto CRUD with validation, guards, no-JS forms, schema + seeds, live updates, SEO. No build step.",
5
5
  "homepage": "https://wilkinnovo.github.io/spark-html",
6
6
  "type": "module",
package/src/server.js CHANGED
@@ -300,6 +300,19 @@ export async function serve(options = {}) {
300
300
  const RELOAD_CLIENT = '<script>(()=>{const e=new EventSource("/__spark/reload");let d=0;'
301
301
  + 'e.onmessage=()=>location.reload();e.onerror=()=>{d=1};e.onopen=()=>{if(d)location.reload()}})()</script>';
302
302
 
303
+ // Heartbeats keep every SSE socket outside Bun's idleTimeout (the default
304
+ // would kill them at 10 s — and a killed reload socket reconnects, which
305
+ // the client reads as "the server came back": a spurious reload). The ping
306
+ // also flushes dead clients (enqueue throws → drop).
307
+ const heartbeat = setInterval(() => {
308
+ for (const set of [sseClients, liveClients]) {
309
+ for (const c of set) {
310
+ try { c.enqueue(sseEnc.encode(': ping\n\n')); } catch { set.delete(c); }
311
+ }
312
+ }
313
+ }, 25000);
314
+ heartbeat.unref?.();
315
+
303
316
  // ── live data channel (§9) — a production feature, unlike dev reload ──
304
317
  // Any write through the server pings /__spark/live with the table name;
305
318
  // hydrated pages refetch through their own session (scoping intact) and
@@ -1007,7 +1020,9 @@ export async function serve(options = {}) {
1007
1020
  }
1008
1021
 
1009
1022
  async function buildScope(pd, req) {
1010
- const scope = { ...req.query, ...req.params, session: req.session };
1023
+ // `path` is ambient like `session` the layout's nav highlights the
1024
+ // current page with it. Query/params may shadow it deliberately.
1025
+ const scope = { path: req.path, ...req.query, ...req.params, session: req.session };
1011
1026
  if (pd.code) Object.assign(scope, await runPageScript(pd.code, req));
1012
1027
  for (const p of pd.plan) {
1013
1028
  if (scope[p.var] !== undefined) continue; // the page <script> won
@@ -1216,6 +1231,9 @@ code{color:#fdba74}em{color:#a8a29e}</style></head><body>
1216
1231
  // ── the server ──
1217
1232
  const server = Bun.serve({
1218
1233
  port: options.port ?? 3000,
1234
+ // SSE channels idle between events (heartbeat every 25 s keeps them
1235
+ // alive); slow queries and big uploads get headroom too.
1236
+ idleTimeout: 60,
1219
1237
  async fetch(request, srv) {
1220
1238
  const url = new URL(request.url);
1221
1239
  let pathname;
@@ -1417,6 +1435,7 @@ code{color:#fdba74}em{color:#a8a29e}</style></head><body>
1417
1435
  db,
1418
1436
  stop(force) {
1419
1437
  if (watchTimer) clearInterval(watchTimer);
1438
+ clearInterval(heartbeat);
1420
1439
  for (const c of sseClients) { try { c.close(); } catch { /* gone */ } }
1421
1440
  sseClients.clear();
1422
1441
  for (const c of liveClients) { try { c.close(); } catch { /* gone */ } }