Skip to content

Commit

Permalink
Merge pull request #821 from Portkey-AI/fix/help-page
Browse files Browse the repository at this point in the history
Fixed static file loading, don't open browser by default
  • Loading branch information
VisargD authored Dec 18, 2024
2 parents cb949f3 + d139e01 commit ca1f952
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 53 deletions.
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.
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');

0 comments on commit ca1f952

Please sign in to comment.