Wire SW update prompt and request persistent storage (M6, web.dev)

This commit is contained in:
2026-06-07 20:39:07 +03:00
parent 5dc5ef8724
commit d94480f56d
4 changed files with 175 additions and 0 deletions
@@ -0,0 +1,75 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { requestPersistentStorage } from './storage-persist';
const realNavigator = globalThis.navigator;
/** Swap `navigator` for one exposing the given `storage` (or none). */
function withStorage(storage: unknown) {
Object.defineProperty(globalThis, 'navigator', {
value: storage === undefined ? {} : { storage },
configurable: true,
});
}
afterEach(() => {
Object.defineProperty(globalThis, 'navigator', { value: realNavigator, configurable: true });
vi.restoreAllMocks();
});
describe('requestPersistentStorage', () => {
test('reports unsupported when the StorageManager API is absent', async () => {
withStorage(undefined);
expect(await requestPersistentStorage()).toEqual({ supported: false });
});
test('reports unsupported when persist/persisted are missing', async () => {
withStorage({ estimate: () => Promise.resolve({}) });
expect(await requestPersistentStorage()).toEqual({ supported: false });
});
test('does not re-request when already persisted', async () => {
const persist = vi.fn();
withStorage({ persisted: vi.fn().mockResolvedValue(true), persist });
expect(await requestPersistentStorage()).toEqual({
supported: true,
persisted: true,
alreadyPersisted: true,
});
expect(persist).not.toHaveBeenCalled();
});
test('requests persistence once and reports granted', async () => {
const persist = vi.fn().mockResolvedValue(true);
withStorage({ persisted: vi.fn().mockResolvedValue(false), persist });
expect(await requestPersistentStorage()).toEqual({
supported: true,
persisted: true,
alreadyPersisted: false,
});
expect(persist).toHaveBeenCalledTimes(1);
});
test('reports not granted when the request is denied', async () => {
withStorage({
persisted: vi.fn().mockResolvedValue(false),
persist: vi.fn().mockResolvedValue(false),
});
expect(await requestPersistentStorage()).toEqual({
supported: true,
persisted: false,
alreadyPersisted: false,
});
});
test('never rejects — a thrown request resolves to not-persisted', async () => {
withStorage({
persisted: vi.fn().mockRejectedValue(new Error('blocked')),
persist: vi.fn(),
});
expect(await requestPersistentStorage()).toEqual({
supported: true,
persisted: false,
alreadyPersisted: false,
});
});
});
+45
View File
@@ -0,0 +1,45 @@
/**
* Persistent-storage request (web.dev → council; see reference/principles/web-dev.md).
*
* By default browser storage is **best-effort**: under storage pressure the
* browser may evict the whole origin's IndexedDB + Cache API data. For a
* local-first app whose workspace lives only in the browser, that is data loss.
* `navigator.storage.persist()` asks the browser not to evict us.
*
* Behaviour per web.dev guidance:
* - feature-detect; do nothing where the API is absent (never throw),
* - check `persisted()` first and request **at most once** — don't nag,
* - stay silent on denial: Chromium decides automatically from heuristics
* (installed / engagement / notifications), and there is nothing the user
* can act on, so no toast.
*
* Browser-only; the rest of the app reaches it through `orchestration/pwa`.
*/
export type PersistResult =
| { supported: false }
| { supported: true; persisted: boolean; alreadyPersisted: boolean };
/**
* Ensure persistent storage, requesting it once if not already granted.
* Resolves to the outcome; never rejects.
*/
export async function requestPersistentStorage(): Promise<PersistResult> {
const storage = typeof navigator !== 'undefined' ? navigator.storage : undefined;
if (
!storage ||
typeof storage.persist !== 'function' ||
typeof storage.persisted !== 'function'
) {
return { supported: false };
}
try {
const alreadyPersisted = await storage.persisted();
if (alreadyPersisted) return { supported: true, persisted: true, alreadyPersisted: true };
const persisted = await storage.persist();
return { supported: true, persisted, alreadyPersisted: false };
} catch {
// A rejected request is not actionable by the user — report not-persisted.
return { supported: true, persisted: false, alreadyPersisted: false };
}
}