@lde/distribution-probe 0.3.0 → 0.3.1

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/dist/probe.d.ts CHANGED
@@ -34,7 +34,7 @@ export interface ProbeOptions {
34
34
  * carries at least one triple. When `false` (the default) a data dump is only
35
35
  * checked for reachability (a `HEAD`, with a body-less `GET` fallback if `HEAD`
36
36
  * is unsupported) and its body is never read. When `true`, every declared-RDF
37
- * dump — regardless of size — is fetched and validated; non-RDF and
37
+ * dump – regardless of size – is fetched and validated; non-RDF and
38
38
  * undeclared-type distributions are still reachability-only. Validation is
39
39
  * opt-in because reading a body forces a slow, generate-on-the-fly endpoint to
40
40
  * start producing its export, which a `HEAD` does not.
@@ -47,10 +47,20 @@ export interface ProbeOptions {
47
47
  * the distribution is reported reachable but unvalidated (no `failureReason`),
48
48
  * never failed. This bounds the extra latency content validation adds on slow,
49
49
  * generate-on-the-fly endpoints. Clamped to {@link timeoutMs} (a longer budget
50
- * is meaningless — the request times out first). Defaults to
50
+ * is meaningless – the request times out first). Defaults to
51
51
  * `min(timeoutMs, 2000)`.
52
52
  */
53
53
  rdfValidationBudgetMs?: number;
54
+ /**
55
+ * Replace remote JSON-LD contexts when {@link validateRdfContent} parses a
56
+ * JSON-LD body. Called with the URL of each remote `@context`; return the
57
+ * parsed document to use instead (shaped like what the URL serves, so
58
+ * `{ '@context': { … } }`), or `undefined` to fetch it as usual. A matching
59
+ * function rather than a fixed list, because one context is often served
60
+ * from several URLs. A context answered here never makes the result
61
+ * inconclusive for a slow or unreachable context host.
62
+ */
63
+ replaceJsonLdContext?: (url: string) => object | undefined;
54
64
  }
55
65
  /**
56
66
  * Options for {@link probeMany}: the per-probe {@link ProbeOptions} plus the
@@ -146,7 +156,7 @@ export declare function probe(distribution: Distribution, options?: ProbeOptions
146
156
  *
147
157
  * The per-host cap keeps the batch a polite client. Distributions sharing a host
148
158
  * (by {@link Distribution.accessUrl}) contend for the same budget, so no single
149
- * server is hit by the full global pool at once — the burst that trips a rate
159
+ * server is hit by the full global pool at once – the burst that trips a rate
150
160
  * limiter (HTTP 429). When the next queued probe’s host is saturated it is
151
161
  * skipped in favour of a later probe on a different host, so one busy host never
152
162
  * idles the global pool (no head-of-line blocking).
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAKA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAKnE;;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;IACjB;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;;;;OASG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD;AAkCD;;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;AAqBD;;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,GACzB,iBAAiB,GAAG,mBAAmB,GAAG,YAAY,CAAC;AAIzD;;;;;;;;;GASG;AACH,wBAAsB,KAAK,CACzB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,eAAe,CAAC,CAqD1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAC7B,aAAa,EAAE,SAAS,YAAY,EAAE,EACtC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,EAAE,CAAC,CAsC5B"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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"}
package/dist/probe.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { hostKeyOf, mapHostLimited, positiveIntOrDefault, } from '@lde/host-limiter';
2
2
  import { compressionMediaTypes } from '@lde/dataset';
3
3
  import { rdfParser } from 'rdf-parse';
4
+ import { FetchDocumentLoader, } from 'jsonld-context-parser';
4
5
  import { Readable } from 'node:stream';
5
6
  import { createGunzip } from 'node:zlib';
6
7
  const DEFAULT_SPARQL_QUERY = 'SELECT * { ?s ?p ?o } LIMIT 1';
@@ -22,7 +23,7 @@ const VALIDATION_TIMED_OUT = Symbol('rdf-validation-timed-out');
22
23
  * releases the connection. Reachability needs only that the endpoint answered
23
24
  * with a success status and produced bytes; a large dump must never be
24
25
  * downloaded in full within the probe's timeout budget. 256 KiB comfortably
25
- * surfaces the first RDF triple — the signal {@link validateBody} needs — while
26
+ * surfaces the first RDF triple – the signal {@link validateBody} needs – while
26
27
  * bounding the read regardless of the dump's true size, chunked transfer, or
27
28
  * compression. Applied to both the raw read and, for a gzip body, the inflated
28
29
  * output.
@@ -180,7 +181,7 @@ export async function probe(distribution, options) {
180
181
  *
181
182
  * The per-host cap keeps the batch a polite client. Distributions sharing a host
182
183
  * (by {@link Distribution.accessUrl}) contend for the same budget, so no single
183
- * server is hit by the full global pool at once — the burst that trips a rate
184
+ * server is hit by the full global pool at once – the burst that trips a rate
184
185
  * limiter (HTTP 429). When the next queued probe’s host is saturated it is
185
186
  * skipped in favour of a later probe on a different host, so one busy host never
186
187
  * idles the global pool (no head-of-line blocking).
@@ -252,6 +253,7 @@ function resolveOptions(options) {
252
253
  validateRdfContent: options?.validateRdfContent ?? false,
253
254
  rdfValidationBudgetMs: options?.rdfValidationBudgetMs ??
254
255
  Math.min(options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, DEFAULT_RDF_VALIDATION_BUDGET_MS),
256
+ replaceJsonLdContext: options?.replaceJsonLdContext,
255
257
  };
256
258
  }
257
259
  /**
@@ -396,8 +398,8 @@ async function probeDataDump(url, distribution, options, authHeaders, start) {
396
398
  // Express a preference for the declared media type, but accept anything as a
397
399
  // fallback. Servers that implement RFC 9110 §12.5.1 content negotiation will
398
400
  // pick the declared type (preserving our ability to detect real Content-Type
399
- // mismatches). Servers that reject any non-*/* Accept with 406 — notably
400
- // Dataverse's /api/access/datafile/ endpoint (IQSS/dataverse#12410) — fall
401
+ // mismatches). Servers that reject any non-*/* Accept with 406 – notably
402
+ // Dataverse's /api/access/datafile/ endpoint (IQSS/dataverse#12410) – fall
401
403
  // back to */* and return the file unchanged.
402
404
  const headers = new Headers({
403
405
  Accept: distribution.mimeType
@@ -418,7 +420,7 @@ async function probeDataDump(url, distribution, options, authHeaders, start) {
418
420
  });
419
421
  // Validate body content only when asked to and the distribution declares an
420
422
  // RDF media type; otherwise the probe is reachability-only and never reads a
421
- // body — which keeps it from forcing a slow, generate-on-the-fly endpoint to
423
+ // body – which keeps it from forcing a slow, generate-on-the-fly endpoint to
422
424
  // start producing its export.
423
425
  if (options.validateRdfContent &&
424
426
  isDeclaredRdf(distribution) &&
@@ -454,8 +456,8 @@ function finalizeDataDump(url, distribution, response, start, failureReason) {
454
456
  /**
455
457
  * GET the dump and validate that its body carries a triple, but only for as long
456
458
  * as the validation budget allows. Reachability is already settled by the prior
457
- * HEAD, so any shortfall — a budget that elapses before a triple, a read error,
458
- * a GET that cannot start — yields a `null` failureReason (reachable,
459
+ * HEAD, so any shortfall – a budget that elapses before a triple, a read error,
460
+ * a GET that cannot start – yields a `null` failureReason (reachable,
459
461
  * unvalidated), never a failure. Returns the response to draw metadata from
460
462
  * (the GET, or the HEAD when the GET could not start) alongside that reason.
461
463
  */
@@ -489,7 +491,7 @@ async function validateDumpBody(url, headers, options, headResponse) {
489
491
  const { text, truncated, corrupt } = await decodeProbeBody(bounded);
490
492
  return corrupt
491
493
  ? 'Distribution is not valid gzip'
492
- : await validateBody(text, getResponse.headers.get('Content-Type'), url, budgetController.signal, truncated);
494
+ : await validateBody(text, getResponse.headers.get('Content-Type'), url, budgetController.signal, truncated, options.replaceJsonLdContext);
493
495
  })().catch(() => null);
494
496
  let budgetTimer;
495
497
  const budgetExpiry = new Promise((resolve) => {
@@ -513,7 +515,7 @@ async function validateDumpBody(url, headers, options, headResponse) {
513
515
  * Read at most `maxBytes` from a response body, then cancel the stream to free
514
516
  * the underlying connection. Returns the bytes read and whether the body was
515
517
  * longer than the cap (`truncated`), so the caller can tell a complete, small
516
- * body — whose emptiness or parse errors are meaningful — from a deliberately
518
+ * body – whose emptiness or parse errors are meaningful – from a deliberately
517
519
  * cut-off prefix of a large one, where only the presence of content is
518
520
  * conclusive. This is what keeps the probe from downloading a multi-hundred-MB
519
521
  * streamed dump in full just to confirm it is reachable.
@@ -527,7 +529,7 @@ async function readBoundedBody(response, maxBytes) {
527
529
  let total = 0;
528
530
  let truncated = false;
529
531
  // Breaking out of `for await` cancels the stream, which stops any further
530
- // download and releases the underlying connection — so a large dump is never
532
+ // download and releases the underlying connection – so a large dump is never
531
533
  // pulled in full once we have the prefix we need.
532
534
  for await (const chunk of stream) {
533
535
  chunks.push(chunk);
@@ -541,12 +543,12 @@ async function readBoundedBody(response, maxBytes) {
541
543
  }
542
544
  /**
543
545
  * Decode a bounded body to text for RDF validation, inflating it first when it
544
- * is a gzip stream that `fetch` did not transparently decompress — e.g. a `.gz`
546
+ * is a gzip stream that `fetch` did not transparently decompress – e.g. a `.gz`
545
547
  * data dump served as-is, or one labelled with a non-standard Content-Encoding
546
548
  * (`application/gzip`) that undici does not recognise as a content coding.
547
549
  * Detection is by the gzip magic on the delivered bytes, so a body that `fetch`
548
550
  * already inflated (a standard `Content-Encoding: gzip`) is passed through
549
- * untouched. A truncated gzip tail is expected — we only read a prefix — and
551
+ * untouched. A truncated gzip tail is expected – we only read a prefix – and
550
552
  * inflates cleanly up to the cut, so it is never mistaken for corruption.
551
553
  */
552
554
  async function decodeProbeBody(bounded) {
@@ -594,7 +596,7 @@ function gunzipPrefix(bytes, maxBytes, inputComplete) {
594
596
  let total = 0;
595
597
  // `resolve` and `destroy` are both idempotent, so the first outcome wins and
596
598
  // any later event (e.g. a premature-close error emitted by `destroy`) is a
597
- // harmless no-op — no `settled` guard needed.
599
+ // harmless no-op – no `settled` guard needed.
598
600
  function finish(outcome) {
599
601
  gunzip.destroy();
600
602
  resolve({ bytes: Buffer.concat(chunks), ...outcome });
@@ -612,8 +614,8 @@ function gunzipPrefix(bytes, maxBytes, inputComplete) {
612
614
  });
613
615
  }
614
616
  // The RDF serializations whose bodies we parse to confirm they carry triples. A
615
- // non-empty body in one of these formats that yields zero triples — an empty
616
- // graph such as a JSON-LD `{}`, an `<rdf:RDF/>`, or prefix-only Turtle — is a
617
+ // non-empty body in one of these formats that yields zero triples – an empty
618
+ // graph such as a JSON-LD `{}`, an `<rdf:RDF/>`, or prefix-only Turtle – is a
617
619
  // faulty distribution, not a usable one, so it must be caught here. Other
618
620
  // content types (CSV, HTML, …) are left untouched: the probe is not the place
619
621
  // to assert what a non-RDF body should contain.
@@ -630,20 +632,20 @@ const rdfContentTypes = [
630
632
  // The line/statement-oriented formats (N-Triples, N-Quads, Turtle, TriG, N3) and
631
633
  // SAX-based RDF/XML all yield their first triple from the opening chunk, but
632
634
  // JSON-LD is a single JSON value whose parser emits nothing until the whole
633
- // document closes — a truncated JSON-LD body parses to an ‘unclosed document’
635
+ // document closes – a truncated JSON-LD body parses to an ‘unclosed document’
634
636
  // error, never a triple. So a truncated body in one of these can only be
635
637
  // validated if it happened to fit the read cap in full; beyond that it is
636
638
  // inconclusive, and we must not download it in full to find out.
637
639
  const nonStreamableRdfContentTypes = ['application/ld+json'];
638
- async function validateBody(body, contentType, baseIRI, budgetSignal, truncated) {
640
+ async function validateBody(body, contentType, baseIRI, budgetSignal, truncated, replaceJsonLdContext) {
639
641
  if (body.length === 0) {
640
642
  // A complete, empty body is a faulty distribution; an empty *prefix* (a
641
643
  // truncated read that yielded no bytes, e.g. a corrupt gzip header) is
642
- // inconclusive — the endpoint answered, we just could not validate content.
644
+ // inconclusive – the endpoint answered, we just could not validate content.
643
645
  return truncated ? null : 'Distribution is empty';
644
646
  }
645
647
  // Media types are case-insensitive (RFC 9110 §8.3.1), so normalise before
646
- // matching the lower-case allow-list — a server sending `Application/LD+JSON`
648
+ // matching the lower-case allow-list – a server sending `Application/LD+JSON`
647
649
  // must still have its body validated.
648
650
  const serialization = contentType?.split(';')[0].trim().toLowerCase();
649
651
  if (!serialization || !rdfContentTypes.includes(serialization)) {
@@ -651,19 +653,19 @@ async function validateBody(body, contentType, baseIRI, budgetSignal, truncated)
651
653
  }
652
654
  if (truncated && nonStreamableRdfContentTypes.includes(serialization)) {
653
655
  // A bounded prefix of a non-streamable serialization (JSON-LD) can never
654
- // yield a triple, so skip the doomed parse and report it inconclusive — only
656
+ // yield a triple, so skip the doomed parse and report it inconclusive – only
655
657
  // a complete document, small enough to fit the read cap, can be validated.
656
658
  return null;
657
659
  }
658
- const outcome = await classifyRdfBody(body, serialization, baseIRI, budgetSignal, truncated);
660
+ const outcome = await classifyRdfBody(body, serialization, baseIRI, budgetSignal, truncated, replaceJsonLdContext);
659
661
  switch (outcome.type) {
660
662
  case 'empty':
661
663
  return 'Distribution contains no RDF triples';
662
664
  case 'parseError':
663
665
  return outcome.message;
664
666
  // 'hasTriples' proves content. 'inconclusive' means the parse timed out or a
665
- // remote JSON-LD @context could not be loaded — a third-party hiccup, not
666
- // evidence the distribution is faulty — so neither is reported as a failure.
667
+ // remote JSON-LD @context could not be loaded – a third-party hiccup, not
668
+ // evidence the distribution is faulty – so neither is reported as a failure.
667
669
  default:
668
670
  return null;
669
671
  }
@@ -680,18 +682,21 @@ async function validateBody(body, contentType, baseIRI, budgetSignal, truncated)
680
682
  * host’s failure. Sharing the caller’s single budget (rather than starting a
681
683
  * second, identical timer) keeps that timeout deterministic: it fires once, when
682
684
  * the budget elapses, instead of leaving an orphan timer to settle on its own
683
- * schedule. `baseIRI` resolves any relative IRIs in the document.
685
+ * schedule. `baseIRI` resolves any relative IRIs in the document. A context
686
+ * that `replaceJsonLdContext` answers is never fetched, so its host cannot
687
+ * stall the parse.
684
688
  *
685
689
  * When `truncated` is true the body is only a bounded prefix of a larger one, so
686
690
  * only finding a triple ('hasTriples') is conclusive: a parse error at the cut
687
691
  * or a clean end with no triple yet means we did not read far enough, not that
688
692
  * the distribution is empty or malformed, and is reported as 'inconclusive'.
689
693
  */
690
- function classifyRdfBody(body, contentType, baseIRI, budgetSignal, truncated) {
694
+ function classifyRdfBody(body, contentType, baseIRI, budgetSignal, truncated, replaceJsonLdContext) {
691
695
  return new Promise((resolve) => {
692
696
  const quads = rdfParser.parse(Readable.from([body]), {
693
697
  contentType,
694
698
  baseIRI,
699
+ ...jsonLdParseContext(replaceJsonLdContext, budgetSignal),
695
700
  });
696
701
  const onBudgetElapsed = () => settle({ type: 'inconclusive' });
697
702
  let settled = false;
@@ -724,6 +729,24 @@ function classifyRdfBody(body, contentType, baseIRI, budgetSignal, truncated) {
724
729
  }
725
730
  });
726
731
  }
732
+ /**
733
+ * `rdf-parse` context that answers replaced JSON-LD contexts locally and
734
+ * fetches the rest. rdf-parse only routes context requests through a document
735
+ * loader set under this Comunica context key. Empty without a replacement, so
736
+ * the parser keeps its own loader. Context fetches stop when `budgetSignal`
737
+ * aborts, so none outlive the probe.
738
+ */
739
+ function jsonLdParseContext(replaceJsonLdContext, budgetSignal) {
740
+ if (replaceJsonLdContext === undefined)
741
+ return {};
742
+ const fetchLoader = new FetchDocumentLoader((url, init) => fetch(url, { ...init, signal: budgetSignal }));
743
+ return {
744
+ '@comunica/actor-rdf-parse-jsonld:documentLoader': {
745
+ load: async (url) => replaceJsonLdContext(url) ??
746
+ fetchLoader.load(url),
747
+ },
748
+ };
749
+ }
727
750
  /**
728
751
  * Whether a parse error is the RDF parser failing to load a remote JSON-LD
729
752
  * `@context` (an unreachable or broken third-party context host) rather than a
@@ -739,8 +762,8 @@ function isRemoteContextError(error) {
739
762
  * The declared compressed form (e.g. `application/n-quads+gzip`) is the expected
740
763
  * answer for a `+gzip`/`+zip` distribution, since the body is a gzip/zip archive
741
764
  * served as-is. The bare media type (`application/n-quads`) is also accepted as a
742
- * lenient fallback — the RDF serialization is the same and only the compression
743
- * wrapper is absent — so a server that serves the uncompressed representation is
765
+ * lenient fallback – the RDF serialization is the same and only the compression
766
+ * wrapper is absent – so a server that serves the uncompressed representation is
744
767
  * not flagged. A different compression suffix or a different base serialization is
745
768
  * still a genuine mismatch. The registry strips the suffix into a separate compress
746
769
  * format on ingest, so comparing against {@link Distribution.mimeType} alone would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/distribution-probe",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/distribution-probe"
@@ -26,6 +26,7 @@
26
26
  "dependencies": {
27
27
  "@lde/dataset": "^0.8.0",
28
28
  "@lde/host-limiter": "^0.1.0",
29
+ "jsonld-context-parser": "^2.4.0",
29
30
  "rdf-parse": "^5.0.0",
30
31
  "tslib": "^2.3.0"
31
32
  }