Fix C1/C2 + reliability cluster (L140/L141/L148, R6/R7)
Critical: - C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts. - C2: break the downloads<->sources circular import via a typed event bus (store/coordinator.ts). App eagerly imports sources for side-effects so startup load + scheduled --sync + downloadCompleted subscription still run. Reliability: - L140/L148: cancel/pause release the active slot synchronously; identity-guarded releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected by a stale entry nor run over the concurrency cap. - L141: renderer history capped at 500 + url de-dup to match main. - R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry instead of an empty catch. - R7: encryptSecret warns before the plaintext fallback. typecheck + 243 tests + eslint green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+67
-11
@@ -33,6 +33,24 @@ fmtBytes/fmtEta/parseProgress unit-tested including NA paths), L38 (webm thumbna
|
||||
test), L39 (CROP_SQUARE_PPA structural test), L40 (looksLikeUrl/looksLikeSingleVideo extracted
|
||||
to lib/urlHelpers.ts; test imports pure module directly). All verified: typecheck + 242 tests + eslint + prettier green.
|
||||
|
||||
**Session 2026-06-30 pass 3 (reliability — B/R/L races & silent failures):** L140 + L148 (the `active`-map
|
||||
teardown race — `cancelDownload`/`pauseDownload` release the concurrency slot synchronously and an
|
||||
identity-guarded `releaseActive` + per-spawn cookie jar make same-id retry/resume safe), L141 (renderer
|
||||
history capped at 500 + url de-dup to mirror main), R6 (`writeJsonAtomic` reports + logs write failures and
|
||||
keeps the data dirty for retry instead of an empty catch — log half; toast → CC8), R7 (`encryptSecret` warns
|
||||
before the plaintext fallback). All verified: typecheck + 243 tests + eslint green; the 5 touched files are
|
||||
prettier-clean. *Deferred this pass (need UI/live-app or larger design): B2/B6 (cancel buttons + abort
|
||||
wiring), R4 (can't safely identify a download's `.part` files at cancel time — needs the yt-dlp "Destination"
|
||||
path tracked), L139 (needs a spawn↔auto-update interlock), L142 (multi-site IPC-return reconciliation,
|
||||
generalises M34), L157 (cross-store cancel-on-remove, ties to C2), R9 (clock-skew — "note for awareness").*
|
||||
|
||||
**Session 2026-06-30 pass 4 (Critical — store architecture):** **C1** (single source of truth for the
|
||||
preview mock + Settings defaults — `DEFAULT_SETTINGS` in `@shared`, one typed `mockApi.ts`, centralized
|
||||
`isPreview`) and **C2** (broke the `downloads ↔ sources` circular import via a pure typed event bus,
|
||||
`store/coordinator.ts` — neither store imports the other now). All verified: typecheck + 243 tests +
|
||||
eslint green; the touched files are prettier-clean (3 pre-existing format violations in
|
||||
DownloadOptionsForm/LibraryView/SettingsView are untouched and out of scope).
|
||||
|
||||
**Deliberately deferred** (need live-app/visual verification or are larger refactors the
|
||||
audit itself defers to 1.x): the wire-or-cut features (M5/M6/UX1), the god-file/store
|
||||
refactors (C1, C2, H1), the SIMP* helpers + shared-UI-token work, the CC* consolidations,
|
||||
@@ -176,15 +194,30 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
|
||||
## Critical
|
||||
|
||||
- [ ] **C1 — Single source of truth for the IPC mock + Settings defaults.** `const PREVIEW`
|
||||
- [x] **C1 — Single source of truth for the IPC mock + Settings defaults.** `const PREVIEW`
|
||||
redeclared in 8 files; `main.tsx` reimplements the entire `Api` (~180 lines); the full
|
||||
`Settings` object is hand-maintained in 3 places (`main/settings.ts` `DEFAULTS`,
|
||||
`renderer/store/settings.ts` `FALLBACK`, `main.tsx` `MOCK_SETTINGS`). Add `DEFAULT_SETTINGS`
|
||||
to `shared/ipc.ts`, extract the preview mock into one `mockApi.ts` typed as `Api`, centralize
|
||||
`isPreview`.
|
||||
- [ ] **C2 — Break the `downloads ↔ sources` store circular dependency.** `downloads.ts`
|
||||
`isPreview`. *Fixed: `DEFAULT_SETTINGS` is now the canonical default in
|
||||
[shared/ipc.ts](src/shared/ipc.ts); main's `DEFAULTS`, the renderer `FALLBACK`
|
||||
([store/settings.ts](src/renderer/src/store/settings.ts)), and the preview `MOCK_SETTINGS`
|
||||
all derive from it (preview-only field tweaks layered on top) instead of three hand-kept
|
||||
copies. The whole browser mock moved into one [mockApi.ts](src/renderer/src/mockApi.ts) typed
|
||||
`Window['api']` (so a missing/mistyped method is a compile error), and `main.tsx` shrank to
|
||||
`window.api = mockApi`. The `PREVIEW` check is single-sourced in
|
||||
[isPreview.ts](src/renderer/src/isPreview.ts) and imported (as `PREVIEW`) by all 8 sites.*
|
||||
- [x] **C2 — Break the `downloads ↔ sources` store circular dependency.** `downloads.ts`
|
||||
imports `useSources`; `sources.ts` imports `useDownloads`. Works only via lazy `.getState()`.
|
||||
Introduce a coordinator/event bus that owns cross-store reactions.
|
||||
Introduce a coordinator/event bus that owns cross-store reactions. *Fixed: a tiny typed event
|
||||
bus [store/coordinator.ts](src/renderer/src/store/coordinator.ts) (no runtime deps — the
|
||||
`AddEntry` import is type-only) now owns the two cross-store reactions. `sources` emits
|
||||
`enqueueDownloads` (consumed by `downloads.addMany`); `downloads` emits `downloadCompleted`
|
||||
(consumed by `sources.markDownloaded`). Each store imports the bus, not the other, so the
|
||||
cycle is gone. Because `downloads` no longer pulls in `sources`, `App` now eagerly imports the
|
||||
`sources` store for side-effects (`import './store/sources'`) so its startup load + scheduled-
|
||||
`--sync` kickoff and the `downloadCompleted` subscription still run at launch — robust even
|
||||
once the views are lazy-loaded (PERF8).*
|
||||
|
||||
## High
|
||||
|
||||
@@ -649,10 +682,19 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
- [ ] **L139 — Auto-update vs concurrent spawn race.** `runStartupYtdlpAutoUpdate` can overwrite
|
||||
`yt-dlp.exe` on launch while a deep-link/queued download spawns the same binary — a Windows
|
||||
file-in-use/corrupt-spawn window.
|
||||
- [ ] **L140 — Retry-during-teardown race.** `retry` reuses the same id; if the previous process's
|
||||
- [x] **L140 — Retry-during-teardown race.** `retry` reuses the same id; if the previous process's
|
||||
`close` handler hasn't removed it from `active` yet, `startDownload` returns "already running" → markError.
|
||||
- [ ] **L141 — Renderer history grows unbounded in memory.** `useHistory.add` prepends without a cap
|
||||
*Fixed in [download.ts](src/main/download.ts): `cancelDownload`/`pauseDownload` now drop the item from
|
||||
`active` synchronously (so a retry/resume's same-id spawn is never rejected by the stale entry), and the
|
||||
doomed child's `close`/`error`/watchdog release the slot through a new `releaseActive(id, rec)` that only
|
||||
deletes when that rec still owns the id — so a late teardown can't evict the fresh spawn. The transient
|
||||
cookie jar is now keyed per spawn (`-${spawnSeq}`) so the old download's cleanup can't unlink the new one's
|
||||
file. (with L148)*
|
||||
- [x] **L141 — Renderer history grows unbounded in memory.** `useHistory.add` prepends without a cap
|
||||
while main caps `history.json` at 500; in a long session the in-memory list exceeds the persisted cap (resets on reload).
|
||||
*Fixed in [store/history.ts](src/renderer/src/store/history.ts): `add` now `.slice(0, MAX_ENTRIES)` (500, the
|
||||
same cap as main's `history.ts`) and de-dupes by `url` as well as `id`, so the optimistic in-memory list
|
||||
matches what a reload would show (also aligns with main's M35 url de-dup).*
|
||||
- [ ] **L142 — IPC mutation return values are discarded everywhere.** history/templates/sources/settings
|
||||
IPC calls return the authoritative (validated/capped/sanitized) state, but every renderer caller does
|
||||
optimistic-only updates and ignores it — client and persisted state can silently diverge until reload (generalizes M34).
|
||||
@@ -668,8 +710,12 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
||||
rather than being rejected up front.
|
||||
- [x] **L147 — macOS branches in a Windows-only app.** `app.on('activate')` and the
|
||||
`process.platform !== 'darwin'` guard in `window-all-closed` are dead on Windows — boilerplate that implies multi-platform support the app doesn't ship.
|
||||
- [ ] **L148 — `clearFinished`/remove can free a slot before the killed process exits.** Removing a
|
||||
- [x] **L148 — `clearFinished`/remove can free a slot before the killed process exits.** Removing a
|
||||
just-canceled item lets `pump()` launch into the "freed" slot while `taskkill` is still tearing down the prior tree — a brief window over the concurrency cap.
|
||||
*Fixed with L140: `cancelDownload`/`pauseDownload` release the `active` slot the instant they issue the kill
|
||||
(rather than on the async `close`), so main's concurrency accounting matches the renderer immediately — the
|
||||
just-promoted next item is neither falsely rejected by the maxConcurrent guard nor run over the cap while the
|
||||
prior tree dies.*
|
||||
|
||||
*Round 7 (2026-06-29) — copy, a11y patterns & micro-behavior:*
|
||||
|
||||
@@ -1344,12 +1390,22 @@ cosmetics. `R` IDs.
|
||||
- [x] **R5 — Settings write failure is unhandled.** `setSettings` calls `store.set(...)` with no try/catch;
|
||||
on disk-full/read-only `electron-store` throws → the IPC rejects → the renderer's `setSettings().catch(()
|
||||
=> {})` swallows it while the optimistic UI keeps the change (the disk-full form of M34). **Fix:** catch and report.
|
||||
- [ ] **R6 — Silent data loss on any store write failure.** A completed download that can't be recorded
|
||||
- [x] **R6 — Silent data loss on any store write failure.** A completed download that can't be recorded
|
||||
(disk full) shows "Completed" in-session but is gone on restart, with no error (the write `catch` is empty,
|
||||
M29). **Fix:** detect write failure and warn.
|
||||
- [ ] **R7 — `safeStorage` unavailable ⇒ secrets silently stored as plaintext.** `encryptSecret` falls back
|
||||
M29). **Fix:** detect write failure and warn. *Fixed (log half) in [jsonStore.ts](src/main/jsonStore.ts):
|
||||
`writeJsonAtomic` now returns whether the write landed and logs the failure (with the path) instead of an
|
||||
empty catch, and removes the leftover `.tmp`; `createJsonStore`'s `flush` only clears the dirty flag on
|
||||
success, so a failed write stays pending and is retried on the next `write()` or the quit-time
|
||||
`flushAllStores()` rather than dropped. Unit-tested in `test/jsonStore.test.ts`. **Scope note:** like M29
|
||||
this delivers the "no longer silent" half (logged + retried); the user-facing toast needs the leveled file
|
||||
logger / toast sink deferred to **CC8**.*
|
||||
- [x] **R7 — `safeStorage` unavailable ⇒ secrets silently stored as plaintext.** `encryptSecret` falls back
|
||||
to plaintext when DPAPI/safeStorage isn't available, with no indication — compounds H7 (cookies) and the
|
||||
backup-cleartext issue (M22). **Fix:** warn (or refuse to store) when encryption is unavailable.
|
||||
backup-cleartext issue (M22). **Fix:** warn (or refuse to store) when encryption is unavailable. *Fixed in
|
||||
[settings.ts](src/main/settings.ts): `encryptSecret` now `console.warn`s before returning a non-empty secret
|
||||
as plaintext, so the cleartext fallback is no longer silent. Chose warn-not-refuse to preserve the
|
||||
pre-encryption behaviour (storing still works); DPAPI is effectively always present on Windows, so this
|
||||
should never fire in practice. (refusing/at-rest enforcement remains an option if ever wanted.)*
|
||||
- [x] **R8 — Pretty-printed JSON for large stores.** `JSON.stringify(value, null, 2)` inflates size/write
|
||||
time for the potentially-20k-item media store (ties R3). **Fix:** compact JSON for the large stores.
|
||||
- [ ] **R9 — Clock-skew sensitivity.** `shouldAutoCheckYtdlp` and the scheduled-download promoter compare
|
||||
|
||||
Reference in New Issue
Block a user