ropegeo-common 1.12.10 → 1.12.12

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
@@ -312,9 +312,9 @@ Abstract classes live under `minimap/abstract/`, concrete under `minimap/concret
312
312
 
313
313
  | Name | Description | Import |
314
314
  | --- | --- | --- |
315
- | `RopeGeoHttpRequest` | Single GET/POST wrapper; parses `Result.fromResponseBody`; optional `timeoutAfterSeconds` (default from helpers) + `timeoutCountdown` children arg. | `import { RopeGeoHttpRequest, Method, Service } from 'ropegeo-common/components'` |
316
- | `RopeGeoCursorPaginationHttpRequest` | Cursor-paginated fetch with `loadMore`; optional `timeoutAfterSeconds`; `timeoutCountdown` on initial and `loadMore` requests. | `import { RopeGeoCursorPaginationHttpRequest } from 'ropegeo-common/components'` |
317
- | `RopeGeoPaginationHttpRequest<T>` | Page-based fetch; page 1 emits `timeoutCountdown`; later pages use the same per-fetch deadline; concatenates `results` into `data` (`T[]` when complete, otherwise `null` with `errors`). | `import { RopeGeoPaginationHttpRequest } from 'ropegeo-common/components'` |
315
+ | `RopeGeoHttpRequest` | Single GET/POST wrapper; parses `Result.fromResponseBody`; optional `timeoutAfterSeconds`, `isOnline`, `refreshOnReconnect`, `timeoutCountdown`, and `refreshing` (stale-while-revalidate) children args. | `import { RopeGeoHttpRequest, Method, Service } from 'ropegeo-common/components'` |
316
+ | `RopeGeoCursorPaginationHttpRequest` | Cursor-paginated fetch with `loadMore`; `data` is `T[] \| null` (null until first success); optional `timeoutAfterSeconds`, `isOnline`, `refreshOnReconnect`, `refreshing`, `timeoutCountdown`. | `import { RopeGeoCursorPaginationHttpRequest } from 'ropegeo-common/components'` |
317
+ | `RopeGeoPaginationHttpRequest<T>` | Page-based fetch; concatenates `results` into `data` (`T[]` when complete, otherwise `null` with `errors`); optional `isOnline`, `refreshOnReconnect`, `refreshing`, `timeoutCountdown`. | `import { RopeGeoPaginationHttpRequest } from 'ropegeo-common/components'` |
318
318
 
319
319
  ---
320
320
 
@@ -14,23 +14,37 @@ export type RopeGeoCursorPaginationHttpRequestProps<T = unknown> = {
14
14
  timeoutAfterSeconds?: number;
15
15
  /**
16
16
  * When `false`, no HTTP requests run and children receive {@link NO_NETWORK_MESSAGE} as the error.
17
- * Previously loaded `data` and cursor `params` are kept until the network returns (same stale
18
- * behavior as {@link RopeGeoPaginationHttpRequest} / {@link RopeGeoHttpRequest} when offline).
17
+ * Previously loaded `data` and cursor `params` are kept until the network returns.
19
18
  */
20
19
  isOnline?: boolean;
20
+ /**
21
+ * When `isOnline` goes from `false` to online and there is already successful data for the same
22
+ * request (only the soft {@link NO_NETWORK_MESSAGE} error), a new fetch runs only if this is
23
+ * `true`. Otherwise stale data stays visible and `errors` is cleared. When there is no
24
+ * successful data yet, or the last error was not the offline placeholder, a fetch always runs.
25
+ * @default false
26
+ */
27
+ refreshOnReconnect?: boolean;
21
28
  /**
22
29
  * Response body is parsed via CursorPaginationResults.fromResponseBody (must include resultType).
23
30
  * Parsed shape is ValidatedCursorPaginationResponse; children receive result.results as data.
31
+ * `data` is `null` until the first successful response for the current request identity, then an
32
+ * array (possibly empty) for loaded pages.
24
33
  */
25
34
  children: (args: {
26
35
  loading: boolean;
27
36
  loadingMore: boolean;
28
- data: T[];
37
+ /**
38
+ * `true` while the initial request is in flight after at least one successful response for the
39
+ * current request identity (stale-while-revalidate). Not used for `loadMore` alone.
40
+ */
41
+ refreshing: boolean;
42
+ data: T[] | null;
29
43
  errors: Error | null;
30
44
  loadMore: () => void;
31
45
  hasMore: boolean;
32
46
  timeoutCountdown: number | null;
33
47
  }) => ReactNode;
34
48
  };
35
- export declare function RopeGeoCursorPaginationHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, timeoutAfterSeconds, isOnline, children, }: RopeGeoCursorPaginationHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
49
+ export declare function RopeGeoCursorPaginationHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, timeoutAfterSeconds, isOnline, refreshOnReconnect, children, }: RopeGeoCursorPaginationHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
36
50
  //# sourceMappingURL=RopeGeoCursorPaginationHttpRequest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RopeGeoCursorPaginationHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoCursorPaginationHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AASvC,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAoB,MAAM,sBAAsB,CAAC;AAsCzE,MAAM,MAAM,uCAAuC,CAAC,CAAC,GAAG,OAAO,IAAI;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,EAAE,sBAAsB,CAAC;IACpC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,EAAE,OAAO,CAAC;QACrB,IAAI,EAAE,CAAC,EAAE,CAAC;QACV,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE,MAAM,IAAI,CAAC;QACrB,OAAO,EAAE,OAAO,CAAC;QACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF,wBAAgB,kCAAkC,CAAC,CAAC,GAAG,OAAO,EAAE,EAC9D,OAAO,EACP,MAAmB,EACnB,IAAI,EACJ,UAAU,EACV,WAAW,EACX,mBAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,uCAAuC,CAAC,CAAC,CAAC,2CAsQ5C"}
1
+ {"version":3,"file":"RopeGeoCursorPaginationHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoCursorPaginationHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAoB,MAAM,sBAAsB,CAAC;AAsCzE,MAAM,MAAM,uCAAuC,CAAC,CAAC,GAAG,OAAO,IAAI;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,EAAE,sBAAsB,CAAC;IACpC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,EAAE,OAAO,CAAC;QACrB;;;WAGG;QACH,UAAU,EAAE,OAAO,CAAC;QACpB,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE,MAAM,IAAI,CAAC;QACrB,OAAO,EAAE,OAAO,CAAC;QACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF,wBAAgB,kCAAkC,CAAC,CAAC,GAAG,OAAO,EAAE,EAC9D,OAAO,EACP,MAAmB,EACnB,IAAI,EACJ,UAAU,EACV,WAAW,EACX,mBAAmB,EACnB,QAAQ,EACR,kBAA0B,EAC1B,QAAQ,GACT,EAAE,uCAAuC,CAAC,CAAC,CAAC,2CAqV5C"}
@@ -30,16 +30,26 @@ function getResponseBody(raw) {
30
30
  }
31
31
  return raw;
32
32
  }
33
- function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.Method.GET, path, pathParams, queryParams, timeoutAfterSeconds, isOnline, children, }) {
33
+ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.Method.GET, path, pathParams, queryParams, timeoutAfterSeconds, isOnline, refreshOnReconnect = false, children, }) {
34
34
  const [loading, setLoading] = (0, react_1.useState)(true);
35
35
  const [loadingMore, setLoadingMore] = (0, react_1.useState)(false);
36
- const [data, setData] = (0, react_1.useState)([]);
36
+ const [data, setData] = (0, react_1.useState)(null);
37
37
  const [params, setParams] = (0, react_1.useState)(queryParams);
38
38
  const [errors, setErrors] = (0, react_1.useState)(null);
39
39
  const [timeoutCountdown, setTimeoutCountdown] = (0, react_1.useState)(null);
40
+ const [hasCommittedOnce, setHasCommittedOnce] = (0, react_1.useState)(false);
41
+ const errorsRef = (0, react_1.useRef)(errors);
42
+ const hasCommittedRef = (0, react_1.useRef)(hasCommittedOnce);
43
+ errorsRef.current = errors;
44
+ hasCommittedRef.current = hasCommittedOnce;
40
45
  const loadingMoreRef = (0, react_1.useRef)(false);
41
46
  const loadMoreAbortRef = (0, react_1.useRef)(null);
42
47
  const hasMore = params.cursor != null;
48
+ const pathParamsKey = JSON.stringify(pathParams ?? null);
49
+ const queryKey = queryParams.toQueryString();
50
+ const requestKey = (0, react_1.useMemo)(() => `${service}|${method}|${path}|${pathParamsKey}|${queryKey}|${timeoutAfterSeconds ?? ""}`, [service, method, path, pathParamsKey, queryKey, timeoutAfterSeconds]);
51
+ const prevIsOnlineRef = (0, react_1.useRef)(undefined);
52
+ const lastRequestKeyRef = (0, react_1.useRef)("");
43
53
  const buildUrl = (0, react_1.useCallback)((p) => {
44
54
  const baseUrl = RopeGeoHttpRequest_1.SERVICE_BASE_URL[service];
45
55
  const resolvedPath = resolvePath(path, pathParams);
@@ -48,26 +58,61 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
48
58
  return new URL(fullPath, baseUrl).toString();
49
59
  }, [service, path, pathParams]);
50
60
  (0, react_1.useEffect)(() => {
51
- if (isOnline === false) {
61
+ const online = isOnline !== false;
62
+ const prevOnline = prevIsOnlineRef.current;
63
+ const reconnecting = prevOnline === false && online;
64
+ const keyChanged = lastRequestKeyRef.current !== requestKey;
65
+ if (!online) {
66
+ if (keyChanged) {
67
+ lastRequestKeyRef.current = requestKey;
68
+ setData(null);
69
+ setHasCommittedOnce(false);
70
+ setParams(queryParams);
71
+ setErrors(null);
72
+ }
52
73
  loadMoreAbortRef.current?.abort();
53
74
  loadMoreAbortRef.current = null;
54
75
  loadingMoreRef.current = false;
55
76
  setLoadingMore(false);
77
+ prevIsOnlineRef.current = false;
56
78
  setLoading(false);
57
79
  setErrors(new Error(network_1.NO_NETWORK_MESSAGE));
58
80
  setTimeoutCountdown(null);
59
81
  return;
60
82
  }
83
+ if (keyChanged) {
84
+ lastRequestKeyRef.current = requestKey;
85
+ setHasCommittedOnce(false);
86
+ setData(null);
87
+ setParams(queryParams);
88
+ setErrors(null);
89
+ }
90
+ if (!keyChanged && reconnecting) {
91
+ const onlyNoNetwork = errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE;
92
+ if (hasCommittedRef.current && onlyNoNetwork && !refreshOnReconnect) {
93
+ setErrors(null);
94
+ setLoading(false);
95
+ prevIsOnlineRef.current = true;
96
+ return;
97
+ }
98
+ }
99
+ prevIsOnlineRef.current = true;
61
100
  let cancelled = false;
62
101
  const abortController = new AbortController();
63
102
  const timedOutRef = { current: false };
64
103
  const requestStartedAt = Date.now();
65
104
  const timeoutMs = (0, network_1.resolveRequestTimeoutMs)(timeoutAfterSeconds);
66
- setData([]);
67
- setParams(queryParams);
68
105
  setLoading(true);
69
106
  setErrors(null);
70
107
  setTimeoutCountdown(null);
108
+ const keepStaleDuringFetch = reconnecting &&
109
+ hasCommittedRef.current &&
110
+ errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE &&
111
+ refreshOnReconnect;
112
+ if (!keyChanged && !keepStaleDuringFetch) {
113
+ setData(null);
114
+ setParams(queryParams);
115
+ }
71
116
  const policyDispose = timeoutMs == null
72
117
  ? () => { }
73
118
  : (0, network_1.installNetworkRequestPolicyTimers)(requestStartedAt, timeoutMs, {
@@ -97,12 +142,16 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
97
142
  return;
98
143
  const text = await res.text();
99
144
  if (!res.ok) {
100
- setErrors(new Error(`HTTP ${res.status}: ${text || res.statusText}`));
101
- setData([]);
145
+ setErrors(new Error((0, network_1.formatHttpStatusMessage)(res.status, text || res.statusText)));
146
+ setData(null);
147
+ setHasCommittedOnce(false);
102
148
  return;
103
149
  }
104
150
  if (text.length === 0) {
105
151
  setData([]);
152
+ setParams(queryParams.withCursor(null));
153
+ setErrors(null);
154
+ setHasCommittedOnce(true);
106
155
  return;
107
156
  }
108
157
  try {
@@ -115,6 +164,7 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
115
164
  setData(results);
116
165
  setParams(queryParams.withCursor(nextCursor));
117
166
  setErrors(null);
167
+ setHasCommittedOnce(true);
118
168
  }
119
169
  catch (parseError) {
120
170
  if (!cancelled) {
@@ -125,7 +175,8 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
125
175
  parseError: parseError instanceof Error ? parseError.message : String(parseError),
126
176
  });
127
177
  setErrors(new Error("Invalid JSON response"));
128
- setData([]);
178
+ setData(null);
179
+ setHasCommittedOnce(false);
129
180
  }
130
181
  }
131
182
  })
@@ -133,8 +184,9 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
133
184
  if (cancelled)
134
185
  return;
135
186
  if (timedOutRef.current) {
136
- setErrors(new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE));
137
- setData([]);
187
+ setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE))));
188
+ setData(null);
189
+ setHasCommittedOnce(false);
138
190
  return;
139
191
  }
140
192
  if ((0, network_1.isAbortError)(err))
@@ -143,8 +195,9 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
143
195
  url,
144
196
  error: err instanceof Error ? err.message : String(err),
145
197
  });
146
- setErrors(err instanceof Error ? err : new Error(String(err)));
147
- setData([]);
198
+ setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(err)));
199
+ setData(null);
200
+ setHasCommittedOnce(false);
148
201
  })
149
202
  .finally(() => {
150
203
  policyDispose();
@@ -162,11 +215,13 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
162
215
  service,
163
216
  method,
164
217
  path,
165
- pathParams,
166
- queryParams,
218
+ pathParamsKey,
219
+ queryKey,
167
220
  buildUrl,
168
221
  timeoutAfterSeconds,
169
222
  isOnline,
223
+ refreshOnReconnect,
224
+ requestKey,
170
225
  ]);
171
226
  (0, react_1.useEffect)(() => {
172
227
  return () => {
@@ -224,7 +279,7 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
224
279
  const body = getResponseBody(raw);
225
280
  const result = models_1.CursorPaginationResults.fromResponseBody(body);
226
281
  const { results, nextCursor } = result;
227
- setData((prev) => [...prev, ...results]);
282
+ setData((prev) => [...(prev ?? []), ...results]);
228
283
  setParams((p) => p.withCursor(nextCursor));
229
284
  }
230
285
  catch (parseError) {
@@ -260,9 +315,11 @@ function RopeGeoCursorPaginationHttpRequest({ service, method = RopeGeoHttpReque
260
315
  setLoadingMore(false);
261
316
  });
262
317
  }, [params, method, buildUrl, timeoutAfterSeconds, isOnline]);
318
+ const refreshing = loading && hasCommittedOnce;
263
319
  return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children({
264
320
  loading,
265
321
  loadingMore,
322
+ refreshing,
266
323
  data,
267
324
  errors,
268
325
  loadMore,
@@ -29,12 +29,25 @@ export type RopeGeoHttpRequestProps<T = unknown> = {
29
29
  * not fired while offline.
30
30
  */
31
31
  isOnline?: boolean;
32
+ /**
33
+ * When `isOnline` goes from `false` to online and there is already successful data for the same
34
+ * request (only the soft {@link NO_NETWORK_MESSAGE} error), a new fetch runs only if this is
35
+ * `true`. Otherwise stale data stays visible and `errors` is cleared. When there is no
36
+ * successful data yet, or the last error was not the offline placeholder, a fetch always runs.
37
+ * @default false
38
+ */
39
+ refreshOnReconnect?: boolean;
32
40
  /**
33
41
  * Response body is parsed via Result.fromResponseBody (must include resultType and result).
34
42
  * Children receive the validated result.result as data (typed by T).
35
43
  */
36
44
  children: (args: {
37
45
  loading: boolean;
46
+ /**
47
+ * `true` while a request is in flight after at least one successful response for the current
48
+ * request identity (stale-while-revalidate).
49
+ */
50
+ refreshing: boolean;
38
51
  data: T | null;
39
52
  errors: Error | null;
40
53
  /**
@@ -45,5 +58,5 @@ export type RopeGeoHttpRequestProps<T = unknown> = {
45
58
  timeoutCountdown: number | null;
46
59
  }) => ReactNode;
47
60
  };
48
- export declare function RopeGeoHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, body, timeoutAfterSeconds, isOnline, children, }: RopeGeoHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
61
+ export declare function RopeGeoHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, body, timeoutAfterSeconds, isOnline, refreshOnReconnect, children, }: RopeGeoHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
49
62
  //# sourceMappingURL=RopeGeoHttpRequest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RopeGeoHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAWvC,eAAO,MAAM,OAAO;;CAEV,CAAC;AACX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAC;AAE7D,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AACX,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;AAE1D,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAEpD,CAAC;AAmCF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,OAAO,IAAI;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB;;;;WAIG;QACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,OAAO,EAAE,EAC9C,OAAO,EACP,MAAM,EACN,IAAI,EACJ,UAAU,EACV,WAAW,EACX,IAAI,EACJ,mBAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CAwJ5B"}
1
+ {"version":3,"file":"RopeGeoHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAavC,eAAO,MAAM,OAAO;;CAEV,CAAC;AACX,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO,OAAO,CAAC,CAAC;AAE7D,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AACX,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;AAE1D,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAEpD,CAAC;AAmCF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,OAAO,IAAI;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,OAAO,EAAE,OAAO,CAAC;QACjB;;;WAGG;QACH,UAAU,EAAE,OAAO,CAAC;QACpB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB;;;;WAIG;QACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,OAAO,EAAE,EAC9C,OAAO,EACP,MAAM,EACN,IAAI,EACJ,UAAU,EACV,WAAW,EACX,IAAI,EACJ,mBAAmB,EACnB,QAAQ,EACR,kBAA0B,EAC1B,QAAQ,GACT,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CAkO5B"}
@@ -40,11 +40,16 @@ function buildUrl(baseUrl, path, pathParams, queryParams) {
40
40
  }
41
41
  return url.toString();
42
42
  }
43
- function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, body, timeoutAfterSeconds, isOnline, children, }) {
43
+ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, body, timeoutAfterSeconds, isOnline, refreshOnReconnect = false, children, }) {
44
44
  const [loading, setLoading] = (0, react_1.useState)(true);
45
45
  const [data, setData] = (0, react_1.useState)(null);
46
46
  const [errors, setErrors] = (0, react_1.useState)(null);
47
47
  const [timeoutCountdown, setTimeoutCountdown] = (0, react_1.useState)(null);
48
+ const [hasCommittedOnce, setHasCommittedOnce] = (0, react_1.useState)(false);
49
+ const errorsRef = (0, react_1.useRef)(errors);
50
+ const hasCommittedRef = (0, react_1.useRef)(hasCommittedOnce);
51
+ errorsRef.current = errors;
52
+ hasCommittedRef.current = hasCommittedOnce;
48
53
  const pathParamsKey = JSON.stringify(pathParams ?? null);
49
54
  const queryParamsKey = JSON.stringify(queryParams ?? null);
50
55
  const bodyKey = body === undefined || body === null
@@ -52,13 +57,43 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
52
57
  : typeof body === "object"
53
58
  ? JSON.stringify(body)
54
59
  : body;
60
+ const requestKey = (0, react_1.useMemo)(() => `${service}|${method}|${path}|${pathParamsKey}|${queryParamsKey}|${String(bodyKey)}|${timeoutAfterSeconds ?? ""}`, [service, method, path, pathParamsKey, queryParamsKey, bodyKey, timeoutAfterSeconds]);
61
+ const prevIsOnlineRef = (0, react_1.useRef)(undefined);
62
+ const lastRequestKeyRef = (0, react_1.useRef)("");
55
63
  (0, react_1.useEffect)(() => {
56
- if (isOnline === false) {
64
+ const online = isOnline !== false;
65
+ const prevOnline = prevIsOnlineRef.current;
66
+ const reconnecting = prevOnline === false && online;
67
+ const keyChanged = lastRequestKeyRef.current !== requestKey;
68
+ if (!online) {
69
+ if (keyChanged) {
70
+ lastRequestKeyRef.current = requestKey;
71
+ setData(null);
72
+ setHasCommittedOnce(false);
73
+ setErrors(null);
74
+ }
75
+ prevIsOnlineRef.current = false;
57
76
  setLoading(false);
58
77
  setErrors(new Error(network_1.NO_NETWORK_MESSAGE));
59
78
  setTimeoutCountdown(null);
60
79
  return;
61
80
  }
81
+ if (keyChanged) {
82
+ lastRequestKeyRef.current = requestKey;
83
+ setHasCommittedOnce(false);
84
+ setData(null);
85
+ setErrors(null);
86
+ }
87
+ if (!keyChanged && reconnecting) {
88
+ const onlyNoNetwork = errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE;
89
+ if (hasCommittedRef.current && onlyNoNetwork && !refreshOnReconnect) {
90
+ setErrors(null);
91
+ setLoading(false);
92
+ prevIsOnlineRef.current = true;
93
+ return;
94
+ }
95
+ }
96
+ prevIsOnlineRef.current = true;
62
97
  let cancelled = false;
63
98
  const abortController = new AbortController();
64
99
  const timedOutRef = { current: false };
@@ -67,6 +102,13 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
67
102
  setLoading(true);
68
103
  setErrors(null);
69
104
  setTimeoutCountdown(null);
105
+ const keepStaleDuringFetch = reconnecting &&
106
+ hasCommittedRef.current &&
107
+ errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE &&
108
+ refreshOnReconnect;
109
+ if (!keyChanged && !keepStaleDuringFetch) {
110
+ setData(null);
111
+ }
70
112
  const policyDispose = timeoutMs == null
71
113
  ? () => { }
72
114
  : (0, network_1.installNetworkRequestPolicyTimers)(requestStartedAt, timeoutMs, {
@@ -102,12 +144,15 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
102
144
  return;
103
145
  const text = await res.text();
104
146
  if (!res.ok) {
105
- setErrors(new Error(`HTTP ${res.status}: ${text || res.statusText}`));
147
+ setErrors(new Error((0, network_1.formatHttpStatusMessage)(res.status, text || res.statusText)));
106
148
  setData(null);
149
+ setHasCommittedOnce(false);
107
150
  return;
108
151
  }
109
152
  if (text.length === 0) {
110
153
  setData(null);
154
+ setErrors(null);
155
+ setHasCommittedOnce(true);
111
156
  return;
112
157
  }
113
158
  try {
@@ -116,6 +161,7 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
116
161
  if (!cancelled) {
117
162
  setData(parsed.result);
118
163
  setErrors(null);
164
+ setHasCommittedOnce(true);
119
165
  }
120
166
  }
121
167
  catch (parseError) {
@@ -128,6 +174,7 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
128
174
  });
129
175
  setErrors(parseError instanceof Error ? parseError : new Error("Invalid JSON response"));
130
176
  setData(null);
177
+ setHasCommittedOnce(false);
131
178
  }
132
179
  }
133
180
  })
@@ -135,8 +182,9 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
135
182
  if (cancelled)
136
183
  return;
137
184
  if (timedOutRef.current) {
138
- setErrors(new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE));
185
+ setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE))));
139
186
  setData(null);
187
+ setHasCommittedOnce(false);
140
188
  return;
141
189
  }
142
190
  if ((0, network_1.isAbortError)(err))
@@ -145,8 +193,9 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
145
193
  url,
146
194
  error: err instanceof Error ? err.message : String(err),
147
195
  });
148
- setErrors(err instanceof Error ? err : new Error(String(err)));
196
+ setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(err)));
149
197
  setData(null);
198
+ setHasCommittedOnce(false);
150
199
  })
151
200
  .finally(() => {
152
201
  policyDispose();
@@ -169,9 +218,13 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
169
218
  bodyKey,
170
219
  timeoutAfterSeconds,
171
220
  isOnline,
221
+ refreshOnReconnect,
222
+ requestKey,
172
223
  ]);
224
+ const refreshing = loading && hasCommittedOnce;
173
225
  return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children({
174
226
  loading,
227
+ refreshing,
175
228
  data,
176
229
  errors,
177
230
  timeoutCountdown,
@@ -25,8 +25,21 @@ export type RopeGeoPaginationHttpRequestProps<T = unknown> = {
25
25
  * {@link RopeGeoCursorPaginationHttpRequest}.
26
26
  */
27
27
  isOnline?: boolean;
28
+ /**
29
+ * When `isOnline` goes from `false` to online and there is already successful data for the same
30
+ * request (only the soft {@link NO_NETWORK_MESSAGE} error), a new fetch runs only if this is
31
+ * `true`. Otherwise stale data stays visible and `errors` is cleared. When there is no
32
+ * successful data yet, or the last error was not the offline placeholder, a fetch always runs.
33
+ * @default false
34
+ */
35
+ refreshOnReconnect?: boolean;
28
36
  children: (args: {
29
37
  loading: boolean;
38
+ /**
39
+ * `true` while a full pagination pass is in flight after at least one successful completion for
40
+ * the current request identity (stale-while-revalidate).
41
+ */
42
+ refreshing: boolean;
30
43
  received: number;
31
44
  total: number | null;
32
45
  /**
@@ -46,5 +59,5 @@ export type RopeGeoPaginationHttpRequestProps<T = unknown> = {
46
59
  * {@link PaginationResults.fromResponseBody}. Final `data` is pages concatenated in page order.
47
60
  * In-flight requests use one {@link AbortController}: unmount or any failure aborts the rest.
48
61
  */
49
- export declare function RopeGeoPaginationHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, batchSize, timeoutAfterSeconds, isOnline, children, }: RopeGeoPaginationHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
62
+ export declare function RopeGeoPaginationHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, batchSize, timeoutAfterSeconds, isOnline, refreshOnReconnect, children, }: RopeGeoPaginationHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
50
63
  //# sourceMappingURL=RopeGeoPaginationHttpRequest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RopeGeoPaginationHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoPaginationHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAUvC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAoB,MAAM,sBAAsB,CAAC;AA8DzE,MAAM,MAAM,iCAAiC,CAAC,CAAC,GAAG,OAAO,IAAI;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB;;;WAGG;QACH,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,+FAA+F;QAC/F,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB,qGAAqG;QACrG,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,GAAG,OAAO,EAAE,EACxD,OAAO,EACP,MAAmB,EACnB,IAAI,EACJ,UAAU,EACV,WAAW,EACX,SAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,iCAAiC,CAAC,CAAC,CAAC,2CAoPtC"}
1
+ {"version":3,"file":"RopeGeoPaginationHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoPaginationHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAYvC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAoB,MAAM,sBAAsB,CAAC;AA8DzE,MAAM,MAAM,iCAAiC,CAAC,CAAC,GAAG,OAAO,IAAI;IAC3D,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,WAAW,EAAE,gBAAgB,CAAC;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,OAAO,EAAE,OAAO,CAAC;QACjB;;;WAGG;QACH,UAAU,EAAE,OAAO,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB;;;WAGG;QACH,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,+FAA+F;QAC/F,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB,qGAAqG;QACrG,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,GAAG,OAAO,EAAE,EACxD,OAAO,EACP,MAAmB,EACnB,IAAI,EACJ,UAAU,EACV,WAAW,EACX,SAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,kBAA0B,EAC1B,QAAQ,GACT,EAAE,iCAAiC,CAAC,CAAC,CAAC,2CAyTtC"}
@@ -57,33 +57,79 @@ function concatPaginationResultItemsSorted(pagesByNum) {
57
57
  * {@link PaginationResults.fromResponseBody}. Final `data` is pages concatenated in page order.
58
58
  * In-flight requests use one {@link AbortController}: unmount or any failure aborts the rest.
59
59
  */
60
- function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.Method.GET, path, pathParams, queryParams, batchSize = 10, timeoutAfterSeconds, isOnline, children, }) {
60
+ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.Method.GET, path, pathParams, queryParams, batchSize = 10, timeoutAfterSeconds, isOnline, refreshOnReconnect = false, children, }) {
61
61
  const [loading, setLoading] = (0, react_1.useState)(true);
62
62
  const [received, setReceived] = (0, react_1.useState)(0);
63
63
  const [total, setTotal] = (0, react_1.useState)(null);
64
64
  const [data, setData] = (0, react_1.useState)(null);
65
65
  const [errors, setErrors] = (0, react_1.useState)(null);
66
66
  const [timeoutCountdown, setTimeoutCountdown] = (0, react_1.useState)(null);
67
+ const [hasCommittedOnce, setHasCommittedOnce] = (0, react_1.useState)(false);
68
+ const errorsRef = (0, react_1.useRef)(errors);
69
+ const hasCommittedRef = (0, react_1.useRef)(hasCommittedOnce);
70
+ errorsRef.current = errors;
71
+ hasCommittedRef.current = hasCommittedOnce;
67
72
  const pathParamsKey = JSON.stringify(pathParams ?? null);
68
73
  const queryParamsKey = queryParams.toQueryString();
69
74
  const effectiveBatch = Math.max(1, Math.floor(batchSize));
75
+ const requestKey = (0, react_1.useMemo)(() => `${service}|${method}|${path}|${pathParamsKey}|${queryParamsKey}|${effectiveBatch}|${timeoutAfterSeconds ?? ""}`, [service, method, path, pathParamsKey, queryParamsKey, effectiveBatch, timeoutAfterSeconds]);
76
+ const prevIsOnlineRef = (0, react_1.useRef)(undefined);
77
+ const lastRequestKeyRef = (0, react_1.useRef)("");
70
78
  (0, react_1.useEffect)(() => {
71
- if (isOnline === false) {
79
+ const online = isOnline !== false;
80
+ const prevOnline = prevIsOnlineRef.current;
81
+ const reconnecting = prevOnline === false && online;
82
+ const keyChanged = lastRequestKeyRef.current !== requestKey;
83
+ if (!online) {
84
+ if (keyChanged) {
85
+ lastRequestKeyRef.current = requestKey;
86
+ setHasCommittedOnce(false);
87
+ setReceived(0);
88
+ setTotal(null);
89
+ setData(null);
90
+ setErrors(null);
91
+ }
92
+ prevIsOnlineRef.current = false;
72
93
  setLoading(false);
73
94
  setErrors(new Error(network_1.NO_NETWORK_MESSAGE));
74
95
  setTimeoutCountdown(null);
75
96
  return;
76
97
  }
98
+ if (keyChanged) {
99
+ lastRequestKeyRef.current = requestKey;
100
+ setHasCommittedOnce(false);
101
+ setReceived(0);
102
+ setTotal(null);
103
+ setData(null);
104
+ setErrors(null);
105
+ }
106
+ if (!keyChanged && reconnecting) {
107
+ const onlyNoNetwork = errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE;
108
+ if (hasCommittedRef.current && onlyNoNetwork && !refreshOnReconnect) {
109
+ setErrors(null);
110
+ setLoading(false);
111
+ prevIsOnlineRef.current = true;
112
+ return;
113
+ }
114
+ }
115
+ prevIsOnlineRef.current = true;
77
116
  let cancelled = false;
78
117
  const abortController = new AbortController();
79
118
  const { signal } = abortController;
80
119
  const timeoutMs = (0, network_1.resolveRequestTimeoutMs)(timeoutAfterSeconds);
81
120
  setLoading(true);
82
- setReceived(0);
83
- setTotal(null);
84
- setData(null);
85
121
  setErrors(null);
86
122
  setTimeoutCountdown(null);
123
+ const keepStaleDuringFetch = reconnecting &&
124
+ hasCommittedRef.current &&
125
+ errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE &&
126
+ refreshOnReconnect;
127
+ if (!keyChanged && !keepStaleDuringFetch) {
128
+ setReceived(0);
129
+ setTotal(null);
130
+ setData(null);
131
+ setHasCommittedOnce(false);
132
+ }
87
133
  const baseUrl = RopeGeoHttpRequest_1.SERVICE_BASE_URL[service];
88
134
  const resolvedPath = resolvePath(path, pathParams);
89
135
  const baseInit = {
@@ -158,7 +204,7 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
158
204
  const text = await res.text();
159
205
  if (!res.ok) {
160
206
  abortController.abort();
161
- throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
207
+ throw new Error((0, network_1.formatHttpStatusMessage)(res.status, text || res.statusText));
162
208
  }
163
209
  if (text.length === 0) {
164
210
  abortController.abort();
@@ -192,7 +238,7 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
192
238
  catch (err) {
193
239
  if (pageNum !== 1 && merged != null && merged.consumeDidTimeout()) {
194
240
  abortController.abort();
195
- throw new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE);
241
+ throw new Error((0, network_1.formatNetworkRequestErrorMessage)(new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE)));
196
242
  }
197
243
  throw err;
198
244
  }
@@ -216,6 +262,7 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
216
262
  return;
217
263
  setData(concatPaginationResultItemsSorted(pagesByNum));
218
264
  setErrors(null);
265
+ setHasCommittedOnce(true);
219
266
  return;
220
267
  }
221
268
  const lastPage = Math.max(1, Math.ceil(totalCount / limit));
@@ -246,6 +293,7 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
246
293
  return;
247
294
  setData(concatPaginationResultItemsSorted(pagesByNum));
248
295
  setErrors(null);
296
+ setHasCommittedOnce(true);
249
297
  }
250
298
  catch (err) {
251
299
  if (cancelled || (0, network_1.isAbortError)(err))
@@ -253,8 +301,9 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
253
301
  console.error("[RopeGeoPaginationHttpRequest] Request failed", {
254
302
  error: err instanceof Error ? err.message : String(err),
255
303
  });
256
- setErrors(err instanceof Error ? err : new Error(String(err)));
304
+ setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(err)));
257
305
  setData(null);
306
+ setHasCommittedOnce(false);
258
307
  }
259
308
  finally {
260
309
  clearActivePolicy();
@@ -272,13 +321,16 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
272
321
  path,
273
322
  pathParamsKey,
274
323
  queryParamsKey,
275
- queryParams,
276
324
  effectiveBatch,
277
325
  timeoutAfterSeconds,
278
326
  isOnline,
327
+ refreshOnReconnect,
328
+ requestKey,
279
329
  ]);
330
+ const refreshing = loading && hasCommittedOnce;
280
331
  return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children({
281
332
  loading,
333
+ refreshing,
282
334
  received,
283
335
  total,
284
336
  data,
@@ -3,5 +3,5 @@
3
3
  * Published as `ropegeo-common/helpers/network`. Prefer this entry over `ropegeo-common/helpers` in
4
4
  * Metro/RN bundles so the full helpers barrel (S3 folder upload, etc.) is not resolved.
5
5
  */
6
- export { NETWORK_REQUEST_TIMED_OUT_MESSAGE, NO_NETWORK_MESSAGE, installNetworkRequestPolicyTimers, isAbortError, isNetworkRequestTimeoutError, mergeParentSignalWithDeadline, resolveRequestTimeoutMs, type MergedDeadlineHandles, type NetworkRequestPolicyTimerCallbacks, } from "./networkRequestPolicy";
6
+ export { formatHttpStatusMessage, formatNetworkRequestErrorMessage, NETWORK_REQUEST_TIMED_OUT_MESSAGE, NO_NETWORK_MESSAGE, installNetworkRequestPolicyTimers, isAbortError, isNetworkRequestTimeoutError, mergeParentSignalWithDeadline, resolveRequestTimeoutMs, type MergedDeadlineHandles, type NetworkRequestPolicyTimerCallbacks, } from "./networkRequestPolicy";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/helpers/network/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iCAAiC,EACjC,kBAAkB,EAClB,iCAAiC,EACjC,YAAY,EACZ,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,kCAAkC,GACxC,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/helpers/network/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,uBAAuB,EACvB,gCAAgC,EAChC,iCAAiC,EACjC,kBAAkB,EAClB,iCAAiC,EACjC,YAAY,EACZ,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,kCAAkC,GACxC,MAAM,wBAAwB,CAAC"}
@@ -5,8 +5,10 @@
5
5
  * Metro/RN bundles so the full helpers barrel (S3 folder upload, etc.) is not resolved.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.resolveRequestTimeoutMs = exports.mergeParentSignalWithDeadline = exports.isNetworkRequestTimeoutError = exports.isAbortError = exports.installNetworkRequestPolicyTimers = exports.NO_NETWORK_MESSAGE = exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE = void 0;
8
+ exports.resolveRequestTimeoutMs = exports.mergeParentSignalWithDeadline = exports.isNetworkRequestTimeoutError = exports.isAbortError = exports.installNetworkRequestPolicyTimers = exports.NO_NETWORK_MESSAGE = exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE = exports.formatNetworkRequestErrorMessage = exports.formatHttpStatusMessage = void 0;
9
9
  var networkRequestPolicy_1 = require("./networkRequestPolicy");
10
+ Object.defineProperty(exports, "formatHttpStatusMessage", { enumerable: true, get: function () { return networkRequestPolicy_1.formatHttpStatusMessage; } });
11
+ Object.defineProperty(exports, "formatNetworkRequestErrorMessage", { enumerable: true, get: function () { return networkRequestPolicy_1.formatNetworkRequestErrorMessage; } });
10
12
  Object.defineProperty(exports, "NETWORK_REQUEST_TIMED_OUT_MESSAGE", { enumerable: true, get: function () { return networkRequestPolicy_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE; } });
11
13
  Object.defineProperty(exports, "NO_NETWORK_MESSAGE", { enumerable: true, get: function () { return networkRequestPolicy_1.NO_NETWORK_MESSAGE; } });
12
14
  Object.defineProperty(exports, "installNetworkRequestPolicyTimers", { enumerable: true, get: function () { return networkRequestPolicy_1.installNetworkRequestPolicyTimers; } });
@@ -3,6 +3,16 @@
3
3
  export declare const NETWORK_REQUEST_TIMED_OUT_MESSAGE = "Network request timed out";
4
4
  /** Use this exact message for client-side offline gating and RN fetch failures treated as offline. */
5
5
  export declare const NO_NETWORK_MESSAGE = "No network connection";
6
+ /**
7
+ * Formats HTTP response failures as user-facing copy (e.g. "500 Internal Server Error").
8
+ * `detail` can be a response body snippet or `statusText`.
9
+ */
10
+ export declare function formatHttpStatusMessage(status: number, detail?: string): string;
11
+ /**
12
+ * Normalizes raw network/request errors into stable user-facing copy.
13
+ * Keeps NO_NETWORK_MESSAGE unchanged for offline gating checks.
14
+ */
15
+ export declare function formatNetworkRequestErrorMessage(error: unknown): string;
6
16
  export declare function isNetworkRequestTimeoutError(e: unknown): boolean;
7
17
  export declare function isAbortError(e: unknown): boolean;
8
18
  /** Milliseconds for `timeoutAfterSeconds` on request components; `null` when timeout is disabled. */
@@ -1 +1 @@
1
- {"version":3,"file":"networkRequestPolicy.d.ts","sourceRoot":"","sources":["../../../src/helpers/network/networkRequestPolicy.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAE7E,8EAA8E;AAC9E,eAAO,MAAM,iCAAiC,8BAA8B,CAAC;AAE7E,sGAAsG;AACtG,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAE1D,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEhE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAUhD;AAED,qGAAqG;AACrG,wBAAgB,uBAAuB,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASnF;AAED,MAAM,MAAM,kCAAkC,GAAG;IAC/C,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,kBAAkB,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,uBAAuB,EAAE,MAAM,IAAI,CAAC;IACpC,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,kBAAkB,EAAE,MAAM,EAC1B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,kCAAkC,GAC5C,MAAM,IAAI,CAuCZ;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,iBAAiB,EAAE,MAAM,OAAO,CAAC;CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,WAAW,EACzB,UAAU,EAAE,MAAM,GACjB,qBAAqB,CAqCvB"}
1
+ {"version":3,"file":"networkRequestPolicy.d.ts","sourceRoot":"","sources":["../../../src/helpers/network/networkRequestPolicy.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAE7E,8EAA8E;AAC9E,eAAO,MAAM,iCAAiC,8BAA8B,CAAC;AAE7E,sGAAsG;AACtG,eAAO,MAAM,kBAAkB,0BAA0B,CAAC;AAoB1D;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAO/E;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAmBvE;AAED,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEhE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAUhD;AAED,qGAAqG;AACrG,wBAAgB,uBAAuB,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASnF;AAED,MAAM,MAAM,kCAAkC,GAAG;IAC/C,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,kBAAkB,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,uBAAuB,EAAE,MAAM,IAAI,CAAC;IACpC,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,kBAAkB,EAAE,MAAM,EAC1B,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,kCAAkC,GAC5C,MAAM,IAAI,CAuCZ;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,iBAAiB,EAAE,MAAM,OAAO,CAAC;CAClC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,YAAY,EAAE,WAAW,EACzB,UAAU,EAAE,MAAM,GACjB,qBAAqB,CAqCvB"}
@@ -2,6 +2,8 @@
2
2
  /** Network helpers for optional request deadlines and timeout countdowns. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.NO_NETWORK_MESSAGE = exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE = void 0;
5
+ exports.formatHttpStatusMessage = formatHttpStatusMessage;
6
+ exports.formatNetworkRequestErrorMessage = formatNetworkRequestErrorMessage;
5
7
  exports.isNetworkRequestTimeoutError = isNetworkRequestTimeoutError;
6
8
  exports.isAbortError = isAbortError;
7
9
  exports.resolveRequestTimeoutMs = resolveRequestTimeoutMs;
@@ -11,6 +13,59 @@ exports.mergeParentSignalWithDeadline = mergeParentSignalWithDeadline;
11
13
  exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE = "Network request timed out";
12
14
  /** Use this exact message for client-side offline gating and RN fetch failures treated as offline. */
13
15
  exports.NO_NETWORK_MESSAGE = "No network connection";
16
+ const HTTP_STATUS_TEXT = {
17
+ 400: "Bad Request",
18
+ 401: "Unauthorized",
19
+ 403: "Forbidden",
20
+ 404: "Not Found",
21
+ 408: "Request Timeout",
22
+ 409: "Conflict",
23
+ 429: "Too Many Requests",
24
+ 500: "Internal Server Error",
25
+ 502: "Bad Gateway",
26
+ 503: "Service Unavailable",
27
+ 504: "Gateway Timeout",
28
+ };
29
+ function firstLineOrEmpty(value) {
30
+ return (value.split("\n")[0] ?? "").trim();
31
+ }
32
+ /**
33
+ * Formats HTTP response failures as user-facing copy (e.g. "500 Internal Server Error").
34
+ * `detail` can be a response body snippet or `statusText`.
35
+ */
36
+ function formatHttpStatusMessage(status, detail) {
37
+ const trimmed = (detail ?? "").trim();
38
+ if (trimmed !== "") {
39
+ const line = firstLineOrEmpty(trimmed);
40
+ return line === "" ? String(status) : `${status} ${line}`;
41
+ }
42
+ return `${status} ${HTTP_STATUS_TEXT[status] ?? "HTTP Error"}`;
43
+ }
44
+ /**
45
+ * Normalizes raw network/request errors into stable user-facing copy.
46
+ * Keeps NO_NETWORK_MESSAGE unchanged for offline gating checks.
47
+ */
48
+ function formatNetworkRequestErrorMessage(error) {
49
+ const raw = error instanceof Error
50
+ ? error.message
51
+ : typeof error === "string"
52
+ ? error
53
+ : String(error ?? "");
54
+ const msg = raw.trim();
55
+ if (msg === "")
56
+ return "Request failed";
57
+ if (msg === exports.NO_NETWORK_MESSAGE)
58
+ return exports.NO_NETWORK_MESSAGE;
59
+ if (msg === exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE)
60
+ return exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE;
61
+ const http = /^HTTP\s+(\d{3})(?::\s*(.*))?$/i.exec(msg);
62
+ if (http != null) {
63
+ const code = Number(http[1]);
64
+ const detail = (http[2] ?? "").trim();
65
+ return formatHttpStatusMessage(code, detail);
66
+ }
67
+ return msg;
68
+ }
14
69
  function isNetworkRequestTimeoutError(e) {
15
70
  return e instanceof Error && e.message === exports.NETWORK_REQUEST_TIMED_OUT_MESSAGE;
16
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ropegeo-common",
3
- "version": "1.12.10",
3
+ "version": "1.12.12",
4
4
  "description": "Shared domain models and helpers for RopeGeo and WebScraper",
5
5
  "license": "ISC",
6
6
  "repository": {