Launch Week Day 2: Introducing Reports: An Extensible Reporting Framework for Socket Data.Learn More
Socket
Book a DemoSign in
Socket

@dvtng/cosmos

Package Overview
Dependencies
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@dvtng/cosmos

The _universal_ state management for React. Cosmos is a unified framework for data-fetching and UI state.

latest
npmnpm
Version
0.0.44
Version published
Maintainers
1
Created
Source

Cosmos

The universal state management for React. Cosmos is a unified framework for data-fetching and UI state.

Cosmos organizes state into models — named, parameterized units of state with composable behaviors like async data fetching, computed derivations, lifecycle hooks, and persistence.

npm install @dvtng/cosmos

Core concepts

Models

A model is a named, reusable definition for a unit of reactive state. Models are defined with model() and instantiated by calling the resulting function, which produces a spec — a description of a particular model instance identified by its name and arguments.

import { model } from "@dvtng/cosmos";

// A simple model with no arguments
const Time = model("Time", () => ({
  value: new Date(),
  onStart({ set }) {
    const interval = setInterval(() => {
      set((draft) => {
        draft.value = new Date();
      });
    }, 1000);
    return () => clearInterval(interval);
  },
}));

// A parameterized model
const Counter = model(
  { name: "Counter", args: (id: number) => [{ id }] },
  () => ({
    value: 0,
    forget: true,
    onStart({ set }) {
      const interval = setInterval(() => {
        set((draft) => {
          draft.value++;
        });
      }, 1000);
      return () => clearInterval(interval);
    },
  }),
);

Models are lazy — their state is only created when first accessed (via useModel, getModel, or compute). When all subscribers are removed, the model's onStart cleanup runs after a short keep-alive period, and if forget is set, the state is eventually deleted entirely.

Behaviors and Traits

A model's resolve function returns a behavior — an object describing the model's initial value and lifecycle hooks:

type State<T> = { value: T; updatedAt: number };
type SetState<T> = (recipe: (draft: State<T>) => void) => void;

type Meta = { name: string; args: unknown[] };

type HookContext<T> = {
  get: () => State<T>; // Latest state (read after updates)
  set: SetState<T>; // Immer-style update, same as setModel
  meta: Meta;
};

type Behavior<T> = {
  value: T; // Initial value (required)
  forget?: Duration | true; // How long to retain state after all subscribers leave
  onLoad?: (context: HookContext<T>) => void; // Called once when state is first created
  onStart?: (context: HookContext<T>) => (() => void) | void; // Called when first subscriber arrives
  onWrite?: (context: HookContext<T>) => void; // Called on every state change
  onDelete?: (context: HookContext<T>) => void; // Called when state is deleted
};

context.set applies an Immer recipe to the model's State<T> (value and updatedAt), then notifies subscribers — the same mechanism as setModel.

A trait is a behavior without a value — it adds lifecycle hooks without defining the initial value. Traits are used by helpers like persist().

The resolve function can return either a single behavior object, or an array containing one behavior and any number of traits. When an array is returned, the behaviors and traits are merged: lifecycle hooks are composed (all run), the value is taken from the behavior, and forget uses the first defined value.

const AppState = model("AppState", () => {
  return [
    value({ nextCounterId: 0, counters: [] as number[] }),
    persist("AppState"),
  ];
});

Snapshots and the Later type

Models that use request() or compute() with async sources produce values of type Later<T>, which is T | Loading | Error. When you access a model via useModel or compute, you get a snapshot with two ways to read it:

  • snapshot.value — Returns the resolved value directly. If the model is still loading, it throws a promise (for React Suspense). If it errored, it throws the error.
  • snapshot.map({ value, loading, error }) — Pattern-matches on the state, letting you handle each case explicitly:
coinPrice.map({
  loading: () => <div>Loading...</div>,
  error: (error) => <div>{error.message}</div>,
  value: (price) => <span>${price}</span>,
});

Lifecycle

  • Creation — When a model is first accessed, resolve() runs to produce the behavior. The state is initialized with behavior.value. onLoad is called.
  • Start — When the first subscriber arrives, onStart receives a HookContext (get, set, meta). It may return a cleanup function.
  • Active — While there are subscribers, the model is alive. onWrite fires on every state mutation.
  • Stop — When the last subscriber leaves, after a 1-second keep-alive delay, the cleanup from onStart runs.
  • Forget — If forget is set, after the stop cleanup runs, the state is deleted after the specified duration. onDelete is called.

If a new subscriber arrives during the keep-alive or forget window, the timers are cancelled and the model stays alive.

API reference

model(name, resolve) / model(identity, resolve)

Defines a model. Returns a function that produces a spec when called.

Signatures:

function model<A extends any[], V>(
  resolve: (...args: A) => Behavior<V> | Traits<V>,
): Model<A, V>;

function model<A extends any[], V>(
  identity: string | { name: string; args: (...args: A) => unknown[] },
  resolve: (...args: A) => Behavior<V> | Traits<V>,
): Model<A, V>;

Parameters:

  • identity — A string name, or an object with name and an args function that maps arguments to a serializable identity array. If omitted, an auto-generated name is used.
  • resolve — A function that receives the model's arguments and returns a behavior (or array of behavior + traits).

The args function controls how model instances are identified. Two calls that produce the same serialized args share the same state:

const Counter = model(
  { name: "Counter", args: (id: number) => [{ id }] },
  () => ({ value: 0 }),
);
Counter(1); // same instance as another Counter(1)

useModel(spec)

React hook that subscribes a component to a model instance. Returns a snapshot.

function useModel<T>(spec: Spec<T>): Snapshot<T>;

The component re-renders when the model's state changes. If the model's value is Later<T> and is in a loading state, accessing snapshot.value throws a promise (triggering React Suspense). If it errored, accessing snapshot.value throws the error.

function Clock() {
  const time = useModel(Time());
  return <div>{time.value.toLocaleTimeString()}</div>;
}

getModel(spec)

Accesses a model's state outside of React. Returns the state object with .value and .updatedAt, plus .then/.catch/.finally methods for awaiting async models.

function getModel<T>(spec: Spec<T>): GetModelResult<T>;

Useful when you need the current state or async resolution outside React. To update state from event handlers or other imperative code, use setModel:

setModel(AppState(), (draft) => {
  draft.value.counters.push(draft.value.nextCounterId++);
});

For async models, you can await the result:

const { value } = await getModel(CoinPrice("btc-bitcoin"));

setModel(spec, recipe)

Sets a model's state from outside React using an Immer recipe (same as context.set in lifecycle hooks).

function setModel<T>(spec: Spec<T>, recipe: (draft: State<T>) => void): void;
setModel(Counter(1), (draft) => {
  draft.value = 42;
});

value(initialValue)

Creates a simple behavior with just an initial value and no lifecycle hooks.

function value<T>(value: T): Behavior<T>;
const Settings = model("Settings", () => value({ theme: "dark" }));

compute(fn, options?)

Creates a behavior whose value is derived from other models. The value automatically updates when any dependency changes.

function compute<T>(
  fn: (get: GetSnapshot) => Later<T>,
  options?: { defaultValue?: T },
): Behavior<Later<T>>;

Parameters:

  • fn — A function that receives a get helper. Call get(spec) to read another model's snapshot and subscribe to its changes.
  • options.defaultValue — If provided, loading and error states from dependencies are replaced with this value, and the return type narrows to Behavior<T>.

Dependencies are tracked dynamically — if the set of models you get() changes between runs, subscriptions are updated accordingly.

const CounterTotal = model("CounterTotal", () => {
  return compute((get) => {
    return get(AppState()).value.counters.reduce((acc, id) => {
      return acc + get(Counter(id)).value;
    }, 0);
  });
});

request(fn, options?)

Creates a behavior for async data fetching. The value is Later<T> — initially loading, then either the resolved value or an error.

function request<T>(
  fn: () => Promise<T> | T,
  options?: RequestOptions,
): Behavior<Later<T>>;

RequestOptions:

OptionTypeDescription
refreshDurationAutomatically re-fetch on an interval after the last update.
refreshOnFocusbooleanRe-fetch when the window regains focus.
const CoinPrice = model("CoinPrice", (coinId: string) => {
  return [
    request(() => fetchCoinPrice(coinId), { refresh: { seconds: 10 } }),
    persist("CoinPrice"),
  ];
});

When used with useModel, accessing snapshot.value integrates with React Suspense (throws a promise while loading). Use snapshot.map() for explicit handling of all states:

function CoinPriceView({ coinId }: { coinId: string }) {
  const coinPrice = useModel(CoinPrice(coinId));

  return coinPrice.map({
    loading: () => <div>Loading...</div>,
    error: (error) => <div>{error.message}</div>,
    value: (price) => <span>${price.toFixed(2)}</span>,
  });
}

persist(key, options?)

Creates a trait that persists a model's state to storage. Restores state on load and saves on every change. Errors are not persisted.

function persist<T>(key: string, options?: PersistOptions<T>): Trait<T>;

PersistOptions:

OptionTypeDefaultDescription
serialize(state: State<T>) => stringJSON.stringifyCustom serialization.
parse(serialized: string) => State<T>JSON.parseCustom deserialization.
storageStoragelocalStorageStorage backend (must implement the Storage interface).

The storage key format is cosmos:{key}:{serializedArgs}.

const AppState = model("AppState", () => [
  value({ count: 0 }),
  persist("AppState"),
]);

isReady(snapshot)

Returns true if the snapshot has a resolved value (not loading or errored).

function isReady<T>(snapshot: Snapshot<T>): boolean;

Duration

An object specifying a time duration, used by request({ refresh }) and Behavior.forget:

type Duration = Partial<{
  ms: number;
  seconds: number;
  minutes: number;
  hours: number;
  days: number;
}>;

Multiple fields are additive: { minutes: 1, seconds: 30 } = 90 seconds.

Setting forget: true is equivalent to forget: { ms: 0 } — state is deleted immediately after the stop timer.

ModelArgs<T> / ModelValue<T>

Utility types for extracting the argument tuple and value type from a model type.

type ModelArgs<T extends Model<any, any>>  // => argument tuple
type ModelValue<T extends Model<any, any>> // => value type

FAQs

Package last updated on 15 Apr 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts