@autobusal/providers 1.26.0 → 1.28.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/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.28.0
4
+
5
+ ### Added
6
+
7
+ - **`registerStaleChunkRecovery()`** — a tab left running a bundle that no
8
+ longer exists now reloads itself onto the page it was asked for, instead of
9
+ freezing.
10
+
11
+ Every route is `lazy(() => import(...))`, the build emits content-hashed
12
+ chunks, and it **empties the output directory** — so a new build deletes the
13
+ old chunk names outright (watched `index-DFPpSljG.js` disappear across one).
14
+ A tab still holding the previous `index.html` goes on asking for those names,
15
+ so the next navigation fetches a chunk that no longer resolves, the import
16
+ rejects, and the routed component never mounts — while the URL has *already*
17
+ changed. The address bar says one page and the screen shows the last one.
18
+ From the outside: "the page does not react at all, but the url seems to
19
+ change."
20
+
21
+ One listener on Vite's `vite:preloadError` covers all 141 lazy routes, which
22
+ is why this is a small module rather than a change to 141 call sites.
23
+
24
+ **Silent, not a prompt.** Every lazy chunk in both apps is a *route* —
25
+ checked, there is no lazily-loaded in-page component — so a chunk is only
26
+ ever fetched while navigating, which means the user has already left the page
27
+ they were on. Reloading to the destination costs them nothing the click was
28
+ not already costing them, and a "new version available?" dialog would be
29
+ asking permission to do the thing they just asked for.
30
+
31
+ **It cannot loop.** A missing chunk is usually a stale bundle but might be a
32
+ broken deploy, and reloading into a repeating failure would be far worse than
33
+ the bug. Each path gets at most one reload per tab; after that the error is
34
+ left to surface. `sessionStorage`, so the budget is per tab.
35
+
36
+ Verified by reproducing it: a tab pinned to an older bundle, its chunk
37
+ orphaned by a rebuild, then a client-side click — `vite:preloadError` fired,
38
+ the document was replaced, and the tab landed on `/operators` running the new
39
+ bundle.
40
+
41
+ ## 1.27.0
42
+
43
+ ### Added
44
+
45
+ - `LabelSettings.sms` (BulkGate: status, credentials, sender) and
46
+ `SettingsData.addons.sms` — the paid per-order confirmation. Both optional,
47
+ so an older payload cannot crash a checkout.
3
48
  ## 1.26.0
4
49
 
5
50
  ### Added
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Recover a tab left running a bundle that no longer exists.
3
+ *
4
+ * Ferjolt Ozuni - Date: 2026-08-06
5
+ *
6
+ * THE FAILURE. Every route is `lazy(() => import(...))`, the build emits
7
+ * content-hashed chunks, and it EMPTIES the output directory - so the moment
8
+ * a new build lands, `index-DFPpSljG.js` and its siblings stop existing.
9
+ * Confirmed by watching that exact file disappear across a build. Any tab
10
+ * still holding the previous index.html goes on asking for the old names, so
11
+ * the next navigation fetches a chunk that 404s, the dynamic import rejects,
12
+ * the routed component never mounts - and the URL has ALREADY changed. The
13
+ * address bar says one page, the screen shows the last one, and only a manual
14
+ * refresh escapes. That is exactly what it looked like from the outside:
15
+ * "the page does not react at all, but the url seems to change".
16
+ *
17
+ * WHY A SILENT RELOAD RATHER THAN A PROMPT. Every lazy chunk in both apps is
18
+ * a ROUTE - checked, there is not one lazily-loaded in-page component - so a
19
+ * chunk is only ever fetched while the user is navigating, which means they
20
+ * have already left the page they were on. Reloading to the destination
21
+ * therefore costs them nothing that the click was not already costing them.
22
+ * A "new version available, reload?" dialog would be asking permission to do
23
+ * the thing they just asked for.
24
+ *
25
+ * WHY IT CANNOT LOOP. A missing chunk is usually a stale bundle, but it might
26
+ * be a broken deploy or a bad cache - and reloading into a failure that
27
+ * repeats would spin forever, which is far worse than the bug being fixed.
28
+ * Each URL gets AT MOST ONE reload per tab: if the same route fails again the
29
+ * error is left to surface. sessionStorage rather than localStorage, so the
30
+ * budget is per tab and does not follow somebody around for weeks.
31
+ */
32
+
33
+ const KEY = 'chunk-reload';
34
+
35
+ /**
36
+ * Which paths this tab has already spent its reload on.
37
+ */
38
+ const attempted = (): string[] => {
39
+ try {
40
+ const raw = sessionStorage.getItem(KEY);
41
+
42
+ return raw ? JSON.parse(raw) : [];
43
+ } catch {
44
+ // Storage denied (private mode) or a hand-mangled value. Answering
45
+ // "nothing attempted" is safe here only because remember() below has to
46
+ // write before it approves anything, and that write fails for the same
47
+ // reason - so an unreadable store still ends in a refusal, not a loop.
48
+ return [];
49
+ }
50
+ };
51
+
52
+ const remember = (path: string): boolean => {
53
+ try {
54
+ const paths = attempted();
55
+
56
+ if (paths.includes(path)) {
57
+ return false;
58
+ }
59
+
60
+ sessionStorage.setItem(KEY, JSON.stringify([...paths, path]));
61
+
62
+ return true;
63
+ } catch {
64
+ // Storage unavailable means no way to remember an attempt, and therefore
65
+ // no way to stop a second one. Refuse rather than risk reloading forever.
66
+ return false;
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Listen for Vite's dynamic-import failure.
72
+ *
73
+ * `vite:preloadError` is emitted by the preload helper the build wraps every
74
+ * `import()` in, so ONE listener covers all 141 lazy routes - which is the
75
+ * only reason this is a ten-line module rather than a change to 141 call
76
+ * sites. Calling preventDefault stops Vite rethrowing, since we are handling
77
+ * it by replacing the document.
78
+ */
79
+ const registerStaleChunkRecovery = (): void => {
80
+ window.addEventListener('vite:preloadError', (event) => {
81
+ const path = window.location.pathname + window.location.search;
82
+
83
+ if (!remember(path)) {
84
+ // Spent already. Let it throw so the error boundary says something,
85
+ // rather than reloading into the same wall a second time.
86
+ return;
87
+ }
88
+
89
+ event.preventDefault();
90
+
91
+ // reload() and not assign(): the URL is already the destination - React
92
+ // Router changes it before rendering the route whose chunk just failed -
93
+ // so this fetches a fresh index.html and lands on the page that was asked
94
+ // for, with the current chunk names.
95
+ window.location.reload();
96
+ });
97
+ };
98
+
99
+ export default registerStaleChunkRecovery;
package/index.ts CHANGED
@@ -4,6 +4,7 @@ import ErrorBoundary from './Errors/ErrorBoundary';
4
4
  import Message from './Errors/Message';
5
5
  import useMenuStore from './stores/menu';
6
6
  import { useUserStore } from './stores/user';
7
+ import registerStaleChunkRecovery from './Setup/staleChunk';
7
8
 
8
9
  export {
9
10
  apiClient,
@@ -11,7 +12,8 @@ export {
11
12
  ErrorBoundary,
12
13
  Message,
13
14
  useMenuStore,
14
- useUserStore
15
+ useUserStore,
16
+ registerStaleChunkRecovery
15
17
  };
16
18
 
17
19
  export default Providers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.26.0",
3
+ "version": "1.28.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/types/settings.ts CHANGED
@@ -40,6 +40,15 @@ export interface SettingsData {
40
40
  addons?: {
41
41
  whatsapp: { available: boolean, price: number }
42
42
  telegram: { available: boolean, price: number }
43
+
44
+ /**
45
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
46
+ * The paid per-order CONFIRMATION by SMS. Route alerts over SMS are
47
+ * free and reach every passenger who gave a phone number, so nothing
48
+ * gates them - this only answers whether a confirmation text may be
49
+ * bought. Optional so an older payload cannot crash the checkout.
50
+ */
51
+ sms?: { available: boolean, price: number }
43
52
  }
44
53
 
45
54
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
@@ -230,6 +239,23 @@ export interface LabelSettings {
230
239
  display_phone?: string
231
240
  }
232
241
 
242
+ /**
243
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
244
+ * BulkGate, for SMS. `status` is only ever 'disabled' or 'dedicated' -
245
+ * deliberately no 'main': the other channels let a whitelabel borrow the
246
+ * main brand's connection, and SMS costs money per message, so borrowing
247
+ * would mean one brand running up another's bill. `credit` is captured
248
+ * from a live check on save, not entered by hand.
249
+ */
250
+ sms?: {
251
+ status: 'disabled' | 'dedicated'
252
+ application_id?: string
253
+ application_token?: string
254
+ sender?: string
255
+ sender_value?: string
256
+ credit?: string
257
+ }
258
+
233
259
  // Edited: Ferjolt Ozuni - Date: 2026-07-29
234
260
  // Admin-managed footer social-share links (public preferences.social
235
261
  // above is populated from this) - distinct from `social` above, which