@ovineko/spa-guard 0.0.1-alpha-28 → 0.0.1-alpha-29
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 +15 -1
- package/dist/_internal.js +5 -5
- package/dist/{chunk-4EXCHJBE.js → chunk-2BUEJXDB.js} +32 -1
- package/dist/{chunk-62AC5565.js → chunk-3LAQJKWC.js} +7 -4
- package/dist/{chunk-BA5VUNSU.js → chunk-5546JVQV.js} +1 -1
- package/dist/{chunk-5ANASVKX.js → chunk-CBAJOLBG.js} +30 -15
- package/dist/{chunk-PE5QPP5Y.js → chunk-LBHLKYQS.js} +2 -0
- package/dist/{chunk-B7C3LV2W.js → chunk-W3TOHCEW.js} +1 -1
- package/dist/common/constants.d.ts +1 -0
- package/dist/common/fallbackState.d.ts +3 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +9 -4
- package/dist/runtime/debug/index.js +4 -4
- package/dist/runtime/index.js +3 -3
- package/dist/schema/parse.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ const cleanup = recommendedSetup({ versionCheck: false });
|
|
|
36
36
|
- `events` — event type constants
|
|
37
37
|
- `options` — runtime options helpers
|
|
38
38
|
- `disableDefaultRetry` / `enableDefaultRetry` / `isDefaultRetryEnabled` — control default retry behaviour
|
|
39
|
+
- `isInFallbackMode` — returns `true` when fallback UI is active (retries exhausted)
|
|
40
|
+
- `resetFallbackMode` — clears the fallback flag; use in tests or programmatic recovery flows
|
|
39
41
|
- `BeaconError` — error class for beacon failures
|
|
40
42
|
- `ForceRetryError` — error class to force a retry
|
|
41
43
|
|
|
@@ -60,7 +62,19 @@ Built-in translation strings.
|
|
|
60
62
|
|
|
61
63
|
### `@ovineko/spa-guard/runtime/debug`
|
|
62
64
|
|
|
63
|
-
Debug helpers for testing error scenarios.
|
|
65
|
+
Debug helpers for testing error scenarios. Use `createDebugger()` to mount an in-page panel with buttons for each scenario.
|
|
66
|
+
|
|
67
|
+
Available error scenarios:
|
|
68
|
+
|
|
69
|
+
- `dispatchChunkLoadError` — unhandled ChunkLoadError rejection
|
|
70
|
+
- `dispatchNetworkTimeout` — unhandled network timeout after a delay
|
|
71
|
+
- `dispatchSyncRuntimeError` — sync error thrown during React render
|
|
72
|
+
- `dispatchAsyncRuntimeError` — uncaught error via setTimeout
|
|
73
|
+
- `dispatchFinallyError` — unhandled rejection from Promise.finally
|
|
74
|
+
- `dispatchForceRetryError` — ForceRetryError to exercise the force-retry path
|
|
75
|
+
- `dispatchUnhandledRejection` — generic unhandled promise rejection
|
|
76
|
+
- `dispatchRetryExhausted` — simulates retry-exhausted state and renders fallback UI
|
|
77
|
+
- `dispatchStaticAsset404` — appends a hashed `<script>` that 404s, exercising the Resource Timing API-based static asset detection path
|
|
64
78
|
|
|
65
79
|
## Related packages
|
|
66
80
|
|
package/dist/_internal.js
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
isChunkError,
|
|
4
4
|
listenInternal,
|
|
5
5
|
serializeError
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CBAJOLBG.js";
|
|
7
7
|
import {
|
|
8
8
|
SPINNER_ID,
|
|
9
9
|
defaultSpinnerSvg,
|
|
10
10
|
extractVersionFromHtml,
|
|
11
11
|
sanitizeCssValue
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-5546JVQV.js";
|
|
13
13
|
import {
|
|
14
14
|
dispatchAsyncRuntimeError,
|
|
15
15
|
dispatchChunkLoadError,
|
|
@@ -18,13 +18,13 @@ import {
|
|
|
18
18
|
dispatchNetworkTimeout,
|
|
19
19
|
dispatchSyncRuntimeError,
|
|
20
20
|
dispatchUnhandledRejection
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-3LAQJKWC.js";
|
|
22
22
|
import {
|
|
23
23
|
attemptReload,
|
|
24
24
|
sendBeacon,
|
|
25
25
|
shouldForceRetry,
|
|
26
26
|
shouldIgnoreMessages
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-2BUEJXDB.js";
|
|
28
28
|
import {
|
|
29
29
|
applyI18n,
|
|
30
30
|
debugSyncErrorEventType,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
isDefaultRetryEnabled,
|
|
40
40
|
optionsWindowKey,
|
|
41
41
|
subscribe
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-LBHLKYQS.js";
|
|
43
43
|
import "./chunk-MLKGABMK.js";
|
|
44
44
|
|
|
45
45
|
// src/common/handleErrorWithSpaGuard.ts
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
clearRetryAttemptFromUrl,
|
|
9
9
|
clearRetryStateFromUrl,
|
|
10
10
|
emitEvent,
|
|
11
|
+
fallbackModeKey,
|
|
11
12
|
generateRetryId,
|
|
12
13
|
getI18n,
|
|
13
14
|
getLastReloadTime,
|
|
@@ -20,7 +21,27 @@ import {
|
|
|
20
21
|
setLastReloadTime,
|
|
21
22
|
setLastRetryResetInfo,
|
|
22
23
|
shouldResetRetryCycle
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-LBHLKYQS.js";
|
|
25
|
+
|
|
26
|
+
// src/common/fallbackState.ts
|
|
27
|
+
var isInFallbackMode = () => {
|
|
28
|
+
if (globalThis.window === void 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return globalThis.window[fallbackModeKey] === true;
|
|
32
|
+
};
|
|
33
|
+
var setFallbackMode = () => {
|
|
34
|
+
if (globalThis.window === void 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
globalThis.window[fallbackModeKey] = true;
|
|
38
|
+
};
|
|
39
|
+
var resetFallbackMode = () => {
|
|
40
|
+
if (globalThis.window === void 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
globalThis.window[fallbackModeKey] = false;
|
|
44
|
+
};
|
|
24
45
|
|
|
25
46
|
// src/common/shouldIgnore.ts
|
|
26
47
|
var shouldIgnoreMessages = (messages) => {
|
|
@@ -93,6 +114,10 @@ var getReloadState = () => {
|
|
|
93
114
|
return globalThis.window[reloadScheduledKey] ?? (globalThis.window[reloadScheduledKey] = { scheduled: false });
|
|
94
115
|
};
|
|
95
116
|
var attemptReload = (error, opts) => {
|
|
117
|
+
if (isInFallbackMode()) {
|
|
118
|
+
getLogger()?.fallbackAlreadyShown(error);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
96
121
|
const reloadState = getReloadState();
|
|
97
122
|
if (reloadState.scheduled) {
|
|
98
123
|
getLogger()?.reloadAlreadyScheduled(error);
|
|
@@ -253,6 +278,10 @@ var showLoadingUI = (attempt) => {
|
|
|
253
278
|
}
|
|
254
279
|
};
|
|
255
280
|
var showFallbackUI = () => {
|
|
281
|
+
if (isInFallbackMode()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
setFallbackMode();
|
|
256
285
|
const options = getOptions();
|
|
257
286
|
const fallbackHtml = options.html?.fallback?.content;
|
|
258
287
|
const selector = options.html?.fallback?.selector ?? "body";
|
|
@@ -300,6 +329,8 @@ var showFallbackUI = () => {
|
|
|
300
329
|
};
|
|
301
330
|
|
|
302
331
|
export {
|
|
332
|
+
isInFallbackMode,
|
|
333
|
+
resetFallbackMode,
|
|
303
334
|
shouldIgnoreMessages,
|
|
304
335
|
shouldForceRetry,
|
|
305
336
|
sendBeacon,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
showFallbackUI
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2BUEJXDB.js";
|
|
4
4
|
import {
|
|
5
5
|
ForceRetryError,
|
|
6
6
|
debugSyncErrorEventType,
|
|
7
7
|
emitEvent,
|
|
8
8
|
getOptions
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LBHLKYQS.js";
|
|
10
10
|
|
|
11
11
|
// src/runtime/debug/errorDispatchers.ts
|
|
12
12
|
function dispatchAsyncRuntimeError() {
|
|
@@ -36,7 +36,7 @@ function dispatchNetworkTimeout(delayMs = 3e3) {
|
|
|
36
36
|
}
|
|
37
37
|
function dispatchRetryExhausted() {
|
|
38
38
|
const options = getOptions();
|
|
39
|
-
const reloadDelays = options.reloadDelays ?? [
|
|
39
|
+
const reloadDelays = options.reloadDelays ?? [];
|
|
40
40
|
emitEvent({
|
|
41
41
|
finalAttempt: reloadDelays.length,
|
|
42
42
|
name: "retry-exhausted",
|
|
@@ -45,9 +45,12 @@ function dispatchRetryExhausted() {
|
|
|
45
45
|
showFallbackUI();
|
|
46
46
|
}
|
|
47
47
|
function dispatchStaticAsset404() {
|
|
48
|
-
const hash = crypto.randomUUID().
|
|
48
|
+
const hash = crypto.randomUUID().slice(0, 8);
|
|
49
49
|
const script = document.createElement("script");
|
|
50
50
|
script.src = `/assets/index-${hash}.js`;
|
|
51
|
+
script.addEventListener("error", () => {
|
|
52
|
+
script.remove();
|
|
53
|
+
});
|
|
51
54
|
document.head.append(script);
|
|
52
55
|
}
|
|
53
56
|
function dispatchSyncRuntimeError() {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
attemptReload,
|
|
3
|
+
isInFallbackMode,
|
|
3
4
|
sendBeacon,
|
|
4
5
|
shouldForceRetry,
|
|
5
6
|
shouldIgnoreMessages
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2BUEJXDB.js";
|
|
7
8
|
import {
|
|
8
9
|
clearRetryStateFromUrl,
|
|
9
10
|
emitEvent,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
setLogger,
|
|
17
18
|
staticAssetRecoveryKey,
|
|
18
19
|
updateRetryStateInUrl
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-LBHLKYQS.js";
|
|
20
21
|
|
|
21
22
|
// src/common/isChunkError.ts
|
|
22
23
|
var isChunkError = (error) => {
|
|
@@ -182,6 +183,9 @@ var isStaticAssetError = (event) => {
|
|
|
182
183
|
return false;
|
|
183
184
|
};
|
|
184
185
|
var checkResourceStatus = (url) => {
|
|
186
|
+
if (typeof performance === "undefined" || typeof performance.getEntriesByName !== "function") {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
185
189
|
const entries = performance.getEntriesByName(url, "resource");
|
|
186
190
|
if (entries.length === 0) {
|
|
187
191
|
return true;
|
|
@@ -195,11 +199,12 @@ var checkResourceStatus = (url) => {
|
|
|
195
199
|
}
|
|
196
200
|
return false;
|
|
197
201
|
};
|
|
198
|
-
var isLikely404 = (url, timeSinceNavMs
|
|
202
|
+
var isLikely404 = (url, timeSinceNavMs) => {
|
|
199
203
|
if (url !== void 0) {
|
|
200
204
|
return checkResourceStatus(url);
|
|
201
205
|
}
|
|
202
|
-
|
|
206
|
+
const elapsed = timeSinceNavMs ?? (typeof performance === "undefined" ? 0 : performance.now());
|
|
207
|
+
return elapsed > 3e4;
|
|
203
208
|
};
|
|
204
209
|
var getAssetUrl = (event) => {
|
|
205
210
|
const target = event.target;
|
|
@@ -226,6 +231,12 @@ var getState = () => {
|
|
|
226
231
|
return globalThis.window[staticAssetRecoveryKey];
|
|
227
232
|
};
|
|
228
233
|
var handleStaticAssetFailure = (url) => {
|
|
234
|
+
if (globalThis.window === void 0) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (isInFallbackMode()) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
229
240
|
const state = getState();
|
|
230
241
|
state.failedAssets.add(url);
|
|
231
242
|
if (state.recoveryTimer !== null) {
|
|
@@ -235,9 +246,12 @@ var handleStaticAssetFailure = (url) => {
|
|
|
235
246
|
const delay = options.staticAssets?.recoveryDelay ?? 500;
|
|
236
247
|
state.recoveryTimer = setTimeout(() => {
|
|
237
248
|
const s = getState();
|
|
249
|
+
s.recoveryTimer = null;
|
|
238
250
|
const assets = [...s.failedAssets];
|
|
239
251
|
s.failedAssets = /* @__PURE__ */ new Set();
|
|
240
|
-
|
|
252
|
+
if (isInFallbackMode()) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
241
255
|
const error = new Error(`Static asset load failed: ${assets.join(", ")}`);
|
|
242
256
|
attemptReload(error, { cacheBust: true });
|
|
243
257
|
}, delay);
|
|
@@ -260,16 +274,17 @@ var listenInternal = (serializeError2, logger) => {
|
|
|
260
274
|
updateRetryStateInUrl(retryState.retryId, -1);
|
|
261
275
|
}
|
|
262
276
|
if (retryState && (retryState.retryAttempt >= reloadDelays.length || retryState.retryAttempt === -1)) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
()
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
const cleanupSentinel = () => {
|
|
278
|
+
const current = getRetryStateFromUrl();
|
|
279
|
+
if (current?.retryAttempt === -1) {
|
|
280
|
+
clearRetryStateFromUrl();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
if (globalThis.window.document?.readyState === "complete") {
|
|
284
|
+
cleanupSentinel();
|
|
285
|
+
} else {
|
|
286
|
+
globalThis.window.addEventListener("load", cleanupSentinel, { once: true });
|
|
287
|
+
}
|
|
273
288
|
}
|
|
274
289
|
const wa = globalThis.window.addEventListener.bind(globalThis.window);
|
|
275
290
|
wa(
|
|
@@ -20,6 +20,7 @@ var inMemoryLastReloadKey = /* @__PURE__ */ Symbol.for(`${name}:in-memory-last-r
|
|
|
20
20
|
var staticAssetRecoveryKey = /* @__PURE__ */ Symbol.for(`${name}:static-asset-recovery`);
|
|
21
21
|
var debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
22
22
|
var spinnerStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:spinner-state`);
|
|
23
|
+
var fallbackModeKey = /* @__PURE__ */ Symbol.for(`${name}:fallback-mode`);
|
|
23
24
|
|
|
24
25
|
// src/common/events/internal.ts
|
|
25
26
|
if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) {
|
|
@@ -459,6 +460,7 @@ export {
|
|
|
459
460
|
staticAssetRecoveryKey,
|
|
460
461
|
debugSyncErrorEventType,
|
|
461
462
|
spinnerStateWindowKey,
|
|
463
|
+
fallbackModeKey,
|
|
462
464
|
setLogger,
|
|
463
465
|
getLogger,
|
|
464
466
|
emitEvent,
|
|
@@ -12,3 +12,4 @@ export declare const inMemoryLastReloadKey: unique symbol;
|
|
|
12
12
|
export declare const staticAssetRecoveryKey: unique symbol;
|
|
13
13
|
export declare const debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
14
14
|
export declare const spinnerStateWindowKey: unique symbol;
|
|
15
|
+
export declare const fallbackModeKey: unique symbol;
|
package/dist/common/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { BeaconError } from "./errors/BeaconError";
|
|
|
2
2
|
export { ForceRetryError } from "./errors/ForceRetryError";
|
|
3
3
|
export * as events from "./events";
|
|
4
4
|
export { disableDefaultRetry, enableDefaultRetry, isDefaultRetryEnabled } from "./events/internal";
|
|
5
|
+
export { isInFallbackMode, resetFallbackMode } from "./fallbackState";
|
|
5
6
|
export { listen } from "./listen";
|
|
6
7
|
export * as options from "./options";
|
package/dist/common/index.js
CHANGED
|
@@ -2,8 +2,11 @@ import {
|
|
|
2
2
|
createLogger,
|
|
3
3
|
listenInternal,
|
|
4
4
|
serializeError
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import
|
|
5
|
+
} from "../chunk-CBAJOLBG.js";
|
|
6
|
+
import {
|
|
7
|
+
isInFallbackMode,
|
|
8
|
+
resetFallbackMode
|
|
9
|
+
} from "../chunk-2BUEJXDB.js";
|
|
7
10
|
import {
|
|
8
11
|
ForceRetryError,
|
|
9
12
|
disableDefaultRetry,
|
|
@@ -12,7 +15,7 @@ import {
|
|
|
12
15
|
isDefaultRetryEnabled,
|
|
13
16
|
options_exports,
|
|
14
17
|
subscribe
|
|
15
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-LBHLKYQS.js";
|
|
16
19
|
import {
|
|
17
20
|
__export
|
|
18
21
|
} from "../chunk-MLKGABMK.js";
|
|
@@ -73,6 +76,8 @@ export {
|
|
|
73
76
|
enableDefaultRetry,
|
|
74
77
|
events_exports as events,
|
|
75
78
|
isDefaultRetryEnabled,
|
|
79
|
+
isInFallbackMode,
|
|
76
80
|
listen,
|
|
77
|
-
options_exports as options
|
|
81
|
+
options_exports as options,
|
|
82
|
+
resetFallbackMode
|
|
78
83
|
};
|
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
dispatchStaticAsset404,
|
|
9
9
|
dispatchSyncRuntimeError,
|
|
10
10
|
dispatchUnhandledRejection
|
|
11
|
-
} from "../../chunk-
|
|
12
|
-
import "../../chunk-
|
|
11
|
+
} from "../../chunk-3LAQJKWC.js";
|
|
12
|
+
import "../../chunk-2BUEJXDB.js";
|
|
13
13
|
import {
|
|
14
14
|
subscribeToState
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-W3TOHCEW.js";
|
|
16
16
|
import {
|
|
17
17
|
subscribe
|
|
18
|
-
} from "../../chunk-
|
|
18
|
+
} from "../../chunk-LBHLKYQS.js";
|
|
19
19
|
import "../../chunk-MLKGABMK.js";
|
|
20
20
|
|
|
21
21
|
// src/runtime/debug/index.ts
|
package/dist/runtime/index.js
CHANGED
|
@@ -3,18 +3,18 @@ import {
|
|
|
3
3
|
extractVersionFromHtml,
|
|
4
4
|
getSpinnerHtml,
|
|
5
5
|
showSpinner
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-5546JVQV.js";
|
|
7
7
|
import {
|
|
8
8
|
getState,
|
|
9
9
|
subscribeToState
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-W3TOHCEW.js";
|
|
11
11
|
import {
|
|
12
12
|
ForceRetryError,
|
|
13
13
|
getLogger,
|
|
14
14
|
getOptions,
|
|
15
15
|
setTranslations,
|
|
16
16
|
versionCheckStateWindowKey
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-LBHLKYQS.js";
|
|
18
18
|
import "../chunk-MLKGABMK.js";
|
|
19
19
|
|
|
20
20
|
// src/common/checkVersion.ts
|
package/dist/schema/parse.js
CHANGED
|
@@ -12,6 +12,8 @@ var STRING_FIELDS = [
|
|
|
12
12
|
"serialized",
|
|
13
13
|
"url"
|
|
14
14
|
];
|
|
15
|
+
var MAX_STRING_FIELD_LENGTH = 500;
|
|
16
|
+
var MAX_SERIALIZED_LENGTH = 1e4;
|
|
15
17
|
function parseBeacon(data) {
|
|
16
18
|
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
17
19
|
throw new Error("Invalid beacon");
|
|
@@ -23,13 +25,17 @@ function parseBeacon(data) {
|
|
|
23
25
|
if (typeof d[field] !== "string") {
|
|
24
26
|
throw new TypeError(`Beacon validation failed: ${field} must be a string`);
|
|
25
27
|
}
|
|
28
|
+
const maxLen = field === "serialized" ? MAX_SERIALIZED_LENGTH : MAX_STRING_FIELD_LENGTH;
|
|
29
|
+
if (d[field].length > maxLen) {
|
|
30
|
+
throw new TypeError(`Beacon validation failed: ${field} exceeds maximum length`);
|
|
31
|
+
}
|
|
26
32
|
result[field] = d[field];
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
35
|
for (const field of ["retryAttempt", "httpStatus"]) {
|
|
30
36
|
if (field in d) {
|
|
31
|
-
if (typeof d[field] !== "number") {
|
|
32
|
-
throw new TypeError(`Beacon validation failed: ${field} must be a number`);
|
|
37
|
+
if (typeof d[field] !== "number" || !Number.isFinite(d[field])) {
|
|
38
|
+
throw new TypeError(`Beacon validation failed: ${field} must be a finite number`);
|
|
33
39
|
}
|
|
34
40
|
result[field] = d[field];
|
|
35
41
|
}
|