refactor(main): introduce explicit AniList runtime transitions

This commit is contained in:
2026-02-21 16:16:30 -08:00
parent 7a869ad291
commit 631e0450b1
10 changed files with 508 additions and 113 deletions

View File

@@ -1,10 +1,11 @@
--- ---
id: TASK-79 id: TASK-79
title: Introduce explicit runtime state transitions and reducers in main title: Introduce explicit runtime state transitions and reducers in main
status: To Do status: Done
assignee: [] assignee:
- '@sudacode'
created_date: '2026-02-18 11:43' created_date: '2026-02-18 11:43'
updated_date: '2026-02-18 11:43' updated_date: '2026-02-22 00:10'
labels: labels:
- main-process - main-process
- state-management - state-management
@@ -41,15 +42,49 @@ Main runtime state is currently mutable from many callsites. This task introduce
## Acceptance Criteria ## Acceptance Criteria
<!-- AC:BEGIN --> <!-- AC:BEGIN -->
- [ ] #1 Critical runtime state domains mutate through explicit transition helpers - [x] #1 Critical runtime state domains mutate through explicit transition helpers
- [ ] #2 State invariants are test-covered - [x] #2 State invariants are test-covered
- [ ] #3 Direct ad-hoc mutation in migrated domains is removed - [x] #3 Direct ad-hoc mutation in migrated domains is removed
- [ ] #4 Ownership/mutation rules documented - [x] #4 Ownership/mutation rules documented
<!-- AC:END --> <!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
Implementation plan (writing-plans):
1) Add pure reducer-style transition helpers in `src/main/state.ts` for critical AniList domains: client-secret state, retry queue metadata, media-guess runtime state, and tracking in-flight flag. Add focused tests in `src/main/state.test.ts`.
2) Rewire `src/main.ts` to use transition helpers instead of direct mutation for migrated domains in runtime deps wiring (`buildAnilistStateRuntimeMainDepsHandler`, media-guess state deps, retry-update deps, post-watch in-flight setter).
3) Add/extend invariant tests in `src/main/runtime/anilist-state.test.ts` and `src/main/runtime/anilist-media-state.test.ts` (metadata preservation, idempotent reset, scoped-field resets).
4) Document ownership/mutation rules in `docs/architecture.md` under composition guidance and run verification gates (`bun run build`, `bun run test:core:src`).
5) Record evidence and AC/DoD progress in TASK-79 notes.
Parallelization: run code-wiring slice and tests/docs slice in parallel subagents where safe, then reconcile and run final gates in the main session.
Detailed step-by-step plan saved at `docs/plans/2026-02-21-task-79-runtime-state-reducers.md`.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
2026-02-21: Started implementation pass via writing-plans/executing-plans workflow. Scoping to explicit transition helpers/reducers for critical AniList runtime state domains in `src/main.ts`/`src/main/state.ts`, plus invariant tests and architecture ownership rules docs.
Implemented explicit AniList runtime transition helpers/reducers in `src/main/state.ts` and rewired migrated `src/main.ts` mutation paths through them (client-secret state, retry queue metadata, media-guess runtime state, in-flight flag).
Added focused reducer tests in `src/main/state.test.ts` plus invariants coverage updates in `src/main/runtime/anilist-state.test.ts` and `src/main/runtime/anilist-media-state.test.ts`.
Documented runtime ownership/mutation rules for migrated domains in `docs/architecture.md` under Composition Pattern.
Validation evidence: `bun test src/main/state.test.ts src/main/runtime/anilist-state.test.ts src/main/runtime/anilist-media-state.test.ts` (12 pass), `bun run build` (pass), `bun run test:core:src` (219 pass, 6 skip, 0 fail).
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Introduced explicit reducer-style runtime transitions for critical AniList state in the main process by adding pure transition helpers in `src/main/state.ts` and routing migrated writes in `src/main.ts` through those helpers. Removed ad-hoc direct mutation in migrated domains (client-secret state, retry queue metadata, media-guess runtime state, in-flight flag), added focused and invariant tests, and documented ownership/mutation rules in architecture docs. Validation passed with focused state suites, full build, and core source test lane.
<!-- SECTION:FINAL_SUMMARY:END -->
## Definition of Done ## Definition of Done
<!-- DOD:BEGIN --> <!-- DOD:BEGIN -->
- [ ] #1 Core tests pass after migration - [x] #1 Core tests pass after migration
- [ ] #2 No behavior regressions in startup/IPC/overlay flows - [x] #2 No behavior regressions in startup/IPC/overlay flows
<!-- DOD:END --> <!-- DOD:END -->

View File

@@ -178,6 +178,16 @@ Composer modules share contract conventions via `src/main/runtime/composers/cont
This keeps side effects explicit and makes behavior easy to unit-test with fakes. This keeps side effects explicit and makes behavior easy to unit-test with fakes.
### Runtime State Ownership (Migrated Domains)
For domains migrated to reducer-style transitions (for example AniList token/queue/media-guess runtime state), follow these rules:
- Composition/runtime modules own mutable state cells and expose narrow `get*`/`set*` accessors.
- Domain handlers do not mutate foreign state directly; they call explicit transition helpers that encode invariants.
- Transition helpers may sync derived counters/snapshots, but must preserve non-owned metadata unless the transition explicitly owns that metadata.
- Reducer boundary: when a domain has transition helpers in `src/main/state.ts`, new callsites should route updates through those helpers instead of ad-hoc object mutation in `main.ts` or composers.
- Tests for migrated domains should assert both the intended field changes and non-targeted field invariants.
## Program Lifecycle ## Program Lifecycle
- **Startup:** `startup.ts` parses CLI args and detects the compositor backend. If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks. - **Startup:** `startup.ts` parses CLI args and detects the compositor backend. If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.

View File

@@ -3,7 +3,7 @@
Read first. Keep concise. Read first. Keep concise.
| agent_id | alias | mission | status | file | last_update_utc | | agent_id | alias | mission | status | file | last_update_utc |
| --------------------------------------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------- | ---------------------- | | ------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------- | ---------------------- |
| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` | | `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` |
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | | `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` | | `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` |
@@ -43,4 +43,14 @@ Read first. Keep concise.
| `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` | | `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` |
| `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` | | `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` |
| `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` | | `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` |
| `codex-docs-unpushed-review-20260221T213707Z-lyej` | `codex-docs-unpushed-review` | `Review unpushed commits for docs drift; patch docs to reflect current code/state` | `in_progress` | `docs/subagents/agents/codex-docs-unpushed-review-20260221T213707Z-lyej.md` | `2026-02-21T21:37:07Z` | | `codex-docs-unpushed-review-20260221T213707Z-lyej` | `codex-docs-unpushed-review` | `Review unpushed commits for docs drift; patch docs to reflect current code/state` | `done` | `docs/subagents/agents/codex-docs-unpushed-review-20260221T213707Z-lyej.md` | `2026-02-21T21:39:15Z` |
| `codex-task72-strict-startup-config-20260221T231804Z-3ngd` | `codex-task72-strict-startup-config` | `Execute TASK-72 strict startup config loading with actionable user-facing errors` | `done` | `docs/subagents/agents/codex-task72-strict-startup-config-20260221T231804Z-3ngd.md` | `2026-02-21T23:26:29Z` |
| `opencode-task77-tokenizer-stages-20260221T232016Z-v9k2` | `opencode-task77-tokenizer-stages` | `Execute TASK-77 tokenizer pipeline split into parser-selection enrichment and annotation stages without commit` | `done` | `docs/subagents/agents/opencode-task77-tokenizer-stages-20260221T232016Z-v9k2.md` | `2026-02-21T23:47:08Z` |
| `codex-task75-mpv-osd-buffered-20260221T231816Z-yj32` | `codex-task75-mpv-osd-buffered` | `Execute TASK-75 move MPV OSD log writes to buffered async path end-to-end` | `done` | `docs/subagents/agents/codex-task75-mpv-osd-buffered-20260221T231816Z-yj32.md` | `2026-02-21T23:48:10Z` |
| `opencode-task72-strict-startup-config-20260221T232155Z-kf0o` | `opencode-task72-strict-startup-config` | `Implement Task 1 from strict startup config loading plan with startup malformed-config failure signal and tests` | `done` | `docs/subagents/agents/opencode-task72-strict-startup-config-20260221T232155Z-kf0o.md` | `2026-02-21T23:24:32Z` |
| `opencode-task72-parse-details-20260221T232137Z-b63t` | `opencode-task72-parse-details` | `Implement TASK-72 Task 2 shared parse-error formatter wiring and tests` | `done` | `docs/subagents/agents/opencode-task72-parse-details-20260221T232137Z-b63t.md` | `2026-02-21T23:24:12Z` |
| `opencode-task77-slice-a-20260222T000100Z-j4p2` | `opencode-task77-slice-a` | `Implement TASK-77 slice A parser-selection-stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-slice-a-20260222T000100Z-j4p2.md` | `2026-02-22T00:03:30Z` |
| `opencode-task78-config-domain-20260221T235604Z-p9x2` | `opencode-task78-config-domain` | `Execute TASK-78 modularize config definitions and validation by domain end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task78-config-domain-20260221T235604Z-p9x2.md` | `2026-02-22T00:06:30Z` |
| `opencode-task77-sliceb-20260221T232507Z-vzk5` | `opencode-task77-sliceb` | `Implement TASK-77 slice B parser-enrichment stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-sliceb-20260221T232507Z-vzk5.md` | `2026-02-21T23:27:40Z` |
| `opencode-task79-runtime-reducers-20260221T235652Z-n4p7` | `opencode-task79-runtime-reducers` | `Execute TASK-79 explicit runtime state transitions/reducers in main via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task79-runtime-reducers-20260221T235652Z-n4p7.md` | `2026-02-22T00:10:51Z` |
| `opencode-task79-sliceb-20260222T000253Z-m2r7` | `opencode-task79-sliceb` | `Implement TASK-79 slice B invariants coverage/tests and composition-boundary docs updates without commit` | `done` | `docs/subagents/agents/opencode-task79-sliceb-20260222T000253Z-m2r7.md` | `2026-02-22T00:04:21Z` |

View File

@@ -0,0 +1,31 @@
# Agent Session: opencode-task79-runtime-reducers-20260221T235652Z-n4p7
- alias: `opencode-task79-runtime-reducers`
- mission: `Execute TASK-79 explicit runtime state transitions/reducers in main via writing-plans + executing-plans (no commit).`
- status: `done`
- started_utc: `2026-02-21T23:56:52Z`
- last_update_utc: `2026-02-22T00:10:51Z`
## Intent
- Load TASK-79 from Backlog MCP; capture plan in task.
- Produce implementation plan doc under `docs/plans/`.
- Execute code/test/docs updates end-to-end without commit.
## Planned Files
- `src/main.ts`
- `src/main/state.ts`
- `src/main/state.test.ts`
## Assumptions
- Backlog task `TASK-79` exists and is ready for execution.
- Existing startup/IPC/overlay behavior must remain unchanged.
- Parallel subagents can own independent slices (state domains/tests/docs) without overlap.
## Phase Log
- `2026-02-21T23:56:52Z` Started; loaded backlog overview + TASK-79 context; beginning planning.
- `2026-02-22T00:06:10Z` Slice A implementation: added explicit AniList state transition helpers + initializers in `src/main/state.ts`; rewired migrated `src/main.ts` AniList writes through transitions; added focused reducer tests in `src/main/state.test.ts`; focused `bun test src/main/state.test.ts` blocked by Bun runtime missing `node:sqlite`.
- `2026-02-22T00:10:51Z` Finalized TASK-79: switched `state.ts` import to direct `mpv-render-metrics` module (removes `node:sqlite` test coupling), focused state/anilist invariant tests passing, build + `test:core:src` passing, backlog TASK-79 marked Done.

View File

@@ -49,3 +49,25 @@ Shared notes. Append-only.
- [2026-02-21T20:35:58Z] [opencode-task76-doc-boundaries-20260221T203558Z-h7q4|opencode-task76-doc-boundaries] overlap note: TASK-76 already has an active planning agent; this pass is docs-only (`docs/anki-integration.md`) to capture ownership boundaries after workflow decomposition. - [2026-02-21T20:35:58Z] [opencode-task76-doc-boundaries-20260221T203558Z-h7q4|opencode-task76-doc-boundaries] overlap note: TASK-76 already has an active planning agent; this pass is docs-only (`docs/anki-integration.md`) to capture ownership boundaries after workflow decomposition.
- [2026-02-21T21:16:18Z] [opencode-task76-anki-workflows-20260221T201659Z-r4p1|opencode-task76-anki-workflows] completed TASK-76: extracted `note-update-workflow` + `field-grouping-workflow` services, delegated facade hotpaths in `src/anki-integration.ts`, added focused workflow seam tests, docs ownership boundaries updated, `bun run build && bun run test:core:dist` green, and backlog TASK-76 marked Done. - [2026-02-21T21:16:18Z] [opencode-task76-anki-workflows-20260221T201659Z-r4p1|opencode-task76-anki-workflows] completed TASK-76: extracted `note-update-workflow` + `field-grouping-workflow` services, delegated facade hotpaths in `src/anki-integration.ts`, added focused workflow seam tests, docs ownership boundaries updated, `bun run build && bun run test:core:dist` green, and backlog TASK-76 marked Done.
- [2026-02-21T21:37:07Z] [codex-docs-unpushed-review-20260221T213707Z-lyej|codex-docs-unpushed-review] starting docs-drift audit for unpushed commits (`origin/main..HEAD`); scope docs alignment only, no behavior/code rewrites. - [2026-02-21T21:37:07Z] [codex-docs-unpushed-review-20260221T213707Z-lyej|codex-docs-unpushed-review] starting docs-drift audit for unpushed commits (`origin/main..HEAD`); scope docs alignment only, no behavior/code rewrites.
- [2026-02-21T21:39:15Z] [codex-docs-unpushed-review-20260221T213707Z-lyej|codex-docs-unpushed-review] completed docs-drift pass for current HEAD: removed stale lowercase config-dir fallback claim (`docs/configuration.md`) and added launcher regression lane in dev test commands (`docs/development.md`).
- [2026-02-21T23:18:04Z] [codex-task72-strict-startup-config-20260221T231804Z-3ngd|codex-task72-strict-startup-config] starting TASK-72 via Backlog MCP + writing-plans/executing-plans workflow; scope expected around startup config strictness parity, user-facing errors, tests, and docs.
- [2026-02-21T23:20:16Z] [opencode-task77-tokenizer-stages-20260221T232016Z-v9k2|opencode-task77-tokenizer-stages] starting TASK-77 via Backlog MCP + writing-plans/executing-plans workflow; expected overlap around tokenizer/pipeline modules and related tests, coordinating to avoid clobber.
- [2026-02-21T23:47:08Z] [opencode-task77-tokenizer-stages-20260221T232016Z-v9k2|opencode-task77-tokenizer-stages] completed TASK-77: split tokenizer into parser-selection/enrichment/annotation/runtime stages, added direct stage tests, ran tokenizer+core src/dist gates green, and finalized Backlog task to Done.
- [2026-02-21T23:18:16Z] [codex-task75-mpv-osd-buffered-20260221T231816Z-yj32|codex-task75-mpv-osd-buffered] starting TASK-75 via Backlog MCP + writing-plans/executing-plans; scope `src/main/runtime/mpv-osd-log*` + wiring/tests for buffered async OSD logging with shutdown flush behavior.
- [2026-02-21T23:48:10Z] [codex-task75-mpv-osd-buffered-20260221T231816Z-yj32|codex-task75-mpv-osd-buffered] completed TASK-75: moved MPV OSD log writes to buffered async queue + flush path, wired `flushMpvLog` into on-will-quit cleanup, focused runtime/lifecycle tests passing, backlog task marked Done; full `bun run build` still blocked by unrelated tokenizer logger typing issue.
- [2026-02-21T23:21:55Z] [opencode-task72-strict-startup-config-20260221T232155Z-kf0o|opencode-task72-strict-startup-config] overlap note: implementing user-requested Task 1 for TASK-72 in `src/config/service.ts` + `src/config/config.test.ts`; another planning agent exists on TASK-72 (`codex-task72-...`), keeping edits minimal and scoped to startup malformed-config strict failure behavior.
- [2026-02-21T23:24:32Z] [opencode-task72-strict-startup-config-20260221T232155Z-kf0o|opencode-task72-strict-startup-config] completed Task 1: constructor now throws `ConfigStartupParseError` on malformed startup config (path + parse reason in message) instead of silently defaulting; added startup malformed-config constructor regression test; `bun run build && node --test dist/config/config.test.js` passed.
- [2026-02-21T23:21:37Z] [opencode-task72-parse-details-20260221T232137Z-b63t|opencode-task72-parse-details] overlap note: implementing only TASK-72 Task 2 (`src/main/config-validation.ts`, `src/main/runtime/startup-config.ts`, and related tests) while preserving existing TASK-72 planning artifacts from `codex-task72-strict-startup-config-20260221T231804Z-3ngd`.
- [2026-02-21T23:25:07Z] [opencode-task77-sliceb-20260221T232507Z-vzk5|opencode-task77-sliceb] overlap note: implementing TASK-77 slice B in `src/core/services/tokenizer/parser-enrichment-stage*.ts` only; preserving concurrent TASK-77 planning/slice-A work and leaving `src/core/services/tokenizer.ts` untouched per request.
- [2026-02-21T23:27:40Z] [opencode-task77-sliceb-20260221T232507Z-vzk5|opencode-task77-sliceb] completed TASK-77 slice B request: extracted parser enrichment stage pure logic into `parser-enrichment-stage.ts`, added focused tests in `parser-enrichment-stage.test.ts`, and validated with `bun test src/core/services/tokenizer/parser-enrichment-stage.test.ts` (3 pass).
- [2026-02-22T00:01:00Z] [opencode-task77-slice-a-20260222T000100Z-j4p2|opencode-task77-slice-a] overlap note: implementing TASK-77 slice A in `src/core/services/tokenizer/parser-selection-stage.ts` + focused tests; no edits to `src/core/services/tokenizer.ts` in this pass.
- [2026-02-22T00:03:30Z] [opencode-task77-slice-a-20260222T000100Z-j4p2|opencode-task77-slice-a] completed TASK-77 slice A: added pure parser-selection-stage module + focused tests (scanning preference, mecab fallback split, suspicious-kana tie-break); targeted bun test command green.
- [2026-02-21T23:24:12Z] [opencode-task72-parse-details-20260221T232137Z-b63t|opencode-task72-parse-details] completed TASK-72 Task 2 scope: shared parse-error formatter now in `src/main/config-validation.ts`, startup hot-reload parse-failure path uses formatter, tests updated (`src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`), required build+node-test command passed.
- [2026-02-21T23:26:29Z] [codex-task72-strict-startup-config-20260221T231804Z-3ngd|codex-task72-strict-startup-config] completed TASK-72 end-to-end (no commit): integrated Task 3 startup guard in `src/main.ts`, docs behavior update in `docs/configuration.md`, focused tests pass (`bun test ...` 46/46), and backlog TASK-72 finalized Done; full build still blocked by unrelated TASK-75/TASK-77 TS errors.
- [2026-02-21T23:56:04Z] [opencode-task78-config-domain-20260221T235604Z-p9x2|opencode-task78-config-domain] starting TASK-78 via Backlog MCP + writing-plans/executing-plans workflow; expected scope `src/config/definitions.ts`, `src/config/service.ts`, new config-domain modules/tests, and docs updates as needed.
- [2026-02-22T00:05:00Z] [opencode-task78-config-domain-20260221T235604Z-p9x2|opencode-task78-config-domain] completed TASK-78: split config definitions into domain modules under `src/config/definitions/*`, kept composed public API at `src/config/definitions.ts`, added domain-registry tests, updated contributor docs, and finalized backlog task to Done; `make generate-config` blocked by unrelated pre-existing `src/main/state.test.ts` export errors.
- [2026-02-21T23:56:52Z] [opencode-task79-runtime-reducers-20260221T235652Z-n4p7|opencode-task79-runtime-reducers] starting TASK-79 via Backlog MCP + writing-plans/executing-plans; initial scope expected around `src/main.ts` runtime-state mutation paths, reducer helpers, invariant tests, and ownership docs; parallel slices where safe.
- [2026-02-22T00:06:10Z] [opencode-task79-runtime-reducers-20260221T235652Z-n4p7|opencode-task79-runtime-reducers] implementing TASK-79 slice A: added reducer helpers + focused `src/main/state.test.ts`; rewired migrated `src/main.ts` AniList mutation paths (client-secret state, retry metadata, media-guess runtime fields, in-flight flag) through `src/main/state.ts` transitions.
- [2026-02-22T00:02:53Z] [opencode-task79-sliceb-20260222T000253Z-m2r7|opencode-task79-sliceb] overlap note: implementing TASK-79 slice B only (`src/main/runtime/anilist-state.test.ts`, `src/main/runtime/anilist-media-state.test.ts`, `docs/architecture.md`) for invariants coverage and composition-boundary ownership docs; no runtime behavior code changes.
- [2026-02-22T00:04:21Z] [opencode-task79-sliceb-20260222T000253Z-m2r7|opencode-task79-sliceb] completed TASK-79 slice B request: added invariants coverage in AniList runtime/media tests (queue metadata preservation, clear-token non-mutation, guess-only reset, tracking reset idempotence), documented migrated runtime reducer ownership rules in architecture docs, and verified focused tests green (8 pass).
- [2026-02-22T00:10:51Z] [opencode-task79-runtime-reducers-20260221T235652Z-n4p7|opencode-task79-runtime-reducers] completed TASK-79 end-to-end: merged slice A/B, added explicit AniList runtime transitions in `src/main/state.ts`, rewired migrated `src/main.ts` mutation paths, fixed `state.ts` core-services import coupling for focused tests, verified `bun test src/main/state.test.ts src/main/runtime/anilist-state.test.ts src/main/runtime/anilist-media-state.test.ts` + `bun run build` + `bun run test:core:src`, and marked backlog task Done.

View File

@@ -392,7 +392,6 @@ import {
} from './core/services'; } from './core/services';
import { import {
guessAnilistMediaInfo, guessAnilistMediaInfo,
type AnilistMediaGuess,
updateAnilistPostWatchProgress, updateAnilistPostWatchProgress,
} from './core/services/anilist/anilist-updater'; } from './core/services/anilist/anilist-updater';
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store'; import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
@@ -417,7 +416,21 @@ import {
} from './main/frequency-dictionary-runtime'; } from './main/frequency-dictionary-runtime';
import { createMediaRuntimeService } from './main/media-runtime'; import { createMediaRuntimeService } from './main/media-runtime';
import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility-runtime'; import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility-runtime';
import { type AppState, type StartupState, applyStartupState, createAppState } from './main/state'; import {
type AnilistMediaGuessRuntimeState,
type AppState,
type StartupState,
applyStartupState,
createAppState,
createInitialAnilistMediaGuessRuntimeState,
createInitialAnilistUpdateInFlightState,
transitionAnilistClientSecretState,
transitionAnilistMediaGuessRuntimeState,
transitionAnilistRetryQueueLastAttemptAt,
transitionAnilistRetryQueueLastError,
transitionAnilistRetryQueueState,
transitionAnilistUpdateInFlightState,
} from './main/state';
import { import {
isAllowedAnilistExternalUrl, isAllowedAnilistExternalUrl,
isAllowedAnilistSetupNavigationUrl, isAllowedAnilistSetupNavigationUrl,
@@ -464,12 +477,9 @@ const JELLYFIN_TOKEN_STORE_FILE = 'jellyfin-token-store.json';
const ANILIST_RETRY_QUEUE_FILE = 'anilist-retry-queue.json'; const ANILIST_RETRY_QUEUE_FILE = 'anilist-retry-queue.json';
const TRAY_TOOLTIP = 'SubMiner'; const TRAY_TOOLTIP = 'SubMiner';
let anilistCurrentMediaKey: string | null = null; let anilistMediaGuessRuntimeState: AnilistMediaGuessRuntimeState =
let anilistCurrentMediaDurationSec: number | null = null; createInitialAnilistMediaGuessRuntimeState();
let anilistCurrentMediaGuess: AnilistMediaGuess | null = null; let anilistUpdateInFlightState = createInitialAnilistUpdateInFlightState();
let anilistCurrentMediaGuessPromise: Promise<AnilistMediaGuess | null> | null = null;
let anilistLastDurationProbeAtMs = 0;
let anilistUpdateInFlight = false;
const anilistAttemptedUpdateKeys = new Set<string>(); const anilistAttemptedUpdateKeys = new Set<string>();
let anilistCachedAccessToken: string | null = null; let anilistCachedAccessToken: string | null = null;
let jellyfinPlayQuitOnDisconnectArmed = false; let jellyfinPlayQuitOnDisconnectArmed = false;
@@ -644,11 +654,17 @@ const buildImmersionMediaRuntimeMainDepsHandler = createBuildImmersionMediaRunti
const buildAnilistStateRuntimeMainDepsHandler = createBuildAnilistStateRuntimeMainDepsHandler({ const buildAnilistStateRuntimeMainDepsHandler = createBuildAnilistStateRuntimeMainDepsHandler({
getClientSecretState: () => appState.anilistClientSecretState, getClientSecretState: () => appState.anilistClientSecretState,
setClientSecretState: (next) => { setClientSecretState: (next) => {
appState.anilistClientSecretState = next; appState.anilistClientSecretState = transitionAnilistClientSecretState(
appState.anilistClientSecretState,
next,
);
}, },
getRetryQueueState: () => appState.anilistRetryQueueState, getRetryQueueState: () => appState.anilistRetryQueueState,
setRetryQueueState: (next) => { setRetryQueueState: (next) => {
appState.anilistRetryQueueState = next; appState.anilistRetryQueueState = transitionAnilistRetryQueueState(
appState.anilistRetryQueueState,
next,
);
}, },
getUpdateQueueSnapshot: () => anilistUpdateQueue.getSnapshot(), getUpdateQueueSnapshot: () => anilistUpdateQueue.getSnapshot(),
clearStoredToken: () => anilistTokenStore.clearToken(), clearStoredToken: () => anilistTokenStore.clearToken(),
@@ -1563,51 +1579,87 @@ const {
}, },
resetMediaTrackingMainDeps: { resetMediaTrackingMainDeps: {
setMediaKey: (value) => { setMediaKey: (value) => {
anilistCurrentMediaKey = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaKey: value },
);
}, },
setMediaDurationSec: (value) => { setMediaDurationSec: (value) => {
anilistCurrentMediaDurationSec = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaDurationSec: value },
);
}, },
setMediaGuess: (value) => { setMediaGuess: (value) => {
anilistCurrentMediaGuess = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaGuess: value },
);
}, },
setMediaGuessPromise: (value) => { setMediaGuessPromise: (value) => {
anilistCurrentMediaGuessPromise = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaGuessPromise: value },
);
}, },
setLastDurationProbeAtMs: (value) => { setLastDurationProbeAtMs: (value) => {
anilistLastDurationProbeAtMs = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ lastDurationProbeAtMs: value },
);
}, },
}, },
getMediaGuessRuntimeStateMainDeps: { getMediaGuessRuntimeStateMainDeps: {
getMediaKey: () => anilistCurrentMediaKey, getMediaKey: () => anilistMediaGuessRuntimeState.mediaKey,
getMediaDurationSec: () => anilistCurrentMediaDurationSec, getMediaDurationSec: () => anilistMediaGuessRuntimeState.mediaDurationSec,
getMediaGuess: () => anilistCurrentMediaGuess, getMediaGuess: () => anilistMediaGuessRuntimeState.mediaGuess,
getMediaGuessPromise: () => anilistCurrentMediaGuessPromise, getMediaGuessPromise: () => anilistMediaGuessRuntimeState.mediaGuessPromise,
getLastDurationProbeAtMs: () => anilistLastDurationProbeAtMs, getLastDurationProbeAtMs: () => anilistMediaGuessRuntimeState.lastDurationProbeAtMs,
}, },
setMediaGuessRuntimeStateMainDeps: { setMediaGuessRuntimeStateMainDeps: {
setMediaKey: (value) => { setMediaKey: (value) => {
anilistCurrentMediaKey = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaKey: value },
);
}, },
setMediaDurationSec: (value) => { setMediaDurationSec: (value) => {
anilistCurrentMediaDurationSec = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaDurationSec: value },
);
}, },
setMediaGuess: (value) => { setMediaGuess: (value) => {
anilistCurrentMediaGuess = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaGuess: value },
);
}, },
setMediaGuessPromise: (value) => { setMediaGuessPromise: (value) => {
anilistCurrentMediaGuessPromise = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaGuessPromise: value },
);
}, },
setLastDurationProbeAtMs: (value) => { setLastDurationProbeAtMs: (value) => {
anilistLastDurationProbeAtMs = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ lastDurationProbeAtMs: value },
);
}, },
}, },
resetMediaGuessStateMainDeps: { resetMediaGuessStateMainDeps: {
setMediaGuess: (value) => { setMediaGuess: (value) => {
anilistCurrentMediaGuess = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaGuess: value },
);
}, },
setMediaGuessPromise: (value) => { setMediaGuessPromise: (value) => {
anilistCurrentMediaGuessPromise = value; anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
anilistMediaGuessRuntimeState,
{ mediaGuessPromise: value },
);
}, },
}, },
maybeProbeDurationMainDeps: { maybeProbeDurationMainDeps: {
@@ -1635,10 +1687,16 @@ const {
nextReady: () => anilistUpdateQueue.nextReady(), nextReady: () => anilistUpdateQueue.nextReady(),
refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(), refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
setLastAttemptAt: (value) => { setLastAttemptAt: (value) => {
appState.anilistRetryQueueState.lastAttemptAt = value; appState.anilistRetryQueueState = transitionAnilistRetryQueueLastAttemptAt(
appState.anilistRetryQueueState,
value,
);
}, },
setLastError: (value) => { setLastError: (value) => {
appState.anilistRetryQueueState.lastError = value; appState.anilistRetryQueueState = transitionAnilistRetryQueueLastError(
appState.anilistRetryQueueState,
value,
);
}, },
refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(), refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(),
updateAnilistPostWatchProgress: (accessToken, title, episode) => updateAnilistPostWatchProgress: (accessToken, title, episode) =>
@@ -1656,15 +1714,18 @@ const {
now: () => Date.now(), now: () => Date.now(),
}, },
maybeRunPostWatchUpdateMainDeps: { maybeRunPostWatchUpdateMainDeps: {
getInFlight: () => anilistUpdateInFlight, getInFlight: () => anilistUpdateInFlightState.inFlight,
setInFlight: (value) => { setInFlight: (value) => {
anilistUpdateInFlight = value; anilistUpdateInFlightState = transitionAnilistUpdateInFlightState(
anilistUpdateInFlightState,
value,
);
}, },
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig), isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
getCurrentMediaKey: () => getCurrentAnilistMediaKey(), getCurrentMediaKey: () => getCurrentAnilistMediaKey(),
hasMpvClient: () => Boolean(appState.mpvClient), hasMpvClient: () => Boolean(appState.mpvClient),
getTrackedMediaKey: () => anilistCurrentMediaKey, getTrackedMediaKey: () => anilistMediaGuessRuntimeState.mediaKey,
resetTrackedMedia: (mediaKey) => { resetTrackedMedia: (mediaKey) => {
resetAnilistMediaTracking(mediaKey); resetAnilistMediaTracking(mediaKey);
}, },

View File

@@ -53,6 +53,40 @@ test('reset anilist media tracking clears duration/guess/probe state', () => {
assert.equal(lastDurationProbeAtMs, 0); assert.equal(lastDurationProbeAtMs, 0);
}); });
test('reset anilist media tracking is idempotent', () => {
const state = {
mediaKey: 'old' as string | null,
mediaDurationSec: 123 as number | null,
mediaGuess: { title: 'guess' } as { title: string } | null,
mediaGuessPromise: Promise.resolve(null) as Promise<unknown> | null,
lastDurationProbeAtMs: 999,
};
const reset = createResetAnilistMediaTrackingHandler({
setMediaKey: (value) => {
state.mediaKey = value;
},
setMediaDurationSec: (value) => {
state.mediaDurationSec = value;
},
setMediaGuess: (value) => {
state.mediaGuess = value as { title: string } | null;
},
setMediaGuessPromise: (value) => {
state.mediaGuessPromise = value;
},
setLastDurationProbeAtMs: (value) => {
state.lastDurationProbeAtMs = value;
},
});
reset('/new/media');
const afterFirstReset = { ...state };
reset('/new/media');
assert.deepEqual(state, afterFirstReset);
});
test('get/set anilist media guess runtime state round-trips fields', () => { test('get/set anilist media guess runtime state round-trips fields', () => {
let state = { let state = {
mediaKey: null as string | null, mediaKey: null as string | null,
@@ -106,19 +140,27 @@ test('get/set anilist media guess runtime state round-trips fields', () => {
}); });
test('reset anilist media guess state clears guess and in-flight promise', () => { test('reset anilist media guess state clears guess and in-flight promise', () => {
let mediaGuess: { title: string } | null = { title: 'guess' }; const state = {
let mediaGuessPromise: Promise<unknown> | null = Promise.resolve(null); mediaKey: '/tmp/video.mkv' as string | null,
mediaDurationSec: 240 as number | null,
mediaGuess: { title: 'guess' } as { title: string } | null,
mediaGuessPromise: Promise.resolve(null) as Promise<unknown> | null,
lastDurationProbeAtMs: 321,
};
const resetGuessState = createResetAnilistMediaGuessStateHandler({ const resetGuessState = createResetAnilistMediaGuessStateHandler({
setMediaGuess: (value) => { setMediaGuess: (value) => {
mediaGuess = value as { title: string } | null; state.mediaGuess = value as { title: string } | null;
}, },
setMediaGuessPromise: (value) => { setMediaGuessPromise: (value) => {
mediaGuessPromise = value; state.mediaGuessPromise = value;
}, },
}); });
resetGuessState(); resetGuessState();
assert.equal(mediaGuess, null); assert.equal(state.mediaGuess, null);
assert.equal(mediaGuessPromise, null); assert.equal(state.mediaGuessPromise, null);
assert.equal(state.mediaKey, '/tmp/video.mkv');
assert.equal(state.mediaDurationSec, 240);
assert.equal(state.lastDurationProbeAtMs, 321);
}); });

View File

@@ -34,8 +34,6 @@ function createRuntime() {
pending: 7, pending: 7,
ready: 8, ready: 8,
deadLetter: 9, deadLetter: 9,
lastAttemptAt: 3000,
lastError: 'boom' as string | null,
}), }),
clearStoredToken: () => { clearStoredToken: () => {
clearedStoredToken = true; clearedStoredToken = true;
@@ -71,7 +69,7 @@ test('setClientSecretState merges partial updates', () => {
}); });
}); });
test('refresh/get queue snapshot uses update queue snapshot', () => { test('queue refresh preserves metadata while syncing counts', () => {
const harness = createRuntime(); const harness = createRuntime();
const snapshot = harness.runtime.getQueueStatusSnapshot(); const snapshot = harness.runtime.getQueueStatusSnapshot();
@@ -79,14 +77,15 @@ test('refresh/get queue snapshot uses update queue snapshot', () => {
pending: 7, pending: 7,
ready: 8, ready: 8,
deadLetter: 9, deadLetter: 9,
lastAttemptAt: 3000, lastAttemptAt: 2000,
lastError: 'boom', lastError: 'none',
}); });
assert.deepEqual(harness.getQueueState(), snapshot); assert.deepEqual(harness.getQueueState(), snapshot);
}); });
test('clearTokenState resets token state and clears caches', () => { test('clearTokenState resets token state and clears caches', () => {
const harness = createRuntime(); const harness = createRuntime();
const queueBeforeClear = { ...harness.getQueueState() };
harness.runtime.clearTokenState(); harness.runtime.clearTokenState();
assert.equal(harness.getClearedStoredToken(), true); assert.equal(harness.getClearedStoredToken(), true);
@@ -98,4 +97,5 @@ test('clearTokenState resets token state and clears caches', () => {
resolvedAt: null, resolvedAt: null,
errorAt: null, errorAt: null,
}); });
assert.deepEqual(harness.getQueueState(), queueBeforeClear);
}); });

93
src/main/state.test.ts Normal file
View File

@@ -0,0 +1,93 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createInitialAnilistMediaGuessRuntimeState,
createInitialAnilistUpdateInFlightState,
transitionAnilistClientSecretState,
transitionAnilistMediaGuessRuntimeState,
transitionAnilistRetryQueueLastAttemptAt,
transitionAnilistRetryQueueLastError,
transitionAnilistUpdateInFlightState,
} from './state';
test('transitionAnilistClientSecretState replaces state object', () => {
const current = {
status: 'not_checked',
source: 'none',
message: null,
resolvedAt: null,
errorAt: null,
} as const;
const next = {
status: 'resolved',
source: 'stored',
message: 'ok',
resolvedAt: 123,
errorAt: null,
} as const;
const transitioned = transitionAnilistClientSecretState(current, next);
assert.deepEqual(transitioned, next);
assert.equal(transitioned, next);
});
test('retry queue metadata transitions preserve queue counts', () => {
const queue = {
pending: 2,
ready: 1,
deadLetter: 4,
lastAttemptAt: null,
lastError: null,
};
const attempted = transitionAnilistRetryQueueLastAttemptAt(queue, 999);
const failed = transitionAnilistRetryQueueLastError(attempted, 'boom');
assert.deepEqual(attempted, {
pending: 2,
ready: 1,
deadLetter: 4,
lastAttemptAt: 999,
lastError: null,
});
assert.deepEqual(failed, {
pending: 2,
ready: 1,
deadLetter: 4,
lastAttemptAt: 999,
lastError: 'boom',
});
assert.notEqual(attempted, queue);
assert.notEqual(failed, attempted);
});
test('transitionAnilistMediaGuessRuntimeState applies partial updates', () => {
const current = createInitialAnilistMediaGuessRuntimeState();
const promise = Promise.resolve(null);
const transitioned = transitionAnilistMediaGuessRuntimeState(current, {
mediaKey: '/tmp/media.mkv',
mediaGuessPromise: promise,
lastDurationProbeAtMs: 500,
});
assert.deepEqual(transitioned, {
mediaKey: '/tmp/media.mkv',
mediaDurationSec: null,
mediaGuess: null,
mediaGuessPromise: promise,
lastDurationProbeAtMs: 500,
});
assert.notEqual(transitioned, current);
});
test('transitionAnilistUpdateInFlightState updates inFlight only', () => {
const current = createInitialAnilistUpdateInFlightState();
const transitioned = transitionAnilistUpdateInFlightState(current, true);
assert.deepEqual(current, { inFlight: false });
assert.deepEqual(transitioned, { inFlight: true });
assert.notEqual(transitioned, current);
});

View File

@@ -12,13 +12,14 @@ import type {
import type { CliArgs } from '../cli/args'; import type { CliArgs } from '../cli/args';
import type { SubtitleTimingTracker } from '../subtitle-timing-tracker'; import type { SubtitleTimingTracker } from '../subtitle-timing-tracker';
import type { AnkiIntegration } from '../anki-integration'; import type { AnkiIntegration } from '../anki-integration';
import type { ImmersionTrackerService } from '../core/services'; import type { ImmersionTrackerService } from '../core/services/immersion-tracker-service';
import type { MpvIpcClient } from '../core/services'; import type { MpvIpcClient } from '../core/services/mpv';
import type { JellyfinRemoteSessionService } from '../core/services'; import type { JellyfinRemoteSessionService } from '../core/services/jellyfin-remote';
import { DEFAULT_MPV_SUBTITLE_RENDER_METRICS } from '../core/services'; import { DEFAULT_MPV_SUBTITLE_RENDER_METRICS } from '../core/services/mpv-render-metrics';
import type { RuntimeOptionsManager } from '../runtime-options'; import type { RuntimeOptionsManager } from '../runtime-options';
import type { MecabTokenizer } from '../mecab-tokenizer'; import type { MecabTokenizer } from '../mecab-tokenizer';
import type { BaseWindowTracker } from '../window-trackers'; import type { BaseWindowTracker } from '../window-trackers';
import type { AnilistMediaGuess } from '../core/services/anilist/anilist-updater';
export interface AnilistSecretResolutionState { export interface AnilistSecretResolutionState {
status: 'not_checked' | 'resolved' | 'error'; status: 'not_checked' | 'resolved' | 'error';
@@ -36,6 +37,108 @@ export interface AnilistRetryQueueState {
lastError: string | null; lastError: string | null;
} }
export interface AnilistMediaGuessRuntimeState {
mediaKey: string | null;
mediaDurationSec: number | null;
mediaGuess: AnilistMediaGuess | null;
mediaGuessPromise: Promise<AnilistMediaGuess | null> | null;
lastDurationProbeAtMs: number;
}
export interface AnilistUpdateInFlightState {
inFlight: boolean;
}
export function createInitialAnilistSecretResolutionState(): AnilistSecretResolutionState {
return {
status: 'not_checked',
source: 'none',
message: null,
resolvedAt: null,
errorAt: null,
};
}
export function createInitialAnilistRetryQueueState(): AnilistRetryQueueState {
return {
pending: 0,
ready: 0,
deadLetter: 0,
lastAttemptAt: null,
lastError: null,
};
}
export function createInitialAnilistMediaGuessRuntimeState(): AnilistMediaGuessRuntimeState {
return {
mediaKey: null,
mediaDurationSec: null,
mediaGuess: null,
mediaGuessPromise: null,
lastDurationProbeAtMs: 0,
};
}
export function createInitialAnilistUpdateInFlightState(): AnilistUpdateInFlightState {
return {
inFlight: false,
};
}
export function transitionAnilistClientSecretState(
_current: AnilistSecretResolutionState,
next: AnilistSecretResolutionState,
): AnilistSecretResolutionState {
return next;
}
export function transitionAnilistRetryQueueState(
_current: AnilistRetryQueueState,
next: AnilistRetryQueueState,
): AnilistRetryQueueState {
return next;
}
export function transitionAnilistRetryQueueLastAttemptAt(
current: AnilistRetryQueueState,
lastAttemptAt: number | null,
): AnilistRetryQueueState {
return {
...current,
lastAttemptAt,
};
}
export function transitionAnilistRetryQueueLastError(
current: AnilistRetryQueueState,
lastError: string | null,
): AnilistRetryQueueState {
return {
...current,
lastError,
};
}
export function transitionAnilistMediaGuessRuntimeState(
current: AnilistMediaGuessRuntimeState,
partial: Partial<AnilistMediaGuessRuntimeState>,
): AnilistMediaGuessRuntimeState {
return {
...current,
...partial,
};
}
export function transitionAnilistUpdateInFlightState(
current: AnilistUpdateInFlightState,
inFlight: boolean,
): AnilistUpdateInFlightState {
return {
...current,
inFlight,
};
}
export interface AppState { export interface AppState {
yomitanExt: Extension | null; yomitanExt: Extension | null;
yomitanSettingsWindow: BrowserWindow | null; yomitanSettingsWindow: BrowserWindow | null;
@@ -123,13 +226,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
currentMediaPath: null, currentMediaPath: null,
currentMediaTitle: null, currentMediaTitle: null,
pendingSubtitlePosition: null, pendingSubtitlePosition: null,
anilistClientSecretState: { anilistClientSecretState: createInitialAnilistSecretResolutionState(),
status: 'not_checked',
source: 'none',
message: null,
resolvedAt: null,
errorAt: null,
},
mecabTokenizer: null, mecabTokenizer: null,
keybindings: [], keybindings: [],
subtitleTimingTracker: null, subtitleTimingTracker: null,
@@ -159,13 +256,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
jlptLevelLookup: () => null, jlptLevelLookup: () => null,
frequencyRankLookup: () => null, frequencyRankLookup: () => null,
anilistSetupPageOpened: false, anilistSetupPageOpened: false,
anilistRetryQueueState: { anilistRetryQueueState: createInitialAnilistRetryQueueState(),
pending: 0,
ready: 0,
deadLetter: 0,
lastAttemptAt: null,
lastError: null,
},
}; };
} }