@ovineko/spa-guard 0.0.1-alpha-26 → 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 +15 -8
- package/dist/{chunk-CSN7MQGX.js → chunk-4EXCHJBE.js} +30 -17
- package/dist/{chunk-ZVYB2746.js → chunk-5ANASVKX.js} +126 -8
- package/dist/{chunk-XFILAQ7P.js → chunk-62AC5565.js} +9 -2
- package/dist/{chunk-T72DERME.js → chunk-B7C3LV2W.js} +1 -1
- package/dist/{chunk-Z75UPJWV.js → chunk-BA5VUNSU.js} +26 -12
- package/dist/{chunk-X6E7KUDK.js → chunk-PE5QPP5Y.js} +56 -19
- package/dist/common/constants.d.ts +5 -0
- package/dist/common/events/types.d.ts +22 -0
- package/dist/common/index.js +3 -3
- package/dist/common/isStaticAssetError.d.ts +3 -0
- package/dist/common/options.d.ts +35 -17
- package/dist/common/reload.d.ts +3 -1
- package/dist/common/spinner.d.ts +1 -0
- package/dist/common/staticAssetRecovery.d.ts +2 -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/index.d.ts +4 -0
- 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
|
|
@@ -95,6 +96,10 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
95
96
|
const { callReloadOnFailure, onRetry, signal } = options ?? {};
|
|
96
97
|
let lastError = new Error("Import failed after all retry attempts");
|
|
97
98
|
const totalAttempts = delays.length + 1;
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
if (delays.length > 0) {
|
|
101
|
+
emitEvent({ name: "lazy-retry-start", totalAttempts });
|
|
102
|
+
}
|
|
98
103
|
for (let attempt = 0; attempt < totalAttempts; attempt++) {
|
|
99
104
|
if (signal?.aborted) {
|
|
100
105
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
@@ -102,7 +107,7 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
102
107
|
try {
|
|
103
108
|
const result = await importFn();
|
|
104
109
|
if (attempt > 0) {
|
|
105
|
-
emitEvent({ attempt, name: "lazy-retry-success" });
|
|
110
|
+
emitEvent({ attempt, name: "lazy-retry-success", totalTime: Date.now() - startTime });
|
|
106
111
|
}
|
|
107
112
|
return result;
|
|
108
113
|
} catch (error) {
|
|
@@ -115,6 +120,7 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
115
120
|
emitEvent({
|
|
116
121
|
attempt: attempt + 1,
|
|
117
122
|
delay: currentDelay,
|
|
123
|
+
error: lastError,
|
|
118
124
|
name: "lazy-retry-attempt",
|
|
119
125
|
totalAttempts
|
|
120
126
|
});
|
|
@@ -122,7 +128,7 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
130
|
const willReload = callReloadOnFailure === true && isChunkError(lastError) && isDefaultRetryEnabled();
|
|
125
|
-
emitEvent({ name: "lazy-retry-exhausted", totalAttempts, willReload });
|
|
131
|
+
emitEvent({ error: lastError, name: "lazy-retry-exhausted", totalAttempts, willReload });
|
|
126
132
|
if (willReload) {
|
|
127
133
|
attemptReload(lastError);
|
|
128
134
|
}
|
|
@@ -157,6 +163,7 @@ export {
|
|
|
157
163
|
logMessage,
|
|
158
164
|
optionsWindowKey,
|
|
159
165
|
retryImport,
|
|
166
|
+
sanitizeCssValue,
|
|
160
167
|
serializeError,
|
|
161
168
|
subscribe
|
|
162
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,
|
|
@@ -15,10 +16,11 @@ import {
|
|
|
15
16
|
getRetryAttemptFromUrl,
|
|
16
17
|
getRetryStateFromUrl,
|
|
17
18
|
isDefaultRetryEnabled,
|
|
19
|
+
reloadScheduledKey,
|
|
18
20
|
setLastReloadTime,
|
|
19
21
|
setLastRetryResetInfo,
|
|
20
22
|
shouldResetRetryCycle
|
|
21
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-PE5QPP5Y.js";
|
|
22
24
|
|
|
23
25
|
// src/common/shouldIgnore.ts
|
|
24
26
|
var shouldIgnoreMessages = (messages) => {
|
|
@@ -81,13 +83,22 @@ var buildReloadUrlAttemptOnly = (retryAttempt) => {
|
|
|
81
83
|
url.searchParams.set(RETRY_ATTEMPT_PARAM, String(retryAttempt));
|
|
82
84
|
return url.toString();
|
|
83
85
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
if (globalThis.window && !globalThis.window[reloadScheduledKey]) {
|
|
87
|
+
globalThis.window[reloadScheduledKey] = { scheduled: false };
|
|
88
|
+
}
|
|
89
|
+
var getReloadState = () => {
|
|
90
|
+
if (globalThis.window === void 0) {
|
|
91
|
+
return { scheduled: false };
|
|
92
|
+
}
|
|
93
|
+
return globalThis.window[reloadScheduledKey] ?? (globalThis.window[reloadScheduledKey] = { scheduled: false });
|
|
94
|
+
};
|
|
95
|
+
var attemptReload = (error, opts) => {
|
|
96
|
+
const reloadState = getReloadState();
|
|
97
|
+
if (reloadState.scheduled) {
|
|
87
98
|
getLogger()?.reloadAlreadyScheduled(error);
|
|
88
99
|
return;
|
|
89
100
|
}
|
|
90
|
-
|
|
101
|
+
reloadState.scheduled = true;
|
|
91
102
|
try {
|
|
92
103
|
const options = getOptions();
|
|
93
104
|
const reloadDelays = options.reloadDelays ?? [1e3, 2e3, 5e3];
|
|
@@ -111,7 +122,7 @@ var attemptReload = (error) => {
|
|
|
111
122
|
name: "chunk-error"
|
|
112
123
|
});
|
|
113
124
|
if (!retryEnabled) {
|
|
114
|
-
|
|
125
|
+
reloadState.scheduled = false;
|
|
115
126
|
return;
|
|
116
127
|
}
|
|
117
128
|
if (enableRetryReset && retryState && retryState.retryAttempt > 0 && shouldResetRetryCycle(retryState, reloadDelays, minTimeBetweenResets)) {
|
|
@@ -138,7 +149,7 @@ var attemptReload = (error) => {
|
|
|
138
149
|
if (!shouldIgnoreMessages([errorMsg2])) {
|
|
139
150
|
getLogger()?.fallbackAlreadyShown(error);
|
|
140
151
|
}
|
|
141
|
-
|
|
152
|
+
reloadState.scheduled = false;
|
|
142
153
|
showFallbackUI();
|
|
143
154
|
return;
|
|
144
155
|
}
|
|
@@ -166,7 +177,7 @@ var attemptReload = (error) => {
|
|
|
166
177
|
if (!useRetryId) {
|
|
167
178
|
clearRetryAttemptFromUrl();
|
|
168
179
|
}
|
|
169
|
-
|
|
180
|
+
reloadState.scheduled = false;
|
|
170
181
|
showFallbackUI();
|
|
171
182
|
return;
|
|
172
183
|
}
|
|
@@ -188,15 +199,17 @@ var attemptReload = (error) => {
|
|
|
188
199
|
if (useRetryId && enableRetryReset) {
|
|
189
200
|
setLastReloadTime(retryId, nextAttempt);
|
|
190
201
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
let reloadUrl;
|
|
203
|
+
reloadUrl = useRetryId ? buildReloadUrl(retryId, nextAttempt) : buildReloadUrlAttemptOnly(nextAttempt);
|
|
204
|
+
if (opts?.cacheBust) {
|
|
205
|
+
const url = new URL(reloadUrl);
|
|
206
|
+
url.searchParams.set(CACHE_BUST_PARAM, String(Date.now()));
|
|
207
|
+
reloadUrl = url.toString();
|
|
196
208
|
}
|
|
209
|
+
globalThis.window.location.href = reloadUrl;
|
|
197
210
|
}, delay);
|
|
198
211
|
} catch {
|
|
199
|
-
|
|
212
|
+
reloadState.scheduled = false;
|
|
200
213
|
}
|
|
201
214
|
};
|
|
202
215
|
var showLoadingUI = (attempt) => {
|
|
@@ -215,10 +228,10 @@ var showLoadingUI = (attempt) => {
|
|
|
215
228
|
container.innerHTML = loadingHtml;
|
|
216
229
|
const spinnerEl = container.querySelector("[data-spa-guard-spinner]");
|
|
217
230
|
if (spinnerEl) {
|
|
218
|
-
if (options.spinner?.disabled) {
|
|
231
|
+
if (options.html?.spinner?.disabled) {
|
|
219
232
|
spinnerEl.remove();
|
|
220
|
-
} else if (options.spinner?.content) {
|
|
221
|
-
spinnerEl.innerHTML = options.spinner.content;
|
|
233
|
+
} else if (options.html?.spinner?.content) {
|
|
234
|
+
spinnerEl.innerHTML = options.html.spinner.content;
|
|
222
235
|
}
|
|
223
236
|
}
|
|
224
237
|
const retryingSection = container.querySelector(
|
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
sendBeacon,
|
|
4
4
|
shouldForceRetry,
|
|
5
5
|
shouldIgnoreMessages
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-4EXCHJBE.js";
|
|
7
7
|
import {
|
|
8
|
+
clearRetryStateFromUrl,
|
|
9
|
+
emitEvent,
|
|
8
10
|
getLogger,
|
|
9
11
|
getOptions,
|
|
10
12
|
getRetryInfoForBeacon,
|
|
@@ -12,8 +14,9 @@ import {
|
|
|
12
14
|
isInitialized,
|
|
13
15
|
markInitialized,
|
|
14
16
|
setLogger,
|
|
17
|
+
staticAssetRecoveryKey,
|
|
15
18
|
updateRetryStateInUrl
|
|
16
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-PE5QPP5Y.js";
|
|
17
20
|
|
|
18
21
|
// src/common/isChunkError.ts
|
|
19
22
|
var isChunkError = (error) => {
|
|
@@ -158,14 +161,96 @@ var extractOwnProperties = (obj) => {
|
|
|
158
161
|
return props;
|
|
159
162
|
};
|
|
160
163
|
|
|
164
|
+
// src/common/isStaticAssetError.ts
|
|
165
|
+
var HASHED_ASSET_RE = /[-._][a-zA-Z0-9]{6,}\.(js|mjs|css)$/i;
|
|
166
|
+
var isHashedAssetUrl = (url) => {
|
|
167
|
+
try {
|
|
168
|
+
const pathname = new URL(url).pathname;
|
|
169
|
+
return HASHED_ASSET_RE.test(pathname);
|
|
170
|
+
} catch {
|
|
171
|
+
return HASHED_ASSET_RE.test(url);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var isStaticAssetError = (event) => {
|
|
175
|
+
const target = event.target;
|
|
176
|
+
if (target instanceof HTMLScriptElement) {
|
|
177
|
+
return isHashedAssetUrl(target.src);
|
|
178
|
+
}
|
|
179
|
+
if (target instanceof HTMLLinkElement) {
|
|
180
|
+
return isHashedAssetUrl(target.href);
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
};
|
|
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
|
+
}
|
|
202
|
+
return timeSinceNavMs > 3e4;
|
|
203
|
+
};
|
|
204
|
+
var getAssetUrl = (event) => {
|
|
205
|
+
const target = event.target;
|
|
206
|
+
if (target instanceof HTMLScriptElement) {
|
|
207
|
+
return target.src;
|
|
208
|
+
}
|
|
209
|
+
if (target instanceof HTMLLinkElement) {
|
|
210
|
+
return target.href;
|
|
211
|
+
}
|
|
212
|
+
return "";
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/common/staticAssetRecovery.ts
|
|
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
|
+
};
|
|
228
|
+
var handleStaticAssetFailure = (url) => {
|
|
229
|
+
const state = getState();
|
|
230
|
+
state.failedAssets.add(url);
|
|
231
|
+
if (state.recoveryTimer !== null) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const options = getOptions();
|
|
235
|
+
const delay = options.staticAssets?.recoveryDelay ?? 500;
|
|
236
|
+
state.recoveryTimer = setTimeout(() => {
|
|
237
|
+
const s = getState();
|
|
238
|
+
const assets = [...s.failedAssets];
|
|
239
|
+
s.failedAssets = /* @__PURE__ */ new Set();
|
|
240
|
+
s.recoveryTimer = null;
|
|
241
|
+
const error = new Error(`Static asset load failed: ${assets.join(", ")}`);
|
|
242
|
+
attemptReload(error, { cacheBust: true });
|
|
243
|
+
}, delay);
|
|
244
|
+
};
|
|
245
|
+
|
|
161
246
|
// src/common/listen/internal.ts
|
|
162
247
|
var listenInternal = (serializeError2, logger) => {
|
|
163
|
-
if (logger) {
|
|
164
|
-
setLogger(logger);
|
|
165
|
-
}
|
|
166
248
|
if (isInitialized()) {
|
|
167
249
|
return;
|
|
168
250
|
}
|
|
251
|
+
if (logger) {
|
|
252
|
+
setLogger(logger);
|
|
253
|
+
}
|
|
169
254
|
markInitialized();
|
|
170
255
|
const options = getOptions();
|
|
171
256
|
const reloadDelays = options.reloadDelays ?? [];
|
|
@@ -174,10 +259,34 @@ var listenInternal = (serializeError2, logger) => {
|
|
|
174
259
|
getLogger()?.retryLimitExceeded(retryState.retryAttempt, reloadDelays.length);
|
|
175
260
|
updateRetryStateInUrl(retryState.retryId, -1);
|
|
176
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
|
+
}
|
|
177
274
|
const wa = globalThis.window.addEventListener.bind(globalThis.window);
|
|
178
275
|
wa(
|
|
179
276
|
"error",
|
|
180
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
|
+
}
|
|
181
290
|
if (shouldIgnoreMessages([event.message])) {
|
|
182
291
|
return;
|
|
183
292
|
}
|
|
@@ -265,10 +374,12 @@ var eventLogConfig = {
|
|
|
265
374
|
"fallback-ui-shown": "warn",
|
|
266
375
|
"lazy-retry-attempt": "warn",
|
|
267
376
|
"lazy-retry-exhausted": "error",
|
|
377
|
+
"lazy-retry-start": "log",
|
|
268
378
|
"lazy-retry-success": "log",
|
|
269
379
|
"retry-attempt": "warn",
|
|
270
380
|
"retry-exhausted": "error",
|
|
271
|
-
"retry-reset": "log"
|
|
381
|
+
"retry-reset": "log",
|
|
382
|
+
"static-asset-load-failed": "error"
|
|
272
383
|
};
|
|
273
384
|
var formatEvent = (event) => {
|
|
274
385
|
switch (event.name) {
|
|
@@ -284,8 +395,12 @@ var formatEvent = (event) => {
|
|
|
284
395
|
case "lazy-retry-exhausted": {
|
|
285
396
|
return `${PREFIX} lazy-retry-exhausted: ${event.totalAttempts} attempts, willReload=${event.willReload}`;
|
|
286
397
|
}
|
|
398
|
+
case "lazy-retry-start": {
|
|
399
|
+
return `${PREFIX} lazy-retry-start: totalAttempts=${event.totalAttempts}`;
|
|
400
|
+
}
|
|
287
401
|
case "lazy-retry-success": {
|
|
288
|
-
|
|
402
|
+
const timePart = event.totalTime === void 0 ? "" : `, totalTime=${event.totalTime}ms`;
|
|
403
|
+
return `${PREFIX} lazy-retry-success: succeeded on attempt ${event.attempt}${timePart}`;
|
|
289
404
|
}
|
|
290
405
|
case "retry-attempt": {
|
|
291
406
|
return `${PREFIX} retry-attempt: attempt ${event.attempt} in ${event.delay}ms (retryId: ${event.retryId})`;
|
|
@@ -296,6 +411,9 @@ var formatEvent = (event) => {
|
|
|
296
411
|
case "retry-reset": {
|
|
297
412
|
return `${PREFIX} retry-reset: ${event.timeSinceReload}ms since last reload (retryId: ${event.previousRetryId})`;
|
|
298
413
|
}
|
|
414
|
+
case "static-asset-load-failed": {
|
|
415
|
+
return `${PREFIX} static-asset-load-failed: ${event.url}`;
|
|
416
|
+
}
|
|
299
417
|
}
|
|
300
418
|
};
|
|
301
419
|
var createLogger = () => ({
|
|
@@ -329,7 +447,7 @@ var createLogger = () => ({
|
|
|
329
447
|
logEvent(event) {
|
|
330
448
|
const level = eventLogConfig[event.name];
|
|
331
449
|
const message = formatEvent(event);
|
|
332
|
-
if (event.name === "chunk-error") {
|
|
450
|
+
if (event.name === "chunk-error" || event.name === "lazy-retry-attempt" || event.name === "lazy-retry-exhausted") {
|
|
333
451
|
console[level](message, event.error);
|
|
334
452
|
} else {
|
|
335
453
|
console[level](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
|
-
if (opts.spinner?.disabled) {
|
|
53
|
+
if (opts.html?.spinner?.disabled) {
|
|
41
54
|
return "";
|
|
42
55
|
}
|
|
43
|
-
const spinnerContent = opts.spinner?.content ?? defaultSpinnerSvg;
|
|
44
|
-
const bg = backgroundOverride ?? opts.spinner?.background ?? "#fff";
|
|
56
|
+
const spinnerContent = opts.html?.spinner?.content ?? defaultSpinnerSvg;
|
|
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) {
|
|
@@ -50,7 +63,7 @@ function showSpinner(options) {
|
|
|
50
63
|
};
|
|
51
64
|
}
|
|
52
65
|
const opts = getOptions();
|
|
53
|
-
if (opts.spinner?.disabled) {
|
|
66
|
+
if (opts.html?.spinner?.disabled) {
|
|
54
67
|
return () => {
|
|
55
68
|
};
|
|
56
69
|
}
|
|
@@ -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,8 +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`);
|
|
18
|
+
var reloadScheduledKey = /* @__PURE__ */ Symbol.for(`${name}:reload-scheduled`);
|
|
19
|
+
var inMemoryLastReloadKey = /* @__PURE__ */ Symbol.for(`${name}:in-memory-last-reload`);
|
|
20
|
+
var staticAssetRecoveryKey = /* @__PURE__ */ Symbol.for(`${name}:static-asset-recovery`);
|
|
17
21
|
var debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
22
|
+
var spinnerStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:spinner-state`);
|
|
18
23
|
|
|
19
24
|
// src/common/events/internal.ts
|
|
20
25
|
if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) {
|
|
@@ -163,6 +168,10 @@ var defaultOptions = {
|
|
|
163
168
|
},
|
|
164
169
|
loading: {
|
|
165
170
|
content: defaultLoadingFallbackHtml
|
|
171
|
+
},
|
|
172
|
+
spinner: {
|
|
173
|
+
background: "#fff",
|
|
174
|
+
disabled: false
|
|
166
175
|
}
|
|
167
176
|
},
|
|
168
177
|
lazyRetry: {
|
|
@@ -171,9 +180,9 @@ var defaultOptions = {
|
|
|
171
180
|
},
|
|
172
181
|
minTimeBetweenResets: 5e3,
|
|
173
182
|
reloadDelays: [1e3, 2e3, 5e3],
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
staticAssets: {
|
|
184
|
+
autoRecover: true,
|
|
185
|
+
recoveryDelay: 500
|
|
177
186
|
},
|
|
178
187
|
useRetryId: true
|
|
179
188
|
};
|
|
@@ -202,6 +211,10 @@ var getOptions = () => {
|
|
|
202
211
|
loading: {
|
|
203
212
|
...defaultOptions.html?.loading,
|
|
204
213
|
...windowOptions?.html?.loading
|
|
214
|
+
},
|
|
215
|
+
spinner: {
|
|
216
|
+
...defaultOptions.html?.spinner,
|
|
217
|
+
...windowOptions?.html?.spinner
|
|
205
218
|
}
|
|
206
219
|
},
|
|
207
220
|
lazyRetry: {
|
|
@@ -212,9 +225,9 @@ var getOptions = () => {
|
|
|
212
225
|
...defaultOptions.reportBeacon,
|
|
213
226
|
...windowOptions?.reportBeacon
|
|
214
227
|
},
|
|
215
|
-
|
|
216
|
-
...defaultOptions.
|
|
217
|
-
...windowOptions?.
|
|
228
|
+
staticAssets: {
|
|
229
|
+
...defaultOptions.staticAssets,
|
|
230
|
+
...windowOptions?.staticAssets
|
|
218
231
|
}
|
|
219
232
|
};
|
|
220
233
|
};
|
|
@@ -231,8 +244,19 @@ var ForceRetryError = class extends Error {
|
|
|
231
244
|
// src/common/lastReloadTime.ts
|
|
232
245
|
var STORAGE_KEY = "__spa_guard_last_reload_timestamp__";
|
|
233
246
|
var RESET_INFO_KEY = "__spa_guard_last_retry_reset__";
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
if (globalThis.window && !globalThis.window[inMemoryLastReloadKey]) {
|
|
248
|
+
globalThis.window[inMemoryLastReloadKey] = {
|
|
249
|
+
resetInfo: null,
|
|
250
|
+
storage: null
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
var getInMemoryState = () => {
|
|
254
|
+
const w = globalThis.window;
|
|
255
|
+
if (!w) {
|
|
256
|
+
return { resetInfo: null, storage: null };
|
|
257
|
+
}
|
|
258
|
+
return w[inMemoryLastReloadKey] ?? (w[inMemoryLastReloadKey] = { resetInfo: null, storage: null });
|
|
259
|
+
};
|
|
236
260
|
var hasSessionStorage = () => {
|
|
237
261
|
try {
|
|
238
262
|
return globalThis.window !== void 0 && typeof sessionStorage !== "undefined";
|
|
@@ -250,10 +274,10 @@ var setLastReloadTime = (retryId, attemptNumber) => {
|
|
|
250
274
|
try {
|
|
251
275
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
252
276
|
} catch {
|
|
253
|
-
|
|
277
|
+
getInMemoryState().storage = data;
|
|
254
278
|
}
|
|
255
279
|
} else {
|
|
256
|
-
|
|
280
|
+
getInMemoryState().storage = data;
|
|
257
281
|
}
|
|
258
282
|
};
|
|
259
283
|
var getLastReloadTime = () => {
|
|
@@ -264,10 +288,14 @@ var getLastReloadTime = () => {
|
|
|
264
288
|
return JSON.parse(stored);
|
|
265
289
|
}
|
|
266
290
|
} catch {
|
|
267
|
-
|
|
291
|
+
try {
|
|
292
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
return getInMemoryState().storage;
|
|
268
296
|
}
|
|
269
297
|
}
|
|
270
|
-
return
|
|
298
|
+
return getInMemoryState().storage;
|
|
271
299
|
};
|
|
272
300
|
var clearLastReloadTime = () => {
|
|
273
301
|
if (hasSessionStorage()) {
|
|
@@ -276,7 +304,7 @@ var clearLastReloadTime = () => {
|
|
|
276
304
|
} catch {
|
|
277
305
|
}
|
|
278
306
|
}
|
|
279
|
-
|
|
307
|
+
getInMemoryState().storage = null;
|
|
280
308
|
};
|
|
281
309
|
var shouldResetRetryCycle = (retryState, reloadDelays, minTimeBetweenResets = 5e3) => {
|
|
282
310
|
if (retryState.retryAttempt === 0) {
|
|
@@ -311,10 +339,10 @@ var setLastRetryResetInfo = (previousRetryId) => {
|
|
|
311
339
|
try {
|
|
312
340
|
sessionStorage.setItem(RESET_INFO_KEY, JSON.stringify(data));
|
|
313
341
|
} catch {
|
|
314
|
-
|
|
342
|
+
getInMemoryState().resetInfo = data;
|
|
315
343
|
}
|
|
316
344
|
} else {
|
|
317
|
-
|
|
345
|
+
getInMemoryState().resetInfo = data;
|
|
318
346
|
}
|
|
319
347
|
};
|
|
320
348
|
var getLastRetryResetInfo = () => {
|
|
@@ -325,10 +353,14 @@ var getLastRetryResetInfo = () => {
|
|
|
325
353
|
return JSON.parse(stored);
|
|
326
354
|
}
|
|
327
355
|
} catch {
|
|
328
|
-
|
|
356
|
+
try {
|
|
357
|
+
sessionStorage.removeItem(RESET_INFO_KEY);
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
return getInMemoryState().resetInfo;
|
|
329
361
|
}
|
|
330
362
|
}
|
|
331
|
-
return
|
|
363
|
+
return getInMemoryState().resetInfo;
|
|
332
364
|
};
|
|
333
365
|
|
|
334
366
|
// src/common/retryState.ts
|
|
@@ -339,7 +371,7 @@ var getRetryStateFromUrl = () => {
|
|
|
339
371
|
const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
|
|
340
372
|
if (retryId && retryAttempt) {
|
|
341
373
|
const parsed = parseInt(retryAttempt, 10);
|
|
342
|
-
if (Number.isNaN(parsed)) {
|
|
374
|
+
if (Number.isNaN(parsed) || parsed < -1) {
|
|
343
375
|
return null;
|
|
344
376
|
}
|
|
345
377
|
return {
|
|
@@ -357,6 +389,7 @@ var clearRetryStateFromUrl = () => {
|
|
|
357
389
|
const url = new URL(globalThis.window.location.href);
|
|
358
390
|
url.searchParams.delete(RETRY_ID_PARAM);
|
|
359
391
|
url.searchParams.delete(RETRY_ATTEMPT_PARAM);
|
|
392
|
+
url.searchParams.delete(CACHE_BUST_PARAM);
|
|
360
393
|
globalThis.window.history.replaceState(null, "", url.toString());
|
|
361
394
|
} catch {
|
|
362
395
|
}
|
|
@@ -387,7 +420,7 @@ var getRetryAttemptFromUrl = () => {
|
|
|
387
420
|
const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
|
|
388
421
|
if (retryAttempt) {
|
|
389
422
|
const parsed = parseInt(retryAttempt, 10);
|
|
390
|
-
if (Number.isNaN(parsed)) {
|
|
423
|
+
if (Number.isNaN(parsed) || parsed < -1) {
|
|
391
424
|
return null;
|
|
392
425
|
}
|
|
393
426
|
return parsed;
|
|
@@ -420,8 +453,12 @@ export {
|
|
|
420
453
|
optionsWindowKey,
|
|
421
454
|
RETRY_ID_PARAM,
|
|
422
455
|
RETRY_ATTEMPT_PARAM,
|
|
456
|
+
CACHE_BUST_PARAM,
|
|
423
457
|
versionCheckStateWindowKey,
|
|
458
|
+
reloadScheduledKey,
|
|
459
|
+
staticAssetRecoveryKey,
|
|
424
460
|
debugSyncErrorEventType,
|
|
461
|
+
spinnerStateWindowKey,
|
|
425
462
|
setLogger,
|
|
426
463
|
getLogger,
|
|
427
464
|
emitEvent,
|
|
@@ -5,5 +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;
|
|
10
|
+
export declare const reloadScheduledKey: unique symbol;
|
|
11
|
+
export declare const inMemoryLastReloadKey: unique symbol;
|
|
12
|
+
export declare const staticAssetRecoveryKey: unique symbol;
|
|
9
13
|
export declare const debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
14
|
+
export declare const spinnerStateWindowKey: unique symbol;
|
|
@@ -13,6 +13,8 @@ export type SPAGuardEvent = (SPAGuardEventChunkError & {
|
|
|
13
13
|
name: "lazy-retry-attempt";
|
|
14
14
|
}) | (SPAGuardEventLazyRetryExhausted & {
|
|
15
15
|
name: "lazy-retry-exhausted";
|
|
16
|
+
}) | (SPAGuardEventLazyRetryStart & {
|
|
17
|
+
name: "lazy-retry-start";
|
|
16
18
|
}) | (SPAGuardEventLazyRetrySuccess & {
|
|
17
19
|
name: "lazy-retry-success";
|
|
18
20
|
}) | (SPAGuardEventRetryAttempt & {
|
|
@@ -21,6 +23,8 @@ export type SPAGuardEvent = (SPAGuardEventChunkError & {
|
|
|
21
23
|
name: "retry-exhausted";
|
|
22
24
|
}) | (SPAGuardEventRetryReset & {
|
|
23
25
|
name: "retry-reset";
|
|
26
|
+
}) | (SPAGuardEventStaticAssetLoadFailed & {
|
|
27
|
+
name: "static-asset-load-failed";
|
|
24
28
|
});
|
|
25
29
|
export interface SPAGuardEventChunkError {
|
|
26
30
|
error: unknown;
|
|
@@ -36,23 +40,35 @@ export interface SPAGuardEventLazyRetryAttempt {
|
|
|
36
40
|
attempt: number;
|
|
37
41
|
/** Delay in milliseconds before this retry attempt. */
|
|
38
42
|
delay: number;
|
|
43
|
+
/** The error that caused the previous attempt to fail. */
|
|
44
|
+
error?: unknown;
|
|
39
45
|
name: "lazy-retry-attempt";
|
|
40
46
|
/** Total number of attempts including the initial try (delays.length + 1). */
|
|
41
47
|
totalAttempts: number;
|
|
42
48
|
}
|
|
43
49
|
/** Emitted when all module-level retry attempts are exhausted. */
|
|
44
50
|
export interface SPAGuardEventLazyRetryExhausted {
|
|
51
|
+
/** The final error after all attempts failed. */
|
|
52
|
+
error: unknown;
|
|
45
53
|
name: "lazy-retry-exhausted";
|
|
46
54
|
/** Total number of attempts that were made (delays.length + 1). */
|
|
47
55
|
totalAttempts: number;
|
|
48
56
|
/** Whether attemptReload() will be called after this event. */
|
|
49
57
|
willReload: boolean;
|
|
50
58
|
}
|
|
59
|
+
/** Emitted once before the first import attempt, when retries are configured. */
|
|
60
|
+
export interface SPAGuardEventLazyRetryStart {
|
|
61
|
+
name: "lazy-retry-start";
|
|
62
|
+
/** Total number of attempts that will be made (delays.length + 1). */
|
|
63
|
+
totalAttempts: number;
|
|
64
|
+
}
|
|
51
65
|
/** Emitted when a module loads successfully after one or more retry attempts. */
|
|
52
66
|
export interface SPAGuardEventLazyRetrySuccess {
|
|
53
67
|
/** 1-based retry number on which the import succeeded (1 = first retry). */
|
|
54
68
|
attempt: number;
|
|
55
69
|
name: "lazy-retry-success";
|
|
70
|
+
/** Total time in milliseconds from first attempt to success. */
|
|
71
|
+
totalTime?: number;
|
|
56
72
|
}
|
|
57
73
|
export interface SPAGuardEventRetryAttempt {
|
|
58
74
|
attempt: number;
|
|
@@ -71,5 +87,11 @@ export interface SPAGuardEventRetryReset {
|
|
|
71
87
|
previousRetryId: string;
|
|
72
88
|
timeSinceReload: number;
|
|
73
89
|
}
|
|
90
|
+
/** Emitted when a hashed static asset (script/link) fails to load, likely due to a stale deployment. */
|
|
91
|
+
export interface SPAGuardEventStaticAssetLoadFailed {
|
|
92
|
+
name: "static-asset-load-failed";
|
|
93
|
+
/** The URL of the asset that failed to load. */
|
|
94
|
+
url: string;
|
|
95
|
+
}
|
|
74
96
|
export type SubscribeFn = (event: SPAGuardEvent) => void;
|
|
75
97
|
export type UnsubscribeFn = () => void;
|
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";
|
package/dist/common/options.d.ts
CHANGED
|
@@ -95,6 +95,30 @@ export interface Options {
|
|
|
95
95
|
/** Custom HTML to display during the loading/retrying state */
|
|
96
96
|
content?: string;
|
|
97
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* Spinner overlay configuration.
|
|
100
|
+
* Controls the full-page loading spinner injected by the Vite plugin
|
|
101
|
+
* and available via `showSpinner()`/`dismissSpinner()` at runtime.
|
|
102
|
+
*/
|
|
103
|
+
spinner?: {
|
|
104
|
+
/**
|
|
105
|
+
* Overlay background color.
|
|
106
|
+
* Used as CSS variable fallback: var(--spa-guard-spinner-bg, <this value>).
|
|
107
|
+
* @default '#fff'
|
|
108
|
+
*/
|
|
109
|
+
background?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Custom spinner HTML (the spinner element only, no container/overlay).
|
|
112
|
+
* If not provided, uses the default SVG spinner.
|
|
113
|
+
*/
|
|
114
|
+
content?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Disable spinner entirely.
|
|
117
|
+
* No injection into body, showSpinner() is a no-op, Spinner returns null.
|
|
118
|
+
* @default false
|
|
119
|
+
*/
|
|
120
|
+
disabled?: boolean;
|
|
121
|
+
};
|
|
98
122
|
};
|
|
99
123
|
/**
|
|
100
124
|
* Options for the lazyWithRetry module-level retry logic.
|
|
@@ -130,28 +154,22 @@ export interface Options {
|
|
|
130
154
|
endpoint?: string;
|
|
131
155
|
};
|
|
132
156
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* and available via `showSpinner()`/`dismissSpinner()` at runtime.
|
|
157
|
+
* Configuration for automatic recovery from static asset 404 errors
|
|
158
|
+
* caused by deployment version mismatches.
|
|
136
159
|
*/
|
|
137
|
-
|
|
160
|
+
staticAssets?: {
|
|
138
161
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @default
|
|
142
|
-
*/
|
|
143
|
-
background?: string;
|
|
144
|
-
/**
|
|
145
|
-
* Custom spinner HTML (the spinner element only, no container/overlay).
|
|
146
|
-
* If not provided, uses the default SVG spinner.
|
|
162
|
+
* Automatically trigger a cache-busting reload when a hashed static asset
|
|
163
|
+
* fails to load and the page has been open long enough to suggest a stale deployment.
|
|
164
|
+
* @default true
|
|
147
165
|
*/
|
|
148
|
-
|
|
166
|
+
autoRecover?: boolean;
|
|
149
167
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* @default
|
|
168
|
+
* Milliseconds to wait after the first failed asset before triggering the reload.
|
|
169
|
+
* Allows collecting multiple concurrent failures into a single reload.
|
|
170
|
+
* @default 500
|
|
153
171
|
*/
|
|
154
|
-
|
|
172
|
+
recoveryDelay?: number;
|
|
155
173
|
};
|
|
156
174
|
/** @default true */
|
|
157
175
|
useRetryId?: boolean;
|
package/dist/common/reload.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** @internal */
|
|
2
2
|
export declare const resetReloadScheduled: () => void;
|
|
3
|
-
export declare const attemptReload: (error: unknown
|
|
3
|
+
export declare const attemptReload: (error: unknown, opts?: {
|
|
4
|
+
cacheBust?: boolean;
|
|
5
|
+
}) => void;
|
|
4
6
|
export declare const showFallbackUI: () => void;
|
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/index.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export interface BeaconSchema {
|
|
2
2
|
appName?: string;
|
|
3
|
+
errorContext?: string;
|
|
3
4
|
errorMessage?: string;
|
|
5
|
+
errorType?: string;
|
|
4
6
|
eventMessage?: string;
|
|
5
7
|
eventName?: string;
|
|
8
|
+
httpStatus?: number;
|
|
6
9
|
retryAttempt?: number;
|
|
7
10
|
retryId?: string;
|
|
8
11
|
serialized?: string;
|
|
12
|
+
url?: string;
|
|
9
13
|
}
|
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
|
}
|