Core Concepts

Readonly properties

Expose a readonly property while still keeping it reactive through explicit state updates.

What it means

A readonly property should not be written from the outside, but it can still change when underlying writable fields change. In this pattern, you store the readonly value in `state manager` under an internal id, then read it through a getter.

Practical value

This keeps your model API safe and clear: consumers can read `aPlusB`, but only model logic can update it. You get immutable-looking API boundaries without losing reactive updates and change events.

Key points

How the pattern works

In the example, `aPlusB` is readonly for callers because it only has a getter. Internally, the model stores that value in StateManager using a private key (`_aPlusBId`).

Whenever `a` or `b` changes, `setAPlusB()` recomputes the new value and writes it through `stateManager.setState(this, _aPlusBId, ...)`.

Why this is still reactive

The readonly getter returns `stateManager.getState(this, _aPlusBId)`, so reads always use the latest committed value.

Because updates go through StateManager, `changed` emissions are produced for the readonly value key, so subscribers can react just like with normal writable state.

Ownership and cleanup

This pattern makes ownership explicit: only the model is allowed to mutate the readonly result, through one internal method.

When the model is disposed, release the registered state id. This prevents stale watches and keeps memory/subscriptions clean.

When to use this approach

Use it when a value should be public and reactive, but never directly assignable by consumers (for example totals, derived status flags, validation summaries, or normalized snapshots).

It is a good fit for domain models where you want strict write control and predictable change propagation.

Example

import { InjectionContainer, printValue } from '@rs-x/core';
import {
  rsx,
  RsXExpressionParserModule,
  type IExpression,
} from '@rs-x/expression-parser';
import {
  type IStateChange,
  type IStateManager,
  RsXStateManagerInjectionTokens,
} from '@rs-x/state-manager';

await InjectionContainer.load(RsXExpressionParserModule);

const stateManager: IStateManager = InjectionContainer.get(
  RsXStateManagerInjectionTokens.IStateManager,
);

class MyModel {
  private readonly _aPlusBId = 'aPlusB';
  private _a = 10;
  private _b = 20;