billstclair / elm-port-funnel / PortFunnel

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.

Types


type alias FunnelSpec state substate message response model msg =
{ 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.


type ModuleDesc message substate response

Everything we need to know to route one module's messages.


type alias StateAccessors state substate =
{ get : state -> substate
, set : substate -> state -> state 
}

Package up an application's functions for accessing one funnel module's state.


type alias GenericMessage =
{ moduleName : String
, tag : String
, args : Json.Encode.Value 
}

A generic message that goes over the wire to/from the module JavaScript.


type alias JSVersion =
{ v4_1 : () }

This is used to force a major version bump when the JS changes.

You'll usually not use it for anything.

PortFunnel-aware Modules

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.

API

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.

Low-level conversion between 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.

Simulated Message Processing

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.