ropegeo-common 1.13.0 → 1.16.0
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 +7 -7
- package/dist/components/RopeGeoDataLoader.d.ts +56 -0
- package/dist/components/RopeGeoDataLoader.d.ts.map +1 -0
- package/dist/components/{RopeGeoHttpRequest.js → RopeGeoDataLoader.js} +173 -45
- package/dist/components/RopeGeoPagedDataLoader.d.ts +23 -0
- package/dist/components/RopeGeoPagedDataLoader.d.ts.map +1 -0
- package/dist/components/{RopeGeoCursorPaginationHttpRequest.js → RopeGeoPagedDataLoader.js} +81 -63
- package/dist/components/RopeGeoProgressDataLoader.d.ts +23 -0
- package/dist/components/RopeGeoProgressDataLoader.d.ts.map +1 -0
- package/dist/components/{RopeGeoPaginationHttpRequest.js → RopeGeoProgressDataLoader.js} +57 -39
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +10 -10
- package/dist/models/api/params/cursorPaginationParams.d.ts +5 -0
- package/dist/models/api/params/cursorPaginationParams.d.ts.map +1 -1
- package/dist/models/api/params/cursorPaginationParams.js +10 -0
- package/dist/models/api/params/paginationParams.d.ts +5 -0
- package/dist/models/api/params/paginationParams.d.ts.map +1 -1
- package/dist/models/api/params/paginationParams.js +10 -0
- package/dist/models/minimap/abstract/pageMiniMap.d.ts +5 -2
- package/dist/models/minimap/abstract/pageMiniMap.d.ts.map +1 -1
- package/dist/models/minimap/abstract/pageMiniMap.js +10 -5
- package/dist/models/minimap/concrete/offlinePageMiniMap.d.ts +1 -1
- package/dist/models/minimap/concrete/offlinePageMiniMap.d.ts.map +1 -1
- package/dist/models/minimap/concrete/offlinePageMiniMap.js +4 -3
- package/dist/models/minimap/concrete/onlinePageMiniMap.d.ts +1 -1
- package/dist/models/minimap/concrete/onlinePageMiniMap.d.ts.map +1 -1
- package/dist/models/minimap/concrete/onlinePageMiniMap.js +3 -3
- package/dist/models/pageViews/offlineRopewikiPageView.d.ts +1 -1
- package/dist/models/pageViews/offlineRopewikiPageView.d.ts.map +1 -1
- package/dist/models/pageViews/offlineRopewikiPageView.js +3 -3
- package/dist/models/pageViews/onlineRopewikiPageView.d.ts +1 -1
- package/dist/models/pageViews/onlineRopewikiPageView.d.ts.map +1 -1
- package/dist/models/pageViews/onlineRopewikiPageView.js +4 -4
- package/dist/models/pageViews/ropewikiPageView.d.ts +4 -1
- package/dist/models/pageViews/ropewikiPageView.d.ts.map +1 -1
- package/dist/models/pageViews/ropewikiPageView.js +13 -1
- package/package.json +1 -1
- package/dist/components/RopeGeoCursorPaginationHttpRequest.d.ts +0 -56
- package/dist/components/RopeGeoCursorPaginationHttpRequest.d.ts.map +0 -1
- package/dist/components/RopeGeoHttpRequest.d.ts +0 -68
- package/dist/components/RopeGeoHttpRequest.d.ts.map +0 -1
- package/dist/components/RopeGeoPaginationHttpRequest.d.ts +0 -69
- package/dist/components/RopeGeoPaginationHttpRequest.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ Helper tables use columns **Name**, **Description**, **Import**. Model tables ad
|
|
|
178
178
|
| Name | Base class | Description | Import |
|
|
179
179
|
| --- | --- | --- | --- |
|
|
180
180
|
| `RopewikiPageView` | `BaseRopewikiPageView` | Backward-compatible alias exported as the online page view class. | `import { RopewikiPageView } from 'ropegeo-common/models'` |
|
|
181
|
-
| `BaseRopewikiPageView` | N/A | Abstract Ropewiki page view base
|
|
181
|
+
| `BaseRopewikiPageView` | N/A | Abstract Ropewiki page view base; includes optional `mapDataId` (vector tile record id) alongside shared validation/fields. | `import { BaseRopewikiPageView } from 'ropegeo-common/models'` |
|
|
182
182
|
| `OnlineRopewikiPageView` | `BaseRopewikiPageView` | Online Ropewiki page view with API image URLs and online minimap variants. | `import { OnlineRopewikiPageView } from 'ropegeo-common/models'` |
|
|
183
183
|
| `OfflineRopewikiPageView` | `BaseRopewikiPageView` | Offline Ropewiki page view with downloaded paths and offline minimap variants. | `import { OfflineRopewikiPageView } from 'ropegeo-common/models'` |
|
|
184
184
|
| `OnlinePageView` | N/A | Interface for online page-view behavior (`toOffline`, `toSavedPage`, etc.). | `import type { OnlinePageView } from 'ropegeo-common/models'` |
|
|
@@ -254,9 +254,9 @@ Abstract classes live under `minimap/abstract/`, concrete under `minimap/concret
|
|
|
254
254
|
| `RegionMiniMap` | `MiniMap` | Abstract region minimap (`miniMapType: region`); online/offline concrete subclasses. | `import { RegionMiniMap } from 'ropegeo-common/models'` |
|
|
255
255
|
| `OnlineRegionMiniMap` | `RegionMiniMap` | API region routes (`routesParams`, `bounds` or null, `fetchType: "online"`). | `import { OnlineRegionMiniMap } from 'ropegeo-common/models'` |
|
|
256
256
|
| `OfflineRegionMiniMap` | `RegionMiniMap` | Bundled region routes GeoJSON path (`downloadedGeojson`, `fetchType: "offline"`). | `import { OfflineRegionMiniMap } from 'ropegeo-common/models'` |
|
|
257
|
-
| `PageMiniMap` | `MiniMap` | Abstract page minimap (`miniMapType: page`);
|
|
258
|
-
| `OnlinePageMiniMap` | `PageMiniMap` | API page tiles
|
|
259
|
-
| `OfflinePageMiniMap` | `PageMiniMap` |
|
|
257
|
+
| `PageMiniMap` | `MiniMap` | Abstract page minimap (`miniMapType: page`); MVT `polyLineLayerId` / `pointLayerId`, tile templates, optional `legend`. | `import { PageMiniMap } from 'ropegeo-common/models'` |
|
|
258
|
+
| `OnlinePageMiniMap` | `PageMiniMap` | API page tiles (`onlineTilesTemplate`, `fetchType: "online"`). | `import { OnlinePageMiniMap } from 'ropegeo-common/models'` |
|
|
259
|
+
| `OfflinePageMiniMap` | `PageMiniMap` | Local page tiles (`offlineTilesTemplate`, `fetchType: "offline"`). | `import { OfflinePageMiniMap } from 'ropegeo-common/models'` |
|
|
260
260
|
| `CenteredRegionMiniMap` | `MiniMap` | Abstract centered-route minimap (`miniMapType: centeredRegion`). | `import { CenteredRegionMiniMap } from 'ropegeo-common/models'` |
|
|
261
261
|
| `OnlineCenteredRegionMiniMap` | `CenteredRegionMiniMap` | API centered-route fallback (`routesParams`, `fetchType: "online"`). | `import { OnlineCenteredRegionMiniMap } from 'ropegeo-common/models'` |
|
|
262
262
|
| `OfflineCenteredRegionMiniMap` | `CenteredRegionMiniMap` | Persisted local centered-route geojson (`downloadedGeojson`, `fetchType: "offline"`). | `import { OfflineCenteredRegionMiniMap } from 'ropegeo-common/models'` |
|
|
@@ -326,9 +326,9 @@ Legend entries for page minimaps are keyed objects (`legend` on `PageMiniMap`);
|
|
|
326
326
|
|
|
327
327
|
| Name | Description | Import |
|
|
328
328
|
| --- | --- | --- |
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
329
|
+
| `RopeGeoDataLoader` | Single-request loader: `onlinePath` / `onlinePathParams`, optional `offlineData` (object or filesystem path + `readOfflineFile`), optional `timeoutAfterSeconds` and `isOnline`. Children receive `data`, `errors`, `timeoutCountdown` (numeric only while an online fetch is in flight, otherwise `null`), and `reload`. Reconnect refetch uses `dirtyWhileOffline` driven by a semantic key (no `refreshOnReconnect` prop). Also exports `Method`, `Service`, `SERVICE_BASE_URL`. | `import { RopeGeoDataLoader, Method, Service } from 'ropegeo-common/components'` |
|
|
330
|
+
| `RopeGeoPagedDataLoader` | Cursor-paginated list: same networking props as above; children get `loadNextPage`, `loadingNextPage`, `morePages`, plus `data`, `errors`, `timeoutCountdown`, `reload`. | `import { RopeGeoPagedDataLoader } from 'ropegeo-common/components'` |
|
|
331
|
+
| `RopeGeoProgressDataLoader` | Page-based “progress” fetch: batches additional pages until `total` is satisfied; children get `received`, `total`, `data`, `errors`, `timeoutCountdown`, `reload`. | `import { RopeGeoProgressDataLoader } from 'ropegeo-common/components'` |
|
|
332
332
|
|
|
333
333
|
---
|
|
334
334
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export declare const Service: {
|
|
3
|
+
readonly WEBSCRAPER: "WEBSCRAPER";
|
|
4
|
+
};
|
|
5
|
+
export type Service = (typeof Service)[keyof typeof Service];
|
|
6
|
+
export declare const Method: {
|
|
7
|
+
readonly GET: "GET";
|
|
8
|
+
readonly POST: "POST";
|
|
9
|
+
readonly PUT: "PUT";
|
|
10
|
+
readonly DELETE: "DELETE";
|
|
11
|
+
};
|
|
12
|
+
export type Method = (typeof Method)[keyof typeof Method];
|
|
13
|
+
export declare const SERVICE_BASE_URL: Record<Service, string>;
|
|
14
|
+
export type RopeGeoDataLoaderProps<T = unknown> = {
|
|
15
|
+
service: Service;
|
|
16
|
+
method: Method;
|
|
17
|
+
/** HTTP path template (e.g. `/ropewiki/page/:id`). Used when loading from the network. */
|
|
18
|
+
onlinePath: string;
|
|
19
|
+
onlinePathParams?: Record<string, string>;
|
|
20
|
+
queryParams?: Record<string, string | number | boolean | undefined>;
|
|
21
|
+
body?: object;
|
|
22
|
+
/**
|
|
23
|
+
* Request deadline in seconds (abort + timeout error). When omitted, {@link timeoutCountdown}
|
|
24
|
+
* stays `null` and no deadline is enforced.
|
|
25
|
+
*/
|
|
26
|
+
timeoutAfterSeconds?: number;
|
|
27
|
+
/**
|
|
28
|
+
* When `false`, the online request is not started and children receive {@link NO_NETWORK_MESSAGE}
|
|
29
|
+
* as the error. Previously loaded `data` is kept until the network returns. When `true` or
|
|
30
|
+
* omitted, behavior is unchanged.
|
|
31
|
+
*/
|
|
32
|
+
isOnline?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Inline offline payload or filesystem path. When an object, it is used as `data` immediately
|
|
35
|
+
* (no network, no file read). When a string, `readOfflineFile` is invoked; on failure the error
|
|
36
|
+
* `Could not read file at {path}` is set and the loader falls back to {@link onlinePath} when online.
|
|
37
|
+
*/
|
|
38
|
+
offlineData?: T | string;
|
|
39
|
+
/**
|
|
40
|
+
* Required when `offlineData` is a string. Reads file contents as UTF-8 text for JSON parsing.
|
|
41
|
+
*/
|
|
42
|
+
readOfflineFile?: (path: string) => Promise<string>;
|
|
43
|
+
children: (args: {
|
|
44
|
+
data: T | null;
|
|
45
|
+
errors: Error | null;
|
|
46
|
+
/**
|
|
47
|
+
* Whole seconds remaining until abort while an **online** fetch is in flight, only when
|
|
48
|
+
* `timeoutAfterSeconds` is set. Otherwise always `null`.
|
|
49
|
+
*/
|
|
50
|
+
timeoutCountdown: number | null;
|
|
51
|
+
/** Re-runs the online request while online. No-op when `isOnline` is `false`. */
|
|
52
|
+
reload: () => void;
|
|
53
|
+
}) => ReactNode;
|
|
54
|
+
};
|
|
55
|
+
export declare function RopeGeoDataLoader<T = unknown>({ service, method, onlinePath, onlinePathParams, queryParams, body, timeoutAfterSeconds, isOnline, offlineData, readOfflineFile, children, }: RopeGeoDataLoaderProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
56
|
+
//# sourceMappingURL=RopeGeoDataLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RopeGeoDataLoader.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoDataLoader.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;AAqEF,MAAM,MAAM,sBAAsB,CAAC,CAAC,GAAG,OAAO,IAAI;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC;IACzB;;OAEG;IACH,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB;;;WAGG;QACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,iFAAiF;QACjF,MAAM,EAAE,MAAM,IAAI,CAAC;KACpB,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,EAC7C,OAAO,EACP,MAAM,EACN,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,IAAI,EACJ,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,eAAe,EACf,QAAQ,GACT,EAAE,sBAAsB,CAAC,CAAC,CAAC,2CA+X3B"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SERVICE_BASE_URL = exports.Method = exports.Service = void 0;
|
|
4
|
-
exports.
|
|
4
|
+
exports.RopeGeoDataLoader = RopeGeoDataLoader;
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
6
|
const react_1 = require("react");
|
|
7
7
|
const network_1 = require("../helpers/network");
|
|
@@ -40,38 +40,160 @@ function buildUrl(baseUrl, path, pathParams, queryParams) {
|
|
|
40
40
|
}
|
|
41
41
|
return url.toString();
|
|
42
42
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
43
|
+
function hasEffectiveQueryParams(queryParams) {
|
|
44
|
+
if (queryParams == null)
|
|
45
|
+
return false;
|
|
46
|
+
return Object.entries(queryParams).some(([, v]) => v !== undefined && v !== "");
|
|
47
|
+
}
|
|
48
|
+
function dataLoaderReconnectSemanticKey(service, method, onlinePath, pathParamsKey, bodyKey, queryParams, queryParamsKey) {
|
|
49
|
+
const bodyStr = bodyKey === undefined || bodyKey === null
|
|
50
|
+
? ""
|
|
51
|
+
: typeof bodyKey === "object"
|
|
52
|
+
? JSON.stringify(bodyKey)
|
|
53
|
+
: String(bodyKey);
|
|
54
|
+
const q = hasEffectiveQueryParams(queryParams) ? queryParamsKey : "";
|
|
55
|
+
return `${service}|${method}|${onlinePath}|${pathParamsKey}|${bodyStr}|${q}`;
|
|
56
|
+
}
|
|
57
|
+
function offlineDataKeyPart(offlineData) {
|
|
58
|
+
if (offlineData === undefined)
|
|
59
|
+
return "";
|
|
60
|
+
if (typeof offlineData === "string")
|
|
61
|
+
return `|file:${offlineData}`;
|
|
62
|
+
return `|obj:${JSON.stringify(offlineData)}`;
|
|
63
|
+
}
|
|
64
|
+
function RopeGeoDataLoader({ service, method, onlinePath, onlinePathParams, queryParams, body, timeoutAfterSeconds, isOnline, offlineData, readOfflineFile, children, }) {
|
|
45
65
|
const [data, setData] = (0, react_1.useState)(null);
|
|
46
66
|
const [errors, setErrors] = (0, react_1.useState)(null);
|
|
47
67
|
const [timeoutCountdown, setTimeoutCountdown] = (0, react_1.useState)(null);
|
|
48
|
-
const [hasCommittedOnce, setHasCommittedOnce] = (0, react_1.useState)(false);
|
|
49
68
|
const [reloadTick, setReloadTick] = (0, react_1.useState)(0);
|
|
50
69
|
const pendingReloadRef = (0, react_1.useRef)(false);
|
|
70
|
+
/** `null` = not using file path, `'pending'`, `'ok'`, `'failed'` */
|
|
71
|
+
const [offlineFileStatus, setOfflineFileStatus] = (0, react_1.useState)(null);
|
|
51
72
|
const errorsRef = (0, react_1.useRef)(errors);
|
|
52
|
-
const hasCommittedRef = (0, react_1.useRef)(
|
|
73
|
+
const hasCommittedRef = (0, react_1.useRef)(false);
|
|
53
74
|
errorsRef.current = errors;
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
pendingReloadRef.current = true;
|
|
59
|
-
setReloadTick((n) => n + 1);
|
|
60
|
-
}, [isOnline]);
|
|
61
|
-
const pathParamsKey = JSON.stringify(pathParams ?? null);
|
|
75
|
+
const dirtyWhileOfflineRef = (0, react_1.useRef)(false);
|
|
76
|
+
const semanticSnapshotRef = (0, react_1.useRef)(null);
|
|
77
|
+
const prevIsOnlineRef = (0, react_1.useRef)(undefined);
|
|
78
|
+
const pathParamsKey = JSON.stringify(onlinePathParams ?? null);
|
|
62
79
|
const queryParamsKey = JSON.stringify(queryParams ?? null);
|
|
63
80
|
const bodyKey = body === undefined || body === null
|
|
64
81
|
? body
|
|
65
82
|
: typeof body === "object"
|
|
66
83
|
? JSON.stringify(body)
|
|
67
84
|
: body;
|
|
68
|
-
const requestKey = (0, react_1.useMemo)(() => `${service}|${method}|${
|
|
69
|
-
|
|
85
|
+
const requestKey = (0, react_1.useMemo)(() => `${service}|${method}|${onlinePath}|${pathParamsKey}|${queryParamsKey}|${String(bodyKey)}|${timeoutAfterSeconds ?? ""}${offlineDataKeyPart(offlineData)}`, [
|
|
86
|
+
service,
|
|
87
|
+
method,
|
|
88
|
+
onlinePath,
|
|
89
|
+
pathParamsKey,
|
|
90
|
+
queryParamsKey,
|
|
91
|
+
bodyKey,
|
|
92
|
+
timeoutAfterSeconds,
|
|
93
|
+
offlineData,
|
|
94
|
+
]);
|
|
95
|
+
const reconnectSemanticKey = (0, react_1.useMemo)(() => dataLoaderReconnectSemanticKey(service, method, onlinePath, pathParamsKey, bodyKey, queryParams, queryParamsKey), [
|
|
96
|
+
service,
|
|
97
|
+
method,
|
|
98
|
+
onlinePath,
|
|
99
|
+
pathParamsKey,
|
|
100
|
+
bodyKey,
|
|
101
|
+
queryParams,
|
|
102
|
+
queryParamsKey,
|
|
103
|
+
]);
|
|
104
|
+
const reload = (0, react_1.useCallback)(() => {
|
|
105
|
+
if (isOnline === false)
|
|
106
|
+
return;
|
|
107
|
+
pendingReloadRef.current = true;
|
|
108
|
+
setReloadTick((n) => n + 1);
|
|
109
|
+
}, [isOnline]);
|
|
70
110
|
const lastRequestKeyRef = (0, react_1.useRef)("");
|
|
111
|
+
/** Inline object offlineData — immediate, no network. */
|
|
112
|
+
(0, react_1.useEffect)(() => {
|
|
113
|
+
if (offlineData == null || typeof offlineData !== "object") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setOfflineFileStatus(null);
|
|
117
|
+
setData(offlineData);
|
|
118
|
+
setErrors(null);
|
|
119
|
+
hasCommittedRef.current = true;
|
|
120
|
+
dirtyWhileOfflineRef.current = false;
|
|
121
|
+
semanticSnapshotRef.current = null;
|
|
122
|
+
}, [offlineData]);
|
|
123
|
+
/** String path: read file, then maybe fall back to online. */
|
|
124
|
+
(0, react_1.useEffect)(() => {
|
|
125
|
+
if (typeof offlineData !== "string") {
|
|
126
|
+
setOfflineFileStatus(null);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let cancelled = false;
|
|
130
|
+
setOfflineFileStatus("pending");
|
|
131
|
+
setData(null);
|
|
132
|
+
setErrors(null);
|
|
133
|
+
hasCommittedRef.current = false;
|
|
134
|
+
if (readOfflineFile == null) {
|
|
135
|
+
setErrors(new Error("readOfflineFile is required when offlineData is a filesystem path"));
|
|
136
|
+
setOfflineFileStatus("failed");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
void (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const text = await readOfflineFile(offlineData);
|
|
142
|
+
const raw = JSON.parse(text);
|
|
143
|
+
const parsed = models_1.Result.fromResponseBody(raw);
|
|
144
|
+
if (!cancelled) {
|
|
145
|
+
setData(parsed.result);
|
|
146
|
+
setErrors(null);
|
|
147
|
+
hasCommittedRef.current = true;
|
|
148
|
+
setOfflineFileStatus("ok");
|
|
149
|
+
dirtyWhileOfflineRef.current = false;
|
|
150
|
+
semanticSnapshotRef.current = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
if (!cancelled) {
|
|
155
|
+
setErrors(new Error(`Could not read file at ${offlineData}`));
|
|
156
|
+
setData(null);
|
|
157
|
+
hasCommittedRef.current = false;
|
|
158
|
+
setOfflineFileStatus("failed");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
return () => {
|
|
163
|
+
cancelled = true;
|
|
164
|
+
};
|
|
165
|
+
}, [offlineData, readOfflineFile]);
|
|
71
166
|
(0, react_1.useEffect)(() => {
|
|
72
167
|
const online = isOnline !== false;
|
|
73
168
|
const prevOnline = prevIsOnlineRef.current;
|
|
169
|
+
const enteringOffline = prevOnline === true && !online;
|
|
74
170
|
const reconnecting = prevOnline === false && online;
|
|
171
|
+
if (enteringOffline) {
|
|
172
|
+
dirtyWhileOfflineRef.current = false;
|
|
173
|
+
semanticSnapshotRef.current = reconnectSemanticKey;
|
|
174
|
+
}
|
|
175
|
+
if (!online && prevOnline !== undefined) {
|
|
176
|
+
if (semanticSnapshotRef.current != null &&
|
|
177
|
+
reconnectSemanticKey !== semanticSnapshotRef.current) {
|
|
178
|
+
dirtyWhileOfflineRef.current = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Inline object: dedicated effect owns `data`; never hit the network here. */
|
|
182
|
+
if (offlineData != null && typeof offlineData === "object") {
|
|
183
|
+
prevIsOnlineRef.current = online;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
/** File path: wait for read outcome; `ok` keeps local data; only `failed` falls through to online fetch. */
|
|
187
|
+
if (typeof offlineData === "string" && offlineFileStatus === "ok") {
|
|
188
|
+
prevIsOnlineRef.current = online;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (typeof offlineData === "string" &&
|
|
192
|
+
offlineFileStatus !== "failed" &&
|
|
193
|
+
offlineFileStatus !== "ok") {
|
|
194
|
+
prevIsOnlineRef.current = online;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
75
197
|
const keyChanged = lastRequestKeyRef.current !== requestKey;
|
|
76
198
|
const isManualReload = pendingReloadRef.current;
|
|
77
199
|
if (isManualReload) {
|
|
@@ -82,30 +204,30 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
82
204
|
if (keyChanged) {
|
|
83
205
|
lastRequestKeyRef.current = requestKey;
|
|
84
206
|
setData(null);
|
|
85
|
-
|
|
207
|
+
hasCommittedRef.current = false;
|
|
86
208
|
setErrors(null);
|
|
87
209
|
}
|
|
88
210
|
prevIsOnlineRef.current = false;
|
|
89
|
-
setLoading(false);
|
|
90
211
|
setErrors(new Error(network_1.NO_NETWORK_MESSAGE));
|
|
91
212
|
setTimeoutCountdown(null);
|
|
92
213
|
return;
|
|
93
214
|
}
|
|
94
215
|
if (keyChanged) {
|
|
95
216
|
lastRequestKeyRef.current = requestKey;
|
|
96
|
-
|
|
217
|
+
hasCommittedRef.current = false;
|
|
97
218
|
setData(null);
|
|
98
219
|
setErrors(null);
|
|
99
220
|
}
|
|
100
221
|
if (!keyChanged && reconnecting) {
|
|
101
222
|
const onlyNoNetwork = errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE;
|
|
102
|
-
|
|
103
|
-
hasCommittedRef.current
|
|
104
|
-
onlyNoNetwork
|
|
105
|
-
|
|
223
|
+
const shouldRefetchAfterReconnect = dirtyWhileOfflineRef.current ||
|
|
224
|
+
!hasCommittedRef.current ||
|
|
225
|
+
!onlyNoNetwork;
|
|
226
|
+
if (!isManualReload && !shouldRefetchAfterReconnect) {
|
|
106
227
|
setErrors(null);
|
|
107
|
-
setLoading(false);
|
|
108
228
|
prevIsOnlineRef.current = true;
|
|
229
|
+
dirtyWhileOfflineRef.current = false;
|
|
230
|
+
semanticSnapshotRef.current = null;
|
|
109
231
|
return;
|
|
110
232
|
}
|
|
111
233
|
}
|
|
@@ -115,15 +237,14 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
115
237
|
const timedOutRef = { current: false };
|
|
116
238
|
const requestStartedAt = Date.now();
|
|
117
239
|
const timeoutMs = (0, network_1.resolveRequestTimeoutMs)(timeoutAfterSeconds);
|
|
118
|
-
setLoading(true);
|
|
119
240
|
setErrors(null);
|
|
120
241
|
setTimeoutCountdown(null);
|
|
121
|
-
const
|
|
242
|
+
const keepStaleForManualReload = isManualReload && hasCommittedRef.current;
|
|
243
|
+
const keepStaleDuringReconnectRefetch = reconnecting &&
|
|
122
244
|
hasCommittedRef.current &&
|
|
123
245
|
errorsRef.current?.message === network_1.NO_NETWORK_MESSAGE &&
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!keyChanged && !keepStaleDuringFetch && !keepStaleForManualReload) {
|
|
246
|
+
dirtyWhileOfflineRef.current;
|
|
247
|
+
if (!keyChanged && !keepStaleForManualReload && !keepStaleDuringReconnectRefetch) {
|
|
127
248
|
setData(null);
|
|
128
249
|
}
|
|
129
250
|
const policyDispose = timeoutMs == null
|
|
@@ -144,7 +265,7 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
144
265
|
},
|
|
145
266
|
});
|
|
146
267
|
const baseUrl = exports.SERVICE_BASE_URL[service];
|
|
147
|
-
const url = buildUrl(baseUrl,
|
|
268
|
+
const url = buildUrl(baseUrl, onlinePath, onlinePathParams, queryParams);
|
|
148
269
|
const init = {
|
|
149
270
|
method,
|
|
150
271
|
headers: {
|
|
@@ -163,13 +284,15 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
163
284
|
if (!res.ok) {
|
|
164
285
|
setErrors(new Error((0, network_1.formatHttpStatusMessage)(res.status, text || res.statusText)));
|
|
165
286
|
setData(null);
|
|
166
|
-
|
|
287
|
+
hasCommittedRef.current = false;
|
|
167
288
|
return;
|
|
168
289
|
}
|
|
169
290
|
if (text.length === 0) {
|
|
170
291
|
setData(null);
|
|
171
292
|
setErrors(null);
|
|
172
|
-
|
|
293
|
+
hasCommittedRef.current = true;
|
|
294
|
+
dirtyWhileOfflineRef.current = false;
|
|
295
|
+
semanticSnapshotRef.current = null;
|
|
173
296
|
return;
|
|
174
297
|
}
|
|
175
298
|
try {
|
|
@@ -178,20 +301,26 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
178
301
|
if (!cancelled) {
|
|
179
302
|
setData(parsed.result);
|
|
180
303
|
setErrors(null);
|
|
181
|
-
|
|
304
|
+
hasCommittedRef.current = true;
|
|
305
|
+
dirtyWhileOfflineRef.current = false;
|
|
306
|
+
semanticSnapshotRef.current = null;
|
|
182
307
|
}
|
|
183
308
|
}
|
|
184
309
|
catch (parseError) {
|
|
185
310
|
if (!cancelled) {
|
|
186
|
-
console.error("[
|
|
311
|
+
console.error("[RopeGeoDataLoader] Invalid JSON response", {
|
|
187
312
|
url,
|
|
188
313
|
status: res.status,
|
|
189
314
|
responseText: text.slice(0, 500),
|
|
190
|
-
parseError: parseError instanceof Error
|
|
315
|
+
parseError: parseError instanceof Error
|
|
316
|
+
? parseError.message
|
|
317
|
+
: String(parseError),
|
|
191
318
|
});
|
|
192
|
-
setErrors(parseError instanceof Error
|
|
319
|
+
setErrors(parseError instanceof Error
|
|
320
|
+
? parseError
|
|
321
|
+
: new Error("Invalid JSON response"));
|
|
193
322
|
setData(null);
|
|
194
|
-
|
|
323
|
+
hasCommittedRef.current = false;
|
|
195
324
|
}
|
|
196
325
|
}
|
|
197
326
|
})
|
|
@@ -201,24 +330,23 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
201
330
|
if (timedOutRef.current) {
|
|
202
331
|
setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(new Error(network_1.NETWORK_REQUEST_TIMED_OUT_MESSAGE))));
|
|
203
332
|
setData(null);
|
|
204
|
-
|
|
333
|
+
hasCommittedRef.current = false;
|
|
205
334
|
return;
|
|
206
335
|
}
|
|
207
336
|
if ((0, network_1.isAbortError)(err))
|
|
208
337
|
return;
|
|
209
|
-
console.error("[
|
|
338
|
+
console.error("[RopeGeoDataLoader] Request failed", {
|
|
210
339
|
url,
|
|
211
340
|
error: err instanceof Error ? err.message : String(err),
|
|
212
341
|
});
|
|
213
342
|
setErrors(new Error((0, network_1.formatNetworkRequestErrorMessage)(err)));
|
|
214
343
|
setData(null);
|
|
215
|
-
|
|
344
|
+
hasCommittedRef.current = false;
|
|
216
345
|
})
|
|
217
346
|
.finally(() => {
|
|
218
347
|
policyDispose();
|
|
219
348
|
if (!cancelled) {
|
|
220
349
|
setTimeoutCountdown(null);
|
|
221
|
-
setLoading(false);
|
|
222
350
|
}
|
|
223
351
|
});
|
|
224
352
|
return () => {
|
|
@@ -229,20 +357,20 @@ function RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, bo
|
|
|
229
357
|
}, [
|
|
230
358
|
service,
|
|
231
359
|
method,
|
|
232
|
-
|
|
360
|
+
onlinePath,
|
|
233
361
|
pathParamsKey,
|
|
234
362
|
queryParamsKey,
|
|
235
363
|
bodyKey,
|
|
236
364
|
timeoutAfterSeconds,
|
|
237
365
|
isOnline,
|
|
238
|
-
refreshOnReconnect,
|
|
239
366
|
requestKey,
|
|
240
367
|
reloadTick,
|
|
368
|
+
offlineData,
|
|
369
|
+
offlineFileStatus,
|
|
370
|
+
reconnectSemanticKey,
|
|
371
|
+
queryParams,
|
|
241
372
|
]);
|
|
242
|
-
const refreshing = loading && hasCommittedOnce;
|
|
243
373
|
return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children({
|
|
244
|
-
loading,
|
|
245
|
-
refreshing,
|
|
246
374
|
data,
|
|
247
375
|
errors,
|
|
248
376
|
timeoutCountdown,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type CursorPaginationParams } from "../models";
|
|
3
|
+
import { Method, Service } from "./RopeGeoDataLoader";
|
|
4
|
+
export type RopeGeoPagedDataLoaderProps<T = unknown> = {
|
|
5
|
+
service: Service;
|
|
6
|
+
method?: (typeof Method)[keyof typeof Method];
|
|
7
|
+
onlinePath: string;
|
|
8
|
+
onlinePathParams?: Record<string, string>;
|
|
9
|
+
queryParams: CursorPaginationParams;
|
|
10
|
+
timeoutAfterSeconds?: number;
|
|
11
|
+
isOnline?: boolean;
|
|
12
|
+
children: (args: {
|
|
13
|
+
loadingNextPage: boolean;
|
|
14
|
+
data: T[] | null;
|
|
15
|
+
errors: Error | null;
|
|
16
|
+
loadNextPage: () => void;
|
|
17
|
+
morePages: boolean;
|
|
18
|
+
timeoutCountdown: number | null;
|
|
19
|
+
reload: () => void;
|
|
20
|
+
}) => ReactNode;
|
|
21
|
+
};
|
|
22
|
+
export declare function RopeGeoPagedDataLoader<T = unknown>({ service, method, onlinePath, onlinePathParams, queryParams, timeoutAfterSeconds, isOnline, children, }: RopeGeoPagedDataLoaderProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
//# sourceMappingURL=RopeGeoPagedDataLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RopeGeoPagedDataLoader.d.ts","sourceRoot":"","sources":["../../src/components/RopeGeoPagedDataLoader.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,qBAAqB,CAAC;AAqCxE,MAAM,MAAM,2BAA2B,CAAC,CAAC,GAAG,OAAO,IAAI;IACrD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,OAAO,MAAM,CAAC,CAAC;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,WAAW,EAAE,sBAAsB,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE;QACf,eAAe,EAAE,OAAO,CAAC;QACzB,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;QACjB,MAAM,EAAE,KAAK,GAAG,IAAI,CAAC;QACrB,YAAY,EAAE,MAAM,IAAI,CAAC;QACzB,SAAS,EAAE,OAAO,CAAC;QACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,MAAM,EAAE,MAAM,IAAI,CAAC;KACpB,KAAK,SAAS,CAAC;CACjB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,OAAO,EAAE,EAClD,OAAO,EACP,MAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,mBAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,2BAA2B,CAAC,CAAC,CAAC,2CAsahC"}
|