Skip to main content

dex_sdk/
context.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use std::collections::{HashMap, HashSet};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Condvar, Mutex};
12use std::time::{Duration, SystemTime};
13
14use dex_protocol::dex::{
15    AttributeWrite, ChannelInfo, ChannelMessage as ProtoChannelMessage, ChannelMessageDeletion,
16    ChannelValues, ConditionResults, ConditionStatus, Context as ProtoContext, Kv, StepStreamWrite,
17    Value as ProtoValue, value,
18};
19
20use crate::attribute::{
21    AttributeWriteSettings, require_registered_hard_delete_setting,
22    validate_hard_delete_attribute_write,
23};
24use crate::persistence::PersistenceKind;
25use crate::registry::{RegisteredFlow, decode_instance, physical_name, validate_map_instance};
26use crate::server_protocol::NegotiatedServerProtocolVersion;
27use crate::value_mapper;
28use crate::worker_output::StepOutputEmitter;
29use crate::{
30    Attribute, AttributeMap, Channel, ChannelMap, ChannelMessage, FlowResult, HandlerError,
31    HandlerResult, Stream, Value,
32};
33
34#[derive(Clone, Copy, Eq, PartialEq)]
35pub(crate) enum InvocationMethod {
36    WaitFor,
37    Execute,
38    Rpc,
39}
40
41pub(crate) struct ContextInput {
42    pub(crate) method: InvocationMethod,
43    pub(crate) flow: RegisteredFlow,
44    pub(crate) metadata: ProtoContext,
45    pub(crate) attributes: Vec<Kv>,
46    pub(crate) locals: Vec<Kv>,
47    pub(crate) condition_results: Option<ConditionResults>,
48    pub(crate) channel_infos: HashMap<String, ChannelInfo>,
49    pub(crate) loaded_channel_messages: HashMap<String, ChannelValues>,
50    pub(crate) loaded_attribute_map_instances: Vec<String>,
51    pub(crate) loaded_channel_names: Vec<String>,
52    pub(crate) loaded_channel_map_instances: Vec<String>,
53    pub(crate) negotiated_protocol_version: NegotiatedServerProtocolVersion,
54}
55
56pub(crate) struct InvocationOutputs {
57    pub(crate) attributes: Vec<AttributeWrite>,
58    pub(crate) locals: Vec<Kv>,
59    pub(crate) events: Vec<Kv>,
60    pub(crate) publications: Vec<ProtoChannelMessage>,
61    pub(crate) channel_deletions: Vec<ChannelMessageDeletion>,
62}
63
64/// Describes the final failure that caused a Step method recovery.
65///
66/// Read this value from [`Context::recovery_error`]. Worker failures preserve their original
67/// error type and detail. Backend failures such as timeouts use the Temporal or Cadence failure
68/// type.
69///
70/// ```no_run
71/// # fn inspect(context: &dex_sdk::Context) {
72/// if let Some(error) = context.recovery_error() {
73///     eprintln!("Recovering from {}: {}", error.error_type(), error.detail());
74/// }
75/// # }
76/// ```
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct RecoveryErrorInfo {
79    detail: String,
80    error_type: String,
81}
82
83impl RecoveryErrorInfo {
84    /// Returns the failure detail supplied by the Worker or backend.
85    pub fn detail(&self) -> &str {
86        &self.detail
87    }
88
89    /// Returns the original Worker error type or backend failure type.
90    pub fn error_type(&self) -> &str {
91        &self.error_type
92    }
93}
94
95/// Provides invocation metadata and staged durable changes to Step and RPC handlers.
96///
97/// A Context belongs to one handler attempt and must not outlive the call. Attribute writes,
98/// Channel publications, locals, and events become visible atomically only when the handler returns
99/// successfully. Application values are freshly decoded and owned by the invocation.
100pub struct Context {
101    method: InvocationMethod,
102    flow: RegisteredFlow,
103    output_emitter: Option<StepOutputEmitter>,
104    metadata: ProtoContext,
105    recovery_error: Option<RecoveryErrorInfo>,
106    attributes: HashMap<String, ProtoValue>,
107    locals: HashMap<String, ProtoValue>,
108    condition_results: Option<ConditionResults>,
109    channel_infos: HashMap<String, ChannelInfo>,
110    loaded_channel_messages: HashMap<String, ChannelValues>,
111    loaded_attribute_map_instances: HashSet<String>,
112    loaded_channel_names: HashSet<String>,
113    loaded_channel_map_instances: HashSet<String>,
114    attribute_writes: HashMap<String, AttributeWrite>,
115    local_writes: HashMap<String, Kv>,
116    events: Vec<Kv>,
117    event_names: HashSet<String>,
118    publications: Vec<ProtoChannelMessage>,
119    channel_deletions: Vec<ChannelMessageDeletion>,
120    cancellation: InvocationCancellation,
121    runtime: Option<tokio::runtime::Handle>,
122    step_output_finalizers: Vec<Arc<dyn StepOutputFinalizer>>,
123    step_outputs_finalized: bool,
124    negotiated_protocol_version: NegotiatedServerProtocolVersion,
125}
126
127pub(crate) trait StepOutputFinalizer: Send + Sync {
128    fn finalize_step_output(&self) -> HandlerResult<()>;
129}
130
131impl Context {
132    pub(crate) fn new(
133        input: ContextInput,
134        output_emitter: Option<StepOutputEmitter>,
135        cancellation: InvocationCancellation,
136    ) -> HandlerResult<Self> {
137        let recovery_error =
138            input
139                .metadata
140                .recovery_error
141                .as_ref()
142                .map(|error| RecoveryErrorInfo {
143                    detail: error.detail.clone(),
144                    error_type: error.error_type.clone(),
145                });
146        Ok(Self {
147            method: input.method,
148            flow: input.flow,
149            output_emitter,
150            metadata: input.metadata,
151            recovery_error,
152            attributes: map_values("Attribute", input.attributes)?,
153            locals: map_values("step-execution local", input.locals)?,
154            condition_results: input.condition_results,
155            channel_infos: input.channel_infos,
156            loaded_channel_messages: input.loaded_channel_messages,
157            loaded_attribute_map_instances: input
158                .loaded_attribute_map_instances
159                .into_iter()
160                .collect(),
161            loaded_channel_names: input.loaded_channel_names.into_iter().collect(),
162            loaded_channel_map_instances: input.loaded_channel_map_instances.into_iter().collect(),
163            attribute_writes: HashMap::new(),
164            local_writes: HashMap::new(),
165            events: Vec::new(),
166            event_names: HashSet::new(),
167            publications: Vec::new(),
168            channel_deletions: Vec::new(),
169            cancellation,
170            runtime: tokio::runtime::Handle::try_current().ok(),
171            step_output_finalizers: Vec::new(),
172            step_outputs_finalized: false,
173            negotiated_protocol_version: input.negotiated_protocol_version,
174        })
175    }
176
177    /// Returns the application-assigned Flow ID.
178    pub fn flow_id(&self) -> &str {
179        &self.metadata.flow_id
180    }
181
182    /// Returns the server-assigned run ID for this execution.
183    pub fn run_id(&self) -> &str {
184        &self.metadata.run_id
185    }
186
187    /// Returns when the current Flow run started.
188    pub fn flow_started_at(&self) -> SystemTime {
189        system_time(self.metadata.flow_started_timestamp)
190    }
191
192    /// Returns the current Step execution ID, or an empty string for an RPC.
193    pub fn step_execution_id(&self) -> &str {
194        &self.metadata.step_execution_id
195    }
196
197    /// Returns the predecessor Step execution ID, or an empty string when absent.
198    pub fn from_step_execution_id(&self) -> &str {
199        &self.metadata.from_step_execution_id
200    }
201
202    /// Returns the final failure that caused recovery, or `None` outside a recovery Step.
203    pub fn recovery_error(&self) -> Option<&RecoveryErrorInfo> {
204        self.recovery_error.as_ref()
205    }
206
207    /// Returns when the first attempt of this handler invocation started.
208    pub fn first_attempt_at(&self) -> SystemTime {
209        system_time(self.metadata.first_attempt_timestamp)
210    }
211
212    /// Returns the one-based handler attempt number.
213    pub fn attempt(&self) -> u32 {
214        u32::try_from(self.metadata.attempt).unwrap_or_default()
215    }
216
217    /// Records a heartbeat without a Value and clears persisted heartbeat details.
218    ///
219    /// The call blocks only while the Worker's one-frame output buffer is full. It does not wait
220    /// for Dex to process the heartbeat. Flow RPC Contexts reject this operation.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`HandlerError`] for an RPC Context, cancellation, or a closed Worker response
225    /// stream.
226    pub fn record_heartbeat(&mut self) -> HandlerResult<()> {
227        self.step_output_emitter()?.send_heartbeat(None)
228    }
229
230    /// Records a heartbeat carrying a typed checkpoint Value.
231    ///
232    /// A later regular attempt can decode the most recently accepted checkpoint with
233    /// [`Self::last_heartbeat_value`]. Local activities accept the frame but do not persist it.
234    /// The call uses the same bounded local buffering as [`Self::record_heartbeat`].
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// use dex_sdk::{Context, HandlerResult};
240    /// use serde::{Deserialize, Serialize};
241    ///
242    /// #[derive(Deserialize, Serialize)]
243    /// struct Checkpoint {
244    ///     offset: u64,
245    /// }
246    ///
247    /// fn import_batch(context: &mut Context, next_offset: u64) -> HandlerResult<()> {
248    ///     let previous = context.last_heartbeat_value::<Checkpoint>()?;
249    ///     let offset = previous.map_or(next_offset, |checkpoint| checkpoint.offset);
250    ///     context.record_heartbeat_value(Checkpoint { offset })
251    /// }
252    /// ```
253    ///
254    /// # Errors
255    ///
256    /// Returns [`HandlerError`] for an RPC Context, encoding failure, cancellation, or a closed
257    /// Worker response stream.
258    pub fn record_heartbeat_value<T: Value>(&mut self, value: T) -> HandlerResult<()> {
259        let value = value_mapper::encode_handler(&value)?;
260        self.step_output_emitter()?.send_heartbeat(Some(value))
261    }
262
263    /// Decodes the checkpoint persisted by the previous regular attempt.
264    ///
265    /// `None` means no heartbeat Value is present. To distinguish a null Value, request an
266    /// optional application type: `last_heartbeat_value::<Option<T>>()` returns `Some(None)` for a
267    /// present null Value.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`HandlerError`] for an RPC Context or when the persisted Value cannot be decoded
272    /// as `T`.
273    pub fn last_heartbeat_value<T: Value>(&self) -> HandlerResult<Option<T>> {
274        if self.method == InvocationMethod::Rpc {
275            return Err(HandlerError::new(
276                "dex_sdk::HandlerError",
277                "Heartbeat values require a Step Context",
278            ));
279        }
280        self.metadata
281            .last_heartbeat_value
282            .as_ref()
283            .map(value_mapper::decode_handler)
284            .transpose()
285    }
286
287    /// Returns whether any timer condition completed for this `execute` invocation.
288    pub fn has_any_timer_fired(&self) -> bool {
289        self.condition_results.as_ref().is_some_and(|results| {
290            results
291                .timer_results
292                .iter()
293                .any(|timer| timer.condition_status == ConditionStatus::Completed as i32)
294        })
295    }
296
297    /// Returns whether the zero-based timer at `index` completed.
298    ///
299    /// Missing indexes return `false`.
300    pub fn has_timer_fired(&self, index: usize) -> bool {
301        self.condition_results
302            .as_ref()
303            .and_then(|results| results.timer_results.get(index))
304            .is_some_and(|timer| timer.condition_status == ConditionStatus::Completed as i32)
305    }
306
307    /// Returns whether `execute` is running because `wait_for` exhausted retries with Proceed.
308    pub fn wait_for_method_failed(&self) -> bool {
309        self.condition_results
310            .as_ref()
311            .is_some_and(|results| results.wait_for_failed)
312    }
313
314    pub(crate) fn sub_flow_result(&self, index: usize) -> HandlerResult<FlowResult> {
315        if self.method != InvocationMethod::Execute {
316            return Err(HandlerError::new(
317                "dex_sdk::HandlerError",
318                "SubFlow results are available only during execute",
319            ));
320        }
321        let result = self
322            .condition_results
323            .as_ref()
324            .and_then(|results| results.sub_flow_results.get(index))
325            .ok_or_else(|| {
326                HandlerError::new(
327                    "dex_sdk::HandlerError",
328                    format!("SubFlow result index is unavailable: {index}"),
329                )
330            })?;
331        FlowResult::from_proto(result).map_err(HandlerError::from_error)
332    }
333
334    pub(crate) fn sub_flow_id(&self, index: usize) -> HandlerResult<String> {
335        self.sub_flow_result(index)?;
336        Ok(format!(
337            "SubFlow:{}-{}-{index}",
338            self.flow_id(),
339            self.step_execution_id()
340        ))
341    }
342
343    /// Blocks the current thread until Dex cancels this handler attempt.
344    ///
345    /// Use this only for interruptible application work; ordinary handlers should return normally.
346    pub fn wait_for_cancellation(&self) {
347        self.cancellation.wait();
348    }
349
350    /// Returns immediately with the current handler-cancellation state.
351    pub fn is_cancelled(&self) -> bool {
352        self.cancellation.is_cancelled()
353    }
354
355    /// Stages a Step-execution-local value visible to later attempts of the same execution.
356    ///
357    /// # Errors
358    ///
359    /// Returns [`HandlerError`] for an empty key or an encoding failure.
360    pub fn set_step_execution_local<T: Value>(&mut self, key: &str, value: T) -> HandlerResult<()> {
361        require_name(key, "step-execution local key")?;
362        let value = value_mapper::encode_handler(&value)?;
363        self.local_writes.insert(
364            key.to_string(),
365            Kv {
366                key: key.to_string(),
367                value: Some(value),
368            },
369        );
370        Ok(())
371    }
372
373    /// Reads a required Step-execution-local value, including writes staged by this attempt.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`HandlerError`] for an empty key, a missing value, or a decoding failure.
378    pub fn step_execution_local<T: Value>(&self, key: &str) -> HandlerResult<T> {
379        require_name(key, "step-execution local key")?;
380        let value = self
381            .local_writes
382            .get(key)
383            .and_then(|entry| entry.value.as_ref())
384            .or_else(|| self.locals.get(key))
385            .ok_or_else(|| {
386                HandlerError::new(
387                    "dex_sdk::HandlerError",
388                    format!("step-execution local {key} is missing"),
389                )
390            })?;
391        value_mapper::decode_handler(value)
392    }
393
394    /// Records one named diagnostic event with a typed payload.
395    ///
396    /// Event names must be unique within the invocation.
397    ///
398    /// # Errors
399    ///
400    /// Returns [`HandlerError`] for an empty or duplicate name or an encoding failure.
401    pub fn record_event<T: Value>(&mut self, name: &str, value: T) -> HandlerResult<()> {
402        require_name(name, "event name")?;
403        if !self.event_names.insert(name.to_string()) {
404            return Err(HandlerError::new(
405                "dex_sdk::HandlerError",
406                format!("event was already recorded: {name}"),
407            ));
408        }
409        self.events.push(Kv {
410            key: name.to_string(),
411            value: Some(value_mapper::encode_handler(&value)?),
412        });
413        Ok(())
414    }
415
416    /// Reads an Attribute value, including writes staged by this invocation.
417    pub fn get_attribute<T: Value>(&self, attribute: &Attribute<T>) -> HandlerResult<Option<T>> {
418        self.get_attribute_value(attribute.name(), PersistenceKind::Attribute, None)
419    }
420
421    /// Reads one Attribute-map instance, including staged writes.
422    pub fn get_attribute_map<T: Value>(
423        &self,
424        attribute: &AttributeMap<T>,
425        instance: &str,
426    ) -> HandlerResult<Option<T>> {
427        self.get_attribute_value(
428            attribute.name(),
429            PersistenceKind::AttributeMap,
430            Some(instance),
431        )
432    }
433
434    /// Stages a typed Attribute write.
435    pub fn set_attribute<T: Value>(
436        &mut self,
437        attribute: &Attribute<T>,
438        value: T,
439    ) -> HandlerResult<()> {
440        self.set_attribute_value(
441            attribute.name(),
442            PersistenceKind::Attribute,
443            None,
444            value_mapper::encode_handler(&value)?,
445            attribute.write_settings(),
446        )
447    }
448
449    /// Stages a typed write for one Attribute-map instance.
450    pub fn set_attribute_map<T: Value>(
451        &mut self,
452        attribute: &AttributeMap<T>,
453        instance: &str,
454        value: T,
455    ) -> HandlerResult<()> {
456        self.set_attribute_value(
457            attribute.name(),
458            PersistenceKind::AttributeMap,
459            Some(instance),
460            value_mapper::encode_handler(&value)?,
461            attribute.write_settings(),
462        )
463    }
464
465    /// Stages deletion of an Attribute value.
466    pub fn delete_attribute<T>(&mut self, attribute: &Attribute<T>) -> HandlerResult<()> {
467        self.set_attribute_value(
468            attribute.name(),
469            PersistenceKind::Attribute,
470            None,
471            value_mapper::deletion(),
472            attribute.write_settings(),
473        )
474    }
475
476    /// Stages deletion of one Attribute-map instance.
477    pub fn delete_attribute_map<T>(
478        &mut self,
479        attribute: &AttributeMap<T>,
480        instance: &str,
481    ) -> HandlerResult<()> {
482        self.set_attribute_value(
483            attribute.name(),
484            PersistenceKind::AttributeMap,
485            Some(instance),
486            value_mapper::deletion(),
487            attribute.write_settings(),
488        )
489    }
490
491    pub(crate) fn attribute_map_keys<T>(
492        &self,
493        attribute: &AttributeMap<T>,
494    ) -> HandlerResult<Vec<String>> {
495        self.registered_name(
496            attribute.name(),
497            PersistenceKind::AttributeMap,
498            Some("key-check"),
499        )?;
500        if !self
501            .loaded_attribute_map_instances
502            .contains(&format!("{}/", attribute.name()))
503        {
504            return Err(attribute_map_not_loaded(format!(
505                "all AttributeMap instances were not loaded for this invocation: {}",
506                attribute.name()
507            )));
508        }
509        let prefix = format!("{}/", attribute.name());
510        let mut physical_keys = self
511            .attributes
512            .keys()
513            .filter(|key| key.starts_with(&prefix))
514            .cloned()
515            .collect::<HashSet<_>>();
516        for (key, write) in &self.attribute_writes {
517            if !key.starts_with(&prefix) {
518                continue;
519            }
520            if matches!(
521                write.value.as_ref().and_then(|value| value.kind.as_ref()),
522                Some(value::Kind::NullValue(_))
523            ) {
524                physical_keys.remove(key);
525            } else {
526                physical_keys.insert(key.clone());
527            }
528        }
529        sorted_instance_keys(&prefix, physical_keys)
530    }
531
532    /// Stages one typed Channel publication.
533    pub fn publish<T: Value>(&mut self, channel: &Channel<T>, value: T) -> HandlerResult<()> {
534        self.publish_value(
535            channel.name(),
536            PersistenceKind::Channel,
537            None,
538            value_mapper::encode_handler(&value)?,
539        )
540    }
541
542    /// Stages one typed publication to a Channel-map instance.
543    pub fn publish_map<T: Value>(
544        &mut self,
545        channel: &ChannelMap<T>,
546        instance: &str,
547        value: T,
548    ) -> HandlerResult<()> {
549        self.publish_value(
550            channel.name(),
551            PersistenceKind::ChannelMap,
552            Some(instance),
553            value_mapper::encode_handler(&value)?,
554        )
555    }
556
557    /// Returns the current invocation snapshot's Channel queue size.
558    pub fn channel_size<T>(&self, channel: &Channel<T>) -> HandlerResult<usize> {
559        self.channel_size_value(channel.name(), PersistenceKind::Channel, None)
560    }
561
562    /// Returns one Channel-map instance's queue size.
563    pub fn channel_map_size<T>(
564        &self,
565        channel: &ChannelMap<T>,
566        instance: &str,
567    ) -> HandlerResult<usize> {
568        self.channel_size_value(channel.name(), PersistenceKind::ChannelMap, Some(instance))
569    }
570
571    /// Returns one singleton Channel's loaded pending-message snapshot.
572    pub fn pending_channel_messages<T: Value>(
573        &self,
574        channel: &Channel<T>,
575    ) -> HandlerResult<Vec<ChannelMessage<T>>> {
576        self.pending_channel_messages_value(channel.name(), PersistenceKind::Channel, None)
577    }
578
579    /// Returns one Channel-map instance's loaded pending-message snapshot.
580    pub fn pending_channel_map_messages<T: Value>(
581        &self,
582        channel: &ChannelMap<T>,
583        instance: &str,
584    ) -> HandlerResult<Vec<ChannelMessage<T>>> {
585        self.pending_channel_messages_value(
586            channel.name(),
587            PersistenceKind::ChannelMap,
588            Some(instance),
589        )
590    }
591
592    pub(crate) fn channel_map_keys<T>(
593        &self,
594        channel: &ChannelMap<T>,
595    ) -> HandlerResult<Vec<String>> {
596        self.registered_name(
597            channel.name(),
598            PersistenceKind::ChannelMap,
599            Some("key-check"),
600        )?;
601        let prefix = format!("{}/", channel.name());
602        let physical_keys = self
603            .channel_infos
604            .iter()
605            .filter(|(key, info)| key.starts_with(&prefix) && info.size > 0)
606            .map(|(key, _)| key.clone())
607            .collect::<HashSet<_>>();
608        sorted_instance_keys(&prefix, physical_keys)
609    }
610
611    /// Decodes the messages consumed by a satisfied Channel condition.
612    pub fn channel_results<T: Value>(&self, channel: &Channel<T>) -> HandlerResult<Vec<T>> {
613        self.channel_results_value(channel.name(), PersistenceKind::Channel, None)
614    }
615
616    /// Decodes messages consumed by one satisfied Channel-map condition.
617    pub fn channel_map_results<T: Value>(
618        &self,
619        channel: &ChannelMap<T>,
620        instance: &str,
621    ) -> HandlerResult<Vec<T>> {
622        self.channel_results_value(channel.name(), PersistenceKind::ChannelMap, Some(instance))
623    }
624
625    /// Appends one best-effort Stream message to the current Worker response stream.
626    ///
627    /// The call returns after the Worker's bounded output buffer accepts the frame. It does not
628    /// wait for Dex or the Stream Store, and a later server-side write failure is not returned to
629    /// the handler. One invocation may append any number of messages to the same Stream.
630    ///
631    /// # Errors
632    ///
633    /// Returns [`HandlerError`] for an RPC or Flow timeout Context, an unregistered Stream, a
634    /// capacity mismatch, encoding failure, cancellation, or a closed Worker response stream.
635    pub fn write_stream<T: Value>(&mut self, stream: &Stream<T>, value: T) -> HandlerResult<()> {
636        let output_emitter = self.step_output_emitter()?.clone();
637        let definition = self
638            .flow
639            .persistence
640            .get(stream.name())
641            .filter(|definition| {
642                definition.kind == PersistenceKind::Stream
643                    && definition.stream_identity == Some(stream.identity())
644            })
645            .ok_or_else(|| {
646                HandlerError::new(
647                    "dex_sdk::HandlerError",
648                    format!("Stream does not belong to Flow: {}", stream.name()),
649                )
650            })?;
651        if definition.stream_capacity_bytes != Some(stream.stream_capacity_bytes()) {
652            return Err(HandlerError::new(
653                "dex_sdk::HandlerError",
654                format!(
655                    "Stream capacity does not match its registered definition: {}",
656                    stream.name()
657                ),
658            ));
659        }
660        output_emitter.send_stream_write(StepStreamWrite {
661            stream_name: stream.name().to_string(),
662            stream_capacity_bytes: stream.stream_capacity_bytes(),
663            value: Some(value_mapper::encode_handler(&value)?),
664        })
665    }
666
667    pub(crate) fn prepare_buffered_text_stream(
668        &self,
669        stream: &Stream<String>,
670    ) -> HandlerResult<(
671        StepOutputEmitter,
672        InvocationCancellation,
673        tokio::runtime::Handle,
674    )> {
675        let output_emitter = self.step_output_emitter()?.clone();
676        let definition = self
677            .flow
678            .persistence
679            .get(stream.name())
680            .filter(|definition| {
681                definition.kind == PersistenceKind::Stream
682                    && definition.stream_identity == Some(stream.identity())
683            })
684            .ok_or_else(|| {
685                HandlerError::new(
686                    "dex_sdk::HandlerError",
687                    format!("Stream does not belong to Flow: {}", stream.name()),
688                )
689            })?;
690        if definition.stream_capacity_bytes != Some(stream.stream_capacity_bytes()) {
691            return Err(HandlerError::new(
692                "dex_sdk::HandlerError",
693                format!(
694                    "Stream capacity does not match its registered definition: {}",
695                    stream.name()
696                ),
697            ));
698        }
699        let runtime = self.runtime.clone().ok_or_else(|| {
700            HandlerError::new(
701                "dex_sdk::HandlerError",
702                "Buffered Stream runtime is unavailable",
703            )
704        })?;
705        Ok((output_emitter, self.cancellation.clone(), runtime))
706    }
707
708    pub(crate) fn register_step_output_finalizer(
709        &mut self,
710        finalizer: Arc<dyn StepOutputFinalizer>,
711    ) -> HandlerResult<()> {
712        if self.step_outputs_finalized {
713            return Err(HandlerError::new(
714                "dex_sdk::HandlerError",
715                "Buffered Stream invocation has finished",
716            ));
717        }
718        self.step_output_finalizers.push(finalizer);
719        Ok(())
720    }
721
722    pub(crate) fn finalize_step_outputs<T>(
723        &mut self,
724        result: HandlerResult<T>,
725    ) -> HandlerResult<T> {
726        if self.step_outputs_finalized {
727            return result;
728        }
729        self.step_outputs_finalized = true;
730        let mut result = result;
731        for finalizer in &self.step_output_finalizers {
732            if let Err(finalization_failure) = finalizer.finalize_step_output() {
733                match &mut result {
734                    Ok(_) => result = Err(finalization_failure),
735                    Err(handler_failure) => {
736                        handler_failure.attach_finalizer_error(&finalization_failure);
737                    }
738                }
739            }
740        }
741        result
742    }
743
744    pub(crate) fn cancellation(&self) -> InvocationCancellation {
745        self.cancellation.clone()
746    }
747
748    pub(crate) fn take_outputs(self) -> InvocationOutputs {
749        InvocationOutputs {
750            attributes: self.attribute_writes.into_values().collect(),
751            locals: self.local_writes.into_values().collect(),
752            events: self.events,
753            publications: self.publications,
754            channel_deletions: self.channel_deletions,
755        }
756    }
757
758    fn step_output_emitter(&self) -> HandlerResult<&StepOutputEmitter> {
759        if self.method == InvocationMethod::Rpc {
760            return Err(HandlerError::new(
761                "dex_sdk::HandlerError",
762                "Step progress requires a Step Context",
763            ));
764        }
765        if self.cancellation.is_cancelled() {
766            return Err(HandlerError::new(
767                "dex_sdk::HandlerError",
768                "Step invocation is canceled",
769            ));
770        }
771        self.output_emitter.as_ref().ok_or_else(|| {
772            HandlerError::new("dex_sdk::HandlerError", "Step output stream is unavailable")
773        })
774    }
775
776    fn get_attribute_value<T: Value>(
777        &self,
778        name: &str,
779        kind: PersistenceKind,
780        instance: Option<&str>,
781    ) -> HandlerResult<Option<T>> {
782        let key = self.registered_name(name, kind, instance)?;
783        if kind == PersistenceKind::AttributeMap {
784            let is_loaded = self
785                .loaded_attribute_map_instances
786                .contains(&format!("{name}/"))
787                || self.loaded_attribute_map_instances.contains(&key);
788            if !is_loaded {
789                return Err(attribute_map_not_loaded(format!(
790                    "AttributeMap instance was not loaded for this invocation: {key}"
791                )));
792            }
793        }
794        if let Some(write) = self.attribute_writes.get(&key) {
795            let value = write.value.as_ref().ok_or_else(|| {
796                HandlerError::new(
797                    "dex_sdk::HandlerError",
798                    format!("Attribute write {key} has no Value"),
799                )
800            })?;
801            if matches!(value.kind, Some(value::Kind::NullValue(_))) {
802                return Ok(None);
803            }
804            return value_mapper::decode_handler(value).map(Some);
805        }
806        self.attributes
807            .get(&key)
808            .map(value_mapper::decode_handler)
809            .transpose()
810    }
811
812    fn set_attribute_value(
813        &mut self,
814        name: &str,
815        kind: PersistenceKind,
816        instance: Option<&str>,
817        value: ProtoValue,
818        write_settings: AttributeWriteSettings,
819    ) -> HandlerResult<()> {
820        let key = self.registered_name(name, kind, instance)?;
821        let is_registered_hard_delete_on_null = self
822            .flow
823            .persistence
824            .get(name)
825            .is_some_and(|definition| definition.is_hard_delete_on_null);
826        require_registered_hard_delete_setting(
827            name,
828            is_registered_hard_delete_on_null,
829            write_settings.is_hard_delete_on_null,
830        )
831        .map_err(|error| HandlerError::new("dex_sdk::HandlerError", error))?;
832        let write = write_settings.attribute_write(key.clone(), value);
833        validate_hard_delete_attribute_write(&write)
834            .map_err(|error| HandlerError::new("dex_sdk::HandlerError", error))?;
835        if write.is_hard_delete_on_null {
836            self.negotiated_protocol_version
837                .require_hard_delete_attribute_support(&key)
838                .map_err(|error| HandlerError::new("dex_sdk::HandlerError", error))?;
839        }
840        self.attribute_writes.insert(key, write);
841        Ok(())
842    }
843
844    fn publish_value(
845        &mut self,
846        name: &str,
847        kind: PersistenceKind,
848        instance: Option<&str>,
849        value: ProtoValue,
850    ) -> HandlerResult<()> {
851        let channel_name = self.registered_name(name, kind, instance)?;
852        self.publications.push(ProtoChannelMessage {
853            channel_name: channel_name.clone(),
854            value: Some(value),
855            message_id: String::new(),
856        });
857        self.channel_infos
858            .entry(channel_name)
859            .and_modify(|info| info.size += 1)
860            .or_insert(ChannelInfo { size: 1 });
861        Ok(())
862    }
863
864    /// Stages best-effort deletion of one singleton Channel message.
865    pub fn delete_channel_message<T>(
866        &mut self,
867        channel: &Channel<T>,
868        message_id: &str,
869    ) -> HandlerResult<()> {
870        self.delete_channel_message_value(
871            channel.name(),
872            PersistenceKind::Channel,
873            None,
874            message_id,
875        )
876    }
877
878    /// Stages best-effort deletion of one Channel-map message.
879    pub fn delete_channel_map_message<T>(
880        &mut self,
881        channel: &ChannelMap<T>,
882        instance: &str,
883        message_id: &str,
884    ) -> HandlerResult<()> {
885        self.delete_channel_message_value(
886            channel.name(),
887            PersistenceKind::ChannelMap,
888            Some(instance),
889            message_id,
890        )
891    }
892
893    fn delete_channel_message_value(
894        &mut self,
895        name: &str,
896        kind: PersistenceKind,
897        instance: Option<&str>,
898        message_id: &str,
899    ) -> HandlerResult<()> {
900        if message_id.is_empty() {
901            return Err(HandlerError::new(
902                "dex_sdk::HandlerError",
903                "Channel message ID must not be empty",
904            ));
905        }
906        let channel_name = self.registered_name(name, kind, instance)?;
907        self.channel_deletions.push(ChannelMessageDeletion {
908            channel_name: channel_name.clone(),
909            message_id: message_id.to_string(),
910        });
911        if let Some(info) = self.channel_infos.get_mut(&channel_name) {
912            info.size = info.size.saturating_sub(1);
913        }
914        Ok(())
915    }
916
917    fn channel_size_value(
918        &self,
919        name: &str,
920        kind: PersistenceKind,
921        instance: Option<&str>,
922    ) -> HandlerResult<usize> {
923        let channel_name = self.registered_name(name, kind, instance)?;
924        Ok(self
925            .channel_infos
926            .get(&channel_name)
927            .map_or(0, |info| usize::try_from(info.size).unwrap_or_default()))
928    }
929
930    fn channel_results_value<T: Value>(
931        &self,
932        name: &str,
933        kind: PersistenceKind,
934        instance: Option<&str>,
935    ) -> HandlerResult<Vec<T>> {
936        let channel_name = self.registered_name(name, kind, instance)?;
937        let Some(results) = &self.condition_results else {
938            return Ok(Vec::new());
939        };
940        let mut decoded = Vec::new();
941        for result in &results.channel_results {
942            if result.channel_name == channel_name
943                && result.condition_status == ConditionStatus::Completed as i32
944            {
945                for value in &result.values {
946                    decoded.push(value_mapper::decode_handler(value)?);
947                }
948            }
949        }
950        Ok(decoded)
951    }
952
953    fn pending_channel_messages_value<T: Value>(
954        &self,
955        name: &str,
956        kind: PersistenceKind,
957        instance: Option<&str>,
958    ) -> HandlerResult<Vec<ChannelMessage<T>>> {
959        let physical_name = self.registered_name(name, kind, instance)?;
960        let is_loaded = match kind {
961            PersistenceKind::Channel => self.loaded_channel_names.contains(&physical_name),
962            PersistenceKind::ChannelMap => {
963                self.loaded_channel_map_instances
964                    .contains(&format!("{name}/"))
965                    || self.loaded_channel_map_instances.contains(&physical_name)
966            }
967            _ => false,
968        };
969        if !is_loaded {
970            return Err(channel_messages_not_loaded(format!(
971                "Channel messages were not loaded for this invocation: {physical_name}"
972            )));
973        }
974        self.loaded_channel_messages
975            .get(&physical_name)
976            .map_or(&[][..], |values| values.messages.as_slice())
977            .iter()
978            .map(|message| {
979                let value = message.value.as_ref().ok_or_else(|| {
980                    HandlerError::new(
981                        "dex_sdk::HandlerError",
982                        format!("Channel message {} has no Value", message.message_id),
983                    )
984                })?;
985                Ok(ChannelMessage {
986                    message_id: message.message_id.clone(),
987                    value: value_mapper::decode_handler(value)?,
988                })
989            })
990            .collect()
991    }
992
993    fn registered_name(
994        &self,
995        name: &str,
996        kind: PersistenceKind,
997        instance: Option<&str>,
998    ) -> HandlerResult<String> {
999        let definition = self.flow.persistence.get(name).ok_or_else(|| {
1000            HandlerError::new(
1001                "dex_sdk::HandlerError",
1002                format!("persistence definition does not belong to Flow: {name}"),
1003            )
1004        })?;
1005        if definition.kind != kind {
1006            return Err(HandlerError::new(
1007                "dex_sdk::HandlerError",
1008                format!("persistence definition kind does not match: {name}"),
1009            ));
1010        }
1011        match kind {
1012            PersistenceKind::AttributeMap | PersistenceKind::ChannelMap => {
1013                let instance = instance.ok_or_else(|| {
1014                    HandlerError::new(
1015                        "dex_sdk::HandlerError",
1016                        format!("{name} requires an instance"),
1017                    )
1018                })?;
1019                validate_map_instance(instance)
1020                    .map_err(|error| HandlerError::new("dex_sdk::HandlerError", error))?;
1021                Ok(physical_name(name, instance))
1022            }
1023            PersistenceKind::Attribute | PersistenceKind::Channel if instance.is_none() => {
1024                Ok(name.to_string())
1025            }
1026            _ => Err(HandlerError::new(
1027                "dex_sdk::HandlerError",
1028                format!("static persistence definition cannot use an instance: {name}"),
1029            )),
1030        }
1031    }
1032}
1033
1034fn attribute_map_not_loaded(message: impl Into<String>) -> HandlerError {
1035    HandlerError::new("dex_sdk::AttributeMapNotLoadedError", message)
1036}
1037
1038fn channel_messages_not_loaded(message: impl Into<String>) -> HandlerError {
1039    HandlerError::new("dex_sdk::ChannelMessagesNotLoadedError", message)
1040}
1041
1042fn sorted_instance_keys(
1043    prefix: &str,
1044    physical_keys: HashSet<String>,
1045) -> HandlerResult<Vec<String>> {
1046    let mut keys = physical_keys
1047        .iter()
1048        .map(|key| {
1049            decode_instance(key, prefix)
1050                .map_err(|error| HandlerError::new("dex_sdk::HandlerError", error))
1051        })
1052        .collect::<HandlerResult<Vec<_>>>()?;
1053    keys.sort();
1054    Ok(keys)
1055}
1056
1057#[derive(Clone)]
1058pub(crate) struct InvocationCancellation {
1059    inner: Arc<InvocationCancellationInner>,
1060}
1061
1062struct InvocationCancellationInner {
1063    cancelled: AtomicBool,
1064    mutex: Mutex<()>,
1065    condition: Condvar,
1066    notification: tokio::sync::Notify,
1067}
1068
1069impl InvocationCancellation {
1070    pub(crate) fn new() -> Self {
1071        Self {
1072            inner: Arc::new(InvocationCancellationInner {
1073                cancelled: AtomicBool::new(false),
1074                mutex: Mutex::new(()),
1075                condition: Condvar::new(),
1076                notification: tokio::sync::Notify::new(),
1077            }),
1078        }
1079    }
1080
1081    fn wait(&self) {
1082        let mut guard = self.inner.mutex.lock().expect("cancellation lock");
1083        while !self.is_cancelled() {
1084            guard = self.inner.condition.wait(guard).expect("cancellation lock");
1085        }
1086    }
1087
1088    pub(crate) fn is_cancelled(&self) -> bool {
1089        self.inner.cancelled.load(Ordering::Acquire)
1090    }
1091
1092    pub(crate) fn cancel(&self) {
1093        let _guard = self.inner.mutex.lock().expect("cancellation lock");
1094        self.inner.cancelled.store(true, Ordering::Release);
1095        self.inner.condition.notify_all();
1096        self.inner.notification.notify_waiters();
1097    }
1098
1099    pub(crate) async fn cancelled(&self) {
1100        let notified = self.inner.notification.notified();
1101        if self.is_cancelled() {
1102            return;
1103        }
1104        notified.await;
1105    }
1106}
1107
1108fn system_time(timestamp: i64) -> SystemTime {
1109    if timestamp >= 0 {
1110        SystemTime::UNIX_EPOCH + Duration::from_secs(timestamp as u64)
1111    } else {
1112        SystemTime::UNIX_EPOCH - Duration::from_secs(timestamp.unsigned_abs())
1113    }
1114}
1115
1116fn map_values(kind: &str, entries: Vec<Kv>) -> HandlerResult<HashMap<String, ProtoValue>> {
1117    let mut mapped = HashMap::new();
1118    for entry in entries {
1119        require_name(&entry.key, kind)?;
1120        let value = entry.value.ok_or_else(|| {
1121            HandlerError::new(
1122                "dex_sdk::HandlerError",
1123                format!("{kind} {} has no Value", entry.key),
1124            )
1125        })?;
1126        if mapped.insert(entry.key.clone(), value).is_some() {
1127            return Err(HandlerError::new(
1128                "dex_sdk::HandlerError",
1129                format!("duplicate {kind} {}", entry.key),
1130            ));
1131        }
1132    }
1133    Ok(mapped)
1134}
1135
1136fn require_name(value: &str, kind: &str) -> HandlerResult<()> {
1137    if value.is_empty() {
1138        Err(HandlerError::new(
1139            "dex_sdk::HandlerError",
1140            format!("{kind} is required"),
1141        ))
1142    } else {
1143        Ok(())
1144    }
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149    use super::{Context, ContextInput, InvocationCancellation, InvocationMethod};
1150    use crate::server_protocol::NegotiatedServerProtocolVersion;
1151    use crate::{
1152        Attribute, AttributeMap, Channel, ChannelMap, Flow, PersistenceSchema, Registry,
1153        registry::physical_name, value_mapper,
1154    };
1155    use dex_protocol::dex::{
1156        ChannelInfo, ChannelMessage as ProtoChannelMessage, ChannelValues, Context as ProtoContext,
1157        Kv,
1158    };
1159    use std::collections::HashMap;
1160
1161    struct MapFlow {
1162        attributes: AttributeMap<String>,
1163        channels: ChannelMap<String>,
1164    }
1165
1166    impl Flow for MapFlow {
1167        type StartInput = ();
1168
1169        fn persistence(&self) -> PersistenceSchema {
1170            PersistenceSchema::new()
1171                .attribute_map(&self.attributes)
1172                .channel_map(&self.channels)
1173        }
1174    }
1175
1176    #[test]
1177    fn map_introspection_tracks_buffered_changes() {
1178        let flow = MapFlow {
1179            attributes: AttributeMap::new("items"),
1180            channels: ChannelMap::new("messages"),
1181        };
1182        let registry = Registry::new().register(flow).expect("register map Flow");
1183        let registered = registry.flow("MapFlow").expect("lookup map Flow").clone();
1184        let attributes = AttributeMap::<String>::new("items");
1185        let channels = ChannelMap::<String>::new("messages");
1186        let special = "special % key";
1187        let mut context = Context::new(
1188            ContextInput {
1189                method: InvocationMethod::Rpc,
1190                flow: registered,
1191                metadata: ProtoContext::default(),
1192                attributes: vec![
1193                    Kv {
1194                        key: physical_name("items", special),
1195                        value: Some(value_mapper::encode_handler(&"initial".to_string()).unwrap()),
1196                    },
1197                    Kv {
1198                        key: physical_name("items", "z"),
1199                        value: Some(value_mapper::encode_handler(&"remove".to_string()).unwrap()),
1200                    },
1201                ],
1202                locals: Vec::new(),
1203                condition_results: None,
1204                channel_infos: HashMap::from([
1205                    (physical_name("messages", special), ChannelInfo { size: 1 }),
1206                    (physical_name("messages", "empty"), ChannelInfo { size: 0 }),
1207                ]),
1208                loaded_channel_messages: HashMap::new(),
1209                loaded_attribute_map_instances: vec!["items/".to_string()],
1210                loaded_channel_names: Vec::new(),
1211                loaded_channel_map_instances: Vec::new(),
1212                negotiated_protocol_version: NegotiatedServerProtocolVersion::default(),
1213            },
1214            None,
1215            InvocationCancellation::new(),
1216        )
1217        .expect("create RPC Context");
1218
1219        assert!(
1220            attributes
1221                .set(&mut context, "tenant/order", "invalid".to_string())
1222                .is_err()
1223        );
1224        assert!(
1225            channels
1226                .publish(&mut context, "tenant/order", "invalid".to_string())
1227                .is_err()
1228        );
1229
1230        assert_eq!(
1231            vec![special.to_string(), "z".to_string()],
1232            attributes.all_instance_keys(&context).unwrap()
1233        );
1234        attributes
1235            .set(&mut context, "a", "added".to_string())
1236            .unwrap();
1237        attributes.delete(&mut context, "z").unwrap();
1238        assert_eq!(
1239            vec!["a".to_string(), special.to_string()],
1240            attributes.all_instance_keys(&context).unwrap()
1241        );
1242        assert_eq!(2, attributes.map_size(&context).unwrap());
1243
1244        assert_eq!(
1245            vec![special.to_string()],
1246            channels.all_instance_keys(&context).unwrap()
1247        );
1248        channels
1249            .publish(&mut context, "a", "published".to_string())
1250            .unwrap();
1251        assert_eq!(
1252            vec!["a".to_string(), special.to_string()],
1253            channels.all_instance_keys(&context).unwrap()
1254        );
1255        assert_eq!(2, channels.map_size(&context).unwrap());
1256        assert!(context.record_heartbeat().is_err());
1257        assert!(
1258            context
1259                .record_heartbeat_value("checkpoint".to_string())
1260                .is_err()
1261        );
1262        assert!(context.last_heartbeat_value::<String>().is_err());
1263    }
1264
1265    struct SelectiveFlow {
1266        attributes: AttributeMap<String>,
1267        queued: Channel<String>,
1268        by_tenant: ChannelMap<String>,
1269    }
1270
1271    impl Flow for SelectiveFlow {
1272        type StartInput = ();
1273
1274        fn persistence(&self) -> PersistenceSchema {
1275            PersistenceSchema::new()
1276                .attribute_map(&self.attributes)
1277                .channel(&self.queued)
1278                .channel_map(&self.by_tenant)
1279        }
1280    }
1281
1282    #[test]
1283    fn rpc_selective_state_snapshots_are_typed_and_presence_aware() {
1284        let flow = SelectiveFlow {
1285            attributes: AttributeMap::new("selected-items"),
1286            queued: Channel::new("selected-commands"),
1287            by_tenant: ChannelMap::new("selected-by-tenant"),
1288        };
1289        let registry = Registry::new()
1290            .register(flow)
1291            .expect("register selective Flow");
1292        let registered = registry
1293            .flow("SelectiveFlow")
1294            .expect("lookup selective Flow")
1295            .clone();
1296        let attributes = AttributeMap::<String>::new("selected-items");
1297        let queued = Channel::<String>::new("selected-commands");
1298        let by_tenant = ChannelMap::<String>::new("selected-by-tenant");
1299        let attribute_name = physical_name("selected-items", "tenant-a");
1300        let channel_map_name = physical_name("selected-by-tenant", "tenant-a");
1301        let mut context = Context::new(
1302            ContextInput {
1303                method: InvocationMethod::Rpc,
1304                flow: registered,
1305                metadata: ProtoContext::default(),
1306                attributes: vec![Kv {
1307                    key: attribute_name.clone(),
1308                    value: Some(value_mapper::encode_handler(&"value".to_string()).unwrap()),
1309                }],
1310                locals: Vec::new(),
1311                condition_results: None,
1312                channel_infos: HashMap::new(),
1313                loaded_channel_messages: HashMap::from([
1314                    (
1315                        "selected-commands".to_string(),
1316                        ChannelValues {
1317                            messages: vec![ProtoChannelMessage {
1318                                channel_name: "selected-commands".to_string(),
1319                                value: Some(
1320                                    value_mapper::encode_handler(&"first".to_string()).unwrap(),
1321                                ),
1322                                message_id: "message-1".to_string(),
1323                            }],
1324                        },
1325                    ),
1326                    (
1327                        channel_map_name.clone(),
1328                        ChannelValues {
1329                            messages: vec![ProtoChannelMessage {
1330                                channel_name: channel_map_name.clone(),
1331                                value: Some(
1332                                    value_mapper::encode_handler(&"second".to_string()).unwrap(),
1333                                ),
1334                                message_id: "message-2".to_string(),
1335                            }],
1336                        },
1337                    ),
1338                ]),
1339                loaded_attribute_map_instances: vec![attribute_name],
1340                loaded_channel_names: vec!["selected-commands".to_string()],
1341                loaded_channel_map_instances: vec![channel_map_name],
1342                negotiated_protocol_version: NegotiatedServerProtocolVersion::default(),
1343            },
1344            None,
1345            InvocationCancellation::new(),
1346        )
1347        .expect("create selective RPC Context");
1348
1349        assert_eq!(
1350            Some("value".to_string()),
1351            attributes.get(&context, "tenant-a").unwrap()
1352        );
1353        assert_eq!("first", queued.pending_messages(&context).unwrap()[0].value);
1354        assert_eq!(
1355            "message-2",
1356            by_tenant.pending_messages(&context, "tenant-a").unwrap()[0].message_id
1357        );
1358        assert_eq!(
1359            Some("first".to_string()),
1360            queued
1361                .find_pending_message(&context, "message-1")
1362                .unwrap()
1363                .map(|message| message.value)
1364        );
1365        assert!(attributes.get(&context, "other").is_err());
1366        assert!(by_tenant.pending_messages(&context, "other").is_err());
1367        assert!(queued.publish(&mut context, "later".to_string()).is_ok());
1368        assert_eq!(1, queued.pending_messages(&context).unwrap().len());
1369    }
1370
1371    struct HardDeleteFlow;
1372
1373    #[derive(serde::Deserialize, serde::Serialize)]
1374    struct NumericNewtype(u64);
1375
1376    static API_KEY: std::sync::LazyLock<Attribute<String>> =
1377        std::sync::LazyLock::new(|| Attribute::new("api-key").hard_delete_on_null());
1378    static NUMERIC_SECRET: std::sync::LazyLock<Attribute<NumericNewtype>> =
1379        std::sync::LazyLock::new(|| Attribute::new("numeric-secret").hard_delete_on_null());
1380    static TENANT_KEYS: std::sync::LazyLock<AttributeMap<String>> =
1381        std::sync::LazyLock::new(|| AttributeMap::new("tenant-keys").hard_delete_on_null());
1382    static PLAIN_API_KEY: std::sync::LazyLock<Attribute<String>> =
1383        std::sync::LazyLock::new(|| Attribute::new("api-key"));
1384
1385    impl Flow for HardDeleteFlow {
1386        type StartInput = ();
1387
1388        fn persistence(&self) -> PersistenceSchema {
1389            PersistenceSchema::new()
1390                .attribute(&API_KEY)
1391                .attribute(&NUMERIC_SECRET)
1392                .attribute_map(&TENANT_KEYS)
1393        }
1394    }
1395
1396    fn hard_delete_context(negotiated_protocol_version: u32) -> Context {
1397        let registry = Registry::new()
1398            .register(HardDeleteFlow)
1399            .expect("register hard-delete Flow");
1400        let registered = registry
1401            .flow("HardDeleteFlow")
1402            .expect("lookup hard-delete Flow")
1403            .clone();
1404        let negotiated = NegotiatedServerProtocolVersion::default();
1405        negotiated.store(negotiated_protocol_version);
1406        Context::new(
1407            ContextInput {
1408                method: InvocationMethod::Rpc,
1409                flow: registered,
1410                metadata: ProtoContext::default(),
1411                attributes: Vec::new(),
1412                locals: Vec::new(),
1413                condition_results: None,
1414                channel_infos: HashMap::new(),
1415                loaded_channel_messages: HashMap::new(),
1416                loaded_attribute_map_instances: vec!["tenant-keys/".to_string()],
1417                loaded_channel_names: Vec::new(),
1418                loaded_channel_map_instances: Vec::new(),
1419                negotiated_protocol_version: negotiated,
1420            },
1421            None,
1422            InvocationCancellation::new(),
1423        )
1424        .expect("create hard-delete Context")
1425    }
1426
1427    #[test]
1428    fn hard_delete_writes_set_the_flag_and_validate_values_and_protocol() {
1429        let mut context = hard_delete_context(3);
1430        API_KEY.set(&mut context, "key-1".to_string()).unwrap();
1431        TENANT_KEYS
1432            .set(&mut context, "tenant-a", "key-a".to_string())
1433            .unwrap();
1434        TENANT_KEYS.delete(&mut context, "tenant-b").unwrap();
1435        let error = NUMERIC_SECRET
1436            .set(&mut context, NumericNewtype(7))
1437            .expect_err("numeric value");
1438        assert!(
1439            error
1440                .to_string()
1441                .contains("numeric and Boolean values are rejected")
1442        );
1443        NUMERIC_SECRET.delete(&mut context).unwrap();
1444        let error = PLAIN_API_KEY
1445            .set(&mut context, "leak".to_string())
1446            .expect_err("mismatched definition");
1447        assert!(
1448            error
1449                .to_string()
1450                .contains("does not match its registered definition")
1451        );
1452        assert_eq!(Some("key-1".to_string()), API_KEY.get(&context).unwrap());
1453
1454        let mut writes = context.take_outputs().attributes;
1455        writes.sort_by(|left, right| left.key.cmp(&right.key));
1456        let keys = writes
1457            .iter()
1458            .map(|write| write.key.as_str())
1459            .collect::<Vec<_>>();
1460        assert_eq!(
1461            vec![
1462                "api-key",
1463                "numeric-secret",
1464                "tenant-keys/tenant-a",
1465                "tenant-keys/tenant-b"
1466            ],
1467            keys
1468        );
1469        assert!(writes.iter().all(|write| write.is_hard_delete_on_null
1470            && write.index_config.is_none()
1471            && write.sync_config.is_none()));
1472
1473        for version in [0, 1, 2] {
1474            let mut context = hard_delete_context(version);
1475            let error = API_KEY
1476                .set(&mut context, "key-1".to_string())
1477                .expect_err("old Server");
1478            assert!(error.to_string().contains("protocol version 3 or later"));
1479            let error = API_KEY.delete(&mut context).expect_err("old Server");
1480            assert!(error.to_string().contains("protocol version 3 or later"));
1481            assert!(context.take_outputs().attributes.is_empty());
1482        }
1483    }
1484}