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.
Files changed (44) hide show
  1. package/README.md +7 -7
  2. package/dist/components/RopeGeoDataLoader.d.ts +56 -0
  3. package/dist/components/RopeGeoDataLoader.d.ts.map +1 -0
  4. package/dist/components/{RopeGeoHttpRequest.js → RopeGeoDataLoader.js} +173 -45
  5. package/dist/components/RopeGeoPagedDataLoader.d.ts +23 -0
  6. package/dist/components/RopeGeoPagedDataLoader.d.ts.map +1 -0
  7. package/dist/components/{RopeGeoCursorPaginationHttpRequest.js → RopeGeoPagedDataLoader.js} +81 -63
  8. package/dist/components/RopeGeoProgressDataLoader.d.ts +23 -0
  9. package/dist/components/RopeGeoProgressDataLoader.d.ts.map +1 -0
  10. package/dist/components/{RopeGeoPaginationHttpRequest.js → RopeGeoProgressDataLoader.js} +57 -39
  11. package/dist/components/index.d.ts +3 -3
  12. package/dist/components/index.d.ts.map +1 -1
  13. package/dist/components/index.js +10 -10
  14. package/dist/models/api/params/cursorPaginationParams.d.ts +5 -0
  15. package/dist/models/api/params/cursorPaginationParams.d.ts.map +1 -1
  16. package/dist/models/api/params/cursorPaginationParams.js +10 -0
  17. package/dist/models/api/params/paginationParams.d.ts +5 -0
  18. package/dist/models/api/params/paginationParams.d.ts.map +1 -1
  19. package/dist/models/api/params/paginationParams.js +10 -0
  20. package/dist/models/minimap/abstract/pageMiniMap.d.ts +5 -2
  21. package/dist/models/minimap/abstract/pageMiniMap.d.ts.map +1 -1
  22. package/dist/models/minimap/abstract/pageMiniMap.js +10 -5
  23. package/dist/models/minimap/concrete/offlinePageMiniMap.d.ts +1 -1
  24. package/dist/models/minimap/concrete/offlinePageMiniMap.d.ts.map +1 -1
  25. package/dist/models/minimap/concrete/offlinePageMiniMap.js +4 -3
  26. package/dist/models/minimap/concrete/onlinePageMiniMap.d.ts +1 -1
  27. package/dist/models/minimap/concrete/onlinePageMiniMap.d.ts.map +1 -1
  28. package/dist/models/minimap/concrete/onlinePageMiniMap.js +3 -3
  29. package/dist/models/pageViews/offlineRopewikiPageView.d.ts +1 -1
  30. package/dist/models/pageViews/offlineRopewikiPageView.d.ts.map +1 -1
  31. package/dist/models/pageViews/offlineRopewikiPageView.js +3 -3
  32. package/dist/models/pageViews/onlineRopewikiPageView.d.ts +1 -1
  33. package/dist/models/pageViews/onlineRopewikiPageView.d.ts.map +1 -1
  34. package/dist/models/pageViews/onlineRopewikiPageView.js +4 -4
  35. package/dist/models/pageViews/ropewikiPageView.d.ts +4 -1
  36. package/dist/models/pageViews/ropewikiPageView.d.ts.map +1 -1
  37. package/dist/models/pageViews/ropewikiPageView.js +13 -1
  38. package/package.json +1 -1
  39. package/dist/components/RopeGeoCursorPaginationHttpRequest.d.ts +0 -56
  40. package/dist/components/RopeGeoCursorPaginationHttpRequest.d.ts.map +0 -1
  41. package/dist/components/RopeGeoHttpRequest.d.ts +0 -68
  42. package/dist/components/RopeGeoHttpRequest.d.ts.map +0 -1
  43. package/dist/components/RopeGeoPaginationHttpRequest.d.ts +0 -69
  44. 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 with shared validation/fields. | `import { BaseRopewikiPageView } from 'ropegeo-common/models'` |
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`); online/offline tile templates and optional `legend`. | `import { PageMiniMap } from 'ropegeo-common/models'` |
258
- | `OnlinePageMiniMap` | `PageMiniMap` | API page tiles template (`onlineTilesTemplate`, `fetchType: "online"`). | `import { OnlinePageMiniMap } from 'ropegeo-common/models'` |
259
- | `OfflinePageMiniMap` | `PageMiniMap` | Persisted local tiles template (`offlineTilesTemplate`, `fetchType: "offline"`). | `import { OfflinePageMiniMap } from 'ropegeo-common/models'` |
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
- | `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'` |
330
- | `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'` |
331
- | `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'` |
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.RopeGeoHttpRequest = RopeGeoHttpRequest;
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 RopeGeoHttpRequest({ service, method, path, pathParams, queryParams, body, timeoutAfterSeconds, isOnline, refreshOnReconnect = false, children, }) {
44
- const [loading, setLoading] = (0, react_1.useState)(true);
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)(hasCommittedOnce);
73
+ const hasCommittedRef = (0, react_1.useRef)(false);
53
74
  errorsRef.current = errors;
54
- hasCommittedRef.current = hasCommittedOnce;
55
- const reload = (0, react_1.useCallback)(() => {
56
- if (isOnline === false)
57
- return;
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}|${path}|${pathParamsKey}|${queryParamsKey}|${String(bodyKey)}|${timeoutAfterSeconds ?? ""}`, [service, method, path, pathParamsKey, queryParamsKey, bodyKey, timeoutAfterSeconds]);
69
- const prevIsOnlineRef = (0, react_1.useRef)(undefined);
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
- setHasCommittedOnce(false);
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
- setHasCommittedOnce(false);
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
- if (!isManualReload &&
103
- hasCommittedRef.current &&
104
- onlyNoNetwork &&
105
- !refreshOnReconnect) {
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 keepStaleDuringFetch = reconnecting &&
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
- refreshOnReconnect;
125
- const keepStaleForManualReload = isManualReload && hasCommittedRef.current;
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, path, pathParams, queryParams);
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
- setHasCommittedOnce(false);
287
+ hasCommittedRef.current = false;
167
288
  return;
168
289
  }
169
290
  if (text.length === 0) {
170
291
  setData(null);
171
292
  setErrors(null);
172
- setHasCommittedOnce(true);
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
- setHasCommittedOnce(true);
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("[RopeGeoHttpRequest] Invalid JSON response", {
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 ? parseError.message : String(parseError),
315
+ parseError: parseError instanceof Error
316
+ ? parseError.message
317
+ : String(parseError),
191
318
  });
192
- setErrors(parseError instanceof Error ? parseError : new Error("Invalid JSON response"));
319
+ setErrors(parseError instanceof Error
320
+ ? parseError
321
+ : new Error("Invalid JSON response"));
193
322
  setData(null);
194
- setHasCommittedOnce(false);
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
- setHasCommittedOnce(false);
333
+ hasCommittedRef.current = false;
205
334
  return;
206
335
  }
207
336
  if ((0, network_1.isAbortError)(err))
208
337
  return;
209
- console.error("[RopeGeoHttpRequest] Request failed", {
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
- setHasCommittedOnce(false);
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
- path,
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"}