urbit/main/pub/doc/guide/c-gall.md

285 lines
11 KiB
Markdown
Raw Normal View History

2015-02-18 06:03:21 +03:00
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`.
1.
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.
2.
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.
3.
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.
4.
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.
5.
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.
5.
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]
==
6.
`++deliver`
7.
main.js