Per the robustness plan's §9 (flagged as the single highest-leverage
perf change in the crate): headless_engine_dispatch_json used to clone
the entire EngineState before the reducer ran, clone it again after,
then StatePatch::diff did a deep PartialEq walk across all 16 domains
and cloned each changed one a third time — cost scaled with everything
the user has ever loaded (full catalogs, episode lists), not with what
the action actually touched.
Introduces Tracked<T> (src/headless_engine/state.rs): wraps each
EngineState domain field so any mutable access — a field write, a
whole-value replacement via DerefMut, a method call taking &mut —
flips a dirty bit automatically. Serialize/Deserialize delegate
straight to the wrapped value, so the wire format is byte-for-byte
unchanged (verified by the golden wire fixtures added in the prior
commit, which pass unmodified). EngineState::diff_dirty() replaces
StatePatch::diff(before, after): it clones only domains whose dirty
bit is set and clears it, so both the pre-dispatch full clone and the
post-dispatch PartialEq walk are gone entirely. The ~16 call sites that
did whole-domain replacement (`engine.state.detail = DetailState {
...}`) needed `*engine.state.detail = ...` instead — the compiler
catches any site the migration missed as a type error, so there's no
way for this rewrite to silently skip a domain the way a hand-added
mark_dirty() call could.
Also fixes headless_engine_snapshot_json, which used to serialize
while still holding the engines mutex; it now clones the state and
drops the lock before calling serde_json::to_string.
Adds benches/ (criterion, `--features bench` dev-only surface via a
new bench_targets re-export module mirroring the existing fuzz_targets
pattern): headless_engine_dispatch benches a NavigationRequested
dispatch against a 500-stream detail state (a domain the action
doesn't touch, so the win is directly visible), and stream_ranking
benches player_source_sidebar_plan_json grouping 500 streams. CI gets
a bench-check job compiling and smoke-running both (`--test`) without
paying for full timing runs on every push.
Per the robustness plan's §2/§3: introduce a pub(crate) CoreError enum
(BadInput/NotFound) plus a LogAndDiscard trait converting
Result<T, CoreError> back to the existing Option<T> FFI shims while
recording the failure. Rather than a push callback (no such pattern
exists anywhere in this codebase — checked jni.rs/uniffi.rs/ffi.rs and
the streaming engine, which all use poll-based Arc<Mutex<..>> state
instead), the sink is a bounded ring buffer drained via a new
`core.drainErrorLog` core_invoke method, a UniFFI
`drain_core_error_log_json` export, and a JNI
`drainCoreErrorLogJsonNative` extern — the host polls it (e.g. once per
dispatch tick) and forwards lines to logcat/desktop logs. This follows
the codebase's own established poll-not-push convention instead of
inventing a new one.
Wired into headless_engine's two real FFI boundary functions
(headless_engine_dispatch_json / headless_engine_complete_effect_json)
for both bad JSON and unknown-handle cases, and applied end-to-end
across the player_policy module (6 functions) as the first fully
migrated module, demonstrating the pattern for the remaining ~290
Option<String> functions to pick up incrementally.
Also hardens EffectResultInput per §3: effect_id is now a required
field (previously #[serde(default)] silently produced effect_id: ""
that matched no in-flight effect), and status is a closed EffectStatus
enum (Ok/Error/Cancelled) instead of a bare String — an unrecognized
status string now fails deserialization (and gets logged) instead of
comparing false against every "== \"ok\"" call site. Updated the ~39
`result.status == "ok"` / `!= "ok"` comparisons across headless_engine
to `result.status.is_ok()`; the wire strings themselves are unchanged
(serde's rename_all = "camelCase" already produces "ok"/"error").
Per the robustness plan's §8: a tests/wire/ directory holds one real
AppAction input per representative action family, each paired with a
checked-in golden DispatchResult captured from an actual dispatch (a
new #[cfg(test)] wire_fixtures_match_golden_dispatch_output test
compares against it, with UPDATE_WIRE_FIXTURES=1 to regenerate when a
change is intentional). Any future camelCase/field drift on the
dispatch/completeEffect wire now fails in this repo instead of surfacing
as a silent Android/desktop regression.
Also add fuzz/fuzz_targets/engine_dispatch.rs, feeding arbitrary bytes
into both headless_engine_dispatch_json and
headless_engine_complete_effect_json against one engine handle — this
is the one path in the crate that runs global engine-mutating logic
without a catch_unwind guard on the desktop call path, so it's the
highest-value fuzz target missing from fuzz/. Exposes the four
headless_engine entry points as `pub` (still unreachable outside the
crate except through the fuzzing-gated `fuzz_targets` re-export module)
following the same pattern already used for parse_manifest and the
content_identity helpers.
Add warn-level unsafe_op_in_unsafe_fn / unwrap_used / expect_used /
indexing_slicing / panic clippy lints (test code exempted via cfg_attr),
per the robustness plan's ratchet-to-deny strategy. Add a CI workflow
covering workspace test/clippy/fmt plus a wasm-only cargo check. Run
`cargo fmt` to clear pre-existing formatting drift so the fmt gate starts
clean, and drop a now-unnecessary `mut` flagged by clippy.
b0bb52a added real HLS segment-level RPU conversion (fluxa-streaming-engine's
OkHttp interceptor) for P7 streams with a DV decoder, but left behind a test
that still expected the old manifest-only passthrough. Split it into two
cases: decoder present -> hls_rpu_convert, decoder absent -> manifest_handled.
resolve_transport_url_json and resolve_feed_option_genre_json feed
opt_json() at the FFI boundary, which requires valid JSON — a bare
URL/genre string isn't, so resolveTransportUrl/resolveFeedOptionGenre
would fail from the frontend whenever they had a result. Finishes an
in-progress fix that JSON-encoded the transport-url return but missed
the early-return genre branch, and updates home_ranking.rs's direct
(non-FFI) callers to unwrap the now JSON-encoded string before
embedding it in a json!() literal, since json!() already does its own
string encoding.
start_torrent_server is a process-wide singleton that fully tears
down and recreates the engine on every play. A stop invoked for an
old session could race a fast replay's start and land after the new
session was already up, silently killing the just-started server and
leaving mpv with a dead socket (MPV_ERROR_LOADING_FAILED / "Loading
failed" on the same torrent). Each server generation is now tagged;
stop is a no-op unless it targets the currently active generation.
.env.example lists the Trakt/Simkl/MAL client id/secret vars companion_server
and oauth_proxy read at runtime; ignore fluxa-streaming-engine/.env so a real
copy never gets committed.
Bind the listener only after the torrent session is up (with an 18s init
timeout) instead of before, wrap add_lock/api_add_torrent in a 45s timeout
so a stuck peer/tracker can't hang requests forever, and stop stat-only
polls from triggering ensure_torrent so they can't contend with the real
stream request. Also track bytes actually streamed to the client
(CountingReader) so the preload/stat readiness check reflects playback
progress past a seek, not just bytes written to disk.
The methods added for the desktop migration (detectAnimePlayback,
anilistEntriesToSync, mergeLibraryItemsById, tmdb people helpers) now
take plain nested JSON instead of stringified-JSON-in-JSON, so callers
encode once and the router hands Values straight to the domain
functions. The AniList mapping deserializes into AnilistEntry/
AnilistMedia serde structs instead of spelunking Value paths.
Ports pure logic the desktop shell was keeping in TypeScript: the anime
playback confidence heuristic, AniList entry classification into
watchlist/completed/dropped/watching buckets with watched keys and
progress records, an id-keyed library item merge, and the TMDB
find/credits URL planning plus credits-to-people-images mapping.
tmdb_language now also accepts the desktop pref values english_us and
tr_tr.
Maps Stremio datastore library items to watchlist entries (content-level,
skipping removed/temp and per-episode rows) and to the watched-ids map
(flaggedWatched/timesWatched, episode _ids already match the id:s:e key
format). Exposed as stremioWatchlistToItems / stremioWatchedToIds for the
desktop shell; Android keeps using the JNI library_state entry points.
- Add fluxa-streaming-engine companion server (torrent start/stop,
ffmpeg transcode/probe, OAuth token exchange) for the web build
- Add SSRF guard restricting transcode/probe url param to http(s)
loopback hosts, blocking file:// and remote-host abuse via ffmpeg
- Add src/ffi.rs string-routed RPC dispatcher and wasm bindings
- Add headless engine profile module and several FFI surface
additions (scrobble plans, episode navigation, collections import/export)
- Remove dead runtime::msg/update scaffolding and addon_transport trait
- Remove fluxa_play.rs dev scratch binary (hardcoded scraper, not
part of the product)
- Reorder dv_rewrite.rs so implementation precedes its test module;
strip decorative section dividers
- Add docs/, fuzz/ targets, and a rewritten README
- Fix .gitignore to cover fluxa-streaming-engine/target
- Add headless engine with full action/effect dispatch (detail, player,
home, library, search, discover, calendar, offline, auth, sync)
- Add ffi.rs RPC dispatch table used by UniFFI (iOS) and WASM bindings
- Add wasm.rs binding surface (core_invoke + fluxa_core_version)
- Extend FluxaCore public API with tmdb, intro_segments, external_sync,
library_state, player_policy, watchlist, and search plan methods
- Fix torrent server race: replace AtomicBool + Mutex dual-state with
mutex-only source of truth for running status
- Document result_json drain contract in headless engine
- Remove decorative section dividers from core_api.rs (style)
- Remove unused normalize_skip_time function (dead code warning)
- Remove generated doc block from FluxaCore struct (style)
- Clarify app_state comment in ffi.rs (not legacy, used by Android JNI)