mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor(main): introduce explicit AniList runtime transitions
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
---
|
||||
id: TASK-79
|
||||
title: Introduce explicit runtime state transitions and reducers in main
|
||||
status: To Do
|
||||
assignee: []
|
||||
status: Done
|
||||
assignee:
|
||||
- '@sudacode'
|
||||
created_date: '2026-02-18 11:43'
|
||||
updated_date: '2026-02-18 11:43'
|
||||
updated_date: '2026-02-22 00:10'
|
||||
labels:
|
||||
- main-process
|
||||
- state-management
|
||||
@@ -41,15 +42,49 @@ Main runtime state is currently mutable from many callsites. This task introduce
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 Critical runtime state domains mutate through explicit transition helpers
|
||||
- [ ] #2 State invariants are test-covered
|
||||
- [ ] #3 Direct ad-hoc mutation in migrated domains is removed
|
||||
- [ ] #4 Ownership/mutation rules documented
|
||||
- [x] #1 Critical runtime state domains mutate through explicit transition helpers
|
||||
- [x] #2 State invariants are test-covered
|
||||
- [x] #3 Direct ad-hoc mutation in migrated domains is removed
|
||||
- [x] #4 Ownership/mutation rules documented
|
||||
<!-- 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
|
||||
<!-- DOD:BEGIN -->
|
||||
- [ ] #1 Core tests pass after migration
|
||||
- [ ] #2 No behavior regressions in startup/IPC/overlay flows
|
||||
- [x] #1 Core tests pass after migration
|
||||
- [x] #2 No behavior regressions in startup/IPC/overlay flows
|
||||
<!-- DOD:END -->
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Read first. Keep concise.
|
||||
|
||||
| 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-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` |
|
||||
@@ -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` |
|
||||
| `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` |
|
||||
| `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` |
|
||||
|
||||
@@ -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.
|
||||
@@ -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-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: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.
|
||||
|
||||
125
src/main.ts
125
src/main.ts
@@ -392,7 +392,6 @@ import {
|
||||
} from './core/services';
|
||||
import {
|
||||
guessAnilistMediaInfo,
|
||||
type AnilistMediaGuess,
|
||||
updateAnilistPostWatchProgress,
|
||||
} from './core/services/anilist/anilist-updater';
|
||||
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
||||
@@ -417,7 +416,21 @@ import {
|
||||
} from './main/frequency-dictionary-runtime';
|
||||
import { createMediaRuntimeService } from './main/media-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 {
|
||||
isAllowedAnilistExternalUrl,
|
||||
isAllowedAnilistSetupNavigationUrl,
|
||||
@@ -464,12 +477,9 @@ const JELLYFIN_TOKEN_STORE_FILE = 'jellyfin-token-store.json';
|
||||
const ANILIST_RETRY_QUEUE_FILE = 'anilist-retry-queue.json';
|
||||
const TRAY_TOOLTIP = 'SubMiner';
|
||||
|
||||
let anilistCurrentMediaKey: string | null = null;
|
||||
let anilistCurrentMediaDurationSec: number | null = null;
|
||||
let anilistCurrentMediaGuess: AnilistMediaGuess | null = null;
|
||||
let anilistCurrentMediaGuessPromise: Promise<AnilistMediaGuess | null> | null = null;
|
||||
let anilistLastDurationProbeAtMs = 0;
|
||||
let anilistUpdateInFlight = false;
|
||||
let anilistMediaGuessRuntimeState: AnilistMediaGuessRuntimeState =
|
||||
createInitialAnilistMediaGuessRuntimeState();
|
||||
let anilistUpdateInFlightState = createInitialAnilistUpdateInFlightState();
|
||||
const anilistAttemptedUpdateKeys = new Set<string>();
|
||||
let anilistCachedAccessToken: string | null = null;
|
||||
let jellyfinPlayQuitOnDisconnectArmed = false;
|
||||
@@ -644,11 +654,17 @@ const buildImmersionMediaRuntimeMainDepsHandler = createBuildImmersionMediaRunti
|
||||
const buildAnilistStateRuntimeMainDepsHandler = createBuildAnilistStateRuntimeMainDepsHandler({
|
||||
getClientSecretState: () => appState.anilistClientSecretState,
|
||||
setClientSecretState: (next) => {
|
||||
appState.anilistClientSecretState = next;
|
||||
appState.anilistClientSecretState = transitionAnilistClientSecretState(
|
||||
appState.anilistClientSecretState,
|
||||
next,
|
||||
);
|
||||
},
|
||||
getRetryQueueState: () => appState.anilistRetryQueueState,
|
||||
setRetryQueueState: (next) => {
|
||||
appState.anilistRetryQueueState = next;
|
||||
appState.anilistRetryQueueState = transitionAnilistRetryQueueState(
|
||||
appState.anilistRetryQueueState,
|
||||
next,
|
||||
);
|
||||
},
|
||||
getUpdateQueueSnapshot: () => anilistUpdateQueue.getSnapshot(),
|
||||
clearStoredToken: () => anilistTokenStore.clearToken(),
|
||||
@@ -1563,51 +1579,87 @@ const {
|
||||
},
|
||||
resetMediaTrackingMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
anilistCurrentMediaKey = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaKey: value },
|
||||
);
|
||||
},
|
||||
setMediaDurationSec: (value) => {
|
||||
anilistCurrentMediaDurationSec = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaDurationSec: value },
|
||||
);
|
||||
},
|
||||
setMediaGuess: (value) => {
|
||||
anilistCurrentMediaGuess = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaGuess: value },
|
||||
);
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
anilistCurrentMediaGuessPromise = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaGuessPromise: value },
|
||||
);
|
||||
},
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
anilistLastDurationProbeAtMs = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ lastDurationProbeAtMs: value },
|
||||
);
|
||||
},
|
||||
},
|
||||
getMediaGuessRuntimeStateMainDeps: {
|
||||
getMediaKey: () => anilistCurrentMediaKey,
|
||||
getMediaDurationSec: () => anilistCurrentMediaDurationSec,
|
||||
getMediaGuess: () => anilistCurrentMediaGuess,
|
||||
getMediaGuessPromise: () => anilistCurrentMediaGuessPromise,
|
||||
getLastDurationProbeAtMs: () => anilistLastDurationProbeAtMs,
|
||||
getMediaKey: () => anilistMediaGuessRuntimeState.mediaKey,
|
||||
getMediaDurationSec: () => anilistMediaGuessRuntimeState.mediaDurationSec,
|
||||
getMediaGuess: () => anilistMediaGuessRuntimeState.mediaGuess,
|
||||
getMediaGuessPromise: () => anilistMediaGuessRuntimeState.mediaGuessPromise,
|
||||
getLastDurationProbeAtMs: () => anilistMediaGuessRuntimeState.lastDurationProbeAtMs,
|
||||
},
|
||||
setMediaGuessRuntimeStateMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
anilistCurrentMediaKey = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaKey: value },
|
||||
);
|
||||
},
|
||||
setMediaDurationSec: (value) => {
|
||||
anilistCurrentMediaDurationSec = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaDurationSec: value },
|
||||
);
|
||||
},
|
||||
setMediaGuess: (value) => {
|
||||
anilistCurrentMediaGuess = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaGuess: value },
|
||||
);
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
anilistCurrentMediaGuessPromise = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaGuessPromise: value },
|
||||
);
|
||||
},
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
anilistLastDurationProbeAtMs = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ lastDurationProbeAtMs: value },
|
||||
);
|
||||
},
|
||||
},
|
||||
resetMediaGuessStateMainDeps: {
|
||||
setMediaGuess: (value) => {
|
||||
anilistCurrentMediaGuess = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaGuess: value },
|
||||
);
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
anilistCurrentMediaGuessPromise = value;
|
||||
anilistMediaGuessRuntimeState = transitionAnilistMediaGuessRuntimeState(
|
||||
anilistMediaGuessRuntimeState,
|
||||
{ mediaGuessPromise: value },
|
||||
);
|
||||
},
|
||||
},
|
||||
maybeProbeDurationMainDeps: {
|
||||
@@ -1635,10 +1687,16 @@ const {
|
||||
nextReady: () => anilistUpdateQueue.nextReady(),
|
||||
refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(),
|
||||
setLastAttemptAt: (value) => {
|
||||
appState.anilistRetryQueueState.lastAttemptAt = value;
|
||||
appState.anilistRetryQueueState = transitionAnilistRetryQueueLastAttemptAt(
|
||||
appState.anilistRetryQueueState,
|
||||
value,
|
||||
);
|
||||
},
|
||||
setLastError: (value) => {
|
||||
appState.anilistRetryQueueState.lastError = value;
|
||||
appState.anilistRetryQueueState = transitionAnilistRetryQueueLastError(
|
||||
appState.anilistRetryQueueState,
|
||||
value,
|
||||
);
|
||||
},
|
||||
refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(),
|
||||
updateAnilistPostWatchProgress: (accessToken, title, episode) =>
|
||||
@@ -1656,15 +1714,18 @@ const {
|
||||
now: () => Date.now(),
|
||||
},
|
||||
maybeRunPostWatchUpdateMainDeps: {
|
||||
getInFlight: () => anilistUpdateInFlight,
|
||||
getInFlight: () => anilistUpdateInFlightState.inFlight,
|
||||
setInFlight: (value) => {
|
||||
anilistUpdateInFlight = value;
|
||||
anilistUpdateInFlightState = transitionAnilistUpdateInFlightState(
|
||||
anilistUpdateInFlightState,
|
||||
value,
|
||||
);
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig),
|
||||
getCurrentMediaKey: () => getCurrentAnilistMediaKey(),
|
||||
hasMpvClient: () => Boolean(appState.mpvClient),
|
||||
getTrackedMediaKey: () => anilistCurrentMediaKey,
|
||||
getTrackedMediaKey: () => anilistMediaGuessRuntimeState.mediaKey,
|
||||
resetTrackedMedia: (mediaKey) => {
|
||||
resetAnilistMediaTracking(mediaKey);
|
||||
},
|
||||
|
||||
@@ -53,6 +53,40 @@ test('reset anilist media tracking clears duration/guess/probe state', () => {
|
||||
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', () => {
|
||||
let state = {
|
||||
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', () => {
|
||||
let mediaGuess: { title: string } | null = { title: 'guess' };
|
||||
let mediaGuessPromise: Promise<unknown> | null = Promise.resolve(null);
|
||||
const state = {
|
||||
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({
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuess = value as { title: string } | null;
|
||||
state.mediaGuess = value as { title: string } | null;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromise = value;
|
||||
state.mediaGuessPromise = value;
|
||||
},
|
||||
});
|
||||
|
||||
resetGuessState();
|
||||
assert.equal(mediaGuess, null);
|
||||
assert.equal(mediaGuessPromise, null);
|
||||
assert.equal(state.mediaGuess, null);
|
||||
assert.equal(state.mediaGuessPromise, null);
|
||||
assert.equal(state.mediaKey, '/tmp/video.mkv');
|
||||
assert.equal(state.mediaDurationSec, 240);
|
||||
assert.equal(state.lastDurationProbeAtMs, 321);
|
||||
});
|
||||
|
||||
@@ -34,8 +34,6 @@ function createRuntime() {
|
||||
pending: 7,
|
||||
ready: 8,
|
||||
deadLetter: 9,
|
||||
lastAttemptAt: 3000,
|
||||
lastError: 'boom' as string | null,
|
||||
}),
|
||||
clearStoredToken: () => {
|
||||
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 snapshot = harness.runtime.getQueueStatusSnapshot();
|
||||
|
||||
@@ -79,14 +77,15 @@ test('refresh/get queue snapshot uses update queue snapshot', () => {
|
||||
pending: 7,
|
||||
ready: 8,
|
||||
deadLetter: 9,
|
||||
lastAttemptAt: 3000,
|
||||
lastError: 'boom',
|
||||
lastAttemptAt: 2000,
|
||||
lastError: 'none',
|
||||
});
|
||||
assert.deepEqual(harness.getQueueState(), snapshot);
|
||||
});
|
||||
|
||||
test('clearTokenState resets token state and clears caches', () => {
|
||||
const harness = createRuntime();
|
||||
const queueBeforeClear = { ...harness.getQueueState() };
|
||||
harness.runtime.clearTokenState();
|
||||
|
||||
assert.equal(harness.getClearedStoredToken(), true);
|
||||
@@ -98,4 +97,5 @@ test('clearTokenState resets token state and clears caches', () => {
|
||||
resolvedAt: null,
|
||||
errorAt: null,
|
||||
});
|
||||
assert.deepEqual(harness.getQueueState(), queueBeforeClear);
|
||||
});
|
||||
|
||||
93
src/main/state.test.ts
Normal file
93
src/main/state.test.ts
Normal 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);
|
||||
});
|
||||
@@ -12,13 +12,14 @@ import type {
|
||||
import type { CliArgs } from '../cli/args';
|
||||
import type { SubtitleTimingTracker } from '../subtitle-timing-tracker';
|
||||
import type { AnkiIntegration } from '../anki-integration';
|
||||
import type { ImmersionTrackerService } from '../core/services';
|
||||
import type { MpvIpcClient } from '../core/services';
|
||||
import type { JellyfinRemoteSessionService } from '../core/services';
|
||||
import { DEFAULT_MPV_SUBTITLE_RENDER_METRICS } from '../core/services';
|
||||
import type { ImmersionTrackerService } from '../core/services/immersion-tracker-service';
|
||||
import type { MpvIpcClient } from '../core/services/mpv';
|
||||
import type { JellyfinRemoteSessionService } from '../core/services/jellyfin-remote';
|
||||
import { DEFAULT_MPV_SUBTITLE_RENDER_METRICS } from '../core/services/mpv-render-metrics';
|
||||
import type { RuntimeOptionsManager } from '../runtime-options';
|
||||
import type { MecabTokenizer } from '../mecab-tokenizer';
|
||||
import type { BaseWindowTracker } from '../window-trackers';
|
||||
import type { AnilistMediaGuess } from '../core/services/anilist/anilist-updater';
|
||||
|
||||
export interface AnilistSecretResolutionState {
|
||||
status: 'not_checked' | 'resolved' | 'error';
|
||||
@@ -36,6 +37,108 @@ export interface AnilistRetryQueueState {
|
||||
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 {
|
||||
yomitanExt: Extension | null;
|
||||
yomitanSettingsWindow: BrowserWindow | null;
|
||||
@@ -123,13 +226,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
||||
currentMediaPath: null,
|
||||
currentMediaTitle: null,
|
||||
pendingSubtitlePosition: null,
|
||||
anilistClientSecretState: {
|
||||
status: 'not_checked',
|
||||
source: 'none',
|
||||
message: null,
|
||||
resolvedAt: null,
|
||||
errorAt: null,
|
||||
},
|
||||
anilistClientSecretState: createInitialAnilistSecretResolutionState(),
|
||||
mecabTokenizer: null,
|
||||
keybindings: [],
|
||||
subtitleTimingTracker: null,
|
||||
@@ -159,13 +256,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
||||
jlptLevelLookup: () => null,
|
||||
frequencyRankLookup: () => null,
|
||||
anilistSetupPageOpened: false,
|
||||
anilistRetryQueueState: {
|
||||
pending: 0,
|
||||
ready: 0,
|
||||
deadLetter: 0,
|
||||
lastAttemptAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
anilistRetryQueueState: createInitialAnilistRetryQueueState(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user