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.
Files changed (158) hide show
  1. package/.genie/AGENTS.md +13 -0
  2. package/.genie/agents/README.md +110 -0
  3. package/.genie/agents/analyze.md +176 -0
  4. package/.genie/agents/forge.md +290 -0
  5. package/.genie/agents/garbage-cleaner.md +324 -0
  6. package/.genie/agents/garbage-collector.md +596 -0
  7. package/.genie/agents/github-issue-gc.md +618 -0
  8. package/.genie/agents/review.md +380 -0
  9. package/.genie/agents/semantic-analyzer/find-duplicates.md +90 -0
  10. package/.genie/agents/semantic-analyzer/find-orphans.md +99 -0
  11. package/.genie/agents/semantic-analyzer.md +101 -0
  12. package/.genie/agents/update.md +182 -0
  13. package/.genie/agents/wish.md +357 -0
  14. package/.genie/code/AGENTS.md +692 -0
  15. package/.genie/code/agents/audit/risk.md +173 -0
  16. package/.genie/code/agents/audit/security.md +189 -0
  17. package/.genie/code/agents/audit.md +145 -0
  18. package/.genie/code/agents/challenge.md +230 -0
  19. package/.genie/code/agents/change-reviewer.md +295 -0
  20. package/.genie/code/agents/code-garbage-collector.md +425 -0
  21. package/.genie/code/agents/code-quality.md +410 -0
  22. package/.genie/code/agents/commit-suggester.md +255 -0
  23. package/.genie/code/agents/commit.md +124 -0
  24. package/.genie/code/agents/consensus.md +204 -0
  25. package/.genie/code/agents/daily-standup.md +722 -0
  26. package/.genie/code/agents/docgen.md +48 -0
  27. package/.genie/code/agents/explore.md +79 -0
  28. package/.genie/code/agents/fix.md +100 -0
  29. package/.genie/code/agents/git/commit-advisory.md +219 -0
  30. package/.genie/code/agents/git/workflows/issue.md +244 -0
  31. package/.genie/code/agents/git/workflows/pr.md +179 -0
  32. package/.genie/code/agents/git/workflows/release.md +460 -0
  33. package/.genie/code/agents/git/workflows/report.md +342 -0
  34. package/.genie/code/agents/git.md +432 -0
  35. package/.genie/code/agents/implementor.md +161 -0
  36. package/.genie/code/agents/install.md +515 -0
  37. package/.genie/code/agents/issue-creator.md +344 -0
  38. package/.genie/code/agents/polish.md +116 -0
  39. package/.genie/code/agents/qa.md +653 -0
  40. package/.genie/code/agents/refactor.md +294 -0
  41. package/.genie/code/agents/release.md +1129 -0
  42. package/.genie/code/agents/roadmap.md +885 -0
  43. package/.genie/code/agents/tests.md +557 -0
  44. package/.genie/code/agents/tracer.md +50 -0
  45. package/.genie/code/agents/update/upstream-update.md +85 -0
  46. package/.genie/code/agents/update/versions/generic-update.md +305 -0
  47. package/.genie/code/agents/vibe.md +1317 -0
  48. package/.genie/code/spells/agent-configuration.md +58 -0
  49. package/.genie/code/spells/automated-rc-publishing.md +106 -0
  50. package/.genie/code/spells/branch-tracker-guidance.md +28 -0
  51. package/.genie/code/spells/debug.md +320 -0
  52. package/.genie/code/spells/emoji-naming-convention.md +303 -0
  53. package/.genie/code/spells/evidence-storage.md +26 -0
  54. package/.genie/code/spells/file-naming-rules.md +35 -0
  55. package/.genie/code/spells/forge-code-blueprints.md +195 -0
  56. package/.genie/code/spells/genie-integration.md +153 -0
  57. package/.genie/code/spells/publishing-protocol.md +61 -0
  58. package/.genie/code/spells/team-consultation-protocol.md +284 -0
  59. package/.genie/code/spells/tool-requirements.md +20 -0
  60. package/.genie/code/spells/triad-maintenance-protocol.md +154 -0
  61. package/.genie/code/teams/tech-council/council.md +328 -0
  62. package/.genie/code/teams/tech-council/jt.md +352 -0
  63. package/.genie/code/teams/tech-council/nayr.md +305 -0
  64. package/.genie/code/teams/tech-council/oettam.md +375 -0
  65. package/.genie/neurons/README.md +193 -0
  66. package/.genie/neurons/forge.md +106 -0
  67. package/.genie/neurons/genie.md +63 -0
  68. package/.genie/neurons/review.md +106 -0
  69. package/.genie/neurons/wish.md +104 -0
  70. package/.genie/product/README.md +20 -0
  71. package/.genie/product/cli-automation.md +359 -0
  72. package/.genie/product/environment.md +60 -0
  73. package/.genie/product/mission.md +60 -0
  74. package/.genie/product/roadmap.md +44 -0
  75. package/.genie/product/tech-stack.md +34 -0
  76. package/.genie/product/templates/context-template.md +218 -0
  77. package/.genie/product/templates/qa-done-report-template.md +68 -0
  78. package/.genie/product/templates/review-report-template.md +89 -0
  79. package/.genie/product/templates/wish-template.md +120 -0
  80. package/.genie/scripts/helpers/analyze-commit.js +195 -0
  81. package/.genie/scripts/helpers/bullet-counter.js +194 -0
  82. package/.genie/scripts/helpers/bullet-find.js +289 -0
  83. package/.genie/scripts/helpers/bullet-id.js +244 -0
  84. package/.genie/scripts/helpers/check-secrets.js +237 -0
  85. package/.genie/scripts/helpers/count-tokens.js +200 -0
  86. package/.genie/scripts/helpers/create-frontmatter.js +456 -0
  87. package/.genie/scripts/helpers/detect-markers.js +293 -0
  88. package/.genie/scripts/helpers/detect-todos.js +267 -0
  89. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +135 -0
  90. package/.genie/scripts/helpers/embeddings.js +344 -0
  91. package/.genie/scripts/helpers/find-empty-sections.js +158 -0
  92. package/.genie/scripts/helpers/index.js +319 -0
  93. package/.genie/scripts/helpers/validate-frontmatter.js +578 -0
  94. package/.genie/scripts/helpers/validate-links.js +207 -0
  95. package/.genie/scripts/helpers/validate-paths.js +373 -0
  96. package/.genie/spells/README.md +9 -0
  97. package/.genie/spells/ace-protocol.md +118 -0
  98. package/.genie/spells/ask-one-at-a-time.md +175 -0
  99. package/.genie/spells/backup-analyzer.md +542 -0
  100. package/.genie/spells/blocker.md +12 -0
  101. package/.genie/spells/break-things-move-fast.md +56 -0
  102. package/.genie/spells/context-candidates.md +72 -0
  103. package/.genie/spells/context-critic.md +51 -0
  104. package/.genie/spells/defer-to-expertise.md +278 -0
  105. package/.genie/spells/delegate-dont-do.md +292 -0
  106. package/.genie/spells/error-investigation-protocol.md +328 -0
  107. package/.genie/spells/evidence-based-completion.md +273 -0
  108. package/.genie/spells/experiment.md +65 -0
  109. package/.genie/spells/file-creation-protocol.md +229 -0
  110. package/.genie/spells/forge-integration.md +281 -0
  111. package/.genie/spells/forge-orchestration.md +514 -0
  112. package/.genie/spells/gather-context.md +18 -0
  113. package/.genie/spells/global-health-check.md +34 -0
  114. package/.genie/spells/global-noop-roundtrip.md +25 -0
  115. package/.genie/spells/install-genie.md +1232 -0
  116. package/.genie/spells/install.md +82 -0
  117. package/.genie/spells/investigate-before-commit.md +112 -0
  118. package/.genie/spells/know-yourself.md +288 -0
  119. package/.genie/spells/learn.md +828 -0
  120. package/.genie/spells/mcp-diagnostic-protocol.md +246 -0
  121. package/.genie/spells/mcp-first.md +124 -0
  122. package/.genie/spells/multi-step-execution.md +67 -0
  123. package/.genie/spells/orchestration-boundary-protocol.md +256 -0
  124. package/.genie/spells/orchestrator-not-implementor.md +189 -0
  125. package/.genie/spells/prompt.md +746 -0
  126. package/.genie/spells/reflect.md +404 -0
  127. package/.genie/spells/routing-decision-matrix.md +368 -0
  128. package/.genie/spells/run-in-parallel.md +12 -0
  129. package/.genie/spells/session-state-updater-example.md +196 -0
  130. package/.genie/spells/session-state-updater.md +220 -0
  131. package/.genie/spells/track-long-running-tasks.md +133 -0
  132. package/.genie/spells/troubleshoot-infrastructure.md +176 -0
  133. package/.genie/spells/upgrade-genie.md +415 -0
  134. package/.genie/spells/url-presentation-protocol.md +301 -0
  135. package/.genie/spells/wish-initiation.md +158 -0
  136. package/.genie/spells/wish-issue-linkage.md +410 -0
  137. package/.genie/spells/wish-lifecycle.md +100 -0
  138. package/.genie/state/provider-status.json +3 -0
  139. package/.genie/state/version.json +16 -0
  140. package/AGENTS.md +422 -0
  141. package/CLAUDE.md +1 -0
  142. package/LICENSE +21 -0
  143. package/Makefile +235 -0
  144. package/README.md +323 -0
  145. package/bin/pglite-server.js +457 -0
  146. package/ecosystem.config.cjs +23 -0
  147. package/examples/multi-tenant-demo.js +104 -0
  148. package/package.json +47 -0
  149. package/src/detector.js +105 -0
  150. package/src/index.js +177 -0
  151. package/src/pool.js +320 -0
  152. package/src/ports.js +114 -0
  153. package/src/protocol.js +216 -0
  154. package/src/registry.js +134 -0
  155. package/src/router.js +289 -0
  156. package/src/server.js +265 -0
  157. package/tests/benchmarks/runner.js +489 -0
  158. package/tests/multi-tenant.test.js +201 -0
@@ -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
+ }
@@ -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
+ }