Make it easier to create complex state machines from LocalStorage
contents.
This is too complicated for an example here, but see ZapMeme.Sequencer for a real-life example.
This does not support simulated LocalStorage
. Certainly possible, but it was already plenty complicated without that.
Since an instance of LocalStorageStates
is stored in your Model
, and it contains two functions which take a model
as argument, there needs to be a custom class to prevent infinite type recursion:
type WrappedModel =
WrappedModel Model
type alias Model =
{ localStorageStates : LocalStorageStates WrappedModel Msg
, ...
}
type Msg =
SequenceDone (WrappedModel -> ( WrappedModel, Cmd Msg ))
| ProcessLocalStorage Value
...
{ sender : PortFunnel.LocalStorage.Message -> Platform.Cmd.Cmd msg
, injector : Injector msg
, localStorageStates : model -> LocalStorageStates key state model msg
, setLocalStorageStates : LocalStorageStates key state model msg -> model -> model
, sequenceDone : (model -> ( model
, Platform.Cmd.Cmd msg )) -> msg
, nullState : NullState key state model msg
}
Communication between Sequence
and your main Model
and Msg
.
sender
is how you send messages out your command port. It's usually defined in your main program, with something like:
{-| The `model` parameter is necessary here for `PortFunnels.makeFunnelDict`.
-}
getCmdPort : String -> model -> (Value -> Cmd Msg)
getCmdPort moduleName _ =
PortFunnels.getCmdPort ProcessLocalStorage moduleName False
localStorageSend : LocalStorage.Message -> Cmd Msg
localStorageSend message =
LocalStorage.send (getCmdPort LocalStorage.moduleName ())
message
funnelState.storage
localStorageStates
and setLocalStorageStates
are functions that read/write a LocalStorageStates
from/to your Model
.
sequenceDone
creates a Msg
that calls its arg with the Model, and expects back the standard update
result. E.g.:
type Msg
= SequenceDone (WrappedModel -> (WrappedModel, Cmd Msg))
...
update model msg =
SequenceDone updater ->
let
( WrappedModel mdl, cmd ) =
updater (WrappedModel model)
in
( mdl, cmd )
...
Make a NullState
with makeNullState
.
Opaque type used to store a table of state machines.
Create one with makeLocalStorageStates
. Store it in your Model
.
{ state : state
, label : String
, process : Wrappers key state model msg -> DbResponse -> state -> ( DbRequest msg
, state )
}
Holds the state, label, and LocalStorage subscription port processing function for one state machine.
You will get very confused if you use the same label for two different State
values passed to makeLocalStorageStates
.
See the definition of initialStorageStates
in ZapMeme.Sequencer.
{ prefix : String
, subkey : String
}
These are converted to and from the strings sent as LocalStorage keys by encodePair
and decodePair
.
A high-level representation of a LocalStorage operation.
DbNothing
does nothing.
DbCustomRequest
allows you to send some command other than a message out the LocalStorage cmdPort
.
DbBatch
allows you to gang more than one request.
The others have obvious correlation to the LocalStorage messages, but DbPut
and DbRemove
are decoupled, instead of using a Maybe Value
.
A high-level represenation of a message returned from LocalStorage through the subscription port.
Since this module doesn't support simulated storage, only DbGot
in response to DbGet
and DbKeys
in response to DbListKeys
need to be distinguished.
The other messages are all turned into DbNoResponse
, but there wont be any in a non-simulated environment.
makeLocalStorageStates : Wrappers key state model msg -> List ( key, State key state model msg ) -> LocalStorageStates key state model msg
Create a LocalStorageStates
to store in your Model
.
The key
type is usually a simple enumerator custom type. You could use strings, but then the compiler won't catch typos for you.
The state
type is usually a custom type with one tag per key
value.
the model
here is usually WrappedModel
in your code, to prevent type recursion through wrappers.localStorageStates
and wrappers.setLocalStorageStates
.
makeNullState : state -> NullState key state model msg
This is how you create a NullState
for your Wrappers
.
The state
you pass will never be used for anything.
You will usually use only update
, but the others let you do lower-level processing yourself.
update : Wrappers key state model msg -> model -> PortFunnel.LocalStorage.Response -> ( model, Platform.Cmd.Cmd msg )
Modulo wrapping and unwrapping, designed to be the entire handler for incoming LocalStorage responses.
funnelDict : FunnelDict Model Msg
funnelDict =
PortFunnels.makeFunnelDict
[ LocalStorageHandler wrappedStorageHandler ]
getCmdPort
wrappedStorageHandler : LocalStorage.Response -> PortFunnels.State -> Model -> ( Model, Cmd Msg )
wrappedStorageHandler response _ model =
let
( WrappedModel mdl, cmd ) =
Sequence.storageHandler
sequenceWrappers
(WrappedModel model)
response
in
( mdl, cmd )
processStates : PortFunnel.LocalStorage.Response -> LocalStorageStates key state model msg -> ( LocalStorageStates key state model msg, Platform.Cmd.Cmd msg )
Calls process
on each of the State
s registered in the LocalStorageStates
.
Accumulates the results, but in practice, only one of them will do anything other than note that the LocalStorage.Response
's label
doesn't match, and return the model
unchanged with Cmd.none
.
You will usually call this via update
, not directly.
process : Wrappers key state model msg -> PortFunnel.LocalStorage.Response -> State key state model msg -> Maybe ( State key state model msg, Platform.Cmd.Cmd msg )
Process a response for a single State
.
If the response's label
matches the State
's, calls state.process
, updates the user state
inside the State
, and turns the returned DbRequest
into a Cmd msg
(usually by calling send
). Otherwise, returns Nothing
.
You will usually call this indirectly, via update
.
send : Wrappers key state model msg -> State key state model msg -> DbRequest msg -> Platform.Cmd.Cmd msg
Turn a DbRequest
into a Cmd msg
that sends a LocalStorage.Message
out to the JavaScript through the Cmd
port.
Uses the sender
in Wrappers
and label
in State
.
inject : Wrappers key state model msg -> State key state model msg -> DbResponse -> Platform.Cmd.Cmd msg
Call injectTask
, and use Task.perform
to turn that Task
into a Cmd
.
The (Value -> msg)
function will usually be the Msg
that receives subscription input from your LocalStorage port.
injectTask : Wrappers key state model msg -> State key state model msg -> DbResponse -> Task Basics.Never Json.Decode.Value
If you receive something outside of a LocalStorage return, and want to get it back into your state machine, use this.
It returns a Task
that, if you send it with your LocalStorage sub
port message, will make it seem as if the given DbResponse was received from LocalStorage.
It's a trivial task wrapper on the result of dbResponseToValue, using label
property of the State
.
getState : Wrappers key state model msg -> key -> model -> Maybe state
Look up the State
associated with key
in model
's LocalStorageStates
(with getFullState
). If found, return itsstate
field.
getFullState : Wrappers key state model msg -> key -> model -> Maybe (State key state model msg)
Look up the State
associated with key
in model
's LocalStorageStates
setState : Wrappers key state model msg -> key -> state -> model -> ( model, State key state model msg )
Update one State
inside the LocalStorageStates
inside of the model
.
Returns the updated model
and State
. This version is usually used when you start up a state machine, since you need the State
to call send
.
Use setStateOnly
if you need only the updated model
.
setStateOnly : Wrappers key state model msg -> key -> state -> model -> model
Update one State
inside the LocalStorageStates
inside of the model
.
Return the updated model
.
Use setState
if you also need the updated State
(or call getFullState
).
decodeExpectedDbGot : Json.Decode.Decoder value -> String -> DbResponse -> Maybe ( KeyPair, Maybe value )
Eliminate error checking boilerplate inside State
process
functions.
decodeExpectedDbGot decoder expectedSubkey response
If response
is not a DbGot
, returns Nothing.
If expectedSubkey
is not ""
, and also not the subkey
of that DbGot
's KeyPair
, returns Just (KeyPair, Nothing)
.
If the DbGot
's value is Nothing
, returns Just (KeyPair, Nothing)
.
Otherwise, uses decoder
to decode that value. If the result is an Err
, returns Just (KeyPair, Nothing)
.
If all goes well, returns Just (KeyPair, value)
.
dbResponseToValue : String -> String -> DbResponse -> Json.Decode.Value
Turn a DbResponse into the Value that would create it,
if received from the LocalStorage port code, in response to a DbGet
or DbListKeys
request.
NoResponse
is treated as DbGot
with empty string components in its KeyPair
and Nothing
as its value. You will likely never want to do that.
decodePair : String -> KeyPair
Turn a string that comes back from the subscription port into a KeyPair
.
Splits it on the first ".".
encodePair : KeyPair -> String
Turn a KeyPair
into a string.
That string is used as the Key
arg to LocalStorage.getLabeled
or the Prefix
arg to LocalStorage.listKeysLabeled
.