177 lines
4.8 KiB
TypeScript
177 lines
4.8 KiB
TypeScript
import { SandboxProvider } from './types';
|
|
import { SandboxFactory } from './factory';
|
|
|
|
interface SandboxInfo {
|
|
sandboxId: string;
|
|
provider: SandboxProvider;
|
|
createdAt: Date;
|
|
lastAccessed: Date;
|
|
}
|
|
|
|
class SandboxManager {
|
|
private sandboxes: Map<string, SandboxInfo> = new Map();
|
|
private activeSandboxId: string | null = null;
|
|
|
|
/**
|
|
* Get or create a sandbox provider for the given sandbox ID
|
|
*/
|
|
async getOrCreateProvider(sandboxId: string): Promise<SandboxProvider> {
|
|
// Check if we already have this sandbox
|
|
const existing = this.sandboxes.get(sandboxId);
|
|
if (existing) {
|
|
existing.lastAccessed = new Date();
|
|
return existing.provider;
|
|
}
|
|
|
|
// Try to reconnect to existing sandbox
|
|
console.log(`[SandboxManager] Attempting to reconnect to sandbox ${sandboxId}`);
|
|
|
|
try {
|
|
const provider = SandboxFactory.create();
|
|
|
|
// For E2B provider, try to reconnect
|
|
if (provider.constructor.name === 'E2BProvider') {
|
|
// E2B sandboxes can be reconnected using the sandbox ID
|
|
const reconnected = await (provider as any).reconnect(sandboxId);
|
|
if (reconnected) {
|
|
this.sandboxes.set(sandboxId, {
|
|
sandboxId,
|
|
provider,
|
|
createdAt: new Date(),
|
|
lastAccessed: new Date()
|
|
});
|
|
this.activeSandboxId = sandboxId;
|
|
console.log(`[SandboxManager] Successfully reconnected to sandbox ${sandboxId}`);
|
|
return provider;
|
|
}
|
|
}
|
|
|
|
// For Vercel or if reconnection failed, return the new provider
|
|
// The caller will need to handle creating a new sandbox
|
|
console.log(`[SandboxManager] Could not reconnect to ${sandboxId}, returning new provider`);
|
|
return provider;
|
|
} catch (error) {
|
|
console.error(`[SandboxManager] Error reconnecting to sandbox ${sandboxId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a new sandbox
|
|
*/
|
|
registerSandbox(sandboxId: string, provider: SandboxProvider): void {
|
|
this.sandboxes.set(sandboxId, {
|
|
sandboxId,
|
|
provider,
|
|
createdAt: new Date(),
|
|
lastAccessed: new Date()
|
|
});
|
|
this.activeSandboxId = sandboxId;
|
|
console.log(`[SandboxManager] Registered sandbox ${sandboxId}`);
|
|
}
|
|
|
|
/**
|
|
* Get the active sandbox provider
|
|
*/
|
|
getActiveProvider(): SandboxProvider | null {
|
|
if (!this.activeSandboxId) {
|
|
return null;
|
|
}
|
|
|
|
const sandbox = this.sandboxes.get(this.activeSandboxId);
|
|
if (sandbox) {
|
|
sandbox.lastAccessed = new Date();
|
|
return sandbox.provider;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a specific sandbox provider
|
|
*/
|
|
getProvider(sandboxId: string): SandboxProvider | null {
|
|
const sandbox = this.sandboxes.get(sandboxId);
|
|
if (sandbox) {
|
|
sandbox.lastAccessed = new Date();
|
|
return sandbox.provider;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set the active sandbox
|
|
*/
|
|
setActiveSandbox(sandboxId: string): boolean {
|
|
if (this.sandboxes.has(sandboxId)) {
|
|
this.activeSandboxId = sandboxId;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Terminate a sandbox
|
|
*/
|
|
async terminateSandbox(sandboxId: string): Promise<void> {
|
|
const sandbox = this.sandboxes.get(sandboxId);
|
|
if (sandbox) {
|
|
try {
|
|
await sandbox.provider.terminate();
|
|
} catch (error) {
|
|
console.error(`[SandboxManager] Error terminating sandbox ${sandboxId}:`, error);
|
|
}
|
|
this.sandboxes.delete(sandboxId);
|
|
|
|
if (this.activeSandboxId === sandboxId) {
|
|
this.activeSandboxId = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Terminate all sandboxes
|
|
*/
|
|
async terminateAll(): Promise<void> {
|
|
const promises = Array.from(this.sandboxes.values()).map(sandbox =>
|
|
sandbox.provider.terminate().catch(err =>
|
|
console.error(`[SandboxManager] Error terminating sandbox ${sandbox.sandboxId}:`, err)
|
|
)
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
this.sandboxes.clear();
|
|
this.activeSandboxId = null;
|
|
}
|
|
|
|
/**
|
|
* Clean up old sandboxes (older than maxAge milliseconds)
|
|
*/
|
|
async cleanup(maxAge: number = 3600000): Promise<void> {
|
|
const now = new Date();
|
|
const toDelete: string[] = [];
|
|
|
|
for (const [id, info] of this.sandboxes.entries()) {
|
|
const age = now.getTime() - info.lastAccessed.getTime();
|
|
if (age > maxAge) {
|
|
toDelete.push(id);
|
|
}
|
|
}
|
|
|
|
for (const id of toDelete) {
|
|
await this.terminateSandbox(id);
|
|
console.log(`[SandboxManager] Cleaned up old sandbox ${id}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const sandboxManager = new SandboxManager();
|
|
|
|
// Also maintain backward compatibility with global state
|
|
declare global {
|
|
var sandboxManager: SandboxManager;
|
|
}
|
|
|
|
// Ensure the global reference points to our singleton
|
|
global.sandboxManager = sandboxManager; |