@prose-reader/metadata-fetcher 1.340.0 → 1.342.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.
@@ -1,10 +1,7 @@
1
- import { ResolvedMetadata } from '@prose-reader/archive-reader';
2
1
  import { MetadataMatch } from './match.ts';
3
2
  /**
4
- * What one provider had to say — the twin of `ResolvedArchiveSources`, with
5
- * the same contract: everything a provider contributed is also represented,
6
- * merged, in {@link FetchedMetadata.metadata}, so a wrong precedence opinion
7
- * stays revisable because the per-provider values never left the entity.
3
+ * What one provider had to say: every candidate, with the provider identity
4
+ * needed to present or revisit those results independently.
8
5
  */
9
6
  export type FetchedMetadataSource = {
10
7
  readonly provider: {
@@ -38,8 +35,9 @@ export type FailedMetadataProvider = {
38
35
  readonly status?: number;
39
36
  };
40
37
  /**
41
- * What remote catalogs know about a book — the equivalent of `ResolvedArchive`
42
- * and just as plain: structured-clone-able, persistable, cacheable.
38
+ * What remote catalogs found for a book, kept as ranked alternatives rather
39
+ * than consolidated into a potentially synthetic record. The entity is plain
40
+ * JSON: structured-clone-able, persistable and cacheable.
43
41
  */
44
42
  export type FetchedMetadata = {
45
43
  /**
@@ -47,19 +45,6 @@ export type FetchedMetadata = {
47
45
  * shape or meaning of existing fields changes incompatibly.
48
46
  */
49
47
  readonly version: number;
50
- /**
51
- * What the providers found, merged field-wise with the highest-scoring
52
- * accepted match winning. Empty when nothing matched confidently enough.
53
- *
54
- * **Only** the remote answer: the local metadata is deliberately not folded
55
- * in, so a consumer decides who wins.
56
- *
57
- * ```ts
58
- * // trust the book over the catalog, fill the gaps from the catalog
59
- * const metadata = mergeResolvedMetadata(resolved.metadata, fetched.metadata)
60
- * ```
61
- */
62
- readonly metadata: ResolvedMetadata;
63
48
  /**
64
49
  * Every match across every provider, ranked best-first — the list a "did you
65
50
  * mean?" picker renders. Ties keep the order the providers were declared in,
@@ -73,7 +58,8 @@ export type FetchedMetadata = {
73
58
  * leaves — a consumer refusing to cache a partial answer needs it.
74
59
  *
75
60
  * ```ts
76
- * const fetched = await fetchMetadata(resolved, { providers })
61
+ * const input = metadataInputFromResolvedArchive(resolved)
62
+ * const fetched = await fetchMetadata(input, { providers })
77
63
  *
78
64
  * // don't persist "we found nothing" when we simply couldn't ask
79
65
  * if (fetched.failedProviders.length === 0) cache.set(bookId, fetched)
@@ -3,7 +3,7 @@ import { ResolvedMetadata } from '@prose-reader/archive-reader';
3
3
  * The fields the matcher compares — only ever when **both** sides state one,
4
4
  * so what the query doesn't know cannot count against a candidate.
5
5
  */
6
- export type MetadataMatchField = "isbn" | "gtin" | "identifiers" | "title" | "contributors" | "series" | "publication.original.date" | "publication.original.publisher" | "publication.edition.date" | "publication.edition.publisher" | "languages" | "numberOfPages";
6
+ export type MetadataMatchField = "isbn" | "gtin" | "identifiers" | "title" | "authors" | "series" | "publisher" | "publishedYear" | "languages" | "numberOfPages";
7
7
  /**
8
8
  * One field comparison. The compared values sit next to the score so a match
9
9
  * is explainable to a user — "same title, different edition publisher" —
@@ -36,9 +36,8 @@ export type MetadataMatch = {
36
36
  readonly score: number;
37
37
  readonly signals: ReadonlyArray<MetadataMatchSignal>;
38
38
  /**
39
- * Reached `minScore`, and so contributed to the merged
40
- * `FetchedMetadata.metadata`. Rejected matches are kept and ranked, for a
41
- * consumer to offer for manual confirmation.
39
+ * Reached `minScore`. Rejected matches are kept and ranked, for a consumer
40
+ * to offer for manual confirmation.
42
41
  */
43
42
  readonly accepted: boolean;
44
43
  readonly metadata: ResolvedMetadata;
@@ -1,5 +1,5 @@
1
1
  import { ResolvedMetadata } from '@prose-reader/archive-reader';
2
- export type MetadataIdentifier = NonNullable<ResolvedMetadata["identifiers"]>[number];
2
+ import { FetchMetadataInput } from './fetchMetadataInput.ts';
3
3
  export type MetadataProviderContext = {
4
4
  /**
5
5
  * How many candidates the caller will keep. A hint for the provider's own
@@ -27,28 +27,24 @@ export type MetadataCandidate = {
27
27
  };
28
28
  /**
29
29
  * A pluggable metadata source. Implementing one is: pick a stable `id`, read
30
- * the terms you can search on out of the metadata, return normalized
30
+ * the terms you can search on out of the compact input, return normalized
31
31
  * candidates.
32
32
  *
33
- * Both sides of a lookup speak {@link ResolvedMetadata} — what the book said
34
- * going in, what the catalog says coming back. There is no separate query
35
- * shape to learn: the vocabulary is already sparse by contract (`field !==
36
- * undefined` is a reliable presence check) and already carries everything a
37
- * catalog could key on, down to the format-scoped corners. `metadataAuthors`
38
- * is exported for the one derivation that needs the role vocabulary.
33
+ * Inputs contain only fields understood for lookup and matching. Returned
34
+ * candidates remain rich {@link ResolvedMetadata} entities.
39
35
  *
40
36
  * ```ts
41
- * import { type MetadataProvider, metadataAuthors } from "@prose-reader/metadata-fetcher"
37
+ * import { type MetadataProvider } from "@prose-reader/metadata-fetcher"
42
38
  *
43
39
  * const myProvider: MetadataProvider = {
44
40
  * id: "myCatalog",
45
41
  * name: "My Catalog",
46
- * search: async (metadata, { limit, signal }) => {
47
- * if (metadata.title === undefined) return []
42
+ * search: async (input, { limit, signal }) => {
43
+ * if (input.title === undefined) return []
48
44
  *
49
45
  * const response = await fetch(
50
- * `https://example.com/search?q=${encodeURIComponent(metadata.title)}` +
51
- * `&author=${encodeURIComponent(metadataAuthors(metadata)[0] ?? "")}`,
46
+ * `https://example.com/search?q=${encodeURIComponent(input.title)}` +
47
+ * `&author=${encodeURIComponent(input.authors?.[0] ?? "")}`,
52
48
  * { signal },
53
49
  * )
54
50
  * const { results } = await response.json()
@@ -72,5 +68,5 @@ export type MetadataProvider = {
72
68
  * nothing to search on, or nothing found. Throwing is allowed — a failing
73
69
  * provider never fails the fetch, it lands in `failedProviders`.
74
70
  */
75
- readonly search: (metadata: ResolvedMetadata, context: MetadataProviderContext) => Promise<ReadonlyArray<MetadataCandidate>>;
71
+ readonly search: (input: FetchMetadataInput, context: MetadataProviderContext) => Promise<ReadonlyArray<MetadataCandidate>>;
76
72
  };
@@ -1,7 +1,7 @@
1
- import { ResolvedMetadata } from '@prose-reader/archive-reader';
1
+ import { FetchMetadataInput } from '../types/fetchMetadataInput.ts';
2
2
  /**
3
3
  * Whether a catalog has anything to go on, so a lookup with nothing to ask
4
4
  * about costs no round trip. Publication details, a language, or a page count
5
5
  * narrow a search but cannot start one.
6
6
  */
7
- export declare const hasSearchTerms: (metadata: ResolvedMetadata) => boolean;
7
+ export declare const hasSearchTerms: (input: FetchMetadataInput) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prose-reader/metadata-fetcher",
3
- "version": "1.340.0",
3
+ "version": "1.342.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -29,9 +29,9 @@
29
29
  "test:watch": "vitest watch"
30
30
  },
31
31
  "dependencies": {
32
- "@prose-reader/archive-reader": "^1.340.0",
33
- "@prose-reader/shared": "^1.340.0",
32
+ "@prose-reader/archive-reader": "^1.342.0",
33
+ "@prose-reader/shared": "^1.342.0",
34
34
  "xmldoc": "^2.0.0"
35
35
  },
36
- "gitHead": "b8aea840260994b069963e15474aa1dfdbb685e7"
36
+ "gitHead": "99cc96dab3e29677ab017246bbad836b54720284"
37
37
  }
@@ -1,21 +0,0 @@
1
- import { ResolvedMetadata } from '@prose-reader/archive-reader';
2
- /**
3
- * Merges several {@link ResolvedMetadata} into one, **first defined wins**.
4
- * Precedence is the caller's — pass the sources in the order you trust them.
5
- *
6
- * ```ts
7
- * // the book over the catalogs, catalogs filling the gaps
8
- * const metadata = mergeResolvedMetadata(resolved.metadata, fetched.metadata)
9
- * ```
10
- *
11
- * Field-wise rather than object-wise, so a source knowing only a cover
12
- * contributes it without hiding another's title. Two fields are additive
13
- * instead: `identifiers` concatenate (deduped on scheme + value, mirroring
14
- * `resolveMetadata`), and `belongsTo` merges `series` and `collection`
15
- * independently.
16
- *
17
- * Everything else takes the first stated value whole. Unioning keyword lists
18
- * across catalogs is a judgement call for the consumer, not for a merge that
19
- * has to stay predictable.
20
- */
21
- export declare const mergeResolvedMetadata: (...entries: ReadonlyArray<ResolvedMetadata | undefined>) => ResolvedMetadata;