pgserve 0.1.5 → 1.0.1

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.
@@ -1,458 +1,275 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * pgserve - Embedded PostgreSQL Server
5
+ *
6
+ * True concurrent connections, zero config, auto-provision databases.
7
+ * Uses embedded-postgres (real PostgreSQL binaries).
8
+ */
9
+
3
10
  import { fileURLToPath } from 'url';
4
11
  import path from 'path';
5
- import {
6
- startServer,
7
- stopServer,
8
- list,
9
- findByDataDir,
10
- findByPort,
11
- portInfo,
12
- cleanup,
13
- startMultiTenantServer
14
- } from '../src/index.js';
12
+ import os from 'os';
13
+ import { startMultiTenantServer } from '../src/index.js';
14
+ import { startClusterServer } from '../src/cluster.js';
15
15
 
16
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
17
 
18
18
  // Global error handlers
19
19
  process.on('unhandledRejection', (reason, promise) => {
20
- // ExitStatus errors are expected from PGlite WASM cleanup - ignore them
21
- if (reason && reason.name === 'ExitStatus') {
22
- return;
23
- }
24
- console.error('❌ Unhandled Promise Rejection:', reason);
25
- // Don't exit - log and continue (PM2 will handle restarts if needed)
20
+ console.error('Unhandled Promise Rejection:', reason);
26
21
  });
27
22
 
28
23
  process.on('uncaughtException', (error) => {
29
- console.error('Uncaught Exception:', error);
24
+ console.error('Uncaught Exception:', error);
30
25
  process.exit(1);
31
26
  });
32
27
 
33
28
  // Parse CLI arguments
34
29
  const args = process.argv.slice(2);
35
30
 
36
- // Default to router mode if first arg is a flag (e.g., --port) or no args
37
- let command = args[0];
38
- if (command?.startsWith('--') || command === undefined) {
39
- command = 'router';
40
- // Don't modify args - router will parse them
41
- }
42
-
43
31
  /**
44
32
  * Print usage help
45
33
  */
46
34
  function printHelp() {
47
35
  console.log(`
48
- ╔═══════════════════════════════════════════════════════════════════╗
49
- ║ pgserve - Multi-Tenant PostgreSQL Router using PGlite ║
50
- ╚═══════════════════════════════════════════════════════════════════╝
51
-
52
- USAGE:
53
- pgserve <command> [options]
54
-
55
- COMMANDS:
56
- 🚀 MULTI-TENANT MODE (Recommended):
57
-
58
- router Start multi-tenant router (single port, auto-provision)
59
- --port <number> PostgreSQL port (default: 8432)
60
- --data <path> Base directory for databases (enables persistence)
61
- --max <number> Max concurrent databases (default: 100)
62
- --log <level> Log level: error, warn, info, debug (default: info)
63
- --no-provision Disable auto-provisioning
64
-
65
- 📦 LEGACY MODE (Single instance):
66
-
67
- start <dataDir> Start server for specific data directory
68
- --port <number> Use specific port (default: auto-allocate 12000-12999)
69
- --log <level> Log level (default: info)
70
-
71
- stop <dataDir> Stop server by data directory
72
- stop --port <number> Stop server by port
73
- stop --all Stop all running instances
74
-
75
- list List all running instances
76
-
77
- url <dataDir> Get connection URL for instance
78
-
79
- health <dataDir> Check health of instance
80
- health --port <number> Check health by port
81
-
82
- cleanup Remove stale instances from registry
36
+ pgserve - Embedded PostgreSQL Server
37
+ =====================================
83
38
 
84
- info Show port range and system info
39
+ True concurrent connections, zero config, auto-provision databases.
85
40
 
86
- help Show this help message
41
+ USAGE:
42
+ pgserve [options]
43
+
44
+ OPTIONS:
45
+ --port <number> PostgreSQL port (default: 5432)
46
+ --data <path> Data directory for persistence (default: in-memory)
47
+ --host <host> Host to bind to (default: 127.0.0.1)
48
+ --log <level> Log level: error, warn, info, debug (default: info)
49
+ --cluster Force cluster mode (auto-enabled on multi-core systems)
50
+ --no-cluster Force single-process mode (disables auto-cluster)
51
+ --workers <n> Number of worker processes (default: CPU cores)
52
+ --no-provision Disable auto-provisioning of databases
53
+ --sync-to <url> Sync to real PostgreSQL (async replication)
54
+ --sync-databases Database patterns to sync (comma-separated, e.g. "myapp,tenant_*")
55
+ --help Show this help message
56
+
57
+ MODES:
58
+ In-memory (default): Fast, ephemeral - data lost on restart
59
+ Persistent: Use --data to persist databases to disk
87
60
 
88
61
  EXAMPLES:
89
- 🚀 Multi-tenant mode (RECOMMENDED):
90
-
91
- # Start router (in-memory mode, default)
62
+ # Start in memory mode (default, fast, ephemeral)
92
63
  pgserve
93
64
 
94
65
  # Start with persistent storage
95
66
  pgserve --data ./data
96
67
 
97
- # Custom port with persistence
98
- pgserve --port 8433 --data /var/lib/pglite
99
-
100
- # Connect clients:
101
- # postgresql://localhost:8432/user123 → in-memory db "user123"
102
- # postgresql://localhost:8432/app456 → in-memory db "app456"
68
+ # Custom port
69
+ pgserve --port 5433
103
70
 
104
- 📦 Legacy mode:
71
+ # Sync to real PostgreSQL (async replication)
72
+ pgserve --sync-to "postgresql://user:pass@host:5432/db"
105
73
 
106
- # Start single instance
107
- pgserve start ./data/my-db --port 12000
74
+ # Sync specific databases
75
+ pgserve --sync-to "postgresql://..." --sync-databases "myapp,tenant_*"
108
76
 
109
- # List instances
110
- pgserve list
111
-
112
- # Stop all
113
- pgserve stop --all
77
+ CONNECTING:
78
+ # Any PostgreSQL client works (psql, pg, Prisma, etc.)
79
+ postgresql://localhost:5432/mydb # Auto-creates "mydb" database
80
+ postgresql://localhost:5432/app123 # Auto-creates "app123" database
114
81
 
82
+ FEATURES:
83
+ - TRUE concurrent connections (native PostgreSQL)
84
+ - Auto-provision databases on first connection
85
+ - Zero configuration required
86
+ - PostgreSQL 17 (native binaries, auto-downloaded)
115
87
  `);
116
88
  }
117
89
 
118
90
  /**
119
- * Format uptime
120
- */
121
- function formatUptime(started) {
122
- const now = new Date();
123
- const start = new Date(started);
124
- const diff = now - start;
125
-
126
- const hours = Math.floor(diff / (1000 * 60 * 60));
127
- const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
128
-
129
- if (hours > 0) {
130
- return `${hours}h ${minutes}m`;
131
- }
132
- return `${minutes}m`;
133
- }
134
-
135
- /**
136
- * Command: start
137
- */
138
- async function cmdStart() {
139
- const dataDir = args[1];
140
-
141
- if (!dataDir) {
142
- console.error('❌ Error: Data directory required');
143
- console.log('Usage: pglite-server start <dataDir> [--port <number>]');
144
- process.exit(1);
145
- }
146
-
147
- const portIndex = args.indexOf('--port');
148
- const port = portIndex >= 0 ? parseInt(args[portIndex + 1], 10) : null;
149
-
150
- const logIndex = args.indexOf('--log');
151
- const logLevel = logIndex >= 0 ? args[logIndex + 1] : 'info';
152
-
153
- try {
154
- const instance = await startServer({ dataDir, port, logLevel });
155
-
156
- console.log(`\n✅ Server started successfully`);
157
- console.log(`📍 Connection: ${instance.connectionUrl}`);
158
- console.log(`📁 Data: ${instance.dataDir}`);
159
- console.log(`🔌 Port: ${instance.port}`);
160
- console.log(`🆔 PID: ${instance.pid}`);
161
- console.log(`\nPress Ctrl+C to stop\n`);
162
-
163
- // Keep process alive
164
- await new Promise(() => {});
165
- } catch (error) {
166
- console.error(`❌ Failed to start server: ${error.message}`);
167
- process.exit(1);
168
- }
169
- }
170
-
171
- /**
172
- * Command: stop
173
- */
174
- async function cmdStop() {
175
- const stopAll = args.includes('--all');
176
-
177
- if (stopAll) {
178
- const instances = list();
179
-
180
- if (instances.length === 0) {
181
- console.log('ℹ️ No running instances');
182
- return;
183
- }
184
-
185
- console.log(`🛑 Stopping ${instances.length} instances...`);
186
-
187
- for (const instance of instances) {
188
- try {
189
- await stopServer({ dataDir: instance.dataDir });
190
- } catch (error) {
191
- console.error(`⚠️ Failed to stop ${instance.dataDir}: ${error.message}`);
192
- }
193
- }
194
-
195
- console.log(`✅ Stopped ${instances.length} instances`);
196
- return;
197
- }
198
-
199
- const portIndex = args.indexOf('--port');
200
-
201
- if (portIndex >= 0) {
202
- const port = parseInt(args[portIndex + 1], 10);
203
- try {
204
- await stopServer({ port });
205
- } catch (error) {
206
- console.error(`❌ ${error.message}`);
207
- process.exit(1);
208
- }
209
- return;
210
- }
211
-
212
- const dataDir = args[1];
213
-
214
- if (!dataDir) {
215
- console.error('❌ Error: Data directory or --port required');
216
- console.log('Usage: pglite-server stop <dataDir> | --port <number> | --all');
217
- process.exit(1);
218
- }
219
-
220
- try {
221
- await stopServer({ dataDir });
222
- } catch (error) {
223
- console.error(`❌ ${error.message}`);
224
- process.exit(1);
225
- }
226
- }
227
-
228
- /**
229
- * Command: list
230
- */
231
- function cmdList() {
232
- const instances = list();
233
-
234
- if (instances.length === 0) {
235
- console.log('ℹ️ No running instances');
236
- return;
237
- }
238
-
239
- console.log('\nActive Instances:');
240
- console.log('┌────────┬─────────────────────────────────────────────┬────────┬─────────────┐');
241
- console.log('│ Port │ Data Directory │ PID │ Uptime │');
242
- console.log('├────────┼─────────────────────────────────────────────┼────────┼─────────────┤');
243
-
244
- for (const instance of instances) {
245
- const dataDir = instance.dataDir.length > 39
246
- ? '...' + instance.dataDir.slice(-36)
247
- : instance.dataDir;
248
-
249
- console.log(
250
- `│ ${String(instance.port).padEnd(6)} │ ${dataDir.padEnd(43)} │ ${String(instance.pid).padEnd(6)} │ ${formatUptime(instance.started).padEnd(11)} │`
251
- );
252
- }
253
-
254
- console.log('└────────┴─────────────────────────────────────────────┴────────┴─────────────┘');
255
- console.log(`\nTotal: ${instances.length} instances`);
256
-
257
- const info = portInfo();
258
- console.log(`Port range: ${info.start}-${info.end} (${info.used}/${info.total} used)\n`);
259
- }
260
-
261
- /**
262
- * Command: url
91
+ * Parse command line arguments
263
92
  */
264
- function cmdUrl() {
265
- const dataDir = args[1];
266
-
267
- if (!dataDir) {
268
- console.error('❌ Error: Data directory required');
269
- console.log('Usage: pglite-server url <dataDir>');
270
- process.exit(1);
271
- }
272
-
273
- const instance = findByDataDir(dataDir);
274
-
275
- if (!instance) {
276
- console.error(`❌ No running instance found for ${dataDir}`);
277
- process.exit(1);
278
- }
279
-
280
- console.log(`postgresql://localhost:${instance.port}`);
281
- }
282
-
283
- /**
284
- * Command: health
285
- */
286
- function cmdHealth() {
287
- const portIndex = args.indexOf('--port');
288
- let instance;
289
-
290
- if (portIndex >= 0) {
291
- const port = parseInt(args[portIndex + 1], 10);
292
- instance = findByPort(port);
293
- } else {
294
- const dataDir = args[1];
295
- if (!dataDir) {
296
- console.error('❌ Error: Data directory or --port required');
297
- console.log('Usage: pglite-server health <dataDir> | --port <number>');
298
- process.exit(1);
93
+ function parseArgs() {
94
+ // Auto-enable cluster mode on multi-core systems for best performance
95
+ const cpuCount = os.cpus().length;
96
+
97
+ const options = {
98
+ port: 5432,
99
+ host: '127.0.0.1',
100
+ dataDir: null, // null = memory mode
101
+ logLevel: 'info',
102
+ autoProvision: true,
103
+ cluster: cpuCount > 1, // Auto-enable on multi-core (use --no-cluster to disable)
104
+ workers: null, // null = use CPU count
105
+ syncTo: null, // Sync target PostgreSQL URL
106
+ syncDatabases: null // Database patterns to sync (comma-separated)
107
+ };
108
+
109
+ for (let i = 0; i < args.length; i++) {
110
+ const arg = args[i];
111
+
112
+ switch (arg) {
113
+ case '--port':
114
+ case '-p':
115
+ options.port = parseInt(args[++i], 10);
116
+ break;
117
+
118
+ case '--data':
119
+ case '-d':
120
+ options.dataDir = args[++i];
121
+ break;
122
+
123
+ case '--host':
124
+ case '-h':
125
+ options.host = args[++i];
126
+ break;
127
+
128
+ case '--log':
129
+ case '-l':
130
+ options.logLevel = args[++i];
131
+ break;
132
+
133
+ case '--cluster':
134
+ options.cluster = true;
135
+ break;
136
+
137
+ case '--no-cluster':
138
+ options.cluster = false;
139
+ break;
140
+
141
+ case '--workers':
142
+ options.workers = parseInt(args[++i], 10);
143
+ break;
144
+
145
+ case '--no-provision':
146
+ options.autoProvision = false;
147
+ break;
148
+
149
+ case '--sync-to':
150
+ options.syncTo = args[++i];
151
+ break;
152
+
153
+ case '--sync-databases':
154
+ options.syncDatabases = args[++i];
155
+ break;
156
+
157
+ case '--help':
158
+ case 'help':
159
+ printHelp();
160
+ process.exit(0);
161
+
162
+ default:
163
+ if (arg.startsWith('-')) {
164
+ console.error(`Unknown option: ${arg}`);
165
+ printHelp();
166
+ process.exit(1);
167
+ }
299
168
  }
300
- instance = findByDataDir(dataDir);
301
- }
302
-
303
- if (!instance) {
304
- console.error('❌ No running instance found');
305
- process.exit(1);
306
169
  }
307
170
 
308
- console.log(`\n✅ Instance healthy`);
309
- console.log(`📍 URL: postgresql://localhost:${instance.port}`);
310
- console.log(`📁 Data: ${instance.dataDir}`);
311
- console.log(`🔌 Port: ${instance.port}`);
312
- console.log(`🆔 PID: ${instance.pid}`);
313
- console.log(`⏱️ Uptime: ${formatUptime(instance.started)}`);
314
- console.log(`📊 Version: ${instance.version}\n`);
171
+ return options;
315
172
  }
316
173
 
317
174
  /**
318
- * Command: info
175
+ * Main entry point
319
176
  */
320
- function cmdInfo() {
321
- const info = portInfo();
322
-
323
- console.log('\n📊 Port Range Information:');
324
- console.log(` Range: ${info.start}-${info.end}`);
325
- console.log(` Total: ${info.total} ports`);
326
- console.log(` Used: ${info.used} ports`);
327
- console.log(` Available: ${info.available} ports`);
328
-
329
- if (info.usedPorts.length > 0) {
330
- console.log(` Active ports: ${info.usedPorts.join(', ')}`);
331
- }
332
-
333
- console.log('');
334
- }
335
-
336
- /**
337
- * Command: cleanup
338
- */
339
- function cmdCleanup() {
340
- const cleaned = cleanup();
341
-
342
- if (cleaned === 0) {
343
- console.log('✅ No stale instances to clean up');
344
- } else {
345
- console.log(`✅ Cleaned up ${cleaned} stale instance${cleaned > 1 ? 's' : ''}`);
346
- }
347
- }
348
-
349
- /**
350
- * Command: router (multi-tenant mode)
351
- */
352
- async function cmdRouter() {
353
- // Parse options
354
- const portIndex = args.indexOf('--port');
355
- const port = portIndex >= 0 ? parseInt(args[portIndex + 1], 10) : 8432;
356
-
357
- const dataIndex = args.indexOf('--data');
358
- const dataDir = dataIndex >= 0 ? args[dataIndex + 1] : null;
359
- const memoryMode = dataDir === null;
360
-
361
- const maxIndex = args.indexOf('--max');
362
- const maxInstances = maxIndex >= 0 ? parseInt(args[maxIndex + 1], 10) : 100;
363
-
364
- const logIndex = args.indexOf('--log');
365
- const logLevel = logIndex >= 0 ? args[logIndex + 1] : 'info';
366
- const autoProvision = !args.includes('--no-provision');
177
+ async function main() {
178
+ const options = parseArgs();
179
+ const memoryMode = !options.dataDir;
367
180
 
368
- try {
369
- console.log(`
370
- ╔═══════════════════════════════════════════════════════════════════╗
371
- ║ Starting Multi-Tenant PostgreSQL Router ║
372
- ╚═══════════════════════════════════════════════════════════════════╝
181
+ console.log(`
182
+ pgserve - Embedded PostgreSQL Server
183
+ =====================================
373
184
  `);
374
185
 
375
- const router = await startMultiTenantServer({
376
- port,
377
- baseDir: memoryMode ? null : dataDir,
378
- memoryMode,
379
- maxInstances,
380
- logLevel,
381
- autoProvision
382
- });
383
-
384
- console.log(`
385
- Multi-tenant router started successfully!
386
-
387
- 📍 PostgreSQL endpoint: postgresql://localhost:${port}/<database>
388
- 📁 Data directory: ${memoryMode ? '(in-memory)' : dataDir}
389
- 🎯 Auto-provision: ${autoProvision ? 'enabled' : 'disabled'}
390
- 💾 Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'}
391
- 📊 Max instances: ${maxInstances}
186
+ try {
187
+ let server;
188
+
189
+ if (options.cluster) {
190
+ // Cluster mode - multi-core scaling
191
+ server = await startClusterServer({
192
+ port: options.port,
193
+ host: options.host,
194
+ baseDir: options.dataDir,
195
+ logLevel: options.logLevel,
196
+ autoProvision: options.autoProvision,
197
+ workers: options.workers
198
+ });
199
+
200
+ // Only primary process shows full startup message
201
+ if (server.workers) {
202
+ const stats = server.getStats();
203
+ console.log(`
204
+ Cluster started successfully!
205
+
206
+ Endpoint: postgresql://${options.host}:${options.port}/<database>
207
+ Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'} (Cluster)
208
+ Workers: ${stats.workers} processes
209
+ Data: ${memoryMode ? '(temp directory)' : options.dataDir}
210
+ Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
211
+
212
+ Examples:
213
+ postgresql://${options.host}:${options.port}/myapp
214
+ postgresql://${options.host}:${options.port}/testdb
392
215
 
393
- 💡 Examples:
394
- postgresql://localhost:${port}/user123 → ${memoryMode ? 'Creates in-memory database' : `Creates ${dataDir}/user123/`}
395
- postgresql://localhost:${port}/app456 → ${memoryMode ? 'Creates in-memory database' : `Creates ${dataDir}/app456/`}
216
+ Press Ctrl+C to stop
217
+ `);
218
+ }
219
+ } else {
220
+ // Single process mode
221
+ const router = await startMultiTenantServer({
222
+ port: options.port,
223
+ host: options.host,
224
+ baseDir: options.dataDir,
225
+ logLevel: options.logLevel,
226
+ autoProvision: options.autoProvision,
227
+ syncTo: options.syncTo,
228
+ syncDatabases: options.syncDatabases
229
+ });
230
+
231
+ server = router;
232
+
233
+ // Build sync status string
234
+ const syncStatus = options.syncTo
235
+ ? `Enabled → ${options.syncTo.replace(/:[^:@]+@/, ':***@')}`
236
+ : 'Disabled';
237
+
238
+ console.log(`
239
+ Server started successfully!
240
+
241
+ Endpoint: postgresql://${options.host}:${options.port}/<database>
242
+ Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'}
243
+ Data: ${memoryMode ? '(temp directory)' : options.dataDir}
244
+ PostgreSQL: Port ${router.pgPort} (internal)
245
+ Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
246
+ Sync: ${syncStatus}${options.syncDatabases ? ` (${options.syncDatabases})` : ''}
247
+
248
+ Examples:
249
+ postgresql://${options.host}:${options.port}/myapp
250
+ postgresql://${options.host}:${options.port}/testdb
396
251
 
397
252
  Press Ctrl+C to stop
398
253
  `);
254
+ }
255
+
256
+ // Graceful shutdown
257
+ const shutdown = async () => {
258
+ console.log('\nShutting down...');
259
+ await server.stop();
260
+ console.log('Server stopped.');
261
+ process.exit(0);
262
+ };
263
+
264
+ process.on('SIGINT', shutdown);
265
+ process.on('SIGTERM', shutdown);
399
266
 
400
267
  // Keep process alive
401
268
  await new Promise(() => {});
402
269
  } catch (error) {
403
- console.error(`❌ Failed to start router: ${error.message}`);
270
+ console.error(`Failed to start server:`, error);
404
271
  process.exit(1);
405
272
  }
406
273
  }
407
274
 
408
- // Main CLI router
409
- async function main() {
410
- switch (command) {
411
- case 'router':
412
- await cmdRouter();
413
- break;
414
-
415
- case 'start':
416
- await cmdStart();
417
- break;
418
-
419
- case 'stop':
420
- await cmdStop();
421
- break;
422
-
423
- case 'list':
424
- cmdList();
425
- break;
426
-
427
- case 'url':
428
- cmdUrl();
429
- break;
430
-
431
- case 'health':
432
- cmdHealth();
433
- break;
434
-
435
- case 'info':
436
- cmdInfo();
437
- break;
438
-
439
- case 'cleanup':
440
- cmdCleanup();
441
- break;
442
-
443
- case 'help':
444
- case undefined:
445
- printHelp();
446
- break;
447
-
448
- default:
449
- console.error(`❌ Unknown command: ${command}`);
450
- printHelp();
451
- process.exit(1);
452
- }
453
- }
454
-
455
- main().catch((error) => {
456
- console.error(`❌ Error: ${error.message}`);
457
- process.exit(1);
458
- });
275
+ main();