From 76f6e4b0bb2bd20e58f6d908cd485aee4538e698 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 12:44:38 -0600 Subject: [PATCH 01/27] refactor: format Cloudflare Worker code --- backend/ai/package.json | 42 ++--- backend/ai/src/index.ts | 183 +++++++++++----------- backend/ai/test/index.spec.ts | 39 +++-- backend/ai/test/tsconfig.json | 18 +-- backend/ai/tsconfig.json | 4 +- backend/ai/vitest.config.ts | 18 +-- backend/ai/worker-configuration.d.ts | 3 +- backend/database/drizzle.config.ts | 4 +- backend/database/package.json | 62 ++++---- backend/database/src/index.ts | 6 +- backend/database/src/schema.ts | 43 ++--- backend/database/test/index.spec.ts | 41 ++--- backend/database/test/tsconfig.json | 18 +-- backend/database/tsconfig.json | 4 +- backend/database/vitest.config.ts | 18 +-- backend/storage/package.json | 44 +++--- backend/storage/src/index.ts | 26 +-- backend/storage/test/index.spec.ts | 39 +++-- backend/storage/test/tsconfig.json | 18 +-- backend/storage/tsconfig.json | 4 +- backend/storage/vitest.config.ts | 18 +-- backend/storage/worker-configuration.d.ts | 3 +- 22 files changed, 344 insertions(+), 311 deletions(-) diff --git a/backend/ai/package.json b/backend/ai/package.json index 59dc0ae..c5c2216 100644 --- a/backend/ai/package.json +++ b/backend/ai/package.json @@ -1,22 +1,22 @@ { - "name": "ai", - "version": "0.0.0", - "private": true, - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", - "start": "wrangler dev", - "test": "vitest", - "cf-typegen": "wrangler types" - }, - "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.1.0", - "@cloudflare/workers-types": "^4.20240512.0", - "typescript": "^5.0.4", - "vitest": "1.3.0", - "wrangler": "^3.0.0" - }, - "dependencies": { - "@anthropic-ai/sdk": "^0.27.2" - } -} + "name": "ai", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "test": "vitest", + "cf-typegen": "wrangler types" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.1.0", + "@cloudflare/workers-types": "^4.20240512.0", + "typescript": "^5.0.4", + "vitest": "1.3.0", + "wrangler": "^3.0.0" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.27.2" + } +} \ No newline at end of file diff --git a/backend/ai/src/index.ts b/backend/ai/src/index.ts index ff4635f..cd9bd4a 100644 --- a/backend/ai/src/index.ts +++ b/backend/ai/src/index.ts @@ -1,57 +1,61 @@ -import { Anthropic } from "@anthropic-ai/sdk"; -import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js"; +import { Anthropic } from "@anthropic-ai/sdk" +import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js" export interface Env { - ANTHROPIC_API_KEY: string; + ANTHROPIC_API_KEY: string } export default { - async fetch(request: Request, env: Env): Promise { - // Handle CORS preflight requests - if (request.method === "OPTIONS") { - return new Response(null, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", - }, - }); - } + async fetch(request: Request, env: Env): Promise { + // Handle CORS preflight requests + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }) + } - if (request.method !== "GET" && request.method !== "POST") { - return new Response("Method Not Allowed", { status: 405 }); - } + if (request.method !== "GET" && request.method !== "POST") { + return new Response("Method Not Allowed", { status: 405 }) + } - let body; - let isEditCodeWidget = false; - if (request.method === "POST") { - body = await request.json() as { messages: unknown; context: unknown; activeFileContent: string }; - } else { - const url = new URL(request.url); - const fileName = url.searchParams.get("fileName") || ""; - const code = url.searchParams.get("code") || ""; - const line = url.searchParams.get("line") || ""; - const instructions = url.searchParams.get("instructions") || ""; + let body + let isEditCodeWidget = false + if (request.method === "POST") { + body = (await request.json()) as { + messages: unknown + context: unknown + activeFileContent: string + } + } else { + const url = new URL(request.url) + const fileName = url.searchParams.get("fileName") || "" + const code = url.searchParams.get("code") || "" + const line = url.searchParams.get("line") || "" + const instructions = url.searchParams.get("instructions") || "" - body = { - messages: [{ role: "human", content: instructions }], - context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`, - activeFileContent: code, - }; - isEditCodeWidget = true; - } + body = { + messages: [{ role: "human", content: instructions }], + context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`, + activeFileContent: code, + } + isEditCodeWidget = true + } - const messages = body.messages; - const context = body.context; - const activeFileContent = body.activeFileContent; + const messages = body.messages + const context = body.context + const activeFileContent = body.activeFileContent - if (!Array.isArray(messages) || messages.length === 0) { - return new Response("Invalid or empty messages", { status: 400 }); - } + if (!Array.isArray(messages) || messages.length === 0) { + return new Response("Invalid or empty messages", { status: 400 }) + } - let systemMessage; - if (isEditCodeWidget) { - systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code. + let systemMessage + if (isEditCodeWidget) { + systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code. Context: ${context} @@ -61,9 +65,9 @@ ${activeFileContent} Instructions: ${messages[0].content} -Respond only with the modified code that can directly replace the existing code.`; - } else { - systemMessage = `You are an intelligent programming assistant. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example: +Respond only with the modified code that can directly replace the existing code.` + } else { + systemMessage = `You are an intelligent programming assistant. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example: \`\`\`python print("Hello, World!") @@ -71,51 +75,54 @@ print("Hello, World!") Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point. -${context ? `Context:\n${context}\n` : ''} -${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ''}`; - } +${context ? `Context:\n${context}\n` : ""} +${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}` + } - const anthropicMessages = messages.map(msg => ({ - role: msg.role === 'human' ? 'user' : 'assistant', - content: msg.content - })) as MessageParam[]; + const anthropicMessages = messages.map((msg) => ({ + role: msg.role === "human" ? "user" : "assistant", + content: msg.content, + })) as MessageParam[] - try { - const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); + try { + const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }) - const stream = await anthropic.messages.create({ - model: "claude-3-5-sonnet-20240620", - max_tokens: 1024, - system: systemMessage, - messages: anthropicMessages, - stream: true, - }); + const stream = await anthropic.messages.create({ + model: "claude-3-5-sonnet-20240620", + max_tokens: 1024, + system: systemMessage, + messages: anthropicMessages, + stream: true, + }) - const encoder = new TextEncoder(); + const encoder = new TextEncoder() - const streamResponse = new ReadableStream({ - async start(controller) { - for await (const chunk of stream) { - if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') { - const bytes = encoder.encode(chunk.delta.text); - controller.enqueue(bytes); - } - } - controller.close(); - }, - }); + const streamResponse = new ReadableStream({ + async start(controller) { + for await (const chunk of stream) { + if ( + chunk.type === "content_block_delta" && + chunk.delta.type === "text_delta" + ) { + const bytes = encoder.encode(chunk.delta.text) + controller.enqueue(bytes) + } + } + controller.close() + }, + }) - return new Response(streamResponse, { - headers: { - "Content-Type": "text/plain; charset=utf-8", - "Access-Control-Allow-Origin": "*", - "Cache-Control": "no-cache", - "Connection": "keep-alive", - }, - }); - } catch (error) { - console.error("Error:", error); - return new Response("Internal Server Error", { status: 500 }); - } - }, -}; + return new Response(streamResponse, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + } catch (error) { + console.error("Error:", error) + return new Response("Internal Server Error", { status: 500 }) + } + }, +} diff --git a/backend/ai/test/index.spec.ts b/backend/ai/test/index.spec.ts index fbee335..706f17c 100644 --- a/backend/ai/test/index.spec.ts +++ b/backend/ai/test/index.spec.ts @@ -1,25 +1,30 @@ // test/index.spec.ts -import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; -import { describe, it, expect } from 'vitest'; -import worker from '../src/index'; +import { + createExecutionContext, + env, + SELF, + waitOnExecutionContext, +} from "cloudflare:test" +import { describe, expect, it } from "vitest" +import worker from "../src/index" // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. -const IncomingRequest = Request; +const IncomingRequest = Request -describe('Hello World worker', () => { - it('responds with Hello World! (unit style)', async () => { - const request = new IncomingRequest('http://example.com'); +describe("Hello World worker", () => { + it("responds with Hello World! (unit style)", async () => { + const request = new IncomingRequest("http://example.com") // Create an empty context to pass to `worker.fetch()`. - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); + const ctx = createExecutionContext() + const response = await worker.fetch(request, env, ctx) // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - await waitOnExecutionContext(ctx); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); + await waitOnExecutionContext(ctx) + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) - it('responds with Hello World! (integration style)', async () => { - const response = await SELF.fetch('https://example.com'); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); -}); + it("responds with Hello World! (integration style)", async () => { + const response = await SELF.fetch("https://example.com") + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) +}) diff --git a/backend/ai/test/tsconfig.json b/backend/ai/test/tsconfig.json index 509425f..339ee9b 100644 --- a/backend/ai/test/tsconfig.json +++ b/backend/ai/test/tsconfig.json @@ -1,11 +1,11 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "types": [ - "@cloudflare/workers-types/experimental", - "@cloudflare/vitest-pool-workers" - ] - }, - "include": ["./**/*.ts", "../src/env.d.ts"], - "exclude": [] + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [ + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers" + ] + }, + "include": ["./**/*.ts", "../src/env.d.ts"], + "exclude": [] } diff --git a/backend/ai/tsconfig.json b/backend/ai/tsconfig.json index 9192490..8b55b9c 100644 --- a/backend/ai/tsconfig.json +++ b/backend/ai/tsconfig.json @@ -12,7 +12,9 @@ /* Language and Environment */ "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "lib": [ + "es2021" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, "jsx": "react" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ diff --git a/backend/ai/vitest.config.ts b/backend/ai/vitest.config.ts index 973627c..5643ba3 100644 --- a/backend/ai/vitest.config.ts +++ b/backend/ai/vitest.config.ts @@ -1,11 +1,11 @@ -import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" export default defineWorkersConfig({ - test: { - poolOptions: { - workers: { - wrangler: { configPath: "./wrangler.toml" }, - }, - }, - }, -}); + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + }, +}) diff --git a/backend/ai/worker-configuration.d.ts b/backend/ai/worker-configuration.d.ts index 5b2319b..a3f43d2 100644 --- a/backend/ai/worker-configuration.d.ts +++ b/backend/ai/worker-configuration.d.ts @@ -1,4 +1,3 @@ // Generated by Wrangler // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` -interface Env { -} +interface Env {} diff --git a/backend/database/drizzle.config.ts b/backend/database/drizzle.config.ts index 6b6291b..a551fed 100644 --- a/backend/database/drizzle.config.ts +++ b/backend/database/drizzle.config.ts @@ -1,4 +1,4 @@ -import type { Config } from "drizzle-kit"; +import type { Config } from "drizzle-kit" export default process.env.LOCAL_DB_PATH ? ({ @@ -16,4 +16,4 @@ export default process.env.LOCAL_DB_PATH wranglerConfigPath: "wrangler.toml", dbName: "d1-sandbox", }, - } satisfies Config); + } satisfies Config) diff --git a/backend/database/package.json b/backend/database/package.json index 6aae34c..d1aad0f 100644 --- a/backend/database/package.json +++ b/backend/database/package.json @@ -1,32 +1,32 @@ { - "name": "database", - "version": "0.0.0", - "private": true, - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", - "start": "wrangler dev", - "test": "vitest", - "generate": "drizzle-kit generate:sqlite --schema=src/schema.ts", - "up": "drizzle-kit up:sqlite --schema=src/schema.ts", - "db:studio": "cross-env LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio" - }, - "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.1.0", - "@cloudflare/workers-types": "^4.20240405.0", - "@types/itty-router-extras": "^0.4.3", - "drizzle-kit": "^0.20.17", - "typescript": "^5.0.4", - "vitest": "1.3.0", - "wrangler": "^3.0.0" - }, - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "better-sqlite3": "^9.5.0", - "cross-env": "^7.0.3", - "drizzle-orm": "^0.30.8", - "itty-router": "^5.0.16", - "itty-router-extras": "^0.4.6", - "zod": "^3.22.4" - } -} + "name": "database", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "test": "vitest", + "generate": "drizzle-kit generate:sqlite --schema=src/schema.ts", + "up": "drizzle-kit up:sqlite --schema=src/schema.ts", + "db:studio": "cross-env LOCAL_DB_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.1.0", + "@cloudflare/workers-types": "^4.20240405.0", + "@types/itty-router-extras": "^0.4.3", + "drizzle-kit": "^0.20.17", + "typescript": "^5.0.4", + "vitest": "1.3.0", + "wrangler": "^3.0.0" + }, + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "better-sqlite3": "^9.5.0", + "cross-env": "^7.0.3", + "drizzle-orm": "^0.30.8", + "itty-router": "^5.0.16", + "itty-router-extras": "^0.4.6", + "zod": "^3.22.4" + } +} \ No newline at end of file diff --git a/backend/database/src/index.ts b/backend/database/src/index.ts index f4eec2a..d0069e2 100644 --- a/backend/database/src/index.ts +++ b/backend/database/src/index.ts @@ -1,11 +1,11 @@ // import type { DrizzleD1Database } from "drizzle-orm/d1"; import { drizzle } from "drizzle-orm/d1" import { json } from "itty-router-extras" -import { ZodError, z } from "zod" +import { z } from "zod" -import { user, sandbox, usersToSandboxes } from "./schema" -import * as schema from "./schema" import { and, eq, sql } from "drizzle-orm" +import * as schema from "./schema" +import { sandbox, user, usersToSandboxes } from "./schema" export interface Env { DB: D1Database diff --git a/backend/database/src/schema.ts b/backend/database/src/schema.ts index 0d088dd..5b974a7 100644 --- a/backend/database/src/schema.ts +++ b/backend/database/src/schema.ts @@ -1,6 +1,6 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { createId } from "@paralleldrive/cuid2"; -import { relations, sql } from "drizzle-orm"; +import { createId } from "@paralleldrive/cuid2" +import { relations } from "drizzle-orm" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" export const user = sqliteTable("user", { id: text("id") @@ -11,14 +11,14 @@ export const user = sqliteTable("user", { email: text("email").notNull(), image: text("image"), generations: integer("generations").default(0), -}); +}) -export type User = typeof user.$inferSelect; +export type User = typeof user.$inferSelect export const userRelations = relations(user, ({ many }) => ({ sandbox: many(sandbox), usersToSandboxes: many(usersToSandboxes), -})); +})) export const sandbox = sqliteTable("sandbox", { id: text("id") @@ -32,9 +32,9 @@ export const sandbox = sqliteTable("sandbox", { userId: text("user_id") .notNull() .references(() => user.id), -}); +}) -export type Sandbox = typeof sandbox.$inferSelect; +export type Sandbox = typeof sandbox.$inferSelect export const sandboxRelations = relations(sandbox, ({ one, many }) => ({ author: one(user, { @@ -42,7 +42,7 @@ export const sandboxRelations = relations(sandbox, ({ one, many }) => ({ references: [user.id], }), usersToSandboxes: many(usersToSandboxes), -})); +})) export const usersToSandboxes = sqliteTable("users_to_sandboxes", { userId: text("userId") @@ -52,15 +52,18 @@ export const usersToSandboxes = sqliteTable("users_to_sandboxes", { .notNull() .references(() => sandbox.id), sharedOn: integer("sharedOn", { mode: "timestamp_ms" }), -}); +}) -export const usersToSandboxesRelations = relations(usersToSandboxes, ({ one }) => ({ - group: one(sandbox, { - fields: [usersToSandboxes.sandboxId], - references: [sandbox.id], - }), - user: one(user, { - fields: [usersToSandboxes.userId], - references: [user.id], - }), -})); +export const usersToSandboxesRelations = relations( + usersToSandboxes, + ({ one }) => ({ + group: one(sandbox, { + fields: [usersToSandboxes.sandboxId], + references: [sandbox.id], + }), + user: one(user, { + fields: [usersToSandboxes.userId], + references: [user.id], + }), + }) +) diff --git a/backend/database/test/index.spec.ts b/backend/database/test/index.spec.ts index 7522d5b..706f17c 100644 --- a/backend/database/test/index.spec.ts +++ b/backend/database/test/index.spec.ts @@ -1,25 +1,30 @@ // test/index.spec.ts -import { env, createExecutionContext, waitOnExecutionContext, SELF } from "cloudflare:test"; -import { describe, it, expect } from "vitest"; -import worker from "../src/index"; +import { + createExecutionContext, + env, + SELF, + waitOnExecutionContext, +} from "cloudflare:test" +import { describe, expect, it } from "vitest" +import worker from "../src/index" // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. -const IncomingRequest = Request; +const IncomingRequest = Request describe("Hello World worker", () => { - it("responds with Hello World! (unit style)", async () => { - const request = new IncomingRequest("http://example.com"); - // Create an empty context to pass to `worker.fetch()`. - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - await waitOnExecutionContext(ctx); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); + it("responds with Hello World! (unit style)", async () => { + const request = new IncomingRequest("http://example.com") + // Create an empty context to pass to `worker.fetch()`. + const ctx = createExecutionContext() + const response = await worker.fetch(request, env, ctx) + // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions + await waitOnExecutionContext(ctx) + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) - it("responds with Hello World! (integration style)", async () => { - const response = await SELF.fetch("https://example.com"); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); -}); + it("responds with Hello World! (integration style)", async () => { + const response = await SELF.fetch("https://example.com") + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) +}) diff --git a/backend/database/test/tsconfig.json b/backend/database/test/tsconfig.json index 509425f..339ee9b 100644 --- a/backend/database/test/tsconfig.json +++ b/backend/database/test/tsconfig.json @@ -1,11 +1,11 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "types": [ - "@cloudflare/workers-types/experimental", - "@cloudflare/vitest-pool-workers" - ] - }, - "include": ["./**/*.ts", "../src/env.d.ts"], - "exclude": [] + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [ + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers" + ] + }, + "include": ["./**/*.ts", "../src/env.d.ts"], + "exclude": [] } diff --git a/backend/database/tsconfig.json b/backend/database/tsconfig.json index 9192490..8b55b9c 100644 --- a/backend/database/tsconfig.json +++ b/backend/database/tsconfig.json @@ -12,7 +12,9 @@ /* Language and Environment */ "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "lib": [ + "es2021" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, "jsx": "react" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ diff --git a/backend/database/vitest.config.ts b/backend/database/vitest.config.ts index 973627c..5643ba3 100644 --- a/backend/database/vitest.config.ts +++ b/backend/database/vitest.config.ts @@ -1,11 +1,11 @@ -import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" export default defineWorkersConfig({ - test: { - poolOptions: { - workers: { - wrangler: { configPath: "./wrangler.toml" }, - }, - }, - }, -}); + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + }, +}) diff --git a/backend/storage/package.json b/backend/storage/package.json index 3f5c39f..215832e 100644 --- a/backend/storage/package.json +++ b/backend/storage/package.json @@ -1,23 +1,23 @@ { - "name": "storage", - "version": "0.0.0", - "private": true, - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev --remote", - "start": "wrangler dev", - "test": "vitest", - "cf-typegen": "wrangler types" - }, - "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.1.0", - "@cloudflare/workers-types": "^4.20240419.0", - "typescript": "^5.0.4", - "vitest": "1.3.0", - "wrangler": "^3.0.0" - }, - "dependencies": { - "p-limit": "^6.1.0", - "zod": "^3.23.4" - } -} + "name": "storage", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev --remote", + "start": "wrangler dev", + "test": "vitest", + "cf-typegen": "wrangler types" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.1.0", + "@cloudflare/workers-types": "^4.20240419.0", + "typescript": "^5.0.4", + "vitest": "1.3.0", + "wrangler": "^3.0.0" + }, + "dependencies": { + "p-limit": "^6.1.0", + "zod": "^3.23.4" + } +} \ No newline at end of file diff --git a/backend/storage/src/index.ts b/backend/storage/src/index.ts index c9b371e..e7a7294 100644 --- a/backend/storage/src/index.ts +++ b/backend/storage/src/index.ts @@ -1,5 +1,5 @@ +import pLimit from "p-limit" import { z } from "zod" -import pLimit from 'p-limit'; export interface Env { R2: R2Bucket @@ -144,20 +144,24 @@ export default { const body = await request.json() const { sandboxId, type } = initSchema.parse(body) - console.log(`Copying template: ${type}`); + console.log(`Copying template: ${type}`) // List all objects under the directory - const { objects } = await env.Templates.list({ prefix: type }); + const { objects } = await env.Templates.list({ prefix: type }) // Copy each object to the new directory with a 5 concurrency limit - const limit = pLimit(5); - await Promise.all(objects.map(({ key }) => - limit(async () => { - const destinationKey = key.replace(type, `projects/${sandboxId}`); - const fileBody = await env.Templates.get(key).then(res => res?.body ?? ""); - await env.R2.put(destinationKey, fileBody); - }) - )); + const limit = pLimit(5) + await Promise.all( + objects.map(({ key }) => + limit(async () => { + const destinationKey = key.replace(type, `projects/${sandboxId}`) + const fileBody = await env.Templates.get(key).then( + (res) => res?.body ?? "" + ) + await env.R2.put(destinationKey, fileBody) + }) + ) + ) return success } else { diff --git a/backend/storage/test/index.spec.ts b/backend/storage/test/index.spec.ts index fbee335..706f17c 100644 --- a/backend/storage/test/index.spec.ts +++ b/backend/storage/test/index.spec.ts @@ -1,25 +1,30 @@ // test/index.spec.ts -import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; -import { describe, it, expect } from 'vitest'; -import worker from '../src/index'; +import { + createExecutionContext, + env, + SELF, + waitOnExecutionContext, +} from "cloudflare:test" +import { describe, expect, it } from "vitest" +import worker from "../src/index" // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. -const IncomingRequest = Request; +const IncomingRequest = Request -describe('Hello World worker', () => { - it('responds with Hello World! (unit style)', async () => { - const request = new IncomingRequest('http://example.com'); +describe("Hello World worker", () => { + it("responds with Hello World! (unit style)", async () => { + const request = new IncomingRequest("http://example.com") // Create an empty context to pass to `worker.fetch()`. - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); + const ctx = createExecutionContext() + const response = await worker.fetch(request, env, ctx) // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - await waitOnExecutionContext(ctx); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); + await waitOnExecutionContext(ctx) + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) - it('responds with Hello World! (integration style)', async () => { - const response = await SELF.fetch('https://example.com'); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); -}); + it("responds with Hello World! (integration style)", async () => { + const response = await SELF.fetch("https://example.com") + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) +}) diff --git a/backend/storage/test/tsconfig.json b/backend/storage/test/tsconfig.json index 509425f..339ee9b 100644 --- a/backend/storage/test/tsconfig.json +++ b/backend/storage/test/tsconfig.json @@ -1,11 +1,11 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "types": [ - "@cloudflare/workers-types/experimental", - "@cloudflare/vitest-pool-workers" - ] - }, - "include": ["./**/*.ts", "../src/env.d.ts"], - "exclude": [] + "extends": "../tsconfig.json", + "compilerOptions": { + "types": [ + "@cloudflare/workers-types/experimental", + "@cloudflare/vitest-pool-workers" + ] + }, + "include": ["./**/*.ts", "../src/env.d.ts"], + "exclude": [] } diff --git a/backend/storage/tsconfig.json b/backend/storage/tsconfig.json index 9192490..8b55b9c 100644 --- a/backend/storage/tsconfig.json +++ b/backend/storage/tsconfig.json @@ -12,7 +12,9 @@ /* Language and Environment */ "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "lib": [ + "es2021" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, "jsx": "react" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ diff --git a/backend/storage/vitest.config.ts b/backend/storage/vitest.config.ts index 973627c..5643ba3 100644 --- a/backend/storage/vitest.config.ts +++ b/backend/storage/vitest.config.ts @@ -1,11 +1,11 @@ -import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config" export default defineWorkersConfig({ - test: { - poolOptions: { - workers: { - wrangler: { configPath: "./wrangler.toml" }, - }, - }, - }, -}); + test: { + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.toml" }, + }, + }, + }, +}) diff --git a/backend/storage/worker-configuration.d.ts b/backend/storage/worker-configuration.d.ts index 5b2319b..a3f43d2 100644 --- a/backend/storage/worker-configuration.d.ts +++ b/backend/storage/worker-configuration.d.ts @@ -1,4 +1,3 @@ // Generated by Wrangler // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` -interface Env { -} +interface Env {} From 67f3efa038ce1ad899428cb9d145c238c6a0a629 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 15:59:21 -0600 Subject: [PATCH 02/27] refactor: move DokkuResponse to types --- backend/server/src/index.ts | 92 ++++++++++++++++--------------------- backend/server/src/types.ts | 5 ++ 2 files changed, 44 insertions(+), 53 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 788c329..028f916 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -18,7 +18,7 @@ import { } from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" import { TerminalManager } from "./TerminalManager" -import { User } from "./types" +import { DokkuResponse, User } from "./types" import { LockManager } from "./utils" // Handle uncaught exceptions @@ -296,63 +296,49 @@ io.on("connection", async (socket) => { } ) - interface CallbackResponse { - success: boolean - apps?: string[] - message?: string - } - // Handle request to list apps - socket.on( - "list", - async (callback: (response: CallbackResponse) => void) => { - console.log("Retrieving apps list...") - try { - if (!client) - throw Error("Failed to retrieve apps list: No Dokku client") - callback({ - success: true, - apps: await client.listApps(), - }) - } catch (error) { - callback({ - success: false, - message: "Failed to retrieve apps list", - }) - } + socket.on("list", async (callback: (response: DokkuResponse) => void) => { + console.log("Retrieving apps list...") + try { + if (!client) + throw Error("Failed to retrieve apps list: No Dokku client") + callback({ + success: true, + apps: await client.listApps(), + }) + } catch (error) { + callback({ + success: false, + message: "Failed to retrieve apps list", + }) } - ) + }) // Handle request to deploy project - socket.on( - "deploy", - async (callback: (response: CallbackResponse) => void) => { - try { - // Push the project files to the Dokku server - console.log("Deploying project ${data.sandboxId}...") - if (!git) throw Error("Failed to retrieve apps list: No git client") - // Remove the /project/[id]/ component of each file path: - const fixedFilePaths = fileManager.sandboxFiles.fileData.map( - (file) => { - return { - ...file, - id: file.id.split("/").slice(2).join("/"), - } - } - ) - // Push all files to Dokku. - await git.pushFiles(fixedFilePaths, data.sandboxId) - callback({ - success: true, - }) - } catch (error) { - callback({ - success: false, - message: "Failed to deploy project: " + error, - }) - } + socket.on("deploy", async (callback: (response: DokkuResponse) => void) => { + try { + // Push the project files to the Dokku server + console.log("Deploying project ${data.sandboxId}...") + if (!git) throw Error("Failed to retrieve apps list: No git client") + // Remove the /project/[id]/ component of each file path: + const fixedFilePaths = fileManager.sandboxFiles.fileData.map((file) => { + return { + ...file, + id: file.id.split("/").slice(2).join("/"), + } + }) + // Push all files to Dokku. + await git.pushFiles(fixedFilePaths, data.sandboxId) + callback({ + success: true, + }) + } catch (error) { + callback({ + success: false, + message: "Failed to deploy project: " + error, + }) } - ) + }) // Handle request to create a new file socket.on("createFile", async (name: string, callback) => { diff --git a/backend/server/src/types.ts b/backend/server/src/types.ts index 42ad6d0..93e45e6 100644 --- a/backend/server/src/types.ts +++ b/backend/server/src/types.ts @@ -68,3 +68,8 @@ export type R2FileBody = R2FileData & { json: Promise blob: Promise } +export interface DokkuResponse { + success: boolean + apps?: string[] + message?: string +} From 98eda3b080a72b2a2296797f5ed7d8205ab1339a Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 16:20:24 -0600 Subject: [PATCH 03/27] refactor: move event handlers to a separate file --- backend/server/src/SocketHandlers.ts | 119 ++++++++++++++++++ backend/server/src/constants.ts | 2 + backend/server/src/index.ts | 181 +++++++-------------------- 3 files changed, 166 insertions(+), 136 deletions(-) create mode 100644 backend/server/src/SocketHandlers.ts create mode 100644 backend/server/src/constants.ts diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts new file mode 100644 index 0000000..1285280 --- /dev/null +++ b/backend/server/src/SocketHandlers.ts @@ -0,0 +1,119 @@ +import { AIWorker } from "./AIWorker" +import { CONTAINER_TIMEOUT } from "./constants" +import { DokkuClient } from "./DokkuClient" +import { FileManager } from "./FileManager" +import { SecureGitClient } from "./SecureGitClient" +import { TerminalManager } from "./TerminalManager" +import { LockManager } from "./utils" + +// Extract port number from a string +function extractPortNumber(inputString: string): number | null { + const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "") + const regex = /http:\/\/localhost:(\d+)/ + const match = cleanedString.match(regex) + return match ? parseInt(match[1]) : null +} + +// Handle heartbeat from a socket connection +export function handleHeartbeat(socket: any, data: any, containers: any) { + containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT) +} + +// Handle getting a file +export function handleGetFile(fileManager: FileManager, fileId: string) { + return fileManager.getFile(fileId) +} + +// Handle getting a folder +export function handleGetFolder(fileManager: FileManager, folderId: string) { + return fileManager.getFolder(folderId) +} + +// Handle saving a file +export function handleSaveFile(fileManager: FileManager, fileId: string, body: string) { + return fileManager.saveFile(fileId, body) +} + +// Handle moving a file +export function handleMoveFile(fileManager: FileManager, fileId: string, folderId: string) { + return fileManager.moveFile(fileId, folderId) +} + +// Handle listing apps +export async function handleListApps(client: DokkuClient | null) { + if (!client) throw Error("Failed to retrieve apps list: No Dokku client") + return { success: true, apps: await client.listApps() } +} + +// Handle deploying code +export async function handleDeploy(git: SecureGitClient | null, fileManager: FileManager, sandboxId: string) { + if (!git) throw Error("Failed to retrieve apps list: No git client") + const fixedFilePaths = fileManager.sandboxFiles.fileData.map((file) => ({ + ...file, + id: file.id.split("/").slice(2).join("/"), + })) + await git.pushFiles(fixedFilePaths, sandboxId) + return { success: true } +} + +// Handle creating a file +export function handleCreateFile(fileManager: FileManager, name: string) { + return fileManager.createFile(name) +} + +// Handle creating a folder +export function handleCreateFolder(fileManager: FileManager, name: string) { + return fileManager.createFolder(name) +} + +// Handle renaming a file +export function handleRenameFile(fileManager: FileManager, fileId: string, newName: string) { + return fileManager.renameFile(fileId, newName) +} + +// Handle deleting a file +export function handleDeleteFile(fileManager: FileManager, fileId: string) { + return fileManager.deleteFile(fileId) +} + +// Handle deleting a folder +export function handleDeleteFolder(fileManager: FileManager, folderId: string) { + return fileManager.deleteFolder(folderId) +} + +// Handle creating a terminal session +export async function handleCreateTerminal(lockManager: LockManager, terminalManager: TerminalManager, id: string, socket: any, containers: any, data: any) { + await lockManager.acquireLock(data.sandboxId, async () => { + await terminalManager.createTerminal(id, (responseString: string) => { + socket.emit("terminalResponse", { id, data: responseString }) + const port = extractPortNumber(responseString) + if (port) { + socket.emit( + "previewURL", + "https://" + containers[data.sandboxId].getHost(port) + ) + } + }) + }) +} + +// Handle resizing a terminal +export function handleResizeTerminal(terminalManager: TerminalManager, dimensions: { cols: number; rows: number }) { + terminalManager.resizeTerminal(dimensions) +} + +// Handle sending data to a terminal +export function handleTerminalData(terminalManager: TerminalManager, id: string, data: string) { + return terminalManager.sendTerminalData(id, data) +} + +// Handle closing a terminal +export function handleCloseTerminal(terminalManager: TerminalManager, id: string) { + return terminalManager.closeTerminal(id) +} + +// Handle generating code +export function handleGenerateCode(aiWorker: AIWorker, userId: string, fileName: string, code: string, line: number, instructions: string) { + return aiWorker.generateCode(userId, fileName, code, line, instructions) +} +} \ No newline at end of file diff --git a/backend/server/src/constants.ts b/backend/server/src/constants.ts new file mode 100644 index 0000000..dfd5ce3 --- /dev/null +++ b/backend/server/src/constants.ts @@ -0,0 +1,2 @@ +// The amount of time in ms that a container will stay alive without a hearbeat. +export const CONTAINER_TIMEOUT = 120_000 \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 028f916..794af71 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -7,6 +7,7 @@ import { createServer } from "http" import { Server } from "socket.io" import { z } from "zod" import { AIWorker } from "./AIWorker" +import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" import { FileManager, SandboxFiles } from "./FileManager" import { @@ -17,6 +18,7 @@ import { saveFileRL, } from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" +import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleDisconnect, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" import { TerminalManager } from "./TerminalManager" import { DokkuResponse, User } from "./types" import { LockManager } from "./utils" @@ -35,9 +37,6 @@ process.on("unhandledRejection", (reason, promise) => { // You can also handle the rejected promise here if needed }) -// The amount of time in ms that a container will stay alive without a hearbeat. -const CONTAINER_TIMEOUT = 120_000 - // Load environment variables dotenv.config() @@ -57,14 +56,6 @@ function isOwnerConnected(sandboxId: string): boolean { return (connections[sandboxId] ?? 0) > 0 } -// Extract port number from a string -function extractPortNumber(inputString: string): number | null { - const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "") - const regex = /http:\/\/localhost:(\d+)/ - const match = cleanedString.match(regex) - return match ? parseInt(match[1]) : null -} - // Initialize containers and managers const containers: Record = {} const connections: Record = {} @@ -146,10 +137,10 @@ if (!process.env.DOKKU_KEY) const client = process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME ? new DokkuClient({ - host: process.env.DOKKU_HOST, - username: process.env.DOKKU_USERNAME, - privateKey: fs.readFileSync(process.env.DOKKU_KEY), - }) + host: process.env.DOKKU_HOST, + username: process.env.DOKKU_USERNAME, + privateKey: fs.readFileSync(process.env.DOKKU_KEY), + }) : null client?.connect() @@ -157,9 +148,9 @@ client?.connect() const git = process.env.DOKKU_HOST && process.env.DOKKU_KEY ? new SecureGitClient( - `dokku@${process.env.DOKKU_HOST}`, - process.env.DOKKU_KEY - ) + `dokku@${process.env.DOKKU_HOST}`, + process.env.DOKKU_KEY + ) : null // Add this near the top of the file, after other initializations @@ -170,7 +161,7 @@ const aiWorker = new AIWorker( process.env.WORKERS_KEY! ) -// Handle socket connections +// Handle a client connecting to the server io.on("connection", async (socket) => { try { const data = socket.data as { @@ -240,72 +231,54 @@ io.on("connection", async (socket) => { // Handle various socket events (heartbeat, file operations, terminal operations, etc.) socket.on("heartbeat", async () => { try { - // This keeps the container alive for another CONTAINER_TIMEOUT seconds. - // The E2B docs are unclear, but the timeout is relative to the time of this method call. - await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT) + handleHeartbeat(socket, data, containers) } catch (e: any) { console.error("Error setting timeout:", e) socket.emit("error", `Error: set timeout. ${e.message ?? e}`) } }) - // Handle request to get file content socket.on("getFile", async (fileId: string, callback) => { try { - const fileContent = await fileManager.getFile(fileId) - callback(fileContent) + callback(await handleGetFile(fileManager, fileId)) } catch (e: any) { console.error("Error getting file:", e) socket.emit("error", `Error: get file. ${e.message ?? e}`) } }) - // Handle request to get folder contents socket.on("getFolder", async (folderId: string, callback) => { try { - const files = await fileManager.getFolder(folderId) - callback(files) + callback(await handleGetFolder(fileManager, folderId)) } catch (e: any) { console.error("Error getting folder:", e) socket.emit("error", `Error: get folder. ${e.message ?? e}`) } }) - // Handle request to save file socket.on("saveFile", async (fileId: string, body: string) => { try { await saveFileRL.consume(data.userId, 1) - await fileManager.saveFile(fileId, body) + await handleSaveFile(fileManager, fileId, body) } catch (e: any) { console.error("Error saving file:", e) socket.emit("error", `Error: file saving. ${e.message ?? e}`) } }) - // Handle request to move file - socket.on( - "moveFile", - async (fileId: string, folderId: string, callback) => { - try { - const newFiles = await fileManager.moveFile(fileId, folderId) - callback(newFiles) - } catch (e: any) { - console.error("Error moving file:", e) - socket.emit("error", `Error: file moving. ${e.message ?? e}`) - } + socket.on("moveFile", async (fileId: string, folderId: string, callback) => { + try { + callback(await handleMoveFile(fileManager, fileId, folderId)) + } catch (e: any) { + console.error("Error moving file:", e) + socket.emit("error", `Error: file moving. ${e.message ?? e}`) } - ) + }) - // Handle request to list apps socket.on("list", async (callback: (response: DokkuResponse) => void) => { console.log("Retrieving apps list...") try { - if (!client) - throw Error("Failed to retrieve apps list: No Dokku client") - callback({ - success: true, - apps: await client.listApps(), - }) + callback(await handleListApps(client)) } catch (error) { callback({ success: false, @@ -314,24 +287,10 @@ io.on("connection", async (socket) => { } }) - // Handle request to deploy project socket.on("deploy", async (callback: (response: DokkuResponse) => void) => { try { - // Push the project files to the Dokku server console.log("Deploying project ${data.sandboxId}...") - if (!git) throw Error("Failed to retrieve apps list: No git client") - // Remove the /project/[id]/ component of each file path: - const fixedFilePaths = fileManager.sandboxFiles.fileData.map((file) => { - return { - ...file, - id: file.id.split("/").slice(2).join("/"), - } - }) - // Push all files to Dokku. - await git.pushFiles(fixedFilePaths, data.sandboxId) - callback({ - success: true, - }) + callback(await handleDeploy(git, fileManager, data.sandboxId)) } catch (error) { callback({ success: false, @@ -340,23 +299,20 @@ io.on("connection", async (socket) => { } }) - // Handle request to create a new file socket.on("createFile", async (name: string, callback) => { try { await createFileRL.consume(data.userId, 1) - const success = await fileManager.createFile(name) - callback({ success }) + callback({ success: await handleCreateFile(fileManager, name) }) } catch (e: any) { console.error("Error creating file:", e) socket.emit("error", `Error: file creation. ${e.message ?? e}`) } }) - // Handle request to create a new folder socket.on("createFolder", async (name: string, callback) => { try { await createFolderRL.consume(data.userId, 1) - await fileManager.createFolder(name) + await handleCreateFolder(fileManager, name) callback() } catch (e: any) { console.error("Error creating folder:", e) @@ -364,61 +320,38 @@ io.on("connection", async (socket) => { } }) - // Handle request to rename a file socket.on("renameFile", async (fileId: string, newName: string) => { try { await renameFileRL.consume(data.userId, 1) - await fileManager.renameFile(fileId, newName) + await handleRenameFile(fileManager, fileId, newName) } catch (e: any) { console.error("Error renaming file:", e) socket.emit("error", `Error: file renaming. ${e.message ?? e}`) } }) - // Handle request to delete a file socket.on("deleteFile", async (fileId: string, callback) => { try { await deleteFileRL.consume(data.userId, 1) - const newFiles = await fileManager.deleteFile(fileId) - callback(newFiles) + callback(await handleDeleteFile(fileManager, fileId)) } catch (e: any) { console.error("Error deleting file:", e) socket.emit("error", `Error: file deletion. ${e.message ?? e}`) } }) - // Handle request to delete a folder socket.on("deleteFolder", async (folderId: string, callback) => { try { - const newFiles = await fileManager.deleteFolder(folderId) - callback(newFiles) + callback(await handleDeleteFolder(fileManager, folderId)) } catch (e: any) { console.error("Error deleting folder:", e) socket.emit("error", `Error: folder deletion. ${e.message ?? e}`) } }) - // Handle request to create a new terminal socket.on("createTerminal", async (id: string, callback) => { try { - await lockManager.acquireLock(data.sandboxId, async () => { - let terminalManager = terminalManagers[data.sandboxId] - if (!terminalManager) { - terminalManager = terminalManagers[data.sandboxId] = - new TerminalManager(containers[data.sandboxId]) - } - - await terminalManager.createTerminal(id, (responseString: string) => { - socket.emit("terminalResponse", { id, data: responseString }) - const port = extractPortNumber(responseString) - if (port) { - socket.emit( - "previewURL", - "https://" + containers[data.sandboxId].getHost(port) - ) - } - }) - }) + await handleCreateTerminal(lockManager, terminalManager, id, socket, containers, data) callback() } catch (e: any) { console.error(`Error creating terminal ${id}:`, e) @@ -426,33 +359,27 @@ io.on("connection", async (socket) => { } }) - // Handle request to resize terminal - socket.on( - "resizeTerminal", - (dimensions: { cols: number; rows: number }) => { - try { - terminalManager.resizeTerminal(dimensions) - } catch (e: any) { - console.error("Error resizing terminal:", e) - socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`) - } + socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => { + try { + handleResizeTerminal(terminalManager, dimensions) + } catch (e: any) { + console.error("Error resizing terminal:", e) + socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`) } - ) + }) - // Handle terminal input data socket.on("terminalData", async (id: string, data: string) => { try { - await terminalManager.sendTerminalData(id, data) + await handleTerminalData(terminalManager, id, data) } catch (e: any) { console.error("Error writing to terminal:", e) socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`) } }) - // Handle request to close terminal socket.on("closeTerminal", async (id: string, callback) => { try { - await terminalManager.closeTerminal(id) + await handleCloseTerminal(terminalManager, id) callback() } catch (e: any) { console.error("Error closing terminal:", e) @@ -460,33 +387,15 @@ io.on("connection", async (socket) => { } }) - // Handle request to generate code - socket.on( - "generateCode", - async ( - fileName: string, - code: string, - line: number, - instructions: string, - callback - ) => { - try { - const result = await aiWorker.generateCode( - data.userId, - fileName, - code, - line, - instructions - ) - callback(result) - } catch (e: any) { - console.error("Error generating code:", e) - socket.emit("error", `Error: code generation. ${e.message ?? e}`) - } + socket.on("generateCode", async (fileName: string, code: string, line: number, instructions: string, callback) => { + try { + callback(await handleGenerateCode(aiWorker, data.userId, fileName, code, line, instructions)) + } catch (e: any) { + console.error("Error generating code:", e) + socket.emit("error", `Error: code generation. ${e.message ?? e}`) } - ) + }) - // Handle socket disconnection socket.on("disconnect", async () => { try { if (data.isOwner) { From af83b33f51c88b89e566f7aba3a642a7659842ba Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 17:15:58 -0600 Subject: [PATCH 04/27] refactor: pass context as object to event handlers --- backend/server/src/SocketHandlers.ts | 89 +++++++++++++++------------- backend/server/src/index.ts | 52 +++++++++------- 2 files changed, 80 insertions(+), 61 deletions(-) diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts index 1285280..43a60cc 100644 --- a/backend/server/src/SocketHandlers.ts +++ b/backend/server/src/SocketHandlers.ts @@ -6,6 +6,16 @@ import { SecureGitClient } from "./SecureGitClient" import { TerminalManager } from "./TerminalManager" import { LockManager } from "./utils" +export interface HandlerContext { + fileManager: FileManager; + terminalManager: TerminalManager; + sandboxManager: any; + aiWorker: AIWorker; + dokkuClient: DokkuClient | null; + gitClient: SecureGitClient | null; + lockManager: LockManager +} + // Extract port number from a string function extractPortNumber(inputString: string): number | null { const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "") @@ -15,82 +25,82 @@ function extractPortNumber(inputString: string): number | null { } // Handle heartbeat from a socket connection -export function handleHeartbeat(socket: any, data: any, containers: any) { - containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT) +export function handleHeartbeat(data: any, context: HandlerContext) { + context.sandboxManager.setTimeout(CONTAINER_TIMEOUT) } // Handle getting a file -export function handleGetFile(fileManager: FileManager, fileId: string) { - return fileManager.getFile(fileId) +export function handleGetFile(fileId: string, context: HandlerContext) { + return context.fileManager.getFile(fileId) } // Handle getting a folder -export function handleGetFolder(fileManager: FileManager, folderId: string) { - return fileManager.getFolder(folderId) +export function handleGetFolder(folderId: string, context: HandlerContext) { + return context.fileManager.getFolder(folderId) } // Handle saving a file -export function handleSaveFile(fileManager: FileManager, fileId: string, body: string) { - return fileManager.saveFile(fileId, body) +export function handleSaveFile(fileId: string, body: string, context: HandlerContext) { + return context.fileManager.saveFile(fileId, body) } // Handle moving a file -export function handleMoveFile(fileManager: FileManager, fileId: string, folderId: string) { - return fileManager.moveFile(fileId, folderId) +export function handleMoveFile(fileId: string, folderId: string, context: HandlerContext) { + return context.fileManager.moveFile(fileId, folderId) } // Handle listing apps -export async function handleListApps(client: DokkuClient | null) { - if (!client) throw Error("Failed to retrieve apps list: No Dokku client") - return { success: true, apps: await client.listApps() } +export async function handleListApps(context: HandlerContext) { + if (!context.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") + return { success: true, apps: await context.dokkuClient.listApps() } } // Handle deploying code -export async function handleDeploy(git: SecureGitClient | null, fileManager: FileManager, sandboxId: string) { - if (!git) throw Error("Failed to retrieve apps list: No git client") - const fixedFilePaths = fileManager.sandboxFiles.fileData.map((file) => ({ +export async function handleDeploy(sandboxId: string, context: HandlerContext) { + if (!context.gitClient) throw Error("Failed to retrieve apps list: No git client") + const fixedFilePaths = context.fileManager.sandboxFiles.fileData.map((file) => ({ ...file, id: file.id.split("/").slice(2).join("/"), })) - await git.pushFiles(fixedFilePaths, sandboxId) + await context.gitClient.pushFiles(fixedFilePaths, sandboxId) return { success: true } } // Handle creating a file -export function handleCreateFile(fileManager: FileManager, name: string) { - return fileManager.createFile(name) +export function handleCreateFile(name: string, context: HandlerContext) { + return context.fileManager.createFile(name) } // Handle creating a folder -export function handleCreateFolder(fileManager: FileManager, name: string) { - return fileManager.createFolder(name) +export function handleCreateFolder(name: string, context: HandlerContext) { + return context.fileManager.createFolder(name) } // Handle renaming a file -export function handleRenameFile(fileManager: FileManager, fileId: string, newName: string) { - return fileManager.renameFile(fileId, newName) +export function handleRenameFile(fileId: string, newName: string, context: HandlerContext) { + return context.fileManager.renameFile(fileId, newName) } // Handle deleting a file -export function handleDeleteFile(fileManager: FileManager, fileId: string) { - return fileManager.deleteFile(fileId) +export function handleDeleteFile(fileId: string, context: HandlerContext) { + return context.fileManager.deleteFile(fileId) } // Handle deleting a folder -export function handleDeleteFolder(fileManager: FileManager, folderId: string) { - return fileManager.deleteFolder(folderId) +export function handleDeleteFolder(folderId: string, context: HandlerContext) { + return context.fileManager.deleteFolder(folderId) } // Handle creating a terminal session -export async function handleCreateTerminal(lockManager: LockManager, terminalManager: TerminalManager, id: string, socket: any, containers: any, data: any) { - await lockManager.acquireLock(data.sandboxId, async () => { - await terminalManager.createTerminal(id, (responseString: string) => { +export async function handleCreateTerminal(id: string, socket: any, data: any, context: HandlerContext) { + await context.lockManager.acquireLock(data.sandboxId, async () => { + await context.terminalManager.createTerminal(id, (responseString: string) => { socket.emit("terminalResponse", { id, data: responseString }) const port = extractPortNumber(responseString) if (port) { socket.emit( "previewURL", - "https://" + containers[data.sandboxId].getHost(port) + "https://" + context.sandboxManager.getHost(port) ) } }) @@ -98,22 +108,21 @@ export async function handleCreateTerminal(lockManager: LockManager, terminalMan } // Handle resizing a terminal -export function handleResizeTerminal(terminalManager: TerminalManager, dimensions: { cols: number; rows: number }) { - terminalManager.resizeTerminal(dimensions) +export function handleResizeTerminal(dimensions: { cols: number; rows: number }, context: HandlerContext) { + context.terminalManager.resizeTerminal(dimensions) } // Handle sending data to a terminal -export function handleTerminalData(terminalManager: TerminalManager, id: string, data: string) { - return terminalManager.sendTerminalData(id, data) +export function handleTerminalData(id: string, data: string, context: HandlerContext) { + return context.terminalManager.sendTerminalData(id, data) } // Handle closing a terminal -export function handleCloseTerminal(terminalManager: TerminalManager, id: string) { - return terminalManager.closeTerminal(id) +export function handleCloseTerminal(id: string, context: HandlerContext) { + return context.terminalManager.closeTerminal(id) } // Handle generating code -export function handleGenerateCode(aiWorker: AIWorker, userId: string, fileName: string, code: string, line: number, instructions: string) { - return aiWorker.generateCode(userId, fileName, code, line, instructions) -} +export function handleGenerateCode(userId: string, fileName: string, code: string, line: number, instructions: string, context: HandlerContext) { + return context.aiWorker.generateCode(userId, fileName, code, line, instructions) } \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 794af71..142f3d3 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -18,7 +18,7 @@ import { saveFileRL, } from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" -import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleDisconnect, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" +import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, HandlerContext, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" import { TerminalManager } from "./TerminalManager" import { DokkuResponse, User } from "./types" import { LockManager } from "./utils" @@ -134,7 +134,7 @@ if (!process.env.DOKKU_KEY) console.error("Environment variable DOKKU_KEY is not defined") // Initialize Dokku client -const client = +const dokkuClient = process.env.DOKKU_HOST && process.env.DOKKU_KEY && process.env.DOKKU_USERNAME ? new DokkuClient({ host: process.env.DOKKU_HOST, @@ -142,10 +142,10 @@ const client = privateKey: fs.readFileSync(process.env.DOKKU_KEY), }) : null -client?.connect() +dokkuClient?.connect() // Initialize Git client used to deploy Dokku apps -const git = +const gitClient = process.env.DOKKU_HOST && process.env.DOKKU_KEY ? new SecureGitClient( `dokku@${process.env.DOKKU_HOST}`, @@ -228,10 +228,20 @@ io.on("connection", async (socket) => { // Load file list from the file manager into the editor sendLoadedEvent(fileManager.sandboxFiles) + const handlerContext: HandlerContext = { + fileManager, + terminalManager, + aiWorker, + dokkuClient, + gitClient, + lockManager, + sandboxManager: containers[data.sandboxId], + } + // Handle various socket events (heartbeat, file operations, terminal operations, etc.) socket.on("heartbeat", async () => { try { - handleHeartbeat(socket, data, containers) + handleHeartbeat(data, handlerContext) } catch (e: any) { console.error("Error setting timeout:", e) socket.emit("error", `Error: set timeout. ${e.message ?? e}`) @@ -240,7 +250,7 @@ io.on("connection", async (socket) => { socket.on("getFile", async (fileId: string, callback) => { try { - callback(await handleGetFile(fileManager, fileId)) + callback(await handleGetFile(fileId, handlerContext)) } catch (e: any) { console.error("Error getting file:", e) socket.emit("error", `Error: get file. ${e.message ?? e}`) @@ -249,7 +259,7 @@ io.on("connection", async (socket) => { socket.on("getFolder", async (folderId: string, callback) => { try { - callback(await handleGetFolder(fileManager, folderId)) + callback(await handleGetFolder(folderId, handlerContext)) } catch (e: any) { console.error("Error getting folder:", e) socket.emit("error", `Error: get folder. ${e.message ?? e}`) @@ -259,7 +269,7 @@ io.on("connection", async (socket) => { socket.on("saveFile", async (fileId: string, body: string) => { try { await saveFileRL.consume(data.userId, 1) - await handleSaveFile(fileManager, fileId, body) + await handleSaveFile(fileId, body, handlerContext) } catch (e: any) { console.error("Error saving file:", e) socket.emit("error", `Error: file saving. ${e.message ?? e}`) @@ -268,7 +278,7 @@ io.on("connection", async (socket) => { socket.on("moveFile", async (fileId: string, folderId: string, callback) => { try { - callback(await handleMoveFile(fileManager, fileId, folderId)) + callback(await handleMoveFile(fileId, folderId, handlerContext)) } catch (e: any) { console.error("Error moving file:", e) socket.emit("error", `Error: file moving. ${e.message ?? e}`) @@ -278,7 +288,7 @@ io.on("connection", async (socket) => { socket.on("list", async (callback: (response: DokkuResponse) => void) => { console.log("Retrieving apps list...") try { - callback(await handleListApps(client)) + callback(await handleListApps(handlerContext)) } catch (error) { callback({ success: false, @@ -290,7 +300,7 @@ io.on("connection", async (socket) => { socket.on("deploy", async (callback: (response: DokkuResponse) => void) => { try { console.log("Deploying project ${data.sandboxId}...") - callback(await handleDeploy(git, fileManager, data.sandboxId)) + callback(await handleDeploy(data.sandboxId, handlerContext)) } catch (error) { callback({ success: false, @@ -302,7 +312,7 @@ io.on("connection", async (socket) => { socket.on("createFile", async (name: string, callback) => { try { await createFileRL.consume(data.userId, 1) - callback({ success: await handleCreateFile(fileManager, name) }) + callback({ success: await handleCreateFile(name, handlerContext) }) } catch (e: any) { console.error("Error creating file:", e) socket.emit("error", `Error: file creation. ${e.message ?? e}`) @@ -312,7 +322,7 @@ io.on("connection", async (socket) => { socket.on("createFolder", async (name: string, callback) => { try { await createFolderRL.consume(data.userId, 1) - await handleCreateFolder(fileManager, name) + await handleCreateFolder(name, handlerContext) callback() } catch (e: any) { console.error("Error creating folder:", e) @@ -323,7 +333,7 @@ io.on("connection", async (socket) => { socket.on("renameFile", async (fileId: string, newName: string) => { try { await renameFileRL.consume(data.userId, 1) - await handleRenameFile(fileManager, fileId, newName) + await handleRenameFile(fileId, newName, handlerContext) } catch (e: any) { console.error("Error renaming file:", e) socket.emit("error", `Error: file renaming. ${e.message ?? e}`) @@ -333,7 +343,7 @@ io.on("connection", async (socket) => { socket.on("deleteFile", async (fileId: string, callback) => { try { await deleteFileRL.consume(data.userId, 1) - callback(await handleDeleteFile(fileManager, fileId)) + callback(await handleDeleteFile(fileId, handlerContext)) } catch (e: any) { console.error("Error deleting file:", e) socket.emit("error", `Error: file deletion. ${e.message ?? e}`) @@ -342,7 +352,7 @@ io.on("connection", async (socket) => { socket.on("deleteFolder", async (folderId: string, callback) => { try { - callback(await handleDeleteFolder(fileManager, folderId)) + callback(await handleDeleteFolder(folderId, handlerContext)) } catch (e: any) { console.error("Error deleting folder:", e) socket.emit("error", `Error: folder deletion. ${e.message ?? e}`) @@ -351,7 +361,7 @@ io.on("connection", async (socket) => { socket.on("createTerminal", async (id: string, callback) => { try { - await handleCreateTerminal(lockManager, terminalManager, id, socket, containers, data) + await handleCreateTerminal(id, socket, data, handlerContext) callback() } catch (e: any) { console.error(`Error creating terminal ${id}:`, e) @@ -361,7 +371,7 @@ io.on("connection", async (socket) => { socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => { try { - handleResizeTerminal(terminalManager, dimensions) + handleResizeTerminal(dimensions, handlerContext) } catch (e: any) { console.error("Error resizing terminal:", e) socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`) @@ -370,7 +380,7 @@ io.on("connection", async (socket) => { socket.on("terminalData", async (id: string, data: string) => { try { - await handleTerminalData(terminalManager, id, data) + await handleTerminalData(id, data, handlerContext) } catch (e: any) { console.error("Error writing to terminal:", e) socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`) @@ -379,7 +389,7 @@ io.on("connection", async (socket) => { socket.on("closeTerminal", async (id: string, callback) => { try { - await handleCloseTerminal(terminalManager, id) + await handleCloseTerminal(id, handlerContext) callback() } catch (e: any) { console.error("Error closing terminal:", e) @@ -389,7 +399,7 @@ io.on("connection", async (socket) => { socket.on("generateCode", async (fileName: string, code: string, line: number, instructions: string, callback) => { try { - callback(await handleGenerateCode(aiWorker, data.userId, fileName, code, line, instructions)) + callback(await handleGenerateCode(data.userId, fileName, code, line, instructions, handlerContext)) } catch (e: any) { console.error("Error generating code:", e) socket.emit("error", `Error: code generation. ${e.message ?? e}`) From 162da9f7cec30093986a9aec8fb75ec6b19ec997 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 17:37:34 -0600 Subject: [PATCH 05/27] refactor: move socket authentication middleware to a separate file --- backend/server/src/index.ts | 85 ++++++-------------------------- backend/server/src/socketAuth.ts | 63 +++++++++++++++++++++++ 2 files changed, 77 insertions(+), 71 deletions(-) create mode 100644 backend/server/src/socketAuth.ts diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 142f3d3..f214d7d 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -5,7 +5,6 @@ import express, { Express } from "express" import fs from "fs" import { createServer } from "http" import { Server } from "socket.io" -import { z } from "zod" import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" @@ -18,9 +17,10 @@ import { saveFileRL, } from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" +import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, HandlerContext, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" import { TerminalManager } from "./TerminalManager" -import { DokkuResponse, User } from "./types" +import { DokkuResponse } from "./types" import { LockManager } from "./utils" // Handle uncaught exceptions @@ -37,6 +37,17 @@ process.on("unhandledRejection", (reason, promise) => { // You can also handle the rejected promise here if needed }) +// Check if the sandbox owner is connected +function isOwnerConnected(sandboxId: string): boolean { + return (connections[sandboxId] ?? 0) > 0 +} + +// Initialize containers and managers +const containers: Record = {} +const connections: Record = {} +const fileManagers: Record = {} +const terminalManagers: Record = {} + // Load environment variables dotenv.config() @@ -51,76 +62,8 @@ const io = new Server(httpServer, { }, }) -// Check if the sandbox owner is connected -function isOwnerConnected(sandboxId: string): boolean { - return (connections[sandboxId] ?? 0) > 0 -} - -// Initialize containers and managers -const containers: Record = {} -const connections: Record = {} -const fileManagers: Record = {} -const terminalManagers: Record = {} - // Middleware for socket authentication -io.use(async (socket, next) => { - // Define the schema for handshake query validation - const handshakeSchema = z.object({ - userId: z.string(), - sandboxId: z.string(), - EIO: z.string(), - transport: z.string(), - }) - - const q = socket.handshake.query - const parseQuery = handshakeSchema.safeParse(q) - - // Check if the query is valid according to the schema - if (!parseQuery.success) { - next(new Error("Invalid request.")) - return - } - - const { sandboxId, userId } = parseQuery.data - // Fetch user data from the database - const dbUser = await fetch( - `${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`, - { - headers: { - Authorization: `${process.env.WORKERS_KEY}`, - }, - } - ) - const dbUserJSON = (await dbUser.json()) as User - - // Check if user data was retrieved successfully - if (!dbUserJSON) { - next(new Error("DB error.")) - return - } - - // Check if the user owns the sandbox or has shared access - const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId) - const sharedSandboxes = dbUserJSON.usersToSandboxes.find( - (uts) => uts.sandboxId === sandboxId - ) - - // If user doesn't own or have shared access to the sandbox, deny access - if (!sandbox && !sharedSandboxes) { - next(new Error("Invalid credentials.")) - return - } - - // Set socket data with user information - socket.data = { - userId, - sandboxId: sandboxId, - isOwner: sandbox !== undefined, - } - - // Allow the connection - next() -}) +io.use(socketAuth) // Use the new socketAuth middleware // Initialize lock manager const lockManager = new LockManager() diff --git a/backend/server/src/socketAuth.ts b/backend/server/src/socketAuth.ts new file mode 100644 index 0000000..3bd83b1 --- /dev/null +++ b/backend/server/src/socketAuth.ts @@ -0,0 +1,63 @@ +import { Socket } from "socket.io" +import { z } from "zod" +import { User } from "./types" + +// Middleware for socket authentication +export const socketAuth = async (socket: Socket, next: Function) => { + // Define the schema for handshake query validation + const handshakeSchema = z.object({ + userId: z.string(), + sandboxId: z.string(), + EIO: z.string(), + transport: z.string(), + }) + + const q = socket.handshake.query + const parseQuery = handshakeSchema.safeParse(q) + + // Check if the query is valid according to the schema + if (!parseQuery.success) { + next(new Error("Invalid request.")) + return + } + + const { sandboxId, userId } = parseQuery.data + // Fetch user data from the database + const dbUser = await fetch( + `${process.env.DATABASE_WORKER_URL}/api/user?id=${userId}`, + { + headers: { + Authorization: `${process.env.WORKERS_KEY}`, + }, + } + ) + const dbUserJSON = (await dbUser.json()) as User + + // Check if user data was retrieved successfully + if (!dbUserJSON) { + next(new Error("DB error.")) + return + } + + // Check if the user owns the sandbox or has shared access + const sandbox = dbUserJSON.sandbox.find((s) => s.id === sandboxId) + const sharedSandboxes = dbUserJSON.usersToSandboxes.find( + (uts) => uts.sandboxId === sandboxId + ) + + // If user doesn't own or have shared access to the sandbox, deny access + if (!sandbox && !sharedSandboxes) { + next(new Error("Invalid credentials.")) + return + } + + // Set socket data with user information + socket.data = { + userId, + sandboxId: sandboxId, + isOwner: sandbox !== undefined, + } + + // Allow the connection + next() +} From 33c8ed8b3258a86352181bb45b4717b53bee8aa8 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 17:38:43 -0600 Subject: [PATCH 06/27] chore: change Dokku errors to warnings --- backend/server/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index f214d7d..e134c6a 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -70,11 +70,11 @@ const lockManager = new LockManager() // Check for required environment variables if (!process.env.DOKKU_HOST) - console.error("Environment variable DOKKU_HOST is not defined") + console.warn("Environment variable DOKKU_HOST is not defined") if (!process.env.DOKKU_USERNAME) - console.error("Environment variable DOKKU_USERNAME is not defined") + console.warn("Environment variable DOKKU_USERNAME is not defined") if (!process.env.DOKKU_KEY) - console.error("Environment variable DOKKU_KEY is not defined") + console.warn("Environment variable DOKKU_KEY is not defined") // Initialize Dokku client const dokkuClient = From c644b0054e7d5cd21bb08715c9f83451e60c1b7d Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 19:15:03 -0600 Subject: [PATCH 07/27] refactor: add callback usage to all event handlers --- backend/server/src/index.ts | 68 ++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index e134c6a..da288de 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -20,7 +20,6 @@ import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, HandlerContext, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" import { TerminalManager } from "./TerminalManager" -import { DokkuResponse } from "./types" import { LockManager } from "./utils" // Handle uncaught exceptions @@ -182,9 +181,9 @@ io.on("connection", async (socket) => { } // Handle various socket events (heartbeat, file operations, terminal operations, etc.) - socket.on("heartbeat", async () => { + socket.on("heartbeat", async (callback) => { try { - handleHeartbeat(data, handlerContext) + callback?.(handleHeartbeat(data, handlerContext)) } catch (e: any) { console.error("Error setting timeout:", e) socket.emit("error", `Error: set timeout. ${e.message ?? e}`) @@ -193,7 +192,7 @@ io.on("connection", async (socket) => { socket.on("getFile", async (fileId: string, callback) => { try { - callback(await handleGetFile(fileId, handlerContext)) + callback?.(await handleGetFile(fileId, handlerContext)) } catch (e: any) { console.error("Error getting file:", e) socket.emit("error", `Error: get file. ${e.message ?? e}`) @@ -202,17 +201,17 @@ io.on("connection", async (socket) => { socket.on("getFolder", async (folderId: string, callback) => { try { - callback(await handleGetFolder(folderId, handlerContext)) + callback?.(await handleGetFolder(folderId, handlerContext)) } catch (e: any) { console.error("Error getting folder:", e) socket.emit("error", `Error: get folder. ${e.message ?? e}`) } }) - socket.on("saveFile", async (fileId: string, body: string) => { + socket.on("saveFile", async (fileId: string, body: string, callback) => { try { await saveFileRL.consume(data.userId, 1) - await handleSaveFile(fileId, body, handlerContext) + callback?.(await handleSaveFile(fileId, body, handlerContext)) } catch (e: any) { console.error("Error saving file:", e) socket.emit("error", `Error: file saving. ${e.message ?? e}`) @@ -221,41 +220,37 @@ io.on("connection", async (socket) => { socket.on("moveFile", async (fileId: string, folderId: string, callback) => { try { - callback(await handleMoveFile(fileId, folderId, handlerContext)) + callback?.(await handleMoveFile(fileId, folderId, handlerContext)) } catch (e: any) { console.error("Error moving file:", e) socket.emit("error", `Error: file moving. ${e.message ?? e}`) } }) - socket.on("list", async (callback: (response: DokkuResponse) => void) => { + socket.on("list", async (callback) => { console.log("Retrieving apps list...") try { - callback(await handleListApps(handlerContext)) - } catch (error) { - callback({ - success: false, - message: "Failed to retrieve apps list", - }) + callback?.(await handleListApps(handlerContext)) + } catch (e: any) { + console.error("Error retrieving apps list:", e) + socket.emit("error", `Error: app list retrieval. ${e.message ?? e}`) } }) - socket.on("deploy", async (callback: (response: DokkuResponse) => void) => { + socket.on("deploy", async (callback) => { try { console.log("Deploying project ${data.sandboxId}...") - callback(await handleDeploy(data.sandboxId, handlerContext)) - } catch (error) { - callback({ - success: false, - message: "Failed to deploy project: " + error, - }) + callback?.(await handleDeploy(data.sandboxId, handlerContext)) + } catch (e: any) { + console.error("Error deploying project:", e) + socket.emit("error", `Error: project deployment. ${e.message ?? e}`) } }) socket.on("createFile", async (name: string, callback) => { try { await createFileRL.consume(data.userId, 1) - callback({ success: await handleCreateFile(name, handlerContext) }) + callback?.({ success: await handleCreateFile(name, handlerContext) }) } catch (e: any) { console.error("Error creating file:", e) socket.emit("error", `Error: file creation. ${e.message ?? e}`) @@ -265,18 +260,17 @@ io.on("connection", async (socket) => { socket.on("createFolder", async (name: string, callback) => { try { await createFolderRL.consume(data.userId, 1) - await handleCreateFolder(name, handlerContext) - callback() + callback?.(await handleCreateFolder(name, handlerContext)) } catch (e: any) { console.error("Error creating folder:", e) socket.emit("error", `Error: folder creation. ${e.message ?? e}`) } }) - socket.on("renameFile", async (fileId: string, newName: string) => { + socket.on("renameFile", async (fileId: string, newName: string, callback) => { try { await renameFileRL.consume(data.userId, 1) - await handleRenameFile(fileId, newName, handlerContext) + callback?.(await handleRenameFile(fileId, newName, handlerContext)) } catch (e: any) { console.error("Error renaming file:", e) socket.emit("error", `Error: file renaming. ${e.message ?? e}`) @@ -286,7 +280,7 @@ io.on("connection", async (socket) => { socket.on("deleteFile", async (fileId: string, callback) => { try { await deleteFileRL.consume(data.userId, 1) - callback(await handleDeleteFile(fileId, handlerContext)) + callback?.(await handleDeleteFile(fileId, handlerContext)) } catch (e: any) { console.error("Error deleting file:", e) socket.emit("error", `Error: file deletion. ${e.message ?? e}`) @@ -295,7 +289,7 @@ io.on("connection", async (socket) => { socket.on("deleteFolder", async (folderId: string, callback) => { try { - callback(await handleDeleteFolder(folderId, handlerContext)) + callback?.(await handleDeleteFolder(folderId, handlerContext)) } catch (e: any) { console.error("Error deleting folder:", e) socket.emit("error", `Error: folder deletion. ${e.message ?? e}`) @@ -304,26 +298,25 @@ io.on("connection", async (socket) => { socket.on("createTerminal", async (id: string, callback) => { try { - await handleCreateTerminal(id, socket, data, handlerContext) - callback() + callback?.(await handleCreateTerminal(id, socket, data, handlerContext)) } catch (e: any) { console.error(`Error creating terminal ${id}:`, e) socket.emit("error", `Error: terminal creation. ${e.message ?? e}`) } }) - socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => { + socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }, callback) => { try { - handleResizeTerminal(dimensions, handlerContext) + callback?.(handleResizeTerminal(dimensions, handlerContext)) } catch (e: any) { console.error("Error resizing terminal:", e) socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`) } }) - socket.on("terminalData", async (id: string, data: string) => { + socket.on("terminalData", async (id: string, data: string, callback) => { try { - await handleTerminalData(id, data, handlerContext) + callback?.(await handleTerminalData(id, data, handlerContext)) } catch (e: any) { console.error("Error writing to terminal:", e) socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`) @@ -332,8 +325,7 @@ io.on("connection", async (socket) => { socket.on("closeTerminal", async (id: string, callback) => { try { - await handleCloseTerminal(id, handlerContext) - callback() + callback?.(await handleCloseTerminal(id, handlerContext)) } catch (e: any) { console.error("Error closing terminal:", e) socket.emit("error", `Error: closing terminal. ${e.message ?? e}`) @@ -342,7 +334,7 @@ io.on("connection", async (socket) => { socket.on("generateCode", async (fileName: string, code: string, line: number, instructions: string, callback) => { try { - callback(await handleGenerateCode(data.userId, fileName, code, line, instructions, handlerContext)) + callback?.(await handleGenerateCode(data.userId, fileName, code, line, instructions, handlerContext)) } catch (e: any) { console.error("Error generating code:", e) socket.emit("error", `Error: code generation. ${e.message ?? e}`) From 1de980cdd6c62a58b8b7e3e9f54115f6fea80bd4 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 20:00:50 -0600 Subject: [PATCH 08/27] refactor: pass event handler arguments as a single object --- backend/server/src/SocketHandlers.ts | 34 ++-- backend/server/src/index.ts | 71 ++++---- frontend/components/editor/generate.tsx | 10 +- frontend/components/editor/index.tsx | 157 ++++++++---------- frontend/components/editor/sidebar/index.tsx | 6 +- frontend/components/editor/sidebar/new.tsx | 4 +- .../components/editor/terminals/terminal.tsx | 4 +- frontend/context/TerminalContext.tsx | 4 +- frontend/lib/terminal.ts | 10 +- tests/index.ts | 6 +- 10 files changed, 148 insertions(+), 158 deletions(-) diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts index 43a60cc..a0d1f10 100644 --- a/backend/server/src/SocketHandlers.ts +++ b/backend/server/src/SocketHandlers.ts @@ -25,38 +25,38 @@ function extractPortNumber(inputString: string): number | null { } // Handle heartbeat from a socket connection -export function handleHeartbeat(data: any, context: HandlerContext) { +export function handleHeartbeat({ data }: { data: any }, context: HandlerContext) { context.sandboxManager.setTimeout(CONTAINER_TIMEOUT) } // Handle getting a file -export function handleGetFile(fileId: string, context: HandlerContext) { +export function handleGetFile({ fileId }: { fileId: string }, context: HandlerContext) { return context.fileManager.getFile(fileId) } // Handle getting a folder -export function handleGetFolder(folderId: string, context: HandlerContext) { +export function handleGetFolder({ folderId }: { folderId: string }, context: HandlerContext) { return context.fileManager.getFolder(folderId) } // Handle saving a file -export function handleSaveFile(fileId: string, body: string, context: HandlerContext) { +export function handleSaveFile({ fileId, body }: { fileId: string, body: string }, context: HandlerContext) { return context.fileManager.saveFile(fileId, body) } // Handle moving a file -export function handleMoveFile(fileId: string, folderId: string, context: HandlerContext) { +export function handleMoveFile({ fileId, folderId }: { fileId: string, folderId: string }, context: HandlerContext) { return context.fileManager.moveFile(fileId, folderId) } // Handle listing apps -export async function handleListApps(context: HandlerContext) { +export async function handleListApps({ }, context: HandlerContext) { if (!context.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") return { success: true, apps: await context.dokkuClient.listApps() } } // Handle deploying code -export async function handleDeploy(sandboxId: string, context: HandlerContext) { +export async function handleDeploy({ sandboxId }: { sandboxId: string }, context: HandlerContext) { if (!context.gitClient) throw Error("Failed to retrieve apps list: No git client") const fixedFilePaths = context.fileManager.sandboxFiles.fileData.map((file) => ({ ...file, @@ -67,32 +67,32 @@ export async function handleDeploy(sandboxId: string, context: HandlerContext) { } // Handle creating a file -export function handleCreateFile(name: string, context: HandlerContext) { +export function handleCreateFile({ name }: { name: string }, context: HandlerContext) { return context.fileManager.createFile(name) } // Handle creating a folder -export function handleCreateFolder(name: string, context: HandlerContext) { +export function handleCreateFolder({ name }: { name: string }, context: HandlerContext) { return context.fileManager.createFolder(name) } // Handle renaming a file -export function handleRenameFile(fileId: string, newName: string, context: HandlerContext) { +export function handleRenameFile({ fileId, newName }: { fileId: string, newName: string }, context: HandlerContext) { return context.fileManager.renameFile(fileId, newName) } // Handle deleting a file -export function handleDeleteFile(fileId: string, context: HandlerContext) { +export function handleDeleteFile({ fileId }: { fileId: string }, context: HandlerContext) { return context.fileManager.deleteFile(fileId) } // Handle deleting a folder -export function handleDeleteFolder(folderId: string, context: HandlerContext) { +export function handleDeleteFolder({ folderId }: { folderId: string }, context: HandlerContext) { return context.fileManager.deleteFolder(folderId) } // Handle creating a terminal session -export async function handleCreateTerminal(id: string, socket: any, data: any, context: HandlerContext) { +export async function handleCreateTerminal({ id, socket, data }: { id: string, socket: any, data: any }, context: HandlerContext) { await context.lockManager.acquireLock(data.sandboxId, async () => { await context.terminalManager.createTerminal(id, (responseString: string) => { socket.emit("terminalResponse", { id, data: responseString }) @@ -108,21 +108,21 @@ export async function handleCreateTerminal(id: string, socket: any, data: any, c } // Handle resizing a terminal -export function handleResizeTerminal(dimensions: { cols: number; rows: number }, context: HandlerContext) { +export function handleResizeTerminal({ dimensions }: { dimensions: { cols: number; rows: number } }, context: HandlerContext) { context.terminalManager.resizeTerminal(dimensions) } // Handle sending data to a terminal -export function handleTerminalData(id: string, data: string, context: HandlerContext) { +export function handleTerminalData({ id, data }: { id: string, data: string }, context: HandlerContext) { return context.terminalManager.sendTerminalData(id, data) } // Handle closing a terminal -export function handleCloseTerminal(id: string, context: HandlerContext) { +export function handleCloseTerminal({ id }: { id: string }, context: HandlerContext) { return context.terminalManager.closeTerminal(id) } // Handle generating code -export function handleGenerateCode(userId: string, fileName: string, code: string, line: number, instructions: string, context: HandlerContext) { +export function handleGenerateCode({ userId, fileName, code, line, instructions }: { userId: string, fileName: string, code: string, line: number, instructions: string }, context: HandlerContext) { return context.aiWorker.generateCode(userId, fileName, code, line, instructions) } \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index da288de..9d5aa67 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -181,160 +181,159 @@ io.on("connection", async (socket) => { } // Handle various socket events (heartbeat, file operations, terminal operations, etc.) - socket.on("heartbeat", async (callback) => { + socket.on("heartbeat", async (options, callback) => { try { - callback?.(handleHeartbeat(data, handlerContext)) + callback?.(handleHeartbeat(options, handlerContext)) } catch (e: any) { console.error("Error setting timeout:", e) socket.emit("error", `Error: set timeout. ${e.message ?? e}`) } }) - socket.on("getFile", async (fileId: string, callback) => { + socket.on("getFile", async (options, callback) => { try { - callback?.(await handleGetFile(fileId, handlerContext)) + callback?.(await handleGetFile(options, handlerContext)) } catch (e: any) { console.error("Error getting file:", e) socket.emit("error", `Error: get file. ${e.message ?? e}`) } }) - socket.on("getFolder", async (folderId: string, callback) => { + socket.on("getFolder", async (options, callback) => { try { - callback?.(await handleGetFolder(folderId, handlerContext)) + callback?.(await handleGetFolder(options, handlerContext)) } catch (e: any) { console.error("Error getting folder:", e) socket.emit("error", `Error: get folder. ${e.message ?? e}`) } }) - socket.on("saveFile", async (fileId: string, body: string, callback) => { + socket.on("saveFile", async (options, callback) => { try { await saveFileRL.consume(data.userId, 1) - callback?.(await handleSaveFile(fileId, body, handlerContext)) + callback?.(await handleSaveFile(options, handlerContext)) } catch (e: any) { console.error("Error saving file:", e) socket.emit("error", `Error: file saving. ${e.message ?? e}`) } }) - socket.on("moveFile", async (fileId: string, folderId: string, callback) => { + socket.on("moveFile", async (options, callback) => { try { - callback?.(await handleMoveFile(fileId, folderId, handlerContext)) + callback?.(await handleMoveFile(options, handlerContext)) } catch (e: any) { console.error("Error moving file:", e) socket.emit("error", `Error: file moving. ${e.message ?? e}`) } }) - socket.on("list", async (callback) => { + socket.on("list", async (options, callback) => { console.log("Retrieving apps list...") try { - callback?.(await handleListApps(handlerContext)) + callback?.(await handleListApps(options, handlerContext)) } catch (e: any) { console.error("Error retrieving apps list:", e) socket.emit("error", `Error: app list retrieval. ${e.message ?? e}`) } }) - socket.on("deploy", async (callback) => { + socket.on("deploy", async (options, callback) => { try { - console.log("Deploying project ${data.sandboxId}...") - callback?.(await handleDeploy(data.sandboxId, handlerContext)) + callback?.(await handleDeploy(options, handlerContext)) } catch (e: any) { console.error("Error deploying project:", e) socket.emit("error", `Error: project deployment. ${e.message ?? e}`) } }) - socket.on("createFile", async (name: string, callback) => { + socket.on("createFile", async (options, callback) => { try { await createFileRL.consume(data.userId, 1) - callback?.({ success: await handleCreateFile(name, handlerContext) }) + callback?.({ success: await handleCreateFile(options, handlerContext) }) } catch (e: any) { console.error("Error creating file:", e) socket.emit("error", `Error: file creation. ${e.message ?? e}`) } }) - socket.on("createFolder", async (name: string, callback) => { + socket.on("createFolder", async (options, callback) => { try { await createFolderRL.consume(data.userId, 1) - callback?.(await handleCreateFolder(name, handlerContext)) + callback?.(await handleCreateFolder(options, handlerContext)) } catch (e: any) { console.error("Error creating folder:", e) socket.emit("error", `Error: folder creation. ${e.message ?? e}`) } }) - socket.on("renameFile", async (fileId: string, newName: string, callback) => { + socket.on("renameFile", async (options, callback) => { try { await renameFileRL.consume(data.userId, 1) - callback?.(await handleRenameFile(fileId, newName, handlerContext)) + callback?.(await handleRenameFile(options, handlerContext)) } catch (e: any) { console.error("Error renaming file:", e) socket.emit("error", `Error: file renaming. ${e.message ?? e}`) } }) - socket.on("deleteFile", async (fileId: string, callback) => { + socket.on("deleteFile", async (options, callback) => { try { await deleteFileRL.consume(data.userId, 1) - callback?.(await handleDeleteFile(fileId, handlerContext)) + callback?.(await handleDeleteFile(options, handlerContext)) } catch (e: any) { console.error("Error deleting file:", e) socket.emit("error", `Error: file deletion. ${e.message ?? e}`) } }) - socket.on("deleteFolder", async (folderId: string, callback) => { + socket.on("deleteFolder", async (options, callback) => { try { - callback?.(await handleDeleteFolder(folderId, handlerContext)) + callback?.(await handleDeleteFolder(options, handlerContext)) } catch (e: any) { console.error("Error deleting folder:", e) socket.emit("error", `Error: folder deletion. ${e.message ?? e}`) } }) - socket.on("createTerminal", async (id: string, callback) => { + socket.on("createTerminal", async (options, callback) => { try { - callback?.(await handleCreateTerminal(id, socket, data, handlerContext)) + callback?.(await handleCreateTerminal(options, handlerContext)) } catch (e: any) { - console.error(`Error creating terminal ${id}:`, e) + console.error(`Error creating terminal ${options.id}:`, e) socket.emit("error", `Error: terminal creation. ${e.message ?? e}`) } }) - socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }, callback) => { + socket.on("resizeTerminal", (options, callback) => { try { - callback?.(handleResizeTerminal(dimensions, handlerContext)) + callback?.(handleResizeTerminal(options, handlerContext)) } catch (e: any) { console.error("Error resizing terminal:", e) socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`) } }) - socket.on("terminalData", async (id: string, data: string, callback) => { + socket.on("terminalData", async (options, callback) => { try { - callback?.(await handleTerminalData(id, data, handlerContext)) + callback?.(await handleTerminalData(options, handlerContext)) } catch (e: any) { console.error("Error writing to terminal:", e) socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`) } }) - socket.on("closeTerminal", async (id: string, callback) => { + socket.on("closeTerminal", async (options, callback) => { try { - callback?.(await handleCloseTerminal(id, handlerContext)) + callback?.(await handleCloseTerminal(options, handlerContext)) } catch (e: any) { console.error("Error closing terminal:", e) socket.emit("error", `Error: closing terminal. ${e.message ?? e}`) } }) - socket.on("generateCode", async (fileName: string, code: string, line: number, instructions: string, callback) => { + socket.on("generateCode", async (options, callback) => { try { - callback?.(await handleGenerateCode(data.userId, fileName, code, line, instructions, handlerContext)) + callback?.(await handleGenerateCode(options, handlerContext)) } catch (e: any) { console.error("Error generating code:", e) socket.emit("error", `Error: code generation. ${e.message ?? e}`) diff --git a/frontend/components/editor/generate.tsx b/frontend/components/editor/generate.tsx index 9e4bd09..0b5ff39 100644 --- a/frontend/components/editor/generate.tsx +++ b/frontend/components/editor/generate.tsx @@ -68,10 +68,12 @@ export default function GenerateInput({ setCurrentPrompt(input) socket.emit( "generateCode", - data.fileName, - data.code, - data.line, - regenerate ? currentPrompt : input, + { + fileName: data.fileName, + code: data.code, + line: data.line, + instructions: regenerate ? currentPrompt : input + }, (res: { response: string; success: boolean }) => { console.log("Generated code", res.response, res.success) // if (!res.success) { diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index e20a6d0..5435b8d 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -107,7 +107,7 @@ export default function CodeEditor({ // Editor state const [editorLanguage, setEditorLanguage] = useState("plaintext") - console.log("editor language: ",editorLanguage) + console.log("editor language: ", editorLanguage) const [cursorLine, setCursorLine] = useState(0) const [editorRef, setEditorRef] = useState() @@ -207,7 +207,7 @@ export default function CodeEditor({ ) const fetchFileContent = (fileId: string): Promise => { return new Promise((resolve) => { - socket?.emit("getFile", fileId, (content: string) => { + socket?.emit("getFile", { fileId }, (content: string) => { resolve(content) }) }) @@ -532,7 +532,7 @@ export default function CodeEditor({ ) console.log(`Saving file...${activeFileId}`) console.log(`Saving file...${content}`) - socket?.emit("saveFile", activeFileId, content) + socket?.emit("saveFile", { fileId: activeFileId, body: content }) } }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), [socket, fileContents] @@ -649,7 +649,7 @@ export default function CodeEditor({ // Socket event listener effect useEffect(() => { - const onConnect = () => {} + const onConnect = () => { } const onDisconnect = () => { setTerminals([]) @@ -715,7 +715,7 @@ export default function CodeEditor({ // Debounced function to get file content const debouncedGetFile = (tabId: any, callback: any) => { - socket?.emit("getFile", tabId, callback) + socket?.emit("getFile", { fileId: tabId }, callback) } // 300ms debounce delay, adjust as needed const selectFile = (tab: TTab) => { @@ -777,8 +777,8 @@ export default function CodeEditor({ ? numTabs === 1 ? null : index < numTabs - 1 - ? tabs[index + 1].id - : tabs[index - 1].id + ? tabs[index + 1].id + : tabs[index - 1].id : activeFileId setTabs((prev) => prev.filter((t) => t.id !== id)) @@ -835,7 +835,7 @@ export default function CodeEditor({ return false } - socket?.emit("renameFile", id, newName) + socket?.emit("renameFile", { fileId: id, newName }) setTabs((prev) => prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) ) @@ -844,7 +844,7 @@ export default function CodeEditor({ } const handleDeleteFile = (file: TFile) => { - socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { + socket?.emit("deleteFile", { fileId: file.id }, (response: (TFolder | TFile)[]) => { setFiles(response) }) closeTab(file.id) @@ -854,11 +854,11 @@ export default function CodeEditor({ setDeletingFolderId(folder.id) console.log("deleting folder", folder.id) - socket?.emit("getFolder", folder.id, (response: string[]) => + socket?.emit("getFolder", { folderId: folder.id }, (response: string[]) => closeTabs(response) ) - socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { + socket?.emit("deleteFolder", { folderId: folder.id }, (response: (TFolder | TFile)[]) => { setFiles(response) setDeletingFolderId("") }) @@ -902,7 +902,7 @@ export default function CodeEditor({ {}} + setOpen={() => { }} /> @@ -944,8 +944,8 @@ export default function CodeEditor({ code: (isSelected && editorRef?.getSelection() ? editorRef - ?.getModel() - ?.getValueInRange(editorRef?.getSelection()!) + ?.getModel() + ?.getValueInRange(editorRef?.getSelection()!) : editorRef?.getValue()) ?? "", line: generate.line, }} @@ -1075,62 +1075,62 @@ export default function CodeEditor({ ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 - clerk.loaded ? ( - <> - {provider && userInfo ? ( - - ) : null} - { - // If the new content is different from the cached content, update it - if (value !== fileContents[activeFileId]) { - setActiveFileContent(value ?? "") // Update the active file content - // Mark the file as unsaved by setting 'saved' to false - setTabs((prev) => - prev.map((tab) => - tab.id === activeFileId - ? { ...tab, saved: false } - : tab + clerk.loaded ? ( + <> + {provider && userInfo ? ( + + ) : null} + { + // If the new content is different from the cached content, update it + if (value !== fileContents[activeFileId]) { + setActiveFileContent(value ?? "") // Update the active file content + // Mark the file as unsaved by setting 'saved' to false + setTabs((prev) => + prev.map((tab) => + tab.id === activeFileId + ? { ...tab, saved: false } + : tab + ) ) - ) - } else { - // If the content matches the cached content, mark the file as saved - setTabs((prev) => - prev.map((tab) => - tab.id === activeFileId - ? { ...tab, saved: true } - : tab + } else { + // If the content matches the cached content, mark the file as saved + setTabs((prev) => + prev.map((tab) => + tab.id === activeFileId + ? { ...tab, saved: true } + : tab + ) ) - ) - } - }} - options={{ - tabSize: 2, - minimap: { - enabled: false, - }, - padding: { - bottom: 4, - top: 4, - }, - scrollBeyondLastLine: false, - fixedOverflowWidgets: true, - fontFamily: "var(--font-geist-mono)", - }} - theme={theme === "light" ? "vs" : "vs-dark"} - value={activeFileContent} - /> - - ) : ( -
- - Waiting for Clerk to load... -
- )} + } + }} + options={{ + tabSize: 2, + minimap: { + enabled: false, + }, + padding: { + bottom: 4, + top: 4, + }, + scrollBeyondLastLine: false, + fixedOverflowWidgets: true, + fontFamily: "var(--font-geist-mono)", + }} + theme={theme === "light" ? "vs" : "vs-dark"} + value={activeFileContent} + /> + + ) : ( +
+ + Waiting for Clerk to load... +
+ )} @@ -1140,10 +1140,10 @@ export default function CodeEditor({ isAIChatOpen && isHorizontalLayout ? "horizontal" : isAIChatOpen - ? "vertical" - : isHorizontalLayout - ? "horizontal" - : "vertical" + ? "vertical" + : isHorizontalLayout + ? "horizontal" + : "vertical" } > { setFiles(response) setMovingId("") diff --git a/frontend/components/editor/sidebar/new.tsx b/frontend/components/editor/sidebar/new.tsx index 7fec344..ca7dbc9 100644 --- a/frontend/components/editor/sidebar/new.tsx +++ b/frontend/components/editor/sidebar/new.tsx @@ -27,7 +27,7 @@ export default function New({ if (type === "file") { socket.emit( "createFile", - name, + { name }, ({ success }: { success: boolean }) => { if (success) { addNew(name, type) @@ -35,7 +35,7 @@ export default function New({ } ) } else { - socket.emit("createFolder", name, () => { + socket.emit("createFolder", { name }, () => { addNew(name, type) }) } diff --git a/frontend/components/editor/terminals/terminal.tsx b/frontend/components/editor/terminals/terminal.tsx index 4790a44..19b3980 100644 --- a/frontend/components/editor/terminals/terminal.tsx +++ b/frontend/components/editor/terminals/terminal.tsx @@ -65,12 +65,12 @@ export default function EditorTerminal({ } const disposableOnData = term.onData((data) => { - socket.emit("terminalData", id, data) + socket.emit("terminalData", { id, data }) }) const disposableOnResize = term.onResize((dimensions) => { fitAddonRef.current?.fit() - socket.emit("terminalResize", dimensions) + socket.emit("terminalResize", { dimensions }) }) const resizeObserver = new ResizeObserver( debounce((entries) => { diff --git a/frontend/context/TerminalContext.tsx b/frontend/context/TerminalContext.tsx index a7f131a..5af74a8 100644 --- a/frontend/context/TerminalContext.tsx +++ b/frontend/context/TerminalContext.tsx @@ -63,7 +63,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ terminals, setTerminals, setActiveTerminalId, - setClosingTerminal: () => {}, + setClosingTerminal: () => { }, socket, activeTerminalId, }) @@ -73,7 +73,7 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ const deploy = (callback: () => void) => { if (!socket) console.error("Couldn't deploy: No socket") console.log("Deploying...") - socket?.emit("deploy", () => { + socket?.emit("deploy", {}, () => { callback() }) } diff --git a/frontend/lib/terminal.ts b/frontend/lib/terminal.ts index a91db3c..1d0edbc 100644 --- a/frontend/lib/terminal.ts +++ b/frontend/lib/terminal.ts @@ -32,9 +32,9 @@ export const createTerminal = ({ setActiveTerminalId(id) setTimeout(() => { - socket.emit("createTerminal", id, () => { + socket.emit("createTerminal", { id }, () => { setCreatingTerminal(false) - if (command) socket.emit("terminalData", id, command + "\n") + if (command) socket.emit("terminalData", { id, data: command + "\n" }) }) }, 1000) } @@ -75,7 +75,7 @@ export const closeTerminal = ({ setClosingTerminal(term.id) - socket.emit("closeTerminal", term.id, () => { + socket.emit("closeTerminal", { id: term.id }, () => { setClosingTerminal("") const nextId = @@ -83,8 +83,8 @@ export const closeTerminal = ({ ? numTerminals === 1 ? null : index < numTerminals - 1 - ? terminals[index + 1].id - : terminals[index - 1].id + ? terminals[index + 1].id + : terminals[index - 1].id : activeTerminalId setTerminals((prev) => prev.filter((t) => t.id !== term.id)) diff --git a/tests/index.ts b/tests/index.ts index a36868c..951eefe 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,6 +1,6 @@ // Import necessary modules -import { io, Socket } from "socket.io-client"; import dotenv from "dotenv"; +import { io, Socket } from "socket.io-client"; dotenv.config(); @@ -21,7 +21,7 @@ socketRef.on("connect", async () => { console.log("Connected to the server"); await new Promise((resolve) => setTimeout(resolve, 1000)); - socketRef.emit("list", (response: CallbackResponse) => { + socketRef.emit("list", {}, (response: CallbackResponse) => { if (response.success) { console.log("List of apps:", response.apps); } else { @@ -29,7 +29,7 @@ socketRef.on("connect", async () => { } }); - socketRef.emit("deploy", (response: CallbackResponse) => { + socketRef.emit("deploy", {}, (response: CallbackResponse) => { if (response.success) { console.log("It worked!"); } else { From 1479d25d49494af12e8d6be78f016a5a30919c8a Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 22:18:01 -0600 Subject: [PATCH 09/27] refactor: reuse try...catch and rate limiting code across handlers --- backend/server/src/SocketHandlers.ts | 39 +++--- backend/server/src/index.ts | 195 ++++++--------------------- 2 files changed, 59 insertions(+), 175 deletions(-) diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts index a0d1f10..64dc05a 100644 --- a/backend/server/src/SocketHandlers.ts +++ b/backend/server/src/SocketHandlers.ts @@ -25,38 +25,38 @@ function extractPortNumber(inputString: string): number | null { } // Handle heartbeat from a socket connection -export function handleHeartbeat({ data }: { data: any }, context: HandlerContext) { +export const handleHeartbeat: SocketHandler = (_: any, context: HandlerContext) => { context.sandboxManager.setTimeout(CONTAINER_TIMEOUT) } // Handle getting a file -export function handleGetFile({ fileId }: { fileId: string }, context: HandlerContext) { +export const handleGetFile: SocketHandler = ({ fileId }: any, context: HandlerContext) => { return context.fileManager.getFile(fileId) } // Handle getting a folder -export function handleGetFolder({ folderId }: { folderId: string }, context: HandlerContext) { +export const handleGetFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { return context.fileManager.getFolder(folderId) } // Handle saving a file -export function handleSaveFile({ fileId, body }: { fileId: string, body: string }, context: HandlerContext) { +export const handleSaveFile: SocketHandler = ({ fileId, body }: any, context: HandlerContext) => { return context.fileManager.saveFile(fileId, body) } // Handle moving a file -export function handleMoveFile({ fileId, folderId }: { fileId: string, folderId: string }, context: HandlerContext) { +export const handleMoveFile: SocketHandler = ({ fileId, folderId }: any, context: HandlerContext) => { return context.fileManager.moveFile(fileId, folderId) } // Handle listing apps -export async function handleListApps({ }, context: HandlerContext) { +export const handleListApps: SocketHandler = async ({ }: any, context: HandlerContext) => { if (!context.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") return { success: true, apps: await context.dokkuClient.listApps() } } // Handle deploying code -export async function handleDeploy({ sandboxId }: { sandboxId: string }, context: HandlerContext) { +export const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: HandlerContext) => { if (!context.gitClient) throw Error("Failed to retrieve apps list: No git client") const fixedFilePaths = context.fileManager.sandboxFiles.fileData.map((file) => ({ ...file, @@ -67,32 +67,32 @@ export async function handleDeploy({ sandboxId }: { sandboxId: string }, context } // Handle creating a file -export function handleCreateFile({ name }: { name: string }, context: HandlerContext) { +export const handleCreateFile: SocketHandler = ({ name }: any, context: HandlerContext) => { return context.fileManager.createFile(name) } // Handle creating a folder -export function handleCreateFolder({ name }: { name: string }, context: HandlerContext) { +export const handleCreateFolder: SocketHandler = ({ name }: any, context: HandlerContext) => { return context.fileManager.createFolder(name) } // Handle renaming a file -export function handleRenameFile({ fileId, newName }: { fileId: string, newName: string }, context: HandlerContext) { +export const handleRenameFile: SocketHandler = ({ fileId, newName }: any, context: HandlerContext) => { return context.fileManager.renameFile(fileId, newName) } // Handle deleting a file -export function handleDeleteFile({ fileId }: { fileId: string }, context: HandlerContext) { +export const handleDeleteFile: SocketHandler = ({ fileId }: any, context: HandlerContext) => { return context.fileManager.deleteFile(fileId) } // Handle deleting a folder -export function handleDeleteFolder({ folderId }: { folderId: string }, context: HandlerContext) { +export const handleDeleteFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { return context.fileManager.deleteFolder(folderId) } // Handle creating a terminal session -export async function handleCreateTerminal({ id, socket, data }: { id: string, socket: any, data: any }, context: HandlerContext) { +export const handleCreateTerminal: SocketHandler = async ({ id, socket, data }: any, context: HandlerContext) => { await context.lockManager.acquireLock(data.sandboxId, async () => { await context.terminalManager.createTerminal(id, (responseString: string) => { socket.emit("terminalResponse", { id, data: responseString }) @@ -108,21 +108,24 @@ export async function handleCreateTerminal({ id, socket, data }: { id: string, s } // Handle resizing a terminal -export function handleResizeTerminal({ dimensions }: { dimensions: { cols: number; rows: number } }, context: HandlerContext) { +export const handleResizeTerminal: SocketHandler = ({ dimensions }: any, context: HandlerContext) => { context.terminalManager.resizeTerminal(dimensions) } // Handle sending data to a terminal -export function handleTerminalData({ id, data }: { id: string, data: string }, context: HandlerContext) { +export const handleTerminalData: SocketHandler = ({ id, data }: any, context: HandlerContext) => { return context.terminalManager.sendTerminalData(id, data) } // Handle closing a terminal -export function handleCloseTerminal({ id }: { id: string }, context: HandlerContext) { +export const handleCloseTerminal: SocketHandler = ({ id }: any, context: HandlerContext) => { return context.terminalManager.closeTerminal(id) } // Handle generating code -export function handleGenerateCode({ userId, fileName, code, line, instructions }: { userId: string, fileName: string, code: string, line: number, instructions: string }, context: HandlerContext) { +export const handleGenerateCode: SocketHandler = ({ userId, fileName, code, line, instructions }: any, context: HandlerContext) => { return context.aiWorker.generateCode(userId, fileName, code, line, instructions) -} \ No newline at end of file +} + +// Define a type for SocketHandler functions +type SocketHandler> = (args: T, context: HandlerContext) => any; diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 9d5aa67..a496c96 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -180,165 +180,46 @@ io.on("connection", async (socket) => { sandboxManager: containers[data.sandboxId], } - // Handle various socket events (heartbeat, file operations, terminal operations, etc.) - socket.on("heartbeat", async (options, callback) => { - try { - callback?.(handleHeartbeat(options, handlerContext)) - } catch (e: any) { - console.error("Error setting timeout:", e) - socket.emit("error", `Error: set timeout. ${e.message ?? e}`) - } - }) + // Helper function to handle socket events with error handling and optional rate limiting + const handleSocketEvent = ( + event: string, + handler: any, + rateLimiter: any | null = null + ) => { + socket.on(event, async (options: any, callback?: (response: any) => void) => { + try { + // Consume rate limiter if provided + if (rateLimiter) { + await rateLimiter.consume(data.userId, 1); // Adjust as needed for the specific rate limiter + } + const response = await handler(options, handlerContext) + callback?.(response); + } catch (e: any) { + console.error(`Error processing event "${event}":`, e); + socket.emit("error", `Error: ${event}. ${e.message ?? e}`); + } + }); + }; - socket.on("getFile", async (options, callback) => { - try { - callback?.(await handleGetFile(options, handlerContext)) - } catch (e: any) { - console.error("Error getting file:", e) - socket.emit("error", `Error: get file. ${e.message ?? e}`) - } - }) + // Register socket events with optional rate limiters + handleSocketEvent("heartbeat", handleHeartbeat); + handleSocketEvent("getFile", handleGetFile); + handleSocketEvent("getFolder", handleGetFolder); + handleSocketEvent("saveFile", handleSaveFile, saveFileRL); + handleSocketEvent("moveFile", handleMoveFile); + handleSocketEvent("list", handleListApps); + handleSocketEvent("deploy", handleDeploy); + handleSocketEvent("createFile", handleCreateFile, createFileRL); + handleSocketEvent("createFolder", handleCreateFolder, createFolderRL); + handleSocketEvent("renameFile", handleRenameFile, renameFileRL); + handleSocketEvent("deleteFile", handleDeleteFile, deleteFileRL); + handleSocketEvent("deleteFolder", handleDeleteFolder); + handleSocketEvent("createTerminal", handleCreateTerminal); + handleSocketEvent("resizeTerminal", handleResizeTerminal); + handleSocketEvent("terminalData", handleTerminalData); + handleSocketEvent("closeTerminal", handleCloseTerminal); + handleSocketEvent("generateCode", handleGenerateCode); - socket.on("getFolder", async (options, callback) => { - try { - callback?.(await handleGetFolder(options, handlerContext)) - } catch (e: any) { - console.error("Error getting folder:", e) - socket.emit("error", `Error: get folder. ${e.message ?? e}`) - } - }) - - socket.on("saveFile", async (options, callback) => { - try { - await saveFileRL.consume(data.userId, 1) - callback?.(await handleSaveFile(options, handlerContext)) - } catch (e: any) { - console.error("Error saving file:", e) - socket.emit("error", `Error: file saving. ${e.message ?? e}`) - } - }) - - socket.on("moveFile", async (options, callback) => { - try { - callback?.(await handleMoveFile(options, handlerContext)) - } catch (e: any) { - console.error("Error moving file:", e) - socket.emit("error", `Error: file moving. ${e.message ?? e}`) - } - }) - - socket.on("list", async (options, callback) => { - console.log("Retrieving apps list...") - try { - callback?.(await handleListApps(options, handlerContext)) - } catch (e: any) { - console.error("Error retrieving apps list:", e) - socket.emit("error", `Error: app list retrieval. ${e.message ?? e}`) - } - }) - - socket.on("deploy", async (options, callback) => { - try { - callback?.(await handleDeploy(options, handlerContext)) - } catch (e: any) { - console.error("Error deploying project:", e) - socket.emit("error", `Error: project deployment. ${e.message ?? e}`) - } - }) - - socket.on("createFile", async (options, callback) => { - try { - await createFileRL.consume(data.userId, 1) - callback?.({ success: await handleCreateFile(options, handlerContext) }) - } catch (e: any) { - console.error("Error creating file:", e) - socket.emit("error", `Error: file creation. ${e.message ?? e}`) - } - }) - - socket.on("createFolder", async (options, callback) => { - try { - await createFolderRL.consume(data.userId, 1) - callback?.(await handleCreateFolder(options, handlerContext)) - } catch (e: any) { - console.error("Error creating folder:", e) - socket.emit("error", `Error: folder creation. ${e.message ?? e}`) - } - }) - - socket.on("renameFile", async (options, callback) => { - try { - await renameFileRL.consume(data.userId, 1) - callback?.(await handleRenameFile(options, handlerContext)) - } catch (e: any) { - console.error("Error renaming file:", e) - socket.emit("error", `Error: file renaming. ${e.message ?? e}`) - } - }) - - socket.on("deleteFile", async (options, callback) => { - try { - await deleteFileRL.consume(data.userId, 1) - callback?.(await handleDeleteFile(options, handlerContext)) - } catch (e: any) { - console.error("Error deleting file:", e) - socket.emit("error", `Error: file deletion. ${e.message ?? e}`) - } - }) - - socket.on("deleteFolder", async (options, callback) => { - try { - callback?.(await handleDeleteFolder(options, handlerContext)) - } catch (e: any) { - console.error("Error deleting folder:", e) - socket.emit("error", `Error: folder deletion. ${e.message ?? e}`) - } - }) - - socket.on("createTerminal", async (options, callback) => { - try { - callback?.(await handleCreateTerminal(options, handlerContext)) - } catch (e: any) { - console.error(`Error creating terminal ${options.id}:`, e) - socket.emit("error", `Error: terminal creation. ${e.message ?? e}`) - } - }) - - socket.on("resizeTerminal", (options, callback) => { - try { - callback?.(handleResizeTerminal(options, handlerContext)) - } catch (e: any) { - console.error("Error resizing terminal:", e) - socket.emit("error", `Error: terminal resizing. ${e.message ?? e}`) - } - }) - - socket.on("terminalData", async (options, callback) => { - try { - callback?.(await handleTerminalData(options, handlerContext)) - } catch (e: any) { - console.error("Error writing to terminal:", e) - socket.emit("error", `Error: writing to terminal. ${e.message ?? e}`) - } - }) - - socket.on("closeTerminal", async (options, callback) => { - try { - callback?.(await handleCloseTerminal(options, handlerContext)) - } catch (e: any) { - console.error("Error closing terminal:", e) - socket.emit("error", `Error: closing terminal. ${e.message ?? e}`) - } - }) - - socket.on("generateCode", async (options, callback) => { - try { - callback?.(await handleGenerateCode(options, handlerContext)) - } catch (e: any) { - console.error("Error generating code:", e) - socket.emit("error", `Error: code generation. ${e.message ?? e}`) - } - }) socket.on("disconnect", async () => { try { From 5ba6bdba154d3a9da38f371a9d848de5cef6c862 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 23:13:01 -0600 Subject: [PATCH 10/27] fix: fix problems with event handler arguments --- backend/server/src/SocketHandlers.ts | 20 +++++++++++--------- backend/server/src/index.ts | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts index 64dc05a..9169dc8 100644 --- a/backend/server/src/SocketHandlers.ts +++ b/backend/server/src/SocketHandlers.ts @@ -1,3 +1,4 @@ +import { Socket } from 'socket.io' import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" @@ -14,6 +15,7 @@ export interface HandlerContext { dokkuClient: DokkuClient | null; gitClient: SecureGitClient | null; lockManager: LockManager + socket: Socket } // Extract port number from a string @@ -50,7 +52,7 @@ export const handleMoveFile: SocketHandler = ({ fileId, folderId }: any, context } // Handle listing apps -export const handleListApps: SocketHandler = async ({ }: any, context: HandlerContext) => { +export const handleListApps: SocketHandler = async (_: any, context: HandlerContext) => { if (!context.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") return { success: true, apps: await context.dokkuClient.listApps() } } @@ -67,13 +69,13 @@ export const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: H } // Handle creating a file -export const handleCreateFile: SocketHandler = ({ name }: any, context: HandlerContext) => { - return context.fileManager.createFile(name) +export const handleCreateFile: SocketHandler = async ({ name }: any, context: HandlerContext) => { + return { "success": await context.fileManager.createFile(name) } } // Handle creating a folder -export const handleCreateFolder: SocketHandler = ({ name }: any, context: HandlerContext) => { - return context.fileManager.createFolder(name) +export const handleCreateFolder: SocketHandler = async ({ name }: any, context: HandlerContext) => { + return { "success": await context.fileManager.createFolder(name) } } // Handle renaming a file @@ -92,13 +94,13 @@ export const handleDeleteFolder: SocketHandler = ({ folderId }: any, context: Ha } // Handle creating a terminal session -export const handleCreateTerminal: SocketHandler = async ({ id, socket, data }: any, context: HandlerContext) => { - await context.lockManager.acquireLock(data.sandboxId, async () => { +export const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any, context: HandlerContext) => { + await context.lockManager.acquireLock(sandboxId, async () => { await context.terminalManager.createTerminal(id, (responseString: string) => { - socket.emit("terminalResponse", { id, data: responseString }) + context.socket.emit("terminalResponse", { id, data: responseString }) const port = extractPortNumber(responseString) if (port) { - socket.emit( + context.socket.emit( "previewURL", "https://" + context.sandboxManager.getHost(port) ) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index a496c96..4850077 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -178,6 +178,7 @@ io.on("connection", async (socket) => { gitClient, lockManager, sandboxManager: containers[data.sandboxId], + socket } // Helper function to handle socket events with error handling and optional rate limiting @@ -192,7 +193,7 @@ io.on("connection", async (socket) => { if (rateLimiter) { await rateLimiter.consume(data.userId, 1); // Adjust as needed for the specific rate limiter } - const response = await handler(options, handlerContext) + const response = await handler({ ...options, ...data }, handlerContext) callback?.(response); } catch (e: any) { console.error(`Error processing event "${event}":`, e); @@ -220,7 +221,6 @@ io.on("connection", async (socket) => { handleSocketEvent("closeTerminal", handleCloseTerminal); handleSocketEvent("generateCode", handleGenerateCode); - socket.on("disconnect", async () => { try { if (data.isOwner) { From 09ab81f5bd702c1275d75a78476d611d2b6d5b9c Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 23:36:04 -0600 Subject: [PATCH 11/27] refactor: move rate limiting to handler functions --- backend/server/src/SocketHandlers.ts | 22 +++++++++++++++++----- backend/server/src/index.ts | 23 ++++++----------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts index 9169dc8..5b2f663 100644 --- a/backend/server/src/SocketHandlers.ts +++ b/backend/server/src/SocketHandlers.ts @@ -3,6 +3,13 @@ import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" import { FileManager } from "./FileManager" +import { + createFileRL, + createFolderRL, + deleteFileRL, + renameFileRL, + saveFileRL, +} from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" import { TerminalManager } from "./TerminalManager" import { LockManager } from "./utils" @@ -42,7 +49,8 @@ export const handleGetFolder: SocketHandler = ({ folderId }: any, context: Handl } // Handle saving a file -export const handleSaveFile: SocketHandler = ({ fileId, body }: any, context: HandlerContext) => { +export const handleSaveFile: SocketHandler = async ({ fileId, body, userId }: any, context: HandlerContext) => { + await saveFileRL.consume(userId, 1); return context.fileManager.saveFile(fileId, body) } @@ -69,22 +77,26 @@ export const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: H } // Handle creating a file -export const handleCreateFile: SocketHandler = async ({ name }: any, context: HandlerContext) => { +export const handleCreateFile: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { + await createFileRL.consume(userId, 1); return { "success": await context.fileManager.createFile(name) } } // Handle creating a folder -export const handleCreateFolder: SocketHandler = async ({ name }: any, context: HandlerContext) => { +export const handleCreateFolder: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { + await createFolderRL.consume(userId, 1); return { "success": await context.fileManager.createFolder(name) } } // Handle renaming a file -export const handleRenameFile: SocketHandler = ({ fileId, newName }: any, context: HandlerContext) => { +export const handleRenameFile: SocketHandler = async ({ fileId, newName, userId }: any, context: HandlerContext) => { + await renameFileRL.consume(userId, 1) return context.fileManager.renameFile(fileId, newName) } // Handle deleting a file -export const handleDeleteFile: SocketHandler = ({ fileId }: any, context: HandlerContext) => { +export const handleDeleteFile: SocketHandler = async ({ fileId, userId }: any, context: HandlerContext) => { + await deleteFileRL.consume(userId, 1) return context.fileManager.deleteFile(fileId) } diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 4850077..bda426d 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -9,13 +9,6 @@ import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" import { FileManager, SandboxFiles } from "./FileManager" -import { - createFileRL, - createFolderRL, - deleteFileRL, - renameFileRL, - saveFileRL, -} from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, HandlerContext, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" @@ -184,15 +177,11 @@ io.on("connection", async (socket) => { // Helper function to handle socket events with error handling and optional rate limiting const handleSocketEvent = ( event: string, - handler: any, - rateLimiter: any | null = null + handler: any ) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { // Consume rate limiter if provided - if (rateLimiter) { - await rateLimiter.consume(data.userId, 1); // Adjust as needed for the specific rate limiter - } const response = await handler({ ...options, ...data }, handlerContext) callback?.(response); } catch (e: any) { @@ -206,14 +195,14 @@ io.on("connection", async (socket) => { handleSocketEvent("heartbeat", handleHeartbeat); handleSocketEvent("getFile", handleGetFile); handleSocketEvent("getFolder", handleGetFolder); - handleSocketEvent("saveFile", handleSaveFile, saveFileRL); + handleSocketEvent("saveFile", handleSaveFile); handleSocketEvent("moveFile", handleMoveFile); handleSocketEvent("list", handleListApps); handleSocketEvent("deploy", handleDeploy); - handleSocketEvent("createFile", handleCreateFile, createFileRL); - handleSocketEvent("createFolder", handleCreateFolder, createFolderRL); - handleSocketEvent("renameFile", handleRenameFile, renameFileRL); - handleSocketEvent("deleteFile", handleDeleteFile, deleteFileRL); + handleSocketEvent("createFile", handleCreateFile); + handleSocketEvent("createFolder", handleCreateFolder); + handleSocketEvent("renameFile", handleRenameFile); + handleSocketEvent("deleteFile", handleDeleteFile); handleSocketEvent("deleteFolder", handleDeleteFolder); handleSocketEvent("createTerminal", handleCreateTerminal); handleSocketEvent("resizeTerminal", handleResizeTerminal); From fcc7a836a60898eef227045e7adc95b4d07c32f5 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 23:39:53 -0600 Subject: [PATCH 12/27] refactor: export all event handlers as one object --- backend/server/src/SocketHandlers.ts | 54 +++++++++++++++++++--------- backend/server/src/index.ts | 23 +++--------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts index 5b2f663..4627d5f 100644 --- a/backend/server/src/SocketHandlers.ts +++ b/backend/server/src/SocketHandlers.ts @@ -34,39 +34,39 @@ function extractPortNumber(inputString: string): number | null { } // Handle heartbeat from a socket connection -export const handleHeartbeat: SocketHandler = (_: any, context: HandlerContext) => { +const handleHeartbeat: SocketHandler = (_: any, context: HandlerContext) => { context.sandboxManager.setTimeout(CONTAINER_TIMEOUT) } // Handle getting a file -export const handleGetFile: SocketHandler = ({ fileId }: any, context: HandlerContext) => { +const handleGetFile: SocketHandler = ({ fileId }: any, context: HandlerContext) => { return context.fileManager.getFile(fileId) } // Handle getting a folder -export const handleGetFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { +const handleGetFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { return context.fileManager.getFolder(folderId) } // Handle saving a file -export const handleSaveFile: SocketHandler = async ({ fileId, body, userId }: any, context: HandlerContext) => { +const handleSaveFile: SocketHandler = async ({ fileId, body, userId }: any, context: HandlerContext) => { await saveFileRL.consume(userId, 1); return context.fileManager.saveFile(fileId, body) } // Handle moving a file -export const handleMoveFile: SocketHandler = ({ fileId, folderId }: any, context: HandlerContext) => { +const handleMoveFile: SocketHandler = ({ fileId, folderId }: any, context: HandlerContext) => { return context.fileManager.moveFile(fileId, folderId) } // Handle listing apps -export const handleListApps: SocketHandler = async (_: any, context: HandlerContext) => { +const handleListApps: SocketHandler = async (_: any, context: HandlerContext) => { if (!context.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") return { success: true, apps: await context.dokkuClient.listApps() } } // Handle deploying code -export const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: HandlerContext) => { +const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: HandlerContext) => { if (!context.gitClient) throw Error("Failed to retrieve apps list: No git client") const fixedFilePaths = context.fileManager.sandboxFiles.fileData.map((file) => ({ ...file, @@ -77,36 +77,36 @@ export const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: H } // Handle creating a file -export const handleCreateFile: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { +const handleCreateFile: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { await createFileRL.consume(userId, 1); return { "success": await context.fileManager.createFile(name) } } // Handle creating a folder -export const handleCreateFolder: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { +const handleCreateFolder: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { await createFolderRL.consume(userId, 1); return { "success": await context.fileManager.createFolder(name) } } // Handle renaming a file -export const handleRenameFile: SocketHandler = async ({ fileId, newName, userId }: any, context: HandlerContext) => { +const handleRenameFile: SocketHandler = async ({ fileId, newName, userId }: any, context: HandlerContext) => { await renameFileRL.consume(userId, 1) return context.fileManager.renameFile(fileId, newName) } // Handle deleting a file -export const handleDeleteFile: SocketHandler = async ({ fileId, userId }: any, context: HandlerContext) => { +const handleDeleteFile: SocketHandler = async ({ fileId, userId }: any, context: HandlerContext) => { await deleteFileRL.consume(userId, 1) return context.fileManager.deleteFile(fileId) } // Handle deleting a folder -export const handleDeleteFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { +const handleDeleteFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { return context.fileManager.deleteFolder(folderId) } // Handle creating a terminal session -export const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any, context: HandlerContext) => { +const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any, context: HandlerContext) => { await context.lockManager.acquireLock(sandboxId, async () => { await context.terminalManager.createTerminal(id, (responseString: string) => { context.socket.emit("terminalResponse", { id, data: responseString }) @@ -122,24 +122,44 @@ export const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any } // Handle resizing a terminal -export const handleResizeTerminal: SocketHandler = ({ dimensions }: any, context: HandlerContext) => { +const handleResizeTerminal: SocketHandler = ({ dimensions }: any, context: HandlerContext) => { context.terminalManager.resizeTerminal(dimensions) } // Handle sending data to a terminal -export const handleTerminalData: SocketHandler = ({ id, data }: any, context: HandlerContext) => { +const handleTerminalData: SocketHandler = ({ id, data }: any, context: HandlerContext) => { return context.terminalManager.sendTerminalData(id, data) } // Handle closing a terminal -export const handleCloseTerminal: SocketHandler = ({ id }: any, context: HandlerContext) => { +const handleCloseTerminal: SocketHandler = ({ id }: any, context: HandlerContext) => { return context.terminalManager.closeTerminal(id) } // Handle generating code -export const handleGenerateCode: SocketHandler = ({ userId, fileName, code, line, instructions }: any, context: HandlerContext) => { +const handleGenerateCode: SocketHandler = ({ userId, fileName, code, line, instructions }: any, context: HandlerContext) => { return context.aiWorker.generateCode(userId, fileName, code, line, instructions) } // Define a type for SocketHandler functions type SocketHandler> = (args: T, context: HandlerContext) => any; + +export const eventHandlers = { + "heartbeat": handleHeartbeat, + "getFile": handleGetFile, + "getFolder": handleGetFolder, + "saveFile": handleSaveFile, + "moveFile": handleMoveFile, + "list": handleListApps, + "deploy": handleDeploy, + "createFile": handleCreateFile, + "createFolder": handleCreateFolder, + "renameFile": handleRenameFile, + "deleteFile": handleDeleteFile, + "deleteFolder": handleDeleteFolder, + "createTerminal": handleCreateTerminal, + "resizeTerminal": handleResizeTerminal, + "terminalData": handleTerminalData, + "closeTerminal": handleCloseTerminal, + "generateCode": handleGenerateCode, +}; diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index bda426d..f944684 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -11,7 +11,7 @@ import { DokkuClient } from "./DokkuClient" import { FileManager, SandboxFiles } from "./FileManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware -import { handleCloseTerminal, handleCreateFile, handleCreateFolder, handleCreateTerminal, handleDeleteFile, handleDeleteFolder, handleDeploy, handleGenerateCode, handleGetFile, handleGetFolder, handleHeartbeat, handleListApps, handleMoveFile, HandlerContext, handleRenameFile, handleResizeTerminal, handleSaveFile, handleTerminalData } from "./SocketHandlers" +import { eventHandlers, HandlerContext } from "./SocketHandlers" import { TerminalManager } from "./TerminalManager" import { LockManager } from "./utils" @@ -191,24 +191,9 @@ io.on("connection", async (socket) => { }); }; - // Register socket events with optional rate limiters - handleSocketEvent("heartbeat", handleHeartbeat); - handleSocketEvent("getFile", handleGetFile); - handleSocketEvent("getFolder", handleGetFolder); - handleSocketEvent("saveFile", handleSaveFile); - handleSocketEvent("moveFile", handleMoveFile); - handleSocketEvent("list", handleListApps); - handleSocketEvent("deploy", handleDeploy); - handleSocketEvent("createFile", handleCreateFile); - handleSocketEvent("createFolder", handleCreateFolder); - handleSocketEvent("renameFile", handleRenameFile); - handleSocketEvent("deleteFile", handleDeleteFile); - handleSocketEvent("deleteFolder", handleDeleteFolder); - handleSocketEvent("createTerminal", handleCreateTerminal); - handleSocketEvent("resizeTerminal", handleResizeTerminal); - handleSocketEvent("terminalData", handleTerminalData); - handleSocketEvent("closeTerminal", handleCloseTerminal); - handleSocketEvent("generateCode", handleGenerateCode); + Object.entries(eventHandlers).forEach(([event, handler]) => { + handleSocketEvent(event, handler); + }); socket.on("disconnect", async () => { try { From 16e0c250d6d76416775a811a3a6ae8995ef0595a Mon Sep 17 00:00:00 2001 From: James Murdza Date: Thu, 24 Oct 2024 23:58:28 -0600 Subject: [PATCH 13/27] refactor: create sandboxManager class --- backend/server/src/SandboxManager.ts | 181 +++++++++++++++++++++++++++ backend/server/src/SocketHandlers.ts | 165 ------------------------ backend/server/src/index.ts | 20 +-- 3 files changed, 187 insertions(+), 179 deletions(-) create mode 100644 backend/server/src/SandboxManager.ts delete mode 100644 backend/server/src/SocketHandlers.ts diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts new file mode 100644 index 0000000..4d957bf --- /dev/null +++ b/backend/server/src/SandboxManager.ts @@ -0,0 +1,181 @@ +import { Socket } from 'socket.io' +import { AIWorker } from "./AIWorker" +import { CONTAINER_TIMEOUT } from "./constants" +import { DokkuClient } from "./DokkuClient" +import { FileManager } from "./FileManager" +import { + createFileRL, + createFolderRL, + deleteFileRL, + renameFileRL, + saveFileRL, +} from "./ratelimit" +import { SecureGitClient } from "./SecureGitClient" +import { TerminalManager } from "./TerminalManager" +import { LockManager } from "./utils" + +// Define a type for SocketHandler functions +type SocketHandler> = (args: T) => any; + +// Extract port number from a string +function extractPortNumber(inputString: string): number | null { + const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "") + const regex = /http:\/\/localhost:(\d+)/ + const match = cleanedString.match(regex) + return match ? parseInt(match[1]) : null +} + +export class SandboxManager { + fileManager: FileManager; + terminalManager: TerminalManager; + container: any; + aiWorker: AIWorker; + dokkuClient: DokkuClient | null; + gitClient: SecureGitClient | null; + lockManager: LockManager; + socket: Socket; + + constructor(fileManager: FileManager, terminalManager: TerminalManager, aiWorker: AIWorker, dokkuClient: DokkuClient | null, gitClient: SecureGitClient | null, lockManager: LockManager, sandboxManager: any, socket: Socket) { + this.fileManager = fileManager; + this.terminalManager = terminalManager; + this.aiWorker = aiWorker; + this.dokkuClient = dokkuClient; + this.gitClient = gitClient; + this.lockManager = lockManager; + this.socket = socket; + this.container = sandboxManager; + } + + handlers() { + + // Handle heartbeat from a socket connection + const handleHeartbeat: SocketHandler = (_: any) => { + this.container.setTimeout(CONTAINER_TIMEOUT) + } + + // Handle getting a file + const handleGetFile: SocketHandler = ({ fileId }: any) => { + return this.fileManager.getFile(fileId) + } + + // Handle getting a folder + const handleGetFolder: SocketHandler = ({ folderId }: any) => { + return this.fileManager.getFolder(folderId) + } + + // Handle saving a file + const handleSaveFile: SocketHandler = async ({ fileId, body, userId }: any) => { + await saveFileRL.consume(userId, 1); + return this.fileManager.saveFile(fileId, body) + } + + // Handle moving a file + const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => { + return this.fileManager.moveFile(fileId, folderId) + } + + // Handle listing apps + const handleListApps: SocketHandler = async (_: any) => { + if (!this.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") + return { success: true, apps: await this.dokkuClient.listApps() } + } + + // Handle deploying code + const handleDeploy: SocketHandler = async ({ sandboxId }: any) => { + if (!this.gitClient) throw Error("Failed to retrieve apps list: No git client") + const fixedFilePaths = this.fileManager.sandboxFiles.fileData.map((file) => ({ + ...file, + id: file.id.split("/").slice(2).join("/"), + })) + await this.gitClient.pushFiles(fixedFilePaths, sandboxId) + return { success: true } + } + + // Handle creating a file + const handleCreateFile: SocketHandler = async ({ name, userId }: any) => { + await createFileRL.consume(userId, 1); + return { "success": await this.fileManager.createFile(name) } + } + + // Handle creating a folder + const handleCreateFolder: SocketHandler = async ({ name, userId }: any) => { + await createFolderRL.consume(userId, 1); + return { "success": await this.fileManager.createFolder(name) } + } + + // Handle renaming a file + const handleRenameFile: SocketHandler = async ({ fileId, newName, userId }: any) => { + await renameFileRL.consume(userId, 1) + return this.fileManager.renameFile(fileId, newName) + } + + // Handle deleting a file + const handleDeleteFile: SocketHandler = async ({ fileId, userId }: any) => { + await deleteFileRL.consume(userId, 1) + return this.fileManager.deleteFile(fileId) + } + + // Handle deleting a folder + const handleDeleteFolder: SocketHandler = ({ folderId }: any) => { + return this.fileManager.deleteFolder(folderId) + } + + // Handle creating a terminal session + const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any) => { + await this.lockManager.acquireLock(sandboxId, async () => { + await this.terminalManager.createTerminal(id, (responseString: string) => { + this.socket.emit("terminalResponse", { id, data: responseString }) + const port = extractPortNumber(responseString) + if (port) { + this.socket.emit( + "previewURL", + "https://" + this.container.getHost(port) + ) + } + }) + }) + } + + // Handle resizing a terminal + const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => { + this.terminalManager.resizeTerminal(dimensions) + } + + // Handle sending data to a terminal + const handleTerminalData: SocketHandler = ({ id, data }: any) => { + return this.terminalManager.sendTerminalData(id, data) + } + + // Handle closing a terminal + const handleCloseTerminal: SocketHandler = ({ id }: any) => { + return this.terminalManager.closeTerminal(id) + } + + // Handle generating code + const handleGenerateCode: SocketHandler = ({ userId, fileName, code, line, instructions }: any) => { + return this.aiWorker.generateCode(userId, fileName, code, line, instructions) + } + + return { + "heartbeat": handleHeartbeat, + "getFile": handleGetFile, + "getFolder": handleGetFolder, + "saveFile": handleSaveFile, + "moveFile": handleMoveFile, + "list": handleListApps, + "deploy": handleDeploy, + "createFile": handleCreateFile, + "createFolder": handleCreateFolder, + "renameFile": handleRenameFile, + "deleteFile": handleDeleteFile, + "deleteFolder": handleDeleteFolder, + "createTerminal": handleCreateTerminal, + "resizeTerminal": handleResizeTerminal, + "terminalData": handleTerminalData, + "closeTerminal": handleCloseTerminal, + "generateCode": handleGenerateCode, + }; + + } + +} \ No newline at end of file diff --git a/backend/server/src/SocketHandlers.ts b/backend/server/src/SocketHandlers.ts deleted file mode 100644 index 4627d5f..0000000 --- a/backend/server/src/SocketHandlers.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Socket } from 'socket.io' -import { AIWorker } from "./AIWorker" -import { CONTAINER_TIMEOUT } from "./constants" -import { DokkuClient } from "./DokkuClient" -import { FileManager } from "./FileManager" -import { - createFileRL, - createFolderRL, - deleteFileRL, - renameFileRL, - saveFileRL, -} from "./ratelimit" -import { SecureGitClient } from "./SecureGitClient" -import { TerminalManager } from "./TerminalManager" -import { LockManager } from "./utils" - -export interface HandlerContext { - fileManager: FileManager; - terminalManager: TerminalManager; - sandboxManager: any; - aiWorker: AIWorker; - dokkuClient: DokkuClient | null; - gitClient: SecureGitClient | null; - lockManager: LockManager - socket: Socket -} - -// Extract port number from a string -function extractPortNumber(inputString: string): number | null { - const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, "") - const regex = /http:\/\/localhost:(\d+)/ - const match = cleanedString.match(regex) - return match ? parseInt(match[1]) : null -} - -// Handle heartbeat from a socket connection -const handleHeartbeat: SocketHandler = (_: any, context: HandlerContext) => { - context.sandboxManager.setTimeout(CONTAINER_TIMEOUT) -} - -// Handle getting a file -const handleGetFile: SocketHandler = ({ fileId }: any, context: HandlerContext) => { - return context.fileManager.getFile(fileId) -} - -// Handle getting a folder -const handleGetFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { - return context.fileManager.getFolder(folderId) -} - -// Handle saving a file -const handleSaveFile: SocketHandler = async ({ fileId, body, userId }: any, context: HandlerContext) => { - await saveFileRL.consume(userId, 1); - return context.fileManager.saveFile(fileId, body) -} - -// Handle moving a file -const handleMoveFile: SocketHandler = ({ fileId, folderId }: any, context: HandlerContext) => { - return context.fileManager.moveFile(fileId, folderId) -} - -// Handle listing apps -const handleListApps: SocketHandler = async (_: any, context: HandlerContext) => { - if (!context.dokkuClient) throw Error("Failed to retrieve apps list: No Dokku client") - return { success: true, apps: await context.dokkuClient.listApps() } -} - -// Handle deploying code -const handleDeploy: SocketHandler = async ({ sandboxId }: any, context: HandlerContext) => { - if (!context.gitClient) throw Error("Failed to retrieve apps list: No git client") - const fixedFilePaths = context.fileManager.sandboxFiles.fileData.map((file) => ({ - ...file, - id: file.id.split("/").slice(2).join("/"), - })) - await context.gitClient.pushFiles(fixedFilePaths, sandboxId) - return { success: true } -} - -// Handle creating a file -const handleCreateFile: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { - await createFileRL.consume(userId, 1); - return { "success": await context.fileManager.createFile(name) } -} - -// Handle creating a folder -const handleCreateFolder: SocketHandler = async ({ name, userId }: any, context: HandlerContext) => { - await createFolderRL.consume(userId, 1); - return { "success": await context.fileManager.createFolder(name) } -} - -// Handle renaming a file -const handleRenameFile: SocketHandler = async ({ fileId, newName, userId }: any, context: HandlerContext) => { - await renameFileRL.consume(userId, 1) - return context.fileManager.renameFile(fileId, newName) -} - -// Handle deleting a file -const handleDeleteFile: SocketHandler = async ({ fileId, userId }: any, context: HandlerContext) => { - await deleteFileRL.consume(userId, 1) - return context.fileManager.deleteFile(fileId) -} - -// Handle deleting a folder -const handleDeleteFolder: SocketHandler = ({ folderId }: any, context: HandlerContext) => { - return context.fileManager.deleteFolder(folderId) -} - -// Handle creating a terminal session -const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any, context: HandlerContext) => { - await context.lockManager.acquireLock(sandboxId, async () => { - await context.terminalManager.createTerminal(id, (responseString: string) => { - context.socket.emit("terminalResponse", { id, data: responseString }) - const port = extractPortNumber(responseString) - if (port) { - context.socket.emit( - "previewURL", - "https://" + context.sandboxManager.getHost(port) - ) - } - }) - }) -} - -// Handle resizing a terminal -const handleResizeTerminal: SocketHandler = ({ dimensions }: any, context: HandlerContext) => { - context.terminalManager.resizeTerminal(dimensions) -} - -// Handle sending data to a terminal -const handleTerminalData: SocketHandler = ({ id, data }: any, context: HandlerContext) => { - return context.terminalManager.sendTerminalData(id, data) -} - -// Handle closing a terminal -const handleCloseTerminal: SocketHandler = ({ id }: any, context: HandlerContext) => { - return context.terminalManager.closeTerminal(id) -} - -// Handle generating code -const handleGenerateCode: SocketHandler = ({ userId, fileName, code, line, instructions }: any, context: HandlerContext) => { - return context.aiWorker.generateCode(userId, fileName, code, line, instructions) -} - -// Define a type for SocketHandler functions -type SocketHandler> = (args: T, context: HandlerContext) => any; - -export const eventHandlers = { - "heartbeat": handleHeartbeat, - "getFile": handleGetFile, - "getFolder": handleGetFolder, - "saveFile": handleSaveFile, - "moveFile": handleMoveFile, - "list": handleListApps, - "deploy": handleDeploy, - "createFile": handleCreateFile, - "createFolder": handleCreateFolder, - "renameFile": handleRenameFile, - "deleteFile": handleDeleteFile, - "deleteFolder": handleDeleteFolder, - "createTerminal": handleCreateTerminal, - "resizeTerminal": handleResizeTerminal, - "terminalData": handleTerminalData, - "closeTerminal": handleCloseTerminal, - "generateCode": handleGenerateCode, -}; diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index f944684..bdbc7af 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -9,9 +9,9 @@ import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" import { FileManager, SandboxFiles } from "./FileManager" +import { SandboxManager } from "./SandboxManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware -import { eventHandlers, HandlerContext } from "./SocketHandlers" import { TerminalManager } from "./TerminalManager" import { LockManager } from "./utils" @@ -163,36 +163,28 @@ io.on("connection", async (socket) => { // Load file list from the file manager into the editor sendLoadedEvent(fileManager.sandboxFiles) - const handlerContext: HandlerContext = { + const sandboxManager = new SandboxManager( fileManager, terminalManager, aiWorker, dokkuClient, gitClient, lockManager, - sandboxManager: containers[data.sandboxId], + containers[data.sandboxId], socket - } + ) - // Helper function to handle socket events with error handling and optional rate limiting - const handleSocketEvent = ( - event: string, - handler: any - ) => { + Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { // Consume rate limiter if provided - const response = await handler({ ...options, ...data }, handlerContext) + const response = await handler({ ...options, ...data }) callback?.(response); } catch (e: any) { console.error(`Error processing event "${event}":`, e); socket.emit("error", `Error: ${event}. ${e.message ?? e}`); } }); - }; - - Object.entries(eventHandlers).forEach(([event, handler]) => { - handleSocketEvent(event, handler); }); socket.on("disconnect", async () => { From 3e891e6ab19ca9c23201812b48d646b798e42438 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 06:38:25 -0600 Subject: [PATCH 14/27] refactor: move initialization code to SandboxManager --- backend/server/src/SandboxManager.ts | 138 ++++++++++++++++++--------- backend/server/src/index.ts | 82 +++------------- 2 files changed, 107 insertions(+), 113 deletions(-) diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts index 4d957bf..809e2e8 100644 --- a/backend/server/src/SandboxManager.ts +++ b/backend/server/src/SandboxManager.ts @@ -1,8 +1,9 @@ +import { Sandbox } from "e2b" import { Socket } from 'socket.io' import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" -import { FileManager } from "./FileManager" +import { FileManager, SandboxFiles } from "./FileManager" import { createFileRL, createFolderRL, @@ -14,6 +15,8 @@ import { SecureGitClient } from "./SecureGitClient" import { TerminalManager } from "./TerminalManager" import { LockManager } from "./utils" +const lockManager = new LockManager() + // Define a type for SocketHandler functions type SocketHandler> = (args: T) => any; @@ -25,53 +28,99 @@ function extractPortNumber(inputString: string): number | null { return match ? parseInt(match[1]) : null } -export class SandboxManager { - fileManager: FileManager; - terminalManager: TerminalManager; - container: any; +type SandboxManagerContext = { aiWorker: AIWorker; dokkuClient: DokkuClient | null; gitClient: SecureGitClient | null; - lockManager: LockManager; socket: Socket; +}; - constructor(fileManager: FileManager, terminalManager: TerminalManager, aiWorker: AIWorker, dokkuClient: DokkuClient | null, gitClient: SecureGitClient | null, lockManager: LockManager, sandboxManager: any, socket: Socket) { - this.fileManager = fileManager; - this.terminalManager = terminalManager; +export class SandboxManager { + fileManager: FileManager | null; + terminalManager: TerminalManager | null; + container: Sandbox | null; + dokkuClient: DokkuClient | null; + gitClient: SecureGitClient | null; + aiWorker: AIWorker; + socket: Socket; + sandboxId: string; + userId: string; + + constructor(sandboxId: string, userId: string, { aiWorker, dokkuClient, gitClient, socket }: SandboxManagerContext) { + this.fileManager = null; + this.terminalManager = null; + this.container = null; + this.sandboxId = sandboxId; + this.userId = userId; this.aiWorker = aiWorker; this.dokkuClient = dokkuClient; this.gitClient = gitClient; - this.lockManager = lockManager; this.socket = socket; - this.container = sandboxManager; + } + + async initializeContainer() { + + await lockManager.acquireLock(this.sandboxId, async () => { + if (this.container && await this.container.isRunning()) { + console.log(`Found existing container ${this.sandboxId}`) + } else { + console.log("Creating container", this.sandboxId) + this.container = await Sandbox.create({ + timeoutMs: CONTAINER_TIMEOUT, + }) + } + }) + if (!this.container) throw new Error("Failed to create container") + + if (!this.terminalManager) { + this.terminalManager = new TerminalManager(this.container) + console.log(`Terminal manager set up for ${this.sandboxId}`) + } + + if (!this.fileManager) { + this.fileManager = new FileManager( + this.sandboxId, + this.container, + (files: SandboxFiles) => { + this.socket.emit("loaded", files.files) + } + ) + this.fileManager.initialize() + this.socket.emit("loaded", this.fileManager.sandboxFiles.files) + } + } + + async disconnect() { + await this.terminalManager?.closeAllTerminals() + await this.fileManager?.closeWatchers() } handlers() { // Handle heartbeat from a socket connection const handleHeartbeat: SocketHandler = (_: any) => { - this.container.setTimeout(CONTAINER_TIMEOUT) + this.container?.setTimeout(CONTAINER_TIMEOUT) } // Handle getting a file const handleGetFile: SocketHandler = ({ fileId }: any) => { - return this.fileManager.getFile(fileId) + return this.fileManager?.getFile(fileId) } // Handle getting a folder const handleGetFolder: SocketHandler = ({ folderId }: any) => { - return this.fileManager.getFolder(folderId) + return this.fileManager?.getFolder(folderId) } // Handle saving a file - const handleSaveFile: SocketHandler = async ({ fileId, body, userId }: any) => { - await saveFileRL.consume(userId, 1); - return this.fileManager.saveFile(fileId, body) + const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => { + await saveFileRL.consume(this.userId, 1); + return this.fileManager?.saveFile(fileId, body) } // Handle moving a file const handleMoveFile: SocketHandler = ({ fileId, folderId }: any) => { - return this.fileManager.moveFile(fileId, folderId) + return this.fileManager?.moveFile(fileId, folderId) } // Handle listing apps @@ -81,55 +130,56 @@ export class SandboxManager { } // Handle deploying code - const handleDeploy: SocketHandler = async ({ sandboxId }: any) => { - if (!this.gitClient) throw Error("Failed to retrieve apps list: No git client") - const fixedFilePaths = this.fileManager.sandboxFiles.fileData.map((file) => ({ + const handleDeploy: SocketHandler = async (_: any) => { + if (!this.gitClient) throw Error("No git client") + if (!this.fileManager) throw Error("No file manager") + const fixedFilePaths = this.fileManager?.sandboxFiles.fileData.map((file) => ({ ...file, id: file.id.split("/").slice(2).join("/"), })) - await this.gitClient.pushFiles(fixedFilePaths, sandboxId) + await this.gitClient.pushFiles(fixedFilePaths, this.sandboxId) return { success: true } } // Handle creating a file - const handleCreateFile: SocketHandler = async ({ name, userId }: any) => { - await createFileRL.consume(userId, 1); - return { "success": await this.fileManager.createFile(name) } + const handleCreateFile: SocketHandler = async ({ name }: any) => { + await createFileRL.consume(this.userId, 1); + return { "success": await this.fileManager?.createFile(name) } } // Handle creating a folder - const handleCreateFolder: SocketHandler = async ({ name, userId }: any) => { - await createFolderRL.consume(userId, 1); - return { "success": await this.fileManager.createFolder(name) } + const handleCreateFolder: SocketHandler = async ({ name }: any) => { + await createFolderRL.consume(this.userId, 1); + return { "success": await this.fileManager?.createFolder(name) } } // Handle renaming a file - const handleRenameFile: SocketHandler = async ({ fileId, newName, userId }: any) => { - await renameFileRL.consume(userId, 1) - return this.fileManager.renameFile(fileId, newName) + const handleRenameFile: SocketHandler = async ({ fileId, newName }: any) => { + await renameFileRL.consume(this.userId, 1) + return this.fileManager?.renameFile(fileId, newName) } // Handle deleting a file - const handleDeleteFile: SocketHandler = async ({ fileId, userId }: any) => { - await deleteFileRL.consume(userId, 1) - return this.fileManager.deleteFile(fileId) + const handleDeleteFile: SocketHandler = async ({ fileId }: any) => { + await deleteFileRL.consume(this.userId, 1) + return this.fileManager?.deleteFile(fileId) } // Handle deleting a folder const handleDeleteFolder: SocketHandler = ({ folderId }: any) => { - return this.fileManager.deleteFolder(folderId) + return this.fileManager?.deleteFolder(folderId) } // Handle creating a terminal session - const handleCreateTerminal: SocketHandler = async ({ id, sandboxId }: any) => { - await this.lockManager.acquireLock(sandboxId, async () => { - await this.terminalManager.createTerminal(id, (responseString: string) => { + const handleCreateTerminal: SocketHandler = async ({ id }: any) => { + await lockManager.acquireLock(this.sandboxId, async () => { + await this.terminalManager?.createTerminal(id, (responseString: string) => { this.socket.emit("terminalResponse", { id, data: responseString }) const port = extractPortNumber(responseString) if (port) { this.socket.emit( "previewURL", - "https://" + this.container.getHost(port) + "https://" + this.container?.getHost(port) ) } }) @@ -138,22 +188,22 @@ export class SandboxManager { // Handle resizing a terminal const handleResizeTerminal: SocketHandler = ({ dimensions }: any) => { - this.terminalManager.resizeTerminal(dimensions) + this.terminalManager?.resizeTerminal(dimensions) } // Handle sending data to a terminal const handleTerminalData: SocketHandler = ({ id, data }: any) => { - return this.terminalManager.sendTerminalData(id, data) + return this.terminalManager?.sendTerminalData(id, data) } // Handle closing a terminal const handleCloseTerminal: SocketHandler = ({ id }: any) => { - return this.terminalManager.closeTerminal(id) + return this.terminalManager?.closeTerminal(id) } // Handle generating code - const handleGenerateCode: SocketHandler = ({ userId, fileName, code, line, instructions }: any) => { - return this.aiWorker.generateCode(userId, fileName, code, line, instructions) + const handleGenerateCode: SocketHandler = ({ fileName, code, line, instructions }: any) => { + return this.aiWorker.generateCode(this.userId, fileName, code, line, instructions) } return { diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index bdbc7af..54f1315 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -1,19 +1,15 @@ import cors from "cors" import dotenv from "dotenv" -import { Sandbox } from "e2b" import express, { Express } from "express" import fs from "fs" import { createServer } from "http" import { Server } from "socket.io" import { AIWorker } from "./AIWorker" -import { CONTAINER_TIMEOUT } from "./constants" + import { DokkuClient } from "./DokkuClient" -import { FileManager, SandboxFiles } from "./FileManager" import { SandboxManager } from "./SandboxManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware -import { TerminalManager } from "./TerminalManager" -import { LockManager } from "./utils" // Handle uncaught exceptions process.on("uncaughtException", (error) => { @@ -35,10 +31,8 @@ function isOwnerConnected(sandboxId: string): boolean { } // Initialize containers and managers -const containers: Record = {} const connections: Record = {} -const fileManagers: Record = {} -const terminalManagers: Record = {} +const sandboxManagers: Record = {} // Load environment variables dotenv.config() @@ -57,9 +51,6 @@ const io = new Server(httpServer, { // Middleware for socket authentication io.use(socketAuth) // Use the new socketAuth middleware -// Initialize lock manager -const lockManager = new LockManager() - // Check for required environment variables if (!process.env.DOKKU_HOST) console.warn("Environment variable DOKKU_HOST is not defined") @@ -99,6 +90,7 @@ const aiWorker = new AIWorker( // Handle a client connecting to the server io.on("connection", async (socket) => { try { + // This data comes is added by our authentication middleware const data = socket.data as { userId: string sandboxId: string @@ -115,70 +107,23 @@ io.on("connection", async (socket) => { } } - // Create or retrieve container - const createdContainer = await lockManager.acquireLock( + const sandboxManager = sandboxManagers[data.sandboxId] ?? new SandboxManager( data.sandboxId, - async () => { - try { - // Start a new container if the container doesn't exist or it timed out. - if ( - !containers[data.sandboxId] || - !(await containers[data.sandboxId].isRunning()) - ) { - containers[data.sandboxId] = await Sandbox.create({ - timeoutMs: CONTAINER_TIMEOUT, - }) - console.log("Created container ", data.sandboxId) - return true - } - } catch (e: any) { - console.error(`Error creating container ${data.sandboxId}:`, e) - socket.emit("error", `Error: container creation. ${e.message ?? e}`) - } - } + data.userId, + { aiWorker, dokkuClient, gitClient, socket } ) - // Function to send loaded event - const sendLoadedEvent = (files: SandboxFiles) => { - socket.emit("loaded", files.files) + try { + sandboxManager.initializeContainer() + } catch (e: any) { + console.error(`Error initializing sandbox ${data.sandboxId}:`, e); + socket.emit("error", `Error: initialize sandbox ${data.sandboxId}. ${e.message ?? e}`); } - // Initialize file and terminal managers if container was created - if (createdContainer) { - fileManagers[data.sandboxId] = new FileManager( - data.sandboxId, - containers[data.sandboxId], - sendLoadedEvent - ) - terminalManagers[data.sandboxId] = new TerminalManager( - containers[data.sandboxId] - ) - console.log(`terminal manager set up for ${data.sandboxId}`) - await fileManagers[data.sandboxId].initialize() - } - - const fileManager = fileManagers[data.sandboxId] - const terminalManager = terminalManagers[data.sandboxId] - - // Load file list from the file manager into the editor - sendLoadedEvent(fileManager.sandboxFiles) - - const sandboxManager = new SandboxManager( - fileManager, - terminalManager, - aiWorker, - dokkuClient, - gitClient, - lockManager, - containers[data.sandboxId], - socket - ) - Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { - // Consume rate limiter if provided - const response = await handler({ ...options, ...data }) + const response = await handler(options) callback?.(response); } catch (e: any) { console.error(`Error processing event "${event}":`, e); @@ -193,8 +138,7 @@ io.on("connection", async (socket) => { connections[data.sandboxId]-- } - await terminalManager.closeAllTerminals() - await fileManager.closeWatchers() + await sandboxManager.disconnect() if (data.isOwner && connections[data.sandboxId] <= 0) { socket.broadcast.emit( From dc4be6392a7125ceb18804720072d709bd9c2b85 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 06:40:47 -0600 Subject: [PATCH 15/27] refactor: restructure try...catch blocks in server --- backend/server/src/index.ts | 76 +++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 54f1315..d87c71b 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -107,50 +107,52 @@ io.on("connection", async (socket) => { } } - const sandboxManager = sandboxManagers[data.sandboxId] ?? new SandboxManager( - data.sandboxId, - data.userId, - { aiWorker, dokkuClient, gitClient, socket } - ) - try { + const sandboxManager = sandboxManagers[data.sandboxId] ?? new SandboxManager( + data.sandboxId, + data.userId, + { aiWorker, dokkuClient, gitClient, socket } + ) + sandboxManager.initializeContainer() + + Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { + socket.on(event, async (options: any, callback?: (response: any) => void) => { + try { + const response = await handler(options) + callback?.(response); + } catch (e: any) { + console.error(`Error processing event "${event}":`, e); + socket.emit("error", `Error: ${event}. ${e.message ?? e}`); + } + }); + }); + + socket.on("disconnect", async () => { + try { + if (data.isOwner) { + connections[data.sandboxId]-- + } + + await sandboxManager.disconnect() + + if (data.isOwner && connections[data.sandboxId] <= 0) { + socket.broadcast.emit( + "disableAccess", + "The sandbox owner has disconnected." + ) + } + } catch (e: any) { + console.log("Error disconnecting:", e) + socket.emit("error", `Error: disconnecting. ${e.message ?? e}`) + } + }) + } catch (e: any) { console.error(`Error initializing sandbox ${data.sandboxId}:`, e); socket.emit("error", `Error: initialize sandbox ${data.sandboxId}. ${e.message ?? e}`); } - Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { - socket.on(event, async (options: any, callback?: (response: any) => void) => { - try { - const response = await handler(options) - callback?.(response); - } catch (e: any) { - console.error(`Error processing event "${event}":`, e); - socket.emit("error", `Error: ${event}. ${e.message ?? e}`); - } - }); - }); - - socket.on("disconnect", async () => { - try { - if (data.isOwner) { - connections[data.sandboxId]-- - } - - await sandboxManager.disconnect() - - if (data.isOwner && connections[data.sandboxId] <= 0) { - socket.broadcast.emit( - "disableAccess", - "The sandbox owner has disconnected." - ) - } - } catch (e: any) { - console.log("Error disconnecting:", e) - socket.emit("error", `Error: disconnecting. ${e.message ?? e}`) - } - }) } catch (e: any) { console.error("Error connecting:", e) socket.emit("error", `Error: connection. ${e.message ?? e}`) From 28e6e2f88979e73033ae1605a754c3587e0e3b97 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 06:57:28 -0600 Subject: [PATCH 16/27] refactor: simplify file manager properties --- backend/server/src/FileManager.ts | 72 +++++++++++++--------------- backend/server/src/SandboxManager.ts | 11 +++-- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/backend/server/src/FileManager.ts b/backend/server/src/FileManager.ts index 278d060..43fb9e3 100644 --- a/backend/server/src/FileManager.ts +++ b/backend/server/src/FileManager.ts @@ -4,12 +4,6 @@ import RemoteFileStorage from "./RemoteFileStorage" import { MAX_BODY_SIZE } from "./ratelimit" import { TFile, TFileData, TFolder } from "./types" -// Define the structure for sandbox files -export type SandboxFiles = { - files: (TFolder | TFile)[] - fileData: TFileData[] -} - // Convert list of paths to the hierchical file structure used by the editor function generateFileStructure(paths: string[]): (TFolder | TFile)[] { const root: TFolder = { id: "/", type: "folder", name: "/", children: [] } @@ -52,20 +46,22 @@ function generateFileStructure(paths: string[]): (TFolder | TFile)[] { export class FileManager { private sandboxId: string private sandbox: Sandbox - public sandboxFiles: SandboxFiles + public files: (TFolder | TFile)[] + public fileData: TFileData[] private fileWatchers: WatchHandle[] = [] private dirName = "/home/user/project" - private refreshFileList: (files: SandboxFiles) => void + private refreshFileList: (files: (TFolder | TFile)[]) => void // Constructor to initialize the FileManager constructor( sandboxId: string, sandbox: Sandbox, - refreshFileList: (files: SandboxFiles) => void + refreshFileList: (files: (TFolder | TFile)[]) => void ) { this.sandboxId = sandboxId this.sandbox = sandbox - this.sandboxFiles = { files: [], fileData: [] } + this.files = [] + this.fileData = [] this.refreshFileList = refreshFileList } @@ -110,16 +106,16 @@ export class FileManager { private async updateFileData(): Promise { const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId) const localPaths = this.getLocalFileIds(remotePaths) - this.sandboxFiles.fileData = await this.generateFileData(localPaths) - return this.sandboxFiles.fileData + this.fileData = await this.generateFileData(localPaths) + return this.fileData } // Update file structure private async updateFileStructure(): Promise<(TFolder | TFile)[]> { const remotePaths = await RemoteFileStorage.getSandboxPaths(this.sandboxId) const localPaths = this.getLocalFileIds(remotePaths) - this.sandboxFiles.files = generateFileStructure(localPaths) - return this.sandboxFiles.files + this.files = generateFileStructure(localPaths) + return this.files } // Initialize the FileManager @@ -130,7 +126,7 @@ export class FileManager { await this.updateFileData() // Copy all files from the project to the container - const promises = this.sandboxFiles.fileData.map(async (file) => { + const promises = this.fileData.map(async (file) => { try { const filePath = path.join(this.dirName, file.id) const parentDirectory = path.dirname(filePath) @@ -209,7 +205,7 @@ export class FileManager { // Handle file/directory creation event if (event.type === "create") { const folder = findFolderById( - this.sandboxFiles.files, + this.files, sandboxDirectory ) as TFolder const isDir = await this.isDirectory(containerFilePath) @@ -232,7 +228,7 @@ export class FileManager { folder.children.push(newItem) } else { // If folder doesn't exist, add the new item to the root - this.sandboxFiles.files.push(newItem) + this.files.push(newItem) } if (!isDir) { @@ -241,7 +237,7 @@ export class FileManager { ) const fileContents = typeof fileData === "string" ? fileData : "" - this.sandboxFiles.fileData.push({ + this.fileData.push({ id: sandboxFilePath, data: fileContents, }) @@ -253,7 +249,7 @@ export class FileManager { // Handle file/directory removal or rename event else if (event.type === "remove" || event.type == "rename") { const folder = findFolderById( - this.sandboxFiles.files, + this.files, sandboxDirectory ) as TFolder const isDir = await this.isDirectory(containerFilePath) @@ -269,13 +265,13 @@ export class FileManager { ) } else { // Remove from the root if it's not inside a folder - this.sandboxFiles.files = this.sandboxFiles.files.filter( + this.files = this.files.filter( (file: TFolder | TFile) => !isFileMatch(file) ) } // Also remove any corresponding file data - this.sandboxFiles.fileData = this.sandboxFiles.fileData.filter( + this.fileData = this.fileData.filter( (file: TFileData) => !isFileMatch(file) ) @@ -285,10 +281,10 @@ export class FileManager { // Handle file write event else if (event.type === "write") { const folder = findFolderById( - this.sandboxFiles.files, + this.files, sandboxDirectory ) as TFolder - const fileToWrite = this.sandboxFiles.fileData.find( + const fileToWrite = this.fileData.find( (file) => file.id === sandboxFilePath ) @@ -308,7 +304,7 @@ export class FileManager { ) const fileContents = typeof fileData === "string" ? fileData : "" - this.sandboxFiles.fileData.push({ + this.fileData.push({ id: sandboxFilePath, data: fileContents, }) @@ -318,7 +314,7 @@ export class FileManager { } // Tell the client to reload the file list - this.refreshFileList(this.sandboxFiles) + this.refreshFileList(this.files) } catch (error) { console.error( `Error handling ${event.type} event for ${event.name}:`, @@ -350,7 +346,7 @@ export class FileManager { // Get file content async getFile(fileId: string): Promise { - const file = this.sandboxFiles.fileData.find((f) => f.id === fileId) + const file = this.fileData.find((f) => f.id === fileId) return file?.data } @@ -368,7 +364,7 @@ export class FileManager { throw new Error("File size too large. Please reduce the file size.") } await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body) - const file = this.sandboxFiles.fileData.find((f) => f.id === fileId) + const file = this.fileData.find((f) => f.id === fileId) if (!file) return file.data = body @@ -381,9 +377,9 @@ export class FileManager { fileId: string, folderId: string ): Promise<(TFolder | TFile)[]> { - const fileData = this.sandboxFiles.fileData.find((f) => f.id === fileId) - const file = this.sandboxFiles.files.find((f) => f.id === fileId) - if (!fileData || !file) return this.sandboxFiles.files + const fileData = this.fileData.find((f) => f.id === fileId) + const file = this.files.find((f) => f.id === fileId) + if (!fileData || !file) return this.files const parts = fileId.split("/") const newFileId = folderId + "/" + parts.pop() @@ -427,13 +423,13 @@ export class FileManager { await this.sandbox.files.write(path.posix.join(this.dirName, id), "") await this.fixPermissions() - this.sandboxFiles.files.push({ + this.files.push({ id, name, type: "file", }) - this.sandboxFiles.fileData.push({ + this.fileData.push({ id, data: "", }) @@ -451,8 +447,8 @@ export class FileManager { // Rename a file async renameFile(fileId: string, newName: string): Promise { - const fileData = this.sandboxFiles.fileData.find((f) => f.id === fileId) - const file = this.sandboxFiles.files.find((f) => f.id === fileId) + const fileData = this.fileData.find((f) => f.id === fileId) + const file = this.files.find((f) => f.id === fileId) if (!fileData || !file) return const parts = fileId.split("/") @@ -468,11 +464,11 @@ export class FileManager { // Delete a file async deleteFile(fileId: string): Promise<(TFolder | TFile)[]> { - const file = this.sandboxFiles.fileData.find((f) => f.id === fileId) - if (!file) return this.sandboxFiles.files + const file = this.fileData.find((f) => f.id === fileId) + if (!file) return this.files await this.sandbox.files.remove(path.posix.join(this.dirName, fileId)) - this.sandboxFiles.fileData = this.sandboxFiles.fileData.filter( + this.fileData = this.fileData.filter( (f) => f.id !== fileId ) @@ -487,7 +483,7 @@ export class FileManager { await Promise.all( files.map(async (file) => { await this.sandbox.files.remove(path.posix.join(this.dirName, file)) - this.sandboxFiles.fileData = this.sandboxFiles.fileData.filter( + this.fileData = this.fileData.filter( (f) => f.id !== file ) await RemoteFileStorage.deleteFile(this.getRemoteFileId(file)) diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts index 809e2e8..7503b7d 100644 --- a/backend/server/src/SandboxManager.ts +++ b/backend/server/src/SandboxManager.ts @@ -3,7 +3,7 @@ import { Socket } from 'socket.io' import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" -import { FileManager, SandboxFiles } from "./FileManager" +import { FileManager } from "./FileManager" import { createFileRL, createFolderRL, @@ -13,6 +13,7 @@ import { } from "./ratelimit" import { SecureGitClient } from "./SecureGitClient" import { TerminalManager } from "./TerminalManager" +import { TFile, TFolder } from "./types" import { LockManager } from "./utils" const lockManager = new LockManager() @@ -81,12 +82,12 @@ export class SandboxManager { this.fileManager = new FileManager( this.sandboxId, this.container, - (files: SandboxFiles) => { - this.socket.emit("loaded", files.files) + (files: (TFolder | TFile)[]) => { + this.socket.emit("loaded", files) } ) this.fileManager.initialize() - this.socket.emit("loaded", this.fileManager.sandboxFiles.files) + this.socket.emit("loaded", this.fileManager.files) } } @@ -133,7 +134,7 @@ export class SandboxManager { const handleDeploy: SocketHandler = async (_: any) => { if (!this.gitClient) throw Error("No git client") if (!this.fileManager) throw Error("No file manager") - const fixedFilePaths = this.fileManager?.sandboxFiles.fileData.map((file) => ({ + const fixedFilePaths = this.fileManager?.fileData.map((file) => ({ ...file, id: file.id.split("/").slice(2).join("/"), })) From aa554fa39dd695c97d88f3cb4dcc8b415e48bedc Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 06:59:27 -0600 Subject: [PATCH 17/27] fix: use entire file paths when pushing files to Dokku --- backend/server/src/SandboxManager.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts index 7503b7d..960d3c3 100644 --- a/backend/server/src/SandboxManager.ts +++ b/backend/server/src/SandboxManager.ts @@ -134,11 +134,7 @@ export class SandboxManager { const handleDeploy: SocketHandler = async (_: any) => { if (!this.gitClient) throw Error("No git client") if (!this.fileManager) throw Error("No file manager") - const fixedFilePaths = this.fileManager?.fileData.map((file) => ({ - ...file, - id: file.id.split("/").slice(2).join("/"), - })) - await this.gitClient.pushFiles(fixedFilePaths, this.sandboxId) + await this.gitClient.pushFiles(this.fileManager?.fileData, this.sandboxId) return { success: true } } From 87a74d40d6b177aa4428dd3aa728ec77c075d919 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 07:06:07 -0600 Subject: [PATCH 18/27] refactor: simplify server error handling --- backend/server/src/index.ts | 20 ++++++++++---------- backend/server/src/utils.ts | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index d87c71b..83f06bc 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -11,18 +11,22 @@ import { SandboxManager } from "./SandboxManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware +// Log errors and send a notification to the client +export const handleErrors = (message: string, error: any, socket: any) => { + console.error(message, error); + socket.emit("error", `${message} ${error.message ?? error}`); +}; + // Handle uncaught exceptions process.on("uncaughtException", (error) => { console.error("Uncaught Exception:", error) // Do not exit the process - // You can add additional logging or recovery logic here }) // Handle unhandled promise rejections process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled Rejection at:", promise, "reason:", reason) // Do not exit the process - // You can also handle the rejected promise here if needed }) // Check if the sandbox owner is connected @@ -122,8 +126,7 @@ io.on("connection", async (socket) => { const response = await handler(options) callback?.(response); } catch (e: any) { - console.error(`Error processing event "${event}":`, e); - socket.emit("error", `Error: ${event}. ${e.message ?? e}`); + handleErrors(`Error processing event "${event}":`, e, socket); } }); }); @@ -143,19 +146,16 @@ io.on("connection", async (socket) => { ) } } catch (e: any) { - console.log("Error disconnecting:", e) - socket.emit("error", `Error: disconnecting. ${e.message ?? e}`) + handleErrors("Error disconnecting:", e, socket); } }) } catch (e: any) { - console.error(`Error initializing sandbox ${data.sandboxId}:`, e); - socket.emit("error", `Error: initialize sandbox ${data.sandboxId}. ${e.message ?? e}`); + handleErrors(`Error initializing sandbox ${data.sandboxId}:`, e, socket); } } catch (e: any) { - console.error("Error connecting:", e) - socket.emit("error", `Error: connection. ${e.message ?? e}`) + handleErrors("Error connecting:", e, socket); } }) diff --git a/backend/server/src/utils.ts b/backend/server/src/utils.ts index 5ae1377..dd33984 100644 --- a/backend/server/src/utils.ts +++ b/backend/server/src/utils.ts @@ -20,4 +20,4 @@ export class LockManager { } return await this.locks[key] } -} +} \ No newline at end of file From 0b6085c57c6ca1a0bb61e5f3b304bff6d008104d Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 07:24:53 -0600 Subject: [PATCH 19/27] refactor: create connection manager class --- backend/server/src/OwnerConnectionManager.ts | 33 ++++++++++++++++++++ backend/server/src/index.ts | 21 +++++-------- 2 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 backend/server/src/OwnerConnectionManager.ts diff --git a/backend/server/src/OwnerConnectionManager.ts b/backend/server/src/OwnerConnectionManager.ts new file mode 100644 index 0000000..4c9e9ce --- /dev/null +++ b/backend/server/src/OwnerConnectionManager.ts @@ -0,0 +1,33 @@ +class Counter { + private count: number = 0 + + increment() { + this.count++ + } + + decrement() { + this.count = Math.max(0, this.count - 1) + } + + getValue(): number { + return this.count + } +} + +// Owner Connection Management +export class OwnerConnectionManager { + private connections: Record = {} + + ownerConnected(sandboxId: string) { + this.connections[sandboxId] ??= new Counter() + this.connections[sandboxId].increment() + } + + ownerDisconnected(sandboxId: string) { + this.connections[sandboxId]?.decrement() + } + + ownerIsConnected(sandboxId: string): boolean { + return this.connections[sandboxId]?.getValue() > 0 + } +} \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 83f06bc..d02565f 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -7,6 +7,7 @@ import { Server } from "socket.io" import { AIWorker } from "./AIWorker" import { DokkuClient } from "./DokkuClient" +import { OwnerConnectionManager } from "./OwnerConnectionManager" import { SandboxManager } from "./SandboxManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware @@ -29,13 +30,8 @@ process.on("unhandledRejection", (reason, promise) => { // Do not exit the process }) -// Check if the sandbox owner is connected -function isOwnerConnected(sandboxId: string): boolean { - return (connections[sandboxId] ?? 0) > 0 -} - // Initialize containers and managers -const connections: Record = {} +const connectionManager = new OwnerConnectionManager() const sandboxManagers: Record = {} // Load environment variables @@ -103,9 +99,9 @@ io.on("connection", async (socket) => { // Handle connection based on user type (owner or not) if (data.isOwner) { - connections[data.sandboxId] = (connections[data.sandboxId] ?? 0) + 1 + connectionManager.ownerConnected(data.sandboxId) } else { - if (!isOwnerConnected(data.sandboxId)) { + if (!connectionManager.ownerIsConnected(data.sandboxId)) { socket.emit("disableAccess", "The sandbox owner is not connected.") return } @@ -123,8 +119,7 @@ io.on("connection", async (socket) => { Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { - const response = await handler(options) - callback?.(response); + callback?.(await handler(options)); } catch (e: any) { handleErrors(`Error processing event "${event}":`, e, socket); } @@ -134,12 +129,12 @@ io.on("connection", async (socket) => { socket.on("disconnect", async () => { try { if (data.isOwner) { - connections[data.sandboxId]-- + connectionManager.ownerDisconnected(data.sandboxId) } await sandboxManager.disconnect() - if (data.isOwner && connections[data.sandboxId] <= 0) { + if (data.isOwner && !connectionManager.ownerIsConnected(data.sandboxId)) { socket.broadcast.emit( "disableAccess", "The sandbox owner has disconnected." @@ -162,4 +157,4 @@ io.on("connection", async (socket) => { // Start the server httpServer.listen(port, () => { console.log(`Server running on port ${port}`) -}) +}) \ No newline at end of file From 935c3143575062fadcd57c42724e5e818359a6ba Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 07:30:35 -0600 Subject: [PATCH 20/27] chore: add comments to backend server --- backend/server/src/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index d02565f..025512a 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -44,7 +44,7 @@ app.use(cors()) const httpServer = createServer(app) const io = new Server(httpServer, { cors: { - origin: "*", + origin: "*", // Allow connections from any origin }, }) @@ -97,7 +97,7 @@ io.on("connection", async (socket) => { isOwner: boolean } - // Handle connection based on user type (owner or not) + // Disable access unless the sandbox owner is connected if (data.isOwner) { connectionManager.ownerConnected(data.sandboxId) } else { @@ -108,14 +108,17 @@ io.on("connection", async (socket) => { } try { + // Create or retrieve the sandbox manager for the given sandbox ID const sandboxManager = sandboxManagers[data.sandboxId] ?? new SandboxManager( data.sandboxId, data.userId, { aiWorker, dokkuClient, gitClient, socket } ) + // Initialize the sandbox container sandboxManager.initializeContainer() + // Register event handlers for the sandbox Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { @@ -126,6 +129,7 @@ io.on("connection", async (socket) => { }); }); + // Handle disconnection event socket.on("disconnect", async () => { try { if (data.isOwner) { From 3ad7e5d9bcf458ae2e50f68c3452b19fc5e6a207 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 07:36:43 -0600 Subject: [PATCH 21/27] refactor: improve names of server variables --- backend/server/src/SandboxManager.ts | 12 ++++++------ backend/server/src/index.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts index 960d3c3..dd86434 100644 --- a/backend/server/src/SandboxManager.ts +++ b/backend/server/src/SandboxManager.ts @@ -1,4 +1,4 @@ -import { Sandbox } from "e2b" +import { Sandbox as E2BSandbox } from "e2b" import { Socket } from 'socket.io' import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" @@ -29,17 +29,17 @@ function extractPortNumber(inputString: string): number | null { return match ? parseInt(match[1]) : null } -type SandboxManagerContext = { +type ServerContext = { aiWorker: AIWorker; dokkuClient: DokkuClient | null; gitClient: SecureGitClient | null; socket: Socket; }; -export class SandboxManager { +export class Sandbox { fileManager: FileManager | null; terminalManager: TerminalManager | null; - container: Sandbox | null; + container: E2BSandbox | null; dokkuClient: DokkuClient | null; gitClient: SecureGitClient | null; aiWorker: AIWorker; @@ -47,7 +47,7 @@ export class SandboxManager { sandboxId: string; userId: string; - constructor(sandboxId: string, userId: string, { aiWorker, dokkuClient, gitClient, socket }: SandboxManagerContext) { + constructor(sandboxId: string, userId: string, { aiWorker, dokkuClient, gitClient, socket }: ServerContext) { this.fileManager = null; this.terminalManager = null; this.container = null; @@ -66,7 +66,7 @@ export class SandboxManager { console.log(`Found existing container ${this.sandboxId}`) } else { console.log("Creating container", this.sandboxId) - this.container = await Sandbox.create({ + this.container = await E2BSandbox.create({ timeoutMs: CONTAINER_TIMEOUT, }) } diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 025512a..56447b2 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -7,8 +7,8 @@ import { Server } from "socket.io" import { AIWorker } from "./AIWorker" import { DokkuClient } from "./DokkuClient" -import { OwnerConnectionManager } from "./OwnerConnectionManager" -import { SandboxManager } from "./SandboxManager" +import { OwnerConnectionManager as ConnectionManager } from "./OwnerConnectionManager" +import { Sandbox } from "./SandboxManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware @@ -31,8 +31,8 @@ process.on("unhandledRejection", (reason, promise) => { }) // Initialize containers and managers -const connectionManager = new OwnerConnectionManager() -const sandboxManagers: Record = {} +const connections = new ConnectionManager() +const sandboxes: Record = {} // Load environment variables dotenv.config() @@ -99,9 +99,9 @@ io.on("connection", async (socket) => { // Disable access unless the sandbox owner is connected if (data.isOwner) { - connectionManager.ownerConnected(data.sandboxId) + connections.ownerConnected(data.sandboxId) } else { - if (!connectionManager.ownerIsConnected(data.sandboxId)) { + if (!connections.ownerIsConnected(data.sandboxId)) { socket.emit("disableAccess", "The sandbox owner is not connected.") return } @@ -109,7 +109,7 @@ io.on("connection", async (socket) => { try { // Create or retrieve the sandbox manager for the given sandbox ID - const sandboxManager = sandboxManagers[data.sandboxId] ?? new SandboxManager( + const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox( data.sandboxId, data.userId, { aiWorker, dokkuClient, gitClient, socket } @@ -133,12 +133,12 @@ io.on("connection", async (socket) => { socket.on("disconnect", async () => { try { if (data.isOwner) { - connectionManager.ownerDisconnected(data.sandboxId) + connections.ownerDisconnected(data.sandboxId) } await sandboxManager.disconnect() - if (data.isOwner && !connectionManager.ownerIsConnected(data.sandboxId)) { + if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { socket.broadcast.emit( "disableAccess", "The sandbox owner has disconnected." From e229dab8262afd8e5a336268ebef261b32dcf3bc Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 14:14:50 -0600 Subject: [PATCH 22/27] fix: wait until the owner is disconnected from all sockets to close terminals and file manager --- backend/server/src/SandboxManager.ts | 26 +++++++++++++++++++++++--- backend/server/src/index.ts | 20 +++++++++++--------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts index dd86434..941cafb 100644 --- a/backend/server/src/SandboxManager.ts +++ b/backend/server/src/SandboxManager.ts @@ -46,61 +46,81 @@ export class Sandbox { socket: Socket; sandboxId: string; userId: string; + isOwner: boolean; - constructor(sandboxId: string, userId: string, { aiWorker, dokkuClient, gitClient, socket }: ServerContext) { + constructor(sandboxId: string, userId: string, isOwner: boolean, { aiWorker, dokkuClient, gitClient, socket }: ServerContext) { this.fileManager = null; this.terminalManager = null; this.container = null; this.sandboxId = sandboxId; this.userId = userId; + this.isOwner = isOwner; this.aiWorker = aiWorker; this.dokkuClient = dokkuClient; this.gitClient = gitClient; this.socket = socket; } + // Initializes the container for the sandbox environment async initializeContainer() { - + // Acquire a lock to ensure exclusive access to the sandbox environment await lockManager.acquireLock(this.sandboxId, async () => { + // Check if a container already exists and is running if (this.container && await this.container.isRunning()) { console.log(`Found existing container ${this.sandboxId}`) } else { console.log("Creating container", this.sandboxId) + // Create a new container with a specified timeout this.container = await E2BSandbox.create({ timeoutMs: CONTAINER_TIMEOUT, }) } }) + // Ensure a container was successfully created if (!this.container) throw new Error("Failed to create container") + // Initialize the terminal manager if it hasn't been set up yet if (!this.terminalManager) { this.terminalManager = new TerminalManager(this.container) console.log(`Terminal manager set up for ${this.sandboxId}`) } + // Initialize the file manager if it hasn't been set up yet if (!this.fileManager) { this.fileManager = new FileManager( this.sandboxId, this.container, (files: (TFolder | TFile)[]) => { + // Emit an event to the socket when files are loaded this.socket.emit("loaded", files) } ) + // Initialize the file manager and emit the initial files this.fileManager.initialize() this.socket.emit("loaded", this.fileManager.files) } } + // Called when the client disconnects from the Sandbox async disconnect() { + // Close all terminals managed by the terminal manager await this.terminalManager?.closeAllTerminals() + // This way the terminal manager will be set up again if we reconnect + this.terminalManager = null; + // Close all file watchers managed by the file manager await this.fileManager?.closeWatchers() + // This way the file manager will be set up again if we reconnect + this.fileManager = null; } handlers() { // Handle heartbeat from a socket connection const handleHeartbeat: SocketHandler = (_: any) => { - this.container?.setTimeout(CONTAINER_TIMEOUT) + // Only keep the sandbox alive if the owner is still connected + if (this.isOwner) { + this.container?.setTimeout(CONTAINER_TIMEOUT) + } } // Handle getting a file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 56447b2..546e3a4 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -112,10 +112,12 @@ io.on("connection", async (socket) => { const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox( data.sandboxId, data.userId, + data.isOwner, { aiWorker, dokkuClient, gitClient, socket } ) // Initialize the sandbox container + // The file manager and terminal managers will be set up if they have been closed sandboxManager.initializeContainer() // Register event handlers for the sandbox @@ -134,15 +136,15 @@ io.on("connection", async (socket) => { try { if (data.isOwner) { connections.ownerDisconnected(data.sandboxId) - } - - await sandboxManager.disconnect() - - if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { - socket.broadcast.emit( - "disableAccess", - "The sandbox owner has disconnected." - ) + // If the owner has disconnected from all sockets, close open terminals and file watchers.o + // The sandbox itself will timeout after the heartbeat stops. + if (!connections.ownerIsConnected(data.sandboxId)) { + await sandboxManager.disconnect() + socket.broadcast.emit( + "disableAccess", + "The sandbox owner has disconnected." + ) + } } } catch (e: any) { handleErrors("Error disconnecting:", e, socket); From a87a4b5160406de7288d53b4291390834a26a6aa Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 19:02:18 -0600 Subject: [PATCH 23/27] fix: call event handlers when there is no callback --- backend/server/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 546e3a4..f3af14a 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -124,7 +124,8 @@ io.on("connection", async (socket) => { Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { - callback?.(await handler(options)); + const result = await handler(options) + callback?.(result); } catch (e: any) { handleErrors(`Error processing event "${event}":`, e, socket); } From 7ace8f569ac7688b8d29f0f35fd5c15d6bbc3be0 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 19:03:13 -0600 Subject: [PATCH 24/27] fix: forward filesystem change notifications to all relevant connections --- backend/server/src/ConnectionManager.ts | 50 +++++++++++++++++++ backend/server/src/FileManager.ts | 8 +-- backend/server/src/OwnerConnectionManager.ts | 33 ------------- backend/server/src/SandboxManager.ts | 51 +++++++++----------- backend/server/src/index.ts | 32 +++++++++--- 5 files changed, 102 insertions(+), 72 deletions(-) create mode 100644 backend/server/src/ConnectionManager.ts delete mode 100644 backend/server/src/OwnerConnectionManager.ts diff --git a/backend/server/src/ConnectionManager.ts b/backend/server/src/ConnectionManager.ts new file mode 100644 index 0000000..51683d4 --- /dev/null +++ b/backend/server/src/ConnectionManager.ts @@ -0,0 +1,50 @@ +import { Socket } from "socket.io" + +class Counter { + private count: number = 0 + + increment() { + this.count++ + } + + decrement() { + this.count = Math.max(0, this.count - 1) + } + + getValue(): number { + return this.count + } +} + +// Owner Connection Management +export class ConnectionManager { + private ownerConnections: Record = {} + private sockets: Record> = {} + + ownerConnected(sandboxId: string) { + this.ownerConnections[sandboxId] ??= new Counter() + this.ownerConnections[sandboxId].increment() + } + + ownerDisconnected(sandboxId: string) { + this.ownerConnections[sandboxId]?.decrement() + } + + ownerIsConnected(sandboxId: string): boolean { + return this.ownerConnections[sandboxId]?.getValue() > 0 + } + + addConnectionForSandbox(socket: Socket, sandboxId: string) { + this.sockets[sandboxId] ??= new Set() + this.sockets[sandboxId].add(socket) + } + + removeConnectionForSandbox(socket: Socket, sandboxId: string) { + this.sockets[sandboxId]?.delete(socket) + } + + connectionsForSandbox(sandboxId: string): Set { + return this.sockets[sandboxId] ?? new Set(); + } + +} \ No newline at end of file diff --git a/backend/server/src/FileManager.ts b/backend/server/src/FileManager.ts index 43fb9e3..2f58fbf 100644 --- a/backend/server/src/FileManager.ts +++ b/backend/server/src/FileManager.ts @@ -50,13 +50,13 @@ export class FileManager { public fileData: TFileData[] private fileWatchers: WatchHandle[] = [] private dirName = "/home/user/project" - private refreshFileList: (files: (TFolder | TFile)[]) => void + private refreshFileList: ((files: (TFolder | TFile)[]) => void) | null // Constructor to initialize the FileManager constructor( sandboxId: string, sandbox: Sandbox, - refreshFileList: (files: (TFolder | TFile)[]) => void + refreshFileList: ((files: (TFolder | TFile)[]) => void) | null ) { this.sandboxId = sandboxId this.sandbox = sandbox @@ -314,7 +314,9 @@ export class FileManager { } // Tell the client to reload the file list - this.refreshFileList(this.files) + if (event.type !== "chmod") { + this.refreshFileList?.(this.files) + } } catch (error) { console.error( `Error handling ${event.type} event for ${event.name}:`, diff --git a/backend/server/src/OwnerConnectionManager.ts b/backend/server/src/OwnerConnectionManager.ts deleted file mode 100644 index 4c9e9ce..0000000 --- a/backend/server/src/OwnerConnectionManager.ts +++ /dev/null @@ -1,33 +0,0 @@ -class Counter { - private count: number = 0 - - increment() { - this.count++ - } - - decrement() { - this.count = Math.max(0, this.count - 1) - } - - getValue(): number { - return this.count - } -} - -// Owner Connection Management -export class OwnerConnectionManager { - private connections: Record = {} - - ownerConnected(sandboxId: string) { - this.connections[sandboxId] ??= new Counter() - this.connections[sandboxId].increment() - } - - ownerDisconnected(sandboxId: string) { - this.connections[sandboxId]?.decrement() - } - - ownerIsConnected(sandboxId: string): boolean { - return this.connections[sandboxId]?.getValue() > 0 - } -} \ No newline at end of file diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/SandboxManager.ts index 941cafb..fda2237 100644 --- a/backend/server/src/SandboxManager.ts +++ b/backend/server/src/SandboxManager.ts @@ -1,5 +1,5 @@ import { Sandbox as E2BSandbox } from "e2b" -import { Socket } from 'socket.io' +import { Socket } from "socket.io" import { AIWorker } from "./AIWorker" import { CONTAINER_TIMEOUT } from "./constants" import { DokkuClient } from "./DokkuClient" @@ -33,36 +33,35 @@ type ServerContext = { aiWorker: AIWorker; dokkuClient: DokkuClient | null; gitClient: SecureGitClient | null; - socket: Socket; }; export class Sandbox { + // Sandbox properties: + sandboxId: string; fileManager: FileManager | null; terminalManager: TerminalManager | null; container: E2BSandbox | null; + // Server context: dokkuClient: DokkuClient | null; gitClient: SecureGitClient | null; aiWorker: AIWorker; - socket: Socket; - sandboxId: string; - userId: string; - isOwner: boolean; - constructor(sandboxId: string, userId: string, isOwner: boolean, { aiWorker, dokkuClient, gitClient, socket }: ServerContext) { + constructor(sandboxId: string, { aiWorker, dokkuClient, gitClient }: ServerContext) { + // Sandbox properties: + this.sandboxId = sandboxId; this.fileManager = null; this.terminalManager = null; this.container = null; - this.sandboxId = sandboxId; - this.userId = userId; - this.isOwner = isOwner; + // Server context: this.aiWorker = aiWorker; this.dokkuClient = dokkuClient; this.gitClient = gitClient; - this.socket = socket; } // Initializes the container for the sandbox environment - async initializeContainer() { + async initialize( + fileWatchCallback: ((files: (TFolder | TFile)[]) => void) | undefined + ) { // Acquire a lock to ensure exclusive access to the sandbox environment await lockManager.acquireLock(this.sandboxId, async () => { // Check if a container already exists and is running @@ -90,14 +89,10 @@ export class Sandbox { this.fileManager = new FileManager( this.sandboxId, this.container, - (files: (TFolder | TFile)[]) => { - // Emit an event to the socket when files are loaded - this.socket.emit("loaded", files) - } + fileWatchCallback ?? null ) // Initialize the file manager and emit the initial files - this.fileManager.initialize() - this.socket.emit("loaded", this.fileManager.files) + await this.fileManager.initialize() } } @@ -113,12 +108,12 @@ export class Sandbox { this.fileManager = null; } - handlers() { + handlers(connection: { userId: string, isOwner: boolean, socket: Socket }) { // Handle heartbeat from a socket connection const handleHeartbeat: SocketHandler = (_: any) => { // Only keep the sandbox alive if the owner is still connected - if (this.isOwner) { + if (connection.isOwner) { this.container?.setTimeout(CONTAINER_TIMEOUT) } } @@ -135,7 +130,7 @@ export class Sandbox { // Handle saving a file const handleSaveFile: SocketHandler = async ({ fileId, body }: any) => { - await saveFileRL.consume(this.userId, 1); + await saveFileRL.consume(connection.userId, 1); return this.fileManager?.saveFile(fileId, body) } @@ -160,25 +155,25 @@ export class Sandbox { // Handle creating a file const handleCreateFile: SocketHandler = async ({ name }: any) => { - await createFileRL.consume(this.userId, 1); + await createFileRL.consume(connection.userId, 1); return { "success": await this.fileManager?.createFile(name) } } // Handle creating a folder const handleCreateFolder: SocketHandler = async ({ name }: any) => { - await createFolderRL.consume(this.userId, 1); + await createFolderRL.consume(connection.userId, 1); return { "success": await this.fileManager?.createFolder(name) } } // Handle renaming a file const handleRenameFile: SocketHandler = async ({ fileId, newName }: any) => { - await renameFileRL.consume(this.userId, 1) + await renameFileRL.consume(connection.userId, 1) return this.fileManager?.renameFile(fileId, newName) } // Handle deleting a file const handleDeleteFile: SocketHandler = async ({ fileId }: any) => { - await deleteFileRL.consume(this.userId, 1) + await deleteFileRL.consume(connection.userId, 1) return this.fileManager?.deleteFile(fileId) } @@ -191,10 +186,10 @@ export class Sandbox { const handleCreateTerminal: SocketHandler = async ({ id }: any) => { await lockManager.acquireLock(this.sandboxId, async () => { await this.terminalManager?.createTerminal(id, (responseString: string) => { - this.socket.emit("terminalResponse", { id, data: responseString }) + connection.socket.emit("terminalResponse", { id, data: responseString }) const port = extractPortNumber(responseString) if (port) { - this.socket.emit( + connection.socket.emit( "previewURL", "https://" + this.container?.getHost(port) ) @@ -220,7 +215,7 @@ export class Sandbox { // Handle generating code const handleGenerateCode: SocketHandler = ({ fileName, code, line, instructions }: any) => { - return this.aiWorker.generateCode(this.userId, fileName, code, line, instructions) + return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions) } return { diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index f3af14a..08ede15 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -3,17 +3,18 @@ import dotenv from "dotenv" import express, { Express } from "express" import fs from "fs" import { createServer } from "http" -import { Server } from "socket.io" +import { Server, Socket } from "socket.io" import { AIWorker } from "./AIWorker" +import { ConnectionManager } from "./ConnectionManager" import { DokkuClient } from "./DokkuClient" -import { OwnerConnectionManager as ConnectionManager } from "./OwnerConnectionManager" import { Sandbox } from "./SandboxManager" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware +import { TFile, TFolder } from "./types" // Log errors and send a notification to the client -export const handleErrors = (message: string, error: any, socket: any) => { +export const handleErrors = (message: string, error: any, socket: Socket) => { console.error(message, error); socket.emit("error", `${message} ${error.message ?? error}`); }; @@ -106,22 +107,35 @@ io.on("connection", async (socket) => { return } } + connections.addConnectionForSandbox(socket, data.sandboxId) try { // Create or retrieve the sandbox manager for the given sandbox ID const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox( data.sandboxId, - data.userId, - data.isOwner, - { aiWorker, dokkuClient, gitClient, socket } + { + aiWorker, dokkuClient, gitClient, + } ) + sandboxes[data.sandboxId] = sandboxManager + + const sendFileNotifications = (files: (TFolder | TFile)[]) => { + connections.connectionsForSandbox(data.sandboxId).forEach((socket: Socket) => { + socket.emit("loaded", files); + }); + }; // Initialize the sandbox container // The file manager and terminal managers will be set up if they have been closed - sandboxManager.initializeContainer() + await sandboxManager.initialize(sendFileNotifications) + socket.emit("loaded", sandboxManager.fileManager?.files) // Register event handlers for the sandbox - Object.entries(sandboxManager.handlers()).forEach(([event, handler]) => { + Object.entries(sandboxManager.handlers({ + userId: data.userId, + isOwner: data.isOwner, + socket + })).forEach(([event, handler]) => { socket.on(event, async (options: any, callback?: (response: any) => void) => { try { const result = await handler(options) @@ -135,6 +149,8 @@ io.on("connection", async (socket) => { // Handle disconnection event socket.on("disconnect", async () => { try { + connections.removeConnectionForSandbox(socket, data.sandboxId) + if (data.isOwner) { connections.ownerDisconnected(data.sandboxId) // If the owner has disconnected from all sockets, close open terminals and file watchers.o From 224d1904682e86eeb667d992817fb40e6b79a68e Mon Sep 17 00:00:00 2001 From: James Murdza Date: Fri, 25 Oct 2024 19:23:11 -0600 Subject: [PATCH 25/27] refactor: improve readability of connection manager code --- backend/server/src/ConnectionManager.ts | 30 ++++++++++++------- backend/server/src/index.ts | 39 ++++++++++++------------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/backend/server/src/ConnectionManager.ts b/backend/server/src/ConnectionManager.ts index 51683d4..45b5432 100644 --- a/backend/server/src/ConnectionManager.ts +++ b/backend/server/src/ConnectionManager.ts @@ -18,31 +18,39 @@ class Counter { // Owner Connection Management export class ConnectionManager { + // Counts how many times the owner is connected to a sandbox private ownerConnections: Record = {} + // Stores all sockets connected to a given sandbox private sockets: Record> = {} - ownerConnected(sandboxId: string) { - this.ownerConnections[sandboxId] ??= new Counter() - this.ownerConnections[sandboxId].increment() - } - - ownerDisconnected(sandboxId: string) { - this.ownerConnections[sandboxId]?.decrement() - } - + // Checks if the owner of a sandbox is connected ownerIsConnected(sandboxId: string): boolean { return this.ownerConnections[sandboxId]?.getValue() > 0 } - addConnectionForSandbox(socket: Socket, sandboxId: string) { + // Adds a connection for a sandbox + addConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) { this.sockets[sandboxId] ??= new Set() this.sockets[sandboxId].add(socket) + + // If the connection is for the owner, increments the owner connection counter + if (isOwner) { + this.ownerConnections[sandboxId] ??= new Counter() + this.ownerConnections[sandboxId].increment() + } } - removeConnectionForSandbox(socket: Socket, sandboxId: string) { + // Removes a connection for a sandbox + removeConnectionForSandbox(socket: Socket, sandboxId: string, isOwner: boolean) { this.sockets[sandboxId]?.delete(socket) + + // If the connection being removed is for the owner, decrements the owner connection counter + if (isOwner) { + this.ownerConnections[sandboxId]?.decrement() + } } + // Returns the set of sockets connected to a given sandbox connectionsForSandbox(sandboxId: string): Set { return this.sockets[sandboxId] ?? new Set(); } diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 08ede15..ac84fc5 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -98,16 +98,14 @@ io.on("connection", async (socket) => { isOwner: boolean } + // Register the connection + connections.addConnectionForSandbox(socket, data.sandboxId, data.isOwner) + // Disable access unless the sandbox owner is connected - if (data.isOwner) { - connections.ownerConnected(data.sandboxId) - } else { - if (!connections.ownerIsConnected(data.sandboxId)) { - socket.emit("disableAccess", "The sandbox owner is not connected.") - return - } + if (!data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { + socket.emit("disableAccess", "The sandbox owner is not connected.") + return } - connections.addConnectionForSandbox(socket, data.sandboxId) try { // Create or retrieve the sandbox manager for the given sandbox ID @@ -119,6 +117,7 @@ io.on("connection", async (socket) => { ) sandboxes[data.sandboxId] = sandboxManager + // This callback recieves an update when the file list changes, and notifies all relevant connections. const sendFileNotifications = (files: (TFolder | TFile)[]) => { connections.connectionsForSandbox(data.sandboxId).forEach((socket: Socket) => { socket.emit("loaded", files); @@ -131,6 +130,8 @@ io.on("connection", async (socket) => { socket.emit("loaded", sandboxManager.fileManager?.files) // Register event handlers for the sandbox + // For each event handler, listen on the socket for that event + // Pass connection-specific information to the handlers Object.entries(sandboxManager.handlers({ userId: data.userId, isOwner: data.isOwner, @@ -149,19 +150,17 @@ io.on("connection", async (socket) => { // Handle disconnection event socket.on("disconnect", async () => { try { - connections.removeConnectionForSandbox(socket, data.sandboxId) + // Deregister the connection + connections.removeConnectionForSandbox(socket, data.sandboxId, data.isOwner) - if (data.isOwner) { - connections.ownerDisconnected(data.sandboxId) - // If the owner has disconnected from all sockets, close open terminals and file watchers.o - // The sandbox itself will timeout after the heartbeat stops. - if (!connections.ownerIsConnected(data.sandboxId)) { - await sandboxManager.disconnect() - socket.broadcast.emit( - "disableAccess", - "The sandbox owner has disconnected." - ) - } + // If the owner has disconnected from all sockets, close open terminals and file watchers.o + // The sandbox itself will timeout after the heartbeat stops. + if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { + await sandboxManager.disconnect() + socket.broadcast.emit( + "disableAccess", + "The sandbox owner has disconnected." + ) } } catch (e: any) { handleErrors("Error disconnecting:", e, socket); From 8b890fdffeb37f61e8b23669e02efe320090347c Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Tue, 24 Sep 2024 13:00:49 +0100 Subject: [PATCH 26/27] fix: remove editor red squiggly lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit by dynamically loading project'sĀ tsconfigĀ file and adding nice defaults # Conflicts: # frontend/components/editor/index.tsx # frontend/lib/utils.ts --- frontend/components/editor/index.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 5435b8d..b426f9c 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -1228,4 +1228,17 @@ export default function CodeEditor({ ) } +/** + * Configure the typescript compiler to detect JSX and load type definitions + */ +const defaultCompilerOptions: monaco.languages.typescript.CompilerOptions = { + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true, + resolveJsonModule: true, + jsx: monaco.languages.typescript.JsxEmit.ReactJSX, + module: monaco.languages.typescript.ModuleKind.ESNext, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + target: monaco.languages.typescript.ScriptTarget.ESNext, +} From 0809eaca4e448967be581c8075bf0b952e400b95 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sat, 26 Oct 2024 06:41:17 -0600 Subject: [PATCH 27/27] refactor: rename SandboxManager to Sandbox --- .../server/src/{SandboxManager.ts => Sandbox.ts} | 0 backend/server/src/index.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) rename backend/server/src/{SandboxManager.ts => Sandbox.ts} (100%) diff --git a/backend/server/src/SandboxManager.ts b/backend/server/src/Sandbox.ts similarity index 100% rename from backend/server/src/SandboxManager.ts rename to backend/server/src/Sandbox.ts diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index ac84fc5..cf95824 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -8,7 +8,7 @@ import { AIWorker } from "./AIWorker" import { ConnectionManager } from "./ConnectionManager" import { DokkuClient } from "./DokkuClient" -import { Sandbox } from "./SandboxManager" +import { Sandbox } from "./Sandbox" import { SecureGitClient } from "./SecureGitClient" import { socketAuth } from "./socketAuth"; // Import the new socketAuth middleware import { TFile, TFolder } from "./types" @@ -109,13 +109,13 @@ io.on("connection", async (socket) => { try { // Create or retrieve the sandbox manager for the given sandbox ID - const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox( + const sandbox = sandboxes[data.sandboxId] ?? new Sandbox( data.sandboxId, { aiWorker, dokkuClient, gitClient, } ) - sandboxes[data.sandboxId] = sandboxManager + sandboxes[data.sandboxId] = sandbox // This callback recieves an update when the file list changes, and notifies all relevant connections. const sendFileNotifications = (files: (TFolder | TFile)[]) => { @@ -126,13 +126,13 @@ io.on("connection", async (socket) => { // Initialize the sandbox container // The file manager and terminal managers will be set up if they have been closed - await sandboxManager.initialize(sendFileNotifications) - socket.emit("loaded", sandboxManager.fileManager?.files) + await sandbox.initialize(sendFileNotifications) + socket.emit("loaded", sandbox.fileManager?.files) // Register event handlers for the sandbox // For each event handler, listen on the socket for that event // Pass connection-specific information to the handlers - Object.entries(sandboxManager.handlers({ + Object.entries(sandbox.handlers({ userId: data.userId, isOwner: data.isOwner, socket @@ -156,7 +156,7 @@ io.on("connection", async (socket) => { // If the owner has disconnected from all sockets, close open terminals and file watchers.o // The sandbox itself will timeout after the heartbeat stops. if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { - await sandboxManager.disconnect() + await sandbox.disconnect() socket.broadcast.emit( "disableAccess", "The sandbox owner has disconnected."