@ovineko/spa-guard 0.0.1-alpha-30 → 0.0.2-alpha-0

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 CHANGED
@@ -22,6 +22,36 @@ const cleanup = recommendedSetup();
22
22
  // optionally call cleanup() to stop background checks
23
23
  ```
24
24
 
25
+ `recommendedSetup` is idempotent. Repeated calls return the same cleanup function and do not re-run setup side effects.
26
+
27
+ By default, `recommendedSetup` uses `healthyBoot: "auto"`:
28
+
29
+ - if retry params exist in the URL, it waits a dynamic grace period and calls `markRetryHealthyBoot()` only when the orchestrator phase is still `idle`.
30
+ - if no retry params exist, it does nothing.
31
+
32
+ Auto grace period formula:
33
+
34
+ - `max(5000, max(reloadDelays)+1000, sum(lazyRetry.retryDelays)+1000)`
35
+
36
+ If you want strict control, switch to manual healthy-boot mode and call `markRetryHealthyBoot()` yourself.
37
+
38
+ ```ts
39
+ import { markRetryHealthyBoot } from "@ovineko/spa-guard";
40
+ import { recommendedSetup } from "@ovineko/spa-guard/runtime";
41
+
42
+ recommendedSetup();
43
+
44
+ await Promise.all([import("./routes/Home"), import("./routes/Checkout")]);
45
+
46
+ markRetryHealthyBoot();
47
+ ```
48
+
49
+ Disable auto healthy boot:
50
+
51
+ ```ts
52
+ recommendedSetup({ healthyBoot: "manual" });
53
+ ```
54
+
25
55
  Disable version checking:
26
56
 
27
57
  ```ts
@@ -47,7 +77,7 @@ When a recoverable error occurs (chunk load error, unhandled rejection, `vite:pr
47
77
  1. Event listener classifies the event and calls `triggerRetry({ source, error })`.
48
78
  2. Orchestrator checks phase — if already `scheduled` or `fallback`, returns immediately without scheduling another reload.
49
79
  3. On first trigger: reads `RETRY_ATTEMPT_PARAM` from URL to restore attempt count after a reload.
50
- 4. If attempts remain: increments attempt, sets a timer for `reloadDelays[currentAttempt]`, encodes `retryId` and attempt count into the reload URL, then navigates.
80
+ 4. If attempts remain: increments attempt, calls `showLoadingUI(nextAttempt)` to render the loading UI immediately (before the timer fires), sets a timer for `reloadDelays[currentAttempt]`, encodes `retryId` and attempt count into the reload URL, then navigates. Requires `options.html.loading.content` to be configured; if absent, `showLoadingUI` returns silently and the retry still proceeds.
51
81
  5. If attempts are exhausted: transitions to `fallback`, calls `setFallbackMode()`, sends a beacon, and renders fallback UI.
52
82
 
53
83
  ### URL params
@@ -58,13 +88,37 @@ The orchestrator serializes state into URL params for cross-reload continuity:
58
88
  - `RETRY_ID_PARAM` — unique ID for this retry session
59
89
  - `CACHE_BUST_PARAM` — timestamp for cache busting (set when `cacheBust: true` is passed, e.g. for static asset errors)
60
90
 
91
+ ### Loading UI during retry delay
92
+
93
+ When `options.html.loading.content` is configured, `showLoadingUI(attempt)` is called before the reload timer fires. It injects the loading HTML into the target element (selector from `options.html.fallback.selector`, defaulting to `body`), reveals the `data-spa-guard-section="retrying"` element, fills attempt numbers into `[data-spa-guard-content="attempt"]` elements, applies i18n via `applyI18n`/`getI18n`, and optionally hides or replaces the spinner based on `options.html.spinner`. If loading content is not configured or the target element is not found, `showLoadingUI` returns silently — the retry still proceeds normally.
94
+
95
+ ### Unhandledrejection serialization
96
+
97
+ `serializeError` handles `PromiseRejectionEvent` with strict redaction guardrails. For HTTP-like errors it extracts only safe metadata: `status`, `statusText`, `url`, `method`, `response.type`, and `X-Request-ID` from response headers when present. Response body, request/response payload, and the full headers object are **never** included in serialized output. For request wrappers (`reason.request` / `reason.config`) only `method`, `url`, and `baseURL` are extracted. Deep object traversal is bounded by `MAX_DEPTH=4`, `MAX_KEYS=20`, and `MAX_STRING_LEN=500` to prevent oversized beacons. Circular references are handled via a `WeakSet` visited tracker. The output also includes `isTrusted`, `timeStamp` from the event, and runtime context (`pageUrl`, `constructorName`).
98
+
61
99
  ### Retry reset
62
100
 
63
101
  If enough time has passed since the last reload (configurable via `minTimeBetweenResets`, default 5000 ms), the orchestrator resets the attempt counter and starts a fresh retry cycle instead of continuing to fallback. This prevents stale URL params from triggering fallback on a clean page load.
64
102
 
65
103
  ### Healthy boot
66
104
 
67
- After a successful app boot following a retry reload, call `markRetryHealthyBoot()`. This clears retry URL params, cancels any pending timer, and resets orchestrator state.
105
+ After a successful app boot following a retry reload, `markRetryHealthyBoot()` clears retry URL params, cancels any pending timer, and resets orchestrator state.
106
+
107
+ Default behavior in `recommendedSetup`: auto healthy-boot after a grace period (`healthyBoot: "auto"`).
108
+ Manual override: set `healthyBoot: "manual"` and call `markRetryHealthyBoot()` yourself once critical boot is confirmed.
109
+
110
+ ### Avoid false retry loops from app errors
111
+
112
+ By default, regular `unhandledrejection` events also trigger `triggerRetry()` (`handleUnhandledRejections.retry: true`). If your app has non-chunk unhandled rejections, this can look like retry looping even though chunks are healthy. In that case, disable reload-on-unhandled-rejection:
113
+
114
+ ```ts
115
+ window.__SPA_GUARD_OPTIONS__ = {
116
+ handleUnhandledRejections: {
117
+ retry: false,
118
+ sendBeacon: true,
119
+ },
120
+ };
121
+ ```
68
122
 
69
123
  ### Static asset burst coalescing
70
124
 
@@ -100,8 +154,9 @@ Only `retryOrchestrator.ts` may schedule reloads, advance retry state, or transi
100
154
 
101
155
  ### `@ovineko/spa-guard/runtime`
102
156
 
103
- - `recommendedSetup(options?)` — enable recommended runtime features; returns cleanup function
104
- - `RecommendedSetupOptions` — `{ versionCheck?: boolean }`
157
+ - `recommendedSetup(options?)` — enable recommended runtime features; idempotent and returns cleanup function
158
+ - `RecommendedSetupOptions` — `{ versionCheck?: boolean; healthyBoot?: "auto" | "manual" | "off" | false | { mode?: "auto"; graceMs?: number } }`
159
+ - `healthyBoot.graceMs` acts as a lower bound override for auto mode (`max(computedGraceMs, graceMs)`)
105
160
  - `startVersionCheck` / `stopVersionCheck` — manual version check control
106
161
  - `getState` / `subscribeToState` — runtime state access
107
162
  - `SpaGuardState` — state type
package/dist/_internal.js CHANGED
@@ -1,15 +1,15 @@
1
- import {
2
- SPINNER_ID,
3
- defaultSpinnerSvg,
4
- extractVersionFromHtml,
5
- sanitizeCssValue
6
- } from "./chunk-6TP2S7L6.js";
7
1
  import {
8
2
  createLogger,
9
3
  isChunkError,
10
4
  listenInternal,
11
5
  serializeError
12
- } from "./chunk-DINLBER6.js";
6
+ } from "./chunk-4K3AQHLC.js";
7
+ import {
8
+ SPINNER_ID,
9
+ defaultSpinnerSvg,
10
+ extractVersionFromHtml,
11
+ sanitizeCssValue
12
+ } from "./chunk-T42DLOQP.js";
13
13
  import {
14
14
  dispatchAsyncRuntimeError,
15
15
  dispatchChunkLoadError,
@@ -18,7 +18,7 @@ import {
18
18
  dispatchNetworkTimeout,
19
19
  dispatchSyncRuntimeError,
20
20
  dispatchUnhandledRejection
21
- } from "./chunk-A5HF6LY6.js";
21
+ } from "./chunk-ORYUDYHU.js";
22
22
  import {
23
23
  applyI18n,
24
24
  debugSyncErrorEventType,
@@ -40,7 +40,7 @@ import {
40
40
  shouldIgnoreMessages,
41
41
  subscribe,
42
42
  triggerRetry
43
- } from "./chunk-XRBUBTPZ.js";
43
+ } from "./chunk-LIKSIU75.js";
44
44
  import "./chunk-MLKGABMK.js";
45
45
 
46
46
  // src/common/handleErrorWithSpaGuard.ts
@@ -11,7 +11,7 @@ import {
11
11
  shouldIgnoreMessages,
12
12
  staticAssetRecoveryKey,
13
13
  triggerRetry
14
- } from "./chunk-XRBUBTPZ.js";
14
+ } from "./chunk-LIKSIU75.js";
15
15
 
16
16
  // src/common/isChunkError.ts
17
17
  var isChunkError = (error) => {
@@ -58,6 +58,10 @@ var serializeError = (error) => {
58
58
  });
59
59
  }
60
60
  };
61
+ var MAX_DEPTH = 4;
62
+ var MAX_KEYS = 20;
63
+ var MAX_STRING_LEN = 500;
64
+ var truncate = (str) => str.length > MAX_STRING_LEN ? str.slice(0, MAX_STRING_LEN) + "\u2026" : str;
61
65
  var serializeErrorInternal = (error) => {
62
66
  if (error === null || error === void 0) {
63
67
  return { type: "null", value: error };
@@ -69,14 +73,21 @@ var serializeErrorInternal = (error) => {
69
73
  return {
70
74
  message: error.message,
71
75
  name: error.name,
72
- stack: error.stack,
76
+ stack: error.stack ? truncate(error.stack) : void 0,
73
77
  type: "Error",
74
78
  ...extractErrorProperties(error)
75
79
  };
76
80
  }
77
81
  if ("reason" in error && "promise" in error) {
82
+ const evt = error;
83
+ const reason = evt.reason;
84
+ const pageUrl = typeof window !== "undefined" && typeof window.location?.href === "string" ? window.location.href : void 0;
78
85
  return {
79
- reason: serializeErrorInternal(error.reason),
86
+ constructorName: reason?.constructor?.name,
87
+ isTrusted: evt.isTrusted,
88
+ pageUrl,
89
+ reason: serializeRejectionReason(reason, /* @__PURE__ */ new WeakSet(), 0),
90
+ timeStamp: evt.timeStamp,
80
91
  type: "PromiseRejectionEvent"
81
92
  };
82
93
  }
@@ -117,14 +128,158 @@ var serializeErrorInternal = (error) => {
117
128
  value: extractOwnProperties(error)
118
129
  };
119
130
  };
131
+ var serializeRejectionReason = (reason, visited, depth) => {
132
+ if (reason === null || reason === void 0) {
133
+ return { type: "null", value: reason };
134
+ }
135
+ if (typeof reason !== "object") {
136
+ const val = typeof reason === "string" ? truncate(reason) : reason;
137
+ return { type: typeof reason, value: val };
138
+ }
139
+ if (visited.has(reason)) {
140
+ return { type: "circular" };
141
+ }
142
+ visited.add(reason);
143
+ if (typeof AggregateError !== "undefined" && reason instanceof AggregateError) {
144
+ return {
145
+ errors: reason.errors.slice(0, 3).map((e) => serializeSafeError(e, visited, depth + 1)),
146
+ message: truncate(reason.message),
147
+ name: reason.name,
148
+ stack: reason.stack ? truncate(reason.stack) : void 0,
149
+ type: "Error"
150
+ };
151
+ }
152
+ if (typeof DOMException !== "undefined" && reason instanceof DOMException) {
153
+ return {
154
+ code: reason.code,
155
+ message: truncate(reason.message),
156
+ name: reason.name,
157
+ type: "Error"
158
+ };
159
+ }
160
+ if (reason instanceof Error) {
161
+ return serializeSafeError(reason, visited, depth);
162
+ }
163
+ const reasonObj = reason;
164
+ if (reasonObj.response != null) {
165
+ const response = reasonObj.response;
166
+ const result = {
167
+ type: "HttpError"
168
+ };
169
+ if (response.status !== void 0) {
170
+ result.status = response.status;
171
+ }
172
+ if (response.statusText !== void 0) {
173
+ result.statusText = typeof response.statusText === "string" ? truncate(response.statusText) : response.statusText;
174
+ }
175
+ if (response.url !== void 0) {
176
+ result.url = typeof response.url === "string" ? truncate(response.url) : response.url;
177
+ }
178
+ if (response.method !== void 0) {
179
+ result.method = response.method;
180
+ }
181
+ if (response.type !== void 0) {
182
+ result.responseType = response.type;
183
+ }
184
+ try {
185
+ const headers = response.headers;
186
+ if (headers) {
187
+ let xRequestId;
188
+ if (typeof headers.get === "function") {
189
+ xRequestId = headers.get("X-Request-ID") ?? headers.get("x-request-id");
190
+ } else if (typeof headers === "object") {
191
+ xRequestId = headers["X-Request-ID"] ?? headers["x-request-id"];
192
+ }
193
+ if (xRequestId) {
194
+ result.xRequestId = truncate(String(xRequestId));
195
+ }
196
+ }
197
+ } catch {
198
+ }
199
+ const reqSource = reasonObj.config ?? reasonObj.request;
200
+ if (reqSource != null) {
201
+ const req = {};
202
+ if (reqSource.method !== void 0) {
203
+ req.method = reqSource.method;
204
+ }
205
+ if (reqSource.url !== void 0) {
206
+ req.url = typeof reqSource.url === "string" ? truncate(reqSource.url) : reqSource.url;
207
+ }
208
+ if (reqSource.baseURL !== void 0) {
209
+ req.baseURL = typeof reqSource.baseURL === "string" ? truncate(reqSource.baseURL) : reqSource.baseURL;
210
+ }
211
+ result.request = req;
212
+ }
213
+ return result;
214
+ }
215
+ return {
216
+ type: "object",
217
+ value: extractBoundedObject(reasonObj, visited, depth)
218
+ };
219
+ };
220
+ var serializeSafeError = (error, visited, depth) => {
221
+ if (!(error instanceof Error)) {
222
+ return serializeRejectionReason(error, visited, depth + 1);
223
+ }
224
+ const result = {
225
+ message: truncate(error.message),
226
+ name: error.name,
227
+ stack: error.stack ? truncate(error.stack) : void 0,
228
+ type: "Error"
229
+ };
230
+ if (depth < MAX_DEPTH && error.cause !== void 0) {
231
+ const cause = error.cause;
232
+ if (typeof cause === "object" && cause !== null) {
233
+ if (!visited.has(cause)) {
234
+ result.cause = serializeRejectionReason(cause, visited, depth + 1);
235
+ }
236
+ } else {
237
+ result.cause = cause;
238
+ }
239
+ }
240
+ return result;
241
+ };
242
+ var extractBoundedObject = (obj, visited, depth) => {
243
+ const result = {};
244
+ let keyCount = 0;
245
+ for (const key of Object.keys(obj)) {
246
+ if (keyCount >= MAX_KEYS) {
247
+ break;
248
+ }
249
+ try {
250
+ const value = obj[key];
251
+ if (value === null || value === void 0 || typeof value !== "object") {
252
+ result[key] = typeof value === "string" ? truncate(value) : value;
253
+ } else if (depth < MAX_DEPTH && !visited.has(value)) {
254
+ visited.add(value);
255
+ result[key] = extractBoundedObject(value, visited, depth + 1);
256
+ } else {
257
+ result[key] = "[object]";
258
+ }
259
+ } catch {
260
+ }
261
+ keyCount++;
262
+ }
263
+ return result;
264
+ };
120
265
  var extractErrorProperties = (error) => {
121
266
  const props = {};
267
+ let keyCount = 0;
122
268
  for (const key of Object.getOwnPropertyNames(error)) {
269
+ if (keyCount >= MAX_KEYS) {
270
+ break;
271
+ }
123
272
  if (!["message", "name", "stack"].includes(key)) {
124
273
  try {
125
- props[key] = error[key];
274
+ const value = error[key];
275
+ if (value === null || value === void 0 || typeof value !== "object") {
276
+ props[key] = typeof value === "string" ? truncate(value) : value;
277
+ } else {
278
+ props[key] = "[object]";
279
+ }
126
280
  } catch {
127
281
  }
282
+ keyCount++;
128
283
  }
129
284
  }
130
285
  return props;
@@ -146,12 +301,21 @@ var extractEventTarget = (target) => {
146
301
  };
147
302
  var extractOwnProperties = (obj) => {
148
303
  const props = {};
304
+ let keyCount = 0;
149
305
  for (const key of Object.keys(obj)) {
306
+ if (keyCount >= MAX_KEYS) {
307
+ break;
308
+ }
150
309
  try {
151
310
  const value = obj[key];
152
- props[key] = typeof value === "object" ? String(value) : value;
311
+ if (value === null || value === void 0 || typeof value !== "object") {
312
+ props[key] = typeof value === "string" ? truncate(value) : value;
313
+ } else {
314
+ props[key] = "[object]";
315
+ }
153
316
  } catch {
154
317
  }
318
+ keyCount++;
155
319
  }
156
320
  return props;
157
321
  };
@@ -247,6 +411,9 @@ var handleStaticAssetFailure = (url) => {
247
411
 
248
412
  // src/common/listen/internal.ts
249
413
  var listenInternal = (serializeError2, logger) => {
414
+ if (globalThis.window === void 0) {
415
+ return;
416
+ }
250
417
  if (isInitialized()) {
251
418
  return;
252
419
  }
@@ -254,7 +421,6 @@ var listenInternal = (serializeError2, logger) => {
254
421
  setLogger(logger);
255
422
  }
256
423
  markInitialized();
257
- const options = getOptions();
258
424
  const wa = globalThis.window.addEventListener.bind(globalThis.window);
259
425
  wa(
260
426
  "error",
@@ -266,7 +432,7 @@ var listenInternal = (serializeError2, logger) => {
266
432
  }
267
433
  event.preventDefault();
268
434
  emitEvent({ name: "static-asset-load-failed", url: assetUrl });
269
- if (options.staticAssets?.autoRecover !== false) {
435
+ if (getOptions().staticAssets?.autoRecover !== false) {
270
436
  handleStaticAssetFailure(assetUrl);
271
437
  }
272
438
  return;
@@ -311,7 +477,7 @@ var listenInternal = (serializeError2, logger) => {
311
477
  triggerRetry({ error: event.reason, source: "force-retry" });
312
478
  return;
313
479
  }
314
- const rejectionConfig = options.handleUnhandledRejections;
480
+ const rejectionConfig = getOptions().handleUnhandledRejections;
315
481
  if (rejectionConfig?.sendBeacon !== false) {
316
482
  const serialized = serializeError2(event);
317
483
  sendBeacon({
@@ -413,9 +579,6 @@ var createLogger = () => ({
413
579
  capturedError(type, ...args) {
414
580
  console.error(`${PREFIX} ${type}:capture:`, ...args);
415
581
  },
416
- clearingRetryState() {
417
- console.log(`${PREFIX} Clearing retry state from URL to allow clean reload attempt`);
418
- },
419
582
  error(msg, ...args) {
420
583
  console.error(`${PREFIX} ${msg}`, ...args);
421
584
  },
@@ -455,17 +618,11 @@ var createLogger = () => ({
455
618
  retryCycleStarting(retryId, fromAttempt) {
456
619
  console.log(`${PREFIX} Retry cycle starting: retryId=${retryId}, fromAttempt=${fromAttempt}`);
457
620
  },
458
- retryLimitExceeded(attempt, max) {
459
- console.log(`${PREFIX} Retry limit exceeded (${attempt}/${max}), marking as fallback shown`);
460
- },
461
621
  retrySchedulingReload(retryId, attempt, delay) {
462
622
  console.log(
463
623
  `${PREFIX} Scheduling reload: retryId=${retryId}, attempt=${attempt}, delay=${delay}ms`
464
624
  );
465
625
  },
466
- updatedRetryAttempt(attempt) {
467
- console.log(`${PREFIX} Updated retry attempt to ${attempt} in URL for fallback UI`);
468
- },
469
626
  versionChangeDetected(oldVersion, latestVersion) {
470
627
  console.warn(
471
628
  `${PREFIX} New version available (${oldVersion ?? "unknown"} \u2192 ${latestVersion}). Please refresh to get the latest version.`
@@ -3,7 +3,7 @@ import {
3
3
  getRetryAttemptFromUrl,
4
4
  getRetryStateFromUrl,
5
5
  subscribe
6
- } from "./chunk-XRBUBTPZ.js";
6
+ } from "./chunk-LIKSIU75.js";
7
7
 
8
8
  // src/runtime/state.ts
9
9
  var getInitialStateFromUrl = () => {
@@ -327,7 +327,48 @@ var getRetryInfoForBeacon = () => {
327
327
  };
328
328
 
329
329
  // src/common/fallbackRendering.ts
330
- var showFallbackUI = () => {
330
+ var showLoadingUI = (attempt) => {
331
+ const options = getOptions();
332
+ const loadingHtml = options.html?.loading?.content;
333
+ if (!loadingHtml) {
334
+ return;
335
+ }
336
+ const selector = options.html?.fallback?.selector ?? "body";
337
+ try {
338
+ const targetElement = document.querySelector(selector);
339
+ if (!targetElement) {
340
+ return;
341
+ }
342
+ const container = document.createElement("div");
343
+ container.innerHTML = loadingHtml;
344
+ const t = getI18n();
345
+ if (t) {
346
+ applyI18n(container, t);
347
+ }
348
+ targetElement.innerHTML = container.innerHTML;
349
+ const retrySection = targetElement.querySelector('[data-spa-guard-section="retrying"]');
350
+ if (retrySection) {
351
+ const retrySectionEl = retrySection;
352
+ retrySectionEl.style.display = "";
353
+ retrySectionEl.style.visibility = "visible";
354
+ }
355
+ const attemptElements = targetElement.querySelectorAll('[data-spa-guard-content="attempt"]');
356
+ for (const el of attemptElements) {
357
+ el.textContent = String(attempt);
358
+ }
359
+ const spinnerEl = targetElement.querySelector("[data-spa-guard-spinner]");
360
+ if (spinnerEl) {
361
+ const spinnerOptions = options.html?.spinner;
362
+ if (spinnerOptions?.disabled) {
363
+ spinnerEl.style.display = "none";
364
+ } else if (spinnerOptions?.content) {
365
+ spinnerEl.innerHTML = spinnerOptions.content;
366
+ }
367
+ }
368
+ } catch {
369
+ }
370
+ };
371
+ var showFallbackUI = (override) => {
331
372
  const options = getOptions();
332
373
  const fallbackHtml = options.html?.fallback?.content;
333
374
  const selector = options.html?.fallback?.selector ?? "body";
@@ -354,11 +395,11 @@ var showFallbackUI = () => {
354
395
  if (reloadBtn) {
355
396
  reloadBtn.addEventListener("click", () => globalThis.window.location.reload());
356
397
  }
357
- const retryState = getRetryStateFromUrl();
358
- if (retryState) {
398
+ const retryId = override?.retryId ?? getRetryStateFromUrl()?.retryId;
399
+ if (retryId) {
359
400
  const retryIdElements = document.getElementsByClassName("spa-guard-retry-id");
360
401
  for (const element of retryIdElements) {
361
- element.textContent = retryState.retryId;
402
+ element.textContent = retryId;
362
403
  }
363
404
  }
364
405
  emitEvent({
@@ -703,7 +744,8 @@ var triggerRetry = (input = {}) => {
703
744
  });
704
745
  setState({ attempt: currentAttempt, phase: "fallback", retryId });
705
746
  setFallbackMode();
706
- showFallbackUI();
747
+ clearRetryFromUrl();
748
+ showFallbackUI({ retryId });
707
749
  return { status: "fallback" };
708
750
  }
709
751
  const nextAttempt = currentAttempt + 1;
@@ -720,6 +762,7 @@ var triggerRetry = (input = {}) => {
720
762
  );
721
763
  getLogger()?.retrySchedulingReload(retryId, nextAttempt, delay);
722
764
  setState({ attempt: nextAttempt, retryId });
765
+ showLoadingUI(nextAttempt);
723
766
  const timer = setTimeout(() => {
724
767
  try {
725
768
  if (useRetryId && enableRetryReset) {
@@ -729,7 +772,7 @@ var triggerRetry = (input = {}) => {
729
772
  globalThis.window.location.href = reloadUrl;
730
773
  } catch (navError) {
731
774
  getLogger()?.error("triggerRetry navigation failed", navError);
732
- setState({ phase: "idle" });
775
+ setState({ phase: "idle", timer: null });
733
776
  }
734
777
  }, delay);
735
778
  setState({ timer });
@@ -737,7 +780,7 @@ var triggerRetry = (input = {}) => {
737
780
  } catch (error) {
738
781
  getLogger()?.error("triggerRetry internal error", error);
739
782
  setState({ lastSource: void 0, lastTriggerTime: void 0, phase: "idle" });
740
- return { reason: "internal-error", status: "deduped" };
783
+ return { status: "internal-error" };
741
784
  }
742
785
  };
743
786
  var markRetryHealthyBoot = () => {
@@ -768,7 +811,7 @@ var setFallbackStateForDebug = () => {
768
811
  }
769
812
  setState({ phase: "fallback", timer: null });
770
813
  setFallbackMode();
771
- showFallbackUI();
814
+ showFallbackUI({ retryId: state.retryId ?? void 0 });
772
815
  };
773
816
  var resetRetryOrchestratorForTests = () => {
774
817
  const state = getState();
@@ -4,7 +4,7 @@ import {
4
4
  emitEvent,
5
5
  getOptions,
6
6
  setFallbackStateForDebug
7
- } from "./chunk-XRBUBTPZ.js";
7
+ } from "./chunk-LIKSIU75.js";
8
8
 
9
9
  // src/runtime/debug/errorDispatchers.ts
10
10
  function dispatchAsyncRuntimeError() {
@@ -2,7 +2,7 @@ import {
2
2
  defaultSpinnerHtml,
3
3
  getOptions,
4
4
  spinnerStateWindowKey
5
- } from "./chunk-XRBUBTPZ.js";
5
+ } from "./chunk-LIKSIU75.js";
6
6
 
7
7
  // src/common/parseVersion.ts
8
8
  function extractVersionFromHtml(html) {
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Renders the loading UI into the DOM during a retry delay before reload.
3
+ *
4
+ * Fail-safe: if loading content is not configured or the target element is not
5
+ * found, returns silently with no log, no event, and no side effects.
6
+ * All DOM access is wrapped in try/catch.
7
+ */
8
+ export declare const showLoadingUI: (attempt: number) => void;
1
9
  /**
2
10
  * Renders the fallback UI into the DOM.
3
11
  *
@@ -10,4 +18,6 @@
10
18
  * Fails safely: if fallback HTML is not configured or the target element
11
19
  * is not found, logs a warning and returns without side effects or errors.
12
20
  */
13
- export declare const showFallbackUI: () => void;
21
+ export declare const showFallbackUI: (override?: {
22
+ retryId?: string;
23
+ }) => void;
@@ -5,3 +5,5 @@ export { disableDefaultRetry, enableDefaultRetry, isDefaultRetryEnabled } from "
5
5
  export { isInFallbackMode, resetFallbackMode } from "./fallbackState";
6
6
  export { listen } from "./listen";
7
7
  export * as options from "./options";
8
+ export { getRetrySnapshot, markRetryHealthyBoot, triggerRetry } from "./retryOrchestrator";
9
+ export type { RetryPhase, RetrySnapshot, TriggerInput, TriggerResult } from "./retryOrchestrator";
@@ -2,18 +2,21 @@ import {
2
2
  createLogger,
3
3
  listenInternal,
4
4
  serializeError
5
- } from "../chunk-DINLBER6.js";
5
+ } from "../chunk-4K3AQHLC.js";
6
6
  import {
7
7
  ForceRetryError,
8
8
  disableDefaultRetry,
9
9
  emitEvent,
10
10
  enableDefaultRetry,
11
+ getRetrySnapshot,
11
12
  isDefaultRetryEnabled,
12
13
  isInFallbackMode,
14
+ markRetryHealthyBoot,
13
15
  options_exports,
14
16
  resetFallbackMode,
15
- subscribe
16
- } from "../chunk-XRBUBTPZ.js";
17
+ subscribe,
18
+ triggerRetry
19
+ } from "../chunk-LIKSIU75.js";
17
20
  import {
18
21
  __export
19
22
  } from "../chunk-MLKGABMK.js";
@@ -73,9 +76,12 @@ export {
73
76
  disableDefaultRetry,
74
77
  enableDefaultRetry,
75
78
  events_exports as events,
79
+ getRetrySnapshot,
76
80
  isDefaultRetryEnabled,
77
81
  isInFallbackMode,
78
82
  listen,
83
+ markRetryHealthyBoot,
79
84
  options_exports as options,
80
- resetFallbackMode
85
+ resetFallbackMode,
86
+ triggerRetry
81
87
  };
@@ -2,7 +2,6 @@ import type { SPAGuardEvent } from "./events/types";
2
2
  export interface Logger {
3
3
  beaconSendFailed(error: unknown): void;
4
4
  capturedError(type: string, ...args: unknown[]): void;
5
- clearingRetryState(): void;
6
5
  error(msg: string, ...args: unknown[]): void;
7
6
  fallbackAlreadyShown(error: unknown): void;
8
7
  fallbackInjectFailed(error: unknown): void;
@@ -13,9 +12,7 @@ export interface Logger {
13
12
  noFallbackConfigured(): void;
14
13
  reloadAlreadyScheduled(error: unknown): void;
15
14
  retryCycleStarting(retryId: string, fromAttempt: number): void;
16
- retryLimitExceeded(attempt: number, max: number): void;
17
15
  retrySchedulingReload(retryId: string, attempt: number, delay: number): void;
18
- updatedRetryAttempt(attempt: number): void;
19
16
  versionChangeDetected(oldVersion: null | string, latestVersion: string): void;
20
17
  versionCheckAlreadyRunning(): void;
21
18
  versionCheckDisabled(): void;
@@ -18,6 +18,8 @@ export type TriggerResult = {
18
18
  status: "accepted";
19
19
  } | {
20
20
  status: "fallback";
21
+ } | {
22
+ status: "internal-error";
21
23
  } | {
22
24
  status: "retry-disabled";
23
25
  };
@@ -5,7 +5,7 @@ var translations = {
5
5
  ar: {
6
6
  heading: "\u062D\u062F\u062B \u062E\u0637\u0623 \u0645\u0627",
7
7
  loading: "...\u062C\u0627\u0631\u064D \u0627\u0644\u062A\u062D\u0645\u064A\u0644",
8
- message: "\u064A\u0631\u062C\u0649 \u062A\u062D\u062F\u064A\u062B \u0627\u0644\u0635\u0641\u062D\u0629 \u0644\u0644\u0645\u062A\u0627\u0628\u0639\u0629.",
8
+ message: "\u064A\u0631\u062C\u0649 \u062A\u062D\u062F\u064A\u062B \u0627\u0644\u0635\u0641\u062D\u0629 \u0644\u0644\u0645\u062A\u0627\u0628\u0639\u0629",
9
9
  reload: "\u0625\u0639\u0627\u062F\u0629 \u062A\u062D\u0645\u064A\u0644",
10
10
  retrying: "\u0645\u062D\u0627\u0648\u0644\u0629 \u0625\u0639\u0627\u062F\u0629",
11
11
  rtl: true,
@@ -14,7 +14,7 @@ var translations = {
14
14
  az: {
15
15
  heading: "N\u0259s\u0259 s\u0259hv getdi",
16
16
  loading: "Y\xFCkl\u0259nir...",
17
- message: "Davam etm\u0259k \xFC\xE7\xFCn s\u0259hif\u0259ni yenil\u0259yin.",
17
+ message: "Davam etm\u0259k \xFC\xE7\xFCn s\u0259hif\u0259ni yenil\u0259yin",
18
18
  reload: "S\u0259hif\u0259ni yenid\u0259n y\xFCkl\u0259",
19
19
  retrying: "Yenid\u0259n c\u0259hd",
20
20
  tryAgain: "Yenid\u0259n c\u0259r\u0259b edin"
@@ -22,7 +22,7 @@ var translations = {
22
22
  ca: {
23
23
  heading: "Alguna cosa ha anat malament",
24
24
  loading: "Carregant...",
25
- message: "Si us plau, actualitzeu la p\xE0gina per continuar.",
25
+ message: "Si us plau, actualitzeu la p\xE0gina per continuar",
26
26
  reload: "Recarrega la p\xE0gina",
27
27
  retrying: "Intent de reintent",
28
28
  tryAgain: "Torna-ho a provar"
@@ -30,7 +30,7 @@ var translations = {
30
30
  cs: {
31
31
  heading: "N\u011Bco se pokazilo",
32
32
  loading: "Na\u010D\xEDt\xE1n\xED...",
33
- message: "Obnovte str\xE1nku pros\xEDm pro pokra\u010Dov\xE1n\xED.",
33
+ message: "Obnovte str\xE1nku pros\xEDm pro pokra\u010Dov\xE1n\xED",
34
34
  reload: "Znovu na\u010D\xEDst str\xE1nku",
35
35
  retrying: "Pokus o opakov\xE1n\xED",
36
36
  tryAgain: "Zkusit znovu"
@@ -38,7 +38,7 @@ var translations = {
38
38
  da: {
39
39
  heading: "Noget gik galt",
40
40
  loading: "Indl\xE6ser...",
41
- message: "Opdater venligst siden for at forts\xE6tte.",
41
+ message: "Opdater venligst siden for at forts\xE6tte",
42
42
  reload: "Genindl\xE6s side",
43
43
  retrying: "Fors\xF8g igen",
44
44
  tryAgain: "Pr\xF8v igen"
@@ -46,7 +46,7 @@ var translations = {
46
46
  de: {
47
47
  heading: "Etwas ist schief gelaufen",
48
48
  loading: "L\xE4dt...",
49
- message: "Bitte aktualisieren Sie die Seite, um fortzufahren.",
49
+ message: "Bitte aktualisieren Sie die Seite, um fortzufahren",
50
50
  reload: "Seite neu laden",
51
51
  retrying: "Wiederholungsversuch",
52
52
  tryAgain: "Erneut versuchen"
@@ -54,7 +54,7 @@ var translations = {
54
54
  el: {
55
55
  heading: "\u039A\u03AC\u03C4\u03B9 \u03C0\u03AE\u03B3\u03B5 \u03C3\u03C4\u03C1\u03B1\u03B2\u03AC",
56
56
  loading: "\u03A6\u03CC\u03C1\u03C4\u03C9\u03C3\u03B7...",
57
- message: "\u03A0\u03B1\u03C1\u03B1\u03BA\u03B1\u03BB\u03CE \u03B1\u03BD\u03B1\u03BD\u03B5\u03CE\u03C3\u03C4\u03B5 \u03C4\u03B7 \u03C3\u03B5\u03BB\u03AF\u03B4\u03B1 \u03B3\u03B9\u03B1 \u03BD\u03B1 \u03C3\u03C5\u03BD\u03B5\u03C7\u03AF\u03C3\u03B5\u03C4\u03B5.",
57
+ message: "\u03A0\u03B1\u03C1\u03B1\u03BA\u03B1\u03BB\u03CE \u03B1\u03BD\u03B1\u03BD\u03B5\u03CE\u03C3\u03C4\u03B5 \u03C4\u03B7 \u03C3\u03B5\u03BB\u03AF\u03B4\u03B1 \u03B3\u03B9\u03B1 \u03BD\u03B1 \u03C3\u03C5\u03BD\u03B5\u03C7\u03AF\u03C3\u03B5\u03C4\u03B5",
58
58
  reload: "\u0395\u03C0\u03B1\u03BD\u03B1\u03C6\u03CC\u03C1\u03C4\u03C9\u03C3\u03B7 \u03C3\u03B5\u03BB\u03AF\u03B4\u03B1\u03C2",
59
59
  retrying: "\u03A0\u03C1\u03BF\u03C3\u03C0\u03AC\u03B8\u03B5\u03B9\u03B1 \u03B5\u03C0\u03B1\u03BD\u03AC\u03BB\u03B7\u03C8\u03B7\u03C2",
60
60
  tryAgain: "\u0394\u03BF\u03BA\u03B9\u03BC\u03AC\u03C3\u03C4\u03B5 \u03BE\u03B1\u03BD\u03AC"
@@ -62,7 +62,7 @@ var translations = {
62
62
  en: {
63
63
  heading: "Something went wrong",
64
64
  loading: "Loading...",
65
- message: "Please refresh the page to continue.",
65
+ message: "Please refresh the page to continue",
66
66
  reload: "Reload page",
67
67
  retrying: "Retry attempt",
68
68
  tryAgain: "Try again"
@@ -70,7 +70,7 @@ var translations = {
70
70
  es: {
71
71
  heading: "Algo sali\xF3 mal",
72
72
  loading: "Cargando...",
73
- message: "Por favor, actualice la p\xE1gina para continuar.",
73
+ message: "Por favor, actualice la p\xE1gina para continuar",
74
74
  reload: "Recargar p\xE1gina",
75
75
  retrying: "Intento de reintento",
76
76
  tryAgain: "Intentar de nuevo"
@@ -78,7 +78,7 @@ var translations = {
78
78
  eu: {
79
79
  heading: "Zerbait gaizki joan da",
80
80
  loading: "Kargatzen...",
81
- message: "Mesedez, freskatu orria jarraitzeko.",
81
+ message: "Mesedez, freskatu orria jarraitzeko",
82
82
  reload: "Orria berritu",
83
83
  retrying: "Saiakera berri",
84
84
  tryAgain: "Saiatu berriro"
@@ -86,7 +86,7 @@ var translations = {
86
86
  fa: {
87
87
  heading: "\u0645\u0634\u06A9\u0644\u06CC \u067E\u06CC\u0634 \u0622\u0645\u062F",
88
88
  loading: "\u062F\u0631 \u062D\u0627\u0644 \u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC...",
89
- message: "\u0644\u0637\u0641\u0627\u064B \u0635\u0641\u062D\u0647 \u0631\u0627 \u0628\u0631\u0627\u06CC \u0627\u062F\u0627\u0645\u0647 \u062A\u0627\u0632\u0647 \u06A9\u0646\u06CC\u062F.",
89
+ message: "\u0644\u0637\u0641\u0627\u064B \u0635\u0641\u062D\u0647 \u0631\u0627 \u0628\u0631\u0627\u06CC \u0627\u062F\u0627\u0645\u0647 \u062A\u0627\u0632\u0647 \u06A9\u0646\u06CC\u062F",
90
90
  reload: "\u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC \u0645\u062C\u062F\u062F",
91
91
  retrying: "\u062A\u0644\u0627\u0634 \u062F\u0648\u0628\u0627\u0631\u0647",
92
92
  rtl: true,
@@ -95,7 +95,7 @@ var translations = {
95
95
  fi: {
96
96
  heading: "Jokin meni pieleen",
97
97
  loading: "Ladataan...",
98
- message: "P\xE4ivit\xE4 sivu jatkaaksesi.",
98
+ message: "P\xE4ivit\xE4 sivu jatkaaksesi",
99
99
  reload: "Lataa sivu uudelleen",
100
100
  retrying: "Uudelleenyritys",
101
101
  tryAgain: "Yrit\xE4 uudelleen"
@@ -103,7 +103,7 @@ var translations = {
103
103
  fr: {
104
104
  heading: "Quelque chose s'est mal pass\xE9",
105
105
  loading: "Chargement...",
106
- message: "Veuillez actualiser la page pour continuer.",
106
+ message: "Veuillez actualiser la page pour continuer",
107
107
  reload: "Recharger la page",
108
108
  retrying: "Tentative de nouvel essai",
109
109
  tryAgain: "R\xE9essayer"
@@ -111,7 +111,7 @@ var translations = {
111
111
  he: {
112
112
  heading: "\u05DE\u05E9\u05D4\u05D5 \u05D4\u05E9\u05EA\u05D1\u05E9",
113
113
  loading: "...\u05D8\u05D5\u05E2\u05DF",
114
- message: "\u05D0\u05E0\u05D0 \u05E8\u05E2\u05E0\u05DF \u05D0\u05EA \u05D4\u05D3\u05E3 \u05DB\u05D3\u05D9 \u05DC\u05D4\u05DE\u05E9\u05D9\u05DA.",
114
+ message: "\u05D0\u05E0\u05D0 \u05E8\u05E2\u05E0\u05DF \u05D0\u05EA \u05D4\u05D3\u05E3 \u05DB\u05D3\u05D9 \u05DC\u05D4\u05DE\u05E9\u05D9\u05DA",
115
115
  reload: "\u05D8\u05E2\u05DF \u05DE\u05D7\u05D3\u05E9",
116
116
  retrying: "\u05E0\u05D9\u05E1\u05D9\u05D5\u05DF \u05D7\u05D5\u05D6\u05E8",
117
117
  rtl: true,
@@ -120,7 +120,7 @@ var translations = {
120
120
  hr: {
121
121
  heading: "Ne\u0161to je po\u0161lo po zlu",
122
122
  loading: "U\u010Ditavanje...",
123
- message: "Molimo osvje\u017Eite stranicu da biste nastavili.",
123
+ message: "Molimo osvje\u017Eite stranicu da biste nastavili",
124
124
  reload: "Ponovno u\u010Ditaj stranicu",
125
125
  retrying: "Poku\u0161aj ponovnog poku\u0161aja",
126
126
  tryAgain: "Poku\u0161aj ponovo"
@@ -128,7 +128,7 @@ var translations = {
128
128
  hu: {
129
129
  heading: "Valami hiba t\xF6rt\xE9nt",
130
130
  loading: "Bet\xF6lt\xE9s...",
131
- message: "K\xE9rj\xFCk, friss\xEDtse az oldalt a folytat\xE1shoz.",
131
+ message: "K\xE9rj\xFCk, friss\xEDtse az oldalt a folytat\xE1shoz",
132
132
  reload: "Oldal \xFAjrat\xF6lt\xE9se",
133
133
  retrying: "\xDAjrapr\xF3b\xE1lkoz\xE1si k\xEDs\xE9rlet",
134
134
  tryAgain: "Pr\xF3b\xE1lja \xFAjra"
@@ -136,7 +136,7 @@ var translations = {
136
136
  id: {
137
137
  heading: "Terjadi kesalahan",
138
138
  loading: "Memuat...",
139
- message: "Silakan segarkan halaman untuk melanjutkan.",
139
+ message: "Silakan segarkan halaman untuk melanjutkan",
140
140
  reload: "Muat ulang halaman",
141
141
  retrying: "Percobaan ulang",
142
142
  tryAgain: "Coba lagi"
@@ -144,7 +144,7 @@ var translations = {
144
144
  it: {
145
145
  heading: "Qualcosa \xE8 andato storto",
146
146
  loading: "Caricamento...",
147
- message: "Aggiorna la pagina per continuare.",
147
+ message: "Aggiorna la pagina per continuare",
148
148
  reload: "Ricarica pagina",
149
149
  retrying: "Tentativo di ripetizione",
150
150
  tryAgain: "Riprova"
@@ -152,7 +152,7 @@ var translations = {
152
152
  ja: {
153
153
  heading: "\u554F\u984C\u304C\u767A\u751F\u3057\u307E\u3057\u305F",
154
154
  loading: "\u8AAD\u307F\u8FBC\u307F\u4E2D...",
155
- message: "\u30DA\u30FC\u30B8\u3092\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
155
+ message: "\u30DA\u30FC\u30B8\u3092\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044",
156
156
  reload: "\u518D\u8AAD\u307F\u8FBC\u307F",
157
157
  retrying: "\u30EA\u30C8\u30E9\u30A4",
158
158
  tryAgain: "\u3082\u3046\u4E00\u5EA6\u8A66\u3059"
@@ -160,7 +160,7 @@ var translations = {
160
160
  ka: {
161
161
  heading: "\u10E0\u10D0\u10E6\u10D0\u10EA \u10D0\u10E0\u10D0\u10E1\u10EC\u10DD\u10E0\u10D0\u10D3 \u10DB\u10DD\u10EE\u10D3\u10D0",
162
162
  loading: "\u10D8\u10E2\u10D5\u10D8\u10E0\u10D7\u10D4\u10D1\u10D0...",
163
- message: "\u10D2\u10D7\u10EE\u10DD\u10D5\u10D7 \u10D2\u10D0\u10DC\u10D0\u10D0\u10EE\u10DA\u10DD\u10D7 \u10D2\u10D5\u10D4\u10E0\u10D3\u10D8 \u10D2\u10D0\u10E1\u10D0\u10D2\u10E0\u10EB\u10D4\u10DA\u10D4\u10D1\u10DA\u10D0\u10D3.",
163
+ message: "\u10D2\u10D7\u10EE\u10DD\u10D5\u10D7 \u10D2\u10D0\u10DC\u10D0\u10D0\u10EE\u10DA\u10DD\u10D7 \u10D2\u10D5\u10D4\u10E0\u10D3\u10D8 \u10D2\u10D0\u10E1\u10D0\u10D2\u10E0\u10EB\u10D4\u10DA\u10D4\u10D1\u10DA\u10D0\u10D3",
164
164
  reload: "\u10D2\u10D5\u10D4\u10E0\u10D3\u10D8\u10E1 \u10D2\u10D0\u10D3\u10D0\u10E2\u10D5\u10D8\u10E0\u10D7\u10D5\u10D0",
165
165
  retrying: "\u10D2\u10D0\u10DB\u10D4\u10DD\u10E0\u10D4\u10D1\u10D8\u10E1 \u10DB\u10EA\u10D3\u10D4\u10DA\u10DD\u10D1\u10D0",
166
166
  tryAgain: "\u10D9\u10D8\u10D3\u10D4\u10D5 \u10E1\u10EA\u10D0\u10D3\u10D4\u10D7"
@@ -168,7 +168,7 @@ var translations = {
168
168
  kk: {
169
169
  heading: "\u0411\u0456\u0440\u0434\u0435\u04A3\u0435 \u0434\u04B1\u0440\u044B\u0441 \u0431\u043E\u043B\u043C\u0430\u0434\u044B",
170
170
  loading: "\u0416\u04AF\u043A\u0442\u0435\u043B\u0443\u0434\u0435...",
171
- message: "\u0416\u0430\u043B\u0493\u0430\u0441\u0442\u044B\u0440\u0443 \u04AF\u0448\u0456\u043D \u0431\u0435\u0442\u0442\u0456 \u0436\u0430\u04A3\u0430\u0440\u0442\u044B\u04A3\u044B\u0437.",
171
+ message: "\u0416\u0430\u043B\u0493\u0430\u0441\u0442\u044B\u0440\u0443 \u04AF\u0448\u0456\u043D \u0431\u0435\u0442\u0442\u0456 \u0436\u0430\u04A3\u0430\u0440\u0442\u044B\u04A3\u044B\u0437",
172
172
  reload: "\u0411\u0435\u0442\u0442\u0456 \u049B\u0430\u0439\u0442\u0430 \u0436\u04AF\u043A\u0442\u0435\u0443",
173
173
  retrying: "\u049A\u0430\u0439\u0442\u0430\u043B\u0430\u0443 \u04D9\u0440\u0435\u043A\u0435\u0442\u0456",
174
174
  tryAgain: "\u049A\u0430\u0439\u0442\u0430\u043B\u0430\u043F \u043A\u04E9\u0440\u0456\u04A3\u0456\u0437"
@@ -176,7 +176,7 @@ var translations = {
176
176
  ko: {
177
177
  heading: "\uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
178
178
  loading: "\uB85C\uB529 \uC911...",
179
- message: "\uD398\uC774\uC9C0\uB97C \uC0C8\uB85C\uACE0\uCE68\uD574 \uC8FC\uC138\uC694.",
179
+ message: "\uD398\uC774\uC9C0\uB97C \uC0C8\uB85C\uACE0\uCE68\uD574 \uC8FC\uC138\uC694",
180
180
  reload: "\uC0C8\uB85C\uACE0\uCE68",
181
181
  retrying: "\uC7AC\uC2DC\uB3C4",
182
182
  tryAgain: "\uB2E4\uC2DC \uC2DC\uB3C4"
@@ -184,7 +184,7 @@ var translations = {
184
184
  ky: {
185
185
  heading: "\u0411\u0438\u0440 \u043D\u0435\u0440\u0441\u0435 \u0442\u0443\u0443\u0440\u0430 \u044D\u043C\u0435\u0441 \u0431\u043E\u043B\u0434\u0443",
186
186
  loading: "\u0416\u04AF\u043A\u0442\u04E9\u043B\u04AF\u04AF\u0434\u04E9...",
187
- message: "\u0423\u043B\u0430\u043D\u0442\u0443\u0443 \u04AF\u0447\u04AF\u043D \u0431\u0430\u0440\u0430\u043A\u0442\u044B \u0436\u0430\u04A3\u044B\u0440\u0442\u044B\u04A3\u044B\u0437.",
187
+ message: "\u0423\u043B\u0430\u043D\u0442\u0443\u0443 \u04AF\u0447\u04AF\u043D \u0431\u0430\u0440\u0430\u043A\u0442\u044B \u0436\u0430\u04A3\u044B\u0440\u0442\u044B\u04A3\u044B\u0437",
188
188
  reload: "\u0411\u0430\u0440\u0430\u043A\u0442\u044B \u043A\u0430\u0439\u0440\u0430 \u0436\u04AF\u043A\u0442\u04E9\u04E9",
189
189
  retrying: "\u041A\u0430\u0439\u0442\u0430\u043B\u043E\u043E \u0430\u0440\u0430\u043A\u0435\u0442\u0438",
190
190
  tryAgain: "\u041A\u0430\u0439\u0440\u0430 \u0430\u0440\u0430\u043A\u0435\u0442 \u043A\u044B\u043B\u044B\u04A3\u044B\u0437"
@@ -192,7 +192,7 @@ var translations = {
192
192
  lt: {
193
193
  heading: "Ka\u017Ekas nutiko ne taip",
194
194
  loading: "\u012Ekeliama...",
195
- message: "Pra\u0161ome atnaujinti puslap\u012F, kad t\u0119stum\u0117te.",
195
+ message: "Pra\u0161ome atnaujinti puslap\u012F, kad t\u0119stum\u0117te",
196
196
  reload: "I\u0161 naujo \u012Fkelti puslap\u012F",
197
197
  retrying: "Pakartotinis bandymas",
198
198
  tryAgain: "Bandyti dar kart\u0105"
@@ -200,7 +200,7 @@ var translations = {
200
200
  lv: {
201
201
  heading: "Kaut kas nog\u0101ja greizi",
202
202
  loading: "Iel\u0101d\u0113...",
203
- message: "L\u016Bdzu, atsvaidziniet lapu, lai turpin\u0101tu.",
203
+ message: "L\u016Bdzu, atsvaidziniet lapu, lai turpin\u0101tu",
204
204
  reload: "P\u0101rl\u0101d\u0113t lapu",
205
205
  retrying: "Atk\u0101rtots m\u0113\u0123in\u0101jums",
206
206
  tryAgain: "M\u0113\u0123iniet v\u0113lreiz"
@@ -208,7 +208,7 @@ var translations = {
208
208
  nl: {
209
209
  heading: "Er is iets misgegaan",
210
210
  loading: "Laden...",
211
- message: "Ververs de pagina om door te gaan.",
211
+ message: "Ververs de pagina om door te gaan",
212
212
  reload: "Pagina herladen",
213
213
  retrying: "Opnieuw proberen",
214
214
  tryAgain: "Probeer opnieuw"
@@ -216,7 +216,7 @@ var translations = {
216
216
  no: {
217
217
  heading: "Noe gikk galt",
218
218
  loading: "Laster...",
219
- message: "Vennligst oppdater siden for \xE5 fortsette.",
219
+ message: "Vennligst oppdater siden for \xE5 fortsette",
220
220
  reload: "Last inn siden p\xE5 nytt",
221
221
  retrying: "Nytt fors\xF8k",
222
222
  tryAgain: "Pr\xF8v igjen"
@@ -224,7 +224,7 @@ var translations = {
224
224
  pl: {
225
225
  heading: "Co\u015B posz\u0142o nie tak",
226
226
  loading: "\u0141adowanie...",
227
- message: "Od\u015Bwie\u017C stron\u0119, aby kontynuowa\u0107.",
227
+ message: "Od\u015Bwie\u017C stron\u0119, aby kontynuowa\u0107",
228
228
  reload: "Prze\u0142aduj stron\u0119",
229
229
  retrying: "Pr\xF3ba ponowienia",
230
230
  tryAgain: "Spr\xF3buj ponownie"
@@ -232,7 +232,7 @@ var translations = {
232
232
  pt: {
233
233
  heading: "Algo deu errado",
234
234
  loading: "Carregando...",
235
- message: "Por favor, atualize a p\xE1gina para continuar.",
235
+ message: "Por favor, atualize a p\xE1gina para continuar",
236
236
  reload: "Recarregar p\xE1gina",
237
237
  retrying: "Tentativa de nova tentativa",
238
238
  tryAgain: "Tentar novamente"
@@ -240,7 +240,7 @@ var translations = {
240
240
  ro: {
241
241
  heading: "Ceva nu a mers bine",
242
242
  loading: "Se \xEEncarc\u0103...",
243
- message: "V\u0103 rug\u0103m s\u0103 re\xEEmprosp\u0103ta\u021Bi pagina pentru a continua.",
243
+ message: "V\u0103 rug\u0103m s\u0103 re\xEEmprosp\u0103ta\u021Bi pagina pentru a continua",
244
244
  reload: "Re\xEEncarc\u0103 pagina",
245
245
  retrying: "\xCEncercare de re\xEEncercare",
246
246
  tryAgain: "\xCEncearc\u0103 din nou"
@@ -248,7 +248,7 @@ var translations = {
248
248
  ru: {
249
249
  heading: "\u0427\u0442\u043E-\u0442\u043E \u043F\u043E\u0448\u043B\u043E \u043D\u0435 \u0442\u0430\u043A",
250
250
  loading: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...",
251
- message: "\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C.",
251
+ message: "\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
252
252
  reload: "\u041F\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443",
253
253
  retrying: "\u041F\u043E\u0432\u0442\u043E\u0440\u043D\u0430\u044F \u043F\u043E\u043F\u044B\u0442\u043A\u0430",
254
254
  tryAgain: "\u041F\u043E\u043F\u0440\u043E\u0431\u043E\u0432\u0430\u0442\u044C \u0441\u043D\u043E\u0432\u0430"
@@ -256,7 +256,7 @@ var translations = {
256
256
  sk: {
257
257
  heading: "Nie\u010Do sa pokazilo",
258
258
  loading: "Na\u010D\xEDtava sa...",
259
- message: "Obnovte str\xE1nku pros\xEDm pre pokra\u010Dovanie.",
259
+ message: "Obnovte str\xE1nku pros\xEDm pre pokra\u010Dovanie",
260
260
  reload: "Znovu na\u010D\xEDta\u0165 str\xE1nku",
261
261
  retrying: "Pokus o opakovanie",
262
262
  tryAgain: "Sk\xFAsi\u0165 znova"
@@ -264,7 +264,7 @@ var translations = {
264
264
  sl: {
265
265
  heading: "Nekaj je \u0161lo narobe",
266
266
  loading: "Nalaganje...",
267
- message: "Prosimo, osve\u017Eite stran za nadaljevanje.",
267
+ message: "Prosimo, osve\u017Eite stran za nadaljevanje",
268
268
  reload: "Ponovno nalo\u017Ei stran",
269
269
  retrying: "Poskus ponovnega poskusa",
270
270
  tryAgain: "Poskusi znova"
@@ -272,7 +272,7 @@ var translations = {
272
272
  sv: {
273
273
  heading: "N\xE5got gick fel",
274
274
  loading: "Laddar...",
275
- message: "Uppdatera sidan f\xF6r att forts\xE4tta.",
275
+ message: "Uppdatera sidan f\xF6r att forts\xE4tta",
276
276
  reload: "Ladda om sidan",
277
277
  retrying: "Nytt f\xF6rs\xF6k",
278
278
  tryAgain: "F\xF6rs\xF6k igen"
@@ -288,7 +288,7 @@ var translations = {
288
288
  tr: {
289
289
  heading: "Bir \u015Feyler ters gitti",
290
290
  loading: "Y\xFCkleniyor...",
291
- message: "Devam etmek i\xE7in l\xFCtfen sayfay\u0131 yenileyin.",
291
+ message: "Devam etmek i\xE7in l\xFCtfen sayfay\u0131 yenileyin",
292
292
  reload: "Sayfay\u0131 yeniden y\xFCkle",
293
293
  retrying: "Yeniden deneme giri\u015Fimi",
294
294
  tryAgain: "Tekrar dene"
@@ -296,7 +296,7 @@ var translations = {
296
296
  uk: {
297
297
  heading: "\u0429\u043E\u0441\u044C \u043F\u0456\u0448\u043B\u043E \u043D\u0435 \u0442\u0430\u043A",
298
298
  loading: "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...",
299
- message: "\u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u043E\u043D\u043E\u0432\u0456\u0442\u044C \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0443, \u0449\u043E\u0431 \u043F\u0440\u043E\u0434\u043E\u0432\u0436\u0438\u0442\u0438.",
299
+ message: "\u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u043E\u043D\u043E\u0432\u0456\u0442\u044C \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0443, \u0449\u043E\u0431 \u043F\u0440\u043E\u0434\u043E\u0432\u0436\u0438\u0442\u0438",
300
300
  reload: "\u041F\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0443",
301
301
  retrying: "\u041F\u043E\u0432\u0442\u043E\u0440\u043D\u0430 \u0441\u043F\u0440\u043E\u0431\u0430",
302
302
  tryAgain: "\u0421\u043F\u0440\u043E\u0431\u0443\u0432\u0430\u0442\u0438 \u0437\u043D\u043E\u0432\u0443"
@@ -304,7 +304,7 @@ var translations = {
304
304
  zh: {
305
305
  heading: "\u51FA\u4E86\u70B9\u95EE\u9898",
306
306
  loading: "\u52A0\u8F7D\u4E2D...",
307
- message: "\u8BF7\u5237\u65B0\u9875\u9762\u4EE5\u7EE7\u7EED\u3002",
307
+ message: "\u8BF7\u5237\u65B0\u9875\u9762\u4EE5\u7EE7\u7EED",
308
308
  reload: "\u91CD\u65B0\u52A0\u8F7D",
309
309
  retrying: "\u91CD\u8BD5\u6B21\u6570",
310
310
  tryAgain: "\u91CD\u8BD5"
@@ -8,13 +8,13 @@ import {
8
8
  dispatchStaticAsset404,
9
9
  dispatchSyncRuntimeError,
10
10
  dispatchUnhandledRejection
11
- } from "../../chunk-A5HF6LY6.js";
11
+ } from "../../chunk-ORYUDYHU.js";
12
12
  import {
13
13
  subscribeToState
14
- } from "../../chunk-EFWD2I2Y.js";
14
+ } from "../../chunk-IZAXNDHZ.js";
15
15
  import {
16
16
  subscribe
17
- } from "../../chunk-XRBUBTPZ.js";
17
+ } from "../../chunk-LIKSIU75.js";
18
18
  import "../../chunk-MLKGABMK.js";
19
19
 
20
20
  // src/runtime/debug/index.ts
@@ -3,19 +3,21 @@ import {
3
3
  extractVersionFromHtml,
4
4
  getSpinnerHtml,
5
5
  showSpinner
6
- } from "../chunk-6TP2S7L6.js";
6
+ } from "../chunk-T42DLOQP.js";
7
7
  import {
8
8
  getState,
9
9
  subscribeToState
10
- } from "../chunk-EFWD2I2Y.js";
10
+ } from "../chunk-IZAXNDHZ.js";
11
11
  import {
12
12
  ForceRetryError,
13
13
  getLogger,
14
14
  getOptions,
15
+ getRetryAttemptFromUrl,
16
+ getRetrySnapshot,
15
17
  markRetryHealthyBoot,
16
18
  setTranslations,
17
19
  versionCheckStateWindowKey
18
- } from "../chunk-XRBUBTPZ.js";
20
+ } from "../chunk-LIKSIU75.js";
19
21
  import "../chunk-MLKGABMK.js";
20
22
 
21
23
  // src/common/checkVersion.ts
@@ -255,18 +257,120 @@ var stopVersionCheck = () => {
255
257
  };
256
258
 
257
259
  // src/runtime/recommendedSetup.ts
260
+ var AUTO_HEALTHY_BOOT_BUFFER_MS = 1e3;
261
+ var MIN_HEALTHY_BOOT_GRACE_MS = 5e3;
262
+ var setupStateWindowKey = "__spa_guard_runtime_recommended_setup_state__";
263
+ var createFreshState = () => ({
264
+ cleanup: () => {
265
+ },
266
+ initialized: false,
267
+ timer: null,
268
+ versionCheckEnabled: false
269
+ });
270
+ var getState3 = () => {
271
+ if (globalThis.window === void 0) {
272
+ return createFreshState();
273
+ }
274
+ const w = globalThis.window;
275
+ if (!w[setupStateWindowKey]) {
276
+ w[setupStateWindowKey] = createFreshState();
277
+ }
278
+ return w[setupStateWindowKey];
279
+ };
280
+ var resolveOptions = (overrides) => {
281
+ const healthyBoot = overrides?.healthyBoot;
282
+ const versionCheck = overrides?.versionCheck ?? true;
283
+ const computedGraceMs = computeAutoHealthyBootGraceMs();
284
+ if (healthyBoot === false || healthyBoot === "off") {
285
+ return {
286
+ healthyBootGraceMs: computedGraceMs,
287
+ healthyBootMode: "off",
288
+ versionCheck
289
+ };
290
+ }
291
+ if (healthyBoot === "manual") {
292
+ return {
293
+ healthyBootGraceMs: computedGraceMs,
294
+ healthyBootMode: "manual",
295
+ versionCheck
296
+ };
297
+ }
298
+ if (healthyBoot && typeof healthyBoot === "object") {
299
+ return {
300
+ healthyBootGraceMs: Math.max(computedGraceMs, healthyBoot.graceMs ?? computedGraceMs),
301
+ healthyBootMode: "auto",
302
+ versionCheck
303
+ };
304
+ }
305
+ return {
306
+ healthyBootGraceMs: computedGraceMs,
307
+ healthyBootMode: "auto",
308
+ versionCheck
309
+ };
310
+ };
311
+ var computeAutoHealthyBootGraceMs = () => {
312
+ const options = getOptions();
313
+ const reloadDelays = options.reloadDelays ?? [1e3, 2e3, 5e3];
314
+ const lazyRetryDelays = options.lazyRetry?.retryDelays ?? [1e3, 2e3];
315
+ const maxReloadDelay = Math.max(...reloadDelays, 0);
316
+ const lazyRetryTotalDelay = lazyRetryDelays.reduce((acc, delay) => acc + delay, 0);
317
+ return Math.max(
318
+ MIN_HEALTHY_BOOT_GRACE_MS,
319
+ maxReloadDelay + AUTO_HEALTHY_BOOT_BUFFER_MS,
320
+ lazyRetryTotalDelay + AUTO_HEALTHY_BOOT_BUFFER_MS
321
+ );
322
+ };
323
+ var scheduleAutoHealthyBoot = (graceMs) => {
324
+ if (globalThis.window === void 0) {
325
+ return null;
326
+ }
327
+ if (getRetryAttemptFromUrl() === null) {
328
+ return null;
329
+ }
330
+ return setTimeout(() => {
331
+ if (getRetryAttemptFromUrl() === null) {
332
+ return;
333
+ }
334
+ const snapshot = getRetrySnapshot();
335
+ if (snapshot.phase !== "idle") {
336
+ return;
337
+ }
338
+ markRetryHealthyBoot();
339
+ }, graceMs);
340
+ };
258
341
  var recommendedSetup = (overrides) => {
342
+ const state = getState3();
343
+ if (state.initialized) {
344
+ return state.cleanup;
345
+ }
259
346
  dismissSpinner();
260
- markRetryHealthyBoot();
261
- const options = { versionCheck: true, ...overrides };
347
+ const options = resolveOptions(overrides);
262
348
  if (options.versionCheck) {
263
349
  startVersionCheck();
264
350
  }
265
- return () => {
266
- if (options.versionCheck) {
351
+ const timer = options.healthyBootMode === "auto" ? scheduleAutoHealthyBoot(options.healthyBootGraceMs) : null;
352
+ const cleanup = () => {
353
+ const currentState = getState3();
354
+ if (!currentState.initialized) {
355
+ return;
356
+ }
357
+ if (currentState.timer !== null) {
358
+ clearTimeout(currentState.timer);
359
+ }
360
+ if (currentState.versionCheckEnabled) {
267
361
  stopVersionCheck();
268
362
  }
363
+ if (globalThis.window !== void 0) {
364
+ globalThis.window[setupStateWindowKey] = createFreshState();
365
+ }
269
366
  };
367
+ Object.assign(state, {
368
+ cleanup,
369
+ initialized: true,
370
+ timer,
371
+ versionCheckEnabled: options.versionCheck
372
+ });
373
+ return cleanup;
270
374
  };
271
375
  export {
272
376
  ForceRetryError,
@@ -1,12 +1,36 @@
1
1
  export interface RecommendedSetupOptions {
2
+ /**
3
+ * Healthy boot handling strategy.
4
+ *
5
+ * - `"auto"` (default): if retry URL params are present, mark healthy boot
6
+ * after a grace period and only when orchestrator is still idle.
7
+ * - `"manual"`: never auto-mark; call `markRetryHealthyBoot()` yourself.
8
+ * - `"off"` / `false`: disable healthy boot handling.
9
+ */
10
+ healthyBoot?: RecommendedSetupHealthyBoot;
2
11
  /**
3
12
  * Whether to start version checking.
4
13
  * @default true
5
14
  */
6
15
  versionCheck?: boolean;
7
16
  }
17
+ interface HealthyBootAutoConfig {
18
+ /**
19
+ * Delay before auto-marking healthy boot when retry params are present in URL.
20
+ * @default dynamic:
21
+ * max(5000, max(reloadDelays)+1000, sum(lazyRetry.retryDelays)+1000)
22
+ */
23
+ graceMs?: number;
24
+ /**
25
+ * Explicit mode for object config.
26
+ * @default "auto"
27
+ */
28
+ mode?: "auto";
29
+ }
30
+ type RecommendedSetupHealthyBoot = "auto" | "manual" | "off" | false | HealthyBootAutoConfig;
8
31
  /**
9
32
  * Enable recommended runtime features with sensible defaults.
10
33
  * Returns a cleanup function that tears down all started features.
11
34
  */
12
35
  export declare const recommendedSetup: (overrides?: RecommendedSetupOptions) => (() => void);
36
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovineko/spa-guard",
3
- "version": "0.0.1-alpha-30",
3
+ "version": "0.0.2-alpha-0",
4
4
  "description": "Chunk load error handling for SPAs — core runtime, error handling, schema, i18n",
5
5
  "keywords": [
6
6
  "spa",