11import { HttpRouter , HttpServerRequest , HttpServerResponse } from '@effect/platform'
2- import { Effect } from 'effect'
2+ import { Cause , Effect } from 'effect'
33import { idToBytes } from '@sandchest/contract'
44import type { FileEntry , ListFilesResponse } from '@sandchest/contract'
55import {
6+ InternalError ,
67 NotFoundError ,
78 SandboxNotRunningError ,
89 ValidationError ,
@@ -15,6 +16,7 @@ import { NodeClient } from '../services/node-client.js'
1516const MAX_SINGLE_FILE = 5 * 1024 * 1024 * 1024 // 5 GB
1617const MAX_BATCH_FILE = 10 * 1024 * 1024 * 1024 // 10 GB
1718const DEFAULT_LIST_LIMIT = 200
19+ const DEFAULT_NODE_FILE_TIMEOUT_MS = 30_000
1820
1921function parseSandboxId ( idStr : string | undefined ) {
2022 if ( ! idStr ) {
@@ -27,6 +29,40 @@ function parseSandboxId(idStr: string | undefined) {
2729 }
2830}
2931
32+ function resolveNodeFileTimeoutMs ( ) : number {
33+ const raw = process . env [ 'NODE_FILE_TIMEOUT_MS' ]
34+ if ( ! raw ) {
35+ return DEFAULT_NODE_FILE_TIMEOUT_MS
36+ }
37+
38+ const parsed = Number . parseInt ( raw , 10 )
39+ return Number . isInteger ( parsed ) && parsed > 0 ? parsed : DEFAULT_NODE_FILE_TIMEOUT_MS
40+ }
41+
42+ function withNodeFileTimeout < A > (
43+ operation : string ,
44+ effect : Effect . Effect < A , never , never > ,
45+ ) : Effect . Effect < A , InternalError , never > {
46+ const timeoutMs = resolveNodeFileTimeoutMs ( )
47+
48+ return effect . pipe (
49+ Effect . timeoutFail ( {
50+ duration : `${ timeoutMs } millis` ,
51+ onTimeout : ( ) =>
52+ new InternalError ( {
53+ message : `Node ${ operation } timed out after ${ timeoutMs } ms` ,
54+ } ) ,
55+ } ) ,
56+ Effect . catchAllCause ( ( cause ) =>
57+ Effect . fail (
58+ new InternalError ( {
59+ message : `Node ${ operation } failed: ${ Cause . pretty ( cause ) } ` ,
60+ } ) ,
61+ ) ,
62+ ) ,
63+ )
64+ }
65+
3066// -- Upload file -------------------------------------------------------------
3167
3268const uploadFile = Effect . gen ( function * ( ) {
@@ -77,11 +113,14 @@ const uploadFile = Effect.gen(function* () {
77113 )
78114 }
79115
80- const result = yield * nodeClient . putFile ( {
81- sandboxId : sandboxIdBytes ,
82- path,
83- data,
84- } )
116+ const result = yield * withNodeFileTimeout (
117+ 'putFile' ,
118+ nodeClient . putFile ( {
119+ sandboxId : sandboxIdBytes ,
120+ path,
121+ data,
122+ } ) ,
123+ )
85124
86125 return HttpServerResponse . unsafeJson ( {
87126 path,
@@ -136,10 +175,13 @@ const downloadOrListFiles = Effect.gen(function* () {
136175 )
137176 }
138177
139- const entries = yield * nodeClient . listFiles ( {
140- sandboxId : sandboxIdBytes ,
141- path,
142- } )
178+ const entries = yield * withNodeFileTimeout (
179+ 'listFiles' ,
180+ nodeClient . listFiles ( {
181+ sandboxId : sandboxIdBytes ,
182+ path,
183+ } ) ,
184+ )
143185
144186 // Apply cursor-based pagination
145187 let startIdx = 0
@@ -167,10 +209,13 @@ const downloadOrListFiles = Effect.gen(function* () {
167209 }
168210
169211 // File download
170- const data = yield * nodeClient . getFile ( {
171- sandboxId : sandboxIdBytes ,
172- path,
173- } )
212+ const data = yield * withNodeFileTimeout (
213+ 'getFile' ,
214+ nodeClient . getFile ( {
215+ sandboxId : sandboxIdBytes ,
216+ path,
217+ } ) ,
218+ )
174219
175220 return HttpServerResponse . uint8Array ( data , {
176221 contentType : 'application/octet-stream' ,
@@ -216,10 +261,13 @@ const deleteFile = Effect.gen(function* () {
216261 return yield * Effect . fail ( new ValidationError ( { message : 'path query parameter is required' } ) )
217262 }
218263
219- yield * nodeClient . deleteFile ( {
220- sandboxId : sandboxIdBytes ,
221- path,
222- } )
264+ yield * withNodeFileTimeout (
265+ 'deleteFile' ,
266+ nodeClient . deleteFile ( {
267+ sandboxId : sandboxIdBytes ,
268+ path,
269+ } ) ,
270+ )
223271
224272 return HttpServerResponse . unsafeJson ( { ok : true } )
225273} )
0 commit comments