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
78 changes: 78 additions & 0 deletions docs/apis/subsystems/routing/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Caching
tags:
- Routing
- Caching
- Performance
---

<Since version="6.0" issueNumber="MDL-82652" />

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
Comment on lines +41 to +42
```

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=<seconds>, 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.
81 changes: 81 additions & 0 deletions docs/guides/javascript/ajax/batcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
title: The Fetch batcher
tags:
- Javascript
- AJAX
- Fetch
- Routing
---

<Since version="5.2" issueNumber="MDL-82652" />

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)
Loading