urbit/main/pub/doc/guide/c-gall.md
Galen Wolfe-Pauly 2ea66a0114 docs and tree
2015-02-17 19:03:21 -08:00

11 KiB

This guide is focussed on storing application state using the %gall vane. To show off how we store and distribute data in Urbit we're going to examine a simple webapp. Some of the material here expects that you have looked over the %ford guide. If you haven't, it's a good idea to start there. There's also more information in the %gall overview and %gall commentary but it's not necesarry that you read those before going forward.

One important thing to keep in mind is that %gall services aren't 'started' or 'stopped' as in a unix system. When your files are copied in they are compiled and begin running immediately and permanently. %gall services simply wake up when certain events happen.

If you need to make updates to the structure of your stored data, you write connector functions or (when developing) just throw your existing data away. We'll see examples of how this works, just keep in mind that when we talk about a %gall 'service' it has no concept of 'running'.

Going forward we'll refer to the directory you cloned the repo in as /$URB_DIR and assume that your pier is listening for HTTP connections on port 8080.

Get the code.

Clone the GitHub repository and move the files into your /main desk, under the corresponding paths. You will need four files:

  • /main/app/lead/core.hook
  • /main/pub/lead/hymn.hook
  • /main/pub/lead/src/main.css
  • /main/pub/lead/src/main.js

When everything is in place, try it:

http://localhost:8080/gen/main/pub/lead/

That URL should render a page and be self explanatory. Try adding names to the leaderboard and incrementing their scores. It's also fun to open a few tabs and watch how the state gets updated simultaneously.

How is the code structured?

In our %ford guide we generated pages by defining all of their possible states, but we didn't exactly store any data. When building applications on top of Urbit we think of them as existing in two natural parts: page resources and state services. Effectively, we think of any Urbit app talking to the web as a single page app whose resources are generated by %ford which talks to a %gall service if it needs to persist any state. Let's look more closely at the specifics in this simple app.

When we load our page, we render the contents of our /main/pub/lead/hymn.hook. This file should look familiar as ++sail. Our hymn.hook file writes the basic HTML elements to the page, and pulls in our supporting CSS and JavaScript resources.

Our application-specific resources are stored in /main/pub/lead/src/. /main/pub/lead/src/main.css simply produces the page layout, while /main/pub/lead/src/main.js updates the page and sends data.

We also use two utility scripts: /gop/hart.js and /gen/main/lib/urb.js. These are the standard libraries for handling data transfer from a browser to Urbit, and are very frequently used. hart.js handles the page heartbeat, making regular AJAX requests so we can keep track of subscribers, and urb.js offers a more complete set of helper functions. urb.js depends on hart.js, so that's why hart.js always goes in the <head>. For complete documentation, check out the urb.js reference.

Our application state is stored and distributed to connected clients by /main/app/lead/core.hook. Let's take a closer look at how that works.

At the top of our core.hook we have:

/?    314                                               ::  need urbit 314

This should be familiar from the %ford guide. Here we're requiring that this code run on an Urbit ship where (lte zuse 314) is yes. In this core.hook we only use one %ford rune, but this is where we would also pull in any dependencies we might have or use other / runes.

Below our /? you can see that our code is divided into two sections: a |% where we define our models, and a |_ where we define the body of our program. We'll look at these more closely one at a time.

How is our state stored?

In /main/app/lead/core.hook:

++  axle
  $%  [%0 p=(map ,@t ,@ud)]
  ==

is the first arm inside our leading |% that's important to notice. ++axle defines the tile for our state. By convention we store our state as a $%, or labelled cases. We assume that our state can be versioned, so we want its model to be one of many tagged cases. This makes it possible to migrate our state to a new version of the service. Since this is the first version of our app, we tag our state with %0.

In this simple application we're keeping track of pairs of names to scores, and we define that here as (map ,@t ,@ud). You can think of this kind of like an associative array of strings to numbers, or an object with string keys and numeric values.

When we use ++axle to define the type of our state it's kind of like declaring a schema definition. There's no secondary data storage layer. Since %gall services run permanently your data persists as normal application state. We use tiles the way we normally would to declare the type of data that we're passing around.

Looking ahead, you can see that our main |_ takes a ++axle as part of its sample. Let's look at how that core actually works, to get a sense of what our application is doing.

Where do requests go?

In /main/app/lead/core.hook:

  ++  peer
    |=  [ost=bone you=ship pax=path]
    ^-  [(list move) _+>]
    ?~  pax
      [[ost %give %rust %json vat-json]~ +>.$]
    :_  +>.$
    :_  ~
    ?+  -.pax
      =-  [ost %give %mean -]
      `[%not-found [%leaf "you need to specify a path"]~]
      %data
        =-  [ost %give %rush %json -]
        (joba %conn %b &)
    ==

is the most important arm to look at first. ++peer is one of the predefined arms that %gall calls when certain events happen. You can find them all in the %gall overview.

We 'get a ++peer' when we get either a HTTP request, or a subscription request. Each time this happens our main |_ is populated with a ++hide and our current ++axle in its sample and ++peer gets passed three things: [ost=bone you=ship pax=path]. The sample in the |_ that contains ++peer is our application state and all of the contained arms have access to that sample as part of their context. To change the state we simply produce a new context with changed values.

Let's look at each of these parts of our context and the sample in ++peer.

++hide, labelled hid in peer's context, gives us some information about the ++request being passed in. You can look at the specifics in the %arvo ++models, but for our purposes we can think of it simply as request metadata.

++axle, labelled as vat in peer's context, should be familiar from the discussion in the previous step.

ost is a ++bone, or an identifier for an %arvo duct. 'Duct' is actually a pretty good word for what a ++duct does. Informally, when an event is processed in %arvo we patch together our requisite computations with ++ducts. For example, when we get a network packet, parse it, pass it to the webserver, and then pass it to the application framework we use a ++duct to make all those connections. In ++peer our ost just identifies the incoming request by number. We don't have access to the connecting ++duct, but we use ost in the effects we produce so our responses are correctly coupled to the incoming request.

you is a ++ship, which is just a @p or a phonemic string like ~tasfyn- partyv. %eyre does some work to figure out who this is, or uses a submarine name if it can't be determined. You can read more about how we parse identities in %eyre in the %eyre reference.

pax is a ++path, or a list of @ta. In Hoon we most often write paths as you would expect, /something/like/this. In %gall services requests come in on a specific path, like /data or /.

++peer, as with any arm that handles events, must produce a pair of a (list ++move) and our context, with any intended changes. In this peer we handle two cases, when pax is empty, or ~, when our pax is /data. We throw an error if pax is anything else.

What exactly is a list of moves?

Try pointing your browser at:

http://localhost:8082/lead/

to see our response when pax in ++peer is ~. In our case we use this URL to load the initial state of the application as JSON. This is produced by the line [[ost %give %rust %json vat-json]~ +>.$] which produces a single ++move, and our local context. Let's look more closely at our ++move.

++  move  ,[p=bone q=[%give gift]]                    ::  output operation

From our prior discussion we're familiar with a ++bone, and ++gift is defined right above in core.hook:

++  gift                                              ::  output action
  $%  [%rust gilt]                                    ::  total update
      [%rush gilt]                                    ::  partial update
      [%mean (unit (pair term (list tank)))]          ::  Error, maybe w/ msg
      [%nice ~]                                       ::  Response message
  ==
  ::

Which clearly depends on ++gilt:

++  gilt                                              ::  subscription frame
  $%  [%hymn p=manx]                                  ::  html tree
      [%json p=json]                                  ::  json
  ==
  ::

++gift defines the possible actions we can take in the moves that we produce. We can send either partial or total updates with %rush or %rust respectively. We can also send either an error, %mean or default acknowledgement, %nice.

Returning to our original ++move, [ost %give %rust %json vat-json] we can now read it as 'send a total update with ++vat-json as ++json'. ++vat-json simply takes our (map @t @ud) and turns it in to JSON.

Looking at the remainer of ++peer we can see that it is mostly control-flow that produces a %mean if our pax is not matched, and a %rush if our pax is %data. We'll revisit this %data path later on.

How do we change our state?

All of our state changes happen in ++poke-json. Incoming messages are handled by ++poke arms in %gall services. If an incoming message has a %logo it is appeneded after a -. Messages from the web are often sent as JSON, so ++poke- json is common for services that face the web.

Let's walk through this part:

=.  p.vat
  (~(put by p.vat) newl)
:_  +>.$
:*  [ost %give %nice ~]
    (deliver %upd-lead (joba -.newl [%n (scot %ud +.newl)]))
==

Using =. we update the value of p.vat in our context using put:by, one of our map container functions. Then, we produce +>.$ as our context. Since we have changed the value of p.vat within our immediate context, $, this is equivalient to updating the state of our service. Changing a value in your context and producing it is all you need to do to update your permanent state. That's one of the main goals of %gall, to be a single-level store.

So, how did we get to this point in ++poke-json?

=+  ^=  jop
    ^-  kiss
    %-  need  %.  jon
    =>  jo  %- of
    :~  [%new-lead so]
        [%add-lead so]
    ==

++deliver

main.js