@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 +241 -22
- package/dist/chunk-3SCN2UE4.js +38 -0
- package/dist/{chunk-HVBPX75C.js → chunk-563JZA5T.js} +24 -14
- package/dist/{chunk-FAFKLSME.js → chunk-CMBBYLOH.js} +16 -9
- package/dist/{chunk-UMNFZ7IW.js → chunk-FMEBY5ND.js} +5 -7
- package/dist/{chunk-7HWVSDJZ.js → chunk-KZEBQNOZ.js} +1 -0
- package/dist/chunk-OQGJLNZ2.js +13 -0
- package/dist/{chunk-6XKIZF5S.js → chunk-PVLEHXOQ.js} +17 -10
- package/dist/{chunk-DPOQIK4J.js → chunk-UVB6PO4N.js} +9 -1
- package/dist/{chunk-AB733L4R.js → chunk-WE7SWL5H.js} +1 -1
- package/dist/common/errors/BeaconError.d.ts +12 -0
- package/dist/common/errors/ForceRetryError.d.ts +5 -0
- package/dist/common/fallbackHtml.generated.d.ts +1 -1
- package/dist/common/index.d.ts +2 -0
- package/dist/common/index.js +39 -24
- package/dist/common/options.d.ts +22 -0
- package/dist/fastify/index.d.ts +1 -0
- package/dist/fastify/index.js +15 -20
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +8 -4
- package/dist/react-error-boundary/index.js +6 -5
- package/dist/react-router/index.js +6 -5
- package/dist/runtime/debug/index.js +8 -2
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +6 -2
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/parse.js +2 -2
- package/dist/vite-plugin/index.js +1 -1
- package/dist-inline/index.js +1 -1
- package/dist-inline-trace/index.js +1 -1
- package/package.json +1 -1
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: [], //
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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 {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
|
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.
|
|
1402
|
-
- **Trace:** `dist-inline-trace/index.js` ~13.
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
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-
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
42
|
-
setError(null);
|
|
43
|
-
throw err;
|
|
41
|
+
throw error;
|
|
44
42
|
}
|
|
45
43
|
return null;
|
|
46
44
|
}
|
|
@@ -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
|
+
};
|