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/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
- }