Skip to main content

steamkit/types/
job_id.rs

1//! Job ids, and the awaitable jobs built on them.
2//!
3//! A job owns the sending half of a `tokio::sync::oneshot` channel; the waiting
4//! side holds a [`JobReceiver`]. `AsyncJob` (no type argument) is the untyped
5//! job and `AsyncJob<T>` the typed one.
6
7use std::any::Any;
8use std::marker::PhantomData;
9use std::time::{Duration, Instant};
10
11use tokio::sync::oneshot;
12
13use crate::error::SteamError;
14use crate::steam::steam_client::callback_mgr::callback_msg::CallbackMsg;
15
16/// The default job timeout.
17const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
18
19/// How much each heartbeat extends a job's timeout.
20const HEARTBEAT_TIMEOUT_EXTENSION: Duration = Duration::from_secs(10);
21
22/// The id of a network task (a "job").
23#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub struct JobID(u64);
25
26impl JobID {
27    /// An invalid job id (`u64::MAX`).
28    pub const INVALID: JobID = JobID(u64::MAX);
29
30    /// Creates an invalid job id.
31    pub fn new() -> JobID {
32        JobID(u64::MAX)
33    }
34
35    /// Creates a job id from its 64-bit value.
36    pub fn from_value(value: u64) -> JobID {
37        JobID(value)
38    }
39
40    /// The 64-bit value.
41    pub fn value(&self) -> u64 {
42        self.0
43    }
44
45    /// Sets the 64-bit value.
46    pub fn set_value(&mut self, value: u64) {
47        self.0 = value;
48    }
49
50    /// The sequential count.
51    pub fn sequential_count(&self) -> u32 {
52        ((self.0 >> 0) & 0xF_FFFF) as u32
53    }
54
55    /// Sets the sequential count.
56    pub fn set_sequential_count(&mut self, value: u32) {
57        let mask = 0xF_FFFF_u64;
58        self.0 = (self.0 & !mask) | (value as u64 & mask);
59    }
60
61    /// When the server that generated this id started.
62    pub fn start_time(&self) -> u64 {
63        (self.0 >> 20) & 0x3_FFFF_FFFF
64    }
65
66    /// Sets when the server that generated this id started.
67    pub fn set_start_time(&mut self, value: u64) {
68        let mask = 0x3_FFFF_FFFF_u64 << 20;
69        self.0 = (self.0 & !mask) | ((value & 0x3_FFFF_FFFF) << 20);
70    }
71
72    /// The process id of the server that generated this id.
73    pub fn process_id(&self) -> u32 {
74        ((self.0 >> 50) & 0xF) as u32
75    }
76
77    /// Sets the process id of the server that generated this id.
78    pub fn set_process_id(&mut self, value: u32) {
79        let mask = 0xF_u64 << 50;
80        self.0 = (self.0 & !mask) | ((value as u64 & 0xF) << 50);
81    }
82
83    /// The box id of the server that generated this id.
84    pub fn box_id(&self) -> u32 {
85        ((self.0 >> 54) & 0x3_FF) as u32
86    }
87
88    /// Sets the box id of the server that generated this id.
89    pub fn set_box_id(&mut self, value: u32) {
90        let mask = 0x3_FF_u64 << 54;
91        self.0 = (self.0 & !mask) | ((value as u64 & 0x3_FF) << 54);
92    }
93}
94
95impl std::fmt::Display for JobID {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}", self.value())
98    }
99}
100
101impl std::fmt::Debug for JobID {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        std::fmt::Display::fmt(self, f)
104    }
105}
106
107impl From<JobID> for u64 {
108    fn from(job_id: JobID) -> u64 {
109        job_id.value()
110    }
111}
112
113impl From<u64> for JobID {
114    fn from(value: u64) -> JobID {
115        JobID::from_value(value)
116    }
117}
118
119/// The result an async job delivers when it completes.
120type JobResult = Result<Box<dyn CallbackMsg>, SteamError>;
121
122/// The type argument of the untyped [`AsyncJob`].
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
124pub struct AnyCallback;
125
126/// An awaitable [`JobID`]. `AsyncJob` is untyped, `AsyncJob<T>` typed.
127pub struct AsyncJob<T = AnyCallback> {
128    job_id: JobID,
129    timeout: Duration,
130    job_start: Instant,
131    sender: Option<oneshot::Sender<JobResult>>,
132    receiver: JobReceiver,
133    _marker: PhantomData<fn() -> T>,
134}
135
136impl<T> AsyncJob<T> {
137    /// Creates a job. Register it with the job manager by handing over its
138    /// sender ([`AsyncJob::take_sender`]).
139    pub fn new(job_id: JobID) -> AsyncJob<T> {
140        let (sender, receiver) = oneshot::channel();
141        AsyncJob {
142            job_id,
143            timeout: DEFAULT_TIMEOUT,
144            job_start: Instant::now(),
145            sender: Some(sender),
146            receiver: JobReceiver { receiver },
147            _marker: PhantomData,
148        }
149    }
150
151    /// The [`JobID`] of this job.
152    pub fn job_id(&self) -> JobID {
153        self.job_id
154    }
155
156    /// How long until this job times out and is cancelled. 10 seconds by default.
157    pub fn timeout(&self) -> Duration {
158        self.timeout
159    }
160
161    /// Sets how long until this job times out and is cancelled.
162    pub fn set_timeout(&mut self, timeout: Duration) {
163        self.timeout = timeout;
164    }
165
166    /// Whether this job has timed out.
167    pub fn is_timedout(&self) -> bool {
168        self.job_start.elapsed() >= self.timeout
169    }
170
171    /// Records a heartbeat and extends the job's timeout.
172    pub fn heartbeat(&mut self) {
173        self.timeout += HEARTBEAT_TIMEOUT_EXTENSION;
174    }
175
176    /// Adds a callback to the job's results. For the untyped job, one callback
177    /// always completes it.
178    pub fn add_result(&mut self, callback: Box<dyn CallbackMsg>) -> bool {
179        if let Some(sender) = self.sender.take() {
180            let _ = sender.send(Ok(callback));
181        }
182        true
183    }
184
185    /// Fails the job: `due_to_remote_failure` if Steam reported a failure,
186    /// otherwise it timed out.
187    pub fn set_failed(&mut self, due_to_remote_failure: bool) {
188        if let Some(sender) = self.sender.take() {
189            let error = if due_to_remote_failure {
190                SteamError::JobFailed
191            } else {
192                SteamError::Timeout(self.timeout)
193            };
194            let _ = sender.send(Err(error));
195        }
196    }
197
198    /// Hands the sending half of the completion channel to the job manager,
199    /// which completes the job when the matching callback arrives.
200    pub fn take_sender(&mut self) -> Option<oneshot::Sender<JobResult>> {
201        self.sender.take()
202    }
203
204    /// Takes the receiving half of the completion channel.
205    pub fn into_receiver(self) -> JobReceiver {
206        self.receiver
207    }
208}
209
210impl<T> From<&AsyncJob<T>> for JobID {
211    fn from(job: &AsyncJob<T>) -> JobID {
212        job.job_id()
213    }
214}
215
216impl<T> std::fmt::Debug for AsyncJob<T> {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("AsyncJob")
219            .field("job_id", &self.job_id)
220            .field("timeout", &self.timeout)
221            .finish()
222    }
223}
224
225impl<T: CallbackMsg> AsyncJob<T> {
226    /// Converts this job into its awaitable form.
227    pub fn to_task(self) -> TypedJobReceiver<T> {
228        TypedJobReceiver {
229            receiver: self.receiver,
230            _marker: PhantomData,
231        }
232    }
233
234    /// Waits for the job's result.
235    pub async fn wait(self) -> Result<T, SteamError> {
236        self.to_task().wait().await
237    }
238}
239
240/// The receiving half of a job's completion channel.
241pub struct JobReceiver {
242    receiver: oneshot::Receiver<JobResult>,
243}
244
245impl JobReceiver {
246    /// Waits for the job to complete.
247    pub async fn wait(self) -> Result<Box<dyn CallbackMsg>, SteamError> {
248        match self.receiver.await {
249            Ok(result) => result,
250            Err(_) => Err(SteamError::Cancelled),
251        }
252    }
253}
254
255impl std::fmt::Debug for JobReceiver {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        f.write_str("JobReceiver")
258    }
259}
260
261/// The waiting side of a typed job.
262pub struct TypedJobReceiver<T: CallbackMsg> {
263    receiver: JobReceiver,
264    _marker: PhantomData<fn() -> T>,
265}
266
267impl<T: CallbackMsg> TypedJobReceiver<T> {
268    /// Waits for the job to complete and downcasts the callback to `T`.
269    pub async fn wait(self) -> Result<T, SteamError> {
270        let message = self.receiver.wait().await?;
271
272        // `CallbackMsg: Any`, so upcast to `dyn Any` and downcast from there.
273        let any: Box<dyn Any> = message;
274        match any.downcast::<T>() {
275            Ok(typed) => Ok(*typed),
276            Err(_) => Err(SteamError::Malformed(
277                "callback message does not match the expected type".to_string(),
278            )),
279        }
280    }
281}
282
283impl<T: CallbackMsg> std::fmt::Debug for TypedJobReceiver<T> {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        f.write_str("TypedJobReceiver")
286    }
287}
288
289/// The results of an [`AsyncJobMultiple`].
290#[derive(Clone, Debug, PartialEq, Eq)]
291pub struct ResultSet<T> {
292    /// Whether the set is complete, i.e. holds every result Steam sent.
293    pub complete: bool,
294    /// Whether the job got an incomplete set and then hit a remote failure.
295    pub failed: bool,
296    /// The results.
297    pub results: Option<Vec<T>>,
298}
299
300/// An awaitable [`JobID`] that can get several callbacks.
301pub struct AsyncJobMultiple<T: CallbackMsg> {
302    job_id: JobID,
303    timeout: Duration,
304    job_start: Instant,
305    sender: Option<oneshot::Sender<Result<ResultSet<T>, SteamError>>>,
306    receiver: MultiJobReceiver<T>,
307    finish_condition: Box<dyn FnMut(&T) -> bool + Send + Sync>,
308    results: Vec<T>,
309    _marker: PhantomData<fn() -> T>,
310}
311
312impl<T: CallbackMsg> AsyncJobMultiple<T> {
313    /// Creates a job; it's complete once `finish_condition` returns `true` for
314    /// a result.
315    pub fn new(
316        job_id: JobID,
317        finish_condition: impl FnMut(&T) -> bool + Send + Sync + 'static,
318    ) -> AsyncJobMultiple<T> {
319        let (sender, receiver) = oneshot::channel();
320        AsyncJobMultiple {
321            job_id,
322            timeout: DEFAULT_TIMEOUT,
323            job_start: Instant::now(),
324            sender: Some(sender),
325            receiver: MultiJobReceiver {
326                receiver,
327                _marker: PhantomData,
328            },
329            finish_condition: Box::new(finish_condition),
330            results: Vec::new(),
331            _marker: PhantomData,
332        }
333    }
334
335    /// The [`JobID`] of this job.
336    pub fn job_id(&self) -> JobID {
337        self.job_id
338    }
339
340    /// The timeout of this job.
341    pub fn timeout(&self) -> Duration {
342        self.timeout
343    }
344
345    /// Sets the timeout of this job.
346    pub fn set_timeout(&mut self, timeout: Duration) {
347        self.timeout = timeout;
348    }
349
350    /// Whether this job has timed out.
351    pub fn is_timedout(&self) -> bool {
352        self.job_start.elapsed() >= self.timeout
353    }
354
355    /// Records a heartbeat and extends the job's timeout.
356    pub fn heartbeat(&mut self) {
357        self.timeout += HEARTBEAT_TIMEOUT_EXTENSION;
358    }
359
360    /// The results collected so far.
361    pub fn results(&self) -> &[T] {
362        &self.results
363    }
364
365    /// Adds a callback to the results. Returns `true` if it completes the set.
366    pub fn add_result(&mut self, callback: Box<dyn CallbackMsg>) -> bool {
367        let Some(callback) = downcast_callback::<T>(callback) else {
368            return false;
369        };
370
371        let is_complete = (self.finish_condition)(&callback);
372
373        // add this callback to our result set
374        self.results.push(callback);
375
376        if is_complete {
377            // if we've passed our finish condition based on this callback
378            // (for instance, if steam tells us we have no more pending messages
379            // for this job) then we're complete
380            let result_set = ResultSet {
381                complete: true,
382                failed: false,
383                results: Some(std::mem::take(&mut self.results)),
384            };
385
386            if let Some(sender) = self.sender.take() {
387                let _ = sender.send(Ok(result_set));
388            }
389
390            return true;
391        }
392
393        // otherwise, we're not complete and we'll wait for the next message.
394        // trigger heartbeat logic to keep this job alive as it waits for the next
395        // message
396        self.heartbeat();
397
398        false
399    }
400
401    /// Fails the job: `due_to_remote_failure` if Steam reported a failure,
402    /// otherwise it timed out.
403    pub fn set_failed(&mut self, due_to_remote_failure: bool) {
404        // steamclient is informing this async job that we've either timed out or
405        // failed while waiting for an additional callback. Now we have to determine
406        // what to do:
407        if self.results.is_empty() {
408            // if we have zero callbacks in our result set, we cancel this task
409            let error = if due_to_remote_failure {
410                SteamError::JobFailed
411            } else {
412                SteamError::Timeout(self.timeout)
413            };
414
415            if let Some(sender) = self.sender.take() {
416                let _ = sender.send(Err(error));
417            }
418        } else {
419            // if we do have a partial result set, we can complete this job with the
420            // results we have and let consumers work out what they want to do with
421            // the incomplete set
422            let result_set = ResultSet {
423                // we're not a complete set
424                complete: false,
425                // let consumers know if we've failed because of a remote failure
426                failed: due_to_remote_failure,
427                results: Some(std::mem::take(&mut self.results)),
428            };
429
430            if let Some(sender) = self.sender.take() {
431                let _ = sender.send(Ok(result_set));
432            }
433        }
434    }
435
436    /// Hands the sending half of the completion channel to the job manager.
437    pub fn take_sender(&mut self) -> Option<oneshot::Sender<Result<ResultSet<T>, SteamError>>> {
438        self.sender.take()
439    }
440
441    /// Converts this job into its awaitable form.
442    pub fn to_task(self) -> MultiJobReceiver<T> {
443        self.receiver
444    }
445
446    /// Waits for the job's results.
447    pub async fn wait(self) -> Result<ResultSet<T>, SteamError> {
448        self.to_task().wait().await
449    }
450}
451
452impl<T: CallbackMsg> std::fmt::Debug for AsyncJobMultiple<T> {
453    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454        f.debug_struct("AsyncJobMultiple")
455            .field("job_id", &self.job_id)
456            .field("timeout", &self.timeout)
457            .field("results", &self.results.len())
458            .finish()
459    }
460}
461
462/// The waiting side of a multi-result job.
463pub struct MultiJobReceiver<T: CallbackMsg> {
464    receiver: oneshot::Receiver<Result<ResultSet<T>, SteamError>>,
465    _marker: PhantomData<fn() -> T>,
466}
467
468impl<T: CallbackMsg> MultiJobReceiver<T> {
469    /// Waits for the job to complete.
470    pub async fn wait(self) -> Result<ResultSet<T>, SteamError> {
471        match self.receiver.await {
472            Ok(result) => result,
473            Err(_) => Err(SteamError::Cancelled),
474        }
475    }
476}
477
478impl<T: CallbackMsg> std::fmt::Debug for MultiJobReceiver<T> {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        f.write_str("MultiJobReceiver")
481    }
482}
483
484/// Alias for the untyped [`AsyncJob`].
485pub type JobState = AsyncJob;
486
487/// Alias for the typed [`AsyncJob`].
488pub type JobHandle<T> = AsyncJob<T>;
489
490/// Alias for [`TypedJobReceiver`].
491pub type TypedJob<T> = TypedJobReceiver<T>;
492
493/// Alias for [`AsyncJobMultiple`].
494pub type JobGroup<T> = AsyncJobMultiple<T>;
495
496/// Alias for [`MultiJobReceiver`].
497pub type MultiJob<T> = MultiJobReceiver<T>;
498
499/// Downcasts a callback message to the concrete callback type.
500fn downcast_callback<T: CallbackMsg>(callback: Box<dyn CallbackMsg>) -> Option<T> {
501    // `CallbackMsg: Any`, so upcast to `dyn Any` and downcast from there.
502    let any: Box<dyn Any> = callback;
503    any.downcast::<T>().ok().map(|typed| *typed)
504}