@autobusal/providers 1.6.5 → 1.6.7
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/CHANGELOG.md +47 -0
- package/Providers.tsx +97 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to `@autobusal/providers` are documented here. This project
|
|
|
4
4
|
adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.6.7] - 2026-07-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Providers.tsx`: self-healing reload if the DOM ever corrupts near the
|
|
12
|
+
root.** 1.6.6 closed the deterministic trigger for the
|
|
13
|
+
`GoogleReCaptchaProvider` remount corruption (see 1.6.6 below), but its
|
|
14
|
+
script/badge injection is still fundamentally async - a script tag's
|
|
15
|
+
`onload` racing React's synchronous mount/cleanup - so a rare remount
|
|
16
|
+
can still leave the DOM in a state that throws an uncaught
|
|
17
|
+
`removeChild`/`insertBefore` error on the next reconciliation anywhere
|
|
18
|
+
near the root. That class of error isn't render-phase, so no
|
|
19
|
+
`ErrorBoundary` catches it, and it silently aborts whatever navigation
|
|
20
|
+
was in flight: the URL updates but the page never re-renders, with no
|
|
21
|
+
visible error and no way to navigate out. A reload of the current URL
|
|
22
|
+
always recovers cleanly (the router's `history.pushState` already
|
|
23
|
+
applied), so `Providers` now listens for exactly this error signature
|
|
24
|
+
and self-heals with one reload, guarded by a 10s `sessionStorage`
|
|
25
|
+
cooldown so a genuinely persistent error can't loop.
|
|
26
|
+
|
|
27
|
+
## [1.6.6] - 2026-07-31
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`Providers.tsx`: `Setup` (and its `useGetMenu()` call) now has its own
|
|
32
|
+
inner `<Suspense>` boundary**, instead of sharing the outer one with
|
|
33
|
+
`Recaptcha`/`Styles`/`Notifications`. `Setup` calls a second,
|
|
34
|
+
independently-resolving `useSuspenseQuery` (`get-menu`) after
|
|
35
|
+
`useGetSettings()`; on a cold menu cache this suspended AFTER
|
|
36
|
+
Recaptcha's `GoogleReCaptchaProvider` had already committed and
|
|
37
|
+
injected its script/badge - and a Suspense boundary that re-suspends
|
|
38
|
+
discards its already-committed content and remounts it once ready.
|
|
39
|
+
`GoogleReCaptchaProvider` isn't remount-safe (its script/badge
|
|
40
|
+
injection is async, outside React's own DOM bookkeeping), so the
|
|
41
|
+
remount left a duplicate, half-torn-down `.grecaptcha-badge` behind.
|
|
42
|
+
That corrupted DOM state then threw an uncaught `TypeError: Cannot
|
|
43
|
+
read properties of null (reading 'removeChild')` the next time React
|
|
44
|
+
reconciled anywhere near the root - i.e. on every subsequent route
|
|
45
|
+
change, since Recaptcha wraps the whole router. The exception isn't
|
|
46
|
+
caught by the app's `ErrorBoundary` (render-phase only), so it
|
|
47
|
+
silently aborted the commit: the URL would update (the router's
|
|
48
|
+
`history.pushState` already ran) but the page itself never
|
|
49
|
+
re-rendered - freezing client-side navigation app-wide, most visibly
|
|
50
|
+
reproduced via the account dropdown menu (Password, Telegram, and the
|
|
51
|
+
impersonation-aware Logout/"Return to my account" link, which is what
|
|
52
|
+
surfaced this).
|
|
53
|
+
|
|
7
54
|
## [1.6.5] - 2026-07-31
|
|
8
55
|
|
|
9
56
|
### Added
|
package/Providers.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Suspense } from 'react';
|
|
1
|
+
import { Suspense, useEffect } from 'react';
|
|
2
2
|
import Recaptcha from './Recaptcha/Recaptcha';
|
|
3
3
|
import Queries from './Queries/Queries';
|
|
4
4
|
import Styles from './Styles/Styles';
|
|
@@ -11,34 +11,103 @@ interface Props {
|
|
|
11
11
|
children: JSX.Element
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
14
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-31
|
|
15
|
+
// Defensive backstop for the removeChild corruption described below: even
|
|
16
|
+
// with Setup's suspension isolated, GoogleReCaptchaProvider's script/badge
|
|
17
|
+
// injection is inherently async (a script tag's onload race against React's
|
|
18
|
+
// synchronous mount/cleanup), so a rare remount can still leave the DOM in a
|
|
19
|
+
// state that throws on the next reconciliation - and that throw is NOT
|
|
20
|
+
// render-phase, so no ErrorBoundary catches it. Left alone, this strands the
|
|
21
|
+
// user on whatever was on screen with a URL that no longer matches it and no
|
|
22
|
+
// working navigation (the exact "stuck, can't log out" symptom). A hard
|
|
23
|
+
// reload of the current URL always recovers cleanly (proven: the router's
|
|
24
|
+
// history.pushState already applied), so self-heal with one, guarded against
|
|
25
|
+
// looping if the error turns out to be persistent rather than transient.
|
|
26
|
+
const useRecoverFromDomCorruption = (): void => {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const RECOVERY_KEY = 'autobusalDomRecoveryAt';
|
|
29
|
+
|
|
30
|
+
const onError = (event: ErrorEvent): void => {
|
|
31
|
+
const message = event.message || '';
|
|
32
|
+
|
|
33
|
+
if (!message.includes('removeChild') && !message.includes('insertBefore')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const last = Number(sessionStorage.getItem(RECOVERY_KEY) || 0);
|
|
38
|
+
|
|
39
|
+
if (Date.now() - last < 10000) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
sessionStorage.setItem(RECOVERY_KEY, String(Date.now()));
|
|
44
|
+
window.location.reload();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
window.addEventListener('error', onError);
|
|
48
|
+
|
|
49
|
+
return () => window.removeEventListener('error', onError);
|
|
50
|
+
}, []);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const Providers = ({ type, loading, children }: Props): JSX.Element => {
|
|
54
|
+
useRecoverFromDomCorruption();
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
// Styles and Setup both call useGetSettings() (a useSuspenseQuery) and
|
|
58
|
+
// sit ABOVE the inner <Suspense> below, so on a cold settings cache their
|
|
59
|
+
// suspension had no boundary to catch it and React threw error #426
|
|
60
|
+
// ("a component suspended while responding to synchronous input"). This
|
|
61
|
+
// outer boundary catches those two suspensions (showing `loading` during
|
|
62
|
+
// the initial settings fetch). The QueryClient in <Queries> is created
|
|
63
|
+
// at module scope, so remounting this subtree while suspended keeps the
|
|
64
|
+
// same client and cache - no refetch loop.
|
|
65
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-25
|
|
66
|
+
// Recaptcha moved INSIDE Queries: it now reads the label's runtime
|
|
67
|
+
// captcha settings via useGetSettings(), which needs the
|
|
68
|
+
// QueryClientProvider (and suspends into the outer boundary on a cold
|
|
69
|
+
// cache, like Styles/Setup).
|
|
70
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-31
|
|
71
|
+
// Bug: Setup also calls useGetMenu() (a SEPARATE useSuspenseQuery, its
|
|
72
|
+
// own cache key/promise) in its own render body, still inside this same
|
|
73
|
+
// outer boundary. On a cold menu cache, that suspends AFTER Recaptcha/
|
|
74
|
+
// Styles/Notifications above it have already committed - and a Suspense
|
|
75
|
+
// boundary re-suspending after its content is already committed hides/
|
|
76
|
+
// discards that whole committed subtree and mounts it again once ready.
|
|
77
|
+
// Recaptcha's GoogleReCaptchaProvider isn't remount-safe: its script/
|
|
78
|
+
// badge injection is async and outside React's DOM bookkeeping, so
|
|
79
|
+
// remounting it left a duplicate, half-torn-down
|
|
80
|
+
// <div class="grecaptcha-badge"> behind. That corrupted DOM state then
|
|
81
|
+
// threw an uncaught "Cannot read properties of null (reading
|
|
82
|
+
// 'removeChild')" the next time React reconciled anywhere near the root
|
|
83
|
+
// - which is every route change, since Recaptcha wraps the whole router
|
|
84
|
+
// - silently aborting the commit. The URL would update (the router's
|
|
85
|
+
// history.pushState already ran) but the page itself would never
|
|
86
|
+
// re-render, freezing navigation app-wide with no console-visible error
|
|
87
|
+
// (uncaught DOM exceptions like this don't hit the render-phase-only
|
|
88
|
+
// ErrorBoundary). Giving Setup its own inner boundary means its menu
|
|
89
|
+
// suspension can only hide/retry ITSELF, not the already-committed
|
|
90
|
+
// Recaptcha/Styles/Notifications above it - this closed off the
|
|
91
|
+
// deterministic trigger, and useRecoverFromDomCorruption() above is the
|
|
92
|
+
// backstop for whatever async-timing sliver still gets through.
|
|
93
|
+
<Suspense fallback={ loading }>
|
|
94
|
+
<Queries>
|
|
95
|
+
<Recaptcha>
|
|
96
|
+
<Styles>
|
|
97
|
+
<Notifications>
|
|
33
98
|
<Suspense fallback={ loading }>
|
|
34
|
-
{
|
|
99
|
+
<Setup type={ type }>
|
|
100
|
+
<Suspense fallback={ loading }>
|
|
101
|
+
{ children }
|
|
102
|
+
</Suspense>
|
|
103
|
+
</Setup>
|
|
35
104
|
</Suspense>
|
|
36
|
-
</
|
|
37
|
-
</
|
|
38
|
-
</
|
|
39
|
-
</
|
|
40
|
-
</
|
|
41
|
-
|
|
42
|
-
|
|
105
|
+
</Notifications>
|
|
106
|
+
</Styles>
|
|
107
|
+
</Recaptcha>
|
|
108
|
+
</Queries>
|
|
109
|
+
</Suspense>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
43
112
|
|
|
44
113
|
export default Providers;
|