@ovineko/spa-guard 0.0.1-alpha-26 → 0.0.1-alpha-28

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.
@@ -15,5 +15,5 @@ export { extractVersionFromHtml } from "./common/parseVersion";
15
15
  export { attemptReload } from "./common/reload";
16
16
  export { retryImport } from "./common/retryImport";
17
17
  export { serializeError } from "./common/serializeError";
18
- export { defaultSpinnerSvg, SPINNER_ID } from "./common/spinner";
18
+ export { defaultSpinnerSvg, sanitizeCssValue, SPINNER_ID } from "./common/spinner";
19
19
  export { dispatchAsyncRuntimeError, dispatchChunkLoadError, dispatchFinallyError, dispatchForceRetryError, dispatchNetworkTimeout, dispatchSyncRuntimeError, dispatchUnhandledRejection, } from "./runtime/debug/errorDispatchers";
package/dist/_internal.js CHANGED
@@ -3,12 +3,13 @@ import {
3
3
  isChunkError,
4
4
  listenInternal,
5
5
  serializeError
6
- } from "./chunk-ZVYB2746.js";
6
+ } from "./chunk-5ANASVKX.js";
7
7
  import {
8
8
  SPINNER_ID,
9
9
  defaultSpinnerSvg,
10
- extractVersionFromHtml
11
- } from "./chunk-Z75UPJWV.js";
10
+ extractVersionFromHtml,
11
+ sanitizeCssValue
12
+ } from "./chunk-BA5VUNSU.js";
12
13
  import {
13
14
  dispatchAsyncRuntimeError,
14
15
  dispatchChunkLoadError,
@@ -17,13 +18,13 @@ import {
17
18
  dispatchNetworkTimeout,
18
19
  dispatchSyncRuntimeError,
19
20
  dispatchUnhandledRejection
20
- } from "./chunk-XFILAQ7P.js";
21
+ } from "./chunk-62AC5565.js";
21
22
  import {
22
23
  attemptReload,
23
24
  sendBeacon,
24
25
  shouldForceRetry,
25
26
  shouldIgnoreMessages
26
- } from "./chunk-CSN7MQGX.js";
27
+ } from "./chunk-4EXCHJBE.js";
27
28
  import {
28
29
  applyI18n,
29
30
  debugSyncErrorEventType,
@@ -38,7 +39,7 @@ import {
38
39
  isDefaultRetryEnabled,
39
40
  optionsWindowKey,
40
41
  subscribe
41
- } from "./chunk-X6E7KUDK.js";
42
+ } from "./chunk-PE5QPP5Y.js";
42
43
  import "./chunk-MLKGABMK.js";
43
44
 
44
45
  // src/common/handleErrorWithSpaGuard.ts
@@ -95,6 +96,10 @@ var retryImport = async (importFn, delays, options) => {
95
96
  const { callReloadOnFailure, onRetry, signal } = options ?? {};
96
97
  let lastError = new Error("Import failed after all retry attempts");
97
98
  const totalAttempts = delays.length + 1;
99
+ const startTime = Date.now();
100
+ if (delays.length > 0) {
101
+ emitEvent({ name: "lazy-retry-start", totalAttempts });
102
+ }
98
103
  for (let attempt = 0; attempt < totalAttempts; attempt++) {
99
104
  if (signal?.aborted) {
100
105
  throw signal.reason ?? new DOMException("Aborted", "AbortError");
@@ -102,7 +107,7 @@ var retryImport = async (importFn, delays, options) => {
102
107
  try {
103
108
  const result = await importFn();
104
109
  if (attempt > 0) {
105
- emitEvent({ attempt, name: "lazy-retry-success" });
110
+ emitEvent({ attempt, name: "lazy-retry-success", totalTime: Date.now() - startTime });
106
111
  }
107
112
  return result;
108
113
  } catch (error) {
@@ -115,6 +120,7 @@ var retryImport = async (importFn, delays, options) => {
115
120
  emitEvent({
116
121
  attempt: attempt + 1,
117
122
  delay: currentDelay,
123
+ error: lastError,
118
124
  name: "lazy-retry-attempt",
119
125
  totalAttempts
120
126
  });
@@ -122,7 +128,7 @@ var retryImport = async (importFn, delays, options) => {
122
128
  }
123
129
  }
124
130
  const willReload = callReloadOnFailure === true && isChunkError(lastError) && isDefaultRetryEnabled();
125
- emitEvent({ name: "lazy-retry-exhausted", totalAttempts, willReload });
131
+ emitEvent({ error: lastError, name: "lazy-retry-exhausted", totalAttempts, willReload });
126
132
  if (willReload) {
127
133
  attemptReload(lastError);
128
134
  }
@@ -157,6 +163,7 @@ export {
157
163
  logMessage,
158
164
  optionsWindowKey,
159
165
  retryImport,
166
+ sanitizeCssValue,
160
167
  serializeError,
161
168
  subscribe
162
169
  };
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CACHE_BUST_PARAM,
2
3
  FORCE_RETRY_MAGIC,
3
4
  RETRY_ATTEMPT_PARAM,
4
5
  RETRY_ID_PARAM,
@@ -15,10 +16,11 @@ import {
15
16
  getRetryAttemptFromUrl,
16
17
  getRetryStateFromUrl,
17
18
  isDefaultRetryEnabled,
19
+ reloadScheduledKey,
18
20
  setLastReloadTime,
19
21
  setLastRetryResetInfo,
20
22
  shouldResetRetryCycle
21
- } from "./chunk-X6E7KUDK.js";
23
+ } from "./chunk-PE5QPP5Y.js";
22
24
 
23
25
  // src/common/shouldIgnore.ts
24
26
  var shouldIgnoreMessages = (messages) => {
@@ -81,13 +83,22 @@ var buildReloadUrlAttemptOnly = (retryAttempt) => {
81
83
  url.searchParams.set(RETRY_ATTEMPT_PARAM, String(retryAttempt));
82
84
  return url.toString();
83
85
  };
84
- var reloadScheduled = false;
85
- var attemptReload = (error) => {
86
- if (reloadScheduled) {
86
+ if (globalThis.window && !globalThis.window[reloadScheduledKey]) {
87
+ globalThis.window[reloadScheduledKey] = { scheduled: false };
88
+ }
89
+ var getReloadState = () => {
90
+ if (globalThis.window === void 0) {
91
+ return { scheduled: false };
92
+ }
93
+ return globalThis.window[reloadScheduledKey] ?? (globalThis.window[reloadScheduledKey] = { scheduled: false });
94
+ };
95
+ var attemptReload = (error, opts) => {
96
+ const reloadState = getReloadState();
97
+ if (reloadState.scheduled) {
87
98
  getLogger()?.reloadAlreadyScheduled(error);
88
99
  return;
89
100
  }
90
- reloadScheduled = true;
101
+ reloadState.scheduled = true;
91
102
  try {
92
103
  const options = getOptions();
93
104
  const reloadDelays = options.reloadDelays ?? [1e3, 2e3, 5e3];
@@ -111,7 +122,7 @@ var attemptReload = (error) => {
111
122
  name: "chunk-error"
112
123
  });
113
124
  if (!retryEnabled) {
114
- reloadScheduled = false;
125
+ reloadState.scheduled = false;
115
126
  return;
116
127
  }
117
128
  if (enableRetryReset && retryState && retryState.retryAttempt > 0 && shouldResetRetryCycle(retryState, reloadDelays, minTimeBetweenResets)) {
@@ -138,7 +149,7 @@ var attemptReload = (error) => {
138
149
  if (!shouldIgnoreMessages([errorMsg2])) {
139
150
  getLogger()?.fallbackAlreadyShown(error);
140
151
  }
141
- reloadScheduled = false;
152
+ reloadState.scheduled = false;
142
153
  showFallbackUI();
143
154
  return;
144
155
  }
@@ -166,7 +177,7 @@ var attemptReload = (error) => {
166
177
  if (!useRetryId) {
167
178
  clearRetryAttemptFromUrl();
168
179
  }
169
- reloadScheduled = false;
180
+ reloadState.scheduled = false;
170
181
  showFallbackUI();
171
182
  return;
172
183
  }
@@ -188,15 +199,17 @@ var attemptReload = (error) => {
188
199
  if (useRetryId && enableRetryReset) {
189
200
  setLastReloadTime(retryId, nextAttempt);
190
201
  }
191
- if (useRetryId) {
192
- const reloadUrl = buildReloadUrl(retryId, nextAttempt);
193
- globalThis.window.location.href = reloadUrl;
194
- } else {
195
- globalThis.window.location.href = buildReloadUrlAttemptOnly(nextAttempt);
202
+ let reloadUrl;
203
+ reloadUrl = useRetryId ? buildReloadUrl(retryId, nextAttempt) : buildReloadUrlAttemptOnly(nextAttempt);
204
+ if (opts?.cacheBust) {
205
+ const url = new URL(reloadUrl);
206
+ url.searchParams.set(CACHE_BUST_PARAM, String(Date.now()));
207
+ reloadUrl = url.toString();
196
208
  }
209
+ globalThis.window.location.href = reloadUrl;
197
210
  }, delay);
198
211
  } catch {
199
- reloadScheduled = false;
212
+ reloadState.scheduled = false;
200
213
  }
201
214
  };
202
215
  var showLoadingUI = (attempt) => {
@@ -215,10 +228,10 @@ var showLoadingUI = (attempt) => {
215
228
  container.innerHTML = loadingHtml;
216
229
  const spinnerEl = container.querySelector("[data-spa-guard-spinner]");
217
230
  if (spinnerEl) {
218
- if (options.spinner?.disabled) {
231
+ if (options.html?.spinner?.disabled) {
219
232
  spinnerEl.remove();
220
- } else if (options.spinner?.content) {
221
- spinnerEl.innerHTML = options.spinner.content;
233
+ } else if (options.html?.spinner?.content) {
234
+ spinnerEl.innerHTML = options.html.spinner.content;
222
235
  }
223
236
  }
224
237
  const retryingSection = container.querySelector(
@@ -3,8 +3,10 @@ import {
3
3
  sendBeacon,
4
4
  shouldForceRetry,
5
5
  shouldIgnoreMessages
6
- } from "./chunk-CSN7MQGX.js";
6
+ } from "./chunk-4EXCHJBE.js";
7
7
  import {
8
+ clearRetryStateFromUrl,
9
+ emitEvent,
8
10
  getLogger,
9
11
  getOptions,
10
12
  getRetryInfoForBeacon,
@@ -12,8 +14,9 @@ import {
12
14
  isInitialized,
13
15
  markInitialized,
14
16
  setLogger,
17
+ staticAssetRecoveryKey,
15
18
  updateRetryStateInUrl
16
- } from "./chunk-X6E7KUDK.js";
19
+ } from "./chunk-PE5QPP5Y.js";
17
20
 
18
21
  // src/common/isChunkError.ts
19
22
  var isChunkError = (error) => {
@@ -158,14 +161,96 @@ var extractOwnProperties = (obj) => {
158
161
  return props;
159
162
  };
160
163
 
164
+ // src/common/isStaticAssetError.ts
165
+ var HASHED_ASSET_RE = /[-._][a-zA-Z0-9]{6,}\.(js|mjs|css)$/i;
166
+ var isHashedAssetUrl = (url) => {
167
+ try {
168
+ const pathname = new URL(url).pathname;
169
+ return HASHED_ASSET_RE.test(pathname);
170
+ } catch {
171
+ return HASHED_ASSET_RE.test(url);
172
+ }
173
+ };
174
+ var isStaticAssetError = (event) => {
175
+ const target = event.target;
176
+ if (target instanceof HTMLScriptElement) {
177
+ return isHashedAssetUrl(target.src);
178
+ }
179
+ if (target instanceof HTMLLinkElement) {
180
+ return isHashedAssetUrl(target.href);
181
+ }
182
+ return false;
183
+ };
184
+ var checkResourceStatus = (url) => {
185
+ const entries = performance.getEntriesByName(url, "resource");
186
+ if (entries.length === 0) {
187
+ return true;
188
+ }
189
+ const entry = entries.at(-1);
190
+ if (entry.responseStatus >= 400) {
191
+ return true;
192
+ }
193
+ if (entry.transferSize === 0 && entry.decodedBodySize === 0) {
194
+ return true;
195
+ }
196
+ return false;
197
+ };
198
+ var isLikely404 = (url, timeSinceNavMs = typeof performance === "undefined" ? 0 : performance.now()) => {
199
+ if (url !== void 0) {
200
+ return checkResourceStatus(url);
201
+ }
202
+ return timeSinceNavMs > 3e4;
203
+ };
204
+ var getAssetUrl = (event) => {
205
+ const target = event.target;
206
+ if (target instanceof HTMLScriptElement) {
207
+ return target.src;
208
+ }
209
+ if (target instanceof HTMLLinkElement) {
210
+ return target.href;
211
+ }
212
+ return "";
213
+ };
214
+
215
+ // src/common/staticAssetRecovery.ts
216
+ var getState = () => {
217
+ if (globalThis.window === void 0) {
218
+ return { failedAssets: /* @__PURE__ */ new Set(), recoveryTimer: null };
219
+ }
220
+ if (!globalThis.window[staticAssetRecoveryKey]) {
221
+ globalThis.window[staticAssetRecoveryKey] = {
222
+ failedAssets: /* @__PURE__ */ new Set(),
223
+ recoveryTimer: null
224
+ };
225
+ }
226
+ return globalThis.window[staticAssetRecoveryKey];
227
+ };
228
+ var handleStaticAssetFailure = (url) => {
229
+ const state = getState();
230
+ state.failedAssets.add(url);
231
+ if (state.recoveryTimer !== null) {
232
+ return;
233
+ }
234
+ const options = getOptions();
235
+ const delay = options.staticAssets?.recoveryDelay ?? 500;
236
+ state.recoveryTimer = setTimeout(() => {
237
+ const s = getState();
238
+ const assets = [...s.failedAssets];
239
+ s.failedAssets = /* @__PURE__ */ new Set();
240
+ s.recoveryTimer = null;
241
+ const error = new Error(`Static asset load failed: ${assets.join(", ")}`);
242
+ attemptReload(error, { cacheBust: true });
243
+ }, delay);
244
+ };
245
+
161
246
  // src/common/listen/internal.ts
162
247
  var listenInternal = (serializeError2, logger) => {
163
- if (logger) {
164
- setLogger(logger);
165
- }
166
248
  if (isInitialized()) {
167
249
  return;
168
250
  }
251
+ if (logger) {
252
+ setLogger(logger);
253
+ }
169
254
  markInitialized();
170
255
  const options = getOptions();
171
256
  const reloadDelays = options.reloadDelays ?? [];
@@ -174,10 +259,34 @@ var listenInternal = (serializeError2, logger) => {
174
259
  getLogger()?.retryLimitExceeded(retryState.retryAttempt, reloadDelays.length);
175
260
  updateRetryStateInUrl(retryState.retryId, -1);
176
261
  }
262
+ if (retryState && (retryState.retryAttempt >= reloadDelays.length || retryState.retryAttempt === -1)) {
263
+ globalThis.window.addEventListener(
264
+ "load",
265
+ () => {
266
+ const current = getRetryStateFromUrl();
267
+ if (current?.retryAttempt === -1) {
268
+ clearRetryStateFromUrl();
269
+ }
270
+ },
271
+ { once: true }
272
+ );
273
+ }
177
274
  const wa = globalThis.window.addEventListener.bind(globalThis.window);
178
275
  wa(
179
276
  "error",
180
277
  (event) => {
278
+ const assetUrl = getAssetUrl(event);
279
+ if (isStaticAssetError(event) && isLikely404(assetUrl)) {
280
+ if (shouldIgnoreMessages([assetUrl, event.message])) {
281
+ return;
282
+ }
283
+ event.preventDefault();
284
+ emitEvent({ name: "static-asset-load-failed", url: assetUrl });
285
+ if (options.staticAssets?.autoRecover !== false) {
286
+ handleStaticAssetFailure(assetUrl);
287
+ }
288
+ return;
289
+ }
181
290
  if (shouldIgnoreMessages([event.message])) {
182
291
  return;
183
292
  }
@@ -265,10 +374,12 @@ var eventLogConfig = {
265
374
  "fallback-ui-shown": "warn",
266
375
  "lazy-retry-attempt": "warn",
267
376
  "lazy-retry-exhausted": "error",
377
+ "lazy-retry-start": "log",
268
378
  "lazy-retry-success": "log",
269
379
  "retry-attempt": "warn",
270
380
  "retry-exhausted": "error",
271
- "retry-reset": "log"
381
+ "retry-reset": "log",
382
+ "static-asset-load-failed": "error"
272
383
  };
273
384
  var formatEvent = (event) => {
274
385
  switch (event.name) {
@@ -284,8 +395,12 @@ var formatEvent = (event) => {
284
395
  case "lazy-retry-exhausted": {
285
396
  return `${PREFIX} lazy-retry-exhausted: ${event.totalAttempts} attempts, willReload=${event.willReload}`;
286
397
  }
398
+ case "lazy-retry-start": {
399
+ return `${PREFIX} lazy-retry-start: totalAttempts=${event.totalAttempts}`;
400
+ }
287
401
  case "lazy-retry-success": {
288
- return `${PREFIX} lazy-retry-success: succeeded on attempt ${event.attempt}`;
402
+ const timePart = event.totalTime === void 0 ? "" : `, totalTime=${event.totalTime}ms`;
403
+ return `${PREFIX} lazy-retry-success: succeeded on attempt ${event.attempt}${timePart}`;
289
404
  }
290
405
  case "retry-attempt": {
291
406
  return `${PREFIX} retry-attempt: attempt ${event.attempt} in ${event.delay}ms (retryId: ${event.retryId})`;
@@ -296,6 +411,9 @@ var formatEvent = (event) => {
296
411
  case "retry-reset": {
297
412
  return `${PREFIX} retry-reset: ${event.timeSinceReload}ms since last reload (retryId: ${event.previousRetryId})`;
298
413
  }
414
+ case "static-asset-load-failed": {
415
+ return `${PREFIX} static-asset-load-failed: ${event.url}`;
416
+ }
299
417
  }
300
418
  };
301
419
  var createLogger = () => ({
@@ -329,7 +447,7 @@ var createLogger = () => ({
329
447
  logEvent(event) {
330
448
  const level = eventLogConfig[event.name];
331
449
  const message = formatEvent(event);
332
- if (event.name === "chunk-error") {
450
+ if (event.name === "chunk-error" || event.name === "lazy-retry-attempt" || event.name === "lazy-retry-exhausted") {
333
451
  console[level](message, event.error);
334
452
  } else {
335
453
  console[level](message);
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  showFallbackUI
3
- } from "./chunk-CSN7MQGX.js";
3
+ } from "./chunk-4EXCHJBE.js";
4
4
  import {
5
5
  ForceRetryError,
6
6
  debugSyncErrorEventType,
7
7
  emitEvent,
8
8
  getOptions
9
- } from "./chunk-X6E7KUDK.js";
9
+ } from "./chunk-PE5QPP5Y.js";
10
10
 
11
11
  // src/runtime/debug/errorDispatchers.ts
12
12
  function dispatchAsyncRuntimeError() {
@@ -44,6 +44,12 @@ function dispatchRetryExhausted() {
44
44
  });
45
45
  showFallbackUI();
46
46
  }
47
+ function dispatchStaticAsset404() {
48
+ const hash = crypto.randomUUID().replaceAll("-", "").slice(0, 8);
49
+ const script = document.createElement("script");
50
+ script.src = `/assets/index-${hash}.js`;
51
+ document.head.append(script);
52
+ }
47
53
  function dispatchSyncRuntimeError() {
48
54
  const error = new Error("Simulated sync runtime error from spa-guard debug panel");
49
55
  globalThis.dispatchEvent(new CustomEvent(debugSyncErrorEventType, { detail: { error } }));
@@ -61,6 +67,7 @@ export {
61
67
  dispatchForceRetryError,
62
68
  dispatchNetworkTimeout,
63
69
  dispatchRetryExhausted,
70
+ dispatchStaticAsset404,
64
71
  dispatchSyncRuntimeError,
65
72
  dispatchUnhandledRejection
66
73
  };
@@ -3,7 +3,7 @@ import {
3
3
  getRetryAttemptFromUrl,
4
4
  getRetryStateFromUrl,
5
5
  subscribe
6
- } from "./chunk-X6E7KUDK.js";
6
+ } from "./chunk-PE5QPP5Y.js";
7
7
 
8
8
  // src/runtime/state.ts
9
9
  var getInitialStateFromUrl = () => {
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  defaultSpinnerHtml,
3
- getOptions
4
- } from "./chunk-X6E7KUDK.js";
3
+ getOptions,
4
+ spinnerStateWindowKey
5
+ } from "./chunk-PE5QPP5Y.js";
5
6
 
6
7
  // src/common/parseVersion.ts
7
8
  function extractVersionFromHtml(html) {
@@ -10,7 +11,12 @@ function extractVersionFromHtml(html) {
10
11
  if (versionMatch?.[1]) {
11
12
  return versionMatch[1];
12
13
  }
13
- const optionsMatch = collapsed.match(
14
+ const markerIdx = collapsed.indexOf("__SPA_GUARD_OPTIONS__");
15
+ if (markerIdx === -1) {
16
+ return null;
17
+ }
18
+ const segment = collapsed.slice(markerIdx, markerIdx + 1e3);
19
+ const optionsMatch = segment.match(
14
20
  /__SPA_GUARD_OPTIONS__\s*=\s*\{.*?"?version"?\s*:\s*"([^"]+)"/
15
21
  );
16
22
  return optionsMatch?.[1] ?? null;
@@ -19,7 +25,12 @@ function extractVersionFromHtml(html) {
19
25
  // src/common/spinner.ts
20
26
  var SPINNER_ID = "__spa-guard-spinner";
21
27
  var defaultSpinnerSvg = defaultSpinnerHtml;
22
- var savedOverflow = null;
28
+ var getState = () => {
29
+ if (!globalThis.window[spinnerStateWindowKey]) {
30
+ globalThis.window[spinnerStateWindowKey] = { savedOverflow: null };
31
+ }
32
+ return globalThis.window[spinnerStateWindowKey];
33
+ };
23
34
  function dismissSpinner() {
24
35
  if (typeof document === "undefined") {
25
36
  return;
@@ -28,20 +39,22 @@ function dismissSpinner() {
28
39
  if (el) {
29
40
  el.remove();
30
41
  }
31
- if (savedOverflow !== null) {
32
- document.body.style.overflow = savedOverflow;
33
- savedOverflow = null;
42
+ const state = getState();
43
+ if (state.savedOverflow !== null) {
44
+ document.body.style.overflow = state.savedOverflow;
45
+ state.savedOverflow = null;
34
46
  } else if (el) {
35
47
  document.body.style.overflow = "";
36
48
  }
37
49
  }
50
+ var sanitizeCssValue = (value) => value.replaceAll(/["'<>\\{};\n]/g, "");
38
51
  function getSpinnerHtml(backgroundOverride) {
39
52
  const opts = getOptions();
40
- if (opts.spinner?.disabled) {
53
+ if (opts.html?.spinner?.disabled) {
41
54
  return "";
42
55
  }
43
- const spinnerContent = opts.spinner?.content ?? defaultSpinnerSvg;
44
- const bg = backgroundOverride ?? opts.spinner?.background ?? "#fff";
56
+ const spinnerContent = opts.html?.spinner?.content ?? defaultSpinnerSvg;
57
+ const bg = sanitizeCssValue(backgroundOverride ?? opts.html?.spinner?.background ?? "#fff");
45
58
  return `<div id="${SPINNER_ID}" style="position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;background:var(--spa-guard-spinner-bg,${bg})">${spinnerContent}</div>`;
46
59
  }
47
60
  function showSpinner(options) {
@@ -50,7 +63,7 @@ function showSpinner(options) {
50
63
  };
51
64
  }
52
65
  const opts = getOptions();
53
- if (opts.spinner?.disabled) {
66
+ if (opts.html?.spinner?.disabled) {
54
67
  return () => {
55
68
  };
56
69
  }
@@ -67,7 +80,7 @@ function showSpinner(options) {
67
80
  wrapper.innerHTML = html;
68
81
  const overlay = wrapper.firstElementChild;
69
82
  if (!existing) {
70
- savedOverflow = document.body.style.overflow;
83
+ getState().savedOverflow = document.body.style.overflow;
71
84
  }
72
85
  document.body.style.overflow = "hidden";
73
86
  document.body.append(overlay);
@@ -79,6 +92,7 @@ export {
79
92
  SPINNER_ID,
80
93
  defaultSpinnerSvg,
81
94
  dismissSpinner,
95
+ sanitizeCssValue,
82
96
  getSpinnerHtml,
83
97
  showSpinner
84
98
  };
@@ -13,8 +13,13 @@ var initializedKey = /* @__PURE__ */ Symbol.for(`${name}:initialized`);
13
13
  var loggerWindowKey = /* @__PURE__ */ Symbol.for(`${name}:logger`);
14
14
  var RETRY_ID_PARAM = "spaGuardRetryId";
15
15
  var RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
16
+ var CACHE_BUST_PARAM = "spaGuardCacheBust";
16
17
  var versionCheckStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:version-check-state`);
18
+ var reloadScheduledKey = /* @__PURE__ */ Symbol.for(`${name}:reload-scheduled`);
19
+ var inMemoryLastReloadKey = /* @__PURE__ */ Symbol.for(`${name}:in-memory-last-reload`);
20
+ var staticAssetRecoveryKey = /* @__PURE__ */ Symbol.for(`${name}:static-asset-recovery`);
17
21
  var debugSyncErrorEventType = "spa-guard:debug-sync-error";
22
+ var spinnerStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:spinner-state`);
18
23
 
19
24
  // src/common/events/internal.ts
20
25
  if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) {
@@ -163,6 +168,10 @@ var defaultOptions = {
163
168
  },
164
169
  loading: {
165
170
  content: defaultLoadingFallbackHtml
171
+ },
172
+ spinner: {
173
+ background: "#fff",
174
+ disabled: false
166
175
  }
167
176
  },
168
177
  lazyRetry: {
@@ -171,9 +180,9 @@ var defaultOptions = {
171
180
  },
172
181
  minTimeBetweenResets: 5e3,
173
182
  reloadDelays: [1e3, 2e3, 5e3],
174
- spinner: {
175
- background: "#fff",
176
- disabled: false
183
+ staticAssets: {
184
+ autoRecover: true,
185
+ recoveryDelay: 500
177
186
  },
178
187
  useRetryId: true
179
188
  };
@@ -202,6 +211,10 @@ var getOptions = () => {
202
211
  loading: {
203
212
  ...defaultOptions.html?.loading,
204
213
  ...windowOptions?.html?.loading
214
+ },
215
+ spinner: {
216
+ ...defaultOptions.html?.spinner,
217
+ ...windowOptions?.html?.spinner
205
218
  }
206
219
  },
207
220
  lazyRetry: {
@@ -212,9 +225,9 @@ var getOptions = () => {
212
225
  ...defaultOptions.reportBeacon,
213
226
  ...windowOptions?.reportBeacon
214
227
  },
215
- spinner: {
216
- ...defaultOptions.spinner,
217
- ...windowOptions?.spinner
228
+ staticAssets: {
229
+ ...defaultOptions.staticAssets,
230
+ ...windowOptions?.staticAssets
218
231
  }
219
232
  };
220
233
  };
@@ -231,8 +244,19 @@ var ForceRetryError = class extends Error {
231
244
  // src/common/lastReloadTime.ts
232
245
  var STORAGE_KEY = "__spa_guard_last_reload_timestamp__";
233
246
  var RESET_INFO_KEY = "__spa_guard_last_retry_reset__";
234
- var inMemoryStorage = null;
235
- var inMemoryResetInfo = null;
247
+ if (globalThis.window && !globalThis.window[inMemoryLastReloadKey]) {
248
+ globalThis.window[inMemoryLastReloadKey] = {
249
+ resetInfo: null,
250
+ storage: null
251
+ };
252
+ }
253
+ var getInMemoryState = () => {
254
+ const w = globalThis.window;
255
+ if (!w) {
256
+ return { resetInfo: null, storage: null };
257
+ }
258
+ return w[inMemoryLastReloadKey] ?? (w[inMemoryLastReloadKey] = { resetInfo: null, storage: null });
259
+ };
236
260
  var hasSessionStorage = () => {
237
261
  try {
238
262
  return globalThis.window !== void 0 && typeof sessionStorage !== "undefined";
@@ -250,10 +274,10 @@ var setLastReloadTime = (retryId, attemptNumber) => {
250
274
  try {
251
275
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
252
276
  } catch {
253
- inMemoryStorage = data;
277
+ getInMemoryState().storage = data;
254
278
  }
255
279
  } else {
256
- inMemoryStorage = data;
280
+ getInMemoryState().storage = data;
257
281
  }
258
282
  };
259
283
  var getLastReloadTime = () => {
@@ -264,10 +288,14 @@ var getLastReloadTime = () => {
264
288
  return JSON.parse(stored);
265
289
  }
266
290
  } catch {
267
- return inMemoryStorage;
291
+ try {
292
+ sessionStorage.removeItem(STORAGE_KEY);
293
+ } catch {
294
+ }
295
+ return getInMemoryState().storage;
268
296
  }
269
297
  }
270
- return inMemoryStorage;
298
+ return getInMemoryState().storage;
271
299
  };
272
300
  var clearLastReloadTime = () => {
273
301
  if (hasSessionStorage()) {
@@ -276,7 +304,7 @@ var clearLastReloadTime = () => {
276
304
  } catch {
277
305
  }
278
306
  }
279
- inMemoryStorage = null;
307
+ getInMemoryState().storage = null;
280
308
  };
281
309
  var shouldResetRetryCycle = (retryState, reloadDelays, minTimeBetweenResets = 5e3) => {
282
310
  if (retryState.retryAttempt === 0) {
@@ -311,10 +339,10 @@ var setLastRetryResetInfo = (previousRetryId) => {
311
339
  try {
312
340
  sessionStorage.setItem(RESET_INFO_KEY, JSON.stringify(data));
313
341
  } catch {
314
- inMemoryResetInfo = data;
342
+ getInMemoryState().resetInfo = data;
315
343
  }
316
344
  } else {
317
- inMemoryResetInfo = data;
345
+ getInMemoryState().resetInfo = data;
318
346
  }
319
347
  };
320
348
  var getLastRetryResetInfo = () => {
@@ -325,10 +353,14 @@ var getLastRetryResetInfo = () => {
325
353
  return JSON.parse(stored);
326
354
  }
327
355
  } catch {
328
- return inMemoryResetInfo;
356
+ try {
357
+ sessionStorage.removeItem(RESET_INFO_KEY);
358
+ } catch {
359
+ }
360
+ return getInMemoryState().resetInfo;
329
361
  }
330
362
  }
331
- return inMemoryResetInfo;
363
+ return getInMemoryState().resetInfo;
332
364
  };
333
365
 
334
366
  // src/common/retryState.ts
@@ -339,7 +371,7 @@ var getRetryStateFromUrl = () => {
339
371
  const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
340
372
  if (retryId && retryAttempt) {
341
373
  const parsed = parseInt(retryAttempt, 10);
342
- if (Number.isNaN(parsed)) {
374
+ if (Number.isNaN(parsed) || parsed < -1) {
343
375
  return null;
344
376
  }
345
377
  return {
@@ -357,6 +389,7 @@ var clearRetryStateFromUrl = () => {
357
389
  const url = new URL(globalThis.window.location.href);
358
390
  url.searchParams.delete(RETRY_ID_PARAM);
359
391
  url.searchParams.delete(RETRY_ATTEMPT_PARAM);
392
+ url.searchParams.delete(CACHE_BUST_PARAM);
360
393
  globalThis.window.history.replaceState(null, "", url.toString());
361
394
  } catch {
362
395
  }
@@ -387,7 +420,7 @@ var getRetryAttemptFromUrl = () => {
387
420
  const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
388
421
  if (retryAttempt) {
389
422
  const parsed = parseInt(retryAttempt, 10);
390
- if (Number.isNaN(parsed)) {
423
+ if (Number.isNaN(parsed) || parsed < -1) {
391
424
  return null;
392
425
  }
393
426
  return parsed;
@@ -420,8 +453,12 @@ export {
420
453
  optionsWindowKey,
421
454
  RETRY_ID_PARAM,
422
455
  RETRY_ATTEMPT_PARAM,
456
+ CACHE_BUST_PARAM,
423
457
  versionCheckStateWindowKey,
458
+ reloadScheduledKey,
459
+ staticAssetRecoveryKey,
424
460
  debugSyncErrorEventType,
461
+ spinnerStateWindowKey,
425
462
  setLogger,
426
463
  getLogger,
427
464
  emitEvent,
@@ -5,5 +5,10 @@ export declare const initializedKey: unique symbol;
5
5
  export declare const loggerWindowKey: unique symbol;
6
6
  export declare const RETRY_ID_PARAM = "spaGuardRetryId";
7
7
  export declare const RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
8
+ export declare const CACHE_BUST_PARAM = "spaGuardCacheBust";
8
9
  export declare const versionCheckStateWindowKey: unique symbol;
10
+ export declare const reloadScheduledKey: unique symbol;
11
+ export declare const inMemoryLastReloadKey: unique symbol;
12
+ export declare const staticAssetRecoveryKey: unique symbol;
9
13
  export declare const debugSyncErrorEventType = "spa-guard:debug-sync-error";
14
+ export declare const spinnerStateWindowKey: unique symbol;
@@ -13,6 +13,8 @@ export type SPAGuardEvent = (SPAGuardEventChunkError & {
13
13
  name: "lazy-retry-attempt";
14
14
  }) | (SPAGuardEventLazyRetryExhausted & {
15
15
  name: "lazy-retry-exhausted";
16
+ }) | (SPAGuardEventLazyRetryStart & {
17
+ name: "lazy-retry-start";
16
18
  }) | (SPAGuardEventLazyRetrySuccess & {
17
19
  name: "lazy-retry-success";
18
20
  }) | (SPAGuardEventRetryAttempt & {
@@ -21,6 +23,8 @@ export type SPAGuardEvent = (SPAGuardEventChunkError & {
21
23
  name: "retry-exhausted";
22
24
  }) | (SPAGuardEventRetryReset & {
23
25
  name: "retry-reset";
26
+ }) | (SPAGuardEventStaticAssetLoadFailed & {
27
+ name: "static-asset-load-failed";
24
28
  });
25
29
  export interface SPAGuardEventChunkError {
26
30
  error: unknown;
@@ -36,23 +40,35 @@ export interface SPAGuardEventLazyRetryAttempt {
36
40
  attempt: number;
37
41
  /** Delay in milliseconds before this retry attempt. */
38
42
  delay: number;
43
+ /** The error that caused the previous attempt to fail. */
44
+ error?: unknown;
39
45
  name: "lazy-retry-attempt";
40
46
  /** Total number of attempts including the initial try (delays.length + 1). */
41
47
  totalAttempts: number;
42
48
  }
43
49
  /** Emitted when all module-level retry attempts are exhausted. */
44
50
  export interface SPAGuardEventLazyRetryExhausted {
51
+ /** The final error after all attempts failed. */
52
+ error: unknown;
45
53
  name: "lazy-retry-exhausted";
46
54
  /** Total number of attempts that were made (delays.length + 1). */
47
55
  totalAttempts: number;
48
56
  /** Whether attemptReload() will be called after this event. */
49
57
  willReload: boolean;
50
58
  }
59
+ /** Emitted once before the first import attempt, when retries are configured. */
60
+ export interface SPAGuardEventLazyRetryStart {
61
+ name: "lazy-retry-start";
62
+ /** Total number of attempts that will be made (delays.length + 1). */
63
+ totalAttempts: number;
64
+ }
51
65
  /** Emitted when a module loads successfully after one or more retry attempts. */
52
66
  export interface SPAGuardEventLazyRetrySuccess {
53
67
  /** 1-based retry number on which the import succeeded (1 = first retry). */
54
68
  attempt: number;
55
69
  name: "lazy-retry-success";
70
+ /** Total time in milliseconds from first attempt to success. */
71
+ totalTime?: number;
56
72
  }
57
73
  export interface SPAGuardEventRetryAttempt {
58
74
  attempt: number;
@@ -71,5 +87,11 @@ export interface SPAGuardEventRetryReset {
71
87
  previousRetryId: string;
72
88
  timeSinceReload: number;
73
89
  }
90
+ /** Emitted when a hashed static asset (script/link) fails to load, likely due to a stale deployment. */
91
+ export interface SPAGuardEventStaticAssetLoadFailed {
92
+ name: "static-asset-load-failed";
93
+ /** The URL of the asset that failed to load. */
94
+ url: string;
95
+ }
74
96
  export type SubscribeFn = (event: SPAGuardEvent) => void;
75
97
  export type UnsubscribeFn = () => void;
@@ -2,8 +2,8 @@ import {
2
2
  createLogger,
3
3
  listenInternal,
4
4
  serializeError
5
- } from "../chunk-ZVYB2746.js";
6
- import "../chunk-CSN7MQGX.js";
5
+ } from "../chunk-5ANASVKX.js";
6
+ import "../chunk-4EXCHJBE.js";
7
7
  import {
8
8
  ForceRetryError,
9
9
  disableDefaultRetry,
@@ -12,7 +12,7 @@ import {
12
12
  isDefaultRetryEnabled,
13
13
  options_exports,
14
14
  subscribe
15
- } from "../chunk-X6E7KUDK.js";
15
+ } from "../chunk-PE5QPP5Y.js";
16
16
  import {
17
17
  __export
18
18
  } from "../chunk-MLKGABMK.js";
@@ -0,0 +1,3 @@
1
+ export declare const isStaticAssetError: (event: Event) => boolean;
2
+ export declare const isLikely404: (url?: string, timeSinceNavMs?: number) => boolean;
3
+ export declare const getAssetUrl: (event: Event) => string;
@@ -95,6 +95,30 @@ export interface Options {
95
95
  /** Custom HTML to display during the loading/retrying state */
96
96
  content?: string;
97
97
  };
98
+ /**
99
+ * Spinner overlay configuration.
100
+ * Controls the full-page loading spinner injected by the Vite plugin
101
+ * and available via `showSpinner()`/`dismissSpinner()` at runtime.
102
+ */
103
+ spinner?: {
104
+ /**
105
+ * Overlay background color.
106
+ * Used as CSS variable fallback: var(--spa-guard-spinner-bg, <this value>).
107
+ * @default '#fff'
108
+ */
109
+ background?: string;
110
+ /**
111
+ * Custom spinner HTML (the spinner element only, no container/overlay).
112
+ * If not provided, uses the default SVG spinner.
113
+ */
114
+ content?: string;
115
+ /**
116
+ * Disable spinner entirely.
117
+ * No injection into body, showSpinner() is a no-op, Spinner returns null.
118
+ * @default false
119
+ */
120
+ disabled?: boolean;
121
+ };
98
122
  };
99
123
  /**
100
124
  * Options for the lazyWithRetry module-level retry logic.
@@ -130,28 +154,22 @@ export interface Options {
130
154
  endpoint?: string;
131
155
  };
132
156
  /**
133
- * Spinner overlay configuration.
134
- * Controls the full-page loading spinner injected by the Vite plugin
135
- * and available via `showSpinner()`/`dismissSpinner()` at runtime.
157
+ * Configuration for automatic recovery from static asset 404 errors
158
+ * caused by deployment version mismatches.
136
159
  */
137
- spinner?: {
160
+ staticAssets?: {
138
161
  /**
139
- * Overlay background color.
140
- * Used as CSS variable fallback: var(--spa-guard-spinner-bg, <this value>).
141
- * @default '#fff'
142
- */
143
- background?: string;
144
- /**
145
- * Custom spinner HTML (the spinner element only, no container/overlay).
146
- * If not provided, uses the default SVG spinner.
162
+ * Automatically trigger a cache-busting reload when a hashed static asset
163
+ * fails to load and the page has been open long enough to suggest a stale deployment.
164
+ * @default true
147
165
  */
148
- content?: string;
166
+ autoRecover?: boolean;
149
167
  /**
150
- * Disable spinner entirely.
151
- * No injection into body, showSpinner() is a no-op, Spinner returns null.
152
- * @default false
168
+ * Milliseconds to wait after the first failed asset before triggering the reload.
169
+ * Allows collecting multiple concurrent failures into a single reload.
170
+ * @default 500
153
171
  */
154
- disabled?: boolean;
172
+ recoveryDelay?: number;
155
173
  };
156
174
  /** @default true */
157
175
  useRetryId?: boolean;
@@ -1,4 +1,6 @@
1
1
  /** @internal */
2
2
  export declare const resetReloadScheduled: () => void;
3
- export declare const attemptReload: (error: unknown) => void;
3
+ export declare const attemptReload: (error: unknown, opts?: {
4
+ cacheBust?: boolean;
5
+ }) => void;
4
6
  export declare const showFallbackUI: () => void;
@@ -1,6 +1,7 @@
1
1
  export declare const SPINNER_ID = "__spa-guard-spinner";
2
2
  export declare const defaultSpinnerSvg = "<svg width=\"40\" height=\"40\" viewBox=\"0 0 40 40\" style=\"animation:spa-guard-spin .8s linear infinite\"><circle cx=\"20\" cy=\"20\" r=\"16\" fill=\"none\" stroke=\"#e8e8e8\" stroke-width=\"3\"/><circle cx=\"20\" cy=\"20\" r=\"16\" fill=\"none\" stroke=\"#666\" stroke-width=\"3\" stroke-dasharray=\"80\" stroke-dashoffset=\"60\" stroke-linecap=\"round\"/></svg><style>@keyframes spa-guard-spin{to{transform:rotate(360deg)}}</style>";
3
3
  export declare function dismissSpinner(): void;
4
+ export declare const sanitizeCssValue: (value: string) => string;
4
5
  export declare function getSpinnerHtml(backgroundOverride?: string): string;
5
6
  export declare function showSpinner(options?: {
6
7
  background?: string;
@@ -0,0 +1,2 @@
1
+ export declare const handleStaticAssetFailure: (url: string) => void;
2
+ export declare const resetStaticAssetRecovery: () => void;
@@ -41,6 +41,13 @@ export declare function dispatchNetworkTimeout(delayMs?: number): void;
41
41
  * the fallback UI into the DOM.
42
42
  */
43
43
  export declare function dispatchRetryExhausted(): void;
44
+ /**
45
+ * Simulates a static asset 404 by appending a <script> element with a
46
+ * nonexistent hashed URL to document.head. The browser fires an "error"
47
+ * event on the element, which spa-guard's listenInternal() detects via
48
+ * the Resource Timing API-based isLikely404 check.
49
+ */
50
+ export declare function dispatchStaticAsset404(): void;
44
51
  /**
45
52
  * Dispatches a sync runtime error via CustomEvent.
46
53
  * DebugSyncErrorTrigger (a React component) listens for this event,
@@ -5,16 +5,17 @@ import {
5
5
  dispatchForceRetryError,
6
6
  dispatchNetworkTimeout,
7
7
  dispatchRetryExhausted,
8
+ dispatchStaticAsset404,
8
9
  dispatchSyncRuntimeError,
9
10
  dispatchUnhandledRejection
10
- } from "../../chunk-XFILAQ7P.js";
11
- import "../../chunk-CSN7MQGX.js";
11
+ } from "../../chunk-62AC5565.js";
12
+ import "../../chunk-4EXCHJBE.js";
12
13
  import {
13
14
  subscribeToState
14
- } from "../../chunk-T72DERME.js";
15
+ } from "../../chunk-B7C3LV2W.js";
15
16
  import {
16
17
  subscribe
17
- } from "../../chunk-X6E7KUDK.js";
18
+ } from "../../chunk-PE5QPP5Y.js";
18
19
  import "../../chunk-MLKGABMK.js";
19
20
 
20
21
  // src/runtime/debug/index.ts
@@ -30,7 +31,8 @@ var SCENARIOS = [
30
31
  key: "unhandled-rejection",
31
32
  label: "Unhandled Rejection"
32
33
  },
33
- { dispatch: dispatchRetryExhausted, key: "exhaust-retries", label: "Exhaust Retries" }
34
+ { dispatch: dispatchRetryExhausted, key: "exhaust-retries", label: "Exhaust Retries" },
35
+ { dispatch: dispatchStaticAsset404, key: "static-asset-404", label: "Static Asset 404" }
34
36
  ];
35
37
  var POSITION_MAP = {
36
38
  "bottom-left": "bottom:16px;left:16px;",
@@ -3,18 +3,18 @@ import {
3
3
  extractVersionFromHtml,
4
4
  getSpinnerHtml,
5
5
  showSpinner
6
- } from "../chunk-Z75UPJWV.js";
6
+ } from "../chunk-BA5VUNSU.js";
7
7
  import {
8
8
  getState,
9
9
  subscribeToState
10
- } from "../chunk-T72DERME.js";
10
+ } from "../chunk-B7C3LV2W.js";
11
11
  import {
12
12
  ForceRetryError,
13
13
  getLogger,
14
14
  getOptions,
15
15
  setTranslations,
16
16
  versionCheckStateWindowKey
17
- } from "../chunk-X6E7KUDK.js";
17
+ } from "../chunk-PE5QPP5Y.js";
18
18
  import "../chunk-MLKGABMK.js";
19
19
 
20
20
  // src/common/checkVersion.ts
@@ -38,45 +38,60 @@ var getState2 = () => {
38
38
  }
39
39
  return globalThis.window[versionCheckStateWindowKey] ?? (globalThis.window[versionCheckStateWindowKey] = createInitialState());
40
40
  };
41
+ var FETCH_TIMEOUT_MS = 3e4;
41
42
  var fetchJsonVersion = async () => {
42
43
  const endpoint = getOptions().checkVersion?.endpoint;
43
44
  if (!endpoint) {
44
45
  getLogger()?.versionCheckRequiresEndpoint();
45
46
  return null;
46
47
  }
47
- const response = await fetch(endpoint, {
48
- cache: getOptions().checkVersion?.cache ?? "no-store",
49
- headers: { Accept: "application/json" }
50
- });
51
- if (!response.ok) {
52
- getLogger()?.versionCheckHttpError(response.status);
53
- return null;
54
- }
55
- const data = await response.json();
56
- if (typeof data !== "object" || data === null) {
57
- return null;
48
+ const controller = new AbortController();
49
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
50
+ try {
51
+ const response = await fetch(endpoint, {
52
+ cache: getOptions().checkVersion?.cache ?? "no-store",
53
+ headers: { Accept: "application/json" },
54
+ signal: controller.signal
55
+ });
56
+ if (!response.ok) {
57
+ getLogger()?.versionCheckHttpError(response.status);
58
+ return null;
59
+ }
60
+ const data = await response.json();
61
+ if (typeof data !== "object" || data === null) {
62
+ return null;
63
+ }
64
+ return "version" in data && typeof data.version === "string" ? data.version : null;
65
+ } finally {
66
+ clearTimeout(timeoutId);
58
67
  }
59
- return "version" in data && typeof data.version === "string" ? data.version : null;
60
68
  };
61
69
  var fetchHtmlVersion = async () => {
62
70
  const url = new URL(globalThis.location.href);
63
71
  url.search = "";
64
72
  url.hash = "";
65
- const response = await fetch(url.toString(), {
66
- cache: getOptions().checkVersion?.cache ?? "no-store",
67
- headers: { Accept: "text/html" }
68
- });
69
- if (!response.ok) {
70
- getLogger()?.versionCheckHttpError(response.status);
71
- return null;
72
- }
73
- const html = await response.text();
74
- const version = extractVersionFromHtml(html);
75
- if (!version) {
76
- getLogger()?.versionCheckParseError();
77
- return null;
73
+ const controller = new AbortController();
74
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
75
+ try {
76
+ const response = await fetch(url.toString(), {
77
+ cache: getOptions().checkVersion?.cache ?? "no-store",
78
+ headers: { Accept: "text/html" },
79
+ signal: controller.signal
80
+ });
81
+ if (!response.ok) {
82
+ getLogger()?.versionCheckHttpError(response.status);
83
+ return null;
84
+ }
85
+ const html = await response.text();
86
+ const version = extractVersionFromHtml(html);
87
+ if (!version) {
88
+ getLogger()?.versionCheckParseError();
89
+ return null;
90
+ }
91
+ return version;
92
+ } finally {
93
+ clearTimeout(timeoutId);
78
94
  }
79
- return version;
80
95
  };
81
96
  var fetchRemoteVersion = async (mode) => {
82
97
  return mode === "json" ? fetchJsonVersion() : fetchHtmlVersion();
@@ -143,6 +158,9 @@ var handleVisibilityHidden = () => {
143
158
  getLogger()?.versionCheckPaused();
144
159
  };
145
160
  var handleResume = (mode, interval) => {
161
+ if (document.visibilityState !== "visible" || !document.hasFocus()) {
162
+ return;
163
+ }
146
164
  const s = getState2();
147
165
  if (s.versionCheckInterval !== null || s.versionCheckTimeout !== null) {
148
166
  return;
@@ -243,7 +261,9 @@ var recommendedSetup = (overrides) => {
243
261
  startVersionCheck();
244
262
  }
245
263
  return () => {
246
- stopVersionCheck();
264
+ if (options.versionCheck) {
265
+ stopVersionCheck();
266
+ }
247
267
  };
248
268
  };
249
269
  export {
@@ -1,9 +1,13 @@
1
1
  export interface BeaconSchema {
2
2
  appName?: string;
3
+ errorContext?: string;
3
4
  errorMessage?: string;
5
+ errorType?: string;
4
6
  eventMessage?: string;
5
7
  eventName?: string;
8
+ httpStatus?: number;
6
9
  retryAttempt?: number;
7
10
  retryId?: string;
8
11
  serialized?: string;
12
+ url?: string;
9
13
  }
@@ -3,11 +3,14 @@ import "../chunk-MLKGABMK.js";
3
3
  // src/schema/parse.ts
4
4
  var STRING_FIELDS = [
5
5
  "appName",
6
+ "errorContext",
6
7
  "errorMessage",
8
+ "errorType",
7
9
  "eventMessage",
8
10
  "eventName",
9
11
  "retryId",
10
- "serialized"
12
+ "serialized",
13
+ "url"
11
14
  ];
12
15
  function parseBeacon(data) {
13
16
  if (!data || typeof data !== "object" || Array.isArray(data)) {
@@ -23,11 +26,13 @@ function parseBeacon(data) {
23
26
  result[field] = d[field];
24
27
  }
25
28
  }
26
- if ("retryAttempt" in d) {
27
- if (typeof d.retryAttempt !== "number") {
28
- throw new TypeError("Beacon validation failed: retryAttempt must be a number");
29
+ for (const field of ["retryAttempt", "httpStatus"]) {
30
+ if (field in d) {
31
+ if (typeof d[field] !== "number") {
32
+ throw new TypeError(`Beacon validation failed: ${field} must be a number`);
33
+ }
34
+ result[field] = d[field];
29
35
  }
30
- result.retryAttempt = d.retryAttempt;
31
36
  }
32
37
  return result;
33
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovineko/spa-guard",
3
- "version": "0.0.1-alpha-26",
3
+ "version": "0.0.1-alpha-28",
4
4
  "description": "Chunk load error handling for SPAs — core runtime, error handling, schema, i18n",
5
5
  "keywords": [
6
6
  "spa",