@everdeep/pubmed 0.1.1 → 0.1.3

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
@@ -77,12 +77,14 @@ Cursors are versioned, base64url-encoded, unsigned, implementation-specific cont
77
77
 
78
78
  ### Count changes during pagination
79
79
 
80
- NCBI can return HTTP 200 with valid IDs but a different total on a later page. By default, the client stops with `PaginationConsistencyError` (`code: "PAGINATION_INCONSISTENT"`, `retryable: false`), not `INVALID_RESPONSE` or `SEARCH_LIMIT`. This is a consistency signal, **not provider unavailability**. Retrying may succeed, but does not establish consistent results; the client does not automatically retry or restart such searches.
80
+ NCBI can return HTTP 200 with valid IDs but a different total on a later page. By default, `search()` and `searchAll()` **continue retrieving valid pages**, returning count-drift diagnostics and emitting a `search-total-drift` event. The default `totalDriftPolicy` is `"warn"` (version 0.1.1 defaulted to `"error"`); no explicit opt-in or event handler is required. This is best-effort pagination, not a snapshot guarantee.
81
81
 
82
- If best-effort pagination is acceptable, opt in explicitly at client construction:
82
+ To stop on count changes, explicitly configure `totalDriftPolicy: "error"`. Strict mode throws `PaginationConsistencyError` (`code: "PAGINATION_INCONSISTENT"`, `retryable: false`) before fetching the changed page's records. This is a consistency signal, **not provider unavailability**. The client does not automatically retry or restart such searches.
83
+
84
+ With the default policy:
83
85
 
84
86
  ```ts
85
- const client = new PubMedClient({ email, tool, totalDriftPolicy: "warn" });
87
+ const client = new PubMedClient({ email, tool }); // totalDriftPolicy: "warn" by default
86
88
  const first = await client.search({ query: "cancer", pageSize: 2 });
87
89
  if (first.nextCursor) {
88
90
  const second = await client.search({ cursor: first.nextCursor });
@@ -94,7 +96,7 @@ In both modes, `SearchBatch.total` and the pagination bound remain the **initial
94
96
 
95
97
  Warn mode returns a `diagnostics` entry on each drifted page and emits a `search-total-drift` event. A consistency error exposes the same metadata through `.diagnostic` and `toJSON()`: `reason`, `originalTotal`, `observedTotal`, `offset`, `requestedIds`, and `returnedIds`. No queries, PMIDs, cursors, or history tokens are included. Record parse `warnings` remain separate.
96
98
 
97
- Neither mode guarantees snapshot enumeration: equal counts do not prove stable membership or ordering, and page-local uniqueness does not detect cross-page duplicates or omissions. Warn mode explicitly accepts that risk; it does not silently deduplicate or claim complete results. Consumers requiring exact enumeration should not opt in.
99
+ Neither mode guarantees snapshot enumeration: equal counts do not prove stable membership or ordering, and page-local uniqueness does not detect cross-page duplicates or omissions. Warn mode accepts that risk; it does not silently deduplicate or claim complete results. Consumers should persist by PMID idempotently and inspect `missingPmids` and `diagnostics`. Consumers requiring a stop on count drift should select `"error"`, but that alone does not establish exact enumeration.
98
100
 
99
101
  For progressive consumption, use `searchAll()`. `maxResults` is required so a caller must make the retrieval bound explicit:
100
102
 
@@ -130,7 +132,17 @@ if (record?.kind === "article") {
130
132
  }
131
133
  ```
132
134
 
133
- Every record includes `rawXml`, which is the exact direct-child XML fragment received from PubMed, without serialization or normalization. `source` retains parsed source data for fields not represented in the normalized surface.
135
+ `rawXml` is omitted by default, including on unknown records. Enable it for a client or individual full-record request to retain the exact direct-child XML fragment received from PubMed, without serialization or normalization:
136
+
137
+ ```ts
138
+ const client = new PubMedClient({ email, tool, includeRawXml: true });
139
+ const record = await client.get("123"); // includes rawXml
140
+ const compact = await client.get("123", { includeRawXml: false }); // omits rawXml
141
+ ```
142
+
143
+ The per-request option also applies to `getMany`, `search` (including cursor requests), and every page of `searchAll`. Each cursor request uses its own option or the client's default, not the previous page's option. The standalone parser similarly supports `parsePubMedXml(xml, { includeRawXml: true })` and omits XML by default.
144
+
145
+ **Compatibility change:** `rawXml` is now optional (`string | undefined`); consumers that previously relied on its presence must explicitly opt in. This controls returned fields only: XML is still fetched and parsed, and any configured response cache still stores XML bodies. `source` continues to retain parsed source data for fields not represented in the normalized surface. Summary methods are unchanged and do not support this option.
134
146
 
135
147
  ## Citation export
136
148
 
package/dist/index.cjs CHANGED
@@ -715,8 +715,19 @@ function parseFragment(fragment) {
715
715
  }
716
716
 
717
717
  // src/parser.ts
718
- function parsePubMedXml(xml) {
719
- const records = extractFragments(xml).map(parseFragment);
718
+ function parsePubMedXml(xml, options = {}) {
719
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
720
+ throw new ValidationError("parser options must be an object");
721
+ }
722
+ if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
723
+ throw new ValidationError("includeRawXml must be a boolean");
724
+ }
725
+ const records = extractFragments(xml).map((fragment) => {
726
+ const record = parseFragment(fragment);
727
+ if (options.includeRawXml === true) return record;
728
+ const { rawXml, ...withoutRawXml } = record;
729
+ return withoutRawXml;
730
+ });
720
731
  const warnings = records.flatMap((record) => record.kind === "unknown" ? [{ code: "UNKNOWN_RECORD", message: `Unknown PubMed record type: ${record.recordType}`, recordType: record.recordType }] : []);
721
732
  return { records, warnings };
722
733
  }
@@ -1567,7 +1578,9 @@ var Transport = class {
1567
1578
  const correlationId = (0, import_node_crypto.randomUUID)();
1568
1579
  safeEvent(this.#onEvent, { type: "correlation-id", endpoint, correlationId });
1569
1580
  try {
1570
- return await this.#coordinate(endpoint, parameters, decode, options, correlationId);
1581
+ const value = await this.#coordinate(endpoint, parameters, decode, options, correlationId);
1582
+ if (options.signal?.aborted) throw new AbortedError();
1583
+ return value;
1571
1584
  } catch (error) {
1572
1585
  safeEvent(this.#onEvent, {
1573
1586
  type: "terminal-failure",
@@ -1591,6 +1604,7 @@ ${semantic.toString()}`);
1591
1604
  const cached = await this.#cache.read(key, signal);
1592
1605
  if (cached.status === "hit") {
1593
1606
  safeEvent(this.#onEvent, { type: "cache-hit", endpoint, correlationId });
1607
+ if (signal?.aborted) throw new AbortedError();
1594
1608
  if (encoder.encode(cached.value).byteLength > this.#maxResponseBytes) {
1595
1609
  await this.#cache.invalidate(key);
1596
1610
  } else {
@@ -1671,6 +1685,9 @@ function isAbortSignal(value) {
1671
1685
  function validateRequestOptions(options, name) {
1672
1686
  const candidate = object3(options);
1673
1687
  if (candidate === void 0) throw new ValidationError(`${name} must be an object`);
1688
+ if (candidate.includeRawXml !== void 0 && typeof candidate.includeRawXml !== "boolean") {
1689
+ throw new ValidationError("includeRawXml must be a boolean");
1690
+ }
1674
1691
  if (candidate.includeLinkOuts !== void 0 && typeof candidate.includeLinkOuts !== "boolean") {
1675
1692
  throw new ValidationError("includeLinkOuts must be a boolean");
1676
1693
  }
@@ -1681,6 +1698,9 @@ function validateRequestOptions(options, name) {
1681
1698
  function validateSummaryRequestOptions(options) {
1682
1699
  const candidate = object3(options);
1683
1700
  if (candidate === void 0) throw new ValidationError("summary request options must be an object");
1701
+ if (candidate.includeRawXml !== void 0) {
1702
+ throw new ValidationError("includeRawXml is not supported for summary requests");
1703
+ }
1684
1704
  if (candidate.includeLinkOuts !== void 0) {
1685
1705
  throw new ValidationError("includeLinkOuts is not supported for summary requests");
1686
1706
  }
@@ -1813,7 +1833,7 @@ function validateSearchState(state, expectedIds, offset = 0) {
1813
1833
  }
1814
1834
  }
1815
1835
  function parseExpectedFetch(body, expectedPmids) {
1816
- const parsed = parsePubMedXml(body);
1836
+ const parsed = parsePubMedXml(body, { includeRawXml: true });
1817
1837
  const expected = new Set(expectedPmids);
1818
1838
  const seen = /* @__PURE__ */ new Set();
1819
1839
  for (const record of parsed.records) {
@@ -1894,15 +1914,20 @@ function withLinks(record, extra) {
1894
1914
  var PubMedClient = class {
1895
1915
  #transport;
1896
1916
  #maxBatchSize;
1917
+ #includeRawXml;
1897
1918
  #totalDriftPolicy;
1898
1919
  #onEvent;
1899
1920
  constructor(options) {
1900
1921
  if (typeof options !== "object" || options === null) throw new ValidationError("PubMedClient options are required");
1901
1922
  validateAdapterShapes(options);
1923
+ if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
1924
+ throw new ValidationError("includeRawXml must be a boolean");
1925
+ }
1926
+ this.#includeRawXml = options.includeRawXml ?? false;
1902
1927
  if (options.totalDriftPolicy !== void 0 && options.totalDriftPolicy !== "error" && options.totalDriftPolicy !== "warn") {
1903
1928
  throw new ValidationError('totalDriftPolicy must be "error" or "warn"');
1904
1929
  }
1905
- this.#totalDriftPolicy = options.totalDriftPolicy ?? "error";
1930
+ this.#totalDriftPolicy = options.totalDriftPolicy ?? "warn";
1906
1931
  if (typeof options.email !== "string" || options.email.trim() === "") throw new ValidationError("email is required");
1907
1932
  if (typeof options.tool !== "string" || options.tool.trim() === "") throw new ValidationError("tool is required");
1908
1933
  if (options.apiKey !== void 0 && (typeof options.apiKey !== "string" || options.apiKey.trim() === "")) {
@@ -1965,7 +1990,11 @@ var PubMedClient = class {
1965
1990
  }
1966
1991
  let ordered = input.flatMap((pmid) => byPmid.get(pmid) ?? []);
1967
1992
  if (unknown.length > 0) ordered = [...ordered, ...unknown];
1993
+ if (!(options.includeRawXml ?? this.#includeRawXml)) {
1994
+ ordered = ordered.map(({ rawXml, ...record }) => record);
1995
+ }
1968
1996
  if (options.includeLinkOuts === true && ordered.length > 0) ordered = await this.#enrich(ordered, options.signal);
1997
+ if (options.signal?.aborted) throw new AbortedError();
1969
1998
  return {
1970
1999
  records: ordered,
1971
2000
  missingPmids: input.filter((pmid) => !byPmid.has(pmid)),
@@ -1994,6 +2023,7 @@ var PubMedClient = class {
1994
2023
  );
1995
2024
  received.push(...summaries);
1996
2025
  }
2026
+ if (options.signal?.aborted) throw new AbortedError();
1997
2027
  const byPmid = new Map(received.map((summary) => [summary.pmid, summary]));
1998
2028
  return {
1999
2029
  summaries: input.flatMap((pmid) => byPmid.get(pmid) ?? []),
@@ -2004,7 +2034,7 @@ var PubMedClient = class {
2004
2034
  validateRequestOptions(options, "search options");
2005
2035
  if ("cursor" in options) {
2006
2036
  if (typeof options.cursor !== "string" || options.cursor === "") throw new ValidationError("cursor must be a non-empty string");
2007
- return (await this.#searchCursorPage(options.cursor, options.includeLinkOuts === true, options.signal)).batch;
2037
+ return (await this.#searchCursorPage(options.cursor, options)).batch;
2008
2038
  }
2009
2039
  validateQueryOptions(options);
2010
2040
  return (await this.#searchQueryPage(options)).batch;
@@ -2020,6 +2050,7 @@ var PubMedClient = class {
2020
2050
  query: options.query,
2021
2051
  pageSize,
2022
2052
  ...options.sort === void 0 ? {} : { sort: options.sort },
2053
+ ...options.includeRawXml === void 0 ? {} : { includeRawXml: options.includeRawXml },
2023
2054
  ...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
2024
2055
  ...options.signal === void 0 ? {} : { signal: options.signal }
2025
2056
  };
@@ -2036,8 +2067,7 @@ var PubMedClient = class {
2036
2067
  if (page.batch.nextCursor === null) throw new InvalidResponseError("PubMed search ended before the requested result count");
2037
2068
  page = await this.#searchCursorPage(
2038
2069
  page.batch.nextCursor,
2039
- options.includeLinkOuts === true,
2040
- options.signal,
2070
+ options,
2041
2071
  target - processed
2042
2072
  );
2043
2073
  }
@@ -2077,10 +2107,7 @@ var PubMedClient = class {
2077
2107
  expectedPmids: []
2078
2108
  };
2079
2109
  }
2080
- const batch = await this.getMany(state.ids, {
2081
- ...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
2082
- ...options.signal === void 0 ? {} : { signal: options.signal }
2083
- });
2110
+ const batch = await this.getMany(state.ids, options);
2084
2111
  const offset = state.ids.length;
2085
2112
  return {
2086
2113
  batch: {
@@ -2091,7 +2118,8 @@ var PubMedClient = class {
2091
2118
  expectedPmids: state.ids
2092
2119
  };
2093
2120
  }
2094
- async #searchCursorPage(cursorValue, includeLinkOuts, signal, maxExpected) {
2121
+ async #searchCursorPage(cursorValue, options, maxExpected) {
2122
+ const { signal } = options;
2095
2123
  const cursor = decodeCursor(cursorValue);
2096
2124
  if (cursor.offset >= SEARCH_WINDOW) throw new SearchLimitError();
2097
2125
  const remainingWindow = SEARCH_WINDOW - cursor.offset;
@@ -2130,7 +2158,7 @@ var PubMedClient = class {
2130
2158
  { cache: false, ...signal === void 0 ? {} : { signal } }
2131
2159
  );
2132
2160
  if (diagnostic !== void 0) safeEvent(this.#onEvent, { type: "search-total-drift", ...diagnostic });
2133
- const batch = await this.getMany(state.ids, { includeLinkOuts, ...signal === void 0 ? {} : { signal } });
2161
+ const batch = await this.getMany(state.ids, options);
2134
2162
  const offset = cursor.offset + state.ids.length;
2135
2163
  return {
2136
2164
  batch: {
package/dist/index.d.cts CHANGED
@@ -169,7 +169,8 @@ interface PubMedWarning {
169
169
  interface BasePubMedRecord {
170
170
  readonly kind: "article" | "book" | "unknown";
171
171
  readonly recordType: string;
172
- readonly rawXml: string;
172
+ /** Exact source fragment, present only when includeRawXml is enabled. */
173
+ readonly rawXml?: string;
173
174
  readonly source: JsonObject;
174
175
  readonly identifiers: readonly PubMedIdentifier[];
175
176
  readonly links: readonly PubMedLink[];
@@ -305,6 +306,8 @@ interface SearchBatch extends BatchResult {
305
306
  readonly diagnostics?: readonly SearchTotalDriftDiagnostic[];
306
307
  }
307
308
  interface RequestOptions {
309
+ /** Override the client's raw XML inclusion default for this request. */
310
+ readonly includeRawXml?: boolean;
308
311
  readonly includeLinkOuts?: boolean;
309
312
  readonly signal?: AbortSignal;
310
313
  }
@@ -396,6 +399,8 @@ type PubMedEvent = Readonly<{
396
399
  recordType?: string;
397
400
  }>;
398
401
  interface PubMedClientOptions {
402
+ /** Include exact source XML fragments in full records. Default false. */
403
+ readonly includeRawXml?: boolean;
399
404
  readonly email: string;
400
405
  readonly tool: string;
401
406
  readonly apiKey?: string;
@@ -408,7 +413,7 @@ interface PubMedClientOptions {
408
413
  readonly maxResponseBytes?: number;
409
414
  readonly maxQueuedRequests?: number;
410
415
  readonly maxBatchSize?: number;
411
- /** Default "error". "warn" explicitly permits best-effort, non-snapshot pagination. */
416
+ /** Default "warn": continue valid pages with drift diagnostics. "error" opts into stopping on count changes. Neither guarantees a snapshot. */
412
417
  readonly totalDriftPolicy?: "error" | "warn";
413
418
  }
414
419
 
@@ -449,7 +454,11 @@ interface ParsedPubMedXml {
449
454
  readonly records: readonly PubMedRecord[];
450
455
  readonly warnings: readonly PubMedWarning[];
451
456
  }
452
- /** Parse an entire PubmedArticleSet while retaining each direct child byte-for-byte. */
453
- declare function parsePubMedXml(xml: string): ParsedPubMedXml;
457
+ interface ParsePubMedXmlOptions {
458
+ /** Include each direct child's exact source XML fragment. Default false. */
459
+ readonly includeRawXml?: boolean;
460
+ }
461
+ /** Parse an entire PubmedArticleSet, optionally retaining exact source fragments. */
462
+ declare function parsePubMedXml(xml: string, options?: ParsePubMedXmlOptions): ParsedPubMedXml;
454
463
 
455
- export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
464
+ export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsePubMedXmlOptions, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
package/dist/index.d.ts CHANGED
@@ -169,7 +169,8 @@ interface PubMedWarning {
169
169
  interface BasePubMedRecord {
170
170
  readonly kind: "article" | "book" | "unknown";
171
171
  readonly recordType: string;
172
- readonly rawXml: string;
172
+ /** Exact source fragment, present only when includeRawXml is enabled. */
173
+ readonly rawXml?: string;
173
174
  readonly source: JsonObject;
174
175
  readonly identifiers: readonly PubMedIdentifier[];
175
176
  readonly links: readonly PubMedLink[];
@@ -305,6 +306,8 @@ interface SearchBatch extends BatchResult {
305
306
  readonly diagnostics?: readonly SearchTotalDriftDiagnostic[];
306
307
  }
307
308
  interface RequestOptions {
309
+ /** Override the client's raw XML inclusion default for this request. */
310
+ readonly includeRawXml?: boolean;
308
311
  readonly includeLinkOuts?: boolean;
309
312
  readonly signal?: AbortSignal;
310
313
  }
@@ -396,6 +399,8 @@ type PubMedEvent = Readonly<{
396
399
  recordType?: string;
397
400
  }>;
398
401
  interface PubMedClientOptions {
402
+ /** Include exact source XML fragments in full records. Default false. */
403
+ readonly includeRawXml?: boolean;
399
404
  readonly email: string;
400
405
  readonly tool: string;
401
406
  readonly apiKey?: string;
@@ -408,7 +413,7 @@ interface PubMedClientOptions {
408
413
  readonly maxResponseBytes?: number;
409
414
  readonly maxQueuedRequests?: number;
410
415
  readonly maxBatchSize?: number;
411
- /** Default "error". "warn" explicitly permits best-effort, non-snapshot pagination. */
416
+ /** Default "warn": continue valid pages with drift diagnostics. "error" opts into stopping on count changes. Neither guarantees a snapshot. */
412
417
  readonly totalDriftPolicy?: "error" | "warn";
413
418
  }
414
419
 
@@ -449,7 +454,11 @@ interface ParsedPubMedXml {
449
454
  readonly records: readonly PubMedRecord[];
450
455
  readonly warnings: readonly PubMedWarning[];
451
456
  }
452
- /** Parse an entire PubmedArticleSet while retaining each direct child byte-for-byte. */
453
- declare function parsePubMedXml(xml: string): ParsedPubMedXml;
457
+ interface ParsePubMedXmlOptions {
458
+ /** Include each direct child's exact source XML fragment. Default false. */
459
+ readonly includeRawXml?: boolean;
460
+ }
461
+ /** Parse an entire PubmedArticleSet, optionally retaining exact source fragments. */
462
+ declare function parsePubMedXml(xml: string, options?: ParsePubMedXmlOptions): ParsedPubMedXml;
454
463
 
455
- export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
464
+ export { AbortedError, type AbstractSection, type Affiliation, type AuthorIdentifier, type BasePubMedRecord, type BatchResult, type CacheAdapter, type CitationFormat, type CitationSource, type CollectiveAuthor, CursorExpiredError, CursorInvalidError, HttpError, InvalidResponseError, type JournalCitation, type JsonObject, type JsonPrimitive, type JsonValue, type Keyword, MemoryCache, type MemoryCacheOptions, type MeshHeading, NetworkError, PaginationConsistencyError, ParseError, type ParsePubMedXmlOptions, type ParsedPubMedXml, type PartialDate, type PersonalAuthor, type PubMedArticleRecord, type PubMedAuthor, type PubMedBookRecord, PubMedClient, type PubMedClientOptions, type PubMedEndpoint, PubMedError, type PubMedErrorCode, type PubMedEvent, type PubMedIdentifier, type PubMedLink, type PubMedRecord, type PubMedSummary, type PubMedSummaryAuthor, type PubMedSummaryBook, type PubMedSummaryHistoryEntry, type PubMedSummaryIdentifier, type PubMedSummaryJournal, type PubMedWarning, type PublicationDates, type PublicationHistoryEntry, QueueFullError, type RateLimitBucket, type RateLimitCoordinator, RateLimitError, type RequestOptions, ResponseTooLargeError, type SearchAllOptions, type SearchBatch, type SearchCursorOptions, SearchLimitError, type SearchOptions, type SearchQueryOptions, type SearchTotalDriftDiagnostic, type SummaryBatchResult, type SummaryRequestOptions, TimeoutError, type UnknownPubMedRecord, ValidationError, formatCitation, formatCitations, parsePubMedXml };
package/dist/index.js CHANGED
@@ -670,8 +670,19 @@ function parseFragment(fragment) {
670
670
  }
671
671
 
672
672
  // src/parser.ts
673
- function parsePubMedXml(xml) {
674
- const records = extractFragments(xml).map(parseFragment);
673
+ function parsePubMedXml(xml, options = {}) {
674
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
675
+ throw new ValidationError("parser options must be an object");
676
+ }
677
+ if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
678
+ throw new ValidationError("includeRawXml must be a boolean");
679
+ }
680
+ const records = extractFragments(xml).map((fragment) => {
681
+ const record = parseFragment(fragment);
682
+ if (options.includeRawXml === true) return record;
683
+ const { rawXml, ...withoutRawXml } = record;
684
+ return withoutRawXml;
685
+ });
675
686
  const warnings = records.flatMap((record) => record.kind === "unknown" ? [{ code: "UNKNOWN_RECORD", message: `Unknown PubMed record type: ${record.recordType}`, recordType: record.recordType }] : []);
676
687
  return { records, warnings };
677
688
  }
@@ -1522,7 +1533,9 @@ var Transport = class {
1522
1533
  const correlationId = randomUUID();
1523
1534
  safeEvent(this.#onEvent, { type: "correlation-id", endpoint, correlationId });
1524
1535
  try {
1525
- return await this.#coordinate(endpoint, parameters, decode, options, correlationId);
1536
+ const value = await this.#coordinate(endpoint, parameters, decode, options, correlationId);
1537
+ if (options.signal?.aborted) throw new AbortedError();
1538
+ return value;
1526
1539
  } catch (error) {
1527
1540
  safeEvent(this.#onEvent, {
1528
1541
  type: "terminal-failure",
@@ -1546,6 +1559,7 @@ ${semantic.toString()}`);
1546
1559
  const cached = await this.#cache.read(key, signal);
1547
1560
  if (cached.status === "hit") {
1548
1561
  safeEvent(this.#onEvent, { type: "cache-hit", endpoint, correlationId });
1562
+ if (signal?.aborted) throw new AbortedError();
1549
1563
  if (encoder.encode(cached.value).byteLength > this.#maxResponseBytes) {
1550
1564
  await this.#cache.invalidate(key);
1551
1565
  } else {
@@ -1626,6 +1640,9 @@ function isAbortSignal(value) {
1626
1640
  function validateRequestOptions(options, name) {
1627
1641
  const candidate = object3(options);
1628
1642
  if (candidate === void 0) throw new ValidationError(`${name} must be an object`);
1643
+ if (candidate.includeRawXml !== void 0 && typeof candidate.includeRawXml !== "boolean") {
1644
+ throw new ValidationError("includeRawXml must be a boolean");
1645
+ }
1629
1646
  if (candidate.includeLinkOuts !== void 0 && typeof candidate.includeLinkOuts !== "boolean") {
1630
1647
  throw new ValidationError("includeLinkOuts must be a boolean");
1631
1648
  }
@@ -1636,6 +1653,9 @@ function validateRequestOptions(options, name) {
1636
1653
  function validateSummaryRequestOptions(options) {
1637
1654
  const candidate = object3(options);
1638
1655
  if (candidate === void 0) throw new ValidationError("summary request options must be an object");
1656
+ if (candidate.includeRawXml !== void 0) {
1657
+ throw new ValidationError("includeRawXml is not supported for summary requests");
1658
+ }
1639
1659
  if (candidate.includeLinkOuts !== void 0) {
1640
1660
  throw new ValidationError("includeLinkOuts is not supported for summary requests");
1641
1661
  }
@@ -1768,7 +1788,7 @@ function validateSearchState(state, expectedIds, offset = 0) {
1768
1788
  }
1769
1789
  }
1770
1790
  function parseExpectedFetch(body, expectedPmids) {
1771
- const parsed = parsePubMedXml(body);
1791
+ const parsed = parsePubMedXml(body, { includeRawXml: true });
1772
1792
  const expected = new Set(expectedPmids);
1773
1793
  const seen = /* @__PURE__ */ new Set();
1774
1794
  for (const record of parsed.records) {
@@ -1849,15 +1869,20 @@ function withLinks(record, extra) {
1849
1869
  var PubMedClient = class {
1850
1870
  #transport;
1851
1871
  #maxBatchSize;
1872
+ #includeRawXml;
1852
1873
  #totalDriftPolicy;
1853
1874
  #onEvent;
1854
1875
  constructor(options) {
1855
1876
  if (typeof options !== "object" || options === null) throw new ValidationError("PubMedClient options are required");
1856
1877
  validateAdapterShapes(options);
1878
+ if (options.includeRawXml !== void 0 && typeof options.includeRawXml !== "boolean") {
1879
+ throw new ValidationError("includeRawXml must be a boolean");
1880
+ }
1881
+ this.#includeRawXml = options.includeRawXml ?? false;
1857
1882
  if (options.totalDriftPolicy !== void 0 && options.totalDriftPolicy !== "error" && options.totalDriftPolicy !== "warn") {
1858
1883
  throw new ValidationError('totalDriftPolicy must be "error" or "warn"');
1859
1884
  }
1860
- this.#totalDriftPolicy = options.totalDriftPolicy ?? "error";
1885
+ this.#totalDriftPolicy = options.totalDriftPolicy ?? "warn";
1861
1886
  if (typeof options.email !== "string" || options.email.trim() === "") throw new ValidationError("email is required");
1862
1887
  if (typeof options.tool !== "string" || options.tool.trim() === "") throw new ValidationError("tool is required");
1863
1888
  if (options.apiKey !== void 0 && (typeof options.apiKey !== "string" || options.apiKey.trim() === "")) {
@@ -1920,7 +1945,11 @@ var PubMedClient = class {
1920
1945
  }
1921
1946
  let ordered = input.flatMap((pmid) => byPmid.get(pmid) ?? []);
1922
1947
  if (unknown.length > 0) ordered = [...ordered, ...unknown];
1948
+ if (!(options.includeRawXml ?? this.#includeRawXml)) {
1949
+ ordered = ordered.map(({ rawXml, ...record }) => record);
1950
+ }
1923
1951
  if (options.includeLinkOuts === true && ordered.length > 0) ordered = await this.#enrich(ordered, options.signal);
1952
+ if (options.signal?.aborted) throw new AbortedError();
1924
1953
  return {
1925
1954
  records: ordered,
1926
1955
  missingPmids: input.filter((pmid) => !byPmid.has(pmid)),
@@ -1949,6 +1978,7 @@ var PubMedClient = class {
1949
1978
  );
1950
1979
  received.push(...summaries);
1951
1980
  }
1981
+ if (options.signal?.aborted) throw new AbortedError();
1952
1982
  const byPmid = new Map(received.map((summary) => [summary.pmid, summary]));
1953
1983
  return {
1954
1984
  summaries: input.flatMap((pmid) => byPmid.get(pmid) ?? []),
@@ -1959,7 +1989,7 @@ var PubMedClient = class {
1959
1989
  validateRequestOptions(options, "search options");
1960
1990
  if ("cursor" in options) {
1961
1991
  if (typeof options.cursor !== "string" || options.cursor === "") throw new ValidationError("cursor must be a non-empty string");
1962
- return (await this.#searchCursorPage(options.cursor, options.includeLinkOuts === true, options.signal)).batch;
1992
+ return (await this.#searchCursorPage(options.cursor, options)).batch;
1963
1993
  }
1964
1994
  validateQueryOptions(options);
1965
1995
  return (await this.#searchQueryPage(options)).batch;
@@ -1975,6 +2005,7 @@ var PubMedClient = class {
1975
2005
  query: options.query,
1976
2006
  pageSize,
1977
2007
  ...options.sort === void 0 ? {} : { sort: options.sort },
2008
+ ...options.includeRawXml === void 0 ? {} : { includeRawXml: options.includeRawXml },
1978
2009
  ...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
1979
2010
  ...options.signal === void 0 ? {} : { signal: options.signal }
1980
2011
  };
@@ -1991,8 +2022,7 @@ var PubMedClient = class {
1991
2022
  if (page.batch.nextCursor === null) throw new InvalidResponseError("PubMed search ended before the requested result count");
1992
2023
  page = await this.#searchCursorPage(
1993
2024
  page.batch.nextCursor,
1994
- options.includeLinkOuts === true,
1995
- options.signal,
2025
+ options,
1996
2026
  target - processed
1997
2027
  );
1998
2028
  }
@@ -2032,10 +2062,7 @@ var PubMedClient = class {
2032
2062
  expectedPmids: []
2033
2063
  };
2034
2064
  }
2035
- const batch = await this.getMany(state.ids, {
2036
- ...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
2037
- ...options.signal === void 0 ? {} : { signal: options.signal }
2038
- });
2065
+ const batch = await this.getMany(state.ids, options);
2039
2066
  const offset = state.ids.length;
2040
2067
  return {
2041
2068
  batch: {
@@ -2046,7 +2073,8 @@ var PubMedClient = class {
2046
2073
  expectedPmids: state.ids
2047
2074
  };
2048
2075
  }
2049
- async #searchCursorPage(cursorValue, includeLinkOuts, signal, maxExpected) {
2076
+ async #searchCursorPage(cursorValue, options, maxExpected) {
2077
+ const { signal } = options;
2050
2078
  const cursor = decodeCursor(cursorValue);
2051
2079
  if (cursor.offset >= SEARCH_WINDOW) throw new SearchLimitError();
2052
2080
  const remainingWindow = SEARCH_WINDOW - cursor.offset;
@@ -2085,7 +2113,7 @@ var PubMedClient = class {
2085
2113
  { cache: false, ...signal === void 0 ? {} : { signal } }
2086
2114
  );
2087
2115
  if (diagnostic !== void 0) safeEvent(this.#onEvent, { type: "search-total-drift", ...diagnostic });
2088
- const batch = await this.getMany(state.ids, { includeLinkOuts, ...signal === void 0 ? {} : { signal } });
2116
+ const batch = await this.getMany(state.ids, options);
2089
2117
  const offset = cursor.offset + state.ids.length;
2090
2118
  return {
2091
2119
  batch: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everdeep/pubmed",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Safe, typed TypeScript client for the NCBI PubMed E-utilities API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",