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
4 changes: 2 additions & 2 deletions .tech-debt/localstorage-allowlist-budget.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"production": 14,
"rationale": "Updated 2026-05-04 (Item 6 round-8 follow-up): production count 1514 after migrating apps/web/src/shared/hooks/usePushNotifications.ts off direct localStorage onto safeReadStringLS/safeWriteLS/safeRemoveLS. (Headroom: 0 — bump only with a deliberate decision; new sites must migrate an existing one to keep parity.) Burndown plan in docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md §2.2 + docs/tech-debt/frontend.md §2."
"production": 13,
"rationale": "Updated 2026-05-04 (Item 6 round-9 follow-up): production count 1413 after migrating apps/web/src/shared/lib/storage/weeklyDigestStorage.ts off direct localStorage onto safeReadStringLS. (Headroom: 0 — bump only with a deliberate decision; new sites must migrate an existing one to keep parity.) Burndown plan in docs/diagnostics/2026-05-03-web-deep-dive/02-architecture-and-state.md §2.2 + docs/tech-debt/frontend.md §2."
}
42 changes: 41 additions & 1 deletion apps/web/src/shared/lib/storage/weeklyDigestStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { loadDigest, hasLiveWeeklyDigest } from "./weeklyDigestStorage";

beforeEach(() => {
Expand All @@ -22,3 +22,43 @@ describe("hasLiveWeeklyDigest", () => {
expect(hasLiveWeeklyDigest(new Date("2025-06-15"))).toBe(false);
});
});

// `safeReadStringLS` — це новий адаптер замість inline try/catch (Item 6
// round 9). Перевіряємо, що Safari Private Mode / quota / disabled-storage
// випадки не пробивають через `loadDigest` як throw.
describe("storage adapter resilience (Item 6 round 9)", () => {
const original = Object.getOwnPropertyDescriptor(
Storage.prototype,
"getItem",
);

afterEach(() => {
if (original) {
Object.defineProperty(Storage.prototype, "getItem", original);
}
});

it("повертає null коли localStorage.getItem кидає (Safari Private Mode)", () => {
Object.defineProperty(Storage.prototype, "getItem", {
configurable: true,
value: vi.fn(() => {
throw new DOMException("SecurityError");
}),
});

expect(() => loadDigest("2025-W24")).not.toThrow();
expect(loadDigest("2025-W24")).toBeNull();
});

it("hasLiveWeeklyDigest не кидає коли storage недоступний", () => {
Object.defineProperty(Storage.prototype, "getItem", {
configurable: true,
value: vi.fn(() => {
throw new Error("disabled");
}),
});

expect(() => hasLiveWeeklyDigest(new Date("2025-06-15"))).not.toThrow();
expect(hasLiveWeeklyDigest(new Date("2025-06-15"))).toBe(false);
});
});
13 changes: 8 additions & 5 deletions apps/web/src/shared/lib/storage/weeklyDigestStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
* adapts the real `localStorage` to that contract and returns wrapped
* helpers with the storage argument pre-bound. Every web call-site
* keeps its old signature after importing from here.
*
* The adapter delegates to `safeReadStringLS` so quota-exceeded /
* Safari Private Mode / disabled-storage failures collapse to `null`
* — the same try/catch contract the prior inline version provided.
* Going through the shared helper keeps this file off the
* `no-raw-local-storage` allowlist (Item 6 burndown).
*/

import {
Expand All @@ -15,14 +21,11 @@ import {
type StorageReader,
type WeeklyDigestRecord,
} from "@sergeant/shared";
import { safeReadStringLS } from "./storage";

const webStorageReader: StorageReader = {
getItem(key) {
try {
return localStorage.getItem(key);
} catch {
return null;
}
return safeReadStringLS(key);
},
};

Expand Down
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,6 @@ export default [
"apps/web/src/shared/lib/storage/storageQuota.ts",
"apps/web/src/shared/lib/storage/typedStore.ts",
"apps/web/src/shared/lib/storage/createModuleStorage.ts",
"apps/web/src/shared/lib/storage/weeklyDigestStorage.ts",
"apps/web/src/shared/hooks/useLocalStorageState.ts",
// Мігровано на `safeReadStringLS`/`safeReadLS`/`safeWriteLS` у PR-и Item 6
// follow-up (docs/diagnostics/2026-05-03-web-deep-dive/02 §2.2):
Expand All @@ -435,6 +434,8 @@ export default [
// - apps/web/src/shared/hooks/useActiveFizrukWorkout.ts (round 7)
// - apps/web/src/shared/hooks/usePushNotifications.ts (round 8: 1 read +
// 2 writes + 4 removes на ключ `hub_push_subscribed`).
// - apps/web/src/shared/lib/storage/weeklyDigestStorage.ts (round 9:
// `webStorageReader.getItem` → `safeReadStringLS`).
// Cloud-sync internals — the queue / enqueue / state writer all
// need direct access; users should call the cloud-sync API.
"apps/web/src/core/cloudSync/logger.ts",
Expand Down
Loading