Telescope

Build Status Netlify Status Documentation GitHub Stars

Introduction

Minimum viable product. Not production ready.

Telescope is a Haskell framework for building reactive applications. Apps built with Telescope react to changes in your database, so they are always up-to-date. The Telescope framework abstracts away some of the common tasks when developing an application, allowing you to focus on your business logic and reducing the time you need to build your reactive app!

An application built with Telescope is..

What are Telescope's limitations?

Telescope is particularly well-suited for applications where events are pushed by the server e.g. notifications and dashboards. Telescope also handles forms and input-validation very well. On the flip-side, applications with heavy client-side computation such as animations are not well-suited for Telescope.

In short the way Telescope works: Telescope derives a schema for your data types via Generics. Telescope exports functions F to manipulate data types in your database. Telescope exports a server which acts as a proxy to the database for any web clients, allowing web clients to execute the functions F as if they were server-side. The Telescope typeclass determines how to communicate with a database, Telescope is not specific to any one database (e.g. MongoDB) or frontend library (e.g. Reflex-DOM). "Reactive" variants of the functions F are provided, where function parameters and return values are streams of data (e.g. Event Int) as opposed to single values (e.g. Int).

Getting Started

To see what building a reactive application with Telescope looks like, we will build a simple, yet functional, public chat room. Currently Telescope only has support for Reflex-DOM as a frontend, though support for reflex-vty is planned.

1. Define the data types used in your application.

data Message = Message
  { time     :: Int
  , room     :: Text
  , username :: Text
  , message  :: Text
  } deriving (Eq, Ord, Generic, Show)

instance PrimaryKey Message Int where
  primaryKey = time

2. Include Telescope's server in your backend code.

Server.run port id

3. Write your frontend with Reflex-DOM!

main = mainWidget $ do
  -- A text field to enter chat room name and username.
  (roomNameDyn, usernameDyn) <- do
    roomNameInput <- textInputPlaceholder "Chat Room"
    usernameInput <- textInputPlaceholder "Username"
    pure (roomNameInput ^. textInput_value, usernameInput ^. textInput_value)
  -- View messages live from the database.
  dbMessagesEvn <- T.viewTableRx $ const (Proxy @Message) <$> updated roomNameDyn
  -- Filter messages to the current chat room.
  roomMessagesDyn <- holdDyn [] $ attachPromptlyDynWith
    (\rn ms -> [m | m <- ms, room m == rn]) roomNameDyn dbMessagesEvn
  -- Display messages for the current chat room.
  el "ul" $ simpleList roomMessagesDyn $ el "li" . dynText . fmap
    (\m -> "“" <> username m <> "”: " <> message m)
  -- A text field for entering messages, and button to send the message.
  messageTextDyn <- (^. textInput_value) <$> textInputPlaceholder "Enter Message"
  timeEvn        <- fmap (fmap round) . tagTime =<< button "Send"
  -- Construct a 'Message' from user input, and send on button click.
  let messageToSendDyn = (\room username message time -> Message {..})
        <$> roomNameDyn <*> usernameDyn <*> messageTextDyn
      messageToSendEvn = attachPromptlyDynWith ($) messageToSendDyn timeEvn
  T.setRx messageToSendEvn

Finally build and run the application. You can now open the app in two browser tabs, interact with the app in one tab and watch the other app react!

A full tutorial is available here. In the tutorial we will develop a more full-featured version of this chat room application that utilizes some additional features of the Telescope framework.

Contributing

Feedback, questions and contributions are all very welcome! The instructions here should contain all the commands you need to get started hacking on this project. If you want to open an issue you can do that here.

About Telescope

Application Architecture

The most important component of Telescope from an application developer's perspective is the Telescope interface, a set of functions that allow you to read/write datatypes from/to a data source. This interface is available both server-side and client-side. The diagram below shows one possible setup of a Telescope application, each telescope icon represents usage of the Telescope interface.

The bottom row of the diagram represents a developer interacting with a database on their own machine. More specifically the developer has opened a REPL and is using the Telescope interface to interact with the local database.

The top row of the diagram shows two uses of the Telescope interface, one by a server, and one by a web client. The server is interacting with the database via the Telescope interface and acts as a proxy to the database for any web clients. The web client is communicating with the server via the Telescope interface, but since the server is only acting as a proxy the client is really interacting with the database.

There are a number of important points about the Telescope interface which we will now discuss in turn, referring back to the architecture diagram above.

Another Web Framework?

There are many different web frameworks out there, and they all have pros and cons. They pretty much all allow you to write reuseable components. Some can ship a small file to the client, some allow you to write your server-side and client-side code in the same language, some can pre-render server-side for a speedy TTI (time to interactive).

One idea that has become fairly popular in recent years is that of a reactive frontend. Whereby the frontend is written as a function of the current state, and whenever the state changes, the frontend "reacts" to the change and updates itself.

Implementations of reactive frontends vary, however in the vast majority of cases one significant limitation is that the frontend only reacts to client-side changes in data. At this network boundary the developer still has to manage communication with a server.

The primary motivation behind creating Telescope is that a developer should be able to write a reactive frontend as a function of data in their one true data source. Telescope solves this by providing a direct Reflex-DOM <-> database link. Even better, the Telescope interface is not specific to Reflex-DOM or the database. You could write an instance to use in e.g. a reflex-vty application, or to communicate with a different server or database.

Technical Details

So Telescope is a web framework? More generally Telescope provides a reactive interface (the Telescope typeclass) to read/write datatypes from/to a data source. The data source's location (e.g. filepath or URL) is a parameter of the interface, allowing you to use the same interface to read/write data regardless of where that data is stored. Even better, you can use the Telescope interface both server-side and client-side. The use of the term "reactive" in "reactive interface" refers to the fact that clients can subscribe to changes in data, reacting to any changes.

The telescope package provides the Telescope interface without any instances. The telescope-ds-file package provides an instance of the interface, that stores data in local files. The telescope-ds-reflex-dom package provides an instance of the interface to be used in a Reflex-DOM web app, it talks to a server to read/write data. The telescope-server package provides a Servant server that serves data via a provided instance of the Telescope interface e.g. from telescope-ds-file.

The Telescope interface provides functions that operate on datatypes which are instances of the Entity typeclass. You only need to define a primary key for your datatype and then the Entity instance can be derived for your datatype via Generics. Generic programming allows conversion of your datatype to/from storable representation. The following diagram shows this conversion.

-- Example of a datatype to be stored.
data Person { name :: Text, age  :: Int } deriving Generic
instance PrimaryKey Person where primaryKey = name
  
-- Diagram showing conversion to/from storable representation.
Person "john" 70     <--->     "Person"
                               | ID     | "name" | "age" |
                               | "john" | "john" | 70    |

Tutorial

In this tutorial we will walk through some of the features that Telescope provides. The first few features will be presented as if we were building a simple chat room application. Note that this is not a tutorial on Reflex. If you are new to Reflex you may find some of the following links helpful:

Installation

First install Nix the package manager, and install Cachix to make use of our binary cache. Then clone this repo (with submodules) and change in to the telescope directory. You will also want to configure use of the Reflex-FRP cache, follow step 2 here. Finally download pre-built binaries with Cachix, and perform an initial build:

curl -L https://nixos.org/nix/install | sh
nix-env -iA cachix -f https://cachix.org/api/v1/install
git clone --recurse-submodules https://github.com/jerbaroo/telescope
cd telescope
./scripts/cachix/use.sh # Will take a long time the first time.

Building & Running

The Telescope repo provides example projects which you can build or browse the source code of. One of these is a /very/ simple chat room web application. To build and run the chat room application execute the commands below. These two commands will start a development server each, the first builds and runs your app's backend server, reloading everytime the source code changes. And the second builds and runs a server that serves your frontend, reloading everytime the source code changes. Visit localhost:3003 to see the app in your browser.

./scripts/run/dev.sh chatroom-backend
./scripts/run/dev.sh chatroom-frontend

When you are ready to run the application in production you can run the commands below. These commands will build your frontend files (HTML and JavaScript), and build and run your backend server. Visit localhost:3002 to see the app in your browser.

./scripts/build/prod.sh chatroom-frontend
./scripts/build/prod.sh chatroom-backend
./scripts/run/prod.sh   chatroom-backend

Defining Data Types

Telescope applications are primarily built as functions of data that is stored in a database. Therefore we need to define data types that model our application while keeping in mind that the data will be stored in a database. A simple method of storing our data is to store each chat room message as a row in a database along with any information associated with the message, such as: time the message was sent, who sent the message, and what chat room the message was sent in. The data type below captures these requirements. Each field of the data type will be stored in one column of a table "Message" in the database. The following code which defines the Message data type for our chat room app is available in telescope/apps/chatroom-common/src/ChatRoom/Common.hs.

data Message = Message
  { time     :: Int
  , room     :: Text
  , username :: Text
  , message  :: Text
  } deriving (Eq, Ord, Generic, Show)

instance PrimaryKey Message Int where
  primaryKey = time

Database Operations

During development you might want to insert some data into your database for testing. Or when running in production you might want to perform some database maintenance. Telescope provides a number of operations for interacting with your database, these operations are available both server-side and client-side. These operations are available in the Telescope.Operations module. The following backend code is available in telescope/apps/chatroom-backend/app/Main.hs.

main = do
  let msg = Message 1 "main" "john" "Hello everyone"
  -- You might find the following pattern useful in development. Running this
  -- code before the server starts will remove all messages and insert 'msg'.
  runT $ T.rmTable @Message >> T.set msg
  -- ...

Telescope's Server

Telescope exports a server which understands data types that are an instance of the Entity typeclass, such as the Message data type defined above for our chat room example. When using Telescope operations client-side, the data types are decomposed into storable representation and sent to the server, which then completes the operation server-side. Running an instance of Telescope's server is super simple. The following backend code to start a server is available in telescope/apps/chatroom-backend/app/Main.hs.

main = do
  -- ...
  Server.run 3002 Server.developmentCors

Reflex-DOM Frontend

To complete our chat room application, we just need a frontend. The Telescope interface is not specific to any frontend, however Telescope currently only provides integrations with Reflex-DOM. In our chat room frontend we will view messages live from the database, and anytime the user presses enter we will set their message into the database. As previously mentioned this is not a tutorial on Reflex-DOM, so we won't be discussing the whole frontend. You can find this frontend code in telescope/apps/chatroom-frontend/app/Main.hs.

Notice the use of the functions T.viewTableRx and T.setRx in the code below. These Telescope operations will respectively, "view" an entire table live from the database, and set a stream of data into the database.

main = mainWidget $ do
  -- A text field to enter chat room name and username.
  (roomNameDyn, usernameDyn) <- do
    roomNameInput <- textInputPlaceholder "Chat Room"
    usernameInput <- textInputPlaceholder "Username"
    pure (roomNameInput ^. textInput_value, usernameInput ^. textInput_value)
  -- View messages live from the database.
  dbMessagesEvn <- T.viewTableRx $ const (Proxy @Message) <$> updated roomNameDyn
  -- Filter messages to the current chat room.
  roomMessagesDyn <- holdDyn [] $ attachPromptlyDynWith
    (\rn ms -> [m | m <- ms, room m == rn]) roomNameDyn dbMessagesEvn
  -- Display messages for the current chat room.
  el "ul" $ simpleList roomMessagesDyn $ el "li" . dynText . fmap
    (\m -> "“" <> username m <> "”: " <> message m)
  -- A text field for entering messages, and button to send the message.
  messageTextDyn <- (^. textInput_value) <$> textInputPlaceholder "Enter Message"
  timeEvn        <- fmap (fmap round) . tagTime =<< button "Send"
  -- Construct a 'Message' from user input, and send on button click.
  let messageToSendDyn = (\room username message time -> Message {..})
        <$> roomNameDyn <*> usernameDyn <*> messageTextDyn
      messageToSendEvn = attachPromptlyDynWith ($) messageToSendDyn timeEvn
  T.setRx messageToSendEvn

Server Integration

Telescope is useful when you have data that is uniquely determined by a primary key, Telescope helps you store that data in a database and manipulate/access that data. However it may be the case that you don't want to use Telescope to develop part of your server's API, perhaps you already have a server that you don't want to rewrite, perhaps you have data that you want to serve but not store in a database, or perhaps you simply prefer another solution to develop part of your API.

The server that Telescope exports is part of the Servant package (Hackage link). If your additional server is a Servant Server it can very easily be combined with Telescope's server, see the code below. On the other hand if your existing server is not a Servant Server then you have two options. One option is to expose both servers to the client. The second option is to setup your server to proxy all requests intended for Telescope to Telescope's server. The following code integrating an existing Server with Telescope's Server is available in telescope/apps/chatroom-backend/app/Main.hs. The frontend code that interacts with the integrated server is in telescope/apps/chatroom-frontend/app/Main.hs.

main = do
  -- ...
  Warp.run 3002 $ Server.developmentCors $
    Servant.serve (Proxy :: Proxy API) (additionalServer :<|> Server.server)

-- | Additional server with only one endpoint, for demonstrative purposes.
additionalServer :: Servant.Server AdditionalAPI
additionalServer = pure "This is data returned by the additional server"

type AdditionalAPI = "additional" :> Get '[JSON] Text
type API = AdditionalAPI :<|> Telescope.API

Custom Handlers

Authentication

Authorization

Nested Data Types

Multiple Data Constructors

Avoiding Infinite Recursion

Migrations

reflex-vty

FAQ

Why the Name Telescope?

The telescope package provides an interface to read/write remote data i.e. data stored in a database or data accessed over the network. This interface is "lens-like" i.e. the functions are similar to the functions view, set etc. that you may know from the lens library. So if you squint your eyes a little you could say this library provides a lens to look at remote data... like a telescope.