Skip to content
Closed
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
26 changes: 21 additions & 5 deletions src/data/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isResponseEnvelope, isServer, type JSX } from "@solidjs/web";
import {
createServerReference,
decodeResponse,
isServerFunction,
subscribeFlightData,
type SingleFlightPayload
} from "@solidjs/web/server-functions";
Expand Down Expand Up @@ -148,7 +149,7 @@ function createServerFormAction(
(form: FormData | URLSearchParams) => stub(form) as Promise<unknown>,
{ url }
);
return actionImpl(caller);
return actionImpl(caller, {}, true);
}

/**
Expand Down Expand Up @@ -219,7 +220,8 @@ export function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>) {

function actionImpl<T extends Array<any>, U = void>(
fn: (...args: T) => Promise<U>,
options: string | { name?: string } = {}
options: string | { name?: string } = {},
serverFunction = isServerFunction(fn)
): Action<T, U> {
async function invoke(
this: { r: RouterContext; f?: HTMLFormElement },
Expand Down Expand Up @@ -262,7 +264,12 @@ function actionImpl<T extends Array<any>, U = void>(
: undefined
})
);
response = await handleResponse(settled.value, settled.error, router.navigatorFactory());
response = await handleResponse(
settled.value,
settled.error,
router.navigatorFactory(),
serverFunction && router.singleFlight
);
} finally {
form && setFormBusy(form, -1);
}
Expand Down Expand Up @@ -430,7 +437,12 @@ async function applyResponseMetadata(
await revalidate(keys, false);
}

async function handleResponse(response: unknown, error: boolean | undefined, navigate: Navigator) {
async function handleResponse(
response: unknown,
error: boolean | undefined,
navigate: Navigator,
metadataHandled: boolean
) {
let data: any;
let flightData: Record<string, any> | undefined;
let metadata: Response | undefined;
Expand All @@ -455,6 +467,10 @@ async function handleResponse(response: unknown, error: boolean | undefined, nav
}
} else if (error) return { error: response };
else data = response;
await applyResponseMetadata(metadata, navigate, flightData);
// The transport consumer applies metadata before returning a server
// function's unwrapped value. Do not treat that value as a second plain
// action response and invalidate the freshly seeded query cache again.
if (!metadataHandled || metadata || flightData)
await applyResponseMetadata(metadata, navigate, flightData);
return data != null ? { data } : undefined;
}
48 changes: 48 additions & 0 deletions test/data/flight-consumer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ let consumer: FlightDataConsumer<Record<string, any>> | undefined;

vi.mock("@solidjs/web/server-functions", () => ({
decodeResponse: vi.fn(),
isServerFunction: (fn: unknown) =>
typeof fn === "function" && !!(fn as any)[Symbol.for("solid.ServerFunctionMetadata")],
subscribeFlightData: (c: FlightDataConsumer<Record<string, any>>) => {
consumer = c;
return () => {
Expand Down Expand Up @@ -70,6 +72,52 @@ describe("setupFlightDataConsumer", () => {
);
expect(query.get("notes[]")).toEqual(["fresh"]);
});

test("does not revalidate flight data again after a server action settles", async () => {
const fetchNotes = vi.fn(async () => ["stale"]);
const notes = query(fetchNotes, "notes");
await notes();

(router as any).singleFlight = true;
setupFlightDataConsumer(router);
const save = async () => {
await consumer!(
{ "notes[]": ["fresh"] },
{ response: new Response(null, { headers: { "X-Revalidate": "notes" } }) }
);
return "saved";
};
(save as any)[Symbol.for("solid.ServerFunctionMetadata")] = {};

await action(save, "save-notes").call({ r: router });

expect(await notes()).toEqual(["fresh"]);
expect(fetchNotes).toHaveBeenCalledTimes(1);
});

test("continues to revalidate plain client action results", async () => {
const fetchNotes = vi.fn(async () => ["notes"]);
const notes = query(fetchNotes, "notes");
await notes();

await action(async () => "saved", "save-notes").call({ r: router });
await notes();

expect(fetchNotes).toHaveBeenCalledTimes(2);
});

test("continues to revalidate server actions when single flight is disabled", async () => {
const fetchNotes = vi.fn(async () => ["notes"]);
const notes = query(fetchNotes, "notes");
await notes();
const save = async () => "saved";
(save as any)[Symbol.for("solid.ServerFunctionMetadata")] = {};

await action(save, "save-notes").call({ r: router });
await notes();

expect(fetchNotes).toHaveBeenCalledTimes(2);
});
});

// The Router no longer imports setupFlightDataConsumer; it registers itself
Expand Down
Loading