Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed static file loading, don't open browser by default #821

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"README.md",
"SECURITY.md",
"LICENSE",
"docs"
"docs",
"build/public"
],
"scripts": {
"dev": "wrangler dev src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {
terser(),
json(),
copy({
targets: [{ src: 'public/*', dest: 'build/public' }],
targets: [{ src: 'src/public/*', dest: 'build/public' }],
}),
],
};
12 changes: 6 additions & 6 deletions src/middlewares/log/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ const logClients: Map<string | number, any> = new Map();

const addLogClient = (clientId: any, client: any) => {
logClients.set(clientId, client);
console.log(
`New client ${clientId} connected. Total clients: ${logClients.size}`
);
// console.log(
// `New client ${clientId} connected. Total clients: ${logClients.size}`
// );
};

const removeLogClient = (clientId: any) => {
logClients.delete(clientId);
console.log(
`Client ${clientId} disconnected. Total clients: ${logClients.size}`
);
// console.log(
// `Client ${clientId} disconnected. Total clients: ${logClients.size}`
// );
};

const broadcastLog = async (log: any) => {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
171 changes: 126 additions & 45 deletions src/start-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { serve } from '@hono/node-server';
import { serveStatic } from '@hono/node-server/serve-static';
import { exec } from 'child_process';

import app from './index';
import { streamSSE } from 'hono/streaming';
Expand All @@ -19,15 +18,64 @@ const port = portArg ? parseInt(portArg.split('=')[1]) : defaultPort;

const isHeadless = args.includes('--headless');

// Setup static file serving only if not in headless mode
if (
!isHeadless &&
!(
process.env.NODE_ENV === 'production' ||
process.env.ENVIRONMENT === 'production'
)
) {
app.get('/public/*', serveStatic({ root: './' }));
app.get('/public/logs', serveStatic({ path: './public/index.html' }));
const setupStaticServing = async () => {
const { join, dirname } = await import('path');
const { fileURLToPath } = await import('url');
const { readFileSync } = await import('fs');

const scriptDir = dirname(fileURLToPath(import.meta.url));

// Serve the index.html content directly for both routes
const indexPath = join(scriptDir, 'public/index.html');
const indexContent = readFileSync(indexPath, 'utf-8');

const serveIndex = (c: Context) => {
return c.html(indexContent);
};

// Set up routes
app.get('/public/logs', serveIndex);
app.get('/public', serveIndex);
app.get('/public/', serveIndex);

// Serve other static files
app.use(
'/public/*',
serveStatic({
root: '.',
rewriteRequestPath: (path) => {
return join(scriptDir, path).replace(process.cwd(), '');
},
})
);
};

// Initialize static file serving
await setupStaticServing();

/**
* A helper function to enforce a timeout on SSE sends.
* @param fn A function that returns a Promise (e.g. stream.writeSSE())
* @param timeoutMs The timeout in milliseconds (default: 2000)
*/
async function sendWithTimeout(fn: () => Promise<void>, timeoutMs = 200) {
const timeoutPromise = new Promise<void>((_, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(new Error('Write timeout'));
}, timeoutMs);
});

return Promise.race([fn(), timeoutPromise]);
}

app.get('/log/stream', (c: Context) => {
const clientId = Date.now().toString();
Expand All @@ -37,29 +85,59 @@ if (
c.header('X-Accel-Buffering', 'no');

return streamSSE(c, async (stream) => {
const addLogClient: any = c.get('addLogClient');
const removeLogClient: any = c.get('removeLogClient');

const client = {
sendLog: (message: any) => stream.writeSSE(message),
sendLog: (message: any) =>
sendWithTimeout(() => stream.writeSSE(message)),
};
// Add this client to the set of log clients
const addLogClient: any = c.get('addLogClient');
const removeLogClient: any = c.get('removeLogClient');
addLogClient(clientId, client);

// If the client disconnects (closes the tab, etc.), this signal will be aborted
const onAbort = () => {
removeLogClient(clientId);
};
c.req.raw.signal.addEventListener('abort', onAbort);

try {
// Send an initial connection event
await stream.writeSSE({ event: 'connected', data: clientId });

// Keep the connection open
while (true) {
await stream.sleep(10000); // Heartbeat every 10 seconds
await stream.writeSSE({ event: 'heartbeat', data: 'pulse' });
}
await sendWithTimeout(() =>
stream.writeSSE({ event: 'connected', data: clientId })
);

// Use an interval instead of a while loop
const heartbeatInterval = setInterval(async () => {
if (c.req.raw.signal.aborted) {
clearInterval(heartbeatInterval);
return;
}

try {
await sendWithTimeout(() =>
stream.writeSSE({ event: 'heartbeat', data: 'pulse' })
);
} catch (error) {
// console.error(`Heartbeat failed for client ${clientId}:`, error);
clearInterval(heartbeatInterval);
removeLogClient(clientId);
}
}, 10000);

// Wait for abort signal
await new Promise((resolve) => {
c.req.raw.signal.addEventListener('abort', () => {
clearInterval(heartbeatInterval);
resolve(undefined);
});
});
} catch (error) {
console.error(`Error in log stream for client ${clientId}:`, error);
removeLogClient(clientId);
// console.error(`Error in log stream for client ${clientId}:`, error);
} finally {
// Remove this client when the connection is closed
removeLogClient(clientId);
c.req.raw.signal.removeEventListener('abort', onAbort);
}
});
});
Expand All @@ -80,38 +158,41 @@ const server = serve({

const url = `http://localhost:${port}`;

// Function to open URL in the default browser
function openBrowser(url: string) {
let command: string;
// In Docker container, just log the URL in a clickable format
if (process.env.DOCKER || process.env.CONTAINER) {
console.log('\n🔗 Access your AI Gateway at: \x1b[36m%s\x1b[0m\n', url);
command = ''; // No-op for Docker/containers
} else {
switch (process.platform) {
case 'darwin':
command = `open ${url}`;
break;
case 'win32':
command = `start ${url}`;
break;
default:
command = `xdg-open ${url}`;
}
}
injectWebSocket(server);

if (command) {
exec(command, (error) => {
if (error) {
console.log('\n🔗 Access your AI Gateway at: \x1b[36m%s\x1b[0m\n', url);
}
});
}
// Loading animation function
async function showLoadingAnimation() {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let i = 0;

return new Promise((resolve) => {
const interval = setInterval(() => {
process.stdout.write(`\r${frames[i]} Starting AI Gateway...`);
i = (i + 1) % frames.length;
}, 80);

// Stop after 1 second
setTimeout(() => {
clearInterval(interval);
process.stdout.write('\r');
resolve(undefined);
}, 1000);
});
}

// Open the browser only when --headless is not provided
// Clear the console and show animation before main output
console.clear();
await showLoadingAnimation();

// Main server information with minimal spacing
console.log('\x1b[1m%s\x1b[0m', '🚀 Your AI Gateway is running at:');
console.log(' ' + '\x1b[1;4;32m%s\x1b[0m', `${url}`);

// Secondary information on single lines
if (!isHeadless) {
openBrowser(`${url}/public/`);
console.log('\n\x1b[90m📱 UI:\x1b[0m \x1b[36m%s\x1b[0m', `${url}/public/`);
}
injectWebSocket(server);
console.log(`Your AI Gateway is now running on http://localhost:${port} 🚀`);
// console.log('\x1b[90m📚 Docs:\x1b[0m \x1b[36m%s\x1b[0m', 'https://portkey.ai/docs');

// Single-line ready message
console.log('\n\x1b[32m✨ Ready for connections!\x1b[0m');
Loading