@lde/distribution-probe 0.1.9 → 0.1.11

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
@@ -32,4 +32,6 @@ Sends `HEAD` with `Accept: <distribution.mimeType>` and `Accept-Encoding: identi
32
32
 
33
33
  ### Network errors
34
34
 
35
- Any thrown exception from `fetch` (DNS, connection refused, TLS, timeout after the configured `timeout` – default 5 000 ms) is caught and returned as a `NetworkError` with the original message.
35
+ A thrown exception from `fetch` (DNS failure, connection refused, socket reset, TLS error, timeout after the configured `timeoutMs` – default 5 000 ms) is a connection-level failure. The probe retries these up to `retries` times (default 2) with a short backoff before giving up and returning a `NetworkError`. This turns a transient transport blip into a reliable single measurement without looking backward across checks. A genuine outage still resolves to a `NetworkError` on the current check – every attempt fails – but note each attempt gets its own `timeoutMs`, so an endpoint that fails only by timing out takes up to `(retries + 1) × timeoutMs` (plus backoff) to be reported down. HTTP error responses (4xx/5xx) and content-validation failures are real ‘down’ states and are **never** retried.
36
+
37
+ `NetworkError.message` includes the underlying `error.cause` (e.g. `ECONNRESET`, `UND_ERR_SOCKET “other side closed”`) when Node wraps one, so observations record what actually failed rather than a bare ‘fetch failed’.
package/dist/probe.d.ts CHANGED
@@ -17,6 +17,17 @@ export interface ProbeOptions {
17
17
  * distributions. Defaults to `SELECT * { ?s ?p ?o } LIMIT 1`.
18
18
  */
19
19
  sparqlQuery?: string;
20
+ /**
21
+ * How many times to retry a connection-level failure (DNS, connection
22
+ * refused, socket reset, TLS error, timeout) before returning a
23
+ * {@link NetworkError}. Only transport errors are retried within the same
24
+ * check, so a transient blip does not flip an otherwise healthy distribution
25
+ * to ‘unavailable’; HTTP error responses and content-validation failures are
26
+ * genuine ‘down’ states and are never retried. Set to `0` to disable.
27
+ * Defaults to `2`. A non-integer or otherwise invalid value falls back to
28
+ * the default; negative values are clamped to `0`.
29
+ */
30
+ retries?: number;
20
31
  }
21
32
  /**
22
33
  * Result of a network error during probing.
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAInE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAKD;;GAEG;AACH,qBAAa,YAAY;aAEL,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;aACf,cAAc,EAAE,MAAM;gBAFtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM;CAEzC;AAED;;GAEG;AACH,uBAAe,WAAW;aAUN,GAAG,EAAE,MAAM;IAT7B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAQ;IACjD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAgB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,SAAgB,QAAQ,EAAE,MAAM,EAAE,CAAM;IACxC,SAAgB,cAAc,EAAE,MAAM,CAAC;gBAGrB,GAAG,EAAE,MAAM,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;IAa9B,SAAS,IAAI,OAAO;CAO5B;AAMD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD;;;;;OAKG;IACH,SAAgB,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;gBAGtD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,oBAAoB,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChD,aAAa,GAAE,MAAM,GAAG,IAAW;IAS5B,SAAS,IAAI,OAAO;CAQ9B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,WAAW;IAClD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAGhD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;CAQtC;AAED,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,mBAAmB,GACnB,YAAY,CAAC;AAIjB;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CACzB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,eAAe,CAAC,CAkC1B"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAInE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AASD;;GAEG;AACH,qBAAa,YAAY;aAEL,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;aACf,cAAc,EAAE,MAAM;gBAFtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM;CAEzC;AAED;;GAEG;AACH,uBAAe,WAAW;aAUN,GAAG,EAAE,MAAM;IAT7B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAQ;IACjD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAgB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,SAAgB,QAAQ,EAAE,MAAM,EAAE,CAAM;IACxC,SAAgB,cAAc,EAAE,MAAM,CAAC;gBAGrB,GAAG,EAAE,MAAM,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;IAa9B,SAAS,IAAI,OAAO;CAO5B;AAMD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD;;;;;OAKG;IACH,SAAgB,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;gBAGtD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,oBAAoB,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChD,aAAa,GAAE,MAAM,GAAG,IAAW;IAS5B,SAAS,IAAI,OAAO;CAQ9B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,WAAW;IAClD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAGhD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;CAQtC;AAED,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,mBAAmB,GACnB,YAAY,CAAC;AAIjB;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CACzB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,eAAe,CAAC,CAqD1B"}
package/dist/probe.js CHANGED
@@ -3,6 +3,9 @@ import { rdfParser } from 'rdf-parse';
3
3
  import { Readable } from 'node:stream';
4
4
  const DEFAULT_SPARQL_QUERY = 'SELECT * { ?s ?p ?o } LIMIT 1';
5
5
  const DEFAULT_TIMEOUT_MS = 5000;
6
+ const DEFAULT_RETRIES = 2;
7
+ /** Base backoff between retries; the nth retry waits `n × base`. */
8
+ const RETRY_BACKOFF_MS = 250;
6
9
  /**
7
10
  * Result of a network error during probing.
8
11
  */
@@ -100,23 +103,75 @@ export async function probe(distribution, options) {
100
103
  const [authUrl, authHeaders] = distribution.accessUrl !== undefined
101
104
  ? extractUrlCredentials(distribution.accessUrl, resolved.headers)
102
105
  : [new URL(url), new Headers(resolved.headers)];
103
- const start = performance.now();
104
- try {
105
- if (distribution.isSparql()) {
106
- return await probeSparqlEndpoint(authUrl.toString(), distribution, resolved, authHeaders, start);
106
+ // Retry only connection-level failures (a thrown `fetch`): HTTP error
107
+ // responses and content-validation failures are returned as result objects,
108
+ // never thrown, so they exit the loop on the first attempt and are not
109
+ // retried. A genuine outage still resolves to a NetworkError – every attempt
110
+ // fails – but note each attempt gets its own `timeoutMs`, so an endpoint that
111
+ // fails only by timing out takes up to (retries + 1) × timeoutMs (plus
112
+ // backoff) to be reported down.
113
+ const overallStart = performance.now();
114
+ let lastError;
115
+ for (let attempt = 0; attempt <= resolved.retries; attempt++) {
116
+ if (attempt > 0) {
117
+ await delay(RETRY_BACKOFF_MS * attempt);
118
+ }
119
+ const start = performance.now();
120
+ try {
121
+ if (distribution.isSparql()) {
122
+ return await probeSparqlEndpoint(authUrl.toString(), distribution, resolved, authHeaders, start);
123
+ }
124
+ return await probeDataDump(authUrl.toString(), distribution, resolved, authHeaders, start);
125
+ }
126
+ catch (error) {
127
+ lastError = error;
107
128
  }
108
- return await probeDataDump(authUrl.toString(), distribution, resolved, authHeaders, start);
109
129
  }
110
- catch (e) {
111
- const responseTimeMs = Math.round(performance.now() - start);
112
- return new NetworkError(url, e instanceof Error ? e.message : String(e), responseTimeMs);
130
+ // A successful probe reports its own attempt's latency (computed inside the
131
+ // probe functions); a NetworkError reports the total time spent failing,
132
+ // across every attempt and backoff, so observations do not understate the
133
+ // real cost of a down endpoint.
134
+ return new NetworkError(url, describeNetworkError(lastError), Math.round(performance.now() - overallStart));
135
+ }
136
+ function delay(milliseconds) {
137
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
138
+ }
139
+ /**
140
+ * Describe a thrown fetch error for a {@link NetworkError} message. undici wraps
141
+ * * the real reason (`ECONNRESET`, `UND_ERR_SOCKET “other side closed”`, TLS
142
+ * errors, …) in `error.cause`, while `error.message` is usually a bare
143
+ * ‘fetch failed’. Including the cause’s code and message preserves the
144
+ * diagnostic detail that would otherwise be discarded.
145
+ */
146
+ function describeNetworkError(error) {
147
+ if (!(error instanceof Error)) {
148
+ return String(error);
149
+ }
150
+ const { cause } = error;
151
+ if (cause === undefined || cause === null) {
152
+ return error.message;
113
153
  }
154
+ const detail = cause instanceof Error
155
+ ? [cause.code, cause.message]
156
+ .filter(Boolean)
157
+ .join(': ')
158
+ : String(cause);
159
+ return detail && detail !== error.message
160
+ ? `${error.message} (${detail})`
161
+ : error.message;
114
162
  }
115
163
  function resolveOptions(options) {
164
+ const retries = options?.retries;
116
165
  return {
117
166
  timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
118
167
  headers: options?.headers ?? new Headers(),
119
168
  sparqlQuery: options?.sparqlQuery ?? DEFAULT_SPARQL_QUERY,
169
+ // Guard the loop bound: a non-integer (NaN, Infinity, fractional) would
170
+ // otherwise either skip the loop entirely or never terminate. Negatives
171
+ // clamp to 0 (retries disabled).
172
+ retries: retries === undefined || !Number.isInteger(retries)
173
+ ? DEFAULT_RETRIES
174
+ : Math.max(0, retries),
120
175
  };
121
176
  }
122
177
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/distribution-probe",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/distribution-probe"
@@ -24,7 +24,7 @@
24
24
  "!**/*.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
- "@lde/dataset": "0.7.6",
27
+ "@lde/dataset": "0.7.7",
28
28
  "rdf-parse": "^5.0.0",
29
29
  "tslib": "^2.3.0"
30
30
  }