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

sure-state

v0.1.0

Published

Client-server state synchronization for TypeScript apps — server is always the source of truth.

Readme

sure-state

JavaEE reliability. JavaScript developer velocity.

Write stateful apps with the consistency of managed beans and the ergonomics of React hooks. The server is always the source of truth — no guessing, no stale caches, no overwritten changes.

npm install sure-state

Why?

Every non-trivial web app faces the same problem: the client has one version of the data, the server has another. Most state management libraries (Zustand, Redux, Jotai) handle local state beautifully but leave synchronization as an exercise for the developer. JavaEE handled this correctly — managed beans owned the state, transactions guaranteed consistency — but at the cost of heavy servers and slow iteration.

sure-state brings the same reliability model to modern TypeScript apps, with a single pattern:

  1. Fetch data from the server into a typed store
  2. Mutate via the store — it calls the API, then re-fetches to guarantee consistency
  3. Subscribe to server-pushed updates via WebSocket for real-time sync
  4. Inspect every action with built-in devtools, Prometheus metrics, and OpenTelemetry spans

Philosophy

| Principle | Why | |---|---| | Server is source of truth | Local state is a cache, not authority. Every write re-fetches (or applies push events). | | Explicit sync strategy | Choose client-first (optimistic, single-user) or server-first (pessimistic, collaborative). | | Zustand-native | Uses Zustand under the hood. No new reactivity model to learn. | | Framework-agnostic | Works with React, Vue, Svelte, or vanilla JS via Zustand's vanilla store. | | Real-time optional | WebSocket integration is opt-in. Without it, the store still guarantees consistency via re-fetch. |

Quick start

import { createEntityStore } from 'sure-state'
import { apiClient } from './your-api-client'

interface Persona {
  id: string
  name: string
  description: string | null
  isActive: boolean
}

const personaStore = createEntityStore<Persona>({
  name: 'persona',
  api: {
    list:    () => apiClient.get('/personas').then(r => r.data),
    getById: (id) => apiClient.get(`/personas/${id}`).then(r => r.data),
    create:  (data) => apiClient.post('/personas', data).then(r => r.data),
    update:  (id, data) => apiClient.put(`/personas/${id}`, data).then(r => r.data),
    remove:  (id) => apiClient.delete(`/personas/${id}`),
  },
})

// Use it
await personaStore.fetch()
console.log(personaStore.items)   // typed array
console.log(personaStore.isLoading)
console.log(personaStore.error)

await personaStore.create({ name: 'Support Bot', identity: 'You are helpful...' })
// After create: items list is refreshed from server

personaStore.select('abc-123')
console.log(personaStore.selected) // single entity or null

Sync strategies

client-first (default)

Optimistic local update + re-fetch. Use when the current user is the only writer of this data.

create → api.create(data) → prepend to local items → done
delete → api.remove(id)   → filter from local items → done

Fast UX. Safe for single-user apps (user owns their data).

server-first

Always re-fetch after every mutation. Never trust local state after a write. Required when multiple users or agents can modify the same entities.

create → api.create(data) → re-fetch full list → done
delete → api.remove(id)   → re-fetch full list → done

Slower per-mutation but guarantees consistency. Required for collaborative multi-user apps.

Real-time sync (WebSocket)

import { createEntityStore, createWebSocketClient } from 'sure-state'

const ws = createWebSocketClient({
  url: 'wss://api.example.com/ws',
  getToken: () => localStorage.getItem('access_token'),
})

const personaStore = createEntityStore<Persona>({
  name: 'persona',
  api: { /* ... */ },
  onSubscribe: (handler) => ws.subscribe('persona', handler),
})

When the server pushes a { type: 'updated', entityType: 'persona', entity: {...} } event, the store automatically patches its local items array.

Version stamps (optimistic concurrency)

When multiple writers can race on the same entity (user edits a persona while an agent updates its context), use version stamps:

import { stampFor, ConflictError } from 'sure-state'

// Client side:
const stamp = stampFor(existingPersona)
await api.update(id, { ...changes, ...stamp })

// Server side (SQL example):
const result = await sql`
  UPDATE "Persona" SET name = ${name}, version = version + 1
  WHERE id = ${id} AND version = ${stamp.version}
  RETURNING *
`
if (result.length === 0) throw new ConflictError('Persona')

Token management

import { createTokenManager } from 'sure-state'

const tokenManager = createTokenManager({
  getTokens:    () => authStore.getState().tokens,
  setTokens:    (t) => authStore.getState().setTokens(t),
  clearTokens:  () => authStore.getState().clearTokens(),
  refresh:      (rt) => apiClient.post('/auth/refresh', { refreshToken: rt }).then(r => r.data),
})

// In your HTTP client interceptor:
apiClient.interceptors.request.use(async (config) => {
  const token = await tokenManager.getAccessToken()
  config.headers.Authorization = `Bearer ${token}`
  return config
})

API

createEntityStore<T, TCreate, TUpdate>(config)

Creates a Zustand-powered entity store.

| Method | Returns | Description | |---|---|---| | fetch() | Promise<void> | Replace local items with server data | | fetchById(id) | Promise<void> | Fetch single entity, then re-fetch list | | create(data) | Promise<T> | Create, then re-fetch (or optimistic insert) | | update(id, data) | Promise<T> | Update, then re-fetch (or optimistic patch) | | delete(id) | Promise<void> | Delete, then re-fetch (or optimistic remove) | | select(id) | void | Set selectedId locally | | clearError() | void | Clear error state | | reset() | void | Reset to initial state |

| Property | Type | Description | |---|---|---| | items | T[] | Cached entity list (read-only) | | selected | T \| null | Entity matching selectedId | | isLoading | boolean | True during any API call | | error | string \| null | Last error message |

Config

| Option | Default | Description | |---|---|---| | name | required | Human-readable entity name | | sync | 'client-first' | 'client-first' or 'server-first' | | api | required | { list, getById, create, update, remove } | | onSubscribe | undefined | (handler) => unsubscribe for WebSocket integration | | versioning | false | Enable version stamp helpers | | versionSelector | (e) => e.version | Extract version from entity | | onMutate | undefined | Callback after every successful mutation |

createWebSocketClient(options)

| Method | Returns | Description | |---|---|---| | subscribe(type, handler) | () => void | Register handler for entity type | | connect() | void | Connect (auto-called) | | disconnect() | void | Disconnect and stop reconnecting |

createTokenManager(options)

| Method | Returns | Description | |---|---|---| | getAccessToken() | Promise<string> | Valid token, refreshing if needed | | refreshNow() | Promise<void> | Force refresh | | invalidate() | void | Clear tokens, notify listeners | | onStatusChange(cb) | () => void | Subscribe to auth status |

Migration guide

From raw Zustand stores

If you have existing Zustand stores that manually call APIs and update local state, migrate one entity at a time:

Before:

const usePersonaStore = create((set) => ({
  items: [],
  fetch: async () => {
    const res = await api.get('/personas')
    set({ items: res.data })
  },
  delete: async (id) => {
    await api.delete(`/personas/${id}`)
    set((s) => ({ items: s.items.filter(p => p.id !== id) }))
  },
}))

After:

const personaStore = createEntityStore<Persona>({
  name: 'persona',
  api: {
    list:    () => api.get('/personas').then(r => r.data),
    remove:  (id) => api.delete(`/personas/${id}`),
    // ... etc
  },
})

For React, wrap with useSyncExternalStore or subscribe via useStore(personaStore).

License

MIT