Skip to content
Merged
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
25 changes: 20 additions & 5 deletions TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,19 +283,23 @@ Lists every built file with its content hash:
```json
{
"version": "<hash of all file hashes>",
"files": { "assets/javascripts/bundle.min.js": "abc123...", "index.html": "def456..." }
"files": { "assets/javascripts/bundle.min.js": "abc123...", "index.html": "def456..." },
"sizes": { "assets/javascripts/bundle.min.js": 116474, "index.html": 8212 }
}
```

Used by the SW for hash-based invalidation: on sync, files whose hash changed
are re-fetched; files no longer in the manifest are evicted.
are re-fetched; files no longer in the manifest are evicted. The `sizes` map
records the exact byte count of every built file on disk, so quota eviction
accounts entries at their true size instead of guessing.

### Service worker (`src/assets/javascripts/sw.js`)

- **Caches**: `docsforge-<BUILD_HASH>` (content), `docsforge-meta`
(manifest + previous-files list).
- **Constants**: `BUILD_HASH`, `BASE_URL` (with trailing-slash normalization),
`ORIGIN_BASE`, `SYNC_CONCURRENCY = 6`, quota margins.
`ORIGIN_BASE`, `SYNC_CONCURRENCY = 6`, `DOWNLOAD_COST_BYTES = 20 MiB`
(per-download budget reservation), `QUOTA_MARGIN_RATIO = 0.1`.
- **IndexedDB**: `docsforge-i18n` (locale preference).
- **Messages**:
- `DOCSFORGE_RELOAD_DETECTED` (from the page, on Navigation Timing
Expand All @@ -308,15 +312,26 @@ are re-fetched; files no longer in the manifest are evicted.
- **Manifest sync**: fetch `cache-manifest.json` (`cache: 'no-cache'`),
diff against the previous files list, fetch changed URLs (concurrency 6),
evict orphaned entries (LRU by access time when quota exceeded, plus
manifest-driven eviction for files no longer tracked).
manifest-driven eviction for files no longer tracked). The sync is
budgeted against the free space reported by `storage.estimate()`: each
download reserves a flat `DOWNLOAD_COST_BYTES` (20 MiB — the usage
estimate lags behind in-flight writes) and the sync stops once the budget
is exhausted, so files that cannot possibly fit are never downloaded in
the first place. Unbudgeted files are cached on demand when visited.
- **Fetch strategy**: pages served from cache with background revalidation;
static assets cache-first; navigation requests matched against the manifest
(`manifestHasFile`) so pages absent from the manifest never 404 as
`index.en.html` style false positives. Hard refreshes and back/forward
(`nav.type === 'back_forward'`) are deliberately **not** treated as
revalidation triggers (design decision).
- **Quota handling**: on QuotaExceeded, evicts LRU entries (using
`docsforge-access-times`), with a configurable margin, and retries.
`docsforge-access-times`) and retries. Eviction accounts each entry at its
**measured** byte size (Content-Length, falling back to reading the body),
frees until `available + freed` covers the required bytes plus a
proportional 10%-of-quota margin, and drops evicted entries from the
persisted previous-files list (`docsforge-manifest-files`) so a later sync
re-fetches them instead of believing they are cached. A single resource
larger than the whole quota is never cached.

---

Expand Down
35 changes: 35 additions & 0 deletions docs/docs/changelog/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
## [12.5.5] — 2026-08-19

### Added

- **File sizes in `cache-manifest.json`** — the manifest now ships a `sizes`
map with the exact byte count of every built file. The service worker uses
it (plus the response's `Content-Length`, measured from the body when
missing) to account quota evictions at their real size instead of the old
guessed 5 MiB per entry.

### Changed

- **Service worker quota eviction is exact** — when the browser cache is
full, the SW evicts least-recently-used entries until the actually-freed
bytes (measured, not estimated) cover the required space plus a
proportional 10%-of-quota margin, then retries. The old flat 20 MiB free
margin is gone. Evicted entries are removed from the persisted
previous-files list, so a file that was evicted is never falsely recorded
as cached — the next sync re-fetches it instead of skipping it.
- **Manifest sync is budget-aware** — the background sync now checks the
free space reported by `storage.estimate()` before downloading: every
changed file reserves a flat 20 MiB of budget (the usage estimate lags
behind in-flight writes), and the sync stops once the budget is exhausted
instead of downloading files that would only be evicted again. Files that
don't fit are cached on demand when actually visited. A single resource
larger than the whole quota is never cached.

### Fixed

- **Safari (and any small-quota browser) no longer thrashes** — previously
the sync downloaded the whole manifest, hit the quota wall, evicted the
just-cached files, and recorded them as cached anyway; offline coverage
silently degraded to a few tail files. Now the sync stops early and the
tracked-file list stays truthful.

## [12.5.4] — 2026-08-18

### Added
Expand Down
30 changes: 30 additions & 0 deletions docs/docs/changelog/index.zh.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
## [12.5.5] — 2026-08-19

### 新增

- **`cache-manifest.json` 附带文件大小** —— manifest 现在带有一个
`sizes` 映射,记录每个构建文件的精确字节数。Service Worker 用它
(以及响应的 `Content-Length`,缺失时直接测量响应体)按真实大小进行
配额逐出,取代旧的"每个条目按 5 MiB 估算"的做法。

### 变更

- **Service Worker 配额逐出精确化** —— 浏览器缓存满时,SW 按
最近最少使用(LRU)顺序逐出条目,直到实际释放的字节数(真实测量,
不再估算)覆盖所需空间并留出配额 10% 的成比例余量,然后重试。旧的
固定 20 MiB 空闲余量已移除。被逐出的条目会从持久化的"上次同步文件
列表"中删除,被逐出的文件绝不会被错误记录为已缓存 —— 下次同步会
重新拉取,而不是跳过。
- **manifest 同步带预算控制** —— 后台同步在下载前先查询
`storage.estimate()` 报告的空闲空间:每个变更文件预占 20 MiB 预算
(用量估算落后于在途写入),预算耗尽后同步即停止,不再下载那些注定
会被再次逐出的文件。放不下的文件在真正访问时才按需缓存。单个资源
大于整个配额时永远不会被缓存。

### 修复

- **Safari(以及任何小额配额的浏览器)不再抖动** —— 此前同步会下载
整个 manifest,撞上配额上限后把刚缓存的文件逐出,却仍把它们记录为
已缓存;离线覆盖范围静默退化为 manifest 末尾的少量文件。现在同步会
提前停止,已跟踪文件列表始终真实可信。

## [12.5.4] — 2026-08-18

### 新增
Expand Down
7 changes: 7 additions & 0 deletions docsforge/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1185,8 +1185,13 @@ def _generate_cache_manifest(site_dir: str, page_urls: list[str], files: Files |
index, sitemap, PWA manifest, fonts, etc.). Hashes are computed from the
Markdown SOURCE file when one exists, otherwise from the built file on disk.
The SW uses this manifest to cache everything directly, without parsing HTML.

The ``sizes`` map records the byte size of every built file on disk (the
exact number of bytes the SW stores in the browser cache), so quota
eviction can free precisely the space it claims instead of guessing.
"""
manifest_files = {}
manifest_sizes = {}

# Build a lookup from page URL to source Markdown path. Multiple URL forms
# can map to the same source (e.g. 'second/', 'second', 'second/index.html').
Expand Down Expand Up @@ -1241,10 +1246,12 @@ def _generate_cache_manifest(site_dir: str, page_urls: list[str], files: Files |
h = hashlib.sha256(f.read()).hexdigest()[:16]

manifest_files[url] = h
manifest_sizes[url] = os.path.getsize(abs_path)

manifest = {
"version": hashlib.sha256(json.dumps(manifest_files, sort_keys=True).encode()).hexdigest()[:12],
"files": manifest_files,
"sizes": manifest_sizes,
}

manifest_path = os.path.join(site_dir, 'cache-manifest.json')
Expand Down
2 changes: 1 addition & 1 deletion docsforge/templates/assets/javascripts/sw.js

Large diffs are not rendered by default.

Loading