cloudflare-next-intl 0.9.19 → 0.9.21

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
@@ -687,7 +687,7 @@ export default function GlobalError({
687
687
  }
688
688
  ```
689
689
 
690
- - `useStaleDeployRecovery(error: unknown, onRecover?: () => Promise<unknown>, delayMs = 5000): boolean`: Detects a stale-deploy error via `isStaleDeployError` and, **once per build id**, waits `delayMs`, runs `onRecover` (if provided) and `clearClientCache` in parallel, then reloads the page. The build id is read from `localStorage['buildId']` (set by `IntlHelperScript`'s `BUILD_ID` check) and a `sessionStorage` marker records which build id already spent its reload, so a repeat failure on the same build falls through to `false` (render your normal error UI) instead of reloading forever. A redeploy changes the build id and re-arms exactly one more attempt. **Exception:** if the build id was written within the last 60 seconds (`localStorage['buildIdSetAt']`, also set by `IntlHelperScript`), the hook recovers anyway — a stale-deploy error moments after adopting a new build is the deploy still settling, not a real failure, so the one-reload cap is bypassed for that window.
690
+ - `useStaleDeployRecovery(error: unknown, onRecover?: () => Promise<unknown>, delayMs = 5000): boolean`: Detects a stale-deploy error via `isStaleDeployError` and, **once per build id**, waits `delayMs`, runs `onRecover` (if provided) and `clearClientCache` in parallel, then reloads the page. The build id is read from `localStorage['buildId']` (set by `IntlHelperScript`'s `BUILD_ID` check) and a `sessionStorage` marker records which build id already spent its reload, so a repeat failure on the same build falls through to `false` (render your normal error UI) instead of reloading forever. A redeploy changes the build id and re-arms exactly one more attempt.
691
691
  - `shouldRecoverFromStaleDeploy(error: unknown, buildId: string, marker: string | null, recentBuild = false): boolean`: The pure predicate behind the hook, exported for testing.
692
692
  - `isRecentBuild(setAt: number | null, now: number, windowMs = 60000): boolean`: Whether `setAt` (typically `buildIdSetAt`) falls within `windowMs` of `now`.
693
693
 
@@ -1,3 +1,4 @@
1
1
  export declare function isRecentBuild(setAt: number | null, now: number, windowMs?: number): boolean;
2
- export declare function shouldRecoverFromStaleDeploy(error: unknown, buildId: string, marker: string | null, recentBuild?: boolean): boolean;
2
+ export declare function shouldRecoverFromStaleDeploy(error: unknown, buildId: string, marker: string | null, recentBuild?: boolean, reloadTime?: number | null, now?: number, throttleMs?: number): boolean;
3
+ export declare function performCacheBustReload(): void;
3
4
  export default function useStaleDeployRecovery(error: unknown, onRecover?: () => Promise<unknown>, delayMs?: number): boolean;
@@ -3,9 +3,11 @@ import { useEffect, useState } from 'react';
3
3
  import isStaleDeployError from './is_stale_deploy_error.js';
4
4
  import clearClientCache from './clear_client_cache.js';
5
5
  const RECOVERY_RELOAD_KEY = 'stale-deploy-recovery-reloaded';
6
+ const RECOVERY_TIME_KEY = 'stale-deploy-recovery-time';
6
7
  const BUILD_ID_KEY = 'buildId';
7
8
  const BUILD_ID_SET_AT_KEY = 'buildIdSetAt';
8
9
  const RECENT_BUILD_WINDOW_MS = 60000;
10
+ const RELOAD_THROTTLE_MS = 15000;
9
11
  function currentBuildId() {
10
12
  try {
11
13
  return localStorage.getItem(BUILD_ID_KEY) ?? 'unknown';
@@ -26,21 +28,41 @@ function buildIdSetAt() {
26
28
  export function isRecentBuild(setAt, now, windowMs = RECENT_BUILD_WINDOW_MS) {
27
29
  return setAt !== null && now - setAt < windowMs;
28
30
  }
29
- export function shouldRecoverFromStaleDeploy(error, buildId, marker, recentBuild = false) {
30
- const isMarkerActive = marker !== null && marker !== '' && (buildId === 'unknown' || marker === buildId);
31
- return isStaleDeployError(error) && (!isMarkerActive || recentBuild);
31
+ export function shouldRecoverFromStaleDeploy(error, buildId, marker, recentBuild = false, reloadTime = null, now = Date.now(), throttleMs = RELOAD_THROTTLE_MS) {
32
+ if (!isStaleDeployError(error))
33
+ return false;
34
+ const isRecentlyReloaded = reloadTime !== null && now - reloadTime < throttleMs;
35
+ const isSameBuildMarker = marker !== null && marker !== '' && (buildId === 'unknown' || marker === buildId);
36
+ if (isSameBuildMarker && isRecentlyReloaded && !recentBuild) {
37
+ return false;
38
+ }
39
+ return true;
32
40
  }
33
41
  function canRecover(error) {
34
42
  if (typeof window === 'undefined')
35
43
  return false;
36
44
  try {
37
- return shouldRecoverFromStaleDeploy(error, currentBuildId(), sessionStorage.getItem(RECOVERY_RELOAD_KEY), isRecentBuild(buildIdSetAt(), Date.now()));
45
+ const reloadTimeRaw = sessionStorage.getItem(RECOVERY_TIME_KEY);
46
+ const reloadTime = reloadTimeRaw ? Number(reloadTimeRaw) : null;
47
+ return shouldRecoverFromStaleDeploy(error, currentBuildId(), sessionStorage.getItem(RECOVERY_RELOAD_KEY), isRecentBuild(buildIdSetAt(), Date.now()), reloadTime, Date.now());
38
48
  }
39
49
  catch {
40
50
  return false;
41
51
  }
42
52
  }
43
- export default function useStaleDeployRecovery(error, onRecover, delayMs = 5000) {
53
+ export function performCacheBustReload() {
54
+ if (typeof window === 'undefined')
55
+ return;
56
+ try {
57
+ const url = new URL(window.location.href);
58
+ url.searchParams.set('_stale_reload', String(Date.now()));
59
+ window.location.replace(url.toString());
60
+ }
61
+ catch {
62
+ window.location.reload();
63
+ }
64
+ }
65
+ export default function useStaleDeployRecovery(error, onRecover, delayMs = 1000) {
44
66
  const [recovering] = useState(() => canRecover(error));
45
67
  const [initialOnRecover] = useState(() => onRecover);
46
68
  const [initialDelayMs] = useState(() => delayMs);
@@ -53,9 +75,10 @@ export default function useStaleDeployRecovery(error, onRecover, delayMs = 5000)
53
75
  .finally(() => {
54
76
  try {
55
77
  sessionStorage.setItem(RECOVERY_RELOAD_KEY, buildId);
78
+ sessionStorage.setItem(RECOVERY_TIME_KEY, String(Date.now()));
56
79
  }
57
80
  catch { }
58
- window.location.reload();
81
+ performCacheBustReload();
59
82
  });
60
83
  }, initialDelayMs);
61
84
  return () => clearTimeout(timeout);
@@ -12,11 +12,16 @@ export function LocalTime({ format, timestampMs }) {
12
12
  }
13
13
  export function CopyButton({ text, label = 'Copy', copiedLabel = 'Copied', }) {
14
14
  const [copied, setCopied] = useState(false);
15
+ useEffect(() => {
16
+ if (!copied)
17
+ return;
18
+ const timer = setTimeout(() => setCopied(false), 1500);
19
+ return () => clearTimeout(timer);
20
+ }, [copied]);
15
21
  function handleCopy(event) {
16
22
  event.preventDefault();
17
23
  void navigator.clipboard.writeText(text).then(() => {
18
24
  setCopied(true);
19
- setTimeout(() => setCopied(false), 1500);
20
25
  });
21
26
  }
22
27
  return (_jsx("button", { type: "button", onClick: handleCopy, className: "rounded-md border border-gray-300 px-2 py-1 text-[11px] font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200", children: copied ? copiedLabel : label }));
@@ -40,7 +40,13 @@ export default function HelperScript() {
40
40
  if (sessionStorage.getItem(key) === buildId) return;
41
41
  attemptedThisLoad = true;
42
42
  sessionStorage.setItem(key, buildId);
43
- window.location.reload();
43
+ try {
44
+ var u = new URL(window.location.href);
45
+ u.searchParams.set('_stale_reload', String(Date.now()));
46
+ window.location.replace(u.toString());
47
+ } catch (e) {
48
+ window.location.reload();
49
+ }
44
50
  } catch (e) {
45
51
  console.error('Stale Deploy Early Catch Script Error:', e);
46
52
  }
@@ -107,7 +113,16 @@ export default function HelperScript() {
107
113
  // 3. Handle Locale Redirect.
108
114
  // The logic is clearer: redirect only if a non-default locale is set
109
115
  // and the URL isn't already localized.
116
+ // Clean up stale reload query parameter if present
110
117
  const { pathname, search, hash } = window.location;
118
+ if (search && search.indexOf('_stale_reload=') > -1) {
119
+ try {
120
+ const cleanUrl = new URL(window.location.href);
121
+ cleanUrl.searchParams.delete('_stale_reload');
122
+ window.history.replaceState(history.state, '', cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
123
+ } catch (e) {}
124
+ }
125
+
111
126
  if (locale && locale !== '${config.defaultLocale}' && !pathname.startsWith(\`/\${locale}\`)) {
112
127
  const newPath = \`/\${locale}\${pathname === '/' ? '' : pathname}\${search}\${hash}\`;
113
128
  // Redirecting will stop further script execution on this page.
@@ -54,12 +54,13 @@ export async function getCountry(input, generate, headerNames) {
54
54
  const names = headerNames
55
55
  ?? gen?.countryHeaderNames
56
56
  ?? defaultCountryHeaderNames;
57
+ const cookieName = gen?.countryCookieName ?? countryCookieKey;
57
58
  if (input) {
58
59
  if ('headers' in input && input.headers) {
59
60
  const country = extractFromHeaderNames(input.headers, names);
60
61
  if (country)
61
62
  return country;
62
- const cookieCountry = extractCookieHeader(input.headers, countryCookieKey);
63
+ const cookieCountry = extractCookieHeader(input.headers, cookieName);
63
64
  if (cookieCountry)
64
65
  return cookieCountry;
65
66
  }
@@ -67,11 +68,11 @@ export async function getCountry(input, generate, headerNames) {
67
68
  const country = extractFromHeaderNames(input, names);
68
69
  if (country)
69
70
  return country;
70
- const cookieCountry = extractCookieHeader(input, countryCookieKey);
71
+ const cookieCountry = extractCookieHeader(input, cookieName);
71
72
  if (cookieCountry)
72
73
  return cookieCountry;
73
74
  }
74
- const cookieCountry = extractCookie(input.cookies, countryCookieKey);
75
+ const cookieCountry = extractCookie(input.cookies, cookieName);
75
76
  if (cookieCountry) {
76
77
  return cookieCountry;
77
78
  }
@@ -86,7 +87,7 @@ export async function getCountry(input, generate, headerNames) {
86
87
  const country = extractFromHeaderNames(h, names);
87
88
  if (country)
88
89
  return country;
89
- const cookieCountry = extractCookieHeader(h, countryCookieKey);
90
+ const cookieCountry = extractCookieHeader(h, cookieName);
90
91
  if (cookieCountry)
91
92
  return cookieCountry;
92
93
  }
@@ -95,7 +96,7 @@ export async function getCountry(input, generate, headerNames) {
95
96
  try {
96
97
  const { cookies } = await import('next/headers.js');
97
98
  const c = await cookies();
98
- const country = extractCookie(c, countryCookieKey);
99
+ const country = extractCookie(c, cookieName);
99
100
  if (country)
100
101
  return country;
101
102
  }
@@ -129,12 +130,13 @@ export async function getTimezone(input, fallback, generate, headerNames) {
129
130
  const names = headerNames
130
131
  ?? gen?.timezoneHeaderNames
131
132
  ?? defaultTimezoneHeaderNames;
133
+ const cookieName = gen?.timezoneCookieName ?? timezoneCookieKey;
132
134
  if (input) {
133
135
  if ('headers' in input && input.headers) {
134
136
  const tz = extractFromHeaderNames(input.headers, names);
135
137
  if (tz)
136
138
  return tz;
137
- const cookieTimezone = extractCookieHeader(input.headers, timezoneCookieKey);
139
+ const cookieTimezone = extractCookieHeader(input.headers, cookieName);
138
140
  if (cookieTimezone)
139
141
  return cookieTimezone;
140
142
  }
@@ -142,11 +144,11 @@ export async function getTimezone(input, fallback, generate, headerNames) {
142
144
  const tz = extractFromHeaderNames(input, names);
143
145
  if (tz)
144
146
  return tz;
145
- const cookieTimezone = extractCookieHeader(input, timezoneCookieKey);
147
+ const cookieTimezone = extractCookieHeader(input, cookieName);
146
148
  if (cookieTimezone)
147
149
  return cookieTimezone;
148
150
  }
149
- const cookieTimezone = extractCookie(input.cookies, timezoneCookieKey);
151
+ const cookieTimezone = extractCookie(input.cookies, cookieName);
150
152
  if (cookieTimezone) {
151
153
  return cookieTimezone;
152
154
  }
@@ -161,7 +163,7 @@ export async function getTimezone(input, fallback, generate, headerNames) {
161
163
  const tz = extractFromHeaderNames(h, names);
162
164
  if (tz)
163
165
  return tz;
164
- const cookieTimezone = extractCookieHeader(h, timezoneCookieKey);
166
+ const cookieTimezone = extractCookieHeader(h, cookieName);
165
167
  if (cookieTimezone)
166
168
  return cookieTimezone;
167
169
  }
@@ -170,7 +172,7 @@ export async function getTimezone(input, fallback, generate, headerNames) {
170
172
  try {
171
173
  const { cookies } = await import('next/headers.js');
172
174
  const c = await cookies();
173
- const tz = extractCookie(c, timezoneCookieKey);
175
+ const tz = extractCookie(c, cookieName);
174
176
  if (tz)
175
177
  return tz;
176
178
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.19",
3
+ "version": "0.9.21",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",