med-research-mcp 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/README.zh-CN.md +10 -1
- package/dist/core/adapters/europepmc.d.ts +12 -1
- package/dist/core/adapters/europepmc.d.ts.map +1 -1
- package/dist/core/adapters/http.d.ts +4 -3
- package/dist/core/adapters/http.d.ts.map +1 -1
- package/dist/core/adapters/pubmed.d.ts +1 -1
- package/dist/core/adapters/pubmed.d.ts.map +1 -1
- package/dist/core/adapters/types.d.ts +4 -2
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/adapters/util.d.ts +19 -0
- package/dist/core/adapters/util.d.ts.map +1 -1
- package/dist/core/cache.cjs +34 -15
- package/dist/core/cache.cjs.map +1 -1
- package/dist/core/cache.d.ts +22 -1
- package/dist/core/cache.d.ts.map +1 -1
- package/dist/core/cache.js +33 -15
- package/dist/core/cache.js.map +1 -1
- package/dist/core/fulltext.d.ts +7 -3
- package/dist/core/fulltext.d.ts.map +1 -1
- package/dist/core/grader.d.ts +25 -2
- package/dist/core/grader.d.ts.map +1 -1
- package/dist/core/index.cjs +505 -271
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +504 -271
- package/dist/core/index.js.map +1 -1
- package/dist/core/literature.d.ts +4 -3
- package/dist/core/literature.d.ts.map +1 -1
- package/dist/core/pubmed.d.ts +24 -3
- package/dist/core/pubmed.d.ts.map +1 -1
- package/dist/core/request.d.ts +41 -0
- package/dist/core/request.d.ts.map +1 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.d.ts.map +1 -0
- package/dist/core/types.d.ts +20 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/index.cjs +528 -273
- package/dist/http/index.cjs.map +1 -1
- package/dist/http/index.js +528 -273
- package/dist/http/index.js.map +1 -1
- package/dist/mcp/index.cjs +526 -272
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +526 -272
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/mcpb/manifest.json +1 -1
- package/package.json +2 -1
- package/skills/med-research/SKILL.md +2 -2
- package/skills/med-research-setup/SKILL.md +1 -1
- package/src/core/adapters/europepmc.ts +18 -4
- package/src/core/adapters/http.ts +12 -40
- package/src/core/adapters/pubmed.ts +3 -2
- package/src/core/adapters/types.ts +4 -2
- package/src/core/adapters/util.ts +40 -0
- package/src/core/cache.ts +51 -15
- package/src/core/fulltext.ts +136 -38
- package/src/core/grader.ts +70 -3
- package/src/core/index.ts +1 -1
- package/src/core/literature.ts +35 -50
- package/src/core/pubmed.ts +110 -76
- package/src/core/request.ts +119 -0
- package/src/core/text.ts +150 -0
- package/src/core/types.ts +20 -3
- package/src/http/handler.ts +8 -1
- package/src/mcp/server.ts +58 -15
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
<strong>Evidence-graded scientific literature search for AI agents.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/med-research-mcp"><img src="https://img.shields.io/npm/v/med-research-mcp" alt="npm version"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
11
15
|
<p align="center">
|
|
12
16
|
English · <a href="https://github.com/emilkrebs/med-research-mcp/blob/main/README.zh-CN.md">简体中文</a>
|
|
13
17
|
</p>
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
|
|
36
40
|
## Install
|
|
37
41
|
|
|
38
|
-
You need [Node.js](https://nodejs.org) 22.22.2 or later on 22, 24.15.0 or later on 24, or 26+. The server runs straight from npm, so there's nothing to clone or build. In the examples, replace `you@example.com` with your email (see [Configuration](#configuration)).
|
|
42
|
+
You need [Node.js](https://nodejs.org) 22.22.2 or later on 22, 24.15.0 or later on 24, or 26+. The server runs straight from [npm](https://www.npmjs.com/package/med-research-mcp), so there's nothing to clone or build. In the examples, replace `you@example.com` with your email (see [Configuration](#configuration)).
|
|
39
43
|
|
|
40
44
|
### Claude Code
|
|
41
45
|
|
|
@@ -69,6 +73,10 @@ Add this to the client's MCP configuration:
|
|
|
69
73
|
|
|
70
74
|
Clients configured in YAML, such as Hermes, take the same `command`, `args` and `env`.
|
|
71
75
|
|
|
76
|
+
### ModelScope
|
|
77
|
+
|
|
78
|
+
The server is also listed on [ModelScope](https://www.modelscope.cn/mcp/servers/emilkrebs/med-research-mcp) (魔搭社区), which can host it for you, so there's nothing to install.
|
|
79
|
+
|
|
72
80
|
### From source
|
|
73
81
|
|
|
74
82
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
<strong>面向 AI 智能体、按证据等级标注的科学文献检索。</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/med-research-mcp"><img src="https://img.shields.io/npm/v/med-research-mcp" alt="npm 版本"></a>
|
|
13
|
+
<a href="https://www.modelscope.cn/mcp/servers/emilkrebs/med-research-mcp"><img src="https://img.shields.io/badge/%E9%AD%94%E6%90%AD%E7%A4%BE%E5%8C%BA-MCP%20%E6%9C%8D%E5%8A%A1%E5%99%A8-624aff" alt="魔搭社区 MCP 服务器"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
11
16
|
<p align="center">
|
|
12
17
|
<a href="https://github.com/emilkrebs/med-research-mcp/blob/main/README.md">English</a> · 简体中文
|
|
13
18
|
</p>
|
|
@@ -40,7 +45,7 @@
|
|
|
40
45
|
|
|
41
46
|
## 安装
|
|
42
47
|
|
|
43
|
-
需要 [Node.js](https://nodejs.org) 22.22.2 或更高的 22.x 版本、24.15.0 或更高的 24.x 版本,或 26 及以上版本。服务器直接从 npm 运行,无需克隆仓库或构建。请将示例中的 `you@example.com` 替换为你的邮箱(见[配置](#配置))。
|
|
48
|
+
需要 [Node.js](https://nodejs.org) 22.22.2 或更高的 22.x 版本、24.15.0 或更高的 24.x 版本,或 26 及以上版本。服务器直接从 [npm](https://www.npmjs.com/package/med-research-mcp) 运行,无需克隆仓库或构建。请将示例中的 `you@example.com` 替换为你的邮箱(见[配置](#配置))。
|
|
44
49
|
|
|
45
50
|
### Claude Code
|
|
46
51
|
|
|
@@ -74,6 +79,10 @@ claude mcp add med-research-mcp -s user \
|
|
|
74
79
|
|
|
75
80
|
使用 YAML 配置的客户端(如 Hermes)填写相同的 `command`、`args` 和 `env` 即可。
|
|
76
81
|
|
|
82
|
+
### 魔搭社区(ModelScope)
|
|
83
|
+
|
|
84
|
+
本服务器也已在[魔搭社区](https://www.modelscope.cn/mcp/servers/emilkrebs/med-research-mcp)上架,可由魔搭托管运行,无需在本地安装任何软件。
|
|
85
|
+
|
|
77
86
|
### 从源码安装
|
|
78
87
|
|
|
79
88
|
```bash
|
|
@@ -17,7 +17,17 @@
|
|
|
17
17
|
* server in `bookOrReportDetails.publisher`; `journalTitle` exists only in `lite`
|
|
18
18
|
* results. The date is the issue date (`journalInfo.dateOfPublication`, "2026
|
|
19
19
|
* Sep", like PubMed's), else the first publication date; recently added PMC
|
|
20
|
-
* records have neither `pubYear` nor an issue date.
|
|
20
|
+
* records have neither `pubYear` nor an issue date. `electronicPublicationDate`
|
|
21
|
+
* becomes `epubDate`. A date range is matched against FIRST_PDATE, the earlier of
|
|
22
|
+
* the two: record 39680131, published online 2024-12-16 and in print 2025-07-01,
|
|
23
|
+
* matches FIRST_PDATE in December 2024 and P_PDATE in July 2025 (2026-10-22). A
|
|
24
|
+
* date sort orders by FIRST_PDATE too, as the merged results are: sorted by P_PDATE,
|
|
25
|
+
* `semaglutide AND SRC:MED` led with an issue of January 2027 published online in
|
|
26
|
+
* August 2026, and records published online on 2026-10-20 didn't make the first page
|
|
27
|
+
* (2026-10-22).
|
|
28
|
+
*
|
|
29
|
+
* Titles and abstracts carry HTML (`<h4>` section headings, `<sup>`, `<i>`), titles
|
|
30
|
+
* of MEDLINE records sometimes escaped (`<i>`); both are turned into text.
|
|
21
31
|
*
|
|
22
32
|
* Ids are only unique within a source: non-PubMed sources such as CBA (Chinese
|
|
23
33
|
* Biological Abstracts) reuse plain numbers that collide with PMIDs, so those
|
|
@@ -50,6 +60,7 @@ interface EpmcResult {
|
|
|
50
60
|
};
|
|
51
61
|
pubYear?: string;
|
|
52
62
|
firstPublicationDate?: string;
|
|
63
|
+
electronicPublicationDate?: string;
|
|
53
64
|
abstractText?: string;
|
|
54
65
|
language?: string;
|
|
55
66
|
fullTextUrlList?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"europepmc.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/europepmc.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"europepmc.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/europepmc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,OAAO,KAAK,EAAE,OAAO,EAA8B,WAAW,EAAE,MAAM,aAAa,CAAC;AAKpF,UAAU,UAAU;IAClB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,6CAA6C;IAC7C,mBAAmB,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;IACvD,WAAW,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAC;IAC9C,eAAe,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE;YAAE,cAAc,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;IAClE,qBAAqB,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE;YAAE,EAAE,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CACnG;AAqBD,UAAU,YAAY;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;CACxC;AAED,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAQ/C;AAsCD,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CASzF;AAED,qBAAa,gBAAiB,SAAQ,WAAW;IAC/C,QAAQ,CAAC,EAAE,eAAe;IAC1B,QAAQ,CAAC,IAAI,gBAAgB;IAE7B,YAAY,IAAI,GAAE,WAAmD,EAEpE;IAED,OAAO,CAAC,UAAU;IAYZ,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAe1E;IAED,UAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAS5D;CACF"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A tiny HTTP seam shared by the REST/HTML adapters.
|
|
3
3
|
*
|
|
4
|
-
* It centralizes timeout, User-Agent,
|
|
5
|
-
* mapping so each adapter is just URL
|
|
4
|
+
* It centralizes timeout, User-Agent, retries (of a 429, a server error or a
|
|
5
|
+
* timeout; see ../request.ts) and error mapping so each adapter is just URL
|
|
6
|
+
* construction + response parsing. The
|
|
6
7
|
* `AdapterHttp` interface is injectable: adapters take one in their constructor
|
|
7
8
|
* and default to {@link defaultHttp}, so tests pass a fake that returns fixtures
|
|
8
9
|
* without touching the network — mirroring how `NcbiClient` and `fetchFulltext`
|
|
@@ -20,7 +21,7 @@ export interface HttpOptions {
|
|
|
20
21
|
userAgent?: string;
|
|
21
22
|
/** Extra headers merged into every request (after User-Agent/Accept, so they can override). */
|
|
22
23
|
headers?: Record<string, string>;
|
|
23
|
-
/** Service name for
|
|
24
|
+
/** Service name for errors, e.g. "Europe PMC". */
|
|
24
25
|
source?: string;
|
|
25
26
|
}
|
|
26
27
|
export declare function defaultHttp(options?: HttpOptions): AdapterHttp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/http.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,WAAW,CAsBlE"}
|
|
@@ -20,6 +20,6 @@ export declare class PubMedAdapter implements DatabaseAdapter {
|
|
|
20
20
|
count: number;
|
|
21
21
|
ids: string[];
|
|
22
22
|
}>;
|
|
23
|
-
fetch(ids: string[], cache: Cache): Promise<Article[]>;
|
|
23
|
+
fetch(ids: string[], cache: Cache, warn?: (warning: string) => void): Promise<Article[]>;
|
|
24
24
|
}
|
|
25
25
|
//# sourceMappingURL=pubmed.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pubmed.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/pubmed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAe,UAAU,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,qBAAa,aAAc,YAAW,eAAe;IACnD,QAAQ,CAAC,EAAE,YAAY;IACvB,QAAQ,CAAC,IAAI,mBAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IAEpC,yEAAyE;IACzE,YAAY,MAAM,GAAE,UAA6B,EAEhD;IAEK,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAG1E;IAEK,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"pubmed.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/pubmed.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAe,UAAU,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,qBAAa,aAAc,YAAW,eAAe;IACnD,QAAQ,CAAC,EAAE,YAAY;IACvB,QAAQ,CAAC,IAAI,mBAAmB;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IAEpC,yEAAyE;IACzE,YAAY,MAAM,GAAE,UAA6B,EAEhD;IAEK,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAG1E;IAEK,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAK7F;CACF"}
|
|
@@ -32,9 +32,11 @@ export interface DatabaseAdapter {
|
|
|
32
32
|
/**
|
|
33
33
|
* Fetch full metadata ({@link Article}[]) for one or more ids. Order is not
|
|
34
34
|
* guaranteed; ids with no result are simply absent from the returned array.
|
|
35
|
-
* The cache is scoped per-database (see {@link RestAdapter}).
|
|
35
|
+
* The cache is scoped per-database (see {@link RestAdapter}). `warn` receives a
|
|
36
|
+
* warning for records returned incomplete, such as PubMed records without their
|
|
37
|
+
* abstract and retraction links when efetch failed.
|
|
36
38
|
*/
|
|
37
|
-
fetch(ids: string[], cache: Cache): Promise<Article[]>;
|
|
39
|
+
fetch(ids: string[], cache: Cache, warn?: (warning: string) => void): Promise<Article[]>;
|
|
38
40
|
/**
|
|
39
41
|
* Complete records for an explicit fetch (`literature_fetch`), for databases
|
|
40
42
|
* whose search endpoint only returns previews (e.g. ChinaXiv cuts abstracts at
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxF
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxF;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACzF;;;;;OAKG;IACH,SAAS,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CAC7D"}
|
|
@@ -19,6 +19,25 @@ export declare function withoutPubMedTags(query: string): string;
|
|
|
19
19
|
* 2020-05-01 or 2020-05-31. Undefined for anything that isn't a date.
|
|
20
20
|
*/
|
|
21
21
|
export declare function isoDateBound(value: string | undefined, bound: "from" | "to"): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* A publication date as a number that sorts chronologically (YYYYMMDD, 0 for a missing month
|
|
24
|
+
* or day), from the forms the databases report: "2019 Nov-Dec" (the first month counts),
|
|
25
|
+
* "2021 Mar 15", "2021 Spring" (year only), "2024-12-16". Undefined without a year.
|
|
26
|
+
*/
|
|
27
|
+
export declare function dateKey(date: string | undefined): number | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* When a record was first published, as a {@link dateKey}; -1 without a date. That is the earlier
|
|
30
|
+
* of its issue and electronic publication dates, as PubMed's website sorts by publication date
|
|
31
|
+
* ("Citations with more than one publication date, such as electronic and print, are sorted by their
|
|
32
|
+
* earliest publication date", PubMed Help, checked 2026-10-22) and both PubMed's and Europe PMC's date
|
|
33
|
+
* filters match, unless
|
|
34
|
+
* one date only narrows the other: PMID 42422741, issue "2026" and online "2026 Jun 24", was first
|
|
35
|
+
* published on June 24, as NCBI's own sort date for it says, not at the start of 2026 (2026-10-22).
|
|
36
|
+
*/
|
|
37
|
+
export declare function firstPublished(article: {
|
|
38
|
+
pubDate?: string;
|
|
39
|
+
epubDate?: string;
|
|
40
|
+
}): number;
|
|
22
41
|
/** Split a comma/semicolon-separated author string ("Smith J, Doe A") into names. */
|
|
23
42
|
export declare function splitAuthors(value: unknown): string[];
|
|
24
43
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/util.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0FAA0F;AAC1F,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAKxD;AAED,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAKjD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKvD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAUhG;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAOrD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQpE;AAED,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAElE;AAkBD,4FAA4F;AAC5F,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKnE"}
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/util.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0FAA0F;AAC1F,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAKxD;AAED,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAKjD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKvD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAUhG;AAID;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAOvF;AAQD,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAOrD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAQpE;AAED,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAElE;AAkBD,4FAA4F;AAC5F,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKnE"}
|
package/dist/core/cache.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/core/cache.ts
|
|
21
21
|
var cache_exports = {};
|
|
22
22
|
__export(cache_exports, {
|
|
23
|
+
CACHE_FORMAT_VERSION: () => CACHE_FORMAT_VERSION,
|
|
23
24
|
FULLTEXT_TTL_MS: () => FULLTEXT_TTL_MS,
|
|
24
25
|
LRUBackend: () => LRUBackend,
|
|
25
26
|
NullCache: () => NullCache,
|
|
@@ -84,18 +85,23 @@ var NullCache = class {
|
|
|
84
85
|
setFulltext() {
|
|
85
86
|
}
|
|
86
87
|
};
|
|
88
|
+
var CACHE_FORMAT_VERSION = 1;
|
|
89
|
+
var SUMMARIES = `summaries_v${CACHE_FORMAT_VERSION}`;
|
|
90
|
+
var ABSTRACTS = `abstracts_v${CACHE_FORMAT_VERSION}`;
|
|
91
|
+
var FULLTEXTS = `fulltexts_v${CACHE_FORMAT_VERSION}`;
|
|
92
|
+
var VERSIONED_TABLE = /^(?:summaries|abstracts|fulltexts)(?:_v(\d+))?$/;
|
|
87
93
|
var SCHEMA_SQL = `
|
|
88
|
-
CREATE TABLE IF NOT EXISTS
|
|
94
|
+
CREATE TABLE IF NOT EXISTS ${SUMMARIES} (
|
|
89
95
|
pmid TEXT PRIMARY KEY,
|
|
90
96
|
article TEXT NOT NULL,
|
|
91
97
|
fetched_at INTEGER NOT NULL
|
|
92
98
|
);
|
|
93
|
-
CREATE TABLE IF NOT EXISTS
|
|
99
|
+
CREATE TABLE IF NOT EXISTS ${ABSTRACTS} (
|
|
94
100
|
pmid TEXT PRIMARY KEY,
|
|
95
101
|
abstract TEXT NOT NULL,
|
|
96
102
|
fetched_at INTEGER NOT NULL
|
|
97
103
|
);
|
|
98
|
-
CREATE TABLE IF NOT EXISTS
|
|
104
|
+
CREATE TABLE IF NOT EXISTS ${FULLTEXTS} (
|
|
99
105
|
pmid TEXT PRIMARY KEY,
|
|
100
106
|
source TEXT NOT NULL,
|
|
101
107
|
fulltext TEXT NOT NULL,
|
|
@@ -117,19 +123,20 @@ var SQLiteBackend = class {
|
|
|
117
123
|
this.db.pragma("journal_mode = WAL");
|
|
118
124
|
this.db.pragma("synchronous = NORMAL");
|
|
119
125
|
this.db.exec(SCHEMA_SQL);
|
|
126
|
+
this.clearOlderVersions();
|
|
120
127
|
this.stmts = {
|
|
121
|
-
getSummary: this.db.prepare(
|
|
122
|
-
vacuumSummary: this.db.prepare(
|
|
123
|
-
setSummary: this.db.prepare(
|
|
124
|
-
delSummary: this.db.prepare(
|
|
125
|
-
getAbstract: this.db.prepare(
|
|
126
|
-
setAbstract: this.db.prepare(
|
|
127
|
-
getFulltext: this.db.prepare(
|
|
128
|
+
getSummary: this.db.prepare(`SELECT article, fetched_at FROM ${SUMMARIES} WHERE pmid = ?`),
|
|
129
|
+
vacuumSummary: this.db.prepare(`DELETE FROM ${SUMMARIES} WHERE fetched_at < ?`),
|
|
130
|
+
setSummary: this.db.prepare(`INSERT OR REPLACE INTO ${SUMMARIES} (pmid, article, fetched_at) VALUES (?, ?, ?)`),
|
|
131
|
+
delSummary: this.db.prepare(`DELETE FROM ${SUMMARIES} WHERE pmid = ?`),
|
|
132
|
+
getAbstract: this.db.prepare(`SELECT abstract FROM ${ABSTRACTS} WHERE pmid = ?`),
|
|
133
|
+
setAbstract: this.db.prepare(`INSERT OR REPLACE INTO ${ABSTRACTS} (pmid, abstract, fetched_at) VALUES (?, ?, ?)`),
|
|
134
|
+
getFulltext: this.db.prepare(`SELECT source, fulltext, format, fetched_at FROM ${FULLTEXTS} WHERE pmid = ?`),
|
|
128
135
|
setFulltext: this.db.prepare(
|
|
129
|
-
|
|
136
|
+
`INSERT OR REPLACE INTO ${FULLTEXTS} (pmid, source, fulltext, format, fetched_at) VALUES (?, ?, ?, ?, ?)`
|
|
130
137
|
),
|
|
131
|
-
delFulltext: this.db.prepare(
|
|
132
|
-
vacuumFulltext: this.db.prepare(
|
|
138
|
+
delFulltext: this.db.prepare(`DELETE FROM ${FULLTEXTS} WHERE pmid = ?`),
|
|
139
|
+
vacuumFulltext: this.db.prepare(`DELETE FROM ${FULLTEXTS} WHERE fetched_at < ?`)
|
|
133
140
|
};
|
|
134
141
|
this.vacuum();
|
|
135
142
|
}
|
|
@@ -174,15 +181,26 @@ var SQLiteBackend = class {
|
|
|
174
181
|
setFulltext(pmid, data) {
|
|
175
182
|
this.stmts.setFulltext.run(pmid, data.source, data.fulltext, data.format, data.fetchedAt ?? Date.now());
|
|
176
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Empty the tables of older versions (see {@link CACHE_FORMAT_VERSION}); a newer version's are left
|
|
186
|
+
* alone. They are emptied, not dropped: an older version still running on the file would fail.
|
|
187
|
+
*/
|
|
188
|
+
clearOlderVersions() {
|
|
189
|
+
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").pluck().all();
|
|
190
|
+
for (const name of tables) {
|
|
191
|
+
const match = VERSIONED_TABLE.exec(name);
|
|
192
|
+
if (match && Number(match[1] ?? 0) < CACHE_FORMAT_VERSION) this.db.exec(`DELETE FROM ${name}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
177
195
|
/** Delete every expired summary and full-text row. Runs on construction; call periodically if desired. */
|
|
178
196
|
vacuum() {
|
|
179
197
|
this.stmts.vacuumSummary.run(Date.now() - SUMMARY_TTL_MS);
|
|
180
198
|
this.stmts.vacuumFulltext.run(Date.now() - FULLTEXT_TTL_MS);
|
|
181
199
|
}
|
|
182
|
-
/** Total rows across
|
|
200
|
+
/** Total rows across this version's three tables — handy for debugging. */
|
|
183
201
|
get size() {
|
|
184
202
|
const count = (table) => this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
|
|
185
|
-
return count(
|
|
203
|
+
return count(SUMMARIES) + count(ABSTRACTS) + count(FULLTEXTS);
|
|
186
204
|
}
|
|
187
205
|
/** Close the database connection. Call on process exit; operations afterward throw. */
|
|
188
206
|
close() {
|
|
@@ -203,6 +221,7 @@ function createCache(options) {
|
|
|
203
221
|
}
|
|
204
222
|
// Annotate the CommonJS export names for ESM import in node:
|
|
205
223
|
0 && (module.exports = {
|
|
224
|
+
CACHE_FORMAT_VERSION,
|
|
206
225
|
FULLTEXT_TTL_MS,
|
|
207
226
|
LRUBackend,
|
|
208
227
|
NullCache,
|
package/dist/core/cache.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/cache.ts"],"sourcesContent":["/**\n * Cache interface + backends.\n *\n * The interface is what the rest of the library depends on; backends are\n * swappable. Two ship: the in-memory `LRUBackend` (dies with the process) and\n * the persistent `SQLiteBackend` (survives restarts).\n *\n * better-sqlite3 is a native module, so it's loaded lazily — only when the\n * sqlite backend is actually constructed. That keeps `LRUBackend`-only\n * consumers from paying for (or being broken by) a native binding they never\n * use, and lets `createCache` fall back to LRU if the binding fails to load.\n */\n\nimport { createRequire } from \"node:module\";\nimport { LRUCache } from \"lru-cache\";\nimport type SqliteDatabase from \"better-sqlite3\";\nimport type { FulltextData, Article } from \"./types.js\";\n\n/** Full texts expire after 30 days — the paper is stable but resolved OA URLs rot. */\nexport const FULLTEXT_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\n/**\n * Summaries expire after 7 days. Abstracts don't change, but a summary's\n * metadata does: NLM adds publication types and MeSH when it indexes a record,\n * and retractions, expressions of concern and errata can arrive at any time.\n * A cached summary must not hide a retraction for long.\n */\nexport const SUMMARY_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport interface Cache {\n /** Returns a cached summary, or null on a miss or if older than {@link SUMMARY_TTL_MS}. */\n getSummary(pmid: string): Article | null;\n setSummary(pmid: string, article: Article): void;\n getAbstract(pmid: string): string | null;\n setAbstract(pmid: string, abstract: string): void;\n /** Returns a cached full text, or null on a miss or if older than {@link FULLTEXT_TTL_MS}. */\n getFulltext(pmid: string): FulltextData | null;\n setFulltext(pmid: string, data: FulltextData): void;\n}\n\nexport type CacheBackend = \"memory\" | \"sqlite\";\n\nexport interface CacheOptions {\n backend: CacheBackend;\n /** Max entries per store for the memory backend. Default 200. */\n max?: number;\n /** Filesystem path for the sqlite backend. Use \":memory:\" for an ephemeral DB. Default \"./pubmed-cache.db\". */\n path?: string;\n}\n\n/**\n * In-memory LRU cache. Dies with the process — right for short-lived agent\n * sessions. Abstracts never expire; summaries expire after {@link SUMMARY_TTL_MS}\n * (a server can run for days) and full texts after {@link FULLTEXT_TTL_MS}.\n */\nexport class LRUBackend implements Cache {\n private readonly summaries: LRUCache<string, Article>;\n private readonly abstracts: LRUCache<string, string>;\n private readonly fulltexts: LRUCache<string, FulltextData>;\n\n constructor(max = 200) {\n this.summaries = new LRUCache({ max, ttl: SUMMARY_TTL_MS });\n this.abstracts = new LRUCache({ max });\n // The LRU's own TTL evicts stale entries; getFulltext double-checks fetchedAt\n // so a caller-supplied stale timestamp is also honored.\n this.fulltexts = new LRUCache({ max, ttl: FULLTEXT_TTL_MS });\n }\n\n getSummary(pmid: string): Article | null {\n return this.summaries.get(pmid) ?? null;\n }\n\n setSummary(pmid: string, article: Article): void {\n this.summaries.set(pmid, article);\n }\n\n getAbstract(pmid: string): string | null {\n return this.abstracts.get(pmid) ?? null;\n }\n\n setAbstract(pmid: string, abstract: string): void {\n this.abstracts.set(pmid, abstract);\n }\n\n getFulltext(pmid: string): FulltextData | null {\n const data = this.fulltexts.get(pmid);\n if (!data) return null;\n if (Date.now() - data.fetchedAt > FULLTEXT_TTL_MS) {\n this.fulltexts.delete(pmid);\n return null;\n }\n return data;\n }\n\n setFulltext(pmid: string, data: FulltextData): void {\n this.fulltexts.set(pmid, data);\n }\n}\n\n/** A cache that stores nothing — useful for tests or explicit opt-out. */\nexport class NullCache implements Cache {\n getSummary(): Article | null {\n return null;\n }\n setSummary(): void {}\n getAbstract(): string | null {\n return null;\n }\n setAbstract(): void {}\n getFulltext(): FulltextData | null {\n return null;\n }\n setFulltext(): void {}\n}\n\n// ─── SQLite backend ─────────────────────────────────────────────\n\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS summaries (\n pmid TEXT PRIMARY KEY,\n article TEXT NOT NULL,\n fetched_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS abstracts (\n pmid TEXT PRIMARY KEY,\n abstract TEXT NOT NULL,\n fetched_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS fulltexts (\n pmid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n fulltext TEXT NOT NULL,\n format TEXT NOT NULL,\n fetched_at INTEGER NOT NULL\n);\n`;\n\ninterface FulltextRow {\n source: string;\n fulltext: string;\n format: string;\n fetched_at: number;\n}\n\n/**\n * Loads the native better-sqlite3 module on first use (see the module header).\n *\n * Resolves from this module's own location so better-sqlite3 is found even when\n * med-research-mcp is a nested dependency. `__filename` exists in the CJS build;\n * `import.meta.url` exists in the ESM build — esbuild leaves the other one\n * undefined per format, so `typeof` picks whichever is real. (`typeof` on an\n * undeclared identifier is safe — it evaluates to \"undefined\" rather than\n * throwing.)\n */\nfunction loadSqlite(): typeof SqliteDatabase {\n const from = typeof __filename !== \"undefined\" ? __filename : import.meta.url;\n const require = createRequire(from);\n return require(\"better-sqlite3\") as typeof SqliteDatabase;\n}\n\n/**\n * Persistent cache backed by a single SQLite file. Drop-in replacement for\n * {@link LRUBackend} — same interface, same semantics, but survives restarts so\n * abstracts are fetched from NCBI at most once.\n *\n * Abstracts never expire. Summaries expire after {@link SUMMARY_TTL_MS} because\n * their metadata changes (indexing, retractions, corrections); full texts after\n * {@link FULLTEXT_TTL_MS} because resolved OA URLs and proxy sessions rot.\n * Expiry is enforced at read time and swept by {@link vacuum}.\n */\nexport class SQLiteBackend implements Cache {\n private readonly db: SqliteDatabase.Database;\n private readonly stmts: {\n getSummary: SqliteDatabase.Statement<[string]>;\n vacuumSummary: SqliteDatabase.Statement<[number]>;\n setSummary: SqliteDatabase.Statement<[string, string, number]>;\n delSummary: SqliteDatabase.Statement<[string]>;\n getAbstract: SqliteDatabase.Statement<[string]>;\n setAbstract: SqliteDatabase.Statement<[string, string, number]>;\n getFulltext: SqliteDatabase.Statement<[string]>;\n setFulltext: SqliteDatabase.Statement<[string, string, string, string, number]>;\n delFulltext: SqliteDatabase.Statement<[string]>;\n vacuumFulltext: SqliteDatabase.Statement<[number]>;\n };\n\n constructor(path: string) {\n const Database = loadSqlite();\n this.db = new Database(path);\n this.db.pragma(\"journal_mode = WAL\"); // concurrent reads while another process writes\n this.db.pragma(\"synchronous = NORMAL\"); // safe with WAL, faster writes\n this.db.exec(SCHEMA_SQL);\n\n this.stmts = {\n getSummary: this.db.prepare(\"SELECT article, fetched_at FROM summaries WHERE pmid = ?\"),\n vacuumSummary: this.db.prepare(\"DELETE FROM summaries WHERE fetched_at < ?\"),\n setSummary: this.db.prepare(\"INSERT OR REPLACE INTO summaries (pmid, article, fetched_at) VALUES (?, ?, ?)\"),\n delSummary: this.db.prepare(\"DELETE FROM summaries WHERE pmid = ?\"),\n getAbstract: this.db.prepare(\"SELECT abstract FROM abstracts WHERE pmid = ?\"),\n setAbstract: this.db.prepare(\"INSERT OR REPLACE INTO abstracts (pmid, abstract, fetched_at) VALUES (?, ?, ?)\"),\n getFulltext: this.db.prepare(\"SELECT source, fulltext, format, fetched_at FROM fulltexts WHERE pmid = ?\"),\n setFulltext: this.db.prepare(\n \"INSERT OR REPLACE INTO fulltexts (pmid, source, fulltext, format, fetched_at) VALUES (?, ?, ?, ?, ?)\",\n ),\n delFulltext: this.db.prepare(\"DELETE FROM fulltexts WHERE pmid = ?\"),\n vacuumFulltext: this.db.prepare(\"DELETE FROM fulltexts WHERE fetched_at < ?\"),\n };\n\n this.vacuum();\n }\n\n getSummary(pmid: string): Article | null {\n const row = this.stmts.getSummary.get(pmid) as { article: string; fetched_at: number } | undefined;\n if (!row) return null;\n if (Date.now() - row.fetched_at > SUMMARY_TTL_MS) {\n this.stmts.delSummary.run(pmid);\n return null;\n }\n try {\n return JSON.parse(row.article) as Article;\n } catch {\n // Corrupt JSON — drop the row and treat as a miss.\n this.stmts.delSummary.run(pmid);\n return null;\n }\n }\n\n setSummary(pmid: string, article: Article): void {\n this.stmts.setSummary.run(pmid, JSON.stringify(article), Date.now());\n }\n\n getAbstract(pmid: string): string | null {\n const row = this.stmts.getAbstract.get(pmid) as { abstract: string } | undefined;\n return row ? row.abstract : null;\n }\n\n setAbstract(pmid: string, abstract: string): void {\n this.stmts.setAbstract.run(pmid, abstract, Date.now());\n }\n\n getFulltext(pmid: string): FulltextData | null {\n const row = this.stmts.getFulltext.get(pmid) as FulltextRow | undefined;\n if (!row) return null;\n if (Date.now() - row.fetched_at > FULLTEXT_TTL_MS) {\n this.stmts.delFulltext.run(pmid);\n return null;\n }\n return {\n source: row.source,\n fulltext: row.fulltext,\n format: row.format === \"pdf\" ? \"pdf\" : \"markdown\",\n fetchedAt: row.fetched_at,\n };\n }\n\n setFulltext(pmid: string, data: FulltextData): void {\n this.stmts.setFulltext.run(pmid, data.source, data.fulltext, data.format, data.fetchedAt ?? Date.now());\n }\n\n /** Delete every expired summary and full-text row. Runs on construction; call periodically if desired. */\n vacuum(): void {\n this.stmts.vacuumSummary.run(Date.now() - SUMMARY_TTL_MS);\n this.stmts.vacuumFulltext.run(Date.now() - FULLTEXT_TTL_MS);\n }\n\n /** Total rows across all three tables — handy for debugging. */\n get size(): number {\n const count = (table: string): number =>\n (this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number }).n;\n return count(\"summaries\") + count(\"abstracts\") + count(\"fulltexts\");\n }\n\n /** Close the database connection. Call on process exit; operations afterward throw. */\n close(): void {\n this.db.close();\n }\n}\n\nexport function createCache(options: CacheOptions): Cache {\n switch (options.backend) {\n case \"memory\":\n return new LRUBackend(options.max ?? 200);\n case \"sqlite\":\n return new SQLiteBackend(options.path ?? \"./pubmed-cache.db\");\n default: {\n const exhaustive: never = options.backend;\n throw new Error(`Unknown cache backend: ${String(exhaustive)}`);\n }\n }\n}\n"],"mappings":"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","names":["require"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/cache.ts"],"sourcesContent":["/**\n * Cache interface + backends.\n *\n * The interface is what the rest of the library depends on; backends are\n * swappable. Two ship: the in-memory `LRUBackend` (dies with the process) and\n * the persistent `SQLiteBackend` (survives restarts).\n *\n * better-sqlite3 is a native module, so it's loaded lazily — only when the\n * sqlite backend is actually constructed. That keeps `LRUBackend`-only\n * consumers from paying for (or being broken by) a native binding they never\n * use, and lets `createCache` fall back to LRU if the binding fails to load.\n */\n\nimport { createRequire } from \"node:module\";\nimport { LRUCache } from \"lru-cache\";\nimport type SqliteDatabase from \"better-sqlite3\";\nimport type { FulltextData, Article } from \"./types.js\";\n\n/** Full texts expire after 30 days — the paper is stable but resolved OA URLs rot. */\nexport const FULLTEXT_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\n/**\n * Summaries expire after 7 days. Abstracts don't change, but a summary's\n * metadata does: NLM adds publication types and MeSH when it indexes a record,\n * and retractions, expressions of concern and errata can arrive at any time.\n * A cached summary must not hide a retraction for long.\n */\nexport const SUMMARY_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\n/**\n * Where the library keeps what it fetched. What it stores changes between versions (see\n * {@link CACHE_FORMAT_VERSION}): a persistent implementation should keep each version's entries\n * apart, in its keys or tables, as {@link SQLiteBackend} does.\n */\nexport interface Cache {\n /** Returns a cached summary, or null on a miss or if older than {@link SUMMARY_TTL_MS}. */\n getSummary(pmid: string): Article | null;\n setSummary(pmid: string, article: Article): void;\n getAbstract(pmid: string): string | null;\n setAbstract(pmid: string, abstract: string): void;\n /** Returns a cached full text, or null on a miss or if older than {@link FULLTEXT_TTL_MS}. */\n getFulltext(pmid: string): FulltextData | null;\n setFulltext(pmid: string, data: FulltextData): void;\n}\n\nexport type CacheBackend = \"memory\" | \"sqlite\";\n\nexport interface CacheOptions {\n backend: CacheBackend;\n /** Max entries per store for the memory backend. Default 200. */\n max?: number;\n /** Filesystem path for the sqlite backend. Use \":memory:\" for an ephemeral DB. Default \"./pubmed-cache.db\". */\n path?: string;\n}\n\n/**\n * In-memory LRU cache. Dies with the process — right for short-lived agent\n * sessions. Abstracts never expire; summaries expire after {@link SUMMARY_TTL_MS}\n * (a server can run for days) and full texts after {@link FULLTEXT_TTL_MS}.\n */\nexport class LRUBackend implements Cache {\n private readonly summaries: LRUCache<string, Article>;\n private readonly abstracts: LRUCache<string, string>;\n private readonly fulltexts: LRUCache<string, FulltextData>;\n\n constructor(max = 200) {\n this.summaries = new LRUCache({ max, ttl: SUMMARY_TTL_MS });\n this.abstracts = new LRUCache({ max });\n // The LRU's own TTL evicts stale entries; getFulltext double-checks fetchedAt\n // so a caller-supplied stale timestamp is also honored.\n this.fulltexts = new LRUCache({ max, ttl: FULLTEXT_TTL_MS });\n }\n\n getSummary(pmid: string): Article | null {\n return this.summaries.get(pmid) ?? null;\n }\n\n setSummary(pmid: string, article: Article): void {\n this.summaries.set(pmid, article);\n }\n\n getAbstract(pmid: string): string | null {\n return this.abstracts.get(pmid) ?? null;\n }\n\n setAbstract(pmid: string, abstract: string): void {\n this.abstracts.set(pmid, abstract);\n }\n\n getFulltext(pmid: string): FulltextData | null {\n const data = this.fulltexts.get(pmid);\n if (!data) return null;\n if (Date.now() - data.fetchedAt > FULLTEXT_TTL_MS) {\n this.fulltexts.delete(pmid);\n return null;\n }\n return data;\n }\n\n setFulltext(pmid: string, data: FulltextData): void {\n this.fulltexts.set(pmid, data);\n }\n}\n\n/** A cache that stores nothing — useful for tests or explicit opt-out. */\nexport class NullCache implements Cache {\n getSummary(): Article | null {\n return null;\n }\n setSummary(): void {}\n getAbstract(): string | null {\n return null;\n }\n setAbstract(): void {}\n getFulltext(): FulltextData | null {\n return null;\n }\n setFulltext(): void {}\n}\n\n// ─── SQLite backend ─────────────────────────────────────────────\n\n/**\n * The version of what the cache stores. The SQLite backend keeps each version's entries in tables\n * of their own (`abstracts_v1`) and empties older versions' tables when it opens a file: abstracts\n * never expire, so text parsed by an older version would otherwise be served for good. An older\n * version still running on the same file goes on writing to its own tables, which this one never reads.\n *\n * 1 (0.6.1): PubMed abstracts lost the text of inline markup and kept character references;\n * Europe PMC titles and abstracts carried HTML; Europe PMC full texts could end in an empty\n * References heading. Up to 0.6.0 the tables had no version in their names.\n */\nexport const CACHE_FORMAT_VERSION = 1;\n\nconst SUMMARIES = `summaries_v${CACHE_FORMAT_VERSION}`;\nconst ABSTRACTS = `abstracts_v${CACHE_FORMAT_VERSION}`;\nconst FULLTEXTS = `fulltexts_v${CACHE_FORMAT_VERSION}`;\n/** A table of some version's, as named up to 0.6.0 (\"abstracts\") or since (\"abstracts_v1\"). */\nconst VERSIONED_TABLE = /^(?:summaries|abstracts|fulltexts)(?:_v(\\d+))?$/;\n\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS ${SUMMARIES} (\n pmid TEXT PRIMARY KEY,\n article TEXT NOT NULL,\n fetched_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS ${ABSTRACTS} (\n pmid TEXT PRIMARY KEY,\n abstract TEXT NOT NULL,\n fetched_at INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS ${FULLTEXTS} (\n pmid TEXT PRIMARY KEY,\n source TEXT NOT NULL,\n fulltext TEXT NOT NULL,\n format TEXT NOT NULL,\n fetched_at INTEGER NOT NULL\n);\n`;\n\ninterface FulltextRow {\n source: string;\n fulltext: string;\n format: string;\n fetched_at: number;\n}\n\n/**\n * Loads the native better-sqlite3 module on first use (see the module header).\n *\n * Resolves from this module's own location so better-sqlite3 is found even when\n * med-research-mcp is a nested dependency. `__filename` exists in the CJS build;\n * `import.meta.url` exists in the ESM build — esbuild leaves the other one\n * undefined per format, so `typeof` picks whichever is real. (`typeof` on an\n * undeclared identifier is safe — it evaluates to \"undefined\" rather than\n * throwing.)\n */\nfunction loadSqlite(): typeof SqliteDatabase {\n const from = typeof __filename !== \"undefined\" ? __filename : import.meta.url;\n const require = createRequire(from);\n return require(\"better-sqlite3\") as typeof SqliteDatabase;\n}\n\n/**\n * Persistent cache backed by a single SQLite file. Drop-in replacement for\n * {@link LRUBackend} — same interface, same semantics, but survives restarts so\n * abstracts are fetched from NCBI at most once.\n *\n * Abstracts never expire. Summaries expire after {@link SUMMARY_TTL_MS} because\n * their metadata changes (indexing, retractions, corrections); full texts after\n * {@link FULLTEXT_TTL_MS} because resolved OA URLs and proxy sessions rot.\n * Expiry is enforced at read time and swept by {@link vacuum}.\n */\nexport class SQLiteBackend implements Cache {\n private readonly db: SqliteDatabase.Database;\n private readonly stmts: {\n getSummary: SqliteDatabase.Statement<[string]>;\n vacuumSummary: SqliteDatabase.Statement<[number]>;\n setSummary: SqliteDatabase.Statement<[string, string, number]>;\n delSummary: SqliteDatabase.Statement<[string]>;\n getAbstract: SqliteDatabase.Statement<[string]>;\n setAbstract: SqliteDatabase.Statement<[string, string, number]>;\n getFulltext: SqliteDatabase.Statement<[string]>;\n setFulltext: SqliteDatabase.Statement<[string, string, string, string, number]>;\n delFulltext: SqliteDatabase.Statement<[string]>;\n vacuumFulltext: SqliteDatabase.Statement<[number]>;\n };\n\n constructor(path: string) {\n const Database = loadSqlite();\n this.db = new Database(path);\n this.db.pragma(\"journal_mode = WAL\"); // concurrent reads while another process writes\n this.db.pragma(\"synchronous = NORMAL\"); // safe with WAL, faster writes\n this.db.exec(SCHEMA_SQL);\n this.clearOlderVersions();\n\n this.stmts = {\n getSummary: this.db.prepare(`SELECT article, fetched_at FROM ${SUMMARIES} WHERE pmid = ?`),\n vacuumSummary: this.db.prepare(`DELETE FROM ${SUMMARIES} WHERE fetched_at < ?`),\n setSummary: this.db.prepare(`INSERT OR REPLACE INTO ${SUMMARIES} (pmid, article, fetched_at) VALUES (?, ?, ?)`),\n delSummary: this.db.prepare(`DELETE FROM ${SUMMARIES} WHERE pmid = ?`),\n getAbstract: this.db.prepare(`SELECT abstract FROM ${ABSTRACTS} WHERE pmid = ?`),\n setAbstract: this.db.prepare(`INSERT OR REPLACE INTO ${ABSTRACTS} (pmid, abstract, fetched_at) VALUES (?, ?, ?)`),\n getFulltext: this.db.prepare(`SELECT source, fulltext, format, fetched_at FROM ${FULLTEXTS} WHERE pmid = ?`),\n setFulltext: this.db.prepare(\n `INSERT OR REPLACE INTO ${FULLTEXTS} (pmid, source, fulltext, format, fetched_at) VALUES (?, ?, ?, ?, ?)`,\n ),\n delFulltext: this.db.prepare(`DELETE FROM ${FULLTEXTS} WHERE pmid = ?`),\n vacuumFulltext: this.db.prepare(`DELETE FROM ${FULLTEXTS} WHERE fetched_at < ?`),\n };\n\n this.vacuum();\n }\n\n getSummary(pmid: string): Article | null {\n const row = this.stmts.getSummary.get(pmid) as { article: string; fetched_at: number } | undefined;\n if (!row) return null;\n if (Date.now() - row.fetched_at > SUMMARY_TTL_MS) {\n this.stmts.delSummary.run(pmid);\n return null;\n }\n try {\n return JSON.parse(row.article) as Article;\n } catch {\n // Corrupt JSON — drop the row and treat as a miss.\n this.stmts.delSummary.run(pmid);\n return null;\n }\n }\n\n setSummary(pmid: string, article: Article): void {\n this.stmts.setSummary.run(pmid, JSON.stringify(article), Date.now());\n }\n\n getAbstract(pmid: string): string | null {\n const row = this.stmts.getAbstract.get(pmid) as { abstract: string } | undefined;\n return row ? row.abstract : null;\n }\n\n setAbstract(pmid: string, abstract: string): void {\n this.stmts.setAbstract.run(pmid, abstract, Date.now());\n }\n\n getFulltext(pmid: string): FulltextData | null {\n const row = this.stmts.getFulltext.get(pmid) as FulltextRow | undefined;\n if (!row) return null;\n if (Date.now() - row.fetched_at > FULLTEXT_TTL_MS) {\n this.stmts.delFulltext.run(pmid);\n return null;\n }\n return {\n source: row.source,\n fulltext: row.fulltext,\n format: row.format === \"pdf\" ? \"pdf\" : \"markdown\",\n fetchedAt: row.fetched_at,\n };\n }\n\n setFulltext(pmid: string, data: FulltextData): void {\n this.stmts.setFulltext.run(pmid, data.source, data.fulltext, data.format, data.fetchedAt ?? Date.now());\n }\n\n /**\n * Empty the tables of older versions (see {@link CACHE_FORMAT_VERSION}); a newer version's are left\n * alone. They are emptied, not dropped: an older version still running on the file would fail.\n */\n private clearOlderVersions(): void {\n const tables = this.db.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table'\").pluck().all() as string[];\n for (const name of tables) {\n const match = VERSIONED_TABLE.exec(name);\n if (match && Number(match[1] ?? 0) < CACHE_FORMAT_VERSION) this.db.exec(`DELETE FROM ${name}`);\n }\n }\n\n /** Delete every expired summary and full-text row. Runs on construction; call periodically if desired. */\n vacuum(): void {\n this.stmts.vacuumSummary.run(Date.now() - SUMMARY_TTL_MS);\n this.stmts.vacuumFulltext.run(Date.now() - FULLTEXT_TTL_MS);\n }\n\n /** Total rows across this version's three tables — handy for debugging. */\n get size(): number {\n const count = (table: string): number =>\n (this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number }).n;\n return count(SUMMARIES) + count(ABSTRACTS) + count(FULLTEXTS);\n }\n\n /** Close the database connection. Call on process exit; operations afterward throw. */\n close(): void {\n this.db.close();\n }\n}\n\nexport function createCache(options: CacheOptions): Cache {\n switch (options.backend) {\n case \"memory\":\n return new LRUBackend(options.max ?? 200);\n case \"sqlite\":\n return new SQLiteBackend(options.path ?? \"./pubmed-cache.db\");\n default: {\n const exhaustive: never = options.backend;\n throw new Error(`Unknown cache backend: ${String(exhaustive)}`);\n }\n }\n}\n"],"mappings":"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","names":["require"]}
|
package/dist/core/cache.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export declare const FULLTEXT_TTL_MS: number;
|
|
|
20
20
|
* A cached summary must not hide a retraction for long.
|
|
21
21
|
*/
|
|
22
22
|
export declare const SUMMARY_TTL_MS: number;
|
|
23
|
+
/**
|
|
24
|
+
* Where the library keeps what it fetched. What it stores changes between versions (see
|
|
25
|
+
* {@link CACHE_FORMAT_VERSION}): a persistent implementation should keep each version's entries
|
|
26
|
+
* apart, in its keys or tables, as {@link SQLiteBackend} does.
|
|
27
|
+
*/
|
|
23
28
|
export interface Cache {
|
|
24
29
|
/** Returns a cached summary, or null on a miss or if older than {@link SUMMARY_TTL_MS}. */
|
|
25
30
|
getSummary(pmid: string): Article | null;
|
|
@@ -64,6 +69,17 @@ export declare class NullCache implements Cache {
|
|
|
64
69
|
getFulltext(): FulltextData | null;
|
|
65
70
|
setFulltext(): void;
|
|
66
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The version of what the cache stores. The SQLite backend keeps each version's entries in tables
|
|
74
|
+
* of their own (`abstracts_v1`) and empties older versions' tables when it opens a file: abstracts
|
|
75
|
+
* never expire, so text parsed by an older version would otherwise be served for good. An older
|
|
76
|
+
* version still running on the same file goes on writing to its own tables, which this one never reads.
|
|
77
|
+
*
|
|
78
|
+
* 1 (0.6.1): PubMed abstracts lost the text of inline markup and kept character references;
|
|
79
|
+
* Europe PMC titles and abstracts carried HTML; Europe PMC full texts could end in an empty
|
|
80
|
+
* References heading. Up to 0.6.0 the tables had no version in their names.
|
|
81
|
+
*/
|
|
82
|
+
export declare const CACHE_FORMAT_VERSION = 1;
|
|
67
83
|
/**
|
|
68
84
|
* Persistent cache backed by a single SQLite file. Drop-in replacement for
|
|
69
85
|
* {@link LRUBackend} — same interface, same semantics, but survives restarts so
|
|
@@ -84,9 +100,14 @@ export declare class SQLiteBackend implements Cache {
|
|
|
84
100
|
setAbstract(pmid: string, abstract: string): void;
|
|
85
101
|
getFulltext(pmid: string): FulltextData | null;
|
|
86
102
|
setFulltext(pmid: string, data: FulltextData): void;
|
|
103
|
+
/**
|
|
104
|
+
* Empty the tables of older versions (see {@link CACHE_FORMAT_VERSION}); a newer version's are left
|
|
105
|
+
* alone. They are emptied, not dropped: an older version still running on the file would fail.
|
|
106
|
+
*/
|
|
107
|
+
private clearOlderVersions;
|
|
87
108
|
/** Delete every expired summary and full-text row. Runs on construction; call periodically if desired. */
|
|
88
109
|
vacuum(): void;
|
|
89
|
-
/** Total rows across
|
|
110
|
+
/** Total rows across this version's three tables — handy for debugging. */
|
|
90
111
|
get size(): number;
|
|
91
112
|
/** Close the database connection. Call on process exit; operations afterward throw. */
|
|
92
113
|
close(): void;
|
package/dist/core/cache.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/core/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAExD,sFAAsF;AACtF,eAAO,MAAM,eAAe,QAA2B,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAA0B,CAAC;AAEtD,MAAM,WAAW,KAAK;IACpB,2FAA2F;IAC3F,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACzC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,8FAA8F;IAC9F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;IAC/C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;CACrD;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,YAAY,CAAC;IACtB,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+GAA+G;IAC/G,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,qBAAa,UAAW,YAAW,KAAK;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiC;IAE3D,YAAY,GAAG,SAAM,EAMpB;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAEvC;IAED,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAE/C;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEvC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAEhD;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAQ7C;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAElD;CACF;AAED,0EAA0E;AAC1E,qBAAa,SAAU,YAAW,KAAK;IACrC,UAAU,IAAI,OAAO,GAAG,IAAI,CAE3B;IACD,UAAU,IAAI,IAAI,CAAG;IACrB,WAAW,IAAI,MAAM,GAAG,IAAI,CAE3B;IACD,WAAW,IAAI,IAAI,CAAG;IACtB,WAAW,IAAI,YAAY,GAAG,IAAI,CAEjC;IACD,WAAW,IAAI,IAAI,CAAG;CACvB;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/core/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/core/cache.js
CHANGED
|
@@ -54,18 +54,23 @@ var NullCache = class {
|
|
|
54
54
|
setFulltext() {
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
|
+
var CACHE_FORMAT_VERSION = 1;
|
|
58
|
+
var SUMMARIES = `summaries_v${CACHE_FORMAT_VERSION}`;
|
|
59
|
+
var ABSTRACTS = `abstracts_v${CACHE_FORMAT_VERSION}`;
|
|
60
|
+
var FULLTEXTS = `fulltexts_v${CACHE_FORMAT_VERSION}`;
|
|
61
|
+
var VERSIONED_TABLE = /^(?:summaries|abstracts|fulltexts)(?:_v(\d+))?$/;
|
|
57
62
|
var SCHEMA_SQL = `
|
|
58
|
-
CREATE TABLE IF NOT EXISTS
|
|
63
|
+
CREATE TABLE IF NOT EXISTS ${SUMMARIES} (
|
|
59
64
|
pmid TEXT PRIMARY KEY,
|
|
60
65
|
article TEXT NOT NULL,
|
|
61
66
|
fetched_at INTEGER NOT NULL
|
|
62
67
|
);
|
|
63
|
-
CREATE TABLE IF NOT EXISTS
|
|
68
|
+
CREATE TABLE IF NOT EXISTS ${ABSTRACTS} (
|
|
64
69
|
pmid TEXT PRIMARY KEY,
|
|
65
70
|
abstract TEXT NOT NULL,
|
|
66
71
|
fetched_at INTEGER NOT NULL
|
|
67
72
|
);
|
|
68
|
-
CREATE TABLE IF NOT EXISTS
|
|
73
|
+
CREATE TABLE IF NOT EXISTS ${FULLTEXTS} (
|
|
69
74
|
pmid TEXT PRIMARY KEY,
|
|
70
75
|
source TEXT NOT NULL,
|
|
71
76
|
fulltext TEXT NOT NULL,
|
|
@@ -87,19 +92,20 @@ var SQLiteBackend = class {
|
|
|
87
92
|
this.db.pragma("journal_mode = WAL");
|
|
88
93
|
this.db.pragma("synchronous = NORMAL");
|
|
89
94
|
this.db.exec(SCHEMA_SQL);
|
|
95
|
+
this.clearOlderVersions();
|
|
90
96
|
this.stmts = {
|
|
91
|
-
getSummary: this.db.prepare(
|
|
92
|
-
vacuumSummary: this.db.prepare(
|
|
93
|
-
setSummary: this.db.prepare(
|
|
94
|
-
delSummary: this.db.prepare(
|
|
95
|
-
getAbstract: this.db.prepare(
|
|
96
|
-
setAbstract: this.db.prepare(
|
|
97
|
-
getFulltext: this.db.prepare(
|
|
97
|
+
getSummary: this.db.prepare(`SELECT article, fetched_at FROM ${SUMMARIES} WHERE pmid = ?`),
|
|
98
|
+
vacuumSummary: this.db.prepare(`DELETE FROM ${SUMMARIES} WHERE fetched_at < ?`),
|
|
99
|
+
setSummary: this.db.prepare(`INSERT OR REPLACE INTO ${SUMMARIES} (pmid, article, fetched_at) VALUES (?, ?, ?)`),
|
|
100
|
+
delSummary: this.db.prepare(`DELETE FROM ${SUMMARIES} WHERE pmid = ?`),
|
|
101
|
+
getAbstract: this.db.prepare(`SELECT abstract FROM ${ABSTRACTS} WHERE pmid = ?`),
|
|
102
|
+
setAbstract: this.db.prepare(`INSERT OR REPLACE INTO ${ABSTRACTS} (pmid, abstract, fetched_at) VALUES (?, ?, ?)`),
|
|
103
|
+
getFulltext: this.db.prepare(`SELECT source, fulltext, format, fetched_at FROM ${FULLTEXTS} WHERE pmid = ?`),
|
|
98
104
|
setFulltext: this.db.prepare(
|
|
99
|
-
|
|
105
|
+
`INSERT OR REPLACE INTO ${FULLTEXTS} (pmid, source, fulltext, format, fetched_at) VALUES (?, ?, ?, ?, ?)`
|
|
100
106
|
),
|
|
101
|
-
delFulltext: this.db.prepare(
|
|
102
|
-
vacuumFulltext: this.db.prepare(
|
|
107
|
+
delFulltext: this.db.prepare(`DELETE FROM ${FULLTEXTS} WHERE pmid = ?`),
|
|
108
|
+
vacuumFulltext: this.db.prepare(`DELETE FROM ${FULLTEXTS} WHERE fetched_at < ?`)
|
|
103
109
|
};
|
|
104
110
|
this.vacuum();
|
|
105
111
|
}
|
|
@@ -144,15 +150,26 @@ var SQLiteBackend = class {
|
|
|
144
150
|
setFulltext(pmid, data) {
|
|
145
151
|
this.stmts.setFulltext.run(pmid, data.source, data.fulltext, data.format, data.fetchedAt ?? Date.now());
|
|
146
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Empty the tables of older versions (see {@link CACHE_FORMAT_VERSION}); a newer version's are left
|
|
155
|
+
* alone. They are emptied, not dropped: an older version still running on the file would fail.
|
|
156
|
+
*/
|
|
157
|
+
clearOlderVersions() {
|
|
158
|
+
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").pluck().all();
|
|
159
|
+
for (const name of tables) {
|
|
160
|
+
const match = VERSIONED_TABLE.exec(name);
|
|
161
|
+
if (match && Number(match[1] ?? 0) < CACHE_FORMAT_VERSION) this.db.exec(`DELETE FROM ${name}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
147
164
|
/** Delete every expired summary and full-text row. Runs on construction; call periodically if desired. */
|
|
148
165
|
vacuum() {
|
|
149
166
|
this.stmts.vacuumSummary.run(Date.now() - SUMMARY_TTL_MS);
|
|
150
167
|
this.stmts.vacuumFulltext.run(Date.now() - FULLTEXT_TTL_MS);
|
|
151
168
|
}
|
|
152
|
-
/** Total rows across
|
|
169
|
+
/** Total rows across this version's three tables — handy for debugging. */
|
|
153
170
|
get size() {
|
|
154
171
|
const count = (table) => this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
|
|
155
|
-
return count(
|
|
172
|
+
return count(SUMMARIES) + count(ABSTRACTS) + count(FULLTEXTS);
|
|
156
173
|
}
|
|
157
174
|
/** Close the database connection. Call on process exit; operations afterward throw. */
|
|
158
175
|
close() {
|
|
@@ -172,6 +189,7 @@ function createCache(options) {
|
|
|
172
189
|
}
|
|
173
190
|
}
|
|
174
191
|
export {
|
|
192
|
+
CACHE_FORMAT_VERSION,
|
|
175
193
|
FULLTEXT_TTL_MS,
|
|
176
194
|
LRUBackend,
|
|
177
195
|
NullCache,
|