add worker service binding + inactivity detection
This commit is contained in:
parent
c5762d430c
commit
84c49f0d9d
@ -9,7 +9,7 @@ import { and, eq, sql } from "drizzle-orm";
|
|||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
DB: D1Database;
|
DB: D1Database;
|
||||||
RL: any;
|
STORAGE: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
|
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
|
||||||
@ -86,11 +86,15 @@ export default {
|
|||||||
|
|
||||||
const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get();
|
const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get();
|
||||||
|
|
||||||
await fetch("https://storage.ishaan1013.workers.dev/api/init", {
|
const initStorageRequest = new Request("https://storage.ishaan1013.workers.dev/api/init", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ sandboxId: sb.id, type }),
|
body: JSON.stringify({ sandboxId: sb.id, type }),
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
|
const initStorageRes = await env.STORAGE.fetch(initStorageRequest);
|
||||||
|
|
||||||
|
const initStorage = await initStorageRes.text();
|
||||||
|
console.log("initStorage: ", initStorage);
|
||||||
|
|
||||||
return new Response(sb.id, { status: 200 });
|
return new Response(sb.id, { status: 200 });
|
||||||
} else {
|
} else {
|
||||||
|
@ -36,14 +36,12 @@ const readAndParseKubeYaml = (
|
|||||||
const regex = new RegExp(`<SANDBOX>`, "g")
|
const regex = new RegExp(`<SANDBOX>`, "g")
|
||||||
docString = docString.replace(regex, sandboxId)
|
docString = docString.replace(regex, sandboxId)
|
||||||
|
|
||||||
// replace <CF_API_TOKEN> with process.env.CF_API_TOKEN
|
|
||||||
if (!process.env.CF_API_TOKEN) {
|
if (!process.env.CF_API_TOKEN) {
|
||||||
throw new Error("CF_API_TOKEN is not defined")
|
throw new Error("CF_API_TOKEN is not defined")
|
||||||
}
|
}
|
||||||
const regexEnv1 = new RegExp(`<CF_API_TOKEN>`, "g")
|
const regexEnv1 = new RegExp(`<CF_API_TOKEN>`, "g")
|
||||||
docString = docString.replace(regexEnv1, process.env.CF_API_TOKEN)
|
docString = docString.replace(regexEnv1, process.env.CF_API_TOKEN)
|
||||||
|
|
||||||
// replace <CF_USER_ID> with process.env.CF_USER_ID
|
|
||||||
if (!process.env.CF_USER_ID) {
|
if (!process.env.CF_USER_ID) {
|
||||||
throw new Error("CF_USER_ID is not defined")
|
throw new Error("CF_USER_ID is not defined")
|
||||||
}
|
}
|
||||||
@ -55,12 +53,13 @@ const readAndParseKubeYaml = (
|
|||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post("/start", async (req, res) => {
|
const dataSchema = z.object({
|
||||||
const initSchema = z.object({
|
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
sandboxId: z.string(),
|
sandboxId: z.string(),
|
||||||
})
|
})
|
||||||
const { userId, sandboxId } = initSchema.parse(req.body)
|
|
||||||
|
app.post("/start", async (req, res) => {
|
||||||
|
const { userId, sandboxId } = dataSchema.parse(req.body)
|
||||||
const namespace = "default"
|
const namespace = "default"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -83,6 +82,39 @@ app.post("/start", async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post("/stop", async (req, res) => {
|
||||||
|
const { userId, sandboxId } = dataSchema.parse(req.body)
|
||||||
|
const namespace = "default"
|
||||||
|
|
||||||
|
try {
|
||||||
|
const kubeManifests = readAndParseKubeYaml(
|
||||||
|
path.join(__dirname, "../service.yaml"),
|
||||||
|
sandboxId
|
||||||
|
)
|
||||||
|
kubeManifests.forEach(async (manifest) => {
|
||||||
|
if (manifest.kind === "Deployment")
|
||||||
|
await appsV1Api.deleteNamespacedDeployment(
|
||||||
|
manifest.metadata?.name || "",
|
||||||
|
namespace
|
||||||
|
)
|
||||||
|
else if (manifest.kind === "Service")
|
||||||
|
await coreV1Api.deleteNamespacedService(
|
||||||
|
manifest.metadata?.name || "",
|
||||||
|
namespace
|
||||||
|
)
|
||||||
|
else if (manifest.kind === "Ingress")
|
||||||
|
await networkingV1Api.deleteNamespacedIngress(
|
||||||
|
manifest.metadata?.name || "",
|
||||||
|
namespace
|
||||||
|
)
|
||||||
|
})
|
||||||
|
res.status(200).send({ message: "Resources deleted." })
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Failed to delete resources", error)
|
||||||
|
res.status(500).send({ message: "Failed to delete resources." })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Listening on port: ${port}`)
|
console.log(`Listening on port: ${port}`)
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
ARG cf_api_token
|
ARG CF_API_TOKEN
|
||||||
ARG cf_user_id
|
ARG CF_USER_ID
|
||||||
|
|
||||||
FROM node:20
|
FROM node:20
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ RUN npm run build
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV cf_api_token=$cf_api_token
|
ENV CF_API_TOKEN=$CF_API_TOKEN
|
||||||
ENV cf_user_id=$cf_user_id
|
ENV CF_USER_ID=$CF_USER_ID
|
||||||
|
|
||||||
CMD [ "node", "dist/index.js" ]
|
CMD [ "node", "dist/index.js" ]
|
10
backend/server/src/inactivity.ts
Normal file
10
backend/server/src/inactivity.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Server } from "socket.io";
|
||||||
|
import { DefaultEventsMap } from "socket.io/dist/typed-events";
|
||||||
|
|
||||||
|
export function checkForInactivity(io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>) {
|
||||||
|
io.fetchSockets().then(sockets => {
|
||||||
|
if (sockets.length === 0) {
|
||||||
|
console.log("No users have been connected for 15 seconds.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -38,6 +38,8 @@ const io = new Server(httpServer, {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let inactivityTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
const terminals: {
|
const terminals: {
|
||||||
[id: string]: { terminal: IPty; onData: IDisposable; onExit: IDisposable }
|
[id: string]: { terminal: IPty; onData: IDisposable; onExit: IDisposable }
|
||||||
} = {}
|
} = {}
|
||||||
@ -85,15 +87,20 @@ io.use(async (socket, next) => {
|
|||||||
socket.data = {
|
socket.data = {
|
||||||
userId,
|
userId,
|
||||||
sandboxId: sandboxId,
|
sandboxId: sandboxId,
|
||||||
|
isOwner: sandbox !== undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
io.on("connection", async (socket) => {
|
io.on("connection", async (socket) => {
|
||||||
|
|
||||||
|
if (inactivityTimeout) clearTimeout(inactivityTimeout);
|
||||||
|
|
||||||
const data = socket.data as {
|
const data = socket.data as {
|
||||||
userId: string
|
userId: string
|
||||||
sandboxId: string
|
sandboxId: string
|
||||||
|
isOwner: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const sandboxFiles = await getSandboxFiles(data.sandboxId)
|
const sandboxFiles = await getSandboxFiles(data.sandboxId)
|
||||||
@ -298,7 +305,8 @@ io.on("connection", async (socket) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", async () => {
|
||||||
|
if (data.isOwner) {
|
||||||
Object.entries(terminals).forEach((t) => {
|
Object.entries(terminals).forEach((t) => {
|
||||||
const { terminal, onData, onExit } = t[1]
|
const { terminal, onData, onExit } = t[1]
|
||||||
if (os.platform() !== "win32") terminal.kill()
|
if (os.platform() !== "win32") terminal.kill()
|
||||||
@ -306,6 +314,28 @@ io.on("connection", async (socket) => {
|
|||||||
onExit.dispose()
|
onExit.dispose()
|
||||||
delete terminals[t[0]]
|
delete terminals[t[0]]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("The owner disconnected.")
|
||||||
|
socket.broadcast.emit("ownerDisconnected")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("A shared user disconnected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sockets = await io.fetchSockets()
|
||||||
|
if (inactivityTimeout) {
|
||||||
|
clearTimeout(inactivityTimeout)
|
||||||
|
};
|
||||||
|
if (sockets.length === 0) {
|
||||||
|
inactivityTimeout = setTimeout(() => {
|
||||||
|
io.fetchSockets().then(sockets => {
|
||||||
|
if (sockets.length === 0) {
|
||||||
|
console.log("No users have been connected for 15 seconds.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -60,7 +60,8 @@ const data: {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(1).max(16),
|
name: z.string().min(1).max(16)
|
||||||
|
.refine((value) => /^[a-zA-Z0-9_]+$/.test(value), "Name must be alphanumeric and can contain underscores"),
|
||||||
visibility: z.enum(["public", "private"]),
|
visibility: z.enum(["public", "private"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export default function UserButton({ userData }: { userData: User }) {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<div className="w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium">
|
<div className="w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium">
|
||||||
{userData.name
|
{userData.name && userData.name
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map((name) => name[0].toUpperCase())}
|
.map((name) => name[0].toUpperCase())}
|
||||||
|
@ -8,6 +8,7 @@ export async function createSandbox(body: {
|
|||||||
userId: string
|
userId: string
|
||||||
visibility: string
|
visibility: string
|
||||||
}) {
|
}) {
|
||||||
|
console.log("creating. body:", body)
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
"https://database.ishaan1013.workers.dev/api/sandbox",
|
"https://database.ishaan1013.workers.dev/api/sandbox",
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user