@linxiraos/pi-utils 1.4.7 → 1.4.10

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.
@@ -4,6 +4,7 @@ import * as Module from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { withFileLock } from "./file-lock";
6
6
  import { isEexist, isEnoent } from "./fs-error";
7
+ import { isRecord } from "./type-guards";
7
8
 
8
9
  /**
9
10
  * On-demand runtime dependency support for native-heavy optional packages
@@ -32,10 +33,6 @@ const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, de
32
33
  /** Extension probes appended to a `main`/`exports` target that lacks one. */
33
34
  const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
34
35
 
35
- function isRecord(value: unknown): value is Record<string, unknown> {
36
- return typeof value === "object" && value !== null && !Array.isArray(value);
37
- }
38
-
39
36
  /**
40
37
  * Walk a conditional `exports` target (string, array of fallbacks, or a
41
38
  * condition object) and return the first relative path that matches a runtime
package/src/sqlite.ts CHANGED
@@ -2,15 +2,43 @@
2
2
  import { Database } from "bun:sqlite";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { installSqliteLockShim } from "@linxiraos/pi-natives/sqlite-locks";
5
6
  import { getDbBusyTimeoutMs } from "./env";
6
7
  import { withFileLockSync } from "./file-lock";
7
8
  import { isEnoent } from "./fs-error";
9
+ import { toError } from "./type-guards";
8
10
  import * as logger from "./logger";
9
11
 
10
12
  const BUSY_MAX_ATTEMPTS = 4;
11
13
  const BUSY_BASE_DELAY_MS = 100;
12
14
  const SQLITE_STORE_SUFFIXES = ["-wal", "-shm", "-journal", ""];
13
15
 
16
+ let sqliteLockShimInstalled: boolean | undefined;
17
+
18
+ /**
19
+ * Moves this process's SQLite locks onto open-file-description locks (Linux), once.
20
+ *
21
+ * SQLite's POSIX record locks belong to the process, so any close of another
22
+ * descriptor of a database or its `-shm` released them, including Bun's own file
23
+ * APIs and the bash tool's builtins; a peer omp then unlinked the WAL under the live
24
+ * connection (issue #15586). Call before the process opens its first database: a
25
+ * connection opened earlier keeps POSIX locks. Returns whether the shim is active; a
26
+ * Linux build where it cannot be installed (Bun's SQLite layout changed) logs a warning
27
+ * once, and callers with a UI surface it.
28
+ */
29
+ export function ensureSqliteLockShim(): boolean {
30
+ if (sqliteLockShimInstalled === undefined) {
31
+ sqliteLockShimInstalled = installSqliteLockShim();
32
+ if (!sqliteLockShimInstalled && process.platform === "linux") {
33
+ logger.warn("SQLite OFD lock shim unavailable; in-process reads of a live store can release its locks", {
34
+ bun: Bun.version,
35
+ issue: "https://github.com/can1357/oh-my-pi/issues/15586",
36
+ });
37
+ }
38
+ }
39
+ return sqliteLockShimInstalled;
40
+ }
41
+
14
42
  /**
15
43
  * A store's identity as `dev:ino:birthtimeMs`, `null` when the path holds no
16
44
  * file, or `undefined` when the stat itself failed and no conclusion is safe.
@@ -100,7 +128,7 @@ function closeFailedDatabase(db: Database | undefined, error: unknown, identity:
100
128
  try {
101
129
  db?.close();
102
130
  } catch (closeError) {
103
- const original = error instanceof Error ? error : new Error(String(error));
131
+ const original = toError(error);
104
132
  const detail = closeError instanceof Error ? closeError.message : String(closeError);
105
133
  original.message += `; failed to close the SQLite handle: ${detail}`;
106
134
  throw new SqliteAttemptFailure(original, identity, { canRecover: false });
@@ -152,6 +180,7 @@ async function openWithBusyRetries<T>(
152
180
  let db: Database | undefined;
153
181
  const identity = sqliteFileIdentity(dbPath);
154
182
  try {
183
+ ensureSqliteLockShim();
155
184
  db = new Database(dbPath);
156
185
  // WAL recovery can bypass the busy handler; both it and retries are needed (#2421).
157
186
  db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
@@ -175,6 +204,7 @@ function openOnce<T>(dbPath: string, initialize: (db: Database) => T, options: S
175
204
  let db: Database | undefined;
176
205
  const identity = sqliteFileIdentity(dbPath);
177
206
  try {
207
+ ensureSqliteLockShim();
178
208
  db = new Database(dbPath);
179
209
  db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
180
210
  return initialize(db);
@@ -333,7 +363,7 @@ export function openSqliteDatabaseSync<T>(
333
363
 
334
364
  /** Adds the failing store's path to an error without losing SQLite result codes or its original stack. */
335
365
  export function annotateSqliteError(error: unknown, dbPath: string): Error {
336
- const annotated = error instanceof Error ? error : new Error(String(error));
366
+ const annotated = toError(error);
337
367
  annotated.message = `Database "${dbPath}": ${annotated.message}`;
338
368
  return annotated;
339
369
  }