@webority/web-core 0.51.2 → 0.51.3

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
@@ -222,9 +222,14 @@ write returns `false`.
222
222
 
223
223
  ## Chunk reload
224
224
 
225
- `importWithReload` reloads once when a lazy chunk from an older deploy fails, then refuses for 60
226
- seconds. An app's own error boundary shares that window: reload only when
227
- `!hasRecentlyReloaded() && markReloaded()`.
225
+ `importWithReload` reloads once when a lazy chunk from an older deploy fails, and reloads the page
226
+ being opened (path, query and hash), not the page being left: the failed route renders once the
227
+ navigation commits and reloads from there. A form posted to that route reaches the route's error
228
+ element with the load error, and `useRouteErrorReport` reloads it once. `lazyWithReload` and
229
+ `useRouteErrorReport` reload the same way. The reload is claimed in sessionStorage for that page with a time stamp; if the page's
230
+ chunk still fails after its reload, inside 60 seconds, the route's error element shows it and
231
+ `useRouteErrorReport` warns once, with no further reload. An app's own error boundary shares the
232
+ claim: reload only when `!hasRecentlyReloaded() && markReloaded()`.
228
233
 
229
234
  ## Logger
230
235
 
@@ -1 +1 @@
1
- import{Logger as e}from"../logger/logger.js";import{lazy as t}from"react";var n=`webority.chunkReloadAt`,r=6e4,i=[/failed to fetch dynamically imported module/i,/error loading dynamically imported module/i,/importing a module script failed/i,/unable to preload css/i,/loading (css )?chunk [\w-]+ failed/i],a=e=>{if(!e||typeof e!=`object`)return!1;let{name:t,message:n}=e;return t===`ChunkLoadError`||typeof n==`string`&&i.some(e=>e.test(n))},o=()=>{try{let e=Number(globalThis.sessionStorage.getItem(n));return e>0&&Date.now()-e<r}catch{return!0}},s=()=>{try{return globalThis.sessionStorage.setItem(n,String(Date.now())),!0}catch{return!1}},c=()=>!o()&&s(),l=t=>()=>t().catch(t=>{if(!a(t)||!c())throw t;return e.warn(`[router] a code chunk failed to load; reloading once for the new build`,t),globalThis.location.reload(),new Promise(()=>void 0)}),u=e=>()=>l(e)().catch(e=>({Component(){throw e}})),d=e=>t(l(e));export{o as hasRecentlyReloaded,u as importWithReload,a as isChunkLoadError,d as lazyWithReload,s as markReloaded};
1
+ import{Logger as e}from"../logger/logger.js";import{lazy as t,useEffect as n,useState as r}from"react";var i=`webority.chunkReload`,a=6e4,o=[/failed to fetch dynamically imported module/i,/error loading dynamically imported module/i,/importing a module script failed/i,/unable to preload css/i,/loading (css )?chunk [\w-]+ failed/i],s=e=>{if(!e||typeof e!=`object`)return!1;let{name:t,message:n}=e;return t===`ChunkLoadError`||typeof n==`string`&&o.some(e=>e.test(n))},c=()=>{let{pathname:e,search:t,hash:n}=globalThis.location;return`${e}${t}${n}`},l=e=>typeof e==`object`&&!!e&&typeof e.target==`string`&&typeof e.at==`number`,u=e=>{if(!e)return null;try{let t=JSON.parse(e);return l(t)?t:null}catch{return null}},d=()=>{try{return u(globalThis.sessionStorage.getItem(i))}catch{return`blocked`}},f=e=>{let t=d();return t===`blocked`?`blocked`:!t||t.target!==e||Date.now()-t.at>=a?`none`:t.at>globalThis.performance.timeOrigin?`pending`:`reloaded`},p=()=>f(c())!==`none`,m=()=>{try{let e={target:c(),at:Date.now()};return globalThis.sessionStorage.setItem(i,JSON.stringify(e)),!0}catch{return!1}},h=()=>{let e=d();if(!(!e||e===`blocked`||e.target===c()))try{globalThis.sessionStorage.removeItem(i)}catch{}},g=t=>{let n=f(c());return n===`pending`?`reloading`:n===`none`?m()?(e.warn(`[router] a code chunk failed to load; reloading once for the new build`,t),globalThis.location.reload(),`reloading`):`blocked`:n},_=e=>(h(),e),v=e=>({Component(){throw e}}),y=e=>({action(){throw e},Component(){let[t,i]=r(()=>f(c()));if(n(()=>{t===`none`&&g(e)===`blocked`&&i(`blocked`)},[t,e]),t===`reloaded`||t===`blocked`)throw e;return null}}),b=e=>()=>e().then(_,e=>s(e)?y(e):v(e)),x=e=>t(()=>e().then(_,e=>{if(s(e)&&g(e)===`reloading`)return new Promise(()=>void 0);throw e}));export{p as hasRecentlyReloaded,b as importWithReload,s as isChunkLoadError,x as lazyWithReload,m as markReloaded,g as reloadForChunkError};
@@ -1 +1 @@
1
- import{Logger as e}from"../logger/logger.js";import{isChunkLoadError as t}from"./chunkReload.js";import{useEffect as n}from"react";import{isRouteErrorResponse as r,useRouteError as i}from"react-router";var a=e=>r(e)&&e.status===404?`not-found`:t(e)?`stale-build`:`error`,o=()=>{let t=i(),r=a(t);return n(()=>{r===`not-found`?e.breadcrumb(`Route not found`):r===`stale-build`?e.warn(`[router] a code chunk failed to load after the reload`,t):e.recordError(t,{source:`RouteError`})},[t,r]),{error:t,kind:r}};export{a as classifyRouteError,o as useRouteErrorReport};
1
+ import{Logger as e}from"../logger/logger.js";import{isChunkLoadError as t,reloadForChunkError as n}from"./chunkReload.js";import{useEffect as r}from"react";import{isRouteErrorResponse as i,useRouteError as a}from"react-router";var o=e=>i(e)&&e.status===404?`not-found`:t(e)?`stale-build`:`error`,s=()=>{let t=a(),i=o(t);return r(()=>{if(i===`not-found`)e.breadcrumb(`Route not found`);else if(i===`stale-build`){let r=n(t);r===`reloaded`?e.warn(`[router] a code chunk failed to load after the reload`,t):r===`blocked`&&e.warn(`[router] a code chunk failed to load; sessionStorage is unavailable, so the page is not reloaded`,t)}else e.recordError(t,{source:`RouteError`})},[t,i]),{error:t,kind:i}};export{o as classifyRouteError,s as useRouteErrorReport};
@@ -2,29 +2,41 @@ import { type ComponentType, type LazyExoticComponent } from "react";
2
2
  /** Whether `error` is a lazy chunk that failed to load, the signature of a tab older than the deploy. */
3
3
  export declare const isChunkLoadError: (error: unknown) => boolean;
4
4
  /**
5
- * Whether a recovery reload happened inside the window (60 s). An app's own error boundary
6
- * asks this before reloading for a chunk error, so the router and the boundary share one
7
- * window and the page reloads at most once between them. Without sessionStorage there is no
8
- * way to stop a loop, so the answer is then `true`: never reload.
5
+ * Whether a recovery reload of the current page (path, query and hash) was claimed inside the
6
+ * window (60 s). An app's own error boundary asks this before reloading for a chunk error, so the
7
+ * router and the boundary share one claim and the page reloads at most once between them. Without
8
+ * sessionStorage there is no way to stop a loop, so the answer is then `true`: never reload.
9
9
  */
10
10
  export declare const hasRecentlyReloaded: () => boolean;
11
11
  /**
12
- * Record that a recovery reload is about to happen; call it right before `location.reload()`.
12
+ * Claim a recovery reload of the current page; call it right before `location.reload()`.
13
13
  * Returns false when it could not be recorded, in which case the caller must not reload.
14
14
  */
15
15
  export declare const markReloaded: () => boolean;
16
+ /**
17
+ * What a chunk failure on the current page leads to: `reloading` (the one reload has started, or
18
+ * this document already started it), `reloaded` (the page already had its reload) or `blocked`
19
+ * (sessionStorage is unavailable, so the page is not reloaded).
20
+ */
21
+ export type ChunkErrorOutcome = "reloading" | "reloaded" | "blocked";
22
+ /** Starts the one reload of the current page for a chunk error, unless it already had it. */
23
+ export declare const reloadForChunkError: (error: unknown) => ChunkErrorOutcome;
16
24
  /**
17
25
  * A route object's `lazy` (`lazy: importWithReload(() => import('./routes/projects'))`) that
18
- * reloads the page once on a stale-deploy chunk failure.
26
+ * reloads the page being opened once on a stale-deploy chunk failure.
19
27
  *
20
- * Any failure it cannot recover resolves to a route whose Component throws that error, so the
21
- * route's errorElement shows it. Rejecting instead leaves react-router holding the route as still
22
- * lazy, and it renders nothing below the nearest HydrateFallback: a blank page.
28
+ * Every failure resolves to a route rather than rejecting: a rejected `lazy` leaves react-router
29
+ * holding the route as still lazy, and it renders nothing below the nearest HydrateFallback, a
30
+ * blank page. A chunk failure's route reloads when it renders; a failure it cannot recover, and
31
+ * any other error, is thrown by the route's Component, so the route's errorElement shows it.
23
32
  */
24
33
  export declare const importWithReload: <T extends object>(importFn: () => Promise<T>) => (() => Promise<T | {
25
34
  Component: ComponentType;
26
35
  }>);
27
- /** `React.lazy` with the same one reload: a lazy screen that recovers a tab opened before a deploy. */
36
+ /**
37
+ * `React.lazy` with the same one reload: a lazy screen that recovers a tab opened before a deploy.
38
+ * It renders after the navigation has committed, so the reload is of the page it is on.
39
+ */
28
40
  export declare const lazyWithReload: <C extends ComponentType<any>>(importFn: () => Promise<{
29
41
  default: C;
30
42
  }>) => LazyExoticComponent<C>;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * What went wrong, in the terms the error screen speaks:
3
3
  * - `not-found`: no route matched, or a loader threw a 404.
4
- * - `stale-build`: a code chunk would not load even after the one reload; a
5
- * newer build is live and a reload by the person is the way out.
4
+ * - `stale-build`: a code chunk would not load; a newer build is live. The
5
+ * page reloads once by itself, and if the chunk still will not load after
6
+ * that, a reload by the person is the way out.
6
7
  * - `error`: anything else.
7
8
  */
8
9
  export type RouteErrorKind = "not-found" | "stale-build" | "error";
@@ -13,8 +14,11 @@ export interface RouteErrorReport {
13
14
  }
14
15
  /**
15
16
  * For a route's `errorElement`: reads the error React Router caught, reports it
16
- * once (a missing page as a breadcrumb, a stale build as a warning, anything
17
- * else through `Logger.recordError`), and returns its kind for the error screen.
18
- * The error itself is for the logs; the screen shows no error text.
17
+ * once (a missing page as a breadcrumb, anything else but a stale build through
18
+ * `Logger.recordError`), and returns its kind for the error screen. A stale build
19
+ * reloads the page once, the page the navigation was going to, since the error
20
+ * element renders after the navigation has committed; only a failure after that
21
+ * reload is warned as one. The error itself is for the logs; the screen shows no
22
+ * error text.
19
23
  */
20
24
  export declare const useRouteErrorReport: () => RouteErrorReport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/web-core",
3
- "version": "0.51.2",
3
+ "version": "0.51.3",
4
4
  "description": "Platform layer for Webority React web portals: HTTP clients with single-flight token refresh, auth store, API error extraction, RTK Query base query, logger, formatters, storage, version check and route guards. No UI.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",