@@ -5,6 +5,12 @@ import type { NextRequest } from 'next/server'
55import { NextResponse } from 'next/server'
66import { fileServeParamsSchema , fileServeQuerySchema } from '@/lib/api/contracts/storage-transfer'
77import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
8+ import {
9+ DocCompileUserError ,
10+ getE2BDocFormat ,
11+ loadCompiledDocByExt ,
12+ } from '@/lib/copilot/tools/server/files/doc-compile'
13+ import { isE2BDocEnabled } from '@/lib/core/config/feature-flags'
814import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
915import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
1016import { CopilotFiles , isUsingCloudStorage } from '@/lib/uploads'
@@ -72,14 +78,55 @@ async function compileDocumentIfNeeded(
7278 if ( raw ) return { buffer, contentType : getContentType ( filename ) }
7379
7480 const ext = filename . slice ( filename . lastIndexOf ( '.' ) ) . toLowerCase ( )
81+ const extNoDot = ext . replace ( / ^ \. / , '' )
7582 const format = COMPILABLE_FORMATS [ ext ]
76- if ( ! format ) return { buffer, contentType : getContentType ( filename ) }
7783
78- const magicLen = format . magic . length
79- if ( buffer . length >= magicLen && buffer . subarray ( 0 , magicLen ) . equals ( format . magic ) ) {
84+ // Already a binary file (uploaded or pre-compiled)? Serve as-is.
85+ if ( format ) {
86+ const magicLen = format . magic . length
87+ if ( buffer . length >= magicLen && buffer . subarray ( 0 , magicLen ) . equals ( format . magic ) ) {
88+ return { buffer, contentType : getContentType ( filename ) }
89+ }
90+ }
91+
92+ // .xlsx is a ZIP container with no JS compile path. An uploaded/binary xlsx
93+ // must short-circuit here (it isn't in COMPILABLE_FORMATS) — otherwise every
94+ // xlsx open would utf-8-decode the whole binary and do an always-miss S3 GET.
95+ // Only a Python-source xlsx (UTF-8 text, no ZIP magic) falls through.
96+ if (
97+ extNoDot === 'xlsx' &&
98+ buffer . length >= ZIP_MAGIC . length &&
99+ buffer . subarray ( 0 , ZIP_MAGIC . length ) . equals ( ZIP_MAGIC )
100+ ) {
80101 return { buffer, contentType : getContentType ( filename ) }
81102 }
82103
104+ // Generated docs render from a content-addressed compiled binary that is built
105+ // exactly ONCE per edit_content/create (at write time) and stored in S3. Serve
106+ // only LOADS it — it must never compile, or it would re-run E2B on every preview
107+ // fetch, including against the incomplete source mid-generation. A hit returns
108+ // the (possibly partial) committed doc; a miss in the E2B regime means the doc
109+ // is still being generated → 409, and the client polls until the artifact lands.
110+ if ( workspaceId && ( format || extNoDot === 'xlsx' ) ) {
111+ const source = buffer . toString ( 'utf-8' )
112+ // Load the prebuilt artifact directly from S3 (content-addressed). No extra
113+ // in-memory layer here: the store is the source of truth, the client (react
114+ // query) already caches the bytes, and this branch never recomputes.
115+ const stored = await loadCompiledDocByExt ( workspaceId , source , extNoDot )
116+ if ( stored ) {
117+ return { buffer : stored . buffer , contentType : stored . contentType }
118+ }
119+
120+ if ( isE2BDocEnabled && getE2BDocFormat ( filename ) ) {
121+ // Artifact not built yet (still generating, or the source didn't compile at
122+ // write time). Signal "not ready" without compiling — handled as 409.
123+ throw new DocCompileUserError ( 'Document is still being generated' )
124+ }
125+ }
126+
127+ if ( ! format ) return { buffer, contentType : getContentType ( filename ) }
128+
129+ // E2B disabled and no stored artifact → compile JS source via isolated-vm.
83130 const code = buffer . toString ( 'utf-8' )
84131 const cacheKey = sha256Hex ( `${ ext } ${ code } ${ workspaceId ?? '' } ` )
85132 const cached = compiledDocCache . get ( cacheKey )
@@ -164,6 +211,17 @@ export const GET = withRouteHandler(
164211
165212 return await handleLocalFile ( cloudKey , userId , raw , request . signal )
166213 } catch ( error ) {
214+ // An in-progress/incomplete doc source fails to compile — this is expected
215+ // mid-generation, not a server fault. Return 409 (not 500) so it isn't an
216+ // alarming error; the client re-fetches once the doc finishes (the serve
217+ // URL is busted on the file's updatedAt).
218+ if ( error instanceof DocCompileUserError ) {
219+ logger . info ( 'Serve: document still compiling, returning 409' , {
220+ message : error . message ,
221+ } )
222+ return NextResponse . json ( { error : 'Document is still being generated' } , { status : 409 } )
223+ }
224+
167225 logger . error ( 'Error serving file:' , error )
168226
169227 if ( error instanceof FileNotFoundError ) {
0 commit comments