@autobusal/providers 1.37.5 → 1.37.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 +17 -0
- package/Providers.tsx +47 -8
- package/Queries/apiClient.ts +29 -0
- package/package.json +1 -1
- package/types/orders.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.37.7
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`OrderData.owner_category`** - `'agency' | 'operator' | 'individual'`, who actually owns an order, collapsed to what a viewer outside the account needs to know: who to contact if it needs to change. See `Libraries\Orders\View::ownerCategory()` (obtapi).
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`OrderData.comission_affiliate_display` is now optional.** obtapi's `View::get()` hides every `comission_*_display` field on a restricted (public lookup) response - see that method's own `$restricted` parameter for why.
|
|
12
|
+
|
|
13
|
+
## 1.37.6
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The DOM-corruption self-heal watchdog now actually recovers every navigation it's supposed to.** It had two gaps that left users stuck exactly like the bug it exists to catch: its 10-second reload cooldown was tracked globally, so a SECOND corrupted navigation to a different URL within 10s of a first one was silently swallowed - no reload, no recovery, stuck. Scoped the cooldown per-URL instead: a fresh URL always gets a fresh recovery attempt; only a repeat failure on the SAME URL is throttled (the loop-protection this cooldown exists for in the first place). It also only listened for synchronous `error` events - a promise-rejection-based failure (plausible from the lazy()/dynamic-import machinery a route transition drives) was invisible to it and got no recovery at all. Added an `unhandledrejection` listener alongside the existing one, both routed through the same recovery check.
|
|
18
|
+
- **`apiClient`'s 401 handler no longer redirects multiple times for one dead session.** A page boot fires several requests in parallel (settings, menu, account refresh, whatever the current page needs); on a stale/invalid session every one of them 401s within the same tick, and each independently ran its own async `/session` confirmation and its own `window.location.href` redirect - the existing pathname guard only protects against a request that 401s AFTER a redirect has already navigated away, not against several 401s racing each other before any of them has. Reproduced live: a stale session landed on `/account/logout`, and before that page's own logout mutation finished, a second queued redirect (from another request's 401, still in flight when the first one fired) reloaded the page again - repeatedly, stacking a fresh "logged out" toast for every concurrent 401 the boot had fired. Now guarded with a synchronous flag set before the async confirmation, so only the first 401 of a burst does the work; every other one concurrent with it short-circuits immediately.
|
|
19
|
+
|
|
3
20
|
## 1.37.5
|
|
4
21
|
|
|
5
22
|
### Fixed
|
package/Providers.tsx
CHANGED
|
@@ -31,30 +31,69 @@ interface Props {
|
|
|
31
31
|
// LoginAs.tsx carry a second, more targeted watchdog (a plain timer, not
|
|
32
32
|
// error-triggered) for the two pages users actually get stuck on during
|
|
33
33
|
// impersonation - this one is the general backstop for everywhere else.
|
|
34
|
+
//
|
|
35
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-11
|
|
36
|
+
// Two gaps closed after this was reported as still reproducing in the wild
|
|
37
|
+
// ("have to click twice", and separately "sometimes I have to refresh
|
|
38
|
+
// myself"):
|
|
39
|
+
//
|
|
40
|
+
// 1. The cooldown used to be a single global timestamp, so ANY two
|
|
41
|
+
// corruptions within 10s of each other - on two DIFFERENT navigations, e.g.
|
|
42
|
+
// a visitor clicking through Help > a category > an article - had the
|
|
43
|
+
// SECOND one silently swallowed: no reload, no error shown, stuck exactly
|
|
44
|
+
// as reported, with a manual refresh as the only way out. The cooldown's
|
|
45
|
+
// actual job is loop protection - stop the SAME failing reload from firing
|
|
46
|
+
// forever - so it only needs to compare against the last recovery done for
|
|
47
|
+
// THIS SAME url; a corruption on a new url is a fresh problem and should
|
|
48
|
+
// always self-heal immediately.
|
|
49
|
+
//
|
|
50
|
+
// 2. This only ever listened for a synchronous, thrown `error` event. React's
|
|
51
|
+
// commit-phase work (and the lazy()/dynamic import() machinery a route
|
|
52
|
+
// transition drives) also has promise-based failure paths, which surface as
|
|
53
|
+
// `unhandledrejection`, not `error` - a corruption landing through one of
|
|
54
|
+
// those was never seen by this handler at all, so no reload ever fired.
|
|
55
|
+
// Both event types are handled by the same recovery function now, since the
|
|
56
|
+
// only thing distinguishing them is which browser event carries the message.
|
|
34
57
|
const useRecoverFromDomCorruption = (): void => {
|
|
35
58
|
useEffect(() => {
|
|
36
|
-
const
|
|
59
|
+
const RECOVERY_AT_KEY = 'autobusalDomRecoveryAt';
|
|
60
|
+
const RECOVERY_URL_KEY = 'autobusalDomRecoveryUrl';
|
|
37
61
|
|
|
38
|
-
const
|
|
39
|
-
|
|
62
|
+
const isDomCorruption = (message: string): boolean => (
|
|
63
|
+
message.includes('removeChild') || message.includes('insertBefore')
|
|
64
|
+
);
|
|
40
65
|
|
|
41
|
-
|
|
66
|
+
const recover = (message: string): void => {
|
|
67
|
+
if (!isDomCorruption(message)) {
|
|
42
68
|
return;
|
|
43
69
|
}
|
|
44
70
|
|
|
45
|
-
const
|
|
71
|
+
const here = window.location.href;
|
|
72
|
+
const lastUrl = sessionStorage.getItem(RECOVERY_URL_KEY);
|
|
73
|
+
const lastAt = Number(sessionStorage.getItem(RECOVERY_AT_KEY) || 0);
|
|
46
74
|
|
|
47
|
-
if (Date.now() -
|
|
75
|
+
if (lastUrl === here && Date.now() - lastAt < 10000) {
|
|
48
76
|
return;
|
|
49
77
|
}
|
|
50
78
|
|
|
51
|
-
sessionStorage.setItem(
|
|
79
|
+
sessionStorage.setItem(RECOVERY_AT_KEY, String(Date.now()));
|
|
80
|
+
sessionStorage.setItem(RECOVERY_URL_KEY, here);
|
|
52
81
|
window.location.reload();
|
|
53
82
|
};
|
|
54
83
|
|
|
84
|
+
const onError = (event: ErrorEvent): void => recover(event.message || '');
|
|
85
|
+
|
|
86
|
+
const onRejection = (event: PromiseRejectionEvent): void => (
|
|
87
|
+
recover(event.reason?.message || String(event.reason ?? ''))
|
|
88
|
+
);
|
|
89
|
+
|
|
55
90
|
window.addEventListener('error', onError);
|
|
91
|
+
window.addEventListener('unhandledrejection', onRejection);
|
|
56
92
|
|
|
57
|
-
return () =>
|
|
93
|
+
return () => {
|
|
94
|
+
window.removeEventListener('error', onError);
|
|
95
|
+
window.removeEventListener('unhandledrejection', onRejection);
|
|
96
|
+
};
|
|
58
97
|
}, []);
|
|
59
98
|
};
|
|
60
99
|
|
package/Queries/apiClient.ts
CHANGED
|
@@ -56,12 +56,39 @@ apiClient.interceptors.request.use(config => {
|
|
|
56
56
|
return config;
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-11
|
|
60
|
+
// A page boot fires several requests in parallel (settings, menu, account
|
|
61
|
+
// refresh, whatever the current page needs) - on a stale/invalid session
|
|
62
|
+
// every one of them 401s within the same tick. Without this, EACH ran its
|
|
63
|
+
// own async `/session` confirmation and, once that resolved, its own
|
|
64
|
+
// `window.location.href` assignment - independently, since the pathname
|
|
65
|
+
// guard below only protects against a request that 401s AFTER a redirect
|
|
66
|
+
// has already navigated the page away, not against several 401s racing each
|
|
67
|
+
// other BEFORE any of them has. Reproduced live: a stale session landed on
|
|
68
|
+
// /account/logout, Logout.tsx ran its own fresh logout call and toast, and
|
|
69
|
+
// before that finished a SECOND queued redirect (from another request's
|
|
70
|
+
// 401, still in flight when the first one fired) reloaded the page again -
|
|
71
|
+
// repeatedly, each reload restarting Logout.tsx's mutation and stacking
|
|
72
|
+
// another "logged out" toast, for as many concurrent 401s as the boot had
|
|
73
|
+
// fired. Sets synchronously, before the async confirmation - JS runs the
|
|
74
|
+
// check-and-set with nothing else able to interleave before the next
|
|
75
|
+
// `await` - so only the first 401 of a burst does the confirmation work;
|
|
76
|
+
// every other one concurrent with it short-circuits immediately instead of
|
|
77
|
+
// each independently re-deciding the same thing.
|
|
78
|
+
let redirectingToLogout = false;
|
|
79
|
+
|
|
59
80
|
apiClient.interceptors.response.use(response => (
|
|
60
81
|
response
|
|
61
82
|
), async (error) => {
|
|
62
83
|
if (error.response) {
|
|
63
84
|
// if we have a 401, the session is gone, so we go to the logout page.
|
|
64
85
|
if (error.response.status === 401 && window.location.pathname !== '/account/logout') {
|
|
86
|
+
if (redirectingToLogout) {
|
|
87
|
+
return Promise.reject(error);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
redirectingToLogout = true;
|
|
91
|
+
|
|
65
92
|
// bff mode: auth is a server-side httpOnly cookie the BFF manages, so a
|
|
66
93
|
// single request's 401 does NOT reliably mean "logged out" the way a
|
|
67
94
|
// bearer 401 does - a query that momentarily races the login/cookie
|
|
@@ -76,6 +103,8 @@ apiClient.interceptors.response.use(response => (
|
|
|
76
103
|
const { data } = await apiClient.get('/session');
|
|
77
104
|
|
|
78
105
|
if (data?.authenticated) {
|
|
106
|
+
redirectingToLogout = false;
|
|
107
|
+
|
|
79
108
|
return Promise.reject(error);
|
|
80
109
|
}
|
|
81
110
|
} catch {
|
package/package.json
CHANGED
package/types/orders.ts
CHANGED
|
@@ -42,7 +42,17 @@ export interface OrderData {
|
|
|
42
42
|
note_cancel: string
|
|
43
43
|
actions: string[]
|
|
44
44
|
created_at: string
|
|
45
|
-
|
|
45
|
+
/*
|
|
46
|
+
* Edited: Ferjolt Ozuni - Date: 2026-08-11
|
|
47
|
+
* Who owns this order, collapsed to what a viewer outside the account
|
|
48
|
+
* needs to know: who to contact if it needs to change. See
|
|
49
|
+
* Libraries\Orders\View::ownerCategory() (obtapi) for the mapping.
|
|
50
|
+
*/
|
|
51
|
+
owner_category: 'agency' | 'operator' | 'individual'
|
|
52
|
+
// Edited: Ferjolt Ozuni - Date: 2026-08-11
|
|
53
|
+
// Optional now: hidden on a restricted response (the public order lookup
|
|
54
|
+
// and any other non-self-service view) - see View::get()'s $restricted.
|
|
55
|
+
comission_affiliate_display?: string
|
|
46
56
|
name_display?: string
|
|
47
57
|
occupied: string
|
|
48
58
|
reference?: OrderData
|