@ovineko/spa-guard 0.0.2-alpha-1 → 0.0.4
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 +22 -173
- package/dist/ForceRetryError-BWLv3UVK.d.mts +6 -0
- package/dist/_internal.d.ts +149 -20
- package/dist/_internal.js +113 -170
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/common/index.d.ts +29 -9
- package/dist/common/index.js +47 -83
- package/dist/errorDispatchers-Cl_pa0DT.mjs +105 -0
- package/dist/i18n/index.d.ts +2 -21
- package/dist/i18n/index.js +344 -341
- package/dist/index-DL8CfPXg.d.mts +26 -0
- package/dist/index-rPxPv6iu.d.mts +16 -0
- package/dist/logger-Cp1Eyk6S.mjs +534 -0
- package/dist/retryOrchestrator-DNGIHV2M.mjs +758 -0
- package/dist/retryOrchestrator-mn9XcIVu.d.mts +257 -0
- package/dist/runtime/debug/index.d.ts +6 -4
- package/dist/runtime/debug/index.js +218 -238
- package/dist/runtime/index.d.ts +66 -8
- package/dist/runtime/index.js +279 -367
- package/dist/schema/index.d.ts +2 -13
- package/dist/schema/index.js +1 -0
- package/dist/schema/parse.d.ts +6 -2
- package/dist/schema/parse.js +29 -44
- package/dist/spinner-BbZVKZ-6.mjs +60 -0
- package/dist/spinner-X23gI09z.d.mts +37 -0
- package/dist/state-I20jENMD.mjs +93 -0
- package/dist/types-DrN8pgyc.d.mts +107 -0
- package/package.json +1 -4
- package/dist/chunk-3UJ67DPX.js +0 -98
- package/dist/chunk-GE63YJOT.js +0 -865
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-PERG4557.js +0 -74
- package/dist/chunk-VZ2DLGXX.js +0 -111
- package/dist/chunk-XIFXSNSD.js +0 -678
- package/dist/common/checkVersion.d.ts +0 -5
- package/dist/common/constants.d.ts +0 -14
- package/dist/common/errors/BeaconError.d.ts +0 -12
- package/dist/common/errors/ForceRetryError.d.ts +0 -5
- package/dist/common/events/index.d.ts +0 -2
- package/dist/common/events/internal.d.ts +0 -13
- package/dist/common/events/types.d.ts +0 -104
- package/dist/common/fallbackRendering.d.ts +0 -23
- package/dist/common/fallbackState.d.ts +0 -3
- package/dist/common/handleErrorWithSpaGuard.d.ts +0 -18
- package/dist/common/html.generated.d.ts +0 -3
- package/dist/common/i18n.d.ts +0 -23
- package/dist/common/isChunkError.d.ts +0 -1
- package/dist/common/isStaticAssetError.d.ts +0 -3
- package/dist/common/lastReloadTime.d.ts +0 -17
- package/dist/common/listen/index.d.ts +0 -1
- package/dist/common/listen/internal.d.ts +0 -2
- package/dist/common/log.d.ts +0 -1
- package/dist/common/logger.d.ts +0 -30
- package/dist/common/options.d.ts +0 -182
- package/dist/common/parseVersion.d.ts +0 -9
- package/dist/common/retryImport.d.ts +0 -43
- package/dist/common/retryOrchestrator.d.ts +0 -35
- package/dist/common/retryState.d.ts +0 -11
- package/dist/common/sendBeacon.d.ts +0 -2
- package/dist/common/serializeError.d.ts +0 -1
- package/dist/common/shouldIgnore.d.ts +0 -13
- package/dist/common/spinner.d.ts +0 -8
- package/dist/common/staticAssetRecovery.d.ts +0 -2
- package/dist/i18n/translations.d.ts +0 -2
- package/dist/runtime/debug/errorDispatchers.d.ts +0 -63
- package/dist/runtime/recommendedSetup.d.ts +0 -36
- package/dist/runtime/state.d.ts +0 -20
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
//#region package.json
|
|
3
|
+
var name = "@ovineko/spa-guard";
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/common/constants.ts
|
|
6
|
+
const optionsWindowKey = "__SPA_GUARD_OPTIONS__";
|
|
7
|
+
const eventSubscribersWindowKey = Symbol.for(`${name}:event-subscribers`);
|
|
8
|
+
const internalConfigWindowKey = Symbol.for(`${name}:internal-config`);
|
|
9
|
+
const initializedKey = Symbol.for(`${name}:initialized`);
|
|
10
|
+
const loggerWindowKey = Symbol.for(`${name}:logger`);
|
|
11
|
+
const RETRY_ID_PARAM = "spaGuardRetryId";
|
|
12
|
+
const RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
|
|
13
|
+
const CACHE_BUST_PARAM = "spaGuardCacheBust";
|
|
14
|
+
const versionCheckStateWindowKey = Symbol.for(`${name}:version-check-state`);
|
|
15
|
+
const inMemoryLastReloadKey = Symbol.for(`${name}:in-memory-last-reload`);
|
|
16
|
+
const staticAssetRecoveryKey = Symbol.for(`${name}:static-asset-recovery`);
|
|
17
|
+
const debugSyncErrorEventType = "spa-guard:debug-sync-error";
|
|
18
|
+
const spinnerStateWindowKey = Symbol.for(`${name}:spinner-state`);
|
|
19
|
+
const fallbackModeKey = Symbol.for(`${name}:fallback-mode`);
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/common/events/internal.ts
|
|
22
|
+
if (globalThis.window && !globalThis.window[eventSubscribersWindowKey]) globalThis.window[eventSubscribersWindowKey] = /* @__PURE__ */ new Set();
|
|
23
|
+
if (globalThis.window && !globalThis.window[internalConfigWindowKey]) globalThis.window[internalConfigWindowKey] = {
|
|
24
|
+
defaultRetryEnabled: true,
|
|
25
|
+
initialized: false
|
|
26
|
+
};
|
|
27
|
+
const subscribers = globalThis.window?.[eventSubscribersWindowKey] ?? /* @__PURE__ */ new Set();
|
|
28
|
+
const internalConfig = globalThis.window?.[internalConfigWindowKey] ?? {
|
|
29
|
+
defaultRetryEnabled: true,
|
|
30
|
+
initialized: false
|
|
31
|
+
};
|
|
32
|
+
const setLogger = (logger) => {
|
|
33
|
+
if (globalThis.window !== void 0) globalThis.window[loggerWindowKey] = logger;
|
|
34
|
+
};
|
|
35
|
+
const getLogger = () => {
|
|
36
|
+
return globalThis.window?.[loggerWindowKey];
|
|
37
|
+
};
|
|
38
|
+
const emitEvent = (event, options) => {
|
|
39
|
+
if (!options?.silent) getLogger()?.logEvent(event);
|
|
40
|
+
subscribers.forEach((cb) => {
|
|
41
|
+
try {
|
|
42
|
+
cb(event);
|
|
43
|
+
} catch {}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const subscribe = (cb) => {
|
|
47
|
+
subscribers.add(cb);
|
|
48
|
+
return () => subscribers.delete(cb);
|
|
49
|
+
};
|
|
50
|
+
const isInitialized = () => {
|
|
51
|
+
return internalConfig.initialized;
|
|
52
|
+
};
|
|
53
|
+
const markInitialized = () => {
|
|
54
|
+
internalConfig.initialized = true;
|
|
55
|
+
if (globalThis.window !== void 0) globalThis.window[initializedKey] = true;
|
|
56
|
+
};
|
|
57
|
+
const disableDefaultRetry = () => {
|
|
58
|
+
internalConfig.defaultRetryEnabled = false;
|
|
59
|
+
};
|
|
60
|
+
const enableDefaultRetry = () => {
|
|
61
|
+
internalConfig.defaultRetryEnabled = true;
|
|
62
|
+
};
|
|
63
|
+
const isDefaultRetryEnabled = () => {
|
|
64
|
+
return internalConfig.defaultRetryEnabled;
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/common/i18n.ts
|
|
68
|
+
/**
|
|
69
|
+
* Apply i18n translations to a virtual container's data-attributed elements.
|
|
70
|
+
* Patches `[data-spa-guard-content]` and `[data-spa-guard-action]` elements,
|
|
71
|
+
* and applies RTL direction if needed.
|
|
72
|
+
*
|
|
73
|
+
* Must be called on a virtual (detached) container BEFORE inserting into DOM
|
|
74
|
+
* to avoid flash of untranslated content.
|
|
75
|
+
*/
|
|
76
|
+
function applyI18n(container, t) {
|
|
77
|
+
const contentEls = container.querySelectorAll("[data-spa-guard-content]");
|
|
78
|
+
for (const el of contentEls) {
|
|
79
|
+
const key = el.dataset.spaGuardContent;
|
|
80
|
+
if (key && key in t) {
|
|
81
|
+
const value = t[key];
|
|
82
|
+
if (typeof value === "string") el.textContent = value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const actionEls = container.querySelectorAll("[data-spa-guard-action]");
|
|
86
|
+
for (const el of actionEls) {
|
|
87
|
+
const action = el.dataset.spaGuardAction;
|
|
88
|
+
const tKey = action === "try-again" ? "tryAgain" : action;
|
|
89
|
+
if (tKey && tKey in t) {
|
|
90
|
+
const value = t[tKey];
|
|
91
|
+
if (typeof value === "string") el.textContent = value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (t.rtl) {
|
|
95
|
+
for (const child of container.children) if (child instanceof HTMLElement && child.tagName !== "STYLE") child.style.direction = "rtl";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read i18n translations from the `<meta name="spa-guard-i18n">` tag.
|
|
100
|
+
* Returns parsed translations or null if the tag is absent or malformed.
|
|
101
|
+
*/
|
|
102
|
+
function getI18n() {
|
|
103
|
+
try {
|
|
104
|
+
const el = document.querySelector("meta[name=\"spa-guard-i18n\"]");
|
|
105
|
+
if (!el) return null;
|
|
106
|
+
const content = el.getAttribute("content");
|
|
107
|
+
if (!content) return null;
|
|
108
|
+
return JSON.parse(content);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Write i18n translations to the `<meta name="spa-guard-i18n">` tag.
|
|
115
|
+
* Creates the tag if it doesn't exist, or updates it if it does.
|
|
116
|
+
*
|
|
117
|
+
* Use this at runtime to dynamically patch the inline fallback/loading UI
|
|
118
|
+
* translations without server-side rendering.
|
|
119
|
+
*/
|
|
120
|
+
function setTranslations(translations) {
|
|
121
|
+
let el = document.querySelector("meta[name=\"spa-guard-i18n\"]");
|
|
122
|
+
if (!el) {
|
|
123
|
+
el = document.createElement("meta");
|
|
124
|
+
el.setAttribute("name", "spa-guard-i18n");
|
|
125
|
+
document.head.append(el);
|
|
126
|
+
}
|
|
127
|
+
el.setAttribute("content", JSON.stringify(translations));
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/common/html.generated.ts
|
|
131
|
+
const defaultErrorFallbackHtml = `<style>.spa-guard-error-id:has(.spa-guard-retry-id:empty){display:none}.spa-guard-error-id{font-family:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",Menlo,monospace}.spa-guard-fallback-root{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:2rem;font-family:system-ui,sans-serif;background:#fff;color:#1a1a1a;color-scheme:light dark}.spa-guard-fallback-icon{stroke:#b0b0b0}.spa-guard-fallback-message{color:#666}.spa-guard-fallback-muted{color:#999}.spa-guard-btn-secondary{border:1px solid #d0d0d0;background:#fff;color:#333}.spa-guard-btn-primary{border:1px solid transparent;background:#111;color:#fff}@media (prefers-color-scheme:dark){.spa-guard-fallback-root{background:#111318;color:#e7eaf0}.spa-guard-fallback-icon{stroke:#8b95a7}.spa-guard-fallback-message{color:#b8bfca}.spa-guard-fallback-muted{color:#8b95a7}.spa-guard-btn-secondary{border-color:#3b4351;background:#1a1f28;color:#d8deea}.spa-guard-btn-primary{background:#e7eaf0;color:#151922}}</style><div class="spa-guard-fallback-root"><div style="text-align:center;max-width:480px"><div style="margin-bottom:1.5rem"><svg class="spa-guard-fallback-icon" xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div><h1 data-spa-guard-content="heading" style="font-size:1.375rem;font-weight:600;margin:0 0 .5rem;line-height:1.3">Something went wrong</h1><p data-spa-guard-content="message" class="spa-guard-fallback-message" style="max-width:600px;margin:0 auto 1.5rem;font-size:.9375rem;line-height:1.5">Please refresh the page to continue.</p><div style="display:flex;gap:.5rem;justify-content:center;flex-wrap:wrap"><button data-spa-guard-action="try-again" type="button" class="spa-guard-btn-secondary" style="display:none;padding:.5rem 1.25rem;font-size:.875rem;font-family:inherit;border-radius:6px;cursor:pointer;line-height:1.5">Try again</button> <button data-spa-guard-action="reload" type="button" class="spa-guard-btn-primary" style="padding:.5rem 1.25rem;font-size:.875rem;font-family:inherit;border-radius:6px;cursor:pointer;line-height:1.5">Reload page</button></div><p class="spa-guard-error-id spa-guard-fallback-muted" style="margin-top:1.5rem;font-size:.6875rem">Error ID: <span class="spa-guard-retry-id"></span></p></div></div>`;
|
|
132
|
+
const defaultLoadingFallbackHtml = `<style>.spa-guard-loading-root{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:2rem;font-family:system-ui,sans-serif;background:#fff;color:#1a1a1a;color-scheme:light dark}.spa-guard-loading-muted{color:#999}@media (prefers-color-scheme:dark){.spa-guard-loading-root{background:#111318;color:#e7eaf0}.spa-guard-loading-muted{color:#8b95a7}}</style><div class="spa-guard-loading-root"><div style="text-align:center"><div data-spa-guard-spinner style="margin-bottom:1.25rem"></div><h2 data-spa-guard-content="loading" style="font-size:1.125rem;font-weight:600;margin:0 0 .25rem">Loading...</h2><p data-spa-guard-section="retrying" class="spa-guard-loading-muted" style="display:none;font-size:.8125rem;margin:.5rem 0 0"><span data-spa-guard-content="retrying">Retry attempt</span> <span data-spa-guard-content="attempt"></span></p></div></div>`;
|
|
133
|
+
const defaultSpinnerHtml = `<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>`;
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/common/options.ts
|
|
136
|
+
var options_exports = /* @__PURE__ */ __exportAll({
|
|
137
|
+
getOptions: () => getOptions,
|
|
138
|
+
optionsWindowKey: () => optionsWindowKey
|
|
139
|
+
});
|
|
140
|
+
const defaultOptions = {
|
|
141
|
+
checkVersion: {
|
|
142
|
+
cache: "no-store",
|
|
143
|
+
interval: 3e5,
|
|
144
|
+
mode: "html",
|
|
145
|
+
onUpdate: "reload"
|
|
146
|
+
},
|
|
147
|
+
enableRetryReset: true,
|
|
148
|
+
errors: {
|
|
149
|
+
forceRetry: [],
|
|
150
|
+
ignore: []
|
|
151
|
+
},
|
|
152
|
+
handleUnhandledRejections: {
|
|
153
|
+
retry: false,
|
|
154
|
+
sendBeacon: true
|
|
155
|
+
},
|
|
156
|
+
html: {
|
|
157
|
+
fallback: {
|
|
158
|
+
content: defaultErrorFallbackHtml,
|
|
159
|
+
selector: "body"
|
|
160
|
+
},
|
|
161
|
+
loading: { content: defaultLoadingFallbackHtml },
|
|
162
|
+
spinner: {
|
|
163
|
+
background: "#fff",
|
|
164
|
+
disabled: false
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
lazyRetry: {
|
|
168
|
+
callReloadOnFailure: true,
|
|
169
|
+
retryDelays: [1e3, 2e3]
|
|
170
|
+
},
|
|
171
|
+
minTimeBetweenResets: 5e3,
|
|
172
|
+
reloadDelays: [
|
|
173
|
+
1e3,
|
|
174
|
+
2e3,
|
|
175
|
+
5e3
|
|
176
|
+
],
|
|
177
|
+
staticAssets: {
|
|
178
|
+
autoRecover: true,
|
|
179
|
+
recoveryDelay: 500
|
|
180
|
+
},
|
|
181
|
+
useRetryId: true
|
|
182
|
+
};
|
|
183
|
+
const getOptions = () => {
|
|
184
|
+
const windowOptions = globalThis.window?.[optionsWindowKey];
|
|
185
|
+
return {
|
|
186
|
+
...defaultOptions,
|
|
187
|
+
...windowOptions,
|
|
188
|
+
checkVersion: {
|
|
189
|
+
...defaultOptions.checkVersion,
|
|
190
|
+
...windowOptions?.checkVersion
|
|
191
|
+
},
|
|
192
|
+
errors: {
|
|
193
|
+
...defaultOptions.errors,
|
|
194
|
+
...windowOptions?.errors
|
|
195
|
+
},
|
|
196
|
+
handleUnhandledRejections: {
|
|
197
|
+
...defaultOptions.handleUnhandledRejections,
|
|
198
|
+
...windowOptions?.handleUnhandledRejections
|
|
199
|
+
},
|
|
200
|
+
html: {
|
|
201
|
+
fallback: {
|
|
202
|
+
...defaultOptions.html?.fallback,
|
|
203
|
+
...windowOptions?.html?.fallback
|
|
204
|
+
},
|
|
205
|
+
loading: {
|
|
206
|
+
...defaultOptions.html?.loading,
|
|
207
|
+
...windowOptions?.html?.loading
|
|
208
|
+
},
|
|
209
|
+
spinner: {
|
|
210
|
+
...defaultOptions.html?.spinner,
|
|
211
|
+
...windowOptions?.html?.spinner
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
lazyRetry: {
|
|
215
|
+
...defaultOptions.lazyRetry,
|
|
216
|
+
...windowOptions?.lazyRetry
|
|
217
|
+
},
|
|
218
|
+
reportBeacon: {
|
|
219
|
+
...defaultOptions.reportBeacon,
|
|
220
|
+
...windowOptions?.reportBeacon
|
|
221
|
+
},
|
|
222
|
+
staticAssets: {
|
|
223
|
+
...defaultOptions.staticAssets,
|
|
224
|
+
...windowOptions?.staticAssets
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/common/retryState.ts
|
|
230
|
+
const getRetryStateFromUrl = () => {
|
|
231
|
+
try {
|
|
232
|
+
const params = new URLSearchParams(globalThis.window.location.search);
|
|
233
|
+
const retryId = params.get(RETRY_ID_PARAM);
|
|
234
|
+
const retryAttempt = params.get(RETRY_ATTEMPT_PARAM);
|
|
235
|
+
if (retryId && retryAttempt) {
|
|
236
|
+
if (!/^\d+$/.test(retryAttempt)) return null;
|
|
237
|
+
const parsed = parseInt(retryAttempt, 10);
|
|
238
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) return null;
|
|
239
|
+
return {
|
|
240
|
+
retryAttempt: parsed,
|
|
241
|
+
retryId
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const generateRetryId = () => {
|
|
250
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
|
|
251
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
252
|
+
const array = new Uint32Array(2);
|
|
253
|
+
crypto.getRandomValues(array);
|
|
254
|
+
return `${Date.now()}-${array[0].toString(36)}-${array[1].toString(36)}`;
|
|
255
|
+
}
|
|
256
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 15)}`;
|
|
257
|
+
};
|
|
258
|
+
const getRetryAttemptFromUrl = () => {
|
|
259
|
+
try {
|
|
260
|
+
const retryAttempt = new URLSearchParams(globalThis.window.location.search).get(RETRY_ATTEMPT_PARAM);
|
|
261
|
+
if (retryAttempt) {
|
|
262
|
+
if (!/^\d+$/.test(retryAttempt)) return null;
|
|
263
|
+
const parsed = parseInt(retryAttempt, 10);
|
|
264
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) return null;
|
|
265
|
+
return parsed;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const getRetryInfoForBeacon = () => {
|
|
273
|
+
const retryState = getRetryStateFromUrl();
|
|
274
|
+
if (!retryState) return {};
|
|
275
|
+
return {
|
|
276
|
+
retryAttempt: retryState.retryAttempt,
|
|
277
|
+
retryId: retryState.retryId
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/common/fallbackRendering.ts
|
|
282
|
+
/**
|
|
283
|
+
* Renders the loading UI into the DOM during a retry delay before reload.
|
|
284
|
+
*
|
|
285
|
+
* Fail-safe: if loading content is not configured or the target element is not
|
|
286
|
+
* found, returns silently with no log, no event, and no side effects.
|
|
287
|
+
* All DOM access is wrapped in try/catch.
|
|
288
|
+
*/
|
|
289
|
+
const showLoadingUI = (attempt) => {
|
|
290
|
+
const options = getOptions();
|
|
291
|
+
const loadingHtml = options.html?.loading?.content;
|
|
292
|
+
if (!loadingHtml) return;
|
|
293
|
+
const selector = options.html?.fallback?.selector ?? "body";
|
|
294
|
+
try {
|
|
295
|
+
const targetElement = document.querySelector(selector);
|
|
296
|
+
if (!targetElement) return;
|
|
297
|
+
const container = document.createElement("div");
|
|
298
|
+
container.innerHTML = loadingHtml;
|
|
299
|
+
const t = getI18n();
|
|
300
|
+
if (t) applyI18n(container, t);
|
|
301
|
+
targetElement.innerHTML = container.innerHTML;
|
|
302
|
+
const retrySection = targetElement.querySelector("[data-spa-guard-section=\"retrying\"]");
|
|
303
|
+
if (retrySection) {
|
|
304
|
+
const retrySectionEl = retrySection;
|
|
305
|
+
retrySectionEl.style.display = "";
|
|
306
|
+
retrySectionEl.style.visibility = "visible";
|
|
307
|
+
}
|
|
308
|
+
const attemptElements = targetElement.querySelectorAll("[data-spa-guard-content=\"attempt\"]");
|
|
309
|
+
for (const el of attemptElements) el.textContent = String(attempt);
|
|
310
|
+
const spinnerEl = targetElement.querySelector("[data-spa-guard-spinner]");
|
|
311
|
+
if (spinnerEl) {
|
|
312
|
+
const spinnerOptions = options.html?.spinner;
|
|
313
|
+
if (spinnerOptions?.disabled) spinnerEl.style.display = "none";
|
|
314
|
+
else if (spinnerOptions?.content) spinnerEl.innerHTML = spinnerOptions.content;
|
|
315
|
+
}
|
|
316
|
+
} catch {}
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* Renders the fallback UI into the DOM.
|
|
320
|
+
*
|
|
321
|
+
* This is a pure rendering helper. It has no lifecycle side effects:
|
|
322
|
+
* it does not set fallback mode, does not check whether fallback mode is
|
|
323
|
+
* already active, and does not modify orchestrator state. The caller is
|
|
324
|
+
* responsible for ensuring the lifecycle transition has already occurred
|
|
325
|
+
* before invoking this function.
|
|
326
|
+
*
|
|
327
|
+
* Fails safely: if fallback HTML is not configured or the target element
|
|
328
|
+
* is not found, logs a warning and returns without side effects or errors.
|
|
329
|
+
*/
|
|
330
|
+
const showFallbackUI = (override) => {
|
|
331
|
+
const options = getOptions();
|
|
332
|
+
const fallbackHtml = options.html?.fallback?.content;
|
|
333
|
+
const selector = options.html?.fallback?.selector ?? "body";
|
|
334
|
+
if (!fallbackHtml) {
|
|
335
|
+
getLogger()?.noFallbackConfigured();
|
|
336
|
+
emitEvent({
|
|
337
|
+
name: "fallback-ui-not-rendered",
|
|
338
|
+
reason: "no-html-configured"
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const targetElement = document.querySelector(selector);
|
|
344
|
+
if (!targetElement) {
|
|
345
|
+
getLogger()?.fallbackTargetNotFound(selector);
|
|
346
|
+
emitEvent({
|
|
347
|
+
name: "fallback-ui-not-rendered",
|
|
348
|
+
reason: "target-not-found",
|
|
349
|
+
selector
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const container = document.createElement("div");
|
|
354
|
+
container.innerHTML = fallbackHtml;
|
|
355
|
+
const t = getI18n();
|
|
356
|
+
if (t) applyI18n(container, t);
|
|
357
|
+
targetElement.innerHTML = container.innerHTML;
|
|
358
|
+
const reloadBtn = targetElement.querySelector("[data-spa-guard-action=\"reload\"]");
|
|
359
|
+
if (reloadBtn) reloadBtn.addEventListener("click", () => globalThis.window.location.reload());
|
|
360
|
+
const retryId = override?.retryId ?? getRetryStateFromUrl()?.retryId;
|
|
361
|
+
if (retryId) {
|
|
362
|
+
const retryIdElements = document.getElementsByClassName("spa-guard-retry-id");
|
|
363
|
+
for (const element of retryIdElements) element.textContent = retryId;
|
|
364
|
+
}
|
|
365
|
+
emitEvent({ name: "fallback-ui-shown" });
|
|
366
|
+
} catch (error) {
|
|
367
|
+
getLogger()?.fallbackInjectFailed(error);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/common/fallbackState.ts
|
|
372
|
+
const isInFallbackMode = () => {
|
|
373
|
+
if (globalThis.window === void 0) return false;
|
|
374
|
+
return globalThis.window[fallbackModeKey] === true;
|
|
375
|
+
};
|
|
376
|
+
const setFallbackMode = () => {
|
|
377
|
+
if (globalThis.window === void 0) return;
|
|
378
|
+
globalThis.window[fallbackModeKey] = true;
|
|
379
|
+
};
|
|
380
|
+
const resetFallbackMode = () => {
|
|
381
|
+
if (globalThis.window === void 0) return;
|
|
382
|
+
globalThis.window[fallbackModeKey] = false;
|
|
383
|
+
};
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/common/lastReloadTime.ts
|
|
386
|
+
const STORAGE_KEY = "__spa_guard_last_reload_timestamp__";
|
|
387
|
+
const RESET_INFO_KEY = "__spa_guard_last_retry_reset__";
|
|
388
|
+
if (globalThis.window && !globalThis.window[inMemoryLastReloadKey]) globalThis.window[inMemoryLastReloadKey] = {
|
|
389
|
+
resetInfo: null,
|
|
390
|
+
storage: null
|
|
391
|
+
};
|
|
392
|
+
const getInMemoryState = () => {
|
|
393
|
+
const w = globalThis.window;
|
|
394
|
+
if (!w) return {
|
|
395
|
+
resetInfo: null,
|
|
396
|
+
storage: null
|
|
397
|
+
};
|
|
398
|
+
return w[inMemoryLastReloadKey] ?? (w[inMemoryLastReloadKey] = {
|
|
399
|
+
resetInfo: null,
|
|
400
|
+
storage: null
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
const hasSessionStorage = () => {
|
|
404
|
+
try {
|
|
405
|
+
return globalThis.window !== void 0 && typeof sessionStorage !== "undefined";
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const setLastReloadTime = (retryId, attemptNumber) => {
|
|
411
|
+
const data = {
|
|
412
|
+
attemptNumber,
|
|
413
|
+
retryId,
|
|
414
|
+
timestamp: Date.now()
|
|
415
|
+
};
|
|
416
|
+
if (hasSessionStorage()) try {
|
|
417
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
418
|
+
} catch {
|
|
419
|
+
getInMemoryState().storage = data;
|
|
420
|
+
}
|
|
421
|
+
else getInMemoryState().storage = data;
|
|
422
|
+
};
|
|
423
|
+
const getLastReloadTime = () => {
|
|
424
|
+
if (hasSessionStorage()) try {
|
|
425
|
+
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
426
|
+
if (stored) return JSON.parse(stored);
|
|
427
|
+
} catch {
|
|
428
|
+
try {
|
|
429
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
430
|
+
} catch {}
|
|
431
|
+
return getInMemoryState().storage;
|
|
432
|
+
}
|
|
433
|
+
return getInMemoryState().storage;
|
|
434
|
+
};
|
|
435
|
+
const clearLastReloadTime = () => {
|
|
436
|
+
if (hasSessionStorage()) try {
|
|
437
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
438
|
+
} catch {}
|
|
439
|
+
getInMemoryState().storage = null;
|
|
440
|
+
};
|
|
441
|
+
const shouldResetRetryCycle = (retryState, reloadDelays, minTimeBetweenResets = 5e3) => {
|
|
442
|
+
if (retryState.retryAttempt === 0) return false;
|
|
443
|
+
const lastReload = getLastReloadTime();
|
|
444
|
+
if (!lastReload) return false;
|
|
445
|
+
if (lastReload.retryId !== retryState.retryId) return false;
|
|
446
|
+
const lastReset = getLastRetryResetInfo();
|
|
447
|
+
if (lastReset) {
|
|
448
|
+
if (Date.now() - lastReset.timestamp < minTimeBetweenResets) return false;
|
|
449
|
+
}
|
|
450
|
+
return Date.now() - lastReload.timestamp > (reloadDelays[lastReload.attemptNumber - 1] ?? 1e3) + 3e4;
|
|
451
|
+
};
|
|
452
|
+
const setLastRetryResetInfo = (previousRetryId) => {
|
|
453
|
+
const data = {
|
|
454
|
+
previousRetryId,
|
|
455
|
+
timestamp: Date.now()
|
|
456
|
+
};
|
|
457
|
+
if (hasSessionStorage()) try {
|
|
458
|
+
sessionStorage.setItem(RESET_INFO_KEY, JSON.stringify(data));
|
|
459
|
+
} catch {
|
|
460
|
+
getInMemoryState().resetInfo = data;
|
|
461
|
+
}
|
|
462
|
+
else getInMemoryState().resetInfo = data;
|
|
463
|
+
};
|
|
464
|
+
const getLastRetryResetInfo = () => {
|
|
465
|
+
if (hasSessionStorage()) try {
|
|
466
|
+
const stored = sessionStorage.getItem(RESET_INFO_KEY);
|
|
467
|
+
if (stored) return JSON.parse(stored);
|
|
468
|
+
} catch {
|
|
469
|
+
try {
|
|
470
|
+
sessionStorage.removeItem(RESET_INFO_KEY);
|
|
471
|
+
} catch {}
|
|
472
|
+
return getInMemoryState().resetInfo;
|
|
473
|
+
}
|
|
474
|
+
return getInMemoryState().resetInfo;
|
|
475
|
+
};
|
|
476
|
+
const clearLastRetryResetInfo = () => {
|
|
477
|
+
if (hasSessionStorage()) try {
|
|
478
|
+
sessionStorage.removeItem(RESET_INFO_KEY);
|
|
479
|
+
} catch {}
|
|
480
|
+
getInMemoryState().resetInfo = null;
|
|
481
|
+
};
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/common/errors/ForceRetryError.ts
|
|
484
|
+
const FORCE_RETRY_MAGIC = "__SPA_GUARD_FORCE_RETRY__";
|
|
485
|
+
var ForceRetryError = class extends Error {
|
|
486
|
+
constructor(message, options) {
|
|
487
|
+
super(`${FORCE_RETRY_MAGIC}${message ?? ""}`, options);
|
|
488
|
+
this.name = "ForceRetryError";
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/common/shouldIgnore.ts
|
|
493
|
+
/**
|
|
494
|
+
* Checks if any of the provided messages should be ignored based on errors.ignore option.
|
|
495
|
+
*/
|
|
496
|
+
const shouldIgnoreMessages = (messages) => {
|
|
497
|
+
const ignorePatterns = (getOptions().errors?.ignore ?? []).filter((p) => p !== "");
|
|
498
|
+
if (ignorePatterns.length === 0) return false;
|
|
499
|
+
return messages.filter((msg) => typeof msg === "string").some((message) => ignorePatterns.some((pattern) => message.includes(pattern)));
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Checks if any of the provided messages match a forceRetry pattern.
|
|
503
|
+
*/
|
|
504
|
+
const shouldForceRetry = (messages) => {
|
|
505
|
+
const forceRetryPatterns = [...getOptions().errors?.forceRetry ?? [], FORCE_RETRY_MAGIC].filter((p) => p !== "");
|
|
506
|
+
return messages.filter((msg) => typeof msg === "string").some((message) => forceRetryPatterns.some((pattern) => message.includes(pattern)));
|
|
507
|
+
};
|
|
508
|
+
/**
|
|
509
|
+
* Checks if a beacon should be ignored based on errors.ignore option.
|
|
510
|
+
*/
|
|
511
|
+
const shouldIgnoreBeacon = (beacon) => {
|
|
512
|
+
return shouldIgnoreMessages([beacon.errorMessage, beacon.eventMessage]);
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
//#region src/common/sendBeacon.ts
|
|
516
|
+
const sendBeacon = (beacon) => {
|
|
517
|
+
if (shouldIgnoreBeacon(beacon)) return;
|
|
518
|
+
const options = getOptions();
|
|
519
|
+
if (!options.reportBeacon?.endpoint) {
|
|
520
|
+
getLogger()?.noBeaconEndpoint();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const enrichedBeacon = options.appName ? {
|
|
524
|
+
...beacon,
|
|
525
|
+
appName: options.appName
|
|
526
|
+
} : beacon;
|
|
527
|
+
const body = JSON.stringify(enrichedBeacon);
|
|
528
|
+
if (!(typeof globalThis.window?.navigator?.sendBeacon === "function" && globalThis.window.navigator.sendBeacon(options.reportBeacon.endpoint, body)) && typeof fetch === "function") fetch(options.reportBeacon.endpoint, {
|
|
529
|
+
body,
|
|
530
|
+
keepalive: true,
|
|
531
|
+
method: "POST"
|
|
532
|
+
}).catch((error) => {
|
|
533
|
+
getLogger()?.beaconSendFailed(error);
|
|
534
|
+
});
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/common/retryOrchestrator.ts
|
|
538
|
+
const retryOrchestratorKey = Symbol.for(`${name}:retry-orchestrator`);
|
|
539
|
+
const createFreshState = () => ({
|
|
540
|
+
attempt: 0,
|
|
541
|
+
phase: "idle",
|
|
542
|
+
retryId: null,
|
|
543
|
+
timer: null
|
|
544
|
+
});
|
|
545
|
+
const getState = () => {
|
|
546
|
+
if (globalThis.window === void 0) return createFreshState();
|
|
547
|
+
const w = globalThis.window;
|
|
548
|
+
if (!w[retryOrchestratorKey]) w[retryOrchestratorKey] = createFreshState();
|
|
549
|
+
return w[retryOrchestratorKey];
|
|
550
|
+
};
|
|
551
|
+
const setState = (updates) => {
|
|
552
|
+
if (globalThis.window === void 0) return;
|
|
553
|
+
const w = globalThis.window;
|
|
554
|
+
if (!w[retryOrchestratorKey]) w[retryOrchestratorKey] = createFreshState();
|
|
555
|
+
Object.assign(w[retryOrchestratorKey], updates);
|
|
556
|
+
};
|
|
557
|
+
const buildReloadUrl = (retryId, attempt, cacheBust, includeRetryId = true) => {
|
|
558
|
+
const url = new URL(globalThis.window.location.href);
|
|
559
|
+
if (includeRetryId) url.searchParams.set(RETRY_ID_PARAM, retryId);
|
|
560
|
+
url.searchParams.set(RETRY_ATTEMPT_PARAM, String(attempt));
|
|
561
|
+
if (cacheBust) url.searchParams.set(CACHE_BUST_PARAM, String(Date.now()));
|
|
562
|
+
return url.toString();
|
|
563
|
+
};
|
|
564
|
+
const parseAttemptFromUrl = () => {
|
|
565
|
+
try {
|
|
566
|
+
const raw = new URLSearchParams(globalThis.window.location.search).get(RETRY_ATTEMPT_PARAM);
|
|
567
|
+
if (!raw) return null;
|
|
568
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
569
|
+
const parsed = parseInt(raw, 10);
|
|
570
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed) || parsed < 0) return null;
|
|
571
|
+
return parsed;
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const parseRetryIdFromUrl = () => {
|
|
577
|
+
try {
|
|
578
|
+
return new URLSearchParams(globalThis.window.location.search).get(RETRY_ID_PARAM);
|
|
579
|
+
} catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const clearRetryFromUrl = () => {
|
|
584
|
+
try {
|
|
585
|
+
const url = new URL(globalThis.window.location.href);
|
|
586
|
+
url.searchParams.delete(RETRY_ID_PARAM);
|
|
587
|
+
url.searchParams.delete(RETRY_ATTEMPT_PARAM);
|
|
588
|
+
url.searchParams.delete(CACHE_BUST_PARAM);
|
|
589
|
+
globalThis.window.history.replaceState(null, "", url.toString());
|
|
590
|
+
} catch (error) {
|
|
591
|
+
getLogger()?.error("clearRetryFromUrl failed", error);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const triggerRetry = (input = {}) => {
|
|
595
|
+
const state = getState();
|
|
596
|
+
if (state.phase === "fallback" || isInFallbackMode()) {
|
|
597
|
+
getLogger()?.fallbackAlreadyShown(input.error);
|
|
598
|
+
return { status: "fallback" };
|
|
599
|
+
}
|
|
600
|
+
if (state.phase === "scheduled") {
|
|
601
|
+
getLogger()?.reloadAlreadyScheduled(input.error);
|
|
602
|
+
return {
|
|
603
|
+
reason: "already-scheduled",
|
|
604
|
+
status: "deduped"
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
if (!isDefaultRetryEnabled()) return { status: "retry-disabled" };
|
|
608
|
+
setState({
|
|
609
|
+
...input.source !== void 0 && { lastSource: input.source },
|
|
610
|
+
lastTriggerTime: Date.now(),
|
|
611
|
+
phase: "scheduled"
|
|
612
|
+
});
|
|
613
|
+
try {
|
|
614
|
+
const options = getOptions();
|
|
615
|
+
const reloadDelays = options.reloadDelays ?? [
|
|
616
|
+
1e3,
|
|
617
|
+
2e3,
|
|
618
|
+
5e3
|
|
619
|
+
];
|
|
620
|
+
const useRetryId = options.useRetryId ?? true;
|
|
621
|
+
const enableRetryReset = options.enableRetryReset ?? true;
|
|
622
|
+
const minTimeBetweenResets = options.minTimeBetweenResets ?? 5e3;
|
|
623
|
+
const urlAttempt = parseAttemptFromUrl();
|
|
624
|
+
const urlRetryId = parseRetryIdFromUrl();
|
|
625
|
+
let currentAttempt = urlAttempt ?? 0;
|
|
626
|
+
let retryId = useRetryId && urlRetryId ? urlRetryId : generateRetryId();
|
|
627
|
+
getLogger()?.retryCycleStarting(retryId, currentAttempt);
|
|
628
|
+
if (enableRetryReset && urlRetryId && urlAttempt !== null && urlAttempt > 0) {
|
|
629
|
+
if (shouldResetRetryCycle({
|
|
630
|
+
retryAttempt: urlAttempt,
|
|
631
|
+
retryId: urlRetryId
|
|
632
|
+
}, reloadDelays, minTimeBetweenResets)) {
|
|
633
|
+
const lastReload = getLastReloadTime();
|
|
634
|
+
const timeSinceReload = lastReload ? Date.now() - lastReload.timestamp : 0;
|
|
635
|
+
clearRetryFromUrl();
|
|
636
|
+
clearLastReloadTime();
|
|
637
|
+
setLastRetryResetInfo(urlRetryId);
|
|
638
|
+
const errorMsg = String(input.error);
|
|
639
|
+
emitEvent({
|
|
640
|
+
name: "retry-reset",
|
|
641
|
+
previousAttempt: urlAttempt,
|
|
642
|
+
previousRetryId: urlRetryId,
|
|
643
|
+
timeSinceReload
|
|
644
|
+
}, { silent: shouldIgnoreMessages([errorMsg]) });
|
|
645
|
+
currentAttempt = 0;
|
|
646
|
+
retryId = generateRetryId();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
emitEvent({
|
|
650
|
+
error: input.error,
|
|
651
|
+
isRetrying: currentAttempt < reloadDelays.length,
|
|
652
|
+
name: "chunk-error"
|
|
653
|
+
});
|
|
654
|
+
if (currentAttempt >= reloadDelays.length) {
|
|
655
|
+
const errorMsg = String(input.error);
|
|
656
|
+
emitEvent({
|
|
657
|
+
finalAttempt: currentAttempt,
|
|
658
|
+
name: "retry-exhausted",
|
|
659
|
+
retryId
|
|
660
|
+
}, { silent: shouldIgnoreMessages([errorMsg]) });
|
|
661
|
+
sendBeacon({
|
|
662
|
+
errorMessage: "Exceeded maximum reload attempts",
|
|
663
|
+
eventName: "chunk_error_max_reloads",
|
|
664
|
+
retryAttempt: currentAttempt,
|
|
665
|
+
retryId,
|
|
666
|
+
serialized: JSON.stringify({
|
|
667
|
+
error: String(input.error),
|
|
668
|
+
retryAttempt: currentAttempt,
|
|
669
|
+
retryId
|
|
670
|
+
})
|
|
671
|
+
});
|
|
672
|
+
setState({
|
|
673
|
+
attempt: currentAttempt,
|
|
674
|
+
phase: "fallback",
|
|
675
|
+
retryId
|
|
676
|
+
});
|
|
677
|
+
setFallbackMode();
|
|
678
|
+
clearRetryFromUrl();
|
|
679
|
+
showFallbackUI({ retryId });
|
|
680
|
+
return { status: "fallback" };
|
|
681
|
+
}
|
|
682
|
+
const nextAttempt = currentAttempt + 1;
|
|
683
|
+
const delay = reloadDelays[currentAttempt] ?? 1e3;
|
|
684
|
+
const errorMsg = String(input.error);
|
|
685
|
+
emitEvent({
|
|
686
|
+
attempt: nextAttempt,
|
|
687
|
+
delay,
|
|
688
|
+
name: "retry-attempt",
|
|
689
|
+
retryId
|
|
690
|
+
}, { silent: shouldIgnoreMessages([errorMsg]) });
|
|
691
|
+
getLogger()?.retrySchedulingReload(retryId, nextAttempt, delay);
|
|
692
|
+
setState({
|
|
693
|
+
attempt: nextAttempt,
|
|
694
|
+
retryId
|
|
695
|
+
});
|
|
696
|
+
showLoadingUI(nextAttempt);
|
|
697
|
+
setState({ timer: setTimeout(() => {
|
|
698
|
+
try {
|
|
699
|
+
if (useRetryId && enableRetryReset) setLastReloadTime(retryId, nextAttempt);
|
|
700
|
+
const reloadUrl = buildReloadUrl(retryId, nextAttempt, input.cacheBust, useRetryId);
|
|
701
|
+
globalThis.window.location.href = reloadUrl;
|
|
702
|
+
} catch (navError) {
|
|
703
|
+
getLogger()?.error("triggerRetry navigation failed", navError);
|
|
704
|
+
setState({
|
|
705
|
+
phase: "idle",
|
|
706
|
+
timer: null
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}, delay) });
|
|
710
|
+
return { status: "accepted" };
|
|
711
|
+
} catch (error) {
|
|
712
|
+
getLogger()?.error("triggerRetry internal error", error);
|
|
713
|
+
setState({ phase: "idle" });
|
|
714
|
+
return { status: "internal-error" };
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
const markRetryHealthyBoot = () => {
|
|
718
|
+
const state = getState();
|
|
719
|
+
if (state.timer !== null) clearTimeout(state.timer);
|
|
720
|
+
clearRetryFromUrl();
|
|
721
|
+
clearLastReloadTime();
|
|
722
|
+
clearLastRetryResetInfo();
|
|
723
|
+
if (globalThis.window !== void 0) globalThis.window[retryOrchestratorKey] = createFreshState();
|
|
724
|
+
resetFallbackMode();
|
|
725
|
+
};
|
|
726
|
+
const getRetrySnapshot = () => {
|
|
727
|
+
const state = getState();
|
|
728
|
+
return {
|
|
729
|
+
attempt: state.attempt,
|
|
730
|
+
...state.lastSource !== void 0 && { lastSource: state.lastSource },
|
|
731
|
+
...state.lastTriggerTime !== void 0 && { lastTriggerTime: state.lastTriggerTime },
|
|
732
|
+
phase: state.phase,
|
|
733
|
+
retryId: state.retryId
|
|
734
|
+
};
|
|
735
|
+
};
|
|
736
|
+
/**
|
|
737
|
+
* Sets orchestrator state to fallback and renders the fallback UI.
|
|
738
|
+
* For use in debug/simulation contexts only — bypasses retry scheduling
|
|
739
|
+
* while keeping the orchestrator snapshot consistent with fallback mode.
|
|
740
|
+
*/
|
|
741
|
+
const setFallbackStateForDebug = () => {
|
|
742
|
+
const state = getState();
|
|
743
|
+
if (state.timer !== null) clearTimeout(state.timer);
|
|
744
|
+
setState({
|
|
745
|
+
phase: "fallback",
|
|
746
|
+
timer: null
|
|
747
|
+
});
|
|
748
|
+
setFallbackMode();
|
|
749
|
+
showFallbackUI({ ...state.retryId !== null && { retryId: state.retryId } });
|
|
750
|
+
};
|
|
751
|
+
const resetRetryOrchestratorForTests = () => {
|
|
752
|
+
const state = getState();
|
|
753
|
+
if (state.timer !== null) clearTimeout(state.timer);
|
|
754
|
+
if (globalThis.window !== void 0) globalThis.window[retryOrchestratorKey] = createFreshState();
|
|
755
|
+
resetFallbackMode();
|
|
756
|
+
};
|
|
757
|
+
//#endregion
|
|
758
|
+
export { markInitialized as A, setTranslations as C, getLogger as D, enableDefaultRetry as E, spinnerStateWindowKey as F, staticAssetRecoveryKey as I, versionCheckStateWindowKey as L, subscribe as M, debugSyncErrorEventType as N, isDefaultRetryEnabled as O, optionsWindowKey as P, getI18n as S, emitEvent as T, options_exports as _, triggerRetry as a, defaultSpinnerHtml as b, shouldIgnoreMessages as c, isInFallbackMode as d, resetFallbackMode as f, getOptions as g, getRetryStateFromUrl as h, setFallbackStateForDebug as i, setLogger as j, isInitialized as k, ForceRetryError as l, getRetryInfoForBeacon as m, markRetryHealthyBoot as n, sendBeacon as o, getRetryAttemptFromUrl as p, resetRetryOrchestratorForTests as r, shouldForceRetry as s, getRetrySnapshot as t, getLastRetryResetInfo as u, defaultErrorFallbackHtml as v, disableDefaultRetry as w, applyI18n as x, defaultLoadingFallbackHtml as y };
|