pgserve 2.0.0 → 2.0.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/src/sdk.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Public SDK helpers for applications that want to consume the singleton
3
+ * pgserve daemon without shelling out themselves.
4
+ *
5
+ * The intended flow is:
6
+ * 1. App calls ensureDaemon() during install/startup.
7
+ * 2. App connects with daemonClientOptions().
8
+ * 3. pgserve derives the app identity from the Unix-socket peer creds and
9
+ * routes it to that app's fingerprinted database.
10
+ */
11
+
12
+ import { spawn } from 'child_process';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import {
17
+ isProcessAlive,
18
+ resolveControlSocketDir,
19
+ resolveControlSocketPath,
20
+ resolveLibpqCompatPath,
21
+ resolvePidLockPath,
22
+ } from './daemon.js';
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ export function probeDaemon({ controlSocketDir = resolveControlSocketDir() } = {}) {
27
+ const socketPath = resolveControlSocketPath(controlSocketDir);
28
+ const libpqSocketPath = resolveLibpqCompatPath(controlSocketDir);
29
+ const pidLockPath = resolvePidLockPath(controlSocketDir);
30
+ const socketPresent = fs.existsSync(socketPath);
31
+ const libpqSocketPresent = fs.existsSync(libpqSocketPath);
32
+ let pid = null;
33
+
34
+ try {
35
+ const raw = fs.readFileSync(pidLockPath, 'utf8').trim();
36
+ const parsed = Number.parseInt(raw, 10);
37
+ if (Number.isInteger(parsed) && parsed > 0) pid = parsed;
38
+ } catch {
39
+ // Missing/unreadable pid file means no live daemon can be trusted.
40
+ }
41
+
42
+ const pidAlive = pid !== null && isProcessAlive(pid);
43
+ const running = pidAlive && socketPresent && libpqSocketPresent;
44
+ return {
45
+ running,
46
+ pid: pidAlive ? pid : null,
47
+ socketPresent,
48
+ libpqSocketPresent,
49
+ controlSocketDir,
50
+ controlSocketPath: socketPath,
51
+ libpqSocketPath,
52
+ pidLockPath,
53
+ reason: running ? null : explainProbeMiss({ pid, pidAlive, socketPresent, libpqSocketPresent }),
54
+ };
55
+ }
56
+
57
+ function explainProbeMiss({ pid, pidAlive, socketPresent, libpqSocketPresent }) {
58
+ if (pid === null && !socketPresent && !libpqSocketPresent) return 'no daemon';
59
+ if (pid !== null && !pidAlive) return 'stale pid';
60
+ if (!socketPresent) return 'control socket missing';
61
+ if (!libpqSocketPresent) return 'libpq socket missing';
62
+ return 'not running';
63
+ }
64
+
65
+ export function daemonClientOptions({
66
+ controlSocketDir = resolveControlSocketDir(),
67
+ database = 'postgres',
68
+ username = 'postgres',
69
+ } = {}) {
70
+ return {
71
+ host: controlSocketDir,
72
+ port: 5432,
73
+ database,
74
+ username,
75
+ password: '',
76
+ };
77
+ }
78
+
79
+ export function buildDaemonArgs({
80
+ dataDir,
81
+ ram = false,
82
+ logLevel,
83
+ noProvision = false,
84
+ listens = [],
85
+ pgvector = false,
86
+ } = {}) {
87
+ const args = ['daemon'];
88
+ if (dataDir) args.push('--data', dataDir);
89
+ if (ram) args.push('--ram');
90
+ if (logLevel) args.push('--log', logLevel);
91
+ if (noProvision) args.push('--no-provision');
92
+ if (pgvector) args.push('--pgvector');
93
+ for (const listen of Array.isArray(listens) ? listens : [listens]) {
94
+ if (listen) args.push('--listen', String(listen));
95
+ }
96
+ return args;
97
+ }
98
+
99
+ export async function ensureDaemon(options = {}) {
100
+ const controlSocketDir = options.controlSocketDir || resolveControlSocketDir();
101
+ const initial = probeDaemon({ controlSocketDir });
102
+ if (initial.running) return initial;
103
+
104
+ const bin = options.bin || resolveBundledCliBin();
105
+ const env = { ...process.env, ...envForControlSocketDir(controlSocketDir), ...(options.env || {}) };
106
+ const child = spawn(bin, buildDaemonArgs(options), {
107
+ detached: true,
108
+ stdio: 'ignore',
109
+ env,
110
+ });
111
+ child.unref();
112
+
113
+ const timeoutMs = options.timeoutMs || 16000;
114
+ const deadline = Date.now() + timeoutMs;
115
+ while (Date.now() < deadline) {
116
+ const state = probeDaemon({ controlSocketDir });
117
+ if (state.running) return state;
118
+ await new Promise((resolve) => setTimeout(resolve, 250));
119
+ }
120
+
121
+ const state = probeDaemon({ controlSocketDir });
122
+ const err = new Error(`pgserve daemon did not become ready within ${timeoutMs}ms (${state.reason})`);
123
+ err.code = 'EPGSERVE_DAEMON_TIMEOUT';
124
+ err.state = state;
125
+ throw err;
126
+ }
127
+
128
+ export function resolveBundledCliBin() {
129
+ return path.join(__dirname, '..', 'bin', 'pgserve-wrapper.cjs');
130
+ }
131
+
132
+ function envForControlSocketDir(controlSocketDir) {
133
+ if (path.basename(controlSocketDir) !== 'pgserve') {
134
+ throw new Error('ensureDaemon: controlSocketDir must be a pgserve runtime directory ending in /pgserve');
135
+ }
136
+ return { XDG_RUNTIME_DIR: path.dirname(controlSocketDir) };
137
+ }