- 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
4.6 KiB
The effect loop
The effect loop is how the core communicates with the host platform. The core never performs I/O — it returns a list of effects describing what it needs done, and the host executes them and reports results back.
Basic cycle
Every interaction follows this pattern:
1. Host calls dispatch(handle, actionJson)
2. Core updates state, produces a list of effects
3. Core returns { state, effects }
4. Host executes each effect
5. Host calls completeEffect(handle, { effectId, result })
6. Core may produce more effects in response — goto 4
Effects from a single dispatch can chain: completing effect A may produce effects B and C, completing B may produce effect D, and so on until the operation is fully resolved.
DispatchResult shape
Every dispatch and completeEffect call returns the same JSON envelope:
{
"state": { ... },
"effects": [
{
"id": "fx-1",
"type": "fetchAddonResource",
"generation": 3,
"payload": { ... }
}
]
}
stateis the full serializedEngineState. Platforms snapshot this and use it to render UI.effectsis the list of work the platform must execute. An empty array means no I/O is needed.idis an opaque monotonically-increasing string ("fx-N"). The platform echoes it back incompleteEffect.generationlets the platform discard completions that arrived after a newer dispatch superseded them.
completeEffect shape
{
"effectId": "fx-1",
"result": { ... }
}
The result structure is effect-specific. See effects.md for each type's expected payload.
Stale effects
Effects that the platform never completes (dropped exceptions, IPC failures) are expired after 5 minutes. The core will stop waiting for them and they will not produce further state changes.
Actions
Actions are dispatched as tagged JSON objects. The type field is camelCase and selects which handler runs. Example:
{
"type": "homeLoadRequested",
"profile": { ... },
"language": "en",
"force": false
}
All action types are defined in src/headless_engine/contracts.rs (AppAction enum). The serde tag is the wire name.
Common actions
| type | When to dispatch |
|---|---|
navigationRequested |
User navigates to a new screen |
homeLoadRequested |
Home screen loads or refreshes |
detailLoadRequested |
Detail screen opens for a content item |
detailStreamsRequested |
Stream list needs to be fetched from addons |
playerLoadStreamsRequested |
Player opens and needs stream resolution |
playerResolvePlaybackRequested |
A specific URL/stream needs playback resolution |
savePlaybackProgressRequested |
User position should be persisted |
toggleWatchlistRequested |
User adds/removes an item from watchlist |
profileActivated |
Active profile changed |
introSegmentsRequested |
Intro skip segments needed for an episode |
scrobbleRequested |
Trakt/Simkl scrobble should be sent |
homeSearchRequested |
User typed a search query |
discoverLoadRequested |
Discover/browse screen loads |
libraryHydrateRequested |
Library state should be refreshed |
Engine lifecycle
The headless engine is accessed through an integer handle. Multiple engine instances can coexist (though typically one exists per app session).
Via FluxaCore (desktop/Tauri)
let handle = FluxaCore::create_headless_engine(initial_json);
let result = FluxaCore::headless_engine_dispatch_json(handle, action_json);
let result = FluxaCore::headless_engine_complete_effect_json(handle, effect_result_json);
let snapshot = FluxaCore::headless_engine_snapshot_json(handle);
Via core_invoke (desktop/Swift)
// Create
core_invoke("engine.create", "<initial_json>")
→ { "ok": true, "value": 1 }
// Dispatch
core_invoke("engine.dispatch", { "handle": 1, "action": { "type": "homeLoadRequested" } })
→ { "ok": true, "value": { "state": {...}, "effects": [...] } }
// Complete an effect
core_invoke("engine.completeEffect", { "handle": 1, "result": { "effectId": "fx-1", "result": {...} } })
→ { "ok": true, "value": { "state": {...}, "effects": [...] } }
// Destroy
core_invoke("engine.destroy", 1)
Via JNI (Android)
Android calls the corresponding Java_com_fluxa_app_core_rust_FluxaCoreNative_* native methods from FluxaCoreNative.kt. The JNI functions wrap the same underlying headless_engine::* functions.
Error envelope
core_invoke always returns a string. On failure:
{
"ok": false,
"error": {
"kind": "unknown_method",
"message": "no such method `foo`",
"method": "foo"
}
}
Error kinds: unknown_method, invalid_args, not_found, internal.