Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/server/runtime-handler/adapter-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,53 @@ describe("adapter-factory", () => {
assertEquals("isLocalProject" in result, true);
});

it("survives an adapter cache that drops the entry it was just given", async () => {
Deno.env.set("VERYFRONT_TRUST_FORWARDED_HEADERS", "1");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const cache = new ProjectDiscoveryCache();
const adapter = createMockAdapter({
"/trusted/project": { isDirectory: true },
"/trusted/project/app": { isDirectory: true },
});

// The adapter LRU estimates a RuntimeAdapter's size and evicts it again
// when it exceeds the byte budget, so set() can silently store nothing.
// Observed under Bun 1.3.6, where the same adapter object that caches
// fine on Node is dropped: set() then has()===false and size===0.
// resolveAdapter must not depend on reading back what it just wrote.
cache.adapters.set = () => {};

const result = await resolveAdapter({
req: await makeReq({ projectPath: "/trusted/project", trusted: true }),
projectDir: "/base/project",
adapter,
config: undefined,
projectSlug: "myproject",
projectId: "proj_123",
proxyToken: undefined,
releaseId: undefined,
proxyEnv: "preview",
branch: null,
environmentName: undefined,
parsedDomain: {
slug: null,
branch: null,
environment: null,
isVeryfrontDomain: false,
isDraft: false,
allowIframeEmbed: false,
},
isProxyMode: true,
cache,
prepareHostedConfigContext: preparePreviewHostedConfigContext,
});

assertEquals(
result.adapter === undefined || result.adapter === null,
false,
"resolveAdapter must hand on a real adapter even when the cache drops it",
);
});

it("uses injected cache instead of default singleton", async () => {
Deno.env.set("VERYFRONT_TRUST_FORWARDED_HEADERS", "1");
const cache = new ProjectDiscoveryCache();
Expand Down
20 changes: 15 additions & 5 deletions src/server/runtime-handler/adapter-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,27 @@ export async function resolveAdapter(
projectDir: effectiveProjectDir,
});

// Get or create local adapter
if (!cache.adapters.has(effectiveProjectDir)) {
const baseAdapter = await runtime.get();
cache.adapters.set(effectiveProjectDir, baseAdapter);
// Get or create local adapter.
//
// Hold the adapter rather than reading it back: the cache is an LRU that
// estimates each value's size and can evict an oversized entry as part of
// the same set(), so a write is not guaranteed to be readable afterwards.
// A RuntimeAdapter crosses that budget under Bun, where set() then leaves
// has() === false and size === 0, and the non-null assertion this replaces
// turned that miss into an undefined adapter that reached getConfig and
// threw "undefined is not an object (evaluating 'adapter.fs')" on every
// request. Caching stays best effort; correctness no longer depends on it.
let localAdapter = cache.adapters.get(effectiveProjectDir);
if (!localAdapter) {
localAdapter = await runtime.get();
cache.adapters.set(effectiveProjectDir, localAdapter);
logger.debug("Created local adapter for project", {
projectSlug: opts.projectSlug,
projectDir: effectiveProjectDir,
});
}

effectiveAdapter = cache.adapters.get(effectiveProjectDir)!;
effectiveAdapter = localAdapter;

if (shouldDeferConfigLoad(opts)) {
effectiveConfig = undefined;
Expand Down