pgserve 1.1.2 → 1.1.3-rc.11
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/.github/workflows/build-all-platforms.yml +35 -15
- package/README.md +11 -0
- package/assets/icon.ico +0 -0
- package/bin/pgserve-wrapper.cjs +114 -21
- package/bun.lock +3 -1
- package/package.json +2 -2
- package/src/postgres.js +335 -17
|
@@ -38,26 +38,23 @@ concurrency:
|
|
|
38
38
|
jobs:
|
|
39
39
|
build:
|
|
40
40
|
name: Build ${{ matrix.platform }}
|
|
41
|
-
runs-on:
|
|
41
|
+
runs-on: ${{ matrix.os }}
|
|
42
42
|
strategy:
|
|
43
43
|
fail-fast: false
|
|
44
44
|
matrix:
|
|
45
45
|
include:
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
# Linux x64
|
|
47
|
+
- platform: linux-x64
|
|
48
48
|
output: pgserve-linux-x64
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- target: bun-darwin-x64
|
|
53
|
-
platform: darwin-x64
|
|
54
|
-
output: pgserve-darwin-x64
|
|
55
|
-
- target: bun-darwin-arm64
|
|
56
|
-
platform: darwin-arm64
|
|
49
|
+
os: ubuntu-latest
|
|
50
|
+
# macOS ARM64 (Apple Silicon)
|
|
51
|
+
- platform: darwin-arm64
|
|
57
52
|
output: pgserve-darwin-arm64
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
os: macos-latest
|
|
54
|
+
# Windows x64 - must build on Windows for --windows-icon
|
|
55
|
+
- platform: windows-x64
|
|
60
56
|
output: pgserve-windows-x64.exe
|
|
57
|
+
os: windows-latest
|
|
61
58
|
|
|
62
59
|
steps:
|
|
63
60
|
- name: Checkout
|
|
@@ -73,10 +70,32 @@ jobs:
|
|
|
73
70
|
- name: Install dependencies
|
|
74
71
|
run: bun install
|
|
75
72
|
|
|
73
|
+
# Windows: native build with icon and proper metadata
|
|
74
|
+
- name: Build for Windows (with branding)
|
|
75
|
+
if: matrix.platform == 'windows-x64'
|
|
76
|
+
run: |
|
|
77
|
+
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
|
78
|
+
$RAW_VERSION = node -p "require('./package.json').version"
|
|
79
|
+
# Convert 1.1.3-rc.10 to 1.1.3.10 (Windows requires X.Y.Z.W format)
|
|
80
|
+
$WIN_VERSION = $RAW_VERSION -replace '-rc\.', '.'
|
|
81
|
+
Write-Host "Raw version: $RAW_VERSION -> Windows version: $WIN_VERSION"
|
|
82
|
+
bun build --compile `
|
|
83
|
+
--windows-icon=assets/icon.ico `
|
|
84
|
+
--windows-title="pgserve" `
|
|
85
|
+
--windows-publisher="Namastex Labs" `
|
|
86
|
+
--windows-description="Embedded PostgreSQL Server - Zero config, auto-provision, unlimited connections" `
|
|
87
|
+
--windows-version="$WIN_VERSION" `
|
|
88
|
+
--windows-copyright="Copyright (c) 2025 Namastex Labs" `
|
|
89
|
+
bin/pglite-server.js --outfile dist/${{ matrix.output }}
|
|
90
|
+
Get-ChildItem dist/
|
|
91
|
+
shell: pwsh
|
|
92
|
+
|
|
93
|
+
# Linux/macOS: native build
|
|
76
94
|
- name: Build for ${{ matrix.platform }}
|
|
95
|
+
if: matrix.platform != 'windows-x64'
|
|
77
96
|
run: |
|
|
78
97
|
mkdir -p dist
|
|
79
|
-
bun build --compile
|
|
98
|
+
bun build --compile bin/pglite-server.js --outfile dist/${{ matrix.output }}
|
|
80
99
|
ls -lh dist/
|
|
81
100
|
|
|
82
101
|
- name: Upload artifact
|
|
@@ -129,7 +148,8 @@ jobs:
|
|
|
129
148
|
ls -la dist/
|
|
130
149
|
|
|
131
150
|
MISSING=""
|
|
132
|
-
|
|
151
|
+
# Supported platforms: linux-x64, darwin-arm64, windows-x64
|
|
152
|
+
for platform in linux-x64 darwin-arm64; do
|
|
133
153
|
if [ ! -f "dist/pgserve-$platform" ]; then
|
|
134
154
|
MISSING="$MISSING pgserve-$platform"
|
|
135
155
|
fi
|
package/README.md
CHANGED
|
@@ -99,6 +99,17 @@ npm install pgserve
|
|
|
99
99
|
|
|
100
100
|
> PostgreSQL binaries are automatically downloaded on first run (~100MB).
|
|
101
101
|
|
|
102
|
+
### Windows
|
|
103
|
+
|
|
104
|
+
Download `pgserve-windows-x64.exe` from [GitHub Releases](https://github.com/namastexlabs/pgserve/releases).
|
|
105
|
+
|
|
106
|
+
Double-click to run, or use CLI:
|
|
107
|
+
|
|
108
|
+
```cmd
|
|
109
|
+
pgserve-windows-x64.exe --port 5432
|
|
110
|
+
pgserve-windows-x64.exe --data C:\pgserve-data
|
|
111
|
+
```
|
|
112
|
+
|
|
102
113
|
<br>
|
|
103
114
|
|
|
104
115
|
## CLI Reference
|
package/assets/icon.ico
ADDED
|
Binary file
|
package/bin/pgserve-wrapper.cjs
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* This wrapper enables `npx pgserve` to work without requiring
|
|
6
6
|
* users to install bun globally. The bun runtime is bundled as
|
|
7
7
|
* an npm dependency and this wrapper finds and invokes it.
|
|
8
|
+
*
|
|
9
|
+
* Windows EBUSY fix: Uses synchronous waiting and taskkill for
|
|
10
|
+
* reliable process termination and file handle cleanup.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
const { spawn } = require('child_process');
|
|
13
|
+
const { spawn, execSync } = require('child_process');
|
|
11
14
|
const path = require('path');
|
|
12
15
|
const fs = require('fs');
|
|
13
16
|
|
|
@@ -52,30 +55,120 @@ if (!bunPath) {
|
|
|
52
55
|
|
|
53
56
|
const scriptPath = path.join(__dirname, 'pglite-server.js');
|
|
54
57
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
windowsHide: true
|
|
59
|
-
});
|
|
58
|
+
// Platform-specific spawning strategy:
|
|
59
|
+
// - Windows: Use pipes for explicit handle control (prevents EBUSY errors)
|
|
60
|
+
// - Unix: Use inherit for simplicity (works fine)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (isWindows) {
|
|
63
|
+
// WINDOWS PATH: Explicit pipe control to prevent EBUSY errors
|
|
64
|
+
// Using stdio: 'inherit' causes file handle inheritance that we cannot release,
|
|
65
|
+
// leading to npm cleanup failures. With pipes, we control when handles are destroyed.
|
|
66
|
+
|
|
67
|
+
const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
windowsHide: true
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Manually pipe stdio - we now control the handles
|
|
73
|
+
// Handle stdin errors gracefully (may not be connected in some environments)
|
|
74
|
+
process.stdin.on('error', () => {});
|
|
75
|
+
child.stdin.on('error', () => {});
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
process.
|
|
69
|
-
} else {
|
|
70
|
-
process.exit(code ?? 0);
|
|
77
|
+
// Only pipe stdin if it's readable
|
|
78
|
+
if (process.stdin.readable) {
|
|
79
|
+
process.stdin.pipe(child.stdin);
|
|
71
80
|
}
|
|
72
|
-
|
|
81
|
+
child.stdout.pipe(process.stdout);
|
|
82
|
+
child.stderr.pipe(process.stderr);
|
|
83
|
+
|
|
84
|
+
child.on('error', (err) => {
|
|
85
|
+
console.error('Failed to start pgserve:', err.message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.on('close', (code, signal) => {
|
|
90
|
+
// CRITICAL: Explicitly destroy ALL streams to release file handles
|
|
91
|
+
// This must happen BEFORE process.exit() to prevent EBUSY
|
|
92
|
+
try {
|
|
93
|
+
if (process.stdin.readable) {
|
|
94
|
+
process.stdin.unpipe(child.stdin);
|
|
95
|
+
}
|
|
96
|
+
child.stdin.destroy();
|
|
97
|
+
child.stdout.destroy();
|
|
98
|
+
child.stderr.destroy();
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore stream destruction errors
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Remove all listeners to prevent memory leaks
|
|
104
|
+
child.removeAllListeners();
|
|
105
|
+
|
|
106
|
+
// Use setImmediate to ensure stream destruction completes before exit
|
|
107
|
+
// This gives the event loop one tick to process pending I/O cleanup
|
|
108
|
+
setImmediate(() => {
|
|
109
|
+
process.exit(signal ? 1 : (code ?? 0));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
73
112
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
process.on(
|
|
113
|
+
// Windows: use taskkill for reliable process termination
|
|
114
|
+
// process.kill(pid, 'SIGINT') does NOT work properly on Windows
|
|
115
|
+
process.on('SIGINT', () => {
|
|
77
116
|
if (child.pid) {
|
|
78
|
-
|
|
117
|
+
try {
|
|
118
|
+
// /T = terminate child processes (tree), /F = force
|
|
119
|
+
execSync(`taskkill /PID ${child.pid} /T /F`, {
|
|
120
|
+
stdio: 'ignore',
|
|
121
|
+
windowsHide: true
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
// Process may have already exited, ignore errors
|
|
125
|
+
}
|
|
79
126
|
}
|
|
80
127
|
});
|
|
81
|
-
|
|
128
|
+
|
|
129
|
+
// Handle Ctrl+C via readline for Windows terminal compatibility
|
|
130
|
+
// Some Windows terminals don't emit SIGINT properly
|
|
131
|
+
const readline = require('readline');
|
|
132
|
+
if (process.stdin.isTTY) {
|
|
133
|
+
const rl = readline.createInterface({
|
|
134
|
+
input: process.stdin,
|
|
135
|
+
output: process.stdout
|
|
136
|
+
});
|
|
137
|
+
rl.on('SIGINT', () => {
|
|
138
|
+
process.emit('SIGINT');
|
|
139
|
+
});
|
|
140
|
+
// Clean up readline on close
|
|
141
|
+
child.on('close', () => {
|
|
142
|
+
rl.close();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
} else {
|
|
147
|
+
// UNIX PATH: Simple stdio inheritance (works fine, no EBUSY issues)
|
|
148
|
+
const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
windowsHide: true
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
child.on('error', (err) => {
|
|
154
|
+
console.error('Failed to start pgserve:', err.message);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child.on('close', (code, signal) => {
|
|
159
|
+
if (signal) {
|
|
160
|
+
process.kill(process.pid, signal);
|
|
161
|
+
} else {
|
|
162
|
+
process.exit(code ?? 0);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Unix: forward signals to child process normally
|
|
167
|
+
['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(sig => {
|
|
168
|
+
process.on(sig, () => {
|
|
169
|
+
if (child.pid) {
|
|
170
|
+
process.kill(child.pid, sig);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
package/bun.lock
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@embedded-postgres/darwin-arm64": "17.7.0-beta.15",
|
|
21
21
|
"@embedded-postgres/darwin-x64": "17.7.0-beta.15",
|
|
22
22
|
"@embedded-postgres/linux-x64": "17.7.0-beta.15",
|
|
23
|
-
"@embedded-postgres/
|
|
23
|
+
"@embedded-postgres/windows-x64": "17.7.0-beta.15",
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
},
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
"@embedded-postgres/linux-x64": ["@embedded-postgres/linux-x64@17.7.0-beta.15", "", { "os": "linux", "cpu": "x64" }, "sha512-HeaxSHsw6ccVh8l5iC4OgXqvaaCGWnnZR9CpgNgrAfnKPPGiEhUPBmO2XhEsFQIhc+ad/+36h0NTvKo4bdi40w=="],
|
|
35
35
|
|
|
36
|
+
"@embedded-postgres/windows-x64": ["@embedded-postgres/windows-x64@17.7.0-beta.15", "", { "os": "win32", "cpu": "x64" }, "sha512-Oq11yyKxISjefuYdKljcp3Q+uxx237zn9YpP9hO43+6Feorq7USuMIDqk5ofLSQ30FAnVyTqaIQK8ZIjW+tQXQ=="],
|
|
37
|
+
|
|
36
38
|
"@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
|
37
39
|
|
|
38
40
|
"@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgserve",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3-rc.11",
|
|
4
4
|
"description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@embedded-postgres/darwin-arm64": "17.7.0-beta.15",
|
|
45
45
|
"@embedded-postgres/darwin-x64": "17.7.0-beta.15",
|
|
46
46
|
"@embedded-postgres/linux-x64": "17.7.0-beta.15",
|
|
47
|
-
"@embedded-postgres/
|
|
47
|
+
"@embedded-postgres/windows-x64": "17.7.0-beta.15"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@electric-sql/pglite": "^0.2.17",
|
package/src/postgres.js
CHANGED
|
@@ -13,11 +13,162 @@
|
|
|
13
13
|
* - No locale dependency (works on any system)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
/* global fetch, Bun */
|
|
16
17
|
import os from 'os';
|
|
17
18
|
import path from 'path';
|
|
18
19
|
import fs from 'fs';
|
|
19
20
|
import crypto from 'crypto';
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Get platform key for binary lookup (e.g., 'windows-x64', 'linux-x64', 'darwin-arm64')
|
|
24
|
+
* @returns {string} Platform key
|
|
25
|
+
*/
|
|
26
|
+
function getPlatformKey() {
|
|
27
|
+
const platform = os.platform();
|
|
28
|
+
const arch = os.arch();
|
|
29
|
+
|
|
30
|
+
if (platform === 'win32') return 'windows-x64';
|
|
31
|
+
if (platform === 'linux' && arch === 'x64') return 'linux-x64';
|
|
32
|
+
if (platform === 'darwin' && arch === 'arm64') return 'darwin-arm64';
|
|
33
|
+
if (platform === 'darwin' && arch === 'x64') return 'darwin-x64';
|
|
34
|
+
if (platform === 'linux' && arch === 'arm64') return 'linux-arm64';
|
|
35
|
+
|
|
36
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the directory where extracted binaries are cached
|
|
41
|
+
* @returns {string} Cache directory path
|
|
42
|
+
*/
|
|
43
|
+
function getBinaryCacheDir() {
|
|
44
|
+
const platformKey = getPlatformKey();
|
|
45
|
+
return path.join(os.homedir(), '.pgserve', 'bin', platformKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Download and extract PostgreSQL binaries on first run.
|
|
50
|
+
* Downloads from npm registry (@embedded-postgres packages).
|
|
51
|
+
*
|
|
52
|
+
* @returns {Promise<string>} Path to extracted directory
|
|
53
|
+
*/
|
|
54
|
+
async function downloadPostgresBinaries() {
|
|
55
|
+
const platform = os.platform();
|
|
56
|
+
const cacheDir = getBinaryCacheDir();
|
|
57
|
+
const cacheBinDir = path.join(cacheDir, 'bin');
|
|
58
|
+
const initdbName = platform === 'win32' ? 'initdb.exe' : 'initdb';
|
|
59
|
+
const postgresName = platform === 'win32' ? 'postgres.exe' : 'postgres';
|
|
60
|
+
|
|
61
|
+
// Check if already downloaded
|
|
62
|
+
if (fs.existsSync(path.join(cacheBinDir, initdbName)) &&
|
|
63
|
+
fs.existsSync(path.join(cacheBinDir, postgresName))) {
|
|
64
|
+
return cacheDir;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const platformKey = getPlatformKey();
|
|
68
|
+
const pkgName = `@embedded-postgres/${platformKey}`;
|
|
69
|
+
const pkgVersion = '17.7.0-beta.15';
|
|
70
|
+
|
|
71
|
+
console.log(`[pgserve] PostgreSQL binaries not found.`);
|
|
72
|
+
console.log(`[pgserve] Downloading ${pkgName}@${pkgVersion}...`);
|
|
73
|
+
|
|
74
|
+
// Get tarball URL from npm registry
|
|
75
|
+
const registryUrl = `https://registry.npmjs.org/${pkgName}`;
|
|
76
|
+
const registryRes = await fetch(registryUrl);
|
|
77
|
+
if (!registryRes.ok) {
|
|
78
|
+
throw new Error(`Failed to fetch package info: ${registryRes.status}`);
|
|
79
|
+
}
|
|
80
|
+
const pkgInfo = await registryRes.json();
|
|
81
|
+
const tarballUrl = pkgInfo.versions[pkgVersion]?.dist?.tarball;
|
|
82
|
+
|
|
83
|
+
if (!tarballUrl) {
|
|
84
|
+
throw new Error(`Version ${pkgVersion} not found for ${pkgName}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Download tarball
|
|
88
|
+
console.log(`[pgserve] Downloading from npm registry...`);
|
|
89
|
+
const tarballRes = await fetch(tarballUrl);
|
|
90
|
+
if (!tarballRes.ok) {
|
|
91
|
+
throw new Error(`Failed to download tarball: ${tarballRes.status}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const tarballBuffer = await tarballRes.arrayBuffer();
|
|
95
|
+
const tarballSize = (tarballBuffer.byteLength / 1024 / 1024).toFixed(1);
|
|
96
|
+
console.log(`[pgserve] Downloaded ${tarballSize} MB`);
|
|
97
|
+
|
|
98
|
+
// Create temp file for tarball
|
|
99
|
+
const tempDir = path.join(os.tmpdir(), `pgserve-download-${Date.now()}`);
|
|
100
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
101
|
+
const tarballPath = path.join(tempDir, 'package.tgz');
|
|
102
|
+
fs.writeFileSync(tarballPath, Buffer.from(tarballBuffer));
|
|
103
|
+
|
|
104
|
+
// Extract tarball using tar (available on all platforms via bun/node)
|
|
105
|
+
console.log(`[pgserve] Extracting binaries...`);
|
|
106
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
// Use Bun.spawn for extraction
|
|
109
|
+
const extractProc = Bun.spawn(['tar', '-xzf', tarballPath, '-C', tempDir], {
|
|
110
|
+
stdout: 'pipe',
|
|
111
|
+
stderr: 'pipe'
|
|
112
|
+
});
|
|
113
|
+
await extractProc.exited;
|
|
114
|
+
|
|
115
|
+
// Copy native/* to cache dir
|
|
116
|
+
const nativeDir = path.join(tempDir, 'package', 'native');
|
|
117
|
+
if (!fs.existsSync(nativeDir)) {
|
|
118
|
+
throw new Error('Extracted package does not contain native/ directory');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Copy files recursively
|
|
122
|
+
await copyDirRecursive(nativeDir, cacheDir);
|
|
123
|
+
|
|
124
|
+
// Make executables executable (Unix only)
|
|
125
|
+
if (platform !== 'win32') {
|
|
126
|
+
const binDir = path.join(cacheDir, 'bin');
|
|
127
|
+
if (fs.existsSync(binDir)) {
|
|
128
|
+
const files = fs.readdirSync(binDir);
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const filePath = path.join(binDir, file);
|
|
131
|
+
try {
|
|
132
|
+
fs.chmodSync(filePath, 0o755);
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Cleanup temp
|
|
141
|
+
try {
|
|
142
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore cleanup errors
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`[pgserve] PostgreSQL binaries installed to ${cacheDir}`);
|
|
148
|
+
return cacheDir;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Recursively copy a directory
|
|
153
|
+
* @param {string} src - Source directory
|
|
154
|
+
* @param {string} dest - Destination directory
|
|
155
|
+
*/
|
|
156
|
+
async function copyDirRecursive(src, dest) {
|
|
157
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
158
|
+
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const srcPath = path.join(src, entry.name);
|
|
161
|
+
const destPath = path.join(dest, entry.name);
|
|
162
|
+
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
165
|
+
await copyDirRecursive(srcPath, destPath);
|
|
166
|
+
} else {
|
|
167
|
+
fs.copyFileSync(srcPath, destPath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
21
172
|
/**
|
|
22
173
|
* Ensure library symlinks exist in the lib directory.
|
|
23
174
|
* The @embedded-postgres package ships versioned libraries but binaries look for soname versions.
|
|
@@ -94,10 +245,45 @@ function ensureLibrarySymlinks(libDir, platform) {
|
|
|
94
245
|
}
|
|
95
246
|
|
|
96
247
|
// Resolve binary paths from embedded-postgres platform packages
|
|
97
|
-
|
|
248
|
+
// Now async to support bundled binary extraction
|
|
249
|
+
async function getBinaryPaths() {
|
|
98
250
|
const platform = os.platform();
|
|
99
251
|
const arch = os.arch();
|
|
252
|
+
const exeSuffix = platform === 'win32' ? '.exe' : '';
|
|
253
|
+
|
|
254
|
+
// Priority 1: Check extracted cache directory (standalone exe mode)
|
|
255
|
+
// This is where bundled binaries are extracted on first run
|
|
256
|
+
const cacheDir = getBinaryCacheDir();
|
|
257
|
+
const cacheBinDir = path.join(cacheDir, 'bin');
|
|
258
|
+
const cachedInitdb = path.join(cacheBinDir, 'initdb' + exeSuffix);
|
|
259
|
+
const cachedPostgres = path.join(cacheBinDir, 'postgres' + exeSuffix);
|
|
260
|
+
|
|
261
|
+
if (fs.existsSync(cachedInitdb) && fs.existsSync(cachedPostgres)) {
|
|
262
|
+
const libDir = path.join(cacheDir, 'lib');
|
|
263
|
+
if ((platform === 'linux' || platform === 'darwin') && fs.existsSync(libDir)) {
|
|
264
|
+
ensureLibrarySymlinks(libDir, platform);
|
|
265
|
+
}
|
|
266
|
+
return { initdb: cachedInitdb, postgres: cachedPostgres, binDir: cacheBinDir, libDir };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Priority 2: Download binaries if not found (standalone exe mode)
|
|
270
|
+
// This downloads from npm registry on first run
|
|
271
|
+
const downloadedDir = await downloadPostgresBinaries();
|
|
272
|
+
if (downloadedDir) {
|
|
273
|
+
const downloadedBinDir = path.join(downloadedDir, 'bin');
|
|
274
|
+
const downloadedInitdb = path.join(downloadedBinDir, 'initdb' + exeSuffix);
|
|
275
|
+
const downloadedPostgres = path.join(downloadedBinDir, 'postgres' + exeSuffix);
|
|
100
276
|
|
|
277
|
+
if (fs.existsSync(downloadedInitdb) && fs.existsSync(downloadedPostgres)) {
|
|
278
|
+
const libDir = path.join(downloadedDir, 'lib');
|
|
279
|
+
if ((platform === 'linux' || platform === 'darwin') && fs.existsSync(libDir)) {
|
|
280
|
+
ensureLibrarySymlinks(libDir, platform);
|
|
281
|
+
}
|
|
282
|
+
return { initdb: downloadedInitdb, postgres: downloadedPostgres, binDir: downloadedBinDir, libDir };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Priority 3: Find the package in node_modules (npm install case)
|
|
101
287
|
let pkgName;
|
|
102
288
|
if (platform === 'linux' && arch === 'x64') {
|
|
103
289
|
pkgName = '@embedded-postgres/linux-x64';
|
|
@@ -106,12 +292,11 @@ function getBinaryPaths() {
|
|
|
106
292
|
} else if (platform === 'darwin' && arch === 'x64') {
|
|
107
293
|
pkgName = '@embedded-postgres/darwin-x64';
|
|
108
294
|
} else if (platform === 'win32' && arch === 'x64') {
|
|
109
|
-
pkgName = '@embedded-postgres/
|
|
295
|
+
pkgName = '@embedded-postgres/windows-x64';
|
|
110
296
|
} else {
|
|
111
297
|
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
112
298
|
}
|
|
113
299
|
|
|
114
|
-
// Find the package in node_modules (check multiple locations for npx/pnpm/npm compatibility)
|
|
115
300
|
const possiblePaths = [
|
|
116
301
|
path.join(process.cwd(), 'node_modules', pkgName, 'native', 'bin'),
|
|
117
302
|
path.join(import.meta.dirname, '..', 'node_modules', pkgName, 'native', 'bin'),
|
|
@@ -120,8 +305,8 @@ function getBinaryPaths() {
|
|
|
120
305
|
];
|
|
121
306
|
|
|
122
307
|
for (const binDir of possiblePaths) {
|
|
123
|
-
const initdb = path.join(binDir,
|
|
124
|
-
const postgres = path.join(binDir,
|
|
308
|
+
const initdb = path.join(binDir, 'initdb' + exeSuffix);
|
|
309
|
+
const postgres = path.join(binDir, 'postgres' + exeSuffix);
|
|
125
310
|
if (fs.existsSync(initdb) && fs.existsSync(postgres)) {
|
|
126
311
|
// Resolve the actual binary paths (handles symlinks from package managers)
|
|
127
312
|
const realInitdb = fs.realpathSync(initdb);
|
|
@@ -237,8 +422,8 @@ export class PostgresManager {
|
|
|
237
422
|
* Start the embedded PostgreSQL instance
|
|
238
423
|
*/
|
|
239
424
|
async start() {
|
|
240
|
-
// Get binary paths
|
|
241
|
-
this.binaries = getBinaryPaths();
|
|
425
|
+
// Get binary paths (may extract bundled binaries on first run)
|
|
426
|
+
this.binaries = await getBinaryPaths();
|
|
242
427
|
|
|
243
428
|
// Make binaries executable
|
|
244
429
|
await fs.promises.chmod(this.binaries.initdb, '755');
|
|
@@ -308,6 +493,12 @@ export class PostgresManager {
|
|
|
308
493
|
// Initialize admin connection pool (for database creation operations)
|
|
309
494
|
await this._initAdminPool();
|
|
310
495
|
|
|
496
|
+
// For persistent mode, load existing databases into createdDatabases
|
|
497
|
+
// This prevents "database already exists" errors when reusing data directories
|
|
498
|
+
if (this.persistent) {
|
|
499
|
+
await this._loadExistingDatabases();
|
|
500
|
+
}
|
|
501
|
+
|
|
311
502
|
this.logger.info({
|
|
312
503
|
databaseDir: this.databaseDir,
|
|
313
504
|
port: this.port,
|
|
@@ -407,6 +598,33 @@ export class PostgresManager {
|
|
|
407
598
|
}, 'Admin connection pool initialized (Bun.sql)');
|
|
408
599
|
}
|
|
409
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Load existing databases into createdDatabases Set (for persistent mode)
|
|
603
|
+
* This allows pgserve to reuse existing data directories without
|
|
604
|
+
* attempting to CREATE DATABASE for databases that already exist.
|
|
605
|
+
*/
|
|
606
|
+
async _loadExistingDatabases() {
|
|
607
|
+
try {
|
|
608
|
+
const result = await this.adminPool`
|
|
609
|
+
SELECT datname FROM pg_database
|
|
610
|
+
WHERE datistemplate = false
|
|
611
|
+
AND datname NOT IN ('postgres', 'template0', 'template1')
|
|
612
|
+
`;
|
|
613
|
+
|
|
614
|
+
for (const row of result) {
|
|
615
|
+
this.createdDatabases.add(row.datname);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
this.logger.info({
|
|
619
|
+
databases: Array.from(this.createdDatabases),
|
|
620
|
+
count: this.createdDatabases.size
|
|
621
|
+
}, 'Loaded existing databases from persistent storage');
|
|
622
|
+
} catch (error) {
|
|
623
|
+
// Non-fatal - if we can't load existing DBs, createDatabase will handle it
|
|
624
|
+
this.logger.warn({ error: error.message }, 'Failed to load existing databases');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
410
628
|
/**
|
|
411
629
|
* Start the PostgreSQL server process
|
|
412
630
|
* Uses Bun.spawn() for ~40% faster process startup
|
|
@@ -449,8 +667,24 @@ export class PostgresManager {
|
|
|
449
667
|
|
|
450
668
|
let started = false;
|
|
451
669
|
let startupOutput = '';
|
|
670
|
+
let processExited = false;
|
|
671
|
+
let portBindingSeen = false;
|
|
672
|
+
const portStr = this.port.toString();
|
|
673
|
+
|
|
674
|
+
// Hybrid startup detection:
|
|
675
|
+
// 1. TCP connection polling (works on Linux/macOS)
|
|
676
|
+
// 2. Log-based detection (fallback for Windows where Bun.connect may fail)
|
|
677
|
+
// Whichever succeeds first wins
|
|
678
|
+
|
|
679
|
+
const markReady = (method) => {
|
|
680
|
+
if (!started) {
|
|
681
|
+
started = true;
|
|
682
|
+
this.logger.info({ port: this.port, method }, 'PostgreSQL ready');
|
|
683
|
+
resolve();
|
|
684
|
+
}
|
|
685
|
+
};
|
|
452
686
|
|
|
453
|
-
// Read stderr in
|
|
687
|
+
// Read stderr - detect port binding in logs (locale-independent: just look for port number)
|
|
454
688
|
const readStream = async (stream) => {
|
|
455
689
|
const reader = stream.getReader();
|
|
456
690
|
const decoder = new TextDecoder();
|
|
@@ -462,11 +696,16 @@ export class PostgresManager {
|
|
|
462
696
|
startupOutput += message;
|
|
463
697
|
this.logger.debug({ pgOutput: message.trim() }, 'PostgreSQL output');
|
|
464
698
|
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
699
|
+
// Detect port binding - look for our port number in log output
|
|
700
|
+
// This is locale-independent (numbers are universal)
|
|
701
|
+
if (!portBindingSeen && message.includes(portStr)) {
|
|
702
|
+
portBindingSeen = true;
|
|
703
|
+
// Give PostgreSQL 500ms after port binding to finish startup
|
|
704
|
+
setTimeout(() => {
|
|
705
|
+
if (!started && !processExited) {
|
|
706
|
+
markReady('log-port-binding');
|
|
707
|
+
}
|
|
708
|
+
}, 500);
|
|
470
709
|
}
|
|
471
710
|
}
|
|
472
711
|
} catch {
|
|
@@ -480,16 +719,91 @@ export class PostgresManager {
|
|
|
480
719
|
|
|
481
720
|
// Handle process exit
|
|
482
721
|
this.process.exited.then((code) => {
|
|
722
|
+
processExited = true;
|
|
483
723
|
if (!started) {
|
|
484
724
|
reject(new Error(`PostgreSQL exited with code ${code} before starting: ${startupOutput}`));
|
|
485
725
|
}
|
|
486
726
|
this.process = null;
|
|
487
727
|
});
|
|
488
728
|
|
|
489
|
-
//
|
|
729
|
+
// Method 1: TCP connection polling (preferred, works on Linux/macOS)
|
|
730
|
+
const tryConnect = () => {
|
|
731
|
+
return new Promise((resolveConn, rejectConn) => {
|
|
732
|
+
let resolved = false;
|
|
733
|
+
const timeout = setTimeout(() => {
|
|
734
|
+
if (!resolved) {
|
|
735
|
+
resolved = true;
|
|
736
|
+
rejectConn(new Error('Connection timeout'));
|
|
737
|
+
}
|
|
738
|
+
}, 500);
|
|
739
|
+
|
|
740
|
+
Bun.connect({
|
|
741
|
+
hostname: '127.0.0.1',
|
|
742
|
+
port: this.port,
|
|
743
|
+
socket: {
|
|
744
|
+
open(socket) {
|
|
745
|
+
if (!resolved) {
|
|
746
|
+
resolved = true;
|
|
747
|
+
clearTimeout(timeout);
|
|
748
|
+
socket.end();
|
|
749
|
+
resolveConn(true);
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
connectError(_socket, error) {
|
|
753
|
+
if (!resolved) {
|
|
754
|
+
resolved = true;
|
|
755
|
+
clearTimeout(timeout);
|
|
756
|
+
rejectConn(error);
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
error(_socket, error) {
|
|
760
|
+
if (!resolved) {
|
|
761
|
+
resolved = true;
|
|
762
|
+
clearTimeout(timeout);
|
|
763
|
+
rejectConn(error);
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
data() {},
|
|
767
|
+
close() {},
|
|
768
|
+
},
|
|
769
|
+
}).catch((err) => {
|
|
770
|
+
if (!resolved) {
|
|
771
|
+
resolved = true;
|
|
772
|
+
clearTimeout(timeout);
|
|
773
|
+
rejectConn(err);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const pollConnection = async () => {
|
|
780
|
+
const startTime = Date.now();
|
|
781
|
+
const timeoutMs = 30000;
|
|
782
|
+
|
|
783
|
+
while (Date.now() - startTime < timeoutMs && !started) {
|
|
784
|
+
if (processExited) return;
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
await tryConnect();
|
|
788
|
+
markReady('tcp');
|
|
789
|
+
return;
|
|
790
|
+
} catch {
|
|
791
|
+
await Bun.sleep(200);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Start TCP polling (log detection is handled inline above via setTimeout)
|
|
797
|
+
pollConnection();
|
|
798
|
+
|
|
799
|
+
// Overall timeout with helpful error for Windows firewall
|
|
490
800
|
setTimeout(() => {
|
|
491
|
-
if (!started) {
|
|
492
|
-
|
|
801
|
+
if (!started && !processExited) {
|
|
802
|
+
const isWindows = process.platform === 'win32';
|
|
803
|
+
const hint = isWindows
|
|
804
|
+
? '\n\nOn Windows, this may be caused by Windows Firewall blocking localhost connections.\nTry: netsh advfirewall firewall add rule name="pgserve" dir=in action=allow protocol=TCP localport=' + this.port
|
|
805
|
+
: '';
|
|
806
|
+
reject(new Error(`PostgreSQL startup timed out after 30s.${hint}\n\nOutput: ${startupOutput}`));
|
|
493
807
|
}
|
|
494
808
|
}, 30000);
|
|
495
809
|
});
|
|
@@ -543,7 +857,11 @@ export class PostgresManager {
|
|
|
543
857
|
} catch (error) {
|
|
544
858
|
// Database might already exist (from previous persistent session or race condition)
|
|
545
859
|
// 42P04 = duplicate_database, 23505 = unique_violation
|
|
546
|
-
|
|
860
|
+
// Also check error.message for Bun.sql compatibility (may not expose SQLSTATE codes)
|
|
861
|
+
const isAlreadyExists = error.code === '42P04' ||
|
|
862
|
+
error.code === '23505' ||
|
|
863
|
+
error.message?.includes('already exists');
|
|
864
|
+
if (isAlreadyExists) {
|
|
547
865
|
this.createdDatabases.add(dbName);
|
|
548
866
|
this.logger.debug({ dbName }, 'Database already exists');
|
|
549
867
|
} else {
|