@pnpm/store.index 1000.0.0-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/LICENSE +22 -0
- package/README.md +29 -0
- package/lib/index.d.ts +68 -0
- package/lib/index.js +254 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors
|
|
4
|
+
Copyright (c) 2016-2026 Zoltan Kochan and other contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @pnpm/store.index
|
|
2
|
+
|
|
3
|
+
> SQLite-backed index for the pnpm content-addressable store
|
|
4
|
+
|
|
5
|
+
## Why SQLite instead of individual index files?
|
|
6
|
+
|
|
7
|
+
Previously, pnpm stored package metadata as individual JSON files under
|
|
8
|
+
`$STORE/index/`. Each resolved package had its own file, keyed by its integrity
|
|
9
|
+
hash. This worked but had several downsides at scale:
|
|
10
|
+
|
|
11
|
+
- **Filesystem overhead.** Every lookup required `open` / `read` / `close`
|
|
12
|
+
syscalls, and every write needed an atomic `write` + `rename` per entry.
|
|
13
|
+
On repositories with thousands of dependencies the accumulated I/O was
|
|
14
|
+
significant.
|
|
15
|
+
- **Space inefficiency.** Small metadata entries still consumed a minimum
|
|
16
|
+
filesystem block each (typically 4 KiB), wasting space.
|
|
17
|
+
Storing all entries in a single SQLite database (`$STORE/index.db`) addresses
|
|
18
|
+
these issues:
|
|
19
|
+
|
|
20
|
+
- **Fewer syscalls.** Reads and writes go through SQLite's page cache and
|
|
21
|
+
memory-mapped I/O instead of individual file operations.
|
|
22
|
+
- **Space efficiency.** Small entries share database pages instead of each
|
|
23
|
+
occupying a full filesystem block.
|
|
24
|
+
- **Batch writes.** Multiple entries can be inserted in a single transaction,
|
|
25
|
+
reducing disk flushes.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack data for storage using msgpackr.
|
|
3
|
+
* Use this when data will be packed in one thread and stored by another,
|
|
4
|
+
* to ensure the same Packr instance is used for pack and unpack within each thread.
|
|
5
|
+
*/
|
|
6
|
+
export declare function packForStorage(data: unknown): Uint8Array;
|
|
7
|
+
/**
|
|
8
|
+
* Create a store index key from an integrity hash and package id.
|
|
9
|
+
* The key is `${integrity}\t${pkgId}` — tab-separated.
|
|
10
|
+
* Integrity strings never contain tabs, so this is unambiguous.
|
|
11
|
+
*/
|
|
12
|
+
export declare function storeIndexKey(integrity: string, pkgId: string): string;
|
|
13
|
+
export declare function gitHostedStoreIndexKey(pkgId: string, opts: {
|
|
14
|
+
built: boolean;
|
|
15
|
+
}): string;
|
|
16
|
+
/**
|
|
17
|
+
* Close all open StoreIndex instances.
|
|
18
|
+
* Useful in tests that need to remove the store directory.
|
|
19
|
+
*/
|
|
20
|
+
export declare function closeAllStoreIndexes(): void;
|
|
21
|
+
export declare class StoreIndex {
|
|
22
|
+
private db;
|
|
23
|
+
private closed;
|
|
24
|
+
private pendingWrites;
|
|
25
|
+
private flushScheduled;
|
|
26
|
+
private stmtGet;
|
|
27
|
+
private stmtSet;
|
|
28
|
+
private stmtDel;
|
|
29
|
+
private stmtHas;
|
|
30
|
+
private stmtAll;
|
|
31
|
+
private readonly exitHandler;
|
|
32
|
+
constructor(storeDir: string);
|
|
33
|
+
get(key: string): unknown | undefined;
|
|
34
|
+
set(key: string, data: unknown): void;
|
|
35
|
+
delete(key: string): boolean;
|
|
36
|
+
has(key: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Iterate over all index entries.
|
|
39
|
+
* Yields [key, data] pairs where key is `integrity\tpkgId`.
|
|
40
|
+
*/
|
|
41
|
+
entries(): IterableIterator<[string, unknown]>;
|
|
42
|
+
/**
|
|
43
|
+
* Queue pre-packed writes to be flushed on the next tick.
|
|
44
|
+
* Used by the fetch phase for throughput.
|
|
45
|
+
*/
|
|
46
|
+
queueWrites(writes: Array<{
|
|
47
|
+
key: string;
|
|
48
|
+
buffer: Uint8Array;
|
|
49
|
+
}>): void;
|
|
50
|
+
/**
|
|
51
|
+
* Flush all pending queued writes immediately.
|
|
52
|
+
*/
|
|
53
|
+
flush(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Write multiple pre-packed entries in a single transaction.
|
|
56
|
+
* The buffers must already be msgpack-encoded.
|
|
57
|
+
*/
|
|
58
|
+
setRawMany(entries: Array<{
|
|
59
|
+
key: string;
|
|
60
|
+
buffer: Uint8Array;
|
|
61
|
+
}>): void;
|
|
62
|
+
/**
|
|
63
|
+
* Delete multiple index entries in a single transaction,
|
|
64
|
+
* then VACUUM to reclaim disk space.
|
|
65
|
+
*/
|
|
66
|
+
deleteMany(keys: string[]): void;
|
|
67
|
+
close(): void;
|
|
68
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { Packr } from 'msgpackr';
|
|
4
|
+
// Use createRequire to load node:sqlite because it is a prefix-only builtin
|
|
5
|
+
// that Jest's ESM module resolver cannot handle.
|
|
6
|
+
const req = createRequire(import.meta.url);
|
|
7
|
+
const { DatabaseSync } = req('node:sqlite');
|
|
8
|
+
const packr = new Packr({
|
|
9
|
+
useRecords: true,
|
|
10
|
+
moreTypes: true,
|
|
11
|
+
});
|
|
12
|
+
const SQLITE_BUSY = 5;
|
|
13
|
+
const RETRY_DELAY_MS = 50;
|
|
14
|
+
const MAX_RETRIES = 100; // ~5 seconds total
|
|
15
|
+
function sqliteRetry(fn) {
|
|
16
|
+
for (let attempt = 0;; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
return fn();
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (isSqliteBusy(err) && attempt < MAX_RETRIES) {
|
|
22
|
+
sleepSync(RETRY_DELAY_MS);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isSqliteBusy(err) {
|
|
30
|
+
// errcode may be an extended error code (e.g. SQLITE_BUSY_RECOVERY = 261),
|
|
31
|
+
// so mask off the upper bits to get the primary error code.
|
|
32
|
+
return (err?.errcode & 0xFF) === SQLITE_BUSY;
|
|
33
|
+
}
|
|
34
|
+
const sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
35
|
+
function sleepSync(ms) {
|
|
36
|
+
Atomics.wait(sleepBuffer, 0, 0, ms);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pack data for storage using msgpackr.
|
|
40
|
+
* Use this when data will be packed in one thread and stored by another,
|
|
41
|
+
* to ensure the same Packr instance is used for pack and unpack within each thread.
|
|
42
|
+
*/
|
|
43
|
+
export function packForStorage(data) {
|
|
44
|
+
return packr.pack(data);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a store index key from an integrity hash and package id.
|
|
48
|
+
* The key is `${integrity}\t${pkgId}` — tab-separated.
|
|
49
|
+
* Integrity strings never contain tabs, so this is unambiguous.
|
|
50
|
+
*/
|
|
51
|
+
export function storeIndexKey(integrity, pkgId) {
|
|
52
|
+
return `${integrity}\t${pkgId}`;
|
|
53
|
+
}
|
|
54
|
+
export function gitHostedStoreIndexKey(pkgId, opts) {
|
|
55
|
+
return storeIndexKey(pkgId, opts.built ? 'built' : 'not-built');
|
|
56
|
+
}
|
|
57
|
+
const openInstances = new Set();
|
|
58
|
+
/**
|
|
59
|
+
* Close all open StoreIndex instances.
|
|
60
|
+
* Useful in tests that need to remove the store directory.
|
|
61
|
+
*/
|
|
62
|
+
export function closeAllStoreIndexes() {
|
|
63
|
+
for (const si of openInstances) {
|
|
64
|
+
si.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export class StoreIndex {
|
|
68
|
+
db;
|
|
69
|
+
closed = false;
|
|
70
|
+
pendingWrites = [];
|
|
71
|
+
flushScheduled = false;
|
|
72
|
+
stmtGet;
|
|
73
|
+
stmtSet;
|
|
74
|
+
stmtDel;
|
|
75
|
+
stmtHas;
|
|
76
|
+
stmtAll;
|
|
77
|
+
exitHandler;
|
|
78
|
+
constructor(storeDir) {
|
|
79
|
+
const dbPath = `${storeDir}/index.db`;
|
|
80
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
81
|
+
this.db = new DatabaseSync(dbPath);
|
|
82
|
+
// Set busy_timeout FIRST so SQLite's internal busy handler is active
|
|
83
|
+
// during all subsequent operations. On Windows, file locking is mandatory
|
|
84
|
+
// and concurrent processes (e.g. parallel dlx calls) will contend.
|
|
85
|
+
this.db.exec('PRAGMA busy_timeout=5000');
|
|
86
|
+
sqliteRetry(() => {
|
|
87
|
+
this.db.exec('PRAGMA journal_mode=WAL');
|
|
88
|
+
this.db.exec('PRAGMA synchronous=NORMAL');
|
|
89
|
+
// Increase memory map size to 512MB
|
|
90
|
+
this.db.exec('PRAGMA mmap_size=536870912');
|
|
91
|
+
// Increase page cache size to ~32MB
|
|
92
|
+
this.db.exec('PRAGMA cache_size=-32000');
|
|
93
|
+
this.db.exec('PRAGMA temp_store=MEMORY');
|
|
94
|
+
// Increase wal autocheckpoint interval to reduce I/O during heavy writes
|
|
95
|
+
this.db.exec('PRAGMA wal_autocheckpoint=10000');
|
|
96
|
+
this.db.exec(`
|
|
97
|
+
CREATE TABLE IF NOT EXISTS package_index (
|
|
98
|
+
key TEXT PRIMARY KEY,
|
|
99
|
+
data BLOB NOT NULL
|
|
100
|
+
) WITHOUT ROWID
|
|
101
|
+
`);
|
|
102
|
+
});
|
|
103
|
+
this.stmtGet = this.db.prepare('SELECT data FROM package_index WHERE key = ?');
|
|
104
|
+
this.stmtSet = this.db.prepare('INSERT OR REPLACE INTO package_index (key, data) VALUES (?, ?)');
|
|
105
|
+
this.stmtDel = this.db.prepare('DELETE FROM package_index WHERE key = ?');
|
|
106
|
+
this.stmtHas = this.db.prepare('SELECT 1 FROM package_index WHERE key = ?');
|
|
107
|
+
this.stmtAll = this.db.prepare('SELECT key, data FROM package_index');
|
|
108
|
+
this.exitHandler = () => this.close();
|
|
109
|
+
process.on('exit', this.exitHandler);
|
|
110
|
+
openInstances.add(this);
|
|
111
|
+
}
|
|
112
|
+
get(key) {
|
|
113
|
+
const row = sqliteRetry(() => this.stmtGet.get(key));
|
|
114
|
+
if (row) {
|
|
115
|
+
return packr.unpack(row.data);
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
set(key, data) {
|
|
120
|
+
const buffer = packr.pack(data);
|
|
121
|
+
sqliteRetry(() => {
|
|
122
|
+
this.stmtSet.run(key, buffer);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
delete(key) {
|
|
126
|
+
let result;
|
|
127
|
+
sqliteRetry(() => {
|
|
128
|
+
result = this.stmtDel.run(key);
|
|
129
|
+
});
|
|
130
|
+
return result.changes > 0;
|
|
131
|
+
}
|
|
132
|
+
has(key) {
|
|
133
|
+
return sqliteRetry(() => this.stmtHas.get(key)) != null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Iterate over all index entries.
|
|
137
|
+
* Yields [key, data] pairs where key is `integrity\tpkgId`.
|
|
138
|
+
*/
|
|
139
|
+
*entries() {
|
|
140
|
+
for (const row of this.stmtAll.iterate()) {
|
|
141
|
+
yield [row.key, packr.unpack(row.data)];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Queue pre-packed writes to be flushed on the next tick.
|
|
146
|
+
* Used by the fetch phase for throughput.
|
|
147
|
+
*/
|
|
148
|
+
queueWrites(writes) {
|
|
149
|
+
for (const w of writes) {
|
|
150
|
+
this.pendingWrites.push(w);
|
|
151
|
+
}
|
|
152
|
+
if (!this.flushScheduled) {
|
|
153
|
+
this.flushScheduled = true;
|
|
154
|
+
process.nextTick(() => this.flush());
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Flush all pending queued writes immediately.
|
|
159
|
+
*/
|
|
160
|
+
flush() {
|
|
161
|
+
this.flushScheduled = false;
|
|
162
|
+
if (this.pendingWrites.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
this.setRawMany(this.pendingWrites);
|
|
165
|
+
this.pendingWrites = [];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Write multiple pre-packed entries in a single transaction.
|
|
169
|
+
* The buffers must already be msgpack-encoded.
|
|
170
|
+
*/
|
|
171
|
+
setRawMany(entries) {
|
|
172
|
+
if (this.closed || entries.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
if (entries.length === 1) {
|
|
175
|
+
sqliteRetry(() => {
|
|
176
|
+
this.stmtSet.run(entries[0].key, entries[0].buffer);
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
sqliteRetry(() => {
|
|
181
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
182
|
+
let committed = false;
|
|
183
|
+
try {
|
|
184
|
+
for (const { key, buffer } of entries) {
|
|
185
|
+
this.stmtSet.run(key, buffer);
|
|
186
|
+
}
|
|
187
|
+
this.db.exec('COMMIT');
|
|
188
|
+
committed = true;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (!committed) {
|
|
192
|
+
try {
|
|
193
|
+
this.db.exec('ROLLBACK');
|
|
194
|
+
}
|
|
195
|
+
catch { }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Delete multiple index entries in a single transaction,
|
|
202
|
+
* then VACUUM to reclaim disk space.
|
|
203
|
+
*/
|
|
204
|
+
deleteMany(keys) {
|
|
205
|
+
if (keys.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
if (keys.length === 1) {
|
|
208
|
+
this.delete(keys[0]);
|
|
209
|
+
this.db.exec('VACUUM');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
sqliteRetry(() => {
|
|
213
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
214
|
+
let committed = false;
|
|
215
|
+
try {
|
|
216
|
+
for (const key of keys) {
|
|
217
|
+
this.stmtDel.run(key);
|
|
218
|
+
}
|
|
219
|
+
this.db.exec('COMMIT');
|
|
220
|
+
committed = true;
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
if (!committed) {
|
|
224
|
+
try {
|
|
225
|
+
this.db.exec('ROLLBACK');
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
this.db.exec('VACUUM');
|
|
232
|
+
}
|
|
233
|
+
close() {
|
|
234
|
+
if (this.closed)
|
|
235
|
+
return;
|
|
236
|
+
this.flush();
|
|
237
|
+
this.closed = true;
|
|
238
|
+
openInstances.delete(this);
|
|
239
|
+
process.removeListener('exit', this.exitHandler);
|
|
240
|
+
try {
|
|
241
|
+
this.db.exec('PRAGMA optimize');
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// PRAGMA optimize is a performance hint; safe to ignore if the DB is locked.
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
this.db.close();
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// The DB may be locked by another connection; the OS will reclaim it on process exit.
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pnpm/store.index",
|
|
3
|
+
"version": "1000.0.0-0",
|
|
4
|
+
"description": "SQLite-backed index for the pnpm content-addressable store",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pnpm",
|
|
7
|
+
"pnpm11",
|
|
8
|
+
"store"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"funding": "https://opencollective.com/pnpm",
|
|
12
|
+
"repository": "https://github.com/pnpm/pnpm/tree/main/store/index",
|
|
13
|
+
"homepage": "https://github.com/pnpm/pnpm/tree/main/store/index#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pnpm/pnpm/issues"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "lib/index.js",
|
|
19
|
+
"types": "lib/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./lib/index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"!*.map"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"msgpackr": "^1.11.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.19.11",
|
|
32
|
+
"tempy": "3.0.0",
|
|
33
|
+
"@pnpm/store.index": "1000.0.0-0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.13"
|
|
37
|
+
},
|
|
38
|
+
"jest": {
|
|
39
|
+
"preset": "@pnpm/jest-config"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
43
|
+
"_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest",
|
|
44
|
+
"test": "pnpm run compile && pnpm run _test",
|
|
45
|
+
"compile": "tsgo --build && pnpm run lint --fix"
|
|
46
|
+
}
|
|
47
|
+
}
|