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/protocol.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Wire Protocol Parser (Performance Optimized)
|
|
3
|
+
*
|
|
4
|
+
* Extracts database name from PostgreSQL startup message
|
|
5
|
+
* https://www.postgresql.org/docs/current/protocol-message-formats.html
|
|
6
|
+
*
|
|
7
|
+
* Optimizations:
|
|
8
|
+
* - Fast path for database extraction (skip full parsing if possible)
|
|
9
|
+
* - Minimize string allocations
|
|
10
|
+
* - Use Buffer.indexOf for faster null-byte search
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PROTOCOL_VERSION_3 = 196608;
|
|
14
|
+
const SSL_REQUEST_CODE = 80877103; // PostgreSQL SSL negotiation request
|
|
15
|
+
const DATABASE_KEY = Buffer.from('database\0');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse PostgreSQL startup message to extract connection parameters
|
|
19
|
+
* OPTIMIZED: Fast path for database extraction
|
|
20
|
+
*
|
|
21
|
+
* @param {Buffer} data - Raw startup message data
|
|
22
|
+
* @param {boolean} [fastPath=true] - Use fast path (only extract database)
|
|
23
|
+
* @returns {Object} Parsed parameters (user, database, application_name, etc.)
|
|
24
|
+
*/
|
|
25
|
+
export function parseStartupMessage(data, fastPath = true) {
|
|
26
|
+
const length = data.readInt32BE(0);
|
|
27
|
+
const version = data.readInt32BE(4);
|
|
28
|
+
|
|
29
|
+
// Verify protocol version (3.0 = 196608)
|
|
30
|
+
if (version !== PROTOCOL_VERSION_3) {
|
|
31
|
+
throw new Error(`Unsupported protocol version: ${version}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Fast path: only extract database name (most common case)
|
|
35
|
+
if (fastPath) {
|
|
36
|
+
const dbName = extractDatabaseFast(data, 8, length);
|
|
37
|
+
if (dbName) {
|
|
38
|
+
return { database: dbName };
|
|
39
|
+
}
|
|
40
|
+
// Fallback to full parse if fast path failed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Full parse (slower but complete)
|
|
44
|
+
const params = {};
|
|
45
|
+
let offset = 8;
|
|
46
|
+
|
|
47
|
+
while (offset < length - 1) {
|
|
48
|
+
// Find next null byte (key end)
|
|
49
|
+
const keyEnd = data.indexOf(0, offset);
|
|
50
|
+
if (keyEnd === -1 || keyEnd >= length) break;
|
|
51
|
+
|
|
52
|
+
// Extract key (avoid toString for common keys)
|
|
53
|
+
const key = data.toString('utf8', offset, keyEnd);
|
|
54
|
+
offset = keyEnd + 1;
|
|
55
|
+
|
|
56
|
+
// Find next null byte (value end)
|
|
57
|
+
const valueEnd = data.indexOf(0, offset);
|
|
58
|
+
if (valueEnd === -1 || valueEnd >= length) break;
|
|
59
|
+
|
|
60
|
+
// Extract value
|
|
61
|
+
const value = data.toString('utf8', offset, valueEnd);
|
|
62
|
+
offset = valueEnd + 1;
|
|
63
|
+
|
|
64
|
+
params[key] = value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return params;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Fast path: Extract database name without parsing all parameters
|
|
72
|
+
* PERFORMANCE: ~3x faster than full parse
|
|
73
|
+
*
|
|
74
|
+
* @param {Buffer} data - Startup message buffer
|
|
75
|
+
* @param {number} offset - Start offset (after header)
|
|
76
|
+
* @param {number} length - Total message length
|
|
77
|
+
* @returns {string|null} Database name or null
|
|
78
|
+
*/
|
|
79
|
+
function extractDatabaseFast(data, offset, length) {
|
|
80
|
+
// Search for "database\0" key
|
|
81
|
+
while (offset < length - 1) {
|
|
82
|
+
// Find next null byte
|
|
83
|
+
const nullPos = data.indexOf(0, offset);
|
|
84
|
+
if (nullPos === -1 || nullPos >= length) break;
|
|
85
|
+
|
|
86
|
+
const keyLength = nullPos - offset;
|
|
87
|
+
|
|
88
|
+
// Check if this is the "database" key (compare bytes directly)
|
|
89
|
+
if (keyLength === 8 && data[offset] === 0x64 /* 'd' */) {
|
|
90
|
+
// Quick byte comparison for "database"
|
|
91
|
+
if (
|
|
92
|
+
data[offset + 1] === 0x61 && // 'a'
|
|
93
|
+
data[offset + 2] === 0x74 && // 't'
|
|
94
|
+
data[offset + 3] === 0x61 && // 'a'
|
|
95
|
+
data[offset + 4] === 0x62 && // 'b'
|
|
96
|
+
data[offset + 5] === 0x61 && // 'a'
|
|
97
|
+
data[offset + 6] === 0x73 && // 's'
|
|
98
|
+
data[offset + 7] === 0x65 // 'e'
|
|
99
|
+
) {
|
|
100
|
+
// Found "database" key, extract value
|
|
101
|
+
offset = nullPos + 1;
|
|
102
|
+
const valueEnd = data.indexOf(0, offset);
|
|
103
|
+
if (valueEnd === -1 || valueEnd >= length) return null;
|
|
104
|
+
|
|
105
|
+
return data.toString('utf8', offset, valueEnd);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip to next key-value pair
|
|
110
|
+
offset = nullPos + 1;
|
|
111
|
+
const valueEnd = data.indexOf(0, offset);
|
|
112
|
+
if (valueEnd === -1) break;
|
|
113
|
+
offset = valueEnd + 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract database name from startup message
|
|
121
|
+
*
|
|
122
|
+
* @param {Buffer} data - Raw startup message data
|
|
123
|
+
* @returns {string} Database name (defaults to 'postgres')
|
|
124
|
+
*/
|
|
125
|
+
export function extractDatabaseName(data) {
|
|
126
|
+
try {
|
|
127
|
+
const params = parseStartupMessage(data);
|
|
128
|
+
return params.database || 'postgres';
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn('Failed to parse startup message:', error.message);
|
|
131
|
+
return 'postgres'; // Fallback to default
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read startup message from socket and buffer it
|
|
137
|
+
*
|
|
138
|
+
* @param {net.Socket} socket - TCP socket
|
|
139
|
+
* @returns {Promise<{message: Buffer, allData: Buffer}>} Startup message and all buffered data
|
|
140
|
+
*/
|
|
141
|
+
export async function readStartupMessage(socket) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
let buffer = Buffer.alloc(0);
|
|
144
|
+
let expectedLength = null;
|
|
145
|
+
let resolved = false;
|
|
146
|
+
|
|
147
|
+
const onData = (chunk) => {
|
|
148
|
+
if (resolved) return;
|
|
149
|
+
|
|
150
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
151
|
+
|
|
152
|
+
// Read expected length from first 4 bytes
|
|
153
|
+
if (expectedLength === null && buffer.length >= 4) {
|
|
154
|
+
expectedLength = buffer.readInt32BE(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if we have full message
|
|
158
|
+
if (expectedLength !== null && buffer.length >= expectedLength) {
|
|
159
|
+
resolved = true;
|
|
160
|
+
socket.removeListener('data', onData);
|
|
161
|
+
socket.removeListener('error', onError);
|
|
162
|
+
|
|
163
|
+
const message = buffer.slice(0, expectedLength);
|
|
164
|
+
resolve({ message, allData: buffer });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const onError = (error) => {
|
|
169
|
+
if (resolved) return;
|
|
170
|
+
resolved = true;
|
|
171
|
+
socket.removeListener('data', onData);
|
|
172
|
+
socket.removeListener('error', onError);
|
|
173
|
+
reject(error);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
socket.on('data', onData);
|
|
177
|
+
socket.on('error', onError);
|
|
178
|
+
|
|
179
|
+
// Timeout after 5 seconds
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
if (resolved) return;
|
|
182
|
+
resolved = true;
|
|
183
|
+
socket.removeListener('data', onData);
|
|
184
|
+
socket.removeListener('error', onError);
|
|
185
|
+
reject(new Error('Timeout reading startup message'));
|
|
186
|
+
}, 5000);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract database name from socket connection (with SSL negotiation support)
|
|
192
|
+
*
|
|
193
|
+
* @param {net.Socket} socket - TCP socket
|
|
194
|
+
* @returns {Promise<{dbName: string, buffered: Buffer}>} Database name and buffered data
|
|
195
|
+
*/
|
|
196
|
+
export async function extractDatabaseNameFromSocket(socket) {
|
|
197
|
+
let { message, allData } = await readStartupMessage(socket);
|
|
198
|
+
|
|
199
|
+
// Check if this is an SSL request
|
|
200
|
+
if (message.length >= 8) {
|
|
201
|
+
const version = message.readInt32BE(4);
|
|
202
|
+
|
|
203
|
+
if (version === SSL_REQUEST_CODE) {
|
|
204
|
+
// Respond with 'N' (no SSL support)
|
|
205
|
+
socket.write(Buffer.from('N'));
|
|
206
|
+
|
|
207
|
+
// Read the actual startup message
|
|
208
|
+
const result = await readStartupMessage(socket);
|
|
209
|
+
message = result.message;
|
|
210
|
+
allData = result.allData;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const dbName = extractDatabaseName(message);
|
|
215
|
+
return { dbName, buffered: allData };
|
|
216
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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/router.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Tenant Router (Performance Optimized)
|
|
3
|
+
*
|
|
4
|
+
* Single TCP server that routes connections to different PGlite instances
|
|
5
|
+
* based on database name from PostgreSQL connection string
|
|
6
|
+
*
|
|
7
|
+
* Performance Optimizations:
|
|
8
|
+
* - Pino logger (5x faster than console.log)
|
|
9
|
+
* - TCP socket optimizations (nodelay, keepalive)
|
|
10
|
+
* - Minimal event emitter overhead
|
|
11
|
+
* - Optimized connection tracking
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import net from 'net';
|
|
15
|
+
import { PGLiteSocketHandler } from '@electric-sql/pglite-socket';
|
|
16
|
+
import { InstancePool } from './pool.js';
|
|
17
|
+
import { extractDatabaseNameFromSocket } from './protocol.js';
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
19
|
+
import pino from 'pino';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Multi-Tenant Router Server
|
|
23
|
+
*/
|
|
24
|
+
export class MultiTenantRouter extends EventEmitter {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
super();
|
|
27
|
+
this.port = options.port || 8432;
|
|
28
|
+
this.host = options.host || '127.0.0.1';
|
|
29
|
+
this.baseDir = options.baseDir || './data';
|
|
30
|
+
this.memoryMode = options.memoryMode || false;
|
|
31
|
+
this.maxInstances = options.maxInstances || 100;
|
|
32
|
+
this.autoProvision = options.autoProvision !== false;
|
|
33
|
+
this.inspect = options.inspect || false;
|
|
34
|
+
|
|
35
|
+
// Pino logger (ultra-fast structured logging)
|
|
36
|
+
const logLevel = options.logLevel || 'info';
|
|
37
|
+
this.logger = options.logger || pino({
|
|
38
|
+
level: logLevel,
|
|
39
|
+
transport: logLevel === 'debug' ? {
|
|
40
|
+
target: 'pino-pretty',
|
|
41
|
+
options: { colorize: true }
|
|
42
|
+
} : undefined
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Instance pool
|
|
46
|
+
this.pool = new InstancePool({
|
|
47
|
+
baseDir: this.baseDir,
|
|
48
|
+
memoryMode: this.memoryMode,
|
|
49
|
+
maxInstances: this.maxInstances,
|
|
50
|
+
autoProvision: this.autoProvision,
|
|
51
|
+
logger: this.logger.child({ component: 'pool' })
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// TCP server
|
|
55
|
+
this.server = null;
|
|
56
|
+
this.connections = new Set();
|
|
57
|
+
|
|
58
|
+
// Performance: Reduce event listener overhead
|
|
59
|
+
this.setMaxListeners(this.maxInstances + 10);
|
|
60
|
+
|
|
61
|
+
// Forward pool events (optimized logging)
|
|
62
|
+
this.pool.on('instance-created', (dbName) => {
|
|
63
|
+
this.logger.info({ dbName }, 'Database created');
|
|
64
|
+
this.emit('database-created', dbName);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.pool.on('instance-locked', (dbName) => {
|
|
68
|
+
this.logger.debug({ dbName }, 'Database locked');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.pool.on('instance-unlocked', (dbName) => {
|
|
72
|
+
this.logger.debug({ dbName }, 'Database unlocked');
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Optimize TCP socket for PostgreSQL wire protocol
|
|
78
|
+
* @param {net.Socket} socket - TCP socket to optimize
|
|
79
|
+
*/
|
|
80
|
+
optimizeSocket(socket) {
|
|
81
|
+
// Disable Nagle's algorithm for lower latency
|
|
82
|
+
socket.setNoDelay(true);
|
|
83
|
+
|
|
84
|
+
// Enable TCP keepalive (detect dead connections)
|
|
85
|
+
socket.setKeepAlive(true, 60000); // 60s initial delay
|
|
86
|
+
|
|
87
|
+
// Increase socket buffer sizes for better throughput
|
|
88
|
+
// Note: These are hints to OS, actual values may differ
|
|
89
|
+
try {
|
|
90
|
+
socket.setRecvBufferSize && socket.setRecvBufferSize(128 * 1024); // 128KB
|
|
91
|
+
socket.setSendBufferSize && socket.setSendBufferSize(128 * 1024); // 128KB
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// Ignore if not supported
|
|
94
|
+
this.logger.debug({ err }, 'Could not set socket buffer sizes');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Prevent socket timeout during long-running queries
|
|
98
|
+
socket.setTimeout(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Start multi-tenant router
|
|
103
|
+
*/
|
|
104
|
+
async start() {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
// Create TCP server with optimizations
|
|
107
|
+
this.server = net.createServer({
|
|
108
|
+
// Performance: Allow half-open sockets (faster cleanup)
|
|
109
|
+
allowHalfOpen: false,
|
|
110
|
+
// Performance: Pause on connect (manual resume after setup)
|
|
111
|
+
pauseOnConnect: true
|
|
112
|
+
}, async (socket) => {
|
|
113
|
+
await this.handleConnection(socket);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Set max connections (system limit)
|
|
117
|
+
this.server.maxConnections = this.maxInstances * 2;
|
|
118
|
+
|
|
119
|
+
// Error handling
|
|
120
|
+
this.server.on('error', (error) => {
|
|
121
|
+
this.logger.error({ err: error }, 'Server error');
|
|
122
|
+
this.emit('error', error);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Start listening
|
|
126
|
+
this.server.listen(this.port, this.host, () => {
|
|
127
|
+
this.logger.info({
|
|
128
|
+
host: this.host,
|
|
129
|
+
port: this.port,
|
|
130
|
+
baseDir: this.memoryMode ? '(in-memory)' : this.baseDir,
|
|
131
|
+
memoryMode: this.memoryMode,
|
|
132
|
+
autoProvision: this.autoProvision,
|
|
133
|
+
maxInstances: this.maxInstances
|
|
134
|
+
}, 'Multi-tenant router started');
|
|
135
|
+
|
|
136
|
+
this.emit('listening');
|
|
137
|
+
resolve();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handle incoming connection (Performance Optimized)
|
|
144
|
+
*/
|
|
145
|
+
async handleConnection(socket) {
|
|
146
|
+
const connId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
|
|
149
|
+
// Optimize socket BEFORE any I/O
|
|
150
|
+
this.optimizeSocket(socket);
|
|
151
|
+
|
|
152
|
+
// Track connection
|
|
153
|
+
this.connections.add(socket);
|
|
154
|
+
|
|
155
|
+
// Resume socket (was paused on connect)
|
|
156
|
+
socket.resume();
|
|
157
|
+
|
|
158
|
+
let dbName = null;
|
|
159
|
+
let handler = null;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Extract database name from PostgreSQL handshake
|
|
163
|
+
this.logger.debug({ connId }, 'Reading startup message');
|
|
164
|
+
const { dbName: extractedDbName, buffered } = await extractDatabaseNameFromSocket(socket);
|
|
165
|
+
dbName = extractedDbName;
|
|
166
|
+
|
|
167
|
+
this.logger.info({ dbName, connId }, 'Connection request');
|
|
168
|
+
|
|
169
|
+
// Get or create PGlite instance (with locking)
|
|
170
|
+
const instance = await this.pool.acquire(dbName, socket);
|
|
171
|
+
|
|
172
|
+
const routingTime = Date.now() - startTime;
|
|
173
|
+
this.logger.info({
|
|
174
|
+
dbName,
|
|
175
|
+
connId,
|
|
176
|
+
dataDir: instance.dataDir,
|
|
177
|
+
routingTimeMs: routingTime
|
|
178
|
+
}, 'Routed to database');
|
|
179
|
+
|
|
180
|
+
// Push buffered data back to socket for handler to read
|
|
181
|
+
socket.unshift(buffered);
|
|
182
|
+
|
|
183
|
+
// Create handler for this connection
|
|
184
|
+
handler = new PGLiteSocketHandler({
|
|
185
|
+
db: instance.db,
|
|
186
|
+
closeOnDetach: true,
|
|
187
|
+
inspect: this.inspect
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Attach socket to handler
|
|
191
|
+
await handler.attach(socket);
|
|
192
|
+
|
|
193
|
+
this.logger.debug({ dbName, connId }, 'Socket attached');
|
|
194
|
+
|
|
195
|
+
// Handle socket close (cleanup)
|
|
196
|
+
const cleanup = () => {
|
|
197
|
+
this.logger.debug({ dbName, connId }, 'Connection closed');
|
|
198
|
+
if (handler) {
|
|
199
|
+
handler.detach();
|
|
200
|
+
}
|
|
201
|
+
this.connections.delete(socket);
|
|
202
|
+
// Note: Don't call socket.removeAllListeners() here as it removes
|
|
203
|
+
// the pool's unlock handlers before they can fire, causing stuck locks
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
socket.once('close', cleanup);
|
|
207
|
+
socket.once('error', (error) => {
|
|
208
|
+
this.logger.warn({ dbName, connId, err: error }, 'Socket error');
|
|
209
|
+
cleanup();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
this.emit('connection', { dbName, socket, connId });
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.logger.error({ dbName, connId, err: error }, 'Connection error');
|
|
215
|
+
|
|
216
|
+
// Cleanup
|
|
217
|
+
if (handler) {
|
|
218
|
+
try {
|
|
219
|
+
handler.detach();
|
|
220
|
+
} catch (detachErr) {
|
|
221
|
+
this.logger.debug({ err: detachErr }, 'Error detaching handler');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
socket.destroy(); // Force close on error
|
|
226
|
+
this.connections.delete(socket);
|
|
227
|
+
this.emit('connection-error', { error, dbName, connId });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Stop router (graceful shutdown)
|
|
233
|
+
*/
|
|
234
|
+
async stop() {
|
|
235
|
+
this.logger.info('Stopping multi-tenant router');
|
|
236
|
+
|
|
237
|
+
// Close all connections gracefully
|
|
238
|
+
const activeConns = this.connections.size;
|
|
239
|
+
for (const socket of this.connections) {
|
|
240
|
+
socket.end(); // Graceful close (vs destroy())
|
|
241
|
+
}
|
|
242
|
+
this.connections.clear();
|
|
243
|
+
|
|
244
|
+
// Close TCP server
|
|
245
|
+
if (this.server) {
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
this.server.close(resolve);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Close all PGlite instances
|
|
252
|
+
await this.pool.closeAll();
|
|
253
|
+
|
|
254
|
+
this.logger.info({
|
|
255
|
+
activeConnections: activeConns,
|
|
256
|
+
closedInstances: this.pool.instances.size
|
|
257
|
+
}, 'Router stopped');
|
|
258
|
+
|
|
259
|
+
this.emit('stopped');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get router stats
|
|
264
|
+
*/
|
|
265
|
+
getStats() {
|
|
266
|
+
return {
|
|
267
|
+
port: this.port,
|
|
268
|
+
host: this.host,
|
|
269
|
+
activeConnections: this.connections.size,
|
|
270
|
+
pool: this.pool.getStats()
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* List all databases
|
|
276
|
+
*/
|
|
277
|
+
listDatabases() {
|
|
278
|
+
return this.pool.list();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Start multi-tenant router (convenience function)
|
|
284
|
+
*/
|
|
285
|
+
export async function startMultiTenantServer(options = {}) {
|
|
286
|
+
const router = new MultiTenantRouter(options);
|
|
287
|
+
await router.start();
|
|
288
|
+
return router;
|
|
289
|
+
}
|