@state-flow/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilya Pirogov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # StateFlow
2
+
3
+ Type-safe, immutable state management for TypeScript, built on **signals**, **flows**, and frozen **state snapshots**.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@state-flow/core.svg)](https://www.npmjs.com/package/@state-flow/core)
6
+ [![CI](https://github.com/ilya-pirogov/stateflow/actions/workflows/ci.yml/badge.svg)](https://github.com/ilya-pirogov/stateflow/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
8
+
9
+ StateFlow models your application state as a set of explicit, named variants and the
10
+ transitions between them. Every change goes through a **signal**, every transition is
11
+ described by a pure **flow** function, and every state instance is a frozen, immutable
12
+ snapshot. The result is state logic that is predictable, traceable, and fully type-checked
13
+ end to end.
14
+
15
+ ## Why StateFlow
16
+
17
+ - **Signals are the only path to change.** State can never be mutated directly — you
18
+ dispatch a typed signal and the flow decides what happens. Every change is tracked.
19
+ - **Immutable snapshots.** State instances are frozen with `Object.freeze()`; transitions
20
+ produce new snapshots rather than mutating existing ones.
21
+ - **Pure, synchronous transitions.** `defineFlow` handlers are synchronous and return a
22
+ new state or a `Result` (`ok` / `ignore` / `reject` / `error`) — the right place to
23
+ validate input. Side effects and async work live in `applyFlow` handlers via
24
+ `Result.transition`.
25
+ - **First-class type safety.** Signals, state variants, and flow handlers are all inferred
26
+ and checked. Illegal transitions are compile errors, not runtime surprises.
27
+ - **Tiny footprint.** No framework lock-in; the only runtime dependency is `events`.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install @state-flow/core
33
+ # or
34
+ yarn add @state-flow/core
35
+ # or
36
+ pnpm add @state-flow/core
37
+ ```
38
+
39
+ **Requirements.** The published package is downleveled to ES2020 and ships a `Symbol.dispose` /
40
+ `Symbol.asyncDispose` polyfill, so it runs on Node.js 18+ and modern browsers with no special
41
+ runtime support. The examples below use `await using` (explicit resource management) for
42
+ ergonomic cleanup — that syntax is optional and requires TypeScript 5.2+ with an ES2022+
43
+ target. You can always dispose manually instead (e.g. `observer[Symbol.dispose]()`).
44
+
45
+ ## Quick start
46
+
47
+ ```typescript
48
+ import {
49
+ defineSignal,
50
+ defineState,
51
+ defineFlow,
52
+ applyFlow,
53
+ lock,
54
+ observe,
55
+ Result,
56
+ ResultKind,
57
+ type Infer,
58
+ } from "@state-flow/core";
59
+
60
+ // 1. Signals — the only way to request a state change.
61
+ const signals = {
62
+ play: defineSignal("play"),
63
+ pause: defineSignal("pause"),
64
+ seek: defineSignal<{ position: number }>("seek"),
65
+ };
66
+
67
+ // 2. State — immutable, frozen snapshots with named variants.
68
+ const playback = defineState<{ position: number; duration: number }>()
69
+ .name("playback")
70
+ .signals(signals)
71
+ .variant("paused", true) // the initial variant
72
+ .variant("playing")
73
+ .stringRepr((s) => `pos=${s.position}/${s.duration}`)
74
+ .build();
75
+
76
+ // 3. Flow — how each variant responds to signals (must be synchronous).
77
+ defineFlow(playback.paused, {
78
+ play: (state) => playback.playing(state),
79
+ seek: (state, signal) => {
80
+ if (signal.position < 0 || signal.position > state.duration) {
81
+ return Result.reject("Invalid seek position");
82
+ }
83
+ return { ...state, position: signal.position };
84
+ },
85
+ });
86
+
87
+ defineFlow(playback.playing, {
88
+ pause: (state) => playback.paused(state),
89
+ });
90
+
91
+ // 4. Bind the flow to an object and register side-effect handlers.
92
+ type Player = { playback: Infer<typeof playback> };
93
+ const player: Player = { playback: { position: 0, duration: 180 } };
94
+
95
+ applyFlow(player, [playback], (sm) => {
96
+ sm.addEnterHandler(playback.playing, () => Result.ok());
97
+ });
98
+
99
+ // 5. Observe state changes (the only thing mutable after setup).
100
+ const observer = observe(
101
+ player,
102
+ [playback.playing, playback.paused],
103
+ (state) => console.log("playback ->", String(state)),
104
+ );
105
+
106
+ // 6. Drive it: acquire a lock, then send signals — queued and type-safe.
107
+ async function main() {
108
+ await using send = await lock(player);
109
+ await send(signals.play()).expect(ResultKind.OK, ResultKind.Ignored).done();
110
+ await send(signals.seek({ position: 30 })).done();
111
+
112
+ observer[Symbol.dispose]();
113
+ }
114
+
115
+ main();
116
+ ```
117
+
118
+ For a full walkthrough — including asynchronous transitions, observers, result merging,
119
+ testing, and architecture — see the [documentation](#documentation).
120
+
121
+ ## Core concepts
122
+
123
+ | Concept | What it is |
124
+ | --- | --- |
125
+ | **Signal** | A typed message that requests a state change. Created with `defineSignal`. |
126
+ | **State** | An immutable definition with one or more named variants. Created with `defineState`. |
127
+ | **Flow** | The synchronous mapping from a variant + signal to a new state or `Result`. Created with `defineFlow`. |
128
+ | **Result** | The outcome of a transition: `ok`, `ignore`, `reject`, `error`, or an async `transition`. |
129
+ | **`applyFlow`** | Binds state definitions to a target object and registers enter/update/exit/rollback handlers. |
130
+ | **`lock` / `send`** | Acquire an async-disposable lock, then dispatch signals so they queue safely instead of throwing during an active transition. |
131
+ | **`observe`** | Subscribe to variant changes for UI updates and side effects. |
132
+
133
+ ## Documentation
134
+
135
+ Full documentation lives at **[stateflow.dev](https://stateflow.dev)** — core concepts, the
136
+ signal system, state consistency and visibility, the API reference, a complete media-player
137
+ example, testing, and the architecture guide.
138
+
139
+ The site's source is in [`docs/`](./docs) (a Next.js app). To run it locally:
140
+
141
+ ```bash
142
+ cd docs
143
+ yarn install
144
+ yarn dev
145
+ ```
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ npm install # install dependencies
151
+ npm test # run the test suite (vitest)
152
+ npm run build # bundle (tsup) + emit type declarations (tsc)
153
+ ```
154
+
155
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
156
+
157
+ ## License
158
+
159
+ [MIT](./LICENSE) © Ilya Pirogov
package/dist/flow.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { EventEmitter } from "events";
2
+ import { type StateFlowLogEntry, type StateFlowLogHandler } from "./logger";
3
+ import { Result } from "./result";
4
+ import type { Signal } from "./signal";
5
+ import { type ArrayToRecord, type StateDefinition, type StateInstance, type StateVariant } from "./state";
6
+ export type StateFlowMeta = {
7
+ emitter: EventEmitter;
8
+ transitioning: Promise<Result> | true | null;
9
+ lockHolder: symbol | null;
10
+ lockQueue: Array<{
11
+ id: symbol;
12
+ resolve: () => void;
13
+ }>;
14
+ states: Array<StateDefinition>;
15
+ name: string;
16
+ logHandlers: StateFlowLogHandler[];
17
+ logGroup: {
18
+ label: string;
19
+ pending: Array<Promise<StateFlowLogEntry>>;
20
+ context: string | null;
21
+ } | null;
22
+ };
23
+ export interface FlowConfig {
24
+ logHandlers?: StateFlowLogHandler[];
25
+ }
26
+ type StateHandler<TProps> = (state: StateInstance<TProps>, context: unknown) => Result;
27
+ type ObserverHandler<TProps = unknown> = (state: StateInstance<TProps>) => void;
28
+ type ObserverComparerFn<TProps = unknown> = (a: TProps, b: TProps) => boolean;
29
+ type Disposer = {
30
+ [Symbol.dispose](): void;
31
+ };
32
+ export interface StateManager {
33
+ addEnterHandler<TProps>(state: StateVariant<TProps>, cb: StateHandler<TProps>, context?: unknown): void;
34
+ addExitHandler<TProps>(state: StateVariant<TProps>, cb: StateHandler<TProps>, context?: unknown): void;
35
+ addUpdateHandler<TProps>(state: StateVariant<TProps>, cb: StateHandler<TProps>, context?: unknown): void;
36
+ addRollbackHandler<TProps>(state: StateVariant<TProps>, cb: StateHandler<TProps>, context?: unknown): void;
37
+ }
38
+ type StateManagerCallback = (sm: StateManager) => void;
39
+ export type DispatchFn = ((signal: Signal, mute?: boolean) => Result) & {
40
+ [Symbol.asyncDispose](): Promise<void>;
41
+ };
42
+ export declare function applyFlow<TStates extends Array<object>>(target: ArrayToRecord<TStates>, states: TStates, initializer: StateManagerCallback, config?: FlowConfig): void;
43
+ export declare function dispatch(target: object, signal: Signal, mute?: boolean): Result;
44
+ export declare function lock(target: object, label?: string): Promise<DispatchFn>;
45
+ export declare function sync(target: object): Promise<void>;
46
+ export declare function observe<T>(target: object, stateVariants: StateVariant<T>[], handlerFn: ObserverHandler<T>, compareFn?: ObserverComparerFn<T>, ctx?: object): Disposer;
47
+ export declare function disposeFlow(target: object): void;
48
+ export {};
49
+ //# sourceMappingURL=flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../src/flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export { applyFlow, DispatchFn, dispatch, disposeFlow, lock, observe, StateManager, sync } from "./flow";
2
+ export { addGlobalLogHandler, consoleLogHandler, StateFlowLogEntry, StateFlowLogHandler, setConsoleLogSilenced, setGlobalDispatchContextProvider, } from "./logger";
3
+ export { Result, ResultCollector, ResultKind } from "./result";
4
+ export { defineSignal, Signal, StateSignal } from "./signal";
5
+ export { defineFlow, defineState, isState, StateResult, StateVariant, stateVar } from "./state";
6
+ export { PARSER, SIGNALS, STRING_REPR, VARIANT } from "./symbols";
7
+ export { Infer, StateFlowError, serializeDebug } from "./utils";
8
+ declare global {
9
+ var __STATE_FLOW__: {
10
+ version: string;
11
+ };
12
+ }
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACzG,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,EACrB,gCAAgC,GACjC,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChG,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEhE,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,cAAc,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC"}