@ovineko/spa-guard 0.0.1-alpha-25 → 0.0.1-alpha-27
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 +76 -0
- package/dist/_internal.js +10 -7
- package/dist/{chunk-X6E7KUDK.js → chunk-67WBMNUS.js} +47 -17
- package/dist/{chunk-XFILAQ7P.js → chunk-7KNE2UWX.js} +2 -2
- package/dist/{chunk-Z75UPJWV.js → chunk-7PDOUY6W.js} +5 -5
- package/dist/{chunk-ZVYB2746.js → chunk-FRCJOMRP.js} +77 -5
- package/dist/{chunk-CSN7MQGX.js → chunk-KAH6PVTJ.js} +29 -17
- package/dist/{chunk-T72DERME.js → chunk-V2OOMVZK.js} +1 -1
- package/dist/common/constants.d.ts +2 -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/staticAssetRecovery.d.ts +2 -0
- package/dist/runtime/debug/index.js +4 -4
- package/dist/runtime/index.js +7 -4
- package/dist/schema/index.d.ts +4 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @ovineko/spa-guard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@ovineko/spa-guard)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Core runtime for spa-guard — chunk load error handling, version checking, spinner, i18n, and event schema for SPAs.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @ovineko/spa-guard
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Call `recommendedSetup` once when your app boots. It dismisses the loading spinner, starts version checking, and returns a cleanup function.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { recommendedSetup } from "@ovineko/spa-guard/runtime";
|
|
20
|
+
|
|
21
|
+
const cleanup = recommendedSetup();
|
|
22
|
+
// optionally call cleanup() to stop background checks
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Disable version checking:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const cleanup = recommendedSetup({ versionCheck: false });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
### `@ovineko/spa-guard` (common)
|
|
34
|
+
|
|
35
|
+
- `listen` — subscribe to spa-guard events
|
|
36
|
+
- `events` — event type constants
|
|
37
|
+
- `options` — runtime options helpers
|
|
38
|
+
- `disableDefaultRetry` / `enableDefaultRetry` / `isDefaultRetryEnabled` — control default retry behaviour
|
|
39
|
+
- `BeaconError` — error class for beacon failures
|
|
40
|
+
- `ForceRetryError` — error class to force a retry
|
|
41
|
+
|
|
42
|
+
### `@ovineko/spa-guard/runtime`
|
|
43
|
+
|
|
44
|
+
- `recommendedSetup(options?)` — enable recommended runtime features; returns cleanup function
|
|
45
|
+
- `RecommendedSetupOptions` — `{ versionCheck?: boolean }`
|
|
46
|
+
- `startVersionCheck` / `stopVersionCheck` — manual version check control
|
|
47
|
+
- `getState` / `subscribeToState` — runtime state access
|
|
48
|
+
- `SpaGuardState` — state type
|
|
49
|
+
- `showSpinner` / `dismissSpinner` / `getSpinnerHtml` — spinner helpers
|
|
50
|
+
- `setTranslations` — override i18n strings
|
|
51
|
+
- `ForceRetryError`
|
|
52
|
+
|
|
53
|
+
### `@ovineko/spa-guard/schema`
|
|
54
|
+
|
|
55
|
+
Schemas for spa-guard configuration.
|
|
56
|
+
|
|
57
|
+
### `@ovineko/spa-guard/i18n`
|
|
58
|
+
|
|
59
|
+
Built-in translation strings.
|
|
60
|
+
|
|
61
|
+
### `@ovineko/spa-guard/runtime/debug`
|
|
62
|
+
|
|
63
|
+
Debug helpers for testing error scenarios.
|
|
64
|
+
|
|
65
|
+
## Related packages
|
|
66
|
+
|
|
67
|
+
- [@ovineko/spa-guard-react](../react/README.md) — `lazyWithRetry` and React error boundary
|
|
68
|
+
- [@ovineko/spa-guard-react-router](../react-router/README.md) — React Router error boundary
|
|
69
|
+
- [@ovineko/spa-guard-vite](../vite/README.md) — Vite plugin (injects runtime script)
|
|
70
|
+
- [@ovineko/spa-guard-node](../node/README.md) — Node.js cache and builder API
|
|
71
|
+
- [@ovineko/spa-guard-fastify](../fastify/README.md) — Fastify plugin
|
|
72
|
+
- [@ovineko/spa-guard-eslint](../eslint/README.md) — ESLint rules
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/dist/_internal.js
CHANGED
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
isChunkError,
|
|
4
4
|
listenInternal,
|
|
5
5
|
serializeError
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-FRCJOMRP.js";
|
|
7
7
|
import {
|
|
8
8
|
SPINNER_ID,
|
|
9
9
|
defaultSpinnerSvg,
|
|
10
10
|
extractVersionFromHtml
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-7PDOUY6W.js";
|
|
12
12
|
import {
|
|
13
13
|
dispatchAsyncRuntimeError,
|
|
14
14
|
dispatchChunkLoadError,
|
|
@@ -17,13 +17,13 @@ import {
|
|
|
17
17
|
dispatchNetworkTimeout,
|
|
18
18
|
dispatchSyncRuntimeError,
|
|
19
19
|
dispatchUnhandledRejection
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-7KNE2UWX.js";
|
|
21
21
|
import {
|
|
22
22
|
attemptReload,
|
|
23
23
|
sendBeacon,
|
|
24
24
|
shouldForceRetry,
|
|
25
25
|
shouldIgnoreMessages
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-KAH6PVTJ.js";
|
|
27
27
|
import {
|
|
28
28
|
applyI18n,
|
|
29
29
|
debugSyncErrorEventType,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
isDefaultRetryEnabled,
|
|
39
39
|
optionsWindowKey,
|
|
40
40
|
subscribe
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-67WBMNUS.js";
|
|
42
42
|
import "./chunk-MLKGABMK.js";
|
|
43
43
|
|
|
44
44
|
// src/common/handleErrorWithSpaGuard.ts
|
|
@@ -95,6 +95,8 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
95
95
|
const { callReloadOnFailure, onRetry, signal } = options ?? {};
|
|
96
96
|
let lastError = new Error("Import failed after all retry attempts");
|
|
97
97
|
const totalAttempts = delays.length + 1;
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
emitEvent({ name: "lazy-retry-start", totalAttempts });
|
|
98
100
|
for (let attempt = 0; attempt < totalAttempts; attempt++) {
|
|
99
101
|
if (signal?.aborted) {
|
|
100
102
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
@@ -102,7 +104,7 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
102
104
|
try {
|
|
103
105
|
const result = await importFn();
|
|
104
106
|
if (attempt > 0) {
|
|
105
|
-
emitEvent({ attempt, name: "lazy-retry-success" });
|
|
107
|
+
emitEvent({ attempt, name: "lazy-retry-success", totalTime: Date.now() - startTime });
|
|
106
108
|
}
|
|
107
109
|
return result;
|
|
108
110
|
} catch (error) {
|
|
@@ -115,6 +117,7 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
115
117
|
emitEvent({
|
|
116
118
|
attempt: attempt + 1,
|
|
117
119
|
delay: currentDelay,
|
|
120
|
+
error: lastError,
|
|
118
121
|
name: "lazy-retry-attempt",
|
|
119
122
|
totalAttempts
|
|
120
123
|
});
|
|
@@ -122,7 +125,7 @@ var retryImport = async (importFn, delays, options) => {
|
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
const willReload = callReloadOnFailure === true && isChunkError(lastError) && isDefaultRetryEnabled();
|
|
125
|
-
emitEvent({ name: "lazy-retry-exhausted", totalAttempts, willReload });
|
|
128
|
+
emitEvent({ error: lastError, name: "lazy-retry-exhausted", totalAttempts, willReload });
|
|
126
129
|
if (willReload) {
|
|
127
130
|
attemptReload(lastError);
|
|
128
131
|
}
|
|
@@ -14,6 +14,8 @@ var loggerWindowKey = /* @__PURE__ */ Symbol.for(`${name}:logger`);
|
|
|
14
14
|
var RETRY_ID_PARAM = "spaGuardRetryId";
|
|
15
15
|
var RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
|
|
16
16
|
var versionCheckStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:version-check-state`);
|
|
17
|
+
var reloadScheduledKey = /* @__PURE__ */ Symbol.for(`${name}:reload-scheduled`);
|
|
18
|
+
var inMemoryLastReloadKey = /* @__PURE__ */ Symbol.for(`${name}:in-memory-last-reload`);
|
|
17
19
|
var debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
18
20
|
|
|
19
21
|
// src/common/events/internal.ts
|
|
@@ -163,6 +165,10 @@ var defaultOptions = {
|
|
|
163
165
|
},
|
|
164
166
|
loading: {
|
|
165
167
|
content: defaultLoadingFallbackHtml
|
|
168
|
+
},
|
|
169
|
+
spinner: {
|
|
170
|
+
background: "#fff",
|
|
171
|
+
disabled: false
|
|
166
172
|
}
|
|
167
173
|
},
|
|
168
174
|
lazyRetry: {
|
|
@@ -171,9 +177,9 @@ var defaultOptions = {
|
|
|
171
177
|
},
|
|
172
178
|
minTimeBetweenResets: 5e3,
|
|
173
179
|
reloadDelays: [1e3, 2e3, 5e3],
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
staticAssets: {
|
|
181
|
+
autoRecover: true,
|
|
182
|
+
recoveryDelay: 500
|
|
177
183
|
},
|
|
178
184
|
useRetryId: true
|
|
179
185
|
};
|
|
@@ -202,6 +208,10 @@ var getOptions = () => {
|
|
|
202
208
|
loading: {
|
|
203
209
|
...defaultOptions.html?.loading,
|
|
204
210
|
...windowOptions?.html?.loading
|
|
211
|
+
},
|
|
212
|
+
spinner: {
|
|
213
|
+
...defaultOptions.html?.spinner,
|
|
214
|
+
...windowOptions?.html?.spinner
|
|
205
215
|
}
|
|
206
216
|
},
|
|
207
217
|
lazyRetry: {
|
|
@@ -212,9 +222,9 @@ var getOptions = () => {
|
|
|
212
222
|
...defaultOptions.reportBeacon,
|
|
213
223
|
...windowOptions?.reportBeacon
|
|
214
224
|
},
|
|
215
|
-
|
|
216
|
-
...defaultOptions.
|
|
217
|
-
...windowOptions?.
|
|
225
|
+
staticAssets: {
|
|
226
|
+
...defaultOptions.staticAssets,
|
|
227
|
+
...windowOptions?.staticAssets
|
|
218
228
|
}
|
|
219
229
|
};
|
|
220
230
|
};
|
|
@@ -231,8 +241,19 @@ var ForceRetryError = class extends Error {
|
|
|
231
241
|
// src/common/lastReloadTime.ts
|
|
232
242
|
var STORAGE_KEY = "__spa_guard_last_reload_timestamp__";
|
|
233
243
|
var RESET_INFO_KEY = "__spa_guard_last_retry_reset__";
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
if (globalThis.window && !globalThis.window[inMemoryLastReloadKey]) {
|
|
245
|
+
globalThis.window[inMemoryLastReloadKey] = {
|
|
246
|
+
resetInfo: null,
|
|
247
|
+
storage: null
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
var getInMemoryState = () => {
|
|
251
|
+
const w = globalThis.window;
|
|
252
|
+
if (!w) {
|
|
253
|
+
return { resetInfo: null, storage: null };
|
|
254
|
+
}
|
|
255
|
+
return w[inMemoryLastReloadKey] ?? (w[inMemoryLastReloadKey] = { resetInfo: null, storage: null });
|
|
256
|
+
};
|
|
236
257
|
var hasSessionStorage = () => {
|
|
237
258
|
try {
|
|
238
259
|
return globalThis.window !== void 0 && typeof sessionStorage !== "undefined";
|
|
@@ -250,10 +271,10 @@ var setLastReloadTime = (retryId, attemptNumber) => {
|
|
|
250
271
|
try {
|
|
251
272
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
252
273
|
} catch {
|
|
253
|
-
|
|
274
|
+
getInMemoryState().storage = data;
|
|
254
275
|
}
|
|
255
276
|
} else {
|
|
256
|
-
|
|
277
|
+
getInMemoryState().storage = data;
|
|
257
278
|
}
|
|
258
279
|
};
|
|
259
280
|
var getLastReloadTime = () => {
|
|
@@ -264,10 +285,14 @@ var getLastReloadTime = () => {
|
|
|
264
285
|
return JSON.parse(stored);
|
|
265
286
|
}
|
|
266
287
|
} catch {
|
|
267
|
-
|
|
288
|
+
try {
|
|
289
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
return getInMemoryState().storage;
|
|
268
293
|
}
|
|
269
294
|
}
|
|
270
|
-
return
|
|
295
|
+
return getInMemoryState().storage;
|
|
271
296
|
};
|
|
272
297
|
var clearLastReloadTime = () => {
|
|
273
298
|
if (hasSessionStorage()) {
|
|
@@ -276,7 +301,7 @@ var clearLastReloadTime = () => {
|
|
|
276
301
|
} catch {
|
|
277
302
|
}
|
|
278
303
|
}
|
|
279
|
-
|
|
304
|
+
getInMemoryState().storage = null;
|
|
280
305
|
};
|
|
281
306
|
var shouldResetRetryCycle = (retryState, reloadDelays, minTimeBetweenResets = 5e3) => {
|
|
282
307
|
if (retryState.retryAttempt === 0) {
|
|
@@ -311,10 +336,10 @@ var setLastRetryResetInfo = (previousRetryId) => {
|
|
|
311
336
|
try {
|
|
312
337
|
sessionStorage.setItem(RESET_INFO_KEY, JSON.stringify(data));
|
|
313
338
|
} catch {
|
|
314
|
-
|
|
339
|
+
getInMemoryState().resetInfo = data;
|
|
315
340
|
}
|
|
316
341
|
} else {
|
|
317
|
-
|
|
342
|
+
getInMemoryState().resetInfo = data;
|
|
318
343
|
}
|
|
319
344
|
};
|
|
320
345
|
var getLastRetryResetInfo = () => {
|
|
@@ -325,10 +350,14 @@ var getLastRetryResetInfo = () => {
|
|
|
325
350
|
return JSON.parse(stored);
|
|
326
351
|
}
|
|
327
352
|
} catch {
|
|
328
|
-
|
|
353
|
+
try {
|
|
354
|
+
sessionStorage.removeItem(RESET_INFO_KEY);
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
return getInMemoryState().resetInfo;
|
|
329
358
|
}
|
|
330
359
|
}
|
|
331
|
-
return
|
|
360
|
+
return getInMemoryState().resetInfo;
|
|
332
361
|
};
|
|
333
362
|
|
|
334
363
|
// src/common/retryState.ts
|
|
@@ -421,6 +450,7 @@ export {
|
|
|
421
450
|
RETRY_ID_PARAM,
|
|
422
451
|
RETRY_ATTEMPT_PARAM,
|
|
423
452
|
versionCheckStateWindowKey,
|
|
453
|
+
reloadScheduledKey,
|
|
424
454
|
debugSyncErrorEventType,
|
|
425
455
|
setLogger,
|
|
426
456
|
getLogger,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
showFallbackUI
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KAH6PVTJ.js";
|
|
4
4
|
import {
|
|
5
5
|
ForceRetryError,
|
|
6
6
|
debugSyncErrorEventType,
|
|
7
7
|
emitEvent,
|
|
8
8
|
getOptions
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-67WBMNUS.js";
|
|
10
10
|
|
|
11
11
|
// src/runtime/debug/errorDispatchers.ts
|
|
12
12
|
function dispatchAsyncRuntimeError() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defaultSpinnerHtml,
|
|
3
3
|
getOptions
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-67WBMNUS.js";
|
|
5
5
|
|
|
6
6
|
// src/common/parseVersion.ts
|
|
7
7
|
function extractVersionFromHtml(html) {
|
|
@@ -37,11 +37,11 @@ function dismissSpinner() {
|
|
|
37
37
|
}
|
|
38
38
|
function getSpinnerHtml(backgroundOverride) {
|
|
39
39
|
const opts = getOptions();
|
|
40
|
-
if (opts.spinner?.disabled) {
|
|
40
|
+
if (opts.html?.spinner?.disabled) {
|
|
41
41
|
return "";
|
|
42
42
|
}
|
|
43
|
-
const spinnerContent = opts.spinner?.content ?? defaultSpinnerSvg;
|
|
44
|
-
const bg = backgroundOverride ?? opts.spinner?.background ?? "#fff";
|
|
43
|
+
const spinnerContent = opts.html?.spinner?.content ?? defaultSpinnerSvg;
|
|
44
|
+
const bg = backgroundOverride ?? opts.html?.spinner?.background ?? "#fff";
|
|
45
45
|
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
46
|
}
|
|
47
47
|
function showSpinner(options) {
|
|
@@ -50,7 +50,7 @@ function showSpinner(options) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
const opts = getOptions();
|
|
53
|
-
if (opts.spinner?.disabled) {
|
|
53
|
+
if (opts.html?.spinner?.disabled) {
|
|
54
54
|
return () => {
|
|
55
55
|
};
|
|
56
56
|
}
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
sendBeacon,
|
|
4
4
|
shouldForceRetry,
|
|
5
5
|
shouldIgnoreMessages
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-KAH6PVTJ.js";
|
|
7
7
|
import {
|
|
8
|
+
emitEvent,
|
|
8
9
|
getLogger,
|
|
9
10
|
getOptions,
|
|
10
11
|
getRetryInfoForBeacon,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
markInitialized,
|
|
14
15
|
setLogger,
|
|
15
16
|
updateRetryStateInUrl
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-67WBMNUS.js";
|
|
17
18
|
|
|
18
19
|
// src/common/isChunkError.ts
|
|
19
20
|
var isChunkError = (error) => {
|
|
@@ -158,6 +159,59 @@ var extractOwnProperties = (obj) => {
|
|
|
158
159
|
return props;
|
|
159
160
|
};
|
|
160
161
|
|
|
162
|
+
// src/common/isStaticAssetError.ts
|
|
163
|
+
var HASHED_ASSET_RE = /[-._][a-zA-Z0-9]{6,}\.(js|mjs|css)$/i;
|
|
164
|
+
var isHashedAssetUrl = (url) => {
|
|
165
|
+
try {
|
|
166
|
+
const pathname = new URL(url).pathname;
|
|
167
|
+
return HASHED_ASSET_RE.test(pathname);
|
|
168
|
+
} catch {
|
|
169
|
+
return HASHED_ASSET_RE.test(url);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var isStaticAssetError = (event) => {
|
|
173
|
+
const target = event.target;
|
|
174
|
+
if (target instanceof HTMLScriptElement) {
|
|
175
|
+
return isHashedAssetUrl(target.src);
|
|
176
|
+
}
|
|
177
|
+
if (target instanceof HTMLLinkElement) {
|
|
178
|
+
return isHashedAssetUrl(target.href);
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
var isLikely404 = (timeSinceNavMs = typeof performance === "undefined" ? 0 : performance.now()) => {
|
|
183
|
+
return timeSinceNavMs > 3e4;
|
|
184
|
+
};
|
|
185
|
+
var getAssetUrl = (event) => {
|
|
186
|
+
const target = event.target;
|
|
187
|
+
if (target instanceof HTMLScriptElement) {
|
|
188
|
+
return target.src;
|
|
189
|
+
}
|
|
190
|
+
if (target instanceof HTMLLinkElement) {
|
|
191
|
+
return target.href;
|
|
192
|
+
}
|
|
193
|
+
return "";
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/common/staticAssetRecovery.ts
|
|
197
|
+
var recoveryTimer = null;
|
|
198
|
+
var failedAssets = /* @__PURE__ */ new Set();
|
|
199
|
+
var handleStaticAssetFailure = (url) => {
|
|
200
|
+
failedAssets.add(url);
|
|
201
|
+
if (recoveryTimer !== null) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const options = getOptions();
|
|
205
|
+
const delay = options.staticAssets?.recoveryDelay ?? 500;
|
|
206
|
+
recoveryTimer = setTimeout(() => {
|
|
207
|
+
const assets = [...failedAssets];
|
|
208
|
+
failedAssets = /* @__PURE__ */ new Set();
|
|
209
|
+
recoveryTimer = null;
|
|
210
|
+
const error = new Error(`Static asset load failed: ${assets.join(", ")}`);
|
|
211
|
+
attemptReload(error, { cacheBust: true });
|
|
212
|
+
}, delay);
|
|
213
|
+
};
|
|
214
|
+
|
|
161
215
|
// src/common/listen/internal.ts
|
|
162
216
|
var listenInternal = (serializeError2, logger) => {
|
|
163
217
|
if (logger) {
|
|
@@ -192,6 +246,15 @@ var listenInternal = (serializeError2, logger) => {
|
|
|
192
246
|
attemptReload(event.error ?? event);
|
|
193
247
|
return;
|
|
194
248
|
}
|
|
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
|
+
}
|
|
195
258
|
const serialized = serializeError2(event);
|
|
196
259
|
sendBeacon({
|
|
197
260
|
errorMessage: event.message,
|
|
@@ -265,10 +328,12 @@ var eventLogConfig = {
|
|
|
265
328
|
"fallback-ui-shown": "warn",
|
|
266
329
|
"lazy-retry-attempt": "warn",
|
|
267
330
|
"lazy-retry-exhausted": "error",
|
|
331
|
+
"lazy-retry-start": "log",
|
|
268
332
|
"lazy-retry-success": "log",
|
|
269
333
|
"retry-attempt": "warn",
|
|
270
334
|
"retry-exhausted": "error",
|
|
271
|
-
"retry-reset": "log"
|
|
335
|
+
"retry-reset": "log",
|
|
336
|
+
"static-asset-load-failed": "error"
|
|
272
337
|
};
|
|
273
338
|
var formatEvent = (event) => {
|
|
274
339
|
switch (event.name) {
|
|
@@ -284,8 +349,12 @@ var formatEvent = (event) => {
|
|
|
284
349
|
case "lazy-retry-exhausted": {
|
|
285
350
|
return `${PREFIX} lazy-retry-exhausted: ${event.totalAttempts} attempts, willReload=${event.willReload}`;
|
|
286
351
|
}
|
|
352
|
+
case "lazy-retry-start": {
|
|
353
|
+
return `${PREFIX} lazy-retry-start: totalAttempts=${event.totalAttempts}`;
|
|
354
|
+
}
|
|
287
355
|
case "lazy-retry-success": {
|
|
288
|
-
|
|
356
|
+
const timePart = event.totalTime === void 0 ? "" : `, totalTime=${event.totalTime}ms`;
|
|
357
|
+
return `${PREFIX} lazy-retry-success: succeeded on attempt ${event.attempt}${timePart}`;
|
|
289
358
|
}
|
|
290
359
|
case "retry-attempt": {
|
|
291
360
|
return `${PREFIX} retry-attempt: attempt ${event.attempt} in ${event.delay}ms (retryId: ${event.retryId})`;
|
|
@@ -296,6 +365,9 @@ var formatEvent = (event) => {
|
|
|
296
365
|
case "retry-reset": {
|
|
297
366
|
return `${PREFIX} retry-reset: ${event.timeSinceReload}ms since last reload (retryId: ${event.previousRetryId})`;
|
|
298
367
|
}
|
|
368
|
+
case "static-asset-load-failed": {
|
|
369
|
+
return `${PREFIX} static-asset-load-failed: ${event.url}`;
|
|
370
|
+
}
|
|
299
371
|
}
|
|
300
372
|
};
|
|
301
373
|
var createLogger = () => ({
|
|
@@ -329,7 +401,7 @@ var createLogger = () => ({
|
|
|
329
401
|
logEvent(event) {
|
|
330
402
|
const level = eventLogConfig[event.name];
|
|
331
403
|
const message = formatEvent(event);
|
|
332
|
-
if (event.name === "chunk-error") {
|
|
404
|
+
if (event.name === "chunk-error" || event.name === "lazy-retry-attempt" || event.name === "lazy-retry-exhausted") {
|
|
333
405
|
console[level](message, event.error);
|
|
334
406
|
} else {
|
|
335
407
|
console[level](message);
|
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
getRetryAttemptFromUrl,
|
|
16
16
|
getRetryStateFromUrl,
|
|
17
17
|
isDefaultRetryEnabled,
|
|
18
|
+
reloadScheduledKey,
|
|
18
19
|
setLastReloadTime,
|
|
19
20
|
setLastRetryResetInfo,
|
|
20
21
|
shouldResetRetryCycle
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-67WBMNUS.js";
|
|
22
23
|
|
|
23
24
|
// src/common/shouldIgnore.ts
|
|
24
25
|
var shouldIgnoreMessages = (messages) => {
|
|
@@ -81,13 +82,22 @@ var buildReloadUrlAttemptOnly = (retryAttempt) => {
|
|
|
81
82
|
url.searchParams.set(RETRY_ATTEMPT_PARAM, String(retryAttempt));
|
|
82
83
|
return url.toString();
|
|
83
84
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
if (globalThis.window && !globalThis.window[reloadScheduledKey]) {
|
|
86
|
+
globalThis.window[reloadScheduledKey] = { scheduled: false };
|
|
87
|
+
}
|
|
88
|
+
var getReloadState = () => {
|
|
89
|
+
if (globalThis.window === void 0) {
|
|
90
|
+
return { scheduled: false };
|
|
91
|
+
}
|
|
92
|
+
return globalThis.window[reloadScheduledKey] ?? (globalThis.window[reloadScheduledKey] = { scheduled: false });
|
|
93
|
+
};
|
|
94
|
+
var attemptReload = (error, opts) => {
|
|
95
|
+
const reloadState = getReloadState();
|
|
96
|
+
if (reloadState.scheduled) {
|
|
87
97
|
getLogger()?.reloadAlreadyScheduled(error);
|
|
88
98
|
return;
|
|
89
99
|
}
|
|
90
|
-
|
|
100
|
+
reloadState.scheduled = true;
|
|
91
101
|
try {
|
|
92
102
|
const options = getOptions();
|
|
93
103
|
const reloadDelays = options.reloadDelays ?? [1e3, 2e3, 5e3];
|
|
@@ -111,7 +121,7 @@ var attemptReload = (error) => {
|
|
|
111
121
|
name: "chunk-error"
|
|
112
122
|
});
|
|
113
123
|
if (!retryEnabled) {
|
|
114
|
-
|
|
124
|
+
reloadState.scheduled = false;
|
|
115
125
|
return;
|
|
116
126
|
}
|
|
117
127
|
if (enableRetryReset && retryState && retryState.retryAttempt > 0 && shouldResetRetryCycle(retryState, reloadDelays, minTimeBetweenResets)) {
|
|
@@ -138,7 +148,7 @@ var attemptReload = (error) => {
|
|
|
138
148
|
if (!shouldIgnoreMessages([errorMsg2])) {
|
|
139
149
|
getLogger()?.fallbackAlreadyShown(error);
|
|
140
150
|
}
|
|
141
|
-
|
|
151
|
+
reloadState.scheduled = false;
|
|
142
152
|
showFallbackUI();
|
|
143
153
|
return;
|
|
144
154
|
}
|
|
@@ -166,7 +176,7 @@ var attemptReload = (error) => {
|
|
|
166
176
|
if (!useRetryId) {
|
|
167
177
|
clearRetryAttemptFromUrl();
|
|
168
178
|
}
|
|
169
|
-
|
|
179
|
+
reloadState.scheduled = false;
|
|
170
180
|
showFallbackUI();
|
|
171
181
|
return;
|
|
172
182
|
}
|
|
@@ -188,15 +198,17 @@ var attemptReload = (error) => {
|
|
|
188
198
|
if (useRetryId && enableRetryReset) {
|
|
189
199
|
setLastReloadTime(retryId, nextAttempt);
|
|
190
200
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
let reloadUrl;
|
|
202
|
+
reloadUrl = useRetryId ? buildReloadUrl(retryId, nextAttempt) : buildReloadUrlAttemptOnly(nextAttempt);
|
|
203
|
+
if (opts?.cacheBust) {
|
|
204
|
+
const url = new URL(reloadUrl);
|
|
205
|
+
url.searchParams.set("spaGuardCacheBust", String(Date.now()));
|
|
206
|
+
reloadUrl = url.toString();
|
|
196
207
|
}
|
|
208
|
+
globalThis.window.location.href = reloadUrl;
|
|
197
209
|
}, delay);
|
|
198
210
|
} catch {
|
|
199
|
-
|
|
211
|
+
reloadState.scheduled = false;
|
|
200
212
|
}
|
|
201
213
|
};
|
|
202
214
|
var showLoadingUI = (attempt) => {
|
|
@@ -215,10 +227,10 @@ var showLoadingUI = (attempt) => {
|
|
|
215
227
|
container.innerHTML = loadingHtml;
|
|
216
228
|
const spinnerEl = container.querySelector("[data-spa-guard-spinner]");
|
|
217
229
|
if (spinnerEl) {
|
|
218
|
-
if (options.spinner?.disabled) {
|
|
230
|
+
if (options.html?.spinner?.disabled) {
|
|
219
231
|
spinnerEl.remove();
|
|
220
|
-
} else if (options.spinner?.content) {
|
|
221
|
-
spinnerEl.innerHTML = options.spinner.content;
|
|
232
|
+
} else if (options.html?.spinner?.content) {
|
|
233
|
+
spinnerEl.innerHTML = options.html.spinner.content;
|
|
222
234
|
}
|
|
223
235
|
}
|
|
224
236
|
const retryingSection = container.querySelector(
|
|
@@ -6,4 +6,6 @@ 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
8
|
export declare const versionCheckStateWindowKey: unique symbol;
|
|
9
|
+
export declare const reloadScheduledKey: unique symbol;
|
|
10
|
+
export declare const inMemoryLastReloadKey: unique symbol;
|
|
9
11
|
export declare const debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
@@ -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-FRCJOMRP.js";
|
|
6
|
+
import "../chunk-KAH6PVTJ.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-67WBMNUS.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;
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
dispatchRetryExhausted,
|
|
8
8
|
dispatchSyncRuntimeError,
|
|
9
9
|
dispatchUnhandledRejection
|
|
10
|
-
} from "../../chunk-
|
|
11
|
-
import "../../chunk-
|
|
10
|
+
} from "../../chunk-7KNE2UWX.js";
|
|
11
|
+
import "../../chunk-KAH6PVTJ.js";
|
|
12
12
|
import {
|
|
13
13
|
subscribeToState
|
|
14
|
-
} from "../../chunk-
|
|
14
|
+
} from "../../chunk-V2OOMVZK.js";
|
|
15
15
|
import {
|
|
16
16
|
subscribe
|
|
17
|
-
} from "../../chunk-
|
|
17
|
+
} from "../../chunk-67WBMNUS.js";
|
|
18
18
|
import "../../chunk-MLKGABMK.js";
|
|
19
19
|
|
|
20
20
|
// 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-7PDOUY6W.js";
|
|
7
7
|
import {
|
|
8
8
|
getState,
|
|
9
9
|
subscribeToState
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-V2OOMVZK.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-67WBMNUS.js";
|
|
18
18
|
import "../chunk-MLKGABMK.js";
|
|
19
19
|
|
|
20
20
|
// src/common/checkVersion.ts
|
|
@@ -33,7 +33,10 @@ if (globalThis.window && !globalThis.window[versionCheckStateWindowKey]) {
|
|
|
33
33
|
globalThis.window[versionCheckStateWindowKey] = createInitialState();
|
|
34
34
|
}
|
|
35
35
|
var getState2 = () => {
|
|
36
|
-
|
|
36
|
+
if (globalThis.window === void 0) {
|
|
37
|
+
return createInitialState();
|
|
38
|
+
}
|
|
39
|
+
return globalThis.window[versionCheckStateWindowKey] ?? (globalThis.window[versionCheckStateWindowKey] = createInitialState());
|
|
37
40
|
};
|
|
38
41
|
var fetchJsonVersion = async () => {
|
|
39
42
|
const endpoint = getOptions().checkVersion?.endpoint;
|
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
|
}
|