@matheuspuel/state-machine 0.1.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/.eslintrc.js ADDED
@@ -0,0 +1,34 @@
1
+ module.exports = {
2
+ extends: [
3
+ 'eslint:recommended',
4
+ 'plugin:@typescript-eslint/recommended',
5
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
6
+ 'plugin:@typescript-eslint/strict',
7
+ ],
8
+ parserOptions: {
9
+ tsconfigRootDir: __dirname,
10
+ project: ['./tsconfig.json'],
11
+ ecmaVersion: 'latest',
12
+ sourceType: 'module',
13
+ },
14
+ ignorePatterns: [
15
+ '/.vscode/*',
16
+ '/coverage/*',
17
+ '/dist/*',
18
+ '/experiments/*',
19
+ '/node_modules/*',
20
+ '.eslintrc.js',
21
+ '.eslintrc.production.js',
22
+ ],
23
+ rules: {
24
+ // already checked by typescript
25
+ '@typescript-eslint/no-redeclare': 'off',
26
+ // warn instead of error
27
+ '@typescript-eslint/no-unused-vars': 'warn',
28
+ // allow namespace declaration for types
29
+ '@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
30
+ // warn instead of error to prevent covering more specific errors
31
+ '@typescript-eslint/no-unsafe-return': 'warn',
32
+ '@typescript-eslint/no-unsafe-call': 'warn',
33
+ },
34
+ }
@@ -0,0 +1,11 @@
1
+ const base = require('./.eslintrc.js')
2
+
3
+ module.exports = {
4
+ ...base,
5
+ extends: [...base.extends, 'prettier'],
6
+ plugins: [...(base.plugins ?? []), 'prettier'],
7
+ rules: {
8
+ ...base.rules,
9
+ 'prettier/prettier': ['warn'],
10
+ },
11
+ }
package/.prettierrc ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": false,
3
+ "trailingComma": "all",
4
+ "arrowParens": "avoid",
5
+ "singleQuote": true,
6
+ "printWidth": 80,
7
+ "tabWidth": 2,
8
+ "endOfLine": "lf"
9
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@matheuspuel/state-machine",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "module": "index.ts",
6
+ "exports": {
7
+ "./package.json": "./package.json",
8
+ ".": "./src/index.ts",
9
+ "./mahcines": "./src/machines/index.ts",
10
+ "./react": "./src/react/index.ts",
11
+ "./runtime": "./src/runtime/index.ts"
12
+ },
13
+ "scripts": {
14
+ "lint": "eslint . --max-warnings 0 --config .eslintrc.production.js",
15
+ "lint:fix": "eslint . --fix --config .eslintrc.production.js",
16
+ "typecheck": "tsc",
17
+ "test": "vitest"
18
+ },
19
+ "dependencies": {
20
+ "@matheuspuel/optic": "^0.1.0",
21
+ "effect": "^3.18.1",
22
+ "react": "19.1.0",
23
+ "use-sync-external-store": "^1.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "@effect/language-service": "^0.41.1",
27
+ "@effect/vitest": "^0.26.0",
28
+ "@types/react": "~19.1.17",
29
+ "@types/use-sync-external-store": "^1.5.0",
30
+ "@typescript-eslint/parser": "^8.29.1",
31
+ "@vitest/coverage-v8": "^3.2.4",
32
+ "cross-env": "^7.0.3",
33
+ "eslint": "^8.57.1",
34
+ "eslint-config-prettier": "^10.1.1",
35
+ "eslint-plugin-prettier": "^5.2.6",
36
+ "prettier": "^3.6.2",
37
+ "tsx": "^4.20.6",
38
+ "typescript": "^5.9.3",
39
+ "vite": "^6.3.5",
40
+ "vitest": "^3.2.4"
41
+ }
42
+ }
@@ -0,0 +1,2 @@
1
+ onlyBuiltDependencies:
2
+ - esbuild
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { Optic } from '@matheuspuel/optic'
2
+
3
+ export type MachineStoreBase<State> = {
4
+ get: () => State
5
+ update: (stateUpdate: (_: State) => State) => void
6
+ }
7
+
8
+ export type MachineStore<State> = MachineStoreBase<State> & {
9
+ zoom: <A, Optional extends boolean>(
10
+ f: (optic: Optic<State, State>) => Optic<A, State, Optional>,
11
+ ) => MachineStoreBase<A>
12
+ }
13
+
14
+ export const makeMachineStore = <State>(
15
+ base: MachineStoreBase<State>,
16
+ ): MachineStore<State> => ({
17
+ ...base,
18
+ zoom: zoomF => ({
19
+ get: () => (zoomF(Optic.id<State>()) as any).get(base.get()),
20
+ getOption: () => zoomF(Optic.id<State>()).getOption(base.get()),
21
+ update: f => base.update(zoomF(Optic.id<State>()).update(f)),
22
+ }),
23
+ })
24
+
25
+ export type StateAction<A extends unknown[], B> = (...args: A) => B
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ export type AnyStateAction = StateAction<any[], unknown>
29
+
30
+ export type AnyStateActions = Record<string, AnyStateAction>
31
+
32
+ export type StateMachine<State, Actions extends AnyStateActions> = {
33
+ initialState: State
34
+ actions: (machine: { Store: MachineStore<State> }) => Actions
35
+ start?: (machine: {
36
+ Store: MachineStore<State>
37
+ }) => undefined | Promise<unknown>
38
+ onUpdate?: (state: State) => void | Promise<void>
39
+ }
40
+
41
+ export const makeStateMachine =
42
+ <State>() =>
43
+ <Actions extends AnyStateActions>(args: StateMachine<State, Actions>) =>
44
+ args
45
+
46
+ export type PreparedStateActions<Actions extends AnyStateActions> = Actions
47
+
48
+ export const prepareStateMachineActions = <
49
+ State,
50
+ Actions extends AnyStateActions,
51
+ >(
52
+ machine: StateMachine<State, Actions>,
53
+ store: MachineStore<State>,
54
+ ): PreparedStateActions<Actions> => machine.actions({ Store: store })
@@ -0,0 +1 @@
1
+ export * from './simple'
@@ -0,0 +1,7 @@
1
+ import { makeStateMachine } from '..'
2
+
3
+ export const simpleStateMachine = <A>(initialState: A) =>
4
+ makeStateMachine<A>()({
5
+ initialState,
6
+ actions: ({ Store }) => ({ set: (value: A) => Store.update(() => value) }),
7
+ })
@@ -0,0 +1,25 @@
1
+ import * as React from 'react'
2
+ import { AnyStateActions, PreparedStateActions, StateMachine } from '..'
3
+ import { useStateMachine } from './useStateMachine'
4
+
5
+ export const makeStateMachineContext = <State, Actions extends AnyStateActions>(
6
+ machine: StateMachine<State, Actions>,
7
+ ) => {
8
+ const Context = React.createContext<{
9
+ state: State
10
+ actions: PreparedStateActions<Actions>
11
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
12
+ }>(undefined as any)
13
+ return {
14
+ Provider: (props: { children: React.ReactNode }) => {
15
+ const value = useStateMachine(machine)
16
+ return <Context.Provider value={value}>{props.children}</Context.Provider>
17
+ },
18
+ useActions: () => React.useContext(Context).actions,
19
+ useSelector: <A,>(fn: (state: State) => A) => {
20
+ const state = React.useContext(Context).state
21
+ // eslint-disable-next-line react-hooks/exhaustive-deps
22
+ return React.useMemo(() => fn(state), [state])
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ export * from './context'
2
+ export * from './useSelector'
3
+ export * from './useStateMachine'
@@ -0,0 +1,25 @@
1
+ import { Effect, Equal, Equivalence } from 'effect'
2
+ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'
3
+ import { AnyStateActions } from '../index.js'
4
+ import { RunningStateMachine } from '../runtime'
5
+
6
+ export const makeUseSelector =
7
+ <State, Actions extends AnyStateActions>(
8
+ stateMachine: RunningStateMachine<State, Actions>,
9
+ ) =>
10
+ <A>(
11
+ selector: (state: State) => A,
12
+ equivalence?: Equivalence.Equivalence<A>,
13
+ ): A =>
14
+ useSyncExternalStoreWithSelector<State, A>(
15
+ onChange => {
16
+ const subscription = stateMachine
17
+ .subscribe(() => Effect.sync(onChange))
18
+ .pipe(Effect.runSync)
19
+ return () => subscription.unsubscribe.pipe(Effect.runSync)
20
+ },
21
+ () => stateMachine.ref.get.pipe(Effect.runSync),
22
+ undefined,
23
+ selector,
24
+ equivalence ?? Equal.equivalence<A>(),
25
+ )
@@ -0,0 +1,42 @@
1
+ import * as React from 'react'
2
+ import {
3
+ AnyStateActions,
4
+ makeMachineStore,
5
+ prepareStateMachineActions,
6
+ StateMachine,
7
+ } from '..'
8
+
9
+ export const useStateMachine = <State, Actions extends AnyStateActions>(
10
+ stateMachine:
11
+ | StateMachine<State, Actions>
12
+ | (() => StateMachine<State, Actions>),
13
+ ) => {
14
+ const machine =
15
+ typeof stateMachine === 'function' ? stateMachine() : stateMachine
16
+ const [state, setState] = React.useState(() => machine.initialState)
17
+ const stateRef = React.useRef(machine.initialState)
18
+ const getState = React.useMemo(() => () => stateRef.current, [])
19
+ const store = React.useMemo(
20
+ () =>
21
+ makeMachineStore<State>({
22
+ get: getState,
23
+ update: fn => {
24
+ const previousState = getState()
25
+ const state = fn(previousState)
26
+ stateRef.current = state
27
+ void machine.onUpdate?.(state)
28
+ setState(() => state)
29
+ },
30
+ }),
31
+ [machine, getState],
32
+ )
33
+ const actions = React.useMemo(
34
+ () => prepareStateMachineActions(machine, store),
35
+ [machine, store],
36
+ )
37
+ React.useEffect(() => {
38
+ void machine.start?.({ Store: store })
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [])
41
+ return { state, actions }
42
+ }
@@ -0,0 +1,56 @@
1
+ import { Array, Effect, Ref } from 'effect'
2
+ import {
3
+ AnyStateActions,
4
+ makeMachineStore,
5
+ PreparedStateActions,
6
+ prepareStateMachineActions,
7
+ StateMachine,
8
+ } from '..'
9
+
10
+ type SubscriptionTask<State> = (state: State) => Effect.Effect<void>
11
+
12
+ export type RunningStateMachine<State, Actions extends AnyStateActions> = {
13
+ ref: Ref.Ref<State>
14
+ actions: PreparedStateActions<Actions>
15
+ startPromise: Promise<unknown> | undefined
16
+ subscribe: (
17
+ task: SubscriptionTask<State>,
18
+ ) => Effect.Effect<
19
+ { unsubscribe: Effect.Effect<void, never, never> },
20
+ never,
21
+ never
22
+ >
23
+ }
24
+
25
+ export const runStateMachine = <State, Actions extends AnyStateActions>(
26
+ machine: StateMachine<State, Actions>,
27
+ ): RunningStateMachine<State, Actions> => {
28
+ const subscriptionsRef = Ref.unsafeMake<SubscriptionTask<State>[]>([])
29
+ const ref = Ref.unsafeMake(machine.initialState)
30
+ const store = makeMachineStore<State>({
31
+ get: () => ref.get.pipe(Effect.runSync),
32
+ update: fn =>
33
+ Effect.gen(function* () {
34
+ const previousState = yield* ref.get
35
+ const state = fn(previousState)
36
+ yield* Ref.set(ref, state)
37
+ yield* Effect.forEach(yield* subscriptionsRef.get, _ => _(state)).pipe(
38
+ Effect.exit,
39
+ )
40
+ void machine.onUpdate?.(state)
41
+ }).pipe(Effect.runSync),
42
+ })
43
+ const actions = prepareStateMachineActions(machine, store)
44
+ const subscribe = (task: SubscriptionTask<State>) =>
45
+ Effect.gen(function* () {
46
+ yield* Ref.update(subscriptionsRef, Array.append(task))
47
+ return {
48
+ unsubscribe: Ref.update(
49
+ subscriptionsRef,
50
+ Array.filter(_ => _ !== task),
51
+ ),
52
+ }
53
+ })
54
+ const startPromise = machine.start?.({ Store: store })
55
+ return { ref, actions, subscribe, startPromise }
56
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "skipLibCheck": true,
5
+ "moduleResolution": "bundler",
6
+ "target": "ESNext",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "plugins": [{ "name": "@effect/language-service" }]
12
+ },
13
+ "include": ["**/*.ts", "**/*.tsx"]
14
+ }
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {},
5
+ })