npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chocbite/ts-lib-state

v2.2.0

Published

State management library for TypeScript

Readme

@chocbite/ts-lib-state

A type-safe state management library for TypeScript with support for synchronous/asynchronous, read-only/writable, and result-based states.

Installation

npm install @chocbite/ts-lib-state

Overview

The library classifies states along three axes:

  • Sync — Synchronous (ROS, RES) or Asynchronous (ROA, REA)
  • Error — Always OK (ROS, ROA) or Error-capable (RES, REA)
  • Write — Read-only or Writable (ROSW, ROAW, RESW, REAW)

This gives eight state types that cover every combination:

| Type | Sync | OK-only | Writable | | ------ | ---- | ------- | -------- | | ROS | ✓ | ✓ | | | ROSW | ✓ | ✓ | ✓ | | RES | ✓ | | | | RESW | ✓ | | ✓ | | ROA | | ✓ | | | ROAW | | ✓ | ✓ | | REA | | | | | REAW | | | ✓ |

Quick Start

import state from "@chocbite/ts-lib-state";
import { ok } from "@chocbite/ts-lib-result";

// Create a writable, sync, ok-only state
const counter = state.ok_w(0);

// Subscribe to changes (true = call immediately with current value)
counter.sub((result) => {
  console.log("Count:", result.value);
}, true);

// Owner change state value
counter.set(ok(1));

// User request change to state value
counter.write(ok(2));

// Read the current value synchronously
console.log(counter.ok()); // 2
console.log(counter.get()); // ResultOk<2>

// Read the current value asynchronously
console.log(await counter); // ResultOk<2>

State Methods

Every state has a common set of methods. The specific state type determines which additional methods are available and how return types are narrowed — all enforced at compile time by TypeScript.

Common Methods

Available on every state type:

| Method | Description | | -------------------------------- | ---------------------------------------------------------------------------------------------------------- | | await state / state.then(fn) | Get the current value. Returns ResultOk<T> on ok-only types, Result<T, string> on error-capable types. | | sub(fn, immediate?) | Subscribe to value changes. Pass true to also call fn immediately with the current value. | | unsub(fn) | Remove a subscription. | | related() | Returns helper metadata (e.g., number min/max, string max_length). | | in_use() | Returns the state if it has any subscribers, undefined otherwise. | | has(fn) | Returns the state if fn is a current subscriber. | | amount() | Returns the subscriber count. |

Narrowed Methods

These methods are only guaranteed on certain state types. TypeScript will error if you call them on the wrong type:

| Method | Available when | Description | | -------------- | ---------------------------- | ------------------------------------------------ | | get() | Sync (rsync: true) | Synchronously read the current Result value. | | ok() | Sync + OK-only (rok: true) | Synchronously read the unwrapped value directly. | | write(value) | Writable (writable: true) | Request a value change for the state. | | limit(value) | Writable | Clamp a value to the valid range via the helper. | | check(value) | Writable | Validate a value without applying it. |

Per-Type Narrowing

Each of the eight state types narrows the available methods and result types:

| Type | get() | ok() | write()/ limit()/ check() | then / await result | | ----------- | ------------- | ------ | --------------------------- | ----------------------- | | StateROS | ResultOk<T> | T | — | ResultOk<T> | | StateROSW | ResultOk<T> | T | ✓ | ResultOk<T> | | StateRES | Result<T> | — | — | Result<T> | | StateRESW | Result<T> | — | ✓ | Result<T> | | StateROA | — | — | — | ResultOk<T> | | StateROAW | — | — | ✓ | ResultOk<T> | | StateREA | — | — | — | Result<T> | | StateREAW | — | — | ✓ | Result<T> |

The three boolean properties rsync, rok, and writable act as runtime discriminants matching the same axes:

  • Sync (rsync: true) — enables get(), and ok() when also ok-only
  • OK-only (rok: true) — narrows then result to ResultOk<T> (value is never an error)
  • Writable (writable: true) — enables write(), limit(), and check()

The types form a subtype hierarchy — more specific types are assignable to less specific ones. For example, a StateROSW<number> (sync + ok + writable) can be passed anywhere a StateROS<number>, StateROA<number>, or State<number> is expected.

function show_sync(s: StateROS<number>) {
  // Sync + OK → get() and ok() available
  console.log(s.ok()); // number — unwrapped value
  console.log(s.get().value); // number — via ResultOk
}

async function show_async(s: StateROA<number>) {
  // Async + OK → no get()/ok(), must await
  const result = await s; // ResultOk<number>
  console.log(result.value); // always ok, no error check needed
}

function handle_errors(s: StateRES<number>) {
  // Sync + Error-capable → get() returns Result<T, string>
  const result = s.get();
  if (result.ok) console.log(result.value);
  else console.log(result.error);
}

async function update(s: StateROSW<number>) {
  // Writable → write(), limit(), check() available
  s.write(42);
  // This will only be correct if the state accepts the write
  console.log(s.ok()); // 42
  // If the write is denied it should return a Result<void,string> with the reason for denying the write as a string
  console.log(await s.write(42));
}

Each state type can be narrowed down to a less restricted state type with StateROS being the least restricted and State/StateREA being the most restricted.

async function narrowing(s: State<number>) {
  if (s.ok) {
    //State -> StateROXX
    if (s.sync) {
      //StateROXX -> StateROSX
      if (s.writable) {
        //StateROSX -> StateROSW
      }
    }
  }
}

Creating States

Shorthand state generators

state.ok(value); // ROS  — read-only, sync, always ok
state.ok_w(value); // ROSW — writable, sync, always ok
state.from(value); // RES  — read-only, sync, error-capable
state.from_w(value); // RESW — writable, sync, error-capable
state.err(message); // RES  — read-only, sync, starts with error
state.err_w(message); // RESW — writable, sync, starts with error

Local States

For full control over initial values, use the lower-level local factories. Each accepts a Result value:

import { ok, err } from "@chocbite/ts-lib-result";

const a = state.ros(ok(42)); // ROS
const b = state.rosw(ok(42)); // ROSW
const c = state.res(ok(42)); // RES
const d = state.resw(err("n/a")); // RESW

State generators also accept a function which will be lazily executed at first access of state value:

import { ok } from "@chocbite/ts-lib-result";
const f = state.ros(() => ok(42)); // ROS

Async local states accept an async function, which is lazily executed at first state access:

const e = state.roa(async () => ok(await fetchData()));
const f = state.roaw(async () => ok(await fetchData()));

Owner

Every local generator returns an owner — the full state object that includes both the public state interface and privileged methods for updating it. The owner is what you keep internally; you hand out state.read_only or state.read_write to consumers.

const counter = state.rosw(ok(0));

// Owner methods
counter.set(ok(5)); // set the raw Result value
counter.set_ok(5); // shorthand: wraps the value in ok() for you
counter.set_err("fail"); // set an error (only on error-capable states: RES/REA)

// The owner can get basic state types without access to the owner methods for passing on, or just set them to a variable with appropriate typeing
const read_only = counter.read_only; //Returns as StateROS<number>
const read_only_type: StateROS<number> = counter;
const read_write = counter.read_write; // Returns as StateROSW<number>
const read_write_type: StateROSW<number> = counter;
const state = counter.state; // Returns as State<number>
const state_type: State<number> = counter.state;

set and set_ok bypass the setter/validation pipeline — they directly replace the state value and notify subscribers. In contrast, .write() is the public API that goes through the setter function (which may apply helpers like limit).

You can also provide a custom setter to control how writes are applied:

const limited = state.rosw(ok(0), async (value, owner, old) => {
  // Custom write logic
  if (Number.isNaN(value)) return err("NaN is not allowed");
  owner.set_ok(Math.min(100, Math.max(0, value)));
  return ok(undefined);
});

Subscribing to State

// Subscribe — callback receives a Result<T, string>
const unsub = counter.sub((result) => {
  if (result.ok) {
    console.log(result.value);
  }
});

// Unsubscribe
counter.unsub(unsub);

// Subscribe with immediate invocation
counter.sub(callback, true);

// Promise-style using then executes immediately
counter.then((result) => {
  console.log(result.value);
});
// Promise-style using await syntax executes in microloop
console.log((await counter).value);

Collected States

Combine multiple states into a single derived state: Combination function is evaluated as a promise/microtask, so if you change first_name and last_name in the same cycle, it will only be called once.

const first_name = state.ok("Alice");
const last_name = state.ok("Smith");

const full_name = state.c.ros(
  (vals) => ok(`${vals[0].value} ${vals[1].value}`),
  first_name,
  last_name,
);
// full_name.ok() === "Alice Smith"

Available variants: state.c.ros, state.c.roa, state.c.res, state.c.rea.

Proxy States

Wrap a state with read/write transformation functions or proxy its value:

const source = state.ok_w(5);

// Read-only proxy that doubles the value
const doubled = state.p.ros(source, (val) => ok(val.value * 2));
// doubled.ok() === 10

// Writable proxy
// A proxy can also be writable, but this requires two transform functions to convert both ways, this is to support the write helper methods limit and check
const offset = state.p.rosw(
  source,
  (val) => ok(val.value + 10), // read: add 10
  {
    wout_win: (val) => ok(val.value - 10), // write→inner: subtract 10
    win_wout: (val) => ok(val.value + 10), // inner→write: add 10
  },
);

Remote States

Remote states represent asynchronous resources that are fetched on demand or streamed via a subscription. They are always async (ROA/REA/ROAW/REAW).

Each remote generator (state.r.*.from) takes three callback functions and an optional timing configuration:

  • once — called when the state value is awaited (one-shot fetch). Call owner.update_single(value) to deliver the result.
  • setup — called when the first subscriber arrives. Use this to open a connection or start polling. Call owner.update_value(value) to push new values.
  • teardown — called when the last subscriber leaves. Clean up connections here.
let ws;
const user_data = state.r.roa.from<User>(
  // once: one-shot fetch when awaited
  async (owner) => {
    const data = await fetch("/api/user").then((r) => r.json());
    owner.update_single(ok(data));
  },
  // setup: called on first subscription
  (owner) => {
    const ws = new WebSocket("/api/user_subscribe");
    ws.onmessage = (e) => owner.update_value(ok(JSON.parse(e.data)));
  },
  // teardown: called when all subscribers leave
  () => {
    ws.close();
  },
);

Timing options control debounce, validity caching, and retention:

state.r.rea.from(once, setup, teardown, {
  timeout: 1000, // ms before a generic error if no response
  debounce: 50, // ms to batch multiple awaits into one fetch
  validity: 5000, // ms the buffered value stays fresh (or true = until unsubscribe)
  retention: 200, // ms to keep the connection after last unsubscribe
});

Writable remote variants (state.r.roaw, state.r.reaw) additionally accept a write_action callback:

const setting = state.r.roaw.from<number>(
  once,
  setup,
  teardown,
  async (value, owner) => {
    await fetch("/api/setting", {
      method: "POST",
      body: JSON.stringify(value),
    });
    return ok(undefined);
  },
  { write_debounce: 300 }, // ms to wait before using the last value written
);

Available variants: state.r.roa, state.r.rea, state.r.roaw, state.r.reaw.

Helpers

Array

States holding arrays get built-in mutation helpers:

const list = state.rosw(ok([1, 2, 3]));

list.array.push(4); // [1, 2, 3, 4]
list.array.unshift(0); // [0, 1, 2, 3, 4]
list.array.pop(); // [0, 1, 2, 3]
list.array.shift(); // [1, 2, 3]
list.array.insert(1, [99]); // [1, 99, 2, 3]
list.array.remove(0, 1); // [99, 2, 3]
list.array.change(0, 50); // [50, 2, 3]

Each mutation is tracked with metadata (added, removed, changed, fresh).

Object

States holding objects get field-level mutation helpers:

const obj = state.rosw(ok({ a: 1, b: 2 }));

obj.object.set({ a: 10 }); // { a: 10, b: 2 }
obj.object.remove("b"); // { a: 10 }

Each mutation is tracked with metadata (added, removed, changed, fresh).

Number

const temperature = state.rosw(
  state.n.help(ok(20), {
    min: state.ok(0),
    max: state.ok(100),
    step: state.ok(0.5),
  }),
  true,
);

await temperature.write(150); // limited to 100 by the helper
await temperature.check(150); // returns err("150 is bigger than the limit of 100")
temperature.related().unwrap().min; // state holding 0

Options: min, max, step, start, decimals, unit — each is itself a State, so limits can be reactive.

String

const name = state.rosw(
  state.s.help(ok("hello"), {
    max_length: state.ok(10),
    max_bytes: state.ok(10),
  }),
  true,
);

await name.write("this is too long"); // limited to 10 characters by the helper
await name.check("this is too long"); // returns err("the text is longer than the limit of 10 characters")
await name.write("this is ææ"); // limited to 10 bytes by the helper
await name.check("this is ææ"); // returns err("the text is longer than the limit of 10 bytes")

Options: max_length, max_length_bytes.

Enum

export const Color = {
  Red: "red",
  Green: "green",
  Blue: "blue",
} as const;
export type Color = (typeof Color)[keyof typeof Color];

const list = state.e.list<Color>({
  [Color.Red]: { name: "Red" },
  [Color.Green]: {
    name: "Green",
    description: "The color green, what more is needed",
  },
  [Color.Blue]: {
    name: "Blue",
    description: "I'm blue dabudedabuda",
    icon: () => blue_icon.cloneNode(),
  },
});

const color = state.rosw(
  state.e.help(ok(Color.Red as Color), { list: state.ok(list) }),
  true,
);

Boolean

const flag = state.rosw(state.b.help(ok(true)), true);

Type Guards

Check state types at runtime:

state.is.state(obj); // Is any state?
state.is.ros(obj); // Is ROS?
state.is.rosw(obj); // Is ROSW?
state.is.res(obj); // Is RES?
// ... and so on for all eight types

Utilities

// Await the first emitted value from any state
const value = await state.u.await_value(myState);

// Compare any two state values for equality, returns a promise
const equal = state.u.compare(stateA, stateB);

// Compare two states sync state values
const equal = state.u.compare_sync(stateA, stateB);

License

MIT