Skip to content

Commit 53fad9d

Browse files
committed
improvement(files): make serve cacheability a declared, required property
An optional boolean let a branch added to the resolver inherit the cacheable default by saying nothing — the exact failure this change exists to prevent. Cacheability is now a required field every branch must declare, so forgetting it fails the build rather than silently promising a year of immutability.
1 parent b9e26ae commit 53fad9d

1 file changed

Lines changed: 45 additions & 27 deletions

File tree

  • apps/sim/app/api/files/serve/[...path]

‎apps/sim/app/api/files/serve/[...path]/route.ts‎

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,25 @@ interface ServeOptions {
6868
versioned: boolean
6969
}
7070

71+
/**
72+
* Whether a resolved response is a function of the stored object alone.
73+
*
74+
* `stored-bytes` may be cached for the life of the storage key: a content write stores the new
75+
* bytes under a NEW key, so a given key's response never changes. `derived-from-referenced-files`
76+
* may not — a sim page inlining its images, or a document compiled against the files it
77+
* references, is re-resolved per request and changes when a referenced file changes, while this
78+
* file's own key and `updatedAt` stay put.
79+
*
80+
* Declared rather than inferred, and REQUIRED, so a branch added to the resolver cannot inherit
81+
* the cacheable default by saying nothing — the same reason the transfer ceiling is asserted where
82+
* the branches converge rather than inside each one.
83+
*/
84+
type ServableCacheability = 'stored-bytes' | 'derived-from-referenced-files'
85+
7186
interface ServableBytes {
7287
buffer: Buffer
7388
contentType: string
74-
/**
75-
* These bytes were resolved against OTHER files' current content — a sim page inlining its
76-
* images, or a document compiled against the files it references. The same storage key can
77-
* therefore serve different bytes over time, with nothing about this file changing, so the
78-
* response must stay revalidated even when the request carries a version.
79-
*/
80-
dependsOnReferencedFiles?: boolean
89+
cacheability: ServableCacheability
8190
}
8291

8392
/**
@@ -111,8 +120,12 @@ async function resolveServableBytes(params: {
111120
// `raw` is the stored source, already bounded by the read that produced it, but it
112121
// goes through the same check so the ceiling holds for everything this returns
113122
// rather than for every branch someone remembered to cover.
114-
const resolved = params.options.raw
115-
? { buffer: params.buffer, contentType: getContentType(params.filename) }
123+
const resolved: ServableBytes = params.options.raw
124+
? {
125+
buffer: params.buffer,
126+
contentType: getContentType(params.filename),
127+
cacheability: 'stored-bytes',
128+
}
116129
: await resolveTransformedBytes(params)
117130
assertKnownSizeWithinLimit(
118131
resolved.buffer.length,
@@ -159,25 +172,35 @@ async function resolveTransformedBytes(params: {
159172
'utf8'
160173
)
161174
// Inlines the workspace images the page references, read at their CURRENT content.
162-
return { buffer: rendered, contentType: 'text/html', dependsOnReferencedFiles: true }
175+
return {
176+
buffer: rendered,
177+
contentType: 'text/html',
178+
cacheability: 'derived-from-referenced-files',
179+
}
163180
}
164181
}
165182

166183
if (options.preview) {
167184
// Images resolve independently of the document path: a HEIF has no compiled-source
168185
// concept, so it never reaches the doc branch.
169186
const image = await resolveServableImageBytes(buffer, storageKey)
170-
if (image) return image
187+
// Transcoded from THIS file's stored bytes, so it lives and dies with the storage key.
188+
if (image) return { ...image, cacheability: 'stored-bytes' }
171189
}
172190

173-
return resolveServableDocBytes({
191+
const doc = await resolveServableDocBytes({
174192
rawBuffer: buffer,
175193
fileName: filename,
176194
workspaceId,
177195
filePrincipal,
178196
ownerKey,
179197
signal,
180198
})
199+
return {
200+
buffer: doc.buffer,
201+
contentType: doc.contentType,
202+
cacheability: doc.dependsOnReferencedFiles ? 'derived-from-referenced-files' : 'stored-bytes',
203+
}
181204
}
182205

183206
const STORAGE_KEY_PREFIX_RE = /^\d{13}-[a-z0-9]{7}-/
@@ -205,7 +228,7 @@ const PUBLIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000'
205228
* then resolve from cache with no round trip.
206229
*
207230
* That promise does NOT hold when the response was resolved against other files' current content
208-
* (`dependsOnReferencedFiles`): a document compiled against the files it references, or a sim page
231+
* (`derived-from-referenced-files`): a document compiled against the files it references, or a page
209232
* inlining its images, recompiles per request, so the same key serves different bytes once a
210233
* referenced file changes — while this file's key and `updatedAt`, and therefore the whole URL,
211234
* stay put. Promising immutability there pins a stale render in the browser cache for a year, so
@@ -214,12 +237,11 @@ const PUBLIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000'
214237
function resolveServeCacheControl(
215238
versioned: boolean,
216239
context: string | undefined,
217-
dependsOnReferencedFiles: boolean | undefined
240+
cacheability: ServableCacheability
218241
): string | undefined {
219-
if (versioned && !dependsOnReferencedFiles) return IMMUTABLE_CACHE_CONTROL
220-
return context === 'workspace' || dependsOnReferencedFiles
221-
? WORKSPACE_REVALIDATE_CACHE_CONTROL
222-
: undefined
242+
const derived = cacheability === 'derived-from-referenced-files'
243+
if (versioned && !derived) return IMMUTABLE_CACHE_CONTROL
244+
return context === 'workspace' || derived ? WORKSPACE_REVALIDATE_CACHE_CONTROL : undefined
223245
}
224246

225247
export const GET = withRouteHandler(
@@ -409,11 +431,7 @@ async function handleWorkspaceFile(
409431
buffer: resolved.buffer,
410432
contentType: resolved.contentType,
411433
filename: file.name,
412-
cacheControl: resolveServeCacheControl(
413-
options.versioned,
414-
'workspace',
415-
resolved.dependsOnReferencedFiles
416-
),
434+
cacheControl: resolveServeCacheControl(options.versioned, 'workspace', resolved.cacheability),
417435
})
418436
}
419437

@@ -458,7 +476,7 @@ async function handleLocalFile(
458476
const {
459477
buffer: fileBuffer,
460478
contentType,
461-
dependsOnReferencedFiles,
479+
cacheability,
462480
} = await resolveServableBytes({
463481
buffer: rawBuffer,
464482
filename: displayName,
@@ -475,7 +493,7 @@ async function handleLocalFile(
475493
buffer: fileBuffer,
476494
contentType,
477495
filename: displayName,
478-
cacheControl: resolveServeCacheControl(options.versioned, context, dependsOnReferencedFiles),
496+
cacheControl: resolveServeCacheControl(options.versioned, context, cacheability),
479497
})
480498
} catch (error) {
481499
logServeFailure('Error reading local file:', error)
@@ -529,7 +547,7 @@ async function handleCloudProxy(
529547
const {
530548
buffer: fileBuffer,
531549
contentType,
532-
dependsOnReferencedFiles,
550+
cacheability,
533551
} = await resolveServableBytes({
534552
buffer: rawBuffer,
535553
filename: displayName,
@@ -551,7 +569,7 @@ async function handleCloudProxy(
551569
buffer: fileBuffer,
552570
contentType,
553571
filename: displayName,
554-
cacheControl: resolveServeCacheControl(options.versioned, context, dependsOnReferencedFiles),
572+
cacheControl: resolveServeCacheControl(options.versioned, context, cacheability),
555573
})
556574
} catch (error) {
557575
logServeFailure('Error downloading from cloud storage:', error)

0 commit comments

Comments
 (0)