1414class StateShim :
1515 """In-memory view of an entity's state during a batch.
1616
17- The state arriving from the wire is held as its raw serialized JSON string
18- and is **not** deserialized in the constructor: deserialization is deferred
19- until :meth:`get_state` is called, so the caller's requested type reaches the
20- data converter together with the original payload (a custom converter can
21- then deserialize the string directly into the target type). Once the state
22- has been read into a Python value or replaced via :meth:`set_state`, it is
23- held as that live object instead.
24-
25- Tracking whether the current value is still the raw serialized string also
26- lets :meth:`encode_state` pass an unmodified payload straight back to the
27- wire instead of re-serializing it, which would double-encode the JSON.
17+ The state is held internally as its serialized JSON string at all times.
18+ The raw payload off the wire is stored verbatim; a live value supplied via
19+ :meth:`set_state` (or as a non-serialized constructor argument) is
20+ serialized immediately. Keeping a single, always-serialized representation
21+ has two consequences worth noting:
22+
23+ * Deserialization is deferred to :meth:`get_state`, so the caller's
24+ requested type reaches the data converter together with the original
25+ payload (a custom converter can deserialize the string directly into the
26+ target type), and the unmodified wire payload is handed back by
27+ :meth:`encode_state` without being re-encoded.
28+ * Serialization errors surface inside the failing operation (at
29+ :meth:`set_state`) rather than after the batch has run, so a bad write
30+ rolls back just that operation.
31+
32+ Because the held value is always the serialized form, :meth:`get_state`
33+ returns a freshly reconstructed object on every call; it does **not** return
34+ a reference to a stored live object. Mutating a value read from
35+ :meth:`get_state` therefore has no effect on the persisted state unless it
36+ is written back with :meth:`set_state`.
2837 """
2938
3039 def __init__ (self , start_state : Any , data_converter : "DataConverter | None" = None ,
3140 * , is_serialized : bool = False ):
32- # ``is_serialized`` marks ``start_state`` as a raw serialized payload
33- # (the value off the wire) whose deserialization should be deferred. A
34- # ``None`` state is never treated as serialized.
35- serialized = is_serialized and start_state is not None
36- self ._current_state : Any = start_state
37- self ._current_is_serialized : bool = serialized
38- self ._checkpoint_state : Any = start_state
39- self ._checkpoint_is_serialized : bool = serialized
40- self ._operation_actions : list [pb .OperationAction ] = []
41- self ._actions_checkpoint_state : int = 0
4241 if data_converter is None :
4342 from durabletask .serialization import JsonDataConverter
4443 data_converter = JsonDataConverter ()
4544 self ._data_converter = data_converter
45+ # The state is normalized to its serialized string form. ``is_serialized``
46+ # marks ``start_state`` as a raw payload already off the wire (stored
47+ # verbatim); otherwise a live value is serialized now. ``None`` stays
48+ # ``None`` (no persisted state).
49+ serialized_start = self ._serialize (start_state , is_serialized )
50+ self ._current_state : str | None = serialized_start
51+ self ._checkpoint_state : str | None = serialized_start
52+ self ._operation_actions : list [pb .OperationAction ] = []
53+ self ._actions_checkpoint_state : int = 0
54+
55+ def _serialize (self , state : Any , is_serialized : bool = False ) -> str | None :
56+ if state is None :
57+ return None
58+ if is_serialized :
59+ return state
60+ return self ._data_converter .serialize (state )
4661
4762 @overload
4863 def get_state (self , intended_type : type [TState ], default : TState ) -> TState :
@@ -60,16 +75,11 @@ def get_state(self, intended_type: type[TState] | None = None, default: TState |
6075 if self ._current_state is None :
6176 return default
6277
63- if self ._current_is_serialized :
64- # Deferred deserialization: the converter receives the raw payload
65- # together with the requested type.
66- if intended_type is None :
67- return self ._data_converter .deserialize (self ._current_state )
68- result = self ._data_converter .deserialize (self ._current_state , intended_type )
69- else :
70- if intended_type is None :
71- return self ._current_state
72- result = self ._data_converter .coerce (self ._current_state , intended_type )
78+ # Deferred deserialization: the converter receives the raw payload
79+ # together with the requested type.
80+ if intended_type is None :
81+ return self ._data_converter .deserialize (self ._current_state )
82+ result = self ._data_converter .deserialize (self ._current_state , intended_type )
7383
7484 # An explicit ``intended_type`` is a request to receive that type. The
7585 # default converter is best-effort and would silently return the raw
@@ -80,30 +90,25 @@ def get_state(self, intended_type: type[TState] | None = None, default: TState |
8090 if (isinstance (intended_type , type ) # pyright: ignore[reportUnnecessaryIsInstance]
8191 and not isinstance (result , intended_type )):
8292 raise TypeError (
83- f"Could not convert state of type '{ type (self . _current_state ).__name__ } ' to '{ intended_type .__name__ } '"
93+ f"Could not convert state of type '{ type (result ).__name__ } ' to '{ intended_type .__name__ } '"
8494 )
8595
8696 return result
8797
8898 def set_state (self , state : Any ) -> None :
89- # A value set in-process is a live Python object, not a serialized payload.
90- self . _current_state = state
91- self ._current_is_serialized = False
99+ # Serialize eagerly so the held value is always the wire form and any
100+ # serialization error surfaces here, inside the failing operation.
101+ self ._current_state = self . _serialize ( state )
92102
93103 def encode_state (self ) -> str | None :
94- """Serialize the current state for persistence back to the wire.
104+ """Return the serialized current state for persistence back to the wire.
95105
96- Returns ``None`` only when the state is actually ``None`` (which clears
97- the persisted entity state). When the current value is still the raw
98- serialized payload (the state was never modified), it is returned
99- unchanged to avoid double-encoding; otherwise the live value is
100- serialized.
106+ The state is already held in serialized form, so this is the stored
107+ value verbatim: ``None`` when there is no state (which clears the
108+ persisted entity state), otherwise the JSON string. No re-encoding
109+ occurs, so a payload that was never modified round-trips unchanged.
101110 """
102- if self ._current_state is None :
103- return None
104- if self ._current_is_serialized :
105- return self ._current_state
106- return self ._data_converter .serialize (self ._current_state )
111+ return self ._current_state
107112
108113 def add_operation_action (self , action : pb .OperationAction ) -> None :
109114 self ._operation_actions .append (action )
@@ -113,18 +118,14 @@ def get_operation_actions(self) -> list[pb.OperationAction]:
113118
114119 def commit (self ) -> None :
115120 self ._checkpoint_state = self ._current_state
116- self ._checkpoint_is_serialized = self ._current_is_serialized
117121 self ._actions_checkpoint_state = len (self ._operation_actions )
118122
119123 def rollback (self ) -> None :
120124 self ._current_state = self ._checkpoint_state
121- self ._current_is_serialized = self ._checkpoint_is_serialized
122125 self ._operation_actions = self ._operation_actions [:self ._actions_checkpoint_state ]
123126
124127 def reset (self ) -> None :
125128 self ._current_state = None
126- self ._current_is_serialized = False
127129 self ._checkpoint_state = None
128- self ._checkpoint_is_serialized = False
129130 self ._operation_actions = []
130131 self ._actions_checkpoint_state = 0
0 commit comments