@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.
Files changed (67) hide show
  1. package/README.md +22 -173
  2. package/dist/ForceRetryError-BWLv3UVK.d.mts +6 -0
  3. package/dist/_internal.d.ts +149 -20
  4. package/dist/_internal.js +113 -170
  5. package/dist/chunk-CfYAbeIz.mjs +13 -0
  6. package/dist/common/index.d.ts +29 -9
  7. package/dist/common/index.js +47 -83
  8. package/dist/errorDispatchers-Cl_pa0DT.mjs +105 -0
  9. package/dist/i18n/index.d.ts +2 -21
  10. package/dist/i18n/index.js +344 -341
  11. package/dist/index-DL8CfPXg.d.mts +26 -0
  12. package/dist/index-rPxPv6iu.d.mts +16 -0
  13. package/dist/logger-Cp1Eyk6S.mjs +534 -0
  14. package/dist/retryOrchestrator-DNGIHV2M.mjs +758 -0
  15. package/dist/retryOrchestrator-mn9XcIVu.d.mts +257 -0
  16. package/dist/runtime/debug/index.d.ts +6 -4
  17. package/dist/runtime/debug/index.js +218 -238
  18. package/dist/runtime/index.d.ts +66 -8
  19. package/dist/runtime/index.js +279 -367
  20. package/dist/schema/index.d.ts +2 -13
  21. package/dist/schema/index.js +1 -0
  22. package/dist/schema/parse.d.ts +6 -2
  23. package/dist/schema/parse.js +29 -44
  24. package/dist/spinner-BbZVKZ-6.mjs +60 -0
  25. package/dist/spinner-X23gI09z.d.mts +37 -0
  26. package/dist/state-I20jENMD.mjs +93 -0
  27. package/dist/types-DrN8pgyc.d.mts +107 -0
  28. package/package.json +1 -4
  29. package/dist/chunk-3UJ67DPX.js +0 -98
  30. package/dist/chunk-GE63YJOT.js +0 -865
  31. package/dist/chunk-MLKGABMK.js +0 -9
  32. package/dist/chunk-PERG4557.js +0 -74
  33. package/dist/chunk-VZ2DLGXX.js +0 -111
  34. package/dist/chunk-XIFXSNSD.js +0 -678
  35. package/dist/common/checkVersion.d.ts +0 -5
  36. package/dist/common/constants.d.ts +0 -14
  37. package/dist/common/errors/BeaconError.d.ts +0 -12
  38. package/dist/common/errors/ForceRetryError.d.ts +0 -5
  39. package/dist/common/events/index.d.ts +0 -2
  40. package/dist/common/events/internal.d.ts +0 -13
  41. package/dist/common/events/types.d.ts +0 -104
  42. package/dist/common/fallbackRendering.d.ts +0 -23
  43. package/dist/common/fallbackState.d.ts +0 -3
  44. package/dist/common/handleErrorWithSpaGuard.d.ts +0 -18
  45. package/dist/common/html.generated.d.ts +0 -3
  46. package/dist/common/i18n.d.ts +0 -23
  47. package/dist/common/isChunkError.d.ts +0 -1
  48. package/dist/common/isStaticAssetError.d.ts +0 -3
  49. package/dist/common/lastReloadTime.d.ts +0 -17
  50. package/dist/common/listen/index.d.ts +0 -1
  51. package/dist/common/listen/internal.d.ts +0 -2
  52. package/dist/common/log.d.ts +0 -1
  53. package/dist/common/logger.d.ts +0 -30
  54. package/dist/common/options.d.ts +0 -182
  55. package/dist/common/parseVersion.d.ts +0 -9
  56. package/dist/common/retryImport.d.ts +0 -43
  57. package/dist/common/retryOrchestrator.d.ts +0 -35
  58. package/dist/common/retryState.d.ts +0 -11
  59. package/dist/common/sendBeacon.d.ts +0 -2
  60. package/dist/common/serializeError.d.ts +0 -1
  61. package/dist/common/shouldIgnore.d.ts +0 -13
  62. package/dist/common/spinner.d.ts +0 -8
  63. package/dist/common/staticAssetRecovery.d.ts +0 -2
  64. package/dist/i18n/translations.d.ts +0 -2
  65. package/dist/runtime/debug/errorDispatchers.d.ts +0 -63
  66. package/dist/runtime/recommendedSetup.d.ts +0 -36
  67. 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 };