Skip to content

Commit 9dbb777

Browse files
fsecada01claude
andcommitted
feat: add state size guard with configurable warn and hard limits
Closes #14 — StateSerializer.serialize() now checks serialised state size: - Logs a warning when state exceeds warn_bytes (default 64 KB) - Raises ComponentError when state exceeds max_bytes (default 512 KB) - Both thresholds are configurable via class attributes and can be disabled by setting to 0 - 5 new tests covering warning, error, threshold, and disable paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 731336f commit 9dbb777

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

src/component_framework/core/component.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,12 +335,45 @@ async def async_dispatch(
335335

336336

337337
class StateSerializer:
338-
"""Handles safe serialization/deserialization of component state."""
338+
"""Handles safe serialization/deserialization of component state.
339+
340+
Class attributes:
341+
warn_bytes: Emit a warning when serialised state exceeds this size
342+
(default 64 KB). Set to ``0`` to disable warnings.
343+
max_bytes: Raise :class:`ComponentError` when serialised state exceeds
344+
this size (default 512 KB). Set to ``0`` to disable the hard limit.
345+
"""
346+
347+
warn_bytes: int = 64 * 1024 # 64 KB
348+
max_bytes: int = 512 * 1024 # 512 KB
339349

340350
@staticmethod
341351
def serialize(state: dict) -> str:
342-
"""Serialize state to JSON string."""
343-
return json.dumps(state, default=str)
352+
"""Serialize state to JSON string.
353+
354+
Emits a warning if the result exceeds :attr:`warn_bytes` and raises
355+
:class:`ComponentError` if it exceeds :attr:`max_bytes`.
356+
"""
357+
serialized = json.dumps(state, default=str)
358+
size = len(serialized)
359+
360+
if StateSerializer.max_bytes and size > StateSerializer.max_bytes:
361+
raise ComponentError(
362+
f"Component state is {size:,} bytes "
363+
f"(hard limit: {StateSerializer.max_bytes:,}). "
364+
"Move large data out of state — store IDs/keys instead of "
365+
"full objects, or use server-side caching."
366+
)
367+
368+
if StateSerializer.warn_bytes and size > StateSerializer.warn_bytes:
369+
logger.warning(
370+
"Component state is %s bytes (threshold: %s). "
371+
"Consider moving large data out of state.",
372+
f"{size:,}",
373+
f"{StateSerializer.warn_bytes:,}",
374+
)
375+
376+
return serialized
344377

345378
@staticmethod
346379
def deserialize(data: str) -> dict:

tests/test_component.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,61 @@ def test_deserialize_invalid_json_raises(self):
260260
StateSerializer.deserialize("not json")
261261

262262

263+
# ---------- State Size Guard ----------
264+
265+
266+
class TestStateSizeGuard:
267+
def setup_method(self):
268+
self._orig_warn = StateSerializer.warn_bytes
269+
self._orig_max = StateSerializer.max_bytes
270+
271+
def teardown_method(self):
272+
StateSerializer.warn_bytes = self._orig_warn
273+
StateSerializer.max_bytes = self._orig_max
274+
275+
def test_warning_on_large_state(self, caplog):
276+
StateSerializer.warn_bytes = 50
277+
StateSerializer.max_bytes = 0 # disable hard limit
278+
big_state = {"data": "x" * 100}
279+
import logging
280+
281+
with caplog.at_level(logging.WARNING):
282+
StateSerializer.serialize(big_state)
283+
assert "Component state is" in caplog.text
284+
assert "threshold" in caplog.text
285+
286+
def test_no_warning_under_threshold(self, caplog):
287+
StateSerializer.warn_bytes = 100_000
288+
StateSerializer.max_bytes = 0
289+
import logging
290+
291+
with caplog.at_level(logging.WARNING):
292+
StateSerializer.serialize({"small": True})
293+
assert "Component state is" not in caplog.text
294+
295+
def test_error_on_exceeding_hard_limit(self):
296+
StateSerializer.max_bytes = 50
297+
big_state = {"data": "x" * 100}
298+
with pytest.raises(ComponentError, match="hard limit"):
299+
StateSerializer.serialize(big_state)
300+
301+
def test_no_error_under_hard_limit(self):
302+
StateSerializer.max_bytes = 100_000
303+
result = StateSerializer.serialize({"small": True})
304+
assert result # no exception
305+
306+
def test_guards_disabled_when_zero(self, caplog):
307+
StateSerializer.warn_bytes = 0
308+
StateSerializer.max_bytes = 0
309+
big_state = {"data": "x" * 10_000}
310+
import logging
311+
312+
with caplog.at_level(logging.WARNING):
313+
result = StateSerializer.serialize(big_state)
314+
assert result
315+
assert "Component state is" not in caplog.text
316+
317+
263318
# ---------- Async Event Handling ----------
264319

265320

0 commit comments

Comments
 (0)