keepset 0.3.0 → 0.4.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.
@@ -57,11 +57,11 @@ declare const CitationSchema: z.ZodObject<{
57
57
  type Citation = z.infer<typeof CitationSchema>;
58
58
  /** A relative path inside an archive or a catalogue: `/`-separated segments, none empty, `.` or `..`, so it can never climb out of the directory it is resolved against. */
59
59
  declare const RelativePathSchema: z.ZodString;
60
- /** Which file of an archive to read: a member path, matched against a member's whole path or its trailing segments (so `net.gml` finds `net/net.gml`), or the member whose name ends with an extension. Either must match exactly one member when the recipe runs; macOS resource-fork members (`__MACOSX/` and `._` files) never match. */
60
+ /** Which file of an archive to read: a member path, matched against a member's whole path or its trailing segments (so `net.gml` finds `net/net.gml`), or the member whose name ends with one of a list of extensions (a format can have several, such as Pajek's `.net` and `.paj`). Either must match exactly one member when the recipe runs; macOS resource-fork members (`__MACOSX/` and `._` files) never match. */
61
61
  declare const ArchiveMemberSchema: z.ZodUnion<readonly [z.ZodObject<{
62
62
  path: z.ZodString;
63
63
  }, z.core.$strict>, z.ZodObject<{
64
- extension: z.ZodString;
64
+ extensions: z.ZodArray<z.ZodString>;
65
65
  }, z.core.$strict>]>;
66
66
  type ArchiveMember = z.infer<typeof ArchiveMemberSchema>;
67
67
  /** How the downloaded bytes unpack into the one file the parser reads. */
@@ -74,21 +74,21 @@ declare const ArchiveSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
74
74
  member: z.ZodUnion<readonly [z.ZodObject<{
75
75
  path: z.ZodString;
76
76
  }, z.core.$strict>, z.ZodObject<{
77
- extension: z.ZodString;
77
+ extensions: z.ZodArray<z.ZodString>;
78
78
  }, z.core.$strict>]>;
79
79
  }, z.core.$strict>, z.ZodObject<{
80
80
  kind: z.ZodLiteral<"tar">;
81
81
  member: z.ZodUnion<readonly [z.ZodObject<{
82
82
  path: z.ZodString;
83
83
  }, z.core.$strict>, z.ZodObject<{
84
- extension: z.ZodString;
84
+ extensions: z.ZodArray<z.ZodString>;
85
85
  }, z.core.$strict>]>;
86
86
  }, z.core.$strict>, z.ZodObject<{
87
87
  kind: z.ZodLiteral<"tar.gz">;
88
88
  member: z.ZodUnion<readonly [z.ZodObject<{
89
89
  path: z.ZodString;
90
90
  }, z.core.$strict>, z.ZodObject<{
91
- extension: z.ZodString;
91
+ extensions: z.ZodArray<z.ZodString>;
92
92
  }, z.core.$strict>]>;
93
93
  }, z.core.$strict>], "kind">;
94
94
  type Archive = z.infer<typeof ArchiveSchema>;
@@ -143,21 +143,21 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
143
143
  member: z.ZodUnion<readonly [z.ZodObject<{
144
144
  path: z.ZodString;
145
145
  }, z.core.$strict>, z.ZodObject<{
146
- extension: z.ZodString;
146
+ extensions: z.ZodArray<z.ZodString>;
147
147
  }, z.core.$strict>]>;
148
148
  }, z.core.$strict>, z.ZodObject<{
149
149
  kind: z.ZodLiteral<"tar">;
150
150
  member: z.ZodUnion<readonly [z.ZodObject<{
151
151
  path: z.ZodString;
152
152
  }, z.core.$strict>, z.ZodObject<{
153
- extension: z.ZodString;
153
+ extensions: z.ZodArray<z.ZodString>;
154
154
  }, z.core.$strict>]>;
155
155
  }, z.core.$strict>, z.ZodObject<{
156
156
  kind: z.ZodLiteral<"tar.gz">;
157
157
  member: z.ZodUnion<readonly [z.ZodObject<{
158
158
  path: z.ZodString;
159
159
  }, z.core.$strict>, z.ZodObject<{
160
- extension: z.ZodString;
160
+ extensions: z.ZodArray<z.ZodString>;
161
161
  }, z.core.$strict>]>;
162
162
  }, z.core.$strict>], "kind">;
163
163
  parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -261,21 +261,21 @@ declare const EntrySchema: z.ZodObject<{
261
261
  member: z.ZodUnion<readonly [z.ZodObject<{
262
262
  path: z.ZodString;
263
263
  }, z.core.$strict>, z.ZodObject<{
264
- extension: z.ZodString;
264
+ extensions: z.ZodArray<z.ZodString>;
265
265
  }, z.core.$strict>]>;
266
266
  }, z.core.$strict>, z.ZodObject<{
267
267
  kind: z.ZodLiteral<"tar">;
268
268
  member: z.ZodUnion<readonly [z.ZodObject<{
269
269
  path: z.ZodString;
270
270
  }, z.core.$strict>, z.ZodObject<{
271
- extension: z.ZodString;
271
+ extensions: z.ZodArray<z.ZodString>;
272
272
  }, z.core.$strict>]>;
273
273
  }, z.core.$strict>, z.ZodObject<{
274
274
  kind: z.ZodLiteral<"tar.gz">;
275
275
  member: z.ZodUnion<readonly [z.ZodObject<{
276
276
  path: z.ZodString;
277
277
  }, z.core.$strict>, z.ZodObject<{
278
- extension: z.ZodString;
278
+ extensions: z.ZodArray<z.ZodString>;
279
279
  }, z.core.$strict>]>;
280
280
  }, z.core.$strict>], "kind">;
281
281
  parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -367,21 +367,21 @@ declare const EntrySchema: z.ZodObject<{
367
367
  member: {
368
368
  path: string;
369
369
  } | {
370
- extension: string;
370
+ extensions: string[];
371
371
  };
372
372
  } | {
373
373
  kind: "tar";
374
374
  member: {
375
375
  path: string;
376
376
  } | {
377
- extension: string;
377
+ extensions: string[];
378
378
  };
379
379
  } | {
380
380
  kind: "tar.gz";
381
381
  member: {
382
382
  path: string;
383
383
  } | {
384
- extension: string;
384
+ extensions: string[];
385
385
  };
386
386
  };
387
387
  parser: {
@@ -552,21 +552,21 @@ declare const CatalogueIndexSchema: z.ZodObject<{
552
552
  member: z.ZodUnion<readonly [z.ZodObject<{
553
553
  path: z.ZodString;
554
554
  }, z.core.$strict>, z.ZodObject<{
555
- extension: z.ZodString;
555
+ extensions: z.ZodArray<z.ZodString>;
556
556
  }, z.core.$strict>]>;
557
557
  }, z.core.$strict>, z.ZodObject<{
558
558
  kind: z.ZodLiteral<"tar">;
559
559
  member: z.ZodUnion<readonly [z.ZodObject<{
560
560
  path: z.ZodString;
561
561
  }, z.core.$strict>, z.ZodObject<{
562
- extension: z.ZodString;
562
+ extensions: z.ZodArray<z.ZodString>;
563
563
  }, z.core.$strict>]>;
564
564
  }, z.core.$strict>, z.ZodObject<{
565
565
  kind: z.ZodLiteral<"tar.gz">;
566
566
  member: z.ZodUnion<readonly [z.ZodObject<{
567
567
  path: z.ZodString;
568
568
  }, z.core.$strict>, z.ZodObject<{
569
- extension: z.ZodString;
569
+ extensions: z.ZodArray<z.ZodString>;
570
570
  }, z.core.$strict>]>;
571
571
  }, z.core.$strict>], "kind">;
572
572
  parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -658,21 +658,21 @@ declare const CatalogueIndexSchema: z.ZodObject<{
658
658
  member: {
659
659
  path: string;
660
660
  } | {
661
- extension: string;
661
+ extensions: string[];
662
662
  };
663
663
  } | {
664
664
  kind: "tar";
665
665
  member: {
666
666
  path: string;
667
667
  } | {
668
- extension: string;
668
+ extensions: string[];
669
669
  };
670
670
  } | {
671
671
  kind: "tar.gz";
672
672
  member: {
673
673
  path: string;
674
674
  } | {
675
- extension: string;
675
+ extensions: string[];
676
676
  };
677
677
  };
678
678
  parser: {
@@ -762,21 +762,21 @@ declare const CatalogueIndexSchema: z.ZodObject<{
762
762
  member: {
763
763
  path: string;
764
764
  } | {
765
- extension: string;
765
+ extensions: string[];
766
766
  };
767
767
  } | {
768
768
  kind: "tar";
769
769
  member: {
770
770
  path: string;
771
771
  } | {
772
- extension: string;
772
+ extensions: string[];
773
773
  };
774
774
  } | {
775
775
  kind: "tar.gz";
776
776
  member: {
777
777
  path: string;
778
778
  } | {
779
- extension: string;
779
+ extensions: string[];
780
780
  };
781
781
  };
782
782
  parser: {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as SnapshotSchema, A as SemVerSchema, B as Entry, C as Dataset, D as LineageSchema, E as Lineage, F as ArchiveSchema, G as LicenceSchema, H as HttpUrlSchema, I as Citation, J as RESERVED_META_KEYS, K as Parser, L as CitationSchema, M as Archive, N as ArchiveMember, O as Manifest, P as ArchiveMemberSchema, Q as Snapshot, R as CrosswalkReference, S as CatalogueIndexSchema, T as createCatalogue, U as IdSchema, V as EntrySchema, W as Licence, X as RecipeSchema, Y as Recipe, Z as RelativePathSchema, _ as GraphV1Schema, a as CategoryTargets, b as Catalogue, c as getCategory, d as listCategories, et as Source, f as GraphEdge, g as GraphV1, h as GraphNodeSchema, i as CategoryTarget, it as waybackUrl, j as parseManifest, k as ManifestSchema, l as getTarget, m as GraphNode, n as CategoryId, nt as WaybackTimestampSchema, o as CountedKey, p as GraphEdgeSchema, q as ParserSchema, r as CategoryIdSchema, rt as parseEntry, s as countTarget, t as Category, tt as SourceSchema, u as kindOf, v as JsonScalar, w as DatasetQuery, x as CatalogueIndex, y as JsonScalarSchema, z as CrosswalkReferenceSchema } from "./categories-BIZJUKUw.js";
1
+ import { $ as SnapshotSchema, A as SemVerSchema, B as Entry, C as Dataset, D as LineageSchema, E as Lineage, F as ArchiveSchema, G as LicenceSchema, H as HttpUrlSchema, I as Citation, J as RESERVED_META_KEYS, K as Parser, L as CitationSchema, M as Archive, N as ArchiveMember, O as Manifest, P as ArchiveMemberSchema, Q as Snapshot, R as CrosswalkReference, S as CatalogueIndexSchema, T as createCatalogue, U as IdSchema, V as EntrySchema, W as Licence, X as RecipeSchema, Y as Recipe, Z as RelativePathSchema, _ as GraphV1Schema, a as CategoryTargets, b as Catalogue, c as getCategory, d as listCategories, et as Source, f as GraphEdge, g as GraphV1, h as GraphNodeSchema, i as CategoryTarget, it as waybackUrl, j as parseManifest, k as ManifestSchema, l as getTarget, m as GraphNode, n as CategoryId, nt as WaybackTimestampSchema, o as CountedKey, p as GraphEdgeSchema, q as ParserSchema, r as CategoryIdSchema, rt as parseEntry, s as countTarget, t as Category, tt as SourceSchema, u as kindOf, v as JsonScalar, w as DatasetQuery, x as CatalogueIndex, y as JsonScalarSchema, z as CrosswalkReferenceSchema } from "./categories-CUf1VecC.js";
2
2
  import { Crosswalk } from "twindex";
3
3
  //#region src/errors.d.ts
4
4
  /** The root of this package's error taxonomy: every error keepset throws is a `KeepsetError`, so callers can catch the family with one check. */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as getCategory, B as DatasetNotFoundError, C as SnapshotSchema, D as waybackUrl, E as parseEntry, F as GraphNodeSchema, H as NativeParseError, I as GraphV1Schema, L as JsonScalarSchema, M as kindOf, N as listCategories, O as CategoryIdSchema, P as GraphEdgeSchema, R as ArchiveError, S as RelativePathSchema, T as WaybackTimestampSchema, U as RecipeError, V as KeepsetError, W as UnknownCategoryError, _ as IdSchema, a as createCatalogue, b as RESERVED_META_KEYS, c as ManifestSchema, d as ArchiveMemberSchema, f as ArchiveSchema, g as HttpUrlSchema, h as EntrySchema, i as CatalogueIndexSchema, j as getTarget, k as countTarget, l as SemVerSchema, m as CrosswalkReferenceSchema, n as getManifest, p as CitationSchema, r as listDatasets, s as LineageSchema, t as getDataset, u as parseManifest, v as LicenceSchema, w as SourceSchema, x as RecipeSchema, y as ParserSchema, z as CatalogueSyntaxError } from "./bundled--fOitQlP.js";
1
+ import { A as getCategory, B as DatasetNotFoundError, C as SnapshotSchema, D as waybackUrl, E as parseEntry, F as GraphNodeSchema, H as NativeParseError, I as GraphV1Schema, L as JsonScalarSchema, M as kindOf, N as listCategories, O as CategoryIdSchema, P as GraphEdgeSchema, R as ArchiveError, S as RelativePathSchema, T as WaybackTimestampSchema, U as RecipeError, V as KeepsetError, W as UnknownCategoryError, _ as IdSchema, a as createCatalogue, b as RESERVED_META_KEYS, c as ManifestSchema, d as ArchiveMemberSchema, f as ArchiveSchema, g as HttpUrlSchema, h as EntrySchema, i as CatalogueIndexSchema, j as getTarget, k as countTarget, l as SemVerSchema, m as CrosswalkReferenceSchema, n as getManifest, p as CitationSchema, r as listDatasets, s as LineageSchema, t as getDataset, u as parseManifest, v as LicenceSchema, w as SourceSchema, x as RecipeSchema, y as ParserSchema, z as CatalogueSyntaxError } from "./bundled-DN3sSHfu.js";
2
2
  //#region src/serialise.ts
3
3
  /**
4
4
  * Copies a JSON value with every object's keys in ascending code-unit order, the order the house JSON lint (`json/sort-keys`) requires of committed JSON. `Object.fromEntries` defines each key as an own data property, so a key such as `__proto__` stays a key.
@@ -11,12 +11,20 @@ function sortKeys(value) {
11
11
  return value;
12
12
  }
13
13
  /**
14
+ * Serialises any JSON value in the committed form: two-space indented, keys in ascending order, with a trailing newline.
15
+ * @param value - A JSON value.
16
+ * @returns The file text.
17
+ */
18
+ function canonicalJson(value) {
19
+ return `${JSON.stringify(sortKeys(value), void 0, 2)}\n`;
20
+ }
21
+ /**
14
22
  * Serialises a catalogue document (an entry, a manifest or a crosswalk file) as its file holds it: two-space indented JSON with keys in ascending order and a trailing newline. The form depends only on the document's content, so parsing a file written this way and serialising the result reproduces the file byte for byte.
15
23
  * @param doc - A parsed entry, manifest or crosswalk.
16
24
  * @returns The file text.
17
25
  */
18
26
  function serialise(doc) {
19
- return `${JSON.stringify(sortKeys(doc), void 0, 2)}\n`;
27
+ return canonicalJson(doc);
20
28
  }
21
29
  //#endregion
22
30
  export { ArchiveError, ArchiveMemberSchema, ArchiveSchema, CatalogueIndexSchema, CatalogueSyntaxError, CategoryIdSchema, CitationSchema, CrosswalkReferenceSchema, DatasetNotFoundError, EntrySchema, GraphEdgeSchema, GraphNodeSchema, GraphV1Schema, HttpUrlSchema, IdSchema, JsonScalarSchema, KeepsetError, LicenceSchema, LineageSchema, ManifestSchema, NativeParseError, ParserSchema, RESERVED_META_KEYS, RecipeError, RecipeSchema, RelativePathSchema, SemVerSchema, SnapshotSchema, SourceSchema, UnknownCategoryError, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getDataset, getManifest, getTarget, kindOf, listCategories, listDatasets, parseEntry, parseManifest, serialise, waybackUrl };
@@ -1,6 +1,6 @@
1
- import { B as Entry, C as Dataset, Q as Snapshot, et as Source, i as CategoryTarget } from "../categories-BIZJUKUw.js";
1
+ import { B as Entry, C as Dataset, Q as Snapshot, et as Source, i as CategoryTarget } from "../categories-CUf1VecC.js";
2
2
  //#region src/node/fetch-dataset.d.ts
3
- /** The network port a recipe downloads through: the platform `fetch`, or a stand-in under test. */
3
+ /** The network port a recipe downloads through: `downloadRaw` by default, which returns the bytes exactly as served, or a stand-in under test. */
4
4
  export type FetchPort = (url: string, init: {
5
5
  readonly signal?: AbortSignal;
6
6
  }) => Promise<Response>;
@@ -48,7 +48,7 @@ export declare function downloadUrl(source: Source): string;
48
48
  */
49
49
  export declare function fetchEntry(dataset: Dataset, options?: FetchOptions): Promise<FetchedDataset>;
50
50
  /**
51
- * Fetches a dataset of the bundled catalogue by id and resolves it to its category's target shape, so `fetchDataset("cora")` yields graph/v1 `{ meta, nodes, edges }` directly. Node-only: it downloads with the platform `fetch` and unpacks with Node's zlib.
51
+ * Fetches a dataset of the bundled catalogue by id and resolves it to its category's target shape, so `fetchDataset("cora")` yields graph/v1 `{ meta, nodes, edges }` directly. Node-only: it downloads with Node's http and https clients and unpacks with Node's zlib.
52
52
  * @param id - The entry id.
53
53
  * @param options - The network port, abort signal and clock.
54
54
  * @returns The resolved target with its provenance and any count warnings.
@@ -1,8 +1,10 @@
1
- import { H as NativeParseError, R as ArchiveError, U as RecipeError, V as KeepsetError, j as getTarget, k as countTarget, o as getOwn, t as getDataset } from "../bundled--fOitQlP.js";
1
+ import { H as NativeParseError, R as ArchiveError, U as RecipeError, V as KeepsetError, j as getTarget, k as countTarget, o as getOwn, t as getDataset } from "../bundled-DN3sSHfu.js";
2
2
  import { encode } from "twindex";
3
3
  import { createHash } from "node:crypto";
4
4
  import { gunzipSync } from "node:zlib";
5
5
  import { unzipSync } from "fflate";
6
+ import { request } from "node:http";
7
+ import { request as request$1 } from "node:https";
6
8
  //#region src/parsers/native.ts
7
9
  /**
8
10
  * Collects node ids in order of first appearance, so a parser that learns its nodes from its edges emits them deterministically.
@@ -346,8 +348,9 @@ function parseMatrixMarket(text) {
346
348
  }
347
349
  //#endregion
348
350
  //#region src/parsers/pajek.ts
349
- /** Section keywords, lower-cased, mapped to how their lines are read. `*Partition` and `*Vector` hold node attributes this version does not carry, so their lines are skipped, as are the `*Vertices` lines inside them. */
351
+ /** Section keywords, lower-cased, mapped to how their lines are read. `*Network` names the network and starts no section. `*Partition` and `*Vector` hold node attributes this version does not carry, so their lines are skipped, as are the `*Vertices` lines inside them. */
350
352
  const sections = /* @__PURE__ */ new Map([
353
+ ["*network", "network"],
351
354
  ["*vertices", "vertices"],
352
355
  ["*arcs", "pairs"],
353
356
  ["*edges", "pairs"],
@@ -358,6 +361,18 @@ const sections = /* @__PURE__ */ new Map([
358
361
  ["*vector", "attributes"]
359
362
  ]);
360
363
  /**
364
+ * Resolves a section keyword, which Pajek reads case-insensitively and accepts abbreviated (`*net` for `*Network`, `*vert` for `*Vertices`): an exact keyword first, otherwise the shortest keyword it abbreviates, so `*edge` is `*Edges` rather than `*Edgeslist`.
365
+ * @param keyword - The keyword as written.
366
+ * @returns How its section is read, or undefined for a keyword that abbreviates none.
367
+ */
368
+ function sectionOf(keyword) {
369
+ const name = keyword.toLowerCase();
370
+ const exact = sections.get(name);
371
+ if (exact !== void 0) return exact;
372
+ const [shortest] = [...sections.keys()].filter((candidate) => candidate.startsWith(name)).sort((left, right) => left.length - right.length);
373
+ return shortest === void 0 ? void 0 : sections.get(shortest);
374
+ }
375
+ /**
361
376
  * Reads a vertex number, which Pajek numbers from 1 to the declared vertex count.
362
377
  * @param token - The token.
363
378
  * @param count - The declared vertex count.
@@ -388,14 +403,13 @@ function parsePajek(text) {
388
403
  const tokens = content.split(/\s+/);
389
404
  const [keyword] = tokens;
390
405
  if (keyword?.startsWith("*") === true) {
391
- const name = keyword.toLowerCase();
392
- if (name === "*network") {
406
+ const next = sectionOf(keyword);
407
+ if (next === void 0) throw new NativeParseError("pajek", lineNumber, `unknown section ${keyword}`);
408
+ if (next === "network") {
393
409
  networks++;
394
410
  if (networks > 1) throw new NativeParseError("pajek", lineNumber, "a project file holding more than one network is not supported");
395
411
  continue;
396
412
  }
397
- const next = sections.get(name);
398
- if (next === void 0) throw new NativeParseError("pajek", lineNumber, `unknown section ${keyword}`);
399
413
  if (next === "vertices" && section !== "attributes") {
400
414
  if (count !== void 0) throw new NativeParseError("pajek", lineNumber, "a network declares *Vertices twice");
401
415
  const declared = tokens[1];
@@ -616,7 +630,7 @@ function isResourceFork(path) {
616
630
  return path.startsWith("__MACOSX/") || path.split("/").some((segment) => segment.startsWith("._"));
617
631
  }
618
632
  /**
619
- * Whether a member path matches a selector: a path selector matches the whole member path or its trailing segments, an extension selector the end of the member's name.
633
+ * Whether a member path matches a selector: a path selector matches the whole member path or its trailing segments, an extensions selector the end of the member's name, ignoring case.
620
634
  * @param path - The member path.
621
635
  * @param member - The recipe's selector.
622
636
  * @returns True on a match.
@@ -624,7 +638,7 @@ function isResourceFork(path) {
624
638
  function selects(path, member) {
625
639
  if (isResourceFork(path) || path.endsWith("/")) return false;
626
640
  if ("path" in member) return path === member.path || path.endsWith(`/${member.path}`);
627
- return path.toLowerCase().endsWith(member.extension.toLowerCase());
641
+ return member.extensions.some((extension) => path.toLowerCase().endsWith(extension.toLowerCase()));
628
642
  }
629
643
  /**
630
644
  * Picks the one member a selector matches.
@@ -637,7 +651,7 @@ function pick(paths, member) {
637
651
  const matched = paths.filter((path) => selects(path, member));
638
652
  const [only] = matched;
639
653
  if (matched.length !== 1 || only === void 0) {
640
- const wanted = "path" in member ? `member ${member.path}` : `one member ending ${member.extension}`;
654
+ const wanted = "path" in member ? `member ${member.path}` : `one member ending ${member.extensions.join(" or ")}`;
641
655
  throw new ArchiveError(`expected ${wanted}, found ${String(matched.length)} among: ${paths.join(", ")}`);
642
656
  }
643
657
  return only;
@@ -701,6 +715,82 @@ function unzip(bytes, filter) {
701
715
  }
702
716
  }
703
717
  //#endregion
718
+ //#region src/node/download.ts
719
+ /** The most redirects one download follows: the Fetch Standard's own limit (https://fetch.spec.whatwg.org/#http-redirect-fetch, step 5). */
720
+ const MAX_REDIRECTS = 20;
721
+ const REDIRECTS = new Set(Object.values({
722
+ movedPermanently: 301,
723
+ found: 302,
724
+ seeOther: 303,
725
+ temporaryRedirect: 307,
726
+ permanentRedirect: 308
727
+ }));
728
+ /** What the downloader says it is, so a server operator can tell where the traffic comes from. */
729
+ const USER_AGENT = "keepset (+https://github.com/ExaDev/keepset)";
730
+ /**
731
+ * Sends one GET and resolves with the response head, its body still unread.
732
+ * @param url - The URL.
733
+ * @param signal - Aborts the request.
734
+ * @returns The response.
735
+ */
736
+ async function get(url, signal) {
737
+ const send = url.protocol === "https:" ? request$1 : request;
738
+ return new Promise((resolve, reject) => {
739
+ const request = send(url, {
740
+ method: "GET",
741
+ headers: { "user-agent": USER_AGENT },
742
+ ...signal === void 0 ? {} : { signal }
743
+ }, resolve);
744
+ request.on("error", reject);
745
+ request.end();
746
+ });
747
+ }
748
+ /**
749
+ * Reads a response body to the end.
750
+ * @param response - The response.
751
+ * @returns The body's bytes.
752
+ */
753
+ async function readBody(response) {
754
+ const chunks = [];
755
+ for await (const chunk of response) {
756
+ if (!(chunk instanceof Uint8Array)) throw new KeepsetError("the response body yielded a chunk that is not bytes");
757
+ chunks.push(chunk);
758
+ }
759
+ return Buffer.concat(chunks);
760
+ }
761
+ /**
762
+ * Requests one URL, following its redirects by recursion.
763
+ * @param url - The URL to request.
764
+ * @param signal - Aborts the request.
765
+ * @param redirects - How many redirects led here.
766
+ * @returns The final response.
767
+ */
768
+ async function follow(url, signal, redirects) {
769
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new KeepsetError(`${url.href} is not an http or https URL`);
770
+ const response = await get(url, signal);
771
+ const { statusCode: status, statusMessage } = response;
772
+ if (status === void 0 || statusMessage === void 0) throw new KeepsetError(`${url.href} answered without a status line`);
773
+ if (!REDIRECTS.has(status)) return new Response(await readBody(response), {
774
+ status,
775
+ statusText: statusMessage
776
+ });
777
+ response.resume();
778
+ const { location } = response.headers;
779
+ if (location === void 0) throw new KeepsetError(`${url.href} answered ${String(status)} without a location`);
780
+ if (redirects === MAX_REDIRECTS) throw new KeepsetError(`${url.href} redirected more than ${String(MAX_REDIRECTS)} times`);
781
+ return follow(new URL(location, url), signal, redirects + 1);
782
+ }
783
+ /**
784
+ * Downloads a URL's bytes exactly as the server sends them, following redirects. Unlike the platform `fetch`, it never undoes a `Content-Encoding`: servers commonly label a `.gz` file `Content-Encoding: gzip`, and a recipe that unpacks gzip needs the file as published, which is also what its SHA-256 is recorded over. No `Accept-Encoding` is sent, so a server that compresses on the fly has no reason to.
785
+ * @param url - An http or https URL.
786
+ * @param init - The request options.
787
+ * @returns A Response holding the raw bytes, with the final status.
788
+ * @throws KeepsetError when a redirect has no location, leaves http and https, or passes the redirect limit.
789
+ */
790
+ async function downloadRaw(url, init) {
791
+ return follow(new URL(url), init.signal, 0);
792
+ }
793
+ //#endregion
704
794
  //#region src/node/fetch-dataset.ts
705
795
  /**
706
796
  * The URL a recipe downloads a source from. A dead source is fetched from its pinned snapshot in the Wayback Machine's `id_` form, which serves the archived bytes as they were captured rather than wrapped in the archive's page chrome.
@@ -750,7 +840,7 @@ function metaOf(entry) {
750
840
  */
751
841
  async function fetchEntry(dataset, options = {}) {
752
842
  const { entry, crosswalk } = dataset;
753
- const { fetch: fetchPort = globalThis.fetch, signal, now = () => /* @__PURE__ */ new Date() } = options;
843
+ const { fetch: fetchPort = downloadRaw, signal, now = () => /* @__PURE__ */ new Date() } = options;
754
844
  const { recipe } = entry;
755
845
  if (recipe.status === "not-fetchable") throw new RecipeError(entry.id, "recipe", `the entry has no fetchable recipe: ${recipe.reason}`);
756
846
  const source = entry.sources[recipe.source];
@@ -798,7 +888,7 @@ async function fetchEntry(dataset, options = {}) {
798
888
  };
799
889
  }
800
890
  /**
801
- * Fetches a dataset of the bundled catalogue by id and resolves it to its category's target shape, so `fetchDataset("cora")` yields graph/v1 `{ meta, nodes, edges }` directly. Node-only: it downloads with the platform `fetch` and unpacks with Node's zlib.
891
+ * Fetches a dataset of the bundled catalogue by id and resolves it to its category's target shape, so `fetchDataset("cora")` yields graph/v1 `{ meta, nodes, edges }` directly. Node-only: it downloads with Node's http and https clients and unpacks with Node's zlib.
802
892
  * @param id - The entry id.
803
893
  * @param options - The network port, abort signal and clock.
804
894
  * @returns The resolved target with its provenance and any count warnings.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "dependencies": {
6
6
  "fflate": "0.8.3",
7
7
  "twindex": "0.1.1",
@@ -79,6 +79,8 @@
79
79
  "lint:check": "eslint . --cache --max-warnings 0",
80
80
  "prepare": "husky",
81
81
  "prepublishOnly": "pnpm run lint:check && pnpm run build && pnpm run typecheck && pnpm run test && publint && attw --pack --profile esm-only",
82
+ "seed:check": "tsx scripts/seed/import-phd-catalogue.ts --pipeline scripts/seed/sources/pipeline-catalog.json --graphbox scripts/seed/sources/graphbox-catalog.json --check",
83
+ "seed:import": "tsx scripts/seed/import-phd-catalogue.ts",
82
84
  "test": "vitest run",
83
85
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json"
84
86
  },
@@ -1 +1 @@
1
- {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.1/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"category":{"enum":["graph/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extension":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"}},"required":["extension"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extension":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"}},"required":["extension"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extension":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"}},"required":["extension"],"type":"object"}]}},"required":["kind","member"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.3.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.1/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"category":{"enum":["graph/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.4.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
@@ -1 +1 @@
1
- {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.3.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.4.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}