From 28d081359fbec9d8ded239b916b37a39dfc85db8 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 12 Aug 2026 01:04:35 +0200 Subject: [PATCH 1/6] wip --- .../reflex-base/src/reflex_base/registry.py | 91 ++++++++++++- tests/units/reflex_base/test_registry.py | 124 ++++++++++++++++++ 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/packages/reflex-base/src/reflex_base/registry.py b/packages/reflex-base/src/reflex_base/registry.py index 71b4d723e5e..7293cc238c1 100644 --- a/packages/reflex-base/src/reflex_base/registry.py +++ b/packages/reflex-base/src/reflex_base/registry.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar from typing_extensions import Self @@ -14,6 +14,8 @@ from reflex.state import BaseState from reflex_base.event import EventHandler +STATE_TYPE = TypeVar("STATE_TYPE", bound="BaseState") + @dataclasses.dataclass(frozen=True, kw_only=True, slots=True) class RegisteredEventHandler: @@ -39,6 +41,20 @@ class RegistrationContext(BaseContext): default_factory=dict, repr=False, ) + # Memoized results of get_states_implementing / resolve_implementation, + # invalidated whenever a new base state is registered. + _implementations: dict[type[BaseState], tuple[type[BaseState], ...]] = ( + dataclasses.field( + default_factory=dict, + repr=False, + ) + ) + _resolved_implementations: dict[type[BaseState], type[BaseState]] = ( + dataclasses.field( + default_factory=dict, + repr=False, + ) + ) @classmethod def ensure_context(cls) -> Self: @@ -81,6 +97,8 @@ def _register_base_state(self, state_cls: type[BaseState]) -> type[BaseState]: The registered base state class. """ self.base_states[state_cls.get_full_name()] = state_cls + self._implementations.clear() + self._resolved_implementations.clear() for event_handler in state_cls.event_handlers.values(): self._register_event_handler(event_handler, states=(state_cls,)) if (parent_state := state_cls.get_parent_state()) is not None: @@ -153,3 +171,74 @@ def get_substates( return self.base_state_substates.setdefault( base_state_cls.get_full_name(), set() ) + + def get_states_implementing( + self, interface: type[STATE_TYPE] + ) -> tuple[type[STATE_TYPE], ...]: + """Get every registered state class that is a subclass of the given interface. + + The interface is typically a state mixin, which has no place in the state + tree of its own and therefore cannot be passed to `get_state` directly. + + Args: + interface: The state mixin (or state class) to look up implementations of. + + Returns: + The registered states implementing the interface, in registration order. + """ + if (cached := self._implementations.get(interface)) is not None: + return cached # pyright: ignore [reportReturnType] + implementations = tuple( + state_cls + for state_cls in self.base_states.values() + if issubclass(state_cls, interface) + ) + self._implementations[interface] = implementations + return implementations + + def resolve_implementation(self, interface: type[STATE_TYPE]) -> type[STATE_TYPE]: + """Get the single registered state class implementing the given interface. + + Subclasses of another implementation are ignored, so an app is free to + further subclass the state a mixin was applied to. What must be + unambiguous is where the mixin entered the state tree, since each such + state holds its own independent copy of the mixin's vars. + + Args: + interface: The state mixin (or state class) to resolve. + + Returns: + The state class the interface was mixed into. + + Raises: + StateValueError: If no or more than one state implements the interface. + """ + if (cached := self._resolved_implementations.get(interface)) is not None: + return cached # pyright: ignore [reportReturnType] + implementations = self.get_states_implementing(interface) + # Keep only the states where the interface entered the tree, dropping + # any implementation that inherits from another implementation. + roots = [ + state_cls + for state_cls in implementations + if not any( + other is not state_cls and issubclass(state_cls, other) + for other in implementations + ) + ] + if not roots: + msg = ( + f"No registered state implements {interface.__name__}. " + "Define a state which inherits from it before resolving it." + ) + raise StateValueError(msg) + if len(roots) > 1: + names = ", ".join(sorted(state.get_full_name() for state in roots)) + msg = ( + f"{interface.__name__} is implemented by more than one state: {names}. " + "Resolving it is ambiguous, pass the wanted state class explicitly." + ) + raise StateValueError(msg) + resolved = roots[0] + self._resolved_implementations[interface] = resolved + return resolved diff --git a/tests/units/reflex_base/test_registry.py b/tests/units/reflex_base/test_registry.py index 474acf874c8..8721300b1fd 100644 --- a/tests/units/reflex_base/test_registry.py +++ b/tests/units/reflex_base/test_registry.py @@ -131,3 +131,127 @@ async def _tmp(): handler = EventHandler(fn=_tmp) RegistrationContext.register_event_handler(handler) assert len(forked_registration_context.event_handlers) > 0 + + +def test_resolve_implementation(clean_registration_context: RegistrationContext): + """resolve_implementation finds the state a mixin was mixed into. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + from reflex.state import BaseState + + class ResolveMixin(BaseState, mixin=True): + pass + + class ResolveImpl(ResolveMixin, BaseState): + pass + + assert clean_registration_context.resolve_implementation(ResolveMixin) is ( + ResolveImpl + ) + assert clean_registration_context.get_states_implementing(ResolveMixin) == ( + ResolveImpl, + ) + + +def test_resolve_implementation_ignores_subclasses( + clean_registration_context: RegistrationContext, +): + """Subclasses of an implementation do not make the resolution ambiguous. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + from reflex.state import BaseState + + class SubclassMixin(BaseState, mixin=True): + pass + + class SubclassImpl(SubclassMixin, BaseState): + pass + + class SubclassChild(SubclassImpl): + pass + + assert set(clean_registration_context.get_states_implementing(SubclassMixin)) == { + SubclassImpl, + SubclassChild, + } + assert clean_registration_context.resolve_implementation(SubclassMixin) is ( + SubclassImpl + ) + + +def test_resolve_implementation_without_implementation( + clean_registration_context: RegistrationContext, +): + """Resolving a mixin no state implements raises StateValueError. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + from reflex.state import BaseState + + class UnusedMixin(BaseState, mixin=True): + pass + + assert clean_registration_context.get_states_implementing(UnusedMixin) == () + with pytest.raises(StateValueError, match="No registered state implements"): + clean_registration_context.resolve_implementation(UnusedMixin) + + +def test_resolve_implementation_ambiguous( + clean_registration_context: RegistrationContext, +): + """Resolving a mixin used by two unrelated states raises StateValueError. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + from reflex.state import BaseState + + class AmbiguousMixin(BaseState, mixin=True): + pass + + class AmbiguousImpl1(AmbiguousMixin, BaseState): + pass + + class AmbiguousImpl2(AmbiguousMixin, BaseState): + pass + + with pytest.raises(StateValueError, match="implemented by more than one state"): + clean_registration_context.resolve_implementation(AmbiguousMixin) + + +def test_resolve_implementation_cache_is_invalidated( + clean_registration_context: RegistrationContext, +): + """Registering a state invalidates previously cached lookups. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + from reflex.state import BaseState + + class CacheMixin(BaseState, mixin=True): + pass + + class CacheImpl(CacheMixin, BaseState): + pass + + assert clean_registration_context.resolve_implementation(CacheMixin) is CacheImpl + # Repeated lookups are served from the cache. + assert clean_registration_context.get_states_implementing( + CacheMixin + ) is clean_registration_context.get_states_implementing(CacheMixin) + + class CacheImpl2(CacheMixin, BaseState): + pass + + assert set(clean_registration_context.get_states_implementing(CacheMixin)) == { + CacheImpl, + CacheImpl2, + } + with pytest.raises(StateValueError, match="implemented by more than one state"): + clean_registration_context.resolve_implementation(CacheMixin) From b324ae403d7ae8a074823ed611ccba4a833aa5bf Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 12 Aug 2026 19:06:27 +0200 Subject: [PATCH 2/6] state api sugar. --- .../reflex-base/src/reflex_base/registry.py | 17 +-- reflex/istate/manager/__init__.py | 12 +- reflex/state.py | 8 +- tests/units/test_state.py | 113 ++++++++++++++++++ 4 files changed, 134 insertions(+), 16 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/registry.py b/packages/reflex-base/src/reflex_base/registry.py index 7293cc238c1..fab2ab611b7 100644 --- a/packages/reflex-base/src/reflex_base/registry.py +++ b/packages/reflex-base/src/reflex_base/registry.py @@ -41,8 +41,7 @@ class RegistrationContext(BaseContext): default_factory=dict, repr=False, ) - # Memoized results of get_states_implementing / resolve_implementation, - # invalidated whenever a new base state is registered. + # Memoized lookups, invalidated whenever a base state is registered. _implementations: dict[type[BaseState], tuple[type[BaseState], ...]] = ( dataclasses.field( default_factory=dict, @@ -177,9 +176,6 @@ def get_states_implementing( ) -> tuple[type[STATE_TYPE], ...]: """Get every registered state class that is a subclass of the given interface. - The interface is typically a state mixin, which has no place in the state - tree of its own and therefore cannot be passed to `get_state` directly. - Args: interface: The state mixin (or state class) to look up implementations of. @@ -197,12 +193,7 @@ def get_states_implementing( return implementations def resolve_implementation(self, interface: type[STATE_TYPE]) -> type[STATE_TYPE]: - """Get the single registered state class implementing the given interface. - - Subclasses of another implementation are ignored, so an app is free to - further subclass the state a mixin was applied to. What must be - unambiguous is where the mixin entered the state tree, since each such - state holds its own independent copy of the mixin's vars. + """Get the single registered state class the given interface was mixed into. Args: interface: The state mixin (or state class) to resolve. @@ -216,8 +207,8 @@ def resolve_implementation(self, interface: type[STATE_TYPE]) -> type[STATE_TYPE if (cached := self._resolved_implementations.get(interface)) is not None: return cached # pyright: ignore [reportReturnType] implementations = self.get_states_implementing(interface) - # Keep only the states where the interface entered the tree, dropping - # any implementation that inherits from another implementation. + # Keep only where the interface entered the tree: an implementation + # inheriting from another implementation is not a candidate. roots = [ state_cls for state_cls in implementations diff --git a/reflex/istate/manager/__init__.py b/reflex/istate/manager/__init__.py index 5e3d71ee0e6..029be384f92 100644 --- a/reflex/istate/manager/__init__.py +++ b/reflex/istate/manager/__init__.py @@ -4,7 +4,7 @@ import dataclasses from abc import ABC, abstractmethod from collections.abc import AsyncIterator -from typing import TYPE_CHECKING, TypedDict, overload +from typing import TYPE_CHECKING, TypedDict, cast, overload from reflex_base import constants from reflex_base.config import get_config @@ -93,7 +93,7 @@ def create(cls): @staticmethod def _coerce_token(token: StateToken[TOKEN_TYPE] | str) -> StateToken[TOKEN_TYPE]: - """Convert a legacy string token to a StateToken if needed. + """Normalize a token: convert legacy string tokens and resolve state mixins. Args: token: The token, either a StateToken or legacy string. @@ -106,6 +106,14 @@ def _coerce_token(token: StateToken[TOKEN_TYPE] | str) -> StateToken[TOKEN_TYPE] from reflex.state import State return BaseStateToken.from_legacy_token(token, root_state=State) # type: ignore[return-value] + # The store is never keyed on a mixin: resolve it to the state it was mixed into. + if getattr(token.cls, "_mixin", False): + from reflex_base.registry import RegistrationContext + + resolved = RegistrationContext.get().resolve_implementation( + cast("type[BaseState]", token.cls) + ) + return token.with_cls(cast("type[TOKEN_TYPE]", resolved)) return token @overload diff --git a/reflex/state.py b/reflex/state.py index bd54b5efa48..52e094594c9 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1734,11 +1734,17 @@ async def get_state(self, state_cls: type[T_STATE]) -> T_STATE: Allows for arbitrary access to sibling states from within an event handler. Args: - state_cls: The class of the state. + state_cls: The class of the state, or a state mixin to resolve. Returns: The instance of state_cls associated with this state's client_token. """ + # A mixin has no place in the state tree of its own: resolve it. + if state_cls._mixin: + from reflex_base.registry import RegistrationContext + + state_cls = RegistrationContext.get().resolve_implementation(state_cls) + # Fast case - if this state instance is already cached, get_substate from root state. try: return self._get_state_from_cache(state_cls) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 7106287a43d..1ac4cf78c79 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -33,6 +33,7 @@ ReflexRuntimeError, SetUndefinedStateVarError, StateSerializationError, + StateValueError, UnretrievableVarValueError, ) from reflex_base.utils.format import json_dumps @@ -5195,3 +5196,115 @@ async def _coro(value): # noqa: RUF029 - a trivial coroutine value for the delt } resolved = await _resolve_delta(delta) assert resolved == {"s2": {"keep": 1}} + + +async def test_get_state_mixin(clean_registration_context): + """get_state resolves a mixin to the state it was applied to. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + + class MixinRootState(BaseState): + pass + + class GreetMixin(BaseState, mixin=True): + greeting: str = "hi" + + class GreetState(GreetMixin, MixinRootState): + pass + + class UnrelatedState(MixinRootState): + pass + + root = MixinRootState(_reflex_internal_init=True) + unrelated = await root.get_state(UnrelatedState) + + greet_state = await unrelated.get_state(GreetMixin) + assert isinstance(greet_state, GreetState) + # Resolving the mixin and naming the state directly yield the same instance. + assert greet_state is await unrelated.get_state(GreetState) + assert greet_state.greeting == "hi" + + +async def test_get_state_mixin_ambiguous(clean_registration_context): + """get_state raises if more than one state implements the mixin. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + + class AmbiguousRootState(BaseState): + pass + + class AmbiguousMixin(BaseState, mixin=True): + pass + + class AmbiguousState1(AmbiguousMixin, AmbiguousRootState): + pass + + class AmbiguousState2(AmbiguousMixin, AmbiguousRootState): + pass + + root = AmbiguousRootState(_reflex_internal_init=True) + with pytest.raises(StateValueError, match="implemented by more than one state"): + await root.get_state(AmbiguousMixin) + + +async def test_state_manager_mixin_token(clean_registration_context): + """A token carrying a mixin resolves to the state implementing it. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + + class TokenRootState(BaseState): + pass + + class NoteMixin(BaseState, mixin=True): + note: str = "" + + class NoteState(NoteMixin, TokenRootState): + pass + + state_manager = StateManagerMemory() + mixin_token = BaseStateToken(ident="mixin-token", cls=NoteMixin) + + root = await state_manager.get_state(mixin_token) + assert isinstance(root, TokenRootState) + (await root.get_state(NoteMixin)).note = "written" + await state_manager.set_state(mixin_token, root) + + async with state_manager.modify_state(mixin_token) as modified: + assert isinstance(modified, TokenRootState) + assert (await modified.get_state(NoteMixin)).note == "written" + + # The concrete token addresses the very same state. + concrete_token = BaseStateToken(ident="mixin-token", cls=NoteState) + assert await state_manager.get_state(concrete_token) is root + + +async def test_state_manager_mixin_token_ambiguous(clean_registration_context): + """A token carrying an ambiguous mixin raises instead of guessing. + + Args: + clean_registration_context: A fresh, empty registration context. + """ + + class AmbiguousTokenRootState(BaseState): + pass + + class TwiceUsedMixin(BaseState, mixin=True): + pass + + class FirstUserState(TwiceUsedMixin, AmbiguousTokenRootState): + pass + + class SecondUserState(TwiceUsedMixin, AmbiguousTokenRootState): + pass + + state_manager = StateManagerMemory() + with pytest.raises(StateValueError, match="implemented by more than one state"): + await state_manager.get_state( + BaseStateToken(ident="ambiguous-token", cls=TwiceUsedMixin) + ) From 1b612211f111a61f291abd219d67799006d7d072 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 8 Oct 2026 22:15:35 +0200 Subject: [PATCH 3/6] Add news fragments for resolving state mixins --- news/+get-state-mixin.feature.md | 1 + packages/reflex-base/news/+registry-mixin-lookup.feature.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/+get-state-mixin.feature.md create mode 100644 packages/reflex-base/news/+registry-mixin-lookup.feature.md diff --git a/news/+get-state-mixin.feature.md b/news/+get-state-mixin.feature.md new file mode 100644 index 00000000000..93f3e72c024 --- /dev/null +++ b/news/+get-state-mixin.feature.md @@ -0,0 +1 @@ +`get_state()` accepts a state mixin and returns the state it was mixed into, so shared handlers can reach it without naming the concrete class: `await self.get_state(AuthMixin)`. A `BaseStateToken` whose `cls` is a mixin addresses that state in the state manager too. Both raise `StateValueError` when no state, or more than one unrelated state, uses the mixin. diff --git a/packages/reflex-base/news/+registry-mixin-lookup.feature.md b/packages/reflex-base/news/+registry-mixin-lookup.feature.md new file mode 100644 index 00000000000..d375e2a3a34 --- /dev/null +++ b/packages/reflex-base/news/+registry-mixin-lookup.feature.md @@ -0,0 +1 @@ +Add `RegistrationContext.get_states_implementing()`, which lists the registered states that inherit from a state mixin, and `RegistrationContext.resolve_implementation()`, which returns the single state the mixin was mixed into and raises `StateValueError` when there is none or the choice is ambiguous. From 61f52cd9983195f519aea121e30d3c55d5c239ce Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Thu, 8 Oct 2026 22:41:41 +0200 Subject: [PATCH 4/6] Close the state manager in the mixin token tests Use the shared state_manager fixture, which closes the manager on teardown and runs the tests against the memory, disk and redis backends. --- tests/units/test_state.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 2eafeb9745c..024e9eac0be 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -6926,11 +6926,15 @@ class AmbiguousState2(AmbiguousMixin, AmbiguousRootState): await root.get_state(AmbiguousMixin) -async def test_state_manager_mixin_token(clean_registration_context): +async def test_state_manager_mixin_token( + clean_registration_context, state_manager: StateManager, token: str +): """A token carrying a mixin resolves to the state implementing it. Args: clean_registration_context: A fresh, empty registration context. + state_manager: A state manager instance. + token: A token. """ class TokenRootState(BaseState): @@ -6942,28 +6946,26 @@ class NoteMixin(BaseState, mixin=True): class NoteState(NoteMixin, TokenRootState): pass - state_manager = StateManagerMemory() - mixin_token = BaseStateToken(ident="mixin-token", cls=NoteMixin) - - root = await state_manager.get_state(mixin_token) - assert isinstance(root, TokenRootState) - (await root.get_state(NoteMixin)).note = "written" - await state_manager.set_state(mixin_token, root) - - async with state_manager.modify_state(mixin_token) as modified: - assert isinstance(modified, TokenRootState) - assert (await modified.get_state(NoteMixin)).note == "written" + async with state_manager.modify_state( + BaseStateToken(ident=token, cls=NoteMixin) + ) as root: + assert isinstance(root, TokenRootState) + (await root.get_state(NoteMixin)).note = "written" # The concrete token addresses the very same state. - concrete_token = BaseStateToken(ident="mixin-token", cls=NoteState) - assert await state_manager.get_state(concrete_token) is root + root = await state_manager.get_state(BaseStateToken(ident=token, cls=NoteState)) + assert (await root.get_state(NoteState)).note == "written" -async def test_state_manager_mixin_token_ambiguous(clean_registration_context): +async def test_state_manager_mixin_token_ambiguous( + clean_registration_context, state_manager: StateManager, token: str +): """A token carrying an ambiguous mixin raises instead of guessing. Args: clean_registration_context: A fresh, empty registration context. + state_manager: A state manager instance. + token: A token. """ class AmbiguousTokenRootState(BaseState): @@ -6978,11 +6980,8 @@ class FirstUserState(TwiceUsedMixin, AmbiguousTokenRootState): class SecondUserState(TwiceUsedMixin, AmbiguousTokenRootState): pass - state_manager = StateManagerMemory() with pytest.raises(StateValueError, match="implemented by more than one state"): - await state_manager.get_state( - BaseStateToken(ident="ambiguous-token", cls=TwiceUsedMixin) - ) + await state_manager.get_state(BaseStateToken(ident=token, cls=TwiceUsedMixin)) def test_previous_release_pickle_keys_are_reserved(): From 284a3040e3094d8dc321ea1fde21f24dfddab0d8 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Wed, 21 Oct 2026 09:09:42 +0200 Subject: [PATCH 5/6] Drop the redundant RegistrationContext import in get_state --- reflex/state.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 10edee7f1f5..0c2633b2ed0 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1800,8 +1800,6 @@ async def get_state(self, state_cls: type[T_STATE]) -> T_STATE: """ # A mixin has no place in the state tree of its own: resolve it. if state_cls._mixin: - from reflex_base.registry import RegistrationContext - state_cls = RegistrationContext.get().resolve_implementation(state_cls) # Fast case - if this state instance is already cached, get_substate from root state. From 9618a3fc890b625f49eb4f1ac89fd1eabf982023 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Sun, 25 Oct 2026 14:20:48 +0100 Subject: [PATCH 6/6] Flush the oplock lease in the mixin token test and hoist the registry import With oplock enabled, modify_state keeps the write in the lease cache while get_state reads Redis, so the test read the state back before the write was flushed. --- reflex/istate/manager/__init__.py | 3 +-- tests/units/test_state.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/reflex/istate/manager/__init__.py b/reflex/istate/manager/__init__.py index c6b8f1e3ab3..4846da3e62e 100644 --- a/reflex/istate/manager/__init__.py +++ b/reflex/istate/manager/__init__.py @@ -12,6 +12,7 @@ from reflex_base import constants from reflex_base.config import get_config from reflex_base.event import Event +from reflex_base.registry import RegistrationContext from reflex_base.utils.exceptions import InvalidStateManagerModeError from typing_extensions import ReadOnly, Unpack, deprecated @@ -113,8 +114,6 @@ def _coerce_token(token: StateToken[TOKEN_TYPE] | str) -> StateToken[TOKEN_TYPE] return BaseStateToken.from_legacy_token(token, root_state=State) # type: ignore[return-value] # The store is never keyed on a mixin: resolve it to the state it was mixed into. if getattr(token.cls, "_mixin", False): - from reflex_base.registry import RegistrationContext - resolved = RegistrationContext.get().resolve_implementation( cast("type[BaseState]", token.cls) ) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index f794699571e..3dc3d37a95b 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -7199,6 +7199,9 @@ class NoteState(NoteMixin, TokenRootState): assert isinstance(root, TokenRootState) (await root.get_state(NoteMixin)).note = "written" + if environment.REFLEX_OPLOCK_ENABLED.get(): + await state_manager.close() + # The concrete token addresses the very same state. root = await state_manager.get_state(BaseStateToken(ident=token, cls=NoteState)) assert (await root.get_state(NoteState)).note == "written"