@trii/components 2.0.102 → 2.0.104
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/async-state/package.json +9 -0
- package/dist/async-state.d.ts +184 -0
- package/dist/cjs/async-state.js +2 -0
- package/dist/cjs/async-state.js.map +1 -0
- package/dist/cjs/index.js +43 -65
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/async-state.js +2 -0
- package/dist/esm/async-state.js.map +1 -0
- package/dist/esm/index.js +43 -65
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +329 -2
- package/dist/types/asyncState/asyncState.d.ts +99 -0
- package/dist/types/asyncState/getErrorMessage.d.ts +16 -0
- package/dist/types/asyncState/index.d.ts +15 -0
- package/dist/types/asyncState/types.d.ts +17 -0
- package/dist/types/asyncState/useAsyncData.d.ts +51 -0
- package/dist/types/components/AsyncContent/AsyncContent.d.ts +48 -0
- package/dist/types/components/AsyncContent/index.d.ts +2 -0
- package/dist/types/components/EditContactModal/components/Section/ContactInformation/components/EditModal/layout/AddressInput/AddressInput.d.ts +2 -1
- package/dist/types/components/EditContactModal/components/Section/ContactInformation/components/EditModal/layout/PhoneInput/PhoneInput.d.ts +2 -1
- package/dist/types/components/EditContactModal/components/Section/ContactInformation/components/EditModal/layout/WarningsDisplay/WarningsDisplay.d.ts +2 -1
- package/dist/types/components/EditContactModal/components/Section/ContactInformation/components/EditModal/layout/WarningsDisplay/components/Warning/Warning.d.ts +3 -1
- package/dist/types/components/EditContactModal/components/Section/ContactInformation/components/EditModal/layout/WarningsDisplay/components/Warning/hooks/useContactInfoPopup.d.ts +11 -0
- package/dist/types/components/EditContactModal/context/EditContactApiContext.d.ts +2 -0
- package/dist/types/components/EditContactModal/services/api.d.ts +1 -0
- package/dist/types/components/EmptyState/EmptyState.d.ts +22 -0
- package/dist/types/components/EmptyState/index.d.ts +2 -0
- package/dist/types/components/ErrorState/ErrorState.d.ts +23 -0
- package/dist/types/components/ErrorState/index.d.ts +2 -0
- package/dist/types/components/PanelErrorBoundary/PanelErrorBoundary.d.ts +55 -0
- package/dist/types/components/PanelErrorBoundary/index.d.ts +2 -0
- package/dist/types/components/index.d.ts +8 -0
- package/dist/types/i18n/config.d.ts +6 -0
- package/dist/types/i18n/useComponentsTranslation.d.ts +17 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +161 -159
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trii/components-async-state",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "Stub de resolucion para el subpath @trii/components/async-state. No se publica como paquete propio: existe para que Node/webpack/TS con moduleResolution 'node' encuentren la entrada secundaria sin pasar por el campo 'exports'.",
|
|
5
|
+
"main": "../dist/cjs/async-state.js",
|
|
6
|
+
"module": "../dist/esm/async-state.js",
|
|
7
|
+
"types": "../dist/async-state.d.ts",
|
|
8
|
+
"sideEffects": false
|
|
9
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelo canónico de estado asíncrono (loading / error / caché remota).
|
|
3
|
+
*
|
|
4
|
+
* Unifica cómo los paneles representan el ciclo de vida de un dato remoto:
|
|
5
|
+
*
|
|
6
|
+
* - `idle`/`loading` sin datos → carga inicial (bloqueante: skeleton/spinner).
|
|
7
|
+
* - `loading` con datos → revalidación NO bloqueante (`isRevalidating`).
|
|
8
|
+
* - `rejected` con datos → error manteniendo datos previos + retry.
|
|
9
|
+
* - `rejected` sin datos → error bloqueante con retry.
|
|
10
|
+
* - `succeeded` sin datos → estado realmente vacío (`isEmpty`).
|
|
11
|
+
*
|
|
12
|
+
* Los literales de `AsyncStatus` coinciden con el `RequestStatus` que usan los
|
|
13
|
+
* slices de los frontends Trii, para que sean compatibles sin conversión.
|
|
14
|
+
*
|
|
15
|
+
* Módulo puro: sin React, sin MUI, sin Redux, sin dominio. Se publica también
|
|
16
|
+
* por el subpath `@trii/components/async-state` para que un slice pueda
|
|
17
|
+
* consumirlo sin arrastrar la UI del paquete.
|
|
18
|
+
*
|
|
19
|
+
* Guía de uso: docs/ASYNC_STATE.md
|
|
20
|
+
*/
|
|
21
|
+
type AsyncStatus = 'idle' | 'loading' | 'succeeded' | 'rejected';
|
|
22
|
+
/**
|
|
23
|
+
* Estado "crudo" persistible: es lo que guarda un slice de Redux o el estado
|
|
24
|
+
* interno de `useAsyncData`. Los flags derivados se calculan con
|
|
25
|
+
* `deriveAsyncState` (nunca se almacenan, para evitar inconsistencias).
|
|
26
|
+
*/
|
|
27
|
+
interface AsyncData<T> {
|
|
28
|
+
status: AsyncStatus;
|
|
29
|
+
/** Últimos datos válidos. Se preservan durante refetches y errores. */
|
|
30
|
+
data: T | undefined;
|
|
31
|
+
/** Mensaje del último error (null si la última operación fue exitosa). */
|
|
32
|
+
error: string | null;
|
|
33
|
+
/** Timestamp (Date.now()) del último fetch exitoso. Base para staleness. */
|
|
34
|
+
updatedAt: number | null;
|
|
35
|
+
}
|
|
36
|
+
/** Estado derivado listo para consumir desde la UI. */
|
|
37
|
+
interface AsyncState<T> extends AsyncData<T> {
|
|
38
|
+
/** true si hay datos utilizables para mostrar (no undefined ni "vacíos"). */
|
|
39
|
+
hasData: boolean;
|
|
40
|
+
/** Carga inicial: está cargando y NO hay datos que mostrar → bloqueante. */
|
|
41
|
+
isInitialLoading: boolean;
|
|
42
|
+
/** Revalidación: está cargando y SÍ hay datos → no bloquear la UI. */
|
|
43
|
+
isRevalidating: boolean;
|
|
44
|
+
/** Vacío real: la última carga fue exitosa y no hay datos que mostrar. */
|
|
45
|
+
isEmpty: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** Decide si un dato cuenta como "vacío" (p.ej. lista sin elementos). */
|
|
48
|
+
type IsEmptyFn<T> = (data: T) => boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Vacío por defecto: `null`/`undefined` y arrays sin elementos.
|
|
51
|
+
* Cada feature puede aportar su propia regla (p.ej. "objeto sin campos").
|
|
52
|
+
*/
|
|
53
|
+
declare const defaultIsEmpty: <T>(data: T) => boolean;
|
|
54
|
+
/** Estado inicial: nunca se hizo un fetch. */
|
|
55
|
+
declare const asyncIdle: <T>() => AsyncData<T>;
|
|
56
|
+
/**
|
|
57
|
+
* Transición a "request en curso".
|
|
58
|
+
* Por defecto preserva los datos previos (refetch no bloqueante).
|
|
59
|
+
* Pasar `clearData: true` solo cuando los datos viejos dejarían de ser
|
|
60
|
+
* válidos para la nueva request (p.ej. cambio de contacto seleccionado).
|
|
61
|
+
*/
|
|
62
|
+
declare const asyncRequestStarted: <T>(prev: AsyncData<T>, options?: {
|
|
63
|
+
clearData?: boolean;
|
|
64
|
+
}) => AsyncData<T>;
|
|
65
|
+
/** Transición a éxito: guarda datos, limpia error y sella `updatedAt`. */
|
|
66
|
+
declare const asyncRequestSucceeded: <T>(data: T) => AsyncData<T>;
|
|
67
|
+
/**
|
|
68
|
+
* Transición a error: guarda el mensaje y PRESERVA los datos previos
|
|
69
|
+
* para que la UI pueda seguir mostrándolos (error con datos).
|
|
70
|
+
*/
|
|
71
|
+
declare const asyncRequestFailed: <T>(error: string, prev: AsyncData<T>) => AsyncData<T>;
|
|
72
|
+
/**
|
|
73
|
+
* Deriva los flags de UI a partir del estado crudo. Es la ÚNICA fuente de
|
|
74
|
+
* verdad para interpretar un estado asíncrono: la usan `useAsyncData`,
|
|
75
|
+
* los selectores de Redux y `<AsyncContent>`.
|
|
76
|
+
*
|
|
77
|
+
* Uso con un slice existente (sin reescribirlo):
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* const selectChannelsAsyncState = createSelector(
|
|
81
|
+
* selectChannelsState,
|
|
82
|
+
* (state) =>
|
|
83
|
+
* deriveAsyncState({
|
|
84
|
+
* status: state.status.fetch,
|
|
85
|
+
* data: state.channels,
|
|
86
|
+
* error: state.error,
|
|
87
|
+
* })
|
|
88
|
+
* );
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare const deriveAsyncState: <T>(input: {
|
|
92
|
+
status: AsyncStatus;
|
|
93
|
+
data: T | undefined;
|
|
94
|
+
error?: string | null;
|
|
95
|
+
updatedAt?: number | null;
|
|
96
|
+
isEmpty?: IsEmptyFn<T>;
|
|
97
|
+
}) => AsyncState<T>;
|
|
98
|
+
/** true si el dato tiene más de `staleTime` ms (o nunca se cargó). */
|
|
99
|
+
declare const isStale: (updatedAt: number | null, staleTime: number, now?: number) => boolean;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extrae un mensaje legible de cualquier error (axios, Error, string...).
|
|
103
|
+
*
|
|
104
|
+
* Orden de preferencia:
|
|
105
|
+
* 1. `error.response.data.message` / `error.response.data.error` (API Trii).
|
|
106
|
+
* 2. `error.message` (Error estándar, p.ej. "Network Error" de axios).
|
|
107
|
+
* 3. El string recibido tal cual.
|
|
108
|
+
* 4. `fallback`.
|
|
109
|
+
*
|
|
110
|
+
* No traduce: la traducción de mensajes genéricos ocurre en la UI
|
|
111
|
+
* (`<ErrorState>`); esto preserva el detalle técnico o del backend.
|
|
112
|
+
*
|
|
113
|
+
* Tipa axios por duck-typing a propósito: el módulo no importa axios, así que
|
|
114
|
+
* sirve igual con fetch o con cualquier cliente HTTP.
|
|
115
|
+
*/
|
|
116
|
+
declare const getErrorMessage: (error: unknown, fallback?: string) => string;
|
|
117
|
+
|
|
118
|
+
/** Resultado de `useAsyncData`: estado derivado + acciones del ciclo de vida. */
|
|
119
|
+
interface UseAsyncDataResult<T> extends AsyncState<T> {
|
|
120
|
+
/**
|
|
121
|
+
* Re-ejecuta el fetcher. Si ya hay datos, NO bloquea la UI
|
|
122
|
+
* (`isRevalidating` pasa a true y los datos siguen visibles).
|
|
123
|
+
*/
|
|
124
|
+
refetch: () => Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Alias semántico de `refetch` para los estados de error.
|
|
127
|
+
* Es el callback que debe recibir `<AsyncContent onRetry={...}>`.
|
|
128
|
+
*/
|
|
129
|
+
retry: () => Promise<void>;
|
|
130
|
+
/** Vuelve al estado `idle` y cancela cualquier request en curso. */
|
|
131
|
+
reset: () => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ciclo de vida completo de un dato remoto LOCAL a un panel (sin Redux).
|
|
136
|
+
*
|
|
137
|
+
* Comportamiento:
|
|
138
|
+
* - Fetch al montar (configurable con `fetchOnMount`) y cuando cambian `deps`.
|
|
139
|
+
* - Carga inicial sin datos → `isInitialLoading` (bloqueante).
|
|
140
|
+
* - Refetch con datos → `isRevalidating`: los datos siguen visibles.
|
|
141
|
+
* - Error con datos previos → `status: 'rejected'` + datos preservados.
|
|
142
|
+
* - Error sin datos → `error` para mostrar un estado de error con `retry`.
|
|
143
|
+
* - `refetch()`/`retry()` re-ejecutan el fetcher; nunca bloquean si hay datos.
|
|
144
|
+
* - Cancelación automática (AbortSignal) y guarda de carrera: si se dispara
|
|
145
|
+
* una request nueva, las respuestas viejas se descartan.
|
|
146
|
+
* - `autoRetry` opcional con backoff lineal antes de mostrar el error.
|
|
147
|
+
*
|
|
148
|
+
* Usar Redux en su lugar cuando el dato se comparte entre varios paneles o
|
|
149
|
+
* se actualiza por socket (ver docs/ASYNC_STATE.md §¿Redux o hook?).
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* const state = useAsyncData(
|
|
153
|
+
* (signal) => reportsService.getReports(url, { signal }),
|
|
154
|
+
* { deps: [url] }
|
|
155
|
+
* );
|
|
156
|
+
* return (
|
|
157
|
+
* <AsyncContent state={state} onRetry={state.retry}>
|
|
158
|
+
* {(reports) => <ReportList reports={reports} />}
|
|
159
|
+
* </AsyncContent>
|
|
160
|
+
* );
|
|
161
|
+
*/
|
|
162
|
+
declare function useAsyncData<T>(fetcher: (signal: AbortSignal) => Promise<T>, options?: {
|
|
163
|
+
/** Regla de "vacío" del dato (default: null/undefined o array vacío). */
|
|
164
|
+
isEmpty?: IsEmptyFn<T>;
|
|
165
|
+
/** Disparar el fetch al montar y al cambiar `deps` (default: true). */
|
|
166
|
+
fetchOnMount?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Preservar datos previos durante refetches (default: true).
|
|
169
|
+
* Poner en false solo si los datos viejos serían inválidos/engañosos.
|
|
170
|
+
*/
|
|
171
|
+
keepPreviousData?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Reintentos automáticos antes de marcar error (default: sin reintentos;
|
|
174
|
+
* el usuario decide con el botón de retry).
|
|
175
|
+
*/
|
|
176
|
+
autoRetry?: {
|
|
177
|
+
attempts: number;
|
|
178
|
+
delayMs?: number;
|
|
179
|
+
};
|
|
180
|
+
/** Dependencias que re-disparan el fetch (p.ej. un id o filtro). */
|
|
181
|
+
deps?: ReadonlyArray<unknown>;
|
|
182
|
+
}): UseAsyncDataResult<T>;
|
|
183
|
+
|
|
184
|
+
export { type AsyncData, type AsyncState, type AsyncStatus, type IsEmptyFn, type UseAsyncDataResult, asyncIdle, asyncRequestFailed, asyncRequestStarted, asyncRequestSucceeded, defaultIsEmpty, deriveAsyncState, getErrorMessage, isStale, useAsyncData };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var s=require("react");const v=e=>e==null?!0:Array.isArray(e)?e.length===0:!1,A=()=>({status:"idle",data:void 0,error:null,updatedAt:null}),D=(e,t)=>({status:"loading",data:t?.clearData?void 0:e.data,error:null,updatedAt:e.updatedAt}),E=e=>({status:"succeeded",data:e,error:null,updatedAt:Date.now()}),S=(e,t)=>({status:"rejected",data:t.data,error:e,updatedAt:t.updatedAt}),q=e=>{const{status:t,data:r,error:a=null,updatedAt:o=null}=e,l=e.isEmpty??v,n=r!==void 0&&!l(r),i=t==="loading";return{status:t,data:r,error:a,updatedAt:o,hasData:n,isInitialLoading:i&&!n,isRevalidating:i&&n,isEmpty:t==="succeeded"&&!n}},C=(e,t,r=Date.now())=>e===null||r-e>t,w=(e,t="Unknown error")=>{if(typeof e=="string"&&e.length>0)return e;if(e!==null&&typeof e=="object"){const r=e,a=r.response?.data?.message??r.response?.data?.error;if(typeof a=="string"&&a.length>0)return a;if(typeof r.message=="string"&&r.message.length>0)return r.message}return t},j=e=>new Promise(t=>setTimeout(t,e));function x(e,t={}){const{isEmpty:r,fetchOnMount:a=!0,keepPreviousData:o=!0,autoRetry:l,deps:n=[]}=t,[i,y]=s.useState(()=>A()),b=s.useRef(e);b.current=e;const u=s.useRef(0),f=s.useRef(null),R=l?.attempts??0,h=l?.delayMs??500,g=s.useCallback(async()=>{const p=++u.current;f.current?.abort();const c=new AbortController;f.current=c,y(d=>D(d,{clearData:!o}));const I=Math.max(0,R);for(let d=0;;d+=1)try{const m=await b.current(c.signal);if(p!==u.current||c.signal.aborted)return;y(E(m));return}catch(m){if(p!==u.current||c.signal.aborted)return;if(d<I){if(await j(h*(d+1)),p!==u.current||c.signal.aborted)return;continue}y(k=>S(w(m),k));return}},[o,R,h]);s.useEffect(()=>(a&&g(),()=>{u.current+=1,f.current?.abort()}),[a,g,...n]);const M=s.useCallback(()=>{u.current+=1,f.current?.abort(),y(A())},[]);return{...q({...i,isEmpty:r}),refetch:g,retry:g,reset:M}}exports.asyncIdle=A,exports.asyncRequestFailed=S,exports.asyncRequestStarted=D,exports.asyncRequestSucceeded=E,exports.defaultIsEmpty=v,exports.deriveAsyncState=q,exports.getErrorMessage=w,exports.isStale=C,exports.useAsyncData=x;
|
|
2
|
+
//# sourceMappingURL=async-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-state.js","sources":["../../src/asyncState/asyncState.ts","../../src/asyncState/getErrorMessage.ts","../../src/asyncState/useAsyncData.ts"],"sourcesContent":["/**\n * Modelo canónico de estado asíncrono (loading / error / caché remota).\n *\n * Unifica cómo los paneles representan el ciclo de vida de un dato remoto:\n *\n * - `idle`/`loading` sin datos → carga inicial (bloqueante: skeleton/spinner).\n * - `loading` con datos → revalidación NO bloqueante (`isRevalidating`).\n * - `rejected` con datos → error manteniendo datos previos + retry.\n * - `rejected` sin datos → error bloqueante con retry.\n * - `succeeded` sin datos → estado realmente vacío (`isEmpty`).\n *\n * Los literales de `AsyncStatus` coinciden con el `RequestStatus` que usan los\n * slices de los frontends Trii, para que sean compatibles sin conversión.\n *\n * Módulo puro: sin React, sin MUI, sin Redux, sin dominio. Se publica también\n * por el subpath `@trii/components/async-state` para que un slice pueda\n * consumirlo sin arrastrar la UI del paquete.\n *\n * Guía de uso: docs/ASYNC_STATE.md\n */\n\nexport type AsyncStatus = 'idle' | 'loading' | 'succeeded' | 'rejected';\n\n/**\n * Estado \"crudo\" persistible: es lo que guarda un slice de Redux o el estado\n * interno de `useAsyncData`. Los flags derivados se calculan con\n * `deriveAsyncState` (nunca se almacenan, para evitar inconsistencias).\n */\nexport interface AsyncData<T> {\n status: AsyncStatus;\n /** Últimos datos válidos. Se preservan durante refetches y errores. */\n data: T | undefined;\n /** Mensaje del último error (null si la última operación fue exitosa). */\n error: string | null;\n /** Timestamp (Date.now()) del último fetch exitoso. Base para staleness. */\n updatedAt: number | null;\n}\n\n/** Estado derivado listo para consumir desde la UI. */\nexport interface AsyncState<T> extends AsyncData<T> {\n /** true si hay datos utilizables para mostrar (no undefined ni \"vacíos\"). */\n hasData: boolean;\n /** Carga inicial: está cargando y NO hay datos que mostrar → bloqueante. */\n isInitialLoading: boolean;\n /** Revalidación: está cargando y SÍ hay datos → no bloquear la UI. */\n isRevalidating: boolean;\n /** Vacío real: la última carga fue exitosa y no hay datos que mostrar. */\n isEmpty: boolean;\n}\n\n/** Decide si un dato cuenta como \"vacío\" (p.ej. lista sin elementos). */\nexport type IsEmptyFn<T> = (data: T) => boolean;\n\n/**\n * Vacío por defecto: `null`/`undefined` y arrays sin elementos.\n * Cada feature puede aportar su propia regla (p.ej. \"objeto sin campos\").\n */\nexport const defaultIsEmpty = <T>(data: T): boolean => {\n if (data === null || data === undefined) return true;\n if (Array.isArray(data)) return data.length === 0;\n return false;\n};\n\n/** Estado inicial: nunca se hizo un fetch. */\nexport const asyncIdle = <T>(): AsyncData<T> => ({\n status: 'idle',\n data: undefined,\n error: null,\n updatedAt: null,\n});\n\n/**\n * Transición a \"request en curso\".\n * Por defecto preserva los datos previos (refetch no bloqueante).\n * Pasar `clearData: true` solo cuando los datos viejos dejarían de ser\n * válidos para la nueva request (p.ej. cambio de contacto seleccionado).\n */\nexport const asyncRequestStarted = <T>(\n prev: AsyncData<T>,\n options?: { clearData?: boolean }\n): AsyncData<T> => ({\n status: 'loading',\n data: options?.clearData ? undefined : prev.data,\n error: null,\n updatedAt: prev.updatedAt,\n});\n\n/** Transición a éxito: guarda datos, limpia error y sella `updatedAt`. */\nexport const asyncRequestSucceeded = <T>(data: T): AsyncData<T> => ({\n status: 'succeeded',\n data,\n error: null,\n updatedAt: Date.now(),\n});\n\n/**\n * Transición a error: guarda el mensaje y PRESERVA los datos previos\n * para que la UI pueda seguir mostrándolos (error con datos).\n */\nexport const asyncRequestFailed = <T>(\n error: string,\n prev: AsyncData<T>\n): AsyncData<T> => ({\n status: 'rejected',\n data: prev.data,\n error,\n updatedAt: prev.updatedAt,\n});\n\n/**\n * Deriva los flags de UI a partir del estado crudo. Es la ÚNICA fuente de\n * verdad para interpretar un estado asíncrono: la usan `useAsyncData`,\n * los selectores de Redux y `<AsyncContent>`.\n *\n * Uso con un slice existente (sin reescribirlo):\n *\n * ```ts\n * const selectChannelsAsyncState = createSelector(\n * selectChannelsState,\n * (state) =>\n * deriveAsyncState({\n * status: state.status.fetch,\n * data: state.channels,\n * error: state.error,\n * })\n * );\n * ```\n */\nexport const deriveAsyncState = <T>(input: {\n status: AsyncStatus;\n data: T | undefined;\n error?: string | null;\n updatedAt?: number | null;\n isEmpty?: IsEmptyFn<T>;\n}): AsyncState<T> => {\n const { status, data, error = null, updatedAt = null } = input;\n const isEmptyFn: IsEmptyFn<T> = input.isEmpty ?? defaultIsEmpty;\n\n const hasData = data !== undefined && !isEmptyFn(data);\n const isLoading = status === 'loading';\n\n return {\n status,\n data,\n error,\n updatedAt,\n hasData,\n isInitialLoading: isLoading && !hasData,\n isRevalidating: isLoading && hasData,\n isEmpty: status === 'succeeded' && !hasData,\n };\n};\n\n/** true si el dato tiene más de `staleTime` ms (o nunca se cargó). */\nexport const isStale = (\n updatedAt: number | null,\n staleTime: number,\n now: number = Date.now()\n): boolean => updatedAt === null || now - updatedAt > staleTime;\n","/**\n * Extrae un mensaje legible de cualquier error (axios, Error, string...).\n *\n * Orden de preferencia:\n * 1. `error.response.data.message` / `error.response.data.error` (API Trii).\n * 2. `error.message` (Error estándar, p.ej. \"Network Error\" de axios).\n * 3. El string recibido tal cual.\n * 4. `fallback`.\n *\n * No traduce: la traducción de mensajes genéricos ocurre en la UI\n * (`<ErrorState>`); esto preserva el detalle técnico o del backend.\n *\n * Tipa axios por duck-typing a propósito: el módulo no importa axios, así que\n * sirve igual con fetch o con cualquier cliente HTTP.\n */\nexport const getErrorMessage = (\n error: unknown,\n fallback = 'Unknown error'\n): string => {\n if (typeof error === 'string' && error.length > 0) return error;\n\n if (error !== null && typeof error === 'object') {\n const candidate = error as {\n response?: { data?: { message?: unknown; error?: unknown } };\n message?: unknown;\n };\n\n const apiMessage =\n candidate.response?.data?.message ?? candidate.response?.data?.error;\n if (typeof apiMessage === 'string' && apiMessage.length > 0) {\n return apiMessage;\n }\n\n if (typeof candidate.message === 'string' && candidate.message.length > 0) {\n return candidate.message;\n }\n }\n\n return fallback;\n};\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport { UseAsyncDataResult } from './types';\nimport {\n AsyncData,\n IsEmptyFn,\n asyncIdle,\n asyncRequestFailed,\n asyncRequestStarted,\n asyncRequestSucceeded,\n deriveAsyncState,\n} from './asyncState';\nimport { getErrorMessage } from './getErrorMessage';\n\nconst sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * Ciclo de vida completo de un dato remoto LOCAL a un panel (sin Redux).\n *\n * Comportamiento:\n * - Fetch al montar (configurable con `fetchOnMount`) y cuando cambian `deps`.\n * - Carga inicial sin datos → `isInitialLoading` (bloqueante).\n * - Refetch con datos → `isRevalidating`: los datos siguen visibles.\n * - Error con datos previos → `status: 'rejected'` + datos preservados.\n * - Error sin datos → `error` para mostrar un estado de error con `retry`.\n * - `refetch()`/`retry()` re-ejecutan el fetcher; nunca bloquean si hay datos.\n * - Cancelación automática (AbortSignal) y guarda de carrera: si se dispara\n * una request nueva, las respuestas viejas se descartan.\n * - `autoRetry` opcional con backoff lineal antes de mostrar el error.\n *\n * Usar Redux en su lugar cuando el dato se comparte entre varios paneles o\n * se actualiza por socket (ver docs/ASYNC_STATE.md §¿Redux o hook?).\n *\n * @example\n * const state = useAsyncData(\n * (signal) => reportsService.getReports(url, { signal }),\n * { deps: [url] }\n * );\n * return (\n * <AsyncContent state={state} onRetry={state.retry}>\n * {(reports) => <ReportList reports={reports} />}\n * </AsyncContent>\n * );\n */\nexport function useAsyncData<T>(\n fetcher: (signal: AbortSignal) => Promise<T>,\n options: {\n /** Regla de \"vacío\" del dato (default: null/undefined o array vacío). */\n isEmpty?: IsEmptyFn<T>;\n /** Disparar el fetch al montar y al cambiar `deps` (default: true). */\n fetchOnMount?: boolean;\n /**\n * Preservar datos previos durante refetches (default: true).\n * Poner en false solo si los datos viejos serían inválidos/engañosos.\n */\n keepPreviousData?: boolean;\n /**\n * Reintentos automáticos antes de marcar error (default: sin reintentos;\n * el usuario decide con el botón de retry).\n */\n autoRetry?: { attempts: number; delayMs?: number };\n /** Dependencias que re-disparan el fetch (p.ej. un id o filtro). */\n deps?: ReadonlyArray<unknown>;\n } = {}\n): UseAsyncDataResult<T> {\n const {\n isEmpty,\n fetchOnMount = true,\n keepPreviousData = true,\n autoRetry,\n deps = [],\n } = options;\n\n const [state, setState] = useState<AsyncData<T>>(() => asyncIdle<T>());\n\n // Refs: la última versión del fetcher y el control de carrera/cancelación.\n // Guardar el fetcher en ref evita re-ejecutar el efecto cuando el caller\n // pasa una arrow function inline (caso común).\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n const requestIdRef = useRef(0);\n const abortRef = useRef<AbortController | null>(null);\n\n const autoRetryAttempts = autoRetry?.attempts ?? 0;\n const autoRetryDelayMs = autoRetry?.delayMs ?? 500;\n\n const execute = useCallback(async (): Promise<void> => {\n const requestId = ++requestIdRef.current;\n abortRef.current?.abort();\n const controller = new AbortController();\n abortRef.current = controller;\n\n setState((prev) =>\n asyncRequestStarted(prev, { clearData: !keepPreviousData })\n );\n\n const maxAttempts = Math.max(0, autoRetryAttempts);\n\n for (let attempt = 0; ; attempt += 1) {\n try {\n const data = await fetcherRef.current(controller.signal);\n // Respuesta vieja (hubo otra request después): se descarta.\n if (requestId !== requestIdRef.current || controller.signal.aborted) {\n return;\n }\n setState(asyncRequestSucceeded(data));\n return;\n } catch (error) {\n // Cancelada por desmontaje o por una request nueva: no es un error.\n if (requestId !== requestIdRef.current || controller.signal.aborted) {\n return;\n }\n if (attempt < maxAttempts) {\n // Backoff lineal: 1x, 2x, 3x... el delay base.\n await sleep(autoRetryDelayMs * (attempt + 1));\n if (requestId !== requestIdRef.current || controller.signal.aborted) {\n return;\n }\n continue;\n }\n setState((prev) => asyncRequestFailed(getErrorMessage(error), prev));\n return;\n }\n }\n }, [keepPreviousData, autoRetryAttempts, autoRetryDelayMs]);\n\n useEffect(() => {\n if (fetchOnMount) {\n // Fire-and-forget: el estado se actualiza vía setState dentro de execute.\n execute();\n }\n return () => {\n // Desmontaje o cambio de deps: cancela e invalida la request en curso.\n requestIdRef.current += 1;\n abortRef.current?.abort();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fetchOnMount, execute, ...deps]);\n\n const reset = useCallback((): void => {\n requestIdRef.current += 1;\n abortRef.current?.abort();\n setState(asyncIdle<T>());\n }, []);\n\n const derived = deriveAsyncState({ ...state, isEmpty });\n\n return { ...derived, refetch: execute, retry: execute, reset };\n}\n"],"names":["defaultIsEmpty","data","asyncIdle","asyncRequestStarted","prev","options","asyncRequestSucceeded","asyncRequestFailed","error","deriveAsyncState","input","status","updatedAt","isEmptyFn","hasData","isLoading","isStale","staleTime","now","getErrorMessage","fallback","candidate","apiMessage","sleep","ms","resolve","useAsyncData","fetcher","isEmpty","fetchOnMount","keepPreviousData","autoRetry","deps","state","setState","useState","fetcherRef","useRef","requestIdRef","abortRef","autoRetryAttempts","autoRetryDelayMs","execute","useCallback","requestId","controller","maxAttempts","attempt","useEffect","reset"],"mappings":"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"}
|