@absolutejs/absolute 0.20.0-beta.139 → 0.20.0-beta.140

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.
@@ -5,12 +5,15 @@ import type {} from '../../types/globals';
5
5
  import { hmrState } from './hmrState';
6
6
  import {
7
7
  HMR_UPDATE_TIMEOUT_MS,
8
- MAX_RECONNECT_ATTEMPTS,
9
8
  PING_INTERVAL_MS,
10
- RECONNECT_INITIAL_DELAY_MS,
11
- RECONNECT_POLL_INTERVAL_MS,
12
9
  WEBSOCKET_NORMAL_CLOSURE
13
10
  } from './constants';
11
+ import {
12
+ RECONNECT_GIVE_UP_MS,
13
+ missedWhileDisconnected,
14
+ reconnectDelayMs,
15
+ type ServerSnapshot
16
+ } from './reconnect';
14
17
  import { detectCurrentFramework } from './frameworkDetect';
15
18
  import {
16
19
  absoluteHmrClientTarget,
@@ -103,11 +106,9 @@ const removeNativeSyncDevtools =
103
106
  ? () => undefined
104
107
  : installAbsoluteNativeSyncDevtools();
105
108
 
106
- /* Lightweight "server disconnected" banner. When the dev server is
107
- * genuinely down (process restarting or crashed) the browser would
108
- * otherwise show its own blank "site can't be reached" page — this keeps
109
- * a branded, self-explanatory notice on screen while the client polls
110
- * `/hmr-status` and reloads on recovery. Kept separate from the compile
109
+ /* Lightweight "server disconnected" banner, shown while the socket is
110
+ * down (a dropped network or a restarting server) and the client reconnects.
111
+ * The page reloads on recovery only if it missed an update. Kept separate from the compile
111
112
  * error overlay so it never clobbers a build-error message. The
112
113
  * `data-hmr-overlay` attribute exempts it from HMR DOM diffing. */
113
114
  const CONNECTION_LOST_BANNER_ID = 'absolutejs-connection-lost';
@@ -130,7 +131,10 @@ const showConnectionLostBanner = () => {
130
131
  'text-align:center',
131
132
  'box-shadow:0 -2px 12px rgba(0,0,0,0.3)'
132
133
  ].join(';');
133
- banner.textContent = 'AbsoluteJS · dev server disconnected — reconnecting…';
134
+ banner.textContent =
135
+ navigator.onLine === false
136
+ ? 'AbsoluteJS · offline — reconnects when the network is back'
137
+ : 'AbsoluteJS · dev server disconnected — reconnecting…';
134
138
  document.body.appendChild(banner);
135
139
  };
136
140
  const hideConnectionLostBanner = () => {
@@ -203,6 +207,7 @@ type HMRMessage = {
203
207
  // BUN-REACT-REFRESH-LEGACY: false only on stock Bun (see handlers/react.ts).
204
208
  fastRefreshSupported?: boolean;
205
209
  file?: string;
210
+ lastBroadcastAt?: number;
206
211
  framework?: string;
207
212
  hasCSSChanges?: boolean;
208
213
  hasComponentChanges?: boolean;
@@ -217,6 +222,7 @@ type HMRMessage = {
217
222
  primarySource?: string;
218
223
  scriptUrl?: string;
219
224
  serverDuration?: number;
225
+ serverStartedAt?: number;
220
226
  serverVersions?: Record<string, number>;
221
227
  };
222
228
  timestamp?: number;
@@ -381,8 +387,43 @@ const handleHMRMessage = (message: HMRMessage) => {
381
387
  }
382
388
  };
383
389
 
384
- // Prevent multiple WebSocket connections
385
- if (!(window.__HMR_WS__ && window.__HMR_WS__.readyState === WebSocket.OPEN)) {
390
+ // What the server said when this page last connected, and the newest update
391
+ // the page received: on reconnecting, the page reloads only if it missed one.
392
+ let serverSnapshot: ServerSnapshot | undefined;
393
+ let lastReceivedAt = 0;
394
+ let reconnectAttempt = 0;
395
+ let disconnectedAt = 0;
396
+
397
+ const scheduleReconnect = (connect: () => void) => {
398
+ if (hmrState.reconnectTimeout) clearTimeout(hmrState.reconnectTimeout);
399
+ if (!disconnectedAt) disconnectedAt = Date.now();
400
+ if (Date.now() - disconnectedAt > RECONNECT_GIVE_UP_MS) return;
401
+ // An offline browser waits for the network instead of spending attempts.
402
+ if (navigator.onLine === false) return;
403
+ hmrState.reconnectTimeout = setTimeout(
404
+ connect,
405
+ reconnectDelayMs(reconnectAttempt)
406
+ );
407
+ reconnectAttempt += 1;
408
+ };
409
+
410
+ const receivedSnapshot = (message: HMRMessage) => {
411
+ const after: ServerSnapshot = {
412
+ lastBroadcastAt: message.data.lastBroadcastAt,
413
+ serverStartedAt: message.data.serverStartedAt
414
+ };
415
+ const missed = missedWhileDisconnected(
416
+ serverSnapshot,
417
+ after,
418
+ lastReceivedAt
419
+ );
420
+ if (!serverSnapshot) lastReceivedAt = after.lastBroadcastAt ?? 0;
421
+ serverSnapshot = after;
422
+
423
+ return missed;
424
+ };
425
+
426
+ const connectHmrSocket = () => {
386
427
  // Determine WebSocket URL
387
428
  const wsHost = location.hostname;
388
429
  const wsPort =
@@ -395,6 +436,8 @@ if (!(window.__HMR_WS__ && window.__HMR_WS__.readyState === WebSocket.OPEN)) {
395
436
 
396
437
  wsc.onopen = function () {
397
438
  hmrState.isConnected = true;
439
+ reconnectAttempt = 0;
440
+ disconnectedAt = 0;
398
441
  hideConnectionLostBanner();
399
442
  sessionStorage.setItem('__HMR_CONNECTED__', 'true');
400
443
 
@@ -427,6 +470,20 @@ if (!(window.__HMR_WS__ && window.__HMR_WS__.readyState === WebSocket.OPEN)) {
427
470
  return;
428
471
  }
429
472
 
473
+ if (message?.type === 'manifest' && receivedSnapshot(message)) {
474
+ // The server restarted or sent updates while this page was
475
+ // away; only a reload is sure to show the current build.
476
+ window.location.reload();
477
+
478
+ return;
479
+ }
480
+ if (
481
+ typeof message?.timestamp === 'number' &&
482
+ message.type !== 'manifest' &&
483
+ message.type !== 'connected' &&
484
+ message.type !== 'pong'
485
+ )
486
+ lastReceivedAt = Math.max(lastReceivedAt, message.timestamp);
430
487
  handleHMRMessage(message);
431
488
  };
432
489
 
@@ -440,37 +497,32 @@ if (!(window.__HMR_WS__ && window.__HMR_WS__.readyState === WebSocket.OPEN)) {
440
497
 
441
498
  if (event.code !== WEBSOCKET_NORMAL_CLOSURE) {
442
499
  showConnectionLostBanner();
443
- let attempts = 0;
444
- hmrState.reconnectTimeout = setTimeout(function pollServer() {
445
- attempts++;
446
- if (attempts > MAX_RECONNECT_ATTEMPTS) return;
447
-
448
- fetch('/hmr-status', { cache: 'no-store' })
449
- .then((res) => {
450
- if (res.ok) {
451
- window.location.reload();
452
- } else {
453
- hmrState.reconnectTimeout = setTimeout(
454
- pollServer,
455
- RECONNECT_POLL_INTERVAL_MS
456
- );
457
- }
458
-
459
- return undefined;
460
- })
461
- .catch(() => {
462
- hmrState.reconnectTimeout = setTimeout(
463
- pollServer,
464
- RECONNECT_POLL_INTERVAL_MS
465
- );
466
- });
467
- }, RECONNECT_INITIAL_DELAY_MS);
500
+ scheduleReconnect(connectHmrSocket);
468
501
  }
469
502
  };
470
503
 
471
504
  wsc.onerror = function () {
472
505
  hmrState.isConnected = false;
473
506
  };
507
+ };
508
+
509
+ // The network or the tab coming back is the moment to retry, not the next
510
+ // backoff step.
511
+ const reconnectNow = () => {
512
+ if (hmrState.isConnected || !disconnectedAt) return;
513
+ reconnectAttempt = 0;
514
+ if (hmrState.reconnectTimeout) clearTimeout(hmrState.reconnectTimeout);
515
+ hmrState.reconnectTimeout = null;
516
+ connectHmrSocket();
517
+ };
518
+
519
+ // Prevent multiple WebSocket connections
520
+ if (!(window.__HMR_WS__ && window.__HMR_WS__.readyState === WebSocket.OPEN)) {
521
+ connectHmrSocket();
522
+ window.addEventListener('online', reconnectNow);
523
+ document.addEventListener('visibilitychange', () => {
524
+ if (document.visibilityState === 'visible') reconnectNow();
525
+ });
474
526
 
475
527
  window.addEventListener('beforeunload', () => {
476
528
  removeNativeSyncDevtools();
@@ -0,0 +1,43 @@
1
+ import type {} from '../../types/globals';
2
+ /* When the dev-server socket drops, the page used to poll for about 18
3
+ seconds and reload as soon as the server answered. On weak wifi every
4
+ network change became a full reload that refetched every stylesheet and
5
+ script and lost the page's state, and an outage longer than 18 seconds
6
+ left the page disconnected for good. Now the socket reconnects for as long
7
+ as the page is open, and the page reloads only when it missed something:
8
+ the server restarted, or the build changed while it was away. */
9
+
10
+ export const RECONNECT_BASE_DELAY_MS = 500;
11
+ export const RECONNECT_GIVE_UP_MS = 30 * 60_000;
12
+ export const RECONNECT_MAX_DELAY_MS = 10_000;
13
+
14
+ export const reconnectDelayMs = (attempt: number) =>
15
+ Math.min(RECONNECT_BASE_DELAY_MS * 2 ** attempt, RECONNECT_MAX_DELAY_MS);
16
+
17
+ /* What the server says about itself when a socket connects. */
18
+ export type ServerSnapshot = {
19
+ /** When the last update went out to connected pages. */
20
+ lastBroadcastAt?: number;
21
+ /** When this server process started; a new value means it restarted. */
22
+ serverStartedAt?: number;
23
+ };
24
+
25
+ /* Whether the page has to reload after reconnecting: the server restarted,
26
+ or it sent an update after the last one this page received. Without a
27
+ previous snapshot (the first connection) there is nothing to have missed;
28
+ a server that does not say when it started counts as restarted, which is
29
+ the old behavior. */
30
+ export const missedWhileDisconnected = (
31
+ before: ServerSnapshot | undefined,
32
+ after: ServerSnapshot,
33
+ lastReceivedAt: number
34
+ ) => {
35
+ if (!before) return false;
36
+ if (
37
+ before.serverStartedAt === undefined ||
38
+ before.serverStartedAt !== after.serverStartedAt
39
+ )
40
+ return true;
41
+
42
+ return (after.lastBroadcastAt ?? 0) > lastReceivedAt;
43
+ };
package/dist/index.js CHANGED
@@ -25399,7 +25399,7 @@ var resolveJsxDevRuntimeCompatPath = () => {
25399
25399
  return `${lines.join(`
25400
25400
  `)}
25401
25401
  `;
25402
- }, buildReactVendor = async (buildDir) => {
25402
+ }, buildReactVendor = async (buildDir, { production = false } = {}) => {
25403
25403
  const vendorDir = join47(buildDir, "react", "vendor");
25404
25404
  mkdirSync8(vendorDir, { recursive: true });
25405
25405
  const tmpDir = join47(buildDir, "_vendor_tmp");
@@ -25413,9 +25413,10 @@ var resolveJsxDevRuntimeCompatPath = () => {
25413
25413
  return entryPath;
25414
25414
  }));
25415
25415
  const result = await bunBuild3({
25416
+ ...production ? { define: { "process.env.NODE_ENV": '"production"' } } : {},
25416
25417
  entrypoints,
25417
25418
  format: "esm",
25418
- minify: false,
25419
+ minify: production ? { whitespace: true } : false,
25419
25420
  naming: "[name].[ext]",
25420
25421
  outdir: vendorDir,
25421
25422
  splitting: true,
@@ -26931,7 +26932,7 @@ ${offenders.map((o) => ` \u2022 ${o}`).join(`
26931
26932
  }
26932
26933
  if (!hmr && reactDir) {
26933
26934
  await Promise.resolve().then(() => init_buildReactVendor());
26934
- await buildReactVendor(buildPath);
26935
+ await buildReactVendor(buildPath, { production: !isDev });
26935
26936
  vendorPaths = computeVendorPaths();
26936
26937
  setDevVendorPaths(vendorPaths);
26937
26938
  }
@@ -29509,12 +29510,14 @@ var MAX_RETAINED_HMR_UPDATES = 100, normalizedHmrTarget = (value) => {
29509
29510
  state.connectedClients.delete(client);
29510
29511
  state.clientTargets.delete(client);
29511
29512
  });
29512
- }, handleClientConnect = (state, client, manifest) => {
29513
+ }, SERVER_STARTED_AT, handleClientConnect = (state, client, manifest) => {
29513
29514
  state.connectedClients.add(canonicalClient(client));
29514
29515
  const serverVersions = serializeModuleVersions(state.moduleVersions);
29515
29516
  client.send(JSON.stringify({
29516
29517
  data: {
29518
+ lastBroadcastAt: state.lastBroadcastTimestamp,
29517
29519
  manifest,
29520
+ serverStartedAt: SERVER_STARTED_AT,
29518
29521
  serverVersions
29519
29522
  },
29520
29523
  timestamp: Date.now(),
@@ -29634,6 +29637,7 @@ var init_webSocket = __esm(() => {
29634
29637
  init_telemetryEvent();
29635
29638
  init_logger();
29636
29639
  canonicalClients = new WeakMap;
29640
+ SERVER_STARTED_AT = Math.round(performance.timeOrigin);
29637
29641
  });
29638
29642
 
29639
29643
  // src/dev/moduleServer.ts
@@ -33118,6 +33122,20 @@ var moduleServerPromise, getModuleServer = () => moduleServerPromise, clearDevSs
33118
33122
  return nearest;
33119
33123
  const dependents = reactFiles.filter((candidate) => !edited.has(candidate) && isReactComponentPath(candidate) && isImportableModule(candidate));
33120
33124
  return dependents.find((candidate) => !isReactPagePath(candidate)) ?? dependents[0] ?? file;
33125
+ }, MAX_REFRESH_BOUNDARY_HOPS = 8, registersRefreshComponent = (file) => /\$RefreshReg\$\(/u.test(getTransformed(resolvePath(file)) ?? ""), refreshBoundaryUrls = async (targets) => {
33126
+ const urls = [];
33127
+ const seen = new Set(targets.map((file) => resolvePath(file)));
33128
+ const climb = async (file, hops) => {
33129
+ const importer = findNearestComponent(resolvePath(file));
33130
+ if (!importer || seen.has(importer) || hops >= MAX_REFRESH_BOUNDARY_HOPS)
33131
+ return;
33132
+ seen.add(importer);
33133
+ urls.push(await getReactModuleUrl(importer));
33134
+ if (!registersRefreshComponent(importer))
33135
+ await climb(importer, hops + 1);
33136
+ };
33137
+ await targets.reduce((previous, target) => previous.then(() => registersRefreshComponent(target) ? undefined : climb(target, 0)), Promise.resolve());
33138
+ return urls;
33121
33139
  }, resolveReactUpdateTargets = (state, reactFiles) => {
33122
33140
  const importable = reactFiles.filter(isImportableModule);
33123
33141
  const userEdited = importable.filter((file) => state.lastUserEditedFiles?.has(resolvePath(file)));
@@ -33150,7 +33168,8 @@ var moduleServerPromise, getModuleServer = () => moduleServerPromise, clearDevSs
33150
33168
  });
33151
33169
  return state.manifest;
33152
33170
  }
33153
- const moduleUrls = await Promise.all(targets.map(getReactModuleUrl));
33171
+ const targetUrls = await Promise.all(targets.map(getReactModuleUrl));
33172
+ const moduleUrls = fastRefreshSupported ? [...targetUrls, ...await refreshBoundaryUrls(targets)] : targetUrls;
33154
33173
  await Promise.all(editedFiles.filter((file) => !targets.includes(file)).map(getReactModuleUrl));
33155
33174
  const { getTransformError } = await getModuleServer();
33156
33175
  const failed = [...new Set([...editedFiles, ...targets])].map((file) => ({ error: getTransformError(file), file })).find((entry) => entry.error !== undefined);
@@ -43832,5 +43851,5 @@ export {
43832
43851
  wrapPageHandlerWithStreamingSlots
43833
43852
  };
43834
43853
 
43835
- //# debugId=270D4D9EDE92886364756E2164756E21
43854
+ //# debugId=D311F2FBC012903E64756E2164756E21
43836
43855
  //# sourceMappingURL=index.js.map