# @statekit/core

Typed state managers that can run in a component, a server, a worker, or a normal function.

> Work in progress.

## Install

```sh
npm i @statekit/core
```

## Quick start

```ts
import { createMachine } from "@statekit/core"

const counter = createMachine({
   data: { count: 0 },
   events: {
      increment: ({ draft }, by = 1) => {
         draft.count += by
      },
   },
   selectors: {
      isPositive: ({ data }) => data.count > 0,
   },
})

counter.event.increment(2)
counter.data.count // 2
counter.select.isPositive() // true
```

`counter.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.

```ts
counter.event.increment()
counter.data.count // 3

setTimeout(counter.event.increment, 1000)
```

Read `machine.data` again when you need the latest snapshot. An older snapshot stays unchanged:

```ts
const before = counter.data

counter.event.increment()

const after = counter.data

before.count // 3
after.count // 4
```

## States and transitions

The first declared state is the starting state. Use the event's `setState` when an event changes state.

```ts
const upload = 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"],
      },
   },
})

upload.state === upload.STATE.idle // true

upload.event.start()
upload.state === upload.STATE.uploading // true
```

For a named state, `only` lists the states it may move to. `deny` lists the states it may not move to. A blocked transition throws.

Outside an event, call `machine.setState(machine.STATE.name)` directly.

## Async events

Async events publish changes made before the first `await` immediately, then publish later changes when the promise settles.

```ts
const fileImport = 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,ada@example.com"], "users.csv")
const request = fileImport.event.importFile(file)

fileImport.state === fileImport.STATE.importing // true

const contents = await request

contents // CSV text returned by the event
fileImport.data.fileName // "users.csv"
fileImport.data.contents // latest snapshot
fileImport.state === fileImport.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 `await`s.

Parallel async events share current work safely. When one finishes, it does not replace unrelated changes made while it was waiting.

## Subscribe to events

Use `subscribeEvents` to observe event names, arguments, and async results.

```ts
const stop = fileImport.subscribeEvents(([name, args, success]) => {
   console.log(name, args, success)
})

await fileImport.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.

```ts
const machine = createMachine({
   initializer: () => ({ count: 0 }),
})
```

`@statekit/react` runs the initializer when the machine provider first mounts 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.

## Do

- Change data through an event's `draft`.
- Use the event's `setState` when changing state inside an event.
- 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`.
- Don't place the same state in both `only` and `deny`.

## Development

From the `node` directory:

```sh
pnpm --filter @statekit/core test -- --runInBand
pnpm --filter @statekit/core build
```
