aiiinotate 0.10.0 → 0.10.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/README.md CHANGED
@@ -180,7 +180,7 @@ npm run cli -- import <arguments>
180
180
 
181
181
  ## Test coverage
182
182
 
183
- aiiinotate is well tested: **over 90% test coverage** on all files !
183
+ aiiinotate is well tested: **~90% test coverage** on all files !
184
184
 
185
185
  ```
186
186
  ℹ ----------------------------------------------------------------------------------------
package/docs/cli.md CHANGED
@@ -64,17 +64,20 @@ aiiinotate migrate revert-all
64
64
 
65
65
  ## `import`: import data
66
66
 
67
+ The CLI can be used to import IIIF annotations or to index IIIF manifests in aiiinotate.
68
+
67
69
  ```bash
68
70
  # in prod
69
- aiiinotate import -i 2 -f <path/to/import/file.txt>
71
+ aiiinotate import <manifests|annotations> -i 2 -f <path/to/import/file.txt>
70
72
  ```
71
73
 
72
74
  Where:
75
+ - `<manifests|annotations>` is the kind of data to import: manifests or annotations
73
76
  - `-i` `--iiif-version` (`2|3`) is the IIIF Presentation API version of the data to import:
74
- - if `-i 3`, we import IIIF presentation 3.x annotations
75
- - if `-i 2`, we import IIIF presentation 2.x annotations
77
+ - if `-i 3`, we import IIIF presentation 3.x data
78
+ - if `-i 2`, we import IIIF presentation 2.x data
76
79
  - `-f --file` is a relative or absolute path the the import file. This import file:
77
- - contains a list of paths to annotation lists or annotation pages
80
+ - contains a list of paths to annotation lists, annotation pages or manifests
78
81
  - with 1 path per line
79
82
  - paths can be relative or absolute
80
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiiinotate",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "a fast IIIF-compliant annotation server",
5
5
  "main": "./cli/index.js",
6
6
  "type": "module",
@@ -345,7 +345,6 @@ class Annotations2 extends CollectionAbstract {
345
345
  const totalCount = await this.#memoizePaginationTotalCount(queryFilter);
346
346
 
347
347
  const skip = Math.max((page-1) * pageSize, 0); // number of queried items up until the previous page included.
348
-
349
348
  const cursor = await this.find(queryFilter, {}, true);
350
349
  const annotations = await cursor
351
350
  .sort({ "@id": 1 })
@@ -3,7 +3,7 @@ import fastifyPlugin from "fastify-plugin";
3
3
  import CollectionAbstract from "#data/collectionAbstract.js";
4
4
  import { getManifestShortId } from "#utils/iiif2Utils.js";
5
5
  import { formatInsertResponse } from "#utils/routeUtils.js";
6
- import { inspectObj, visibleLog, ajvCompile, memoize } from "#utils/utils.js";
6
+ import { inspectObj, visibleLog, ajvCompile, memoize, objectHasKey } from "#utils/utils.js";
7
7
  import { IIIF_PRESENTATION_2_CONTEXT } from "#utils/iiifUtils.js";
8
8
  import { PUBLIC_URL } from "#constants";
9
9
 
@@ -70,7 +70,11 @@ class Manifests2 extends CollectionAbstract {
70
70
  */
71
71
  #validateManifest(manifest) {
72
72
  if (!this.validatorManifest(manifest)) {
73
- throw this.insertError("validateManifest: invalid manifest structure", manifest);
73
+ let info = {};
74
+ if (objectHasKey(manifest, "@id")) {
75
+ info = { "@id": manifest["@id"] };
76
+ }
77
+ throw this.insertError("validateManifest: invalid manifest structure", info);
74
78
  }
75
79
  }
76
80
 
@@ -282,41 +282,67 @@ const recursiveSort = (x) => {
282
282
  }
283
283
 
284
284
  /**
285
- * Map cache with a timeout: after `timeout` ms have passed, clears the cache of the key `n`.
286
- * if `timeout` is negative, the cache will never be cleared of `n`.
285
+ * Map cache with a timeout and a max size:
286
+ * - after `timeout` ms have passed, clears the cache of the key `n`.
287
+ * - to avoid the cache to grow unbounded, we define a max size after which
288
+ * we delete the greatest items in the cache.
287
289
  *
288
290
  * NOTE: LIMITATIONS:
289
- * - `memoize` converts `fn` to an async function to work with both sync/async patterns, remember to await !
290
- * - `fn` should accept a single agument `n`
291
- * - `n` should be JSON-stringify-able: a primitive, an array, or an object, but not a function, a class or class instance
291
+ * - `memoize` converts `fn` to an async function to work with both
292
+ * sync/async patterns, remember to await !
293
+ * - the args applied to `fn` must be JSON-stringify-able: a primitive,
294
+ * an array, or an object, but not a function, a class or class instance
292
295
  *
293
296
  * adapted from: https://dev.to/codewithjohnson/the-power-of-a-simple-cache-system-with-javascript-map-3j01
294
297
  *
295
298
  * @param {Function} fn - the function whose result will be cached
296
299
  * @param {number} timeout - timeout in ms to clear the cache of a newly assigned value
300
+ * @param {number} maxSize - maximm number of elements in the cache
297
301
  * @returns {async Function} - a function that takes a value and caches its result.
298
302
  */
299
- const memoize = (fn, timeout = 2000) => {
303
+ const memoize = (fn, timeout = 2000, maxSize = 200) => {
304
+ if (timeout <= 0 || maxSize <= 0) {
305
+ throw new Error("memoize: 'timeout' and 'maxSize' must be greater than 0.")
306
+ }
300
307
  const cache = new Map();
301
- return async (n) => {
302
- // stringify `n` to `key`, the key to find in the cache.
303
- // since `map` is a key-value store, we need to stringify `n` to ensure it can be used as a key
304
- // to ensure consistency, we sort our objects/array before stringifying
305
- const key = JSON.stringify(recursiveSort(n));
308
+ return async (...args) => {
309
+ // if cache.size > cacheSize, remove the oldest items.
310
+ const extraCount = cache.size - maxSize;
311
+ if (extraCount > 0) {
312
+ // get the `extraCount` oldest items in the cache.
313
+ const deleteItems = [ ...cache.entries() ] // convert iterator to array
314
+ .map(([ k, { timestamp }]) => [ k, timestamp ])
315
+ .sort((a, b) => a[1] - b[1])
316
+ .slice(0, extraCount);
317
+
318
+ deleteItems.forEach(([ k, _ ]) => cache.delete(k));
319
+ }
320
+
321
+ // stringify `args` to `key`, the key to find in the cache.
322
+ // since `map` is a key-value store, we need to stringify `args`
323
+ // to ensure it can be used as a key to ensure consistency,
324
+ // we sort our objects/array before stringifying
325
+ const key = JSON.stringify(recursiveSort(args));
326
+
327
+ // fetch result promise, from cache or by executing `fn`.
328
+ // instead of caching the result, we cache the promise:
329
+ // all concurrent callers awaiting the same key get the same promise
330
+ // instance, so `fn` is only ever called once per unique key,
331
+ // regardless of how many callers arrive before it resolves.
332
+ let promise;
306
333
  if (cache.has(key)) {
307
- return cache.get(key);
334
+ promise = cache.get(key).promise;
308
335
  } else {
309
- const result = await fn(n); // apply the original arguments to `n`, not the sorted key.
310
- cache.set(key, result);
336
+ promise = fn(...args);
337
+ cache.set(key, { timestamp: Date.now(), promise: promise });
311
338
  // after `timeout` ms, clear the cache of `key`
312
- if (timeout > 0) {
313
- setTimeout(
314
- () => cache.delete(key),
315
- timeout
316
- )
317
- }
318
- return result;
339
+ setTimeout(
340
+ () => cache.delete(key),
341
+ timeout
342
+ )
319
343
  }
344
+
345
+ return await promise;
320
346
  }
321
347
  }
322
348