@ovineko/spa-guard 0.0.1-alpha-4 → 0.0.1-alpha-6

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/README.md CHANGED
@@ -37,6 +37,10 @@ Peer dependencies vary by integration - see sections below for specific requirem
37
37
  - ✅ **Event hooks** - React hooks for subscribing to spa-guard events (`useSPAGuardEvents`, `useSPAGuardChunkError`)
38
38
  - ✅ **Retry control** - Programmatic control over default retry behavior (`disableDefaultRetry`, `enableDefaultRetry`)
39
39
  - ✅ **ESLint plugin** - Enforces usage of spa-guard wrappers instead of direct React imports
40
+ - ✅ **ForceRetryError** - Typed error class that triggers automatic retry without `forceRetry` config
41
+ - ✅ **appName option** - Beacon source identification for monorepo setups
42
+ - ✅ **BeaconError** - Utility class for error tracking service integration (Sentry, Datadog, etc.)
43
+ - ✅ **Configurable unhandled rejection handling** - Control retry and beacon behavior for non-chunk unhandled promise rejections
40
44
 
41
45
  ## Quick Start
42
46
 
@@ -52,6 +56,7 @@ export default defineConfig({
52
56
  plugins: [
53
57
  spaGuardVitePlugin({
54
58
  // Production configuration
59
+ appName: "my-app", // Identifies this app in beacon reports (useful for monorepos)
55
60
  reloadDelays: [1000, 2000, 5000], // 3 attempts with increasing delays
56
61
  html: {
57
62
  fallback: {
@@ -71,7 +76,7 @@ export default defineConfig({
71
76
  endpoint: "/api/beacon",
72
77
  },
73
78
  errors: {
74
- ignore: [], // Filter out specific error messages from reporting
79
+ ignore: [], // Error substrings to fully skip (no log, no beacon, no reload)
75
80
  forceRetry: [], // Custom error messages that trigger retry/reload (like chunk errors)
76
81
  },
77
82
  useRetryId: true, // Use query parameters for cache busting (default: true)
@@ -245,6 +250,9 @@ The Vite plugin injects an inline script into your HTML `<head>` that runs **bef
245
250
  import { spaGuardVitePlugin } from "@ovineko/spa-guard/vite-plugin";
246
251
 
247
252
  spaGuardVitePlugin({
253
+ // App identification (useful for monorepos)
254
+ appName: "my-app", // Included in beacon payloads for source identification
255
+
248
256
  // Retry configuration
249
257
  reloadDelays: [1000, 2000, 5000], // Array of delays in milliseconds
250
258
  useRetryId: true, // Use query parameters for cache busting (default: true)
@@ -266,10 +274,16 @@ spaGuardVitePlugin({
266
274
 
267
275
  // Error filtering and retry
268
276
  errors: {
269
- ignore: [], // Array of error message substrings to ignore
277
+ ignore: [], // Array of error message substrings to fully skip (no log, no beacon, no reload)
270
278
  forceRetry: [], // Array of error message substrings that trigger retry/reload
271
279
  },
272
280
 
281
+ // Unhandled rejection behavior
282
+ handleUnhandledRejections: {
283
+ retry: true, // Attempt page reload on unhandled rejections (default: true)
284
+ sendBeacon: true, // Send beacon report on unhandled rejections (default: true)
285
+ },
286
+
273
287
  // Beacon reporting
274
288
  reportBeacon: {
275
289
  endpoint: "/api/beacon", // Server endpoint for error reports
@@ -292,6 +306,7 @@ spaGuardVitePlugin({
292
306
 
293
307
  ```typescript
294
308
  interface Options {
309
+ appName?: string; // App name for beacon source identification (monorepo setups)
295
310
  reloadDelays?: number[]; // Array of retry delays in ms (default: [1000, 2000, 5000])
296
311
  useRetryId?: boolean; // Use query params for cache busting (default: true)
297
312
  enableRetryReset?: boolean; // Auto-reset retry cycle when enough time passes (default: true)
@@ -307,10 +322,15 @@ interface Options {
307
322
  };
308
323
 
309
324
  errors?: {
310
- ignore?: string[]; // Error message substrings to filter out (default: [])
325
+ ignore?: string[]; // Error message substrings to fully skip (no log, no beacon, no reload) (default: [])
311
326
  forceRetry?: string[]; // Error message substrings that trigger retry/reload (default: [])
312
327
  };
313
328
 
329
+ handleUnhandledRejections?: {
330
+ retry?: boolean; // Attempt page reload on unhandled rejections (default: true)
331
+ sendBeacon?: boolean; // Send beacon report on unhandled rejections (default: true)
332
+ };
333
+
314
334
  html?: {
315
335
  fallback?: {
316
336
  content?: string; // Custom error UI HTML (default: minimal error screen)
@@ -353,6 +373,10 @@ interface VitePluginOptions extends Options {
353
373
  ignore: [],
354
374
  forceRetry: [],
355
375
  },
376
+ handleUnhandledRejections: {
377
+ retry: true,
378
+ sendBeacon: true,
379
+ },
356
380
  html: {
357
381
  fallback: {
358
382
  content: defaultErrorFallbackHtml, // minimal error screen (auto-generated)
@@ -379,9 +403,10 @@ app.register(fastifySPAGuard, {
379
403
  onBeacon: async (beacon, request, reply) => {
380
404
  const error = new Error(beacon.errorMessage || "Unknown client error");
381
405
 
382
- // Log structured data
406
+ // Log structured data (appName identifies the source app in monorepos)
383
407
  request.log.error(
384
408
  {
409
+ appName: beacon.appName,
385
410
  errorMessage: beacon.errorMessage,
386
411
  eventName: beacon.eventName,
387
412
  eventMessage: beacon.eventMessage,
@@ -405,6 +430,7 @@ app.register(fastifySPAGuard, {
405
430
 
406
431
  ```typescript
407
432
  interface BeaconSchema {
433
+ appName?: string; // Source app name (from appName option, useful for monorepos)
408
434
  errorMessage?: string; // Error message
409
435
  eventMessage?: string; // Event-specific message
410
436
  eventName?: string; // Event type (e.g., 'chunk_error_max_reloads', 'error', 'unhandledrejection')
@@ -421,6 +447,83 @@ interface BeaconSchema {
421
447
  - `unhandledrejection` - Non-chunk promise rejection
422
448
  - `securitypolicyviolation` - CSP violation
423
449
 
450
+ ### BeaconError
451
+
452
+ `BeaconError` is a utility class that wraps beacon data into a structured `Error` object with typed properties. This makes it easy to integrate with error tracking services like Sentry, Datadog, and others that expect `Error` instances.
453
+
454
+ ```typescript
455
+ import { BeaconError, fastifySPAGuard } from "@ovineko/spa-guard/fastify";
456
+ // BeaconError is also available from the root entry point:
457
+ // import { BeaconError } from "@ovineko/spa-guard";
458
+
459
+ app.register(fastifySPAGuard, {
460
+ path: "/api/beacon",
461
+ onBeacon: async (beacon, request) => {
462
+ const error = new BeaconError(beacon);
463
+
464
+ // Typed properties from the beacon
465
+ error.appName; // string | undefined
466
+ error.errorMessage; // string | undefined
467
+ error.eventName; // string | undefined
468
+ error.retryAttempt; // number | undefined
469
+ error.retryId; // string | undefined
470
+ error.serialized; // string | undefined
471
+ error.eventMessage; // string | undefined
472
+
473
+ // Standard Error properties
474
+ error.name; // "BeaconError"
475
+ error.message; // errorMessage ?? eventMessage ?? "Unknown beacon error"
476
+ error instanceof Error; // true
477
+ error instanceof BeaconError; // true
478
+
479
+ // JSON serialization
480
+ error.toJSON(); // { appName, errorMessage, eventMessage, ... }
481
+ },
482
+ });
483
+ ```
484
+
485
+ **Sentry integration:**
486
+
487
+ ```typescript
488
+ import * as Sentry from "@sentry/node";
489
+ import { BeaconError, fastifySPAGuard } from "@ovineko/spa-guard/fastify";
490
+
491
+ app.register(fastifySPAGuard, {
492
+ path: "/api/beacon",
493
+ onBeacon: async (beacon) => {
494
+ const error = new BeaconError(beacon);
495
+ Sentry.captureException(error, {
496
+ tags: {
497
+ appName: error.appName,
498
+ eventName: error.eventName,
499
+ },
500
+ extra: error.toJSON(),
501
+ });
502
+ return { skipDefaultLog: true };
503
+ },
504
+ });
505
+ ```
506
+
507
+ **Datadog integration:**
508
+
509
+ ```typescript
510
+ import tracer from "dd-trace";
511
+ import { BeaconError, fastifySPAGuard } from "@ovineko/spa-guard/fastify";
512
+
513
+ app.register(fastifySPAGuard, {
514
+ path: "/api/beacon",
515
+ onBeacon: async (beacon) => {
516
+ const error = new BeaconError(beacon);
517
+ const span = tracer.scope().active();
518
+ span?.setTag("error", true);
519
+ span?.setTag("error.message", error.message);
520
+ span?.setTag("error.type", error.eventName);
521
+ if (error.appName) span?.setTag("app.name", error.appName);
522
+ return { skipDefaultLog: true };
523
+ },
524
+ });
525
+ ```
526
+
424
527
  ### React Integration
425
528
 
426
529
  spa-guard provides two React error boundary components with automatic chunk error recovery.
@@ -1019,6 +1122,7 @@ Return `{ skipDefaultLog: true }` from `onBeacon` or `onUnknownBeacon` to suppre
1019
1122
 
1020
1123
  ```typescript
1021
1124
  interface Options {
1125
+ appName?: string; // App name for beacon source identification (monorepo setups)
1022
1126
  reloadDelays?: number[]; // Retry delays in ms (default: [1000, 2000, 5000])
1023
1127
  useRetryId?: boolean; // Use query params for cache busting (default: true)
1024
1128
  enableRetryReset?: boolean; // Auto-reset retry cycle when enough time passes (default: true)
@@ -1034,10 +1138,15 @@ interface Options {
1034
1138
  };
1035
1139
 
1036
1140
  errors?: {
1037
- ignore?: string[]; // Error message substrings to filter out (default: [])
1141
+ ignore?: string[]; // Error message substrings to fully skip (no log, no beacon, no reload) (default: [])
1038
1142
  forceRetry?: string[]; // Error message substrings that trigger retry/reload (default: [])
1039
1143
  };
1040
1144
 
1145
+ handleUnhandledRejections?: {
1146
+ retry?: boolean; // Attempt page reload on unhandled rejections (default: true)
1147
+ sendBeacon?: boolean; // Send beacon report on unhandled rejections (default: true)
1148
+ };
1149
+
1041
1150
  html?: {
1042
1151
  fallback?: {
1043
1152
  content?: string; // Custom error UI HTML
@@ -1071,6 +1180,7 @@ interface VitePluginOptions extends Options {
1071
1180
 
1072
1181
  ```typescript
1073
1182
  interface BeaconSchema {
1183
+ appName?: string; // Source app name (from appName option)
1074
1184
  errorMessage?: string;
1075
1185
  eventMessage?: string;
1076
1186
  eventName?: string;
@@ -1092,6 +1202,8 @@ From `@ovineko/spa-guard`:
1092
1202
  - `disableDefaultRetry()` - Disable inline script's automatic retry
1093
1203
  - `enableDefaultRetry()` - Re-enable automatic retry
1094
1204
  - `isDefaultRetryEnabled()` - Check if default retry is enabled
1205
+ - `ForceRetryError` - Error class that triggers automatic retry when thrown
1206
+ - `BeaconError` - Utility class that wraps beacon data into a structured Error
1095
1207
 
1096
1208
  ### Schema Exports
1097
1209
 
@@ -1113,6 +1225,7 @@ From `@ovineko/spa-guard/runtime`:
1113
1225
  - `subscribeToState(callback)` - Subscribe to state changes, returns unsubscribe function
1114
1226
  - `startVersionCheck()` - Start periodic version polling
1115
1227
  - `stopVersionCheck()` - Stop version polling
1228
+ - `ForceRetryError` - Error class that triggers automatic retry when thrown
1116
1229
  - `SpaGuardState` - TypeScript type for state object
1117
1230
  - `RecommendedSetupOptions` - TypeScript type for recommendedSetup overrides
1118
1231
 
@@ -1343,14 +1456,14 @@ spa-guard provides 11 export entry points:
1343
1456
 
1344
1457
  | Export | Description | Peer Dependencies |
1345
1458
  | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
1346
- | `.` | Core functionality (events, listen, options, retry control) | None |
1459
+ | `.` | Core functionality (events, listen, options, retry control, ForceRetryError, BeaconError) | None |
1347
1460
  | `./schema` | BeaconSchema type definitions | `typebox@^1` |
1348
1461
  | `./schema/parse` | Beacon parsing utilities | `typebox@^1` |
1349
- | `./runtime` | Runtime state management and subscriptions | None |
1462
+ | `./runtime` | Runtime state management, subscriptions, and ForceRetryError | None |
1350
1463
  | `./react` | React hooks and components (useSpaGuardState, useSPAGuardEvents, useSPAGuardChunkError, lazyWithRetry, DebugSyncErrorTrigger) | `react@^19` |
1351
1464
  | `./runtime/debug` | Debug panel factory (`createDebugger`) - framework-agnostic vanilla JS | None |
1352
1465
  | `./react-router` | React Router error boundary (ErrorBoundaryReactRouter) | `react@^19`, `react-router@^7` |
1353
- | `./fastify` | Fastify server plugin | `fastify@^5 \|\| ^4`, `fastify-plugin@^5 \|\| ^4` |
1466
+ | `./fastify` | Fastify server plugin and BeaconError | `fastify@^5 \|\| ^4`, `fastify-plugin@^5 \|\| ^4` |
1354
1467
  | `./vite-plugin` | Vite build plugin | `vite@^8 \|\| ^7` |
1355
1468
  | `./react-error-boundary` | React error boundary component (ErrorBoundary) | `react@^19` |
1356
1469
  | `./eslint` | ESLint plugin with `configs.recommended` preset (`no-direct-error-boundary`, `no-direct-lazy`) | `eslint@^9 \|\| ^10` (optional) |
@@ -1358,22 +1471,32 @@ spa-guard provides 11 export entry points:
1358
1471
  **Import examples:**
1359
1472
 
1360
1473
  ```typescript
1361
- // Core
1362
- import { events, listen, disableDefaultRetry } from "@ovineko/spa-guard";
1363
-
1364
- // Runtime state + version check
1365
- import { getState, subscribeToState, startVersionCheck } from "@ovineko/spa-guard/runtime";
1474
+ // Core (ForceRetryError and BeaconError also available here)
1475
+ import {
1476
+ events,
1477
+ listen,
1478
+ disableDefaultRetry,
1479
+ ForceRetryError,
1480
+ BeaconError,
1481
+ } from "@ovineko/spa-guard";
1482
+
1483
+ // Runtime state + version check + ForceRetryError
1484
+ import {
1485
+ getState,
1486
+ subscribeToState,
1487
+ startVersionCheck,
1488
+ ForceRetryError,
1489
+ } from "@ovineko/spa-guard/runtime";
1366
1490
 
1367
- // React hooks and components
1491
+ // React hooks, components, and ForceRetryError
1368
1492
  import {
1369
1493
  useSpaGuardState,
1370
1494
  useSPAGuardEvents,
1371
1495
  useSPAGuardChunkError,
1372
1496
  DebugSyncErrorTrigger,
1497
+ ForceRetryError,
1498
+ lazyWithRetry,
1373
1499
  } from "@ovineko/spa-guard/react";
1374
-
1375
- // Lazy imports with retry
1376
- import { lazyWithRetry } from "@ovineko/spa-guard/react";
1377
1500
  import type { LazyRetryOptions } from "@ovineko/spa-guard/react";
1378
1501
 
1379
1502
  // Debug panel (framework-agnostic)
@@ -1389,8 +1512,8 @@ import type { BeaconSchema } from "@ovineko/spa-guard/schema";
1389
1512
  // Vite plugin
1390
1513
  import { spaGuardVitePlugin } from "@ovineko/spa-guard/vite-plugin";
1391
1514
 
1392
- // Fastify
1393
- import { fastifySPAGuard } from "@ovineko/spa-guard/fastify";
1515
+ // Fastify (BeaconError also available here)
1516
+ import { fastifySPAGuard, BeaconError } from "@ovineko/spa-guard/fastify";
1394
1517
 
1395
1518
  // ESLint plugin
1396
1519
  import spaGuardEslint from "@ovineko/spa-guard/eslint";
@@ -1398,8 +1521,8 @@ import spaGuardEslint from "@ovineko/spa-guard/eslint";
1398
1521
 
1399
1522
  ## Build Sizes
1400
1523
 
1401
- - **Production:** `dist-inline/index.js` ~8.3 KB minified (Terser)
1402
- - **Trace:** `dist-inline-trace/index.js` ~13.3 KB minified (Terser)
1524
+ - **Production:** `dist-inline/index.js` ~8.9 KB minified (Terser)
1525
+ - **Trace:** `dist-inline-trace/index.js` ~13.8 KB minified (Terser)
1403
1526
  - **Main library:** `dist/` varies by export
1404
1527
 
1405
1528
  ## Advanced Usage
@@ -1523,10 +1646,106 @@ spaGuardVitePlugin({
1523
1646
  });
1524
1647
  ```
1525
1648
 
1526
- **`errors.ignore`** - Errors containing any of these substrings will not be logged to console and beacons will not be sent. Case-sensitive substring matching.
1649
+ **`errors.ignore`** - Errors containing any of these substrings are fully ignored: no console log, no beacon, no reload, and no further processing. The error handler returns immediately after the ignore check. Case-sensitive substring matching.
1527
1650
 
1528
1651
  **`errors.forceRetry`** - Errors containing any of these substrings will trigger the same retry/reload process as chunk load errors (calls `attemptReload()`). Useful for custom error patterns that indicate a stale deployment. Case-sensitive substring matching.
1529
1652
 
1653
+ ### Configuring Unhandled Rejection Handling
1654
+
1655
+ By default, spa-guard retries **and** sends a beacon for regular unhandled promise rejections (those that are not chunk errors or ForceRetry errors). You can control this behavior with the `handleUnhandledRejections` option:
1656
+
1657
+ ```typescript
1658
+ spaGuardVitePlugin({
1659
+ handleUnhandledRejections: {
1660
+ retry: true, // Attempt page reload (default: true)
1661
+ sendBeacon: true, // Send error report to server (default: true)
1662
+ },
1663
+ });
1664
+ ```
1665
+
1666
+ **Behavior matrix:**
1667
+
1668
+ | `retry` | `sendBeacon` | Behavior |
1669
+ | ------- | ------------ | ------------------------------------------------ |
1670
+ | `true` | `true` | Send beacon first, then attempt reload (default) |
1671
+ | `true` | `false` | Attempt reload only |
1672
+ | `false` | `true` | Send beacon only (pre-v0.0.1-alpha behavior) |
1673
+ | `false` | `false` | Do nothing (only the captured error log remains) |
1674
+
1675
+ **Examples:**
1676
+
1677
+ ```typescript
1678
+ // Restore pre-default behavior: beacon only, no retry
1679
+ spaGuardVitePlugin({
1680
+ handleUnhandledRejections: {
1681
+ retry: false,
1682
+ },
1683
+ });
1684
+
1685
+ // Silent mode: only log, no retry or beacon
1686
+ spaGuardVitePlugin({
1687
+ handleUnhandledRejections: {
1688
+ retry: false,
1689
+ sendBeacon: false,
1690
+ },
1691
+ });
1692
+ ```
1693
+
1694
+ **Note:** Chunk load errors and `ForceRetryError` always bypass this configuration and use their own dedicated handling paths regardless of these settings.
1695
+
1696
+ ### ForceRetryError
1697
+
1698
+ `ForceRetryError` is a typed error class that automatically triggers spa-guard's retry mechanism when thrown, without requiring any `errors.forceRetry` configuration. It works by prepending a magic substring to the error message that spa-guard always recognizes.
1699
+
1700
+ ```typescript
1701
+ import { ForceRetryError } from "@ovineko/spa-guard";
1702
+ // or
1703
+ import { ForceRetryError } from "@ovineko/spa-guard/react";
1704
+ // or
1705
+ import { ForceRetryError } from "@ovineko/spa-guard/runtime";
1706
+
1707
+ // Throw in any error handler to trigger automatic retry
1708
+ throw new ForceRetryError("stale module detected");
1709
+
1710
+ // Works with no message too
1711
+ throw new ForceRetryError();
1712
+
1713
+ // Wrap an original error with { cause } (ES2022 ErrorOptions)
1714
+ try {
1715
+ await initializeAuth();
1716
+ } catch (error) {
1717
+ throw new ForceRetryError("Failed to init auth", { cause: error });
1718
+ }
1719
+
1720
+ // cause-only (no custom message)
1721
+ throw new ForceRetryError(undefined, { cause: originalError });
1722
+ ```
1723
+
1724
+ **Use cases:**
1725
+
1726
+ - Custom module loaders that detect stale deployments
1727
+ - Service workers that need to force a refresh
1728
+ - API responses indicating version mismatch
1729
+
1730
+ **How it works:**
1731
+
1732
+ 1. `ForceRetryError` prepends a magic substring (`__SPA_GUARD_FORCE_RETRY__`) to the error message
1733
+ 2. `shouldForceRetry()` always checks for this substring, regardless of `errors.forceRetry` config
1734
+ 3. When matched, spa-guard triggers the same retry/reload cycle as chunk load errors
1735
+
1736
+ ```typescript
1737
+ const err = new ForceRetryError("version mismatch");
1738
+ err.name; // "ForceRetryError"
1739
+ err.message; // "__SPA_GUARD_FORCE_RETRY__version mismatch"
1740
+ err instanceof ForceRetryError; // true
1741
+ err instanceof Error; // true
1742
+
1743
+ // With cause
1744
+ const original = new TypeError("network timeout");
1745
+ const err2 = new ForceRetryError("version mismatch", { cause: original });
1746
+ err2.cause; // TypeError: network timeout
1747
+ ```
1748
+
1530
1749
  ### Custom Retry Strategy
1531
1750
 
1532
1751
  Adjust retry delays for different environments:
@@ -0,0 +1,38 @@
1
+ // src/common/errors/BeaconError.ts
2
+ var BeaconError = class extends Error {
3
+ appName;
4
+ errorMessage;
5
+ eventMessage;
6
+ eventName;
7
+ retryAttempt;
8
+ retryId;
9
+ serialized;
10
+ constructor(beacon) {
11
+ super(beacon.errorMessage ?? beacon.eventMessage ?? "Unknown beacon error");
12
+ this.name = "BeaconError";
13
+ this.appName = beacon.appName;
14
+ this.errorMessage = beacon.errorMessage;
15
+ this.eventMessage = beacon.eventMessage;
16
+ this.eventName = beacon.eventName;
17
+ this.retryAttempt = beacon.retryAttempt;
18
+ this.retryId = beacon.retryId;
19
+ this.serialized = beacon.serialized;
20
+ }
21
+ toJSON() {
22
+ return {
23
+ appName: this.appName,
24
+ errorMessage: this.errorMessage,
25
+ eventMessage: this.eventMessage,
26
+ eventName: this.eventName,
27
+ message: this.message,
28
+ name: this.name,
29
+ retryAttempt: this.retryAttempt,
30
+ retryId: this.retryId,
31
+ serialized: this.serialized
32
+ };
33
+ }
34
+ };
35
+
36
+ export {
37
+ BeaconError
38
+ };
@@ -5,20 +5,22 @@ import {
5
5
  attemptReload,
6
6
  isChunkError,
7
7
  sendBeacon,
8
- shouldForceRetry
9
- } from "./chunk-6XKIZF5S.js";
10
- import {
11
- getRetryInfoForBeacon
12
- } from "./chunk-T5RWH2HR.js";
8
+ shouldForceRetry,
9
+ shouldIgnoreMessages
10
+ } from "./chunk-PVLEHXOQ.js";
13
11
  import {
14
12
  defaultErrorFallbackHtml,
15
13
  defaultLoadingFallbackHtml
16
- } from "./chunk-DPOQIK4J.js";
14
+ } from "./chunk-UVB6PO4N.js";
15
+ import {
16
+ getRetryInfoForBeacon
17
+ } from "./chunk-T5RWH2HR.js";
17
18
 
18
19
  // src/common/DefaultErrorFallback.tsx
19
20
  import { useLayoutEffect, useMemo, useRef } from "react";
20
21
  import { jsx } from "react/jsx-runtime";
21
22
  var escapeHtml = (str) => str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
23
+ var reloadHandler = () => location.reload();
22
24
  var DefaultErrorFallback = ({
23
25
  error,
24
26
  isChunkError: isChunk,
@@ -52,14 +54,19 @@ var DefaultErrorFallback = ({
52
54
  if (!el) {
53
55
  return;
54
56
  }
55
- if (onReset) {
56
- const tryAgainBtn = el.querySelector('[data-spa-guard-action="try-again"]');
57
- if (tryAgainBtn) {
58
- const handler = () => onReset();
59
- tryAgainBtn.addEventListener("click", handler);
60
- return () => tryAgainBtn.removeEventListener("click", handler);
61
- }
57
+ const reloadBtn = el.querySelector('[data-spa-guard-action="reload"]');
58
+ reloadBtn?.addEventListener("click", reloadHandler);
59
+ const tryAgainHandler = onReset ? () => onReset() : null;
60
+ const tryAgainBtn = onReset ? el.querySelector('[data-spa-guard-action="try-again"]') : null;
61
+ if (tryAgainHandler && tryAgainBtn) {
62
+ tryAgainBtn.addEventListener("click", tryAgainHandler);
62
63
  }
64
+ return () => {
65
+ reloadBtn?.removeEventListener("click", reloadHandler);
66
+ if (tryAgainHandler && tryAgainBtn) {
67
+ tryAgainBtn.removeEventListener("click", tryAgainHandler);
68
+ }
69
+ };
63
70
  }, [onReset, html]);
64
71
  const innerHtml = useMemo(() => ({ __html: html }), [html]);
65
72
  return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: innerHtml, ref: containerRef });
@@ -78,8 +85,11 @@ var handleErrorWithSpaGuard = (error, options) => {
78
85
  onError?.(error);
79
86
  } catch {
80
87
  }
81
- const isChunk = isChunkError(error);
82
88
  const errorMessage = error instanceof Error ? error.message : String(error);
89
+ if (shouldIgnoreMessages([errorMessage])) {
90
+ return;
91
+ }
92
+ const isChunk = isChunkError(error);
83
93
  const isForceRetry = shouldForceRetry([errorMessage]);
84
94
  if ((isChunk || isForceRetry) && autoRetryChunkErrors) {
85
95
  attemptReload(error);
@@ -1,9 +1,9 @@
1
+ import {
2
+ getOptions
3
+ } from "./chunk-UVB6PO4N.js";
1
4
  import {
2
5
  getLogger
3
6
  } from "./chunk-T5RWH2HR.js";
4
- import {
5
- getOptions
6
- } from "./chunk-DPOQIK4J.js";
7
7
 
8
8
  // src/common/checkVersion.ts
9
9
  var versionCheckInterval = null;
@@ -14,7 +14,7 @@ var visibilityHandler = null;
14
14
  var focusHandler = null;
15
15
  var blurHandler = null;
16
16
  var checkInProgress = false;
17
- var stopped = false;
17
+ var runEpoch = 0;
18
18
  var fetchJsonVersion = async () => {
19
19
  const endpoint = getOptions().checkVersion?.endpoint;
20
20
  if (!endpoint) {
@@ -30,7 +30,10 @@ var fetchJsonVersion = async () => {
30
30
  return null;
31
31
  }
32
32
  const data = await response.json();
33
- return data.version ?? null;
33
+ if (typeof data !== "object" || data === null) {
34
+ return null;
35
+ }
36
+ return "version" in data && typeof data.version === "string" ? data.version : null;
34
37
  };
35
38
  var fetchHtmlVersion = async () => {
36
39
  const url = new URL(globalThis.location.href);
@@ -73,9 +76,10 @@ var checkVersionOnce = async (mode) => {
73
76
  return;
74
77
  }
75
78
  checkInProgress = true;
79
+ const epochAtStart = runEpoch;
76
80
  try {
77
81
  const remoteVersion = await fetchRemoteVersion(mode);
78
- if (stopped) {
82
+ if (epochAtStart !== runEpoch) {
79
83
  return;
80
84
  }
81
85
  if (remoteVersion && remoteVersion !== lastKnownVersion) {
@@ -86,7 +90,9 @@ var checkVersionOnce = async (mode) => {
86
90
  } catch (error) {
87
91
  getLogger()?.versionCheckFailed(error);
88
92
  } finally {
89
- checkInProgress = false;
93
+ if (epochAtStart === runEpoch) {
94
+ checkInProgress = false;
95
+ }
90
96
  }
91
97
  };
92
98
  var startPolling = (mode, interval) => {
@@ -144,7 +150,7 @@ var startVersionCheck = () => {
144
150
  getLogger()?.versionCheckAlreadyRunning();
145
151
  return;
146
152
  }
147
- stopped = false;
153
+ runEpoch++;
148
154
  lastKnownVersion = options.version;
149
155
  const interval = options.checkVersion?.interval ?? 3e5;
150
156
  const mode = options.checkVersion?.mode ?? "html";
@@ -176,7 +182,8 @@ var startVersionCheck = () => {
176
182
  globalThis.addEventListener("blur", blurHandler);
177
183
  };
178
184
  var stopVersionCheck = () => {
179
- stopped = true;
185
+ runEpoch++;
186
+ checkInProgress = false;
180
187
  const wasRunning = versionCheckInterval !== null || versionCheckTimeout !== null || visibilityHandler !== null;
181
188
  clearTimers();
182
189
  if (visibilityHandler !== null) {
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  attemptReload,
3
3
  isChunkError
4
- } from "./chunk-6XKIZF5S.js";
4
+ } from "./chunk-PVLEHXOQ.js";
5
5
  import {
6
6
  getState,
7
7
  subscribeToState
8
8
  } from "./chunk-2DFYUVHH.js";
9
+ import {
10
+ getOptions
11
+ } from "./chunk-UVB6PO4N.js";
9
12
  import {
10
13
  emitEvent,
11
14
  isDefaultRetryEnabled,
12
15
  subscribe
13
16
  } from "./chunk-T5RWH2HR.js";
14
- import {
15
- getOptions
16
- } from "./chunk-DPOQIK4J.js";
17
17
  import {
18
18
  debugSyncErrorEventType
19
19
  } from "./chunk-EDRTFPCN.js";
@@ -38,9 +38,7 @@ function DebugSyncErrorTrigger() {
38
38
  };
39
39
  }, []);
40
40
  if (error) {
41
- const err = error;
42
- setError(null);
43
- throw err;
41
+ throw error;
44
42
  }
45
43
  return null;
46
44
  }
@@ -2,6 +2,7 @@
2
2
  import { Type } from "typebox";
3
3
  var beaconSchema = Type.Object(
4
4
  {
5
+ appName: Type.Optional(Type.String()),
5
6
  errorMessage: Type.Optional(Type.String()),
6
7
  eventMessage: Type.Optional(Type.String()),
7
8
  eventName: Type.Optional(Type.String()),
@@ -0,0 +1,13 @@
1
+ // src/common/errors/ForceRetryError.ts
2
+ var FORCE_RETRY_MAGIC = "__SPA_GUARD_FORCE_RETRY__";
3
+ var ForceRetryError = class extends Error {
4
+ constructor(message, options) {
5
+ super(`${FORCE_RETRY_MAGIC}${message ?? ""}`, options);
6
+ this.name = "ForceRetryError";
7
+ }
8
+ };
9
+
10
+ export {
11
+ FORCE_RETRY_MAGIC,
12
+ ForceRetryError
13
+ };