PortFunnel allows you easily use multiple port modules.
You create a single outgoing/incoming pair of ports, and PortFunnel does the rest.
Some very simple JavaScript boilerplate directs PortFunnel.js
to load and wire up all the other PortFunnel-aware JavaScript files. You write one simple case statement to choose which port package's message is coming in, and then write package-specific code to handle each one.
{ accessors : StateAccessors state substate
, moduleDesc : ModuleDesc message substate response
, commander : (GenericMessage -> Platform.Cmd.Cmd msg) -> response -> Platform.Cmd.Cmd msg
, handler : response -> state -> model -> ( model
, Platform.Cmd.Cmd msg )
}
All the information needed to use a PortFunnel-aware application
with a single PortFunnel-aware module.
StateAccessors
is provided by the application.
ModuleDesc
is provided by the module, usually via a moduleDesc
variable.
commander
is provided by the module, usually via a commander
variable.
handler
is provided by the application.
Everything we need to know to route one module's messages.
{ get : state -> substate
, set : substate -> state -> state
}
Package up an application's functions for accessing one funnel module's state.
{ moduleName : String
, tag : String
, args : Json.Encode.Value
}
A generic message that goes over the wire to/from the module JavaScript.
{ v4_1 : () }
This is used to force a major version bump when the JS changes.
You'll usually not use it for anything.
makeModuleDesc : String -> (message -> GenericMessage) -> (GenericMessage -> Result String message) -> (message -> substate -> ( substate, response )) -> ModuleDesc message substate response
Make a ModuleDesc
.
A module-specific one of these is available from a PortFunnel
-aware module. The args are:
name encoder decoder processor
name
is the name of the module, it must match the name of the JS file.
encoder
turns your custom Message
type into a GenericMessage
.
decoder
turns a GenericMessage
into your custom message type.
processor
is called when a message comes in over the subscription port. It's very similar to a standard application update
function. substate
is your module's State
type, not to be confused with state
, which is the user's application state type.
getModuleDescName : ModuleDesc message substate response -> String
Get the name from a ModuleDesc
.
emptyCommander : (GenericMessage -> Platform.Cmd.Cmd msg) -> response -> Platform.Cmd.Cmd msg
A commander
for a FunnelSpec
that always returns Cmd.none
Useful for funnels that do not send themselves messages.
send : (Json.Encode.Value -> Platform.Cmd.Cmd msg) -> GenericMessage -> Platform.Cmd.Cmd msg
Send a GenericMessage
over a Cmd port
.
sendMessage : ModuleDesc message substate response -> (Json.Encode.Value -> Platform.Cmd.Cmd msg) -> message -> Platform.Cmd.Cmd msg
Send a message
over a Cmd port
.
processValue : Dict String funnel -> (GenericMessage -> funnel -> state -> model -> Result String ( model, Platform.Cmd.Cmd msg )) -> Json.Encode.Value -> state -> model -> Result String ( model, Platform.Cmd.Cmd msg )
Process a Value
from your subscription port.
processValue funnels appTrampoline value state model
Parse the Value
into a GenericMessage
.
If successful, use the moduleName
from there to look up a funnel from the Dict
you provide.
If the lookup succeeds, call your appTrampoline
, to unbox the funnel
and call PortFunnel.appProcess
to do the rest of the processing.
See example/boilerplate.elm
and example/simple.elm
for examples of using this.
appProcess : (Json.Encode.Value -> Platform.Cmd.Cmd msg) -> GenericMessage -> FunnelSpec state substate message response model msg -> state -> model -> Result String ( model, Platform.Cmd.Cmd msg )
Finish the processing begun in processValue
.
process : StateAccessors state substate -> ModuleDesc message substate response -> GenericMessage -> state -> Result String ( state, response )
Process a GenericMessage.
This is low-level processing. Most applications will call this through appProcess
via processValue
.
Value
and GenericMessage
encodeGenericMessage : GenericMessage -> Json.Encode.Value
Low-level GenericMessage encoder.
decodeGenericMessage : Json.Encode.Value -> Result String GenericMessage
Turn a Value
from the Sub
port into a GenericMessage
.
genericMessageDecoder : Json.Decode.Decoder GenericMessage
Decoder for a GenericMessage
.
messageToValue : ModuleDesc message substate response -> message -> Json.Encode.Value
Convert a message to a JSON Value
messageToJsonString : ModuleDesc message substate response -> message -> String
Convert a message to a JSON Value
and encode it as a string.
makeSimulatedFunnelCmdPort : ModuleDesc message substate response -> (message -> Maybe message) -> (Json.Encode.Value -> msg) -> Json.Encode.Value -> Platform.Cmd.Cmd msg
Simulate a Cmd
port, outgoing to a funnel's backend.
makeSimulatedFunnelCmdPort moduleDesc simulator tagger value
Usually, a funnel Module
will provide one of these by leaving off the last two args, tagger
and value
:
simulator : Message -> Maybe Message
simulator message =
...
makeSimulatedCmdPort : (Value -> msg) -> Value -> Cmd msg
makeSimulatedCmdPort =
PortFunnel.makeSimulatedFunnelCmdPort
moduleDesc
simulator
Then the application code will call simulatedPort
with a tagger, which turns a Value
into the application msg
type. That gives something with the same signature, Value -> Cmd msg
as a Cmd
port:
type Msg
= Receive Value
| ...
simulatedModuleCmdPort : Value -> Cmd msg
simulatedModuleCmdPort =
Module.makeSimulatedPort Receive
This can only simulate synchronous message responses, but that's sufficient to test a lot. And it works in elm reactor
, with no port JavaScript code.
Note that this ignores errors in decoding a Value
to a GenericMessage
and from there to a message
, returning Cmd.none
if it gets an error from either. Funnel developers will have to test their encoders and decoders separately.