@everdeep/pubmed 0.1.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/index.js ADDED
@@ -0,0 +1,2515 @@
1
+ // src/client.ts
2
+ import { XMLParser as XMLParser2 } from "fast-xml-parser";
3
+
4
+ // src/errors.ts
5
+ var PubMedError = class extends Error {
6
+ code;
7
+ retryable;
8
+ constructor(message, code, retryable = false, options) {
9
+ super(message, options);
10
+ this.name = "PubMedError";
11
+ this.code = code;
12
+ this.retryable = retryable;
13
+ }
14
+ toJSON() {
15
+ return { name: this.name, message: this.message, code: this.code, retryable: this.retryable };
16
+ }
17
+ };
18
+ var ValidationError = class extends PubMedError {
19
+ constructor(message) {
20
+ super(message, "VALIDATION_ERROR");
21
+ this.name = "ValidationError";
22
+ }
23
+ };
24
+ var HttpError = class extends PubMedError {
25
+ status;
26
+ constructor(status, retryable) {
27
+ super(`PubMed request failed with HTTP status ${status}`, "HTTP_ERROR", retryable);
28
+ this.name = "HttpError";
29
+ this.status = status;
30
+ }
31
+ };
32
+ var RateLimitError = class extends PubMedError {
33
+ status = 429;
34
+ constructor() {
35
+ super("PubMed rate limit was exceeded", "RATE_LIMIT_ERROR", true);
36
+ this.name = "RateLimitError";
37
+ }
38
+ };
39
+ var TimeoutError = class extends PubMedError {
40
+ constructor(options) {
41
+ super("PubMed request timed out", "TIMEOUT_ERROR", true, options);
42
+ this.name = "TimeoutError";
43
+ }
44
+ };
45
+ var NetworkError = class extends PubMedError {
46
+ constructor(options) {
47
+ super("PubMed network request failed", "NETWORK_ERROR", true, options);
48
+ this.name = "NetworkError";
49
+ }
50
+ };
51
+ var ResponseTooLargeError = class extends PubMedError {
52
+ limitBytes;
53
+ constructor(limitBytes) {
54
+ super(`PubMed response exceeded the configured ${limitBytes} byte limit`, "RESPONSE_TOO_LARGE");
55
+ this.name = "ResponseTooLargeError";
56
+ this.limitBytes = limitBytes;
57
+ }
58
+ };
59
+ var QueueFullError = class extends PubMedError {
60
+ constructor() {
61
+ super("PubMed rate-limit queue is full", "QUEUE_FULL");
62
+ this.name = "QueueFullError";
63
+ }
64
+ };
65
+ var ParseError = class extends PubMedError {
66
+ constructor(message = "PubMed returned malformed XML", options) {
67
+ super(message, "PARSE_ERROR", false, options);
68
+ this.name = "ParseError";
69
+ }
70
+ };
71
+ var InvalidResponseError = class extends PubMedError {
72
+ constructor(message = "PubMed returned an invalid response", options) {
73
+ super(message, "INVALID_RESPONSE", false, options);
74
+ this.name = "InvalidResponseError";
75
+ }
76
+ };
77
+ var CursorExpiredError = class extends PubMedError {
78
+ constructor() {
79
+ super("The PubMed search cursor has expired", "CURSOR_EXPIRED");
80
+ this.name = "CursorExpiredError";
81
+ }
82
+ };
83
+ var CursorInvalidError = class extends PubMedError {
84
+ constructor() {
85
+ super("The PubMed search cursor is invalid", "CURSOR_INVALID");
86
+ this.name = "CursorInvalidError";
87
+ }
88
+ };
89
+ var AbortedError = class extends PubMedError {
90
+ constructor(options) {
91
+ super("PubMed request was aborted", "ABORTED", false, options);
92
+ this.name = "AbortedError";
93
+ }
94
+ };
95
+ var SearchLimitError = class extends PubMedError {
96
+ limit;
97
+ constructor(limit = 1e4) {
98
+ super(`PubMed searches cannot retrieve results beyond the ${limit} record window`, "SEARCH_LIMIT");
99
+ this.name = "SearchLimitError";
100
+ this.limit = limit;
101
+ }
102
+ };
103
+
104
+ // src/parser/xml-framing.ts
105
+ import { XMLValidator } from "fast-xml-parser";
106
+
107
+ // src/parser/xml-values.ts
108
+ function object(value) {
109
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
110
+ }
111
+ function at(value, key) {
112
+ return object(value)?.[key];
113
+ }
114
+ function list(value) {
115
+ if (value === void 0 || value === null) return [];
116
+ return Array.isArray(value) ? value : [value];
117
+ }
118
+ function isXmlCodePoint(codePoint) {
119
+ return codePoint === 9 || codePoint === 10 || codePoint === 13 || codePoint >= 32 && codePoint <= 55295 || codePoint >= 57344 && codePoint <= 65533 || codePoint >= 65536 && codePoint <= 1114111;
120
+ }
121
+ function decodeNumericReference(body) {
122
+ const hexadecimal = body.toLowerCase().startsWith("#x");
123
+ const codePoint = Number.parseInt(body.slice(hexadecimal ? 2 : 1), hexadecimal ? 16 : 10);
124
+ if (!Number.isSafeInteger(codePoint) || !isXmlCodePoint(codePoint)) throw new ParseError();
125
+ try {
126
+ return String.fromCodePoint(codePoint);
127
+ } catch {
128
+ throw new ParseError();
129
+ }
130
+ }
131
+ function decodeEntities(value) {
132
+ return value.replace(/&(lt|gt|quot|apos|amp|#x[0-9a-f]+|#[0-9]+);/gi, (_entity, body) => {
133
+ const named = body.toLowerCase();
134
+ if (named === "lt") return "<";
135
+ if (named === "gt") return ">";
136
+ if (named === "quot") return '"';
137
+ if (named === "apos") return "'";
138
+ if (named === "amp") return "&";
139
+ return decodeNumericReference(body);
140
+ });
141
+ }
142
+ function normalizeText(value) {
143
+ return value.replace(/\s+/g, " ").trim();
144
+ }
145
+ function cleanText(value) {
146
+ return normalizeText(decodeEntities(value));
147
+ }
148
+ function text(value) {
149
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
150
+ const result2 = cleanText(String(value));
151
+ return result2 === "" ? void 0 : result2;
152
+ }
153
+ if (Array.isArray(value)) {
154
+ const result2 = cleanText(value.map((item) => text(item) ?? "").join(" "));
155
+ return result2 === "" ? void 0 : result2;
156
+ }
157
+ const record = object(value);
158
+ if (record === void 0) return void 0;
159
+ const parts = [];
160
+ for (const [key, child] of Object.entries(record)) {
161
+ if (!key.startsWith("@")) parts.push(text(child) ?? "");
162
+ }
163
+ const result = cleanText(parts.join(" "));
164
+ return result === "" ? void 0 : result;
165
+ }
166
+ function attribute(value, name) {
167
+ return text(object(value)?.[`@${name}`]);
168
+ }
169
+ function json(value) {
170
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
171
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
172
+ if (Array.isArray(value)) return value.map(json);
173
+ const record = object(value);
174
+ if (record === void 0) return null;
175
+ const result = {};
176
+ for (const [key, child] of Object.entries(record)) {
177
+ if (child !== void 0) result[key] = json(child);
178
+ }
179
+ return result;
180
+ }
181
+ function partialDate(value) {
182
+ const source = object(value);
183
+ if (source === void 0) return void 0;
184
+ const result = {};
185
+ const keys = ["Year", "Month", "Day", "Season", "MedlineDate", "Hour", "Minute", "Second"];
186
+ const targets = ["year", "month", "day", "season", "medlineDate", "hour", "minute", "second"];
187
+ keys.forEach((key, index) => {
188
+ const found = text(source[key]);
189
+ const target = targets[index];
190
+ if (found !== void 0 && target !== void 0) result[target] = found;
191
+ });
192
+ return Object.keys(result).length === 0 ? void 0 : result;
193
+ }
194
+
195
+ // src/parser/xml-framing.ts
196
+ function markupEnd(xml, start) {
197
+ if (xml.startsWith("<!--", start)) {
198
+ const end = xml.indexOf("-->", start + 4);
199
+ if (end < 0) throw new ParseError();
200
+ return end + 3;
201
+ }
202
+ if (xml.startsWith("<![CDATA[", start)) {
203
+ const end = xml.indexOf("]]>", start + 9);
204
+ if (end < 0) throw new ParseError();
205
+ return end + 3;
206
+ }
207
+ if (xml.startsWith("<?", start)) {
208
+ const end = xml.indexOf("?>", start + 2);
209
+ if (end < 0) throw new ParseError();
210
+ return end + 2;
211
+ }
212
+ let quote;
213
+ let subsetDepth = 0;
214
+ for (let index = start + 1; index < xml.length; index += 1) {
215
+ const character = xml[index];
216
+ if (quote !== void 0) {
217
+ if (character === quote) quote = void 0;
218
+ continue;
219
+ }
220
+ if (character === '"' || character === "'") {
221
+ quote = character;
222
+ } else if (character === "[") {
223
+ subsetDepth += 1;
224
+ } else if (character === "]" && subsetDepth > 0) {
225
+ subsetDepth -= 1;
226
+ } else if (character === ">" && subsetDepth === 0) {
227
+ return index + 1;
228
+ }
229
+ }
230
+ throw new ParseError();
231
+ }
232
+ function tagDetails(token) {
233
+ if (token.startsWith("<!--") || token.startsWith("<![") || token.startsWith("<?") || /^<!DOCTYPE/i.test(token)) {
234
+ return { kind: "other" };
235
+ }
236
+ const endMatch = /^<\/\s*([^\s>]+)\s*>$/.exec(token);
237
+ if (endMatch?.[1] !== void 0) return { kind: "end", name: endMatch[1] };
238
+ const startMatch = /^<\s*([^\s/>]+)/.exec(token);
239
+ if (startMatch?.[1] === void 0) return { kind: "other" };
240
+ return { kind: "start", name: startMatch[1], selfClosing: /\/\s*>$/.test(token) };
241
+ }
242
+ function validateNumericReferencesIn(value) {
243
+ let position = 0;
244
+ while (true) {
245
+ const start = value.indexOf("&#", position);
246
+ if (start < 0) return;
247
+ const match = /^&#(?:x[0-9a-f]+|[0-9]+);/i.exec(value.slice(start));
248
+ const body = match?.[0].slice(1, -1);
249
+ if (match === null || body === void 0) throw new ParseError();
250
+ decodeNumericReference(body);
251
+ position = start + match[0].length;
252
+ }
253
+ }
254
+ function validateNumericReferences(xml) {
255
+ let position = 0;
256
+ while (position < xml.length) {
257
+ const markupStart = xml.indexOf("<", position);
258
+ const textEnd = markupStart < 0 ? xml.length : markupStart;
259
+ validateNumericReferencesIn(xml.slice(position, textEnd));
260
+ if (markupStart < 0) return;
261
+ const markupFinish = markupEnd(xml, markupStart);
262
+ const token = xml.slice(markupStart, markupFinish);
263
+ if (!token.startsWith("<![CDATA[") && !token.startsWith("<!--") && !token.startsWith("<?")) {
264
+ validateNumericReferencesIn(token);
265
+ }
266
+ position = markupFinish;
267
+ }
268
+ }
269
+ function extractFragments(xml) {
270
+ validateNumericReferences(xml);
271
+ const validation = XMLValidator.validate(xml, { allowBooleanAttributes: false });
272
+ if (validation !== true) throw new ParseError();
273
+ const fragments = [];
274
+ const stack = [];
275
+ let rootSeen = false;
276
+ let rootClosed = false;
277
+ let childStart = -1;
278
+ let childName = "";
279
+ let position = 0;
280
+ while (position < xml.length) {
281
+ const start = xml.indexOf("<", position);
282
+ if (start < 0) break;
283
+ const end = markupEnd(xml, start);
284
+ const details = tagDetails(xml.slice(start, end));
285
+ position = end;
286
+ if (details.kind === "other") continue;
287
+ if (details.kind === "start") {
288
+ const name2 = details.name;
289
+ if (name2 === void 0) throw new ParseError();
290
+ if (!rootSeen) {
291
+ if (name2 !== "PubmedArticleSet") throw new ParseError("Expected a PubmedArticleSet root element");
292
+ rootSeen = true;
293
+ if (details.selfClosing === true) rootClosed = true;
294
+ else stack.push(name2);
295
+ continue;
296
+ }
297
+ if (rootClosed || stack.length === 0) throw new ParseError();
298
+ if (stack.length === 1) {
299
+ childStart = start;
300
+ childName = name2;
301
+ }
302
+ if (details.selfClosing === true) {
303
+ if (stack.length === 1) fragments.push({ name: name2, rawXml: xml.slice(start, end) });
304
+ } else {
305
+ stack.push(name2);
306
+ }
307
+ continue;
308
+ }
309
+ const name = details.name;
310
+ const expected = stack.at(-1);
311
+ if (name === void 0 || expected !== name) throw new ParseError();
312
+ if (stack.length === 2) {
313
+ if (childStart < 0) throw new ParseError();
314
+ fragments.push({ name: childName, rawXml: xml.slice(childStart, end) });
315
+ childStart = -1;
316
+ childName = "";
317
+ }
318
+ stack.pop();
319
+ if (stack.length === 0) rootClosed = true;
320
+ }
321
+ if (!rootSeen || !rootClosed || stack.length !== 0) throw new ParseError();
322
+ return fragments;
323
+ }
324
+ function rawElementTexts(xml, elementName) {
325
+ const results = [];
326
+ let position = 0;
327
+ while (position < xml.length) {
328
+ const candidate = xml.indexOf("<", position);
329
+ if (candidate < 0) break;
330
+ const openingEnd = markupEnd(xml, candidate);
331
+ const opening = tagDetails(xml.slice(candidate, openingEnd));
332
+ if (opening.kind !== "start" || opening.name !== elementName) {
333
+ position = openingEnd;
334
+ continue;
335
+ }
336
+ if (opening.selfClosing === true) {
337
+ results.push("");
338
+ position = openingEnd;
339
+ continue;
340
+ }
341
+ let innerPosition = openingEnd;
342
+ let depth = 1;
343
+ const parts = [];
344
+ while (innerPosition < xml.length) {
345
+ const start = xml.indexOf("<", innerPosition);
346
+ if (start < 0) return results;
347
+ parts.push(decodeEntities(xml.slice(innerPosition, start)));
348
+ const end = markupEnd(xml, start);
349
+ const token = xml.slice(start, end);
350
+ if (token.startsWith("<![CDATA[")) parts.push(token.slice(9, -3));
351
+ const details = tagDetails(token);
352
+ if (details.kind === "start" && details.selfClosing !== true) depth += 1;
353
+ if (details.kind === "end") depth -= 1;
354
+ innerPosition = end;
355
+ if (depth === 0) {
356
+ results.push(normalizeText(parts.join("")));
357
+ position = end;
358
+ break;
359
+ }
360
+ }
361
+ if (depth !== 0) break;
362
+ }
363
+ return results;
364
+ }
365
+ function rawElementText(xml, elementName) {
366
+ const result = rawElementTexts(xml, elementName)[0];
367
+ return result === void 0 || result === "" ? void 0 : result;
368
+ }
369
+
370
+ // src/parser/record-mapping.ts
371
+ import { XMLParser } from "fast-xml-parser";
372
+ var parser = new XMLParser({
373
+ ignoreAttributes: false,
374
+ attributeNamePrefix: "@",
375
+ textNodeName: "#text",
376
+ parseTagValue: false,
377
+ parseAttributeValue: false,
378
+ trimValues: false,
379
+ processEntities: false,
380
+ allowBooleanAttributes: false
381
+ });
382
+ function affiliations(author) {
383
+ return list(at(author, "AffiliationInfo")).map((item) => {
384
+ const identifiers3 = list(at(item, "Identifier")).flatMap((identifier) => {
385
+ const value = text(identifier);
386
+ return value === void 0 ? [] : [{ type: attribute(identifier, "Source") ?? "unknown", value, provenance: "citation" }];
387
+ });
388
+ return { text: text(at(item, "Affiliation")) ?? "", identifiers: identifiers3 };
389
+ }).filter((item) => item.text !== "" || item.identifiers.length > 0);
390
+ }
391
+ function authors(value) {
392
+ return list(at(value, "Author")).flatMap((author) => {
393
+ const collectiveName = text(at(author, "CollectiveName"));
394
+ const identifiers3 = list(at(author, "Identifier")).flatMap((identifier) => {
395
+ const identifierText = text(identifier);
396
+ const source = attribute(identifier, "Source");
397
+ return identifierText === void 0 ? [] : [{ value: identifierText, ...source === void 0 ? {} : { source } }];
398
+ });
399
+ const common = { affiliations: affiliations(author), identifiers: identifiers3 };
400
+ if (collectiveName !== void 0) return [{ type: "collective", name: collectiveName, ...common }];
401
+ const lastName = text(at(author, "LastName"));
402
+ const foreName = text(at(author, "ForeName"));
403
+ const initials = text(at(author, "Initials"));
404
+ const suffix = text(at(author, "Suffix"));
405
+ const fullName = [foreName ?? initials, lastName, suffix].filter((part) => part !== void 0).join(" ");
406
+ if (fullName === "") return [];
407
+ const orcid = identifiers3.find((identifier) => identifier.source?.toLowerCase() === "orcid")?.value;
408
+ return [{
409
+ type: "personal",
410
+ fullName,
411
+ ...common,
412
+ ...lastName === void 0 ? {} : { lastName },
413
+ ...foreName === void 0 ? {} : { foreName },
414
+ ...initials === void 0 ? {} : { initials },
415
+ ...suffix === void 0 ? {} : { suffix },
416
+ ...attribute(author, "ValidYN") === void 0 ? {} : { valid: attribute(author, "ValidYN") === "Y" },
417
+ ...orcid === void 0 ? {} : { orcid }
418
+ }];
419
+ });
420
+ }
421
+ function abstracts(value, rawXml) {
422
+ const rawTexts = rawElementTexts(rawXml, "AbstractText");
423
+ return list(at(value, "AbstractText")).flatMap((section, index) => {
424
+ const rawText = rawTexts[index];
425
+ const sectionText = rawText === void 0 || rawText === "" ? text(section) : rawText;
426
+ if (sectionText === void 0) return [];
427
+ const label = attribute(section, "Label");
428
+ const category = attribute(section, "NlmCategory");
429
+ return [{ text: sectionText, ...label === void 0 ? {} : { label }, ...category === void 0 ? {} : { category } }];
430
+ });
431
+ }
432
+ function meshHeadings(value) {
433
+ return list(at(value, "MeshHeading")).flatMap((heading) => {
434
+ const descriptorNode = at(heading, "DescriptorName");
435
+ const descriptor = text(descriptorNode);
436
+ if (descriptor === void 0) return [];
437
+ const descriptorUi = attribute(descriptorNode, "UI");
438
+ const descriptorMajor = attribute(descriptorNode, "MajorTopicYN");
439
+ const qualifiers = list(at(heading, "QualifierName")).flatMap((qualifier) => {
440
+ const name = text(qualifier);
441
+ if (name === void 0) return [];
442
+ const ui = attribute(qualifier, "UI");
443
+ const major = attribute(qualifier, "MajorTopicYN");
444
+ return [{ name, ...ui === void 0 ? {} : { ui }, ...major === void 0 ? {} : { majorTopic: major === "Y" } }];
445
+ });
446
+ return [{ descriptor, qualifiers, ...descriptorUi === void 0 ? {} : { descriptorUi }, ...descriptorMajor === void 0 ? {} : { majorTopic: descriptorMajor === "Y" } }];
447
+ });
448
+ }
449
+ function keywords(value) {
450
+ return list(value).flatMap((keywordList) => {
451
+ const owner = attribute(keywordList, "Owner");
452
+ return list(at(keywordList, "Keyword")).flatMap((keyword) => {
453
+ const found = text(keyword);
454
+ if (found === void 0) return [];
455
+ const major = attribute(keyword, "MajorTopicYN");
456
+ return [{ value: found, ...owner === void 0 ? {} : { owner }, ...major === void 0 ? {} : { majorTopic: major === "Y" } }];
457
+ });
458
+ });
459
+ }
460
+ function identifiers(pmidNode, articleIds, provenance) {
461
+ const result = [];
462
+ const pmid = text(pmidNode);
463
+ if (pmid !== void 0) result.push({ type: "pubmed", value: pmid, provenance: "citation" });
464
+ for (const id of list(at(articleIds, "ArticleId"))) {
465
+ const value = text(id);
466
+ if (value !== void 0) result.push({ type: attribute(id, "IdType") ?? "unknown", value, provenance });
467
+ }
468
+ return result;
469
+ }
470
+ function conveniences(ids) {
471
+ const find = (...types) => ids.find((id) => types.includes(id.type.toLowerCase()))?.value;
472
+ const pmid = find("pubmed", "pmid");
473
+ const doi = find("doi");
474
+ const pmcid = find("pmc", "pmcid");
475
+ return { ...pmid === void 0 ? {} : { pmid }, ...doi === void 0 ? {} : { doi }, ...pmcid === void 0 ? {} : { pmcid } };
476
+ }
477
+ function canonicalLinks(ids) {
478
+ const values2 = conveniences(ids);
479
+ const links = [];
480
+ if (values2.pmid !== void 0 && /^[1-9][0-9]*$/.test(values2.pmid)) {
481
+ links.push({ url: `https://pubmed.ncbi.nlm.nih.gov/${values2.pmid}/`, type: "pubmed", provenance: "canonical" });
482
+ }
483
+ if (values2.doi !== void 0) {
484
+ links.push({ url: `https://doi.org/${encodeURIComponent(values2.doi)}`, type: "doi", provenance: "canonical" });
485
+ }
486
+ if (values2.pmcid !== void 0 && /^PMC[1-9][0-9]*$/i.test(values2.pmcid)) {
487
+ links.push({ url: `https://pmc.ncbi.nlm.nih.gov/articles/${values2.pmcid.toUpperCase()}/`, type: "pmc", provenance: "canonical" });
488
+ }
489
+ return links;
490
+ }
491
+ function dates(citation, article, pubmedData) {
492
+ const history2 = list(at(at(pubmedData, "History"), "PubMedPubDate")).flatMap((entry) => {
493
+ const date = partialDate(entry);
494
+ return date === void 0 ? [] : [{ status: attribute(entry, "PubStatus") ?? "unknown", date }];
495
+ });
496
+ const articleDates = list(at(article, "ArticleDate"));
497
+ const electronic = partialDate(articleDates.find((entry) => (attribute(entry, "DateType") ?? "").toLowerCase() === "electronic"));
498
+ const journalPubDate = partialDate(at(at(at(article, "Journal"), "JournalIssue"), "PubDate"));
499
+ const pubModel = attribute(article, "PubModel")?.toLowerCase();
500
+ const print = pubModel?.includes("print") === true ? journalPubDate : void 0;
501
+ const completed = partialDate(at(citation, "DateCompleted"));
502
+ const revised = partialDate(at(citation, "DateRevised"));
503
+ return {
504
+ history: history2,
505
+ ...completed === void 0 ? {} : { completed },
506
+ ...revised === void 0 ? {} : { revised },
507
+ ...electronic === void 0 ? {} : { electronic },
508
+ ...print === void 0 ? {} : { print }
509
+ };
510
+ }
511
+ function parseArticle(root, rawXml) {
512
+ const citation = at(root, "MedlineCitation");
513
+ const article = at(citation, "Article");
514
+ const pubmedData = at(root, "PubmedData");
515
+ const baseIds = identifiers(at(citation, "PMID"), at(pubmedData, "ArticleIdList"), "article-id");
516
+ const citationIds = [
517
+ ...list(at(article, "ELocationID")).flatMap((identifier) => {
518
+ const value = text(identifier);
519
+ return value === void 0 ? [] : [{ type: attribute(identifier, "EIdType") ?? "elocation", value, provenance: "citation" }];
520
+ }),
521
+ ...list(at(citation, "OtherID")).flatMap((identifier) => {
522
+ const value = text(identifier);
523
+ return value === void 0 ? [] : [{ type: attribute(identifier, "Source") ?? "other", value, provenance: "citation" }];
524
+ })
525
+ ];
526
+ const [firstId, ...remainingIds] = baseIds;
527
+ const ids = firstId === void 0 ? citationIds : [firstId, ...citationIds, ...remainingIds];
528
+ const values2 = conveniences(ids);
529
+ const journalNode = at(article, "Journal");
530
+ const issueNode = at(journalNode, "JournalIssue");
531
+ const journalTitle = text(at(journalNode, "Title"));
532
+ const isoAbbreviation = text(at(journalNode, "ISOAbbreviation"));
533
+ const issn = text(at(journalNode, "ISSN"));
534
+ const issnType = attribute(at(journalNode, "ISSN"), "IssnType");
535
+ const volume = text(at(issueNode, "Volume"));
536
+ const issue = text(at(issueNode, "Issue"));
537
+ const pagination = text(at(at(article, "Pagination"), "MedlinePgn"));
538
+ const pubDate = partialDate(at(issueNode, "PubDate"));
539
+ const title = rawElementText(rawXml, "ArticleTitle") ?? text(at(article, "ArticleTitle"));
540
+ const vernacularTitle = text(at(article, "VernacularTitle"));
541
+ const citationStatus = attribute(citation, "Status");
542
+ const abstractNode = at(article, "Abstract");
543
+ const abstractCopyright = text(at(abstractNode, "CopyrightInformation"));
544
+ const journal2 = {
545
+ ...journalTitle === void 0 ? {} : { title: journalTitle },
546
+ ...isoAbbreviation === void 0 ? {} : { isoAbbreviation },
547
+ ...issn === void 0 ? {} : { issn },
548
+ ...issnType === void 0 ? {} : { issnType },
549
+ ...volume === void 0 ? {} : { volume },
550
+ ...issue === void 0 ? {} : { issue },
551
+ ...pagination === void 0 ? {} : { pagination },
552
+ ...pubDate === void 0 ? {} : { pubDate }
553
+ };
554
+ return {
555
+ kind: "article",
556
+ recordType: "PubmedArticle",
557
+ rawXml,
558
+ source: json(root),
559
+ identifiers: ids,
560
+ links: canonicalLinks(ids),
561
+ abstract: abstracts(abstractNode, rawXml),
562
+ ...abstractCopyright === void 0 ? {} : { abstractCopyright },
563
+ authors: authors(at(article, "AuthorList")),
564
+ languages: list(at(article, "Language")).flatMap((item) => text(item) ?? []),
565
+ publicationTypes: list(at(at(article, "PublicationTypeList"), "PublicationType")).flatMap((item) => text(item) ?? []),
566
+ keywords: keywords(at(citation, "KeywordList")),
567
+ meshHeadings: meshHeadings(at(citation, "MeshHeadingList")),
568
+ dates: dates(citation, article, pubmedData),
569
+ journal: journal2,
570
+ ...values2,
571
+ ...title === void 0 ? {} : { title },
572
+ ...vernacularTitle === void 0 ? {} : { vernacularTitle },
573
+ ...citationStatus === void 0 ? {} : { citationStatus }
574
+ };
575
+ }
576
+ function parseBook(root, rawXml) {
577
+ const document = at(root, "BookDocument");
578
+ const pubmedBookData = at(root, "PubmedBookData");
579
+ const ids = identifiers(at(document, "PMID"), at(pubmedBookData, "ArticleIdList"), "book");
580
+ const values2 = conveniences(ids);
581
+ const bookNode = at(document, "Book");
582
+ const publisher = at(bookNode, "Publisher");
583
+ const bookTitle = text(at(bookNode, "BookTitle"));
584
+ const collectionTitle = text(at(bookNode, "CollectionTitle"));
585
+ const publisherName = text(at(publisher, "PublisherName"));
586
+ const publisherLocation = text(at(publisher, "PublisherLocation"));
587
+ const edition = text(at(bookNode, "Edition"));
588
+ const title = rawElementText(rawXml, "ArticleTitle") ?? text(at(document, "ArticleTitle"));
589
+ const abstractNode = at(document, "Abstract");
590
+ const abstractCopyright = text(at(abstractNode, "CopyrightInformation"));
591
+ return {
592
+ kind: "book",
593
+ recordType: "PubmedBookArticle",
594
+ rawXml,
595
+ source: json(root),
596
+ identifiers: ids,
597
+ links: canonicalLinks(ids),
598
+ abstract: abstracts(abstractNode, rawXml),
599
+ ...abstractCopyright === void 0 ? {} : { abstractCopyright },
600
+ authors: authors(at(document, "AuthorList")),
601
+ languages: list(at(document, "Language")).flatMap((item) => text(item) ?? []),
602
+ publicationTypes: list(at(at(document, "PublicationTypeList"), "PublicationType")).flatMap((item) => text(item) ?? []),
603
+ keywords: keywords(at(document, "KeywordList")),
604
+ meshHeadings: [],
605
+ dates: dates(document, document, pubmedBookData),
606
+ book: {
607
+ isbn: list(at(bookNode, "Isbn")).flatMap((item) => text(item) ?? []),
608
+ ...bookTitle === void 0 ? {} : { title: bookTitle },
609
+ ...collectionTitle === void 0 ? {} : { collectionTitle },
610
+ ...publisherName === void 0 ? {} : { publisher: publisherName },
611
+ ...publisherLocation === void 0 ? {} : { location: publisherLocation },
612
+ ...edition === void 0 ? {} : { edition }
613
+ },
614
+ ...values2,
615
+ ...title === void 0 ? {} : { title }
616
+ };
617
+ }
618
+ function parseFragment(fragment) {
619
+ let parsed;
620
+ try {
621
+ parsed = parser.parse(fragment.rawXml);
622
+ } catch {
623
+ throw new ParseError();
624
+ }
625
+ const document = object(parsed);
626
+ const rootValue = document?.[fragment.name];
627
+ const root = object(rootValue);
628
+ if (fragment.name === "PubmedArticle") {
629
+ if (root === void 0) throw new ParseError();
630
+ return parseArticle(root, fragment.rawXml);
631
+ }
632
+ if (fragment.name === "PubmedBookArticle") {
633
+ if (root === void 0) throw new ParseError();
634
+ return parseBook(root, fragment.rawXml);
635
+ }
636
+ const source = root === void 0 ? { value: json(rootValue) } : json(root);
637
+ return {
638
+ kind: "unknown",
639
+ recordType: fragment.name,
640
+ rawXml: fragment.rawXml,
641
+ source,
642
+ identifiers: [],
643
+ links: [],
644
+ abstract: [],
645
+ authors: [],
646
+ languages: [],
647
+ publicationTypes: [],
648
+ keywords: [],
649
+ meshHeadings: [],
650
+ dates: { history: [] }
651
+ };
652
+ }
653
+
654
+ // src/parser.ts
655
+ function parsePubMedXml(xml) {
656
+ const records = extractFragments(xml).map(parseFragment);
657
+ const warnings = records.flatMap((record) => record.kind === "unknown" ? [{ code: "UNKNOWN_RECORD", message: `Unknown PubMed record type: ${record.recordType}`, recordType: record.recordType }] : []);
658
+ return { records, warnings };
659
+ }
660
+
661
+ // src/rate-limiter.ts
662
+ var SharedLimiter = class {
663
+ #intervalMs;
664
+ #queue = [];
665
+ #nextAvailable = 0;
666
+ #cooldownUntil = 0;
667
+ #timer;
668
+ constructor(requestsPerSecond) {
669
+ this.#intervalMs = 1e3 / requestsPerSecond;
670
+ }
671
+ acquire(maxQueue, signal, onEvent) {
672
+ if (signal?.aborted === true) return Promise.reject(new AbortedError());
673
+ if (this.#queue.length >= maxQueue) return Promise.reject(new QueueFullError());
674
+ return new Promise((resolve, reject) => {
675
+ const item = { enqueuedAt: Date.now(), resolve, reject, ...signal === void 0 ? {} : { signal } };
676
+ if (signal !== void 0) {
677
+ item.abortListener = () => {
678
+ const index = this.#queue.indexOf(item);
679
+ if (index < 0) return;
680
+ this.#queue.splice(index, 1);
681
+ reject(new AbortedError());
682
+ this.#schedule(onEvent);
683
+ };
684
+ signal.addEventListener("abort", item.abortListener, { once: true });
685
+ }
686
+ this.#queue.push(item);
687
+ this.#schedule(onEvent);
688
+ });
689
+ }
690
+ cooldown(delayMs, onEvent) {
691
+ this.#cooldownUntil = Math.max(this.#cooldownUntil, Date.now() + delayMs);
692
+ safeEvent(onEvent, { type: "rate-cooldown", delayMs });
693
+ this.#schedule(onEvent);
694
+ }
695
+ isIdle(now) {
696
+ return this.#queue.length === 0 && this.#timer === void 0 && this.#nextAvailable <= now && this.#cooldownUntil <= now;
697
+ }
698
+ #schedule(onEvent) {
699
+ if (this.#timer !== void 0) {
700
+ clearTimeout(this.#timer);
701
+ this.#timer = void 0;
702
+ }
703
+ if (this.#queue.length === 0) return;
704
+ const now = Date.now();
705
+ const at2 = Math.max(now, this.#nextAvailable, this.#cooldownUntil);
706
+ const delay = Math.max(0, at2 - now);
707
+ this.#timer = setTimeout(() => {
708
+ this.#timer = void 0;
709
+ if (Date.now() < at2) {
710
+ this.#schedule(onEvent);
711
+ return;
712
+ }
713
+ const item = this.#queue.shift();
714
+ if (item === void 0) return;
715
+ if (item.abortListener !== void 0 && item.signal !== void 0) {
716
+ item.signal.removeEventListener("abort", item.abortListener);
717
+ }
718
+ if (item.signal?.aborted === true) {
719
+ item.reject(new AbortedError());
720
+ } else {
721
+ const waited = Date.now() - item.enqueuedAt;
722
+ if (waited > 0) safeEvent(onEvent, { type: "queue-delay", delayMs: waited });
723
+ this.#nextAvailable = Math.max(Date.now(), this.#nextAvailable) + this.#intervalMs;
724
+ item.resolve();
725
+ }
726
+ this.#schedule(onEvent);
727
+ }, Math.min(delay, 2147e6));
728
+ }
729
+ };
730
+ var REGISTRY_SYMBOL = /* @__PURE__ */ Symbol.for("@everdeep/pubmed/shared-rate-limiters/v1");
731
+ var KEYED_OVERFLOW_SYMBOL = /* @__PURE__ */ Symbol.for("@everdeep/pubmed/shared-rate-limiters/overflow/keyed/v1");
732
+ var NO_KEY_OVERFLOW_SYMBOL = /* @__PURE__ */ Symbol.for("@everdeep/pubmed/shared-rate-limiters/overflow/no-key/v1");
733
+ var HOST = "eutils.ncbi.nlm.nih.gov";
734
+ var MAX_LIMITER_REGISTRY_ENTRIES = 256;
735
+ var LIMITER_IDLE_TTL_MS = 10 * 6e4;
736
+ function limiterRegistry() {
737
+ const existing = Reflect.get(globalThis, REGISTRY_SYMBOL);
738
+ if (existing instanceof Map) return existing;
739
+ const registry = /* @__PURE__ */ new Map();
740
+ Reflect.set(globalThis, REGISTRY_SYMBOL, registry);
741
+ return registry;
742
+ }
743
+ function canEvict(entry, now) {
744
+ return entry.activeAdmissions === 0 && entry.limiter.isIdle(now);
745
+ }
746
+ function evictOne(registry, now, protectedKey) {
747
+ for (const [key, entry] of registry) {
748
+ if (key === protectedKey || !canEvict(entry, now)) continue;
749
+ registry.delete(key);
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+ function pruneRegistry(registry, now, protectedKey) {
755
+ for (const [key, entry] of registry) {
756
+ if (key !== protectedKey && now - entry.lastUsedAt >= LIMITER_IDLE_TTL_MS && canEvict(entry, now)) registry.delete(key);
757
+ }
758
+ while (registry.size > MAX_LIMITER_REGISTRY_ENTRIES && evictOne(registry, now, protectedKey)) {
759
+ }
760
+ }
761
+ function isLimiterRegistryEntry(value) {
762
+ if (typeof value !== "object" || value === null || !("limiter" in value) || !("activeAdmissions" in value) || !("lastUsedAt" in value)) {
763
+ return false;
764
+ }
765
+ const limiter = value.limiter;
766
+ return typeof value.activeAdmissions === "number" && typeof value.lastUsedAt === "number" && typeof limiter === "object" && limiter !== null && "acquire" in limiter && typeof limiter.acquire === "function" && "cooldown" in limiter && typeof limiter.cooldown === "function" && "isIdle" in limiter && typeof limiter.isIdle === "function";
767
+ }
768
+ function overflowLimiterEntry(keyed, requestsPerSecond) {
769
+ const symbol = keyed ? KEYED_OVERFLOW_SYMBOL : NO_KEY_OVERFLOW_SYMBOL;
770
+ const existing = Reflect.get(globalThis, symbol);
771
+ if (isLimiterRegistryEntry(existing)) return existing;
772
+ const created = { limiter: new SharedLimiter(requestsPerSecond), activeAdmissions: 0, lastUsedAt: Date.now() };
773
+ Reflect.set(globalThis, symbol, created);
774
+ return created;
775
+ }
776
+ function limiterEntry(registryKey, requestsPerSecond, keyed) {
777
+ const registry = limiterRegistry();
778
+ const now = Date.now();
779
+ const existing = registry.get(registryKey);
780
+ if (existing !== void 0 && now - existing.lastUsedAt < LIMITER_IDLE_TTL_MS) {
781
+ existing.lastUsedAt = now;
782
+ registry.delete(registryKey);
783
+ registry.set(registryKey, existing);
784
+ pruneRegistry(registry, now, registryKey);
785
+ return { entry: existing, overflow: false };
786
+ }
787
+ if (existing !== void 0 && canEvict(existing, now)) registry.delete(registryKey);
788
+ const current = registry.get(registryKey);
789
+ if (current !== void 0) {
790
+ current.lastUsedAt = now;
791
+ return { entry: current, overflow: false };
792
+ }
793
+ pruneRegistry(registry, now);
794
+ if (registry.size >= MAX_LIMITER_REGISTRY_ENTRIES && !evictOne(registry, now)) {
795
+ return { entry: overflowLimiterEntry(keyed, requestsPerSecond), overflow: true };
796
+ }
797
+ const created = { limiter: new SharedLimiter(requestsPerSecond), activeAdmissions: 0, lastUsedAt: now };
798
+ registry.set(registryKey, created);
799
+ return { entry: created, overflow: false };
800
+ }
801
+ async function waitWithAbort(promise, signal) {
802
+ if (signal === void 0) {
803
+ await promise;
804
+ return;
805
+ }
806
+ if (signal.aborted) throw new AbortedError();
807
+ await new Promise((resolve, reject) => {
808
+ let finished = false;
809
+ const finish = (complete) => {
810
+ if (finished) return;
811
+ finished = true;
812
+ signal.removeEventListener("abort", abort);
813
+ complete();
814
+ };
815
+ const abort = () => finish(() => reject(new AbortedError()));
816
+ signal.addEventListener("abort", abort, { once: true });
817
+ void promise.then(
818
+ () => finish(resolve),
819
+ (error) => finish(() => reject(error))
820
+ );
821
+ });
822
+ }
823
+ function safeEvent(handler, event) {
824
+ if (handler === void 0) return;
825
+ try {
826
+ handler(event);
827
+ } catch {
828
+ }
829
+ }
830
+ var RequestRateLimiter = class {
831
+ #registryKey;
832
+ #requestsPerSecond;
833
+ #keyed;
834
+ #bucket;
835
+ #maxQueue;
836
+ #coordinator;
837
+ #onEvent;
838
+ #overflowEntry;
839
+ constructor(credentialFingerprint, keyed, maxQueue, coordinator, onEvent) {
840
+ this.#requestsPerSecond = keyed ? 9 : 2.8;
841
+ this.#keyed = keyed;
842
+ this.#registryKey = `${HOST}:${credentialFingerprint}`;
843
+ const selection = limiterEntry(this.#registryKey, this.#requestsPerSecond, this.#keyed);
844
+ if (selection.overflow) this.#overflowEntry = selection.entry;
845
+ this.#bucket = { host: HOST, credentialFingerprint, requestsPerSecond: this.#requestsPerSecond };
846
+ this.#maxQueue = maxQueue;
847
+ this.#coordinator = coordinator;
848
+ this.#onEvent = onEvent;
849
+ }
850
+ #entry() {
851
+ if (this.#overflowEntry !== void 0) return this.#overflowEntry;
852
+ const selection = limiterEntry(this.#registryKey, this.#requestsPerSecond, this.#keyed);
853
+ if (selection.overflow) this.#overflowEntry = selection.entry;
854
+ return selection.entry;
855
+ }
856
+ async acquire(signal) {
857
+ const entry = this.#entry();
858
+ entry.activeAdmissions += 1;
859
+ try {
860
+ await entry.limiter.acquire(this.#maxQueue, signal, this.#onEvent);
861
+ if (this.#coordinator !== void 0) await waitWithAbort(this.#coordinator.acquire(this.#bucket, signal), signal);
862
+ } finally {
863
+ entry.activeAdmissions -= 1;
864
+ entry.lastUsedAt = Date.now();
865
+ pruneRegistry(limiterRegistry(), entry.lastUsedAt, this.#registryKey);
866
+ }
867
+ }
868
+ async cooldown(delayMs, signal, waitForCoordinator = true) {
869
+ const entry = this.#entry();
870
+ entry.activeAdmissions += 1;
871
+ try {
872
+ entry.limiter.cooldown(delayMs, this.#onEvent);
873
+ if (this.#coordinator?.cooldown !== void 0) {
874
+ if (waitForCoordinator) {
875
+ await waitWithAbort(this.#coordinator.cooldown(this.#bucket, delayMs), signal);
876
+ } else {
877
+ try {
878
+ void this.#coordinator.cooldown(this.#bucket, delayMs).catch(() => {
879
+ });
880
+ } catch {
881
+ }
882
+ }
883
+ }
884
+ } finally {
885
+ entry.activeAdmissions -= 1;
886
+ entry.lastUsedAt = Date.now();
887
+ pruneRegistry(limiterRegistry(), entry.lastUsedAt, this.#registryKey);
888
+ }
889
+ }
890
+ };
891
+
892
+ // src/summary.ts
893
+ var PMID_PATTERN = /^[1-9][0-9]*$/;
894
+ function object2(value) {
895
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
896
+ }
897
+ function scalarText(value) {
898
+ if (typeof value !== "string") return void 0;
899
+ const normalized = value.replace(/\s+/g, " ").trim();
900
+ return normalized === "" ? void 0 : normalized;
901
+ }
902
+ function items(value) {
903
+ if (value === void 0 || value === null) return [];
904
+ return Array.isArray(value) ? value : [value];
905
+ }
906
+ function stringItems(value) {
907
+ return items(value).flatMap((item) => scalarText(item) ?? []);
908
+ }
909
+ function toJsonObject(value) {
910
+ return value;
911
+ }
912
+ function authors2(value) {
913
+ return items(value).flatMap((item) => {
914
+ const author = object2(item);
915
+ const name = scalarText(author?.name);
916
+ if (name === void 0) return [];
917
+ const type = scalarText(author?.authtype);
918
+ const clusterId = scalarText(author?.clusterid);
919
+ return [{
920
+ name,
921
+ ...type === void 0 ? {} : { type },
922
+ ...clusterId === void 0 ? {} : { clusterId }
923
+ }];
924
+ });
925
+ }
926
+ function identifiers2(value, pmid) {
927
+ const found = items(value).flatMap((item) => {
928
+ const identifier = object2(item);
929
+ const type = scalarText(identifier?.idtype);
930
+ const identifierValue2 = scalarText(identifier?.value);
931
+ if (type === void 0 || identifierValue2 === void 0) return [];
932
+ const numericType = identifier?.idtypen;
933
+ return [{
934
+ type,
935
+ value: identifierValue2,
936
+ ...typeof numericType === "number" && Number.isSafeInteger(numericType) ? { numericType } : {}
937
+ }];
938
+ });
939
+ return found.some((identifier) => ["pubmed", "pmid"].includes(identifier.type.toLowerCase())) ? found : [{ type: "pubmed", value: pmid }, ...found];
940
+ }
941
+ function history(value) {
942
+ return items(value).flatMap((item) => {
943
+ const entry = object2(item);
944
+ const status = scalarText(entry?.pubstatus);
945
+ const date = scalarText(entry?.date);
946
+ return status === void 0 || date === void 0 ? [] : [{ status, date }];
947
+ });
948
+ }
949
+ function journal(record) {
950
+ const title = scalarText(record.fulljournalname);
951
+ const abbreviation = scalarText(record.source);
952
+ const issn = scalarText(record.issn);
953
+ const electronicIssn = scalarText(record.essn);
954
+ const volume = scalarText(record.volume);
955
+ const issue = scalarText(record.issue);
956
+ const pages = scalarText(record.pages);
957
+ if ((title ?? abbreviation ?? issn ?? electronicIssn ?? volume ?? issue ?? pages) === void 0) return void 0;
958
+ return {
959
+ ...title === void 0 ? {} : { title },
960
+ ...abbreviation === void 0 ? {} : { abbreviation },
961
+ ...issn === void 0 ? {} : { issn },
962
+ ...electronicIssn === void 0 ? {} : { electronicIssn },
963
+ ...volume === void 0 ? {} : { volume },
964
+ ...issue === void 0 ? {} : { issue },
965
+ ...pages === void 0 ? {} : { pages }
966
+ };
967
+ }
968
+ function book(record) {
969
+ const title = scalarText(record.booktitle);
970
+ const name = scalarText(record.bookname);
971
+ const chapter = scalarText(record.chapter);
972
+ const edition = scalarText(record.edition);
973
+ const publisher = scalarText(record.publishername);
974
+ const location = scalarText(record.publisherlocation);
975
+ if ((title ?? name ?? chapter ?? edition ?? publisher ?? location) === void 0) return void 0;
976
+ return {
977
+ ...title === void 0 ? {} : { title },
978
+ ...name === void 0 ? {} : { name },
979
+ ...chapter === void 0 ? {} : { chapter },
980
+ ...edition === void 0 ? {} : { edition },
981
+ ...publisher === void 0 ? {} : { publisher },
982
+ ...location === void 0 ? {} : { location }
983
+ };
984
+ }
985
+ function mapSummary(record, pmid) {
986
+ const mappedIdentifiers = identifiers2(record.articleids, pmid);
987
+ const findIdentifier = (...types) => mappedIdentifiers.find((identifier) => types.includes(identifier.type.toLowerCase()))?.value;
988
+ const title = scalarText(record.title);
989
+ const sortTitle = scalarText(record.sorttitle);
990
+ const lastAuthor = scalarText(record.lastauthor);
991
+ const sortFirstAuthor = scalarText(record.sortfirstauthor);
992
+ const publicationDate = scalarText(record.pubdate);
993
+ const electronicPublicationDate = scalarText(record.epubdate);
994
+ const sortPublicationDate = scalarText(record.sortpubdate);
995
+ const electronicLocationId = scalarText(record.elocationid);
996
+ const sourceDate = scalarText(record.srcdate);
997
+ const documentDate = scalarText(record.docdate);
998
+ const recordStatus = scalarText(record.recordstatus);
999
+ const publicationStatus = scalarText(record.pubstatus);
1000
+ const documentType = scalarText(record.doctype);
1001
+ const medium = scalarText(record.medium);
1002
+ const edition = scalarText(record.edition);
1003
+ const publisher = scalarText(record.publishername);
1004
+ const publisherLocation = scalarText(record.publisherlocation);
1005
+ const reportNumber = scalarText(record.reportnumber);
1006
+ const availableFromUrl = scalarText(record.availablefromurl);
1007
+ const doi = findIdentifier("doi");
1008
+ const pmcid = findIdentifier("pmc", "pmcid");
1009
+ const mappedJournal = journal(record);
1010
+ const mappedBook = book(record);
1011
+ return {
1012
+ kind: "summary",
1013
+ uid: pmid,
1014
+ pmid,
1015
+ source: toJsonObject(record),
1016
+ authors: authors2(record.authors),
1017
+ languages: stringItems(record.lang),
1018
+ publicationTypes: stringItems(record.pubtype),
1019
+ identifiers: mappedIdentifiers,
1020
+ history: history(record.history),
1021
+ ...title === void 0 ? {} : { title },
1022
+ ...sortTitle === void 0 ? {} : { sortTitle },
1023
+ ...lastAuthor === void 0 ? {} : { lastAuthor },
1024
+ ...sortFirstAuthor === void 0 ? {} : { sortFirstAuthor },
1025
+ ...mappedJournal === void 0 ? {} : { journal: mappedJournal },
1026
+ ...mappedBook === void 0 ? {} : { book: mappedBook },
1027
+ ...publicationDate === void 0 ? {} : { publicationDate },
1028
+ ...electronicPublicationDate === void 0 ? {} : { electronicPublicationDate },
1029
+ ...sortPublicationDate === void 0 ? {} : { sortPublicationDate },
1030
+ ...electronicLocationId === void 0 ? {} : { electronicLocationId },
1031
+ ...sourceDate === void 0 ? {} : { sourceDate },
1032
+ ...documentDate === void 0 ? {} : { documentDate },
1033
+ ...doi === void 0 ? {} : { doi },
1034
+ ...pmcid === void 0 ? {} : { pmcid },
1035
+ ...recordStatus === void 0 ? {} : { recordStatus },
1036
+ ...publicationStatus === void 0 ? {} : { publicationStatus },
1037
+ ...documentType === void 0 ? {} : { documentType },
1038
+ ...medium === void 0 ? {} : { medium },
1039
+ ...edition === void 0 ? {} : { edition },
1040
+ ...publisher === void 0 ? {} : { publisher },
1041
+ ...publisherLocation === void 0 ? {} : { publisherLocation },
1042
+ ...reportNumber === void 0 ? {} : { reportNumber },
1043
+ ...availableFromUrl === void 0 ? {} : { availableFromUrl }
1044
+ };
1045
+ }
1046
+ function parseESummaryJson(body, expectedPmids) {
1047
+ let parsed;
1048
+ try {
1049
+ parsed = JSON.parse(body);
1050
+ } catch (error) {
1051
+ throw new InvalidResponseError("PubMed returned invalid summary metadata", { cause: error });
1052
+ }
1053
+ const result = object2(object2(parsed)?.result);
1054
+ const rawUids = result?.uids;
1055
+ if (result === void 0 || !Array.isArray(rawUids)) {
1056
+ throw new InvalidResponseError("PubMed returned invalid summary metadata");
1057
+ }
1058
+ const expected = new Set(expectedPmids);
1059
+ const seen = /* @__PURE__ */ new Set();
1060
+ const summaries = [];
1061
+ for (const rawUid of rawUids) {
1062
+ if (typeof rawUid !== "string" || !PMID_PATTERN.test(rawUid) || !expected.has(rawUid) || seen.has(rawUid)) {
1063
+ throw new InvalidResponseError("PubMed returned summaries that did not match the requested PMIDs");
1064
+ }
1065
+ const record = object2(result[rawUid]);
1066
+ if (record === void 0 || record.uid !== rawUid) {
1067
+ throw new InvalidResponseError("PubMed returned summaries that did not match the requested PMIDs");
1068
+ }
1069
+ seen.add(rawUid);
1070
+ if (scalarText(record.error) !== void 0) continue;
1071
+ summaries.push(mapSummary(record, rawUid));
1072
+ }
1073
+ if (Object.keys(result).some((key) => PMID_PATTERN.test(key) && !seen.has(key))) {
1074
+ throw new InvalidResponseError("PubMed returned summaries that did not match the requested PMIDs");
1075
+ }
1076
+ return summaries;
1077
+ }
1078
+
1079
+ // src/transport.ts
1080
+ import { createHash, randomUUID } from "crypto";
1081
+
1082
+ // src/transport/cache-lifecycle.ts
1083
+ var INVALID_CACHE_KEY_LIMIT = 1e3;
1084
+ var INVALID_CACHE_KEY_TTL_MS = 5 * 6e4;
1085
+ var PENDING_CACHE_WRITE_LIMIT = 100;
1086
+ var CACHE_MUTATION_TOKEN_LIMIT = INVALID_CACHE_KEY_LIMIT + PENDING_CACHE_WRITE_LIMIT;
1087
+ var InvalidCacheTracker = class {
1088
+ #entries = /* @__PURE__ */ new Map();
1089
+ #maxEntries;
1090
+ #ttlMs;
1091
+ constructor(maxEntries = INVALID_CACHE_KEY_LIMIT, ttlMs = INVALID_CACHE_KEY_TTL_MS) {
1092
+ this.#maxEntries = maxEntries;
1093
+ this.#ttlMs = ttlMs;
1094
+ }
1095
+ has(key) {
1096
+ const expiresAt = this.#entries.get(key);
1097
+ if (expiresAt === void 0) return false;
1098
+ if (expiresAt <= Date.now()) {
1099
+ this.#entries.delete(key);
1100
+ return false;
1101
+ }
1102
+ this.#entries.delete(key);
1103
+ this.#entries.set(key, expiresAt);
1104
+ return true;
1105
+ }
1106
+ add(key) {
1107
+ const now = Date.now();
1108
+ for (const [cachedKey, expiresAt] of this.#entries) {
1109
+ if (expiresAt <= now) this.#entries.delete(cachedKey);
1110
+ }
1111
+ this.#entries.delete(key);
1112
+ this.#entries.set(key, now + this.#ttlMs);
1113
+ while (this.#entries.size > this.#maxEntries) {
1114
+ const oldest = this.#entries.keys().next();
1115
+ if (oldest.done) break;
1116
+ this.#entries.delete(oldest.value);
1117
+ }
1118
+ }
1119
+ delete(key) {
1120
+ this.#entries.delete(key);
1121
+ }
1122
+ get size() {
1123
+ return this.#entries.size;
1124
+ }
1125
+ };
1126
+ async function waitWithAbort2(promise, signal) {
1127
+ if (signal === void 0) return promise;
1128
+ if (signal.aborted) throw new AbortedError();
1129
+ return new Promise((resolve, reject) => {
1130
+ let finished = false;
1131
+ const finish = (complete) => {
1132
+ if (finished) return;
1133
+ finished = true;
1134
+ signal.removeEventListener("abort", abort);
1135
+ complete();
1136
+ };
1137
+ const abort = () => finish(() => reject(new AbortedError()));
1138
+ signal.addEventListener("abort", abort, { once: true });
1139
+ void promise.then(
1140
+ (value) => finish(() => resolve(value)),
1141
+ (error) => finish(() => reject(error))
1142
+ );
1143
+ });
1144
+ }
1145
+ var CacheLifecycle = class {
1146
+ #cache;
1147
+ #invalidCacheKeys = new InvalidCacheTracker();
1148
+ #pendingCacheDeletes = /* @__PURE__ */ new Map();
1149
+ #pendingCacheWrites = /* @__PURE__ */ new Set();
1150
+ #pendingCacheMutationsByKey = /* @__PURE__ */ new Map();
1151
+ #latestCacheMutation = /* @__PURE__ */ new Map();
1152
+ constructor(cache) {
1153
+ this.#cache = cache;
1154
+ }
1155
+ async read(key, signal) {
1156
+ if (this.#cache === void 0 || this.#invalidCacheKeys.has(key)) return { status: "disabled" };
1157
+ try {
1158
+ const value = await waitWithAbort2(this.#cache.get(key), signal);
1159
+ return value === void 0 ? { status: "miss" } : { status: "hit", value };
1160
+ } catch (error) {
1161
+ if (error instanceof AbortedError) throw error;
1162
+ return { status: "miss" };
1163
+ }
1164
+ }
1165
+ async invalidate(key) {
1166
+ this.#invalidCacheKeys.add(key);
1167
+ if (this.#pendingCacheDeletes.has(key)) return;
1168
+ this.#beginMutation(key);
1169
+ if (this.#cache?.delete === void 0 || this.#pendingCacheDeletes.size >= INVALID_CACHE_KEY_LIMIT) return;
1170
+ const pendingMutation = this.#pendingCacheMutationsByKey.get(key);
1171
+ const deletion = this.#deleteAfterMutation(key, pendingMutation);
1172
+ this.#pendingCacheDeletes.set(key, deletion);
1173
+ this.#pendingCacheMutationsByKey.set(key, deletion);
1174
+ void this.#clearPendingDelete(key, deletion);
1175
+ }
1176
+ write(key, body) {
1177
+ if (this.#cache === void 0) return;
1178
+ const token = this.#beginMutation(key);
1179
+ if (this.#pendingCacheWrites.size >= PENDING_CACHE_WRITE_LIMIT) return;
1180
+ const pendingMutation = this.#pendingCacheMutationsByKey.get(key);
1181
+ const write = this.#performWrite(key, body, token, pendingMutation);
1182
+ this.#pendingCacheWrites.add(write);
1183
+ this.#pendingCacheMutationsByKey.set(key, write);
1184
+ void write.then(() => {
1185
+ this.#pendingCacheWrites.delete(write);
1186
+ if (this.#pendingCacheMutationsByKey.get(key) === write) this.#pendingCacheMutationsByKey.delete(key);
1187
+ });
1188
+ }
1189
+ #beginMutation(key) {
1190
+ const token = {};
1191
+ this.#latestCacheMutation.delete(key);
1192
+ this.#latestCacheMutation.set(key, token);
1193
+ while (this.#latestCacheMutation.size > CACHE_MUTATION_TOKEN_LIMIT) {
1194
+ const oldest = this.#latestCacheMutation.keys().next();
1195
+ if (oldest.done) break;
1196
+ this.#latestCacheMutation.delete(oldest.value);
1197
+ }
1198
+ return token;
1199
+ }
1200
+ async #deleteAfterMutation(key, pendingMutation) {
1201
+ await pendingMutation;
1202
+ await this.#delete(key);
1203
+ }
1204
+ async #delete(key) {
1205
+ try {
1206
+ await this.#cache?.delete?.(key);
1207
+ } catch {
1208
+ }
1209
+ }
1210
+ async #clearPendingDelete(key, deletion) {
1211
+ await deletion;
1212
+ if (this.#pendingCacheDeletes.get(key) === deletion) this.#pendingCacheDeletes.delete(key);
1213
+ if (this.#pendingCacheMutationsByKey.get(key) === deletion) this.#pendingCacheMutationsByKey.delete(key);
1214
+ }
1215
+ async #performWrite(key, body, token, pendingMutation) {
1216
+ await pendingMutation;
1217
+ try {
1218
+ await this.#cache?.set(key, body);
1219
+ if (this.#latestCacheMutation.get(key) === token) {
1220
+ this.#latestCacheMutation.delete(key);
1221
+ this.#invalidCacheKeys.delete(key);
1222
+ } else {
1223
+ await this.#delete(key);
1224
+ }
1225
+ } catch {
1226
+ }
1227
+ }
1228
+ };
1229
+
1230
+ // src/transport/http-execution.ts
1231
+ var ORIGIN = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/";
1232
+ var MAX_RETRY_AFTER_MS = 5 * 6e4;
1233
+ var HTTP_DATE_PATTERN = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2} \d{2}:\d{2}:\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?: \d|\d{2}) \d{2}:\d{2}:\d{2} \d{4})$/;
1234
+ function boundedRetryAfter(delayMs) {
1235
+ return {
1236
+ delayMs: Math.min(delayMs, MAX_RETRY_AFTER_MS),
1237
+ exceedsMaximum: delayMs > MAX_RETRY_AFTER_MS
1238
+ };
1239
+ }
1240
+ function retryAfter(headers) {
1241
+ const raw = headers.get("retry-after")?.trim();
1242
+ if (raw === void 0 || raw.length === 0) return void 0;
1243
+ if (/^\d+$/.test(raw)) {
1244
+ const seconds = Number(raw);
1245
+ if (!Number.isFinite(seconds) || seconds > MAX_RETRY_AFTER_MS / 1e3) {
1246
+ return { delayMs: MAX_RETRY_AFTER_MS, exceedsMaximum: true };
1247
+ }
1248
+ return boundedRetryAfter(seconds * 1e3);
1249
+ }
1250
+ if (!HTTP_DATE_PATTERN.test(raw)) return void 0;
1251
+ const date = Date.parse(raw);
1252
+ return Number.isFinite(date) ? boundedRetryAfter(Math.max(0, date - Date.now())) : void 0;
1253
+ }
1254
+ async function sleep(delayMs, signal) {
1255
+ const deadline = Date.now() + delayMs;
1256
+ while (Date.now() < deadline) {
1257
+ const remaining = Math.min(deadline - Date.now(), 2147e6);
1258
+ await new Promise((resolve, reject) => {
1259
+ const timer = setTimeout(() => {
1260
+ signal.removeEventListener("abort", abort);
1261
+ resolve();
1262
+ }, remaining);
1263
+ const abort = () => {
1264
+ clearTimeout(timer);
1265
+ signal.removeEventListener("abort", abort);
1266
+ reject(new AbortedError());
1267
+ };
1268
+ signal.addEventListener("abort", abort, { once: true });
1269
+ if (signal.aborted) abort();
1270
+ });
1271
+ }
1272
+ }
1273
+ function discardBody(response) {
1274
+ if (response.body === null) return;
1275
+ void response.body.cancel().catch(() => {
1276
+ });
1277
+ }
1278
+ async function readCapped(response, limitBytes) {
1279
+ const declared = Number(response.headers.get("content-length"));
1280
+ if (Number.isFinite(declared) && declared > limitBytes) throw new ResponseTooLargeError(limitBytes);
1281
+ if (response.body === null) return { body: "", bytes: 0 };
1282
+ const reader = response.body.getReader();
1283
+ const decoder = new TextDecoder();
1284
+ const parts = [];
1285
+ let total = 0;
1286
+ try {
1287
+ while (true) {
1288
+ const result = await reader.read();
1289
+ if (result.done) break;
1290
+ total += result.value.byteLength;
1291
+ if (total > limitBytes) {
1292
+ await reader.cancel();
1293
+ throw new ResponseTooLargeError(limitBytes);
1294
+ }
1295
+ parts.push(decoder.decode(result.value, { stream: true }));
1296
+ }
1297
+ parts.push(decoder.decode());
1298
+ return { body: parts.join(""), bytes: total };
1299
+ } finally {
1300
+ reader.releaseLock();
1301
+ }
1302
+ }
1303
+ var HttpExecutor = class {
1304
+ #email;
1305
+ #tool;
1306
+ #apiKey;
1307
+ #fetch;
1308
+ #timeoutMs;
1309
+ #maxAttempts;
1310
+ #maxResponseBytes;
1311
+ #limiter;
1312
+ #onEvent;
1313
+ constructor(options) {
1314
+ this.#email = options.email;
1315
+ this.#tool = options.tool;
1316
+ this.#apiKey = options.apiKey;
1317
+ this.#fetch = options.fetch;
1318
+ this.#timeoutMs = options.timeoutMs;
1319
+ this.#maxAttempts = options.maxAttempts;
1320
+ this.#maxResponseBytes = options.maxResponseBytes;
1321
+ this.#limiter = options.limiter;
1322
+ this.#onEvent = options.onEvent;
1323
+ }
1324
+ async execute(endpoint, parameters, operationSignal, decode, correlationId) {
1325
+ let lastError = new NetworkError();
1326
+ for (let attempt = 1; attempt <= this.#maxAttempts; attempt += 1) {
1327
+ if (operationSignal.aborted) throw new AbortedError();
1328
+ await this.#limiter.acquire(operationSignal);
1329
+ if (operationSignal.aborted) throw new AbortedError();
1330
+ const controller = new AbortController();
1331
+ let timedOut = false;
1332
+ const abortAttempt = () => controller.abort();
1333
+ operationSignal.addEventListener("abort", abortAttempt, { once: true });
1334
+ const timer = setTimeout(() => {
1335
+ timedOut = true;
1336
+ controller.abort();
1337
+ }, this.#timeoutMs);
1338
+ const started = Date.now();
1339
+ let retryDelay;
1340
+ let retryReason = "network";
1341
+ let stopAutomaticRetry = false;
1342
+ try {
1343
+ const form = new URLSearchParams({ db: "pubmed", ...parameters, tool: this.#tool, email: this.#email });
1344
+ if (this.#apiKey !== void 0) form.set("api_key", this.#apiKey);
1345
+ const path = `${ORIGIN}${endpoint}.fcgi`;
1346
+ const response = await this.#fetch(path, {
1347
+ method: "POST",
1348
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1349
+ body: form.toString(),
1350
+ signal: controller.signal
1351
+ });
1352
+ safeEvent(this.#onEvent, { type: "request", endpoint, status: response.status, durationMs: Date.now() - started, attempt, correlationId });
1353
+ if (response.ok) {
1354
+ const capped = await readCapped(response, this.#maxResponseBytes);
1355
+ safeEvent(this.#onEvent, { type: "response-bytes", endpoint, correlationId, attempt, bytes: capped.bytes });
1356
+ if (endpoint === "efetch" && /<(?:ERROR|Error)>[\s\S]*(?:history|webenv|query\s*key)/i.test(capped.body)) {
1357
+ throw new CursorExpiredError();
1358
+ }
1359
+ const value = decode(capped.body);
1360
+ if (operationSignal.aborted) throw new AbortedError();
1361
+ return { value, body: capped.body };
1362
+ }
1363
+ discardBody(response);
1364
+ if (response.status === 429) {
1365
+ const serverDelay = retryAfter(response.headers);
1366
+ retryDelay = serverDelay?.delayMs ?? Math.random() * 1e3 * 2 ** (attempt - 1);
1367
+ stopAutomaticRetry = serverDelay?.exceedsMaximum ?? false;
1368
+ await this.#limiter.cooldown(retryDelay, operationSignal, !stopAutomaticRetry);
1369
+ lastError = new RateLimitError();
1370
+ } else {
1371
+ const retryable = response.status === 408 || response.status >= 500;
1372
+ lastError = new HttpError(response.status, retryable);
1373
+ if (retryable) retryDelay = Math.random() * 500 * 2 ** (attempt - 1);
1374
+ }
1375
+ retryReason = "http";
1376
+ } catch (error) {
1377
+ if (operationSignal.aborted) throw new AbortedError();
1378
+ if (error instanceof ResponseTooLargeError || error instanceof CursorExpiredError) throw error;
1379
+ if (error instanceof PubMedError && !error.retryable) throw error;
1380
+ if (timedOut) {
1381
+ lastError = new TimeoutError();
1382
+ retryDelay = Math.random() * 500 * 2 ** (attempt - 1);
1383
+ retryReason = "timeout";
1384
+ } else if (error instanceof RateLimitError || error instanceof HttpError) {
1385
+ lastError = error;
1386
+ if (error.retryable) retryDelay ??= Math.random() * 500 * 2 ** (attempt - 1);
1387
+ retryReason = "http";
1388
+ } else if (error instanceof AbortedError) {
1389
+ throw error;
1390
+ } else {
1391
+ lastError = new NetworkError();
1392
+ retryDelay = Math.random() * 500 * 2 ** (attempt - 1);
1393
+ retryReason = "network";
1394
+ }
1395
+ } finally {
1396
+ clearTimeout(timer);
1397
+ operationSignal.removeEventListener("abort", abortAttempt);
1398
+ }
1399
+ if (stopAutomaticRetry) throw new RateLimitError();
1400
+ if (retryDelay === void 0 || attempt === this.#maxAttempts) throw lastError;
1401
+ safeEvent(this.#onEvent, { type: "retry", endpoint, attempt, delayMs: retryDelay, reason: retryReason, correlationId });
1402
+ await sleep(retryDelay, operationSignal);
1403
+ }
1404
+ throw lastError;
1405
+ }
1406
+ };
1407
+
1408
+ // src/transport/inflight-coalescer.ts
1409
+ var InflightCoalescer = class {
1410
+ #requests = /* @__PURE__ */ new Map();
1411
+ request(key, correlationId, signal, create, onCoalesced) {
1412
+ const existing = this.#requests.get(key);
1413
+ if (existing !== void 0) {
1414
+ onCoalesced({ correlationId, sharedCorrelationId: existing.correlationId });
1415
+ return this.#subscribe(key, existing, signal);
1416
+ }
1417
+ const controller = new AbortController();
1418
+ const operation = {
1419
+ controller,
1420
+ correlationId,
1421
+ subscribers: 0,
1422
+ settled: false,
1423
+ promise: create(controller.signal)
1424
+ };
1425
+ this.#requests.set(key, operation);
1426
+ void operation.promise.then(
1427
+ () => this.#settle(key, operation),
1428
+ () => this.#settle(key, operation)
1429
+ );
1430
+ return this.#subscribe(key, operation, signal);
1431
+ }
1432
+ #settle(key, operation) {
1433
+ operation.settled = true;
1434
+ if (this.#requests.get(key) === operation) this.#requests.delete(key);
1435
+ }
1436
+ #subscribe(key, operation, signal) {
1437
+ if (signal?.aborted === true) {
1438
+ if (operation.subscribers === 0 && !operation.settled) {
1439
+ if (this.#requests.get(key) === operation) this.#requests.delete(key);
1440
+ operation.controller.abort();
1441
+ }
1442
+ return Promise.reject(new AbortedError());
1443
+ }
1444
+ operation.subscribers += 1;
1445
+ return new Promise((resolve, reject) => {
1446
+ let finished = false;
1447
+ const finish = (aborted, complete) => {
1448
+ if (finished) return;
1449
+ finished = true;
1450
+ signal?.removeEventListener("abort", onAbort);
1451
+ operation.subscribers -= 1;
1452
+ if (aborted && operation.subscribers === 0 && !operation.settled) {
1453
+ if (this.#requests.get(key) === operation) this.#requests.delete(key);
1454
+ operation.controller.abort();
1455
+ }
1456
+ complete();
1457
+ };
1458
+ const onAbort = () => finish(true, () => reject(new AbortedError()));
1459
+ signal?.addEventListener("abort", onAbort, { once: true });
1460
+ void operation.promise.then(
1461
+ (value) => finish(false, () => resolve(value)),
1462
+ (error) => finish(false, () => reject(error))
1463
+ );
1464
+ });
1465
+ }
1466
+ };
1467
+
1468
+ // src/transport.ts
1469
+ var encoder = new TextEncoder();
1470
+ function digest(value) {
1471
+ return createHash("sha256").update(value).digest("hex");
1472
+ }
1473
+ var Transport = class {
1474
+ #maxResponseBytes;
1475
+ #onEvent;
1476
+ #cache;
1477
+ #inflight = new InflightCoalescer();
1478
+ #http;
1479
+ constructor(options) {
1480
+ this.#maxResponseBytes = options.maxResponseBytes;
1481
+ this.#onEvent = options.onEvent;
1482
+ this.#cache = new CacheLifecycle(options.cache);
1483
+ const fingerprint = options.apiKey === void 0 ? digest("no-api-key") : digest(options.apiKey);
1484
+ const limiter = new RequestRateLimiter(
1485
+ fingerprint,
1486
+ options.apiKey !== void 0,
1487
+ options.maxQueuedRequests,
1488
+ options.rateLimitCoordinator,
1489
+ options.onEvent
1490
+ );
1491
+ this.#http = new HttpExecutor({
1492
+ email: options.email,
1493
+ tool: options.tool,
1494
+ fetch: options.fetch,
1495
+ timeoutMs: options.timeoutMs,
1496
+ maxAttempts: options.maxAttempts,
1497
+ maxResponseBytes: options.maxResponseBytes,
1498
+ limiter,
1499
+ ...options.apiKey === void 0 ? {} : { apiKey: options.apiKey },
1500
+ ...options.onEvent === void 0 ? {} : { onEvent: options.onEvent }
1501
+ });
1502
+ }
1503
+ async request(endpoint, parameters, decode, options = {}) {
1504
+ const correlationId = randomUUID();
1505
+ safeEvent(this.#onEvent, { type: "correlation-id", endpoint, correlationId });
1506
+ try {
1507
+ return await this.#coordinate(endpoint, parameters, decode, options, correlationId);
1508
+ } catch (error) {
1509
+ safeEvent(this.#onEvent, {
1510
+ type: "terminal-failure",
1511
+ endpoint,
1512
+ correlationId,
1513
+ errorCode: error instanceof PubMedError ? error.code : "UNKNOWN_ERROR"
1514
+ });
1515
+ throw error;
1516
+ }
1517
+ }
1518
+ async #coordinate(endpoint, parameters, decode, options, correlationId) {
1519
+ const { signal } = options;
1520
+ const useCache = options.cache ?? true;
1521
+ if (signal?.aborted === true) throw new AbortedError();
1522
+ const semantic = new URLSearchParams(parameters);
1523
+ semantic.sort();
1524
+ const key = digest(`${endpoint}
1525
+ ${semantic.toString()}`);
1526
+ const inflightKey = `${key}:${digest(decode.key)}`;
1527
+ if (useCache) {
1528
+ const cached = await this.#cache.read(key, signal);
1529
+ if (cached.status === "hit") {
1530
+ safeEvent(this.#onEvent, { type: "cache-hit", endpoint, correlationId });
1531
+ if (encoder.encode(cached.value).byteLength > this.#maxResponseBytes) {
1532
+ await this.#cache.invalidate(key);
1533
+ } else {
1534
+ try {
1535
+ return decode.decode(cached.value);
1536
+ } catch (error) {
1537
+ if (error instanceof ResponseTooLargeError || error instanceof AbortedError) throw error;
1538
+ await this.#cache.invalidate(key);
1539
+ }
1540
+ }
1541
+ } else if (cached.status === "miss") {
1542
+ safeEvent(this.#onEvent, { type: "cache-miss", endpoint, correlationId });
1543
+ }
1544
+ }
1545
+ return this.#inflight.request(
1546
+ inflightKey,
1547
+ correlationId,
1548
+ signal,
1549
+ async (operationSignal) => {
1550
+ const result = await this.#http.execute(endpoint, parameters, operationSignal, decode.decode, correlationId);
1551
+ if (operationSignal.aborted) throw new AbortedError();
1552
+ if (useCache) this.#cache.write(key, result.body);
1553
+ if (operationSignal.aborted) throw new AbortedError();
1554
+ return result.value;
1555
+ },
1556
+ ({ correlationId: joinedCorrelationId, sharedCorrelationId }) => {
1557
+ safeEvent(this.#onEvent, {
1558
+ type: "request-coalesced",
1559
+ endpoint,
1560
+ correlationId: joinedCorrelationId,
1561
+ sharedCorrelationId
1562
+ });
1563
+ }
1564
+ );
1565
+ }
1566
+ };
1567
+
1568
+ // src/client.ts
1569
+ var DEFAULT_PAGE_SIZE = 20;
1570
+ var MAX_BATCH_SIZE = 200;
1571
+ var SEARCH_WINDOW = 1e4;
1572
+ var CURSOR_TTL_MS = 8 * 60 * 6e4;
1573
+ var CURSOR_FUTURE_SKEW_MS = 5 * 6e4;
1574
+ var linkParser = new XMLParser2({
1575
+ ignoreAttributes: false,
1576
+ attributeNamePrefix: "@",
1577
+ textNodeName: "#text",
1578
+ parseTagValue: false,
1579
+ parseAttributeValue: false,
1580
+ processEntities: false,
1581
+ trimValues: true
1582
+ });
1583
+ function object3(value) {
1584
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1585
+ }
1586
+ function list2(value) {
1587
+ if (value === void 0 || value === null) return [];
1588
+ return Array.isArray(value) ? value : [value];
1589
+ }
1590
+ function text2(value) {
1591
+ if (typeof value === "string" || typeof value === "number") {
1592
+ const result = String(value).trim();
1593
+ return result === "" ? void 0 : result;
1594
+ }
1595
+ const record = object3(value);
1596
+ return record === void 0 ? void 0 : text2(record["#text"]);
1597
+ }
1598
+ function positiveInteger(value, name, maximum) {
1599
+ if (!Number.isSafeInteger(value) || value <= 0 || maximum !== void 0 && value > maximum) {
1600
+ throw new ValidationError(`${name} must be a positive integer${maximum === void 0 ? "" : ` no greater than ${maximum}`}`);
1601
+ }
1602
+ return value;
1603
+ }
1604
+ function isAbortSignal(value) {
1605
+ const candidate = object3(value);
1606
+ return typeof candidate?.aborted === "boolean" && typeof candidate.addEventListener === "function" && typeof candidate.removeEventListener === "function";
1607
+ }
1608
+ function validateRequestOptions(options, name) {
1609
+ const candidate = object3(options);
1610
+ if (candidate === void 0) throw new ValidationError(`${name} must be an object`);
1611
+ if (candidate.includeLinkOuts !== void 0 && typeof candidate.includeLinkOuts !== "boolean") {
1612
+ throw new ValidationError("includeLinkOuts must be a boolean");
1613
+ }
1614
+ if (candidate.signal !== void 0 && !isAbortSignal(candidate.signal)) {
1615
+ throw new ValidationError("signal must be an AbortSignal");
1616
+ }
1617
+ }
1618
+ function validateSummaryRequestOptions(options) {
1619
+ const candidate = object3(options);
1620
+ if (candidate === void 0) throw new ValidationError("summary request options must be an object");
1621
+ if (candidate.includeLinkOuts !== void 0) {
1622
+ throw new ValidationError("includeLinkOuts is not supported for summary requests");
1623
+ }
1624
+ if (candidate.signal !== void 0 && !isAbortSignal(candidate.signal)) {
1625
+ throw new ValidationError("signal must be an AbortSignal");
1626
+ }
1627
+ }
1628
+ function validateAdapterShapes(value) {
1629
+ const options = object3(value);
1630
+ if (options === void 0) throw new ValidationError("PubMedClient options are required");
1631
+ if (options.cache !== void 0) {
1632
+ const cache = object3(options.cache);
1633
+ if (cache === void 0 || typeof cache.get !== "function" || typeof cache.set !== "function" || cache.delete !== void 0 && typeof cache.delete !== "function") {
1634
+ throw new ValidationError("cache must provide get and set functions, and delete must be a function when provided");
1635
+ }
1636
+ }
1637
+ if (options.rateLimitCoordinator !== void 0) {
1638
+ const coordinator = object3(options.rateLimitCoordinator);
1639
+ if (coordinator === void 0 || typeof coordinator.acquire !== "function" || coordinator.cooldown !== void 0 && typeof coordinator.cooldown !== "function") {
1640
+ throw new ValidationError("rateLimitCoordinator must provide an acquire function, and cooldown must be a function when provided");
1641
+ }
1642
+ }
1643
+ if (options.onEvent !== void 0 && typeof options.onEvent !== "function") {
1644
+ throw new ValidationError("onEvent must be a function");
1645
+ }
1646
+ }
1647
+ function validateQueryOptions(options) {
1648
+ validateRequestOptions(options, "search options");
1649
+ if (typeof options.query !== "string" || options.query.trim() === "") throw new ValidationError("query is required");
1650
+ if (options.pageSize !== void 0) positiveInteger(options.pageSize, "pageSize", MAX_BATCH_SIZE);
1651
+ if (options.sort !== void 0 && (typeof options.sort !== "string" || options.sort.trim() === "")) {
1652
+ throw new ValidationError("sort must be a non-empty string");
1653
+ }
1654
+ }
1655
+ function validatePmid(pmid) {
1656
+ if (typeof pmid !== "string" || !/^[1-9][0-9]*$/.test(pmid)) throw new ValidationError("PMIDs must contain only digits and may not begin with zero");
1657
+ return pmid;
1658
+ }
1659
+ function encodeCursor(cursor) {
1660
+ return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
1661
+ }
1662
+ function decodeCursor(value) {
1663
+ try {
1664
+ const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
1665
+ const data = object3(parsed);
1666
+ if (data?.v !== 1 || typeof data.webEnv !== "string" || data.webEnv === "" || typeof data.queryKey !== "string" || data.queryKey === "" || typeof data.total !== "number" || !Number.isSafeInteger(data.total) || data.total < 1 || typeof data.offset !== "number" || !Number.isSafeInteger(data.offset) || data.offset < 0 || data.offset >= data.total || typeof data.pageSize !== "number" || !Number.isSafeInteger(data.pageSize) || data.pageSize < 1 || data.pageSize > MAX_BATCH_SIZE || typeof data.issuedAt !== "number" || !Number.isSafeInteger(data.issuedAt) || data.issuedAt < 0 || data.issuedAt > Date.now() + CURSOR_FUTURE_SKEW_MS) throw new CursorInvalidError();
1667
+ if (Date.now() - data.issuedAt > CURSOR_TTL_MS) throw new CursorExpiredError();
1668
+ return {
1669
+ v: 1,
1670
+ webEnv: data.webEnv,
1671
+ queryKey: data.queryKey,
1672
+ total: data.total,
1673
+ offset: data.offset,
1674
+ pageSize: data.pageSize,
1675
+ issuedAt: data.issuedAt
1676
+ };
1677
+ } catch (error) {
1678
+ if (error instanceof CursorExpiredError || error instanceof CursorInvalidError) throw error;
1679
+ throw new CursorInvalidError();
1680
+ }
1681
+ }
1682
+ function stringLeaves(value) {
1683
+ const found = [];
1684
+ const pending = [value];
1685
+ let visited = 0;
1686
+ while (pending.length > 0 && found.length < 100 && visited < 1e3) {
1687
+ const current = pending.pop();
1688
+ visited += 1;
1689
+ if (typeof current === "string") {
1690
+ const normalized = current.trim();
1691
+ if (normalized !== "") found.push(normalized);
1692
+ continue;
1693
+ }
1694
+ if (Array.isArray(current)) {
1695
+ for (const item of current) {
1696
+ if (pending.length >= 1e3) break;
1697
+ pending.push(item);
1698
+ }
1699
+ continue;
1700
+ }
1701
+ const record = object3(current);
1702
+ if (record === void 0) continue;
1703
+ for (const key in record) {
1704
+ if (pending.length >= 1e3) break;
1705
+ if (Object.prototype.hasOwnProperty.call(record, key)) pending.push(record[key]);
1706
+ }
1707
+ }
1708
+ return found;
1709
+ }
1710
+ function searchErrorMessages(value) {
1711
+ const root = object3(value);
1712
+ const result = object3(root?.esearchresult);
1713
+ return [root?.error, root?.ERROR, root?.errorlist, result?.error, result?.ERROR, result?.errorlist].flatMap(stringLeaves);
1714
+ }
1715
+ function isExpiredHistoryMessage(message) {
1716
+ const historyReference = /(?:history|webenv|query(?:[\s_-]*key|\s*#))/i;
1717
+ const unavailableReference = /(?:expired|invalid|unknown|missing|not\s+found|not\s+available|does\s+not\s+exist|cannot\s+find|could\s+not\s+find|unable\s+to\s+(?:find|obtain))/i;
1718
+ return historyReference.test(message) && unavailableReference.test(message);
1719
+ }
1720
+ function parseSearchResponse(body, cursorContext = false) {
1721
+ let value;
1722
+ try {
1723
+ value = JSON.parse(body);
1724
+ } catch {
1725
+ throw new InvalidResponseError("PubMed returned invalid search metadata");
1726
+ }
1727
+ if (cursorContext && searchErrorMessages(value).some(isExpiredHistoryMessage)) {
1728
+ throw new CursorExpiredError();
1729
+ }
1730
+ const result = object3(object3(value)?.esearchresult);
1731
+ const countText = result?.count;
1732
+ const queryKeyValue = result?.querykey;
1733
+ const rawIds = result?.idlist;
1734
+ if (typeof countText !== "string" || !/^(?:0|[1-9][0-9]*)$/.test(countText) || !Array.isArray(rawIds) || rawIds.some((id) => typeof id !== "string" || !/^[1-9][0-9]*$/.test(id)) || typeof queryKeyValue !== "string" || !/^(?:0|[1-9][0-9]*)$/.test(queryKeyValue)) {
1735
+ throw new InvalidResponseError("PubMed returned invalid search metadata");
1736
+ }
1737
+ const count = Number(countText);
1738
+ if (!Number.isSafeInteger(count)) throw new InvalidResponseError("PubMed returned invalid search metadata");
1739
+ const webEnv = typeof result?.webenv === "string" ? result.webenv : "";
1740
+ const queryKey = queryKeyValue;
1741
+ const ids = rawIds;
1742
+ if (count > 0 && (webEnv === "" || !/^[1-9][0-9]*$/.test(queryKey) || ids.length === 0)) {
1743
+ throw new InvalidResponseError("PubMed did not return complete search history metadata");
1744
+ }
1745
+ return { total: count, webEnv, queryKey, ids };
1746
+ }
1747
+ function validateSearchState(state, requestedIds, expectedTotal) {
1748
+ const expectedIds = Math.min(requestedIds, state.total);
1749
+ if (expectedTotal !== void 0 && state.total !== expectedTotal || state.ids.length !== expectedIds || new Set(state.ids).size !== state.ids.length) {
1750
+ throw new InvalidResponseError("PubMed returned an incomplete search ID page");
1751
+ }
1752
+ }
1753
+ function parseExpectedFetch(body, expectedPmids) {
1754
+ const parsed = parsePubMedXml(body);
1755
+ const expected = new Set(expectedPmids);
1756
+ const seen = /* @__PURE__ */ new Set();
1757
+ for (const record of parsed.records) {
1758
+ if (record.pmid === void 0) {
1759
+ if (record.kind !== "unknown") {
1760
+ throw new InvalidResponseError("PubMed returned a recognized record without a PMID");
1761
+ }
1762
+ if (/^error(?:list)?$/i.test(record.recordType)) {
1763
+ throw new InvalidResponseError("PubMed returned an error response instead of requested records");
1764
+ }
1765
+ continue;
1766
+ }
1767
+ if (!expected.has(record.pmid) || seen.has(record.pmid)) {
1768
+ throw new InvalidResponseError("PubMed returned records that did not match the requested PMIDs");
1769
+ }
1770
+ seen.add(record.pmid);
1771
+ }
1772
+ return parsed;
1773
+ }
1774
+ function safeHttpLink(value) {
1775
+ try {
1776
+ const url = new URL(value);
1777
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : void 0;
1778
+ } catch {
1779
+ return void 0;
1780
+ }
1781
+ }
1782
+ function parseLinkOutResponse(xml) {
1783
+ let parsed;
1784
+ try {
1785
+ parsed = linkParser.parse(xml);
1786
+ } catch {
1787
+ throw new InvalidResponseError("PubMed returned invalid LinkOut metadata");
1788
+ }
1789
+ const root = object3(object3(parsed)?.eLinkResult) ?? object3(object3(parsed)?.ELinkResult);
1790
+ if (root === void 0) throw new InvalidResponseError("PubMed returned invalid LinkOut metadata");
1791
+ const result = /* @__PURE__ */ new Map();
1792
+ const idUrlLists = [
1793
+ ...list2(root.IdUrlList),
1794
+ ...list2(root.LinkSet).flatMap((linkSet) => list2(object3(linkSet)?.IdUrlList))
1795
+ ];
1796
+ for (const idUrlList of idUrlLists) {
1797
+ for (const idSet of list2(object3(idUrlList)?.IdUrlSet)) {
1798
+ const id = text2(object3(idSet)?.Id);
1799
+ if (id === void 0) continue;
1800
+ const links = result.get(id) ?? [];
1801
+ for (const objectUrl of list2(object3(idSet)?.ObjUrl)) {
1802
+ const node = object3(objectUrl);
1803
+ const rawUrl = text2(node?.Url);
1804
+ const url = rawUrl === void 0 ? void 0 : safeHttpLink(rawUrl);
1805
+ if (url === void 0) continue;
1806
+ const providerNode = object3(node?.Provider);
1807
+ const name = text2(providerNode?.Name);
1808
+ const abbreviation = text2(providerNode?.NameAbbr);
1809
+ const providerId = text2(providerNode?.Id);
1810
+ links.push({
1811
+ url,
1812
+ type: "linkout",
1813
+ provenance: "ncbi-linkout",
1814
+ ...(name ?? abbreviation ?? providerId) === void 0 ? {} : {
1815
+ provider: {
1816
+ ...name === void 0 ? {} : { name },
1817
+ ...abbreviation === void 0 ? {} : { abbreviation },
1818
+ ...providerId === void 0 ? {} : { id: providerId }
1819
+ }
1820
+ }
1821
+ });
1822
+ }
1823
+ result.set(id, links);
1824
+ }
1825
+ }
1826
+ return result;
1827
+ }
1828
+ function withLinks(record, extra) {
1829
+ if (extra.length === 0) return record;
1830
+ return { ...record, links: [...record.links, ...extra] };
1831
+ }
1832
+ var PubMedClient = class {
1833
+ #transport;
1834
+ #maxBatchSize;
1835
+ #onEvent;
1836
+ constructor(options) {
1837
+ if (typeof options !== "object" || options === null) throw new ValidationError("PubMedClient options are required");
1838
+ validateAdapterShapes(options);
1839
+ if (typeof options.email !== "string" || options.email.trim() === "") throw new ValidationError("email is required");
1840
+ if (typeof options.tool !== "string" || options.tool.trim() === "") throw new ValidationError("tool is required");
1841
+ if (options.apiKey !== void 0 && (typeof options.apiKey !== "string" || options.apiKey.trim() === "")) {
1842
+ throw new ValidationError("apiKey must be a non-empty string");
1843
+ }
1844
+ const timeoutMs = positiveInteger(options.timeoutMs ?? 3e4, "timeoutMs");
1845
+ const maxAttempts = positiveInteger(options.maxAttempts ?? 4, "maxAttempts");
1846
+ const maxResponseBytes = positiveInteger(options.maxResponseBytes ?? 25 * 1024 * 1024, "maxResponseBytes");
1847
+ const maxQueuedRequests = positiveInteger(options.maxQueuedRequests ?? 1e3, "maxQueuedRequests");
1848
+ this.#maxBatchSize = positiveInteger(options.maxBatchSize ?? MAX_BATCH_SIZE, "maxBatchSize", MAX_BATCH_SIZE);
1849
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
1850
+ if (typeof fetchImplementation !== "function") throw new ValidationError("A Fetch API implementation is required");
1851
+ this.#onEvent = options.onEvent;
1852
+ this.#transport = new Transport({
1853
+ email: options.email.trim(),
1854
+ tool: options.tool.trim(),
1855
+ fetch: fetchImplementation,
1856
+ timeoutMs,
1857
+ maxAttempts,
1858
+ maxResponseBytes,
1859
+ maxQueuedRequests,
1860
+ ...options.apiKey === void 0 ? {} : { apiKey: options.apiKey },
1861
+ ...options.cache === void 0 ? {} : { cache: options.cache },
1862
+ ...options.rateLimitCoordinator === void 0 ? {} : { rateLimitCoordinator: options.rateLimitCoordinator },
1863
+ ...options.onEvent === void 0 ? {} : { onEvent: options.onEvent }
1864
+ });
1865
+ }
1866
+ async get(pmid, options = {}) {
1867
+ validatePmid(pmid);
1868
+ const batch = await this.getMany([pmid], options);
1869
+ return batch.records.find((record) => record.pmid === pmid) ?? null;
1870
+ }
1871
+ async getMany(pmids, options = {}) {
1872
+ if (!Array.isArray(pmids)) throw new ValidationError("pmids must be an array");
1873
+ validateRequestOptions(options, "request options");
1874
+ if (options.signal?.aborted === true) throw new AbortedError();
1875
+ const input = pmids.map(validatePmid);
1876
+ const unique = [...new Set(input)];
1877
+ const received = [];
1878
+ const warnings = [];
1879
+ for (let offset = 0; offset < unique.length; offset += this.#maxBatchSize) {
1880
+ const chunk = unique.slice(offset, offset + this.#maxBatchSize);
1881
+ const parsed = await this.#transport.request(
1882
+ "efetch",
1883
+ { id: chunk.join(","), retmode: "xml" },
1884
+ { key: "efetch-records-v1", decode: (body) => parseExpectedFetch(body, chunk) },
1885
+ options.signal === void 0 ? {} : { signal: options.signal }
1886
+ );
1887
+ received.push(...parsed.records);
1888
+ warnings.push(...parsed.warnings);
1889
+ for (const warning of parsed.warnings) {
1890
+ safeEvent(this.#onEvent, { type: "parse-warning", code: warning.code, ...warning.recordType === void 0 ? {} : { recordType: warning.recordType } });
1891
+ }
1892
+ }
1893
+ const byPmid = /* @__PURE__ */ new Map();
1894
+ const unknown = [];
1895
+ for (const record of received) {
1896
+ if (record.pmid === void 0) unknown.push(record);
1897
+ else if (!byPmid.has(record.pmid)) byPmid.set(record.pmid, record);
1898
+ }
1899
+ let ordered = input.flatMap((pmid) => byPmid.get(pmid) ?? []);
1900
+ if (unknown.length > 0) ordered = [...ordered, ...unknown];
1901
+ if (options.includeLinkOuts === true && ordered.length > 0) ordered = await this.#enrich(ordered, options.signal);
1902
+ return {
1903
+ records: ordered,
1904
+ missingPmids: input.filter((pmid) => !byPmid.has(pmid)),
1905
+ warnings
1906
+ };
1907
+ }
1908
+ async getSummary(pmid, options = {}) {
1909
+ validatePmid(pmid);
1910
+ const batch = await this.getManySummaries([pmid], options);
1911
+ return batch.summaries[0] ?? null;
1912
+ }
1913
+ async getManySummaries(pmids, options = {}) {
1914
+ if (!Array.isArray(pmids)) throw new ValidationError("pmids must be an array");
1915
+ validateSummaryRequestOptions(options);
1916
+ if (options.signal?.aborted === true) throw new AbortedError();
1917
+ const input = pmids.map(validatePmid);
1918
+ const unique = [...new Set(input)];
1919
+ const received = [];
1920
+ for (let offset = 0; offset < unique.length; offset += this.#maxBatchSize) {
1921
+ const chunk = unique.slice(offset, offset + this.#maxBatchSize);
1922
+ const summaries = await this.#transport.request(
1923
+ "esummary",
1924
+ { id: chunk.join(","), retmode: "json", version: "2.0" },
1925
+ { key: "esummary-summaries-v1", decode: (body) => parseESummaryJson(body, chunk) },
1926
+ options.signal === void 0 ? {} : { signal: options.signal }
1927
+ );
1928
+ received.push(...summaries);
1929
+ }
1930
+ const byPmid = new Map(received.map((summary) => [summary.pmid, summary]));
1931
+ return {
1932
+ summaries: input.flatMap((pmid) => byPmid.get(pmid) ?? []),
1933
+ missingPmids: input.filter((pmid) => !byPmid.has(pmid))
1934
+ };
1935
+ }
1936
+ async search(options) {
1937
+ validateRequestOptions(options, "search options");
1938
+ if ("cursor" in options) {
1939
+ if (typeof options.cursor !== "string" || options.cursor === "") throw new ValidationError("cursor must be a non-empty string");
1940
+ return (await this.#searchCursorPage(options.cursor, options.includeLinkOuts === true, options.signal)).batch;
1941
+ }
1942
+ validateQueryOptions(options);
1943
+ return (await this.#searchQueryPage(options)).batch;
1944
+ }
1945
+ async *searchAll(options) {
1946
+ validateRequestOptions(options, "searchAll options");
1947
+ validateQueryOptions(options);
1948
+ if (!Number.isSafeInteger(options.maxResults) || options.maxResults < 0) throw new ValidationError("maxResults must be a non-negative integer");
1949
+ const requestedPageSize = positiveInteger(options.pageSize ?? DEFAULT_PAGE_SIZE, "pageSize", MAX_BATCH_SIZE);
1950
+ if (options.maxResults === 0) return;
1951
+ const pageSize = Math.min(requestedPageSize, options.maxResults);
1952
+ const queryOptions = {
1953
+ query: options.query,
1954
+ pageSize,
1955
+ ...options.sort === void 0 ? {} : { sort: options.sort },
1956
+ ...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
1957
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1958
+ };
1959
+ const state = await this.#fetchInitialSearchState(queryOptions, pageSize);
1960
+ const target = Math.min(options.maxResults, state.total);
1961
+ if (target > SEARCH_WINDOW) throw new SearchLimitError();
1962
+ let page = await this.#materializeInitialSearchPage(state, queryOptions, pageSize);
1963
+ let processed = 0;
1964
+ while (processed < target) {
1965
+ if (page.expectedPmids.length === 0) throw new InvalidResponseError("PubMed search paging made no progress");
1966
+ yield page.batch;
1967
+ processed += page.expectedPmids.length;
1968
+ if (processed >= target) return;
1969
+ if (page.batch.nextCursor === null) throw new InvalidResponseError("PubMed search ended before the requested result count");
1970
+ page = await this.#searchCursorPage(
1971
+ page.batch.nextCursor,
1972
+ options.includeLinkOuts === true,
1973
+ options.signal,
1974
+ target - processed
1975
+ );
1976
+ }
1977
+ }
1978
+ async #searchQueryPage(options) {
1979
+ validateQueryOptions(options);
1980
+ const pageSize = positiveInteger(options.pageSize ?? DEFAULT_PAGE_SIZE, "pageSize", MAX_BATCH_SIZE);
1981
+ const state = await this.#fetchInitialSearchState(options, pageSize);
1982
+ return this.#materializeInitialSearchPage(state, options, pageSize);
1983
+ }
1984
+ async #fetchInitialSearchState(options, pageSize) {
1985
+ return this.#transport.request(
1986
+ "esearch",
1987
+ {
1988
+ term: options.query,
1989
+ retmode: "json",
1990
+ usehistory: "y",
1991
+ retstart: "0",
1992
+ retmax: String(pageSize),
1993
+ ...options.sort === void 0 ? {} : { sort: options.sort }
1994
+ },
1995
+ {
1996
+ key: "esearch-initial-v1",
1997
+ decode: (body) => {
1998
+ const parsed = parseSearchResponse(body);
1999
+ validateSearchState(parsed, pageSize);
2000
+ return parsed;
2001
+ }
2002
+ },
2003
+ { cache: false, ...options.signal === void 0 ? {} : { signal: options.signal } }
2004
+ );
2005
+ }
2006
+ async #materializeInitialSearchPage(state, options, pageSize) {
2007
+ if (state.total === 0) {
2008
+ return {
2009
+ batch: { records: [], missingPmids: [], warnings: [], total: 0, nextCursor: null },
2010
+ expectedPmids: []
2011
+ };
2012
+ }
2013
+ const batch = await this.getMany(state.ids, {
2014
+ ...options.includeLinkOuts === void 0 ? {} : { includeLinkOuts: options.includeLinkOuts },
2015
+ ...options.signal === void 0 ? {} : { signal: options.signal }
2016
+ });
2017
+ const offset = state.ids.length;
2018
+ return {
2019
+ batch: {
2020
+ ...batch,
2021
+ total: state.total,
2022
+ nextCursor: offset < state.total ? encodeCursor({ v: 1, webEnv: state.webEnv, queryKey: state.queryKey, total: state.total, offset, pageSize, issuedAt: Date.now() }) : null
2023
+ },
2024
+ expectedPmids: state.ids
2025
+ };
2026
+ }
2027
+ async #searchCursorPage(cursorValue, includeLinkOuts, signal, maxExpected) {
2028
+ const cursor = decodeCursor(cursorValue);
2029
+ if (cursor.offset >= SEARCH_WINDOW) throw new SearchLimitError();
2030
+ const remainingWindow = SEARCH_WINDOW - cursor.offset;
2031
+ const requested = maxExpected === void 0 ? cursor.pageSize : Math.min(cursor.pageSize, positiveInteger(maxExpected, "maxResults"));
2032
+ const retmax = Math.min(requested, cursor.total - cursor.offset, remainingWindow);
2033
+ const state = await this.#transport.request(
2034
+ "esearch",
2035
+ {
2036
+ term: `#${cursor.queryKey}`,
2037
+ WebEnv: cursor.webEnv,
2038
+ query_key: cursor.queryKey,
2039
+ retstart: String(cursor.offset),
2040
+ retmax: String(retmax),
2041
+ retmode: "json",
2042
+ usehistory: "y"
2043
+ },
2044
+ {
2045
+ key: `esearch-cursor-v1:${cursor.total}`,
2046
+ decode: (body) => {
2047
+ const parsed = parseSearchResponse(body, true);
2048
+ validateSearchState(parsed, retmax, cursor.total);
2049
+ return parsed;
2050
+ }
2051
+ },
2052
+ { cache: false, ...signal === void 0 ? {} : { signal } }
2053
+ );
2054
+ const batch = await this.getMany(state.ids, { includeLinkOuts, ...signal === void 0 ? {} : { signal } });
2055
+ const offset = cursor.offset + state.ids.length;
2056
+ return {
2057
+ batch: {
2058
+ ...batch,
2059
+ total: cursor.total,
2060
+ nextCursor: offset < cursor.total ? encodeCursor({ ...cursor, offset }) : null
2061
+ },
2062
+ expectedPmids: state.ids
2063
+ };
2064
+ }
2065
+ async #enrich(records, signal) {
2066
+ const ids = [...new Set(records.flatMap((record) => record.pmid ?? []))];
2067
+ const links = /* @__PURE__ */ new Map();
2068
+ for (let offset = 0; offset < ids.length; offset += this.#maxBatchSize) {
2069
+ const chunk = ids.slice(offset, offset + this.#maxBatchSize);
2070
+ const parsed = await this.#transport.request(
2071
+ "elink",
2072
+ { id: chunk.join(","), cmd: "llinks", retmode: "xml" },
2073
+ { key: "elink-linkouts-v1", decode: parseLinkOutResponse },
2074
+ signal === void 0 ? {} : { signal }
2075
+ );
2076
+ for (const [id, found] of parsed) links.set(id, [...links.get(id) ?? [], ...found]);
2077
+ }
2078
+ return records.map((record) => record.pmid === void 0 ? record : withLinks(record, links.get(record.pmid) ?? []));
2079
+ }
2080
+ };
2081
+
2082
+ // src/cache.ts
2083
+ var encoder2 = new TextEncoder();
2084
+ var MemoryCache = class {
2085
+ #entries = /* @__PURE__ */ new Map();
2086
+ #maxEntries;
2087
+ #maxBytes;
2088
+ #ttlMs;
2089
+ #totalBytes = 0;
2090
+ constructor(options = {}) {
2091
+ this.#maxEntries = options.maxEntries ?? 500;
2092
+ this.#maxBytes = options.maxBytes ?? 25 * 1024 * 1024;
2093
+ this.#ttlMs = options.ttlMs ?? 5 * 6e4;
2094
+ if (!Number.isSafeInteger(this.#maxEntries) || this.#maxEntries <= 0) {
2095
+ throw new ValidationError("Memory cache maxEntries must be a positive integer");
2096
+ }
2097
+ if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes <= 0) {
2098
+ throw new ValidationError("Memory cache maxBytes must be a positive integer");
2099
+ }
2100
+ if (!Number.isFinite(this.#ttlMs) || this.#ttlMs < 0) {
2101
+ throw new ValidationError("Memory cache ttlMs must be a non-negative number");
2102
+ }
2103
+ }
2104
+ async get(key) {
2105
+ const entry = this.#entries.get(key);
2106
+ if (entry === void 0) return void 0;
2107
+ if (entry.expiresAt <= Date.now()) {
2108
+ this.#remove(key);
2109
+ return void 0;
2110
+ }
2111
+ this.#entries.delete(key);
2112
+ this.#entries.set(key, entry);
2113
+ return entry.value;
2114
+ }
2115
+ async set(key, value) {
2116
+ this.#remove(key);
2117
+ const bytes = encoder2.encode(key).byteLength + encoder2.encode(value).byteLength;
2118
+ if (bytes > this.#maxBytes) return;
2119
+ const now = Date.now();
2120
+ for (const [cachedKey, entry] of this.#entries) {
2121
+ if (entry.expiresAt <= now) this.#remove(cachedKey);
2122
+ }
2123
+ this.#entries.set(key, { value, expiresAt: now + this.#ttlMs, bytes });
2124
+ this.#totalBytes += bytes;
2125
+ while (this.#entries.size > this.#maxEntries || this.#totalBytes > this.#maxBytes) {
2126
+ const oldest = this.#entries.keys().next();
2127
+ if (oldest.done) break;
2128
+ this.#remove(oldest.value);
2129
+ }
2130
+ }
2131
+ async delete(key) {
2132
+ this.#remove(key);
2133
+ }
2134
+ clear() {
2135
+ this.#entries.clear();
2136
+ this.#totalBytes = 0;
2137
+ }
2138
+ #remove(key) {
2139
+ const entry = this.#entries.get(key);
2140
+ if (entry === void 0) return;
2141
+ this.#entries.delete(key);
2142
+ this.#totalBytes -= entry.bytes;
2143
+ }
2144
+ get size() {
2145
+ return this.#entries.size;
2146
+ }
2147
+ };
2148
+
2149
+ // src/citation.ts
2150
+ function object4(value) {
2151
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2152
+ }
2153
+ function values(value) {
2154
+ return Array.isArray(value) ? value : [];
2155
+ }
2156
+ function cleanText2(value) {
2157
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
2158
+ const normalized = String(value).replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(/\s+/g, " ").trim();
2159
+ return normalized === "" ? void 0 : normalized;
2160
+ }
2161
+ function validPmid(value) {
2162
+ const pmid = cleanText2(value);
2163
+ return pmid !== void 0 && /^[1-9][0-9]*$/.test(pmid) ? pmid : void 0;
2164
+ }
2165
+ function safeUrl(value) {
2166
+ const text3 = cleanText2(value);
2167
+ if (text3 === void 0) return void 0;
2168
+ try {
2169
+ const url = new URL(text3);
2170
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : void 0;
2171
+ } catch {
2172
+ return void 0;
2173
+ }
2174
+ }
2175
+ function yearFrom(...candidates) {
2176
+ for (const candidate of candidates) {
2177
+ const value = cleanText2(candidate);
2178
+ const year = value?.match(/(?:^|\D)((?:1[5-9]|20|21)[0-9]{2})(?:\D|$)/)?.[1];
2179
+ if (year !== void 0) return year;
2180
+ }
2181
+ return void 0;
2182
+ }
2183
+ function identifierValue(identifiers3, ...types) {
2184
+ const expected = new Set(types.map((type) => type.toLowerCase()));
2185
+ for (const item of values(identifiers3)) {
2186
+ const identifier = object4(item);
2187
+ const type = cleanText2(identifier?.type);
2188
+ const value = cleanText2(identifier?.value);
2189
+ if (type !== void 0 && value !== void 0 && expected.has(type.toLowerCase())) return value;
2190
+ }
2191
+ return void 0;
2192
+ }
2193
+ function recordAuthors(value) {
2194
+ return values(value).flatMap((item) => {
2195
+ const author = object4(item);
2196
+ if (author?.type === "collective") {
2197
+ const name = cleanText2(author.name);
2198
+ return name === void 0 ? [] : [{ ris: name, bibtex: name, collective: true }];
2199
+ }
2200
+ const lastName = cleanText2(author?.lastName);
2201
+ const foreName = cleanText2(author?.foreName) ?? cleanText2(author?.initials);
2202
+ const suffix = cleanText2(author?.suffix);
2203
+ const risStructured = [lastName, foreName, suffix].filter((part) => part !== void 0 && part !== "").join(", ");
2204
+ const bibtexStructured = [lastName, suffix, foreName].filter((part) => part !== void 0 && part !== "").join(", ");
2205
+ const fullName = cleanText2(author?.fullName);
2206
+ const ris = risStructured === "" ? fullName : risStructured;
2207
+ const bibtex = bibtexStructured === "" ? fullName : bibtexStructured;
2208
+ return ris === void 0 || bibtex === void 0 ? [] : [{ ris, bibtex, collective: false }];
2209
+ });
2210
+ }
2211
+ function summaryBibTexName(name) {
2212
+ if (name.includes(",")) return name;
2213
+ const parts = name.split(" ");
2214
+ const suffixPattern = /^(?:Jr\.?|Sr\.?|I{2,3}|IV)$/i;
2215
+ const possibleSuffix = parts.at(-1);
2216
+ const suffix = possibleSuffix !== void 0 && suffixPattern.test(possibleSuffix) ? possibleSuffix : void 0;
2217
+ const initialsIndex = suffix === void 0 ? parts.length - 1 : parts.length - 2;
2218
+ const initials = initialsIndex > 0 ? parts[initialsIndex] : void 0;
2219
+ const familyName = initials === void 0 ? void 0 : parts.slice(0, initialsIndex).join(" ");
2220
+ if (familyName === void 0 || familyName === "") return name;
2221
+ return suffix === void 0 ? `${familyName}, ${initials}` : `${familyName}, ${suffix}, ${initials}`;
2222
+ }
2223
+ function summaryAuthors(value) {
2224
+ return values(value).flatMap((item) => {
2225
+ const author = object4(item);
2226
+ const name = cleanText2(author?.name);
2227
+ if (name === void 0) return [];
2228
+ const authorType = cleanText2(author?.type)?.toLowerCase() ?? "";
2229
+ const collective = authorType.includes("collective");
2230
+ return [{ ris: name, bibtex: collective ? name : summaryBibTexName(name), collective }];
2231
+ });
2232
+ }
2233
+ function recordYear(record, journal2) {
2234
+ const dates2 = object4(record.dates);
2235
+ const publicationDate = object4(journal2?.pubDate);
2236
+ const electronic = object4(dates2?.electronic);
2237
+ const print = object4(dates2?.print);
2238
+ const completed = object4(dates2?.completed);
2239
+ return yearFrom(
2240
+ publicationDate?.year,
2241
+ publicationDate?.medlineDate,
2242
+ electronic?.year,
2243
+ electronic?.medlineDate,
2244
+ print?.year,
2245
+ print?.medlineDate,
2246
+ completed?.year,
2247
+ completed?.medlineDate
2248
+ );
2249
+ }
2250
+ function canonicalRecordUrl(record, pmid) {
2251
+ for (const item of values(record.links)) {
2252
+ const link = object4(item);
2253
+ if (link?.type === "pubmed" && link.provenance === "canonical") {
2254
+ const url = safeUrl(link.url);
2255
+ if (url !== void 0) return url;
2256
+ }
2257
+ }
2258
+ return pmid === void 0 ? void 0 : `https://pubmed.ncbi.nlm.nih.gov/${pmid}/`;
2259
+ }
2260
+ function mapArticle(record) {
2261
+ const journal2 = object4(record.journal);
2262
+ const pmid = validPmid(record.pmid) ?? validPmid(identifierValue(record.identifiers, "pubmed", "pmid"));
2263
+ const doi = cleanText2(record.doi) ?? identifierValue(record.identifiers, "doi");
2264
+ const issn = cleanText2(journal2?.issn);
2265
+ const title = cleanText2(record.title);
2266
+ const containerTitle = cleanText2(journal2?.title);
2267
+ const containerAbbreviation = cleanText2(journal2?.isoAbbreviation);
2268
+ const year = recordYear(record, journal2);
2269
+ const volume = cleanText2(journal2?.volume);
2270
+ const issue = cleanText2(journal2?.issue);
2271
+ const pages = cleanText2(journal2?.pagination);
2272
+ const url = canonicalRecordUrl(record, pmid);
2273
+ return {
2274
+ type: "article",
2275
+ authors: recordAuthors(record.authors),
2276
+ serialNumbers: issn === void 0 ? [] : [issn],
2277
+ ...pmid === void 0 ? {} : { pmid },
2278
+ ...doi === void 0 ? {} : { doi },
2279
+ ...title === void 0 ? {} : { title },
2280
+ ...containerTitle === void 0 ? {} : { containerTitle },
2281
+ ...containerAbbreviation === void 0 ? {} : { containerAbbreviation },
2282
+ ...year === void 0 ? {} : { year },
2283
+ ...volume === void 0 ? {} : { volume },
2284
+ ...issue === void 0 ? {} : { issue },
2285
+ ...pages === void 0 ? {} : { pages },
2286
+ ...url === void 0 ? {} : { url }
2287
+ };
2288
+ }
2289
+ function mapBook(record) {
2290
+ const book2 = object4(record.book);
2291
+ const pmid = validPmid(record.pmid) ?? validPmid(identifierValue(record.identifiers, "pubmed", "pmid"));
2292
+ const doi = cleanText2(record.doi) ?? identifierValue(record.identifiers, "doi");
2293
+ const recordTitle = cleanText2(record.title);
2294
+ const bookTitle = cleanText2(book2?.title);
2295
+ const type = recordTitle === void 0 ? "book" : "chapter";
2296
+ const title = recordTitle ?? bookTitle;
2297
+ const containerTitle = type === "chapter" ? bookTitle : void 0;
2298
+ const serialNumbers = values(book2?.isbn).flatMap((item) => cleanText2(item) ?? []);
2299
+ const year = recordYear(record, void 0);
2300
+ const publisher = cleanText2(book2?.publisher);
2301
+ const publisherLocation = cleanText2(book2?.location);
2302
+ const edition = cleanText2(book2?.edition);
2303
+ const url = canonicalRecordUrl(record, pmid);
2304
+ return {
2305
+ type,
2306
+ authors: recordAuthors(record.authors),
2307
+ serialNumbers,
2308
+ ...pmid === void 0 ? {} : { pmid },
2309
+ ...doi === void 0 ? {} : { doi },
2310
+ ...title === void 0 ? {} : { title },
2311
+ ...containerTitle === void 0 ? {} : { containerTitle },
2312
+ ...year === void 0 ? {} : { year },
2313
+ ...publisher === void 0 ? {} : { publisher },
2314
+ ...publisherLocation === void 0 ? {} : { publisherLocation },
2315
+ ...edition === void 0 ? {} : { edition },
2316
+ ...url === void 0 ? {} : { url }
2317
+ };
2318
+ }
2319
+ function mapSummary2(summary) {
2320
+ const journal2 = object4(summary.journal);
2321
+ const book2 = object4(summary.book);
2322
+ const summaryTitle = cleanText2(summary.title);
2323
+ const bookContainer = cleanText2(book2?.title) ?? cleanText2(book2?.name);
2324
+ const type = book2 === void 0 ? "article" : summaryTitle === void 0 ? "book" : "chapter";
2325
+ const title = type === "book" ? bookContainer : summaryTitle;
2326
+ const pmid = validPmid(summary.pmid) ?? validPmid(summary.uid);
2327
+ const doi = cleanText2(summary.doi) ?? identifierValue(summary.identifiers, "doi");
2328
+ const serialNumbers = type !== "article" ? values(summary.identifiers).flatMap((item) => {
2329
+ const identifier = object4(item);
2330
+ return cleanText2(identifier?.type)?.toLowerCase() === "isbn" ? cleanText2(identifier?.value) ?? [] : [];
2331
+ }) : [cleanText2(journal2?.issn), cleanText2(journal2?.electronicIssn)].filter((value) => value !== void 0);
2332
+ const availableUrl = safeUrl(summary.availableFromUrl);
2333
+ const canonicalUrl = pmid === void 0 ? availableUrl : `https://pubmed.ncbi.nlm.nih.gov/${pmid}/`;
2334
+ const journalTitle = cleanText2(journal2?.title);
2335
+ const journalAbbreviation = cleanText2(journal2?.abbreviation);
2336
+ const containerTitle = type === "chapter" ? bookContainer : type === "article" ? journalTitle : void 0;
2337
+ const year = yearFrom(summary.publicationDate, summary.electronicPublicationDate, summary.sortPublicationDate);
2338
+ const volume = cleanText2(journal2?.volume);
2339
+ const issue = cleanText2(journal2?.issue);
2340
+ const pages = cleanText2(journal2?.pages);
2341
+ const articleNumber = cleanText2(summary.electronicLocationId);
2342
+ const publisher = cleanText2(book2?.publisher) ?? cleanText2(summary.publisher);
2343
+ const publisherLocation = cleanText2(book2?.location) ?? cleanText2(summary.publisherLocation);
2344
+ const edition = cleanText2(book2?.edition) ?? cleanText2(summary.edition);
2345
+ return {
2346
+ type,
2347
+ authors: summaryAuthors(summary.authors),
2348
+ serialNumbers,
2349
+ ...pmid === void 0 ? {} : { pmid },
2350
+ ...doi === void 0 ? {} : { doi },
2351
+ ...title === void 0 ? {} : { title },
2352
+ ...containerTitle === void 0 ? {} : { containerTitle },
2353
+ ...type === "article" && journalAbbreviation !== void 0 ? { containerAbbreviation: journalAbbreviation } : {},
2354
+ ...year === void 0 ? {} : { year },
2355
+ ...volume === void 0 ? {} : { volume },
2356
+ ...issue === void 0 ? {} : { issue },
2357
+ ...pages === void 0 ? {} : { pages },
2358
+ ...articleNumber === void 0 ? {} : { articleNumber },
2359
+ ...publisher === void 0 ? {} : { publisher },
2360
+ ...publisherLocation === void 0 ? {} : { publisherLocation },
2361
+ ...edition === void 0 ? {} : { edition },
2362
+ ...canonicalUrl === void 0 ? {} : { url: canonicalUrl }
2363
+ };
2364
+ }
2365
+ function citationData(source) {
2366
+ const record = object4(source);
2367
+ if (record?.kind === "article") return mapArticle(record);
2368
+ if (record?.kind === "book") return mapBook(record);
2369
+ if (record?.kind === "summary") return mapSummary2(record);
2370
+ throw new ValidationError("citation source must be a PubMed article, book record, or summary");
2371
+ }
2372
+ function addRis(lines, tag, value) {
2373
+ if (value !== void 0) lines.push(`${tag} - ${value}`);
2374
+ }
2375
+ function pageParts(pages) {
2376
+ if (pages === void 0) return {};
2377
+ const match = pages.match(/^(.+?)[-–—](.+)$/);
2378
+ return match?.[1] === void 0 || match[2] === void 0 ? { start: pages } : { start: match[1].trim(), end: match[2].trim() };
2379
+ }
2380
+ function formatRis(data) {
2381
+ const risType = data.type === "article" ? "JOUR" : data.type === "chapter" ? "CHAP" : "BOOK";
2382
+ const lines = [`TY - ${risType}`];
2383
+ addRis(lines, "TI", data.title);
2384
+ for (const author of data.authors) addRis(lines, "AU", author.ris);
2385
+ addRis(lines, data.type === "article" ? "JF" : "T2", data.containerTitle);
2386
+ if (data.type === "article") addRis(lines, "JA", data.containerAbbreviation);
2387
+ addRis(lines, "PY", data.year);
2388
+ addRis(lines, "VL", data.volume);
2389
+ addRis(lines, "IS", data.issue);
2390
+ const pages = pageParts(data.pages);
2391
+ addRis(lines, "SP", pages.start);
2392
+ addRis(lines, "EP", pages.end);
2393
+ addRis(lines, "C7", data.articleNumber);
2394
+ addRis(lines, "ET", data.edition);
2395
+ addRis(lines, "PB", data.publisher);
2396
+ addRis(lines, "CY", data.publisherLocation);
2397
+ for (const serialNumber of data.serialNumbers) addRis(lines, "SN", serialNumber);
2398
+ addRis(lines, "DO", data.doi);
2399
+ addRis(lines, "AN", data.pmid === void 0 ? void 0 : `PMID:${data.pmid}`);
2400
+ addRis(lines, "UR", data.url);
2401
+ lines.push("ER -");
2402
+ return lines.join("\n");
2403
+ }
2404
+ function escapeBibTex(value) {
2405
+ let escaped = "";
2406
+ for (const character of value) {
2407
+ switch (character) {
2408
+ case "\\":
2409
+ escaped += "\\textbackslash{}";
2410
+ break;
2411
+ case "{":
2412
+ escaped += "\\{";
2413
+ break;
2414
+ case "}":
2415
+ escaped += "\\}";
2416
+ break;
2417
+ case "#":
2418
+ case "$":
2419
+ case "%":
2420
+ case "&":
2421
+ case "_":
2422
+ escaped += `\\${character}`;
2423
+ break;
2424
+ case "~":
2425
+ escaped += "\\textasciitilde{}";
2426
+ break;
2427
+ case "^":
2428
+ escaped += "\\textasciicircum{}";
2429
+ break;
2430
+ default:
2431
+ escaped += character;
2432
+ }
2433
+ }
2434
+ return escaped;
2435
+ }
2436
+ function bibKey(data) {
2437
+ if (data.pmid !== void 0) return `pubmed${data.pmid}`;
2438
+ const basis = data.doi ?? data.title ?? data.containerTitle ?? "citation";
2439
+ const slug = basis.normalize("NFKD").replace(/[^A-Za-z0-9]+/g, "").slice(0, 48);
2440
+ return slug === "" ? "citation" : slug;
2441
+ }
2442
+ function addBib(fields, name, value) {
2443
+ if (value !== void 0) fields.push([name, value]);
2444
+ }
2445
+ function bibTexAuthors(authors3) {
2446
+ if (authors3.length === 0) return void 0;
2447
+ return authors3.map((author) => {
2448
+ const escaped = escapeBibTex(author.bibtex);
2449
+ return author.collective ? `{${escaped}}` : escaped;
2450
+ }).join(" and ");
2451
+ }
2452
+ function formatBibTex(data, key = bibKey(data)) {
2453
+ const fields = [];
2454
+ const renderedAuthors = bibTexAuthors(data.authors);
2455
+ if (renderedAuthors !== void 0) fields.push(["author", renderedAuthors]);
2456
+ addBib(fields, "title", data.title);
2457
+ addBib(fields, data.type === "article" ? "journal" : "booktitle", data.containerTitle);
2458
+ addBib(fields, "year", data.year);
2459
+ addBib(fields, "volume", data.volume);
2460
+ addBib(fields, "number", data.issue);
2461
+ addBib(fields, "pages", data.pages?.replace(/[-–—]+/g, "--"));
2462
+ addBib(fields, "eid", data.articleNumber);
2463
+ addBib(fields, "edition", data.edition);
2464
+ addBib(fields, "publisher", data.publisher);
2465
+ addBib(fields, "address", data.publisherLocation);
2466
+ addBib(fields, data.type === "article" ? "issn" : "isbn", data.serialNumbers.length === 0 ? void 0 : data.serialNumbers.join(", "));
2467
+ addBib(fields, "doi", data.doi);
2468
+ addBib(fields, "pmid", data.pmid);
2469
+ addBib(fields, "url", data.url);
2470
+ const rendered = fields.map(([name, value]) => ` ${name} = {${name === "author" ? value : escapeBibTex(value)}}`).join(",\n");
2471
+ const entryType = data.type === "article" ? "article" : data.type === "chapter" ? "incollection" : "book";
2472
+ return `@${entryType}{${key},${rendered === "" ? "" : `
2473
+ ${rendered}`}
2474
+ }`;
2475
+ }
2476
+ function formatCitation(source, format) {
2477
+ const data = citationData(source);
2478
+ if (format === "ris") return formatRis(data);
2479
+ if (format === "bibtex") return formatBibTex(data);
2480
+ throw new ValidationError('citation format must be "ris" or "bibtex"');
2481
+ }
2482
+ function formatCitations(sources, format) {
2483
+ if (!Array.isArray(sources)) throw new ValidationError("citation sources must be an array");
2484
+ if (format !== "ris" && format !== "bibtex") throw new ValidationError('citation format must be "ris" or "bibtex"');
2485
+ const data = sources.map(citationData);
2486
+ if (format === "ris") return data.map(formatRis).join("\n\n");
2487
+ const keyOccurrences = /* @__PURE__ */ new Map();
2488
+ return data.map((citation) => {
2489
+ const baseKey = bibKey(citation);
2490
+ const occurrence = (keyOccurrences.get(baseKey) ?? 0) + 1;
2491
+ keyOccurrences.set(baseKey, occurrence);
2492
+ return formatBibTex(citation, occurrence === 1 ? baseKey : `${baseKey}-${occurrence}`);
2493
+ }).join("\n\n");
2494
+ }
2495
+ export {
2496
+ AbortedError,
2497
+ CursorExpiredError,
2498
+ CursorInvalidError,
2499
+ HttpError,
2500
+ InvalidResponseError,
2501
+ MemoryCache,
2502
+ NetworkError,
2503
+ ParseError,
2504
+ PubMedClient,
2505
+ PubMedError,
2506
+ QueueFullError,
2507
+ RateLimitError,
2508
+ ResponseTooLargeError,
2509
+ SearchLimitError,
2510
+ TimeoutError,
2511
+ ValidationError,
2512
+ formatCitation,
2513
+ formatCitations,
2514
+ parsePubMedXml
2515
+ };