# @budarin/psw-plugin-serve-range-requests

Плагин Service Worker для [@budarin/pluggable-serviceworker](https://www.npmjs.com/package/@budarin/pluggable-serviceworker), который обслуживает HTTP Range запросы для кэшированных файлов.

Если медиа кэшируется без поддержки Range-запросов, браузер вынужден загружать файл целиком в память и воспроизводить его с начала до конца. Перейти к другому месту (перемотка) при этом нельзя, а для больших файлов объём занятой памяти получается очень большим. На слабых или ограниченных по памяти устройствах это нередко приводит к подвисаниям, сбоям воспроизведения или даже падениям. Плагин добавляет корректную поддержку Range для кэшированного контента: читаются и отдаются только запрошенные диапазоны байтов, поэтому перемотка работает, а потребление памяти остаётся приемлемым.

### Зачем нужен этот плагин

Приложения для воспроизведения и просмотра больших медиафайлов обычно запрашивают данные по частям (HTTP Range), а не целиком. Если такие файлы лежат в обычном кеше, на каждый запрос одного сегмента из кеша будет извлекаться и передаваться клиенту **весь файл** — лишняя нагрузка на память и процессор, из‑за которой приложение может тормозить. Плагин отдаёт из кеша только запрошенный диапазон байтов, поэтому воспроизведение остаётся плавным, а потребление ресурсов — умеренным.

## Быстрый старт

В большинстве случаев достаточно выбрать [готовый пресет](#готовые-пресеты-опционально) и указать имя кеша. Остальные опции можно не трогать.

```typescript
import { initServiceWorker } from '@budarin/pluggable-serviceworker';
import {
    serveRangeRequests,
    VIDEO_PRESET,
} from '@budarin/psw-plugin-serve-range-requests';

initServiceWorker(
    [serveRangeRequests({ ...VIDEO_PRESET, cacheName: 'video-cache' })],
    { version: '1.0.0' }
);
```

Минимальный вариант без пресета — только обязательный параметр:

```typescript
serveRangeRequests({ cacheName: 'media-cache' });
```

Подробнее по сценариям (видео, карты, документы) — ниже. [Все опции](#все-опции) — в конце раздела.

Плагин обрабатывает только GET-запросы с заголовком Range, подходящие под фильтры include/exclude (если они заданы). Для каждого такого range-запроса плагин сам возвращает ответ: либо 206 из кэша (из полного файла или из кэша диапазонов), либо ответ из сети — при промахе кэша, при обходе бага Chromium (уже отдавали этот URL из сети в этой вкладке) или при наличии у клиента If-Range. Остальные запросы — без Range, не GET или не прошедшие фильтры — плагин не обрабатывает и передаёт следующим плагинам в цепочке.

## Когда что кешировать — по сценариям

Плагин кеширует два типа данных: **метаданные файлов** (размер, тип) и **готовые ответы по диапазонам**. Но смысл кеширования диапазонов сильно зависит от того, как пользователь работает с контентом.

### Видео и аудио: при перемотке кеш диапазонов не используется

При перемотке каждый переход (seek) запрашивает новый участок файла. Пользователь редко возвращается к тому же байтовому диапазону — плеер запрашивает другие смещения. Кеш диапазонов почти не попадает в повторные запросы и только занимает память.

Для видео и аудио ставьте **maxCachedRanges** в 0. Важнее: при перемотке браузер шлёт десятки запросов и отменяет старые. С `prioritizeLatestRequest: true` плагин отдаёт приоритет последнему запросу и отменяет лишнюю работу.

### Документы (PDF и т.п.): кеш полезен при листании

Пользователь листает туда-сюда — одни и те же страницы запрашиваются повторно. Кеш диапазонов помогает. `prioritizeLatestRequest: false` — без очередей, все запросы параллельно.

### Карты и тайлы: кеш очень полезен

При панорамировании и зуме запрашиваются одни и те же тайлы снова и снова. Один и тот же диапазон в pmtiles/mbtiles запрашивается многократно — кеш диапазонов даёт заметный выигрыш. Ставьте **maxCachedRanges** побольше (500–1000). `prioritizeLatestRequest: false` — без очередей, все запросы параллельно.

---

**Кеш диапазонов (`maxCachedRanges`, `maxCacheableRangeSize`)**
Плагин кеширует готовые ответы по диапазонам и метаданные файлов (размер, тип) для тех же URL. **maxCachedRanges** ограничивает, сколько ответов и записей метаданных держать. **maxCacheableRangeSize** — верхняя граница на один диапазон; диапазоны крупнее не кешируются (защита от переполнения). Для видео и аудио при перемотках ставьте **maxCachedRanges** в 0 — кеш не используется.

**Параллелизм (`maxConcurrentRangesPerUrl`)**
Действует только при `prioritizeLatestRequest: true`. Для видео и аудио оптимально 1 — очереди тормозят. При `false` ограничение не применяется — все запросы параллельно.

**Приоритет последнего запроса (`prioritizeLatestRequest`)**
`true` (по умолчанию) — для видео и аудио: приоритет последнему запросу, отмена лишних при новом. `false` — для карт и документов: без очередей, все запросы выполняются параллельно.

**Ответы 206 и кеш браузера**
По умолчанию плагин не выставляет заголовок `Cache-Control` для ответов 206. Чтобы разрешить кеширование в браузере, задайте **rangeResponseCacheControl** (например `'max-age=31536000, immutable'`). Чтобы запретить кеширование — `'no-store'`. Если опцию не задавать, заголовок не отправляется.

Для настройки ориентируйтесь на реальные запросы — смотрите Network в DevTools.

## Все опции

Опции сгруппированы по смыслу. Значения по умолчанию подходят для большинства сценариев.

### Основное

| Параметр    | Тип      | По умолчанию | Описание                                 |
| ----------- | -------- | ------------ | ---------------------------------------- |
| `cacheName` | `string` | —            | **Обязательно.** Имя кеша.               |
| `order`     | `number` | `-10`        | Опционально. Порядок выполнения плагина. |

### Кеш диапазонов и память

| Параметр                | Тип      | По умолчанию | Описание                                                                                                                                         |
| ----------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `maxCachedRanges`       | `number` | `100`        | Макс. число закешированных ответов по диапазонам (и записей метаданных). Для видео при перемотке обычно 0. Для карт/документов — 500–1000.       |
| `maxCacheableRangeSize` | `number` | `10MB`       | Макс. размер одного кешируемого диапазона; большие не кешируются.                                                                                |
| `maxTrackedUrls`        | `number` | `512`        | Максимум разных файлов (URL), по которым плагин учитывает ограничение одновременных запросов. Ограничивает рост памяти при большом числе файлов. |

### Параллелизм и приоритет

| Параметр                    | Тип       | По умолчанию | Описание                                                               |
| --------------------------- | --------- | ------------ | ---------------------------------------------------------------------- |
| `maxConcurrentRangesPerUrl` | `number`  | `4`          | Сколько диапазонов одного файла читать параллельно. Для видео часто 1. |
| `prioritizeLatestRequest`   | `boolean` | `true`       | Приоритет последнему запросу (видео/аудио). `false` — карты/документы. |

### Фильтры

| Параметр  | Тип        | По умолчанию | Описание                                      |
| --------- | ---------- | ------------ | --------------------------------------------- |
| `include` | `string[]` | —            | Маски файлов (glob). Только эти обрабатывать. |
| `exclude` | `string[]` | —            | Маски файлов (glob). Исключить из обработки.  |

### Восстановление и отдача

| Параметр                    | Тип        | По умолчанию | Описание                                                                                                                                                                                                                                                                                                 |
| --------------------------- | ---------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `restoreMissingToCache`     | `boolean`  | `true`       | При отсутствии файла в кеше: отдать запрос из сети и в фоне загрузить полный файл в кеш для следующих запросов.                                                                                                                                                                                          |
| `assets`                    | `string[]` | —            | Пути к ресурсам (например `/assets/Meeting.mp4`), не полные URL — при сборке домен неизвестен. Если список задан: фоновое восстановление в кеш только для запросов, путь которых входит в этот список. |
| `rangeResponseCacheControl` | `string`   | —            | Необязательный Cache-Control для ответов 206 (например `'max-age=31536000, immutable'` для кеширования в браузере). Если не задан, заголовок не отправляется.                                                                                                                                            |

### Отладка

| Параметр        | Тип       | По умолчанию | Описание        |
| --------------- | --------- | ------------ | --------------- |
| `enableLogging` | `boolean` | `false`      | Подробные логи. |

## Важные моменты

⚠️ **Не кешируйте огромные файлы и диапазоны** — мобильные устройства могут не справиться с ними. Данные диапазона читаются из потока кешированного файла последовательно (Cache API не поддерживает произвольный доступ), поэтому запрос диапазона в конце очень большого файла может выполняться долго; предпочтительны более мелкие ресурсы.

**Один источник на URL в рамках вкладки (обход бага Chromium)** — В Chromium при смене источника в середине воспроизведения (сначала сеть, затем ответы из кэша SW) медиа-пайплайн игнорирует ответ из кэша и воспроизведение падает с `PIPELINE_ERROR_READ`. Поэтому плагин для каждого URL в рамках вкладки **не переключает источник**: если по этому URL в этой вкладке уже отдавали из сети (например, файла не было в кэше), все последующие запросы по этому URL в той же вкладке идут в сеть до перезагрузки страницы, даже когда файл уже восстановлен в кэш. После перезагрузки при наличии файла в кэше отдача идёт из кэша. Подробнее: [Chromium bug #1026867](https://bugs.chromium.org/p/chromium/issues/detail?id=1026867), [тест-кейс phoboslab](https://phoboslab.org/files/bugs/chrome-serviceworker-video/).

## Пример использования

С пресетами (рекомендуется):

```typescript
import { initServiceWorker } from '@budarin/pluggable-serviceworker';
import {
    serveRangeRequests,
    VIDEO_PRESET,
    AUDIO_PRESET,
} from '@budarin/psw-plugin-serve-range-requests';

initServiceWorker(
    [
        serveRangeRequests({ ...VIDEO_PRESET, cacheName: 'video-cache' }),
        serveRangeRequests({ ...AUDIO_PRESET, cacheName: 'audio-cache' }),
    ],
    { version: '1.0.0' }
);
```

Ручная настройка (если нужно подкрутить под себя):

```typescript
serveRangeRequests({
    cacheName: 'media-cache',
    include: ['*.mp4', '*.webm', '*.mkv'],
    maxCachedRanges: 0,
    maxConcurrentRangesPerUrl: 1,
    prioritizeLatestRequest: true,
});
```

## Готовые пресеты (опционально)

Если не хотите настраивать параметры вручную, используйте готовые пресеты:

### Доступные пресеты

- **VIDEO_PRESET** – для медиаплееров: `*.mp4`, `*.webm`, `*.mkv`, `*.avi`, `*.mov`, `*.m4v`
- **AUDIO_PRESET** – для аудиоплееров: `*.mp3`, `*.flac`, `*.wav`, `*.m4a`, `*.ogg`, `*.aac`
- **MAPS_PRESET** – для карт и тайлов: `*.mbtiles`, `*.pmtiles`, `/tiles/*`, `/maps/*`, `*.mvt`
- **DOCS_PRESET** – для документов: `*.pdf`, `*.epub`, `*.djvu`, `*.mobi`, `*.azw3`

### Адаптивные пресеты

Все пресеты можно адаптировать под характеристики устройства. На устройствах с малым объёмом памяти и слабым процессором лимиты автоматически снижаются.

```typescript
import { getAdaptivePresets } from '@budarin/psw-plugin-serve-range-requests';

const { VIDEO_ADAPTIVE, AUDIO_ADAPTIVE, MAPS_ADAPTIVE, DOCS_ADAPTIVE } =
    getAdaptivePresets();
// Использовать так же: { ...VIDEO_ADAPTIVE, cacheName: 'video-cache' }
```

## Поддерживаемые Range форматы

- `bytes=0-499` — первые 500 байтов
- `bytes=500-999` — байты с 500 по 999
- `bytes=500-` — от 500 байта до конца
- `bytes=-500` — последние 500 байтов

## Как это работает

1. Проверяет заголовок Range в запросе
2. Ищет файл в указанном кеше
3. Если файл есть в кеше: плагин отдаёт из кэша только когда для этой вкладки и URL источник не меняется; иначе запрос передаётся в сеть (обход [бага Chromium #1026867](https://bugs.chromium.org/p/chromium/issues/detail?id=1026867)).
4. При отсутствии файла плагин отдаёт запрос из сети и при `restoreMissingToCache: true` в фоне загружает полный файл в кеш для следующих запросов **только если путь запроса входит в `assets`** (если `assets` задан; иначе — для любого URL). В этой вкладке все последующие запросы по этому URL идут в сеть до перезагрузки страницы.
5. Читает нужный диапазон из файла и возвращает HTTP 206 (Partial Content). При включённом кешировании диапазонов ответ сохраняется для повторного использования.

## 🤝 License

MIT © budarin
