@ovineko/spa-guard 0.0.1-alpha-27 → 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-FRCJOMRP.js";
6
+ } from "./chunk-5ANASVKX.js";
7
7
  import {
8
8
  SPINNER_ID,
9
9
  defaultSpinnerSvg,
10
- extractVersionFromHtml
11
- } from "./chunk-7PDOUY6W.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-7KNE2UWX.js";
21
+ } from "./chunk-62AC5565.js";
21
22
  import {
22
23
  attemptReload,
23
24
  sendBeacon,
24
25
  shouldForceRetry,
25
26
  shouldIgnoreMessages
26
- } from "./chunk-KAH6PVTJ.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-67WBMNUS.js";
42
+ } from "./chunk-PE5QPP5Y.js";
42
43
  import "./chunk-MLKGABMK.js";
43
44
 
44
45
  // src/common/handleErrorWithSpaGuard.ts
@@ -96,7 +97,9 @@ var retryImport = async (importFn, delays, options) => {
96
97
  let lastError = new Error("Import failed after all retry attempts");
97
98
  const totalAttempts = delays.length + 1;
98
99
  const startTime = Date.now();
99
- emitEvent({ name: "lazy-retry-start", totalAttempts });
100
+ if (delays.length > 0) {
101
+ emitEvent({ name: "lazy-retry-start", totalAttempts });
102
+ }
100
103
  for (let attempt = 0; attempt < totalAttempts; attempt++) {
101
104
  if (signal?.aborted) {
102
105
  throw signal.reason ?? new DOMException("Aborted", "AbortError");
@@ -160,6 +163,7 @@ export {
160
163
  logMessage,
161
164
  optionsWindowKey,
162
165
  retryImport,
166
+ sanitizeCssValue,
163
167
  serializeError,
164
168
  subscribe
165
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,
@@ -19,7 +20,7 @@ import {
19
20
  setLastReloadTime,
20
21
  setLastRetryResetInfo,
21
22
  shouldResetRetryCycle
22
- } from "./chunk-67WBMNUS.js";
23
+ } from "./chunk-PE5QPP5Y.js";
23
24
 
24
25
  // src/common/shouldIgnore.ts
25
26
  var shouldIgnoreMessages = (messages) => {
@@ -202,7 +203,7 @@ var attemptReload = (error, opts) => {
202
203
  reloadUrl = useRetryId ? buildReloadUrl(retryId, nextAttempt) : buildReloadUrlAttemptOnly(nextAttempt);
203
204
  if (opts?.cacheBust) {
204
205
  const url = new URL(reloadUrl);
205
- url.searchParams.set("spaGuardCacheBust", String(Date.now()));
206
+ url.searchParams.set(CACHE_BUST_PARAM, String(Date.now()));
206
207
  reloadUrl = url.toString();
207
208
  }
208
209
  globalThis.window.location.href = reloadUrl;
@@ -3,8 +3,9 @@ import {
3
3
  sendBeacon,
4
4
  shouldForceRetry,
5
5
  shouldIgnoreMessages
6
- } from "./chunk-KAH6PVTJ.js";
6
+ } from "./chunk-4EXCHJBE.js";
7
7
  import {
8
+ clearRetryStateFromUrl,
8
9
  emitEvent,
9
10
  getLogger,
10
11
  getOptions,
@@ -13,8 +14,9 @@ import {
13
14
  isInitialized,
14
15
  markInitialized,
15
16
  setLogger,
17
+ staticAssetRecoveryKey,
16
18
  updateRetryStateInUrl
17
- } from "./chunk-67WBMNUS.js";
19
+ } from "./chunk-PE5QPP5Y.js";
18
20
 
19
21
  // src/common/isChunkError.ts
20
22
  var isChunkError = (error) => {
@@ -179,7 +181,24 @@ var isStaticAssetError = (event) => {
179
181
  }
180
182
  return false;
181
183
  };
182
- var isLikely404 = (timeSinceNavMs = typeof performance === "undefined" ? 0 : performance.now()) => {
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
+ }
183
202
  return timeSinceNavMs > 3e4;
184
203
  };
185
204
  var getAssetUrl = (event) => {
@@ -194,19 +213,31 @@ var getAssetUrl = (event) => {
194
213
  };
195
214
 
196
215
  // src/common/staticAssetRecovery.ts
197
- var recoveryTimer = null;
198
- var failedAssets = /* @__PURE__ */ new Set();
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
+ };
199
228
  var handleStaticAssetFailure = (url) => {
200
- failedAssets.add(url);
201
- if (recoveryTimer !== null) {
229
+ const state = getState();
230
+ state.failedAssets.add(url);
231
+ if (state.recoveryTimer !== null) {
202
232
  return;
203
233
  }
204
234
  const options = getOptions();
205
235
  const delay = options.staticAssets?.recoveryDelay ?? 500;
206
- recoveryTimer = setTimeout(() => {
207
- const assets = [...failedAssets];
208
- failedAssets = /* @__PURE__ */ new Set();
209
- recoveryTimer = null;
236
+ state.recoveryTimer = setTimeout(() => {
237
+ const s = getState();
238
+ const assets = [...s.failedAssets];
239
+ s.failedAssets = /* @__PURE__ */ new Set();
240
+ s.recoveryTimer = null;
210
241
  const error = new Error(`Static asset load failed: ${assets.join(", ")}`);
211
242
  attemptReload(error, { cacheBust: true });
212
243
  }, delay);
@@ -214,12 +245,12 @@ var handleStaticAssetFailure = (url) => {
214
245
 
215
246
  // src/common/listen/internal.ts
216
247
  var listenInternal = (serializeError2, logger) => {
217
- if (logger) {
218
- setLogger(logger);
219
- }
220
248
  if (isInitialized()) {
221
249
  return;
222
250
  }
251
+ if (logger) {
252
+ setLogger(logger);
253
+ }
223
254
  markInitialized();
224
255
  const options = getOptions();
225
256
  const reloadDelays = options.reloadDelays ?? [];
@@ -228,10 +259,34 @@ var listenInternal = (serializeError2, logger) => {
228
259
  getLogger()?.retryLimitExceeded(retryState.retryAttempt, reloadDelays.length);
229
260
  updateRetryStateInUrl(retryState.retryId, -1);
230
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
+ }
231
274
  const wa = globalThis.window.addEventListener.bind(globalThis.window);
232
275
  wa(
233
276
  "error",
234
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
+ }
235
290
  if (shouldIgnoreMessages([event.message])) {
236
291
  return;
237
292
  }
@@ -246,15 +301,6 @@ var listenInternal = (serializeError2, logger) => {
246
301
  attemptReload(event.error ?? event);
247
302
  return;
248
303
  }
249
- if (isStaticAssetError(event) && isLikely404()) {
250
- const assetUrl = getAssetUrl(event);
251
- event.preventDefault();
252
- emitEvent({ name: "static-asset-load-failed", url: assetUrl });
253
- if (options.staticAssets?.autoRecover !== false) {
254
- handleStaticAssetFailure(assetUrl);
255
- }
256
- return;
257
- }
258
304
  const serialized = serializeError2(event);
259
305
  sendBeacon({
260
306
  errorMessage: event.message,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  showFallbackUI
3
- } from "./chunk-KAH6PVTJ.js";
3
+ } from "./chunk-4EXCHJBE.js";
4
4
  import {
5
5
  ForceRetryError,
6
6
  debugSyncErrorEventType,
7
7
  emitEvent,
8
8
  getOptions
9
- } from "./chunk-67WBMNUS.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-67WBMNUS.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-67WBMNUS.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
53
  if (opts.html?.spinner?.disabled) {
41
54
  return "";
42
55
  }
43
56
  const spinnerContent = opts.html?.spinner?.content ?? defaultSpinnerSvg;
44
- const bg = backgroundOverride ?? opts.html?.spinner?.background ?? "#fff";
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) {
@@ -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,10 +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`);
17
18
  var reloadScheduledKey = /* @__PURE__ */ Symbol.for(`${name}:reload-scheduled`);
18
19
  var inMemoryLastReloadKey = /* @__PURE__ */ Symbol.for(`${name}:in-memory-last-reload`);
20
+ var staticAssetRecoveryKey = /* @__PURE__ */ Symbol.for(`${name}:static-asset-recovery`);
19
21
  var debugSyncErrorEventType = "spa-guard:debug-sync-error";
22
+ var spinnerStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:spinner-state`);
20
23
 
21
24
  // src/common/events/internal.ts
22
25
  if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) {
@@ -368,7 +371,7 @@ var getRetryStateFromUrl = () => {
368
371
  const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
369
372
  if (retryId && retryAttempt) {
370
373
  const parsed = parseInt(retryAttempt, 10);
371
- if (Number.isNaN(parsed)) {
374
+ if (Number.isNaN(parsed) || parsed < -1) {
372
375
  return null;
373
376
  }
374
377
  return {
@@ -386,6 +389,7 @@ var clearRetryStateFromUrl = () => {
386
389
  const url = new URL(globalThis.window.location.href);
387
390
  url.searchParams.delete(RETRY_ID_PARAM);
388
391
  url.searchParams.delete(RETRY_ATTEMPT_PARAM);
392
+ url.searchParams.delete(CACHE_BUST_PARAM);
389
393
  globalThis.window.history.replaceState(null, "", url.toString());
390
394
  } catch {
391
395
  }
@@ -416,7 +420,7 @@ var getRetryAttemptFromUrl = () => {
416
420
  const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
417
421
  if (retryAttempt) {
418
422
  const parsed = parseInt(retryAttempt, 10);
419
- if (Number.isNaN(parsed)) {
423
+ if (Number.isNaN(parsed) || parsed < -1) {
420
424
  return null;
421
425
  }
422
426
  return parsed;
@@ -449,9 +453,12 @@ export {
449
453
  optionsWindowKey,
450
454
  RETRY_ID_PARAM,
451
455
  RETRY_ATTEMPT_PARAM,
456
+ CACHE_BUST_PARAM,
452
457
  versionCheckStateWindowKey,
453
458
  reloadScheduledKey,
459
+ staticAssetRecoveryKey,
454
460
  debugSyncErrorEventType,
461
+ spinnerStateWindowKey,
455
462
  setLogger,
456
463
  getLogger,
457
464
  emitEvent,
@@ -5,7 +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;
9
10
  export declare const reloadScheduledKey: unique symbol;
10
11
  export declare const inMemoryLastReloadKey: unique symbol;
12
+ export declare const staticAssetRecoveryKey: unique symbol;
11
13
  export declare const debugSyncErrorEventType = "spa-guard:debug-sync-error";
14
+ export declare const spinnerStateWindowKey: unique symbol;
@@ -2,8 +2,8 @@ import {
2
2
  createLogger,
3
3
  listenInternal,
4
4
  serializeError
5
- } from "../chunk-FRCJOMRP.js";
6
- import "../chunk-KAH6PVTJ.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-67WBMNUS.js";
15
+ } from "../chunk-PE5QPP5Y.js";
16
16
  import {
17
17
  __export
18
18
  } from "../chunk-MLKGABMK.js";
@@ -1,3 +1,3 @@
1
1
  export declare const isStaticAssetError: (event: Event) => boolean;
2
- export declare const isLikely404: (timeSinceNavMs?: number) => boolean;
2
+ export declare const isLikely404: (url?: string, timeSinceNavMs?: number) => boolean;
3
3
  export declare const getAssetUrl: (event: Event) => string;
@@ -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;
@@ -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-7KNE2UWX.js";
11
- import "../../chunk-KAH6PVTJ.js";
11
+ } from "../../chunk-62AC5565.js";
12
+ import "../../chunk-4EXCHJBE.js";
12
13
  import {
13
14
  subscribeToState
14
- } from "../../chunk-V2OOMVZK.js";
15
+ } from "../../chunk-B7C3LV2W.js";
15
16
  import {
16
17
  subscribe
17
- } from "../../chunk-67WBMNUS.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-7PDOUY6W.js";
6
+ } from "../chunk-BA5VUNSU.js";
7
7
  import {
8
8
  getState,
9
9
  subscribeToState
10
- } from "../chunk-V2OOMVZK.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-67WBMNUS.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 {
@@ -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-27",
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",