billstclair/elm-port-funnel/1.1.2

NOTE: the latest version is 1.2.0

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 -> Cmd msg) -> response -> Cmd msg
, handler : response -> state -> model -> ( model, 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 : Value
}

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

PortFunnel-aware Modules

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 -> Cmd msg) -> response -> Cmd msg

A commander for a FunnelSpec that always returns Cmd.none

Useful for funnels that do not send themselves messages.

API

send : (Value -> Cmd msg) -> GenericMessage -> Cmd msg

Send a GenericMessage over a Cmd port.

ModuleDesc message substate response
-> (Value -> Cmd msg)
-> message
-> Cmd msg

Send a message over a Cmd port.

Dict String funnel
-> ( GenericMessage
-> funnel
-> state
-> model
-> Result String ( model, Cmd msg )
)
-> Value
-> state
-> model
-> Result String ( model, 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.

(Value -> Cmd msg)
-> GenericMessage
-> FunnelSpec state substate message response model msg
-> state
-> model
-> Result String ( model, Cmd msg )

Finish the processing begun in processValue.

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 -> Value

Low-level GenericMessage encoder.

decodeGenericMessage : Value -> Result String GenericMessage

Turn a Value from the Sub port into a GenericMessage.

genericMessageDecoder : Decoder GenericMessage

Decoder for a GenericMessage.

messageToValue : ModuleDesc message substate response -> message -> 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

ModuleDesc message substate response
-> (message -> Maybe message)
-> (Value -> msg)
-> Value
-> 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.