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

@shing.wong/sure-state

v0.1.8

Published

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

Readme

sure-state

Client-server state synchronization for TypeScript apps — the server is always the source of truth. Fetch from the server, mutate through the store, subscribe to real-time push via WebSocket, and inspect every action with built-in logging, metrics, and agent-inspectable tooling.

import { createEntityStore } from '@shing.wong/sure-state'

interface Persona { id: string; name: string; description: string | null }

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

await personaStore.fetch()
console.log(personaStore.items)    // typed, always consistent with server
console.log(personaStore.isLoading)

Why sure-state?

| Problem | How sure-state solves it | |---------|--------------------------| | Client-server state drift | Every write re-fetches from the server (or applies a push event). The server is always the authority — no stale caches, no silent conflicts. | | No visibility into state | Built-in inspector records every action with timing and state snapshots. attachLogger adds colored console groups with before/after state. | | Blind mutations | createMetricsCollector + attachOtelSpans expose operation counters, latency histograms, and OpenTelemetry spans for every store action. | | Agents can't inspect state | createAgentTools exposes list_stores, get_store_state, get_action_history, dump_report, get_metrics as MCP tools. Agents (OpenCode, Claude Desktop) connect via createMcpServer. | | Multi-writer races | Version stamps (stampFor, versionWhere, ConflictError) for optimistic concurrency control. | | Manual sync logic | Two declarative sync strategies: client-first (optimistic, single-user) and server-first (pessimistic, collaborative). Set once per store. |

How it compares

| | sure-state | Zustand + manual API | TanStack Query | Redux Toolkit | |---|---|---|---|---| | Server is source of truth | ✅ Built-in | ❌ You write it | ✅ Yes | ❌ No | | Sync strategies | ✅ client-first / server-first | ❌ | ❌ Stale-while-revalidate only | ❌ | | WebSocket real-time | ✅ Built-in createWebSocketClient | ❌ | ⚠️ Via QueryClient.setQueryData | ❌ | | Action inspector | ✅ createInspector with full history | ❌ | ❌ DevTools only | ✅ Redux DevTools | | Logging | ✅ attachLogger (colored console groups) | ❌ | ❌ | ❌ | | Prometheus metrics | ✅ createMetricsCollector | ❌ | ❌ | ❌ | | OpenTelemetry | ✅ attachOtelSpans | ❌ | ❌ | ❌ | | Agent MCP tools | ✅ createAgentTools + createMcpServer | ❌ | ❌ | ❌ | | Test utilities | ✅ createMockApi, recordActions, waitForStore | ❌ | ✅ QueryClient mocking | ❌ | | Framework-agnostic | ✅ Plain external store (React/Vue/Svelte/vanilla JS) | ✅ Same | ❌ React-only | ✅ Same | | Optimistic concurrency | ✅ Version stamps | ❌ | ❌ | ❌ | | Bundle size | ~800 lines core, zero runtime deps | ~100 lines + zustand | ~25 KB | ~12 KB + RTK |

Installation

npm install @shing.wong/sure-state

Zero runtime dependencies. The only peer dependency is optional:

npm install react                             # optional (for InspectorPanel)

The store is a plain external store (getState/setState/subscribe) with useSyncExternalStore-compatible semantics, so it works with any framework or none — no state library to install.

Quick start

import { createEntityStore } from '@shing.wong/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}`),
  },
})

await personaStore.fetch()
console.log(personaStore.items)       // typed array
console.log(personaStore.isLoading)   // boolean
console.log(personaStore.error)       // string | null

await personaStore.create({ name: 'Support Bot', description: '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.

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.

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 or agent-managed data.

Logging

Attach a console logger to visualize every mutation with colored groups and before/after state:

import { attachLogger } from '@shing.wong/sure-state'

const detach = attachLogger(personaStore, { collapsed: true })
// Every fetch/create/update/delete now logs:
//   [persona] fetch  (green)
//   items: [...]     isLoading: false   error: null

// Later, to remove:
detach()

Logger options:

| Option | Default | Description | |--------|---------|-------------| | collapsed | true | Start with collapsed console groups | | color | #a6e3a1 | Color for group title | | filter | — | Only log these kinds: fetch, create, update, delete, push |

Instrumentation

Prometheus metrics

import { createEntityStore, createEventBus, createMetricsCollector, attachMetrics } from '@shing.wong/sure-state'

const bus = createEventBus()
const store = createEntityStore({ name: 'persona', api, onSubscribe: (h) => bus.on('push', h) })
const metrics = createMetricsCollector()
attachMetrics(bus, metrics, 'persona')

// Expose on your /metrics endpoint:
app.get('/metrics', (req, res) => {
  res.type('text/plain').send(metrics.dump())
})

Outputs counters (persona_fetch_total, persona_create_total), latency histograms (persona_action_duration_ms), and gauges (persona_items_count).

OpenTelemetry

import { attachOtelSpans } from '@shing.wong/sure-state'

attachOtelSpans(bus, 'persona')
// Every store action generates an OTEL span with attributes:
//   kind, entityName, success, durationMs

Inspection

Action history

import { createInspector } from '@shing.wong/sure-state'

const inspector = createInspector(personaStore)

console.table(inspector.dump().recentActions)
// ┌─────────┬────────┬────────────┬───────────┬─────────┐
// │   id    │  kind  │  success   │ durationMs│ detail  │
// ├─────────┼────────┼────────────┼───────────┼─────────┤
// │ act_1   │ fetch  │ true       │ 142       │         │
// │ act_2   │ create │ true       │ 89        │ {...}   │
// └─────────┴────────┴────────────┴───────────┴─────────┘

const report = inspector.dump()
// { entity: 'persona', sync: 'client-first', itemsCount: 12,
//   isLoading: false, error: null, lastAction: {...}, ... }

| Method | Returns | Description | |--------|---------|-------------| | getActions() | ActionRecord[] | Full action log | | dump() | InspectorReport | Debug report with summary + recent actions | | setLogging(bool) | void | Enable/disable at runtime | | clear() | void | Erase history | | onAction(cb) | () => void | Subscribe to new actions |

React devtools panel

import { InspectorPanel } from '@shing.wong/sure-state/react-devtools'

function DebugDrawer() {
  return (
    <InspectorPanel
      stores={{ personas: personaStore, sessions: sessionStore }}
      inspectors={{ personas: personaInspector, sessions: sessionInspector }}
    />
  )
}

Real-time sync (WebSocket)

import { createEntityStore, createWebSocketClient } from '@shing.wong/sure-state'

const ws = createWebSocketClient({
  url: 'wss://api.example.com/ws',
  getToken: () => localStorage.getItem('access_token'),
  reconnectDelay: 3000,
  maxReconnectAttempts: 10,
  onStatusChange: (status) => console.log('WS:', status),
})

const personaStore = createEntityStore<Persona>({
  name: 'persona',
  api: { /* ... */ },
  onSubscribe: (handler) => ws.subscribe('persona', handler),
})
// Server pushes { type: 'updated', entity: {...} } → store auto-patches

WebSocket features: auto-connect, token-based auth, automatic reconnection with backoff, entity-scoped subscriptions.

Version stamps (optimistic concurrency)

When multiple writers (users + agents) can race on the same entity:

import { stampFor, ConflictError } from '@shing.wong/sure-state'

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

// Server side:
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')

| Export | Purpose | |--------|---------| | stampFor(entity) | Returns { version } from entity | | versionWhere(entity) | Returns { version } for SQL WHERE clause | | getVersion(entity) | Extract raw version value | | ConflictError | Error class for version conflicts |

Token management

import { createTokenManager } from '@shing.wong/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
})

Decodes JWTs to proactively refresh before expiry. Emits onStatusChange for auth state.

Event bus

import { createEventBus } from '@shing.wong/sure-state'

const bus = createEventBus()

bus.on('action', (record) => sendToAnalytics(record))
bus.on('error', ({ message, kind }) => notifySentry(message))
bus.on('slow', (record) => console.warn('Slow action:', record))
bus.on('sync', ({ status }) => updateConnectionIndicator(status))

| Event | Payload | When | |-------|---------|------| | action | ActionRecord | Any mutation completes | | error | { message, kind } | A store operation fails | | slow | ActionRecord | Operation exceeds threshold | | sync | { status } | WebSocket connection changes | | push | PushEvent | Server pushes an update |

Cookie store (client-side persistence)

Persist UI state, preferences, and theme selections across page reloads — no server endpoint needed. Every cookie change emits an action event visible through the event bus and agent tools.

import { createCookieStore } from '@shing.wong/sure-state'

const prefs = createCookieStore({
  prefix: 'sure_',           // namespace your cookies
  defaults: { theme: 'nord', language: 'en' },
  expires: 365,              // days
})

prefs.get('theme')            // → 'nord' (from cookie or default)
prefs.set('theme', 'dracula') // → sets document.cookie
prefs.getAll()                // → { theme: 'dracula', language: 'en' }
prefs.subscribe((key, value) => console.log('changed:', key, value))
prefs.clear()                 // remove all prefixed cookies

Sync entity store to cookies

import { createEntityStore, createCookieStore, syncToCookie } from '@shing.wong/sure-state'

const store = createEntityStore({ name: 'preferences', api: { /* ... */ } })
const cookies = createCookieStore({ prefix: 'sure_' })
const stop = syncToCookie(store, cookies)

// Every store mutation auto-syncs to cookies:
await store.create({ id: 'theme', value: 'dracula' })
// → cookie: sure_theme=dracula

// Detach when done:
stop()

Cookie store API

| Method | Returns | Description | |--------|---------|-------------| | get(key) | string \| null | Read a cookie (or default) | | set(key, value) | void | Write a cookie | | remove(key) | void | Delete a cookie | | getAll() | Record<string, string> | All prefixed cookies + defaults | | clear() | void | Remove all prefixed cookies | | subscribe(cb) | () => void | Subscribe to changes | | dump() | string | Raw cookie string (for agent inspection) |

Options

| Option | Default | Description | |--------|---------|-------------| | prefix | '' | Cookie name prefix for namespacing | | defaults | {} | Fallback values when no cookie exists | | expires | 365 | Days until cookie expiry | | path | '/' | Cookie path | | sameSite | 'lax' | SameSite policy | | secure | false | HTTPS-only | | onChange | — | Callback for any cookie change |

Authentication (SimpleAuth)

Zero-dependency password authentication for lightweight apps. Built on crypto.scrypt — no npm packages needed.

import { createSimpleAuth } from '@shing.wong/sure-state'

const auth = createSimpleAuth({
  passwordPolicy: { minLength: 8, requireUpper: true, requireDigit: true },
  rateLimit: { maxAttempts: 5, windowMs: 60000, banMs: 300000 },
})

const session = await auth.register({ email: '[email protected]', password: 'StrongPass1!' })
const session = await auth.login({ email: '[email protected]', password: 'StrongPass1!' })
const identity = await auth.getSession(session.token)
await auth.logout(session.token)

AuthAdapter Interface

For production apps, implement AuthAdapter with any auth library (Auth.js, Lucia, Clerk):

import type { AuthAdapter } from '@shing.wong/sure-state'

const adapter: AuthAdapter = {
  name: 'my-auth',
  login: async (creds) => { /* delegate */ },
  register: async (creds) => { /* delegate */ },
  logout: async (token) => { /* delegate */ },
  getSession: async (token) => { /* delegate */ },
  authenticate: async (token) => { /* delegate */ },
  can: async (identity, action, entity) => { /* policy check */ },
  on: (event, handler) => { /* subscribe */ },
}

Policy Enforcement

Wrap entity stores with withAuth() to enforce access control on every mutation:

const protectedStore = withAuth(store, auth, getIdentity)
await protectedStore.create(data)  // checks can() before executing

Audit Trail

Auth events flow through the same event bus as store mutations:

| Event | When | |-------|------| | auth:login | Successful login | | auth:failed | Failed login attempt | | auth:denied | Policy blocked mutation |

Agent tools (MCP)

AI coding assistants (OpenCode, Claude Desktop, Cursor) can inspect and reason about your application state in real time:

import { createEntityStore, createInspector, createAgentTools, createMcpServer } from '@shing.wong/sure-state'

const personaStore = createEntityStore<Persona>({ name: 'persona', api: { /* ... */ } })
const inspector = createInspector(personaStore)

const tools = createAgentTools({
  stores: { personas: personaStore },
  inspectors: { personas: inspector },
})

// Start MCP server for OpenCode/Claude Desktop
createMcpServer(tools)

Configure in opencode.json:

{
  "mcpServers": {
    "sure-state": {
      "command": "node",
      "args": ["path/to/your-mcp-server.js"]
    }
  }
}

Available agent tools

| Tool | Purpose | |------|---------| | list_stores | All stores with items count, loading, error, selection | | get_store_state | Full snapshot of any store (all items + metadata) | | get_action_history | Recent actions with timing (fetch, create, update, delete) | | dump_report | Comprehensive debug report (one store or all) | | get_metrics | Prometheus metrics snapshot |

OpenCode workflow example

Prompt OpenCode to debug application state:

Using sure-state, debug the state of my persona store.
1. List all stores with their current status
2. Get the full state of the "personas" store
3. Show the last 10 actions with timing
4. Dump a debug report for the "personas" store
5. Check the metrics for slow operations

OpenCode connects via MCP and calls list_stores, get_store_state, get_action_history, dump_report, get_metrics — inspecting state between every step.

What agents should know

| File | What it tells the agent | |------|------------------------| | src/create-entity-store.ts | createEntityStore — config, sync strategies, CRUD methods | | src/types.ts | Core interfaces: EntityStore, EntityStoreConfig, SyncStrategy, EntityApi | | src/inspector.ts | createInspector — action history, debug reports | | src/logger.ts | attachLogger — colored console logging | | src/instrumentation.ts | createMetricsCollector, attachMetrics, attachOtelSpans | | src/events.ts | createEventBus — typed event system | | src/agent-tools.ts | createAgentTools — all agent-inspectable MCP tools | | src/create-mcp-server.ts | createMcpServer — lightweight MCP protocol over stdio | | src/websocket.ts | createWebSocketClient — real-time sync | | src/auth.ts | createTokenManager — JWT lifecycle | | src/version-stamp.ts | stampFor, ConflictError — optimistic concurrency | | src/test-utils.ts | createMockApi, recordActions, waitForStore, createTestStore |

API

createEntityStore<T, TCreate, TUpdate>(config)

| 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 | | subscribe(selector) | function | Zustand subscription | | getState() | snapshot | Current state | | applyPushEvent(event) | void | Apply a push event (internal, called by WebSocket) |

| Property | Type | Description | |----------|------|-------------| | items | T[] | Cached entity list (read-only) | | selected | T \| null | Entity matching selectedId | | selectedId | string \| null | Currently selected entity ID | | 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 | | versioning | false | Enable version stamp helpers | | versionSelector | (e) => e.version | Extract version from entity | | onMutate | undefined | Callback after every successful mutation |

Test utilities

import { createMockApi, recordActions, waitForStore, createTestStore } from '@shing.wong/sure-state'

// Mock API backed by in-memory array
const api = createMockApi([{ id: '1', name: 'Alice' }])
const store = createEntityStore({ name: 'test', api })

// Record actions for assertions
const actions = recordActions(store)
await store.delete('1')
expect(actions.last().kind).toBe('delete')

// Wait for async state
await waitForStore(store, (s) => !s.isLoading)

// Quick test setup
const { store, api, actions } = createTestStore([{ id: '1', name: 'Alice' }])

Development

git clone [email protected]:ShingWong/sure-state.git
cd sure-state
npm install
npm run build
npm test                      # 27 tests
npm run lint                  # tsc --noEmit