Skip to content
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
6 changes: 3 additions & 3 deletions lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { nanoid } from 'nanoid'
import { getDb } from '@/lib/mongo'

type User = { id: string; email: string; passwordHash: string; createdAt: number }
type Session = { id: string; userId: string; createdAt: number; expiresAt: number }
type Session = { id: string; userId: string; createdAt: number; expiresAt: Date }
type Project = { id: string; userId: string; sandboxId: string; url: string; name?: string; createdAt: number; updatedAt: number }

export async function getUserByEmail(email: string): Promise<User | undefined> {
Expand Down Expand Up @@ -30,7 +30,7 @@ export async function createSession(userId: string, ttlHours = 72): Promise<Sess
id: nanoid(),
userId,
createdAt: Date.now(),
expiresAt: Date.now() + ttlHours * 60 * 60 * 1000
expiresAt: new Date(Date.now() + ttlHours * 60 * 60 * 1000)
}
await db.collection<Session>('sessions').insertOne({ _id: session.id as any, ...session } as any)
return session
Expand All @@ -39,7 +39,7 @@ export async function createSession(userId: string, ttlHours = 72): Promise<Sess
export async function getSession(id: string): Promise<Session | undefined> {
const db = await getDb()
const doc = await db.collection<Session>('sessions').findOne({ id })
if (!doc || doc.expiresAt <= Date.now()) return undefined
if (!doc || doc.expiresAt.getTime() <= Date.now()) return undefined
return doc
}

Expand Down
2 changes: 1 addition & 1 deletion lib/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function getDb(): Promise<Db> {

// Ensure indexes only once per application lifespan
if (!global._mongoIndexesCreated) {
// Ensure TTL index on 'expiresAt' to auto-delete expired sessions
await Promise.all([
db.collection('users').createIndex({ email: 1 }, { unique: true }),
db.collection('sessions').createIndex({ id: 1 }, { unique: true }),
Expand All @@ -36,4 +37,3 @@ export async function getDb(): Promise<Db> {

return db
}

67 changes: 61 additions & 6 deletions lib/sandbox/sandbox-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { SandboxProvider } from './types';
import { EventEmitter } from 'events';
import { appConfig } from '@/config/app.config';
import { SandboxFactory } from './factory';

interface SandboxInfo {
Expand All @@ -10,6 +12,9 @@ interface SandboxInfo {

class SandboxManager {
private sandboxes: Map<string, SandboxInfo> = new Map();
private timeoutWatchers: Map<string, NodeJS.Timeout> = new Map();
private timeoutWarningCallbacks: ((sandboxId: string, remainingMs: number) => void)[] = [];
private warningThresholdMs = 2 * 60 * 1000; // 2 minutes
private activeSandboxId: string | null = null;

/**
Expand Down Expand Up @@ -53,6 +58,46 @@ class SandboxManager {
}
}

private getTimeoutMs(provider: SandboxProvider): number {
const name = provider.constructor.name;
if (name === 'E2BProvider') return appConfig.e2b.timeoutMs;
if (name === 'VercelProvider') return appConfig.vercelSandbox.timeoutMs;
return 0;
}

private emitTimeoutWarning(sandboxId: string, remainingMs: number): void {
this.timeoutWarningCallbacks.forEach(cb => {
try { cb(sandboxId, remainingMs); } catch (e) { console.error('Timeout warning callback error', e); }
});
}

private scheduleWarning(sandboxId: string, timeoutMs: number): void {
const warningTime = timeoutMs - this.warningThresholdMs;
if (warningTime <= 0) return;
const now = Date.now();
const info = this.sandboxes.get(sandboxId);
if (!info) return;
const elapsed = now - info.createdAt.getTime();
const remaining = timeoutMs - elapsed;
const delay = remaining - this.warningThresholdMs;
if (delay <= 0) {
this.emitTimeoutWarning(sandboxId, Math.max(remaining, 0));
return;
}
const watcher = setTimeout(() => {
this.emitTimeoutWarning(sandboxId, this.warningThresholdMs);
}, delay);
this.timeoutWatchers.set(sandboxId, watcher);
}

private startTimeoutWatcher(sandboxId: string): void {
const info = this.sandboxes.get(sandboxId);
if (!info) return;
const timeoutMs = this.getTimeoutMs(info.provider);
if (!timeoutMs) return;
this.scheduleWarning(sandboxId, timeoutMs);
}

/**
* Register a new sandbox
*/
Expand All @@ -64,6 +109,7 @@ class SandboxManager {
lastAccessed: new Date()
});
this.activeSandboxId = sandboxId;
this.startTimeoutWatcher(sandboxId);
}

/**
Expand Down Expand Up @@ -118,7 +164,12 @@ class SandboxManager {
console.error(`[SandboxManager] Error terminating sandbox ${sandboxId}:`, error);
}
this.sandboxes.delete(sandboxId);

// Clear timeout watcher if any
const watcher = this.timeoutWatchers.get(sandboxId);
if (watcher) {
clearTimeout(watcher);
this.timeoutWatchers.delete(sandboxId);
}
if (this.activeSandboxId === sandboxId) {
this.activeSandboxId = null;
}
Expand All @@ -129,21 +180,25 @@ class SandboxManager {
* Terminate all sandboxes
*/
async terminateAll(): Promise<void> {
const promises = Array.from(this.sandboxes.values()).map(sandbox =>
sandbox.provider.terminate().catch(err =>
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();
// Clear all watchers
this.timeoutWatchers.forEach(watcher => clearTimeout(watcher));
this.timeoutWatchers.clear();
this.activeSandboxId = null;
}

/**
* Clean up old sandboxes (older than maxAge milliseconds)
*/
async cleanup(maxAge: number = 3600000): Promise<void> {
// Also clear any timeout watchers for sandboxes being terminated

const now = new Date();
const toDelete: string[] = [];

Expand All @@ -155,8 +210,8 @@ class SandboxManager {
}

for (const id of toDelete) {
await this.terminateSandbox(id);
}
await this.terminateSandbox(id);
}
}
}

Expand Down
Loading