Fix code quality violations for publish
Progressive breadcrumb resolution with caching, fix double headers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
410
node_modules/@jqhtml/ssr/src/server.js
generated
vendored
Executable file
410
node_modules/@jqhtml/ssr/src/server.js
generated
vendored
Executable file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* JQHTML SSR Server
|
||||
*
|
||||
* Long-running Node.js process that accepts render requests via TCP or Unix socket.
|
||||
* See SPECIFICATION.md for protocol details.
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const {
|
||||
ErrorCodes,
|
||||
parseRequest,
|
||||
renderResponse,
|
||||
pingResponse,
|
||||
flushCacheResponse,
|
||||
errorResponse,
|
||||
MessageBuffer
|
||||
} = require('./protocol.js');
|
||||
|
||||
const { SSREnvironment } = require('./environment.js');
|
||||
const { BundleCache, prepareBundleCode } = require('./bundle-cache.js');
|
||||
|
||||
/**
|
||||
* SSR Server
|
||||
*/
|
||||
class SSRServer {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
maxBundles: options.maxBundles || 10,
|
||||
defaultTimeout: options.defaultTimeout || 30000,
|
||||
...options
|
||||
};
|
||||
|
||||
this.server = null;
|
||||
this.bundleCache = new BundleCache(this.options.maxBundles);
|
||||
this.startTime = Date.now();
|
||||
this.requestCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start server on TCP port
|
||||
* @param {number} port
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
listenTCP(port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = net.createServer((socket) => this._handleConnection(socket));
|
||||
|
||||
this.server.on('error', reject);
|
||||
|
||||
this.server.listen(port, () => {
|
||||
console.log(`[SSR] Server listening on tcp://0.0.0.0:${port}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start server on Unix socket
|
||||
* @param {string} socketPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
listenUnix(socketPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Remove existing socket file if present
|
||||
if (fs.existsSync(socketPath)) {
|
||||
fs.unlinkSync(socketPath);
|
||||
}
|
||||
|
||||
this.server = net.createServer((socket) => this._handleConnection(socket));
|
||||
|
||||
this.server.on('error', reject);
|
||||
|
||||
this.server.listen(socketPath, () => {
|
||||
// Set socket permissions (owner read/write only)
|
||||
fs.chmodSync(socketPath, 0o600);
|
||||
console.log(`[SSR] Server listening on unix://${socketPath}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming connection
|
||||
* @param {net.Socket} socket
|
||||
* @private
|
||||
*/
|
||||
_handleConnection(socket) {
|
||||
const messageBuffer = new MessageBuffer();
|
||||
const remoteId = `${socket.remoteAddress || 'unix'}:${socket.remotePort || 'local'}`;
|
||||
|
||||
socket.on('data', async (data) => {
|
||||
const messages = messageBuffer.push(data.toString());
|
||||
|
||||
for (const message of messages) {
|
||||
await this._handleMessage(socket, message);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error(`[SSR] Socket error (${remoteId}):`, err.message);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
if (messageBuffer.hasIncomplete()) {
|
||||
console.warn(`[SSR] Connection closed with incomplete message (${remoteId})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a single message
|
||||
* @param {net.Socket} socket
|
||||
* @param {string} message
|
||||
* @private
|
||||
*/
|
||||
async _handleMessage(socket, message) {
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
|
||||
// Parse request
|
||||
const parseResult = parseRequest(message);
|
||||
|
||||
if (!parseResult.ok) {
|
||||
const response = errorResponse(
|
||||
null,
|
||||
parseResult.error.code,
|
||||
parseResult.error.message,
|
||||
parseResult.error.stack
|
||||
);
|
||||
socket.write(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = parseResult.request;
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
switch (request.type) {
|
||||
case 'ping':
|
||||
response = this._handlePing(request);
|
||||
break;
|
||||
|
||||
case 'flush_cache':
|
||||
response = this._handleFlushCache(request);
|
||||
break;
|
||||
|
||||
case 'render':
|
||||
response = await this._handleRender(request);
|
||||
break;
|
||||
|
||||
default:
|
||||
response = errorResponse(
|
||||
request.id,
|
||||
ErrorCodes.PARSE_ERROR,
|
||||
`Unknown request type: ${request.type}`
|
||||
);
|
||||
}
|
||||
|
||||
socket.write(response);
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[SSR] Error handling request ${request.id}:`, err);
|
||||
|
||||
const response = errorResponse(
|
||||
request.id,
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
err.message,
|
||||
err.stack
|
||||
);
|
||||
socket.write(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ping request
|
||||
* @param {object} request
|
||||
* @returns {string} Response
|
||||
* @private
|
||||
*/
|
||||
_handlePing(request) {
|
||||
const uptime_ms = Date.now() - this.startTime;
|
||||
return pingResponse(request.id, uptime_ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle flush_cache request
|
||||
* @param {object} request
|
||||
* @returns {string} Response
|
||||
* @private
|
||||
*/
|
||||
_handleFlushCache(request) {
|
||||
const bundleId = request.payload?.bundle_id;
|
||||
|
||||
if (bundleId) {
|
||||
// Flush specific bundle - need to find all cache keys containing this bundle
|
||||
const stats = this.bundleCache.stats();
|
||||
let flushed = false;
|
||||
|
||||
for (const key of stats.keys) {
|
||||
if (key.includes(bundleId)) {
|
||||
this.bundleCache.delete(key);
|
||||
flushed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return flushCacheResponse(request.id, flushed);
|
||||
} else {
|
||||
// Flush all
|
||||
this.bundleCache.clear();
|
||||
return flushCacheResponse(request.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle render request
|
||||
* @param {object} request
|
||||
* @returns {Promise<string>} Response
|
||||
* @private
|
||||
*/
|
||||
async _handleRender(request) {
|
||||
const startTime = Date.now();
|
||||
const payload = request.payload;
|
||||
|
||||
// Get or prepare bundle code
|
||||
const cacheKey = BundleCache.generateKey(payload.bundles);
|
||||
let bundleCode = this.bundleCache.get(cacheKey);
|
||||
let bundleLoadMs = 0;
|
||||
|
||||
if (!bundleCode) {
|
||||
const bundleStartTime = Date.now();
|
||||
bundleCode = prepareBundleCode(payload.bundles);
|
||||
this.bundleCache.set(cacheKey, bundleCode);
|
||||
bundleLoadMs = Date.now() - bundleStartTime;
|
||||
}
|
||||
|
||||
// Create fresh environment for this request
|
||||
const env = new SSREnvironment({
|
||||
baseUrl: payload.options.baseUrl,
|
||||
timeout: payload.options.timeout || this.options.defaultTimeout
|
||||
});
|
||||
|
||||
try {
|
||||
// Initialize environment
|
||||
env.init();
|
||||
|
||||
// Execute bundle code
|
||||
const execStartTime = Date.now();
|
||||
env.execute(bundleCode, `bundles:${cacheKey}`);
|
||||
bundleLoadMs += Date.now() - execStartTime;
|
||||
|
||||
// Check if jqhtml loaded
|
||||
if (!env.isJqhtmlReady()) {
|
||||
throw new Error('jqhtml runtime not available after loading bundles');
|
||||
}
|
||||
|
||||
// Check if component is registered
|
||||
const templates = env.getRegisteredTemplates();
|
||||
if (!templates.includes(payload.component)) {
|
||||
return errorResponse(
|
||||
request.id,
|
||||
ErrorCodes.COMPONENT_NOT_FOUND,
|
||||
`Component "${payload.component}" not found. Available: ${templates.slice(0, 10).join(', ')}${templates.length > 10 ? '...' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
// Render component
|
||||
const renderStartTime = Date.now();
|
||||
const result = await env.render(payload.component, payload.args || {});
|
||||
const renderMs = Date.now() - renderStartTime;
|
||||
|
||||
const totalMs = Date.now() - startTime;
|
||||
|
||||
return renderResponse(request.id, result.html, result.cache, {
|
||||
total_ms: totalMs,
|
||||
bundle_load_ms: bundleLoadMs,
|
||||
render_ms: renderMs
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
// Determine error type
|
||||
let errorCode = ErrorCodes.RENDER_ERROR;
|
||||
|
||||
if (err.message.includes('timeout')) {
|
||||
errorCode = ErrorCodes.RENDER_TIMEOUT;
|
||||
} else if (err.message.includes('SyntaxError') || err.message.includes('parse')) {
|
||||
errorCode = ErrorCodes.BUNDLE_ERROR;
|
||||
}
|
||||
|
||||
return errorResponse(request.id, errorCode, err.message, err.stack);
|
||||
|
||||
} finally {
|
||||
// Always destroy environment to free resources
|
||||
env.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
stop() {
|
||||
return new Promise((resolve) => {
|
||||
if (this.server) {
|
||||
this.server.close(() => {
|
||||
console.log('[SSR] Server stopped');
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server statistics
|
||||
* @returns {object}
|
||||
*/
|
||||
stats() {
|
||||
return {
|
||||
uptime_ms: Date.now() - this.startTime,
|
||||
request_count: this.requestCount,
|
||||
bundle_cache: this.bundleCache.stats()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SSRServer };
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let tcpPort = null;
|
||||
let socketPath = null;
|
||||
let maxBundles = 10;
|
||||
let timeout = 30000;
|
||||
|
||||
// Parse arguments
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--tcp':
|
||||
tcpPort = parseInt(args[++i], 10);
|
||||
break;
|
||||
case '--socket':
|
||||
socketPath = args[++i];
|
||||
break;
|
||||
case '--max-bundles':
|
||||
maxBundles = parseInt(args[++i], 10);
|
||||
break;
|
||||
case '--timeout':
|
||||
timeout = parseInt(args[++i], 10);
|
||||
break;
|
||||
case '--help':
|
||||
console.log(`
|
||||
JQHTML SSR Server
|
||||
|
||||
Usage:
|
||||
node server.js --tcp <port>
|
||||
node server.js --socket <path>
|
||||
|
||||
Options:
|
||||
--tcp <port> Listen on TCP port
|
||||
--socket <path> Listen on Unix socket
|
||||
--max-bundles <n> Max cached bundle sets (default: 10)
|
||||
--timeout <ms> Default render timeout (default: 30000)
|
||||
--help Show this help
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tcpPort && !socketPath) {
|
||||
console.error('Error: Must specify --tcp <port> or --socket <path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = new SSRServer({ maxBundles, defaultTimeout: timeout });
|
||||
|
||||
// Start server
|
||||
(async () => {
|
||||
try {
|
||||
if (tcpPort) {
|
||||
await server.listenTCP(tcpPort);
|
||||
} else {
|
||||
await server.listenUnix(socketPath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SSR] Failed to start server:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n[SSR] Shutting down...');
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('\n[SSR] Shutting down...');
|
||||
await server.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user