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
5 changes: 5 additions & 0 deletions .changeset/reference-fetch-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

A server function reference can declare its own client send with `withMeta(fn, { fetch: (address, init, next) => ... })`. The transport uses it for that function's requests, with the configured send (the app's `fetch`, or the global) as `next`, so an integration that answers some calls itself (a prerendered build's static artifacts) no longer competes with the app's `configureServerFunctionsClient({ fetch })`. Exported type `ServerFunctionsSend`.
19 changes: 15 additions & 4 deletions packages/web/server-functions/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ export type PrepareRequestHook = (
context: PrepareRequestContext
) => RequestInit | Promise<RequestInit>;

/** How the transport sends a request: `fetch`'s shape, as the runtime calls it. */
export type ServerFunctionsSend = (
address: string,
init: RequestInit
) => Response | Promise<Response>;

/** Options for `configureServerFunctionsClient`. */
export interface ServerFunctionsClientConfig {
/**
Expand Down Expand Up @@ -184,9 +190,10 @@ export interface ServerFunctionsClientConfig {
codec?: JSONCodecOptions;
/**
* Sends every server-function request — retries, telemetry, a test
* double, or an app's own route. Always called as `(address, init)`, the
* double, or an app's own route. Called as `(address, init)`, the
* address relative to the document as the global one receives it, so
* `parseServerFunctionActionUrl` reads the id back out for telemetry. `null`
* `parseServerFunctionActionUrl` reads the id back out for telemetry (a
* trailing third argument is the transport's own; ignore it). `null`
* restores the global.
*
* ```ts
Expand All @@ -210,7 +217,7 @@ export interface ServerFunctionsClientConfig {
* The wrapper replaces delivery for the requests the runtime chooses to
* send; the call-to-request mapping itself is not contractual.
*/
fetch?: ((address: string, init: RequestInit) => Response | Promise<Response>) | null;
fetch?: ServerFunctionsSend | null;
/**
* Runs before every server-function fetch. Return (or mutate and return)
* the RequestInit the transport will use; `context.meta` is the
Expand Down Expand Up @@ -656,8 +663,12 @@ async function createRequest(base, id, options, meta) {
// The send, as observed: the `"request"` record leaves from here — the
// final address and init, immediately before `fetch` receives them.
if (IS_OBSERVE && observation) observation.request(base, init);
// A reference may declare its own send (`withMeta(fn, { fetch })`): it
// receives the app's as `next`, so the app's transport policy still
// applies to whatever it passes on. (The app's send ignores the extra
// argument, as `fetch` does.)
const send = config.fetch || fetch;
return send(base, init);
return (meta.fetch || send)(base, init, send);
}

async function initializeResponse(base, id, options, args, meta) {
Expand Down
23 changes: 23 additions & 0 deletions packages/web/server-functions/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,29 @@ export interface ServerFunctionMetadata {
* `withMeta` write wins.
*/
readonly name?: string;
/**
* This function's own send, client side: the transport calls it instead
* of the configured `fetch` for every request the function makes, with
* the configured send (the app's `fetch`, or the global) as `next`. For
* an integration that answers a declared function's calls itself — a
* prerendered build's static artifacts, a cache — while the app's
* transport `fetch` stays the app's: pass on what you don't answer
* through `next`. The contract is `fetch`'s (see
* `ServerFunctionsClientConfig.fetch`); hydration adopts a
* server-rendered read only if the send reaches the global `fetch` in the
* same tick.
*
* ```ts
* withMeta(getPosts, {
* fetch: (address, init, next) => cached(address) ?? next(address, init)
* });
* ```
*/
readonly fetch?: (
address: string,
init: RequestInit,
next: (address: string, init: RequestInit) => Response | Promise<Response>
) => Response | Promise<Response>;
/** User-declared transport metadata attached with `withMeta`. */
readonly [key: string]: unknown;
}
Expand Down
37 changes: 37 additions & 0 deletions packages/web/test/server/server-functions-extensions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,43 @@ describe("server-function extension surface (built bundles)", () => {
}
});

it("sends a reference's calls through its declared fetch, with the app's as next", async () => {
registerServerFunction("ext-fetch-9", async (word: string) => word);
const order: string[] = [];
// the reference's own send, declared before the app configures its fetch
const ref = withMeta(createServerReference("ext-fetch-9"), {
fetch: (address: string, init: RequestInit, next: (a: string, i: RequestInit) => any) => {
order.push("own");
return init.body === undefined || String(init.body).includes("cached")
? new Response(JSON.stringify("from own send"), {
headers: { "X-Server-Function-Format": "8" }
})
: next(address, init);
}
});
configureServerFunctionsClient({
fetch(address, init) {
order.push("app");
return deliver(address, init);
}
});
try {
expect(await ref("live")).toBe("live");
expect(order).toEqual(["own", "app"]);
order.length = 0;
// what the reference's send answers never reaches the app's
expect(await ref("cached")).toBe("from own send");
expect(order).toEqual(["own"]);
order.length = 0;
// other references are untouched
registerServerFunction("ext-fetch-10", async () => "plain");
expect(await createServerReference("ext-fetch-10")()).toBe("plain");
expect(order).toEqual(["app"]);
} finally {
configureServerFunctionsClient({ fetch: null });
}
});

it("hands the fetch the (address, init) shape", async () => {
registerServerFunction("ext-fetch-1", async () => "ok");
const shapes: string[] = [];
Expand Down
Loading