@maschinenlesbar.org/pegel-online-cli 0.1.0 → 0.3.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 +29 -8
- package/dist/src/cli/commands/stations.d.ts +0 -1
- package/dist/src/cli/commands/stations.js +29 -5
- package/dist/src/cli/commands/timeseries.d.ts +0 -1
- package/dist/src/cli/commands/timeseries.js +3 -4
- package/dist/src/cli/index.d.ts +0 -1
- package/dist/src/cli/index.js +0 -1
- package/dist/src/cli/io.d.ts +2 -2
- package/dist/src/cli/io.js +7 -3
- package/dist/src/cli/program.d.ts +0 -1
- package/dist/src/cli/program.js +6 -7
- package/dist/src/cli/run.d.ts +12 -1
- package/dist/src/cli/run.js +35 -2
- package/dist/src/cli/shared.d.ts +21 -15
- package/dist/src/cli/shared.js +39 -48
- package/dist/src/client/client.d.ts +46 -1
- package/dist/src/client/client.js +152 -27
- package/dist/src/client/engine.d.ts +51 -13
- package/dist/src/client/engine.js +404 -81
- package/dist/src/client/errors.d.ts +38 -2
- package/dist/src/client/errors.js +74 -4
- package/dist/src/client/http.d.ts +26 -1
- package/dist/src/client/http.js +16 -2
- package/dist/src/client/index.d.ts +6 -4
- package/dist/src/client/index.js +4 -4
- package/dist/src/client/query.d.ts +0 -1
- package/dist/src/client/query.js +0 -1
- package/dist/src/client/types.d.ts +60 -10
- package/dist/src/client/types.js +0 -1
- package/dist/src/client/validate.d.ts +72 -0
- package/dist/src/client/validate.js +155 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/package.json +4 -3
- package/dist/src/cli/commands/stations.d.ts.map +0 -1
- package/dist/src/cli/commands/stations.js.map +0 -1
- package/dist/src/cli/commands/timeseries.d.ts.map +0 -1
- package/dist/src/cli/commands/timeseries.js.map +0 -1
- package/dist/src/cli/index.d.ts.map +0 -1
- package/dist/src/cli/index.js.map +0 -1
- package/dist/src/cli/io.d.ts.map +0 -1
- package/dist/src/cli/io.js.map +0 -1
- package/dist/src/cli/program.d.ts.map +0 -1
- package/dist/src/cli/program.js.map +0 -1
- package/dist/src/cli/run.d.ts.map +0 -1
- package/dist/src/cli/run.js.map +0 -1
- package/dist/src/cli/shared.d.ts.map +0 -1
- package/dist/src/cli/shared.js.map +0 -1
- package/dist/src/client/client.d.ts.map +0 -1
- package/dist/src/client/client.js.map +0 -1
- package/dist/src/client/engine.d.ts.map +0 -1
- package/dist/src/client/engine.js.map +0 -1
- package/dist/src/client/errors.d.ts.map +0 -1
- package/dist/src/client/errors.js.map +0 -1
- package/dist/src/client/http.d.ts.map +0 -1
- package/dist/src/client/http.js.map +0 -1
- package/dist/src/client/index.d.ts.map +0 -1
- package/dist/src/client/index.js.map +0 -1
- package/dist/src/client/query.d.ts.map +0 -1
- package/dist/src/client/query.js.map +0 -1
- package/dist/src/client/types.d.ts.map +0 -1
- package/dist/src/client/types.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
|
@@ -6,17 +6,9 @@
|
|
|
6
6
|
// client.timeseries.currentMeasurement("BONN", "W")
|
|
7
7
|
// client.timeseries.measurements("BONN", "W", { start: "P3D" })
|
|
8
8
|
import { RequestEngine } from "./engine.js";
|
|
9
|
-
import {
|
|
9
|
+
import { PegelParseError, PegelValidationError } from "./errors.js";
|
|
10
|
+
import { assertValid, idListProblem, knownKeysProblem, nonEmptyProblem, normalizeInput, optionalBooleanProblem, } from "./validate.js";
|
|
10
11
|
const API = "/webservices/rest-api/v2";
|
|
11
|
-
/**
|
|
12
|
-
* Station names, waters and ids are matched exactly by the API, which stores them
|
|
13
|
-
* composed (NFC): a decomposed umlaut ("KO" + U+0308 + "LN", as pasted from macOS
|
|
14
|
-
* file names or some PDFs) is a 404 / an empty list. Compose every such input.
|
|
15
|
-
* NFC, not NFKC: an id lookup must not rewrite compatibility characters.
|
|
16
|
-
*/
|
|
17
|
-
function nfc(value) {
|
|
18
|
-
return value.normalize("NFC");
|
|
19
|
-
}
|
|
20
12
|
/**
|
|
21
13
|
* One URL path segment from a caller-supplied station or timeseries id. A blank
|
|
22
14
|
* value would build a different path (`stations/.json`, `stations//W.json`), and
|
|
@@ -25,12 +17,71 @@ function nfc(value) {
|
|
|
25
17
|
*/
|
|
26
18
|
function enc(name, value) {
|
|
27
19
|
if (typeof value !== "string" || value.trim() === "") {
|
|
28
|
-
throw new
|
|
20
|
+
throw new PegelValidationError(`Invalid ${name}: expected a non-empty string, got ${typeof value === "string" ? JSON.stringify(value) : typeof value}.`);
|
|
29
21
|
}
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
const id = normalizeInput(value);
|
|
23
|
+
if (id === "." || id === "..") {
|
|
24
|
+
throw new PegelValidationError(`Invalid ${name} "${id}": "." and ".." cannot be used as an id.`);
|
|
25
|
+
}
|
|
26
|
+
return encodeURIComponent(id);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An optional query value: `undefined` means omitted; anything else must be a
|
|
30
|
+
* non-blank string (nonEmptyProblem), or PegelValidationError is thrown.
|
|
31
|
+
*/
|
|
32
|
+
function optionalValue(name, value) {
|
|
33
|
+
return value === undefined ? undefined : normalizeInput(assertValid(name, value, nonEmptyProblem));
|
|
34
|
+
}
|
|
35
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36
|
+
const hasStrings = (...keys) => (v) => isObject(v) && keys.every((k) => typeof v[k] === "string");
|
|
37
|
+
const arrayOf = (item) => (v) => Array.isArray(v) && v.every(item);
|
|
38
|
+
const isWater = hasStrings("shortname", "longname");
|
|
39
|
+
const isStation = hasStrings("uuid", "shortname");
|
|
40
|
+
const isTimeseries = hasStrings("shortname", "unit");
|
|
41
|
+
const isMeasurement = (v) => hasStrings("timestamp")(v) && (typeof v.value === "number" || v.value === null);
|
|
42
|
+
/** `value` when `shape(value)` holds; otherwise a PegelParseError naming the expectation. */
|
|
43
|
+
function expectShape(path, value, shape, what) {
|
|
44
|
+
if (!shape(value))
|
|
45
|
+
throw new PegelParseError(`Unexpected response from ${path}: expected ${what}.`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
const INCLUDE_KEYS = ["includeTimeseries", "includeCurrentMeasurement", "includeCharacteristicValues"];
|
|
49
|
+
const LIST_KEYS = ["ids", "waters", "fuzzyId", ...INCLUDE_KEYS];
|
|
50
|
+
const MEASUREMENT_KEYS = ["start", "end"];
|
|
51
|
+
/**
|
|
52
|
+
* A method's parameter object, checked before any request: `undefined` (or `null` from
|
|
53
|
+
* JavaScript) means none; otherwise only the documented keys (knownKeysProblem), and the
|
|
54
|
+
* include flags only as booleans. Throws PegelValidationError.
|
|
55
|
+
*/
|
|
56
|
+
function checkParams(name, params, allowed) {
|
|
57
|
+
if (params === undefined || params === null)
|
|
58
|
+
return {};
|
|
59
|
+
assertValid(name, params, knownKeysProblem(allowed));
|
|
60
|
+
for (const key of INCLUDE_KEYS) {
|
|
61
|
+
if (allowed.includes(key))
|
|
62
|
+
assertValid(key, params[key], optionalBooleanProblem);
|
|
32
63
|
}
|
|
33
|
-
return
|
|
64
|
+
return params;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The value PEGELONLINE relays for "no reading" on some gauges (seen on the
|
|
68
|
+
* Rijkswaterstaat gauge PANNERDENSE KOP: `99999` cm, interleaved with real readings of
|
|
69
|
+
* 576–597 cm in a measurement window). As a number it read as a 1 km water level and
|
|
70
|
+
* broke every minimum, maximum, trend and map built on it.
|
|
71
|
+
*/
|
|
72
|
+
export const NO_VALUE_SENTINEL = 99999;
|
|
73
|
+
/** `m` with a sentinel `value` replaced by `null` (no reading at that time). */
|
|
74
|
+
function readingOf(m) {
|
|
75
|
+
return m.value === NO_VALUE_SENTINEL ? { ...m, value: null } : m;
|
|
76
|
+
}
|
|
77
|
+
/** A timeseries with the sentinel mapped in its embedded current measurement. */
|
|
78
|
+
function timeseriesOf(t) {
|
|
79
|
+
const current = t.currentMeasurement;
|
|
80
|
+
return current !== undefined && isMeasurement(current) ? { ...t, currentMeasurement: readingOf(current) } : t;
|
|
81
|
+
}
|
|
82
|
+
/** A station with the sentinel mapped in every embedded current measurement. */
|
|
83
|
+
function stationOf(s) {
|
|
84
|
+
return Array.isArray(s.timeseries) ? { ...s, timeseries: s.timeseries.map((t) => (isObject(t) ? timeseriesOf(t) : t)) } : s;
|
|
34
85
|
}
|
|
35
86
|
/** Drop undefined values so only the parameters the caller set are sent. */
|
|
36
87
|
function prune(params) {
|
|
@@ -68,17 +119,26 @@ class StationsResource {
|
|
|
68
119
|
constructor(e) {
|
|
69
120
|
this.e = e;
|
|
70
121
|
}
|
|
71
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Rejects (PegelValidationError, no request) a blank `waters` or `fuzzyId`, a
|
|
124
|
+
* blank `ids` entry and an empty `ids` array: the API reads an empty parameter as
|
|
125
|
+
* no filter and would answer with every station.
|
|
126
|
+
*/
|
|
127
|
+
async list(params = {}) {
|
|
128
|
+
params = checkParams("stations.list parameters", params, LIST_KEYS);
|
|
72
129
|
const query = prune({
|
|
73
|
-
ids: params.ids
|
|
74
|
-
waters:
|
|
75
|
-
fuzzyId:
|
|
130
|
+
ids: params.ids === undefined ? undefined : assertValid("ids", params.ids, idListProblem).map(normalizeInput).join(","),
|
|
131
|
+
waters: optionalValue("waters", params.waters),
|
|
132
|
+
fuzzyId: optionalValue("fuzzyId", params.fuzzyId),
|
|
76
133
|
...stationIncludes(params),
|
|
77
134
|
});
|
|
78
|
-
|
|
135
|
+
const path = `${API}/stations.json`;
|
|
136
|
+
return expectShape(path, await this.e.getJson(path, query), arrayOf(isStation), "an array of stations").map(stationOf);
|
|
79
137
|
}
|
|
80
138
|
async get(station, params = {}) {
|
|
81
|
-
|
|
139
|
+
params = checkParams("stations.get parameters", params, INCLUDE_KEYS);
|
|
140
|
+
const path = `${API}/stations/${enc("station", station)}.json`;
|
|
141
|
+
return stationOf(expectShape(path, await this.e.getJson(path, stationIncludes(params)), isStation, "a station object"));
|
|
82
142
|
}
|
|
83
143
|
}
|
|
84
144
|
/** Timeseries: metadata, the current measurement, a window of measurements, gauge marks. */
|
|
@@ -89,27 +149,92 @@ class TimeseriesResource {
|
|
|
89
149
|
}
|
|
90
150
|
/** Timeseries metadata (e.g. "W" = water level, "Q" = flow). */
|
|
91
151
|
async get(station, timeseries = "W", params = {}) {
|
|
92
|
-
|
|
152
|
+
params = checkParams("timeseries.get parameters", params, INCLUDE_KEYS);
|
|
153
|
+
const path = `${API}/stations/${enc("station", station)}/${enc("timeseries", timeseries)}.json`;
|
|
154
|
+
return timeseriesOf(expectShape(path, await this.e.getJson(path, includeQuery(params)), isTimeseries, "a timeseries object"));
|
|
93
155
|
}
|
|
94
156
|
async currentMeasurement(station, timeseries = "W") {
|
|
95
|
-
|
|
157
|
+
const path = `${API}/stations/${enc("station", station)}/${enc("timeseries", timeseries)}/currentmeasurement.json`;
|
|
158
|
+
return readingOf(expectShape(path, await this.e.getJson(path), isMeasurement, "a measurement object"));
|
|
96
159
|
}
|
|
160
|
+
/** Rejects (PegelValidationError, no request) a blank `start` or `end`. */
|
|
97
161
|
async measurements(station, timeseries = "W", params = {}) {
|
|
98
|
-
|
|
162
|
+
params = checkParams("timeseries.measurements parameters", params, MEASUREMENT_KEYS);
|
|
163
|
+
const path = `${API}/stations/${enc("station", station)}/${enc("timeseries", timeseries)}/measurements.json`;
|
|
164
|
+
const query = prune({ start: optionalValue("start", params.start), end: optionalValue("end", params.end) });
|
|
165
|
+
return expectShape(path, await this.e.getJson(path, query), arrayOf(isMeasurement), "an array of measurements").map(readingOf);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Case-insensitive equality the way the API's id lookup behaves (`roßdorf` finds `ROSSDORF`). */
|
|
169
|
+
function sameId(a, b) {
|
|
170
|
+
return a.toLowerCase() === b.toLowerCase() || a.toUpperCase() === b.toUpperCase();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The filter values of a `stations.list` call that matched nothing in its result. The
|
|
174
|
+
* API drops an `ids` entry it doesn't know and answers an unknown `waters` or `fuzzyId`
|
|
175
|
+
* with `[]`, both with HTTP 200, so `ids: ["BONN", "KOELN", "EMMERICH"]` silently gave two
|
|
176
|
+
* stations and `waters: "Rhine"` an empty river. This reports each `ids` entry that names
|
|
177
|
+
* none of the returned stations (by uuid, number, shortname or longname, ignoring case),
|
|
178
|
+
* and a `waters` or `fuzzyId` filter whose result is empty. An empty array means every
|
|
179
|
+
* filter matched.
|
|
180
|
+
*
|
|
181
|
+
* When the call looked stations up by name (`ids` or `fuzzyId`), it also reports every
|
|
182
|
+
* shortname that two or more returned stations share: NEUSTADT names a gauge on the
|
|
183
|
+
* LEINE and one on the OSTSEE, and a lookup by that name (`stations.get("NEUSTADT")`,
|
|
184
|
+
* `timeseries.currentMeasurement("NEUSTADT")`) silently returns one of them — the uuid
|
|
185
|
+
* or number picks the right one. The CLI prints all notes on stderr.
|
|
186
|
+
*/
|
|
187
|
+
export function stationListNotes(params, stations) {
|
|
188
|
+
const notes = [];
|
|
189
|
+
for (const raw of params.ids ?? []) {
|
|
190
|
+
const id = normalizeInput(raw);
|
|
191
|
+
const found = stations.some((s) => [s.uuid, s.number, s.shortname, s.longname].some((f) => typeof f === "string" && sameId(f, id)));
|
|
192
|
+
if (!found)
|
|
193
|
+
notes.push({ kind: "unmatched", filter: "ids", value: raw });
|
|
194
|
+
}
|
|
195
|
+
if (stations.length === 0) {
|
|
196
|
+
if (params.waters !== undefined)
|
|
197
|
+
notes.push({ kind: "unmatched", filter: "waters", value: params.waters });
|
|
198
|
+
if (params.fuzzyId !== undefined)
|
|
199
|
+
notes.push({ kind: "unmatched", filter: "fuzzyId", value: params.fuzzyId });
|
|
200
|
+
}
|
|
201
|
+
if (params.ids !== undefined || params.fuzzyId !== undefined) {
|
|
202
|
+
const byName = new Map();
|
|
203
|
+
for (const s of stations) {
|
|
204
|
+
const key = s.shortname.toUpperCase();
|
|
205
|
+
byName.set(key, [...(byName.get(key) ?? []), s]);
|
|
206
|
+
}
|
|
207
|
+
for (const same of byName.values()) {
|
|
208
|
+
if (same.length < 2)
|
|
209
|
+
continue;
|
|
210
|
+
notes.push({
|
|
211
|
+
kind: "ambiguous",
|
|
212
|
+
name: same[0].shortname,
|
|
213
|
+
stations: same.map((s) => ({
|
|
214
|
+
uuid: s.uuid,
|
|
215
|
+
number: s.number,
|
|
216
|
+
shortname: s.shortname,
|
|
217
|
+
longname: s.longname,
|
|
218
|
+
...(s.water?.shortname !== undefined ? { water: s.water.shortname } : {}),
|
|
219
|
+
})),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
99
222
|
}
|
|
223
|
+
return notes;
|
|
100
224
|
}
|
|
101
225
|
export class PegelOnlineClient {
|
|
102
226
|
engine;
|
|
103
227
|
stations;
|
|
104
228
|
timeseries;
|
|
105
229
|
constructor(options = {}) {
|
|
106
|
-
|
|
230
|
+
// A JavaScript caller may pass null for "no options"; treat it like undefined.
|
|
231
|
+
this.engine = new RequestEngine(options ?? {});
|
|
107
232
|
this.stations = new StationsResource(this.engine);
|
|
108
233
|
this.timeseries = new TimeseriesResource(this.engine);
|
|
109
234
|
}
|
|
110
235
|
/** List all bodies of water (Gewässer) covered by the service. */
|
|
111
|
-
waters() {
|
|
112
|
-
|
|
236
|
+
async waters() {
|
|
237
|
+
const path = `${API}/waters.json`;
|
|
238
|
+
return expectShape(path, await this.engine.getJson(path), arrayOf(isWater), "an array of waters");
|
|
113
239
|
}
|
|
114
240
|
}
|
|
115
|
-
//# sourceMappingURL=client.js.map
|
|
@@ -9,7 +9,9 @@ export interface RawResponse {
|
|
|
9
9
|
/**
|
|
10
10
|
* Options for {@link RequestEngine} and the client. The numeric options must be
|
|
11
11
|
* integers within their documented range; anything else (negative, fractional,
|
|
12
|
-
* NaN, Infinity, too large) makes the constructor throw a
|
|
12
|
+
* NaN, Infinity, too large) makes the constructor throw a PegelValidationError, as
|
|
13
|
+
* does a `transport` or `sleep` that is not a function and a `headers` value an HTTP
|
|
14
|
+
* header can't carry.
|
|
13
15
|
*/
|
|
14
16
|
export interface EngineOptions {
|
|
15
17
|
/** Base URL of the API. Defaults to https://www.pegelonline.wsv.de */
|
|
@@ -32,15 +34,16 @@ export interface EngineOptions {
|
|
|
32
34
|
*/
|
|
33
35
|
timeoutMs?: number;
|
|
34
36
|
/**
|
|
35
|
-
* Number of automatic retries for transient (429/503) responses
|
|
36
|
-
* (
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Number of automatic retries for transient (429/503) responses and reset
|
|
38
|
+
* connections (ECONNRESET, EPIPE, ECONNABORTED, undici's UND_ERR_SOCKET, anywhere in
|
|
39
|
+
* the error's `cause` chain; GET and HEAD only), 0..`MAX_RETRIES` (10). Each waits
|
|
40
|
+
* `retryDelayMs * attempt`, or longer when the response's `Retry-After` asks (up to
|
|
41
|
+
* `MAX_RETRY_AFTER_MS`; a longer one is not retried). Timeouts are not retried.
|
|
39
42
|
*/
|
|
40
43
|
maxRetries?: number;
|
|
41
44
|
/**
|
|
42
|
-
* Base backoff between retries in milliseconds (grows linearly);
|
|
43
|
-
* Retry-After. At most `MAX_RETRY_AFTER_MS`.
|
|
45
|
+
* Base backoff between retries in milliseconds (grows linearly); the floor of every
|
|
46
|
+
* wait, Retry-After or not. At most `MAX_RETRY_AFTER_MS`.
|
|
44
47
|
*/
|
|
45
48
|
retryDelayMs?: number;
|
|
46
49
|
/**
|
|
@@ -62,8 +65,10 @@ export declare const MAX_RETRIES = 10;
|
|
|
62
65
|
/**
|
|
63
66
|
* Longest `Retry-After` the engine waits out before retrying a 429/503. When the
|
|
64
67
|
* server asks for longer, the engine does not retry at all and surfaces the error at
|
|
65
|
-
* once
|
|
66
|
-
* out, and a hostile value must
|
|
68
|
+
* once, naming the requested wait (`PegelApiError.retryAfterMs`): retrying early would
|
|
69
|
+
* only land inside the window the server asked us to wait out, and a hostile value must
|
|
70
|
+
* not stall the CLI. A shorter `Retry-After` never makes a wait shorter than the normal
|
|
71
|
+
* backoff.
|
|
67
72
|
*/
|
|
68
73
|
export declare const MAX_RETRY_AFTER_MS = 30000;
|
|
69
74
|
/**
|
|
@@ -77,11 +82,26 @@ export declare const MAX_RETRY_AFTER_MS = 30000;
|
|
|
77
82
|
* read `"1.5"` as a date in 2001 and retry at once.
|
|
78
83
|
*/
|
|
79
84
|
export declare function parseRetryAfter(header: string | string[] | undefined, now?: number): number | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* True for a PegelNetworkError caused by a reset or aborted connection, which the
|
|
87
|
+
* engine retries — whichever transport raised it (a Node error, fetch's TypeError with
|
|
88
|
+
* an undici cause). A refused connection, a DNS failure or a timeout is not transient
|
|
89
|
+
* in that sense and is not retried.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isTransientNetworkError(err: unknown): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Check a base URL against the library's rules (baseUrlProblem: no whitespace or
|
|
94
|
+
* control characters, an absolute http(s) URL, no query or fragment) and return it
|
|
95
|
+
* without trailing slashes. Throws PegelValidationError `Invalid baseUrl: …`: a
|
|
96
|
+
* configuration mistake, not a PegelNetworkError. The RequestEngine constructor
|
|
97
|
+
* calls it on the raw value, so a custom transport never sees a bad base URL; the
|
|
98
|
+
* default transport still re-checks the scheme on every hop.
|
|
99
|
+
*/
|
|
100
|
+
export declare function validateBaseUrl(raw: string): string;
|
|
80
101
|
export declare class RequestEngine {
|
|
81
|
-
private
|
|
102
|
+
#private;
|
|
82
103
|
private readonly transport;
|
|
83
104
|
private readonly userAgent;
|
|
84
|
-
private readonly extraHeaders;
|
|
85
105
|
private readonly timeoutMs;
|
|
86
106
|
private readonly maxRetries;
|
|
87
107
|
private readonly retryDelayMs;
|
|
@@ -92,7 +112,7 @@ export declare class RequestEngine {
|
|
|
92
112
|
/**
|
|
93
113
|
* Build a fully-qualified URL from a path and optional query parameters.
|
|
94
114
|
*
|
|
95
|
-
* Throws a
|
|
115
|
+
* Throws a PegelValidationError for a path with a "." or ".." segment. The resource methods
|
|
96
116
|
* put ids into the path with `encodeURIComponent`, which leaves those two
|
|
97
117
|
* unchanged, and URL parsing then resolves them: `currentMeasurement("BONN", "..")`
|
|
98
118
|
* would request `/stations/currentmeasurement.json` (a station of that name).
|
|
@@ -100,6 +120,25 @@ export declare class RequestEngine {
|
|
|
100
120
|
* encodeURIComponent turns their "%" into "%25".)
|
|
101
121
|
*/
|
|
102
122
|
buildUrl(path: string, query?: QueryParams): string;
|
|
123
|
+
/**
|
|
124
|
+
* `text` without the base URL's credentials: server text (an error body that echoes
|
|
125
|
+
* the request URL) and transport text (fetch's "Failed to fetch <url>") can carry them.
|
|
126
|
+
*/
|
|
127
|
+
private scrub;
|
|
128
|
+
/**
|
|
129
|
+
* A transport failure as the `cause` of the error the engine raises: the original
|
|
130
|
+
* when its text carries no credentials, otherwise a copy with them scrubbed (message,
|
|
131
|
+
* `code` and the cause chain kept), so logging the error with its causes can't reveal
|
|
132
|
+
* the base URL's password.
|
|
133
|
+
*/
|
|
134
|
+
private scrubCause;
|
|
135
|
+
/**
|
|
136
|
+
* Call the transport under the overall deadline (`timeoutMs`): the request gets an
|
|
137
|
+
* AbortSignal that fires at the deadline, and the call rejects then whether the
|
|
138
|
+
* transport stops or not — a custom transport (fetch, a node:http wrapper) that
|
|
139
|
+
* ignores `timeoutMs` can't hang the caller. A synchronous throw becomes a rejection.
|
|
140
|
+
*/
|
|
141
|
+
private callTransport;
|
|
103
142
|
/** Perform a request with Accept negotiation and transient-error retries. */
|
|
104
143
|
request(method: string, path: string, options?: {
|
|
105
144
|
query?: QueryParams;
|
|
@@ -109,4 +148,3 @@ export declare class RequestEngine {
|
|
|
109
148
|
getJson<T>(path: string, query?: QueryParams): Promise<T>;
|
|
110
149
|
private toApiError;
|
|
111
150
|
}
|
|
112
|
-
//# sourceMappingURL=engine.d.ts.map
|