Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions blocks/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface RequestState {
};
bag: WeakMap<any, any>;
flags: Flag[];
dirty?: boolean;
}

export type FnContext<
Expand Down
16 changes: 16 additions & 0 deletions runtime/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async (ctx, next) => {

const DEBUG_COOKIE = "deco_debug";
const DEBUG_ENABLED = "enabled";
const PAGE_CACHE_DRY_RUN = Deno.env.get("DECO_PAGE_CACHE_DRY_RUN") === "true";

export const DEBUG_QS = "__d";
const addHours = function (date: Date, h: number) {
Expand Down Expand Up @@ -446,6 +447,21 @@ export const middlewareFor = <TAppManifest extends AppManifest = AppManifest>(
// If response has set-cookie header, set cache-control to no-store
if (getSetCookies(newHeaders).length > 0) {
newHeaders.set("Cache-Control", "no-store, no-cache, must-revalidate");
} else if (!ctx.var.dirty) {
if (PAGE_CACHE_DRY_RUN) {
console.warn(
`[page-cache] cacheable: ${url.pathname}`,
);
} else {
newHeaders.set(
"Cache-Control",
"public, max-age=120, s-maxage=120",
);
}
} else if (PAGE_CACHE_DRY_RUN) {
console.warn(
`[page-cache] not cacheable (cookies accessed): ${url.pathname}`,
);
}

// for some reason hono deletes content-type when response is not fresh.
Expand Down
38 changes: 37 additions & 1 deletion runtime/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,48 @@ export class Deco<TAppManifest extends AppManifest = AppManifest> {
state.bag = new WeakMap();
state.vary = vary();
state.flags = [];
state.dirty = false;
state.site = {
id: this.ctx.siteId ?? 0,
name: this.ctx.site,
};
state.global = state;
const { resolver } = await this.ctx.runtime!;

// Proxy the request headers to detect cookie access during resolution.
// When any resolver (loader, action, section) reads the "cookie" header,
// we flag state.dirty = true so the middleware knows not to cache.
const proxiedHeaders = new Proxy(request.headers, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === "function") {
return function (this: Headers, ...args: any[]) {
if (
(prop === "get" || prop === "has") &&
typeof args[0] === "string" &&
args[0].toLowerCase() === "cookie"
) {
state.dirty = true;
}
return value.apply(target, args);
};
}
return value;
},
});
const proxiedRequest = new Proxy(request, {
get(target, prop, receiver) {
if (prop === "headers") {
return proxiedHeaders;
}
const value = Reflect.get(target, prop, receiver);
if (typeof value === "function") {
return value.bind(target);
}
return value;
},
});

const ctxResolver = resolver
.resolverFor(
{
Expand All @@ -253,7 +289,7 @@ export class Deco<TAppManifest extends AppManifest = AppManifest> {
return Reflect.get(target, prop, recv);
},
}),
request,
request: proxiedRequest,
},
{
monitoring: state.monitoring,
Expand Down
39 changes: 29 additions & 10 deletions runtime/routes/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { FieldResolver } from "../../engine/core/resolver.ts";
import { badRequest } from "../../engine/errors.ts";
import { stableStringify } from "../../utils/json.ts";
import { singleFlight } from "../../utils/mod.ts";
import { createHandler, DEBUG_QS } from "../middleware.ts";
import type { PageParams } from "../mod.ts";
import Render, { type PageData } from "./entrypoint.tsx";
Expand Down Expand Up @@ -73,6 +75,10 @@ const fromRequest = (req: Request): Options => {
const DECO_RENDER_CACHE_CONTROL = Deno.env.get("DECO_RENDER_CACHE_CONTROL") ||
"public, max-age=60, s-maxage=60, stale-while-revalidate=3600, stale-if-error=86400";

const AsyncRenderSF = singleFlight<Response>();
const SHOULD_USE_ASYNC_RENDER_SINGLE_FLIGHT =
Deno.env.get("SHOULD_USE_ASYNC_RENDER_SINGLE_FLIGHT") === "true";

export const handler = createHandler(async (
ctx,
) => {
Expand All @@ -85,17 +91,30 @@ export const handler = createHandler(async (
if (isDebugRequest) {
return Response.json({ debugData: state.vary.debug.build() });
}
const props = {
params: ctx.req.param(),
url: ctx.var.url,
data: { page },
} satisfies PageParams<PageData>;

const propsString = stableStringify(props);
const renderFn = async () =>
await render({
page: {
Component: Render,
props,
},
});

const response = await render({
page: {
Component: Render,
props: {
params: ctx.req.param(),
url: ctx.var.url,
data: { page },
} satisfies PageParams<PageData>,
},
});
// In case of async render, we use single flight to deduplicate the render calls.
// FEATURE_FLAG: SHOULD_USE_ASYNC_RENDER_SINGLE_FLIGHT
// Needs to measure if CPU time for rendering is higher than the time to deduplicate the render calls.
const response = SHOULD_USE_ASYNC_RENDER_SINGLE_FLIGHT
? await AsyncRenderSF.do(
propsString,
renderFn,
).then((r) => r.clone())
: await renderFn();

// this is a hack to make sure we cache only sections that does not vary based on the loader content.
// so we can calculate cacheBust per page but decide to cache sections individually based on vary.
Expand Down
Loading