Skip to content

Attributes

Maria Aurelia Heine edited this page Jul 30, 2026 · 5 revisions

πŸͺ§ Attributes

HasState

The attribute that you will be using for your matter types that are supposed to hold on to the last emission of this matter type.

New spells that register with a need for such stateful matter will immediately receive its last instance.

The suggested naming convention for stateful matter types is adding State suffix to them.

[HasState]
class PlayerInputState : Matter
{
    public InputState InputState { get; }
    public PlayerInputState(InputState state) => InputState = state;
}

πŸ“œπŸ§¨ State matter is single-writer. Rzeka enforces this at registration: attempting to register a second active writer for a [HasState] type throws InvalidOperationException. Pluck can still seed an initial value before a long-lived writer exists (its registration is disposed synchronously), but Pluck against a [HasState] type while a Loom or Strand already owns it will also throw. Dispose the existing writer first if you need for another spell to provide this state.

Evolving State

πŸ“œ State evolves via a Loom that reads the current state, listens to an event that influences it, and emits the new state back into the same stream.

The signature is a reducer: (state, event) β†’ state.

Q += rzeka.Loom<DragonPetNameState, DragonPetNameChanged, DragonPetNameState>(
    this,
    (nameState, nameChange) => nameChange
        .WithLatestFromMatter(nameState)
        .Select(((DragonPetNameChanged n, DragonPetNameState s) pair) =>
            new DragonPetNameState(pair.n.Name))
);

πŸ“œπŸ§¨ Event triggers state - not the other way around. Use WithLatestFromMatter with the event as the source, not CombineLatestMatter. With CombineLatest, every new state emission re-fires the Loom against the latest event and emits state again - an infinite self-feedback loop that ends in a stack overflow.

Multiple transitions

When state can change due to more than one event type, combine all transitions into one reducer using Observable.Merge. Declare the state itself as the Loom input and pull the trigger streams via Scry:

Q += rzeka.Loom<PlayerScoreState, PlayerScoreState>(
    this,
    state => Observable.Merge(
        rzeka.Scry<StarCollected>()
             .WithLatestFrom(state, (_, s) => new PlayerScoreState(s.Score + 1)),

        rzeka.Scry<BonusCollected>()
             .WithLatestFrom(state, (e, s) => new PlayerScoreState(s.Score + e.Points)),

        rzeka.Scry<PenaltyApplied>()
             .WithLatestFrom(state, (e, s) => new PlayerScoreState(s.Score - e.Points)),

        rzeka.Scry<GameReadyToLoad>()
             .Select(_ => new PlayerScoreState(0)),

        rzeka.Scry<CheatActivated>()
             .Select(_ => new PlayerScoreState(9999))
    )
);

Branches that need current state use WithLatestFrom(state, ...) with a result selector - plain Rx, cleaner than WithLatestFromMatter here since there are no named lambda parameters to echo. Branches that produce a fixed value skip WithLatestFrom entirely.

One writer, all transitions readable in one place - the full lifecycle of the state is visible at a glance.

Clone this wiki locally