API Reference

IIndexWatchRule

IIndexWatchRule is the gatekeeper for recursive observation. It decides which nested member/index updates are turned into reactive change events.

What It Controls

The rule is evaluated with test(index, target) whenever rs-x decides if a nested value should remain observed. Returning true keeps recursive observation active for that member path.

Without a watch rule, rs-x still tracks root assignments and collection membership mutations. But nested member/property changes under leaf values are only tracked when the rule allows them.

Where To Pass The Rule

Use it either at expression binding time (leafIndexWatchRule) or directly in state manager (watchState(..., { indexWatchRule })).

rsx usage
import { rsx } from '@rs-x/expression-parser';
import type { IIndexWatchRule } from '@rs-x/state-manager';

const watchRule: IIndexWatchRule = {
  context: { allow: new Set(['a', 'b']) },
  test(index) {
    return this.context.allow.has(String(index));
  },
};

const model = { a: 1, b: 2, c: 3 };
const expression = rsx<number>('a + b')(model, watchRule);
state manager usage
import type { IStateManager } from '@rs-x/state-manager';

const watchRule = {
  context: { recursive: true },
  test(_index, _target) {
    return this.context.recursive;
  },
};

stateManager.watchState(model, 'user', { indexWatchRule: watchRule });

Parameters

contextunknown

User-defined data bag used by test(...) to hold rule config.

indexunknown

Current member/index candidate under evaluation.

targetunknown

Object/collection that owns the current index/member.

Return Type

test(...) returns boolean.

Return true to include a member in recursive observation; return false to skip it.

Index / Target Semantics By Type

The meaning of index depends on runtime type:

ArrayNumeric slot indexThe array instanceArray.isArray(target) && index === 0
DateDate part key ('year', 'month', 'time', ...)The Date instancetarget === model.date && index === 'hours'
MapMap keyThe map instancetarget instanceof Map && index === 'admin'
Plain objectProperty key (string/symbol)The object that owns the propertytarget === model.user && index === 'profile'
SetMember value itselfThe set instancetarget instanceof Set && trackedMembers.has(index)

Practical Pattern

The usual rule shape is:

  • Allow the leaf container itself (for example model.items) so recursive observation can be installed.
  • Allow specific collection members/keys or nested object properties.
  • Keep rule logic deterministic and side-effect free.
Watch everything under the leaf
import { rsx } from '@rs-x/expression-parser';
import { watchIndexRecursiveRule } from '@rs-x/state-manager';

const model = { a: { b: { c: 1 } } };
const expression = rsx('a.b')(model, watchIndexRecursiveRule);

Plain Object Example

Tracks only user.profile.name while ignoring user.profile.role.

const rsx = api.rsx;

const model = {
  user: {
    profile: { name: 'Ada', role: 'engineer' },
  },
};

const watchRule = {
  context: { trackedLeafProperties: new Set(['name']) },
  test(index, target) {
    if (target === model && index === 'user') {
      return true;
    }

    if (target === model.user && index === 'profile') {
      return true;
    }

    if (target === model.user.profile) {
      return this.context.trackedLeafProperties.has(String(index));
    }

Date Example

Tracks schedule.start hours/minutes updates and ignores seconds.

const rsx = api.rsx;

const model = {
  schedule: {
    start: new Date(2026, 0, 1, 9, 30, 0),
  },
};

const watchRule = {
  context: { trackedDateParts: new Set(['hours', 'minutes']) },
  test(index, target) {
    if (target === model && index === 'schedule') {
      return true;
    }

    if (target === model.schedule && index === 'start') {
      return true;
    }

    if (target === model.schedule.start) {
      return this.context.trackedDateParts.has(String(index));
    }

Array Example

Tracks array slot mutations and qty changes on each item, but not note.

const rsx = api.rsx;

const model = {
  items: [
    { label: 'A', qty: 1, note: 'keep' },
    { label: 'B', qty: 2, note: 'keep' },
  ],
};

const watchRule = {
  context: { trackedItemProperty: 'qty' },
  test(index, target) {
    if (target === model && index === 'items') {
      return true;
    }

    if (Array.isArray(target)) {
      return true; // watch each array slot
    }

    return String(index) === this.context.trackedItemProperty;
  },

Map Example

Tracks only the admin key branch and enabled property changes.

const rsx = api.rsx;
const WaitForEvent = api.WaitForEvent;
const emptyFunction = () => {};

const admin = { enabled: true, rank: 1 };
const guest = { enabled: false, rank: 2 };

const model = {
  roles: new Map([
    ['admin', admin],
    ['guest', guest],
  ]),
};

const watchRule = {
  context: { trackedKeys: new Set(['admin']) },
  test(index, target) {
    if (target === model && index === 'roles') {
      return true;
    }

    if (target instanceof Map) {

Set Example

Tracks a set member path by default (non-recursive): nested done is ignored, membership changes are tracked.

const rsx = api.rsx;
const WaitForEvent = api.WaitForEvent;
const emptyFunction = () => {};

const taskA = { id: 'A', done: false, note: 'leaf object' };
const taskB = { id: 'B', done: false, note: 'other object' };

const model = {
  trackedTask: taskA,
  tasks: new Set([taskA, taskB]),
};

// Default: non-recursive leaf watching
const trackedTaskExpression = rsx('tasks[trackedTask]')(model);
  await new WaitForEvent(trackedTaskExpression, 'changed').wait(emptyFunction);

  const nestedChange = await new WaitForEvent(trackedTaskExpression, 'changed', {
    ignoreInitialValue: true,
    timeout: 100,
  }).wait(() => {
    taskA.done = true;
  });