mirror of
https://github.com/FluxaMedia/fluxa-core.git
synced 2026-07-27 12:22:12 +00:00
- 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
143 lines
4.6 KiB
Markdown
143 lines
4.6 KiB
Markdown
# 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:
|
|
|
|
```json
|
|
{
|
|
"state": { ... },
|
|
"effects": [
|
|
{
|
|
"id": "fx-1",
|
|
"type": "fetchAddonResource",
|
|
"generation": 3,
|
|
"payload": { ... }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
- `state` is the full serialized `EngineState`. Platforms snapshot this and use it to render UI.
|
|
- `effects` is the list of work the platform must execute. An empty array means no I/O is needed.
|
|
- `id` is an opaque monotonically-increasing string (`"fx-N"`). The platform echoes it back in `completeEffect`.
|
|
- `generation` lets the platform discard completions that arrived after a newer dispatch superseded them.
|
|
|
|
## completeEffect shape
|
|
|
|
```json
|
|
{
|
|
"effectId": "fx-1",
|
|
"result": { ... }
|
|
}
|
|
```
|
|
|
|
The `result` structure is effect-specific. See [effects.md](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:
|
|
|
|
```json
|
|
{
|
|
"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)
|
|
|
|
```rust
|
|
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)
|
|
|
|
```json
|
|
// 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:
|
|
|
|
```json
|
|
{
|
|
"ok": false,
|
|
"error": {
|
|
"kind": "unknown_method",
|
|
"message": "no such method `foo`",
|
|
"method": "foo"
|
|
}
|
|
}
|
|
```
|
|
|
|
Error kinds: `unknown_method`, `invalid_args`, `not_found`, `internal`.
|