fix/issue-78-audio-cache #79

Merged
EgorEast merged 10 commits from fix/issue-78-audio-cache into main 2026-09-06 22:22:38 +00:00
Owner

Summary

Чинит #78 (краш iOS PWA при возобновлении «кешировать все») и добавляет жёсткую гарантию: трек считается закешированным, только если он скачан полностью — частичные записи не сохраняются и не отдаются.

Первопричина

  • iOS WebKit крашится, когда PWA, убитая посреди cache.put, потом снова открывает/перезаписывает bucket (WebKit 260962 / 305539). Сценарий усугублялся тем, что веб при возобновлении заново fetch'ил и re-put'ил КАЖДЫЙ трек (натив пропускал закешированные файлы — веб нет).
  • Opaque-ответы (без CORS) нельзя ни провалидировать, ни буферизовать в JS — усечённая запись была неотличима от полной.

Изменения (10 коммитов)

  1. 246a585 веб: пропуск уже закешированных треков (parity с нативом), восстановление повреждённого bucket Cache Storage (open→delete→reopen), устранены unhandled rejection'ы.
  2. d8d70a2 веб: commit-manifest протокол — запись валидна ⇔ перечислена в __manifest__ (манифест пишется только после успешного put); SW отдаёт аудио из кеша только для «закоммиченных» записей; одноразовая миграция старых записей. Натив: атомарность подтверждена (.part + rename), предудаление осиротевшего .part того же URL.
  3. d3edd9e очистка «осиротевших» файлов после нештатного завершения: натив — sweep *.mp3.part при старте; веб — журнал активных скачиваний в AsyncStorage → адресное удаление без перечисления bucket (энумерация повреждённого bucket на iOS — риск crash-loop).
  4. 89b651a фикс гонки запуска: sweep выполняется ДО initializePlayer; журнал валидируется zod; веб-очистка также убирает строки из манифеста.
  5. 02a5fae guard в SW от пустого/битого манифеста; правки доков; отказоустойчивость нативного sweep; журнал очищается вместе с кешем; убран легаси-каст as.
  6. 7dd0c9e веб: реплей прогресса для позднего подписчика inflight-скачивания — мгновенно отдаёт текущий lastValue (parity с нативом).
  7. 40068ab веб: multi-tab-безопасная очистка через «живость» журнала — записи { url, sessionId, lastSeenAt } + heartbeat 10с; удаляются только протухшие (>120с) записи: свежие = живое скачивание в другом табе; stale+закоммиченные — сохраняются, stale+незакоммиченные — удаляются.
  8. 60a14b1 ревью-фиксы: keying журнала по (sessionId, url), margin STALE_MS под троттлинг фоновых табов, переписанный тест late-joiner'а (реально проверяет реплей на inflight-скачивании), failure-path тест.
  9. c83b6f4 веб: пути удаления/чтения больше не строят манифест — uncommitAudioUrl no-op без манифеста, hasCompleteAudio — только чтение + legacy-фолбэк.
  10. 3b288ed рефактор: summarizeAudioCache (легальное перечисление для статистики кеша, getCacheInfo) вынесен в модуль с предупреждением; гарантия в доках сфокусирована точно.

Гарантии / инварианты

  • cacheAudio завершается ⇒ onProgress(1) вызван
  • натив: .mp3 существует ⇔ трек полный (rename — точка коммита)
  • веб: запись отдаётся/хранится ⇔ присутствует в bucket И закоммичена в манифесте
  • веб: пути has/удаления/очистки/стартовые не перечисляют бакет; перечисление — только в commit-миграции и в summarizeAudioCache (getCacheInfo, сегодня без прод-вызовов, с предупреждением в коде)
  • сервисные пути не бросают исключений (безопасно на старте)

Проверка

  • 96 юнит-тестов (suites audio-cache + playlist), полный прогон — 157 suites / 1261 тест зелёные; lint 0 ошибок, tsc чистый, dependency-cruiser чистый, steiger (FSD) чистый.

Follow-ups (docs/debt.md)

  • Краш на уровне процесса WebKit при повреждённых записях не ловится из JS — нужна проверка на реальном iOS-устройстве (PWA: «кешировать все» → убить процесс → возобновить).
  • Старые записи доверяются один раз при миграции; идея chunked-кеша для дополнительной надёжности.
## Summary Чинит #78 (краш iOS PWA при возобновлении «кешировать все») и добавляет жёсткую гарантию: **трек считается закешированным, только если он скачан полностью** — частичные записи не сохраняются и не отдаются. ## Первопричина - iOS WebKit крашится, когда PWA, убитая посреди `cache.put`, потом снова открывает/перезаписывает bucket (WebKit 260962 / 305539). Сценарий усугублялся тем, что веб при возобновлении заново fetch'ил и re-put'ил КАЖДЫЙ трек (натив пропускал закешированные файлы — веб нет). - Opaque-ответы (без CORS) нельзя ни провалидировать, ни буферизовать в JS — усечённая запись была неотличима от полной. ## Изменения (10 коммитов) 1. `246a585` веб: пропуск уже закешированных треков (parity с нативом), восстановление повреждённого bucket Cache Storage (open→delete→reopen), устранены unhandled rejection'ы. 2. `d8d70a2` веб: commit-manifest протокол — запись валидна ⇔ перечислена в `__manifest__` (манифест пишется только после успешного put); SW отдаёт аудио из кеша только для «закоммиченных» записей; одноразовая миграция старых записей. Натив: атомарность подтверждена (`.part` + rename), предудаление осиротевшего `.part` того же URL. 3. `d3edd9e` очистка «осиротевших» файлов после нештатного завершения: натив — sweep `*.mp3.part` при старте; веб — журнал активных скачиваний в AsyncStorage → адресное удаление без перечисления bucket (энумерация повреждённого bucket на iOS — риск crash-loop). 4. `89b651a` фикс гонки запуска: sweep выполняется ДО initializePlayer; журнал валидируется zod; веб-очистка также убирает строки из манифеста. 5. `02a5fae` guard в SW от пустого/битого манифеста; правки доков; отказоустойчивость нативного sweep; журнал очищается вместе с кешем; убран легаси-каст `as`. 6. `7dd0c9e` веб: реплей прогресса для позднего подписчика inflight-скачивания — мгновенно отдаёт текущий `lastValue` (parity с нативом). 7. `40068ab` веб: multi-tab-безопасная очистка через «живость» журнала — записи `{ url, sessionId, lastSeenAt }` + heartbeat 10с; удаляются только протухшие (>120с) записи: свежие = живое скачивание в другом табе; stale+закоммиченные — сохраняются, stale+незакоммиченные — удаляются. 8. `60a14b1` ревью-фиксы: keying журнала по `(sessionId, url)`, margin STALE_MS под троттлинг фоновых табов, переписанный тест late-joiner'а (реально проверяет реплей на inflight-скачивании), failure-path тест. 9. `c83b6f4` веб: пути удаления/чтения больше не строят манифест — `uncommitAudioUrl` no-op без манифеста, `hasCompleteAudio` — только чтение + legacy-фолбэк. 10. `3b288ed` рефактор: `summarizeAudioCache` (легальное перечисление для статистики кеша, `getCacheInfo`) вынесен в модуль с предупреждением; гарантия в доках сфокусирована точно. ## Гарантии / инварианты - cacheAudio завершается ⇒ onProgress(1) вызван - натив: `.mp3` существует ⇔ трек полный (rename — точка коммита) - веб: запись отдаётся/хранится ⇔ присутствует в bucket И закоммичена в манифесте - веб: пути has/удаления/очистки/стартовые не перечисляют бакет; перечисление — только в commit-миграции и в `summarizeAudioCache` (`getCacheInfo`, сегодня без прод-вызовов, с предупреждением в коде) - сервисные пути не бросают исключений (безопасно на старте) ## Проверка - 96 юнит-тестов (suites audio-cache + playlist), полный прогон — 157 suites / 1261 тест зелёные; lint 0 ошибок, tsc чистый, dependency-cruiser чистый, steiger (FSD) чистый. ## Follow-ups (docs/debt.md) - Краш на уровне процесса WebKit при повреждённых записях не ловится из JS — нужна проверка на реальном iOS-устройстве (PWA: «кешировать все» → убить процесс → возобновить). - Старые записи доверяются один раз при миграции; идея chunked-кеша для дополнительной надёжности.
EgorEast added this to the MVP milestone 2026-09-06 17:13:05 +00:00
Signed-off-by: egoreast <egor.east@yandex.ru>
Signed-off-by: egoreast <egor.east@yandex.ru>
Signed-off-by: egoreast <egor.east@yandex.ru>
Signed-off-by: egoreast <egor.east@yandex.ru>
fix(cache): harden sw manifest guard and polish cleanup after review
All checks were successful
CI / dco (pull_request) Successful in 1m2s
CI / checks (pull_request) Successful in 2m4s
02a5fae05f
Signed-off-by: egoreast <egor.east@yandex.ru>
Signed-off-by: egoreast <egor.east@yandex.ru>
fix(cache): make web orphan cleanup multi-tab safe via journal liveness (#78)
All checks were successful
CI / dco (pull_request) Successful in 1m28s
CI / checks (pull_request) Successful in 2m5s
40068abec0
Signed-off-by: egoreast <egor.east@yandex.ru>
fix(cache): address review of journal liveness and replay coverage (#78)
All checks were successful
CI / dco (pull_request) Successful in 1m20s
CI / checks (pull_request) Successful in 2m1s
60a14b1792
Signed-off-by: egoreast <egor.east@yandex.ru>
fix(cache): skip manifest build in web cleanup to avoid bucket enumeration (#78)
All checks were successful
CI / dco (pull_request) Successful in 5s
CI / checks (pull_request) Successful in 1m52s
c83b6f44f3
Signed-off-by: egoreast <egor.east@yandex.ru>
refactor(cache): extract cache summary and scope enumeration guarantee (#78)
All checks were successful
CI / dco (pull_request) Successful in 8s
CI / checks (pull_request) Successful in 1m44s
3b288ed59b
Signed-off-by: egoreast <egor.east@yandex.ru>
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Slovo_Propovedi/slovo-propovedi-mobile!79
No description provided.