| 50 |
50 |
|
- Secret encryption for plugin configs
|
| 51 |
51 |
|
- Circuit breaker enforcement (10 consecutive failures disables auto-fetch)
|
| 52 |
52 |
|
|
| 53 |
|
- |
**Lock discipline:** Hold the `PluginManager` lock only during synchronous operations. Release before any `.await`:
|
|
53 |
+ |
**Lock discipline:** Hold the `PluginManager` lock only for as long as you need it, and never across an `.await`:
|
| 54 |
54 |
|
|
| 55 |
55 |
|
```rust
|
| 56 |
|
- |
// GOOD: Release lock before async DB ops
|
|
56 |
+ |
// GOOD: scope the guard, then do the slow thing outside it
|
| 57 |
57 |
|
let fetch_result = {
|
| 58 |
58 |
|
let plugins = self.plugins.read().await;
|
| 59 |
59 |
|
plugins.fetch(plugin_id, None)
|
| 60 |
60 |
|
};
|
| 61 |
|
- |
// Lock released here, now safe to await
|
| 62 |
|
- |
self.db.feeds().record_fetch_success(feed_id).await?;
|
|
61 |
+ |
// Lock released here.
|
|
62 |
+ |
self.db.feeds().record_fetch_success(feed_id)?;
|
| 63 |
63 |
|
```
|
| 64 |
64 |
|
|
|
65 |
+ |
The same rule applies in the other direction to database connections, and there
|
|
66 |
+ |
it is enforced by the compiler: a rusqlite `Connection` is not `Sync`, and a
|
|
67 |
+ |
Tauri command's future must be `Send`, so holding one across an `.await` is a
|
|
68 |
+ |
compile error rather than a latent stall. Scope the connection in a block. See
|
|
69 |
+ |
`Database::conn` for the related "one connection at a time" rule.
|
|
70 |
+ |
|
| 65 |
71 |
|
## Plugin System
|
| 66 |
72 |
|
|
| 67 |
73 |
|
### Contract: 4 Required Functions
|
| 101 |
107 |
|
filter: ItemFilter,
|
| 102 |
108 |
|
) -> Result<Vec<ItemSnapshot>, ApiError> {
|
| 103 |
109 |
|
let db = state.orchestrator.database();
|
| 104 |
|
- |
let items = db.items().list(filter).await?;
|
|
110 |
+ |
let items = db.items().list(filter)?;
|
| 105 |
111 |
|
Ok(items.into_iter().map(ItemSnapshot::from).collect())
|
| 106 |
112 |
|
}
|
| 107 |
113 |
|
```
|
| 130 |
136 |
|
}
|
| 131 |
137 |
|
```
|
| 132 |
138 |
|
|
|
139 |
+ |
The repositories in `bb-db` are synchronous: SQLite has no async API, and the async this layer used to wear came from sqlx. Call them directly; do not wrap them in `async fn`.
|
|
140 |
+ |
|
| 133 |
141 |
|
`ApiError` implements `From` for `bb_db::DbError`, `rusqlite::Error`, `FeedError`, and `OrchestratorError`. The frontend receives `{code: "BAD_REQUEST", message: "..."}`.
|
| 134 |
142 |
|
|
| 135 |
143 |
|
## Circuit Breaker
|
| 214 |
222 |
|
|
| 215 |
223 |
|
```rust
|
| 216 |
224 |
|
// Common setup
|
| 217 |
|
- |
pub async fn setup(suffix: &str) -> Orchestrator {
|
|
225 |
+ |
pub fn setup(suffix: &str) -> Orchestrator {
|
| 218 |
226 |
|
let config = OrchestratorConfig {
|
| 219 |
|
- |
database_url: "sqlite::memory:".to_string(),
|
|
227 |
+ |
database_path: ":memory:".to_string(),
|
| 220 |
228 |
|
plugins_dir: temp_dir.to_string(),
|
| 221 |
229 |
|
fetch_interval_secs: 300,
|
| 222 |
230 |
|
};
|
| 223 |
|
- |
let orchestrator = Orchestrator::new(config).await.unwrap();
|
| 224 |
|
- |
orchestrator.migrate().await.unwrap();
|
|
231 |
+ |
let orchestrator = Orchestrator::new(config).unwrap();
|
|
232 |
+ |
orchestrator.migrate().unwrap();
|
| 225 |
233 |
|
orchestrator
|
| 226 |
234 |
|
}
|
| 227 |
235 |
|
```
|