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

@statekit/core

v0.10.3-dev

Published

Readme

@statekit/core

Typed state managers that can run in a component, a server, a worker, or a normal function.

Work in progress.

Install

npm i @statekit/core

Quick start

import { createMachine } from "@statekit/core"

const counterMachine = createMachine({
   data: { count: 0 },
   events: {
      increment: ({ draft }, by = 1) => {
         draft.count += by
      },
   },
   selectors: {
      isPositive: ({ data }) => data.count > 0,
   },
})

counterMachine.event.increment(2)
counterMachine.data.count // 2
counterMachine.select.isPositive() // true

counterMachine.data is the latest data snapshot. Events receive a draft; change that draft to publish the next snapshot. Selectors provide named reads without changing data.

Call events anywhere

A machine is a normal object. It does not need a component or hook.

counterMachine.event.increment()
counterMachine.data.count // 3

setTimeout(counterMachine.event.increment, 1000)

Read machine.data again when you need the latest snapshot. An older snapshot stays unchanged:

const before = counterMachine.data

counterMachine.event.increment()

const after = counterMachine.data

before.count // 3
after.count // 4

Async events

Async events publish changes made before the first await immediately, then publish later changes when the promise settles.

const fileImportMachine = createMachine({
   states: ["idle", "importing", "ready", "failed"],
   data: {
      fileName: "",
      contents: "",
   },
   events: {
      importFile: async ({ draft, setState, S }, file: File) => {
         setState(S.importing)

         try {
            const contents = await file.text()

            draft.fileName = file.name
            draft.contents = contents
            setState(S.ready)

            return contents
         } catch (error) {
            setState(S.failed)
            throw error
         }
      },
   },
})

const file = new File(["name,email\nAda,[email protected]"], "users.csv")
const request = fileImportMachine.event.importFile(file)

fileImportMachine.state === fileImportMachine.STATE.importing // true

const contents = await request

contents // CSV text returned by the event
fileImportMachine.data.fileName // "users.csv"
fileImportMachine.data.contents // latest snapshot
fileImportMachine.state === fileImportMachine.STATE.ready // true

In one event handler, update the draft only before the first await or after the last await. Do not update it between two awaits.

Parallel async events share current work safely. When one finishes, it does not replace unrelated changes made while it was waiting.

States

Data can hold a status like isLoading too. States are declared and typed, and setState replaces the current one, so isLoading and isFailed can't both be true. The first declared state is the starting state.

const uploadMachine = createMachine({
   states: ["idle", "uploading", "done", "failed"],
   events: {
      start: ({ setState, S }) => setState(S.uploading),
      finish: ({ setState, S }) => setState(S.done),
   },
})

uploadMachine.state === uploadMachine.STATE.idle // true

uploadMachine.event.start()
uploadMachine.state === uploadMachine.STATE.uploading // true

Outside an event, call machine.setState(machine.STATE.name).

Several active states

setState.enter and setState.exit add or remove one state and keep the others. setState replaces them all.

const playerMachine = createMachine({
   states: ["idle", "playing", "buffering"],
   events: {
      play: ({ setState, S }) => setState(S.playing),
      bufferStart: ({ setState, S }) => setState.enter(S.buffering),
      bufferEnd: ({ setState, S }) => setState.exit(S.buffering),
      stop: ({ setState, S }) => setState(S.idle),
   },
})

playerMachine.event.play()
playerMachine.event.bufferStart() // playing and buffering
playerMachine.event.bufferEnd() // playing

Inside an event, check with hasState(S.buffering), which stays correct after an await. Outside, use (machine.state & machine.STATE.buffering) !== 0.

Subscribe to events

Use subscribeEvents to observe event names, arguments, and async results.

const stop = fileImportMachine.subscribeEvents(([name, args, success]) => {
   console.log(name, args, success)
})

await fileImportMachine.event.importFile(file)

stop()

A sync event sends one notification with success as undefined. An async event sends one when it starts, then another with true after it resolves or false after it rejects.

Initializer

Use data when a value is available during machine creation. An initializer is stored for an adapter to run later; createMachine does not run it.

const machine = createMachine({
   initializer: () => ({ count: 0 }),
})

@statekit/react runs the initializer the first time a component uses the machine in the browser.

StateKit packages

  • @statekit/core runs the machine and owns its data, events, states, and selectors.
  • @statekit/react shares machines with React components and limits rerenders to the data each component reads.
  • @statekit/connector provides the field tracking used by view adapters.
  • @statekit/machine-message builds a request cache on a StateKit machine.
  • @statekit/eslint-plugin reports draft changes between awaits and destructuring that rerenders on every data change.

Do

  • Change data through an event's draft.
  • Use the event's setState when changing state inside an event.
  • Use hasState instead of a destructured state after an await.
  • Await async events when you need their result.
  • Read machine.data again when you need the latest snapshot.
  • Call the function returned by subscribeEvents when you are done listening.

Don't

  • Don't save a draft and use it after the event ends.
  • Don't expect a saved snapshot to update by itself.
  • Don't expect createMachine to run initializer.

Advanced

Transition rules

transitionRules limits where a state can move. For a named state, only lists the states it may move to, and deny lists the states it may not move to. A blocked transition throws.

const uploadMachine = createMachine({
   states: ["idle", "uploading", "done", "failed"],
   events: {
      start: ({ setState, S }) => setState(S.uploading),
      finish: ({ setState, S }) => setState(S.done),
   },
   transitionRules: {
      deny: {
         idle: ["done"],
      },
      only: {
         uploading: ["done", "failed"],
      },
   },
})

Don't place the same state in both only and deny.

Development

From the node directory:

pnpm --filter @statekit/core test -- --runInBand
pnpm --filter @statekit/core build