8.2 Одно состояние, два значения
Переписка, в которой один пишет а другой только читает, - не переписка. Командная строка пишет в журнал. Сервер читает из него при старте. Добавленное через сервер в журнал не попадает: исчезает, когда процесс заканчивается. Два входа, но только один из них оставляет след.
Что нужно
Обработчики видят хранилище. Путь к файлу - нет: он остался в main и дальше не пошёл.
Без него сохранить нельзя.
Выход прямой: собрать хранилище и путь в одну структуру и передать её вместо одного только
хранилища. Axum доставит её в каждый обработчик через State - так же, как раньше доставлял
SharedStore. Единственное условие: структура должна реализовывать Clone, потому что axum
клонирует состояние на каждый вызов. #[derive(Clone)] достаточно - оба поля его уже
реализуют.
Постройка
В crates/api/src/routes.rs появляется AppState и импорт persistence:
// crates/api/src/routes.rs - CHANGED
use axum::{
Json, Router,
extract::{Path, State},
routing::{get, patch, post},
};
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tq_core::persistence;
use tq_core::store::{Store, TaskStore};
use tq_core::task::Task;
pub type SharedStore = Arc<Mutex<TaskStore>>;
#[derive(Clone)] // axum клонирует состояние для каждого вызова
pub struct AppState {
pub store: SharedStore,
pub data_path: PathBuf,
}
#[derive(Deserialize)]
struct AddRequest {
title: String,
}
async fn add(State(state): State<AppState>, Json(req): Json<AddRequest>) -> Json<Task> {
let mut guard = state.store.lock().unwrap();
let id = guard.add(req.title.as_str()).unwrap();
persistence::save(guard.all(), &state.data_path).unwrap();
Json(guard.get(id).unwrap().clone())
}
async fn list(State(state): State<AppState>) -> Json<Vec<Task>> {
let guard = state.store.lock().unwrap();
Json(guard.all().to_vec())
}
async fn get_task(State(state): State<AppState>, Path(id): Path<u64>) -> Json<Task> {
let guard = state.store.lock().unwrap();
Json(guard.get(id).unwrap().clone())
}
async fn done(State(state): State<AppState>, Path(id): Path<u64>) -> Json<Task> {
let mut guard = state.store.lock().unwrap();
let task = guard.get_mut(id).unwrap();
task.complete();
let task = task.clone(); // завершает мутабельный заём - guard.all() требует иммутабельный
persistence::save(guard.all(), &state.data_path).unwrap();
Json(task)
}
pub fn router(state: AppState) -> Router {
Router::new()
.route("/tasks", post(add).get(list))
.route("/tasks/{id}", get(get_task))
.route("/tasks/{id}/done", patch(done))
.with_state(state)
}
В add persistence::save идёт прямо перед возвратом. В done чуть иначе: task - это
мутабельная ссылка из guard, и пока она жива, guard.all() взять нельзя - два займа на
один guard одновременно. let task = task.clone() создаёт владеющее значение и завершает
мутабельный заём; после этого guard.all() свободен.
Блокировка удерживается до конца save в обоих обработчиках - намеренно. Если отпустить её
раньше, два одновременных запроса могут сохранить разные снимки и более старый затрёт более
новый. Для tq это реальная потеря данных.
В crates/api/src/main.rs - собираем AppState из двух частей:
// crates/api/src/main.rs - CHANGED
mod routes;
use axum::{Router, routing::get};
use routes::{AppState, SharedStore};
use std::path::Path;
use std::sync::{Arc, Mutex};
use tq_core::config::Config;
use tq_core::persistence;
use tq_core::store::TaskStore;
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let config = Config::load(Path::new("config.toml"), None).unwrap_or_default();
let data_path = config.data_dir.join("tasks.json");
let tasks = persistence::load(&data_path).unwrap_or_default();
let store: SharedStore = Arc::new(Mutex::new(TaskStore::new(tasks)));
let state = AppState { store, data_path }; // два значения, одно состояние
let app = Router::new()
.route("/", get(health))
.merge(routes::router(state));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Результат
$ rm -f tasks.json
$ make serve
В другом терминале - добавляем задачу через API, останавливаем сервер, запускаем снова:
$ curl -s -X POST localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"buy milk"}'
{"id":1,"title":"buy milk","status":"Todo","created_at":"2026-06-20T10:00:00Z"}
$ curl -s -X POST localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"send report"}'
{"id":2,"title":"send report","status":"Todo","created_at":"2026-06-20T10:05:00Z"}
Останавливаем сервер (Ctrl+C), запускаем снова (make serve):
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk","status":"Todo","created_at":"2026-06-20T10:00:00Z"},
{"id":2,"title":"send report","status":"Todo","created_at":"2026-06-20T10:05:00Z"}]
Оба входа теперь оставляют след. CLI и API пишут в один журнал.
Полный код
tqдля этой главы -8-ready/02-one-state-two-values/.