@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.
- package/dist/_internal.d.ts +1 -1
- package/dist/_internal.js +11 -7
- package/dist/{chunk-KAH6PVTJ.js → chunk-4EXCHJBE.js} +3 -2
- package/dist/{chunk-FRCJOMRP.js → chunk-5ANASVKX.js} +69 -23
- package/dist/{chunk-7KNE2UWX.js → chunk-62AC5565.js} +9 -2
- package/dist/{chunk-V2OOMVZK.js → chunk-B7C3LV2W.js} +1 -1
- package/dist/{chunk-7PDOUY6W.js → chunk-BA5VUNSU.js} +23 -9
- package/dist/{chunk-67WBMNUS.js → chunk-PE5QPP5Y.js} +9 -2
- package/dist/common/constants.d.ts +3 -0
- package/dist/common/index.js +3 -3
- package/dist/common/isStaticAssetError.d.ts +1 -1
- package/dist/common/spinner.d.ts +1 -0
- package/dist/runtime/debug/errorDispatchers.d.ts +7 -0
- package/dist/runtime/debug/index.js +7 -5
- package/dist/runtime/index.js +50 -30
- package/dist/schema/parse.js +10 -5
- package/package.json +1 -1
package/dist/_internal.d.ts
CHANGED
|
@@ -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-
|
|
6
|
+
} from "./chunk-5ANASVKX.js";
|
|
7
7
|
import {
|
|
8
8
|
SPINNER_ID,
|
|
9
9
|
defaultSpinnerSvg,
|
|
10
|
-
extractVersionFromHtml
|
|
11
|
-
|
|
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-
|
|
21
|
+
} from "./chunk-62AC5565.js";
|
|
21
22
|
import {
|
|
22
23
|
attemptReload,
|
|
23
24
|
sendBeacon,
|
|
24
25
|
shouldForceRetry,
|
|
25
26
|
shouldIgnoreMessages
|
|
26
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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(
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
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-
|
|
3
|
+
} from "./chunk-4EXCHJBE.js";
|
|
4
4
|
import {
|
|
5
5
|
ForceRetryError,
|
|
6
6
|
debugSyncErrorEventType,
|
|
7
7
|
emitEvent,
|
|
8
8
|
getOptions
|
|
9
|
-
} from "./chunk-
|
|
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
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defaultSpinnerHtml,
|
|
3
|
-
getOptions
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
package/dist/common/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
createLogger,
|
|
3
3
|
listenInternal,
|
|
4
4
|
serializeError
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
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-
|
|
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;
|
package/dist/common/spinner.d.ts
CHANGED
|
@@ -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-
|
|
11
|
-
import "../../chunk-
|
|
11
|
+
} from "../../chunk-62AC5565.js";
|
|
12
|
+
import "../../chunk-4EXCHJBE.js";
|
|
12
13
|
import {
|
|
13
14
|
subscribeToState
|
|
14
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-B7C3LV2W.js";
|
|
15
16
|
import {
|
|
16
17
|
subscribe
|
|
17
|
-
} from "../../chunk-
|
|
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;",
|
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-BA5VUNSU.js";
|
|
7
7
|
import {
|
|
8
8
|
getState,
|
|
9
9
|
subscribeToState
|
|
10
|
-
} from "../chunk-
|
|
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-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
264
|
+
if (options.versionCheck) {
|
|
265
|
+
stopVersionCheck();
|
|
266
|
+
}
|
|
247
267
|
};
|
|
248
268
|
};
|
|
249
269
|
export {
|
package/dist/schema/parse.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
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
|
}
|