ilokesto

Advanced Semantics

Verified Store behavior for updates, notifications, and implementation boundaries.

Advanced Semantics

@ilokesto/store has fixed behavior for root replacement, bailout checks, notifications, and listener edge cases when you build on top of Store<T>.

For method signatures, start with API Reference. For beginner concepts, read Core Concepts. For adapter boundaries, read Building on Store.

Public contract boundary

The public contract is the behavior consumers can rely on from the documented Store<T> methods: reading state, setting state, and subscribing to changes.

Some sections name implementation facts when those facts produce observable behavior. Application code can rely on the documented behavior, not private fields or exact internal code shape.

Update replacement and ordering

setState replaces the whole state value. It does not merge objects.

const store = new Store({ a: 1, b: 2 });

store.setState({ a: 10 });
// Current state: { a: 10 }

When the next value depends on the previous value, use the updater form so the update is based on the current state at the moment setState runs.

store.setState((prev) => ({ ...prev, b: prev.b + 1 }));

Ordering is synchronous. One setState call runs its update flow before the next line of user code continues. If you call setState twice in a row, the second call sees the state produced by the first successful call.

Equality bailout

The store uses Object.is(prevState, nextState) after resolving the next value. If Object.is returns true, the state is not replaced and subscribers are not notified.

store.setState((prev) => prev);
// No replacement, no notification.

For objects and arrays, returning the same reference is a bailout. To publish a change, create a new reference.

Nested object semantics

Nested values are not tracked. The store compares only the root state value with Object.is.

const state = store.getState();

state.profile.name = "Ada";
// This mutates the object directly. It does not call setState and does not notify subscribers.

Use immutable updates for nested data.

store.setState((prev) => ({
  ...prev,
  profile: {
    ...prev.profile,
    name: "Ada",
  },
}));

Readonly<T> is a TypeScript signal only. Runtime state is not frozen, cloned, or proxied.

Notification timing

Notifications are synchronous. After a successful replacement, subscribers run immediately within the same setState call flow.

Subscribers read the state that was just applied.

store.subscribe(() => {
  console.log(store.getState());
});

store.setState({ count: 1 });
// The subscriber runs before this call returns.

There is no documented batching, async scheduling, timer, promise boundary, or framework render queue inside @ilokesto/store.

Subscriber ordering

Subscribers are stored as listener functions. Notification uses a Set of listeners and a snapshot array.

Observable result: listeners in the snapshot run in subscription insertion order for that notification.

Registering the same function reference more than once does not create multiple entries, because a Set stores a unique reference once.

Unsubscribe during notification

Calling an unsubscribe function during notification is safe.

const unsubscribe = store.subscribe(() => {
  unsubscribe();
});

Listeners are snapshotted with Array.from(this.listeners) before iteration. Removing a listener during notification does not mutate the active notification snapshot.

Public adapters can depend on safe unsubscribe behavior, not private field names or the exact snapshot implementation.

Re-entrant updates

A subscriber can call setState while a notification is running. That starts another synchronous update flow immediately.

store.subscribe(() => {
  const { count } = store.getState();

  if (count < 3) {
    store.setState({ count: count + 1 });
  }
});

Use guards when doing this. Without a condition that eventually stops, re-entrant updates can create an infinite loop.

Error behavior

Subscriber errors are not caught by the store contract. If a subscriber throws, expect the error to escape the current setState call.

Keep subscribers small, predictable, and guarded by your own error handling when failures should not interrupt the caller.

Function-valued root state

setState treats a function argument as an updater. A function root value cannot be set directly through setState because it will be called with the previous state.

Wrap function values in an object when you need to store them.

store.setState({ handler: () => console.log("run") });

Implementation notes

These observable facts are useful for debugging. Application code should depend on the public behavior above, not private code shape.

  • State application resolves a value or updater result, checks Object.is, replaces state, then notifies.
  • Listener notification snapshots with Array.from(this.listeners).
  • Readonly<T> is type-only. Runtime state is not frozen, cloned, or proxied.
  • Middleware exists through source-backed methods, but the main Store contract is the documented public methods. Check API Reference before using middleware in published examples.

Summary

  • Updates replace the root state value.
  • Object.is controls bailout behavior.
  • Notifications are synchronous after successful replacement.
  • Nested mutation is invisible unless you call setState with a new root reference.
  • Listener snapshotting makes unsubscribe during notification safe.
  • Public docs describe observable behavior, not private implementation stability.

On this page