pgserve 0.1.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/.genie/AGENTS.md +13 -0
- package/.genie/agents/README.md +110 -0
- package/.genie/agents/analyze.md +176 -0
- package/.genie/agents/forge.md +290 -0
- package/.genie/agents/garbage-cleaner.md +324 -0
- package/.genie/agents/garbage-collector.md +596 -0
- package/.genie/agents/github-issue-gc.md +618 -0
- package/.genie/agents/review.md +380 -0
- package/.genie/agents/semantic-analyzer/find-duplicates.md +90 -0
- package/.genie/agents/semantic-analyzer/find-orphans.md +99 -0
- package/.genie/agents/semantic-analyzer.md +101 -0
- package/.genie/agents/update.md +182 -0
- package/.genie/agents/wish.md +357 -0
- package/.genie/code/AGENTS.md +692 -0
- package/.genie/code/agents/audit/risk.md +173 -0
- package/.genie/code/agents/audit/security.md +189 -0
- package/.genie/code/agents/audit.md +145 -0
- package/.genie/code/agents/challenge.md +230 -0
- package/.genie/code/agents/change-reviewer.md +295 -0
- package/.genie/code/agents/code-garbage-collector.md +425 -0
- package/.genie/code/agents/code-quality.md +410 -0
- package/.genie/code/agents/commit-suggester.md +255 -0
- package/.genie/code/agents/commit.md +124 -0
- package/.genie/code/agents/consensus.md +204 -0
- package/.genie/code/agents/daily-standup.md +722 -0
- package/.genie/code/agents/docgen.md +48 -0
- package/.genie/code/agents/explore.md +79 -0
- package/.genie/code/agents/fix.md +100 -0
- package/.genie/code/agents/git/commit-advisory.md +219 -0
- package/.genie/code/agents/git/workflows/issue.md +244 -0
- package/.genie/code/agents/git/workflows/pr.md +179 -0
- package/.genie/code/agents/git/workflows/release.md +460 -0
- package/.genie/code/agents/git/workflows/report.md +342 -0
- package/.genie/code/agents/git.md +432 -0
- package/.genie/code/agents/implementor.md +161 -0
- package/.genie/code/agents/install.md +515 -0
- package/.genie/code/agents/issue-creator.md +344 -0
- package/.genie/code/agents/polish.md +116 -0
- package/.genie/code/agents/qa.md +653 -0
- package/.genie/code/agents/refactor.md +294 -0
- package/.genie/code/agents/release.md +1129 -0
- package/.genie/code/agents/roadmap.md +885 -0
- package/.genie/code/agents/tests.md +557 -0
- package/.genie/code/agents/tracer.md +50 -0
- package/.genie/code/agents/update/upstream-update.md +85 -0
- package/.genie/code/agents/update/versions/generic-update.md +305 -0
- package/.genie/code/agents/vibe.md +1317 -0
- package/.genie/code/spells/agent-configuration.md +58 -0
- package/.genie/code/spells/automated-rc-publishing.md +106 -0
- package/.genie/code/spells/branch-tracker-guidance.md +28 -0
- package/.genie/code/spells/debug.md +320 -0
- package/.genie/code/spells/emoji-naming-convention.md +303 -0
- package/.genie/code/spells/evidence-storage.md +26 -0
- package/.genie/code/spells/file-naming-rules.md +35 -0
- package/.genie/code/spells/forge-code-blueprints.md +195 -0
- package/.genie/code/spells/genie-integration.md +153 -0
- package/.genie/code/spells/publishing-protocol.md +61 -0
- package/.genie/code/spells/team-consultation-protocol.md +284 -0
- package/.genie/code/spells/tool-requirements.md +20 -0
- package/.genie/code/spells/triad-maintenance-protocol.md +154 -0
- package/.genie/code/teams/tech-council/council.md +328 -0
- package/.genie/code/teams/tech-council/jt.md +352 -0
- package/.genie/code/teams/tech-council/nayr.md +305 -0
- package/.genie/code/teams/tech-council/oettam.md +375 -0
- package/.genie/neurons/README.md +193 -0
- package/.genie/neurons/forge.md +106 -0
- package/.genie/neurons/genie.md +63 -0
- package/.genie/neurons/review.md +106 -0
- package/.genie/neurons/wish.md +104 -0
- package/.genie/product/README.md +20 -0
- package/.genie/product/cli-automation.md +359 -0
- package/.genie/product/environment.md +60 -0
- package/.genie/product/mission.md +60 -0
- package/.genie/product/roadmap.md +44 -0
- package/.genie/product/tech-stack.md +34 -0
- package/.genie/product/templates/context-template.md +218 -0
- package/.genie/product/templates/qa-done-report-template.md +68 -0
- package/.genie/product/templates/review-report-template.md +89 -0
- package/.genie/product/templates/wish-template.md +120 -0
- package/.genie/scripts/helpers/analyze-commit.js +195 -0
- package/.genie/scripts/helpers/bullet-counter.js +194 -0
- package/.genie/scripts/helpers/bullet-find.js +289 -0
- package/.genie/scripts/helpers/bullet-id.js +244 -0
- package/.genie/scripts/helpers/check-secrets.js +237 -0
- package/.genie/scripts/helpers/count-tokens.js +200 -0
- package/.genie/scripts/helpers/create-frontmatter.js +456 -0
- package/.genie/scripts/helpers/detect-markers.js +293 -0
- package/.genie/scripts/helpers/detect-todos.js +267 -0
- package/.genie/scripts/helpers/detect-unlabeled-blocks.js +135 -0
- package/.genie/scripts/helpers/embeddings.js +344 -0
- package/.genie/scripts/helpers/find-empty-sections.js +158 -0
- package/.genie/scripts/helpers/index.js +319 -0
- package/.genie/scripts/helpers/validate-frontmatter.js +578 -0
- package/.genie/scripts/helpers/validate-links.js +207 -0
- package/.genie/scripts/helpers/validate-paths.js +373 -0
- package/.genie/spells/README.md +9 -0
- package/.genie/spells/ace-protocol.md +118 -0
- package/.genie/spells/ask-one-at-a-time.md +175 -0
- package/.genie/spells/backup-analyzer.md +542 -0
- package/.genie/spells/blocker.md +12 -0
- package/.genie/spells/break-things-move-fast.md +56 -0
- package/.genie/spells/context-candidates.md +72 -0
- package/.genie/spells/context-critic.md +51 -0
- package/.genie/spells/defer-to-expertise.md +278 -0
- package/.genie/spells/delegate-dont-do.md +292 -0
- package/.genie/spells/error-investigation-protocol.md +328 -0
- package/.genie/spells/evidence-based-completion.md +273 -0
- package/.genie/spells/experiment.md +65 -0
- package/.genie/spells/file-creation-protocol.md +229 -0
- package/.genie/spells/forge-integration.md +281 -0
- package/.genie/spells/forge-orchestration.md +514 -0
- package/.genie/spells/gather-context.md +18 -0
- package/.genie/spells/global-health-check.md +34 -0
- package/.genie/spells/global-noop-roundtrip.md +25 -0
- package/.genie/spells/install-genie.md +1232 -0
- package/.genie/spells/install.md +82 -0
- package/.genie/spells/investigate-before-commit.md +112 -0
- package/.genie/spells/know-yourself.md +288 -0
- package/.genie/spells/learn.md +828 -0
- package/.genie/spells/mcp-diagnostic-protocol.md +246 -0
- package/.genie/spells/mcp-first.md +124 -0
- package/.genie/spells/multi-step-execution.md +67 -0
- package/.genie/spells/orchestration-boundary-protocol.md +256 -0
- package/.genie/spells/orchestrator-not-implementor.md +189 -0
- package/.genie/spells/prompt.md +746 -0
- package/.genie/spells/reflect.md +404 -0
- package/.genie/spells/routing-decision-matrix.md +368 -0
- package/.genie/spells/run-in-parallel.md +12 -0
- package/.genie/spells/session-state-updater-example.md +196 -0
- package/.genie/spells/session-state-updater.md +220 -0
- package/.genie/spells/track-long-running-tasks.md +133 -0
- package/.genie/spells/troubleshoot-infrastructure.md +176 -0
- package/.genie/spells/upgrade-genie.md +415 -0
- package/.genie/spells/url-presentation-protocol.md +301 -0
- package/.genie/spells/wish-initiation.md +158 -0
- package/.genie/spells/wish-issue-linkage.md +410 -0
- package/.genie/spells/wish-lifecycle.md +100 -0
- package/.genie/state/provider-status.json +3 -0
- package/.genie/state/version.json +16 -0
- package/AGENTS.md +422 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +21 -0
- package/Makefile +235 -0
- package/README.md +323 -0
- package/bin/pglite-server.js +457 -0
- package/ecosystem.config.cjs +23 -0
- package/examples/multi-tenant-demo.js +104 -0
- package/package.json +47 -0
- package/src/detector.js +105 -0
- package/src/index.js +177 -0
- package/src/pool.js +320 -0
- package/src/ports.js +114 -0
- package/src/protocol.js +216 -0
- package/src/registry.js +134 -0
- package/src/router.js +289 -0
- package/src/server.js +265 -0
- package/tests/benchmarks/runner.js +489 -0
- package/tests/multi-tenant.test.js +201 -0
package/src/detector.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if PostgreSQL connection URL is valid and reachable
|
|
5
|
+
*/
|
|
6
|
+
export async function canConnectToPostgres(connectionUrl, timeout = 5000) {
|
|
7
|
+
if (!connectionUrl || !connectionUrl.startsWith('postgresql://')) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Parse URL to extract host and port
|
|
13
|
+
const url = new URL(connectionUrl);
|
|
14
|
+
const host = url.hostname;
|
|
15
|
+
const port = parseInt(url.port || '5432', 10);
|
|
16
|
+
|
|
17
|
+
return await checkTcpConnection(host, port, timeout);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.warn('Invalid PostgreSQL URL:', error.message);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if TCP connection can be established
|
|
26
|
+
*/
|
|
27
|
+
function checkTcpConnection(host, port, timeout) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const socket = new net.Socket();
|
|
30
|
+
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
socket.destroy();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
cleanup();
|
|
37
|
+
resolve(false);
|
|
38
|
+
}, timeout);
|
|
39
|
+
|
|
40
|
+
socket.once('connect', () => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
cleanup();
|
|
43
|
+
resolve(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
socket.once('error', () => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
cleanup();
|
|
49
|
+
resolve(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
socket.connect(port, host);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Auto-detect database URL
|
|
58
|
+
*
|
|
59
|
+
* Priority:
|
|
60
|
+
* 1. External PostgreSQL (if reachable)
|
|
61
|
+
* 2. Embedded PGlite server (start if needed)
|
|
62
|
+
*/
|
|
63
|
+
export async function autoDetect({
|
|
64
|
+
externalUrl,
|
|
65
|
+
embeddedDataDir,
|
|
66
|
+
embeddedPort = null,
|
|
67
|
+
timeout = 5000
|
|
68
|
+
}) {
|
|
69
|
+
// Try external PostgreSQL first
|
|
70
|
+
if (externalUrl && externalUrl.startsWith('postgresql://')) {
|
|
71
|
+
console.log('🔍 Checking external PostgreSQL connection...');
|
|
72
|
+
|
|
73
|
+
if (await canConnectToPostgres(externalUrl, timeout)) {
|
|
74
|
+
console.log('✅ Using external PostgreSQL');
|
|
75
|
+
return {
|
|
76
|
+
type: 'external',
|
|
77
|
+
url: externalUrl,
|
|
78
|
+
embedded: false
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.warn('⚠️ External PostgreSQL unreachable, falling back to embedded');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Start embedded server
|
|
86
|
+
console.log('🚀 Starting embedded PGlite server...');
|
|
87
|
+
|
|
88
|
+
const { getOrStart } = await import('./index.js');
|
|
89
|
+
|
|
90
|
+
const instance = await getOrStart({
|
|
91
|
+
dataDir: embeddedDataDir,
|
|
92
|
+
port: embeddedPort,
|
|
93
|
+
autoPort: true
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`✅ Using embedded PGlite on port ${instance.port}`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
type: 'embedded',
|
|
100
|
+
url: instance.connectionUrl,
|
|
101
|
+
embedded: true,
|
|
102
|
+
port: instance.port,
|
|
103
|
+
dataDir: instance.dataDir
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgserve - PostgreSQL embedded server using PGlite
|
|
3
|
+
*
|
|
4
|
+
* Multi-tenant PostgreSQL router using PGlite
|
|
5
|
+
* Single port, auto-provisioning, perfect for multi-user apps and AI agents
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Multi-tenant mode (NEW - recommended)
|
|
9
|
+
export { MultiTenantRouter, startMultiTenantServer } from './router.js';
|
|
10
|
+
export { InstancePool } from './pool.js';
|
|
11
|
+
|
|
12
|
+
// Legacy single-instance mode (backwards compatible)
|
|
13
|
+
import { startServer as _startServer, stopServer as _stopServer } from './server.js';
|
|
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
|
+
};
|
package/src/pool.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PGlite Instance Pool (Performance Optimized)
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple PGlite instances (one per database)
|
|
5
|
+
* Handles lazy initialization, connection locking, and cleanup
|
|
6
|
+
*
|
|
7
|
+
* Performance Optimizations:
|
|
8
|
+
* - Fast Map-based lookups (O(1) access)
|
|
9
|
+
* - Minimal memory overhead per instance
|
|
10
|
+
* - Pino structured logging
|
|
11
|
+
* - Proper event listener cleanup
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wrapper for PGlite instance with connection management
|
|
21
|
+
*/
|
|
22
|
+
class ManagedInstance extends EventEmitter {
|
|
23
|
+
constructor(dbName, dataDir, logger, memoryMode = false) {
|
|
24
|
+
super();
|
|
25
|
+
this.dbName = dbName;
|
|
26
|
+
this.dataDir = dataDir;
|
|
27
|
+
this.memoryMode = memoryMode;
|
|
28
|
+
this.logger = logger; // Pino logger
|
|
29
|
+
this.db = null;
|
|
30
|
+
this.locked = false;
|
|
31
|
+
this.activeSocket = null;
|
|
32
|
+
this.lockTimer = null; // Safety timeout for locks
|
|
33
|
+
this.queue = [];
|
|
34
|
+
this.createdAt = Date.now();
|
|
35
|
+
this.lastAccess = Date.now();
|
|
36
|
+
|
|
37
|
+
// Performance: Limit max listeners
|
|
38
|
+
this.setMaxListeners(10);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize PGlite instance (lazy)
|
|
43
|
+
*/
|
|
44
|
+
async initialize() {
|
|
45
|
+
if (this.db) {
|
|
46
|
+
return this.db;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const initStart = Date.now();
|
|
50
|
+
|
|
51
|
+
if (this.memoryMode) {
|
|
52
|
+
// Use in-memory database (unique per instance)
|
|
53
|
+
this.logger.debug({ dbName: this.dbName, mode: 'memory' }, 'Initializing in-memory PGlite instance');
|
|
54
|
+
this.db = new PGlite();
|
|
55
|
+
} else {
|
|
56
|
+
// Ensure directory exists for file-based storage
|
|
57
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
58
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.logger.debug({ dbName: this.dbName, dataDir: this.dataDir }, 'Initializing PGlite instance');
|
|
62
|
+
this.db = new PGlite(this.dataDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await this.db.waitReady;
|
|
66
|
+
|
|
67
|
+
const initTime = Date.now() - initStart;
|
|
68
|
+
this.logger.info({
|
|
69
|
+
dbName: this.dbName,
|
|
70
|
+
dataDir: this.memoryMode ? '(in-memory)' : this.dataDir,
|
|
71
|
+
memoryMode: this.memoryMode,
|
|
72
|
+
initTimeMs: initTime
|
|
73
|
+
}, 'PGlite instance initialized');
|
|
74
|
+
|
|
75
|
+
this.emit('initialized', this.dbName);
|
|
76
|
+
return this.db;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Lock instance to a socket
|
|
81
|
+
* @param {net.Socket} socket - TCP socket to lock to
|
|
82
|
+
* @param {number} timeout - Safety timeout in ms (default 5 minutes)
|
|
83
|
+
*/
|
|
84
|
+
lock(socket, timeout = 300000) {
|
|
85
|
+
if (this.locked) {
|
|
86
|
+
throw new Error(`Instance ${this.dbName} is already locked`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.locked = true;
|
|
90
|
+
this.activeSocket = socket;
|
|
91
|
+
this.lastAccess = Date.now();
|
|
92
|
+
|
|
93
|
+
// Safety net: auto-unlock after timeout (prevents permanent locks)
|
|
94
|
+
this.lockTimer = setTimeout(() => {
|
|
95
|
+
this.logger.warn({ dbName: this.dbName }, 'Lock timeout - forcing unlock');
|
|
96
|
+
this.unlock();
|
|
97
|
+
}, timeout);
|
|
98
|
+
|
|
99
|
+
// Only attach event listeners if socket is provided
|
|
100
|
+
if (socket) {
|
|
101
|
+
socket.on('close', () => this.unlock());
|
|
102
|
+
socket.on('error', () => this.unlock());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.emit('locked', this.dbName, socket);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Unlock instance (connection closed)
|
|
110
|
+
*/
|
|
111
|
+
unlock() {
|
|
112
|
+
// Clear safety timeout if it exists
|
|
113
|
+
if (this.lockTimer) {
|
|
114
|
+
clearTimeout(this.lockTimer);
|
|
115
|
+
this.lockTimer = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.locked = false;
|
|
119
|
+
this.activeSocket = null;
|
|
120
|
+
this.lastAccess = Date.now();
|
|
121
|
+
|
|
122
|
+
this.emit('unlocked', this.dbName);
|
|
123
|
+
|
|
124
|
+
// Resolve one waiting promise (it will lock, then when it unlocks, the next will be resolved)
|
|
125
|
+
if (this.queue.length > 0) {
|
|
126
|
+
const { resolve } = this.queue.shift();
|
|
127
|
+
// Don't lock here - let the acquire() caller handle locking with their socket
|
|
128
|
+
resolve(this);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Wait for instance to be free
|
|
134
|
+
*/
|
|
135
|
+
async waitForFree(timeout = 30000) {
|
|
136
|
+
if (!this.locked) {
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
this.queue.push({ socket: null, resolve, reject });
|
|
142
|
+
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
const index = this.queue.findIndex((item) => item.resolve === resolve);
|
|
145
|
+
if (index !== -1) {
|
|
146
|
+
this.queue.splice(index, 1);
|
|
147
|
+
}
|
|
148
|
+
reject(new Error(`Timeout waiting for database ${this.dbName}`));
|
|
149
|
+
}, timeout);
|
|
150
|
+
|
|
151
|
+
// Clear timeout on resolve
|
|
152
|
+
this.once('unlocked', () => clearTimeout(timer));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Close PGlite instance
|
|
158
|
+
*/
|
|
159
|
+
async close() {
|
|
160
|
+
if (this.db) {
|
|
161
|
+
try {
|
|
162
|
+
await this.db.close();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Ignore ExitStatus errors (normal WASM cleanup)
|
|
165
|
+
if (error.name !== 'ExitStatus') {
|
|
166
|
+
console.error(`Error closing instance ${this.dbName}:`, error.message);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.db = null;
|
|
172
|
+
this.emit('closed', this.dbName);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get instance stats
|
|
177
|
+
*/
|
|
178
|
+
getStats() {
|
|
179
|
+
return {
|
|
180
|
+
dbName: this.dbName,
|
|
181
|
+
locked: this.locked,
|
|
182
|
+
queueLength: this.queue.length,
|
|
183
|
+
uptime: Date.now() - this.createdAt,
|
|
184
|
+
lastAccess: Date.now() - this.lastAccess
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* PGlite Instance Pool
|
|
191
|
+
*/
|
|
192
|
+
export class InstancePool extends EventEmitter {
|
|
193
|
+
constructor(options = {}) {
|
|
194
|
+
super();
|
|
195
|
+
this.baseDir = options.baseDir || './data';
|
|
196
|
+
this.memoryMode = options.memoryMode || false;
|
|
197
|
+
this.maxInstances = options.maxInstances || 100;
|
|
198
|
+
this.autoProvision = options.autoProvision !== false; // Default true
|
|
199
|
+
this.instances = new Map(); // dbName -> ManagedInstance (O(1) lookups)
|
|
200
|
+
this.logger = options.logger; // Pino logger
|
|
201
|
+
|
|
202
|
+
// Performance: Set max listeners based on max instances
|
|
203
|
+
this.setMaxListeners(this.maxInstances + 10);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get or create PGlite instance for database (Performance Optimized)
|
|
208
|
+
*/
|
|
209
|
+
async getOrCreate(dbName) {
|
|
210
|
+
// Fast path: Check cache first (O(1) lookup)
|
|
211
|
+
let instance = this.instances.get(dbName);
|
|
212
|
+
|
|
213
|
+
if (!instance) {
|
|
214
|
+
// Check max instances limit
|
|
215
|
+
if (this.instances.size >= this.maxInstances) {
|
|
216
|
+
this.logger.error({
|
|
217
|
+
dbName,
|
|
218
|
+
currentInstances: this.instances.size,
|
|
219
|
+
maxInstances: this.maxInstances
|
|
220
|
+
}, 'Maximum instances limit reached');
|
|
221
|
+
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Maximum instances limit reached (${this.maxInstances}). ` +
|
|
224
|
+
`Cannot create database: ${dbName}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.autoProvision) {
|
|
229
|
+
this.logger.warn({ dbName }, 'Database does not exist (auto-provision disabled)');
|
|
230
|
+
throw new Error(`Database ${dbName} does not exist (auto-provision disabled)`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Create new instance
|
|
234
|
+
const dataDir = this.memoryMode ? null : path.join(this.baseDir, dbName);
|
|
235
|
+
instance = new ManagedInstance(
|
|
236
|
+
dbName,
|
|
237
|
+
dataDir,
|
|
238
|
+
this.logger.child({ dbName }), // Child logger with context
|
|
239
|
+
this.memoryMode
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Forward events (use once() where appropriate for performance)
|
|
243
|
+
instance.on('initialized', (name) => this.emit('instance-created', name));
|
|
244
|
+
instance.on('locked', (name) => this.emit('instance-locked', name));
|
|
245
|
+
instance.on('unlocked', (name) => this.emit('instance-unlocked', name));
|
|
246
|
+
instance.on('closed', (name) => this.emit('instance-closed', name));
|
|
247
|
+
|
|
248
|
+
// Add to cache BEFORE initialization (prevents race conditions)
|
|
249
|
+
this.instances.set(dbName, instance);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Lazy initialize (async, may already be initialized)
|
|
253
|
+
await instance.initialize();
|
|
254
|
+
|
|
255
|
+
return instance;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Acquire instance (lock to socket)
|
|
260
|
+
*/
|
|
261
|
+
async acquire(dbName, socket, timeout = 30000) {
|
|
262
|
+
const instance = await this.getOrCreate(dbName);
|
|
263
|
+
|
|
264
|
+
// If locked, wait for it to be free
|
|
265
|
+
if (instance.locked) {
|
|
266
|
+
console.log(`⏳ Database ${dbName} is busy, queuing connection...`);
|
|
267
|
+
await instance.waitForFree(timeout);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Lock to this socket
|
|
271
|
+
instance.lock(socket);
|
|
272
|
+
|
|
273
|
+
return instance;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get instance (without locking)
|
|
278
|
+
*/
|
|
279
|
+
get(dbName) {
|
|
280
|
+
return this.instances.get(dbName);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* List all instances
|
|
285
|
+
*/
|
|
286
|
+
list() {
|
|
287
|
+
return Array.from(this.instances.values()).map((instance) => instance.getStats());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Close specific instance
|
|
292
|
+
*/
|
|
293
|
+
async closeInstance(dbName) {
|
|
294
|
+
const instance = this.instances.get(dbName);
|
|
295
|
+
if (instance) {
|
|
296
|
+
await instance.close();
|
|
297
|
+
this.instances.delete(dbName);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Close all instances
|
|
303
|
+
*/
|
|
304
|
+
async closeAll() {
|
|
305
|
+
const promises = Array.from(this.instances.values()).map((instance) => instance.close());
|
|
306
|
+
await Promise.all(promises);
|
|
307
|
+
this.instances.clear();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get pool stats
|
|
312
|
+
*/
|
|
313
|
+
getStats() {
|
|
314
|
+
return {
|
|
315
|
+
totalInstances: this.instances.size,
|
|
316
|
+
maxInstances: this.maxInstances,
|
|
317
|
+
instances: this.list()
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
package/src/ports.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import { loadRegistry } from './registry.js';
|
|
3
|
+
|
|
4
|
+
const PORT_RANGE_START = 12000;
|
|
5
|
+
const PORT_RANGE_END = 12999;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a port is available
|
|
9
|
+
*/
|
|
10
|
+
export function isPortFree(port) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const server = net.createServer();
|
|
13
|
+
|
|
14
|
+
server.once('error', (err) => {
|
|
15
|
+
if (err.code === 'EADDRINUSE') {
|
|
16
|
+
resolve(false);
|
|
17
|
+
} else {
|
|
18
|
+
resolve(false);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.once('listening', () => {
|
|
23
|
+
server.close();
|
|
24
|
+
resolve(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
server.listen(port, '127.0.0.1');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if port is in registry (even if process is dead)
|
|
33
|
+
*/
|
|
34
|
+
function isPortInRegistry(port) {
|
|
35
|
+
const registry = loadRegistry();
|
|
36
|
+
|
|
37
|
+
for (const instance of Object.values(registry.instances)) {
|
|
38
|
+
if (instance.port === port) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Allocate a port for a data directory
|
|
48
|
+
*
|
|
49
|
+
* Priority:
|
|
50
|
+
* 1. If instance already running, return its port
|
|
51
|
+
* 2. Try preferred port (if provided)
|
|
52
|
+
* 3. Find next available port in range
|
|
53
|
+
*/
|
|
54
|
+
export async function allocatePort(dataDir, preferredPort = null) {
|
|
55
|
+
const registry = loadRegistry();
|
|
56
|
+
|
|
57
|
+
// Check if instance already exists for this dataDir
|
|
58
|
+
const existing = registry.instances[dataDir];
|
|
59
|
+
if (existing) {
|
|
60
|
+
// Verify process is still running
|
|
61
|
+
try {
|
|
62
|
+
process.kill(existing.pid, 0);
|
|
63
|
+
console.log(`Instance already running for ${dataDir} on port ${existing.port}`);
|
|
64
|
+
return existing.port;
|
|
65
|
+
} catch {
|
|
66
|
+
// Process dead, continue allocation
|
|
67
|
+
console.log(`Stale instance found for ${dataDir}, reallocating port`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try preferred port first
|
|
72
|
+
if (preferredPort !== null) {
|
|
73
|
+
if (preferredPort < PORT_RANGE_START || preferredPort > PORT_RANGE_END) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Preferred port ${preferredPort} outside allowed range ${PORT_RANGE_START}-${PORT_RANGE_END}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (await isPortFree(preferredPort) && !isPortInRegistry(preferredPort)) {
|
|
80
|
+
return preferredPort;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.warn(`Preferred port ${preferredPort} unavailable, auto-allocating...`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Find next available port
|
|
87
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) {
|
|
88
|
+
if ((await isPortFree(port)) && !isPortInRegistry(port)) {
|
|
89
|
+
return port;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(
|
|
94
|
+
`No available ports in range ${PORT_RANGE_START}-${PORT_RANGE_END}. ` +
|
|
95
|
+
`Stop unused instances with 'pglite-server stop --all'`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get port range info
|
|
101
|
+
*/
|
|
102
|
+
export function getPortRangeInfo() {
|
|
103
|
+
const registry = loadRegistry();
|
|
104
|
+
const usedPorts = Object.values(registry.instances).map((i) => i.port);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
start: PORT_RANGE_START,
|
|
108
|
+
end: PORT_RANGE_END,
|
|
109
|
+
total: PORT_RANGE_END - PORT_RANGE_START + 1,
|
|
110
|
+
used: usedPorts.length,
|
|
111
|
+
available: PORT_RANGE_END - PORT_RANGE_START + 1 - usedPorts.length,
|
|
112
|
+
usedPorts
|
|
113
|
+
};
|
|
114
|
+
}
|