174 lines
7.4 KiB
Swift
174 lines
7.4 KiB
Swift
//
|
|
// Copyright Amazon.com Inc. or its affiliates.
|
|
// All Rights Reserved.
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
/// A protocol describing the discrete attributes describing a system, and
|
|
/// providing mechanisms for changing those attributes in a predictable,
|
|
/// mechanistic way.
|
|
///
|
|
/// ### Properties of a State
|
|
///
|
|
/// States are immutable, mutually-exclusive, trees of value attributes:
|
|
/// - **Immutable**: States are not directly mutable. Instead, new states are
|
|
/// resolved by applying a State's `resolve` logic against the current value of the
|
|
/// state and a new `StateMachineEvent` to return a new `State` value.
|
|
/// - **Mutually exclusive**: A system's State is exactly equivalent to the set of
|
|
/// values that compose it. Thus, if two State values have identical properties,
|
|
/// they are themselves identical. From a practical standpoint, this means States
|
|
/// have value, not reference, semantics.
|
|
/// - **Trees**: Each State has its own set of attributes, and zero or more
|
|
/// substates. The "local" attributes of a State may be derived from the values of
|
|
/// its substates, or they may evolve independently in response to Events. A State
|
|
/// may have at most one "parent" State.
|
|
///
|
|
/// ### Resolving a State
|
|
///
|
|
/// States evolve mechanistically by applying resolution rules that evaluate
|
|
/// incoming Events against the current values of a State's attributes and
|
|
/// substates. The algorithm by which a State resolves an StateMachineEvent is:
|
|
///
|
|
/// - Traverse the State tree depth-first
|
|
/// - Resolve each leaf State (that is, each State that has no substates) by
|
|
/// invoking `resolve(oldState:event:)`. The return value of that method is a
|
|
/// `StateResolution` that contains both a new State, and a set of zero or more
|
|
/// `Effect`s. (See "Side effects" below)
|
|
/// - The parent State assigns the new substate value to the appropriate property,
|
|
/// and appends the returned Effects to the list of Effects to be returned in the
|
|
/// parent State's own `StateResolution`
|
|
/// - Each inner node resolves its own attributes by evaluating the new values of
|
|
/// its substates, the current values of its own properties, and the triggering
|
|
/// StateMachineEvent
|
|
/// - The inner node appends zero or more Effects to the list of effects to be
|
|
/// performed
|
|
/// - The inner node returns its new values (which are the new local values plus
|
|
/// the new values of all substates), and a list of Effects (which are the effects
|
|
/// requested by all substates, plus the effects requested by local state
|
|
/// resolution) in a `StateResolution`
|
|
/// - The process continues up to the "root" State
|
|
/// - The State Machine stores the new composite state as the new state of the
|
|
/// System
|
|
/// - The State Machine dispatches Effects for resolution and execution
|
|
///
|
|
/// ### Side effects
|
|
///
|
|
/// States may wish to perform "side effects" in response to an StateMachineEvent.
|
|
/// Side effects are interactions outside the assignment of a State's own property
|
|
/// values, such as:
|
|
/// - Emitting a new StateMachineEvent to indicate an important state change
|
|
/// - Interacting with an outside system such as making a network call or reading
|
|
/// from storage
|
|
/// - Starting or canceling a timer
|
|
///
|
|
/// Side effects are part of the return value of a State's
|
|
/// `resolve(oldState:event:)` method. They are resolved and executed by the State
|
|
/// Machine after the new State is fully resolved and applied. See `Effect` for
|
|
/// more details on Effect resolution
|
|
///
|
|
/// ### Example
|
|
///
|
|
/// Consider this State tree with the specified initial values:
|
|
///
|
|
/// ```
|
|
/// AuthState
|
|
/// isReady = false
|
|
/// authNState: AuthenticationState()
|
|
/// authZState: AuthorizationState()
|
|
///
|
|
/// AuthenticationState:
|
|
/// isSignedIn: false
|
|
/// lastSignedIn: Date? = nil
|
|
///
|
|
/// AuthorizationState:
|
|
/// isAuthorized = false
|
|
/// credentials: [String:String] = [:]
|
|
/// ```
|
|
///
|
|
/// ```
|
|
/// Event State Effects
|
|
/// --------------- ------------------------------- --------------------
|
|
/// Starting state { isReady: false,
|
|
/// [1] authNState: {
|
|
/// isSignedIn: false,
|
|
/// lastSignedIn: nil
|
|
/// },
|
|
/// authZState: {
|
|
/// isAuthorized: false,
|
|
/// credentials: {}
|
|
/// }
|
|
/// }
|
|
///
|
|
/// Event: { isReady: false,
|
|
/// signIn( authNState: { dispatch(didSignIn) [4]
|
|
/// user, isSignedIn: true, [3]
|
|
/// pass lastSignedIn: 11/26 9:43am
|
|
/// ) },
|
|
/// [2] authZState: {
|
|
/// isAuthorized: false,
|
|
/// credentials: {}
|
|
/// }
|
|
/// }
|
|
///
|
|
/// Event: { isReady: false,
|
|
/// didSignIn authNState: {
|
|
/// [5] isSignedIn: true,
|
|
/// lastSignedIn: 11/26 9:43am
|
|
/// },
|
|
/// authZState: { getCreds() {
|
|
/// isAuthorized: false, dispatch(didGetCreds(result))
|
|
/// credentials: {} }
|
|
/// } [6]
|
|
/// }
|
|
///
|
|
///
|
|
/// Event: { isReady: true, [9] dispatch(authIsReady)
|
|
/// didGetCreds( authNState: { [10]
|
|
/// creds isSignedIn: true,
|
|
/// ) lastSignedIn: 11/26 9:43am
|
|
/// [7] },
|
|
/// authZState: { [8]
|
|
/// isAuthorized: true,
|
|
/// credentials: {"token":"abc"}
|
|
/// }
|
|
/// }
|
|
///
|
|
/// ```
|
|
///
|
|
/// 1. The State Machine initializes. All states are initialized with their default
|
|
/// values.
|
|
/// 2. The State Machine receives a `signIn` event with the user/pass payload.
|
|
/// `AuthState` invokes `authNState.resolve(authNState, signIn)`
|
|
/// 3. For this exercise, we'll assume that the signIn event is a simple
|
|
/// validation, so `authNState` returns a new state with values of
|
|
/// `isSignedIn=true` and `lastSignedIn` set to the current timestamp.
|
|
/// 4. `authNState` returns both its new state plus an Effect to notify the the
|
|
/// state machine of the important state change
|
|
/// 5. The State Machine resolves the Effect from the last step by dispatching a
|
|
/// `didSignIn` event
|
|
/// 6. The `authNState` does not respond to this StateMachineEvent. However, the
|
|
/// `authZState` recognizes that it should take action on this, so it dispatches an
|
|
/// Effect to execute a `getCreds()` call. The completion block of that call will
|
|
/// dispatch the results in a `didGetCreds` StateMachineEvent
|
|
/// 7. The State Machine executes the Effect from the previous step, and receives a
|
|
/// `didGetCreds` when the completion block runs. The State Machine dispatches a
|
|
/// `didGetCreds` StateMachineEvent
|
|
/// 8. The `authZState` recognizes the `didGetCreds` event and updates its
|
|
/// attribute values to show `isAuthorized=true` and to store the value of the
|
|
/// retrieved credentials
|
|
/// 9. After `authZState` resolves, the parent `AuthState` inspects both the
|
|
/// `authNState.isSignedIn` and the `authZState.isAuthorized` and recognizes the
|
|
/// important state change
|
|
/// 10. The parent `AuthState` returns an Effect to dispatch an `authIsReady` event
|
|
///
|
|
/// - seealso: `Effect`
|
|
protocol State: Equatable {
|
|
|
|
var type: String { get }
|
|
}
|
|
|
|
struct StateID: Hashable {
|
|
let id: String
|
|
}
|