use crate::types::{JobClaim, LogEntry, ModelSource, Task, WorkerCapabilities};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HelloFrame {
pub auth_token: String,
pub capabilities: WorkerCapabilities,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WorkerInbound {
Hello(HelloFrame),
#[serde(rename_all = "camelCase")]
Heartbeat {
capabilities: WorkerCapabilities,
#[serde(default, skip_serializing_if = "Option::is_none")]
current_job_id: Option<String>,
},
#[serde(rename_all = "camelCase")]
Accept {
job_id: String,
},
#[serde(rename_all = "camelCase")]
Reject {
job_id: String,
reason: String,
},
#[serde(rename_all = "camelCase")]
CompleteJson {
job_id: String,
result: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
prompt: Option<String>,
},
#[serde(rename_all = "camelCase")]
Fail {
job_id: String,
error: String,
retryable: bool,
},
LogBatch {
entries: Vec<LogEntry>,
},
ReadyForMore,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JobOfferClaim {
pub job_id: String,
pub game_id: String,
pub asset_name: String,
pub model: String,
pub vram_gb_estimate: f32,
pub task: Task,
pub model_source: ModelSource,
}
impl JobOfferClaim {
pub fn into_job_claim(self) -> JobClaim {
JobClaim {
job_id: self.job_id,
game_id: self.game_id,
asset_name: self.asset_name,
model: self.model,
vram_gb_estimate: self.vram_gb_estimate,
task: self.task,
model_source: self.model_source,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerErrorCode {
AuthFailed,
ProtocolViolation,
DuplicateWorker,
WorkerDeleted,
InternalError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum WorkerOutbound {
#[serde(rename_all = "camelCase")]
Welcome {
worker_id: String,
server_time: String,
},
Offer {
claim: Box<JobOfferClaim>,
},
HeartbeatAck,
#[serde(rename_all = "camelCase")]
CompleteAck {
job_id: String,
},
#[serde(rename_all = "camelCase")]
FailAck {
job_id: String,
},
Error {
code: WorkerErrorCode,
message: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum WsCloseCode {
Normal = 1000,
AuthFailed = 4001,
ProtocolViolation = 4002,
DuplicateWorker = 4003,
WorkerDeleted = 4004,
}
impl WsCloseCode {
pub fn from_error_code(code: WorkerErrorCode) -> Self {
match code {
WorkerErrorCode::AuthFailed => Self::AuthFailed,
WorkerErrorCode::ProtocolViolation => Self::ProtocolViolation,
WorkerErrorCode::DuplicateWorker => Self::DuplicateWorker,
WorkerErrorCode::WorkerDeleted => Self::WorkerDeleted,
WorkerErrorCode::InternalError => Self::Normal,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ImageParams, ModelCliDefaults, ModelEngine};
use serde_json::json;
fn synthetic_source() -> ModelSource {
ModelSource {
engine: ModelEngine::Synthetic,
files: vec![],
cli_defaults: ModelCliDefaults {
cfg_scale: 1.0,
steps: 8,
width: 1024,
height: 1024,
sampling_method: None,
..Default::default()
},
}
}
#[test]
fn job_offer_claim_into_job_claim_preserves_fields() {
let offer = JobOfferClaim {
job_id: "j".into(),
game_id: "g".into(),
asset_name: "g/x/y".into(),
model: "m".into(),
vram_gb_estimate: 1.5,
task: Task::Image(ImageParams {
prompt: "hi".into(),
ext: "png".into(),
..Default::default()
}),
model_source: synthetic_source(),
};
let claim = offer.into_job_claim();
assert_eq!(claim.job_id, "j");
assert_eq!(claim.model, "m");
assert_eq!(claim.vram_gb_estimate, 1.5);
match claim.task {
Task::Image(p) => {
assert_eq!(p.prompt, "hi");
assert_eq!(p.ext, "png");
}
other => panic!("expected image, got {:?}", other.kind()),
}
assert!(matches!(claim.model_source.engine, ModelEngine::Synthetic));
}
#[test]
fn job_offer_claim_rejects_offers_without_task_or_model_source() {
let json = json!({
"jobId": "j",
"gameId": "g",
"assetName": "g/x/y",
"model": "m",
"vramGbEstimate": 1.0,
});
let err = serde_json::from_value::<JobOfferClaim>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("task") || msg.contains("modelSource"),
"expected missing-required-field error, got: {msg}"
);
}
}