From 477d270e060888cedd090985fc377b507e2de3d6 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 16 Oct 2026 06:11:22 +0800 Subject: [PATCH] [docs] Document the Batcher and CacheKey features --- docs/apis/subsystems/routing/caching.md | 78 ++++++++++++++++++++++++ docs/guides/javascript/ajax/batcher.md | 81 +++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 docs/apis/subsystems/routing/caching.md create mode 100644 docs/guides/javascript/ajax/batcher.md diff --git a/docs/apis/subsystems/routing/caching.md b/docs/apis/subsystems/routing/caching.md new file mode 100644 index 000000000..6a598959b --- /dev/null +++ b/docs/apis/subsystems/routing/caching.md @@ -0,0 +1,78 @@ +--- +title: Caching +tags: + - Routing + - Caching + - Performance +--- + + + +Routes which return static, public assets (such as language strings and templates) can be made cacheable by browsers and reverse proxies by setting the `cachelifetime` property of the `\core\router\route` attribute. + +## Declaring a cacheable route {/* #declaring-a-cacheable-route */} + +`cachelifetime` accepts any relative time string understood by [`DateInterval::createFromDateString()`](https://www.php.net/manual/en/dateinterval.createfromdatestring.php), for example `'90 days'`. + +```php title="A cacheable route" +#[\core\router\route( + path: '/strings/{language}/{component}[/{identifier:.*}]', + method: ['GET'], + security: [], + cachelifetime: '90 days', + pathtypes: [ + new \core\router\parameters\path_language(), + new \core\router\parameters\path_component(), + ], +)] +``` + +:::danger + +Responses are cached as `public` and `immutable`. Only use `cachelifetime` for routes whose response is identical for every user and which never contains private data. + +::: + +## The cache key {/* #the-cache-key */} + +When `cachelifetime` is set, the route automatically gains an optional `cachekey` path parameter (`\core\router\parameters\path_cachekey`), which is a Unix timestamp placed at the start of the path: + +```text +/api/rest/v2/strings/en/core # No cache key - not cached +/api/rest/v2/cachekey:1737584047/strings/en/core # Cacheable +``` + +Because the cache key forms part of the URI, changing it (typically by using the current JavaScript/theme revision) invalidates every cached copy of the resource. You do not need to declare the parameter yourself. + +## Cache headers {/* #cache-headers */} + +The `\core\router\middleware\cache_control_middleware` adds the following headers when all of these are true: + +- the request method is `GET`; +- the response status is 2xx; +- the route has a `cachelifetime`; +- a numeric `cachekey` was supplied, and it is not in the future (to prevent cache poisoning); and +- the lifetime resolves to a positive number of seconds. + +| Header | Value | +| --- | --- | +| `Cache-Control` | `public, max-age=, immutable` | +| `Expires` | The current time plus the lifetime | +| `Pragma` | _(empty)_ | +| `Accept-Ranges` | `none` | + +The expiry is calculated from the current time, not from the cache key, so cached content does not expire early because an old key was used. + +If any condition is not met, no caching headers are added. + +## Fetching cacheable routes from JavaScript {/* #fetching-cacheable-routes-from-JavaScript */} + +Pass the `cachekey` option to the [`core/fetch`](../../../guides/javascript/ajax/batcher.md) module: + +```js +import Fetch from 'core/fetch'; + +const response = await Fetch.performGet('core', `strings/en/core`, {cachekey: M.cfg.jsrev}); +``` + +Requests with a cache key bypass the [batcher](../../../guides/javascript/ajax/batcher.md) by default, so that every URL can be cached individually. diff --git a/docs/guides/javascript/ajax/batcher.md b/docs/guides/javascript/ajax/batcher.md new file mode 100644 index 000000000..a3d3e781d --- /dev/null +++ b/docs/guides/javascript/ajax/batcher.md @@ -0,0 +1,81 @@ +--- +title: The Fetch batcher +tags: + - Javascript + - AJAX + - Fetch + - Routing +--- + + + +The `core/fetch` module makes requests to the Moodle [REST API (routes)](../../../apis/subsystems/routing/index.md). In addition to immediate requests, it can queue several requests and send them as a single HTTP `$batch` request, reducing the number of round trips. + +## Immediate requests {/* #immediate-requests */} + +Static methods perform a single request immediately: + +```js +import Fetch from 'core/fetch'; + +const response = await Fetch.performGet('mod_example', 'animals', {params: {type: 'mammal'}}); +``` + +`performGet`, `performHead`, `performPost`, `performPut`, `performPatch` and `performDelete` are available. + +## Batching requests {/* #batching-requests */} + +The same methods are available on an _instance_. Instance calls are queued and sent together. + +### The automatic batcher {/* #the-automatic-batcher */} + +`Fetch.getBatcher()` returns a shared singleton which automatically executes the queue 50ms after the last request was added. + +```js +import Fetch from 'core/fetch'; + +const batcher = Fetch.getBatcher(); +const [mammals, reptiles] = await Promise.all([ + batcher.performGet('mod_example', 'animals', {params: {type: 'mammal'}}), + batcher.performGet('mod_example', 'animals', {params: {type: 'reptile'}}), + batcher.performDelete('mod_example', `animals/${pig.id}`), +]); +``` + +Each call returns a `Promise` resolving to a standard `Response` for that sub-request, and failures reject only the individual promise. + +### A manual batcher {/* #a-manual-batcher */} + +Create an instance yourself and call `execute()` when ready. An optional constructor argument sets an auto-execute delay in milliseconds. + +```js +const batcher = new Fetch(); +const results = Promise.all([ + batcher.performGet('mod_example', 'animals', {params: {type: 'mammal'}}), + batcher.performGet('mod_example', 'animals', {params: {type: 'reptile'}}), +]); + +batcher.execute(); +await results; +``` + +## Behaviour notes {/* #behaviour-notes */} + +- If a batch contains a single request, the batch endpoint is _not_ used; it is sent as a normal request. +- The queue is flushed automatically once it holds more than 20 requests. +- If the `$CFG->batch_fetch_requests` setting (JS: `M.cfg.batchFetchRequests`, default `true`) is disabled, each request is executed immediately instead of being queued. + +## Cacheable requests bypass the batcher {/* #cacheable-requests-bypass-the-batcher */} + +Requests made with a `cachekey` (see [route caching](../../../apis/subsystems/routing/caching.md)) are sent individually, even via a batcher. Hiding them inside a generic `$batch` request would prevent browsers and reverse proxies caching each URL. + +```js +const response = await Fetch.getBatcher().performGet('core', 'strings/en/core', {cachekey: M.cfg.jsrev}); +``` + +To batch these requests anyway, set `$CFG->batch_api_asset_requests = true` (JS: `M.cfg.batchApiAssetRequests`). + +## See also {/* #see-also */} + +- [AJAX](./index.md) +- [Routing: Caching](../../../apis/subsystems/routing/caching.md)