@oh-my-pi/pi-utils 18.4.11 → 18.5.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/CHANGELOG.md +17 -0
- package/dist/types/dirs.d.ts +6 -0
- package/package.json +2 -2
- package/src/dirs.ts +9 -0
- package/src/sqlite.ts +58 -1
- package/src/temp.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.5.0] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added the public `getSessionOwnersDir()` utility, which returns the profile-independent `~/.omp/run/session-owners` directory that names session ownership leases ([#14095](https://github.com/can1357/oh-my-pi/pull/14095) by [@andrebrait](https://github.com/andrebrait))
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed SQLite error messages doubling every backslash in Windows database paths
|
|
14
|
+
- Fixed corrupt-database recovery failing with `EBUSY` on Windows when several in-process openers of the same store failed at once
|
|
15
|
+
|
|
16
|
+
## [18.4.12] - 2026-10-02
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed multi-second temp directory removal stalls on Windows by forcing a major GC before the first deletion retry ([#13044](https://github.com/can1357/oh-my-pi/pull/13044) by [@jchanghong023](https://github.com/jchanghong023)).
|
|
21
|
+
|
|
5
22
|
## [18.4.11] - 2026-10-02
|
|
6
23
|
|
|
7
24
|
### Added
|
package/dist/types/dirs.d.ts
CHANGED
|
@@ -322,6 +322,12 @@ export declare function getDaemonRuntimeDir(projectDir: string): string;
|
|
|
322
322
|
export declare function getGlobalDaemonRuntimeRoot(): string;
|
|
323
323
|
/** Get a profile-independent runtime directory for a machine-global daemon service. */
|
|
324
324
|
export declare function getGlobalDaemonRuntimeDir(service: string): string;
|
|
325
|
+
/**
|
|
326
|
+
* Directory naming session ownership leases (~/.omp/run/session-owners; XDG
|
|
327
|
+
* default: $XDG_STATE_HOME/omp/run/session-owners). Shared across profiles:
|
|
328
|
+
* every omp process that opens a session must meet the same lease.
|
|
329
|
+
*/
|
|
330
|
+
export declare function getSessionOwnersDir(): string;
|
|
325
331
|
/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */
|
|
326
332
|
export declare function getProviderInFlightRoot(): string;
|
|
327
333
|
/** Get the marketplaces registry path (~/.omp/marketplaces.json; XDG default: $XDG_DATA_HOME/omp/marketplaces.json). Adopts a legacy registry on first XDG resolution. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.5.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.5.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/dirs.ts
CHANGED
|
@@ -1095,6 +1095,15 @@ export function getGlobalDaemonRuntimeDir(service: string): string {
|
|
|
1095
1095
|
return path.join(getGlobalDaemonRuntimeRoot(), service);
|
|
1096
1096
|
}
|
|
1097
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* Directory naming session ownership leases (~/.omp/run/session-owners; XDG
|
|
1100
|
+
* default: $XDG_STATE_HOME/omp/run/session-owners). Shared across profiles:
|
|
1101
|
+
* every omp process that opens a session must meet the same lease.
|
|
1102
|
+
*/
|
|
1103
|
+
export function getSessionOwnersDir(): string {
|
|
1104
|
+
return dirs.baseRootSubdir(path.join("run", "session-owners"), "state");
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1098
1107
|
/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */
|
|
1099
1108
|
export function getProviderInFlightRoot(): string {
|
|
1100
1109
|
return dirs.rootSubdir(path.join("run", "provider-inflight"), "state");
|
package/src/sqlite.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Shared SQLite opening, error attribution, and result-code classification for persistent stores. */
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
4
5
|
import { getDbBusyTimeoutMs } from "./env";
|
|
5
6
|
import { withFileLockSync } from "./file-lock";
|
|
6
7
|
import { isEnoent } from "./fs-error";
|
|
@@ -12,6 +13,55 @@ const SQLITE_STORE_SUFFIXES = ["-wal", "-shm", "-journal", ""];
|
|
|
12
13
|
|
|
13
14
|
type SqliteFileIdentity = string | null | undefined;
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Corrupt handles held open by in-process openers awaiting recovery, keyed by
|
|
18
|
+
* resolved store path. Windows refuses to unlink a file any handle still holds,
|
|
19
|
+
* so the opener that quarantines the store closes its peers' dead handles too.
|
|
20
|
+
*/
|
|
21
|
+
const pendingCorruptHandles = new Map<string, Set<Database>>();
|
|
22
|
+
|
|
23
|
+
function corruptHandleKey(dbPath: string): string {
|
|
24
|
+
const resolved = path.resolve(dbPath);
|
|
25
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function registerCorruptHandle(dbPath: string, db: Database | undefined): void {
|
|
29
|
+
if (!db) return;
|
|
30
|
+
const key = corruptHandleKey(dbPath);
|
|
31
|
+
let handles = pendingCorruptHandles.get(key);
|
|
32
|
+
if (!handles) {
|
|
33
|
+
handles = new Set();
|
|
34
|
+
pendingCorruptHandles.set(key, handles);
|
|
35
|
+
}
|
|
36
|
+
handles.add(db);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unregisterCorruptHandle(dbPath: string, db: Database | undefined): void {
|
|
40
|
+
if (!db) return;
|
|
41
|
+
const key = corruptHandleKey(dbPath);
|
|
42
|
+
const handles = pendingCorruptHandles.get(key);
|
|
43
|
+
if (!handles) return;
|
|
44
|
+
handles.delete(db);
|
|
45
|
+
if (handles.size === 0) pendingCorruptHandles.delete(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Closes peers' failed handles on the store; each peer still closes (idempotently) and adopts on its own path. */
|
|
49
|
+
function closePeerCorruptHandles(dbPath: string, own: Database | undefined): void {
|
|
50
|
+
const handles = pendingCorruptHandles.get(corruptHandleKey(dbPath));
|
|
51
|
+
if (!handles) return;
|
|
52
|
+
for (const peer of handles) {
|
|
53
|
+
if (peer === own) continue;
|
|
54
|
+
try {
|
|
55
|
+
peer.close();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.warn("Failed to close a peer's corrupt SQLite handle before quarantine", {
|
|
58
|
+
path: dbPath,
|
|
59
|
+
error: String(error),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
15
65
|
class SqliteAttemptFailure extends Error {
|
|
16
66
|
readonly original: unknown;
|
|
17
67
|
readonly identity: SqliteFileIdentity;
|
|
@@ -99,6 +149,7 @@ async function openWithBusyRetries<T>(
|
|
|
99
149
|
} catch (caught) {
|
|
100
150
|
const error = options.recoverCorruption ? revealHiddenCorruption(db, caught) : caught;
|
|
101
151
|
if (options.recoverCorruption && isSqliteCorruptionError(error)) {
|
|
152
|
+
registerCorruptHandle(dbPath, db);
|
|
102
153
|
throw new SqliteAttemptFailure(error, identity, { db });
|
|
103
154
|
}
|
|
104
155
|
closeFailedDatabase(db, error, identity);
|
|
@@ -120,6 +171,7 @@ function openOnce<T>(dbPath: string, initialize: (db: Database) => T, options: S
|
|
|
120
171
|
} catch (caught) {
|
|
121
172
|
const error = options.recoverCorruption ? revealHiddenCorruption(db, caught) : caught;
|
|
122
173
|
if (options.recoverCorruption && isSqliteCorruptionError(error)) {
|
|
174
|
+
registerCorruptHandle(dbPath, db);
|
|
123
175
|
throw new SqliteAttemptFailure(error, identity, { db });
|
|
124
176
|
}
|
|
125
177
|
closeFailedDatabase(db, error, identity);
|
|
@@ -143,6 +195,10 @@ function quarantineCorruptSqliteStore(dbPath: string, db: Database | undefined):
|
|
|
143
195
|
}
|
|
144
196
|
}
|
|
145
197
|
db?.close();
|
|
198
|
+
// Only Windows refuses to unlink a file another handle still holds. Elsewhere
|
|
199
|
+
// each peer must keep its own handle until it closes it after adopting the
|
|
200
|
+
// replacement; closing it here races the peer's recovery on POSIX.
|
|
201
|
+
if (process.platform === "win32") closePeerCorruptHandles(dbPath, db);
|
|
146
202
|
|
|
147
203
|
const removed: string[] = [];
|
|
148
204
|
try {
|
|
@@ -199,6 +255,7 @@ function recoverCorruptDatabase(dbPath: string, error: unknown, options: SqliteO
|
|
|
199
255
|
return quarantineCorruptSqliteStore(dbPath, failure.db);
|
|
200
256
|
});
|
|
201
257
|
} finally {
|
|
258
|
+
unregisterCorruptHandle(dbPath, failure.db);
|
|
202
259
|
closeFailedDatabase(failure.db, failure.original, failure.identity);
|
|
203
260
|
}
|
|
204
261
|
} catch (preservationError) {
|
|
@@ -267,7 +324,7 @@ export function openSqliteDatabaseSync<T>(
|
|
|
267
324
|
/** Adds the failing store's path to an error without losing SQLite result codes or its original stack. */
|
|
268
325
|
export function annotateSqliteError(error: unknown, dbPath: string): Error {
|
|
269
326
|
const annotated = error instanceof Error ? error : new Error(String(error));
|
|
270
|
-
annotated.message = `Database ${
|
|
327
|
+
annotated.message = `Database "${dbPath}": ${annotated.message}`;
|
|
271
328
|
return annotated;
|
|
272
329
|
}
|
|
273
330
|
|
package/src/temp.ts
CHANGED
|
@@ -94,6 +94,11 @@ export async function removeWithRetries(target: string): Promise<void> {
|
|
|
94
94
|
return;
|
|
95
95
|
} catch (err) {
|
|
96
96
|
if (!shouldRetryRemove(err, attempt)) throw err;
|
|
97
|
+
// bun on Windows finalizes SQLite's db/-wal/-shm file and directory
|
|
98
|
+
// handles on GC, so a closed database can still block deletion for
|
|
99
|
+
// seconds. One forced major collection on the first retry releases
|
|
100
|
+
// them immediately instead of burning the retry window.
|
|
101
|
+
if (attempt === 0) Bun.gc(true);
|
|
97
102
|
await Bun.sleep(kRemoveRetryDelayMs);
|
|
98
103
|
}
|
|
99
104
|
}
|
|
@@ -106,6 +111,7 @@ export function removeSyncWithRetries(target: string): void {
|
|
|
106
111
|
return;
|
|
107
112
|
} catch (err) {
|
|
108
113
|
if (!shouldRetryRemove(err, attempt)) throw err;
|
|
114
|
+
if (attempt === 0) Bun.gc(true);
|
|
109
115
|
sleepSync(kRemoveRetryDelayMs);
|
|
110
116
|
}
|
|
111
117
|
}
|