pgserve 1.1.3-rc.7 → 1.1.3

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.
@@ -0,0 +1,119 @@
1
+ # Windows Debug Context
2
+
3
+ ## RESOLVED (2025-12-12)
4
+
5
+ The Windows router binding issue has been fixed. See "Solution Applied" below.
6
+
7
+ ---
8
+
9
+ ## Original Issue
10
+ PostgreSQL starts successfully on port 9432, but the router/proxy on port 8432 was NOT listening.
11
+ Clients could not connect because 8432 wasn't bound.
12
+
13
+ ## Root Causes Found
14
+
15
+ ### 1. `reusePort: true` - Linux-only feature (PRIMARY CAUSE)
16
+ **Location:** `src/cluster.js:75`
17
+
18
+ The cluster mode uses `Bun.listen({ reusePort: true })` which maps to `SO_REUSEPORT`.
19
+ This socket option is Linux-only and **silently fails on Windows**.
20
+
21
+ ### 2. Auto-enabled cluster mode on multi-core Windows systems
22
+ **Location:** `bin/pglite-server.js:106`
23
+
24
+ Windows systems with multiple CPU cores automatically entered cluster mode,
25
+ triggering the `reusePort` failure.
26
+
27
+ ### 3. TCP port opens before PostgreSQL ready for protocol handshakes
28
+ **Location:** `src/postgres.js:803-821`
29
+
30
+ PostgreSQL was marked "ready" when TCP port opened, but it wasn't actually
31
+ ready for protocol-level handshakes. Admin pool connection timed out.
32
+
33
+ ---
34
+
35
+ ## Solution Applied
36
+
37
+ ### Fix 1: Disable cluster mode on Windows
38
+ **File:** `bin/pglite-server.js:98-108`
39
+ ```javascript
40
+ const isWindows = os.platform() === 'win32';
41
+ cluster: cpuCount > 1 && !isWindows,
42
+ ```
43
+
44
+ ### Fix 2: Add platform check to reusePort
45
+ **File:** `src/cluster.js:72-76`
46
+ ```javascript
47
+ const isWindows = os.platform() === 'win32';
48
+ this.server = Bun.listen({
49
+ reusePort: !isWindows, // SO_REUSEPORT only works on Linux/macOS
50
+ ...
51
+ });
52
+ ```
53
+
54
+ ### Fix 3: Add port binding verification
55
+ **File:** `src/cluster.js:99-102`
56
+ ```javascript
57
+ if (!this.server || !this.server.port) {
58
+ throw new Error(`Failed to bind to port ${this.port} - reusePort may not be supported`);
59
+ }
60
+ ```
61
+
62
+ ### Fix 4: Add Windows readiness delay
63
+ **File:** `src/postgres.js:813-817`
64
+ ```javascript
65
+ if (isWindows) {
66
+ await Bun.sleep(2000); // 2 second delay for Windows
67
+ }
68
+ ```
69
+
70
+ ### Fix 5: Increase admin pool retry for Windows
71
+ **File:** `src/postgres.js:598-599`
72
+ ```javascript
73
+ const maxRetries = isWindows ? 10 : 5;
74
+ const baseDelay = isWindows ? 2000 : 1000;
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Verification
80
+
81
+ After applying fixes, server runs correctly on Windows:
82
+ - Router: `127.0.0.1:7432` ✅
83
+ - PostgreSQL: `127.0.0.1:8432` ✅
84
+ - Database auto-provisioning: ✅
85
+ - Query execution: ✅
86
+
87
+ ```
88
+ Server started successfully!
89
+
90
+ Endpoint: postgresql://127.0.0.1:7432/<database>
91
+ Mode: In-memory (ephemeral)
92
+ PostgreSQL: Port 8432 (internal)
93
+ Auto-create: Enabled
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Test Commands (Run from Windows)
99
+ ```cmd
100
+ # Build binary
101
+ bun build --compile bin/pglite-server.js --outfile dist/pgserve-windows-x64.exe
102
+
103
+ # Start server
104
+ dist\pgserve-windows-x64.exe
105
+
106
+ # Check ports are listening (should see BOTH)
107
+ netstat -an | findstr "LISTEN" | findstr "8432 9432"
108
+
109
+ # Test connection
110
+ psql postgresql://localhost:8432/testdb
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Related Files
116
+ - `bin/pglite-server.js` - Entry point, cluster mode decision
117
+ - `src/cluster.js` - Cluster mode with reusePort
118
+ - `src/router.js` - Single-process mode (works on Windows)
119
+ - `src/postgres.js` - PostgreSQL startup and admin pool
@@ -70,21 +70,34 @@ jobs:
70
70
  - name: Install dependencies
71
71
  run: bun install
72
72
 
73
- # Windows: native build with custom icon
74
- - name: Build for Windows (with icon)
73
+ # Windows: native build with icon and proper metadata
74
+ - name: Build for Windows (with branding)
75
75
  if: matrix.platform == 'windows-x64'
76
76
  run: |
77
- mkdir -p dist
78
- bun build --compile --windows-icon=assets/icon.ico bin/pglite-server.js --outfile dist/${{ matrix.output }}
79
- ls -lh dist/
80
- shell: bash
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
+ --define BUILD_VERSION="'$RAW_VERSION'" `
84
+ --windows-icon=assets/icon.ico `
85
+ --windows-title="pgserve" `
86
+ --windows-publisher="Namastex Labs" `
87
+ --windows-description="Embedded PostgreSQL Server - Zero config, auto-provision, unlimited connections" `
88
+ --windows-version="$WIN_VERSION" `
89
+ --windows-copyright="Copyright (c) 2025 Namastex Labs" `
90
+ bin/pglite-server.js --outfile dist/${{ matrix.output }}
91
+ Get-ChildItem dist/
92
+ shell: pwsh
81
93
 
82
94
  # Linux/macOS: native build
83
95
  - name: Build for ${{ matrix.platform }}
84
96
  if: matrix.platform != 'windows-x64'
85
97
  run: |
86
98
  mkdir -p dist
87
- bun build --compile bin/pglite-server.js --outfile dist/${{ matrix.output }}
99
+ VERSION=$(node -p "require('./package.json').version")
100
+ bun build --compile --define BUILD_VERSION="'$VERSION'" bin/pglite-server.js --outfile dist/${{ matrix.output }}
88
101
  ls -lh dist/
89
102
 
90
103
  - name: Upload artifact
package/assets/icon.ico CHANGED
Binary file
@@ -94,7 +94,9 @@ FEATURES:
94
94
  */
95
95
  function parseArgs() {
96
96
  // Auto-enable cluster mode on multi-core systems for best performance
97
+ // Note: Cluster mode uses SO_REUSEPORT which is not supported on Windows
97
98
  const cpuCount = os.cpus().length;
99
+ const isWindows = os.platform() === 'win32';
98
100
 
99
101
  const options = {
100
102
  port: 8432,
@@ -103,7 +105,7 @@ function parseArgs() {
103
105
  useRam: false, // Use /dev/shm for true RAM storage (Linux only)
104
106
  logLevel: 'info',
105
107
  autoProvision: true,
106
- cluster: cpuCount > 1, // Auto-enable on multi-core (use --no-cluster to disable)
108
+ cluster: cpuCount > 1 && !isWindows, // Auto-enable on multi-core (disabled on Windows - no SO_REUSEPORT)
107
109
  workers: null, // null = use CPU count
108
110
  syncTo: null, // Sync target PostgreSQL URL
109
111
  syncDatabases: null // Database patterns to sync (comma-separated)
package/eslint.config.js CHANGED
@@ -24,6 +24,8 @@ export default [
24
24
  TextDecoder: 'readonly',
25
25
  TextEncoder: 'readonly',
26
26
  Response: 'readonly',
27
+ // Build-time constants (injected via --define)
28
+ BUILD_VERSION: 'readonly',
27
29
  },
28
30
  },
29
31
  rules: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.3-rc.7",
3
+ "version": "1.1.3",
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",
package/src/cluster.js CHANGED
@@ -69,10 +69,11 @@ class ClusterRouter extends EventEmitter {
69
69
 
70
70
  // Create TCP server using Bun.listen() for 2-3x throughput
71
71
  const router = this;
72
+ const isWindows = os.platform() === 'win32';
72
73
  this.server = Bun.listen({
73
74
  hostname: this.host,
74
75
  port: this.port,
75
- reusePort: true, // Enable SO_REUSEPORT for multi-worker port sharing (Linux)
76
+ reusePort: !isWindows, // SO_REUSEPORT for multi-worker port sharing (Linux/macOS only)
76
77
  socket: {
77
78
  data(socket, data) {
78
79
  router.handleSocketData(socket, data);
@@ -95,6 +96,11 @@ class ClusterRouter extends EventEmitter {
95
96
  }
96
97
  });
97
98
 
99
+ // Verify port actually bound (detect silent failures on Windows)
100
+ if (!this.server || !this.server.port) {
101
+ throw new Error(`Failed to bind to port ${this.port} - reusePort may not be supported on this platform`);
102
+ }
103
+
98
104
  this.emit('listening');
99
105
  }
100
106
 
package/src/dashboard.js CHANGED
@@ -13,12 +13,18 @@ import { readFileSync } from 'fs';
13
13
  import { join, dirname } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
15
 
16
- // Get package version
16
+ // Get package version - BUILD_VERSION is injected at compile time via --define
17
+ // Falls back to reading package.json for development mode
17
18
  let version = '1.0.0';
18
19
  try {
19
- const __dirname = dirname(fileURLToPath(import.meta.url));
20
- const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
21
- version = pkg.version;
20
+ // Check for build-time injected version first
21
+ if (typeof BUILD_VERSION !== 'undefined') {
22
+ version = BUILD_VERSION;
23
+ } else {
24
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
26
+ version = pkg.version;
27
+ }
22
28
  } catch {
23
29
  // Fallback version
24
30
  }
package/src/postgres.js CHANGED
@@ -575,9 +575,11 @@ export class PostgresManager {
575
575
  */
576
576
  async _initAdminPool() {
577
577
  const { SQL } = await import('bun');
578
+ const isWindows = process.platform === 'win32';
578
579
 
579
580
  // Bun.sql config - uses TCP connections (Unix sockets not directly supported)
580
581
  // This is fine for admin queries (low volume, local connection)
582
+ // Windows needs longer timeout due to higher network stack latency
581
583
  this.adminPool = new SQL({
582
584
  hostname: '127.0.0.1',
583
585
  port: this.port,
@@ -586,16 +588,38 @@ export class PostgresManager {
586
588
  password: this.password,
587
589
  max: 5, // Small pool - only for CREATE DATABASE operations
588
590
  idleTimeout: 30,
589
- connectionTimeout: 5,
591
+ connectionTimeout: isWindows ? 15 : 5,
590
592
  });
591
593
 
592
- // Verify connection is working with a simple query
593
- await this.adminPool`SELECT 1`;
594
+ // Verify connection with retry logic
595
+ // TCP port being open doesn't mean PostgreSQL protocol is ready
596
+ // This handles the race condition on Windows where the port binds
597
+ // before the server is fully ready to accept protocol handshakes
598
+ const maxRetries = isWindows ? 10 : 5; // More retries on Windows
599
+ const baseDelay = isWindows ? 2000 : 1000; // Longer delay on Windows
594
600
 
595
- this.logger.debug({
596
- host: '127.0.0.1',
597
- maxConnections: 5
598
- }, 'Admin connection pool initialized (Bun.sql)');
601
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
602
+ try {
603
+ await this.adminPool`SELECT 1`;
604
+ this.logger.debug({
605
+ host: '127.0.0.1',
606
+ maxConnections: 5,
607
+ attempt
608
+ }, 'Admin connection pool initialized (Bun.sql)');
609
+ return; // Success
610
+ } catch (err) {
611
+ if (attempt === maxRetries) {
612
+ throw new Error(`Failed to initialize admin pool after ${maxRetries} attempts: ${err.message}`);
613
+ }
614
+ this.logger.debug({
615
+ attempt,
616
+ maxRetries,
617
+ error: err.message
618
+ }, 'Admin pool connection failed, retrying...');
619
+ // Exponential backoff: 1s, 2s, 3s, 4s
620
+ await new Promise(resolve => setTimeout(resolve, baseDelay * attempt));
621
+ }
622
+ }
599
623
  }
600
624
 
601
625
  /**
@@ -667,8 +691,24 @@ export class PostgresManager {
667
691
 
668
692
  let started = false;
669
693
  let startupOutput = '';
694
+ let processExited = false;
695
+ let portBindingSeen = false;
696
+ const portStr = this.port.toString();
697
+
698
+ // Hybrid startup detection:
699
+ // 1. TCP connection polling (works on Linux/macOS)
700
+ // 2. Log-based detection (fallback for Windows where Bun.connect may fail)
701
+ // Whichever succeeds first wins
702
+
703
+ const markReady = (method) => {
704
+ if (!started) {
705
+ started = true;
706
+ this.logger.info({ port: this.port, method }, 'PostgreSQL ready');
707
+ resolve();
708
+ }
709
+ };
670
710
 
671
- // Read stderr in streaming fashion to detect startup
711
+ // Read stderr - detect port binding in logs (locale-independent: just look for port number)
672
712
  const readStream = async (stream) => {
673
713
  const reader = stream.getReader();
674
714
  const decoder = new TextDecoder();
@@ -680,11 +720,16 @@ export class PostgresManager {
680
720
  startupOutput += message;
681
721
  this.logger.debug({ pgOutput: message.trim() }, 'PostgreSQL output');
682
722
 
683
- // Check for ready message
684
- if (!started && (message.includes('database system is ready to accept connections') ||
685
- message.includes('ready to accept connections'))) {
686
- started = true;
687
- resolve();
723
+ // Detect port binding - look for our port number in log output
724
+ // This is locale-independent (numbers are universal)
725
+ if (!portBindingSeen && message.includes(portStr)) {
726
+ portBindingSeen = true;
727
+ // Give PostgreSQL 500ms after port binding to finish startup
728
+ setTimeout(() => {
729
+ if (!started && !processExited) {
730
+ markReady('log-port-binding');
731
+ }
732
+ }, 500);
688
733
  }
689
734
  }
690
735
  } catch {
@@ -698,16 +743,97 @@ export class PostgresManager {
698
743
 
699
744
  // Handle process exit
700
745
  this.process.exited.then((code) => {
746
+ processExited = true;
701
747
  if (!started) {
702
748
  reject(new Error(`PostgreSQL exited with code ${code} before starting: ${startupOutput}`));
703
749
  }
704
750
  this.process = null;
705
751
  });
706
752
 
707
- // Timeout after 30 seconds
753
+ // Method 1: TCP connection polling (preferred, works on Linux/macOS)
754
+ const tryConnect = () => {
755
+ return new Promise((resolveConn, rejectConn) => {
756
+ let resolved = false;
757
+ const timeout = setTimeout(() => {
758
+ if (!resolved) {
759
+ resolved = true;
760
+ rejectConn(new Error('Connection timeout'));
761
+ }
762
+ }, 500);
763
+
764
+ Bun.connect({
765
+ hostname: '127.0.0.1',
766
+ port: this.port,
767
+ socket: {
768
+ open(socket) {
769
+ if (!resolved) {
770
+ resolved = true;
771
+ clearTimeout(timeout);
772
+ socket.end();
773
+ resolveConn(true);
774
+ }
775
+ },
776
+ connectError(_socket, error) {
777
+ if (!resolved) {
778
+ resolved = true;
779
+ clearTimeout(timeout);
780
+ rejectConn(error);
781
+ }
782
+ },
783
+ error(_socket, error) {
784
+ if (!resolved) {
785
+ resolved = true;
786
+ clearTimeout(timeout);
787
+ rejectConn(error);
788
+ }
789
+ },
790
+ data() {},
791
+ close() {},
792
+ },
793
+ }).catch((err) => {
794
+ if (!resolved) {
795
+ resolved = true;
796
+ clearTimeout(timeout);
797
+ rejectConn(err);
798
+ }
799
+ });
800
+ });
801
+ };
802
+
803
+ const pollConnection = async () => {
804
+ const startTime = Date.now();
805
+ const timeoutMs = 30000;
806
+ const isWindows = process.platform === 'win32';
807
+
808
+ while (Date.now() - startTime < timeoutMs && !started) {
809
+ if (processExited) return;
810
+
811
+ try {
812
+ await tryConnect();
813
+ // On Windows, TCP port opens before PostgreSQL is fully ready for protocol handshakes
814
+ // Add delay to let PostgreSQL complete its startup sequence
815
+ if (isWindows) {
816
+ await Bun.sleep(2000); // 2 second delay for Windows
817
+ }
818
+ markReady('tcp');
819
+ return;
820
+ } catch {
821
+ await Bun.sleep(200);
822
+ }
823
+ }
824
+ };
825
+
826
+ // Start TCP polling (log detection is handled inline above via setTimeout)
827
+ pollConnection();
828
+
829
+ // Overall timeout with helpful error for Windows firewall
708
830
  setTimeout(() => {
709
- if (!started) {
710
- reject(new Error(`PostgreSQL startup timed out after 30s. Output: ${startupOutput}`));
831
+ if (!started && !processExited) {
832
+ const isWindows = process.platform === 'win32';
833
+ const hint = isWindows
834
+ ? '\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
835
+ : '';
836
+ reject(new Error(`PostgreSQL startup timed out after 30s.${hint}\n\nOutput: ${startupOutput}`));
711
837
  }
712
838
  }, 30000);
713
839
  });