Skip to content
Merged
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2942_keys-0EA5E9" alt="i18n 19 locales — 2942 keys">
<img src="https://img.shields.io/badge/Tests-7644%2B_%2F_604_files-22C55E" alt="7644+ tests / 604 files">
<img src="https://img.shields.io/badge/Tests-7651%2B_%2F_604_files-22C55E" alt="7651+ tests / 604 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -511,7 +511,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (7644+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7651+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
Expand Down Expand Up @@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7644+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7651+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -713,8 +713,8 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
<!-- bundle-budget:source-of-truth -->
Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**.

**Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):**
- **7644+ unit tests** across **604 test files** — CI is authoritative for pass/fail
**Current test metrics (2026-09-10, source-synchronized; CI remains authoritative for pass/fail):**
- **7651+ unit tests** across **604 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)

Expand Down
76 changes: 49 additions & 27 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,23 @@ const swLogger = {
// eslint-disable-next-line no-underscore-dangle
const _WB_MANIFEST = self.__WB_MANIFEST || [];

const PRECACHE_URLS = [
BASE,
`${BASE}index.html`,
`${BASE}manifest.json`,
`${BASE}favicon.svg`,
`${BASE}offline.html`,
..._WB_MANIFEST.map((entry) => (typeof entry === 'string' ? entry : entry.url)),
];
const EXPLICIT_SHELL_URLS = [BASE, `${BASE}index.html`, `${BASE}manifest.json`, `${BASE}favicon.svg`, `${BASE}offline.html`];

// QNBS-v3: keep these files in the injected manifest (their content hash still changes sw.js's own bytes, triggering an update with no version bump) but drop them here so cache.addAll() never sees the same resolved URL twice — tracked against every URL seen so far, not just the explicit list, since two manifest entries could otherwise collide with each other.
const seenResolvedUrls = new Set(EXPLICIT_SHELL_URLS.map((url) => new URL(url, self.location.href).href));
const manifestUrls = _WB_MANIFEST
.map((entry) => (typeof entry === 'string' ? entry : entry.url))
.filter((url) => {
const resolved = new URL(url, self.location.href).href;
if (seenResolvedUrls.has(resolved)) return false;
seenResolvedUrls.add(resolved);
return true;
});

const PRECACHE_URLS = [...EXPLICIT_SHELL_URLS, ...manifestUrls];

// QNBS-v3: marker written into CACHE_STATIC only once precache fully succeeds; activate checks it before pruning an older generation.
const PRECACHE_ADMISSION_URL = `${BASE}__sw-precache-complete__`;

// ── Max age / entry limits ───────────────────────────────────
const MAX_AGE_DYNAMIC = 7 * 24 * 60 * 60 * 1000; // 7 days
Expand Down Expand Up @@ -112,16 +121,22 @@ async function offlineFallback(request) {
// INSTALL — Precache shell
// ════════════════════════════════════════════════════════════
self.addEventListener('install', (event) => {
// QNBS-v3: activates immediately, no waiting — register-sw.ts owns the bounded pre-reload flush mitigation, residual risk tracked in #518.
self.skipWaiting();
// QNBS-v3: Never precache inside Tauri — the desktop app serves its shell from the bundle.
if (IS_TAURI) return;
if (IS_TAURI) {
self.skipWaiting();
return;
}
event.waitUntil(
caches.open(CACHE_STATIC)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then((cache) => cache.addAll(PRECACHE_URLS).then(() => cache.put(PRECACHE_ADMISSION_URL, new Response('ok'))))
Comment thread
qnbs marked this conversation as resolved.
.then(() => {
// QNBS-v3: only claim the update once precache fully succeeded; register-sw.ts owns the pre-reload flush mitigation for the resulting reload.
self.skipWaiting();
})
.catch((err) => {
// Some precache entries (e.g. offline.html) may not exist yet; continue anyway
swLogger.warn('Precache partial failure (non-fatal):', err);
// QNBS-v3: rethrow so install() itself fails — a worker that never reaches "installed" can never be waited-on, activated, or SKIP_WAITING'd.
swLogger.warn('Precache failed — this installation will not complete:', err);
throw err;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
})
Comment thread
qnbs marked this conversation as resolved.
Comment thread
qnbs marked this conversation as resolved.
);
});
Expand Down Expand Up @@ -154,19 +169,26 @@ self.addEventListener('activate', (event) => {
return;
}
event.waitUntil(
caches.keys()
.then((cacheNames) =>
Promise.all(
cacheNames
// QNBS-v3: prune only owned-and-stale — never delete a cache we don't positively own.
.filter((name) => isWorldScriptOwnedCache(name) && !ALL_CACHES.includes(name))
.map((name) => {
swLogger.log('Pruning old cache:', name);
return caches.delete(name);
})
)
)
.then(() => self.clients.claim())
(async () => {
// QNBS-v3: admission gate — only an admitted generation may prune stale caches or claim clients.
const staticCache = await caches.open(CACHE_STATIC);
const precacheComplete = Boolean(await staticCache.match(PRECACHE_ADMISSION_URL));
if (!precacheComplete) {
swLogger.warn('Skipping cache-generation cutover: this generation\'s precache never completed');
return;
Comment thread
qnbs marked this conversation as resolved.
}
const cacheNames = await caches.keys();
await Promise.all(
cacheNames
// QNBS-v3: prune only owned-and-stale — never delete a cache we don't positively own.
.filter((name) => isWorldScriptOwnedCache(name) && !ALL_CACHES.includes(name))
.map((name) => {
swLogger.log('Pruning old cache:', name);
return caches.delete(name);
})
);
await self.clients.claim();
Comment thread
qnbs marked this conversation as resolved.
})()
);
});

Expand Down
Loading
Loading