Skip to content
Open
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
10 changes: 9 additions & 1 deletion website/sections/Rendering/Lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@deco/deco/blocks";
import { useContext } from "preact/hooks";
import { shouldForceRender } from "../../../utils/deferred.ts";
import { newIsolatedRequestState } from "../../utils/isolatedRequestState.ts";
import type { AppContext } from "../../mod.ts";
const useSectionContext = () =>
useContext<SectionContext | undefined>(SectionCtx);
Expand Down Expand Up @@ -83,7 +84,14 @@ export const loader = async (props: Props, req: Request, ctx: AppContext) => {
const resolveSection = isDeferred<Section>(section)
? RequestContext.bind({ signal: abortController.signal }, section)
: () => section;
const resolvedSection = await resolveSection({}, {
// This resolution only discovers the selected section's loading fallback.
// Keep its loaders, matchers and response mutations from affecting the
// cache decision of the parent page. The eager partial resolves the section
// again with the real request state and remains personalized when needed.
const isolatedState = newIsolatedRequestState() as unknown as Parameters<
typeof resolveSection
>[0];
const resolvedSection = await resolveSection(isolatedState, {
propagateOptions: true,
hooks: {
onPropsResolveStart: (resolve, _props, resolver) => {
Expand Down
30 changes: 30 additions & 0 deletions website/utils/isolatedRequestState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { assertEquals, assertNotStrictEquals } from "@std/assert";
import { newIsolatedRequestState } from "./isolatedRequestState.ts";

Deno.test("newIsolatedRequestState does not share resolver side effects", () => {
const first = newIsolatedRequestState();
const second = newIsolatedRequestState();

first.vary.shouldCache = false;
first.vary.push("weather/loaders/temperature.ts", "rio-de-janeiro");
first.flags.push({
name: "weather",
value: true,
isSegment: false,
});
first.response.headers.set("set-cookie", "personalized=true");

assertNotStrictEquals(first.vary, second.vary);
assertNotStrictEquals(first.flags, second.flags);
assertNotStrictEquals(first.response, second.response);
assertNotStrictEquals(first.bag, second.bag);
assertEquals(first.vary.shouldCache, false);
assertEquals(
first.vary.build(),
"rio-de-janeiro,weather/loaders/temperature.ts",
);
assertEquals(second.vary.shouldCache, true);
assertEquals(second.vary.build(), "");
assertEquals(second.flags, []);
assertEquals(second.response.headers.has("set-cookie"), false);
});
37 changes: 37 additions & 0 deletions website/utils/isolatedRequestState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { AppContext } from "../mod.ts";

type IsolatedRequestState = Pick<
AppContext,
"bag" | "flags" | "response" | "vary"
>;

/**
* Creates request-scoped state for speculative/deferred resolutions.
*
* Resolvers can mutate cache eligibility, flags, response headers and the
* request bag. Those mutations must not leak when the resolved value is used
* only to inspect metadata such as a loading fallback.
*/
export const newIsolatedRequestState = (): IsolatedRequestState => {
const keys: string[] = [];
const debugEntries: unknown[] = [];

return {
bag: new WeakMap(),
flags: [],
response: { headers: new Headers() },
vary: {
shouldCache: true,
push: (...key: string[]) => {
keys.push(...key);
},
build: () => keys.sort().join(),
debug: {
push: <T>(entry: T) => {
debugEntries.push(entry);
},
build: <T>() => debugEntries as T[],
},
},
};
};
Loading