Repository navigation
Attributes
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 throwsInvalidOperationException. 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.
π 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
WithLatestFromMatterwith the event as the source, notCombineLatestMatter. WithCombineLatest, 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.
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.