@webjsdev/server 0.8.84 → 0.8.85

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": "@webjsdev/server",
3
- "version": "0.8.84",
3
+ "version": "0.8.85",
4
4
  "type": "module",
5
5
  "description": "The server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Provides the file-based router, SSR, server actions, route handlers, middleware, and live reload on Node 24+ or Bun.",
6
6
  "main": "index.js",
package/src/dev/server.js CHANGED
@@ -273,7 +273,9 @@ export async function startServer(opts) {
273
273
  // shells via listener-core.js, so live-reload + the dev error overlay behave
274
274
  // identically on both). Built before the handler so its onReload / onDevError
275
275
  // callbacks can fan out through it.
276
- const hub = new SseHub();
276
+ // A Bun hot host, or a server outside the dev supervisor (`--no-hot`, an
277
+ // embedder), applies an edit in place; a supervised Node child is restarted.
278
+ const hub = new SseHub({ inPlace: dev && (!!hotKey || process.env.__WEBJS_DEV_CHILD !== '1') });
277
279
  // Pages render the stream's state into a meta tag (#1516), so the relay can
278
280
  // re-check a page against the server on every reconnect.
279
281
  if (dev) setDevReloadState(() => ({ boot: DEV_BOOT_ID, seq: hub.seq }));
@@ -36,6 +36,18 @@
36
36
  */
37
37
  export const RELOAD_QUIET_MS = 2000;
38
38
 
39
+ /**
40
+ * The quiet window for a batch made only of in-process frames from a server
41
+ * that reloads IN PLACE (#1575): Bun's one long-lived dev server, or any
42
+ * `--no-hot` server. No restart follows such a frame, so nothing is worth
43
+ * waiting 2000ms for, and that wait was most of the save-to-paint time (3.8s
44
+ * measured in a hosted preview). 300ms still folds a save's few frames and a
45
+ * quick burst into one reload. A batch holding any restart signal (a changed
46
+ * boot id, a stale page, a frame from a restarting server) keeps the full
47
+ * `RELOAD_QUIET_MS`, since the restarted process is still warming.
48
+ */
49
+ export const RELOAD_QUIET_IN_PLACE_MS = 300;
50
+
39
51
  /**
40
52
  * The longest a reload is ever held, measured from the FIRST signal of a batch
41
53
  * (#1397). A quiet window alone would freeze the page on stale content for a
@@ -116,6 +128,19 @@ export function pageIsStale(page, boot, seq) {
116
128
  }
117
129
 
118
130
  /** A reload frame's `seq` (#1507), or null when it carries none. @param {string} data */
131
+ /**
132
+ * Whether a `reload` frame comes from a server that reloads in place (#1575).
133
+ * Absent or unreadable means it may restart, the safe (slower) reading.
134
+ * @param {string} data
135
+ * @returns {boolean}
136
+ */
137
+ export function frameInPlace(data) {
138
+ try {
139
+ const o = JSON.parse(data);
140
+ return !!(o && o.inPlace === true);
141
+ } catch (_) { return false; }
142
+ }
143
+
119
144
  function reloadSeq(data) {
120
145
  try {
121
146
  const o = JSON.parse(data);
@@ -190,6 +215,8 @@ export function startReloadWorker(scope, EventSourceCtor, eventsUrl, opts) {
190
215
  let batchAll = false;
191
216
  /** @type {Set<any>} tabs whose page fell behind, reloaded on their own */
192
217
  const batchPorts = new Set();
218
+ /** Whether the batch holds a signal from a server that restarts (#1575). */
219
+ let batchRestart = false;
193
220
 
194
221
  function emitReload() {
195
222
  if (quietTimer !== null) { timers.clearTimeout(quietTimer); quietTimer = null; }
@@ -200,6 +227,7 @@ export function startReloadWorker(scope, EventSourceCtor, eventsUrl, opts) {
200
227
  const targets = batchAll ? Array.from(ports.keys()) : Array.from(batchPorts).filter((p) => ports.has(p));
201
228
  batchAll = false;
202
229
  batchPorts.clear();
230
+ batchRestart = false;
203
231
  for (const p of targets) {
204
232
  // The tab applies this reload, so from here its page reflects the state
205
233
  // the relay knows now; a later hello compares against that.
@@ -219,11 +247,13 @@ export function startReloadWorker(scope, EventSourceCtor, eventsUrl, opts) {
219
247
  * @param {string} verdict
220
248
  * @param {any} [port] reload only this tab (its page fell behind); omitted, every tab
221
249
  */
222
- function requestReload(verdict, port) {
250
+ function requestReload(verdict, port, inPlace) {
223
251
  if (port === undefined) batchAll = true; else batchPorts.add(port);
224
252
  if (rank(verdict) < rank(batchVerdict)) batchVerdict = VERDICT_STRENGTH[rank(verdict)];
253
+ // One restart signal anywhere in the batch keeps the long window (#1575).
254
+ if (!inPlace) batchRestart = true;
225
255
  if (quietTimer !== null) timers.clearTimeout(quietTimer);
226
- quietTimer = timers.setTimeout(emitReload, RELOAD_QUIET_MS);
256
+ quietTimer = timers.setTimeout(emitReload, batchRestart ? RELOAD_QUIET_MS : RELOAD_QUIET_IN_PLACE_MS);
227
257
  if (capTimer === null) capTimer = timers.setTimeout(emitReload, RELOAD_MAX_HOLD_MS);
228
258
  }
229
259
 
@@ -278,7 +308,7 @@ export function startReloadWorker(scope, EventSourceCtor, eventsUrl, opts) {
278
308
  lastError = null;
279
309
  const n = reloadSeq(e.data);
280
310
  if (n !== null) lastSeq = n;
281
- requestReload(parseVerdict(e.data));
311
+ requestReload(parseVerdict(e.data), undefined, frameInPlace(e.data));
282
312
  });
283
313
  es.addEventListener('webjs-error', (e) => { lastError = e.data; fanout({ type: 'webjs-error', data: e.data }); });
284
314
  // The stream dropped (a restarting server, a network blip, a host that
@@ -351,7 +351,13 @@ export function loadWsModule(file, dev) {
351
351
  * tab was hidden can tell, on reconnecting, that it missed an edit.
352
352
  */
353
353
  export class SseHub {
354
- constructor() {
354
+ /**
355
+ * @param {{ inPlace?: boolean }} [opts] `inPlace`: this server applies an
356
+ * edit without restarting its process (#1575), so its reload frames say so
357
+ * and the browser relay need not wait for a restart to settle.
358
+ */
359
+ constructor({ inPlace = false } = {}) {
360
+ this.inPlace = inPlace;
355
361
  /** @type {Set<{ send: (s: string) => void, close: () => void }>} */
356
362
  this.clients = new Set();
357
363
  /** Reload frames sent by this process (#1507). */
@@ -399,7 +405,7 @@ export class SseHub {
399
405
  reload(verdict) {
400
406
  const v = verdict && typeof verdict.v === 'string' ? verdict : { v: 'reload' };
401
407
  this.seq++;
402
- this._raw(`event: reload\ndata: ${JSON.stringify({ ...v, seq: this.seq })}\n\n`);
408
+ this._raw(`event: reload\ndata: ${JSON.stringify({ ...v, seq: this.seq, ...(this.inPlace ? { inPlace: true } : {}) })}\n\n`);
403
409
  }
404
410
 
405
411
  /** Push a dev-error overlay frame (#264) to every open tab. @param {object} frame */