pgserve 1.1.2 → 1.1.3-rc.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.
@@ -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
 
@@ -53,6 +56,8 @@ if (!bunPath) {
53
56
  const scriptPath = path.join(__dirname, 'pglite-server.js');
54
57
 
55
58
  // Spawn bun with the actual script, inherit all stdio
59
+ // IMPORTANT: Do NOT use detached mode - wrapper must wait for child to fully terminate
60
+ // Using detached with stdio:'inherit' causes file handle inheritance issues on Windows
56
61
  const child = spawn(bunPath, [scriptPath, ...process.argv.slice(2)], {
57
62
  stdio: 'inherit',
58
63
  windowsHide: true
@@ -63,19 +68,85 @@ child.on('error', (err) => {
63
68
  process.exit(1);
64
69
  });
65
70
 
66
- child.on('exit', (code, signal) => {
71
+ // Safety timeout: force exit if 'close' event never fires after 'exit'
72
+ let forceExitTimeout = null;
73
+
74
+ child.on('exit', () => {
75
+ // Give 5 seconds for 'close' event after 'exit'
76
+ forceExitTimeout = setTimeout(() => {
77
+ console.error('Warning: Child process did not close cleanly, forcing exit');
78
+ process.exit(1);
79
+ }, 5000);
80
+ });
81
+
82
+ // Use 'close' event instead of 'exit' - fires AFTER all stdio streams are closed
83
+ // This is critical for Windows where file handles may remain locked after 'exit' fires
84
+ child.on('close', (code, signal) => {
85
+ // Clear the safety timeout
86
+ if (forceExitTimeout) {
87
+ clearTimeout(forceExitTimeout);
88
+ }
89
+
90
+ // On Windows, use SYNCHRONOUS delay to ensure all file handles are released
91
+ // This prevents EBUSY errors when npx tries to clean up the cache
92
+ // NOTE: async/await does NOT work in EventEmitter callbacks - Node ignores the Promise
93
+ if (isWindows) {
94
+ const delay = 200; // ms - enough for Windows kernel to release handles
95
+ const start = Date.now();
96
+ while (Date.now() - start < delay) {
97
+ // Synchronous busy-wait - actually blocks unlike async setTimeout
98
+ }
99
+ }
100
+
67
101
  if (signal) {
68
- process.kill(process.pid, signal);
102
+ // On Windows, can't reliably re-raise Unix signals
103
+ if (isWindows) {
104
+ process.exit(1);
105
+ } else {
106
+ process.kill(process.pid, signal);
107
+ }
69
108
  } else {
70
109
  process.exit(code ?? 0);
71
110
  }
72
111
  });
73
112
 
74
- // Forward signals to child process
75
- ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
76
- process.on(signal, () => {
113
+ // Platform-specific signal handling
114
+ if (isWindows) {
115
+ // Windows: use taskkill for reliable process termination
116
+ // process.kill(pid, 'SIGINT') does NOT work properly on Windows
117
+ process.on('SIGINT', () => {
77
118
  if (child.pid) {
78
- process.kill(child.pid, signal);
119
+ try {
120
+ // /T = terminate child processes (tree), /F = force
121
+ execSync(`taskkill /PID ${child.pid} /T /F`, {
122
+ stdio: 'ignore',
123
+ windowsHide: true
124
+ });
125
+ } catch {
126
+ // Process may have already exited, ignore errors
127
+ }
79
128
  }
80
129
  });
81
- });
130
+
131
+ // Handle Ctrl+C via readline for Windows terminal compatibility
132
+ // Some Windows terminals don't emit SIGINT properly
133
+ const readline = require('readline');
134
+ if (process.stdin.isTTY) {
135
+ const rl = readline.createInterface({
136
+ input: process.stdin,
137
+ output: process.stdout
138
+ });
139
+ rl.on('SIGINT', () => {
140
+ process.emit('SIGINT');
141
+ });
142
+ }
143
+ } else {
144
+ // Unix: forward signals to child process normally
145
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
146
+ process.on(signal, () => {
147
+ if (child.pid) {
148
+ process.kill(child.pid, signal);
149
+ }
150
+ });
151
+ });
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.2",
3
+ "version": "1.1.3-rc.2",
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/postgres.js CHANGED
@@ -308,6 +308,12 @@ export class PostgresManager {
308
308
  // Initialize admin connection pool (for database creation operations)
309
309
  await this._initAdminPool();
310
310
 
311
+ // For persistent mode, load existing databases into createdDatabases
312
+ // This prevents "database already exists" errors when reusing data directories
313
+ if (this.persistent) {
314
+ await this._loadExistingDatabases();
315
+ }
316
+
311
317
  this.logger.info({
312
318
  databaseDir: this.databaseDir,
313
319
  port: this.port,
@@ -407,6 +413,33 @@ export class PostgresManager {
407
413
  }, 'Admin connection pool initialized (Bun.sql)');
408
414
  }
409
415
 
416
+ /**
417
+ * Load existing databases into createdDatabases Set (for persistent mode)
418
+ * This allows pgserve to reuse existing data directories without
419
+ * attempting to CREATE DATABASE for databases that already exist.
420
+ */
421
+ async _loadExistingDatabases() {
422
+ try {
423
+ const result = await this.adminPool`
424
+ SELECT datname FROM pg_database
425
+ WHERE datistemplate = false
426
+ AND datname NOT IN ('postgres', 'template0', 'template1')
427
+ `;
428
+
429
+ for (const row of result) {
430
+ this.createdDatabases.add(row.datname);
431
+ }
432
+
433
+ this.logger.info({
434
+ databases: Array.from(this.createdDatabases),
435
+ count: this.createdDatabases.size
436
+ }, 'Loaded existing databases from persistent storage');
437
+ } catch (error) {
438
+ // Non-fatal - if we can't load existing DBs, createDatabase will handle it
439
+ this.logger.warn({ error: error.message }, 'Failed to load existing databases');
440
+ }
441
+ }
442
+
410
443
  /**
411
444
  * Start the PostgreSQL server process
412
445
  * Uses Bun.spawn() for ~40% faster process startup
@@ -543,7 +576,11 @@ export class PostgresManager {
543
576
  } catch (error) {
544
577
  // Database might already exist (from previous persistent session or race condition)
545
578
  // 42P04 = duplicate_database, 23505 = unique_violation
546
- if (error.code === '42P04' || error.code === '23505') {
579
+ // Also check error.message for Bun.sql compatibility (may not expose SQLSTATE codes)
580
+ const isAlreadyExists = error.code === '42P04' ||
581
+ error.code === '23505' ||
582
+ error.message?.includes('already exists');
583
+ if (isAlreadyExists) {
547
584
  this.createdDatabases.add(dbName);
548
585
  this.logger.debug({ dbName }, 'Database already exists');
549
586
  } else {