@jgengine/assets 0.6.0 → 0.8.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/dist/download.js CHANGED
@@ -26,11 +26,11 @@ function score(candidate) {
26
26
  value += 1;
27
27
  return value;
28
28
  }
29
- export async function resolveArchiveUrl(source) {
29
+ export async function resolveArchiveUrl(source, fetchImpl = fetch) {
30
30
  const download = source.download;
31
31
  if (!isScrapeDownload(download))
32
32
  return download.url;
33
- const response = await fetch(download.scrape, { redirect: "follow" });
33
+ const response = await fetchImpl(download.scrape, { redirect: "follow" });
34
34
  if (!response.ok)
35
35
  throw new Error(`scrape ${download.scrape} -> HTTP ${response.status}`);
36
36
  const html = await response.text();
@@ -40,12 +40,79 @@ export async function resolveArchiveUrl(source) {
40
40
  }
41
41
  return url;
42
42
  }
43
- export async function downloadArchive(url) {
44
- const response = await fetch(url, { redirect: "follow" });
43
+ export async function downloadArchive(url, fetchImpl = fetch) {
44
+ const response = await fetchImpl(url, { redirect: "follow" });
45
45
  if (!response.ok)
46
46
  throw new Error(`download ${url} -> HTTP ${response.status}`);
47
47
  return new Uint8Array(await response.arrayBuffer());
48
48
  }
49
+ /**
50
+ * Layout for the `--mirror` / `JGENGINE_ASSETS_MIRROR` base URL override: the
51
+ * archive for a pack is expected at `<baseUrl>/<provider>/<packId>.zip`, e.g.
52
+ * `https://my-mirror.example.com/kenney/kenney-nature.zip`.
53
+ */
54
+ export function mirrorOverrideUrl(baseUrl, source) {
55
+ const base = baseUrl.replace(/\/+$/, "");
56
+ return `${base}/${source.provider}/${source.id}.zip`;
57
+ }
58
+ async function verifyPinnedSha(source, archive) {
59
+ const download = source.download;
60
+ if (isScrapeDownload(download) || download.sha256 === undefined)
61
+ return;
62
+ const actual = await sha256Hex(archive);
63
+ if (actual !== download.sha256) {
64
+ throw new Error(`sha256 mismatch: expected ${download.sha256}, got ${actual}`);
65
+ }
66
+ }
67
+ /**
68
+ * Resolves and downloads a pack's archive, trying sources in order until one
69
+ * succeeds: (1) the mirror base override at `mirrorOverrideUrl`, (2) the
70
+ * primary provider path (`resolveArchiveUrl`: scrape or pinned URL), (3) the
71
+ * pack's own `mirror` URL. A pinned `sha256` is verified against whichever
72
+ * source supplied the bytes; a mismatch is treated as a failed attempt so the
73
+ * next source in the chain is tried. Throws with every attempted URL and its
74
+ * failure reason when all sources fail.
75
+ */
76
+ export async function downloadPackArchive(source, options = {}) {
77
+ const fetchImpl = options.fetchImpl ?? fetch;
78
+ const attempted = [];
79
+ const errors = [];
80
+ const tryUrl = async (url) => {
81
+ attempted.push(url);
82
+ try {
83
+ const archive = await downloadArchive(url, fetchImpl);
84
+ await verifyPinnedSha(source, archive);
85
+ return { archive, url, attempted };
86
+ }
87
+ catch (error) {
88
+ errors.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
89
+ return undefined;
90
+ }
91
+ };
92
+ if (options.mirrorBase !== undefined) {
93
+ const result = await tryUrl(mirrorOverrideUrl(options.mirrorBase, source));
94
+ if (result !== undefined)
95
+ return result;
96
+ }
97
+ try {
98
+ const primaryUrl = await resolveArchiveUrl(source, fetchImpl);
99
+ const result = await tryUrl(primaryUrl);
100
+ if (result !== undefined)
101
+ return result;
102
+ }
103
+ catch (error) {
104
+ const label = isScrapeDownload(source.download) ? source.download.scrape : source.download.url;
105
+ errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
106
+ attempted.push(label);
107
+ }
108
+ if (source.mirror !== undefined) {
109
+ const result = await tryUrl(source.mirror);
110
+ if (result !== undefined)
111
+ return result;
112
+ }
113
+ throw new Error(`failed to download ${source.id} from all ${attempted.length} source(s):\n` +
114
+ errors.map((line) => ` - ${line}`).join("\n"));
115
+ }
49
116
  export function extractGlbs(archive) {
50
117
  const entries = unzipSync(archive, {
51
118
  filter: (file) => /\.glb$/i.test(file.name),
@@ -19,6 +19,8 @@ export interface AssetSource {
19
19
  categories: readonly string[];
20
20
  download: AssetDownload;
21
21
  homepage?: string;
22
+ /** Direct archive URL tried as a last resort when the primary provider path fails; see `downloadPackArchive`. */
23
+ mirror?: string;
22
24
  }
23
25
  export interface IndexEntry {
24
26
  id: string;