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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgserve",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -12,14 +12,12 @@
|
|
|
12
12
|
"test": "node --test tests/**/*.test.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
|
-
"pglite",
|
|
16
15
|
"postgresql",
|
|
17
16
|
"embedded",
|
|
18
17
|
"database",
|
|
19
|
-
"
|
|
20
|
-
"electron",
|
|
18
|
+
"postgres",
|
|
21
19
|
"development",
|
|
22
|
-
"multi-
|
|
20
|
+
"multi-tenant"
|
|
23
21
|
],
|
|
24
22
|
"author": "Namastex Labs <labs@namastex.com>",
|
|
25
23
|
"license": "MIT",
|
|
@@ -32,14 +30,19 @@
|
|
|
32
30
|
},
|
|
33
31
|
"homepage": "https://github.com/namastexlabs/pgserve#readme",
|
|
34
32
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"@electric-sql/pglite-socket": "^0.0.19",
|
|
33
|
+
"pg": "^8.16.3",
|
|
37
34
|
"pino": "^10.1.0",
|
|
38
35
|
"pino-pretty": "^13.1.2"
|
|
39
36
|
},
|
|
37
|
+
"optionalDependencies": {
|
|
38
|
+
"@embedded-postgres/linux-x64": "17.7.0-beta.15",
|
|
39
|
+
"@embedded-postgres/darwin-arm64": "17.7.0-beta.15",
|
|
40
|
+
"@embedded-postgres/darwin-x64": "17.7.0-beta.15",
|
|
41
|
+
"@embedded-postgres/win32-x64": "17.7.0-beta.15"
|
|
42
|
+
},
|
|
40
43
|
"devDependencies": {
|
|
41
|
-
"better-sqlite3": "^11.
|
|
42
|
-
"
|
|
44
|
+
"better-sqlite3": "^11.7.0",
|
|
45
|
+
"@electric-sql/pglite": "^0.2.17"
|
|
43
46
|
},
|
|
44
47
|
"engines": {
|
|
45
48
|
"node": ">=18.0.0"
|
package/src/cluster.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster Mode for pgserve
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - PRIMARY process: Runs single embedded PostgreSQL instance
|
|
6
|
+
* - WORKER processes: Only run TCP routing to PRIMARY's PostgreSQL
|
|
7
|
+
*
|
|
8
|
+
* This enables multi-core scaling (3-5x throughput on multi-core systems)
|
|
9
|
+
* while maintaining a single PostgreSQL instance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import cluster from 'cluster';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import net from 'net';
|
|
15
|
+
import pg from 'pg';
|
|
16
|
+
import pino from 'pino';
|
|
17
|
+
import { PostgresManager } from './postgres.js';
|
|
18
|
+
import { extractDatabaseNameFromSocket } from './protocol.js';
|
|
19
|
+
import { EventEmitter } from 'events';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ClusterRouter - Lightweight TCP router for worker processes
|
|
23
|
+
* Does NOT start PostgreSQL - connects to PRIMARY's PostgreSQL via Unix socket
|
|
24
|
+
*/
|
|
25
|
+
class ClusterRouter extends EventEmitter {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
super();
|
|
28
|
+
this.port = options.port || 5432;
|
|
29
|
+
this.host = options.host || '127.0.0.1';
|
|
30
|
+
this.pgSocketPath = options.pgSocketPath; // From PRIMARY
|
|
31
|
+
this.pgPort = options.pgPort;
|
|
32
|
+
this.pgUser = options.pgUser || 'postgres';
|
|
33
|
+
this.pgPassword = options.pgPassword || 'postgres';
|
|
34
|
+
this.autoProvision = options.autoProvision !== false;
|
|
35
|
+
this.maxConnections = options.maxConnections || 1000;
|
|
36
|
+
|
|
37
|
+
this.logger = pino({ level: options.logLevel || 'info' });
|
|
38
|
+
this.adminClient = null;
|
|
39
|
+
this.server = null;
|
|
40
|
+
this.connections = new Set();
|
|
41
|
+
this.setMaxListeners(this.maxConnections + 10);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
optimizeSocket(socket) {
|
|
45
|
+
socket.setNoDelay(true);
|
|
46
|
+
socket.setKeepAlive(true, 60000);
|
|
47
|
+
socket.setTimeout(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async start() {
|
|
51
|
+
// Admin connection for auto-provisioning databases
|
|
52
|
+
if (this.autoProvision) {
|
|
53
|
+
let connectionConfig;
|
|
54
|
+
if (this.pgSocketPath) {
|
|
55
|
+
// pg library expects socket DIRECTORY as host, it appends .s.PGSQL.<port>
|
|
56
|
+
// Socket path format: /tmp/pgserve-sock-xxx/.s.PGSQL.<port>
|
|
57
|
+
// Extract directory by removing the socket file suffix
|
|
58
|
+
const socketDir = this.pgSocketPath.replace(/\/\.s\.PGSQL\.\d+$/, '');
|
|
59
|
+
connectionConfig = {
|
|
60
|
+
host: socketDir,
|
|
61
|
+
port: this.pgPort,
|
|
62
|
+
database: 'postgres',
|
|
63
|
+
user: this.pgUser,
|
|
64
|
+
password: this.pgPassword
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
connectionConfig = {
|
|
68
|
+
host: '127.0.0.1',
|
|
69
|
+
port: this.pgPort,
|
|
70
|
+
database: 'postgres',
|
|
71
|
+
user: this.pgUser,
|
|
72
|
+
password: this.pgPassword
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.adminClient = new pg.Client(connectionConfig);
|
|
77
|
+
await this.adminClient.connect();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
this.server = net.createServer({
|
|
82
|
+
allowHalfOpen: false,
|
|
83
|
+
pauseOnConnect: true
|
|
84
|
+
}, (socket) => this.handleConnection(socket));
|
|
85
|
+
|
|
86
|
+
this.server.maxConnections = this.maxConnections;
|
|
87
|
+
|
|
88
|
+
this.server.on('error', (error) => {
|
|
89
|
+
this.logger.error({ err: error }, 'Router error');
|
|
90
|
+
this.emit('error', error);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
this.server.listen(this.port, this.host, () => {
|
|
94
|
+
this.emit('listening');
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async createDatabase(dbName) {
|
|
101
|
+
if (!this.autoProvision || !this.adminClient) return;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = await this.adminClient.query(
|
|
105
|
+
'SELECT 1 FROM pg_database WHERE datname = $1',
|
|
106
|
+
[dbName]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (result.rows.length === 0) {
|
|
110
|
+
await this.adminClient.query(`CREATE DATABASE "${dbName}"`);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Ignore "already exists" (race condition between workers)
|
|
114
|
+
if (!error.message.includes('already exists')) {
|
|
115
|
+
this.logger.error({ database: dbName, err: error }, 'Failed to create database');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async handleConnection(socket) {
|
|
121
|
+
this.connections.add(socket);
|
|
122
|
+
this.optimizeSocket(socket);
|
|
123
|
+
|
|
124
|
+
let dbName = null;
|
|
125
|
+
let pgSocket = null;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const { dbName: extractedDbName, buffered } = await extractDatabaseNameFromSocket(socket);
|
|
129
|
+
dbName = extractedDbName;
|
|
130
|
+
|
|
131
|
+
await this.createDatabase(dbName);
|
|
132
|
+
|
|
133
|
+
// Connect to PRIMARY's PostgreSQL
|
|
134
|
+
if (this.pgSocketPath) {
|
|
135
|
+
pgSocket = net.connect({ path: this.pgSocketPath });
|
|
136
|
+
} else {
|
|
137
|
+
pgSocket = net.connect({ host: '127.0.0.1', port: this.pgPort });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await new Promise((resolve, reject) => {
|
|
141
|
+
pgSocket.once('connect', resolve);
|
|
142
|
+
pgSocket.once('error', reject);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.optimizeSocket(pgSocket);
|
|
146
|
+
pgSocket.write(buffered);
|
|
147
|
+
socket.resume();
|
|
148
|
+
|
|
149
|
+
// Bidirectional pipe
|
|
150
|
+
socket.pipe(pgSocket);
|
|
151
|
+
pgSocket.pipe(socket);
|
|
152
|
+
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
this.connections.delete(socket);
|
|
155
|
+
if (pgSocket && !pgSocket.destroyed) pgSocket.destroy();
|
|
156
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
socket.once('close', cleanup);
|
|
160
|
+
socket.once('error', cleanup);
|
|
161
|
+
pgSocket.once('close', () => {
|
|
162
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
163
|
+
});
|
|
164
|
+
pgSocket.once('error', cleanup);
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.logger.error({ dbName, err: error }, 'Connection error');
|
|
168
|
+
if (pgSocket && !pgSocket.destroyed) pgSocket.destroy();
|
|
169
|
+
socket.destroy();
|
|
170
|
+
this.connections.delete(socket);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async stop() {
|
|
175
|
+
for (const socket of this.connections) {
|
|
176
|
+
socket.end();
|
|
177
|
+
}
|
|
178
|
+
this.connections.clear();
|
|
179
|
+
|
|
180
|
+
if (this.adminClient) {
|
|
181
|
+
await this.adminClient.end();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.server) {
|
|
185
|
+
await new Promise((resolve) => this.server.close(resolve));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Start pgserve in cluster mode
|
|
192
|
+
*/
|
|
193
|
+
export async function startClusterServer(options = {}) {
|
|
194
|
+
const numWorkers = options.workers || os.cpus().length;
|
|
195
|
+
const port = options.port || 5432;
|
|
196
|
+
const host = options.host || '127.0.0.1';
|
|
197
|
+
const pgPort = options.pgPort || (port + 1000);
|
|
198
|
+
|
|
199
|
+
if (cluster.isPrimary) {
|
|
200
|
+
console.log(`[pgserve] Cluster mode: ${numWorkers} workers`);
|
|
201
|
+
|
|
202
|
+
// PRIMARY: Start our embedded PostgreSQL (single instance)
|
|
203
|
+
const logger = pino({ level: options.logLevel || 'info' });
|
|
204
|
+
const pgManager = new PostgresManager({
|
|
205
|
+
dataDir: options.baseDir,
|
|
206
|
+
port: pgPort,
|
|
207
|
+
logger: logger.child({ component: 'postgres' })
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await pgManager.start();
|
|
211
|
+
const pgSocketPath = pgManager.getSocketPath();
|
|
212
|
+
|
|
213
|
+
console.log(`[pgserve] Embedded PostgreSQL started`);
|
|
214
|
+
console.log(`[pgserve] Socket: ${pgSocketPath || `TCP port ${pgPort}`}`);
|
|
215
|
+
|
|
216
|
+
const workers = new Map();
|
|
217
|
+
|
|
218
|
+
// Fork workers with PostgreSQL connection info
|
|
219
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
220
|
+
const worker = cluster.fork({
|
|
221
|
+
PGSERVE_WORKER: 'true',
|
|
222
|
+
PGSERVE_PORT: String(port),
|
|
223
|
+
PGSERVE_HOST: host,
|
|
224
|
+
PGSERVE_PG_SOCKET: pgSocketPath || '',
|
|
225
|
+
PGSERVE_PG_PORT: String(pgPort),
|
|
226
|
+
PGSERVE_PG_USER: 'postgres',
|
|
227
|
+
PGSERVE_PG_PASSWORD: 'postgres',
|
|
228
|
+
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
229
|
+
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
|
|
230
|
+
});
|
|
231
|
+
workers.set(worker.id, worker);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Restart dead workers
|
|
235
|
+
cluster.on('exit', (worker, code, signal) => {
|
|
236
|
+
console.log(`[pgserve] Worker ${worker.id} died (${signal || code}), restarting...`);
|
|
237
|
+
workers.delete(worker.id);
|
|
238
|
+
|
|
239
|
+
const newWorker = cluster.fork({
|
|
240
|
+
PGSERVE_WORKER: 'true',
|
|
241
|
+
PGSERVE_PORT: String(port),
|
|
242
|
+
PGSERVE_HOST: host,
|
|
243
|
+
PGSERVE_PG_SOCKET: pgSocketPath || '',
|
|
244
|
+
PGSERVE_PG_PORT: String(pgPort),
|
|
245
|
+
PGSERVE_PG_USER: 'postgres',
|
|
246
|
+
PGSERVE_PG_PASSWORD: 'postgres',
|
|
247
|
+
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
248
|
+
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false'
|
|
249
|
+
});
|
|
250
|
+
workers.set(newWorker.id, newWorker);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Wait for workers to be ready
|
|
254
|
+
let readyCount = 0;
|
|
255
|
+
await new Promise((resolve) => {
|
|
256
|
+
cluster.on('message', (worker, message) => {
|
|
257
|
+
if (message.type === 'ready') {
|
|
258
|
+
readyCount++;
|
|
259
|
+
if (readyCount === numWorkers) resolve();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
console.log(`[pgserve] All ${numWorkers} workers ready`);
|
|
265
|
+
console.log(`[pgserve] Listening on ${host}:${port}`);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
workers,
|
|
269
|
+
pgPort,
|
|
270
|
+
pgSocketPath,
|
|
271
|
+
stop: async () => {
|
|
272
|
+
console.log('[pgserve] Stopping cluster...');
|
|
273
|
+
for (const worker of workers.values()) {
|
|
274
|
+
worker.send({ type: 'shutdown' });
|
|
275
|
+
}
|
|
276
|
+
await new Promise((resolve) => {
|
|
277
|
+
const check = setInterval(() => {
|
|
278
|
+
if (workers.size === 0) {
|
|
279
|
+
clearInterval(check);
|
|
280
|
+
resolve();
|
|
281
|
+
}
|
|
282
|
+
}, 100);
|
|
283
|
+
});
|
|
284
|
+
await pgManager.stop();
|
|
285
|
+
console.log('[pgserve] Cluster stopped');
|
|
286
|
+
},
|
|
287
|
+
getStats: () => ({
|
|
288
|
+
workers: workers.size,
|
|
289
|
+
pids: Array.from(workers.values()).map(w => w.process.pid)
|
|
290
|
+
})
|
|
291
|
+
};
|
|
292
|
+
} else {
|
|
293
|
+
// WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL
|
|
294
|
+
const router = new ClusterRouter({
|
|
295
|
+
port: parseInt(process.env.PGSERVE_PORT) || 5432,
|
|
296
|
+
host: process.env.PGSERVE_HOST || '127.0.0.1',
|
|
297
|
+
pgSocketPath: process.env.PGSERVE_PG_SOCKET || null,
|
|
298
|
+
pgPort: parseInt(process.env.PGSERVE_PG_PORT) || 6432,
|
|
299
|
+
pgUser: process.env.PGSERVE_PG_USER || 'postgres',
|
|
300
|
+
pgPassword: process.env.PGSERVE_PG_PASSWORD || 'postgres',
|
|
301
|
+
logLevel: process.env.PGSERVE_LOG_LEVEL || 'info',
|
|
302
|
+
autoProvision: process.env.PGSERVE_AUTO_PROVISION === 'true'
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await router.start();
|
|
306
|
+
|
|
307
|
+
// Tell PRIMARY we're ready
|
|
308
|
+
process.send({ type: 'ready' });
|
|
309
|
+
|
|
310
|
+
// Handle shutdown
|
|
311
|
+
process.on('message', async (message) => {
|
|
312
|
+
if (message.type === 'shutdown') {
|
|
313
|
+
await router.stop();
|
|
314
|
+
process.exit(0);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return router;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default startClusterServer;
|
package/src/index.js
CHANGED
|
@@ -1,177 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pgserve - PostgreSQL
|
|
2
|
+
* pgserve - Embedded PostgreSQL Server
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* True concurrent connections, zero config, auto-provision databases.
|
|
5
|
+
* Uses embedded-postgres (real PostgreSQL binaries).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// Main exports
|
|
9
9
|
export { MultiTenantRouter, startMultiTenantServer } from './router.js';
|
|
10
|
-
export {
|
|
10
|
+
export { PostgresManager } from './postgres.js';
|
|
11
|
+
export { SyncManager } from './sync.js';
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
import { allocatePort, getPortRangeInfo } from './ports.js';
|
|
15
|
-
import {
|
|
16
|
-
findInstanceByDataDir,
|
|
17
|
-
findInstanceByPort,
|
|
18
|
-
listInstances,
|
|
19
|
-
cleanupStaleInstances
|
|
20
|
-
} from './registry.js';
|
|
21
|
-
import { autoDetect as _autoDetect } from './detector.js';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Start a new PGlite server instance
|
|
25
|
-
*
|
|
26
|
-
* @param {Object} options
|
|
27
|
-
* @param {string} options.dataDir - Data directory for the database
|
|
28
|
-
* @param {number} [options.port] - Specific port (optional, auto-allocated if not provided)
|
|
29
|
-
* @param {boolean} [options.autoPort=true] - Auto-allocate port if specified port is unavailable
|
|
30
|
-
* @param {string} [options.logLevel='info'] - Log level (error, warn, info, debug)
|
|
31
|
-
* @returns {Promise<Object>} Server instance
|
|
32
|
-
*/
|
|
33
|
-
export async function startServer({ dataDir, port, autoPort = true, logLevel = 'info' }) {
|
|
34
|
-
// Allocate port (checks for existing instance, reuses if running)
|
|
35
|
-
const allocatedPort = await allocatePort(dataDir, port);
|
|
36
|
-
|
|
37
|
-
if (port && allocatedPort !== port && !autoPort) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Port ${port} unavailable and autoPort is disabled. ` +
|
|
40
|
-
`Use autoPort: true or choose a different port.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return _startServer({ dataDir, port: allocatedPort, logLevel });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Stop a running server instance
|
|
49
|
-
*
|
|
50
|
-
* @param {Object} options
|
|
51
|
-
* @param {string} [options.dataDir] - Data directory of the instance to stop
|
|
52
|
-
* @param {number} [options.port] - Port of the instance to stop
|
|
53
|
-
*/
|
|
54
|
-
export async function stopServer({ dataDir, port }) {
|
|
55
|
-
return _stopServer({ dataDir, port });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get an existing instance or start a new one
|
|
60
|
-
*
|
|
61
|
-
* This is the recommended way to start a server, as it prevents
|
|
62
|
-
* duplicate instances for the same data directory.
|
|
63
|
-
*
|
|
64
|
-
* @param {Object} options
|
|
65
|
-
* @param {string} options.dataDir - Data directory for the database
|
|
66
|
-
* @param {number} [options.port] - Preferred port (auto-allocated if unavailable)
|
|
67
|
-
* @param {boolean} [options.autoPort=true] - Auto-allocate port
|
|
68
|
-
* @param {string} [options.logLevel='info'] - Log level
|
|
69
|
-
* @returns {Promise<Object>} Server instance (existing or new)
|
|
70
|
-
*/
|
|
71
|
-
export async function getOrStart({ dataDir, port, autoPort = true, logLevel = 'info' }) {
|
|
72
|
-
// Check if instance already running
|
|
73
|
-
const existing = findInstanceByDataDir(dataDir);
|
|
74
|
-
|
|
75
|
-
if (existing) {
|
|
76
|
-
// Verify process is still alive
|
|
77
|
-
try {
|
|
78
|
-
process.kill(existing.pid, 0);
|
|
79
|
-
console.log(`✅ Using existing instance on port ${existing.port}`);
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
port: existing.port,
|
|
83
|
-
dataDir,
|
|
84
|
-
pid: existing.pid,
|
|
85
|
-
connectionUrl: `postgresql://localhost:${existing.port}`,
|
|
86
|
-
existing: true
|
|
87
|
-
};
|
|
88
|
-
} catch {
|
|
89
|
-
// Process dead, cleanup and start new
|
|
90
|
-
console.log('🔄 Stale instance found, starting fresh...');
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Start new instance
|
|
95
|
-
return startServer({ dataDir, port, autoPort, logLevel });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Auto-detect database configuration
|
|
100
|
-
*
|
|
101
|
-
* Tries external PostgreSQL first, falls back to embedded PGlite
|
|
102
|
-
*
|
|
103
|
-
* @param {Object} options
|
|
104
|
-
* @param {string} [options.externalUrl] - External PostgreSQL URL to try first
|
|
105
|
-
* @param {string} options.embeddedDataDir - Data directory for embedded fallback
|
|
106
|
-
* @param {number} [options.embeddedPort] - Preferred port for embedded server
|
|
107
|
-
* @param {number} [options.timeout=5000] - Timeout for external connection test
|
|
108
|
-
* @returns {Promise<Object>} Database configuration
|
|
109
|
-
*/
|
|
110
|
-
export async function autoDetect({
|
|
111
|
-
externalUrl,
|
|
112
|
-
embeddedDataDir,
|
|
113
|
-
embeddedPort,
|
|
114
|
-
timeout = 5000
|
|
115
|
-
}) {
|
|
116
|
-
return _autoDetect({ externalUrl, embeddedDataDir, embeddedPort, timeout });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* List all running instances
|
|
121
|
-
*
|
|
122
|
-
* @returns {Array<Object>} Array of instance info
|
|
123
|
-
*/
|
|
124
|
-
export function list() {
|
|
125
|
-
return listInstances();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Find instance by data directory
|
|
130
|
-
*
|
|
131
|
-
* @param {string} dataDir - Data directory path
|
|
132
|
-
* @returns {Object|null} Instance info or null
|
|
133
|
-
*/
|
|
134
|
-
export function findByDataDir(dataDir) {
|
|
135
|
-
return findInstanceByDataDir(dataDir);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Find instance by port
|
|
140
|
-
*
|
|
141
|
-
* @param {number} port - Port number
|
|
142
|
-
* @returns {Object|null} Instance info or null
|
|
143
|
-
*/
|
|
144
|
-
export function findByPort(port) {
|
|
145
|
-
return findInstanceByPort(port);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Get port range information
|
|
150
|
-
*
|
|
151
|
-
* @returns {Object} Port range stats
|
|
152
|
-
*/
|
|
153
|
-
export function portInfo() {
|
|
154
|
-
return getPortRangeInfo();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Cleanup stale instances (dead processes)
|
|
159
|
-
*
|
|
160
|
-
* @returns {number} Number of instances cleaned up
|
|
161
|
-
*/
|
|
162
|
-
export function cleanup() {
|
|
163
|
-
return cleanupStaleInstances();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Export all functions
|
|
167
|
-
export default {
|
|
168
|
-
startServer,
|
|
169
|
-
stopServer,
|
|
170
|
-
getOrStart,
|
|
171
|
-
autoDetect,
|
|
172
|
-
list,
|
|
173
|
-
findByDataDir,
|
|
174
|
-
findByPort,
|
|
175
|
-
portInfo,
|
|
176
|
-
cleanup
|
|
177
|
-
};
|
|
13
|
+
// Default export
|
|
14
|
+
export { startMultiTenantServer as default } from './router.js';
|