# Плоское хранилище (flat store) — план изменений v4

**Статус:** реализовано: плоский store, folderName в метаданных, эвикция только в памяти (без индексного файла), восстановление кэшей при пустом состоянии (ensureCachesPopulated при list/has), deleteFromOpfsCache(url) без folderName на клиенте.  
**Версия пакета:** мажорный ап (breaking change), миграция со старой схемы не выполняется.

---

## 1. Цели

- Один файл на URL в хранилище — без дубликатов при «добавлении в другую папку».
- Папки только логические (виртуальные): группировка и привязка настроек плагинов; обращение к ресурсу всегда по URL.
- Меньше хрупкости: нет отдельного индексного файла для эвикции.

---

## 2. Хранение

### 2.1 Структура на диске

- **Один плоский каталог** — все файлы в одном месте (корень плагина `.opfs-serve-range` или одна подпапка, например `files`). Подпапок по папкам нет.
- **Имя файла** = ключ = `hex(SHA-256(url))` (без изменений, `urlToOpfsKey`).
- **Один файл на ключ** — один URL → один файл в store.

### 2.2 Метаданные в футере файла

- Формат футера без изменений: `[тело][4 байта длина JSON (uint32 LE)][JSON]`.
- В `OpfsMetadata` добавляется поле **`folderName: string`** — единственная логическая папка, к которой относится файл (та, в контексте которой он был записан).
- Остальные поля без изменений: url, size, type, etag, lastModified, lastAccessed, evictable.

---

## 3. Папка (folderName)

**Зачем папки:** разным группам файлов нужны разные настройки кэширования (доля квоты, range cache, правила эвикции, паттерны include и т.д.). Папка — это имя такой группы; к одной папке привязан один набор настроек плагина.

Папка задаёт:

1. **Принадлежность файла** — в метаданных один `folderName` у каждого файла.
2. **Кто отдаёт** — плагин с данным `folderName` отдаёт файл только если `metadata.folderName === folderName` этого плагина.
3. **Границы list/clear** — list и clear работают по множеству файлов с данным `folderName` в метаданных.
4. **Группировка настроек** — к папке привязаны настройки плагина (range cache, cache-control и т.д.); при отдаче применяются настройки того плагина, чей folderName совпадает с файлом.

---

## 4. Запись

- **Перед записью:** проверить по ключу, есть ли уже файл в плоском store. Если есть — **не писать** (ни тела, ни метаданных), запись пропустить.
- **Если файла нет:** писать один раз, в метаданные записать `folderName` текущего плагина/операции.
- **Перед постановкой в очередь загрузки (опционально):** отфильтровать URL, для которых файл уже есть в store, чтобы не загружать повторно.

Места записи: Background Fetch (success), opfsRangeFromNetworkAndCache. В обоих — один общий плоский каталог, проверка существования по ключу, при отсутствии запись с `folderName`.

---

## 5. Чтение (serve)

- Запрос приходит по URL. Ключ = `urlToOpfsKey(url)`.
- Получить единственный плоский каталог (getFlatStoreDir / аналог getOpfsDir без выбора папки по пути).
- Найти файл по ключу. Если нет — вернуть undefined (следующий плагин / сеть).
- Прочитать метаданные (из кэша или из футера). Проверить: `metadata.folderName === folderName` текущего плагина. Если не совпадает — вернуть undefined.
- Если совпадает — отдавать ответ, применяя настройки этого плагина (range cache, cache-control и т.д.).

---

## 6. Эвикция

- **Один общий пул:** одна квота на всё хранилище, один LRU по всем файлам (по lastAccessed).
- **Без отдельного индексного файла.** Состояние для эвикции (key → size, lastAccessed, evictable) собирается при скане store (чтение футеров) и хранится в памяти. При необходимости обновлять lastAccessed в футере файла при доступе (с троттлингом).

---

## 7. Восстановление кэшей

- При любой операции (serve, list, has, delete, clear), если кэш метаданных (и связанные структуры) пуст:
  1. Зайти в плоский store.
  2. Если есть хотя бы один файл — один раз обойти все файлы, прочитать футеры.
  3. Заполнить из них кэш метаданных и состояние для эвикции.
  4. Выполнить запрошенную операцию по заполненным кэшам.
- Если store пустой — кэши остаются пустыми, операция возвращает пустой результат / «не найдено».

---

## 8. Утилиты управления (API)

- **listOpfsCachedResources(folderName)** — список ресурсов «в папке»: по кэшу метаданных, фильтр по `metadata.folderName === folderName`. При пустом кэше — сначала восстановление кэшей из store (п. 7).
- **hasInOpfsCache(url, folderName)** — файл существует и `metadata.folderName === folderName`.
- **deleteFromOpfsCache(url)** — удалить файл по ключу; **параметр папки не нужен** (файл один).
- **clearOpfsCache(folderName)** — удалить с диска все файлы, у которых в метаданных `folderName` совпадает с указанным.

---

## 9. Изменения в коде (сводка)

- **opfsUtil:** убрать физические подпапки по folderName. Ввести один плоский каталог (getFlatStoreDir или переосмыслить getOpfsDir). Реестр папок (folderName) сохранить для конфигов плагинов и для list/clear/has.
- **opfsFormat / OpfsMetadata:** добавить поле `folderName: string`.
- **opfsMetadataCache:** один глобальный кэш по ключу (key → metadata с folderName), не «кэш на папку». Либо один экземпляр, либо ключ кэша = key.
- **opfsEvictionIndex:** убрать запись/чтение отдельного индексного файла. Состояние только в памяти, заполняется при скане store; при доступе — обновление lastAccessed в футере файла (с троттлингом).
- **opfsServeRange:** получать плоский каталог; искать файл по ключу; проверять `metadata.folderName === folderName` плагина; отдавать только при совпадении.
- **opfsBackgroundFetch / opfsRangeFromNetworkAndCache:** перед записью проверять существование файла по ключу в плоском store; если есть — пропуск записи; иначе запись с `folderName`.
- **opfsWrite:** при записи принимать/записывать `folderName` в метаданные. Запись всегда в один плоский каталог.
- **opfsCacheControl:** list/has/clear — с folderName (фильтр по метаданным); delete — только url. При пустом кэше — вызов восстановления кэшей (п. 7), затем операция.
- **opfsLru / эвикция:** один общий лимит, один пул, LRU по всем файлам; данные из футеров при скане и из памяти.
- **Клиент:** deleteFromOpfsCache(url) — убрать второй аргумент folderName (breaking).

---

## 10. Версионирование и миграция

- Мажорная версия (например 4.0.0). Breaking change.
- Миграция со старой схемы (plugin_root/folderName/...) **не выполняется**. После обновления кэш считается пустым; при первом обращении выполняется скан (пустой store или новый плоский формат).
