@matheuspuel/state-machine 0.1.0 → 0.2.2
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/package.json +4 -2
- package/src/StateMachine.ts +3 -0
- package/src/definition.ts +49 -0
- package/src/form/definition.ts +33 -0
- package/src/form/index.ts +1 -0
- package/src/index.ts +1 -54
- package/src/machines/Form.test.ts +68 -0
- package/src/machines/Form.ts +132 -0
- package/src/machines/FormValue.test.ts +19 -0
- package/src/machines/FormValue.ts +17 -0
- package/src/machines/Struct.test.ts +29 -0
- package/src/machines/Struct.ts +47 -0
- package/src/machines/index.ts +4 -1
- package/src/machines/of.test.ts +16 -0
- package/src/machines/of.ts +11 -0
- package/src/react/context.tsx +5 -1
- package/src/react/useSelector.ts +3 -3
- package/src/react/useStateMachine.ts +5 -5
- package/src/runtime/index.ts +8 -8
- package/src/machines/simple.ts +0 -7
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matheuspuel/state-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./package.json": "./package.json",
|
|
8
8
|
".": "./src/index.ts",
|
|
9
|
-
"./
|
|
9
|
+
"./definition": "./src/definition.ts",
|
|
10
|
+
"./form": "./src/form/index.ts",
|
|
11
|
+
"./machines": "./src/machines/index.ts",
|
|
10
12
|
"./react": "./src/react/index.ts",
|
|
11
13
|
"./runtime": "./src/runtime/index.ts"
|
|
12
14
|
},
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Optic } from '@matheuspuel/optic'
|
|
2
|
+
|
|
3
|
+
export type StoreBase<State> = {
|
|
4
|
+
get: () => State
|
|
5
|
+
update: (stateUpdate: (_: State) => State) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Store<State> = StoreBase<State> & {
|
|
9
|
+
zoom: <A, Optional extends boolean>(
|
|
10
|
+
f: (optic: Optic<State, State>) => Optic<A, State, Optional>,
|
|
11
|
+
) => StoreBase<A>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const makeStore = <State>(base: StoreBase<State>): Store<State> => ({
|
|
15
|
+
...base,
|
|
16
|
+
zoom: zoomF => ({
|
|
17
|
+
get: () => (zoomF(Optic.id<State>()) as any).get(base.get()),
|
|
18
|
+
getOption: () => zoomF(Optic.id<State>()).getOption(base.get()),
|
|
19
|
+
update: f => base.update(zoomF(Optic.id<State>()).update(f)),
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export type StateAction<A extends unknown[], B> = (...args: A) => B
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
export type AnyStateAction = StateAction<any[], unknown>
|
|
27
|
+
|
|
28
|
+
export type AnyStateActions = {
|
|
29
|
+
[key: string]: AnyStateAction | AnyStateActions
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type StateMachine<State, Actions extends AnyStateActions> = {
|
|
33
|
+
initialState: State
|
|
34
|
+
actions: (machine: { Store: Store<State> }) => Actions
|
|
35
|
+
start?: (machine: { Store: Store<State> }) => undefined | Promise<unknown>
|
|
36
|
+
onUpdate?: (state: State) => void | Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const make =
|
|
40
|
+
<State>() =>
|
|
41
|
+
<Actions extends AnyStateActions>(args: StateMachine<State, Actions>) =>
|
|
42
|
+
args
|
|
43
|
+
|
|
44
|
+
export type PreparedStateActions<Actions extends AnyStateActions> = Actions
|
|
45
|
+
|
|
46
|
+
export const prepareActions = <State, Actions extends AnyStateActions>(
|
|
47
|
+
machine: StateMachine<State, Actions>,
|
|
48
|
+
store: Store<State>,
|
|
49
|
+
): PreparedStateActions<Actions> => machine.actions({ Store: store })
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect'
|
|
2
|
+
import { ParseError } from 'effect/ParseResult'
|
|
3
|
+
|
|
4
|
+
export type AnyForm = {
|
|
5
|
+
[key: string]: AnyForm | FormField<any, any, any>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FormField<A, I, E> = {
|
|
9
|
+
initial: I
|
|
10
|
+
validate: (value: I) => Effect.Effect<A, E>
|
|
11
|
+
fromData: (data: A) => I
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const field = <A, I, E>(args: FormField<A, I, E>) => args
|
|
15
|
+
|
|
16
|
+
export const fieldSchema = <A, I>(args: {
|
|
17
|
+
initial: I
|
|
18
|
+
schema: Schema.Schema<A, I>
|
|
19
|
+
}) =>
|
|
20
|
+
field<A, I, ParseError>({
|
|
21
|
+
initial: args.initial,
|
|
22
|
+
validate: Schema.decode(args.schema),
|
|
23
|
+
fromData: Schema.encodeSync(args.schema),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const fieldSuccess = <A>(args: { initial: A }) =>
|
|
27
|
+
field<A, A, never>({
|
|
28
|
+
initial: args.initial,
|
|
29
|
+
validate: Effect.succeed,
|
|
30
|
+
fromData: _ => _,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const Struct = <Fields extends AnyForm>(fields: Fields) => fields
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Form from './definition'
|
package/src/index.ts
CHANGED
|
@@ -1,54 +1 @@
|
|
|
1
|
-
|
|
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 })
|
|
1
|
+
export * as StateMachine from './StateMachine'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { StateMachine } from '..'
|
|
4
|
+
import { Form } from '../form'
|
|
5
|
+
import { run } from '../runtime'
|
|
6
|
+
|
|
7
|
+
describe('Form', () => {
|
|
8
|
+
it('should work', () => {
|
|
9
|
+
const formField = Form.field({
|
|
10
|
+
initial: 0,
|
|
11
|
+
validate: _ =>
|
|
12
|
+
_ > 1 ? Effect.succeed({ n: _ }) : Effect.fail('low' as const),
|
|
13
|
+
fromData: (data: { n: number }) => data.n,
|
|
14
|
+
})
|
|
15
|
+
const form = Form.Struct({
|
|
16
|
+
a: formField,
|
|
17
|
+
b: Form.Struct({ c: formField }),
|
|
18
|
+
})
|
|
19
|
+
const machine = StateMachine.Form(form)
|
|
20
|
+
const instance = run(machine)
|
|
21
|
+
const getState = () => instance.ref.get.pipe(Effect.runSync)
|
|
22
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
23
|
+
a: { value: 0, error: null },
|
|
24
|
+
b: { c: { value: 0, error: null } },
|
|
25
|
+
})
|
|
26
|
+
instance.actions.a.set(1)
|
|
27
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
28
|
+
a: { value: 1, error: null },
|
|
29
|
+
b: { c: { value: 0, error: null } },
|
|
30
|
+
})
|
|
31
|
+
instance.actions.validate().pipe(Effect.runSyncExit)
|
|
32
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
33
|
+
a: { value: 1, error: 'low' },
|
|
34
|
+
b: { c: { value: 0, error: 'low' } },
|
|
35
|
+
})
|
|
36
|
+
instance.actions.a.update(_ => _ + 1)
|
|
37
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
38
|
+
a: { value: 2, error: null },
|
|
39
|
+
b: { c: { value: 0, error: 'low' } },
|
|
40
|
+
})
|
|
41
|
+
instance.actions.b.c.set(1)
|
|
42
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
43
|
+
a: { value: 2, error: null },
|
|
44
|
+
b: { c: { value: 1, error: null } },
|
|
45
|
+
})
|
|
46
|
+
instance.actions.validate().pipe(Effect.runSyncExit)
|
|
47
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
48
|
+
a: { value: 2, error: null },
|
|
49
|
+
b: { c: { value: 1, error: 'low' } },
|
|
50
|
+
})
|
|
51
|
+
instance.actions.b.c.update(_ => _ + 1)
|
|
52
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
53
|
+
a: { value: 2, error: null },
|
|
54
|
+
b: { c: { value: 2, error: null } },
|
|
55
|
+
})
|
|
56
|
+
const data = instance.actions.validate().pipe(Effect.runSync)
|
|
57
|
+
expect(data).toStrictEqual<typeof data>({ a: { n: 2 }, b: { c: { n: 2 } } })
|
|
58
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
59
|
+
a: { value: 2, error: null },
|
|
60
|
+
b: { c: { value: 2, error: null } },
|
|
61
|
+
})
|
|
62
|
+
instance.actions.setStateFromData({ a: { n: 3 }, b: { c: { n: 3 } } })
|
|
63
|
+
expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
|
|
64
|
+
a: { value: 3, error: null },
|
|
65
|
+
b: { c: { value: 3, error: null } },
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Effect, Either, Option, Record } from 'effect'
|
|
2
|
+
import { make, makeStore, Store } from '../definition'
|
|
3
|
+
import { AnyForm, FormField } from '../form/definition'
|
|
4
|
+
|
|
5
|
+
export type FormState<Form extends AnyForm> = {
|
|
6
|
+
[K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
|
|
7
|
+
? { value: I; error: E | null }
|
|
8
|
+
: Form[K] extends AnyForm
|
|
9
|
+
? FormState<Form[K]>
|
|
10
|
+
: never
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FormData<Form extends AnyForm> = {
|
|
14
|
+
[K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
|
|
15
|
+
? A
|
|
16
|
+
: Form[K] extends AnyForm
|
|
17
|
+
? FormData<Form[K]>
|
|
18
|
+
: never
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FormError<Form extends AnyForm> = {
|
|
22
|
+
[K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
|
|
23
|
+
? Option.Option<E>
|
|
24
|
+
: Form[K] extends AnyForm
|
|
25
|
+
? FormError<Form[K]>
|
|
26
|
+
: never
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FormActions<Form extends AnyForm> = {
|
|
30
|
+
[K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
|
|
31
|
+
? {
|
|
32
|
+
set: (value: I) => void
|
|
33
|
+
update: (f: (previous: I) => I) => void
|
|
34
|
+
error: { set: (error: E | null) => void }
|
|
35
|
+
}
|
|
36
|
+
: Form[K] extends AnyForm
|
|
37
|
+
? FormActions<Form[K]>
|
|
38
|
+
: never
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isField = (
|
|
42
|
+
value: AnyForm | FormField<any, any, any>,
|
|
43
|
+
): value is FormField<any, any, any> => typeof value.validate === 'function'
|
|
44
|
+
|
|
45
|
+
export const Form = <F extends AnyForm>(form: F) => {
|
|
46
|
+
const getInitialState = <F extends AnyForm>(form: F): FormState<F> =>
|
|
47
|
+
Record.map(form, _ =>
|
|
48
|
+
isField(_) ? { value: _.initial, error: null } : getInitialState(_),
|
|
49
|
+
) as any
|
|
50
|
+
const getActions = <F extends AnyForm>(
|
|
51
|
+
form: F,
|
|
52
|
+
Store: Store<FormState<F>>,
|
|
53
|
+
): FormActions<F> =>
|
|
54
|
+
Record.map(form, (_, key) =>
|
|
55
|
+
isField(_)
|
|
56
|
+
? {
|
|
57
|
+
update: (f: (previous: any) => any) =>
|
|
58
|
+
Store.update(_ => ({
|
|
59
|
+
..._,
|
|
60
|
+
[key]: { value: f(_[key]!.value), error: null },
|
|
61
|
+
})),
|
|
62
|
+
set: (value: any) =>
|
|
63
|
+
Store.update(_ => ({ ..._, [key]: { value, error: null } })),
|
|
64
|
+
error: {
|
|
65
|
+
set: (error: any) =>
|
|
66
|
+
Store.update(_ => ({ ..._, [key]: { value: _.value, error } })),
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
: getActions(
|
|
70
|
+
_,
|
|
71
|
+
makeStore({
|
|
72
|
+
get: () => Store.get()[key],
|
|
73
|
+
update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
|
|
74
|
+
}) as any,
|
|
75
|
+
),
|
|
76
|
+
) as any
|
|
77
|
+
const validate = <F extends AnyForm>(
|
|
78
|
+
form: F,
|
|
79
|
+
Store: Store<FormState<F>>,
|
|
80
|
+
): any =>
|
|
81
|
+
Effect.all(
|
|
82
|
+
Record.map(form, (_, key) =>
|
|
83
|
+
isField(_)
|
|
84
|
+
? _.validate(Store.get()[key]!.value).pipe(
|
|
85
|
+
Effect.either,
|
|
86
|
+
Effect.tap(e =>
|
|
87
|
+
Store.update(_ => ({
|
|
88
|
+
..._,
|
|
89
|
+
[key]: {
|
|
90
|
+
value: _[key]!.value,
|
|
91
|
+
error: Option.getOrNull(Either.getLeft(e)),
|
|
92
|
+
},
|
|
93
|
+
})),
|
|
94
|
+
),
|
|
95
|
+
Effect.flatten,
|
|
96
|
+
)
|
|
97
|
+
: validate(
|
|
98
|
+
_,
|
|
99
|
+
makeStore({
|
|
100
|
+
get: () => Store.get()[key],
|
|
101
|
+
update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
|
|
102
|
+
}) as any,
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
{ mode: 'validate' },
|
|
106
|
+
) as any
|
|
107
|
+
return make<FormState<F>>()<
|
|
108
|
+
FormActions<F> & {
|
|
109
|
+
validate: () => Effect.Effect<FormData<F>, FormError<F>>
|
|
110
|
+
setStateFromData: (data: FormData<F>) => void
|
|
111
|
+
}
|
|
112
|
+
>({
|
|
113
|
+
initialState: getInitialState(form),
|
|
114
|
+
actions: ({ Store }) => ({
|
|
115
|
+
...getActions(form, Store),
|
|
116
|
+
validate: () => validate(form, Store),
|
|
117
|
+
setStateFromData: (data: FormData<F>) =>
|
|
118
|
+
Store.update(() => {
|
|
119
|
+
const updateState = <F extends AnyForm>(
|
|
120
|
+
form: F,
|
|
121
|
+
data: FormData<F>,
|
|
122
|
+
): any =>
|
|
123
|
+
Record.map(form, (_, key) =>
|
|
124
|
+
isField(_)
|
|
125
|
+
? { value: _.fromData(data[key]), error: null }
|
|
126
|
+
: updateState(_, data[key]!),
|
|
127
|
+
)
|
|
128
|
+
return updateState(form, data)
|
|
129
|
+
}),
|
|
130
|
+
}),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { run } from '../runtime'
|
|
4
|
+
import { FormValue } from './FormValue'
|
|
5
|
+
|
|
6
|
+
describe('FormValue', () => {
|
|
7
|
+
it('should work', () => {
|
|
8
|
+
const machine = FormValue<number, string>(0)
|
|
9
|
+
const instance = run(machine)
|
|
10
|
+
const getState = () => instance.ref.get.pipe(Effect.runSync)
|
|
11
|
+
expect(getState()).toStrictEqual({ value: 0, error: null })
|
|
12
|
+
instance.actions.set(1)
|
|
13
|
+
expect(getState()).toStrictEqual({ value: 1, error: null })
|
|
14
|
+
instance.actions.error.set('a')
|
|
15
|
+
expect(getState()).toStrictEqual({ value: 1, error: 'a' })
|
|
16
|
+
instance.actions.set(2)
|
|
17
|
+
expect(getState()).toStrictEqual({ value: 2, error: null })
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { make } from '../definition'
|
|
2
|
+
|
|
3
|
+
export type FormValue<A, E> = { value: A; error: E | null }
|
|
4
|
+
|
|
5
|
+
export const FormValue = <A, E>(initialValue: A) =>
|
|
6
|
+
make<FormValue<A, E>>()({
|
|
7
|
+
initialState: { value: initialValue, error: null },
|
|
8
|
+
actions: ({ Store }) => ({
|
|
9
|
+
update: (f: (previous: A) => A) =>
|
|
10
|
+
Store.update(_ => ({ value: f(_.value), error: null })),
|
|
11
|
+
set: (value: A) => Store.update(() => ({ value, error: null })),
|
|
12
|
+
error: {
|
|
13
|
+
set: (error: E | null) =>
|
|
14
|
+
Store.update(_ => ({ value: _.value, error })),
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { run } from '../runtime'
|
|
4
|
+
import { Struct } from './Struct'
|
|
5
|
+
import { of } from './of'
|
|
6
|
+
|
|
7
|
+
describe('Struct', () => {
|
|
8
|
+
it('should work', () => {
|
|
9
|
+
const machine = Struct(
|
|
10
|
+
{
|
|
11
|
+
a: of(0),
|
|
12
|
+
b: of(''),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
extraActions: ({ Store }) => ({
|
|
16
|
+
get: () => Store.get(),
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
const instance = run(machine)
|
|
21
|
+
const getState = () => instance.ref.get.pipe(Effect.runSync)
|
|
22
|
+
expect(getState()).toStrictEqual({ a: 0, b: '' })
|
|
23
|
+
instance.actions.a.set(1)
|
|
24
|
+
expect(getState()).toStrictEqual({ a: 1, b: '' })
|
|
25
|
+
instance.actions.b.set('a')
|
|
26
|
+
expect(getState()).toStrictEqual({ a: 1, b: 'a' })
|
|
27
|
+
expect(instance.actions.get()).toStrictEqual({ a: 1, b: 'a' })
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Record } from 'effect'
|
|
2
|
+
import {
|
|
3
|
+
AnyStateActions,
|
|
4
|
+
make,
|
|
5
|
+
makeStore,
|
|
6
|
+
StateMachine,
|
|
7
|
+
Store,
|
|
8
|
+
} from '../definition'
|
|
9
|
+
|
|
10
|
+
export const Struct = <
|
|
11
|
+
A extends Record<string, StateMachine<any, AnyStateActions>>,
|
|
12
|
+
ExtraActions extends AnyStateActions = {},
|
|
13
|
+
>(
|
|
14
|
+
fields: A,
|
|
15
|
+
options?: {
|
|
16
|
+
extraActions?: (machine: {
|
|
17
|
+
Store: Store<{
|
|
18
|
+
[K in keyof A]: A[K] extends StateMachine<infer State, any>
|
|
19
|
+
? State
|
|
20
|
+
: never
|
|
21
|
+
}>
|
|
22
|
+
}) => ExtraActions
|
|
23
|
+
},
|
|
24
|
+
) =>
|
|
25
|
+
make<{
|
|
26
|
+
[K in keyof A]: A[K] extends StateMachine<infer State, any> ? State : never
|
|
27
|
+
}>()<
|
|
28
|
+
{
|
|
29
|
+
[K in keyof A]: A[K] extends StateMachine<any, infer Actions>
|
|
30
|
+
? Actions
|
|
31
|
+
: never
|
|
32
|
+
} & ExtraActions
|
|
33
|
+
>({
|
|
34
|
+
initialState: Record.map(fields, _ => _.initialState) as any,
|
|
35
|
+
actions: ({ Store }) => ({
|
|
36
|
+
...(Record.map(fields, (_, key) =>
|
|
37
|
+
_.actions({
|
|
38
|
+
Store: makeStore({
|
|
39
|
+
get: () => Store.get()[key],
|
|
40
|
+
update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
|
|
41
|
+
}) as any,
|
|
42
|
+
}),
|
|
43
|
+
) as any),
|
|
44
|
+
...options?.extraActions?.({ Store }),
|
|
45
|
+
}),
|
|
46
|
+
// TODO implement other options
|
|
47
|
+
})
|
package/src/machines/index.ts
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { run } from '../runtime'
|
|
4
|
+
import { of } from './of'
|
|
5
|
+
|
|
6
|
+
describe('of', () => {
|
|
7
|
+
it('should work', () => {
|
|
8
|
+
const machine = of('')
|
|
9
|
+
const instance = run(machine)
|
|
10
|
+
instance.actions.set('a')
|
|
11
|
+
const state = instance.ref.get.pipe(Effect.runSync)
|
|
12
|
+
expect(state).toStrictEqual('a')
|
|
13
|
+
const data = instance.actions.get()
|
|
14
|
+
expect(data).toStrictEqual('a')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { make } from '../definition'
|
|
2
|
+
|
|
3
|
+
export const of = <A>(initialState: A) =>
|
|
4
|
+
make<A>()({
|
|
5
|
+
initialState,
|
|
6
|
+
actions: ({ Store }) => ({
|
|
7
|
+
get: Store.get,
|
|
8
|
+
update: (f: (previous: A) => A) => Store.update(f),
|
|
9
|
+
set: (value: A) => Store.update(() => value),
|
|
10
|
+
}),
|
|
11
|
+
})
|
package/src/react/context.tsx
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AnyStateActions,
|
|
4
|
+
PreparedStateActions,
|
|
5
|
+
StateMachine,
|
|
6
|
+
} from '../definition'
|
|
3
7
|
import { useStateMachine } from './useStateMachine'
|
|
4
8
|
|
|
5
9
|
export const makeStateMachineContext = <State, Actions extends AnyStateActions>(
|
package/src/react/useSelector.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Effect, Equal, Equivalence } from 'effect'
|
|
2
2
|
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'
|
|
3
|
-
import { AnyStateActions } from '../
|
|
4
|
-
import {
|
|
3
|
+
import { AnyStateActions } from '../definition.js'
|
|
4
|
+
import { Instance } from '../runtime'
|
|
5
5
|
|
|
6
6
|
export const makeUseSelector =
|
|
7
7
|
<State, Actions extends AnyStateActions>(
|
|
8
|
-
stateMachine:
|
|
8
|
+
stateMachine: Instance<State, Actions>,
|
|
9
9
|
) =>
|
|
10
10
|
<A>(
|
|
11
11
|
selector: (state: State) => A,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
AnyStateActions,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
makeStore,
|
|
5
|
+
prepareActions,
|
|
6
6
|
StateMachine,
|
|
7
|
-
} from '
|
|
7
|
+
} from '../definition'
|
|
8
8
|
|
|
9
9
|
export const useStateMachine = <State, Actions extends AnyStateActions>(
|
|
10
10
|
stateMachine:
|
|
@@ -18,7 +18,7 @@ export const useStateMachine = <State, Actions extends AnyStateActions>(
|
|
|
18
18
|
const getState = React.useMemo(() => () => stateRef.current, [])
|
|
19
19
|
const store = React.useMemo(
|
|
20
20
|
() =>
|
|
21
|
-
|
|
21
|
+
makeStore<State>({
|
|
22
22
|
get: getState,
|
|
23
23
|
update: fn => {
|
|
24
24
|
const previousState = getState()
|
|
@@ -31,7 +31,7 @@ export const useStateMachine = <State, Actions extends AnyStateActions>(
|
|
|
31
31
|
[machine, getState],
|
|
32
32
|
)
|
|
33
33
|
const actions = React.useMemo(
|
|
34
|
-
() =>
|
|
34
|
+
() => prepareActions(machine, store),
|
|
35
35
|
[machine, store],
|
|
36
36
|
)
|
|
37
37
|
React.useEffect(() => {
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Array, Effect, Ref } from 'effect'
|
|
2
2
|
import {
|
|
3
3
|
AnyStateActions,
|
|
4
|
-
|
|
4
|
+
makeStore,
|
|
5
|
+
prepareActions,
|
|
5
6
|
PreparedStateActions,
|
|
6
|
-
prepareStateMachineActions,
|
|
7
7
|
StateMachine,
|
|
8
|
-
} from '
|
|
8
|
+
} from '../definition'
|
|
9
9
|
|
|
10
10
|
type SubscriptionTask<State> = (state: State) => Effect.Effect<void>
|
|
11
11
|
|
|
12
|
-
export type
|
|
12
|
+
export type Instance<State, Actions extends AnyStateActions> = {
|
|
13
13
|
ref: Ref.Ref<State>
|
|
14
14
|
actions: PreparedStateActions<Actions>
|
|
15
15
|
startPromise: Promise<unknown> | undefined
|
|
@@ -22,12 +22,12 @@ export type RunningStateMachine<State, Actions extends AnyStateActions> = {
|
|
|
22
22
|
>
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export const
|
|
25
|
+
export const run = <State, Actions extends AnyStateActions>(
|
|
26
26
|
machine: StateMachine<State, Actions>,
|
|
27
|
-
):
|
|
27
|
+
): Instance<State, Actions> => {
|
|
28
28
|
const subscriptionsRef = Ref.unsafeMake<SubscriptionTask<State>[]>([])
|
|
29
29
|
const ref = Ref.unsafeMake(machine.initialState)
|
|
30
|
-
const store =
|
|
30
|
+
const store = makeStore<State>({
|
|
31
31
|
get: () => ref.get.pipe(Effect.runSync),
|
|
32
32
|
update: fn =>
|
|
33
33
|
Effect.gen(function* () {
|
|
@@ -40,7 +40,7 @@ export const runStateMachine = <State, Actions extends AnyStateActions>(
|
|
|
40
40
|
void machine.onUpdate?.(state)
|
|
41
41
|
}).pipe(Effect.runSync),
|
|
42
42
|
})
|
|
43
|
-
const actions =
|
|
43
|
+
const actions = prepareActions(machine, store)
|
|
44
44
|
const subscribe = (task: SubscriptionTask<State>) =>
|
|
45
45
|
Effect.gen(function* () {
|
|
46
46
|
yield* Ref.update(subscriptionsRef, Array.append(task))
|