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.
- package/.claude/settings.local.json +11 -0
- package/README.md +281 -256
- package/bin/pglite-server.js +212 -395
- package/package.json +13 -10
- package/src/cluster.js +322 -0
- package/src/index.js +8 -171
- package/src/postgres.js +479 -0
- package/src/protocol.js +31 -6
- package/src/router.js +117 -114
- package/src/sync.js +344 -0
- package/tests/benchmarks/runner.js +300 -155
- package/tests/sync-perf-test.js +150 -0
- package/src/detector.js +0 -105
- package/src/pool.js +0 -320
- package/src/ports.js +0 -114
- package/src/registry.js +0 -134
- package/src/server.js +0 -265
package/src/postgres.js
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Manager (Direct Binary Execution)
|
|
3
|
+
*
|
|
4
|
+
* Manages an embedded PostgreSQL instance with true concurrent connections.
|
|
5
|
+
* Directly executes PostgreSQL binaries from embedded-postgres packages,
|
|
6
|
+
* bypassing the embedded-postgres library's locale-dependent initialization.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Uses embedded-postgres binaries (auto-downloaded via npm)
|
|
10
|
+
* - Memory mode (default) or persistent storage
|
|
11
|
+
* - True concurrent connections (native PostgreSQL process forking)
|
|
12
|
+
* - Auto-provision databases on demand
|
|
13
|
+
* - No locale dependency (works on any system)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import crypto from 'crypto';
|
|
21
|
+
import net from 'net';
|
|
22
|
+
|
|
23
|
+
// Resolve binary paths from embedded-postgres platform packages
|
|
24
|
+
function getBinaryPaths() {
|
|
25
|
+
const platform = os.platform();
|
|
26
|
+
const arch = os.arch();
|
|
27
|
+
|
|
28
|
+
let pkgName;
|
|
29
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
30
|
+
pkgName = '@embedded-postgres/linux-x64';
|
|
31
|
+
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
32
|
+
pkgName = '@embedded-postgres/darwin-arm64';
|
|
33
|
+
} else if (platform === 'darwin' && arch === 'x64') {
|
|
34
|
+
pkgName = '@embedded-postgres/darwin-x64';
|
|
35
|
+
} else if (platform === 'win32' && arch === 'x64') {
|
|
36
|
+
pkgName = '@embedded-postgres/win32-x64';
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Find the package in node_modules
|
|
42
|
+
const possiblePaths = [
|
|
43
|
+
path.join(process.cwd(), 'node_modules', pkgName, 'native', 'bin'),
|
|
44
|
+
path.join(import.meta.dirname, '..', 'node_modules', pkgName, 'native', 'bin'),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const binDir of possiblePaths) {
|
|
48
|
+
const initdb = path.join(binDir, platform === 'win32' ? 'initdb.exe' : 'initdb');
|
|
49
|
+
const postgres = path.join(binDir, platform === 'win32' ? 'postgres.exe' : 'postgres');
|
|
50
|
+
if (fs.existsSync(initdb) && fs.existsSync(postgres)) {
|
|
51
|
+
return { initdb, postgres, binDir };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new Error(`Could not find PostgreSQL binaries. Please run: npm install ${pkgName}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class PostgresManager {
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
this.dataDir = options.dataDir || null; // null = memory mode (temp dir)
|
|
61
|
+
this.port = options.port || 5433; // Internal PG port (router listens on different port)
|
|
62
|
+
this.user = options.user || 'postgres';
|
|
63
|
+
this.password = options.password || 'postgres';
|
|
64
|
+
this.logger = options.logger;
|
|
65
|
+
this.process = null;
|
|
66
|
+
this.databaseDir = null;
|
|
67
|
+
this.persistent = !!options.dataDir;
|
|
68
|
+
this.createdDatabases = new Set();
|
|
69
|
+
this.binaries = null;
|
|
70
|
+
this.creatingDatabases = new Map(); // Track in-progress creations
|
|
71
|
+
this.socketDir = null; // Unix socket directory for faster local connections
|
|
72
|
+
this.adminPool = null; // Connection pool for database admin operations
|
|
73
|
+
|
|
74
|
+
// Sync/Replication options (for async sync to real PostgreSQL)
|
|
75
|
+
this.syncEnabled = options.syncEnabled || false;
|
|
76
|
+
this.syncManager = null; // Will be set via setSyncManager()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set the SyncManager for async replication
|
|
81
|
+
* Called after PostgresManager is created but before start()
|
|
82
|
+
* @param {SyncManager} syncManager
|
|
83
|
+
*/
|
|
84
|
+
setSyncManager(syncManager) {
|
|
85
|
+
this.syncManager = syncManager;
|
|
86
|
+
this.syncEnabled = !!syncManager;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Start the embedded PostgreSQL instance
|
|
91
|
+
*/
|
|
92
|
+
async start() {
|
|
93
|
+
// Get binary paths
|
|
94
|
+
this.binaries = getBinaryPaths();
|
|
95
|
+
|
|
96
|
+
// Make binaries executable
|
|
97
|
+
await fs.promises.chmod(this.binaries.initdb, '755');
|
|
98
|
+
await fs.promises.chmod(this.binaries.postgres, '755');
|
|
99
|
+
|
|
100
|
+
// Determine data directory
|
|
101
|
+
if (this.persistent) {
|
|
102
|
+
this.databaseDir = this.dataDir;
|
|
103
|
+
// Ensure directory exists
|
|
104
|
+
if (!fs.existsSync(this.databaseDir)) {
|
|
105
|
+
fs.mkdirSync(this.databaseDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Memory mode: use temp directory with unique suffix
|
|
109
|
+
this.databaseDir = path.join(os.tmpdir(), `pgserve-${process.pid}-${Date.now()}`);
|
|
110
|
+
// Clean up if exists from a previous failed run
|
|
111
|
+
if (fs.existsSync(this.databaseDir)) {
|
|
112
|
+
fs.rmSync(this.databaseDir, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Create Unix socket directory (Linux/macOS only, Windows uses TCP)
|
|
117
|
+
if (os.platform() !== 'win32') {
|
|
118
|
+
this.socketDir = path.join(os.tmpdir(), `pgserve-sock-${process.pid}-${Date.now()}`);
|
|
119
|
+
if (!fs.existsSync(this.socketDir)) {
|
|
120
|
+
fs.mkdirSync(this.socketDir, { recursive: true, mode: 0o700 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.logger.info({
|
|
125
|
+
databaseDir: this.databaseDir,
|
|
126
|
+
persistent: this.persistent,
|
|
127
|
+
port: this.port
|
|
128
|
+
}, 'Starting embedded PostgreSQL');
|
|
129
|
+
|
|
130
|
+
// Check if data directory is already initialized
|
|
131
|
+
const pgVersionFile = path.join(this.databaseDir, 'PG_VERSION');
|
|
132
|
+
if (!fs.existsSync(pgVersionFile)) {
|
|
133
|
+
await this._runInitDb();
|
|
134
|
+
} else {
|
|
135
|
+
this.logger.debug({ databaseDir: this.databaseDir }, 'Using existing data directory');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Start PostgreSQL server
|
|
139
|
+
await this._startPostgres();
|
|
140
|
+
|
|
141
|
+
// Initialize admin connection pool (for database creation operations)
|
|
142
|
+
await this._initAdminPool();
|
|
143
|
+
|
|
144
|
+
this.logger.info({
|
|
145
|
+
databaseDir: this.databaseDir,
|
|
146
|
+
port: this.port,
|
|
147
|
+
socketDir: this.socketDir,
|
|
148
|
+
persistent: this.persistent
|
|
149
|
+
}, 'PostgreSQL started successfully');
|
|
150
|
+
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Run initdb to initialize the data directory
|
|
156
|
+
*/
|
|
157
|
+
async _runInitDb() {
|
|
158
|
+
// Create password file
|
|
159
|
+
const randomId = crypto.randomBytes(6).toString('hex');
|
|
160
|
+
const passwordFile = path.join(os.tmpdir(), `pg-password-${randomId}`);
|
|
161
|
+
await fs.promises.writeFile(passwordFile, this.password + '\n');
|
|
162
|
+
|
|
163
|
+
this.logger.debug({ databaseDir: this.databaseDir }, 'Initializing PostgreSQL data directory');
|
|
164
|
+
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const proc = spawn(this.binaries.initdb, [
|
|
167
|
+
`--pgdata=${this.databaseDir}`,
|
|
168
|
+
'--auth=password',
|
|
169
|
+
`--username=${this.user}`,
|
|
170
|
+
`--pwfile=${passwordFile}`,
|
|
171
|
+
], {
|
|
172
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C' }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
let stdout = '';
|
|
176
|
+
let stderr = '';
|
|
177
|
+
|
|
178
|
+
proc.stdout.on('data', (data) => {
|
|
179
|
+
stdout += data.toString();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
proc.stderr.on('data', (data) => {
|
|
183
|
+
stderr += data.toString();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
proc.on('close', async (code) => {
|
|
187
|
+
// Clean up password file
|
|
188
|
+
try {
|
|
189
|
+
await fs.promises.unlink(passwordFile);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
// Ignore
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (code === 0) {
|
|
195
|
+
this.logger.debug('initdb completed successfully');
|
|
196
|
+
resolve();
|
|
197
|
+
} else {
|
|
198
|
+
this.logger.error({ code, stdout, stderr }, 'initdb failed');
|
|
199
|
+
reject(new Error(`initdb failed with code ${code}: ${stderr || stdout}`));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
proc.on('error', (err) => {
|
|
204
|
+
reject(new Error(`Failed to spawn initdb: ${err.message}`));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Initialize admin connection pool for database operations
|
|
211
|
+
* Uses Unix socket when available for faster connections
|
|
212
|
+
*/
|
|
213
|
+
async _initAdminPool() {
|
|
214
|
+
const { default: pg } = await import('pg');
|
|
215
|
+
|
|
216
|
+
// Pool config - use Unix socket when available
|
|
217
|
+
const poolConfig = {
|
|
218
|
+
user: this.user,
|
|
219
|
+
password: this.password,
|
|
220
|
+
database: 'postgres',
|
|
221
|
+
max: 5, // Small pool - only for CREATE DATABASE operations
|
|
222
|
+
idleTimeoutMillis: 30000,
|
|
223
|
+
connectionTimeoutMillis: 5000,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Use Unix socket for faster local connections (Linux/macOS)
|
|
227
|
+
// Note: pg library needs both host (socket dir) AND port to find the socket file
|
|
228
|
+
if (this.socketDir) {
|
|
229
|
+
poolConfig.host = this.socketDir;
|
|
230
|
+
poolConfig.port = this.port; // Required for Unix socket path construction
|
|
231
|
+
} else {
|
|
232
|
+
poolConfig.host = '127.0.0.1';
|
|
233
|
+
poolConfig.port = this.port;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.adminPool = new pg.Pool(poolConfig);
|
|
237
|
+
|
|
238
|
+
// Verify pool is working
|
|
239
|
+
const client = await this.adminPool.connect();
|
|
240
|
+
client.release();
|
|
241
|
+
|
|
242
|
+
this.logger.debug({
|
|
243
|
+
host: poolConfig.host,
|
|
244
|
+
maxConnections: poolConfig.max
|
|
245
|
+
}, 'Admin connection pool initialized');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Start the PostgreSQL server process
|
|
250
|
+
*/
|
|
251
|
+
async _startPostgres() {
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
// Build PostgreSQL arguments
|
|
254
|
+
const pgArgs = [
|
|
255
|
+
'-D', this.databaseDir,
|
|
256
|
+
'-p', this.port.toString(),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
// Enable Unix socket for faster local connections (Linux/macOS)
|
|
260
|
+
// Windows falls back to TCP only
|
|
261
|
+
if (this.socketDir) {
|
|
262
|
+
pgArgs.push('-k', this.socketDir);
|
|
263
|
+
} else {
|
|
264
|
+
pgArgs.push('-k', ''); // Disable Unix socket on Windows
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add logical replication settings when sync is enabled
|
|
268
|
+
// These settings enable PostgreSQL's native WAL-based replication
|
|
269
|
+
// with ZERO hot path impact (handled by PostgreSQL's WAL writer process)
|
|
270
|
+
if (this.syncEnabled) {
|
|
271
|
+
pgArgs.push(
|
|
272
|
+
'-c', 'wal_level=logical', // Enable logical decoding
|
|
273
|
+
'-c', 'max_replication_slots=10', // Support multiple subscriptions
|
|
274
|
+
'-c', 'max_wal_senders=10', // Parallel replication streams
|
|
275
|
+
'-c', 'wal_keep_size=512MB', // Retain WAL for catchup
|
|
276
|
+
);
|
|
277
|
+
this.logger.info('Logical replication enabled for sync');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.process = spawn(this.binaries.postgres, pgArgs, {
|
|
281
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C' }
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
let started = false;
|
|
285
|
+
let startupOutput = '';
|
|
286
|
+
|
|
287
|
+
const onData = (data) => {
|
|
288
|
+
const message = data.toString();
|
|
289
|
+
startupOutput += message;
|
|
290
|
+
this.logger.debug({ pgOutput: message.trim() }, 'PostgreSQL output');
|
|
291
|
+
|
|
292
|
+
// Check for ready message
|
|
293
|
+
if (message.includes('database system is ready to accept connections') ||
|
|
294
|
+
message.includes('ready to accept connections')) {
|
|
295
|
+
started = true;
|
|
296
|
+
resolve();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
this.process.stderr.on('data', onData);
|
|
301
|
+
this.process.stdout.on('data', onData);
|
|
302
|
+
|
|
303
|
+
this.process.on('close', (code) => {
|
|
304
|
+
if (!started) {
|
|
305
|
+
reject(new Error(`PostgreSQL exited with code ${code} before starting: ${startupOutput}`));
|
|
306
|
+
}
|
|
307
|
+
this.process = null;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.process.on('error', (err) => {
|
|
311
|
+
reject(new Error(`Failed to spawn postgres: ${err.message}`));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Timeout after 30 seconds
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
if (!started) {
|
|
317
|
+
reject(new Error(`PostgreSQL startup timed out after 30s. Output: ${startupOutput}`));
|
|
318
|
+
}
|
|
319
|
+
}, 30000);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Create a database if it doesn't exist
|
|
325
|
+
* Uses a promise-based lock to prevent race conditions
|
|
326
|
+
* @param {string} dbName - Database name to create
|
|
327
|
+
*/
|
|
328
|
+
async createDatabase(dbName) {
|
|
329
|
+
// Skip if already created this session
|
|
330
|
+
if (this.createdDatabases.has(dbName)) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Skip 'postgres' database - it always exists
|
|
335
|
+
if (dbName === 'postgres') {
|
|
336
|
+
this.createdDatabases.add(dbName);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check if creation is already in progress for this database
|
|
341
|
+
// If so, wait for it to complete
|
|
342
|
+
if (this.creatingDatabases.has(dbName)) {
|
|
343
|
+
await this.creatingDatabases.get(dbName);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Create a promise that other concurrent requests will wait on
|
|
348
|
+
let resolveCreation;
|
|
349
|
+
const creationPromise = new Promise((resolve) => {
|
|
350
|
+
resolveCreation = resolve;
|
|
351
|
+
});
|
|
352
|
+
this.creatingDatabases.set(dbName, creationPromise);
|
|
353
|
+
|
|
354
|
+
// Use pooled connection for faster database creation
|
|
355
|
+
let createError = null;
|
|
356
|
+
const client = await this.adminPool.connect();
|
|
357
|
+
try {
|
|
358
|
+
await client.query(`CREATE DATABASE ${client.escapeIdentifier(dbName)}`);
|
|
359
|
+
this.createdDatabases.add(dbName);
|
|
360
|
+
this.logger.info({ dbName }, 'Database created');
|
|
361
|
+
|
|
362
|
+
// Trigger async sync setup (non-blocking, doesn't affect hot path)
|
|
363
|
+
if (this.syncManager) {
|
|
364
|
+
this.syncManager.setupDatabaseSync(dbName)
|
|
365
|
+
.catch(err => this.logger.warn({ dbName, err: err.message }, 'Sync setup failed (non-fatal)'));
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
// Database might already exist (from previous persistent session or race condition)
|
|
369
|
+
// 42P04 = duplicate_database, 23505 = unique_violation
|
|
370
|
+
if (error.code === '42P04' || error.code === '23505') {
|
|
371
|
+
this.createdDatabases.add(dbName);
|
|
372
|
+
this.logger.debug({ dbName }, 'Database already exists');
|
|
373
|
+
} else {
|
|
374
|
+
createError = error;
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
client.release();
|
|
378
|
+
// Signal completion to waiting requests
|
|
379
|
+
this.creatingDatabases.delete(dbName);
|
|
380
|
+
resolveCreation();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (createError) {
|
|
384
|
+
throw new Error(`Failed to create database '${dbName}': ${createError.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Check if a database exists
|
|
390
|
+
* @param {string} dbName - Database name to check
|
|
391
|
+
*/
|
|
392
|
+
async databaseExists(dbName) {
|
|
393
|
+
return this.createdDatabases.has(dbName);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Stop the PostgreSQL instance
|
|
398
|
+
*/
|
|
399
|
+
async stop() {
|
|
400
|
+
// Close admin pool first
|
|
401
|
+
if (this.adminPool) {
|
|
402
|
+
await this.adminPool.end();
|
|
403
|
+
this.adminPool = null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (this.process) {
|
|
407
|
+
this.logger.info('Stopping PostgreSQL');
|
|
408
|
+
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
this.process.on('close', () => {
|
|
411
|
+
this.process = null;
|
|
412
|
+
|
|
413
|
+
// Clean up temp directory in memory mode
|
|
414
|
+
if (!this.persistent && this.databaseDir) {
|
|
415
|
+
try {
|
|
416
|
+
fs.rmSync(this.databaseDir, { recursive: true, force: true });
|
|
417
|
+
this.logger.debug({ databaseDir: this.databaseDir }, 'Cleaned up temp directory');
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.logger.warn({ error: error.message }, 'Failed to clean up temp directory');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Clean up socket directory
|
|
424
|
+
if (this.socketDir) {
|
|
425
|
+
try {
|
|
426
|
+
fs.rmSync(this.socketDir, { recursive: true, force: true });
|
|
427
|
+
this.logger.debug({ socketDir: this.socketDir }, 'Cleaned up socket directory');
|
|
428
|
+
} catch (error) {
|
|
429
|
+
this.logger.warn({ error: error.message }, 'Failed to clean up socket directory');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
resolve();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Send SIGINT for graceful shutdown
|
|
437
|
+
this.process.kill('SIGINT');
|
|
438
|
+
|
|
439
|
+
// Force kill after 5 seconds
|
|
440
|
+
setTimeout(() => {
|
|
441
|
+
if (this.process) {
|
|
442
|
+
this.process.kill('SIGKILL');
|
|
443
|
+
}
|
|
444
|
+
}, 5000);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get the Unix socket path for PostgreSQL connections
|
|
451
|
+
* Returns null on Windows (use TCP instead)
|
|
452
|
+
*/
|
|
453
|
+
getSocketPath() {
|
|
454
|
+
if (!this.socketDir) return null;
|
|
455
|
+
return path.join(this.socketDir, `.s.PGSQL.${this.port}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get connection URL for a specific database
|
|
460
|
+
* @param {string} dbName - Database name
|
|
461
|
+
*/
|
|
462
|
+
getConnectionUrl(dbName = 'postgres') {
|
|
463
|
+
return `postgresql://${this.user}:${this.password}@127.0.0.1:${this.port}/${dbName}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Get manager stats
|
|
468
|
+
*/
|
|
469
|
+
getStats() {
|
|
470
|
+
return {
|
|
471
|
+
port: this.port,
|
|
472
|
+
databaseDir: this.databaseDir,
|
|
473
|
+
socketDir: this.socketDir,
|
|
474
|
+
socketPath: this.getSocketPath(),
|
|
475
|
+
persistent: this.persistent,
|
|
476
|
+
databases: Array.from(this.createdDatabases)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
package/src/protocol.js
CHANGED
|
@@ -134,36 +134,59 @@ export function extractDatabaseName(data) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Pre-allocated buffer pool for startup message parsing (avoids allocation per connection)
|
|
138
|
+
const STARTUP_BUFFER_SIZE = 8192; // Max startup message is typically < 1KB
|
|
139
|
+
const bufferPool = [];
|
|
140
|
+
const MAX_POOL_SIZE = 100;
|
|
141
|
+
|
|
142
|
+
function acquireBuffer() {
|
|
143
|
+
return bufferPool.pop() || Buffer.allocUnsafe(STARTUP_BUFFER_SIZE);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function releaseBuffer(buf) {
|
|
147
|
+
if (bufferPool.length < MAX_POOL_SIZE) {
|
|
148
|
+
bufferPool.push(buf);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
/**
|
|
138
153
|
* Read startup message from socket and buffer it
|
|
154
|
+
* OPTIMIZED: Uses pre-allocated buffer pool to avoid allocation per connection
|
|
139
155
|
*
|
|
140
156
|
* @param {net.Socket} socket - TCP socket
|
|
141
157
|
* @returns {Promise<{message: Buffer, allData: Buffer}>} Startup message and all buffered data
|
|
142
158
|
*/
|
|
143
159
|
export async function readStartupMessage(socket) {
|
|
144
160
|
return new Promise((resolve, reject) => {
|
|
145
|
-
|
|
161
|
+
const buffer = acquireBuffer();
|
|
162
|
+
let offset = 0;
|
|
146
163
|
let expectedLength = null;
|
|
147
164
|
let resolved = false;
|
|
148
165
|
|
|
149
166
|
const onData = (chunk) => {
|
|
150
167
|
if (resolved) return;
|
|
151
168
|
|
|
152
|
-
buffer
|
|
169
|
+
// Copy chunk into pre-allocated buffer (avoids Buffer.concat allocation)
|
|
170
|
+
const copyLen = Math.min(chunk.length, STARTUP_BUFFER_SIZE - offset);
|
|
171
|
+
chunk.copy(buffer, offset, 0, copyLen);
|
|
172
|
+
offset += copyLen;
|
|
153
173
|
|
|
154
174
|
// Read expected length from first 4 bytes
|
|
155
|
-
if (expectedLength === null &&
|
|
175
|
+
if (expectedLength === null && offset >= 4) {
|
|
156
176
|
expectedLength = buffer.readInt32BE(0);
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
// Check if we have full message
|
|
160
|
-
if (expectedLength !== null &&
|
|
180
|
+
if (expectedLength !== null && offset >= expectedLength) {
|
|
161
181
|
resolved = true;
|
|
162
182
|
socket.removeListener('data', onData);
|
|
163
183
|
socket.removeListener('error', onError);
|
|
164
184
|
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
// Create result buffers (need to copy since we're reusing pool buffer)
|
|
186
|
+
const message = Buffer.from(buffer.subarray(0, expectedLength));
|
|
187
|
+
const allData = Buffer.from(buffer.subarray(0, offset));
|
|
188
|
+
releaseBuffer(buffer);
|
|
189
|
+
resolve({ message, allData });
|
|
167
190
|
}
|
|
168
191
|
};
|
|
169
192
|
|
|
@@ -172,6 +195,7 @@ export async function readStartupMessage(socket) {
|
|
|
172
195
|
resolved = true;
|
|
173
196
|
socket.removeListener('data', onData);
|
|
174
197
|
socket.removeListener('error', onError);
|
|
198
|
+
releaseBuffer(buffer);
|
|
175
199
|
reject(error);
|
|
176
200
|
};
|
|
177
201
|
|
|
@@ -187,6 +211,7 @@ export async function readStartupMessage(socket) {
|
|
|
187
211
|
resolved = true;
|
|
188
212
|
socket.removeListener('data', onData);
|
|
189
213
|
socket.removeListener('error', onError);
|
|
214
|
+
releaseBuffer(buffer);
|
|
190
215
|
reject(new Error('Timeout reading startup message'));
|
|
191
216
|
}, 2000);
|
|
192
217
|
});
|