Skip to main content

sipx_ua/
publication_client.rs

1//! Sans-I/O event-state publisher (RFC 3903).
2//!
3//! The endpoint driver supplies transactions, fresh digest cnonces and fired timer generations.
4//! The normative state table is `docs/specs/publication-endpoint.md`.
5
6use std::time::Duration;
7
8use bytes::Bytes;
9use sipx_sip::auth::{Challenge, Credentials, respond, strongest};
10use sipx_sip::build::RequestBuilder;
11use sipx_sip::headers::Expires;
12use sipx_sip::{Header, HeaderName, Method, Request, Response, Uri};
13use thiserror::Error;
14
15use crate::event_client::Peer;
16
17/// Default number of publisher usages owned by one runtime.
18pub const DEFAULT_CAPACITY: usize = 1_024;
19/// Default maximum retained publication body.
20pub const DEFAULT_BODY_LIMIT: usize = 65_536;
21
22/// Bounded publisher policy.
23#[derive(Debug, Clone)]
24pub struct Config {
25    /// Maximum logical publishers in one runtime.
26    pub capacity: usize,
27    /// Maximum retained request body.
28    pub body_limit: usize,
29    /// Digest retries per logical operation.
30    pub authentication_retries: u8,
31    /// 423 retries per logical operation.
32    pub interval_retries: u8,
33    /// Maximum requested or accepted expiry.
34    pub maximum_expiry: Duration,
35}
36
37impl Default for Config {
38    fn default() -> Self {
39        Self {
40            capacity: DEFAULT_CAPACITY,
41            body_limit: DEFAULT_BODY_LIMIT,
42            authentication_retries: 2,
43            interval_retries: 1,
44            maximum_expiry: Duration::from_secs(u64::from(u32::MAX)),
45        }
46    }
47}
48
49impl Config {
50    /// Validate allocation and wire-value bounds.
51    pub fn validate(&self) -> Result<(), StartError> {
52        if self.capacity == 0
53            || self.body_limit == 0
54            || self.maximum_expiry.is_zero()
55            || self.maximum_expiry.as_secs() > u64::from(u32::MAX)
56        {
57            return Err(StartError::InvalidConfiguration);
58        }
59        Ok(())
60    }
61}
62
63/// Driver-supplied initial publication.
64#[derive(Debug)]
65pub struct Start {
66    /// Published resource and Request-URI.
67    pub resource: Uri,
68    /// From address without a tag.
69    pub local_identity: String,
70    /// Selected compositor target.
71    pub target: Peer,
72    /// Event package token.
73    pub event: String,
74    /// Positive requested lifetime.
75    pub expires: Duration,
76    /// Mandatory initial event state.
77    pub body: Bytes,
78    /// Event-package media type.
79    pub content_type: String,
80    /// Optional digest credentials.
81    pub credentials: Option<Credentials>,
82    /// Fresh Call-ID.
83    pub call_id: String,
84    /// Fresh From tag.
85    pub from_tag: String,
86    /// Non-zero first `CSeq`.
87    pub initial_cseq: u32,
88}
89
90/// Failure before an initial PUBLISH exists.
91#[derive(Debug, Error, Clone, PartialEq, Eq)]
92#[non_exhaustive]
93pub enum StartError {
94    /// A configured bound cannot be used.
95    #[error("invalid publisher configuration")]
96    InvalidConfiguration,
97    /// An identity, package or interval is invalid.
98    #[error("invalid publication start")]
99    InvalidStart,
100    /// The publication body exceeds its configured maximum.
101    #[error("publication body exceeds configured maximum")]
102    BodyTooLarge,
103    /// A SIP request could not be built.
104    #[error("could not build PUBLISH")]
105    Build,
106}
107
108/// Failure to admit an application operation.
109#[derive(Debug, Error, Clone, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum CommandError {
112    /// The publisher has already terminated.
113    #[error("publication is no longer active")]
114    Terminated,
115    /// Another new PUBLISH request is still in flight.
116    #[error("another publication operation is in flight")]
117    Busy,
118    /// A replacement body is empty or exceeds the configured maximum.
119    #[error("invalid publication body")]
120    InvalidBody,
121}
122
123/// Publisher timer kinds.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum Timer {
126    /// Automatic refresh deadline.
127    Refresh,
128    /// Last authoritative local expiry.
129    Expiry,
130}
131
132/// Observable successful state.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct PublishedState {
135    /// Current opaque entity tag.
136    pub tag: String,
137    /// Current granted lifetime.
138    pub expires: Duration,
139}
140
141/// Typed terminal outcome.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum Termination {
144    /// A conditional tag was rejected and was discarded.
145    StaleTag,
146    /// A successful response omitted or corrupted required authority.
147    MalformedResponse,
148    /// Authentication retry policy was exhausted.
149    AuthenticationExhausted,
150    /// Interval negotiation was invalid or exhausted.
151    IntervalRejected,
152    /// A final response rejected the operation.
153    Rejected(u16),
154    /// The client transaction ended without a final response.
155    TransactionFailed,
156    /// Local `CSeq` could not be incremented safely.
157    LocalCSeqExhausted,
158    /// The last authoritative expiry elapsed.
159    LocalExpiry,
160    /// A successful remove ended the publication.
161    Removed,
162    /// The runtime shutdown deadline released the usage.
163    Shutdown,
164}
165
166/// Observable publisher change.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum StateChange {
169    /// A successful operation installed fresh authority.
170    Published(PublishedState),
171    /// The publisher released all state.
172    Terminated(Termination),
173}
174
175/// One pure action for an I/O driver.
176#[derive(Debug)]
177pub enum Output {
178    /// Send a complete request apart from transport-owned Via.
179    SendPublish {
180        /// Request bytes and headers.
181        request: Box<Request>,
182        /// Selected peer.
183        target: Peer,
184    },
185    /// Arm or replace a timer generation.
186    ArmTimer {
187        /// Timer kind.
188        timer: Timer,
189        /// Exact generation.
190        generation: u64,
191        /// Relative duration.
192        after: Duration,
193    },
194    /// Cancel one timer generation.
195    CancelTimer {
196        /// Timer kind.
197        timer: Timer,
198        /// Generation made stale.
199        generation: u64,
200    },
201    /// Surface application state.
202    StateChanged(StateChange),
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum OperationKind {
207    Initial,
208    Refresh,
209    Modify,
210    Remove,
211}
212
213struct Operation {
214    kind: OperationKind,
215    attempted: Duration,
216    request: Request,
217    auth_retries: u8,
218    interval_retries: u8,
219}
220
221#[derive(Default)]
222struct Timers {
223    refresh: Option<u64>,
224    expiry: Option<u64>,
225    next: u64,
226}
227
228/// One sans-I/O publication lifecycle.
229pub struct Publisher {
230    config: Config,
231    resource: Uri,
232    local_identity: String,
233    target: Peer,
234    event: String,
235    desired: Duration,
236    body: Bytes,
237    content_type: String,
238    credentials: Option<Credentials>,
239    call_id: String,
240    from_tag: String,
241    cseq: u32,
242    tag: Option<String>,
243    granted: Option<Duration>,
244    operation: Option<Operation>,
245    pending_remove: bool,
246    timers: Timers,
247    active: bool,
248}
249
250impl std::fmt::Debug for Publisher {
251    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        formatter
253            .debug_struct("Publisher")
254            .field("resource", &self.resource)
255            .field("event", &self.event)
256            .field("cseq", &self.cseq)
257            .field("has_tag", &self.tag.is_some())
258            .field("granted", &self.granted)
259            .field("active", &self.active)
260            .finish_non_exhaustive()
261    }
262}
263
264impl Publisher {
265    /// Start one initial publication.
266    pub fn start(config: Config, start: Start) -> Result<(Self, Vec<Output>), StartError> {
267        config.validate()?;
268        if start.body.len() > config.body_limit {
269            return Err(StartError::BodyTooLarge);
270        }
271        if start.body.is_empty()
272            || start.event.trim().is_empty()
273            || !token(start.event.as_bytes())
274            || start.content_type.trim().is_empty()
275            || start.local_identity.trim().is_empty()
276            || start.call_id.trim().is_empty()
277            || start.from_tag.trim().is_empty()
278            || !token(start.from_tag.as_bytes())
279            || start.initial_cseq == 0
280            || start.expires.is_zero()
281            || start.expires > config.maximum_expiry
282            || start.expires.as_secs() > u64::from(u32::MAX)
283        {
284            return Err(StartError::InvalidStart);
285        }
286        let request = build_request(
287            &start.resource,
288            &start.local_identity,
289            &start.call_id,
290            &start.from_tag,
291            start.initial_cseq,
292            &start.event,
293            start.expires,
294            None,
295            Some((&start.content_type, start.body.clone())),
296        )?;
297        let output = Output::SendPublish {
298            request: Box::new(request.clone()),
299            target: start.target.clone(),
300        };
301        Ok((
302            Self {
303                config,
304                resource: start.resource,
305                local_identity: start.local_identity,
306                target: start.target,
307                event: start.event,
308                desired: start.expires,
309                body: start.body,
310                content_type: start.content_type,
311                credentials: start.credentials,
312                call_id: start.call_id,
313                from_tag: start.from_tag,
314                cseq: start.initial_cseq,
315                tag: None,
316                granted: None,
317                operation: Some(Operation {
318                    kind: OperationKind::Initial,
319                    attempted: start.expires,
320                    request,
321                    auth_retries: 0,
322                    interval_retries: 0,
323                }),
324                pending_remove: false,
325                timers: Timers::default(),
326                active: true,
327            },
328            vec![output],
329        ))
330    }
331
332    /// Whether this lifecycle still owns state.
333    #[must_use]
334    pub fn is_active(&self) -> bool {
335        self.active
336    }
337
338    /// Current entity tag after a successful operation.
339    #[must_use]
340    pub fn entity_tag(&self) -> Option<&str> {
341        self.tag.as_deref()
342    }
343
344    /// Current granted expiry after a successful operation.
345    #[must_use]
346    pub fn granted_expiry(&self) -> Option<Duration> {
347        self.granted
348    }
349
350    /// Consume a final response; `cnonce` is fresh driver entropy for a possible digest retry.
351    #[allow(
352        clippy::too_many_lines,
353        reason = "the RFC response table remains in protocol order"
354    )]
355    pub fn response(&mut self, response: Option<&Response>, cnonce: &str) -> Vec<Output> {
356        let mut outputs = Vec::new();
357        let Some(mut operation) = self.operation.take() else {
358            return outputs;
359        };
360        let Some(response) = response else {
361            terminate(self, Termination::TransactionFailed, &mut outputs);
362            return outputs;
363        };
364
365        if matches!(response.status.code(), 401 | 407) {
366            let proxy = response.status.code() == 407;
367            let header = if proxy {
368                HeaderName::ProxyAuthenticate
369            } else {
370                HeaderName::WwwAuthenticate
371            };
372            let challenge = strongest(
373                response
374                    .headers
375                    .get_all(&header)
376                    .filter_map(|value| Challenge::parse(&value.value(), proxy))
377                    .collect(),
378            );
379            if operation.auth_retries >= self.config.authentication_retries
380                || challenge.is_none()
381                || self.credentials.is_none()
382                || !increment_cseq(self, &mut outputs)
383            {
384                if self.active {
385                    terminate(self, Termination::AuthenticationExhausted, &mut outputs);
386                }
387                return outputs;
388            }
389            if let (Some(challenge), Some(credentials)) = (challenge, self.credentials.as_ref()) {
390                operation.auth_retries = operation.auth_retries.saturating_add(1);
391                let mut retry = operation.request.clone();
392                replace_cseq(&mut retry, self.cseq);
393                let uri = String::from_utf8_lossy(&retry.uri.to_bytes()).into_owned();
394                let authorization = respond(
395                    &challenge,
396                    credentials,
397                    "PUBLISH",
398                    &uri,
399                    u32::from(operation.auth_retries),
400                    cnonce,
401                );
402                retry.headers.remove_all(&challenge.response_header());
403                if let Ok(header) =
404                    Header::build(challenge.response_header(), Bytes::from(authorization))
405                {
406                    retry.headers.push(header);
407                    operation.request = retry.clone();
408                    self.operation = Some(operation);
409                    outputs.push(Output::SendPublish {
410                        request: Box::new(retry),
411                        target: self.target.clone(),
412                    });
413                    return outputs;
414                }
415            }
416            terminate(self, Termination::AuthenticationExhausted, &mut outputs);
417            return outputs;
418        }
419
420        if response.status.code() == 423 {
421            let minimum = strict_duration(response, &HeaderName::MinExpires);
422            let valid = operation.kind != OperationKind::Remove
423                && operation.interval_retries < self.config.interval_retries
424                && minimum.is_some_and(|minimum| {
425                    minimum > operation.attempted
426                        && minimum <= self.config.maximum_expiry
427                        && u32::try_from(minimum.as_secs()).is_ok()
428                });
429            if valid
430                && increment_cseq(self, &mut outputs)
431                && let Some(minimum) = minimum
432            {
433                operation.interval_retries = operation.interval_retries.saturating_add(1);
434                operation.attempted = minimum;
435                let mut retry = operation.request.clone();
436                replace_cseq(&mut retry, self.cseq);
437                replace_duration(&mut retry, minimum);
438                operation.request = retry.clone();
439                self.operation = Some(operation);
440                outputs.push(Output::SendPublish {
441                    request: Box::new(retry),
442                    target: self.target.clone(),
443                });
444                return outputs;
445            }
446            if self.active {
447                terminate(self, Termination::IntervalRejected, &mut outputs);
448            }
449            return outputs;
450        }
451
452        if response.status.code() == 412 && operation.kind != OperationKind::Initial {
453            self.tag = None;
454            terminate(self, Termination::StaleTag, &mut outputs);
455            return outputs;
456        }
457
458        if response.status.is_success() {
459            let tag = strict_tag(response);
460            let expires = strict_duration(response, &HeaderName::Expires);
461            let valid_expiry = if operation.kind == OperationKind::Remove {
462                expires == Some(Duration::ZERO)
463            } else {
464                expires.is_some_and(|value| !value.is_zero() && value <= operation.attempted)
465            };
466            if tag.is_none() || !valid_expiry {
467                terminate(self, Termination::MalformedResponse, &mut outputs);
468                return outputs;
469            }
470            if operation.kind == OperationKind::Remove {
471                self.tag = None;
472                terminate(self, Termination::Removed, &mut outputs);
473                return outputs;
474            }
475            if let (Some(tag), Some(expires)) = (tag, expires) {
476                self.tag = Some(tag.clone());
477                self.granted = Some(expires);
478                self.operation = None;
479                arm(self, Timer::Expiry, expires, &mut outputs);
480                arm_refresh(self, expires, &mut outputs);
481                outputs.push(Output::StateChanged(StateChange::Published(
482                    PublishedState { tag, expires },
483                )));
484                maybe_begin_remove(self, &mut outputs);
485            }
486            return outputs;
487        }
488
489        terminate(
490            self,
491            Termination::Rejected(response.status.code()),
492            &mut outputs,
493        );
494        outputs
495    }
496
497    /// Replace event state conditionally.
498    pub fn modify(
499        &mut self,
500        body: Bytes,
501        content_type: String,
502    ) -> Result<Vec<Output>, CommandError> {
503        if !self.active || self.tag.is_none() {
504            return Err(CommandError::Terminated);
505        }
506        if self.operation.is_some() {
507            return Err(CommandError::Busy);
508        }
509        if body.is_empty() || body.len() > self.config.body_limit || content_type.trim().is_empty()
510        {
511            return Err(CommandError::InvalidBody);
512        }
513        let mut outputs = Vec::new();
514        cancel(self, Timer::Refresh, &mut outputs);
515        if !increment_cseq(self, &mut outputs) {
516            return Ok(outputs);
517        }
518        self.body = body;
519        self.content_type = content_type;
520        let expires = self.granted.unwrap_or(self.desired);
521        begin(self, OperationKind::Modify, expires, true, &mut outputs);
522        Ok(outputs)
523    }
524
525    /// Remove event state. At most one request is queued behind a live operation.
526    pub fn remove(&mut self) -> Result<Vec<Output>, CommandError> {
527        if !self.active || self.tag.is_none() {
528            return Err(CommandError::Terminated);
529        }
530        let mut outputs = Vec::new();
531        cancel(self, Timer::Refresh, &mut outputs);
532        if self.operation.is_some() {
533            self.pending_remove = true;
534        } else {
535            self.pending_remove = true;
536            maybe_begin_remove(self, &mut outputs);
537        }
538        Ok(outputs)
539    }
540
541    /// Fire one exact timer generation.
542    pub fn timer_fired(&mut self, timer: Timer, generation: u64) -> Vec<Output> {
543        let mut outputs = Vec::new();
544        if timer_generation(&self.timers, timer) != Some(generation) {
545            return outputs;
546        }
547        set_timer(&mut self.timers, timer, None);
548        match timer {
549            Timer::Expiry => terminate(self, Termination::LocalExpiry, &mut outputs),
550            Timer::Refresh => {
551                if self.operation.is_none()
552                    && self.tag.is_some()
553                    && increment_cseq(self, &mut outputs)
554                {
555                    let expires = self.granted.unwrap_or(self.desired);
556                    begin(self, OperationKind::Refresh, expires, false, &mut outputs);
557                }
558            }
559        }
560        outputs
561    }
562
563    /// Force the bounded runtime shutdown deadline.
564    pub fn shutdown_deadline(&mut self) -> Vec<Output> {
565        let mut outputs = Vec::new();
566        if self.active {
567            terminate(self, Termination::Shutdown, &mut outputs);
568        }
569        outputs
570    }
571}
572
573fn begin(
574    publisher: &mut Publisher,
575    kind: OperationKind,
576    expires: Duration,
577    with_body: bool,
578    outputs: &mut Vec<Output>,
579) {
580    let Some(tag) = publisher.tag.as_deref() else {
581        terminate(publisher, Termination::StaleTag, outputs);
582        return;
583    };
584    let body = with_body.then(|| (publisher.content_type.as_str(), publisher.body.clone()));
585    match build_request(
586        &publisher.resource,
587        &publisher.local_identity,
588        &publisher.call_id,
589        &publisher.from_tag,
590        publisher.cseq,
591        &publisher.event,
592        expires,
593        Some(tag),
594        body,
595    ) {
596        Ok(request) => {
597            publisher.operation = Some(Operation {
598                kind,
599                attempted: expires,
600                request: request.clone(),
601                auth_retries: 0,
602                interval_retries: 0,
603            });
604            outputs.push(Output::SendPublish {
605                request: Box::new(request),
606                target: publisher.target.clone(),
607            });
608        }
609        Err(_) => terminate(publisher, Termination::MalformedResponse, outputs),
610    }
611}
612
613fn maybe_begin_remove(publisher: &mut Publisher, outputs: &mut Vec<Output>) {
614    if !publisher.pending_remove || publisher.operation.is_some() || publisher.tag.is_none() {
615        return;
616    }
617    publisher.pending_remove = false;
618    cancel(publisher, Timer::Refresh, outputs);
619    if increment_cseq(publisher, outputs) {
620        begin(
621            publisher,
622            OperationKind::Remove,
623            Duration::ZERO,
624            false,
625            outputs,
626        );
627    }
628}
629
630#[allow(
631    clippy::too_many_arguments,
632    reason = "the arguments are the complete PUBLISH identity"
633)]
634fn build_request(
635    resource: &Uri,
636    local_identity: &str,
637    call_id: &str,
638    from_tag: &str,
639    cseq: u32,
640    event: &str,
641    expires: Duration,
642    tag: Option<&str>,
643    body: Option<(&str, Bytes)>,
644) -> Result<Request, StartError> {
645    let mut builder = RequestBuilder::new(Method::Publish, resource.clone())
646        .header(HeaderName::To, Bytes::from(format!("<{resource}>")))
647        .map_err(|_| StartError::Build)?
648        .header(
649            HeaderName::From,
650            Bytes::from(format!("{local_identity};tag={from_tag}")),
651        )
652        .map_err(|_| StartError::Build)?
653        .header(HeaderName::CallId, Bytes::from(call_id.to_owned()))
654        .map_err(|_| StartError::Build)?
655        .cseq(cseq, &Method::Publish)
656        .map_err(|_| StartError::Build)?
657        .header(HeaderName::Event, Bytes::from(event.to_owned()))
658        .map_err(|_| StartError::Build)?
659        .header(
660            HeaderName::Expires,
661            Bytes::from(expires.as_secs().to_string()),
662        )
663        .map_err(|_| StartError::Build)?
664        .max_forwards(70);
665    if let Some(tag) = tag {
666        builder = builder
667            .header(HeaderName::SipIfMatch, Bytes::from(tag.to_owned()))
668            .map_err(|_| StartError::Build)?;
669    }
670    let payload = match body {
671        Some((content_type, body)) => {
672            builder = builder
673                .header(
674                    HeaderName::ContentType,
675                    Bytes::from(content_type.to_owned()),
676                )
677                .map_err(|_| StartError::Build)?;
678            body
679        }
680        None => Bytes::new(),
681    };
682    Ok(builder.body(payload).build())
683}
684
685fn strict_tag(response: &Response) -> Option<String> {
686    if response.headers.count(&HeaderName::SipETag) != 1 {
687        return None;
688    }
689    let value = response.headers.value(&HeaderName::SipETag)?;
690    token(&value).then(|| String::from_utf8_lossy(&value).into_owned())
691}
692
693fn strict_duration(response: &Response, name: &HeaderName) -> Option<Duration> {
694    if response.headers.count(name) != 1 {
695        return None;
696    }
697    if name == &HeaderName::Expires {
698        return response
699            .headers
700            .typed::<Expires>()?
701            .ok()
702            .map(|value| Duration::from_secs(u64::from(value.0)));
703    }
704    let value = response.headers.value(name)?;
705    let seconds = std::str::from_utf8(&value)
706        .ok()?
707        .trim()
708        .parse::<u32>()
709        .ok()?;
710    Some(Duration::from_secs(u64::from(seconds)))
711}
712
713fn token(value: &[u8]) -> bool {
714    !value.is_empty()
715        && value.iter().all(|byte| {
716            byte.is_ascii_alphanumeric()
717                || matches!(
718                    byte,
719                    b'-' | b'.' | b'!' | b'%' | b'*' | b'_' | b'+' | b'`' | b'\'' | b'~'
720                )
721        })
722}
723
724fn replace_cseq(request: &mut Request, sequence: u32) {
725    request.headers.remove_all(&HeaderName::CSeq);
726    if let Ok(header) = Header::build(HeaderName::CSeq, Bytes::from(format!("{sequence} PUBLISH")))
727    {
728        request.headers.push(header);
729    }
730}
731
732fn replace_duration(request: &mut Request, expires: Duration) {
733    request.headers.remove_all(&HeaderName::Expires);
734    if let Ok(header) = Header::build(
735        HeaderName::Expires,
736        Bytes::from(expires.as_secs().to_string()),
737    ) {
738        request.headers.push(header);
739    }
740}
741
742fn increment_cseq(publisher: &mut Publisher, outputs: &mut Vec<Output>) -> bool {
743    let Some(next) = publisher.cseq.checked_add(1) else {
744        terminate(publisher, Termination::LocalCSeqExhausted, outputs);
745        return false;
746    };
747    publisher.cseq = next;
748    true
749}
750
751fn arm_refresh(publisher: &mut Publisher, expires: Duration, outputs: &mut Vec<Output>) {
752    let seconds = expires.as_secs();
753    let refresh = if seconds <= 1 {
754        // A one-second grant has no positive integral instant before expiry. Waiting for its
755        // boundary avoids an immediate request loop; either timer may then settle the usage.
756        Duration::from_secs(1)
757    } else {
758        Duration::from_secs((seconds.saturating_mul(4) / 5).clamp(1, seconds - 1))
759    };
760    arm(publisher, Timer::Refresh, refresh, outputs);
761}
762
763fn arm(publisher: &mut Publisher, timer: Timer, after: Duration, outputs: &mut Vec<Output>) {
764    cancel(publisher, timer, outputs);
765    publisher.timers.next = publisher.timers.next.saturating_add(1);
766    let generation = publisher.timers.next;
767    set_timer(&mut publisher.timers, timer, Some(generation));
768    outputs.push(Output::ArmTimer {
769        timer,
770        generation,
771        after,
772    });
773}
774
775fn cancel(publisher: &mut Publisher, timer: Timer, outputs: &mut Vec<Output>) {
776    if let Some(generation) = timer_generation(&publisher.timers, timer) {
777        set_timer(&mut publisher.timers, timer, None);
778        outputs.push(Output::CancelTimer { timer, generation });
779    }
780}
781
782fn timer_generation(timers: &Timers, timer: Timer) -> Option<u64> {
783    match timer {
784        Timer::Refresh => timers.refresh,
785        Timer::Expiry => timers.expiry,
786    }
787}
788
789fn set_timer(timers: &mut Timers, timer: Timer, generation: Option<u64>) {
790    match timer {
791        Timer::Refresh => timers.refresh = generation,
792        Timer::Expiry => timers.expiry = generation,
793    }
794}
795
796fn terminate(publisher: &mut Publisher, reason: Termination, outputs: &mut Vec<Output>) {
797    cancel(publisher, Timer::Refresh, outputs);
798    cancel(publisher, Timer::Expiry, outputs);
799    publisher.operation = None;
800    publisher.pending_remove = false;
801    publisher.active = false;
802    publisher.tag = None;
803    publisher.granted = None;
804    outputs.push(Output::StateChanged(StateChange::Terminated(reason)));
805}