[]previous and next page

Batching and single flight

Batching and single flight in the Server functions guide.

For agentsUpdated

The browser stub batches. Calls made in the same tick (one synchronous run of code) to POST functions travel in one request, POST {base}/_dusk/fn/_batch, as an array of { id, input }. The answer is an array of the same envelopes, in order, so one call failing (a validation error, a redirect, a 403 from its middleware) rejects that call alone. A batch carries at most 25 calls (rpc.maxBatchSize); a longer tick is sent as several requests.

Each call runs as it would alone: its own input schema, its own middleware (so a rate limit or an authorization check applies per call), its own errors. The request-level checks run once for the batch: CSRF, the x-dusk-fn header and the body limit. The response shares one budget (rpc.maxResponseBytes): a call whose answer would exceed what is left answers SERVER_FN_RESPONSE_TOO_LARGE, and the others are kept.

Batching is off for a GET function, for a call with its own signal, headers or fetch, and for a function built with batch: false:

src/server/counters.tsts
import { serverFn } from "@shadow-js/dusk/server";

// Sent alone, even when other calls are made in the same tick.
export const resetCounter = serverFn({ batch: false }, async () => ({ count: 0 }));
// A read: a GET, never batched.
export const readCounter = serverFn({ method: "GET" }, async () => ({ count: 1 }));

A mutation can also answer with the loader data of the page the browser is on, so the router updates the screen without a second request. Declare it with singleFlight: true. The browser sends the page's path and query in x-dusk-location; the server runs the matched loaders for that location under the same request (its cookies and middleware) and sends their data with the answer. The router applies it (router.applyHydration) before the call resolves, and the loaders are not called again in the browser:

src/server/todos.tsts
import { serverFn } from "@shadow-js/dusk/server";

export const addTodo = serverFn({ singleFlight: true }, async (input) => {
  const title = typeof input === "object" && input !== null && "title" in input ? String(input.title) : "";
  return { id: "t1", title };
});

Nothing extra is sent when the location is missing, is not a same-origin path, or leads to a redirect: the browser then loads the page as it would without single flight. A page whose data would exceed the hydration payload limit is also left to the browser.