pgserve 1.1.3-rc.1 → 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,42 +56,97 @@ 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
- windowsHide: true,
59
- // Detach on Windows to prevent handle inheritance and EBUSY errors during npx cleanup
60
- detached: isWindows
63
+ windowsHide: true
61
64
  });
62
65
 
63
- // On Windows, unreference the child to allow wrapper to exit independently
64
- if (isWindows) {
65
- child.unref();
66
- }
67
-
68
66
  child.on('error', (err) => {
69
67
  console.error('Failed to start pgserve:', err.message);
70
68
  process.exit(1);
71
69
  });
72
70
 
73
- child.on('exit', async (code, signal) => {
74
- // On Windows, wait briefly for all file handles to be released
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
75
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
76
93
  if (isWindows) {
77
- await new Promise(resolve => setTimeout(resolve, 100));
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
+ }
78
99
  }
79
100
 
80
101
  if (signal) {
81
- 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
+ }
82
108
  } else {
83
109
  process.exit(code ?? 0);
84
110
  }
85
111
  });
86
112
 
87
- // Forward signals to child process
88
- ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach(signal => {
89
- 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', () => {
90
118
  if (child.pid) {
91
- 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
+ }
92
128
  }
93
129
  });
94
- });
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.3-rc.1",
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 {