pgserve 0.1.5 ā 1.0.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.
- package/.claude/settings.local.json +11 -0
- package/.husky/pre-commit +2 -0
- package/README.md +281 -256
- package/bin/pglite-server.js +214 -396
- package/eslint.config.js +54 -0
- package/knip.json +8 -0
- package/package.json +32 -11
- package/src/cluster.js +320 -0
- package/src/dashboard.js +211 -0
- package/src/index.js +10 -171
- package/src/postgres.js +478 -0
- package/src/protocol.js +31 -7
- package/src/restore.js +587 -0
- package/src/router.js +172 -115
- package/src/sync.js +342 -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/registry.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
|
|
5
|
-
const REGISTRY_DIR = path.join(os.homedir(), '.pglite-server');
|
|
6
|
-
const REGISTRY_FILE = path.join(REGISTRY_DIR, 'registry.json');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Ensure registry directory exists
|
|
10
|
-
*/
|
|
11
|
-
function ensureRegistryDir() {
|
|
12
|
-
if (!fs.existsSync(REGISTRY_DIR)) {
|
|
13
|
-
fs.mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Load registry from disk
|
|
19
|
-
*/
|
|
20
|
-
export function loadRegistry() {
|
|
21
|
-
ensureRegistryDir();
|
|
22
|
-
|
|
23
|
-
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
24
|
-
return { instances: {} };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const data = fs.readFileSync(REGISTRY_FILE, 'utf-8');
|
|
29
|
-
return JSON.parse(data);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.warn('Failed to load registry, creating new:', error.message);
|
|
32
|
-
return { instances: {} };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Save registry to disk
|
|
38
|
-
*/
|
|
39
|
-
export function saveRegistry(registry) {
|
|
40
|
-
ensureRegistryDir();
|
|
41
|
-
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2), 'utf-8');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Register a new instance
|
|
46
|
-
*/
|
|
47
|
-
export function registerInstance(dataDir, port, pid) {
|
|
48
|
-
const registry = loadRegistry();
|
|
49
|
-
|
|
50
|
-
registry.instances[dataDir] = {
|
|
51
|
-
port,
|
|
52
|
-
pid,
|
|
53
|
-
started: new Date().toISOString(),
|
|
54
|
-
version: '17.5' // PGlite version
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
saveRegistry(registry);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Unregister an instance
|
|
62
|
-
*/
|
|
63
|
-
export function unregisterInstance(dataDir) {
|
|
64
|
-
const registry = loadRegistry();
|
|
65
|
-
delete registry.instances[dataDir];
|
|
66
|
-
saveRegistry(registry);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Find instance by data directory
|
|
71
|
-
*/
|
|
72
|
-
export function findInstanceByDataDir(dataDir) {
|
|
73
|
-
const registry = loadRegistry();
|
|
74
|
-
return registry.instances[dataDir] || null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Find instance by port
|
|
79
|
-
*/
|
|
80
|
-
export function findInstanceByPort(port) {
|
|
81
|
-
const registry = loadRegistry();
|
|
82
|
-
|
|
83
|
-
for (const [dataDir, instance] of Object.entries(registry.instances)) {
|
|
84
|
-
if (instance.port === port) {
|
|
85
|
-
return { dataDir, ...instance };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* List all instances
|
|
94
|
-
*/
|
|
95
|
-
export function listInstances() {
|
|
96
|
-
const registry = loadRegistry();
|
|
97
|
-
return Object.entries(registry.instances).map(([dataDir, instance]) => ({
|
|
98
|
-
dataDir,
|
|
99
|
-
...instance
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Check if process is running
|
|
105
|
-
*/
|
|
106
|
-
export function isProcessRunning(pid) {
|
|
107
|
-
try {
|
|
108
|
-
process.kill(pid, 0);
|
|
109
|
-
return true;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Cleanup stale instances (process not running)
|
|
117
|
-
*/
|
|
118
|
-
export function cleanupStaleInstances() {
|
|
119
|
-
const registry = loadRegistry();
|
|
120
|
-
let cleaned = 0;
|
|
121
|
-
|
|
122
|
-
for (const [dataDir, instance] of Object.entries(registry.instances)) {
|
|
123
|
-
if (!isProcessRunning(instance.pid)) {
|
|
124
|
-
delete registry.instances[dataDir];
|
|
125
|
-
cleaned++;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (cleaned > 0) {
|
|
130
|
-
saveRegistry(registry);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return cleaned;
|
|
134
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { PGlite } from '@electric-sql/pglite';
|
|
2
|
-
import { PGLiteSocketServer } from '@electric-sql/pglite-socket';
|
|
3
|
-
import { Worker } from 'worker_threads';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import { registerInstance, unregisterInstance } from './registry.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Get optimal configuration based on system resources
|
|
11
|
-
*/
|
|
12
|
-
function getOptimalConfig() {
|
|
13
|
-
const cpus = os.cpus().length;
|
|
14
|
-
const totalMem = os.totalmem();
|
|
15
|
-
const freeMem = os.freemem();
|
|
16
|
-
|
|
17
|
-
// Workers: Use 50% of cores (leave room for app), minimum 1
|
|
18
|
-
const workers = Math.max(1, Math.floor(cpus / 2));
|
|
19
|
-
|
|
20
|
-
// Pool size: Based on available memory
|
|
21
|
-
const poolSize = totalMem > 8 * 1024 * 1024 * 1024 ? 20 : 10;
|
|
22
|
-
|
|
23
|
-
// Cache: 10% of free memory, max 512MB
|
|
24
|
-
const cacheSize = Math.min(512, Math.floor((freeMem / 10) / (1024 * 1024)));
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
workers,
|
|
28
|
-
poolSize,
|
|
29
|
-
cacheSize,
|
|
30
|
-
cpus,
|
|
31
|
-
totalMemGB: (totalMem / (1024 ** 3)).toFixed(1),
|
|
32
|
-
freeMemGB: (freeMem / (1024 ** 3)).toFixed(1)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Create lock file for instance
|
|
38
|
-
*/
|
|
39
|
-
function createLockFile(dataDir, port, pid) {
|
|
40
|
-
const lockFile = path.join(dataDir, '.pglite-server.lock');
|
|
41
|
-
|
|
42
|
-
fs.writeFileSync(
|
|
43
|
-
lockFile,
|
|
44
|
-
JSON.stringify(
|
|
45
|
-
{
|
|
46
|
-
pid,
|
|
47
|
-
port,
|
|
48
|
-
started: new Date().toISOString()
|
|
49
|
-
},
|
|
50
|
-
null,
|
|
51
|
-
2
|
|
52
|
-
)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return lockFile;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Remove lock file
|
|
60
|
-
*/
|
|
61
|
-
function removeLockFile(dataDir) {
|
|
62
|
-
const lockFile = path.join(dataDir, '.pglite-server.lock');
|
|
63
|
-
|
|
64
|
-
if (fs.existsSync(lockFile)) {
|
|
65
|
-
fs.unlinkSync(lockFile);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Check if instance is locked
|
|
71
|
-
*/
|
|
72
|
-
function checkLockFile(dataDir) {
|
|
73
|
-
const lockFile = path.join(dataDir, '.pglite-server.lock');
|
|
74
|
-
|
|
75
|
-
if (!fs.existsSync(lockFile)) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const lock = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
|
81
|
-
|
|
82
|
-
// Check if process is still running
|
|
83
|
-
try {
|
|
84
|
-
process.kill(lock.pid, 0);
|
|
85
|
-
return lock; // Process running, lock valid
|
|
86
|
-
} catch {
|
|
87
|
-
// Process dead, remove stale lock
|
|
88
|
-
removeLockFile(dataDir);
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.warn('Invalid lock file:', error.message);
|
|
93
|
-
removeLockFile(dataDir);
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Start PGlite server with adaptive mode (auto-tuned for hardware)
|
|
100
|
-
*
|
|
101
|
-
* @param {Object} options
|
|
102
|
-
* @param {string} options.dataDir - Data directory (required)
|
|
103
|
-
* @param {number} options.port - Port to listen on (required)
|
|
104
|
-
* @param {string} [options.logLevel='info'] - Log level (error, warn, info, debug)
|
|
105
|
-
* @returns {Promise<Object>} Server instance
|
|
106
|
-
*/
|
|
107
|
-
export async function startServer({ dataDir, port, logLevel = 'info' }) {
|
|
108
|
-
// Get optimal configuration for this machine
|
|
109
|
-
const config = getOptimalConfig();
|
|
110
|
-
|
|
111
|
-
console.log('šļø Auto-tuned configuration:');
|
|
112
|
-
console.log(` ⢠CPUs: ${config.cpus} (using ${config.workers} workers)`);
|
|
113
|
-
console.log(` ⢠Memory: ${config.totalMemGB}GB total, ${config.freeMemGB}GB free`);
|
|
114
|
-
console.log(` ⢠Pool size: ${config.poolSize} connections`);
|
|
115
|
-
console.log(` ⢠Cache: ${config.cacheSize}MB`);
|
|
116
|
-
|
|
117
|
-
// Resolve absolute path
|
|
118
|
-
const absoluteDataDir = path.resolve(dataDir);
|
|
119
|
-
|
|
120
|
-
// Check for existing lock
|
|
121
|
-
const existingLock = checkLockFile(absoluteDataDir);
|
|
122
|
-
if (existingLock) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`Instance already running for ${absoluteDataDir} ` +
|
|
125
|
-
`(PID ${existingLock.pid}, port ${existingLock.port})`
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Ensure data directory exists
|
|
130
|
-
if (!fs.existsSync(absoluteDataDir)) {
|
|
131
|
-
fs.mkdirSync(absoluteDataDir, { recursive: true });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Create PGlite instance
|
|
135
|
-
console.log(`š Initializing PGlite database in ${absoluteDataDir}...`);
|
|
136
|
-
const db = new PGlite(absoluteDataDir);
|
|
137
|
-
|
|
138
|
-
// Wait for PGlite to be ready
|
|
139
|
-
await db.waitReady;
|
|
140
|
-
|
|
141
|
-
// Create socket server
|
|
142
|
-
const server = new PGLiteSocketServer({
|
|
143
|
-
db,
|
|
144
|
-
port,
|
|
145
|
-
host: '127.0.0.1',
|
|
146
|
-
inspect: logLevel === 'debug' // Enable protocol inspection in debug mode
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Start server
|
|
150
|
-
await server.start();
|
|
151
|
-
|
|
152
|
-
console.log(`ā
PGlite server running on postgresql://localhost:${port}`);
|
|
153
|
-
console.log(`š Data directory: ${absoluteDataDir}`);
|
|
154
|
-
console.log(`ā” Mode: Adaptive (${config.workers} ${config.workers === 1 ? 'worker' : 'workers'})`);
|
|
155
|
-
|
|
156
|
-
// Add permanent error handler for server lifetime (if supported)
|
|
157
|
-
if (typeof server.on === 'function') {
|
|
158
|
-
server.on('error', (error) => {
|
|
159
|
-
console.error(`ā ļø Server error on port ${port}:`, error.message);
|
|
160
|
-
// Log but don't crash - PM2 will handle restarts if needed
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Create lock file
|
|
165
|
-
const lockFile = createLockFile(absoluteDataDir, port, process.pid);
|
|
166
|
-
|
|
167
|
-
// Register in global registry
|
|
168
|
-
registerInstance(absoluteDataDir, port, process.pid);
|
|
169
|
-
|
|
170
|
-
// Cleanup on exit
|
|
171
|
-
const cleanup = async () => {
|
|
172
|
-
console.log(`\nš Shutting down server on port ${port}...`);
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
// Stop socket server
|
|
176
|
-
await server.stop();
|
|
177
|
-
} catch (error) {
|
|
178
|
-
console.error('ā ļø Error closing server:', error.message);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
// Close PGlite database (may throw ExitStatus - this is normal for WASM)
|
|
183
|
-
await db.close();
|
|
184
|
-
} catch (error) {
|
|
185
|
-
// ExitStatus errors are expected during WASM cleanup
|
|
186
|
-
if (error.name !== 'ExitStatus') {
|
|
187
|
-
console.error('ā ļø Error closing database:', error.message);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
removeLockFile(absoluteDataDir);
|
|
193
|
-
unregisterInstance(absoluteDataDir);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
console.error('ā ļø Error removing lock/registry:', error.message);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
console.log('ā
Server stopped gracefully');
|
|
199
|
-
process.exit(0);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Wrap async cleanup to handle promise rejections
|
|
203
|
-
const handleShutdown = () => {
|
|
204
|
-
cleanup().catch((error) => {
|
|
205
|
-
console.error('ā Fatal error during shutdown:', error);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
process.on('SIGINT', handleShutdown);
|
|
211
|
-
process.on('SIGTERM', handleShutdown);
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
server,
|
|
215
|
-
db,
|
|
216
|
-
port,
|
|
217
|
-
dataDir: absoluteDataDir,
|
|
218
|
-
pid: process.pid,
|
|
219
|
-
lockFile,
|
|
220
|
-
config,
|
|
221
|
-
connectionUrl: `postgresql://localhost:${port}`,
|
|
222
|
-
|
|
223
|
-
async stop() {
|
|
224
|
-
await cleanup();
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Stop server by data directory or port
|
|
231
|
-
*/
|
|
232
|
-
export async function stopServer({ dataDir, port }) {
|
|
233
|
-
if (dataDir) {
|
|
234
|
-
const absoluteDataDir = path.resolve(dataDir);
|
|
235
|
-
const lock = checkLockFile(absoluteDataDir);
|
|
236
|
-
|
|
237
|
-
if (!lock) {
|
|
238
|
-
throw new Error(`No running instance found for ${absoluteDataDir}`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
process.kill(lock.pid, 'SIGTERM');
|
|
243
|
-
console.log(`ā
Stopped instance at ${absoluteDataDir} (port ${lock.port})`);
|
|
244
|
-
} catch (error) {
|
|
245
|
-
throw new Error(`Failed to stop instance: ${error.message}`);
|
|
246
|
-
}
|
|
247
|
-
} else if (port) {
|
|
248
|
-
// Find instance by port in registry
|
|
249
|
-
const { findInstanceByPort } = await import('./registry.js');
|
|
250
|
-
const instance = findInstanceByPort(port);
|
|
251
|
-
|
|
252
|
-
if (!instance) {
|
|
253
|
-
throw new Error(`No instance found on port ${port}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
process.kill(instance.pid, 'SIGTERM');
|
|
258
|
-
console.log(`ā
Stopped instance on port ${port} (${instance.dataDir})`);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
throw new Error(`Failed to stop instance: ${error.message}`);
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
throw new Error('Must provide either dataDir or port');
|
|
264
|
-
}
|
|
265
|
-
}
|