Skip to content

ThomasWeiser/elmfire

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

84 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ElmFire: Use the Firebase API in Elm

Virtually all features of the Firebase Web API are exposed as a library for Elm:

  • Setting, removing and modifying values
  • Transactions
  • Querying data, both one-time and per subscription
  • Complex queries with sorting, filtering and limiting
  • Authentication
  • User management
  • Offline capabilities

In addition to these base functions the package elmfire-extra provides a higher-level synchronization API, which allows you to treat your Firebase data like a local Elm-Dict.

Demo application for these APIs: Collaborative TodoMVC


This library currently targets Elm version 0.16.

A new version with an effect manager for Elm 0.17/0.18 is under development. Please stay tuned!


API Usage

The API design corresponds closely to the targeted Firebase JavaScript API. Please refer to the original documentation for further discussions of the concepts.

In the following we give a short overview of the API. Detailed documentation is embedded in the source code.

Constructing Firebase Locations

To refer to a Firebase path use a Location. It can be built with the following functions:

-- Location is an opaque type.
fromUrl  : String -> Location
sub      : String -> Location -> Location
parent   : Location -> Location
root     : Location -> Location
push     : Location -> Location
location : Reference -> Location

These are all pure functions. They don't touch a real Firebase until the resulting location is used in one of the tasks outlined below.

Example:

location : Location
location =
  fromUrl "https://elmfire.firebaseio-demo.com/test"
    |> parent
    |> sub "anotherTest"`
    |> push

References to Locations

Most actions on a Firebase location return a reference to that location. Likewise, query results contain a reference to the location of the reported value.

References can inform about the key or the complete URL of the referred location. And a reference may be converted back to a location, which can be used in a new task.

There is a special task to open a location without modifying or querying it, which results in a reference if the location is valid. It's generally not necessary to explicitly open a constructed location, but it may be used to check the validity of a location or to cache Firebase references.

-- Reference is an opaque type
key      : Reference -> String
toUrl    : Reference -> String
location : Reference -> Location
open     : Location -> Task Error Reference

Modifying Values

set             : Value -> Location -> Task Error Reference
setWithPriority : Value -> Priority -> Location -> Task Error Reference
setPriority     : Priority -> Location -> Task Error Reference
update          : Value -> Location -> Task Error Reference
remove          : Location -> Task Error Reference

These tasks complete when synchronization to the Firebase servers has completed. On success they result in a Reference to the modified location. They result in an error if the location is invalid or if you have no permission to modify the data.

Values are given as Json values, i.e. Json.Encode.Value.

Example:

port write : Task Error ()
port write =
  set (Json.Encode.string "new branch") (push location)
  `andThen`
  (\ref -> ... ref.key ... )

Transactions

Atomic modifications of the data at a location can be achieved by transactions.

A transaction takes an update function that maps the previous value to a new value. In case of a conflict with concurrent updates by other clients the update function is called repeatedly until no more conflict is encountered.

transaction : (Maybe Value -> Action) ->
              Location ->
              Bool ->
              Task Error (Bool, Snapshot)
type Action = Abort | Remove | Set Value

Example:

port trans : Task Error -> Task Error ()
port trans =
  transaction
    ( \maybeVal -> case maybeVal of
        Just value ->
          case Json.Decode.decodeValue Json.Decode.int value of
            Ok counter -> Set (Json.Encode.int (counter + 1))
            _          -> Abort
        Nothing ->
          Set (Json.Encode.int 1)
    ) location False
  `andThen`
  (\(committed, snapshot) -> ... )

Querying

once        : Query -> Location -> Task Error Snapshot
subscribe   : (Snapshot -> Task x a) ->
              (Cancellation -> Task y b) ->
              Query ->
              Location ->
              Task Error Subscription
unsubscribe : Subscription -> Task Error ()

Use once to listen to exactly one event of the given type.

The first parameter specifies the event to listen to: valueChanged, childAdded, childChanged, childRemoved or childMoved. Additionally, this parameter may also specify ordering, filtering and limiting of the query (see below). If you don't need these options a simple query specification is valueChanged noOrder.

The second parameter references the queried location.

Use subscribe to start a continuing query of the specified events. Subscription queries return a arbitrary number of data messages, which are reported via running a supplied task.

The first parameter of subscribe is a function used to construct that task from a data message.

The second parameter is a function used to construct a task that is run when the query gets canceled.

The third and fourth parameter of subscribe are the same as the first two of once.

On success the subscribe task returns a Subscription, an identifier that can be used to match the corresponding responses and to cancel the query.

type alias Snapshot =
  { subscription: Subscription
  , key: String
  , reference: Reference
  , existing: Bool
  , value: Value
  , prevKey: Maybe String
  , priority: Priority
  }
type Cancellation
  = Unsubscribed Subscription
  | QueryError Subscription Error

A Snapshot carries the resulting Value (as Json) among other information, e.g. the corresponding Subscription identifier.

In queries of type valueChanged the result may be that there is no value at the queried location. In this case existing will be False and value will be the Json value of null.

key corresponds to the last part of the path. It is the empty string for the root. Keys are relevant notably for child queries.

Example:

responses : Signal.Mailbox (Maybe Snapshot)
responses = Signal.mailbox Nothing

port query : Task Error Subscription
port query =
  subscribe
    (Signal.send responses.address << Just)
    (always (Task.succeed ()))
    (childAdded noOrder noLimit)
    (fromUrl "https:...firebaseio.com/...")

... = Signal.map
        (\response -> case response of
            Nothing -> ...
            Just snapshot -> ...
        )
        responses.signal

Ordering, Filtering and Limiting Queries

Query results can be ordered (by value, by a child's value, by key or by priority), and then filtered by giving a start and/or end value within that order, and limited to the first or last certain number of children.

Example queries to be used in once and subscribe:

childAdded noOrder
childAdded (orderByValue noRange noLimit)
childAdded (orderByChild "size" noRange noLimit)
childAdded (orderByKey noRange noLimit)
childAdded (orderByPriority noRange (limitToFirst 2))
childAdded (orderByValue (startAt (Json.Encode.string "foo")) noLimit)
childAdded (orderByValue (startAt (Json.Encode.string "foo")) (limitToLast 10))
childAdded (orderByChild "size" (equalTo (Json.Encode.int 42)) noLimit)
childAdded (orderByKey (endAt "k") noLimit)
childAdded (orderByPriority (startAt (NumberPriority 17, Just "k")) noLimit)

When doing ordered valuedChanged queries it may be useful to map the result to a list to conserve the ordering:

toSnapshotList : Snapshot -> List Snapshot
toValueList    : Snapshot -> List JE.Value
toKeyList      : Snapshot -> List String
toPairList     : Snapshot -> List (String, JE.Value)

Authentication

The sub-module ElmFire.Auth provides all authentication and user management functions that are offered by Firebase.

Some example tasks:

import ElmFire.Auth exposing (..)

-- create a new user-account with email and password
userOperation (createUser "[email protected]" "myPassword")

-- login with with email and password
authenticate loc [rememberSessionOnly] (withPassword "[email protected]" "myPassword")

-- login with with github account
authenticate loc [] (withOAuthPopup "github")

-- watch for logins and logouts
subscribeAuth
  (\maybeAuth -> case maybeAuth of
    Just auth -> ... auth.uid ...
    Nothing   -> ... -- not authenticated
  )
  loc

Offline Capabilities

  • Detecting connection state changes: subscribeConnected
  • Manually disconnect and reconnect: goOffline, goOnline
  • Managing presence: onDisconnectSet, onDisconnectSetWithPriority, onDisconnectUpdate, onDisconnectRemove, onDisconnectCancel
  • Handling latency: subscribeServerTimeOffset, serverTimeStamp

Examples

There is a basic example app in example/src/Example.elm. To build it:

cd example
make all open

Alternatively without using make:

cd example
elm make --output Example.html src/Example.elm

TodoMVC

A more extensive example is this implementation of TodoMVC as a collaborative real-time app.

Testing

There is a testing app, living in the directory test, that covers most of the code. It runs a given sequence of tasks on the Firebase API and logs these steps along with the several results.

This app uses a small ad-hoc testing framework for task-based code.

There is a Makefile to build the app. On most Unix-like systems a cd test; make all open should do the trick.

An older, still functional testing app lives in the directory demo.