ropegeo-common 1.9.0 → 1.9.2
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
|
@@ -286,7 +286,7 @@ Helper tables use columns **Name**, **Description**, **Import**. Class tables ad
|
|
|
286
286
|
| --- | --- | --- |
|
|
287
287
|
| `RopeGeoHttpRequest` | Single GET/POST wrapper; parses `Result.fromResponseBody`. | `import { RopeGeoHttpRequest, Method, Service } from 'ropegeo-common/components'` |
|
|
288
288
|
| `RopeGeoCursorPaginationHttpRequest` | Cursor-paginated fetch with `loadMore`. | `import { RopeGeoCursorPaginationHttpRequest } from 'ropegeo-common/components'` |
|
|
289
|
-
| `RopeGeoPaginationHttpRequest
|
|
289
|
+
| `RopeGeoPaginationHttpRequest<T>` | Page-based fetch; each page validated with `PaginationResults.fromResponseBody`; concatenates `results` into `data` (`T[]` when complete, otherwise `null` with `errors`). | `import { RopeGeoPaginationHttpRequest } from 'ropegeo-common/components'` |
|
|
290
290
|
|
|
291
291
|
---
|
|
292
292
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { type PaginationParams
|
|
2
|
+
import { type PaginationParams } from "../classes";
|
|
3
3
|
import { Method, Service } from "./RopeGeoHttpRequest";
|
|
4
4
|
export type RopeGeoPaginationHttpRequestProps<T = unknown> = {
|
|
5
5
|
service: Service;
|
|
@@ -8,20 +8,29 @@ export type RopeGeoPaginationHttpRequestProps<T = unknown> = {
|
|
|
8
8
|
pathParams?: Record<string, string>;
|
|
9
9
|
queryParams: PaginationParams;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Max concurrent page requests after page 1 completes (page 1 is always alone so `total` is known).
|
|
12
|
+
* Clamped to at least 1.
|
|
13
|
+
* @default 10
|
|
12
14
|
*/
|
|
13
|
-
|
|
15
|
+
batchSize?: number;
|
|
14
16
|
children: (args: {
|
|
15
17
|
loading: boolean;
|
|
16
18
|
received: number;
|
|
17
19
|
total: number | null;
|
|
18
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Concatenated `results` from every page after each body was parsed with
|
|
22
|
+
* {@link PaginationResults.fromResponseBody}. `null` if any page fails HTTP, JSON parse, or validation.
|
|
23
|
+
*/
|
|
24
|
+
data: T[] | null;
|
|
25
|
+
/** Set when `data` is `null` after a terminal failure; cleared only when all pages succeed. */
|
|
19
26
|
errors: Error | null;
|
|
20
27
|
}) => ReactNode;
|
|
21
28
|
};
|
|
22
29
|
/**
|
|
23
|
-
* Fetches page 1,
|
|
24
|
-
*
|
|
30
|
+
* Fetches page 1, then remaining pages in parallel batches of {@link batchSize}.
|
|
31
|
+
* The initial `page` on `queryParams` is ignored. Each body is parsed with
|
|
32
|
+
* {@link PaginationResults.fromResponseBody}. Final `data` is pages concatenated in page order.
|
|
33
|
+
* In-flight requests use one {@link AbortController}: unmount or any failure aborts the rest.
|
|
25
34
|
*/
|
|
26
|
-
export declare function RopeGeoPaginationHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams,
|
|
35
|
+
export declare function RopeGeoPaginationHttpRequest<T = unknown>({ service, method, path, pathParams, queryParams, batchSize, children, }: RopeGeoPaginationHttpRequestProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
27
36
|
//# 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;AAEvC,OAAO,EACL,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"RopeGeoPaginationHttpRequest.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoPaginationHttpRequest.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAoB,MAAM,sBAAsB,CAAC;AAoEzE,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,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;KACtB,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,QAAQ,GACT,EAAE,iCAAiC,CAAC,CAAC,CAAC,2CA4KtC"}
|
|
@@ -29,11 +29,41 @@ function getResponseBody(raw) {
|
|
|
29
29
|
}
|
|
30
30
|
return raw;
|
|
31
31
|
}
|
|
32
|
+
function isAbortError(e) {
|
|
33
|
+
if (e instanceof DOMException && e.name === "AbortError")
|
|
34
|
+
return true;
|
|
35
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
36
|
+
return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
function sumReceived(pagesByNum) {
|
|
40
|
+
let sum = 0;
|
|
41
|
+
for (const p of pagesByNum.values()) {
|
|
42
|
+
sum += p.results.length;
|
|
43
|
+
}
|
|
44
|
+
return sum;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Concatenates each page's `results` in ascending page order. Call only after every page was built via
|
|
48
|
+
* {@link PaginationResults.fromResponseBody}.
|
|
49
|
+
*/
|
|
50
|
+
function concatPaginationResultItemsSorted(pagesByNum) {
|
|
51
|
+
const keys = [...pagesByNum.keys()].sort((a, b) => a - b);
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const k of keys) {
|
|
54
|
+
const p = pagesByNum.get(k);
|
|
55
|
+
if (p != null)
|
|
56
|
+
out.push(...p.results);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
32
60
|
/**
|
|
33
|
-
* Fetches page 1,
|
|
34
|
-
*
|
|
61
|
+
* Fetches page 1, then remaining pages in parallel batches of {@link batchSize}.
|
|
62
|
+
* The initial `page` on `queryParams` is ignored. Each body is parsed with
|
|
63
|
+
* {@link PaginationResults.fromResponseBody}. Final `data` is pages concatenated in page order.
|
|
64
|
+
* In-flight requests use one {@link AbortController}: unmount or any failure aborts the rest.
|
|
35
65
|
*/
|
|
36
|
-
function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.Method.GET, path, pathParams, queryParams,
|
|
66
|
+
function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.Method.GET, path, pathParams, queryParams, batchSize = 10, children, }) {
|
|
37
67
|
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
38
68
|
const [received, setReceived] = (0, react_1.useState)(0);
|
|
39
69
|
const [total, setTotal] = (0, react_1.useState)(null);
|
|
@@ -41,8 +71,11 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
|
|
|
41
71
|
const [errors, setErrors] = (0, react_1.useState)(null);
|
|
42
72
|
const pathParamsKey = JSON.stringify(pathParams ?? null);
|
|
43
73
|
const queryParamsKey = queryParams.toQueryString();
|
|
74
|
+
const effectiveBatch = Math.max(1, Math.floor(batchSize));
|
|
44
75
|
(0, react_1.useEffect)(() => {
|
|
45
76
|
let cancelled = false;
|
|
77
|
+
const abortController = new AbortController();
|
|
78
|
+
const { signal } = abortController;
|
|
46
79
|
setLoading(true);
|
|
47
80
|
setReceived(0);
|
|
48
81
|
setTotal(null);
|
|
@@ -50,87 +83,107 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
|
|
|
50
83
|
setErrors(null);
|
|
51
84
|
const baseUrl = RopeGeoHttpRequest_1.SERVICE_BASE_URL[service];
|
|
52
85
|
const resolvedPath = resolvePath(path, pathParams);
|
|
53
|
-
const
|
|
86
|
+
const baseInit = {
|
|
54
87
|
method,
|
|
55
88
|
headers: { "Content-Type": "application/json" },
|
|
89
|
+
signal,
|
|
56
90
|
};
|
|
57
91
|
(async () => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
const pagesByNum = new Map();
|
|
93
|
+
const limit = queryParams.limit;
|
|
94
|
+
const fetchPage = async (pageNum) => {
|
|
95
|
+
const params = queryParams.withPage(pageNum);
|
|
96
|
+
const queryString = params.toQueryString();
|
|
97
|
+
const fullPath = queryString
|
|
98
|
+
? `${resolvedPath}?${queryString}`
|
|
99
|
+
: resolvedPath;
|
|
100
|
+
const url = new URL(fullPath, baseUrl).toString();
|
|
101
|
+
const res = await fetch(url, baseInit);
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
abortController.abort();
|
|
105
|
+
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
|
|
106
|
+
}
|
|
107
|
+
if (text.length === 0) {
|
|
108
|
+
abortController.abort();
|
|
109
|
+
throw new Error("Empty response body");
|
|
110
|
+
}
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = JSON.parse(text);
|
|
114
|
+
}
|
|
115
|
+
catch (parseError) {
|
|
116
|
+
abortController.abort();
|
|
117
|
+
console.error("[RopeGeoPaginationHttpRequest] Invalid JSON response", {
|
|
118
|
+
url,
|
|
119
|
+
status: res.status,
|
|
120
|
+
responseText: text.slice(0, 500),
|
|
121
|
+
parseError: parseError instanceof Error
|
|
122
|
+
? parseError.message
|
|
123
|
+
: String(parseError),
|
|
124
|
+
});
|
|
125
|
+
throw new Error("Invalid JSON response");
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return classes_1.PaginationResults.fromResponseBody(getResponseBody(raw));
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
abortController.abort();
|
|
132
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
133
|
+
throw new Error(msg);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
62
136
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
137
|
+
const first = await fetchPage(1);
|
|
138
|
+
if (cancelled)
|
|
139
|
+
return;
|
|
140
|
+
pagesByNum.set(1, first);
|
|
141
|
+
const totalCount = first.total;
|
|
142
|
+
let receivedCount = first.results.length;
|
|
143
|
+
setReceived(receivedCount);
|
|
144
|
+
setTotal(totalCount);
|
|
145
|
+
const doneByTotal = totalCount !== null && receivedCount >= totalCount;
|
|
146
|
+
const doneByShortPage = first.results.length < limit;
|
|
147
|
+
if (doneByTotal || doneByShortPage) {
|
|
72
148
|
if (cancelled)
|
|
73
149
|
return;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
raw = JSON.parse(text);
|
|
87
|
-
}
|
|
88
|
-
catch (parseError) {
|
|
89
|
-
console.error("[RopeGeoPaginationHttpRequest] Invalid JSON response", {
|
|
90
|
-
url,
|
|
91
|
-
status: res.status,
|
|
92
|
-
responseText: text.slice(0, 500),
|
|
93
|
-
parseError: parseError instanceof Error
|
|
94
|
-
? parseError.message
|
|
95
|
-
: String(parseError),
|
|
96
|
-
});
|
|
97
|
-
setErrors(new Error("Invalid JSON response"));
|
|
98
|
-
setData(null);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
let parsed;
|
|
102
|
-
try {
|
|
103
|
-
parsed = classes_1.PaginationResults.fromResponseBody(getResponseBody(raw));
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
107
|
-
setErrors(new Error(msg));
|
|
108
|
-
setData(null);
|
|
150
|
+
setData(concatPaginationResultItemsSorted(pagesByNum));
|
|
151
|
+
setErrors(null);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const lastPage = Math.max(1, Math.ceil(totalCount / limit));
|
|
155
|
+
const toFetch = [];
|
|
156
|
+
for (let p = 2; p <= lastPage; p++) {
|
|
157
|
+
toFetch.push(p);
|
|
158
|
+
}
|
|
159
|
+
for (let i = 0; i < toFetch.length; i += effectiveBatch) {
|
|
160
|
+
if (cancelled)
|
|
109
161
|
return;
|
|
110
|
-
|
|
162
|
+
if (sumReceived(pagesByNum) >= totalCount)
|
|
163
|
+
break;
|
|
164
|
+
const chunk = toFetch.slice(i, i + effectiveBatch);
|
|
165
|
+
const batchResults = await Promise.all(chunk.map(async (pageNum) => {
|
|
166
|
+
const parsed = await fetchPage(pageNum);
|
|
167
|
+
return { pageNum, parsed };
|
|
168
|
+
}));
|
|
111
169
|
if (cancelled)
|
|
112
170
|
return;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (totalCount === null) {
|
|
116
|
-
totalCount = parsed.total;
|
|
171
|
+
for (const { pageNum, parsed } of batchResults) {
|
|
172
|
+
pagesByNum.set(pageNum, parsed);
|
|
117
173
|
}
|
|
174
|
+
receivedCount = sumReceived(pagesByNum);
|
|
118
175
|
setReceived(receivedCount);
|
|
119
176
|
setTotal(totalCount);
|
|
120
|
-
|
|
121
|
-
const doneByShortPage = parsed.results.length < queryParams.limit;
|
|
122
|
-
if (doneByTotal || doneByShortPage) {
|
|
177
|
+
if (receivedCount >= totalCount)
|
|
123
178
|
break;
|
|
124
|
-
}
|
|
125
|
-
pageNum += 1;
|
|
126
179
|
}
|
|
127
180
|
if (cancelled)
|
|
128
181
|
return;
|
|
129
|
-
setData(
|
|
182
|
+
setData(concatPaginationResultItemsSorted(pagesByNum));
|
|
130
183
|
setErrors(null);
|
|
131
184
|
}
|
|
132
185
|
catch (err) {
|
|
133
|
-
if (cancelled)
|
|
186
|
+
if (cancelled || isAbortError(err))
|
|
134
187
|
return;
|
|
135
188
|
console.error("[RopeGeoPaginationHttpRequest] Request failed", {
|
|
136
189
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -145,8 +198,17 @@ function RopeGeoPaginationHttpRequest({ service, method = RopeGeoHttpRequest_1.M
|
|
|
145
198
|
})();
|
|
146
199
|
return () => {
|
|
147
200
|
cancelled = true;
|
|
201
|
+
abortController.abort();
|
|
148
202
|
};
|
|
149
|
-
}, [
|
|
203
|
+
}, [
|
|
204
|
+
service,
|
|
205
|
+
method,
|
|
206
|
+
path,
|
|
207
|
+
pathParamsKey,
|
|
208
|
+
queryParamsKey,
|
|
209
|
+
queryParams,
|
|
210
|
+
effectiveBatch,
|
|
211
|
+
]);
|
|
150
212
|
return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children({
|
|
151
213
|
loading,
|
|
152
214
|
received,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"httpRequest.d.ts","sourceRoot":"","sources":["../../src/helpers/httpRequest.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiDH;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,UAAU,SAAI,EACd,WAAW,CAAC,EAAE,WAAW,EACzB,IAAI,CAAC,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,OAAO,GACnB,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"httpRequest.d.ts","sourceRoot":"","sources":["../../src/helpers/httpRequest.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAiDH;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,UAAU,SAAI,EACd,WAAW,CAAC,EAAE,WAAW,EACzB,IAAI,CAAC,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,OAAO,GACnB,OAAO,CAAC,QAAQ,CAAC,CAoEnB;AAED,eAAe,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ropegeo-common",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "Shared domain classes and helpers for RopeGeo and WebScraper",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"repository": {
|
|
@@ -43,11 +43,15 @@
|
|
|
43
43
|
"@aws-sdk/client-cloudfront": "^3.971.0",
|
|
44
44
|
"@aws-sdk/client-s3": "^3.971.0",
|
|
45
45
|
"@aws-sdk/client-sqs": "^3.971.0",
|
|
46
|
+
"@testing-library/react": "^16.3.2",
|
|
46
47
|
"@types/jest": "^29.5.14",
|
|
47
48
|
"@types/node": "^22.10.0",
|
|
48
49
|
"@types/react": "^19.0.0",
|
|
50
|
+
"@types/react-dom": "^19.2.3",
|
|
49
51
|
"jest": "^30.2.0",
|
|
52
|
+
"jest-environment-jsdom": "^30.3.0",
|
|
50
53
|
"react": "^19.1.0",
|
|
54
|
+
"react-dom": "^19.2.4",
|
|
51
55
|
"ts-jest": "^29.2.5",
|
|
52
56
|
"typescript": "^5.9.3",
|
|
53
57
|
"undici": "^7.11.0"
|
|
@@ -55,4 +59,4 @@
|
|
|
55
59
|
"files": [
|
|
56
60
|
"dist"
|
|
57
61
|
]
|
|
58
|
-
}
|
|
62
|
+
}
|