@budarin/psw-plugin-opfs-serve-range
v5.0.1
Published
Service Worker plugin: serve HTTP Range requests from OPFS files
Maintainers
Readme
@budarin/psw-plugin-opfs-serve-range
Плагины и утилиты для @budarin/pluggable-serviceworker. Большие файлы хранятся в Origin Private File System (OPFS), а запросы по диапазону байтов (HTTP Range) обслуживаются напрямую из файлов: можно читать любую часть файла без последовательного прохода, в отличие от Cache API. Лимиты по квоте, вытеснение по LRU и список закреплённых ресурсов настраиваются вами. Поддерживается сценарий «скачать в фоне и смотреть офлайн» через Background Fetch.
Сторонние ресурсы не поддерживаются: загрузка и кеширование только same-origin. При запросе к другому origin браузер возвращает opaque response: тело ответа недоступно для чтения и записи в OPFS, поэтому плагины не качают такие ресурсы.
Подробнее о поведении кеша (лимиты, LRU, эвикция, оповещения): docs/opfs-cache-behavior.ru.md.
Оглавление
- Установка
- Быстрый старт
- Сценарии использования
- Справочник: плагины (сервис-воркер)
- Справочник: клиентский API
- Формат хранения в OPFS
- Свой плагин записи в OPFS
- Требования
- Лицензия
Установка
pnpm add @budarin/psw-plugin-opfs-serve-rangeБыстрый старт
Типичный сценарий: пользователь сам выбирает, что скачать (кнопка «Скачать для офлайна»). Подключают opfsServeRange (отдаёт диапазоны байтов из OPFS, если файл в кеше) и opfsBackgroundFetch (пишет в OPFS по завершении Background Fetch). Для разных кешей — разные folderName (например видео, аудио).
- opfsServeRange — отдаёт запрошенные диапазоны байтов из OPFS, если файл уже есть в кеше.
- opfsBackgroundFetch — когда пользователь запускает загрузку через Background Fetch (например со страницы через
startDownloadAssetsToOpfs), готовые ответы записываются в OPFS; последующие запросы обслуживаются из кеша через opfsServeRange.
Пример с отдельными кешами для видео и аудио (по два плагина на папку):
import { initServiceWorker } from '@budarin/pluggable-serviceworker';
import { createOpfsServeAndBackgroundFetchPlugins, createOpfsServeAndNetworkCachePlugins } from '@budarin/psw-plugin-opfs-serve-range';
initServiceWorker(
[
createOpfsServeAndBackgroundFetchPlugins({
folderName: 'video-cache',
include: ['*.mp4', '*.webm'],
}),
createOpfsServeAndNetworkCachePlugins({
folderName: 'audio-cache',
include: ['*.mp3', '*.m4a'],
loadOnlyOnWiFi: true,
}),
],
{ version: '1.0.0' }
);Плагины, которые используют один и тот же кеш (одну папку), должны иметь один и тот же folderName и согласованные опции. Важно: в сценарии «кеш при первом запросе» после первого Range (206) запускается фоновый full GET в OPFS; именно он и может быть пропущен на cellular (учитывает saveData и loadOnlyOnWiFi). В примере audio-cache это включено через loadOnlyOnWiFi: true. Для кеша, заполняемого через Background Fetch (в примере video-cache), на странице вызывают startDownloadAssetsToOpfs({ folderName, assets, title }); по завершении эти URL обслуживаются из кеша.
Подробнее — в разделе «Скачать для офлайна».
Сценарии использования
Кеш при первом запросе
Альтернатива Background Fetch: кеш заполняется при первом запросе к ресурсу (например при первом проигрывании видео), без отдельной кнопки «Скачать». Регистрируют opfsServeRange и opfsRangeFromNetworkAndCache (без opfsBackgroundFetch) для этой папки. Если файла нет в OPFS, запрос уходит в сеть, ответ отдаётся клиенту потоком и в фоне сохраняется в OPFS. Дальнейшие запросы обслуживаются из кеша. Загрузка прерывается при закрытии вкладки или обрыве сети. Используйте этот сценарий, когда не нужна кнопка «Скачать для офлайна» и нужна автоматическая загрузка в кеш при первом обращении.
Сервер должен поддерживать запросы по диапазону байтов (HTTP Range): ответ 206 и Content-Length. Квота, вытеснение по LRU и закреплённые ресурсы — в Справочнике по плагинам.
«Скачать для офлайна» (Background Fetch)
В этом сценарии пользователь нажимает кнопку вроде «Скачать для офлайна»; выбранные файлы загружаются в фоне, вкладку можно закрыть. После завершения загрузки приложение обращается к тем же адресам с запросом диапазона байтов (Range) и получает данные уже из кеша.
Сервис-воркер
В сервис-воркере регистрируют opfsServeRange и opfsBackgroundFetch с одним folderName на кеш. Для каждого кеша — свой folderName (например видео и аудио — разные папки).
import { initServiceWorker } from '@budarin/pluggable-serviceworker';
import { createOpfsServeAndBackgroundFetchPlugins } from '@budarin/psw-plugin-opfs-serve-range';
initServiceWorker(
[
createOpfsServeAndBackgroundFetchPlugins({
folderName: 'video-cache',
include: ['*.mp4', '*.webm'],
debug: true,
}),
createOpfsServeAndBackgroundFetchPlugins({
folderName: 'audio-cache',
include: ['*.mp3', '*.m4a'],
}),
],
{ version: '1.0.0' }
);Клиент (страница)
Удобнее всего использовать высокоуровневый API: одна функция запускает загрузку. Она возвращает обещание (промис), которое выполняется, когда ресурсы записаны в OPFS, или отклоняется при ошибке или отмене пользователем. При этом:
- системный UI Background Fetch (например, уведомление о загрузке на Android), если он поддерживается браузером, показывает прогресс и кнопки управления;
- клиентский код получает детализированный прогресс и статусы через колбеки и подписки и может строить собственный расширенный интерфейс состояния загрузки.
import { startDownloadAssetsToOpfs } from '@budarin/psw-plugin-opfs-serve-range/client';
async function downloadForOffline(
assets: string[],
title: string,
totalDownloadSizeInBytes?: number
) {
try {
const result = await startDownloadAssetsToOpfs({
folderName: 'video-cache',
assets,
title,
totalDownloadSizeInBytes,
onProgress: (downloaded, total) =>
console.log(`${downloaded}/${total}`),
signal: myAbortController.signal,
});
console.log('Закешировано:', result.assets);
} catch (e) {
if (e && typeof e === 'object' && 'reason' in e) {
console.warn('Загрузка', (e as { reason: string }).reason);
} else throw e;
}
}Если вы используете React, в пакете есть хук: он хранит состояние загрузки (статус, прогресс по байтам и по файлам, ошибки, результат). При размонтировании компонента хук только перестаёт обновлять состояние, загрузка в фоне продолжается; отменить её можно вызовом reset(). Если пользователь вернулся на страницу и снова нажал «Скачать» с тем же набором файлов, загрузка с таким набором может уже идти — тогда новый вызов не создаёт дубликат, а подписывается на неё (attach), и промис выполнится при завершении той загрузки.
import { useDownloadAssetsToOpfs } from '@budarin/psw-plugin-opfs-serve-range/client/react';
function DownloadButton() {
const { startDownload, status, progress, fileProgress, error, data, reset } = useDownloadAssetsToOpfs();
return (
<>
<button onClick={() => startDownload({ folderName: 'video-cache', assets: ['/assets/video.mp4'], title: 'Видео' })}>
Скачать
</button>
{status === 'pending' && progress && <span>{progress.downloaded}/{progress.total}</span>}
{status === 'success' && data && <span>Готово: {data.assets?.join(', ')}</span>}
{status === 'failure' && error && <span>Ошибка</span>}
</>
);
}Если нужна своя логика (свой идентификатор загрузки, своя фильтрация или свои колбеки), сценарий можно собрать из низкоуровневых функций. Подробности — в разделе Справочник: клиентский API. Запись в OPFS по-прежнему выполняет плагин opfsBackgroundFetch в сервис-воркере; идентификатор загрузки должен начинаться с префикса opfs-ranges- (константа OPFS_BACKGROUND_FETCH_ID_PREFIX в пакете).
Справочник: плагины (сервис-воркер)
Высокоуровневые фабрики
createOpfsServeAndBackgroundFetchPlugins(options: {
folderName: string;
order?: number; // по умолчанию 0
include: string[];
exclude?: string[];
debug?: boolean; // по умолчанию false
logger?: Logger;
pinned?: string[];
rangeResponseCacheControl?: string;
}): Plugin[]createOpfsServeAndNetworkCachePlugins(options: {
folderName: string;
order?: number; // по умолчанию 0
include: string[];
exclude?: string[];
debug?: boolean; // по умолчанию false
logger?: Logger;
pinned?: string[];
loadOnlyOnWiFi?: boolean;
rangeResponseCacheControl?: string;
}): Plugin[]Фабрика возвращает массив плагинов. initServiceWorker (pluggable-serviceworker) разворачивает вложенные массивы плагинов, поэтому результат можно передавать без спреда.
У каждого плагина в опциях обязательны folderName: string и include: string[] (непустой массив). Одна папка = один кеш. include и exclude могут быть glob-паттернами, pathname'ами или полными URL (например ['*.mp4', '/video/*'], ['/assets/video.mp4'] или ['https://example.com/video/*']). При инициализации полные URL приводятся к pathname (same-origin) или отбрасываются (cross-origin). Если после нормализации include оказался пустым (например в include были только cross-origin URL), фабрика возвращает undefined и плагин не создаётся. Когда приходит запрос: если URL запроса с другого origin — запрос не обрабатывается (ни отдача из кеша, ни запись). Если same-origin — по pathname URL запроса сопоставляем с (нормализованными) паттернами: например глоб /video/* совпадает с запросом на https://example.com/video/1.mp4. Плагины, которые обслуживают один и тот же кеш (например opfsServeRange + opfsBackgroundFetch или opfsServeRange + opfsRangeFromNetworkAndCache для сценария «кеш при первом запросе»), должны использовать один и тот же folderName. Очистить кеш: clearOpfsCache(folderName).
Плоское хранилище (flat store): все файлы лежат в одном каталоге; folderName хранится только в метаданных файла и используется для фильтрации (отдача, list, clear). Размер кеша ограничен одной глобальной долей квоты origin: getGlobalMaxCacheFraction() (по умолчанию 0.5) и setGlobalMaxCacheFraction(fraction) задают лимит; getMaxCacheFraction() (без аргументов) возвращает его. Задайте лимит до регистрации плагинов; не задавайте 1.0, если приложение использует и другие хранилища (Cache API, IndexedDB и т.д.).
В средах, где OPFS недоступен, фабрики плагинов возвращают undefined.
Утилиты (SW)
normalizePatternList(patterns: string[] | undefined, baseOrigin: string): { list: string[] | undefined; dropped: NormalizePatternListDropped }
emitDroppedPatternWarnings(dropped: NormalizePatternListDropped, logger: { warn?: (message: string) => void }): void
getRoot(): Promise<FileSystemDirectoryHandle>
getFlatStoreDir(): Promise<FileSystemDirectoryHandle>
getOpfsDir(root: FileSystemDirectoryHandle, create: boolean, folderName: string): Promise<FileSystemDirectoryHandle>
clearOpfsCache(folderName: string): Promise<void>
registerFolderConfig(folderName: string): void
getGlobalMaxCacheFraction(): number
setGlobalMaxCacheFraction(fraction: number): void
getMaxCacheFraction(): number
getCacheLimit(estimate: StorageEstimate): numberПри инициализации полные URL приводятся к pathname; cross-origin и невалидные попадают в dropped. Фабрики плагинов выводят предупреждения через logger. getGlobalMaxCacheFraction по умолчанию 0.5; setGlobalMaxCacheFraction ожидает (0, 1], при неверном значении — throw.
opfsServeRange — читает файлы из OPFS и отдаёт запрошенные диапазоны байтов (206), потоково по чанкам из файла.
opfsServeRange(options: {
folderName: string;
order?: number; // по умолчанию 0
include: string[];
exclude?: string[];
debug?: boolean; // по умолчанию false
logger?: Logger;
rangeResponseCacheControl?: string;
}): Plugin | undefinedopfsRangeFromNetworkAndCache — обрабатывает запросы, которые opfsServeRange не отдал из кеша: загружает из сети, сразу отдаёт ответ клиенту и при возможности сохраняет файл в OPFS в фоне. Загрузка прерывается при закрытии вкладки или обрыве сети.
opfsRangeFromNetworkAndCache(options: {
folderName: string;
order?: number; // по умолчанию 0
include: string[];
exclude?: string[];
debug?: boolean; // по умолчанию false
logger?: Logger;
pinned?: string[];
loadOnlyOnWiFi?: boolean;
}): Plugin | undefinedopfsBackgroundFetch — при успешном завершении Background Fetch записывает ответы в OPFS; последующие запросы по диапазону байтов обслуживает opfsServeRange. Учитываются только загрузки с id, начинающимся с OPFS_BACKGROUND_FETCH_ID_PREFIX (opfs-ranges-). В обработчике сообщений вызывает плагин ответа по фильтру (см. opfsBackgroundFetchFilter).
opfsBackgroundFetch(options: {
folderName: string;
order?: number; // по умолчанию 0
include: string[];
exclude?: string[];
debug?: boolean; // по умолчанию false
logger?: Logger;
pinned?: string[];
}): Plugin | undefinedopfsBackgroundFetchFilter — обрабатывает только сообщения от страницы: на запрос фильтра (OPFS_REQUEST_GET_BACKGROUND_FETCH_FILTER) отвечает текущими include и exclude. Серверная пара клиентской утилиты getBackgroundFetchFilter(). При полном стеке регистрировать отдельно не нужно (opfsBackgroundFetch обрабатывает ответ по фильтру сам). В кастомном SW зарегистрируйте с теми же include и exclude, что и логика загрузки. Фильтр так же нормализует include/exclude (полные URL → pathname или отбрасываются). Возвращает undefined, если после нормализации include пуст.
opfsBackgroundFetchFilter(options: {
include: string[];
exclude?: string[];
logger?: Logger;
}): Plugin | undefinedopfsRegisteredFolders — обрабатывает только сообщения от страницы: на запрос OPFS_REQUEST_GET_REGISTERED_FOLDERS отвечает списком папок, зарегистрированных в SW через registerFolderConfig. Серверная пара клиентской утилиты getRegisteredFolders(). startDownloadAssetsToOpfs использует этот список и отклоняет загрузку, если указанная папка не зарегистрирована. Подключайте в кастомном SW, если используете проверку папки на клиенте.
opfsRegisteredFolders(): Plugin | undefinedЗакреплённые ресурсы (опция pinned): массив масок (glob) по адресам. Ресурсы, подходящие под эти маски, не вытесняются при нехватке места (LRU). Поддерживается обоими плагинами, которые пишут в OPFS: opfsRangeFromNetworkAndCache и opfsBackgroundFetch.
Справочник: клиентский API
Entry point: @budarin/psw-plugin-opfs-serve-range/client. React-хук: @budarin/psw-plugin-opfs-serve-range/client/react.
Загрузка assets в OPFS
Условие: в SW должны быть зарегистрированы плагины, отвечающие на запросы клиента: opfsBackgroundFetchFilter (или opfsBackgroundFetch) — для фильтра include/exclude; opfsRegisteredFolders — для списка зарегистрированных папок. Иначе startDownloadAssetsToOpfs не получит фильтр или список папок и загрузка будет отклонена с ошибкой (см. коды OPFS_ERROR_*).
getBackgroundFetchFilter()
getBackgroundFetchFilter(): Promise<{ include?: string[]; exclude?: string[] }>Запрашивает у SW текущие include и exclude. В SW должен быть зарегистрирован opfsBackgroundFetchFilter или opfsBackgroundFetch. Резолвится объектом с фильтром; пустой объект при таймауте или отсутствии ответа от SW.
getRegisteredFolders()
getRegisteredFolders(): Promise<FolderName[]>Запрашивает у SW список папок, зарегистрированных через registerFolderConfig. В SW должен быть зарегистрирован плагин opfsRegisteredFolders. При таймауте или отсутствии ответа возвращает пустой массив (тогда startDownloadAssetsToOpfs отклонит загрузку с OPFS_ERROR_SERVICE_WORKER_UNAVAILABLE).
filterAssetsForOpfs(assets, include?, exclude?)
filterAssetsForOpfs(
assets: string[],
include?: string[],
exclude?: string[]
): string[]estimateAssetsSizeInBytes(assets)
estimateAssetsSizeInBytes(
assets: string[]
): Promise<{ totalSize: number; sizes: Record<string, number> }>Отправляет HEAD-запросы к указанным ресурсам (same-origin) и пытается прочитать заголовок Content-Length для оценки размеров.
- assets — список pathname'ов ресурсов (например,
['/video/1.mp4']). - Возвращает объект:
- totalSize — суммарный размер в байтах по всем ресурсам, для которых удалось получить
Content-Length(для остальных — 0). - sizes — словарь
pathname → размер в байтах(0, если размер определить не удалось).
- totalSize — суммарный размер в байтах по всем ресурсам, для которых удалось получить
Хелпер работает только для same-origin ресурсов (как и весь пакет). Если сервер не отдаёт Content-Length или ответ неуспешен, размер считается равным 0, при этом промис не отклоняется.
Пример использования вместе с startDownloadAssetsToOpfs:
import {
startDownloadAssetsToOpfs,
estimateAssetsSizeInBytes,
} from '@budarin/psw-plugin-opfs-serve-range/client';
async function downloadForOfflineWithEstimatedSize(assets: string[], title: string) {
const { totalSize } = await estimateAssetsSizeInBytes(assets);
await startDownloadAssetsToOpfs({
folderName: 'video-cache',
assets,
title,
totalDownloadSizeInBytes: totalSize,
});
}startDownloadAssetsToOpfs(options)
При успешном завершении в системном списке загрузок остаётся переданный title (при ошибке/отмене заголовок обновляется). Задавайте осмысленный title, чтобы записи различались.
Логика перед запуском: запрашивается список зарегистрированных в SW папок (getRegisteredFolders()); если папка folderName не в списке или список пуст — промис отклоняется (OPFS_ERROR_FOLDER_NOT_REGISTERED или OPFS_ERROR_SERVICE_WORKER_UNAVAILABLE). Затем из списка assets (после фильтра include/exclude) исключаются те, что уже качаются в других активных Background Fetch (pathname берутся из matchAll() по каждой активной регистрации с префиксом opfs-ranges-). Затем исключаются те, что уже есть в OPFS (один вызов listOpfsCachedResources(folderName)). Порядок такой специально: сначала «в процессе», потом «уже в кеше» — чтобы не пропустить только что завершившуюся загрузку. В загрузку уходит только то, что осталось. Если ничего не осталось, промис сразу выполняется с written: assetsToUse (ничего не качаем). Идентификатор загрузки считается по набору pathname'ов идемпотентно (getOpfsBackgroundFetchId). Если с тем же набором загрузка уже идёт, новый вызов не создаёт вторую, а подписывается на уже идущую (attach); промис выполнится при её завершении.
startDownloadAssetsToOpfs(options: StartDownloadAssetsToOpfsOptions): Promise<DownloadAssetsToOpfsResult>
interface StartDownloadAssetsToOpfsOptions {
folderName: string;
assets: string[];
title?: string;
icons?: { src: string; sizes?: string; type?: string }[];
totalDownloadSizeInBytes?: number;
onProgress?: (downloaded: number, total: number) => void;
onFileWritten?: (loadedAssets: string[], totalCount: number) => void;
signal?: AbortSignal;
}
interface DownloadAssetsToOpfsResult {
registrationId: string;
assets?: string[];
written?: string[];
failedOrSkipped?: string[];
filteredOut?: string[];
}
interface DownloadAssetsToOpfsRejected {
registrationId: string;
reason: 'fail' | 'abort';
}Для отображения в UI обрабатывайте ошибку через try/catch или .catch() и при необходимости проверяйте error?.code.
- useDownloadAssetsToOpfs() — React-хук. Возвращает функцию запуска загрузки, статус, прогресс по байтам и по файлам, error (заполняется при ошибке — можно показывать в UI), результат и функцию сброса. startDownload при ошибке отклоняет промис (те же коды); ошибка также попадает в состояние error. Для проверки кода используйте
error?.code(OPFS_ERROR_FOLDER_NOT_REGISTERED и т.д.). При размонтировании загрузка не отменяется; отменить можно только вызовом reset(). При повторном нажатии «Скачать» с тем же набором файлов происходит подписка на уже идущую загрузку (attach). Требуется установленный React (peer dependency).
useDownloadAssetsToOpfs(): {
startDownload: (options: Omit<StartDownloadAssetsToOpfsOptions, 'signal'>) => Promise<void>;
status: 'idle' | 'pending' | 'success' | 'failure' | 'aborted';
progress: { downloaded: number; total: number } | null;
fileProgress: { loadedAssets: string[]; totalCount: number } | null;
error: StartDownloadError | null;
data: DownloadAssetsToOpfsResult | null;
reset: () => void;
}Низкоуровневый API (без startDownloadAssetsToOpfs и хука)
Если вы не используете startDownloadAssetsToOpfs или хук и собираете сценарий вручную, понадобятся две вещи. Во-первых, функции запуска загрузки и проверки поддержки: startBackgroundFetch и isBackgroundFetchSupported из пакета pluggable-serviceworker (клиентский подмодуль client/background-fetch). Во-вторых, подписка на сообщения от сервис-воркера — об успешном завершении загрузки, об ошибке, об отмене и о записи каждого файла в OPFS; соответствующие функции подписки (onOPFSBackgroundFetchCompleted, onOPFSBackgroundFetchFailed, onOPFSBackgroundFetchAborted, onOPFSBackgroundFetchFileWritten) экспортируются из этого пакета. Идентификатор загрузки обязан быть сформирован при помощи getOpfsBackgroundFetchId(assets, folderName). Id уникален для папки, поэтому один и тот же набор ресурсов в разных кешах не даёт коллизий; плагин в SW обрабатывает только события своей папки.
Подписки на сообщения от сервис-воркера
Каждая функция принимает обработчик и возвращает функцию для отписки. В каком случае отправляется то или иное сообщение — в описании поведения кеша.
Список отменённых (skip list): при потоковой записи в OPFS может произойти превышение квоты (QuotaExceeded). Если к моменту ошибки файл оказался не меньше всего кеша, вытеснять старые файлы бесполезно — места всё равно не хватит. Такой URL заносят в список отменённых (в памяти сервис-воркера на время его жизни). При следующих запросах к этому адресу плагин не пытается кешировать ответ и отправляет onOPFSSkipQuotaExceeded, чтобы клиент мог показать предупреждение.
Назначение каждой подписки:
- onOPFSQuotaExceeded — квота исчерпана при записи в OPFS; URL при этом может быть занесён в список отменённых (см. выше).
- onOPFSWriteSkipped — запись отменена до начала: при известном размере файла проверка места не прошла, файл не помещается даже после эвикции.
- onOPFSEvictionCompleted — эвикция завершена.
- onOPFSWriteFailed — ошибка записи.
- onOPFSSkipQuotaExceeded — пришёл запрос к URL из списка отменённых; плагин не кеширует, только оповещает.
- onOPFSBackgroundFetchFailed — Background Fetch завершился с ошибкой.
- onOPFSBackgroundFetchAborted — Background Fetch отменён.
- onOPFSBackgroundFetchCompleted — Background Fetch успешно завершён, ресурсы в OPFS.
- onOPFSBackgroundFetchFileWritten — очередной файл записан в OPFS (прогресс по файлам).
- onOPFSRangeCacheFetchStarted — плагин opfsRangeFromNetworkAndCache начал фоновую загрузку в кеш (сценарий «кеш при первом запросе»). По нему можно включить индикатор «идёт фоновая загрузка».
- onOPFSRangeCacheFetchAllDone — все такие фоновые загрузки завершены. По нему можно выключить индикатор.
У всех этих функций один и тот же интерфейс. Общий тип обработчика и payload (экспортируются из пакета):
type OpfsMessageHandler = (event: MessageEvent & { data: { type: string } & OpfsMessagePayload }) => void;
interface OpfsMessagePayload {
url?: string;
size?: number;
limit?: number;
reason?: string;
registrationId?: string;
assets?: string[];
written?: string[];
failedOrSkipped?: string[];
asset?: string;
loadedAssets?: string[];
totalCount?: number;
}Какие поля есть в event.data, зависит от типа сообщения (см. список выше и описание поведения кеша). Константы типов сообщений: OPFS_MSG_*, OPFS_REQUEST_GET_BACKGROUND_FETCH_FILTER, OPFS_RESPONSE_BACKGROUND_FETCH_FILTER.
Утилиты кэша
Эти функции вызываются на странице и отправляют запросы в сервис-воркер (плагин opfsCacheControl). SW выполняет операцию в OPFS и инвалидирует свои in-memory кэши. Таймаут запроса 2 с. folderName должен совпадать с папкой, зарегистрированной в SW. Если папка не зарегистрирована, SW отвечает ошибкой: listOpfsCachedResources и hasInOpfsCache в этом случае возвращают [] и false соответственно; deleteFromOpfsCache не требует folderName (файл удаляется по URL из плоского хранилища). clearOpfsCache требует folderName; если папка не зарегистрирована, промис отклоняется с opfs: folder not registered. При использовании createOpfsServeAndBackgroundFetchPlugins или createOpfsServeAndNetworkCachePlugins плагин opfsCacheControl уже входит в набор — list/has/delete/clear работают «из коробки». Очистить кеш со страницы: clearOpfsCache(folderName) (клиент шлёт запрос CLEAR).
listOpfsCachedResources(folderName)
listOpfsCachedResources(folderName: string): Promise<OpfsCachedResource[]>
interface OpfsCachedResource {
url: string;
size: number;
type: string | undefined;
lastModified: string | undefined;
}hasInOpfsCache(url, folderName)
hasInOpfsCache(url: string, folderName: string): Promise<boolean>deleteFromOpfsCache(url)
deleteFromOpfsCache(url: string): Promise<void>Переподключение плеера к OPFS после загрузки файла
Когда файл (текущий источник video/audio) записан в OPFS через Background Fetch, можно переподключить плеер к тому же URL, чтобы последующие запросы шли из OPFS (например, для мгновенного перемотки). Используйте onOPFSBackgroundFetchFileWritten и reconnectPlayerOnFileLoadedIntoOpfs. Для видео во время смены источника показывается последний кадр (оверлей), оверлей убирается только когда новый источник уже отображается (события playing или seeked), поэтому нет чёрного экрана. Обёртка сохраняет layout видео (margin, box-sizing, display), так что контент не смещается. Если у видео не были заданы явные размеры, на время переключения они фиксируются, затем снимаются. Для аудио оверлей не используется.
reconnectPlayerOnFileLoadedIntoOpfs(element, payload, folderName, options?)
Вызывайте из обработчика onOPFSBackgroundFetchFileWritten. Если payload.asset совпадает с текущим источником элемента и файл есть в OPFS, переподключает плеер и восстанавливает состояние воспроизведения. Типы: FileWrittenPayload, ReconnectPlayerOnFileLoadedIntoOpfsOptions (ниже).
reconnectPlayerOnFileLoadedIntoOpfs(
element: HTMLMediaElement,
payload: FileWrittenPayload,
folderName: FolderName,
options?: ReconnectPlayerOnFileLoadedIntoOpfsOptions
): Promise<void>
interface FileWrittenPayload {
asset?: string;
}
interface ReconnectPlayerOnFileLoadedIntoOpfsOptions {
logger?: Logger;
debug?: boolean; // по умолчанию false
}
interface UseReconnectPlayerOnFileLoadedIntoOpfsOptions extends ReconnectPlayerOnFileLoadedIntoOpfsOptions {
folderName: FolderName;
}Пример (ванильный TS):
import {
onOPFSBackgroundFetchFileWritten,
reconnectPlayerOnFileLoadedIntoOpfs,
} from '@budarin/psw-plugin-opfs-serve-range/client';
const video = document.querySelector('video');
const folderName = 'video-cache';
if (video) {
const unsubscribe = onOPFSBackgroundFetchFileWritten((event) => {
reconnectPlayerOnFileLoadedIntoOpfs(video, event.data, folderName).catch(() => {});
});
}Пример (React):
import { useRef } from 'react';
import { useReconnectPlayerOnFileLoadedIntoOpfs } from '@budarin/psw-plugin-opfs-serve-range/client/react';
function VideoPlayer() {
const videoRef = useRef<HTMLVideoElement | null>(null);
useReconnectPlayerOnFileLoadedIntoOpfs(videoRef, { folderName: 'video-cache' });
return <video ref={videoRef} src="/video/lesson-1.mp4" controls />;
}Формат хранения в OPFS
Плоское хранилище (flat store, v4): все закешированные файлы лежат в одном каталоге под корнем плагина. Путь: корень OPFS → OPFS_PLUGIN_ROOT_DIR_NAME (по умолчанию .opfs-serve-range) → один каталог (подкаталогов по folderName нет). Имя файла = ключ = hex(SHA-256(URL)) (64 символа). Один файл на URL. folderName не входит в путь; он хранится в метаданных в футере файла и используется только для фильтрации (какой плагин отдаёт файл, list/clear по папке).
Структура файла: тело ресурса, затем футер (4 байта длины JSON + JSON метаданных). Метаданные: url, size, type, etag, lastModified, lastAccessed, evictable, folderName. Эвикция управляется индексом только в памяти (файла _eviction_index.json на диске нет); индекс при необходимости заполняется сканом каталога. getFlatStoreDir() возвращает этот единственный каталог; getOpfsDir(root, create, folderName) — обёртка совместимости, возвращает тот же каталог.
Если отдаёте файл целиком (ответ 200 без Range), отдавайте только тело, без футера: по футеру вычислите размер тела и отдайте file.slice(0, bodySize). Плагин opfsServeRange отдаёт только диапазоны тела (ответ 206), футер в ответ не входит.
Свой плагин записи в OPFS
Если нужно записывать в OPFS по своей логике, но в том же формате, что и плагины пакета:
getRoot(): Promise<FileSystemDirectoryHandle>
getOpfsDir(root: FileSystemDirectoryHandle, create: boolean, folderName: string): Promise<FileSystemDirectoryHandle>
urlToOpfsKey(url: string): Promise<string>
metadataFromResponse(response: Response, url: string): OpfsMetadata
writeToOpfs(
dir: FileSystemDirectoryHandle,
key: string,
bodyStream: ReadableStream<Uint8Array>,
metadata: OpfsMetadata,
options: WriteToOpfsOptions
): Promise<void>
interface OpfsMetadata {
url: string;
size: number;
type?: string;
etag?: string;
lastModified?: string;
lastAccessed?: number;
evictable?: boolean;
}
interface WriteToOpfsOptions {
folderName: string;
url?: string;
knownSize?: number;
}import {
getRoot,
getOpfsDir,
urlToOpfsKey,
writeToOpfs,
metadataFromResponse,
} from '@budarin/psw-plugin-opfs-serve-range';
const root = await getRoot();
const dir = await getOpfsDir(root, true, 'my-cache');
const key = await urlToOpfsKey(url);
const metadata = metadataFromResponse(response, url);
await writeToOpfs(dir, key, response.body, metadata, { folderName: 'my-cache' });Ответ от сервера может быть без заголовка Content-Length; при записи полного тела размер определяется путём подсчёта байт в теле. Чтобы при записи учитывались лимиты кеша (проверка места до записи, эвикция, оповещения и список отменённых), передавайте в writeToOpfs пятый аргумент options с полями url и knownSize.
Требования
Браузер с поддержкой OPFS (Chrome 108+, Edge 108+, Firefox 111+, Safari 16.4+) и безопасный контекст (страница по HTTPS).
Лицензия
MIT
