pgserve 1.1.3 → 1.1.5-rc.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.
package/README.md CHANGED
@@ -72,6 +72,10 @@ psql postgresql://localhost:8432/myapp
72
72
  <td><b>Async Replication</b></td>
73
73
  <td>Sync to real PostgreSQL with minimal overhead</td>
74
74
  </tr>
75
+ <tr>
76
+ <td><b>pgvector Built-in</b></td>
77
+ <td>Use <code>--pgvector</code> for auto-enabled vector similarity search</td>
78
+ </tr>
75
79
  <tr>
76
80
  <td><b>Cross-Platform</b></td>
77
81
  <td>Linux x64, macOS ARM64/x64, Windows x64</td>
@@ -129,6 +133,8 @@ Options:
129
133
  --no-provision Disable auto-provisioning of databases
130
134
  --sync-to <url> Sync to real PostgreSQL (async replication)
131
135
  --sync-databases <p> Database patterns to sync (comma-separated)
136
+ --pgvector Auto-enable pgvector extension on new databases
137
+ --max-connections <n> Max concurrent connections (default: 1000)
132
138
  --help Show help message
133
139
  ```
134
140
 
@@ -148,6 +154,12 @@ pgserve --data /var/lib/pgserve
148
154
  # Custom port
149
155
  pgserve --port 5433
150
156
 
157
+ # Enable pgvector for AI/RAG applications
158
+ pgserve --pgvector
159
+
160
+ # RAM mode + pgvector (fastest for AI workloads)
161
+ pgserve --ram --pgvector
162
+
151
163
  # Sync to production PostgreSQL
152
164
  pgserve --sync-to "postgresql://user:pass@db.example.com:5432/prod"
153
165
  ```
@@ -167,6 +179,7 @@ const server = await startMultiTenantServer({
167
179
  baseDir: null, // null = memory mode
168
180
  logLevel: 'info',
169
181
  autoProvision: true,
182
+ enablePgvector: true, // Auto-enable pgvector on new databases
170
183
  syncTo: null, // Optional: PostgreSQL URL for replication
171
184
  syncDatabases: null // Optional: patterns like "myapp,tenant_*"
172
185
  });
@@ -258,48 +271,188 @@ pgserve --sync-to "postgresql://..." --sync-databases "myapp,tenant_*"
258
271
 
259
272
  <br>
260
273
 
274
+ ## pgvector (Vector Search)
275
+
276
+ pgvector is **built-in** — no separate installation required. Just enable it:
277
+
278
+ ```bash
279
+ # Auto-enable pgvector on all new databases
280
+ pgserve --pgvector
281
+
282
+ # Combined with RAM mode for fastest vector operations
283
+ pgserve --ram --pgvector
284
+ ```
285
+
286
+ When `--pgvector` is enabled, every new database automatically has the vector extension installed. No SQL setup required.
287
+
288
+ <details>
289
+ <summary><b>Using pgvector</b></summary>
290
+
291
+ ```sql
292
+ -- Create table with vector column (1536 = OpenAI embedding size)
293
+ CREATE TABLE documents (id SERIAL, content TEXT, embedding vector(1536));
294
+
295
+ -- Insert with embedding
296
+ INSERT INTO documents (content, embedding) VALUES ('Hello', '[0.1, 0.2, ...]');
297
+
298
+ -- k-NN similarity search (L2 distance)
299
+ SELECT content FROM documents ORDER BY embedding <-> $1 LIMIT 10;
300
+ ```
301
+
302
+ See [pgvector documentation](https://github.com/pgvector/pgvector) for full API reference.
303
+ </details>
304
+
305
+ <details>
306
+ <summary><b>Without --pgvector flag</b></summary>
307
+
308
+ If you don't use `--pgvector`, you can still enable pgvector manually per database:
309
+
310
+ ```sql
311
+ CREATE EXTENSION IF NOT EXISTS vector;
312
+ ```
313
+
314
+ </details>
315
+
316
+ > pgvector 0.8.1 is bundled with the PostgreSQL binaries. Supports L2 distance (`<->`), inner product (`<#>`), and cosine distance (`<=>`).
317
+
318
+ <br>
319
+
261
320
  ## Performance
262
321
 
263
- ### Benchmark Results
322
+ ### CRUD Benchmarks
264
323
 
265
324
  <table>
266
325
  <tr>
267
326
  <th>Scenario</th>
268
327
  <th>SQLite</th>
269
328
  <th>PGlite</th>
270
- <th>pgserve (Node)</th>
271
- <th>pgserve (Bun)</th>
329
+ <th>PostgreSQL</th>
330
+ <th>pgserve</th>
272
331
  <th>pgserve --ram</th>
273
332
  </tr>
274
333
  <tr>
275
334
  <td><b>Concurrent Writes</b> (10 agents)</td>
276
- <td>85 qps</td>
277
- <td>211 qps</td>
278
- <td>833 qps</td>
279
- <td>1,754 qps</td>
280
- <td><b>4,000 qps</b> 🏆</td>
335
+ <td>91 qps</td>
336
+ <td>204 qps</td>
337
+ <td>1,667 qps</td>
338
+ <td>2,273 qps</td>
339
+ <td><b>4,167 qps</b> 🏆</td>
281
340
  </tr>
282
341
  <tr>
283
342
  <td><b>Mixed Workload</b></td>
284
- <td>339 qps</td>
285
- <td>505 qps</td>
286
- <td>1,017 qps</td>
287
- <td>999 qps</td>
288
- <td><b>1,986 qps</b> 🏆</td>
343
+ <td>383 qps</td>
344
+ <td>484 qps</td>
345
+ <td>507 qps</td>
346
+ <td>1,133 qps</td>
347
+ <td><b>2,109 qps</b> 🏆</td>
289
348
  </tr>
290
349
  <tr>
291
350
  <td><b>Write Lock</b> (50 writers)</td>
292
- <td>86 qps</td>
293
- <td>168 qps</td>
294
- <td>488 qps</td>
295
- <td>1,176 qps</td>
351
+ <td>111 qps</td>
352
+ <td>228 qps</td>
353
+ <td>2,857 qps</td>
354
+ <td>3,030 qps</td>
296
355
  <td><b>4,348 qps</b> 🏆</td>
297
356
  </tr>
298
357
  </table>
299
358
 
300
- > pgserve uses Bun runtime internally for 2x throughput. **RAM mode adds another 2-4x** on Linux.
359
+ ### Vector Benchmarks (pgvector)
360
+
361
+ <table>
362
+ <tr>
363
+ <th>Metric</th>
364
+ <th>PGlite</th>
365
+ <th>PostgreSQL</th>
366
+ <th>pgserve</th>
367
+ <th>pgserve --ram</th>
368
+ </tr>
369
+ <tr>
370
+ <td><b>Vector INSERT</b> (1000 × 1536-dim)</td>
371
+ <td>152/sec</td>
372
+ <td>392/sec</td>
373
+ <td>387/sec</td>
374
+ <td><b>1,082/sec</b> 🏆</td>
375
+ </tr>
376
+ <tr>
377
+ <td><b>k-NN Search</b> (k=10, 10k corpus)</td>
378
+ <td>22 qps</td>
379
+ <td>33 qps</td>
380
+ <td>31 qps</td>
381
+ <td>30 qps</td>
382
+ </tr>
383
+ <tr>
384
+ <td><b>Recall@10</b></td>
385
+ <td>100%</td>
386
+ <td>100%</td>
387
+ <td>100%</td>
388
+ <td>100%</td>
389
+ </tr>
390
+ </table>
391
+
392
+ > <b>Why pgserve wins on writes:</b> RAM mode uses <code>/dev/shm</code> (tmpfs), eliminating fsync latency. Vector search is CPU-bound, so RAM mode shows minimal benefit there.
393
+
394
+ ### Final Score
395
+
396
+ <table>
397
+ <tr>
398
+ <th>Engine</th>
399
+ <th>CRUD QPS</th>
400
+ <th>Vec QPS</th>
401
+ <th>Recall</th>
402
+ <th>P50</th>
403
+ <th>P99</th>
404
+ <th>Score</th>
405
+ </tr>
406
+ <tr>
407
+ <td>SQLite</td>
408
+ <td>195</td>
409
+ <td>N/A</td>
410
+ <td>N/A</td>
411
+ <td>6.3ms</td>
412
+ <td>17.3ms</td>
413
+ <td>117</td>
414
+ </tr>
415
+ <tr>
416
+ <td>PGlite</td>
417
+ <td>305</td>
418
+ <td>65</td>
419
+ <td>100%</td>
420
+ <td>3.3ms</td>
421
+ <td>7.0ms</td>
422
+ <td>209</td>
423
+ </tr>
424
+ <tr>
425
+ <td>PostgreSQL</td>
426
+ <td>1,677</td>
427
+ <td>152</td>
428
+ <td>100%</td>
429
+ <td>6.0ms</td>
430
+ <td>19.0ms</td>
431
+ <td>1,067</td>
432
+ </tr>
433
+ <tr>
434
+ <td>pgserve</td>
435
+ <td>2,145</td>
436
+ <td>149</td>
437
+ <td>100%</td>
438
+ <td>5.3ms</td>
439
+ <td>13.0ms</td>
440
+ <td>1,347</td>
441
+ </tr>
442
+ <tr>
443
+ <td><b>pgserve --ram</b></td>
444
+ <td><b>3,541</b></td>
445
+ <td><b>381</b></td>
446
+ <td><b>100%</b></td>
447
+ <td><b>3.3ms</b></td>
448
+ <td><b>10.7ms</b></td>
449
+ <td><b>2,277</b> 🏆</td>
450
+ </tr>
451
+ </table>
452
+
453
+ > <b>Methodology:</b> Recall@k measured against brute-force ground truth (industry standard). PostgreSQL baseline is Docker <code>pgvector/pgvector:pg17</code>. RAM mode available on Linux and WSL2.
301
454
  >
302
- > Run benchmarks: `bun run bench` (contributors only)
455
+ > Run benchmarks yourself: <code>bun tests/benchmarks/runner.js --include-vector</code>
303
456
 
304
457
  <br>
305
458
 
@@ -53,6 +53,9 @@ OPTIONS:
53
53
  --no-provision Disable auto-provisioning of databases
54
54
  --sync-to <url> Sync to real PostgreSQL (async replication)
55
55
  --sync-databases Database patterns to sync (comma-separated, e.g. "myapp,tenant_*")
56
+ --no-stats Disable real-time stats dashboard (enabled by default)
57
+ --max-connections Max concurrent connections (default: 1000)
58
+ --pgvector Auto-enable pgvector extension on new databases
56
59
  --help Show this help message
57
60
 
58
61
  MODES:
@@ -108,7 +111,10 @@ function parseArgs() {
108
111
  cluster: cpuCount > 1 && !isWindows, // Auto-enable on multi-core (disabled on Windows - no SO_REUSEPORT)
109
112
  workers: null, // null = use CPU count
110
113
  syncTo: null, // Sync target PostgreSQL URL
111
- syncDatabases: null // Database patterns to sync (comma-separated)
114
+ syncDatabases: null, // Database patterns to sync (comma-separated)
115
+ showStats: true, // Show real-time stats dashboard (default: enabled)
116
+ maxConnections: 1000, // Max concurrent connections (high default for multi-tenant)
117
+ enablePgvector: false // Auto-enable pgvector extension on new databases
112
118
  };
113
119
 
114
120
  for (let i = 0; i < args.length; i++) {
@@ -163,6 +169,22 @@ function parseArgs() {
163
169
  options.syncDatabases = args[++i];
164
170
  break;
165
171
 
172
+ case '--stats':
173
+ options.showStats = true;
174
+ break;
175
+
176
+ case '--no-stats':
177
+ options.showStats = false;
178
+ break;
179
+
180
+ case '--max-connections':
181
+ options.maxConnections = parseInt(args[++i], 10);
182
+ break;
183
+
184
+ case '--pgvector':
185
+ options.enablePgvector = true;
186
+ break;
187
+
166
188
  case '--help':
167
189
  case 'help':
168
190
  printHelp();
@@ -211,7 +233,9 @@ pgserve - Embedded PostgreSQL Server
211
233
  useRam: options.useRam,
212
234
  logLevel: options.logLevel,
213
235
  autoProvision: options.autoProvision,
214
- workers: options.workers
236
+ workers: options.workers,
237
+ maxConnections: options.maxConnections,
238
+ enablePgvector: options.enablePgvector
215
239
  });
216
240
 
217
241
  // Only primary process shows full startup message
@@ -222,10 +246,11 @@ pgserve - Embedded PostgreSQL Server
222
246
  Cluster started successfully!
223
247
 
224
248
  Endpoint: postgresql://${options.host}:${options.port}/<database>
225
- Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'} (Cluster)
249
+ Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'} (Cluster)
226
250
  Workers: ${stats.workers} processes
227
251
  Data: ${storageType}
228
252
  Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
253
+ pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
229
254
 
230
255
  Examples:
231
256
  postgresql://${options.host}:${options.port}/myapp
@@ -244,7 +269,9 @@ Press Ctrl+C to stop
244
269
  logLevel: options.logLevel,
245
270
  autoProvision: options.autoProvision,
246
271
  syncTo: options.syncTo,
247
- syncDatabases: options.syncDatabases
272
+ syncDatabases: options.syncDatabases,
273
+ maxConnections: options.maxConnections,
274
+ enablePgvector: options.enablePgvector
248
275
  });
249
276
 
250
277
  server = router;
@@ -258,10 +285,11 @@ Press Ctrl+C to stop
258
285
  Server started successfully!
259
286
 
260
287
  Endpoint: postgresql://${options.host}:${options.port}/<database>
261
- Mode: ${memoryMode ? 'In-memory (ephemeral)' : 'Persistent'}
288
+ Mode: ${memoryMode ? (options.useRam ? 'RAM (/dev/shm)' : 'Ephemeral (temp)') : 'Persistent'}
262
289
  Data: ${storageType}
263
290
  PostgreSQL: Port ${router.pgPort} (internal)
264
291
  Auto-create: ${options.autoProvision ? 'Enabled' : 'Disabled'}
292
+ pgvector: ${options.enablePgvector ? 'Enabled (auto-installed on new DBs)' : 'Disabled (use --pgvector to enable)'}
265
293
  Sync: ${syncStatus}${options.syncDatabases ? ` (${options.syncDatabases})` : ''}
266
294
 
267
295
  Examples:
@@ -272,12 +300,45 @@ Press Ctrl+C to stop
272
300
  `);
273
301
  }
274
302
 
303
+ // Start stats dashboard if requested (only for primary/single-process)
304
+ let dashboard = null;
305
+ if (options.showStats && !process.env.PGSERVE_WORKER) {
306
+ const { StatsDashboard } = await import('../src/stats-dashboard.js');
307
+ const { StatsCollector } = await import('../src/stats-collector.js');
308
+
309
+ // Create stats collector with appropriate sources
310
+ const collector = new StatsCollector({
311
+ router: options.cluster ? null : server,
312
+ pgManager: server.pgManager,
313
+ clusterStats: options.cluster ? () => server.getStats() : null,
314
+ logger: server.logger,
315
+ port: options.port,
316
+ host: options.host
317
+ });
318
+
319
+ dashboard = new StatsDashboard({
320
+ refreshInterval: 2000, // 2 second refresh for real-time feel
321
+ statsProvider: () => collector.collect()
322
+ });
323
+
324
+ dashboard.start();
325
+ }
326
+
275
327
  // Graceful shutdown (only for primary/single-process, workers handle via IPC)
276
328
  if (!process.env.PGSERVE_WORKER) {
277
329
  const shutdown = async () => {
330
+ // Stop dashboard first to restore cursor
331
+ if (dashboard) {
332
+ dashboard.stop();
333
+ }
278
334
  console.log('\nShutting down...');
279
- await server.stop();
280
- console.log('Server stopped.');
335
+ try {
336
+ await server.stop();
337
+ console.log('Server stopped.');
338
+ } catch (err) {
339
+ console.error('Error during shutdown:', err.message);
340
+ // Still exit - best effort cleanup
341
+ }
281
342
  process.exit(0);
282
343
  };
283
344
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.3",
3
+ "version": "1.1.5-rc.1",
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
@@ -23,6 +23,10 @@ const SSL_REQUEST_CODE = 80877103;
23
23
  const GSSAPI_REQUEST_CODE = 80877104;
24
24
  const CANCEL_REQUEST_CODE = 80877102;
25
25
 
26
+ // Stats collection constants
27
+ const WORKER_STATS_TIMEOUT_MS = 10000; // Worker stats older than this are considered stale
28
+ const WORKER_STATS_REPORT_INTERVAL_MS = 4000; // How often workers report stats to primary
29
+
26
30
  /**
27
31
  * ClusterRouter - Lightweight TCP router for worker processes
28
32
  * Does NOT start PostgreSQL - connects to PRIMARY's PostgreSQL via Unix socket
@@ -38,12 +42,19 @@ class ClusterRouter extends EventEmitter {
38
42
  this.pgPassword = options.pgPassword || 'postgres';
39
43
  this.autoProvision = options.autoProvision !== false;
40
44
  this.maxConnections = options.maxConnections || 1000;
45
+ this.enablePgvector = options.enablePgvector || false;
41
46
 
42
47
  this.logger = createLogger({ level: options.logLevel || 'info' });
43
48
  this.sql = null; // Bun.sql for admin queries
44
49
  this.server = null;
45
50
  this.connections = new Set();
46
51
  this.setMaxListeners(this.maxConnections + 10);
52
+
53
+ // Connection stats tracking for IPC reporting
54
+ this.connectionStats = {
55
+ totalConnected: 0,
56
+ totalDisconnected: 0
57
+ };
47
58
  }
48
59
 
49
60
  /**
@@ -114,6 +125,11 @@ class ClusterRouter extends EventEmitter {
114
125
  if (result.length === 0) {
115
126
  // Use sql() helper for safe identifier escaping (like CREATE DATABASE)
116
127
  await this.sql.unsafe(`CREATE DATABASE "${dbName.replace(/"/g, '""')}"`);
128
+
129
+ // Auto-enable pgvector extension if configured
130
+ if (this.enablePgvector) {
131
+ await this.enablePgvectorExtension(dbName);
132
+ }
117
133
  }
118
134
  } catch (error) {
119
135
  // Ignore "already exists" (race condition between workers)
@@ -123,6 +139,41 @@ class ClusterRouter extends EventEmitter {
123
139
  }
124
140
  }
125
141
 
142
+ /**
143
+ * Enable pgvector extension on a database
144
+ * Creates a temporary connection to the specific database to run CREATE EXTENSION
145
+ * @param {string} dbName - Database name to enable pgvector on
146
+ */
147
+ async enablePgvectorExtension(dbName) {
148
+ let dbPool = null;
149
+
150
+ try {
151
+ // Create temporary connection to the specific database
152
+ dbPool = new SQL({
153
+ hostname: '127.0.0.1',
154
+ port: this.pgPort,
155
+ database: dbName,
156
+ username: this.pgUser,
157
+ password: this.pgPassword,
158
+ max: 1,
159
+ idleTimeout: 5,
160
+ connectionTimeout: 5,
161
+ });
162
+
163
+ // Enable pgvector extension
164
+ await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
165
+ this.logger.info({ dbName }, 'pgvector extension enabled');
166
+ } catch (error) {
167
+ // Log but don't fail database creation - pgvector might not be available
168
+ this.logger.warn({ dbName, err: error.message }, 'Failed to enable pgvector extension (non-fatal)');
169
+ } finally {
170
+ // Always close the temporary connection
171
+ if (dbPool) {
172
+ await dbPool.close().catch(() => {});
173
+ }
174
+ }
175
+ }
176
+
126
177
  /**
127
178
  * Handle socket open (Bun TCP handler)
128
179
  */
@@ -134,6 +185,7 @@ class ClusterRouter extends EventEmitter {
134
185
  handshakeComplete: false
135
186
  });
136
187
  this.connections.add(socket);
188
+ this.connectionStats.totalConnected++;
137
189
  }
138
190
 
139
191
  /**
@@ -259,6 +311,19 @@ class ClusterRouter extends EventEmitter {
259
311
  }
260
312
  this.connections.delete(socket);
261
313
  this.socketState.delete(socket);
314
+ this.connectionStats.totalDisconnected++;
315
+ }
316
+
317
+ /**
318
+ * Get router stats for IPC reporting
319
+ */
320
+ getStats() {
321
+ return {
322
+ connections: this.connections.size,
323
+ totalConnected: this.connectionStats.totalConnected,
324
+ totalDisconnected: this.connectionStats.totalDisconnected,
325
+ pid: process.pid
326
+ };
262
327
  }
263
328
 
264
329
  /**
@@ -286,7 +351,7 @@ class ClusterRouter extends EventEmitter {
286
351
  try {
287
352
  await this.sql.close();
288
353
  } catch {
289
- // Ignore - connection may already be terminated
354
+ // Expected: connection may already be terminated during cleanup
290
355
  }
291
356
  }
292
357
 
@@ -297,6 +362,7 @@ class ClusterRouter extends EventEmitter {
297
362
  }
298
363
  }
299
364
 
365
+
300
366
  /**
301
367
  * Start pgserve in cluster mode
302
368
  */
@@ -307,6 +373,8 @@ export async function startClusterServer(options = {}) {
307
373
  const pgPort = options.pgPort || (port + 1000);
308
374
 
309
375
  if (cluster.isPrimary) {
376
+ // Port binding happens in workers via Bun.listen with reusePort
377
+ // If port is in use, first worker will fail with EADDRINUSE
310
378
  console.log(`[pgserve] Cluster mode: ${numWorkers} workers`);
311
379
 
312
380
  // PRIMARY: Start our embedded PostgreSQL (single instance)
@@ -315,7 +383,8 @@ export async function startClusterServer(options = {}) {
315
383
  dataDir: options.baseDir,
316
384
  port: pgPort,
317
385
  logger: logger.child({ component: 'postgres' }),
318
- useRam: options.useRam // Use /dev/shm for true RAM storage (Linux only)
386
+ useRam: options.useRam, // Use /dev/shm for true RAM storage (Linux only)
387
+ enablePgvector: options.enablePgvector // Auto-enable pgvector extension on new databases
319
388
  });
320
389
 
321
390
  await pgManager.start();
@@ -325,6 +394,7 @@ export async function startClusterServer(options = {}) {
325
394
  console.log(`[pgserve] Socket: ${pgSocketPath || `TCP port ${pgPort}`}`);
326
395
 
327
396
  const workers = new Map();
397
+ const workerStats = new Map(); // Track stats from each worker
328
398
 
329
399
  // Fork workers with PostgreSQL connection info
330
400
  for (let i = 0; i < numWorkers; i++) {
@@ -337,7 +407,9 @@ export async function startClusterServer(options = {}) {
337
407
  PGSERVE_PG_USER: 'postgres',
338
408
  PGSERVE_PG_PASSWORD: 'postgres',
339
409
  PGSERVE_LOG_LEVEL: options.logLevel || 'info',
340
- PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
410
+ PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
411
+ PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
412
+ PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
341
413
  });
342
414
  workers.set(worker.id, worker);
343
415
  }
@@ -363,18 +435,26 @@ export async function startClusterServer(options = {}) {
363
435
  PGSERVE_PG_USER: 'postgres',
364
436
  PGSERVE_PG_PASSWORD: 'postgres',
365
437
  PGSERVE_LOG_LEVEL: options.logLevel || 'info',
366
- PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
438
+ PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
439
+ PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
440
+ PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
367
441
  });
368
442
  workers.set(newWorker.id, newWorker);
369
443
  });
370
444
 
371
- // Wait for workers to be ready
445
+ // Wait for workers to be ready and handle IPC messages
372
446
  let readyCount = 0;
373
447
  await new Promise((resolve) => {
374
448
  cluster.on('message', (worker, message) => {
375
449
  if (message.type === 'ready') {
376
450
  readyCount++;
377
451
  if (readyCount === numWorkers) resolve();
452
+ } else if (message.type === 'stats') {
453
+ // Update worker stats from IPC
454
+ workerStats.set(worker.id, {
455
+ ...message.data,
456
+ lastUpdate: Date.now()
457
+ });
378
458
  }
379
459
  });
380
460
  });
@@ -403,10 +483,35 @@ export async function startClusterServer(options = {}) {
403
483
  await pgManager.stop();
404
484
  console.log('[pgserve] Cluster stopped');
405
485
  },
406
- getStats: () => ({
407
- workers: workers.size,
408
- pids: Array.from(workers.values()).map(w => w.process.pid)
409
- })
486
+ getStats: () => {
487
+ // Aggregate stats from all workers
488
+ let totalConnections = 0;
489
+ let totalConnected = 0;
490
+ let totalDisconnected = 0;
491
+ const activeWorkerStats = {};
492
+
493
+ for (const [id, stats] of workerStats) {
494
+ // Only include recent stats (within timeout window)
495
+ if (Date.now() - stats.lastUpdate < WORKER_STATS_TIMEOUT_MS) {
496
+ totalConnections += stats.connections || 0;
497
+ totalConnected += stats.totalConnected || 0;
498
+ totalDisconnected += stats.totalDisconnected || 0;
499
+ activeWorkerStats[id] = stats;
500
+ }
501
+ }
502
+
503
+ return {
504
+ workers: workers.size,
505
+ pids: Array.from(workers.values()).map(w => w.process.pid),
506
+ connections: {
507
+ active: totalConnections,
508
+ totalConnected,
509
+ totalDisconnected
510
+ },
511
+ workerStats: activeWorkerStats
512
+ };
513
+ },
514
+ pgManager
410
515
  };
411
516
  } else {
412
517
  // WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL
@@ -418,7 +523,9 @@ export async function startClusterServer(options = {}) {
418
523
  pgUser: process.env.PGSERVE_PG_USER || 'postgres',
419
524
  pgPassword: process.env.PGSERVE_PG_PASSWORD || 'postgres',
420
525
  logLevel: process.env.PGSERVE_LOG_LEVEL || 'info',
421
- autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true'
526
+ autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true',
527
+ maxConnections: parseInt(process.env.PGSERVE_MAX_CONNECTIONS) || 1000,
528
+ enablePgvector: process.env.PGSERVE_ENABLE_PGVECTOR === 'true'
422
529
  });
423
530
 
424
531
  await router.start();
@@ -426,9 +533,19 @@ export async function startClusterServer(options = {}) {
426
533
  // Tell PRIMARY we're ready
427
534
  process.send({ type: 'ready' });
428
535
 
536
+ // Periodically send stats to PRIMARY
537
+ const statsInterval = setInterval(() => {
538
+ try {
539
+ process.send({ type: 'stats', data: router.getStats() });
540
+ } catch {
541
+ // Expected: IPC channel may be closed during shutdown
542
+ }
543
+ }, WORKER_STATS_REPORT_INTERVAL_MS);
544
+
429
545
  // Handle shutdown
430
546
  process.on('message', async (message) => {
431
547
  if (message.type === 'shutdown') {
548
+ clearInterval(statsInterval);
432
549
  await router.stop();
433
550
  process.exit(0);
434
551
  }
package/src/index.js CHANGED
@@ -11,6 +11,8 @@ export { PostgresManager } from './postgres.js';
11
11
  export { SyncManager } from './sync.js';
12
12
  export { RestoreManager } from './restore.js';
13
13
  export { Dashboard } from './dashboard.js';
14
+ export { StatsCollector } from './stats-collector.js';
15
+ export { StatsDashboard } from './stats-dashboard.js';
14
16
 
15
17
  // Default export
16
18
  export { startMultiTenantServer as default } from './router.js';