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 necessary 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/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
/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