Skip to content

Commit 798f5c3

Browse files
committed
refactor: pause sandbox via SDK in tRPC mutation instead of REST action
1 parent b2bcee9 commit 798f5c3

3 files changed

Lines changed: 63 additions & 73 deletions

File tree

src/core/server/actions/sandbox-actions.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -59,59 +59,3 @@ export const killSandboxAction = authActionClient
5959
return returnServerError('Failed to kill sandbox')
6060
}
6161
})
62-
63-
const PauseSandboxSchema = z.object({
64-
teamSlug: TeamSlugSchema,
65-
sandboxId: z.string().min(1, 'Sandbox ID is required'),
66-
})
67-
68-
export const pauseSandboxAction = authActionClient
69-
.schema(PauseSandboxSchema)
70-
.metadata({ actionName: 'pauseSandbox' })
71-
.use(withTeamSlugResolution)
72-
.action(async ({ parsedInput, ctx }) => {
73-
const { sandboxId } = parsedInput
74-
const { session, teamId } = ctx
75-
76-
const res = await infra.POST('/sandboxes/{sandboxID}/pause', {
77-
headers: {
78-
...authHeaders(session.access_token, teamId),
79-
},
80-
params: {
81-
path: {
82-
sandboxID: sandboxId,
83-
},
84-
},
85-
body: {
86-
memory: true,
87-
},
88-
})
89-
90-
if (res.error) {
91-
const status = res.response.status
92-
93-
l.error(
94-
{
95-
key: 'pause_sandbox_action:infra_error',
96-
error: res.error,
97-
user_id: session.user.id,
98-
team_id: teamId,
99-
sandbox_id: sandboxId,
100-
context: {
101-
status,
102-
},
103-
},
104-
`Failed to pause sandbox: ${res.error.message}`
105-
)
106-
107-
if (status === 404) {
108-
return returnServerError('Sandbox not found')
109-
}
110-
111-
if (status === 409) {
112-
return returnServerError('Sandbox is not running')
113-
}
114-
115-
return returnServerError('Failed to pause sandbox')
116-
}
117-
})

src/core/server/api/routers/sandbox.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,51 @@ export const sandboxRouter = createTRPCRouter({
366366
}
367367
}),
368368

369+
// Explicit, user-triggered pause of a running sandbox. Uses the SDK's
370+
// control-plane pause (which snapshots and pauses) server-side so the
371+
// account token never reaches the browser.
372+
pause: protectedTeamProcedure
373+
.input(
374+
z.object({
375+
sandboxId: SandboxIdSchema,
376+
requestTimeoutMs: z.number().int().positive().optional(),
377+
})
378+
)
379+
.mutation(async ({ ctx, input }) => {
380+
const { sandboxId, requestTimeoutMs } = input
381+
const { session, teamId } = ctx
382+
383+
const connectionOpts = {
384+
apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
385+
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
386+
sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
387+
headers: authHeaders(session.access_token, teamId),
388+
}
389+
390+
try {
391+
// Returns false when the sandbox was already paused, which we treat
392+
// as success since the desired end state is reached.
393+
await Sandbox.pause(sandboxId, {
394+
...connectionOpts,
395+
...(requestTimeoutMs ? { requestTimeoutMs } : {}),
396+
})
397+
} catch (error) {
398+
if (error instanceof TimeoutError) {
399+
throw new TRPCError({
400+
code: 'TIMEOUT',
401+
message: 'Timed out pausing sandbox',
402+
cause: error,
403+
})
404+
}
405+
406+
throw new TRPCError({
407+
code: 'INTERNAL_SERVER_ERROR',
408+
message: 'Failed to pause sandbox',
409+
cause: error,
410+
})
411+
}
412+
}),
413+
369414
killTerminalPty: protectedTeamProcedure
370415
.input(
371416
z.object({

src/features/dashboard/sandbox/header/pause-button.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use client'
22

3-
import { useAction } from 'next-safe-action/hooks'
3+
import { useMutation } from '@tanstack/react-query'
44
import { useState } from 'react'
55
import { toast } from 'sonner'
6-
import { pauseSandboxAction } from '@/core/server/actions/sandbox-actions'
6+
import { useTRPC } from '@/trpc/client'
77
import { AlertPopover } from '@/ui/alert-popover'
88
import { Button } from '@/ui/primitives/button'
99
import { PausedIcon } from '@/ui/primitives/icons'
@@ -18,26 +18,27 @@ export default function PauseButton({ className }: PauseButtonProps) {
1818
const [open, setOpen] = useState(false)
1919
const { sandboxInfo, refetchSandboxInfo } = useSandboxContext()
2020
const { team } = useDashboard()
21+
const trpc = useTRPC()
2122

2223
const canPause = sandboxInfo?.state === 'running'
2324

24-
const { execute, isExecuting } = useAction(pauseSandboxAction, {
25-
onSuccess: async () => {
26-
toast.success('Sandbox paused successfully')
27-
setOpen(false)
28-
refetchSandboxInfo()
29-
},
30-
onError: ({ error }) => {
31-
toast.error(
32-
error.serverError || 'Failed to pause sandbox. Please try again.'
33-
)
34-
},
35-
})
25+
const { mutate: pause, isPending } = useMutation(
26+
trpc.sandbox.pause.mutationOptions({
27+
onSuccess: async () => {
28+
toast.success('Sandbox paused successfully')
29+
setOpen(false)
30+
refetchSandboxInfo()
31+
},
32+
onError: () => {
33+
toast.error('Failed to pause sandbox. Please try again.')
34+
},
35+
})
36+
)
3637

3738
const handlePause = () => {
3839
if (!canPause || !sandboxInfo?.sandboxID) return
3940

40-
execute({ teamSlug: team.slug, sandboxId: sandboxInfo.sandboxID })
41+
pause({ teamSlug: team.slug, sandboxId: sandboxInfo.sandboxID })
4142
}
4243

4344
if (!canPause) return null
@@ -56,8 +57,8 @@ export default function PauseButton({ className }: PauseButtonProps) {
5657
</Button>
5758
}
5859
confirmProps={{
59-
disabled: isExecuting,
60-
loading: isExecuting ? 'Pausing...' : undefined,
60+
disabled: isPending,
61+
loading: isPending ? 'Pausing...' : undefined,
6162
}}
6263
onConfirm={handlePause}
6364
onCancel={() => setOpen(false)}

0 commit comments

Comments
 (0)