@pnpm/store.index 1100.1.0 → 1100.2.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/lib/index.d.ts +44 -8
- package/lib/index.js +111 -16
- package/package.json +10 -6
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DatabaseSync as DatabaseSyncType, StatementSync } from 'node:sqlite';
|
|
1
2
|
/**
|
|
2
3
|
* Pack data for storage using msgpackr.
|
|
3
4
|
* Use this when data will be packed in one thread and stored by another,
|
|
@@ -41,18 +42,22 @@ export declare function pickStoreIndexKey(resolution: {
|
|
|
41
42
|
*/
|
|
42
43
|
export declare function closeAllStoreIndexes(): void;
|
|
43
44
|
export declare class StoreIndex {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
protected db: DatabaseSyncType;
|
|
46
|
+
protected closed: boolean;
|
|
46
47
|
private pendingWrites;
|
|
47
48
|
private flushScheduled;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
protected stmtGet: StatementSync;
|
|
50
|
+
protected stmtSet: StatementSync;
|
|
51
|
+
protected stmtDel: StatementSync;
|
|
52
|
+
protected stmtHas: StatementSync;
|
|
53
|
+
protected stmtAll: StatementSync;
|
|
54
|
+
protected stmtKeys: StatementSync;
|
|
54
55
|
private readonly exitHandler;
|
|
55
56
|
constructor(storeDir: string);
|
|
57
|
+
/** Open the SQLite connection. Overridden by {@link ReadOnlyStoreIndex}. */
|
|
58
|
+
protected openDatabase(storeDir: string): void;
|
|
59
|
+
/** Prepare the prepared statements. Overridden by {@link ReadOnlyStoreIndex} to skip the write statements. */
|
|
60
|
+
protected prepareStatements(): void;
|
|
56
61
|
get(key: string): unknown | undefined;
|
|
57
62
|
/**
|
|
58
63
|
* Get the raw msgpack-encoded buffer for a key without decoding.
|
|
@@ -98,4 +103,35 @@ export declare class StoreIndex {
|
|
|
98
103
|
deleteMany(keys: string[]): void;
|
|
99
104
|
checkpoint(): void;
|
|
100
105
|
close(): void;
|
|
106
|
+
/** Run `PRAGMA optimize` before closing. Overridden by {@link ReadOnlyStoreIndex} to skip it (the DB is immutable). */
|
|
107
|
+
protected optimizeBeforeClose(): void;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A {@link StoreIndex} opened read-only for installs against a store on a
|
|
111
|
+
* read-only filesystem (`frozenStore`). The index is a WAL-mode database, and a
|
|
112
|
+
* normal WAL read creates an `index.db-shm` sidecar in the store directory —
|
|
113
|
+
* which fails on a read-only directory and surfaces as "attempt to write a
|
|
114
|
+
* readonly database" on the first query. Opening via the SQLite `immutable=1`
|
|
115
|
+
* URI tells SQLite the file cannot change, so it bypasses the WAL/shm machinery
|
|
116
|
+
* and reads the file directly, creating no sidecars.
|
|
117
|
+
*
|
|
118
|
+
* The store is assumed complete; every write is a programming error and throws.
|
|
119
|
+
*/
|
|
120
|
+
export declare class ReadOnlyStoreIndex extends StoreIndex {
|
|
121
|
+
protected openDatabase(storeDir: string): void;
|
|
122
|
+
protected prepareStatements(): void;
|
|
123
|
+
protected optimizeBeforeClose(): void;
|
|
124
|
+
set(_key: string, _data: unknown): void;
|
|
125
|
+
delete(_key: string): boolean;
|
|
126
|
+
queueWrites(_writes: Array<{
|
|
127
|
+
key: string;
|
|
128
|
+
buffer: Uint8Array;
|
|
129
|
+
}>): void;
|
|
130
|
+
setRawMany(_entries: Array<{
|
|
131
|
+
key: string;
|
|
132
|
+
buffer: Uint8Array;
|
|
133
|
+
}>): void;
|
|
134
|
+
deleteMany(_keys: string[]): void;
|
|
135
|
+
checkpoint(): void;
|
|
136
|
+
private throwReadOnly;
|
|
101
137
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { PnpmError } from '@pnpm/error';
|
|
3
5
|
import { Packr } from 'msgpackr';
|
|
6
|
+
const FROZEN_STORE_WRITE_MESSAGE = 'Cannot write to the package store because frozenStore is enabled (the store is opened read-only). This indicates the store is missing content the install needs.';
|
|
4
7
|
// Use createRequire to load node:sqlite because it is a prefix-only builtin
|
|
5
8
|
// that Jest's ESM module resolver cannot handle.
|
|
6
9
|
const req = createRequire(import.meta.url);
|
|
@@ -99,9 +102,23 @@ export class StoreIndex {
|
|
|
99
102
|
stmtKeys;
|
|
100
103
|
exitHandler;
|
|
101
104
|
constructor(storeDir) {
|
|
102
|
-
|
|
105
|
+
this.openDatabase(storeDir);
|
|
106
|
+
this.prepareStatements();
|
|
107
|
+
this.exitHandler = () => this.close();
|
|
108
|
+
// Multiple StoreIndex instances may be created (e.g. in tests), each adding
|
|
109
|
+
// an exit listener. Raise the limit to avoid MaxListenersExceededWarning.
|
|
110
|
+
// Skip when maxListeners is 0 (unlimited).
|
|
111
|
+
const currentMax = process.getMaxListeners();
|
|
112
|
+
if (currentMax !== 0 && currentMax < openInstances.size + 11) {
|
|
113
|
+
process.setMaxListeners(Math.max(currentMax + 10, openInstances.size + 11));
|
|
114
|
+
}
|
|
115
|
+
process.on('exit', this.exitHandler);
|
|
116
|
+
openInstances.add(this);
|
|
117
|
+
}
|
|
118
|
+
/** Open the SQLite connection. Overridden by {@link ReadOnlyStoreIndex}. */
|
|
119
|
+
openDatabase(storeDir) {
|
|
103
120
|
fs.mkdirSync(storeDir, { recursive: true });
|
|
104
|
-
this.db = new DatabaseSync(
|
|
121
|
+
this.db = new DatabaseSync(`${storeDir}/index.db`);
|
|
105
122
|
// Set busy_timeout FIRST so SQLite's internal busy handler is active
|
|
106
123
|
// during all subsequent operations. On Windows, file locking is mandatory
|
|
107
124
|
// and concurrent processes (e.g. parallel dlx calls) will contend.
|
|
@@ -123,22 +140,15 @@ export class StoreIndex {
|
|
|
123
140
|
) WITHOUT ROWID
|
|
124
141
|
`);
|
|
125
142
|
});
|
|
143
|
+
}
|
|
144
|
+
/** Prepare the prepared statements. Overridden by {@link ReadOnlyStoreIndex} to skip the write statements. */
|
|
145
|
+
prepareStatements() {
|
|
126
146
|
this.stmtGet = this.db.prepare('SELECT data FROM package_index WHERE key = ?');
|
|
127
147
|
this.stmtSet = this.db.prepare('INSERT OR REPLACE INTO package_index (key, data) VALUES (?, ?)');
|
|
128
148
|
this.stmtDel = this.db.prepare('DELETE FROM package_index WHERE key = ?');
|
|
129
149
|
this.stmtHas = this.db.prepare('SELECT 1 FROM package_index WHERE key = ?');
|
|
130
150
|
this.stmtAll = this.db.prepare('SELECT key, data FROM package_index');
|
|
131
151
|
this.stmtKeys = this.db.prepare('SELECT key FROM package_index');
|
|
132
|
-
this.exitHandler = () => this.close();
|
|
133
|
-
// Multiple StoreIndex instances may be created (e.g. in tests), each adding
|
|
134
|
-
// an exit listener. Raise the limit to avoid MaxListenersExceededWarning.
|
|
135
|
-
// Skip when maxListeners is 0 (unlimited).
|
|
136
|
-
const currentMax = process.getMaxListeners();
|
|
137
|
-
if (currentMax !== 0 && currentMax < openInstances.size + 11) {
|
|
138
|
-
process.setMaxListeners(Math.max(currentMax + 10, openInstances.size + 11));
|
|
139
|
-
}
|
|
140
|
-
process.on('exit', this.exitHandler);
|
|
141
|
-
openInstances.add(this);
|
|
142
152
|
}
|
|
143
153
|
get(key) {
|
|
144
154
|
const row = sqliteRetry(() => this.stmtGet.get(key));
|
|
@@ -292,18 +302,103 @@ export class StoreIndex {
|
|
|
292
302
|
this.closed = true;
|
|
293
303
|
openInstances.delete(this);
|
|
294
304
|
process.removeListener('exit', this.exitHandler);
|
|
305
|
+
this.optimizeBeforeClose();
|
|
295
306
|
try {
|
|
296
|
-
this.db.
|
|
307
|
+
this.db.close();
|
|
297
308
|
}
|
|
298
309
|
catch {
|
|
299
|
-
//
|
|
310
|
+
// The DB may be locked by another connection; the OS will reclaim it on process exit.
|
|
300
311
|
}
|
|
312
|
+
}
|
|
313
|
+
/** Run `PRAGMA optimize` before closing. Overridden by {@link ReadOnlyStoreIndex} to skip it (the DB is immutable). */
|
|
314
|
+
optimizeBeforeClose() {
|
|
301
315
|
try {
|
|
302
|
-
this.db.
|
|
316
|
+
this.db.exec('PRAGMA optimize');
|
|
303
317
|
}
|
|
304
318
|
catch {
|
|
305
|
-
//
|
|
319
|
+
// PRAGMA optimize is a performance hint; safe to ignore if the DB is locked.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* A {@link StoreIndex} opened read-only for installs against a store on a
|
|
325
|
+
* read-only filesystem (`frozenStore`). The index is a WAL-mode database, and a
|
|
326
|
+
* normal WAL read creates an `index.db-shm` sidecar in the store directory —
|
|
327
|
+
* which fails on a read-only directory and surfaces as "attempt to write a
|
|
328
|
+
* readonly database" on the first query. Opening via the SQLite `immutable=1`
|
|
329
|
+
* URI tells SQLite the file cannot change, so it bypasses the WAL/shm machinery
|
|
330
|
+
* and reads the file directly, creating no sidecars.
|
|
331
|
+
*
|
|
332
|
+
* The store is assumed complete; every write is a programming error and throws.
|
|
333
|
+
*/
|
|
334
|
+
export class ReadOnlyStoreIndex extends StoreIndex {
|
|
335
|
+
openDatabase(storeDir) {
|
|
336
|
+
if (!nodeSupportsImmutableSqliteUri()) {
|
|
337
|
+
throw new PnpmError('FROZEN_STORE_UNSUPPORTED_NODE', `frozenStore opens the store index read-only via a SQLite "immutable" URI, which requires Node.js >=22.15.0, >=23.11.0, or >=24.0.0, but the current version is ${process.versions.node}. Upgrade Node.js, or run without frozenStore.`);
|
|
306
338
|
}
|
|
339
|
+
this.db = new DatabaseSync(immutableSqliteUri(`${storeDir}/index.db`));
|
|
340
|
+
}
|
|
341
|
+
prepareStatements() {
|
|
342
|
+
this.stmtGet = this.db.prepare('SELECT data FROM package_index WHERE key = ?');
|
|
343
|
+
this.stmtHas = this.db.prepare('SELECT 1 FROM package_index WHERE key = ?');
|
|
344
|
+
this.stmtAll = this.db.prepare('SELECT key, data FROM package_index');
|
|
345
|
+
this.stmtKeys = this.db.prepare('SELECT key FROM package_index');
|
|
346
|
+
}
|
|
347
|
+
optimizeBeforeClose() { }
|
|
348
|
+
set(_key, _data) {
|
|
349
|
+
this.throwReadOnly();
|
|
307
350
|
}
|
|
351
|
+
delete(_key) {
|
|
352
|
+
this.throwReadOnly();
|
|
353
|
+
}
|
|
354
|
+
queueWrites(_writes) {
|
|
355
|
+
this.throwReadOnly();
|
|
356
|
+
}
|
|
357
|
+
setRawMany(_entries) {
|
|
358
|
+
this.throwReadOnly();
|
|
359
|
+
}
|
|
360
|
+
deleteMany(_keys) {
|
|
361
|
+
this.throwReadOnly();
|
|
362
|
+
}
|
|
363
|
+
checkpoint() {
|
|
364
|
+
this.throwReadOnly();
|
|
365
|
+
}
|
|
366
|
+
throwReadOnly() {
|
|
367
|
+
throw new PnpmError('FROZEN_STORE_WRITE', FROZEN_STORE_WRITE_MESSAGE);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Build the `file://…?immutable=1` URI used to open `index.db` read-only (see
|
|
372
|
+
* the frozen-store rationale at the call site). `pathToFileURL` yields a
|
|
373
|
+
* canonical file URL on every platform: it percent-encodes the URI delimiters
|
|
374
|
+
* that could otherwise truncate the path or inject a query/fragment (`?`, `#`,
|
|
375
|
+
* `%`, spaces) and, on Windows, maps the drive letter and backslashes into a
|
|
376
|
+
* valid `file:///C:/…` form. A raw `file:${path}` concatenation would mis-parse
|
|
377
|
+
* those. See https://sqlite.org/uri.html.
|
|
378
|
+
*/
|
|
379
|
+
function immutableSqliteUri(dbPath) {
|
|
380
|
+
const url = pathToFileURL(dbPath);
|
|
381
|
+
url.searchParams.set('immutable', '1');
|
|
382
|
+
return url.href;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Whether the running Node.js can open a `file:…?immutable=1` SQLite URI.
|
|
386
|
+
*
|
|
387
|
+
* `node:sqlite` only passes `SQLITE_OPEN_URI` to SQLite — so the `immutable=1`
|
|
388
|
+
* query is honored rather than treated as part of a literal filename — starting
|
|
389
|
+
* in v22.15.0 (22.x line), v23.11.0 (23.x line), and every v24+. On older
|
|
390
|
+
* runtimes the URI is opened as a literal path and fails with a cryptic
|
|
391
|
+
* "unable to open database file"; we detect that up front to give actionable
|
|
392
|
+
* guidance instead.
|
|
393
|
+
*/
|
|
394
|
+
function nodeSupportsImmutableSqliteUri() {
|
|
395
|
+
const [major, minor] = process.versions.node.split('.', 2).map(Number);
|
|
396
|
+
if (major < 22)
|
|
397
|
+
return false;
|
|
398
|
+
if (major === 22)
|
|
399
|
+
return minor >= 15;
|
|
400
|
+
if (major === 23)
|
|
401
|
+
return minor >= 11;
|
|
402
|
+
return true;
|
|
308
403
|
}
|
|
309
404
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/store.index",
|
|
3
|
-
"version": "1100.
|
|
3
|
+
"version": "1100.2.0",
|
|
4
4
|
"description": "SQLite-backed index for the pnpm content-addressable store",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"funding": "https://opencollective.com/pnpm",
|
|
12
|
-
"repository":
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/pnpm/pnpm/tree/main/store/index"
|
|
15
|
+
},
|
|
13
16
|
"homepage": "https://github.com/pnpm/pnpm/tree/main/store/index#readme",
|
|
14
17
|
"bugs": {
|
|
15
18
|
"url": "https://github.com/pnpm/pnpm/issues"
|
|
@@ -25,13 +28,14 @@
|
|
|
25
28
|
"!*.map"
|
|
26
29
|
],
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"msgpackr": "
|
|
31
|
+
"msgpackr": "2.0.4",
|
|
32
|
+
"@pnpm/error": "1100.0.0"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
|
-
"@jest/globals": "30.
|
|
32
|
-
"@types/node": "^22.19.
|
|
35
|
+
"@jest/globals": "30.4.1",
|
|
36
|
+
"@types/node": "^22.19.19",
|
|
33
37
|
"tempy": "3.0.0",
|
|
34
|
-
"@pnpm/store.index": "1100.
|
|
38
|
+
"@pnpm/store.index": "1100.2.0"
|
|
35
39
|
},
|
|
36
40
|
"engines": {
|
|
37
41
|
"node": ">=22.13"
|