@canopy-iiif/app 1.10.5 → 1.10.6

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.
@@ -174,9 +174,13 @@ async function build(options = {}) {
174
174
  } catch (_) {}
175
175
  if (!skipIiif && hasIiifSources && currentManifestIds.length) {
176
176
  try {
177
+ const featuredKeepIds = Array.isArray(CONFIG?.featured)
178
+ ? CONFIG.featured
179
+ : [];
177
180
  await iiif.cleanupIiifCache({
178
181
  allowedManifestIds: currentManifestIds,
179
182
  allowedCollectionIds: currentCollectionIds,
183
+ keepManifestIds: featuredKeepIds,
180
184
  });
181
185
  } catch (_) {}
182
186
  }
package/lib/build/iiif.js CHANGED
@@ -82,6 +82,7 @@ const IIIF_CACHE_INDEX_MANIFESTS = path.join(
82
82
  const DEFAULT_THUMBNAIL_SIZE = 400;
83
83
  const DEFAULT_CHUNK_SIZE = 10;
84
84
  const DEFAULT_FETCH_CONCURRENCY = 1;
85
+ const DEFAULT_FEATURED_FETCH_TIMEOUT_MS = 15000;
85
86
  const HERO_THUMBNAIL_SIZE = 800;
86
87
  const HERO_IMAGE_SIZES_ATTR = "(min-width: 1024px) 1280px, 100vw";
87
88
  const OG_IMAGE_WIDTH = 1200;
@@ -873,13 +874,27 @@ function extractCollectionEntries(collection) {
873
874
  }
874
875
 
875
876
  async function readJsonFromUri(uri, options = {log: false}) {
877
+ const opts = options && typeof options === "object" ? options : {};
878
+ const shouldLog = Boolean(opts.log);
879
+ const fetchSignal = opts.signal;
876
880
  try {
877
881
  if (/^https?:\/\//i.test(uri)) {
878
882
  if (typeof fetch !== "function") return null;
879
- const res = await fetch(uri, {
880
- headers: {Accept: "application/json"},
881
- }).catch(() => null);
882
- if (options && options.log) {
883
+ let res = null;
884
+ try {
885
+ const fetchOptions = {headers: {Accept: "application/json"}};
886
+ if (fetchSignal) fetchOptions.signal = fetchSignal;
887
+ res = await fetch(uri, fetchOptions);
888
+ } catch (error) {
889
+ if (shouldLog) {
890
+ try {
891
+ const code = error && error.name === "AbortError" ? "ABORT" : "ERR";
892
+ logLine(`⊘ ${String(uri)} → ${code}`, "red", {bright: true});
893
+ } catch (_) {}
894
+ }
895
+ return null;
896
+ }
897
+ if (shouldLog) {
883
898
  try {
884
899
  if (res && res.ok) {
885
900
  logLine(`↓ ${String(uri)} → ${res.status}`, "yellow", {
@@ -1235,14 +1250,53 @@ async function ensureFeaturedInCache(cfg) {
1235
1250
  if (!featured.length) return;
1236
1251
  const {size: thumbSize, unsafe: unsafeThumbs} =
1237
1252
  resolveThumbnailPreferences();
1253
+ const fetchTimeoutMs = resolvePositiveInteger(
1254
+ process.env.CANOPY_FEATURED_FETCH_TIMEOUT_MS,
1255
+ DEFAULT_FEATURED_FETCH_TIMEOUT_MS,
1256
+ {allowZero: true},
1257
+ );
1258
+ const canAbortFetches =
1259
+ typeof AbortController === "function" && Number(fetchTimeoutMs) > 0;
1238
1260
  for (const rawId of featured) {
1239
1261
  const id = normalizeIiifId(String(rawId || ""));
1240
1262
  if (!id) continue;
1241
1263
  let manifest = await loadCachedManifestById(id);
1242
1264
  if (!manifest) {
1243
- const m = await readJsonFromUri(id).catch(() => null);
1244
- if (!m) continue;
1245
- const upgraded = await upgradeIiifResource(m);
1265
+ let controller = null;
1266
+ let timeoutId = null;
1267
+ let fetched = null;
1268
+ try {
1269
+ if (canAbortFetches) {
1270
+ controller = new AbortController();
1271
+ timeoutId = setTimeout(
1272
+ () => controller.abort(),
1273
+ Math.max(1, fetchTimeoutMs),
1274
+ );
1275
+ }
1276
+ fetched = await readJsonFromUri(id, {
1277
+ log: true,
1278
+ signal: controller ? controller.signal : undefined,
1279
+ });
1280
+ } catch (_) {
1281
+ fetched = null;
1282
+ } finally {
1283
+ if (timeoutId) clearTimeout(timeoutId);
1284
+ }
1285
+ if (!fetched) {
1286
+ if (controller && controller.signal && controller.signal.aborted) {
1287
+ try {
1288
+ logLine(
1289
+ `[iiif] Featured manifest timed out after ${formatDurationMs(
1290
+ fetchTimeoutMs,
1291
+ )}: ${id}`,
1292
+ "red",
1293
+ {dim: true},
1294
+ );
1295
+ } catch (_) {}
1296
+ }
1297
+ continue;
1298
+ }
1299
+ const upgraded = await upgradeIiifResource(fetched);
1246
1300
  if (!upgraded || !upgraded.id) continue;
1247
1301
  manifest = (await saveCachedManifest(upgraded, id, "")) || upgraded;
1248
1302
  manifest = (await loadCachedManifestById(id)) || manifest;
@@ -1522,11 +1576,18 @@ async function cleanupIiifCache(options = {}) {
1522
1576
  const allowedCollectionIds = Array.isArray(options.allowedCollectionIds)
1523
1577
  ? options.allowedCollectionIds
1524
1578
  : [];
1579
+ const keepManifestIds = Array.isArray(options.keepManifestIds)
1580
+ ? options.keepManifestIds
1581
+ : [];
1525
1582
  const manifestSet = new Set(
1526
1583
  allowedManifestIds
1527
1584
  .map((id) => normalizeIiifId(String(id || "")))
1528
1585
  .filter(Boolean),
1529
1586
  );
1587
+ for (const keepId of keepManifestIds) {
1588
+ const normalized = normalizeIiifId(String(keepId || ""));
1589
+ if (normalized) manifestSet.add(normalized);
1590
+ }
1530
1591
  const collectionSet = new Set(
1531
1592
  allowedCollectionIds
1532
1593
  .map((id) => normalizeIiifId(String(id || "")))
@@ -4,6 +4,9 @@ const yaml = require('js-yaml');
4
4
  const { rootRelativeHref } = require('../common');
5
5
  const { resolveCanopyConfigPath } = require('../config-path');
6
6
 
7
+ const IIIF_MANIFESTS_DIR = path.resolve('.cache/iiif/manifests');
8
+ const SLUG_MEMO = new Map();
9
+
7
10
  function firstLabelString(label) {
8
11
  if (!label) return 'Untitled';
9
12
  if (typeof label === 'string') return label;
@@ -67,12 +70,11 @@ function readJson(p) {
67
70
 
68
71
  function findSlugByIdFromDiskSync(nid) {
69
72
  try {
70
- const dir = path.resolve('.cache/iiif/manifests');
71
- if (!fs.existsSync(dir)) return null;
72
- const names = fs.readdirSync(dir);
73
+ if (!fs.existsSync(IIIF_MANIFESTS_DIR)) return null;
74
+ const names = fs.readdirSync(IIIF_MANIFESTS_DIR);
73
75
  for (const name of names) {
74
76
  if (!name || !name.toLowerCase().endsWith('.json')) continue;
75
- const fp = path.join(dir, name);
77
+ const fp = path.join(IIIF_MANIFESTS_DIR, name);
76
78
  try {
77
79
  const obj = readJson(fp);
78
80
  const mid = normalizeIiifId(String((obj && (obj.id || obj['@id'])) || ''));
@@ -83,6 +85,75 @@ function findSlugByIdFromDiskSync(nid) {
83
85
  return null;
84
86
  }
85
87
 
88
+ function manifestPathFromSlug(slug) {
89
+ try {
90
+ const normalized = String(slug || '').trim();
91
+ if (!normalized || /[\\/]/.test(normalized)) return '';
92
+ return path.resolve(IIIF_MANIFESTS_DIR, `${normalized}.json`);
93
+ } catch (_) {
94
+ return '';
95
+ }
96
+ }
97
+
98
+ function manifestExistsForSlug(slug) {
99
+ const fp = manifestPathFromSlug(slug);
100
+ if (!fp) return false;
101
+ try {
102
+ return fs.existsSync(fp);
103
+ } catch (_) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ function readManifestBySlug(slug) {
109
+ const fp = manifestPathFromSlug(slug);
110
+ if (!fp) return null;
111
+ return readJson(fp);
112
+ }
113
+
114
+ function resolveSlugForFeaturedEntry(rawId, byId) {
115
+ if (rawId == null) return null;
116
+ const rawValue = String(rawId).trim();
117
+ if (!rawValue) return null;
118
+ if (!/^https?:\/\//i.test(rawValue) && manifestExistsForSlug(rawValue)) {
119
+ const entry = Array.isArray(byId)
120
+ ? byId.find((candidate) => candidate && candidate.slug === rawValue)
121
+ : null;
122
+ return {slug: rawValue, entry};
123
+ }
124
+ const normalizedId = normalizeIiifId(rawValue);
125
+ if (!normalizedId) return null;
126
+ const memoSlug = SLUG_MEMO.get(normalizedId);
127
+ if (memoSlug && manifestExistsForSlug(memoSlug)) {
128
+ const memoEntry = Array.isArray(byId)
129
+ ? byId.find((candidate) => candidate && candidate.slug === memoSlug)
130
+ : null;
131
+ return {slug: memoSlug, entry: memoEntry};
132
+ }
133
+ const entry = byId.find(
134
+ (candidate) =>
135
+ candidate && candidate.type === 'Manifest' && equalIiifId(candidate.id, normalizedId)
136
+ );
137
+ if (entry && entry.slug && manifestExistsForSlug(entry.slug)) {
138
+ SLUG_MEMO.set(normalizedId, entry.slug);
139
+ return {slug: entry.slug, entry};
140
+ }
141
+ const fallbackSlug = findSlugByIdFromDiskSync(normalizedId);
142
+ if (fallbackSlug && manifestExistsForSlug(fallbackSlug)) {
143
+ SLUG_MEMO.set(normalizedId, fallbackSlug);
144
+ const fallbackEntry = Array.isArray(byId)
145
+ ? byId.find(
146
+ (candidate) =>
147
+ candidate &&
148
+ candidate.type === 'Manifest' &&
149
+ (candidate.slug === fallbackSlug || equalIiifId(candidate.id, normalizedId))
150
+ )
151
+ : null;
152
+ return {slug: fallbackSlug, entry: fallbackEntry};
153
+ }
154
+ return null;
155
+ }
156
+
86
157
  function readFeaturedFromCacheSync() {
87
158
  try {
88
159
  const debug = !!process.env.CANOPY_DEBUG_FEATURED;
@@ -93,13 +164,13 @@ function readFeaturedFromCacheSync() {
93
164
  const byId = Array.isArray(idx && idx.byId) ? idx.byId : [];
94
165
  const out = [];
95
166
  for (const id of featured) {
96
- const nid = normalizeIiifId(id);
97
167
  if (debug) { try { console.log('[featured] id:', id); } catch (_) {} }
98
- const entry = byId.find((e) => e && e.type === 'Manifest' && equalIiifId(e.id, nid));
99
- const slug = entry && entry.slug ? String(entry.slug) : findSlugByIdFromDiskSync(nid);
168
+ const resolved = resolveSlugForFeaturedEntry(id, byId);
169
+ const slug = resolved && resolved.slug ? resolved.slug : '';
170
+ const entry = resolved && resolved.entry ? resolved.entry : null;
100
171
  if (debug) { try { console.log('[featured] - slug:', slug || '(none)'); } catch (_) {} }
101
172
  if (!slug) continue;
102
- const m = readJson(path.resolve('.cache/iiif/manifests', slug + '.json'));
173
+ const m = readManifestBySlug(slug);
103
174
  if (!m) continue;
104
175
  const rec = {
105
176
  title: firstLabelString(m && m.label),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.10.5",
3
+ "version": "1.10.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",