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

@reforgium/statum

v3.1.4

Published

Signals-first API state and query stores for Angular

Readme

@reforgium/statum

npm version License: MIT

Signals-first query and data stores for Angular (18+).

@reforgium/statum provides API-oriented stores, cache strategies, and a serialization layer for Angular applications that talk to HTTP backends.

Designed for request orchestration, pagination, dictionaries, and entity state. It is not a reduced framework and does not try to replace NgRx-style global event modeling.


Features

  • Signals-first API (signal() everywhere)
  • API-driven stores (resource / paginated / dictionaries)
  • Normalized entity store (EntityStore)
  • Built-in cache strategies (TTL, prefix grouping)
  • Deterministic request lifecycle: loading / error / data
  • Optional transport-level: dedupe, debounce/throttle, abort
  • Unified retry and trace hooks in ResourceStore
  • Explicit serialization / deserialization boundary

Best Fit

Use statum when you need:

  • HTTP-first application state for CRUD-heavy Angular apps
  • Predictable request behavior (dedupe, abort, retry, latest-wins)
  • Reusable pagination and dictionary flows for tables and forms
  • Lightweight entity normalization without a full reducer architecture

statum is usually a good fit between:

  • plain HttpClient services that have started to accumulate state logic
  • full application state frameworks where global reducers/effects are too expensive for the problem

statum is usually not the right fit when:

  • you need cross-page event sourcing, time-travel tooling, or centralized action logs
  • your app is mostly local UI state with little backend orchestration
  • you want a framework-agnostic data layer

Installation

npm install @reforgium/statum

Configuration

import { provideStatum } from '@reforgium/statum';

providers: [
  provideStatum({
    pagedQuery: {
      defaultBaseUrl: '/api',
      defaultMethod: 'GET',
      defaultHasCache: true,
      defaultCacheSize: 10,
    },
  }),
];

API Stability

statum v3 narrows its public API around explicit store methods and object-style contracts.

Stability policy for the documented public surface:

  • Existing documented methods are treated as stable within 3.x
  • Signature expansions should stay additive and backward-compatible
  • Behavioral changes should be reflected in CHANGELOG.md and, when needed, MIGRATION.md
  • Removed or renamed APIs should go through a documented migration path first

The safest long-term entry points are:

  • package exports from @reforgium/statum
  • documented store methods in this README
  • provider helpers such as provideStatum(...)

Package Structure

  • Cache - storage strategies for caching
  • Stores - reusable state containers over HttpClient
  • Serializer - configurable data transformation utility

Low-level serializer and storage primitives are shared through hidden @reforgium/internal. statum remains the user-facing package; you do not need to install or reason about @reforgium/internal directly in normal usage.

Behavioral Guarantees

The core stores are designed around explicit, testable runtime guarantees.

ResourceStore guarantees:

  • Identical in-flight requests can be deduplicated into one transport call
  • abort(...) / abortAll(...) cancel active or scheduled requests and clear inflight dedupe references
  • Retry policy can be set globally and overridden per request
  • Trace hooks receive cache/request lifecycle events for observability

PagedQueryStore guarantees:

  • fetch(...) always starts from page 0 and clears page cache first
  • updatePage(...) can read from cache unless ignoreCache is enabled
  • updatePageSize(...) resets cache before loading the first page with the new size
  • updateByOffset(...) maps table offsets into normalized page + size requests
  • latest-wins keeps loading stable during abort/restart request bursts and ignores stale async parse results
  • parallel allows overlapping page requests when the source intentionally supports concurrent fetches

DictStore guarantees:

  • fixed: true performs local search over the loaded cache after initial fill
  • fixed: false delegates search to the server with debounce support
  • Visible options stay normalized to { label, value }

Performance notes:

  • ResourceStore, PagedQueryStore, and DictStore include stress/perf coverage in the test suite
  • The goal of these tests is behavioral regression detection and upper-bound sanity checks, not synthetic benchmark marketing
  • For production evaluation, prefer measuring with your own API latency, payload size, and change detection profile
  • Run npm run bench:statum for a current local timing snapshot
  • Run npm run bench:statum:browser and open /test-routing/statum-bench for browser-side render measurements
  • See PERF.md for the latest checked-in baseline captured in this repository

Reproducible examples are covered in tests:

  • dedupe identical requests - many callers hitting the same ResourceStore.get(...) with dedupe: true still produce one transport request
  • cache hit/miss semantics - cache-only fails on a cold key, then cache-first serves the warmed key without network
  • latest-wins abort behavior - PagedQueryStore.updatePage(...) bursts cancel older in-flight requests and only the newest page is applied
  • pagination cache eviction - when the page cache is full, revisiting an evicted page performs a fresh request
  • warm cache replay - replaying the hot cache window stays network-free across the full cached range

These examples are intentionally implemented as specs, not ad-hoc benchmark scripts, so they can be rerun in CI and used as regression checks.

The checked-in benchmark baseline in PERF.md also shows a practical scaling claim for ResourceStore:

  • identical request fan-in collapses from N transport calls to 1 when dedupe: true
  • in the current local snapshot, a 10,000 caller burst is roughly an order of magnitude faster with dedupe enabled
  • the same 10,000 caller burst reduces memory pressure from tens of MB to low single-digit MB in the deduped path

Those numbers are machine-specific and should be treated as a local envelope, not a universal SLA.


Cache

storageStrategy

storageStrategy<T>(kind) is a factory that returns a cache storage implementing StorageInterface<T>

Available strategies:

  • persist - browser localStorage
  • session - browser sessionStorage
  • memory - in-memory storage
  • lru - size-limited in-memory cache

StorageInterface

All cache strategies implement the same contract.

interface StorageInterface<T> {
  prefix?: string;
  get(key: string): T | null;
  set(key: string, value: T): void;
  remove(key: string): void;
  clear(): void;
  get length(): number;
}

Example:

import { storageStrategy } from '@reforgium/statum';

type User = { id: number; name: string };

const storage = storageStrategy<User>('persist');

storage.set('user:1', { id: 1, name: 'John' });

const user = storage.get('user:1'); // User | null

ResourceStore

Transport-level store over HttpClient with cache strategies, deduplication, abort, and rate-limiting.

When to use

  • CRUD and "resource" endpoints
  • Centralized debounce/throttle
  • Abort in-flight / delayed requests (latest-wins)
  • Dedupe identical calls to one HTTP request
  • Use as a transport engine for other stores (promote: false)

Signals

| Signal | Type | Description | |---------|---------------------------|-------------------------| | value | Signal<T \| null> | Last successful value | | status | Signal<ResourceStatus> | Resource status | | error | Signal<unknown \| null> | Last error | | loading | Signal<boolean> | Request in progress |

Methods

| Method | Description | |----------|--------------------------| | get | GET request | | post | POST request | | put | PUT request | | patch | PATCH request | | delete | DELETE request | | abort | Abort a specific request | | abortAll | Abort all requests |

observe

By default, parseResponse (and the resolved Promise value) receive the response body. Pass observe: 'response' to receive the full HttpResponse instead — useful for reading response headers or status codes inside parseResponse.

const result = await store.get(
  { params: { id: '1' }, query: {} },
  {
    observe: 'response',
    parseResponse: (res) => ({
      data: res.body,
      etag: res.headers.get('ETag'),
    }),
  },
);

onResponse (store-level hook) always fires with the full HttpResponse regardless of observe, and is intended for side-effects only (logging, header extraction).

Cache hits bypass the network entirely. When observe: 'response' is used with strategy: 'cache-first' or 'cache-only' and a cached value exists, parseResponse is not called — the cached result is returned as-is.

Example:

import { ResourceStore } from '@reforgium/statum';

type User = { id: number; name: string };

const userStore = new ResourceStore<User>(
  { GET: '/users/:id' },
  { baseUrl: '/api', ttlMs: 60_000 }
);

const user = await userStore.get(
  { params: { id: '42' }, query: {} },
  { dedupe: true }
);

Retry + trace example:

import { ResourceStore } from '@reforgium/statum';

const store = new ResourceStore<{ id: number; name: string }>(
  { GET: '/users/:id' },
  {
    baseUrl: '/api',
    retry: { attempts: 2, delayMs: 150, backoff: 'exponential' },
    onTrace: (event) => console.debug('[resource-trace]', event),
  }
);

await store.get(
  { params: { id: '42' } },
  { retry: { attempts: 1 } } // per-call override
);

Profiles example:

import { createResourceProfile, ResourceStore } from '@reforgium/statum';

const usersStore = new ResourceStore<{ id: number; name: string }>(
  { GET: '/users' },
  createResourceProfile('table', { baseUrl: '/api' })
);

EntityStore

Normalized entities store (byId + ids) for fast updates and stable list composition.

Example:

import { EntityStore } from '@reforgium/statum';

type User = { id: number; name: string };

const entities = new EntityStore<User, 'id'>({ idKey: 'id' });

entities.setAll([{ id: 1, name: 'Neo' }]);
entities.upsertOne({ id: 2, name: 'Trinity' });
entities.patchOne(2, { name: 'Trin' });
entities.removeOne(1);

PagedQueryStore

Lightweight store for server-side pagination with filtering, dynamic query params, and page cache.

When to use

  • Server-side pagination
  • Noisy filters (debounce)
  • Tables/grids (PrimeNG onLazyLoad and similar)
  • Small local page cache with eviction

State

| Field / Signal | Type | Description | |----------------|---------------------------|----------------------------------------| | items | WritableSignal<T[]> | Current page items | | cached | WritableSignal<T[]> | Flattened cache of cached pages | | loading | WritableSignal<boolean> | Loading indicator | | error | WritableSignal<unknown \| null> | Last request error | | version | WritableSignal<number> | Increments when the dataset is reset | | page | number | Current page (0-based) | | pageSize | number | Page size | | totalElements | number \| undefined | Total items on server, if known | | filters | Partial<F> | Active filters | | query | Record<string, unknown> | Active query params | | sort | ReadonlyArray<{ sort: string; order: 'asc' \| 'desc' }> | Active sort state |

Reactive metadata signals are also available:

  • pageState
  • pageSizeState
  • totalElementsState
  • filtersState
  • queryState
  • sortState
  • routeParamsState

Methods

| Method | Description | |----------------|-----------------------------------------------------------------------------------------| | fetch | Clean first-page request: fetch({ filters, query, routeParams }) | | refetchWith | Repeat request, optional merge overrides: refetchWith({ filters, query }) | | updatePage | Change page: updatePage(page, { ignoreCache }) or updatePage({ page, ignoreCache }) | | updatePageSize | Change page size and reset cache: updatePageSize(size) | | setSort | Update sort state without triggering a request | | updateSort | Apply single-sort state and load from the first page | | updateSorts | Apply multi-sort state and load from the first page | | updateByOffset | Table-event mapper: updateByOffset({ page/first/rows }, { query }) | | setRouteParams | Update route params: setRouteParams(params, { reset, abort }) | | updateConfig | Patch config: updateConfig(config) | | copy | Copy config/meta: copy(store) | | destroy | Manual destroying of caches and abort requests |

version is useful for consumers that keep their own local page buffer. For example, @reforgium/data-grid can use [source]="store" and clear its internal infinity buffer when fetch(), updatePageSize(), or setRouteParams(..., { reset: true }) replace the dataset.

Sorting is built into PagedQueryStore and serialized in the common backend-friendly form:

sort=name,asc
sort=name,asc&sort=createdAt,desc

Concurrency can be configured per store:

  • latest-wins - abort older in-flight requests before the next page request starts
  • parallel - allow overlapping requests and keep loading() true until all active requests finish

Cache behavior

| Method | Cache read | Cache reset | Notes | |----------------|------------|-------------|--------------------------------------------------| | fetch | no | yes | Always starts clean from page 0 | | refetchWith | no | conditional | Reloads current state; when filters/query/sort actually change, resets to page 0 and replaces the dataset | | updatePage | yes | no | Can bypass with ignoreCache: true | | updatePageSize | no | yes | Prevents mixed caches for different page sizes | | updateByOffset | yes | no | Internally maps to page + size |

Example:

import { PagedQueryStore } from '@reforgium/statum';

type User = { id: number; name: string };

const store = new PagedQueryStore<User, { search?: string }>('api/users', {
  baseUrl: '/api',
  method: 'GET',
  debounceTime: 200,
  concurrency: 'latest-wins',
});

store.fetch({ filters: { search: 'neo' }, query: { tenant: 'kg' } });
store.updateByOffset({ first: 20, rows: 20 });
store.updateSort({ sort: 'name', order: 'asc' });

cached() remains a bounded hot-cache view. It is useful for cache-aware revisit/export/search helpers, but it is not the right datasource for infinity scrolling once cache eviction matters. For data-grid infinity mode, prefer passing the whole store as a GridPagedDataSource ([source]="store") and let the grid keep its own page buffer.

sort and routeParams should be changed only through setSort(...) and setRouteParams(...). Direct state mutation setters for page, pageSize, filters, query, and totalElements are available for low-level integration scenarios (such as external data-grid source contracts) but prefer the explicit store methods for typical use. totalElements may be undefined when the backend does not report a total.

PagedQueryStore + DataGrid source mode

PagedQueryStore can be passed directly into @reforgium/data-grid source mode:

<re-data-grid
  mode="infinity"
  [columns]="columns"
  [source]="usersStore"
/>

This works because the store already exposes the expected source contract:

  • items
  • loading
  • error
  • page
  • pageSize
  • totalElements
  • sort
  • version
  • updatePage(...)
  • updatePageSize(...)
  • updateSort(...)
  • updateSorts(...)

Keep data-grid source prefetch in sequential mode when the store uses latest-wins. Switch to parallel only if the store is configured with concurrency: 'parallel' and the backend flow supports overlapping page requests.


DictStore

Helper for dictionaries/options (select/autocomplete) on top of PagedQueryStore.

When to use

  • Select options / autocomplete hints
  • Local search over previously loaded values (fixed mode)
  • Cache across sessions in localStorage
  • Need { label, value } mapping

Key behavior

  • Two modes:
    • fixed: true - first load fills local cache; search happens locally
    • fixed: false - search goes to server (passes filter name)
  • Local cache merges without duplicates and is size-limited

Public API

Signals / fields:

| Field / Signal | Type | Description | |----------------|--------------------------------------------------------|-----------------------| | items | Signal<T[]> | Current visible items | | options | Signal<{ label: string; value: string \| number }[]> | Options for selects | | searchText | Signal<string> | Current search text | | filters | Signal<Record<string, any>> | Current filters |

Methods:

| Method | Description | |--------------|--------------------------------------------------------------------------------------------| | search | Set search text and trigger load (fixed=false) or local filter (fixed=true) | | findLabel | Resolve label by value from local cache (no network) | | restoreCache | Restore cache from the selected storage (persist/session/lru/memory). (no network) |

Config:

| Option | Type | Default | Description | |----------------|--------------------------------------|-------------|---------------------------------------------| | labelKey | string | 'name' | Property name to use as option label | | valueKey | string | 'code' | Property name to use as option value | | fixed | boolean | true | Use local search instead of server requests | | method | 'GET' \| 'POST' | 'POST' | HTTP method for requests | | debounceTime | number | 0 | Debounce time for search requests | | maxOptionsSize | number | undefined | Maximum items exposed in options | | cacheStrategy | 'persist' \| 'session' \| 'memory' | 'persist' | Cache storage strategy | | keyPrefix | string | 're' | Prefix for storage keys | | presetFilters | Record<string, string> | {} | Filters merged into each request |

Example:

import { DictStore } from '@reforgium/statum';

type Country = { code: string; name: string };

const dict = new DictStore<Country>('api/dictionaries/countries', 'countries', {
  labelKey: 'name',
  valueKey: 'code',
  fixed: true,
});

dict.search('');      // initial fill
dict.search('kir');   // local search over cache

Composition Patterns

PagedQueryStore + EntityStore

import { effect } from '@angular/core';
import { EntityStore, PagedQueryStore } from '@reforgium/statum';

type User = { id: number; name: string };

const pages = new PagedQueryStore<User, { search?: string }>('/api/users');
const entities = new EntityStore<User, 'id'>({ idKey: 'id' });

effect(() => {
  entities.upsertMany(pages.items());
});

This keeps page-loading logic in PagedQueryStore and normalized lookup/update logic in EntityStore.


Recipes

Debounced Remote Table

Use PagedQueryStore as a reusable server-table state layer for filters, lazy paging, and cache-aware navigation.

import { PagedQueryStore } from '@reforgium/statum';

type User = { id: number; name: string; role: string };
type UserFilters = { search?: string; role?: string };

const users = new PagedQueryStore<User, UserFilters>('/api/users', {
  baseUrl: '/gateway',
  method: 'GET',
  debounceTime: 250,
  hasCache: true,
  cacheSize: 5,
});

await users.fetch({
  filters: { search: 'neo', role: 'admin' },
  query: { tenant: 'main' },
});

await users.updateByOffset({ first: 20, rows: 20 });

Use this when the component should stay thin and pagination/filter state should live outside the template layer.

Latest-Wins Details Loader

Use ResourceStore when route changes or rapid user clicks can trigger overlapping requests.

import { ResourceStore } from '@reforgium/statum';

type UserDetails = { id: number; name: string; email: string };

const details = new ResourceStore<UserDetails>(
  { GET: '/api/users/:id' },
  { ttlMs: 30_000 }
);

async function openUser(id: number) {
  details.abortAll();

  return details.get(
    { params: { id } },
    { dedupe: true }
  );
}

Use this when the newest selection should win and stale responses should not keep competing for UI state.

Autocomplete With Cached Dictionary

Use DictStore to separate option loading, debounce, and local filtering from the component.

import { DictStore } from '@reforgium/statum';

type Country = { code: string; name: string };

const countries = new DictStore<Country>('/api/dictionaries/countries', 'countries', {
  fixed: true,
  labelKey: 'name',
  valueKey: 'code',
});

await countries.search('');
countries.search('kir');

Use fixed: true when the dataset is small enough to keep locally and you want immediate repeated searches without extra network calls.

Normalize Paged Data For Detail Mutations

Use PagedQueryStore for loading and EntityStore for stable updates after edits.

import { effect } from '@angular/core';
import { EntityStore, PagedQueryStore } from '@reforgium/statum';

type User = { id: number; name: string };

const pages = new PagedQueryStore<User>('/api/users');
const entities = new EntityStore<User, 'id'>({ idKey: 'id' });

effect(() => {
  entities.upsertMany(pages.items());
});

entities.patchOne(1, { name: 'Updated' });

Use this when list fetching and local item mutations need different lifecycles.


Serializer

Serializer

Utility for serialization/deserialization between layers (UI -> API, objects -> query string).

statum keeps the user-facing serializer API and presets, while the low-level codec engine is shared from hidden @reforgium/internal.

Core API:

| Method | Description | |-------------|------------------------------------------------| | serialize | Prepare data for transport | | deserialize | Parse incoming data / query string | | toQuery | Build query string | | withConfig | Clone serializer with partial config overrides |

Config:

| Option | Type | Default | Description | |-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|--------------------------------| | mapString | { trim?: boolean } & ParseFormatConfig | {} | String transformation options | | mapNumber | { fromString?: boolean } & ParseFormatConfig | {} | Number parsing options | | mapBoolean | { true?: string; false?: string } & ParseFormatConfig | {} | Boolean parsing options | | mapDate | { dateFormat?: string } & ParseFormatConfig | {} | Date parsing options | | mapPeriod | { transformMode?: { mode: 'split'; dateFromKeyPostfix: string; dateToKeyPostfix: string } \| { mode: 'join'; concat: string }; dateFormat?: string } & ParseFormatConfig<[Date | null, Date | null]> | {} | Period handling options | | mapNullable | { remove?: boolean; replaceWith?: string; includeEmptyString?: boolean } & ParseFormatConfig<null | undefined> | {} | Null/undefined handling | | mapArray | { concatType: 'comma' \| 'multi' \| 'json'; removeNullable?: boolean } & { format?: (val: any[]) => any } | {} | Array parsing options | | mapObject | { deep: boolean } & { format?: (val: Record<string, any>) => any } | {} | Object transformation options | | mapFields | Record<string, { type: 'string'\|'number'\|'boolean'\|'array'\|'object'\|'date'\|'period'\|'nullable' } \| { parse: (val: any, data: Record<string, any>) => any; format: (val: any, data: Record<string, any>) => any }> | undefined | Field-specific transformations |

Example:

import { Serializer } from '@reforgium/statum';

const serializer = new Serializer({
  mapString: { trim: true },
  mapNullable: { remove: true },
});

const body = serializer.serialize({ name: '  Vasya  ', active: null });
// => { name: 'Vasya' }

Source Structure

  • Cache: src/cache
  • Stores: src/stores
  • Serializer: src/serializer
  • Migration: MIGRATION.md

Compatibility

  • Angular: 18+
  • Signals: required
  • Zone.js: optional

License

MIT